diff --git a/.github/workflows/license-header-format.yml b/.github/workflows/license-header-format.yml index 0ac41c7bc8..6be1171b32 100644 --- a/.github/workflows/license-header-format.yml +++ b/.github/workflows/license-header-format.yml @@ -35,7 +35,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'corretto' # https://github.com/actions/setup-java?tab=readme-ov-file#supported-distributions - java-version: '21' + java-version: '25' cache: 'maven' # https://github.com/actions/setup-java?tab=readme-ov-file#caching-sbt-dependencies - name: License header format diff --git a/README.md b/README.md index bffd87ffa5..169a276db4 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,148 @@ -# 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) +![banner](https://github.com/user-attachments/assets/3584b592-33dd-4fb4-91d4-47b62b34806c) + +
+ +# Open-source IoT platform for data collection, processing, visualization, and device management. + +
+
+
+ +💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/) • 🌐 [Website](https://thingsboard.io/) • 📚 [Documentation](https://thingsboard.io/docs/) • 📔 [Blog](https://thingsboard.io/blog/) • 🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all) + +
+ +## 🚀 Installation options + +Install ThingsBoard [on-premises](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) or use [ThingsBoard Cloud](https://thingsboard.io/installations/). + +## 💡 Getting started with ThingsBoard + +Check out our [Getting Started guide](https://thingsboard.io/docs/getting-started-guides/helloworld/) or [watch the video](https://www.youtube.com/watch?v=80L0ubQLXsc) to learn the basics of ThingsBoard and create your first dashboard! You will learn to: + +* Connect devices to ThingsBoard +* Push data from devices to ThingsBoard +* Build real-time dashboards +* Create a Customer and assign the dashboard with them. +* Define thresholds and trigger alarms +* Set up notifications via email, SMS, mobile apps, or integrate with third-party services. + +## ✨ Features + + + + + + + + + + +
+
+
+ Provision and manage devices and assets +

Provision and manage
devices and assets

+
+
+

Provision, monitor and control your IoT entities in secure way using rich server-side APIs. Define relations between your devices, assets, customers or any other entities.

+
+
+
+ Read more ➜ +
+
+
+
+
+ Collect and visualize your data +

Collect and visualize
your data

+
+
+

Collect and store telemetry data in scalable and fault-tolerant way. Visualize your data with built-in or custom widgets and flexible dashboards. Share dashboards with your customers.

+
+
+
+ Read more ➜ +
+
+
+
+
+ SCADA Dashboards +

SCADA Dashboards

+
+
+

Monitor and control your industrial processes in real time with SCADA. Use SCADA symbols on dashboards to create and manage any workflow, offering full flexibility to design and oversee operations according to your requirements.

+
+
+
+ Read more ➜ +
+
+
+
+
+ Process and React +

Process and React

+
+
+

Define data processing rule chains. Transform and normalize your device data. Raise alarms on incoming telemetry events, attribute updates, device inactivity and user actions.

+
+
+
+
+ Read more ➜ +
+
+
+ +## ⚙️ Powerful IoT Rule Engine + +ThingsBoard allows you to create complex [Rule Chains](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) to process data from your devices and match your application specific use cases. + +[![IoT Rule Engine](https://github.com/user-attachments/assets/43d21dc9-0e18-4f1b-8f9a-b72004e12f07 "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) + +
+ +[**Read more about Rule Engine ➜**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) + +
+ +## 📦 Real-Time IoT Dashboards + +ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that speeds up time-to-market with powerful built-in solution templates. It enables data collection and analysis from any devices, saving resources on routine tasks and letting you focus on your solution’s unique aspects. See more our Use Cases [here](https://thingsboard.io/iot-use-cases/). + +[**Smart energy**](https://thingsboard.io/use-cases/smart-energy/) + +[![Smart energy](https://github.com/user-attachments/assets/2a0abf13-6dc5-4f5e-9c30-1aea1d39af1e "Smart energy")](https://thingsboard.io/use-cases/smart-energy/) + +[**SCADA swimming pool**](https://thingsboard.io/use-cases/scada/) + +[![SCADA Swimming pool](https://github.com/user-attachments/assets/68fd9e29-99f1-4c16-8c4c-476f4ccb20c0 "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) + +[**Site fleet tracking**](https://thingsboard.io/use-cases/site-fleet-tracking/) + +[![Site fleet tracking](https://github.com/user-attachments/assets/d6ce0766-b138-4a42-86aa-7112a543026c "Site fleet tracking")](https://thingsboard.io/use-cases/site-fleet-tracking/) + +[**Smart farming**](https://thingsboard.io/use-cases/smart-farming/) + +[![Smart farming](https://github.com/user-attachments/assets/56b84c99-ef24-44e5-a903-b925b7f9d142 "Smart farming")](https://thingsboard.io/use-cases/smart-farming/) -ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management. - - - - -## Documentation - -ThingsBoard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs). - -## IoT use cases - -[**Smart energy**](https://thingsboard.io/smart-energy/) -[![Smart energy](https://user-images.githubusercontent.com/8308069/152984256-eb48564a-645c-468d-912b-f554b63104a5.gif "Smart energy")](https://thingsboard.io/smart-energy/) - -[**SCADA Swimming pool**](https://thingsboard.io/use-cases/scada/) -[![SCADA Swimming pool](https://github.com/user-attachments/assets/0878a2f5-d358-47c5-b295-03b4533685cf "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) - -[**Fleet tracking**](https://thingsboard.io/fleet-tracking/) -[![Fleet tracking](https://user-images.githubusercontent.com/8308069/152984528-0054ed55-8b8b-4cda-ba45-02fe95a81222.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/) - -[**Smart farming**](https://thingsboard.io/smart-farming/) -[![Smart farming](https://user-images.githubusercontent.com/8308069/152984443-a98b7d3d-ff7a-4037-9011-e71e1e6f755f.gif "Smart farming")](https://thingsboard.io/smart-farming/) +[**Smart metering**](https://thingsboard.io/smart-metering/) -[**IoT Rule Engine**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) -[![IoT Rule Engine](https://img.thingsboard.io/demo/send-email-rule-chain.gif "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) +[![Smart metering](https://github.com/user-attachments/assets/adc05e3d-397c-48ef-bed6-535bbd698455 "Smart metering")](https://thingsboard.io/smart-metering/) -[**Smart metering**](https://thingsboard.io/smart-metering/) -[![Smart metering](https://user-images.githubusercontent.com/8308069/31455788-6888a948-aec1-11e7-9819-410e0ba785e0.gif "Smart metering")](https://thingsboard.io/smart-metering/) +
-## Getting Started +[**Check more of our use cases ➜**](https://thingsboard.io/iot-use-cases/) -Collect and Visualize your IoT data in minutes by following this [guide](https://thingsboard.io/docs/getting-started-guides/helloworld/). +
-## Support +## 🫶 Support - - [Stackoverflow](http://stackoverflow.com/questions/tagged/thingsboard) +To get support, please visit our [GitHub issues page](https://github.com/thingsboard/thingsboard/issues) -## Licenses +## 📄 Licenses -This project is released under [Apache 2.0 License](./LICENSE). +This project is released under [Apache 2.0 License](./LICENSE) diff --git a/application/pom.xml b/application/pom.xml index 8e5e6b2564..9489c627a1 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard application @@ -289,6 +289,11 @@ rest-client test + + org.thingsboard.client + thingsboard-ce-client + test + org.springframework.security spring-security-test @@ -502,6 +507,105 @@ + + + openapi-spec + + true + none + none + + + + + org.springframework.boot + spring-boot-maven-plugin + + org.thingsboard.server.ThingsboardServerApplication + 9001 + + + + build-info + build-info + + false + + + + openapi-start + start + + false + true + -Xmx1024m + + --spring.config.name=thingsboard + --spring.profiles.active=openapi + + 180 + 2000 + + + + openapi-stop + stop + + false + + + + + + org.springdoc + springdoc-openapi-maven-plugin + 1.4 + + + generate-openapi-spec + generate + + + + http://localhost:8080/v3/api-docs/thingsboard + openapi.json + ${project.build.directory} + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + verify-openapi-spec-generated + verify + + enforce + + + + + + ${project.build.directory}/openapi.json + + OpenAPI spec was not generated — target/openapi.json is missing. The springdoc-openapi-maven-plugin logs HTTP failures but does not fail the build; scan the log above for "An error has occured" or a 5xx response from /v3/api-docs/thingsboard and fix the underlying issue (e.g. duplicate @Schema names rejected by SwaggerConfiguration). + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + jenkins diff --git a/application/src/main/data/json/edge/instructions/install/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md index 1822add6d8..964cdcb887 100644 --- a/application/src/main/data/json/edge/instructions/install/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/centos/instructions.md @@ -1,22 +1,24 @@ -Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server. +Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 9/10 and connect to the server. + +**Note:** OpenJDK 25 requires RHEL/CentOS 9+ or RHEL/CentOS 10+. Earlier versions (RHEL/CentOS 7, 8) are not supported. #### Prerequisites Before continuing to installation, execute the following commands to install the necessary tools: ```bash -sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm +sudo dnf install -y nano wget {:copy-code} ``` -#### Step 1. Install Java 17 (OpenJDK) -ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: +#### Step 1. Install Java 25 (OpenJDK) +ThingsBoard service is running on Java 25. To install OpenJDK 25, follow these instructions: ```bash -sudo dnf install java-17-openjdk +sudo dnf install -y java-25-openjdk {:copy-code} ``` -Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command: +Configure your operating system to use OpenJDK 25 by default. You can configure the default version by running the following command: ```bash sudo update-alternatives --config java @@ -33,7 +35,7 @@ java -version The expected result is: ```text -openjdk version "17.x.xx" +openjdk version "25.x.xx" OpenJDK Runtime Environment (...) OpenJDK 64-Bit Server VM (build ...) ``` @@ -41,7 +43,7 @@ OpenJDK 64-Bit Server VM (build ...) #### Step 2. Configure ThingsBoard Edge Database ThingsBoard Edge supports **SQL** and **hybrid** database configurations. -In this guide, we’ll use an **SQL** database. +In this guide, we'll use an **SQL** database. For more details about the hybrid setup, please refer to the official installation instructions on the ThingsBoard documentation site. To install the PostgreSQL database, run these commands: @@ -54,19 +56,19 @@ sudo dnf update Install the repository RPM: -* **For CentOS/RHEL 8:** +* **For RHEL/CentOS 9:** ```bash -# Install the repository RPM (For CentOS/RHEL 8): -sudo sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm +# Install the repository RPM (for RHEL/CentOS 9): +sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm {:copy-code} ``` -* **For CentOS/RHEL 9:** +* **For RHEL/CentOS 10:** ```bash -# Install the repository RPM (for CentOS 9): -sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm +# Install the repository RPM (for RHEL/CentOS 10): +sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-10-x86_64/pgdg-redhat-repo-latest.noarch.rpm {:copy-code} ``` @@ -91,8 +93,8 @@ sudo -u postgres psql -c "\password" Then, enter and confirm the password. -Since ThingsBoard Edge uses the PostgreSQL database for local storage, configuring MD5 authentication ensures that only authenticated users or -applications can access the database, thus protecting your data. After configuring the password, +Since ThingsBoard Edge uses the PostgreSQL database for local storage, configuring MD5 authentication ensures that only authenticated users or +applications can access the database, thus protecting your data. After configuring the password, edit the pg_hba.conf file to use MD5 hashing for authentication instead of the default method (ident) for local IPv4 connections. To replace ident with md5, run the following command: @@ -102,7 +104,7 @@ sudo sed -i 's/^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+ident/host all {:copy-code} ``` -Then run the command that will restart the PostgreSQL service to apply configuration changes, connect to the database as a postgres user, +Then run the command that will restart the PostgreSQL service to apply configuration changes, connect to the database as a postgres user, and create the ThingsBoard Edge database (tb_edge). To connect to the PostgreSQL database, enter the PostgreSQL password. ```bash diff --git a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md index b4782dd46a..6f74c09c08 100644 --- a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md @@ -1,14 +1,16 @@ Here is the list of commands that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. -#### Step 1. Install Java 17 (OpenJDK) -ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: +**Note:** OpenJDK 25 requires Ubuntu 22.04 LTS or newer. Earlier versions (20.04 and below) are not supported. + +#### Step 1. Install Java 25 (OpenJDK) +ThingsBoard service is running on Java 25. To install OpenJDK 25, follow these instructions: ```bash -sudo apt update && sudo apt install openjdk-17-jdk +sudo apt update && sudo apt install openjdk-25-jdk {:copy-code} ``` -Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command: +Configure your operating system to use OpenJDK 25 by default. You can configure the default version by running the following command: ```bash sudo update-alternatives --config java @@ -25,7 +27,7 @@ java -version The expected result is: ```text -openjdk version "17.x.xx" +openjdk version "25.x.xx" OpenJDK Runtime Environment (...) OpenJDK 64-Bit Server VM (build ...) ``` diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index 81f9e6a14d..e614c9b54c 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -10,27 +10,9 @@ "externalId": null }, "metadata": { - "firstNodeIndex": 0, + "firstNodeIndex": 2, "nodes": [ { - "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": 187, - "layoutY": 468 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - }, - "externalId": null - }, - { - "additionalInfo": { - "layoutX": 823, - "layoutY": 157 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", "configurationVersion": 1, @@ -41,13 +23,12 @@ "type": "ON_EVERY_MESSAGE" } }, - "externalId": null + "additionalInfo": { + "layoutX": 823, + "layoutY": 157 + } }, { - "additionalInfo": { - "layoutX": 824, - "layoutY": 52 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", "configurationVersion": 3, @@ -60,25 +41,23 @@ "sendAttributesUpdatedNotification": false, "updateAttributesOnlyOnValueChange": true }, - "externalId": null + "additionalInfo": { + "layoutX": 824, + "layoutY": 52 + } }, { - "additionalInfo": { - "layoutX": 347, - "layoutY": 149 - }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", "configuration": { "version": 0 }, - "externalId": null + "additionalInfo": { + "layoutX": 347, + "layoutY": 149 + } }, { - "additionalInfo": { - "layoutX": 825, - "layoutY": 266 - }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", "configuration": { @@ -86,13 +65,12 @@ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null + "additionalInfo": { + "layoutX": 825, + "layoutY": 266 + } }, { - "additionalInfo": { - "layoutX": 824, - "layoutY": 378 - }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", "configuration": { @@ -100,97 +78,92 @@ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null - }, - { "additionalInfo": { "layoutX": 824, - "layoutY": 466 - }, + "layoutY": 378 + } + }, + { "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", "configuration": { "timeoutInSeconds": 60 }, - "externalId": null + "additionalInfo": { + "layoutX": 824, + "layoutY": 466 + } }, { - "additionalInfo": { - "layoutX": 1126, - "layoutY": 104 - }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { "scope": "CLIENT_SCOPE" }, - "externalId": null + "additionalInfo": { + "layoutX": 1126, + "layoutY": 104 + } }, { - "additionalInfo": { - "layoutX": 826, - "layoutY": 601 - }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { "scope": "SERVER_SCOPE" }, - "externalId": null + "additionalInfo": { + "layoutX": 826, + "layoutY": 601 + } } ], "connections": [ { "fromIndex": 0, - "toIndex": 3, + "toIndex": 6, "type": "Success" }, { "fromIndex": 1, - "toIndex": 7, + "toIndex": 6, "type": "Success" }, { "fromIndex": 2, - "toIndex": 7, - "type": "Success" - }, - { - "fromIndex": 3, - "toIndex": 1, + "toIndex": 0, "type": "Post telemetry" }, { - "fromIndex": 3, - "toIndex": 2, + "fromIndex": 2, + "toIndex": 1, "type": "Post attributes" }, { - "fromIndex": 3, - "toIndex": 4, + "fromIndex": 2, + "toIndex": 3, "type": "RPC Request from Device" }, { - "fromIndex": 3, - "toIndex": 5, + "fromIndex": 2, + "toIndex": 4, "type": "Other" }, { - "fromIndex": 3, - "toIndex": 6, + "fromIndex": 2, + "toIndex": 5, "type": "RPC Request to Device" }, { - "fromIndex": 3, - "toIndex": 8, + "fromIndex": 2, + "toIndex": 7, "type": "Attributes Deleted" }, { - "fromIndex": 3, - "toIndex": 8, + "fromIndex": 2, + "toIndex": 7, "type": "Attributes Updated" } ], "ruleChainConnections": null } -} +} \ 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 5cf929e3ce..f24afb231d 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 @@ -17,6 +17,7 @@ "gateway_widgets.gateway_custom_statistics", "gateway_widgets.gateway_general_chart_statistics", "gateway_widgets.service_rpc", - "gateway_widgets.gateway_status" + "gateway_widgets.gateway_status", + "gateway_widgets.markdown_card" ] } diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index a30d6ee76f..2701abcb84 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -13,6 +13,7 @@ "home_page_widgets.quick_links", "home_page_widgets.documentation_links", "home_page_widgets.dashboards", - "home_page_widgets.usage_info" + "home_page_widgets.usage_info", + "api_usage" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/action_button.json b/application/src/main/data/json/system/widget_types/action_button.json index 3ea996e48e..4be3c87414 100644 --- a/application/src/main/data/json/system/widget_types/action_button.json +++ b/application/src/main/data/json/system/widget_types/action_button.json @@ -12,12 +12,10 @@ "templateHtml": "\n", "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.actionSources = function() {\n return {\n 'click': {\n name: 'widget-action.click',\n multiple: false\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n maxDatasources: 1,\n maxDataKeys: 0,\n singleEntity: true,\n previewWidth: '200px',\n previewHeight: '80px',\n embedTitlePanel: true,\n overflowVisible: true,\n hideDataSettings: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-action-button-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-action-button-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#FFFFFF01\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Action button\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":false,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"borderRadius\":\"4px\",\"configMode\":\"basic\"}" + "defaultConfig": "{\"datasources\":[],\"showTitle\":false,\"backgroundColor\":\"#FFFFFF01\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Action button\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":false,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"borderRadius\":\"4px\",\"configMode\":\"basic\"}" }, "tags": [ "button", diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_card.json b/application/src/main/data/json/system/widget_types/air_quality_index_card.json index 9dc9a99194..a12fb7a5c3 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_card.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json b/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json index 5bf555d92c..801b91330b 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/air_quality_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/air_quality_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/alarm_count.json b/application/src/main/data/json/system/widget_types/alarm_count.json index 7f9dd32b45..c9851a4f8b 100644 --- a/application/src/main/data/json/system/widget_types/alarm_count.json +++ b/application/src/main/data/json/system/widget_types/alarm_count.json @@ -12,12 +12,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.countWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.countWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '220px',\n previewHeight: '100px',\n embedTitlePanel: true,\n hideDataSettings: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-alarm-count-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-alarm-count-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"count\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = Number((prevValue + Math.random() * 4 - 2).toFixed(0));\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 25) {\\n\\tvalue = 25;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"],\"assignedToCurrentUser\":false,\"assigneeId\":null}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLabel\":true,\"label\":\"Total\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":20,\"iconSizeUnit\":\"px\",\"icon\":\"warning\",\"iconColor\":{\"type\":\"constant\",\"color\":\"rgba(255, 255, 255, 1)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIconBackground\":true,\"iconBackgroundSize\":36,\"iconBackgroundSizeUnit\":\"px\",\"iconBackgroundColor\":{\"type\":\"range\",\"color\":\"rgba(0, 105, 92, 1)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"rgba(0, 105, 92, 1)\"},{\"from\":1,\"to\":null,\"color\":\"rgba(209, 39, 48, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showChevron\":false,\"chevronSize\":24,\"chevronSizeUnit\":\"px\",\"chevronColor\":\"rgba(0, 0, 0, 0.38)\",\"layout\":\"column\"},\"title\":\"Alarm count\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":null,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.54)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"count\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = Number((prevValue + Math.random() * 4 - 2).toFixed(0));\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 25) {\\n\\tvalue = 25;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"],\"assignedToCurrentUser\":false,\"assigneeId\":null}}],\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLabel\":true,\"label\":\"Total\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":20,\"iconSizeUnit\":\"px\",\"icon\":\"warning\",\"iconColor\":{\"type\":\"constant\",\"color\":\"rgba(255, 255, 255, 1)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIconBackground\":true,\"iconBackgroundSize\":36,\"iconBackgroundSizeUnit\":\"px\",\"iconBackgroundColor\":{\"type\":\"range\",\"color\":\"rgba(0, 105, 92, 1)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"rgba(0, 105, 92, 1)\"},{\"from\":1,\"to\":null,\"color\":\"rgba(209, 39, 48, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showChevron\":false,\"chevronSize\":24,\"chevronSizeUnit\":\"px\",\"chevronColor\":\"rgba(0, 0, 0, 0.38)\",\"layout\":\"column\"},\"title\":\"Alarm count\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":null,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"titleColor\":\"rgba(0, 0, 0, 0.54)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "alert", diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json index 4f86957f1c..704cb8fb43 100644 --- a/application/src/main/data/json/system/widget_types/alarms_table.json +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -12,18 +12,12 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n supportsUnitConversion: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-alarms-table-basic-config", - "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,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"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},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" + "defaultConfig": "{\"timewindow\":{\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":2592000000}},\"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,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"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\":\"originatorDisplayName\",\"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},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" }, - "tags": [ - "alert", - "alerts" - ], "resources": [ { "link": "/api/images/system/alarms_table_system_widget_image.png", @@ -36,5 +30,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAUUElEQVR42u2dh1sUVxeH/ctsUWMSSUETY/xSTDRKNNUYG4IgmhgVC7ZYA7FEERVUNAqoCIJdaeIConSwUAQEBO73zp5l3KAsEHcV3PN78vjcnXJ35s47554ZNr8zxBjz5MmTmpqakpKSOyrVCwiEAKmtrQ2ohkBVaWlpfX19e3u7UaleQCAESOAEVENAjA86KCpvCZyAagjhS2OVyrtxC6iGMDXqWKi8K6BSsFQKlkrBUilYCpZKwVIpWCoFS6V66WDxvisxMfGv3pSRkaEDquoHWJcuXZozZ87q1avX9qzly5ezTXV19aA47fT09JCQkObmZiXgVYJ15swZoPF8GfLz89nm9u3bg+K0+3JGqtcHrFWrVq34t3z0R8zBAhZ/zd2+ffuCBQsYGa+kwrt27Tp48CCNx48f+/T0BxZYe/bs4cy/+uqrKVOm7HLKz8H65ptvvvvuu3/++ee333575513Hjx48IIdXrx48fr163IPL1q0yL+mwiVLloSGhtox7Ny5c5LnMbi1tbXkRvv37//555+3bdsm2DHcZHgsYXlHR8drAxZn98YbbyQlJdHmvBiKW7du0T59+jQnGxYWVlxcfP/+fQaEf1nO6cfFxdE4f/48Z8dy2T4hIWHHjh1sf+XKFbbhI31+/PHH48eP52NMTMyhQ4fYjN7YpampyV/Amj17Ng3GheWVlZVDhw5lIUPz7rvv7t69m1XTpk1jLfn4hx9+yNPrc/ssKyvbu3evHf+6nVFFRQVr+W3aQGNrzZo1Y8eOXbduHT+ds0POm2++yelv3ryZ8+WYP/nkE1jp7OwMDAxkeW5u7ujRoxkHiAkICOAcV65cOWrUKLYHHQaWBKO8vHzu3LmzZs0qKiqKjY2dMGECu+/cuZOR9JeIdePGjZEjR9bV1b399tvciAIWlLBq06ZNP/744927d1kSHR3N4H7//fcQ9tw+2feXX37hxhW23M8IqhYvXsyXPnz4cKCBxfWW8DN8+HD4IG4tW7Zs6tSpnOyBAwc4cQacJGzevHkEpzFjxnBGUPjpp5+yQXx8PHsxgOxoz3oClvtUyFmzGbtD1d9//+0vYDGy3E+///47Nx9MuIPFHTZz5syCggKWcGdvduro0aM9dctrNthiAuUut89IqALHqqqqgUYVl5xZT+4Eh8MxbNiwq1evMjJffPHF5i5x/Fw+kKItNxXcEMPsDYhSgGWP57NgIeYEcgnw8uLbooEOFmJ0GFPYoi1gcTwcDHcY789aWlpgjjSfu5mbmwvgoWdhi1ucKYOjLSwsHLBUIX7dO2LEiMOHD8MWw8sgkCQRqMiNIKCxsZGMSrDjcYfpj2SA9okTJxgQ7j2GiA24i54LFrfiDz/8ICnp8ePH6ZyZ8TVP3hmIX3/91f7I5QcmxtQGa8aMGYw4M8K9e/dYyKpJkyZxw3GnkmF47pxHAdgKDg7maPkXiLl+AzZ/T01N5dQ4ZfIqbjDiNyQRXUY7Zb+OIQeXfEvS/KioKGIYeRVnx/8ww0d7PGnwkUZmZiYbMAK0SdhpSwrvRy9ICTOkpTKC9lT47GuI1tbWvncIWxztgI1V3URUfvaB0fOLGPDq9VmEHiCVBk8GgOXdFLNPYHGXcxlIBo/1LCYjtuEn9N4dU7oleScVlY/uOdYLwhoeHj4oqPK1iBqEt/Xr17+C91g8lDE9zfEoyV28/j6TNzTkp+43GY/KXvkWXj0rVaihocEXP2/Rn82ofCIFS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlkqlYKkULJVfg6VSeV0asVQ6FaoULJWCpWCpFCyVgqVSsFQqBUulYKkULJVKwVIpWCoFS6V6iWBhXYKfzgGPwoopKytLB1TVD7CuXbsmZqketHDhQnxBxGVVpfLTAgIq/wILg9DNbqI+j79fn+IY6z93Fe00t3c/f+OSOFOVrGA9R5g+Xr58GaM9HEdp4Pj7kodD7IcHEFipH5hTQ01Dl7FqbY71MW3i8zeuPGXuZypYPYpYJQbuorNnz+KliZO2WKURxniSoJgAPm84whHkWEsWyKqbN2/imrxlyxbW5uXlue754mIwjYyMFB9AHGDxQGcJH3EfxdOb3cUWli/Fk5JkkQHBxBCyjdOBcsOGDcZpa8iO+H9iBMeR4NkcERHByPgcrHPjTf5a18ebq6yPLrA6TflRkx1hHJtMW521oCzBVPxjNZ40W4EtK8zcjcWszjkKe0z5EZP7m3lcpWC5qKLmB3BwIcHFOMsFUIcMx1vcpCmvQAEPfFrfe+89Hl3xQsZRGM9q9mIJ2OGrieMt2F24cGHy5Mk4vQIoDTZ49OjRZ599BqMcM/1gFIhVLra5rMLzc+vWrfv27TNO41cqFcg0jX86a1nCV+NhyVEFBQWJ664PwboVZc4EmM4npr3VnHnb+ihgFe8y5z40VUnm0kyTFWItyV5s8lY4n7bmmotBpiLRnP/M3FxjLWEb9s1fbVruKViWKKBAiCJ44Br6wQcfCFhcURpgRLCRzT766CMYYoltD8x8CgR4Wc+fP/+yU3hWM80BFkzINlhOQiFm1BgwY+XNEkoQ5OTk0HguWOKvD5FvvfWW9Ekw87p7Z3ewyo+b9P+Z6rMWQ5lfWmFJwCIsNeRbISo7zKR98hSslofm1DBrFao+Z1LGms4OC6zCbToVPgXr22+/ZcaRXP6PP/5wByslJWXp0qWyGSGHijruYGGQz8wFQwQV+2kA710bLCIcnYMLJv0Eqr6Dhcc69ZLsPtndt2CVxlvZ+vX55trP1tRWEusCq/SQSR1v7uw12eEm3Q2s5gorD3tcaS2py7La7S1OsHYoWE/BooCHzQHTX69gkX1LKKJ+GIdEaSdmUrHJZ3fcp22wmPjwRhfn9OnTp0uahae+vOblAIRjSqp0A4vemHCZOqUT3xZKscA6bFrum+TRJmWMaa0zJftdYF2bY3KXWw0Hk+PHT8HqbLf2KolzrtpoMr5wTYUKljtY5EmUVqNneJK3D57BokFlLGY9smxZRQkGZjqq7jCrkjzZYIEUFFI+hLVQKFX8oIclJP5wA3Z8Lzt2A8s43bxJ1Jhk2ZEnBp+DZWE0z2SFOt8pdIFVddqijczpwlST9IaVpNs51v0LVlp25l2TGmjqchUsl7j83aoBEBXscmc0xI4bg3z7kKj/Ick7UyH7Un3OfXd2sSv9deuckENv9kJo41HR7l8qX5BUPbsj2zAn+qic4lO1N5qONmej1frP1WjsWttsxTCQetJgPSReX2DyVrlWEbdaalyPhNbJNLl2H1Bg8egENCdPnjzds6i6wTZMHK/w3Yl7juV34pVE0kgrwR9E77H4CyAPWXN6EyWTXm3JPwrdUrrCT8FiymtwDJzD6evPZphomnuTUan6C5ZKpWCpFCyVgqVSKVgqBUulYKlUCpZKwVIpWCqVgqVSsFSvJVgqlRYQUOlUqFKwVCoFS6VgqRQslUrBUilYKgVLpVKwVAqWSsHqSZ7/b1WsDXQ0Vf0DC0+OZcuW9fq/2GO76HOTDNXrBBb2m4PCFEQ1yMBSn/dXLOaBnqYCp8VS94376KPBvgkJxjemGwMLLDzWsEGbOHEi/mk06PAlX0EMI33r0NdfNTSY4GAzfLgZOdKEhJgukzCX2trMiBEmO9tqJyYacdrZssX0cdyYXoYONTU1/hKxutlxG6evn9g92rLt1/i3gdH/1x3b3vTvC8DHbk5unIu9O+ZH9nI8SHNzcz0cm4fv9YnCwsz06da1578pU0xUVPdYRY2Zzk6rMWYMPmZWg6co9wOrq3NtYMcz+3z9GSzc9PAF/emnn7DOlnoCWEUGBwdDAEagbEaDVeLUjfEacQ7LWiy4Mbe1e8Ni9MsvvxSHSKwi6YGI6HA4sIfE65H+cZcEFPxOca39+uuvMVd+rrmt7FhUVITXMo6SHBUGpz70bwIRAtXZs66P+M7HxroYmjbNDBuG6aBFBsMCc3xkeUaG2bTJzJ5tbeZwmIkTzdixZvx4I1URtm61Po4bZ4U09vVnsGgLEPQMHwIWlrU0KDnG1CnutAEBARweYIGIc4pogzb4wAUezmQbaMNBDrCEQvTnn3+KUxxssaXpzTWZsgPGaRUGXpQRkMOLlYvtC/EVXPhnK2UAELcN9RBssNwjlg3WjBnGWfrAYBjOsBDhVqywYCJiMYFevOjXYM2aNQuSfnRqDGPXHztuDNyBEgt42R34qCTg7vPOx9DQUEpREPz6bsddVVU1atQo6fPzzz9fsWKFr8DiknPhncfTHay0NKvhASwyB6Jdt+IGSUncQ2bqVCtpS0nxa7AoVZeWltbQpb6DFR4ezksQCghQxsLenbhlg8XpwJOY3lIWrxtY1FPZu3ev6aGAwPvvv09mJn36cCokHwoIMIcPuz5y2CEhfQULMeXJNIpFLwVguMpMlw6Ha5Wfg8XLM4IWxv9UFuHSmj7YcZM8sTHXnpQfY2NctfnIORLDOAUbLJgghrEK695x48bRm3EWwoiPj6dUzokTJ5hVeS1HqZVn7bg50+joaPxOWSLzsq+0Z48Fwf79BspJj3j06wmswEDDTcWDoQ0W0yWpWHKyCQqysn4GDbA4d/zradCVv4FFqRJK39gfk5OTmbCYccDLOIs0iUs2KVSiDLT1iL2F0CIFBKKioghX2fIQbkxBQQGe73ZMolCA3Tl27SynUAAwXSTnwII/O5snA3YhthG0WMtX2EWabIb4LtYyh8bExJDP+fbBkPkrNNRwCwlMxrKu56xcIY2J2Fl8ygpOvJjAdJ6GpH1gJ0EOOuU12MmT1jYs3L7d6o1oze6+ebZ9rV6Q+rUd92B88858ATT8ufDXnkWQIB+SWPKqRNRhptOLOmjAkqIPf3kUWZEU+FOpjP4eS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlg6ESsFSKVgqBUul8j5YKpX6vKt0KlQpWCqVgqVSsFQKlkqlYKkULJWCpVIpWCoFS6VgPVeY+5zuTWqZrOofWHl5eXP6oPnz5+McpGOqMv1ym8GAykNliqysLLXjVv0XsF6CjVF5efmLO4sUFhbmuNkrYpVTWlra307w4sJI0n0JnrZimMst9HKy0nvN9y5XX5X23YaSwroiaWfdyyl7VN7f3tLLM2qaa/wULGz1QsQN8QWEy97GjRvtjxEREdjz9bcTXN3O2nbFXVkmRpLG6cBGbmCc3rjuBnFeFzDNSw1ubGukvTM3OvLKGhodpmNRemjug7z+9naoIOFO/V0Fy7iHHGwagUMM1uPi4qTBBcYJUlJA27/PA1jYG8uObE/UMU7fZQDCUguGODvcuWGlxmmdSNiT2InjMruzHNNKAYvlrMUfEOPusLAw7P/YUfxRibj4PXnr2nR0doSkL7710NFhOhelhcLTo9ZH5Y8qbNpy7ueevHPqWvV1tuTjhYqLBXWFSXeSWzpaUkrOENVSSs9kVGayO2szKy9UNFY0tjWxqryxnM2uVl/rdK6it9OlZ9PK0h21BXkPbvoFWKdOnZoxYwZXMTIyUhxHMYMEC6431rTiJrpkyZLMzEx3sPCGLO8Sho4CFo6jmIvSwLUWo0dZQrcAivlxUFAQ3w5kM2fOZNXRo0fXrLEiBFvyUAJ5s2fPFrDWrl175MgRfFA5XywqAZEDYHvZGG9SL973W7K2Jt89TaQhXO3Iic6qyb5UeXnV5UhWJRafWHkpEiCWXljOvyxZeWnV4vPhO3NiHrU2zk1duPbqhuPFJ1gCMaxdduH3S5VXmF5ZteXG1mO3ExekBYMRI7n26voN1zcfLToGvn/n7/MLsIgKUioC87cJEybU1tZihMzl5+uIFvjesgoDd/fCE4A1efLkkC5hbusBLFmClSiOusZZb0JMv22wAgMDpQiKPRUKWDTAV+ITE6L4y1OOAJS9CNaJ4pPRubuIMfFFR86WpR4qiD9UGH/AcZBVJEwEnuL6O2ywMydawDpTmirTJfQU1lo5GaDE3orrBlZtSy1LgIm4VdpQxpKGVqvORawjzl/Aoq6OOGYjggqOyNiswxOTI5eTbyTFxuq4LzmWB7CIfLbH5OjRo93BEs48gwWOhE9GQEoceFH5D/IJSFuzthFamNqIW+uubrxcZbm3p5WnsQrg1l/bRAYmYDHf2WDdrr0trOzL398NrLoW663QHze2MpPmP7gVnO4a8wP+AxbTkLgdi996Y6OVW1DXBFNuikokJCTQOGw7oXsEi4AnsWe7U30Ei1lS3L8xOH0WLGZq2QurZqqqUFvPu2A1PWmal7qQ7Iq0iTkrPGMp8xdwsIoJ8VxZmhOdAzty/vzPYD14/JCkTR4zo3N2vbZgcevbsxi98ZBP8RKuJTDZl42Km8yDxlmhc/jw4ZVS9ao3sCiVQ+kbEjW84PsOFi7wkyZNWrlyJfGyG1h8ETO11ErhIWDYsGG+8PZdfWUtc5a0Y/J2w5Zk3MduHyclIj2CGLb5z2DROOg4TCq25sq68MyI1xMsnq1uuUmKeMnbV2ZAezPe79vzo4QTd0FbjZspPtjJ3wO44zlISCVREwJ4oJOiYvRmM+Fw1m6Q4gOyhN7I8zgYMZpnud0hT6MSBUmtBDuvi4jy8HGttOtb62uan7LLE2JlUyXPdDSsA2uuJm23Dsx0EoFa2q1iyqRTklHJlk/a21klT5FkaXRI41Gb9bBZ3VRD5DtSeOzlgcUzEdBgn3+jZ1G4hm388+/ZZHs8uqbZBv+DTRtvbCaNI1ELy4hwB9fnYJWVlS1YsKDXvxUyy/hnyXFedvi28ImPRRjjBRgv9CXgvTywjLMyUWlv6lZsUuXP0t9jqRQslYKlUrAULJWCpVKwVAqWgqVSsFQKlsqvweJvq/KbXZXKKwInoBrCX+/r6+t1OFTeEr/+AKohbW1t/KUPtjRuqV48VgESONEYwmd+jQlihC81kVa9iEAIkCRC/R9f3OEsEgi6eAAAAABJRU5ErkJggg==", "public": true } + ], + "scada": false, + "tags": [ + "alert", + "alerts" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/api_usage.json b/application/src/main/data/json/system/widget_types/api_usage.json new file mode 100644 index 0000000000..5c269f4595 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/api_usage.json @@ -0,0 +1,35 @@ +{ + "fqn": "api_usage", + "name": "API Usage", + "deprecated": false, + "image": "tb-image;/api/images/system/api-usage-widget.png", + "description": null, + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "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 self.ctx.$scope.apiUsageWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n datasourcesOptional: true,\n previewWidth: '400px',\n previewHeight: '300px'\n };\n}", + "settingsForm": [], + "dataKeySettingsForm": [], + "settingsDirective": "tb-api-usage-widget-settings", + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{},\"title\":\"API usage\",\"decimals\":null,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\".tb-widget-header {\\n height: 48px;\\n align-items: center !important;\\n padding: 5px 10px 0 10px;\\n}\",\"titleStyle\":{},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"actions\":{\"headerButton\":[{\"name\":\"Go back\",\"buttonType\":\"stroked\",\"showIcon\":true,\"icon\":\"undo\",\"buttonColor\":\"#305680\",\"buttonBorderColor\":\"#0000001F\",\"customButtonStyle\":{\"padding\":\"0 16px\"},\"useShowWidgetActionFunction\":true,\"showWidgetActionFunction\":\"return widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;\",\"type\":\"custom\",\"customFunction\":\"const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"1ea1cca6-47d1-3539-d051-9535129fb12b\"}]},\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"21px\"},\"borderRadius\":\"4px\"}" + }, + "resources": [ + { + "link": "/api/images/system/api-usage-widget.png", + "title": "\"API Usage\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "api-usage-widget.png", + "publicResourceKey": "1TAYhxLWInZC20Xrn8PgbNbQs6fX8Pir", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAChVBMVEUAAADs7Ozd3d3f4+Xd4eLN0tLP0dTg4+Xp6en////y9ffx9/Pt7e0ZgDjl7uvh5Oawvs/7+/vx8fHO1+H9/f31+ffu7u78/f3s8fH9/v6Eu5Xg7uX6/PxHaY7i5efI0t1VdJf5+vvx9PaNornv8vVOb5NffJ262cRphaNaeJrs7/Ouvc2Wqb+BmLKmpqZjgJ9ip3ft8PXBzdmsu8yEm7Rviaf3+Pra4empqann7PHQ2eK/y9i3xNPD3stXdpi8vLyGnbVsh6VRcZV+t5A7X4ff5evW3ebU3eXN1+GzwdHOzs6pucuarcGQpbt7k66OwZ46klXc7OKcrsKx1Lytrq53kaxyjKhDZYs+Yoni5+3L4tOitMeKn7dmgqH09vn09PS6x9V1jqrm6/DT2+TEz9vX2NnV1dbHyMmltsisrKxhfp5cepuJvplLbZHj6e7d3d2hssWer8PBwcGoz7R0sojp7fLj8Oe/3MiYq8C4ubm1tbWvsLCeyauCuZQ3XIRepnTl6u/p8+zc4+rk5OTK1N/O5Na4xdSxv9CTp71+lrCSw6F4tItnq30cgTr29vbt8vTq6urh4eHU59rR5tefn6CMv5zd5OvX3+fIy8yt0riHvZj19/nm5+ja2tvR09S+v8B7to1ur4FkqXlPnmdHmWDz9fjZ4Oi8ydagscOVxaRxsYQqiUchhD/3+/jq8O/o6uzZ6t7Ly8uw07yysrKjzLCho6SVlpZao3Le4eOx1LuozbeZx6dWoW7t9e/c3uDC3czKysrDw8OytbaZm5xrrn8wjUze7eLO0NDI4c8zWYLg5uzV5t7FxcWOjo7v9vHY5+C01r7AydLKzc6nzLWBgoNCllzu8fMzg2RWAAAACXRSTlMA3Vrh2mBe3tXDgts5AAAPHUlEQVR42u3ch1sTZxzAcbr7eyEWaGw8uDR7r0JCAgkz7BFk7733kr2UvYTK0jpQqnW3bm3VWrXaveff0/cYLbXYQgiS0Hyf5y4hPjzw8T1y8X5EBweHl154zcZ74WUH3ItcZPPxX8TrsQ0cWPKywwtoW/SKw5toW/SaHWLG17qE0OVj6LWPEHrzE4SjdsWlP6PLpcdQyWV8/7JNQAK++wiVPnoHBdxC6GYHfoCbhPC94yVoZm6G++hTxP/1uE1A0Kd4Kd7BkEcfXZ6hIMe/wLtb5SV8JSq/fPMm951HNgZ5J6ntZwrSwacOr12PdilR0uWZ0i9unrAxSIny5msYculT/Bj/BJrhz6Cb/Jnfb31qK5B3jiFUXIy++Kj4i8ulCJ14DeG+TSpF7+CtDZ04dvwT24A80S682ebT75PZIf8XSHgrzgf9Wz7W+0p5BaR3yD9rSIreRsgD4fDt4h3+0gd4l1pA3XL5Cx/zKZXH0oYfw4/89Xmr1HmRtnof7jC7XasfWkG+SMZkR7CFPJSWwnTeJxF7IWZQtTM3v9JfF10tqe9ORuhgWlBqsiQIydjGojNslQKliCXpkf5sGUrx98+SqiqDuMMV7Aj0ZDvGaZaG4LhPhaSgM2d9U1Gajyw5wsjyfZsZGuHPqkB1muh8tLAiB5kosJXbzU11jgnKq4yI5Kb6eobqz9ar9GyUkBWdbWCzhOdYhejJ4mmbAeE/FRKLyoIiQ1CaNDIfFSn8+cyaXkFeChpKjlYsQaoRrx6FvN0tb1WjvORsFMYzhms09RUsFTqXlSBsbQ2riWHnoSd7+7NnuiIYMuyVtQgpqpQxm5iB+RofQb0kj4IY6/6E8PNj5ZlD2dHVPgK1pChFk18prW6tzAqvrsv3kWSm1KF/9MP8W6u3e5fZcdGqkKJQ1NuP+JEsX74v9yALva4+g5iGIT5Kl0UgHT7sC4cQkuYhfRiK5HIHIz0QS40/RcZCPrKIQaSXKWKQTq1HPs5nuciMNvU8wqxBay1ZVRGK1pP9hGg9kG/wCcDdHS106RtkRu6NcXFjO7YE8nFwcMby/a4fELp2By00fQStv0txNFzcjq2AnMTb7tkLlwaCg3d1BTfGX7uzo2vePb5xzBzILOUYCx7bohXZtf/S3ZaM+2MtXQPfdF27s/vGndngbzLMgXx46iJt7JfxuK1akf2oU3Tk8BFR1w87Gq/dCT5/J6Nxx7R5kPtuv4zT4rZoRe5TkLuNe691Ne6/f+3OwP7g+/f3N5oD2U071YlfvAVv6bPW28u7hT3fvH8nL/6wX0I2fx7ZEfzbb8GXtvw8spTtnxCRHWKH2EqvOZxz3hadc/jp9W3RTw5vwLbIyQ6xsuwQa+uZQhjAYcDChveAc8GPuQIQrgRw6IA3m4AcKC+GpACAR1GUqQ0gt+Mm0JUNbdDxsBxmHgB8V2sTkKSSY5CkJJxuUZDjkxhDeMOBE+CNbzq03t4wecs2ILdqla5JD4rbSilIO1Dhb/5bUHKSoN1V2dDX0WAZiJSF48Aa6meBGSnh28mkXO/yEgxxfbgEcQpYWJFyhjLX+3qpZSA6kyDFhODfEucAVUIMUNUZYD2VXE+it00EFM+VADTkAiyuy/X2B3CiPQCuQ8exqEkLHVpentCvEEuTxXmQwhMWkpqgTEjWqMJBXaE5ylJ4qbsDqe9fpYrJGRGrdQJJ05A4FoEZHYPVsxzEV9VU2B/KhLR0tUJfqW0lmb2e2aFsjilmKFsKTARQI9AGxvQaPNLoWTIoOz3iDFbRPyAKYKl4aZDGORtL1FdqgMmXCjyTwRA7pIEFSL8XPrQOCXndnCwZka8QZoJVtArEZDKELED6swpSEXOwLqWpOjw5moJU6wE8mIVeMWWK/m6tadgj7YzYGiGRB+HMWfDglcnJVlI3SKg1BmAm8KQQERhNhBkA8oYBoChQzcoZjkngfq3xyQtUF4FV9J/nESYfLNTAyfeogndarg/oa4foGGCZPlgas+91tGTEsz+z99A2A8J49pCd7z/7FTkkhU1I9F4cVeNOC8aAf4XkHQIqdSTYTisgHrFByac5gRVlkMnKC8zOSjey04fEIwhsohWQfQJSk5mZRUrCFJF1I2QqX54JzFC8OjbRSkglyLI0vqAYxBA5CAowhBWUXQBWGf1244UfnwJhhnk5R6aECWqWIAmBSJweKwML5ebmNgVLVTUDiBJhofjdsP6IvTTcSfqqELbpHJ2QBbLg7KEIFmSiAk26PjCBAxZqP95aDncSjuf3EFWd5z8QJdL3dJLk+fPmQA5TjlPzd59yaG1qH7q5kY/jdw/4NY9OV/n5nRcl7hG13Djv12IO5ORn47RTGRffWw1ChsKmRq3Iu7DH7/bsBb+q5p1josSuI6OiRtKsQ+vjC9c+zrhIi9uKiw97ExPdKYjb4f0tVaOziaLEFrfbGYmzu82BdNHmp/CLt8dbAYmfnr7XDN/vJDN6vu9pngJHR/h8AGDKvcecV24Xafglz/vNtn/JdPoU/mG/si2u/X6w034R25qzQ6wtJ4cDLtuiAw6Trtuiye1zaNkh1pUdYm3ZIeuvVukEtR1JLn0d5W9MKgGKywH3kAMQ0BYAJcpyp76k8omS6wBRbVYNORZwALyJqOIoeu2Due+coNwbAOjtAC5R4A0NfQwodymOivpuApRJVg0BDFHCXBQQSblzAW2ubUoAKK4F3IF2uNo246qEvodRAQ/7AiwFOcqC1SLQhiHeUFICAZ/AXGl5mxMFaSfwzrXcBd6A4ihvovZqVIn3daeNQdBXEnYmUKWzYbV6szYM+bZBmXt1pnRurrR4BjAktwEAiFsNpa4ND5VOUQHeb0SVPCif2CAkFXJCGDHkQV8M2WcqYwAAi8eC6EGeDvQ8Z899Mm1dtPwonOXpwZwmGABOE5Dr5JTLmSDwHYA+Vwri5OTkQv0JvJELHA5DS+RuDJLGKmND6lHP5HQ2YfRUlAHo2Hp2jTChTHyayYrlRYg9uotS1CxVhMD6rmyvhHSb0g4tQQpC8lWBAGWCfKOn8FCvpDAb1BSEiUfTw+x8QThYW38/tDJHgOkjwxBtqtSjAMBTTNeRFKSpWhuzDFHHwiECrK0VkKNi4GRLoyXJJp9kMFRkhwEQw8LknJGva2Ihs1LIOxTI9QJnGRko1JBgba31PCI3VHjCxnK867aG9jDWE7FuyMGyfthYjDjaWtprM+Npy0Lozx7iaAUQLlii2XHLQz6AtUOGwkBMWuZirfsa6rHAs1YhLw/KCvSDRKRJB+kmNWEIB7mO7ZUjp5+OkfPpCc4mLhFtCgNrbAWEK4io0PumsHVDKSwBR1AUKwt0hm6uVz0ZQnplyirIrwyx9Z7i8CCwxlZACpn5EhkYY2GkMp+pZwMABYGRQQgh00gI4YRANE+aLTaAVdS8Z0/zU8bT/pxQbYREIJWbIEzLzPFk8TL5X4HCF0MkYenVJAXRFzWlEWBWN1aMpzt7AL5sWfqOboAZjeKnjvHRVSGQKUyRxoZHJuTECk3EoDDfQ1eZXwEGSZOKEZatimCoIK/MI1lcv4Fh6JX4aSAy4qHqywFqzn5lGmDArKluBg332d4vt2QYeuMG+fjwu/GJtxs/rzo/eliU6Dc7Kjo8OmoO5MI4dgy8d2ErICfd3enUVHd69/7l8fR8cKe54+muYOygfbwlEEdHkoKMuXf5VYla7uJfGLjdfOV8i8gcyNj44fj3aLTGrYDgg2hKBFPNPaOiK18mdrp8Hk8/fNeR7LydAetvanw8Dv+0T9n+lcY91Jh9z3a4ZLqzpcXRfu3XirNDrK1tBKl12hbVbp8VsUOsKzvE2rJD1t9kexTgXCcBXL6YBE5b28QbJwCcGmwNoiQ6cvHNCbybO3EVSj/pa+j71RXaZ2wNgmfpxwCIdsDVXoX2XG1H3/UAl3LvrYRwPcyFzBUvQ1wnrvc1eEcVKzcBkhUBK6ObKlN8YKnYUKAKL8A7ffUg/K1+bk72f0IOKDkA7fQlyPHSq1F9DcdvEZsB8fKEQk+ttBcONmkNhVBUQUSOQJOhCfS6dH1OaKiB7yEe5oBWMSIt1OaEcwrDWQSkG1ABW032A6NID9rwwn5YvdwHBwBcShfv469aUkJMHGD0weSmQAa9hr1C/UFyMKi+wnMfs04KPsZzRq4g6HVVuCZ72FgjVByF014p6f5fhwl9QlrZBr1/jP8hgZzPhJQsr5jQtBgjC8zI0pDT9SNMkBT6QzivUg7SmGpfNQ90SFAAGBIJ7BqFLwAkJMACRADyOp4aCklVGJ+ZkwpNAvyXwIsGM7Ig5IwMKvRZ5/AIuk5ST6Q21cvz1MBS+WqgiC/woSDOBFO6DJH0Fi5AzskzYXABQqaSvf7WAPEwqsSkrzEojfD5qgDE2ewsqVBsjNRWiLM5ixB/YSyUGX0WIHJ/1SKkwD87FgIlC362pzVAAHLwpqWv+ABOk9QAHhbTRFL3EAELnYblji5P5zgk/FsE3dIR5p1HZBEbm7w5Wr6dW3BmJxzXltWPp7cNBB9amxCsHRLuA0LSMmtCt3iwKoRrqIHXtdIzEGrIAY4nvm0ClodYzmERRD/eInRFdDhjsL5fOnsCwjHWG9Nbh8We/ZJWIaEaFhZRU919Qo02hMFLVijIr+RCtV5SxgZrbAUkTGCqVHMEXqAIMqWFG6GpZnE8nQchjFQtPW1hPK0zxuwDq4j4PJ6xOkQnrNGhfdVGTqC8RufBJHrDsmT0JUi1NCeVpCAHw84yCTCraT+/HliKGm1cmYKF7vmBGWXE4WnotVUhkJItqQkaHJbXsIO8CFMFW1dUHdQNZWxpCMOX7a9egOgk+V5gXm9RELoLAEmHqmYXajzNIAFczBqGxr9PwxOruIGnvEShw2IIb1oCb4zl1yQkZ3lFT29oqjv7Yc+PwfvvVc0/zhAlxl+4MJ3xeMwcyEnKIXp8cqvm7NRU995d6t3TjvOiRLcutyNjO81akbjGU++L3t2ad08vvw187F6nX9WVz6tEiaNTjO+PuE+ZAzl1MePLd/G7dbcCMuvmNtAJflcy5qsGbs/O90xnOLp1xd+bn/3RrP9h4OJb+OjqtP0rjcE03F7S9iGQUXVERNiv/Vpvdoi1tY0gz8G26DmH5+mwDaI/7+Dw3DaQ0F91wD3/nJON9xxejz8ATqyBto+N2i0AAAAASUVORK5CYII=", + "public": true + } + ], + "scada": false, + "tags": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json index 103382ddf5..3737ba86a8 100644 --- a/application/src/main/data/json/system/widget_types/asset_admin_table.json +++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json @@ -12,13 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: 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 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-entities-table-basic-config", - "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\":{\"entitiesTitle\":\"Asset admin table\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"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\":\"Entity name\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6985800247727296,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.10073938422359707,\"funcBody\":\"return 'Device';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"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;\"}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"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\"}]},\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null}" + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"Asset admin table\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6985800247727296,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.10073938422359707,\"funcBody\":\"return 'Device';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"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;\"}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"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\"}]},\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null}" }, "tags": [ "provisioning", @@ -38,5 +36,6 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAm6SURBVHja7d3pV1PXGsBh/7LobW+txes9YYhiSsAoRKKtA63YWsHSWhwQcQATB4oo9QKptnWgqFhFMbEFLBVEoggyRMIUEwImJif53Q8BynVd5YDpUuneH5KwszmbZ+Xd+7xrveSceQQHuh+95a17IMC8YI9H5i1vsqcnOG/AwxxonoF53fJcgMjd8x4xJ9ojAREQAREQARGQNwDS2Tmj4f13J+Zv/8sgNftCszjON7nPdbRk+18yvHzV+Iv87JnP5XAogYRSpdsvPUz2aUWQ9oLA/3ac3BktiMmkBNKUkJM3/jL43HMku1x7fHJsMDyOD01AApMP4y08mZIWZUY6ghOQ4BSIHH1I3s6GBC8QKl6uXlMPoZLlaqMNaFonrbTQoFVrtJGgaf1E0uwcgdBhjSZvSy4B7amM2OXVDQYp5RrUaYMc2XFME/fVGIBXmxCrbeLJ7gT1xjYo15fo1FuHI5BwZVLshuYoQ7yaW6GUc8BpXcvg0YR+zmrvDJXE9dIeV9lfG3/L71xd5AwDDC89Otxu2AdnNDW959S5BCTj3Z6C2PSmnn0aL9ekIAfVR3vrNRUAIWfeRqefzzO7BnelhCiXCrub0r6KQP6jq+8/pB2NLuRCUpDijUD+p2Gene7lwIYw8ukedm8FCnL+DC1XnR/KDKAvBnJyCUg10C9Vg0O6G4Gsm7J6ijKBm33QJnVTrguBTd1PfjbPEs+DrL2oaJ2bTCZTVpbJZDI5Xg7JMMND6SG0JRnLOoEHuvSTD4G09UVFRRnGKWvEU7E7e7WeMelm5M8NSNfAK9nAJTVGIJunLOeiTCB4Zd+2TKk9stiHpSbys3ko5RYVFemORxXSKekMBoP6COC5lK3O8oO3Jlez5Sn6z0pLS0sr/oS4V223Nh3Q45HqlUN2Gi81VU9Ankj15Gdjlw6WlpaW2qIaWkdW19XV1RV9+AxbL/Sqr/BrDzjjqvliYi+bgFzWhKBCD8vPANsVQZ5ITdAhtVOuB+5I3eRn45Xqor79BnTlAMPqG2ze4qNTfYutm5/SE1tHtdpG6NQ5+CQ/MrZW3UpHmg6O6HtpS5wWUpwawqsuC3tyJTvlUnV4bMuGMPnZkGPsx5P7MJqQOrUzctLbRq9xWVrs/hCOj5amxe4NES6JTUkydMIZSecBkHOk5Um71WN4MyTd6sxpIS1x2t/4IXZpfJF0g3JD5oexOnvk3Sefq1M1uf5oQkb6xzfhfgg9bB4ECHfeifSOtraHADraxnOYrja/3xmE0P0HAc8Twk4fhJz+yIPPGcYzDLhdE/lVyxgMt7hwjuIdDrU3+ybfHRifIpopikjjBURABERABERABERABERABERABERABERABERABERA5hgk7HgKPscsjzXscDgcDt+bAJFVmWBVzfK/HLMWL5y/eLHtzYCormNVyQTsXeB2DdplX6sbcNwLKDnazx/AsIuwwzcw9sABPI2UJl4DZH38mFUlOxLT1PspjE9e/HGydtFjzEt0yaMKIce13Jvv1C9NfqeSTvXKD6pfD6Q2dZ9VJd89zy//DBemhxtU98PxPz2a3xVOsyiEOOZ3Fq9Bf5ifFoU27+JK3OuB1NnfKVbJ7q0rtCq5cBP3VW5WVFyZn5i4sEAhhPQTq86gr6BTNZSwJDFe9fT1QCh8VyWb14Yap0BuL+gbcfmUQr5PXOBCX0H9/GcG08iIK/yaIE81Krly8bEVKvckRDYYS1fUK4W4FmSAPqFEt41LCw/v+vR1hFaorBfuloXlc0fulA03XsVV5ufCPXxnDl1XcrSOSoDkC6A3fVvhhztHv3e9rWd2V8kiL+gr3voU5eGmeuCQVeRaAiIgAiIgAiIgAiIgAiIgAjItpGtONBFaAiIgAiIgAiIgAiIgf1tI04n/8x2yroKCgoKutwrSlG7Ie05iN0w2+0wgoQCAPPPKuwwXALgYZLZXJxoc2mIwnHiu7/RkG3wx5McckNP+OLUsNWX7GMCJlNStQU6sSN2h4LIDI0tSV6ZPXAPh0EXeA0A9Wps/O0ejsXZoS3rTc722gvFme0loBZbZqf6UkmOEdh8FHuhlMn/pSpbZ0Dj9xJ5/gTX9z5/fAzyoIwX60SAQmEmFtzHdsLp26HkHZyci6+zL1sjlTfLydkqOwc9fAa4OyKkFyFAIaTSwt4qB5ey9wHuMGtdsWDh6aRe6L9fF27mc8NH6PTNwGAxrhpgdJJz2zQ4o+bqh5sPxT+5eehDMxh0Kisyef2xct6RtKuTkATzvj17ahe5XKg/JUh+lexTHlcFgSP+NWUJofHcASozm1O8oS10LzpQeYKAx5YECyOIR9xV9eApkmxXUo5d2oeujOu+xDn7e84oOpRCXBJQcw64NArhT70BfA5grlYUW74/uOz8J2VkzFeKOVwx5sWOmEHIsgN943G7v6ltm717VqgCyyN5avBrLth7zOMRq6KxaMAlhraVj455XdCiFjB0CrDfAcRhw5uXl5ZXRvP1LJdXmp3l5e06N4DNlXTjG5WYKoCrLctTfWs23blouMrxn+4F9Ste58QXbi0LIX9rO9PkyLykYt+fFn8ebAfn9izVlYWUQ4wu3+5oJSM2bn2tZCgpefNrynYic2E/6RBovIAIiIAIiIAIiIAIiIDOGiDq7CC0BERABERABERABEZC3GiLq7DfdIF9n6OrVOieEb/1oB5CvKazGtp+96o+iY2jWdXZic8Abg01bVqytZtfX55OvAeWqx4omtqRaDiV7o+ZoXDPrOjuxmht4Y7BtgpaVwVSZmm/AYUx8zLOqyq5pJh6RPGCqocp+EWdllQ/H71Dj62ir/sE9G4fxFerssdYE7zjEZgQoshDOuJf0mOyD1ZppLgbR/HHkOSazujfxXGmaXLMTEocs8T+a9PJsHK9QZ48dPLrTG4NNnf2ZtgW4vUbm3EGSHmOsCnRM8/3h65/jNJttxIxReAqyro9DTGBsnpVj9nX22MFA8tUYbEb7D+uBjhQnQ1LV1fgzo/37k/c+e/ncf6Qz2rDbTEyAnFowVY5DzJBzPVoOxRBa4mKwbSKcbqVf3wm9ZWVlS0zDFuQtV18++bN/d8J3ZmICHP8WMqzXc5DjhiwHIeV+tBzKIRRG1sjvybI+w2w2y0DSY3JzDmt7p5neurRwv+Y3YgJ49IVfb5JdcabNC4cs8aatmVFzKIT8EQD/bdz3gaaxxoaGhoYQ0OwnbL85/dbjsd4agdsh8Dc0hWDI2tcSsJhbG+WZO968OrvFPNPfeEPr7B13ZwERdXaRxguIgAiIgAjI3xwyZ24QPDdu2ewemBeYGzfRlufNjduay/wXtgu0d9kLWo8AAAAASUVORK5CYII=", "public": true } - ] + ], + "scada": false } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/attributes_card.json b/application/src/main/data/json/system/widget_types/attributes_card.json index 2beedd2a76..7dd5a068fb 100644 --- a/application/src/main/data/json/system/widget_types/attributes_card.json +++ b/application/src/main/data/json/system/widget_types/attributes_card.json @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n\n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n for (var i = 0; i < self.ctx.datasources\n .length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\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 = $(\n '.tbDatasource-title',\n datasourceContainer);\n self.ctx.datasourceTitleCells.push(\n datasourceTitleCell);\n\n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx\n .$container);\n\n for (var a = 0; a < tbDatasource.dataKeys\n .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 }\n\n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells\n .length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data\n .length > 0) {\n var tvPair = cellData.data[cellData.data\n .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 ||\n cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey\n .decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(\n value, decimals, units, true);\n } else {\n txtValue = self.ctx.utilsService\n .customTranslation(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(\n datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells\n .length; i++) {\n self.ctx.datasourceTitleCells[i].css(\n 'font-size', datasourceTitleFontSize +\n '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',\n valueFontSize + 'px');\n self.ctx.valueCells[i].css('height',\n valueFontSize * 2.5 + 'px');\n self.ctx.valueCells[i].css('padding', '0px ' +\n valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size',\n labelFontSize + 'px');\n self.ctx.labelCells[i].css('height',\n labelFontSize * 2.5 + 'px');\n self.ctx.labelCells[i].css('padding', '0px ' +\n labelFontSize + 'px');\n }\n}\n\nself.onDestroy = function() {}", "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\":\"Attributes card\",\"decimals\":null}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\",\"decimals\":null}" }, "resources": [ { @@ -29,4 +29,4 @@ "public": true } ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/bar_chart.json b/application/src/main/data/json/system/widget_types/bar_chart.json index 5818f3d610..b195da4e3c 100644 --- a/application/src/main/data/json/system/widget_types/bar_chart.json +++ b/application/src/main/data/json/system/widget_types/bar_chart.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-time-series-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"type\":\"bar\"},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"type\":\"bar\"},\"_hash\":0.5534217244004682,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Bar chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"type\":\"bar\"},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"type\":\"bar\"},\"_hash\":0.5534217244004682,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Bar chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "chart", diff --git a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json index 7e0bfe1e66..f23747fb52 100644 --- a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json +++ b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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 self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], diff --git a/application/src/main/data/json/system/widget_types/bars.json b/application/src/main/data/json/system/widget_types/bars.json index c5ef85be52..3ed89042cc 100644 --- a/application/src/main/data/json/system/widget_types/bars.json +++ b/application/src/main/data/json/system/widget_types/bars.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-bar-chart-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-bar-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Bars\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Bars\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "bars", diff --git a/application/src/main/data/json/system/widget_types/bars_deprecated.json b/application/src/main/data/json/system/widget_types/bars_deprecated.json index 82590680bf..64598dac67 100644 --- a/application/src/main/data/json/system/widget_types/bars_deprecated.json +++ b/application/src/main/data/json/system/widget_types/bars_deprecated.json @@ -16,10 +16,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \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\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-chart-widget-settings", - "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\"}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars\"}" }, "tags": [ "bar", diff --git a/application/src/main/data/json/system/widget_types/basic_gpio_panel.json b/application/src/main/data/json/system/widget_types/basic_gpio_panel.json index ab9ede08ab..62007b61ae 100644 --- a/application/src/main/data/json/system/widget_types/basic_gpio_panel.json +++ b/application/src/main/data/json/system/widget_types/basic_gpio_panel.json @@ -12,10 +12,9 @@ "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.flex-1 {\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-gpio-panel-widget-settings", - "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}}}" + "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;\"}]}]}" }, "tags": [ "pin", diff --git a/application/src/main/data/json/system/widget_types/battery_level.json b/application/src/main/data/json/system/widget_types/battery_level.json index 08c6c6bd0a..45b3378a51 100644 --- a/application/src/main/data/json/system/widget_types/battery_level.json +++ b/application/src/main/data/json/system/widget_types/battery_level.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-battery-level-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-battery-level-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"batteryLevel\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"layout\":\"vertical_solid\",\"showValue\":true,\"autoScaleValueSize\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryLevelColor\":{\"type\":\"range\",\"color\":\"rgb(224, 224, 224)\",\"rangeList\":[{\"from\":null,\"to\":25,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":50,\"to\":null,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryShapeColor\":{\"type\":\"range\",\"color\":\"rgba(224, 224, 224, 0.32)\",\"rangeList\":[{\"from\":null,\"to\":25,\"color\":\"rgba(227, 71, 71, 0.32)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 0.32)\"},{\"from\":50,\"to\":null,\"color\":\"rgba(92, 223, 144, 0.32)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"sectionsCount\":4},\"title\":\"Battery level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:battery-high\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"batteryLevel\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"layout\":\"vertical_solid\",\"showValue\":true,\"autoScaleValueSize\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryLevelColor\":{\"type\":\"range\",\"color\":\"rgb(224, 224, 224)\",\"rangeList\":[{\"from\":null,\"to\":25,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":50,\"to\":null,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryShapeColor\":{\"type\":\"range\",\"color\":\"rgba(224, 224, 224, 0.32)\",\"rangeList\":[{\"from\":null,\"to\":25,\"color\":\"rgba(227, 71, 71, 0.32)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 0.32)\"},{\"from\":50,\"to\":null,\"color\":\"rgba(92, 223, 144, 0.32)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"sectionsCount\":4},\"title\":\"Battery level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:battery-high\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "accumulator", diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json index eb2b8fde5f..7fdb133b93 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "enviroment", diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json index 3c53033b15..ed9e2203b4 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/CO-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/CO-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "enviroment", diff --git a/application/src/main/data/json/system/widget_types/co2_card.json b/application/src/main/data/json/system/widget_types/co2_card.json index 3edb6cb025..5d48dbb64f 100644 --- a/application/src/main/data/json/system/widget_types/co2_card.json +++ b/application/src/main/data/json/system/widget_types/co2_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/co2_card_with_background.json b/application/src/main/data/json/system/widget_types/co2_card_with_background.json index 8f7936e919..ddee660eb3 100644 --- a/application/src/main/data/json/system/widget_types/co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/co2_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/compass.json b/application/src/main/data/json/system/widget_types/compass.json index 5ad01066e4..497491aee0 100644 --- a/application/src/main/data/json/system/widget_types/compass.json +++ b/application/src/main/data/json/system/widget_types/compass.json @@ -12,12 +12,11 @@ "templateHtml": "", "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.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'direction', label: 'Direction', type: 'timeseries' }];\n }\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-analogue-compass-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-compass-gauge-basic-config", - "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\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" + "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;\"}]}],\"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\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" }, "tags": [ "direction finder", diff --git a/application/src/main/data/json/system/widget_types/dashboard_state_widget.json b/application/src/main/data/json/system/widget_types/dashboard_state_widget.json index 3142fec16e..14ee5be560 100644 --- a/application/src/main/data/json/system/widget_types/dashboard_state_widget.json +++ b/application/src/main/data/json/system/widget_types/dashboard_state_widget.json @@ -12,10 +12,8 @@ "templateHtml": "\n\n
\n
Dashboard state widget
\n
(Specify dashboard state id in the advanced widget settings)
\n
\n", "templateCss": ".dashboard-state-widget-prompt {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .title {\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .subtitle {\n font-size: 24px;\n font-weight: normal;\n color: #999;\n text-align: center;\n}", "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n self.ctx.$scope.stateId = self.ctx.settings.stateId || \"\";\n self.ctx.$scope.defaultAutofillLayout = self.ctx.settings.defaultAutofillLayout;\n self.ctx.$scope.defaultMargin = self.ctx.settings.defaultMargin;\n self.ctx.$scope.defaultBackgroundColor = self.ctx.settings.defaultBackgroundColor;\n self.ctx.$scope.syncParentStateParams = self.ctx.settings.syncParentStateParams !== false;\n}\n\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-dashboard-state-widget-settings", - "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\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + "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;\"}]}],\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}" }, "tags": [ "embed", diff --git a/application/src/main/data/json/system/widget_types/date_range_navigator.json b/application/src/main/data/json/system/widget_types/date_range_navigator.json index 11732d5ee0..f7395524f7 100644 --- a/application/src/main/data/json/system/widget_types/date_range_navigator.json +++ b/application/src/main/data/json/system/widget_types/date_range_navigator.json @@ -12,10 +12,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-date-range-navigator-widget-settings", - "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\":{}}" + "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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "time-window", diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json index 515b3b10ea..9510e3a7a1 100644 --- a/application/src/main/data/json/system/widget_types/device_admin_table.json +++ b/application/src/main/data/json/system/widget_types/device_admin_table.json @@ -12,13 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: 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 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-entities-table-basic-config", - "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\":{\"entitiesTitle\":\"Device admin table\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"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\":\"Entity name\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.8332260553092266,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.08583217494773887,\"funcBody\":\"return 'Device';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"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;\"}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"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\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"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 if (vm.editDeviceFormGroup.get('deviceType').value !== vm.device.type) {\\n delete vm.device.deviceProfileId;\\n }\\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\":[],\"openInSeparateDialog\":false,\"openInPopover\":false,\"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\"}]},\"configMode\":\"basic\"}" + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"Device admin table\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.8332260553092266,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.08583217494773887,\"funcBody\":\"return 'Device';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"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;\"}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"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\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"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 if (vm.editDeviceFormGroup.get('deviceType').value !== vm.device.type) {\\n delete vm.device.deviceProfileId;\\n }\\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\":[],\"openInSeparateDialog\":false,\"openInPopover\":false,\"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\"}]},\"configMode\":\"basic\"}" }, "tags": [ "provisioning", diff --git a/application/src/main/data/json/system/widget_types/device_claiming_widget.json b/application/src/main/data/json/system/widget_types/device_claiming_widget.json index ca68456833..12cdfe57f7 100644 --- a/application/src/main/data/json/system/widget_types/device_claiming_widget.json +++ b/application/src/main/data/json/system/widget_types/device_claiming_widget.json @@ -12,10 +12,9 @@ "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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-device-claiming-widget-settings", - "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\":\"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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "provisioning", diff --git a/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json b/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json index e76926e46b..b17877cd51 100644 --- a/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json +++ b/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\":{},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\":{},\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "tags": [ "provisioning", diff --git a/application/src/main/data/json/system/widget_types/digital_speedometer.json b/application/src/main/data/json/system/widget_types/digital_speedometer.json index c1f3a73697..61f1c64836 100644 --- a/application/src/main/data/json/system/widget_types/digital_speedometer.json +++ b/application/src/main/data/json/system/widget_types/digital_speedometer.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\":{},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\":{},\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "tags": [ "velocity", diff --git a/application/src/main/data/json/system/widget_types/digital_thermometer.json b/application/src/main/data/json/system/widget_types/digital_thermometer.json index af1ecc3c82..9d379e002f 100644 --- a/application/src/main/data/json/system/widget_types/digital_thermometer.json +++ b/application/src/main/data/json/system/widget_types/digital_thermometer.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\":{},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\":{},\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "tags": [ "pyrometer", diff --git a/application/src/main/data/json/system/widget_types/digital_vertical_bar.json b/application/src/main/data/json/system/widget_types/digital_vertical_bar.json index da895874c0..7ccab13fd7 100644 --- a/application/src/main/data/json/system/widget_types/digital_vertical_bar.json +++ b/application/src/main/data/json/system/widget_types/digital_vertical_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\":{},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\":{},\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "tags": [ "vertical stripe", diff --git a/application/src/main/data/json/system/widget_types/doughnut.json b/application/src/main/data/json/system/widget_types/doughnut.json index 1982810161..fd1a320fb3 100644 --- a/application/src/main/data/json/system/widget_types/doughnut.json +++ b/application/src/main/data/json/system/widget_types/doughnut.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-doughnut-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-doughnut-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "ring", diff --git a/application/src/main/data/json/system/widget_types/doughnut_deprecated.json b/application/src/main/data/json/system/widget_types/doughnut_deprecated.json index 93a5a21a95..2359749a00 100644 --- a/application/src/main/data/json/system/widget_types/doughnut_deprecated.json +++ b/application/src/main/data/json/system/widget_types/doughnut_deprecated.json @@ -16,10 +16,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\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 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\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-doughnut-chart-widget-settings", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" }, "tags": [ "ring", diff --git a/application/src/main/data/json/system/widget_types/edge_quick_overview.json b/application/src/main/data/json/system/widget_types/edge_quick_overview.json index 6b7a54dbe6..f57e2153dd 100644 --- a/application/src/main/data/json/system/widget_types/edge_quick_overview.json +++ b/application/src/main/data/json/system/widget_types/edge_quick_overview.json @@ -12,10 +12,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-edge-quick-overview-widget-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"showTitleIcon\":true,\"titleIcon\":\"router\",\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edge Quick Overview\",\"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));\"}]}],\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"showTitle\":true,\"showTitleIcon\":true,\"titleIcon\":\"router\",\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edge Quick Overview\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"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));\"}]}],\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "gateway" diff --git a/application/src/main/data/json/system/widget_types/efficiency_card.json b/application/src/main/data/json/system/widget_types/efficiency_card.json index 3ee0c4ea87..92cb86ab35 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_card.json +++ b/application/src/main/data/json/system/widget_types/efficiency_card.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'efficiency', label: 'Efficiency', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency card\",\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json index 31712b3b81..f8bef025dd 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'efficiency', label: 'Efficiency', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/efficiency_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/efficiency_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency card with background\",\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json b/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json index e216dcd9be..a2090db66d 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json index e52de65f59..b337d40439 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/efficiency_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/efficiency_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/entities_hierarchy.json b/application/src/main/data/json/system/widget_types/entities_hierarchy.json index 90f6528f80..17b96bdbaa 100644 --- a/application/src/main/data/json/system/widget_types/entities_hierarchy.json +++ b/application/src/main/data/json/system/widget_types/entities_hierarchy.json @@ -12,10 +12,8 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onEditModeChanged();\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": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-hierarchy-widget-settings", - "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\":\"var 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\\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 \",\"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 \",\"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\",\"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**/\",\"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**/\",\"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**/\"},\"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": "{\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"var 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\\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 \",\"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 \",\"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\",\"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**/\",\"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**/\",\"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**/\"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"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\":{}}" }, "tags": [ "administration", diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index 01034a1d1d..70bba1c7a4 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,19 +11,13 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: 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 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'displayName', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: 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 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsDirective": "tb-entities-table-widget-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-entities-table-basic-config", - "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,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"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\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"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;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"Entities\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"defaultSortOrder\":\"displayName\",\"useRowStyleFunction\":false},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"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;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" }, - "tags": [ - "administration", - "management" - ], "resources": [ { "link": "/api/images/system/entities_table_system_widget_image.png", @@ -36,5 +30,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAnhSURBVHja7d3rU5NXHsBx/7KAa8WuBRahaEAIiQRFQSAKXlrUpagYAmgBJbXKin3qKlaKYAOmtFlQLpWkgCgSRC6GiwZFQAi3J/nuC9Da2RGSTNtZmXNeJWfmOfl9Juc2c855zjoWnU8HPvD01LnAukXHpMwHnuRJx+I65yRrIE061z2V1wJEfrpugDWRBgREQAREQAREQATkQ4A4X/sBkXW34IHO7d9PfqXT6XS6Tr9DztXpsv/zP7mJZf5AFEEjNCn8nEZ2WE4HWSxjfkPUxyyXAxv/IEjoAZoUsnxxx24b5ScOaaqMquwFHqSoL3u8KK1mM0h6xrT9WYXalF7mC2MODnsPKYXdxfycFFdOR2qJKkfmB83hqDK601RFc2Sc1e61HlJVeQep/NjSpJDrEvuMERRHtF9UlHZs+nk6+N8PQhu9hLSuf10V4dakdx1RY4zvyU/2HlI43LKp9rmysUXR3xhQ3RpoHQysatlc5gr5qlt9lvAs++6/35M2LHoFuVuxxayQaSw5qJCLM+hRTBBXfjdQklTnvITIYebDxWiu0a14vf2AVBDodUVVb/xEUeKhtzQvoLnxI9hSW7UNEstaAxe5GU14DcZ99CvGvIO4E6MV8g8RTZVvIeprdwIlSWrxEkLRgaBHaK7Ro5iMPiRJ0oIPVSvtCI6NVY8Cmxs/gvCam9GQWGZbL1P1KeE1GHXeQ+hdr5CLdoycVcy/hYxvKnd++8RbSJciCjSZz/O2U6Ad6pR8aSMPA7qa1/fUBzQuQfoC7jwKKZsOufYs0eAbRGmDUqU8unvLKaXj8in6lVNk/IBVG3z4hReh1O8AUH4NmpStqk5mcsKjzV5DMq7DiVNydmhmgskWC4n1XI1IzqigPSHkn69JrOdKNkPK8b9qQBzd1A+aax/8yN4WkMeagMjjYq4lIAIiIAIiIAIiIAIiIALyZ0MG10QSVUtABERABERABERABERABOQDh3TchuYGuN3x62kZ6DeMAy3lUG8wGDrp0J9aYT+Ay2Aoaf1dTleVL7G4/vVFpZupkpNLy5XdBoOhDsau1PgOeZQIGamwy14R0ADkBzhgIiINci5YLCNPtj9sC3//hoDJTdb6/SXv5ox1+QLJPTeYcZMMqUfdCXAj22LpoU9Z3u87RA6enY/WuOZC5IqkDHB9qnbAcWMapPcCLT+Cru39kGBY3OrgSXZWtysXGuvuV/K6+OBVGXtW9uNVY0noozZ3PsRDTQHAVzUAGc1+tZH0tl9yC5vaM6goiRml6ssdDpqz7qeBWvtprgw83z63IoQTNRNRXfaomaQe0jvNeg5dGfvixliUvStqdrVYTJ/XJXe6wxZo+AzgpGrrgVeEHIk/NO075JtvCu+0nJEkKr68+jU7+2McM/Hj99OgdWp+fzXM7bGxMiTvpinFYtllqzo/Fesx62c3u3nZ9f1+i2XHqrttftpbstvOqayquFMA9mFPUcHMhl6KLvoO6Tiocc1pDndSUTChfJhClOPcLmN25A2A6jPIn1WyCmRva0WSJEmDU+rai5j1M1sArqRKkrTqppTQ19j2IdfVSpeXc7qT5kLgXqbvkIXwdMiIWKSigOPRdUQ5Hlut1+O7ZiLnOF1O7iVWgdSq5K74RboWyYxxYNYT20/9d22JMg9W3coR7qTxADC0fRgZVCNUnyDhAZLRj3Ek5TpcT4WKAjpDF4hyAJ1pUKlO3udqCtRqtRfeDwnURmc+hzJV0iEXdxLBrOd+3L7UF3yt2pM5t1osDap0zWM82lQb6NqxqXXxw9jVKUkTf+SAKLu870fl37VOF4A848Vznt82y7kBz1IxM/70WmKKIiACIiACIiACIiACIiAC8kFAxDq7qFoCIiACIiACIiACIiACIiDvJL8OlM+/+bDgPyRmnPH4jp4UAKKUSqW8WH/4KsB0dN1qZU2FaGOyfneAtD7fh1AcOp1ubyyt0QlZCwDlqp0Zs0D1Nj8gW164Ekx0awEWIgFqC7MlgNzIVdftJ4PBpJHB9e4C7tK5xJk5rzDVRs+2QU5WAcPRC2RVg1PziT+QZ/svswwZ3WVrdwOlEmDLzPMKwl4bl7QJ52fD5zh/zazHFpuhdXJ+Z7w3Lwpwq52z/4DG48DkY8irgc+b/YKkR8hvID0RZ5KOLkNmtU4vIfk37Qmye8dQTgOqF2a9RzmA6XzHHo8c+3z1aOqPg3KIsuVl3L74WX7KnfULcvLYuTcQQI4cXoKcvYmXkOOmGxE6Xdhd2xf9+zHrJ8MAvo3U6UKtq0ezuwcsW1N1ZwAY1zxmYsdr/yAvZmLuLEPsfUsll0rMRWu1IVvrvIAsRA7U5kxNTS26VedrMevlYDdz45X5U1NTq/dgHakAslx4C2Am8R7cjNHGB+7yB8KTiJHuGLvd/qptz0jztvnlNoI3/8jH9ub0LxmPbB06PYUxxIVZzxHJmVPpjPz1af7qq7MHmwDaL+ycp2l8Mc1ot/cDfv0jJdPQcHPUYDAY2rEczR0EGpd2HZlX3YLhMhiKmoD+3EwLOK5DVy2ui0eroVefeXfVWFzFHsBTZJoFaXDcYDAYSoHFIjGyC4iACIiACIiACIiACIiACMj/OUSss4uqJSACIiACIiACIiACIiAfOMQ6Bcz+eOsl4G7zqaxFi6W+782Xh8McAOCY65mXr5L3NFf2APS9Ws6YvMOoxWKx2PyAvNpwBRa0pd8px7msCvQJMhkkXdr55uxoXScbAQib7jZ59/zJE7diG/hp18Y3h8ezg+mTJCnzlB+Q8jNxMHIRjluwzHzkGyQY+mO4P4irjvuDbISe8s6w6aF2fh6+UedBNlU+6Xjv4/NaN7U5tIwlL0OaMoMBSO31A6IZzegEGIwbAnyEbB52nMunwIQzigITG7Fvu1UQOG3WE3uwZreJ7OxbcXkrFlFYASxDZuKfBQN07fOjjTxKoU4PnFFmy75D/nb0s6iGdyEFlRA6bdYT68BUMB0iY1oRYkuWf4Pk354NBjjS7AckP9l4ZpML8GT+6DskGCY/WXgHktkMYdNmPbGj1BqG1awM6Y1z8hbSEWm5HWQBh9rjO2QurMVq/dzUWwLnKvyCzATNF1fgWIaUXEUOfguZC3Xx/QqQUc0gv0HuS1JZ0BXIq/aj+zUfA2zJ84n5xrhx3yEbDLlxl7inNO5dhowqiw8HvYVwYU9x1AoQVbrRaHQvQQzdwGwwjEfO+wEZeA64bR75119cADafILLV2vES6G9+1cngGDaYaHZ2yi8H6JzjxQDuh7arKxyyt1mtVqsHumdgYA6Q2+DlE/yA/Mkp52KVsvdPKvsvhcw3Vo2IuZaACIiACIiACIiAeANZMxcEr40rmyec6xbWxiXa8rq1ca25zH8BTrZIsxZexqkAAAAASUVORK5CYII=", "public": true } + ], + "scada": false, + "tags": [ + "administration", + "management" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/entity_count.json b/application/src/main/data/json/system/widget_types/entity_count.json index 3bd2f26c9d..16b0b05168 100644 --- a/application/src/main/data/json/system/widget_types/entity_count.json +++ b/application/src/main/data/json/system/widget_types/entity_count.json @@ -12,12 +12,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.countWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.countWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '220px',\n previewHeight: '100px',\n embedTitlePanel: true,\n hideDataSettings: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-entity-count-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-entity-count-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"count\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"return 150;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"],\"assignedToCurrentUser\":false,\"assigneeId\":null}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLabel\":true,\"label\":\"Devices\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":20,\"iconSizeUnit\":\"px\",\"icon\":\"devices\",\"iconColor\":{\"type\":\"constant\",\"color\":\"rgba(255, 255, 255, 1)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIconBackground\":true,\"iconBackgroundSize\":36,\"iconBackgroundSizeUnit\":\"px\",\"iconBackgroundColor\":{\"type\":\"constant\",\"color\":\"rgb(241, 141, 23)\",\"rangeList\":[],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showChevron\":false,\"chevronSize\":24,\"chevronSizeUnit\":\"px\",\"chevronColor\":\"rgba(0, 0, 0, 0.38)\",\"layout\":\"column\"},\"title\":\"Entity count\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":null,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.54)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"count\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"return 150;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"],\"assignedToCurrentUser\":false,\"assigneeId\":null}}],\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLabel\":true,\"label\":\"Devices\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":20,\"iconSizeUnit\":\"px\",\"icon\":\"devices\",\"iconColor\":{\"type\":\"constant\",\"color\":\"rgba(255, 255, 255, 1)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIconBackground\":true,\"iconBackgroundSize\":36,\"iconBackgroundSizeUnit\":\"px\",\"iconBackgroundColor\":{\"type\":\"constant\",\"color\":\"rgb(241, 141, 23)\",\"rangeList\":[],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showChevron\":false,\"chevronSize\":24,\"chevronSizeUnit\":\"px\",\"chevronColor\":\"rgba(0, 0, 0, 0.38)\",\"layout\":\"column\"},\"title\":\"Entity count\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":null,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"titleColor\":\"rgba(0, 0, 0, 0.54)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "total", diff --git a/application/src/main/data/json/system/widget_types/flooding_level_card.json b/application/src/main/data/json/system/widget_types/flooding_level_card.json index 9d9215cf0e..a6f6e610b9 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_card.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json index 20fa345a25..727122f32b 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/flooding_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/flooding_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json index b081f8aa99..e14d49d98b 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json index 2048b17063..4c8cd2808b 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/flooding_level_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/flooding_level_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/flow_rate_card.json b/application/src/main/data/json/system/widget_types/flow_rate_card.json index bfe1c34f15..c78fcb704b 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_card.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_card.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'flowRate', label: 'Flow rate', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate card\",\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json index cf127d5bf7..689b6fc5e9 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'flowRate', label: 'Flow rate', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/flow_rate_value_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/flow_rate_value_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate card with background\",\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/flow_rate_gauge.json b/application/src/main/data/json/system/widget_types/flow_rate_gauge.json index 83176686ac..d67ef8ad90 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_gauge.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_gauge.json @@ -18,7 +18,7 @@ "dataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":90,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":9,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":90,\"color\":\"#D81838\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"m³/hr\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"mdi:hydro-power\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":90,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":9,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":90,\"color\":\"#D81838\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"m³/hr\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"mdi:hydro-power\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json index 8e63ab04e8..a7bdcaeeb9 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"flowRate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m³/hr\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"flowRate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m³/hr\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json index d7ee1333e3..69192e5682 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"flowRate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/flow_rate_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m³/hr\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"flowRate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/flow_rate_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m³/hr\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_card.json b/application/src/main/data/json/system/widget_types/fluid_pressure_card.json index 2c881da205..32724202b5 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_card.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'pressure', label: 'Pressure', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure card\",\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json index d059d9b026..6e1175f4f2 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'pressure', label: 'Pressure', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/pressure_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/pressure_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure card with background\",\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json b/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json index 98318d30a4..c555fae338 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json @@ -18,7 +18,7 @@ "dataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 24) {\\n\\tvalue = 24;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":24,\"majorTicksCount\":7,\"colorMajorTicks\":\"#444\",\"minorTicks\":5,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":24,\"color\":\"#D81838\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"bar\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 24) {\\n\\tvalue = 24;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":24,\"majorTicksCount\":7,\"colorMajorTicks\":\"#444\",\"minorTicks\":5,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":24,\"color\":\"#D81838\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"bar\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json index 0a4e80dd60..cd1c1571ac 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":25,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"bar\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":25,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"bar\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json index 8000ff00af..f97347d1e5 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 25) {\\n\\tvalue = 25;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":25,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/pressure_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"bar\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 25) {\\n\\tvalue = 25;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":25,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/pressure_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"bar\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/gateway_configuration.json b/application/src/main/data/json/system/widget_types/gateway_configuration.json index 9eb9ab816b..b87ce49d05 100644 --- a/application/src/main/data/json/system/widget_types/gateway_configuration.json +++ b/application/src/main/data/json/system/widget_types/gateway_configuration.json @@ -42,4 +42,4 @@ "ble", "bluetooth" ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json b/application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json index a02c0112a3..29440d4c64 100644 --- a/application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json +++ b/application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json @@ -42,4 +42,4 @@ "ble", "bluetooth" ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/gateway_connectors.json b/application/src/main/data/json/system/widget_types/gateway_connectors.json index 54ad85048c..b295b1341f 100644 --- a/application/src/main/data/json/system/widget_types/gateway_connectors.json +++ b/application/src/main/data/json/system/widget_types/gateway_connectors.json @@ -40,4 +40,4 @@ "ble", "bluetooth" ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/gateway_custom_statistics.json b/application/src/main/data/json/system/widget_types/gateway_custom_statistics.json index dd1a8ea3ee..1781c15e90 100644 --- a/application/src/main/data/json/system/widget_types/gateway_custom_statistics.json +++ b/application/src/main/data/json/system/widget_types/gateway_custom_statistics.json @@ -44,4 +44,4 @@ "ble", "bluetooth" ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/gateway_events.json b/application/src/main/data/json/system/widget_types/gateway_events.json index 7b9542d2c5..88945de073 100644 --- a/application/src/main/data/json/system/widget_types/gateway_events.json +++ b/application/src/main/data/json/system/widget_types/gateway_events.json @@ -12,10 +12,9 @@ "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\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-gateway-events-widget-settings", - "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\":{}}" + "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));\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "router", diff --git a/application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json b/application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json index 1c89f7ae77..e6fb8c0f54 100644 --- a/application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json +++ b/application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json @@ -44,4 +44,4 @@ "ble", "bluetooth" ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/gateway_logs.json b/application/src/main/data/json/system/widget_types/gateway_logs.json index 314f7e8d0c..21675e550a 100644 --- a/application/src/main/data/json/system/widget_types/gateway_logs.json +++ b/application/src/main/data/json/system/widget_types/gateway_logs.json @@ -41,4 +41,4 @@ "ble", "bluetooth" ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/gateway_markdown_html_card.json b/application/src/main/data/json/system/widget_types/gateway_markdown_html_card.json new file mode 100644 index 0000000000..bb69b66b94 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/gateway_markdown_html_card.json @@ -0,0 +1,29 @@ +{ + "fqn": "gateway_widgets.markdown_card", + "name": "Gateway Markdown/HTML Card", + "deprecated": false, + "image": "tb-image;/api/images/system/markdown_html_card_system_widget_image.png", + "description": "Renders markdown/HTML using the configurable pattern or function with applied attributes or time series values.", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 3.5, + "resources": [ + { + "url": "tb-resource;/api/resource/js_module/system/gateway-management-extension.js", + "isModule": true + } + ], + "templateHtml": "\n", + "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\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 dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", + "settingsDirective": "tb-markdown-widget-settings", + "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\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + }, + "resources": null, + "scada": false, + "tags": [ + "web", + "markup" + ] +} diff --git a/application/src/main/data/json/system/widget_types/gauge.json b/application/src/main/data/json/system/widget_types/gauge.json index 06883eae69..0683383b53 100644 --- a/application/src/main/data/json/system/widget_types/gauge.json +++ b/application/src/main/data/json/system/widget_types/gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "tags": [ "measure", diff --git a/application/src/main/data/json/system/widget_types/google_map.json b/application/src/main/data/json/system/widget_types/google_map.json index cd69b304e9..22750ae737 100644 --- a/application/src/main/data/json/system/widget_types/google_map.json +++ b/application/src/main/data/json/system/widget_types/google_map.json @@ -12,10 +12,8 @@ "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.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7568\",\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Google Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7568\",\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Google Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_card.json b/application/src/main/data/json/system/widget_types/ground_temperature_card.json index 77c2c0ccfb..d93653fb8d 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ground temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ground temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json index 3263548bb2..3b59b417d5 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/ground_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ground temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/ground_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ground temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/here_map.json b/application/src/main/data/json/system/widget_types/here_map.json index b49e1dcf41..117bf9e420 100644 --- a/application/src/main/data/json/system/widget_types/here_map.json +++ b/application/src/main/data/json/system/widget_types/here_map.json @@ -12,10 +12,8 @@ "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('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.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"here\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"HERE.normalDay\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"useV3\":true,\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},\"mapImageUrl\":\"tb-image;/api/images/system/here_map_system_widget_map_image.svg\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"here\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"HERE.normalDay\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"useV3\":true,\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},\"mapImageUrl\":\"tb-image;/api/images/system/here_map_system_widget_map_image.svg\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/horizontal_2_1_elliptical_tank.json b/application/src/main/data/json/system/widget_types/horizontal_2_1_elliptical_tank.json index f0e9731837..d1d282febb 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_2_1_elliptical_tank.json +++ b/application/src/main/data/json/system/widget_types/horizontal_2_1_elliptical_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal 2:1 Elliptical\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal 2:1 Elliptical\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json index 8190e9fcaf..3dc899c2d8 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json index 2bf3dcbe23..989512f7e1 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_air_quality_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-windy\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_air_quality_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"AQI\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_bar.json b/application/src/main/data/json/system/widget_types/horizontal_bar.json index 00dfc56e43..0983ebf8f7 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_bar.json +++ b/application/src/main/data/json/system/widget_types/horizontal_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/horizontal_capsule_tank.json b/application/src/main/data/json/system/widget_types/horizontal_capsule_tank.json index 1eb8158318..706deb93fd 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_capsule_tank.json +++ b/application/src/main/data/json/system/widget_types/horizontal_capsule_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Capsule\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Capsule\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json index 02482fb5c9..ad9a952d30 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json index 38391e23d6..f6f003894a 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/CO-value-card-horizontal-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule-co\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/CO-value-card-horizontal-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_co2_card.json b/application/src/main/data/json/system/widget_types/horizontal_co2_card.json index e6c8e1eb6d..300686a0d1 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_co2_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_co2_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json index 34a4e1aba4..78868b1a7e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_cylinder_tank.json b/application/src/main/data/json/system/widget_types/horizontal_cylinder_tank.json index 6a17402bac..7df8104a86 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_cylinder_tank.json +++ b/application/src/main/data/json/system/widget_types/horizontal_cylinder_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Cylinder\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Cylinder\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/horizontal_dish_ends_tank.json b/application/src/main/data/json/system/widget_types/horizontal_dish_ends_tank.json index c3cc132d9d..47e40673af 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_dish_ends_tank.json +++ b/application/src/main/data/json/system/widget_types/horizontal_dish_ends_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Dish Ends\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Dish Ends\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/horizontal_doughnut.json b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json index 7dd001ae96..a5cc708e04 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_doughnut.json +++ b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-doughnut-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-doughnut-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "ring", diff --git a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json index a955d68869..2d30185a5e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json @@ -12,12 +12,11 @@ "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 self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'efficiency', label: 'Efficiency', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal efficiency card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal efficiency card\",\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json index 78f8863965..44d80a7b96 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'efficiency', label: 'Efficiency', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_efficiency_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal efficiency card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"trending_up\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_efficiency_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal efficiency card with background\",\"configMode\":\"basic\",\"units\":\"%\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json b/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json index 441a5a520c..6571efec85 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Ellipse\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Ellipse\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json index 333d983be9..94179c9507 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json index 7c6794b4f7..c65691497e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_flooding_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"flood\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_flooding_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal flooding level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json index 15aca34b02..b0e2626fbb 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json @@ -12,12 +12,11 @@ "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 self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'flowRate', label: 'Flow rate', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal flow rate card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal flow rate card\",\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json index 4f6de487cd..7b2c4ceb51 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'flowRate', label: 'Flow rate', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_flow_rate_card_background_(1).png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal flow rate card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:hydro-power\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_flow_rate_card_background_(1).png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal flow rate card with background\",\"configMode\":\"basic\",\"units\":\"m³/hr\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json index b9432be3ca..ebeec0fe51 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json @@ -12,12 +12,11 @@ "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 self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'pressure', label: 'Pressure', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal pressure card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal pressure card\",\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json index 5598b8ed1c..e15bd24d4d 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'pressure', label: 'Pressure', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_pressure_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal pressure card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_pressure_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal pressure card with background\",\"configMode\":\"basic\",\"units\":\"bar\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json index 6b0f064707..ac87fdd54f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json index bb03947926..f3e44ba702 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_ground_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_ground_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json b/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json index fa576d2ee0..220a94e9e7 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json index 450d4046f3..05ad28b2d9 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json index 456268a8fa..01d6d22c96 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json index a3ff36784f..232a07701b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json index 8cd5159aab..07db7700b8 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"IAI\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"IAI\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json index 8cc3664ad4..2498e25b89 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/IAI-horizontal-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal individual allergy index (IAI) card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/IAI-horizontal-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal individual allergy index (IAI) card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json index f31a0a7bb3..aeeae67d47 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json index 637c6182cc..8d2cb283ab 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_leaf_wetness_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_leaf_wetness_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json index ce820784bd..aaa8170eef 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal nitrogen dioxide (NO2) card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal nitrogen dioxide (NO2) card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json index d36eb56e95..369d7de0ec 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/NO2-value-card-horizontal-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal nitrogen dioxide (NO2) card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/NO2-value-card-horizontal-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal nitrogen dioxide (NO2) card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json index a6a5cc1004..39ba22f0e7 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json index 02d041e1d6..c45812b66e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_noise_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_noise_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_oval_tank.json b/application/src/main/data/json/system/widget_types/horizontal_oval_tank.json index de7b3dfb75..c31ef7e522 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_oval_tank.json +++ b/application/src/main/data/json/system/widget_types/horizontal_oval_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Oval\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Oval\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json index 61a63dd7b6..bf75f97846 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json index 0a0886ca7b..5b7c3464bc 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/ozone-horizontal-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/ozone-horizontal-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json b/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json index 5f9eb7d645..a50435db0e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json index ca3b39eac9..12e6a9904f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json index 617031cc7e..658c51a0b9 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json index f85406bd75..3aec6a698f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json index b7d93c0cbc..e56f1d307b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json @@ -12,12 +12,11 @@ "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 self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'powerConsumption', label: 'Power consumption', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal power consumption card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal power consumption card\",\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "power", diff --git a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json index d74452d787..fc2c20d623 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'powerConsumption', label: 'Power consumption', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_power_consumption_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal power consumption card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_power_consumption_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal power consumption card with background\",\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "power", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json b/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json index 64ab997988..197ebc2488 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal pressure card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal pressure card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json index 0b4d9dbf35..8872f4e0e1 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_pressure_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal pressure card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_pressure_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal pressure card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json index de91fdde12..cc522255e1 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json @@ -12,12 +12,11 @@ "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 self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'vibration', label: 'Vibration', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal vibration card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal vibration card\",\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "vibration", diff --git a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json index c787de5a2c..9e4f306b0d 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'vibration', label: 'Vibration', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_vibration_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal vibration card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_vibration_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal vibration card with background\",\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "vibration", diff --git a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json index e5edbd240c..3d27d7a9a2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json index f15bce1cb9..9ace2ca876 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_radon_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_radon_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json index 22a7cf3675..7608e5b765 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json index 19f0333ac8..86adcd593f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_rainfall_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_rainfall_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json index 6e041e0fd8..59733ff38f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json @@ -12,12 +12,11 @@ "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 self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'rotationalSpeed', label: 'Rotational speed', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal rotational speed card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal rotational speed card\",\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json index 90cc5dbb51..6e2cccf0a2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '90px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'rotationalSpeed', label: 'Rotational speed', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_rotational_speed_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal rotational speed card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"horizontal\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/horizontal_rotational_speed_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal rotational speed card with background\",\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json index d94b6c218a..b222b915de 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json index da6e3f16cb..6f271d3b67 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_snow_depth_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_snow_depth_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json index 7c8094006c..5ac1793475 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json index 970acb6bc6..c53214bc37 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_soil_moisture_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_soil_moisture_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json index c9b3cbfa93..bf5b30bba8 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json index 7220911d6d..341003da53 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_solar_radiation_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_solar_radiation_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json index ad8d850963..7869a8d0a9 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json index f8c2601a28..197f62850c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/SO2-value-card-horizontal-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/SO2-value-card-horizontal-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json b/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json index d38d5412bd..8685ab39d6 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json index 6ed35afab5..795daa25de 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json index 57eed149c4..e25029b492 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json index 2a542e7c62..12cb3b8dca 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_uv_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_uv_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_value_card.json b/application/src/main/data/json/system/widget_types/horizontal_value_card.json index 7ae3422352..012bd91b9f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_value_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_value_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"constant\",\"color\":\"#5469FF\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"constant\",\"color\":\"#5469FF\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json b/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json index df72fa560f..b43b513437 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json index c7137ba291..eafcdbea5b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_vibration_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_vibration_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json b/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json index f36ba0adca..3f82e76027 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal visibility card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal visibility card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json index e278d5c9a6..df818339b2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_visibility_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal visibility card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_visibility_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal visibility card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json index cccfe7c18b..8adb9110e5 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json index a97f139a51..d864241952 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_volatile_organic_compounds_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_volatile_organic_compounds_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json index 9883066d90..257374763f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json index 0b6700ebc8..d228e88c17 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_wind_speed_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/horizontal_wind_speed_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/html_card.json b/application/src/main/data/json/system/widget_types/html_card.json index 8dc2dd91a7..7ac6bbbfef 100644 --- a/application/src/main/data/json/system/widget_types/html_card.json +++ b/application/src/main/data/json/system/widget_types/html_card.json @@ -12,10 +12,8 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n\n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-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 = 'htmlCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml + self.ctx.widget.id));\n cardHtml = '
' + \n self.ctx.settings.cardHtml + \n '
';\n cardHtml = replaceCustomTranslations(cardHtml);\n self.ctx.$container.html(cardHtml);\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 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\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", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-html-card-widget-settings", - "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}" + "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;\"}]}],\"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}" }, "tags": [ "web", diff --git a/application/src/main/data/json/system/widget_types/html_value_card.json b/application/src/main/data/json/system/widget_types/html_value_card.json index 56a731fbb6..f647b767a6 100644 --- a/application/src/main/data/json/system/widget_types/html_value_card.json +++ b/application/src/main/data/json/system/widget_types/html_value_card.json @@ -12,10 +12,8 @@ "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 + self.ctx.widget.id));\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": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-html-card-widget-settings", - "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\":{}}" + "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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "web", diff --git a/application/src/main/data/json/system/widget_types/humidity_card.json b/application/src/main/data/json/system/widget_types/humidity_card.json index 28b7c88e7c..9d4f1cc177 100644 --- a/application/src/main/data/json/system/widget_types/humidity_card.json +++ b/application/src/main/data/json/system/widget_types/humidity_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/humidity_card_with_background.json index 2f33b5ec33..4dd3d87fcc 100644 --- a/application/src/main/data/json/system/widget_types/humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/humidity_progress_bar.json b/application/src/main/data/json/system/widget_types/humidity_progress_bar.json index b8d80a2040..9ba12ab343 100644 --- a/application/src/main/data/json/system/widget_types/humidity_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/humidity_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json index 4d3ddc7d21..818012bd4c 100644 --- a/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/humidity_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.1)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/humidity_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.1)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/illuminance_card.json b/application/src/main/data/json/system/widget_types/illuminance_card.json index 6e24dcaa4a..b3cb88702c 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/illuminance_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json index 0c990bb1b9..3242efe285 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json b/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json index de3b03cf95..3743fd8eb7 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json index 181208cb24..90011a21ef 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/illuminance_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/illuminance_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 991c0b463f..1091bd4232 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\" \",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" + "defaultConfig": "{\"datasources\":[],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\" \",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Image Map\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/image_map_deprecated.json b/application/src/main/data/json/system/widget_types/image_map_deprecated.json index 23cf7f7a14..f497b1e8fb 100644 --- a/application/src/main/data/json/system/widget_types/image_map_deprecated.json +++ b/application/src/main/data/json/system/widget_types/image_map_deprecated.json @@ -12,10 +12,8 @@ "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.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "building", diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json index a578136d2b..5e5b722af2 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Individual allergy index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Individual allergy index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json index 7f6cfe39a5..1e9df7e26b 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/IAI-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"#FFFFFFB8\",\"blur\":3}},\"autoScale\":true},\"title\":\"Individual allergy index card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"IAI\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:flower-pollen\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/IAI-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"#FFFFFFB8\",\"blur\":3}},\"autoScale\":true},\"title\":\"Individual allergy index card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_card.json b/application/src/main/data/json/system/widget_types/indoor_co2_card.json index 1133460238..21a4ca75cb 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json index e1ddcb3b6b..c624a97f49 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json index b453998ddb..0707b87752 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json index 4ca050fd88..66a3cfa70b 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"co2\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_co2_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal CO2 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json index 0c62fdfe5b..3e76013cfa 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json index 04f8faefcb..c5e7071561 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json index b921411cd8..7810e9003e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json index d81be63a9c..e2c832cb52 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor horizontal illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json index 5d315d0e01..2aead4005e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json index ea3833494c..b8bb16de8e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json index 5ae0d04108..19fbff1e3b 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json index 6e47de67a8..8af6ab208f 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor horizontal PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor horizontal PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json index 4b5f76317c..2a63e7432c 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json index 1b1231720f..eb3d2c0319 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_horizontal_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Horizontal temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_card.json b/application/src/main/data/json/system/widget_types/indoor_humidity_card.json index 1310fcf9e0..9e489f5d80 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json index 998d0cbe8b..24a8689d70 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor humidity card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_humidity_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor humidity card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json index bc02349832..2b7df3ea28 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json index 8f3f721ec5..612fc9a0ce 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_humidity_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_humidity_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json index 73732e8f4b..d6627cf344 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json index b6f143b9f8..0e0acf7698 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:lightbulb-on\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_illuminance_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Illuminance card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json index d58ec4f73d..5067c90a8d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":1000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":1000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json index 676428dbcd..bd150ccfaf 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":1000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_illuminance_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":1000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_illuminance_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"lx\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_card.json b/application/src/main/data/json/system/widget_types/indoor_pm10_card.json index 1a58d29bd4..8e28ea4d3c 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json index 14f1445ebc..88fb98770d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json index 990b7caecc..fcb7017d57 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json index ef1fde22b2..8d7cdf1681 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:broom\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json index 0a442fa4e0..728abc34fd 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":800,\"color\":\"#F36900\"},{\"from\":800,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json index 537f95b673..41146249d0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_co2_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":800,\"color\":\"#F77410\"},{\"from\":800,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_co2_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json index eb278e4c0f..8f9c801dd5 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#FFA600\"},{\"from\":30,\"to\":60,\"color\":\"#3FA71A\"},{\"from\":60,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json index eda7d8919d..7c881e9347 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_humidity_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":30,\"color\":\"#F89E0D\"},{\"from\":30,\"to\":60,\"color\":\"#3B911C\"},{\"from\":60,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_humidity_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json index 4362cdf807..2eeca64aca 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#FFA600\"},{\"from\":300,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json index 5652a3693f..7f47b543ac 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_illuminance_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 400 - 200;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":100,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":100,\"to\":300,\"color\":\"#F89E0D\"},{\"from\":300,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_illuminance_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json index 995181bff6..85e703440c 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":150,\"color\":\"#FFA600\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json index 26b4c74ca6..5006398499 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_pm10_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":150,\"color\":\"#F89E0D\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_pm10_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json index 550bb8d03e..35a6f30a81 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#80C32C\"},{\"from\":35,\"to\":75,\"color\":\"#FFA600\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json index 368e31b0f6..05f3b8591f 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_pm2_5_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 150) {\\n\\tvalue = 150;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":35,\"color\":\"#7CC322\"},{\"from\":35,\"to\":75,\"color\":\"#F89E0D\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_pm2_5_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:broom\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json index 3b118b8994..e6101557f0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json index e62cbba7fd..e9b50ba2b8 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_temperature_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_simple_temperature_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_card.json b/application/src/main/data/json/system/widget_types/indoor_temperature_card.json index 5f55ace191..1058baae12 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json index 23466d55ec..3deb1c409a 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json b/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json index a935befa6a..f0726e05f3 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json @@ -18,7 +18,7 @@ "dataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "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() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":40,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":9,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":40,\"color\":\"#DE2343\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" + "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() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":40,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":9,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":40,\"color\":\"#DE2343\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json index ae5196ce89..3c392c9d26 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":40,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":40,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#234CC7\"},{\"from\":18,\"to\":24,\"color\":\"#3FA71A\"},{\"from\":24,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json index e2b6443efe..d6bb4a992e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":40,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_temperature_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":40,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/indoor_temperature_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":18,\"color\":\"#224AC2\"},{\"from\":18,\"to\":24,\"color\":\"#3B911C\"},{\"from\":24,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/knob_control.json b/application/src/main/data/json/system/widget_types/knob_control.json index 8840787f58..9c56db0fe5 100644 --- a/application/src/main/data/json/system/widget_types/knob_control.json +++ b/application/src/main/data/json/system/widget_types/knob_control.json @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", "dataKeySettingsForm": [], "settingsDirective": "tb-knob-control-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2,\"units\":null}" + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{},\"decimals\":2,\"units\":null}" }, "tags": [ "dial", diff --git a/application/src/main/data/json/system/widget_types/label___value_card.json b/application/src/main/data/json/system/widget_types/label___value_card.json index 352ee4de48..ce1a376a71 100644 --- a/application/src/main/data/json/system/widget_types/label___value_card.json +++ b/application/src/main/data/json/system/widget_types/label___value_card.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-label-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-label-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Label & value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Label & value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "label" diff --git a/application/src/main/data/json/system/widget_types/label_widget.json b/application/src/main/data/json/system/widget_types/label_widget.json index 6a7d0f2e50..46d448ac1a 100644 --- a/application/src/main/data/json/system/widget_types/label_widget.json +++ b/application/src/main/data/json/system/widget_types/label_widget.json @@ -11,11 +11,10 @@ "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 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 self.ctx.imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).subscribe(\n function (transformedUrl) {\n self.ctx.$container.css('background', 'url(\"'+transformedUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\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', transformedUrl);\n }\n );\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": "", + "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 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 self.ctx.imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).subscribe(\n function (transformedUrl) {\n self.ctx.$container.css('background', 'url(\"'+transformedUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\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', transformedUrl);\n }\n );\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 text = self.ctx.utilsService.customTranslation(text);\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", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-label-widget-settings", - "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\":\"tb-image;/api/images/system/here_map_system_widget_map_image.svg\",\"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\":\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"tb-image;/api/images/system/here_map_system_widget_map_image.svg\",\"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}}" }, "tags": [ "tag", diff --git a/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json b/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json index e5588b104b..f81fcdf328 100644 --- a/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json +++ b/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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},\"configMode\":\"basic\"}" + "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;\"}]}],\"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},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/lcd_gauge.json b/application/src/main/data/json/system/widget_types/lcd_gauge.json index fa8faf3255..5334fd86aa 100644 --- a/application/src/main/data/json/system/widget_types/lcd_gauge.json +++ b/application/src/main/data/json/system/widget_types/lcd_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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},\"configMode\":\"basic\"}" + "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;\"}]}],\"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},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_card.json b/application/src/main/data/json/system/widget_types/leaf_wetness_card.json index 65109597fd..2e9efa3027 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_card.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json index 3258de6b32..b7d3b0edec 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/leaf_wetness_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:leaf\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/leaf_wetness_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Humidity card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json index fe3919e6bf..750d2846a7 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json index 271dca2485..1af8f7de7b 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/leaf_wetness_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/leaf_wetness_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/led_indicator.json b/application/src/main/data/json/system/widget_types/led_indicator.json index 7b45863b1c..d9f9d228bf 100644 --- a/application/src/main/data/json/system/widget_types/led_indicator.json +++ b/application/src/main/data/json/system/widget_types/led_indicator.json @@ -12,10 +12,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-led-indicator-widget-settings", - "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}" + "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},\"showLegend\":false,\"actions\":{},\"decimals\":2}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/line_chart.json b/application/src/main/data/json/system/widget_types/line_chart.json index bf67d765f1..69719c5032 100644 --- a/application/src/main/data/json/system/widget_types/line_chart.json +++ b/application/src/main/data/json/system/widget_types/line_chart.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-time-series-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"emptyCircle\",\"pointSize\":4,\"fillAreaSettings\":{\"type\":\"opacity\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"emptyCircle\",\"pointSize\":4,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.3409583261715494,\"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;\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Line chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"emptyCircle\",\"pointSize\":4,\"fillAreaSettings\":{\"type\":\"opacity\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"emptyCircle\",\"pointSize\":4,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.3409583261715494,\"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;\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Line chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "chart", diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index e498922d36..7106cfc4b6 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-map-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\"\\n\",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" + "defaultConfig": "{\"datasources\":[],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\"\\n\",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Map\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/markdown_html_card.json b/application/src/main/data/json/system/widget_types/markdown_html_card.json index b66e3ee88e..e8862ec177 100644 --- a/application/src/main/data/json/system/widget_types/markdown_html_card.json +++ b/application/src/main/data/json/system/widget_types/markdown_html_card.json @@ -13,7 +13,7 @@ "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\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 dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true,\n hideDataSettings: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsDirective": "tb-markdown-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"const baseTemp = 20;\\nconst dailySwing = 10;\\nconst hourlyVariation = Math.sin((time % 24) * Math.PI / 12) * dailySwing;\\nconst randomness = (Math.random() - 0.5) * 2;\\nconst smoothingFactor = 0.8;\\nreturn (prevValue * smoothingFactor) + ((baseTemp + hourlyVariation + randomness) * (1 - smoothingFactor));\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useMarkdownTextFunction\":false,\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Temperature}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"applyDefaultMarkdownStyle\":true,\"markdownCss\":\"\"},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"useDashboardTimewindow\":true,\"displayTimewindow\":true}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"const baseTemp = 20;\\nconst dailySwing = 10;\\nconst hourlyVariation = Math.sin((time % 24) * Math.PI / 12) * dailySwing;\\nconst randomness = (Math.random() - 0.5) * 2;\\nconst smoothingFactor = 0.8;\\nreturn (prevValue * smoothingFactor) + ((baseTemp + hourlyVariation + randomness) * (1 - smoothingFactor));\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useMarkdownTextFunction\":false,\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Temperature}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"applyDefaultMarkdownStyle\":true,\"markdownCss\":\"\"},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json b/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json index bf721b05b7..780ae57e92 100644 --- a/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json +++ b/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json @@ -12,10 +12,8 @@ "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, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "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(() => widgetContext.updateAliases());\",\"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(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"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;\"}]}],\"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},\"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(() => widgetContext.updateAliases());\",\"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(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\"}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/markers_placement___image_map.json b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json index c4bddab86d..37f085c531 100644 --- a/application/src/main/data/json/system/widget_types/markers_placement___image_map.json +++ b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json @@ -12,10 +12,8 @@ "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.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "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\":\"tb-image;/api/images/system/markers_placement_image_map_system_widget_map_image.svg\",\"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(() => widgetContext.updateAliases());\",\"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(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"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\":\"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;\"}]}],\"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\":\"tb-image;/api/images/system/markers_placement_image_map_system_widget_map_image.svg\",\"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},\"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(() => widgetContext.updateAliases());\",\"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(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\"}" }, "tags": [ "building", diff --git a/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json b/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json index da96abb49e..f7e30bd51c 100644 --- a/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json +++ b/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json @@ -12,10 +12,8 @@ "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.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "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(() => widgetContext.updateAliases());\",\"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(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"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\":\"#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;\"}]}],\"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},\"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(() => widgetContext.updateAliases());\",\"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(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\"}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/mini_gauge.json b/application/src/main/data/json/system/widget_types/mini_gauge.json index 6deb0a821c..9214452325 100644 --- a/application/src/main/data/json/system/widget_types/mini_gauge.json +++ b/application/src/main/data/json/system/widget_types/mini_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/navigation_card.json b/application/src/main/data/json/system/widget_types/navigation_card.json index 78c51a0d8c..0a9a5fd0c1 100644 --- a/application/src/main/data/json/system/widget_types/navigation_card.json +++ b/application/src/main/data/json/system/widget_types/navigation_card.json @@ -12,10 +12,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-navigation-card-widget-settings", - "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\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + "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;\"}]}],\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/navigation_cards.json b/application/src/main/data/json/system/widget_types/navigation_cards.json index 4864ef9e53..dddaad3c4d 100644 --- a/application/src/main/data/json/system/widget_types/navigation_cards.json +++ b/application/src/main/data/json/system/widget_types/navigation_cards.json @@ -12,10 +12,9 @@ "templateHtml": "", "templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-navigation-cards-widget-settings", - "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\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + "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;\"}]}],\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/neon_gauge.json b/application/src/main/data/json/system/widget_types/neon_gauge.json index ea4449ab43..abfe988b4a 100644 --- a/application/src/main/data/json/system/widget_types/neon_gauge.json +++ b/application/src/main/data/json/system/widget_types/neon_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json index 95605deae6..c47e0977d2 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Nitrogen dioxide (NO2) card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Nitrogen dioxide (NO2) card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "enviroment", diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json index 7fee7b991a..37671527dd 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/NO2-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Nitrogen dioxide (NO2) card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/NO2-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Nitrogen dioxide (NO2) card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/noise_level_card.json b/application/src/main/data/json/system/widget_types/noise_level_card.json index 4b214b63a0..4a1da677ea 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_card.json +++ b/application/src/main/data/json/system/widget_types/noise_level_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json b/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json index 564d3d665e..44475dc2ce 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/noise_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bar_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/noise_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Noise level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dB\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/openstreet_map.json b/application/src/main/data/json/system/widget_types/openstreet_map.json index b0294e0988..063aa8a9d9 100644 --- a/application/src/main/data/json/system/widget_types/openstreet_map.json +++ b/application/src/main/data/json/system/widget_types/openstreet_map.json @@ -12,10 +12,8 @@ "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.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"OpenStreet Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"OpenStreet Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__card.json b/application/src/main/data/json/system/widget_types/ozone__o3__card.json index 77f12bc288..f8a49252c5 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__card.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone \",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone \",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "enviroment", diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json b/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json index 02145f1e0a..4af0a7b712 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/ozone-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/ozone-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/photo_camera_input.json b/application/src/main/data/json/system/widget_types/photo_camera_input.json index 69327395cc..fc676380ab 100644 --- a/application/src/main/data/json/system/widget_types/photo_camera_input.json +++ b/application/src/main/data/json/system/widget_types/photo_camera_input.json @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-photo-camera-input-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"saveToGallery\":true,\"usePublicGalleryLink\":false,\"imageFormat\":\"image/png\",\"imageQuality\":0.92,\"maxWidth\":640,\"maxHeight\":480},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/pie.json b/application/src/main/data/json/system/widget_types/pie.json index 1a07ce785d..78983b06b4 100644 --- a/application/src/main/data/json/system/widget_types/pie.json +++ b/application/src/main/data/json/system/widget_types/pie.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-pie-chart-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-pie-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Pie\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"pie_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Pie\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"pie_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "pie chart", diff --git a/application/src/main/data/json/system/widget_types/pie___chart_js.json b/application/src/main/data/json/system/widget_types/pie___chart_js.json index 629d0f0f81..920833d9f1 100644 --- a/application/src/main/data/json/system/widget_types/pie___chart_js.json +++ b/application/src/main/data/json/system/widget_types/pie___chart_js.json @@ -16,10 +16,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\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\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-chart-widget-settings", - "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\"}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Chart.js\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/pie___flot.json b/application/src/main/data/json/system/widget_types/pie___flot.json index 9a07685f21..e8bdc2a1d9 100644 --- a/application/src/main/data/json/system/widget_types/pie___flot.json +++ b/application/src/main/data/json/system/widget_types/pie___flot.json @@ -12,11 +12,9 @@ "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.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", "settingsDirective": "tb-flot-pie-widget-settings", "dataKeySettingsDirective": "tb-flot-pie-key-settings", - "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}}" + "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;\"}]}],\"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}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/pm10_card.json b/application/src/main/data/json/system/widget_types/pm10_card.json index 3412808d60..05556452a2 100644 --- a/application/src/main/data/json/system/widget_types/pm10_card.json +++ b/application/src/main/data/json/system/widget_types/pm10_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/pm10_card_with_background.json index fbb8c7251e..0af84a9fc3 100644 --- a/application/src/main/data/json/system/widget_types/pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm10_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pm10_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Indoor PM10 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/pm2_5_card.json b/application/src/main/data/json/system/widget_types/pm2_5_card.json index 0770923327..b9be83e86d 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json index e82a840d27..41ef55b0ac 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bubble_chart\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pm2_5_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"PM2.5 card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/point_chart.json b/application/src/main/data/json/system/widget_types/point_chart.json index 860e8c22e8..372b376439 100644 --- a/application/src/main/data/json/system/widget_types/point_chart.json +++ b/application/src/main/data/json/system/widget_types/point_chart.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-time-series-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":false,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":true,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"circle\",\"pointSize\":8,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":false,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":true,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"circle\",\"pointSize\":8,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.5534217244004682,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Point chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":false,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":true,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"circle\",\"pointSize\":8,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":false,\"step\":false,\"stepType\":\"start\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":true,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"pointShape\":\"circle\",\"pointSize\":8,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}}},\"_hash\":0.5534217244004682,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Point chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "chart", diff --git a/application/src/main/data/json/system/widget_types/polar_area.json b/application/src/main/data/json/system/widget_types/polar_area.json index d3f7fadcf9..9ae33a6829 100644 --- a/application/src/main/data/json/system/widget_types/polar_area.json +++ b/application/src/main/data/json/system/widget_types/polar_area.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-polar-area-chart-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-polar-area-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Polar area\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Polar area\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "polar", diff --git a/application/src/main/data/json/system/widget_types/polar_area_deprecated.json b/application/src/main/data/json/system/widget_types/polar_area_deprecated.json index de6e657b47..bc73865f87 100644 --- a/application/src/main/data/json/system/widget_types/polar_area_deprecated.json +++ b/application/src/main/data/json/system/widget_types/polar_area_deprecated.json @@ -16,10 +16,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\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 floatingPoint;\n if (typeof self.ctx.decimals !== 'undefined' && self.ctx.decimals !== null) {\n floatingPoint = self.ctx.widget.config.decimals;\n } else {\n floatingPoint = 2;\n }\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 scale: {\n ticks: {\n callback: function(tick) {\n \treturn tick.toFixed(floatingPoint);\n }\n }\n }\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 try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-chart-widget-settings", - "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\"}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Polar Area\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_card.json b/application/src/main/data/json/system/widget_types/power_consumption_card.json index e99cc76bb4..1b568491c6 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_card.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_card.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'powerConsumption', label: 'Power consumption', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":10,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Power consumption card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":10,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Power consumption card\",\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "power", diff --git a/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json b/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json index 2f08a16a5b..4f9fb7fbb4 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'powerConsumption', label: 'Power consumption', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":10,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/power_consumption_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Power consumption card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"bolt\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"52px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":10,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/power_consumption_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Power consumption card with background\",\"configMode\":\"basic\",\"units\":\"kW\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "power", diff --git a/application/src/main/data/json/system/widget_types/pressure_card.json b/application/src/main/data/json/system/widget_types/pressure_card.json index 9138ab6391..49c4b6478f 100644 --- a/application/src/main/data/json/system/widget_types/pressure_card.json +++ b/application/src/main/data/json/system/widget_types/pressure_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Pressure card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Pressure card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/pressure_card_with_background.json index d87db84e8a..bc35530963 100644 --- a/application/src/main/data/json/system/widget_types/pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pressure_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Pressure card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"compress\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pressure_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Pressure card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/pressure_progress_bar.json b/application/src/main/data/json/system/widget_types/pressure_progress_bar.json index 93d19780ae..12fd5b917e 100644 --- a/application/src/main/data/json/system/widget_types/pressure_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/pressure_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":870,\"tickMax\":1085,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":870,\"tickMax\":1085,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json index e5189912d2..57fa40f2d5 100644 --- a/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":870,\"tickMax\":1085,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pressure_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.1)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":870,\"tickMax\":1085,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/pressure_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.1)\"},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"hPa\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/progress_bar.json b/application/src/main/data/json/system/widget_types/progress_bar.json index 43b4f97210..0ece481c8c 100644 --- a/application/src/main/data/json/system/widget_types/progress_bar.json +++ b/application/src/main/data/json/system/widget_types/progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Progress bar\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Progress bar\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_card.json b/application/src/main/data/json/system/widget_types/pump_vibration_card.json index 414cfaeb36..688c8b03df 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_card.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'vibration', label: 'Vibration', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration card\",\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "vibration", diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json index c3e90f61a3..05114909c1 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'vibration', label: 'Vibration', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/vibration_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"waves\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/vibration_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration card with background\",\"configMode\":\"basic\",\"units\":\"mm/s\",\"decimals\":1,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "vibration", diff --git a/application/src/main/data/json/system/widget_types/qr_code.json b/application/src/main/data/json/system/widget_types/qr_code.json index 2059719db5..ba7b8f0ddc 100644 --- a/application/src/main/data/json/system/widget_types/qr_code.json +++ b/application/src/main/data/json/system/widget_types/qr_code.json @@ -12,10 +12,8 @@ "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}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.qrCodeWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-qrcode-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7036904308224163,\"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\":{\"qrCodeTextPattern\":\"${entityName}\",\"useQrCodeTextFunction\":false,\"qrCodeTextFunction\":\"return data[0] ? data[0]['entityName'] : '';\"},\"title\":\"QR Code\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7036904308224163,\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"qrCodeTextPattern\":\"${entityName}\",\"useQrCodeTextFunction\":false,\"qrCodeTextFunction\":\"return data[0] ? data[0]['entityName'] : '';\"},\"title\":\"QR Code\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/radar.json b/application/src/main/data/json/system/widget_types/radar.json index be88baa238..24b97698d4 100644 --- a/application/src/main/data/json/system/widget_types/radar.json +++ b/application/src/main/data/json/system/widget_types/radar.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-radar-chart-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-radar-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Radar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"radar\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Radar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"radar\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "radar", diff --git a/application/src/main/data/json/system/widget_types/radar_deprecated.json b/application/src/main/data/json/system/widget_types/radar_deprecated.json index 6a510f54ff..a842d4df40 100644 --- a/application/src/main/data/json/system/widget_types/radar_deprecated.json +++ b/application/src/main/data/json/system/widget_types/radar_deprecated.json @@ -16,10 +16,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \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 floatingPoint;\n if (typeof self.ctx.decimals !== 'undefined' && self.ctx.decimals !== null) {\n floatingPoint = self.ctx.widget.config.decimals;\n } else {\n floatingPoint = 2;\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 scale: {\n ticks: {\n callback: function(tick) {\n \treturn tick.toFixed(floatingPoint);\n }\n }\n }\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\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-chart-widget-settings", - "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\"}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Radar\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/radial_gauge.json b/application/src/main/data/json/system/widget_types/radial_gauge.json index 93922f45d4..139a1cf3d0 100644 --- a/application/src/main/data/json/system/widget_types/radial_gauge.json +++ b/application/src/main/data/json/system/widget_types/radial_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-analogue-radial-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "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,\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" + "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;\"}]}],\"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,\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/radon_level_card.json b/application/src/main/data/json/system/widget_types/radon_level_card.json index 8c8acc6a88..ad789adea4 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_card.json +++ b/application/src/main/data/json/system/widget_types/radon_level_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Radon level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Radon level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json b/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json index 9f5b6f79ad..4560fdb5eb 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/radon_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Radon level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/radon_level_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Radon level card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"Bq/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/rainfall_card.json b/application/src/main/data/json/system/widget_types/rainfall_card.json index 04ce14806b..ca4eb9ad21 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_card.json +++ b/application/src/main/data/json/system/widget_types/rainfall_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall \",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall \",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json b/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json index 81ede92ad1..6a31586919 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall \",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/rainfall_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall \",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:weather-pouring\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/rainfall_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Rainfall card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"mm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/range_chart.json b/application/src/main/data/json/system/widget_types/range_chart.json index e017fe0cec..1e033ecb2b 100644 --- a/application/src/main/data/json/system/widget_types/range_chart.json +++ b/application/src/main/data/json/system/widget_types/range_chart.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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 self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-range-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"dataZoom\":true,\"rangeColors\":[{\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"color\":\"#D81838\"}],\"outOfRangeColor\":\"#ccc\",\"fillArea\":true,\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"tooltipDateInterval\":true},\"title\":\"Range chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"°C\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"dataZoom\":true,\"rangeColors\":[{\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"color\":\"#D81838\"}],\"outOfRangeColor\":\"#ccc\",\"fillArea\":true,\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"tooltipDateInterval\":true},\"title\":\"Range chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"°C\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "range", diff --git a/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json b/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json index ef121fe1fb..c7f104bce1 100644 --- a/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json +++ b/application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json @@ -12,10 +12,9 @@ "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.flex-1 {\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-gpio-panel-widget-settings", - "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": "{\"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;\"}]}]}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/rectangle_tank.json b/application/src/main/data/json/system/widget_types/rectangle_tank.json index 67130346aa..1810aa21df 100644 --- a/application/src/main/data/json/system/widget_types/rectangle_tank.json +++ b/application/src/main/data/json/system/widget_types/rectangle_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Rectangle\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Rectangle\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_card.json b/application/src/main/data/json/system/widget_types/rotational_speed_card.json index 681d53c66d..c4597d3a84 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_card.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_card.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'rotationalSpeed', label: 'Rotational speed', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed card\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed card\",\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json index efaa958688..e51158d89d 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'rotationalSpeed', label: 'Rotational speed', type: 'timeseries' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/rotational_speed_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed card with background\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"square\",\"autoScale\":true,\"showLabel\":true,\"labelFont\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"#000000DE\",\"rangeList\":null,\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"360\",\"iconColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"36px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/rotational_speed_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed card with background\",\"configMode\":\"basic\",\"units\":\"RPM\",\"decimals\":0,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\"}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json b/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json index f42d801090..cf8f0189d5 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json @@ -18,7 +18,7 @@ "dataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":4500,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":8,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":4500,\"color\":\"#F04022\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"RPM\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"360\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":4500,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":8,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":4500,\"color\":\"#F04022\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"RPM\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"360\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json index 7288b70478..96c8889e11 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"RPM\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"RPM\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json index c8d2f0e188..b9e5834f01 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/rotational_speed_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"RPM\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":5000,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/rotational_speed_progress_bar_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"RPM\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/round_switch.json b/application/src/main/data/json/system/widget_types/round_switch.json index 07781c59b2..b93d6fc996 100644 --- a/application/src/main/data/json/system/widget_types/round_switch.json +++ b/application/src/main/data/json/system/widget_types/round_switch.json @@ -12,10 +12,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-round-switch-widget-settings", - "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}" + "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},\"showLegend\":false,\"actions\":{},\"decimals\":2}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/route_map.json b/application/src/main/data/json/system/widget_types/route_map.json index 37346f1157..4e6f70d119 100644 --- a/application/src/main/data/json/system/widget_types/route_map.json +++ b/application/src/main/data/json/system/widget_types/route_map.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First route\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.17490048149347315,\"funcBody\":\"var value = prevValue;\\nif (!value) {\\n value = 45;\\n}\\nif (time % 500 < 500) {\\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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null,\"tagActions\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"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];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"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];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34,\"imageFunction\":\"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.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":4,\"pathStrokeColor\":{\"type\":\"function\",\"color\":\"#307FE5\",\"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', percent).setAlpha(0.65).toRgbString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).setAlpha(0.65).toRgbString();\\n }\\n}\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"tripTimeline\":{\"showTimelineControl\":false}},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":5000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"label\":\"{i18n:widgets.maps.layer.roadmap}\",\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"label\":\"{i18n:widgets.maps.layer.satellite}\",\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"label\":\"{i18n:widgets.maps.layer.hybrid}\",\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\",\"referenceLayer\":\"openstreetmap_hybrid\"}],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First route\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.17490048149347315,\"funcBody\":\"var value = prevValue;\\nif (!value) {\\n value = 45;\\n}\\nif (time % 500 < 500) {\\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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null,\"tagActions\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"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];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"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];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34,\"imageFunction\":\"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.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":4,\"pathStrokeColor\":{\"type\":\"function\",\"color\":\"#307FE5\",\"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', percent).setAlpha(0.65).toRgbString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).setAlpha(0.65).toRgbString();\\n }\\n}\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"markers\":[],\"polygons\":[],\"circles\":[],\"polylines\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"scales\":[],\"dragModeButton\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"mapActionButtons\":[],\"tripTimeline\":{\"showTimelineControl\":false,\"timeStep\":1000,\"speedOptions\":[1,5,10,15,25],\"showTimestamp\":true,\"timestampFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\",\"lastUpdateAgo\":false,\"custom\":false,\"auto\":false},\"snapToRealLocation\":false,\"locationSnapFilter\":\"return true;\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}" }, "resources": [ { @@ -29,7 +29,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "map_marker_image_0.png", - "publicResourceKey": "LPbcriZ2v053mkWb33T5JdK7Agkt1jGg", + "publicResourceKey": "K4kMFb95GFkCPhuXboJgtQH9vykL8riR", "mediaType": "image/png", "data": "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", "public": true @@ -40,7 +40,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "map_marker_image_1.png", - "publicResourceKey": "TwKYnwJfaCIgDbJsetgcj3q7AYK4HUSA", + "publicResourceKey": "FcgQbKh1RSWJYo4JPQV3aCBurt1NB9tu", "mediaType": "image/png", "data": "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==", "public": true @@ -51,7 +51,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "map_marker_image_2.png", - "publicResourceKey": "FazBQsEp1uSeIsT1XL31o2npLAx5s3zJ", + "publicResourceKey": "l9DKBOJ74XPdTzii6RiynM7lP6iI6FyD", "mediaType": "image/png", "data": "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", "public": true diff --git a/application/src/main/data/json/system/widget_types/route_map___google.json b/application/src/main/data/json/system/widget_types/route_map___google.json index 6eda23ef00..98a7fafbe3 100644 --- a/application/src/main/data/json/system/widget_types/route_map___google.json +++ b/application/src/main/data/json/system/widget_types/route_map___google.json @@ -12,10 +12,8 @@ "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.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-route-map-widget-settings", - "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\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d2\",\"useColorFunction\":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', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"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;\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0_(1).png\",\"tb-image;/api/images/system/map_marker_image_1_(1).png\",\"tb-image;/api/images/system/map_marker_image_2_(1).png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Google\",\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d2\",\"useColorFunction\":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', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"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;\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0_(1).png\",\"tb-image;/api/images/system/map_marker_image_1_(1).png\",\"tb-image;/api/images/system/map_marker_image_2_(1).png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Google\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/route_map___openstreet.json b/application/src/main/data/json/system/widget_types/route_map___openstreet.json index 92bb2516a9..cf562b848e 100644 --- a/application/src/main/data/json/system/widget_types/route_map___openstreet.json +++ b/application/src/main/data/json/system/widget_types/route_map___openstreet.json @@ -12,10 +12,8 @@ "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.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-route-map-widget-settings", - "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\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":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', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"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;\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0_(1).png\",\"tb-image;/api/images/system/map_marker_image_1_(1).png\",\"tb-image;/api/images/system/map_marker_image_2_(1).png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - OpenStreet\",\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":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', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"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;\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0_(1).png\",\"tb-image;/api/images/system/map_marker_image_1_(1).png\",\"tb-image;/api/images/system/map_marker_image_2_(1).png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - OpenStreet\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/route_map___tencent.json b/application/src/main/data/json/system/widget_types/route_map___tencent.json index 3de304c8fc..45991884f6 100644 --- a/application/src/main/data/json/system/widget_types/route_map___tencent.json +++ b/application/src/main/data/json/system/widget_types/route_map___tencent.json @@ -12,10 +12,8 @@ "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.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-route-map-widget-settings", - "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\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":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', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"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;\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0_(1).png\",\"tb-image;/api/images/system/map_marker_image_1_(1).png\",\"tb-image;/api/images/system/map_marker_image_2_(1).png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Tencent\",\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":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', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"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;\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0_(1).png\",\"tb-image;/api/images/system/map_marker_image_1_(1).png\",\"tb-image;/api/images/system/map_marker_image_2_(1).png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Tencent\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/rpc_button.json b/application/src/main/data/json/system/widget_types/rpc_button.json index 409731aa12..ed62302240 100644 --- a/application/src/main/data/json/system/widget_types/rpc_button.json +++ b/application/src/main/data/json/system/widget_types/rpc_button.json @@ -12,10 +12,9 @@ "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-mdc-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": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \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 if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\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, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\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\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-send-rpc-widget-settings", - "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\":{}}" + "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},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "command", diff --git a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json index e65e9a1204..67a7fd15b1 100644 --- a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json +++ b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json @@ -11,11 +11,10 @@ "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\n", - "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\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.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\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 var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\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 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\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).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\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", - "settingsSchema": "", + "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\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.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\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 var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\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 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\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).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\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-rpc-terminal-widget-settings", - "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\":{}}" + "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},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "command", diff --git a/application/src/main/data/json/system/widget_types/rpc_remote_shell.json b/application/src/main/data/json/system/widget_types/rpc_remote_shell.json index 9c14e1d155..f25a141494 100644 --- a/application/src/main/data/json/system/widget_types/rpc_remote_shell.json +++ b/application/src/main/data/json/system/widget_types/rpc_remote_shell.json @@ -12,10 +12,9 @@ "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;\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.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\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 (command.trim() === 'exit') {\n if (!simulated) {\n self.ctx.controlApi.sendTwoWayCommand('sendCommand', {\n command: 'exit',\n cwd: cwd\n }, requestTimeout).subscribe();\n }\n this.disable();\n return;\n }\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\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, function (remoteTermInfo) {\n if (remoteTermInfo) {\n terminal.echo('Remote platform info:');\n if (remoteTermInfo.platform) {\n terminal.echo('OS: [[b;#fff;]' + remoteTermInfo.platform + ']');\n } else {\n terminal.echo('OS: [[;#f00;]Unknown]');\n }\n if (remoteTermInfo.release) {\n terminal.echo('OS release: [[b;#fff;]' + remoteTermInfo.release + ']');\n } else {\n terminal.echo('OS release: [[;#f00;]Unknown]');\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(response) {\n let termInfo;\n if (typeof response === 'string') {\n try {\n termInfo = JSON.parse(response);\n } catch (e) {\n terminal.error('Error parsing response: ' + e);\n callback(null);\n return;\n }\n } else {\n termInfo = response;\n }\n if (termInfo && termInfo.cwd) {\n cwd = termInfo.cwd;\n }\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 < commandStatusResponse.data.length; i++) {\n var dataElement = commandStatusResponse.data[i];\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 function () {\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};", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-rpc-shell-widget-settings", - "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 remote shell\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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 remote shell\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "command", diff --git a/application/src/main/data/json/system/widget_types/service_rpc.json b/application/src/main/data/json/system/widget_types/service_rpc.json index eab1bccf0a..3eb8aefa34 100644 --- a/application/src/main/data/json/system/widget_types/service_rpc.json +++ b/application/src/main/data/json/system/widget_types/service_rpc.json @@ -35,4 +35,4 @@ "inter-process communication", "server request" ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/signal_strength.json b/application/src/main/data/json/system/widget_types/signal_strength.json index 4784808baa..4a6fa85a8d 100644 --- a/application/src/main/data/json/system/widget_types/signal_strength.json +++ b/application/src/main/data/json/system/widget_types/signal_strength.json @@ -15,7 +15,7 @@ "settingsDirective": "tb-signal-strength-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-signal-strength-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"rssi\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (!prevValue) {\\n prevValue = Math.random() * -96;\\n}\\nvar value = prevValue + (Math.random() * 60 - 30);\\nif (value > 0) {\\n\\tvalue = 0;\\n} else if (value < -96) {\\n value = -96;\\n}\\nlet rand = Math.random();\\nreturn rand < 0.2 ? (rand < 0.1 ? -101 : '') : value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"wifi\",\"showDate\":false,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"dateColor\":\"rgba(0, 0, 0, 0.38)\",\"activeBarsColor\":{\"color\":\"rgba(92, 223, 144, 1)\",\"type\":\"range\",\"rangeList\":[{\"to\":-85,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":-85,\"to\":-70,\"color\":\"rgba(255, 122, 0, 1)\"},{\"from\":-70,\"to\":-55,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":-55,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"inactiveBarsColor\":\"rgba(224, 224, 224, 1)\",\"noSignalRssiValue\":-100,\"showTooltip\":true,\"showTooltipValue\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0,0,0,0.76)\",\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0,0,0,0.76)\",\"tooltipBackgroundColor\":\"rgba(255,255,255,0.72)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Signal strength\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dBm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"signal_cellular_alt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"rssi\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (!prevValue) {\\n prevValue = Math.random() * -96;\\n}\\nvar value = prevValue + (Math.random() * 60 - 30);\\nif (value > 0) {\\n\\tvalue = 0;\\n} else if (value < -96) {\\n value = -96;\\n}\\nlet rand = Math.random();\\nreturn rand < 0.2 ? (rand < 0.1 ? -101 : '') : value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"wifi\",\"showDate\":false,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"dateColor\":\"rgba(0, 0, 0, 0.38)\",\"activeBarsColor\":{\"color\":\"rgba(92, 223, 144, 1)\",\"type\":\"range\",\"rangeList\":[{\"to\":-85,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":-85,\"to\":-70,\"color\":\"rgba(255, 122, 0, 1)\"},{\"from\":-70,\"to\":-55,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":-55,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"inactiveBarsColor\":\"rgba(224, 224, 224, 1)\",\"noSignalRssiValue\":-100,\"showTooltip\":true,\"showTooltipValue\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0,0,0,0.76)\",\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0,0,0,0.76)\",\"tooltipBackgroundColor\":\"rgba(255,255,255,0.72)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Signal strength\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dBm\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"signal_cellular_alt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "wifi", diff --git a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json index 812aa8694b..130fea3cc9 100644 --- a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Air Quality Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-windy\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"AQI\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":100,\"color\":\"#FFA600\"},{\"from\":100,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":200,\"color\":\"#D81838\"},{\"from\":200,\"to\":300,\"color\":\"#8D268C\"},{\"from\":300,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Air Quality Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-windy\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"AQI\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json index 1c22c060f2..e20970e9a8 100644 --- a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_air_quality_index_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Air Quality Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-windy\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"AQI\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 320) {\\n\\tvalue = 320;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":100,\"color\":\"#F89E0D\"},{\"from\":100,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":200,\"color\":\"#DE2343\"},{\"from\":200,\"to\":300,\"color\":\"#7B287A\"},{\"from\":300,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_air_quality_index_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Air Quality Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-windy\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"AQI\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json index d494338fee..8174b77794 100644 --- a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule-co\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":25,\"color\":\"#FFA600\"},{\"from\":25,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule-co\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json index 66364f8d06..25a13d1989 100644 --- a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/CO-simple-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule-co\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Carbon monoxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":25,\"color\":\"#F89E0D\"},{\"from\":25,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/CO-simple-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Carbon monoxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule-co\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_card.json b/application/src/main/data/json/system/widget_types/simple_card.json index bcaebb7f73..9f3d83f41e 100644 --- a/application/src/main/data/json/system/widget_types/simple_card.json +++ b/application/src/main/data/json/system/widget_types/simple_card.json @@ -12,12 +12,10 @@ "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 singleEntity: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n};\n\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-simple-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-simple-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"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\":{},\"configMode\":\"basic\",\"displayTimewindow\":true}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"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,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json b/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json index 9e4be18356..4a50cd60a7 100644 --- a/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3FA71A\"},{\"from\":600,\"to\":1000,\"color\":\"#80C32C\"},{\"from\":1000,\"to\":1500,\"color\":\"#F36900\"},{\"from\":1500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json index d6ef0088b4..e3ff71d64c 100644 --- a/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_co2_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"CO2 level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 160 - 80;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 400) {\\n\\tvalue = 400;\\n} else if (value > 1600) {\\n\\tvalue = 1600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":600,\"color\":\"#3B911C\"},{\"from\":600,\"to\":1000,\"color\":\"#7CC322\"},{\"from\":1000,\"to\":1500,\"color\":\"#F77410\"},{\"from\":1500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_co2_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"CO2 level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"co2\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json index d957a82a58..80079e409f 100644 --- a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"trending_up\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#FFA600\"},{\"from\":60,\"to\":80,\"color\":\"#3FA71A\"},{\"from\":80,\"to\":null,\"color\":\"#305AD7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"trending_up\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"%\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json index 9fe02a7725..f1b9f961fe 100644 --- a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_efficiency_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"trending_up\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Efficiency\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 30;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":60,\"color\":\"#F89E0D\"},{\"from\":60,\"to\":80,\"color\":\"#3B911C\"},{\"from\":80,\"to\":null,\"color\":\"#2B54CE\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_efficiency_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Efficiency\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"trending_up\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"%\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "productivity", diff --git a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json index 55a4da3aba..a808a4c930 100644 --- a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#234CC7\"},{\"from\":1,\"to\":3,\"color\":\"#F36900\"},{\"from\":3,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json index dab9a38433..07976f61a4 100644 --- a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_flooding_level_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flooding level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 2 - 1;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 5) {\\n\\tvalue = 5;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"#224AC2\"},{\"from\":1,\"to\":3,\"color\":\"#F77410\"},{\"from\":3,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_flooding_level_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flooding level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"flood\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json index 05fd114507..8061c87659 100644 --- a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:hydro-power\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"m³/hr\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#305AD7\"},{\"from\":10,\"to\":30,\"color\":\"#3FA71A\"},{\"from\":30,\"to\":50,\"color\":\"#F36900\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:hydro-power\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"m³/hr\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json index bea92879b0..436d45b9c4 100644 --- a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_flow_rate_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:hydro-power\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"m³/hr\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Flow rate\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#2B54CE\"},{\"from\":10,\"to\":30,\"color\":\"#3B911C\"},{\"from\":30,\"to\":50,\"color\":\"#F77410\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_flow_rate_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Flow rate\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:hydro-power\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"m³/hr\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "liquid", diff --git a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json index 57417c5986..647c5fd971 100644 --- a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"bar\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#305AD7\"},{\"from\":5,\"to\":10,\"color\":\"#3FA71A\"},{\"from\":10,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"bar\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json index e7b8f22933..b6b8fdd8f6 100644 --- a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_pressure_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"bar\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 15 - 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":5,\"color\":\"#2B54CE\"},{\"from\":5,\"to\":10,\"color\":\"#3B911C\"},{\"from\":10,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_pressure_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"bar\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "fluid pressure", diff --git a/application/src/main/data/json/system/widget_types/simple_gauge.json b/application/src/main/data/json/system/widget_types/simple_gauge.json index 1358e21aae..76fda7bc37 100644 --- a/application/src/main/data/json/system/widget_types/simple_gauge.json +++ b/application/src/main/data/json/system/widget_types/simple_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json index e57b06fdd1..28a8c2e5d9 100644 --- a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ground temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ground temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json index 81f971a8ba..dbe53fb714 100644 --- a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_ground_temperature_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ground temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ground temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_ground_temperature_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ground temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json index e45f5f63c5..9532878d13 100644 --- a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#FFA600\"},{\"from\":40,\"to\":60,\"color\":\"#5B7EE6\"},{\"from\":60,\"to\":80,\"color\":\"#305AD7\"},{\"from\":80,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json index 484c9c1a08..21853dafd1 100644 --- a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_humidity_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F89E0D\"},{\"from\":40,\"to\":60,\"color\":\"#5579E5\"},{\"from\":60,\"to\":80,\"color\":\"#2B54CE\"},{\"from\":80,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_humidity_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Humidity\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json index 43c8a79361..0c525ea708 100644 --- a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":20,\"color\":\"#F36900\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json index 373fac46dc..a8f7092e15 100644 --- a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_illuminance_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Illuminance\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1,\"color\":\"rgba(0, 0, 0, 0.76)\"},{\"from\":1,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":20,\"color\":\"#F77410\"},{\"from\":20,\"to\":50,\"color\":\"#F04022\"},{\"from\":50,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_illuminance_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Illuminance\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:lightbulb-on\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"lx\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json index 81764c775f..58f093daff 100644 --- a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"IAI\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:flower-pollen\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3FA71A\"},{\"from\":2,\"to\":6,\"color\":\"#80C32C\"},{\"from\":6,\"to\":9,\"color\":\"#F36900\"},{\"from\":9,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"IAI\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:flower-pollen\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json index 283ff4a8e0..a729bdc5b0 100644 --- a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple-IAI-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"IAI\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:flower-pollen\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Air Quality Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 12) {\\n\\tvalue = 12;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#3B911C\"},{\"from\":2,\"to\":6,\"color\":\"#7CC322\"},{\"from\":6,\"to\":9,\"color\":\"#F77410\"},{\"from\":9,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple-IAI-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"IAI\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:flower-pollen\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json index 2205278c9f..dd8a5c4913 100644 --- a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json index d71fc4a5bb..81e32813f3 100644 --- a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_leaf_wetness_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_leaf_wetness_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Leaf wetness\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:leaf\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_neon_gauge.json b/application/src/main/data/json/system/widget_types/simple_neon_gauge.json index 9526b0512b..562cad5f60 100644 --- a/application/src/main/data/json/system/widget_types/simple_neon_gauge.json +++ b/application/src/main/data/json/system/widget_types/simple_neon_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json index 0a1c1d8657..2caf26bcf0 100644 --- a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Nitrogen dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3FA71A\"},{\"from\":40,\"to\":90,\"color\":\"#80C32C\"},{\"from\":90,\"to\":120,\"color\":\"#FFA600\"},{\"from\":120,\"to\":230,\"color\":\"#F36900\"},{\"from\":230,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Nitrogen dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json index 892238dc9b..72f4425456 100644 --- a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple-NO2-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Nitrogen dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Nitrogen dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":40,\"color\":\"#3B911C\"},{\"from\":40,\"to\":90,\"color\":\"#7CC322\"},{\"from\":90,\"to\":120,\"color\":\"#F89E0D\"},{\"from\":120,\"to\":230,\"color\":\"#F77410\"},{\"from\":230,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple-NO2-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Nitrogen dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json index d33eed776f..6fd4708209 100644 --- a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Noise level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"dB\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#80C32C\"},{\"from\":50,\"to\":70,\"color\":\"#FFA600\"},{\"from\":70,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Noise level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"dB\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json index 225d30b14c..41a360c8a6 100644 --- a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_noise_level_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Noise level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"dB\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Noise level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value;\\nif (!prevValue) {\\n value = Math.random() * 120;\\n} else {\\n value = prevValue + Math.random() * 40 - 20;\\n}\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":50,\"color\":\"#7CC322\"},{\"from\":50,\"to\":70,\"color\":\"#F89E0D\"},{\"from\":70,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_noise_level_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Noise level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bar_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"dB\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json index b2c9eb5e97..acb65d35f6 100644 --- a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3FA71A\"},{\"from\":50,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":130,\"color\":\"#FFA600\"},{\"from\":130,\"to\":240,\"color\":\"#F36900\"},{\"from\":240,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json index 88995bb4f5..129a0e2056 100644 --- a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple-ozone-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Ozone\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 250) {\\n\\tvalue = 250;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":50,\"color\":\"#3B911C\"},{\"from\":50,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":130,\"color\":\"#F89E0D\"},{\"from\":130,\"to\":240,\"color\":\"#F77410\"},{\"from\":240,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple-ozone-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Ozone\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json index 6e7d5c1369..a0e450dd2f 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#80C32C\"},{\"from\":20,\"to\":50,\"color\":\"#FFA600\"},{\"from\":50,\"to\":150,\"color\":\"#F36900\"},{\"from\":150,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json index a0b3d74e54..d10f51d454 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_pm10_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM10\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#7CC322\"},{\"from\":20,\"to\":50,\"color\":\"#F89E0D\"},{\"from\":50,\"to\":150,\"color\":\"#F77410\"},{\"from\":150,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_pm10_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM10\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json index 25502227f3..5a9110d70d 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#80C32C\"},{\"from\":10,\"to\":35,\"color\":\"#FFA600\"},{\"from\":35,\"to\":75,\"color\":\"#F36900\"},{\"from\":75,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json index ccdf3ba0a1..b8f90f9b44 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_pm2_5_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"PM2.5\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 120 - 60;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 500) {\\n\\tvalue = 500;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":10,\"color\":\"#7CC322\"},{\"from\":10,\"to\":35,\"color\":\"#F89E0D\"},{\"from\":35,\"to\":75,\"color\":\"#F77410\"},{\"from\":75,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_pm2_5_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"PM2.5\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bubble_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json index befea52bb3..766a1e1956 100644 --- a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Power consumption\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bolt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"kW\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3FA71A\"},{\"from\":5,\"to\":15,\"color\":\"#F36900\"},{\"from\":15,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Power consumption\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bolt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"kW\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "power", diff --git a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json index f0bb8ba9e9..58c5fc8beb 100644 --- a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_power_consumption_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":4}}},\"title\":\"Power consumption\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bolt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"kW\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Power consumption\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":5,\"color\":\"#3B911C\"},{\"from\":5,\"to\":15,\"color\":\"#F77410\"},{\"from\":15,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_power_consumption_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":4}}},\"title\":\"Power consumption\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"bolt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"kW\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "power", diff --git a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json index dcc4b11510..62178bbf2a 100644 --- a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"hPa\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":1020,\"color\":\"#80C32C\"},{\"from\":1020,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"hPa\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json index 0c177e6364..fde44a0366 100644 --- a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_pressure_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"hPa\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 80 - 40;\\nif (value < 980) {\\n\\tvalue = 980;\\n} else if (value > 1040) {\\n\\tvalue = 1040;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":1020,\"color\":\"#7CC322\"},{\"from\":1020,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_pressure_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Pressure\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"compress\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"hPa\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json index a84a49e2de..92673ec6d2 100644 --- a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"waves\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"mm/s\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Pressure\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3FA71A\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#FFA600\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F36900\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"waves\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"mm/s\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "vibration", diff --git a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json index 5cb064c807..b077b31919 100644 --- a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_vibration_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"waves\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"mm/s\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 3.3 - 1.7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 10) {\\n\\tvalue = 10;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2.8,\"color\":\"#3B911C\"},{\"from\":2.8,\"to\":4.5,\"color\":\"#F89E0D\"},{\"from\":4.5,\"to\":7.1,\"color\":\"#F77410\"},{\"from\":7.1,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_vibration_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"waves\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"mm/s\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "vibration", diff --git a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json index 27faab79ac..72b811f2d1 100644 --- a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Radon level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"Bq/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#80C32C\"},{\"from\":100,\"to\":200,\"color\":\"#FFA600\"},{\"from\":200,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Radon level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"Bq/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json index 84e90026ad..0ee5a825ae 100644 --- a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_radon_level_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Radon level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"Bq/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Radon level\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 75 - 37.5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 300) {\\n\\tvalue = 300;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#7CC322\"},{\"from\":100,\"to\":200,\"color\":\"#F89E0D\"},{\"from\":200,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_radon_level_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Radon level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"Bq/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json index ddbf9c66ee..8cb70f6b3d 100644 --- a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rainfall\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-pouring\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#7191EF\"},{\"from\":0,\"to\":2.5,\"color\":\"#4B70DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#305AD7\"},{\"from\":7.6,\"to\":null,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rainfall\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-pouring\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json index c5f82d49b9..92927e78de 100644 --- a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_rainfall_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rainfall\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-pouring\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rainfall\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4 - 2;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 8) {\\n\\tvalue = 8;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#6083EC\"},{\"from\":0,\"to\":2.5,\"color\":\"#4369DD\"},{\"from\":2.5,\"to\":7.6,\"color\":\"#2B54CE\"},{\"from\":7.6,\"to\":null,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_rainfall_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rainfall\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:weather-pouring\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"mm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json index 9e39e6ad0a..5050fc8184 100644 --- a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"360\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"RPM\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#305AD7\"},{\"from\":500,\"to\":1500,\"color\":\"#3FA71A\"},{\"from\":1500,\"to\":3000,\"color\":\"#FFA600\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"360\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"RPM\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json index 2f338515c5..c1433bb2b4 100644 --- a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_rotational_speed_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"360\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"RPM\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Rotational speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 4000 - 2000;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 4000) {\\n\\tvalue = 4000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":500,\"color\":\"#2B54CE\"},{\"from\":500,\"to\":1500,\"color\":\"#3B911C\"},{\"from\":1500,\"to\":3000,\"color\":\"#F89E0D\"},{\"from\":3000,\"to\":null,\"color\":\"#F04022\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/simple_rotational_speed_chart_card_background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Rotational speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"360\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":\"0px\",\"units\":\"RPM\",\"margin\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "angular speed", diff --git a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json index e23671c563..f4509d1669 100644 --- a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Snow depth\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"ac_unit\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"cm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Snow depth\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"ac_unit\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"cm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json index 48010dbcc9..706f03e477 100644 --- a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_snow_depth_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Snow depth\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"ac_unit\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"cm\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_snow_depth_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Snow depth\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"ac_unit\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"cm\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json index 3f9e4450aa..8627fc8430 100644 --- a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json index 916c7d7d37..096e88941f 100644 --- a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_soil_moisture_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_soil_moisture_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"%\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json index e3ebfac43a..c009adb989 100644 --- a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Solar Radiation\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"W/m²\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Solar Radiation\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"W/m²\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json index f3fc53349f..39b76d6c41 100644 --- a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_solar_radiation_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Solar Radiation\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"W/m²\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_solar_radiation_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Solar Radiation\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:radioactive\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"W/m²\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json index 0a91102cdc..193e1f9042 100644 --- a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json index 2a016e3072..f13af9970c 100644 --- a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3B911C\"},{\"from\":100,\"to\":200,\"color\":\"#7CC322\"},{\"from\":200,\"to\":350,\"color\":\"#F89E0D\"},{\"from\":350,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/SO2-simple-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3B911C\"},{\"from\":100,\"to\":200,\"color\":\"#7CC322\"},{\"from\":200,\"to\":350,\"color\":\"#F89E0D\"},{\"from\":350,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/SO2-simple-value-and-chart-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"public\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"µg/m³\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json index bac4ec201e..5faeaace2a 100644 --- a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json index 50145be107..c0d44315ce 100644 --- a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_temperature_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_temperature_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json index d10f55833b..25792079bc 100644 --- a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"UV Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"light_mode\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"UV Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"light_mode\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json index a1ba49e7bc..a7173133ab 100644 --- a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_uv_index_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"UV Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"light_mode\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_uv_index_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"UV Index\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"light_mode\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":null,\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json b/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json index 0fea1b6682..342c15c917 100644 --- a/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Simple Value and chart card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Simple Value and chart card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json index a045af5ef6..e1be525d80 100644 --- a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"vibration\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s²\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"vibration\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s²\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json index 9e39eed359..367dece05a 100644 --- a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_vibration_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"vibration\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s²\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_vibration_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Vibration\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"vibration\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s²\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json index 7f098c8d96..d7bc882f73 100644 --- a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Visibility\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"visibility\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"km\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Visibility\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"visibility\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"km\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json index 6deff18090..c61471fa6d 100644 --- a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_visibility_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Visibility\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"visibility\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"km\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_visibility_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Visibility\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"visibility\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"km\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json index d176526dcb..3d60d3479a 100644 --- a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"VOCs\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppb\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"VOCs\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppb\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json index 1eba6b3fd0..7adedd8c81 100644 --- a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_volatile_organic_compounds_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"VOCs\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppb\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_volatile_organic_compounds_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"VOCs\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:molecule\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"ppb\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json index 6660f67d78..ea60ba3edf 100644 --- a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json index fadebc2925..d08cbc8a49 100644 --- a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-value-chart-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_wind_speed_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/simple_wind_speed_chart_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":1,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"m/s\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/slide_toggle_control.json b/application/src/main/data/json/system/widget_types/slide_toggle_control.json index 6da8532586..74cbadc52f 100644 --- a/application/src/main/data/json/system/widget_types/slide_toggle_control.json +++ b/application/src/main/data/json/system/widget_types/slide_toggle_control.json @@ -12,10 +12,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-slide-toggle-widget-settings", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Slide toggle control\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\",\"requestPersistent\":false,\"labelPosition\":\"after\",\"sliderColor\":\"accent\"},\"title\":\"Slide Toggle Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2,\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Slide toggle control\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\",\"requestPersistent\":false,\"labelPosition\":\"after\",\"sliderColor\":\"accent\"},\"title\":\"Slide Toggle Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{},\"decimals\":2,\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\"}" }, "tags": [ "command", diff --git a/application/src/main/data/json/system/widget_types/snow_depth_card.json b/application/src/main/data/json/system/widget_types/snow_depth_card.json index 1d8f56a2fb..031db09beb 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_card.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#7191EF\"},{\"from\":1,\"to\":10,\"color\":\"#4B70DD\"},{\"from\":10,\"to\":30,\"color\":\"#305AD7\"},{\"from\":30,\"to\":60,\"color\":\"#234CC7\"},{\"from\":60,\"to\":90,\"color\":\"#F36900\"},{\"from\":90,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json b/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json index c9f1b12488..9c294fbe70 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/snow_depth_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Snow depth\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 120) {\\n\\tvalue = 120;\\n}\\nreturn value;\\n\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"ac_unit\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#6083EC\"},{\"from\":1,\"to\":10,\"color\":\"#4369DD\"},{\"from\":10,\"to\":30,\"color\":\"#2B54CE\"},{\"from\":30,\"to\":60,\"color\":\"#224AC2\"},{\"from\":60,\"to\":90,\"color\":\"#F77410\"},{\"from\":90,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/snow_depth_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Snow depth card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"cm\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_card.json b/application/src/main/data/json/system/widget_types/soil_moisture_card.json index 03df119344..65e0e6311a 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_card.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json index a66fbc1532..38864fb686 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/soil_moisture_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:water-percent\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/soil_moisture_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Soil moisture card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json index efd88de86c..0acba2ec65 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#D81838\"},{\"from\":20,\"to\":40,\"color\":\"#F36900\"},{\"from\":40,\"to\":60,\"color\":\"#4B70DD\"},{\"from\":60,\"to\":100,\"color\":\"#234CC7\"}]},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json index da1ec3617a..d3e63ede6a 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-progress-bar-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-progress-bar-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}]},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/soil_moisture_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil Moisture\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}]},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/soil_moisture_progress_bar_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"barColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#DE2343\"},{\"from\":20,\"to\":40,\"color\":\"#F77410\"},{\"from\":40,\"to\":60,\"color\":\"#4369DD\"},{\"from\":60,\"to\":100,\"color\":\"#224AC2\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"barBackground\":\"rgba(0, 0, 0, 0.04)\"},\"title\":\"Soil Moisture\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "progress", diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_card.json b/application/src/main/data/json/system/widget_types/solar_radiation_card.json index 286668be56..43e5cfc398 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_card.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5B7EE6\"},{\"from\":0,\"to\":250,\"color\":\"#80C32C\"},{\"from\":250,\"to\":500,\"color\":\"#FFA600\"},{\"from\":500,\"to\":1000,\"color\":\"#F36900\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json b/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json index 7264f59fe2..c49effbfec 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/solar_radiation_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar Radiation\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 1100;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:radioactive\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0,\"color\":\"#5579E5\"},{\"from\":0,\"to\":250,\"color\":\"#7CC322\"},{\"from\":250,\"to\":500,\"color\":\"#F89E0D\"},{\"from\":500,\"to\":1000,\"color\":\"#F77410\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/solar_radiation_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Solar radiation card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"W/m²\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/speed_gauge.json b/application/src/main/data/json/system/widget_types/speed_gauge.json index e0e3bc4732..820ca79d50 100644 --- a/application/src/main/data/json/system/widget_types/speed_gauge.json +++ b/application/src/main/data/json/system/widget_types/speed_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-analogue-radial-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "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\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":7,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":180,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":9,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":null,\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"valueColor\":\"#444\",\"valueColorShadow\":\"rgba(0, 0, 0, 0.49)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"showBorder\":false,\"colorPlate\":\"#fff\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"mph\"}" + "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;\"}]}],\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":7,\"defaultColor\":\"#e65100\",\"minValue\":0,\"maxValue\":180,\"majorTicksCount\":9,\"colorMajorTicks\":\"#444\",\"minorTicks\":9,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":null,\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"valueColor\":\"#444\",\"valueColorShadow\":\"rgba(0, 0, 0, 0.49)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"showBorder\":false,\"colorPlate\":\"#fff\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"mph\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/state_chart.json b/application/src/main/data/json/system/widget_types/state_chart.json index d2a3031318..2b30a6b22f 100644 --- a/application/src/main/data/json/system/widget_types/state_chart.json +++ b/application/src/main/data/json/system/widget_types/state_chart.json @@ -12,15 +12,15 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n chartType: 'state',\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('state'),\n defaultDataKeysFunction: function() {\n return [{ name: 'state', label: 'State', type: 'timeseries', units: '', decimals: 0 }];\n }\n };\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}", - "latestDataKeySettingsSchema": "{}", + "settingsForm": [], + "dataKeySettingsForm": [], + "latestDataKeySettingsForm": [], "settingsDirective": "tb-time-series-chart-widget-settings", "dataKeySettingsDirective": "tb-time-series-chart-key-settings", "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-time-series-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"opacity\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":\"\"},\"_hash\":0.676226248393859,\"funcBody\":\"return Math.random() > 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#FFC107\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":null},\"_hash\":0.1106990458957191,\"funcBody\":\"return Math.random() <= 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"NONE\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":\"\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0,\"interval\":null,\"splitNumber\":null,\"min\":null,\"max\":null,\"ticksGenerator\":\"\"}},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"showLegend\":true,\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipValueFormatter\":\"\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{\"millisecond\":\"MMM dd yyyy HH:mm:ss\"}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"states\":[{\"label\":\"Off\",\"value\":0,\"sourceType\":\"constant\",\"sourceValue\":false},{\"label\":\"On\",\"value\":1,\"sourceType\":\"constant\",\"sourceValue\":true}]},\"title\":\"State chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"opacity\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":\"\"},\"_hash\":0.676226248393859,\"funcBody\":\"return Math.random() > 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#FFC107\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":null},\"_hash\":0.1106990458957191,\"funcBody\":\"return Math.random() <= 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":\"\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0,\"interval\":null,\"splitNumber\":null,\"min\":null,\"max\":null,\"ticksGenerator\":\"\"}},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"showLegend\":true,\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipValueFormatter\":\"\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{\"millisecond\":\"MMM dd yyyy HH:mm:ss\"}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"states\":[{\"label\":\"Off\",\"value\":0,\"sourceType\":\"constant\",\"sourceValue\":false},{\"label\":\"On\",\"value\":1,\"sourceType\":\"constant\",\"sourceValue\":true}]},\"title\":\"State chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "chart", diff --git a/application/src/main/data/json/system/widget_types/state_chart_deprecated.json b/application/src/main/data/json/system/widget_types/state_chart_deprecated.json index e5bfdda095..9c33341ce4 100644 --- a/application/src/main/data/json/system/widget_types/state_chart_deprecated.json +++ b/application/src/main/data/json/system/widget_types/state_chart_deprecated.json @@ -12,14 +12,12 @@ "templateHtml": "\n", "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}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-line-widget-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-flot-basic-config", - "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\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json index 25b950df3e..166a0842c5 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3FA71A\"},{\"from\":100,\"to\":200,\"color\":\"#80C32C\"},{\"from\":200,\"to\":350,\"color\":\"#FFA600\"},{\"from\":350,\"to\":500,\"color\":\"#F36900\"},{\"from\":500,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "enviroment", diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json index a097776152..7a0c911bf4 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3B911C\"},{\"from\":100,\"to\":200,\"color\":\"#7CC322\"},{\"from\":200,\"to\":350,\"color\":\"#F89E0D\"},{\"from\":350,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3B911C\"},{\"from\":100,\"to\":200,\"color\":\"#7CC322\"},{\"from\":200,\"to\":350,\"color\":\"#F89E0D\"},{\"from\":350,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/SO2-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sulfur dioxide\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 600) {\\n\\tvalue = 600;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"public\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3B911C\"},{\"from\":100,\"to\":200,\"color\":\"#7CC322\"},{\"from\":200,\"to\":350,\"color\":\"#F89E0D\"},{\"from\":350,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":100,\"color\":\"#3B911C\"},{\"from\":100,\"to\":200,\"color\":\"#7CC322\"},{\"from\":200,\"to\":350,\"color\":\"#F89E0D\"},{\"from\":350,\"to\":500,\"color\":\"#F77410\"},{\"from\":500,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageUrl\":\"tb-image;/api/images/system/SO2-value-card-background.png\",\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Sulfur dioxide\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"µg/m³\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "enviroment", diff --git a/application/src/main/data/json/system/widget_types/switch_control.json b/application/src/main/data/json/system/widget_types/switch_control.json index eab256525e..8747cf0131 100644 --- a/application/src/main/data/json/system/widget_types/switch_control.json +++ b/application/src/main/data/json/system/widget_types/switch_control.json @@ -12,10 +12,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-switch-control-widget-settings", - "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}" + "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},\"showLegend\":false,\"actions\":{},\"decimals\":2}" }, "tags": [ "command", diff --git a/application/src/main/data/json/system/widget_types/temperature_card.json b/application/src/main/data/json/system/widget_types/temperature_card.json index 1308512c13..93219788e6 100644 --- a/application/src/main/data/json/system/widget_types/temperature_card.json +++ b/application/src/main/data/json/system/widget_types/temperature_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/temperature_card_with_background.json index f711b715d5..fee17a4a87 100644 --- a/application/src/main/data/json/system/widget_types/temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/temperature_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/temperature_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Temperature card with background\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/temperature_gauge.json b/application/src/main/data/json/system/widget_types/temperature_gauge.json index 0db4b047f0..a89477ccbf 100644 --- a/application/src/main/data/json/system/widget_types/temperature_gauge.json +++ b/application/src/main/data/json/system/widget_types/temperature_gauge.json @@ -18,7 +18,7 @@ "dataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "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() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":-40,\"maxValue\":40,\"majorTicksCount\":8,\"colorMajorTicks\":\"#444\",\"minorTicks\":8,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":-40,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" + "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() * 20 - 10;\\nif (value < 15) {\\n\\tvalue = 15;\\n} else if (value > 30) {\\n\\tvalue = 30;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":8,\"defaultColor\":\"#e65100\",\"minValue\":-40,\"maxValue\":40,\"majorTicksCount\":8,\"colorMajorTicks\":\"#444\",\"minorTicks\":8,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"numbersColor\":\"#616161\",\"showUnitTitle\":false,\"unitTitle\":\"\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"titleColor\":\"#888\",\"unitsFont\":{\"size\":26,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"size\":27,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"color\":\"rgba(0, 0, 0, 0.54)\",\"shadowColor\":\"#FFFFFF01\"},\"valueColor\":\"rgba(0, 0, 0, 0.54)\",\"valueColorShadow\":\"#FFFFFF01\",\"colorValueBoxRect\":\"#88888800\",\"colorValueBoxRectEnd\":\"#66666600\",\"colorValueBoxBackground\":\"rgba(243, 243, 243, 0.54)\",\"colorValueBoxShadow\":\"rgba(0, 0, 0, 0)\",\"showBorder\":false,\"colorPlate\":\"#FFFFFF\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":15,\"highlights\":[{\"from\":-40,\"to\":-20,\"color\":\"#224AC2\"},{\"from\":-20,\"to\":0,\"color\":\"#2B54CE\"},{\"from\":0,\"to\":10,\"color\":\"#6083EC\"},{\"from\":10,\"to\":20,\"color\":\"#F89E0D\"},{\"from\":20,\"to\":30,\"color\":\"#F77410\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"}],\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\"},\"title\":\"Temperature\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleIcon\":\"device_thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"actions\":{},\"margin\":\"0px\",\"borderRadius\":\"0px\"}" }, "tags": [ "temperature", diff --git a/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json b/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json index a508264166..77114b91cb 100644 --- a/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json +++ b/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-analogue-radial-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "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\":{\"startAngle\":67.5,\"ticksAngle\":225,\"needleCircleSize\":7,\"defaultColor\":\"#e65100\",\"minValue\":-60,\"maxValue\":60,\"majorTicksCount\":12,\"colorMajorTicks\":\"#444\",\"minorTicks\":12,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"numbersColor\":\"#263238\",\"showUnitTitle\":true,\"unitTitle\":\"Temperature\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleColor\":\"#263238\",\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"valueColor\":\"#444\",\"valueColorShadow\":\"rgba(0, 0, 0, 0.49)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"showBorder\":true,\"colorPlate\":\"#cfd8dc\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"highlightsWidth\":15,\"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\"}],\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\"},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\"}" + "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;\"}]}],\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":67.5,\"ticksAngle\":225,\"needleCircleSize\":7,\"defaultColor\":\"#e65100\",\"minValue\":-60,\"maxValue\":60,\"majorTicksCount\":12,\"colorMajorTicks\":\"#444\",\"minorTicks\":12,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"numbersColor\":\"#263238\",\"showUnitTitle\":true,\"unitTitle\":\"Temperature\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleColor\":\"#263238\",\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"unitsColor\":\"#616161\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"valueColor\":\"#444\",\"valueColorShadow\":\"rgba(0, 0, 0, 0.49)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"showBorder\":true,\"colorPlate\":\"#cfd8dc\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"highlightsWidth\":15,\"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\"}],\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\"},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/tencent_map.json b/application/src/main/data/json/system/widget_types/tencent_map.json index ec41717ca4..1c30d6df27 100644 --- a/application/src/main/data/json/system/widget_types/tencent_map.json +++ b/application/src/main/data/json/system/widget_types/tencent_map.json @@ -12,10 +12,8 @@ "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.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings-legacy", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second Point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Tencent Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"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\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second Point\",\"entityAliasId\":null,\"filterId\":null,\"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 \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"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', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\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\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Tencent Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "tags": [ "mapping", diff --git a/application/src/main/data/json/system/widget_types/thermometer_scale.json b/application/src/main/data/json/system/widget_types/thermometer_scale.json index f1270c3e2b..d75e664ec5 100644 --- a/application/src/main/data/json/system/widget_types/thermometer_scale.json +++ b/application/src/main/data/json/system/widget_types/thermometer_scale.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-analogue-linear-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-thermometer-scale-gauge-basic-config", - "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\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":10,\"defaultColor\":\"#e64a19\",\"minValue\":-60,\"maxValue\":100,\"majorTicksCount\":8,\"colorMajorTicks\":\"#444\",\"minorTicks\":8,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"numbersColor\":\"#263238\",\"showUnitTitle\":true,\"unitTitle\":\"Temperature\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"titleColor\":\"#78909c\",\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"unitsColor\":\"#37474f\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"valueColor\":\"#444\",\"valueColorShadow\":\"rgba(0,0,0,0.3)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"showBorder\":false,\"colorPlate\":\"#fff\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":10,\"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\"}],\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"barStrokeWidth\":2.5,\"colorBarStroke\":\"#b0bec5\",\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\"},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\"}" + "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;\"}]}],\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"startAngle\":45,\"ticksAngle\":270,\"needleCircleSize\":10,\"defaultColor\":\"#e64a19\",\"minValue\":-60,\"maxValue\":100,\"majorTicksCount\":8,\"colorMajorTicks\":\"#444\",\"minorTicks\":8,\"colorMinorTicks\":\"#666\",\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"numbersColor\":\"#263238\",\"showUnitTitle\":true,\"unitTitle\":\"Temperature\",\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"titleColor\":\"#78909c\",\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"unitsColor\":\"#37474f\",\"valueBox\":true,\"valueInt\":3,\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"valueColor\":\"#444\",\"valueColorShadow\":\"rgba(0,0,0,0.3)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"showBorder\":false,\"colorPlate\":\"#fff\",\"colorNeedle\":null,\"colorNeedleEnd\":null,\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"highlightsWidth\":10,\"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\"}],\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"barStrokeWidth\":2.5,\"colorBarStroke\":\"#b0bec5\",\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\"},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"units\":\"°C\"}" }, "tags": [ "pyrometer", diff --git a/application/src/main/data/json/system/widget_types/time_series_chart.json b/application/src/main/data/json/system/widget_types/time_series_chart.json index 80afebe8bf..2e6c0dec03 100644 --- a/application/src/main/data/json/system/widget_types/time_series_chart.json +++ b/application/src/main/data/json/system/widget_types/time_series_chart.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-time-series-chart-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"type\":\"bar\"},\"_hash\":0.5534217244004682,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Time series chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"type\":\"bar\"},\"_hash\":0.5534217244004682,\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":null,\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0}},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"padding\":\"12px\"},\"title\":\"Time series chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, "tags": [ "chart", diff --git a/application/src/main/data/json/system/widget_types/timeseries_bar_chart.json b/application/src/main/data/json/system/widget_types/timeseries_bar_chart.json index 83ef6d6bee..7c956dbef4 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_bar_chart.json +++ b/application/src/main/data/json/system/widget_types/timeseries_bar_chart.json @@ -12,14 +12,12 @@ "templateHtml": "\n", "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}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-bar-widget-settings", "dataKeySettingsDirective": "tb-flot-bar-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-flot-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json index 8343eb918c..24443cfecf 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_table.json +++ b/application/src/main/data/json/system/widget_types/timeseries_table.json @@ -1,6 +1,6 @@ { "fqn": "cards.timeseries_table", - "name": "Timeseries table", + "name": "Time series table", "deprecated": false, "image": "tb-image;/api/images/system/timeseries_table_system_widget_image.png", "description": "Displays time series data for one or more entities. Data for each entity is displayed in a separate tab. Columns are configured to display entity fields, attributes, or telemetry data. Highly customizable via cell content functions and row style functions.", @@ -17,7 +17,7 @@ "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-timeseries-table-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"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', 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}\",\"useCellContentFunction\":false},\"_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;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"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;\"}],\"latestDataKeys\":null}],\"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\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"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', 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}\",\"useCellContentFunction\":false},\"_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;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"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;\"}],\"latestDataKeys\":[]}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"sortOrder\":{\"property\":\"createdTime\",\"direction\":\"DESC\"}},\"title\":\"Time series table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"titleIcon\":null}" }, "resources": [ { @@ -32,4 +32,4 @@ "public": true } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/trip_map.json b/application/src/main/data/json/system/widget_types/trip_map.json index 8f54f64b6b..b68bc62981 100644 --- a/application/src/main/data/json/system/widget_types/trip_map.json +++ b/application/src/main/data/json/system/widget_types/trip_map.json @@ -20,7 +20,7 @@ "latestDataKeySettingsDirective": "", "hasBasicMode": true, "basicModeDirective": "tb-map-basic-config", - "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"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)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"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)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":0.5,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null},\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-0.5},\"click\":{\"type\":\"doNothing\"},\"edit\":{\"enabledActions\":[],\"snappable\":false},\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":2,\"pathStrokeColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"tripTimeline\":{\"showTimelineControl\":true}},\"title\":\"Trip Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}" + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"selectedTab\":1,\"history\":{\"historyType\":0,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":5000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"layers\":[{\"label\":\"{i18n:widgets.maps.layer.roadmap}\",\"provider\":\"openstreet\",\"layerType\":\"OpenStreetMap.Mapnik\"},{\"label\":\"{i18n:widgets.maps.layer.satellite}\",\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\"},{\"label\":\"{i18n:widgets.maps.layer.hybrid}\",\"provider\":\"openstreet\",\"layerType\":\"Esri.WorldImagery\",\"referenceLayer\":\"openstreetmap_hybrid\"}],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"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)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"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)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":0.5,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null},\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-0.5},\"click\":{\"type\":\"doNothing\"},\"edit\":{\"enabledActions\":[],\"snappable\":false},\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":2,\"pathStrokeColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"markers\":[],\"polygons\":[],\"circles\":[],\"polylines\":[],\"additionalDataSources\":[],\"controlsPosition\":\"topleft\",\"zoomActions\":[\"scroll\",\"doubleClick\",\"controlButtons\"],\"scales\":[],\"dragModeButton\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"defaultCenterPosition\":\"0,0\",\"defaultZoomLevel\":null,\"minZoomLevel\":16,\"mapPageSize\":16384,\"mapActionButtons\":[],\"tripTimeline\":{\"showTimelineControl\":true,\"timeStep\":1000,\"speedOptions\":[1,5,10,15,25],\"showTimestamp\":true,\"timestampFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\",\"lastUpdateAgo\":false,\"custom\":false,\"auto\":false},\"snapToRealLocation\":false,\"locationSnapFilter\":\"return true;\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"8px\"},\"title\":\"Trip Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/two_segment_button.json b/application/src/main/data/json/system/widget_types/two_segment_button.json index d32d139dce..e9e77fac75 100644 --- a/application/src/main/data/json/system/widget_types/two_segment_button.json +++ b/application/src/main/data/json/system/widget_types/two_segment_button.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n maxDatasources: 1,\n maxDataKeys: 0,\n singleEntity: true,\n previewWidth: '300px',\n previewHeight: '80px',\n embedTitlePanel: true,\n overflowVisible: true,\n hideDataSettings: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-segmented-button-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-segmented-button-basic-config", - "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#E8E8E8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":true,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"},\"executeRpc\":{\"method\":null,\"requestTimeout\":null,\"requestPersistent\":null,\"persistentPollingInterval\":null}},\"leftButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"rightButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"appearance\":{\"layout\":\"squared\",\"autoScale\":true,\"cardBorder\":1,\"cardBorderColor\":\"#305680\",\"leftAppearance\":{\"showLabel\":true,\"label\":\"Traditional\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"rightAppearance\":{\"showLabel\":true,\"label\":\"Hi-Perf\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"selectedStyle\":{\"mainColor\":\"#FFFFFF\",\"backgroundColor\":\"#305680\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}},\"unselectedStyle\":{\"mainColor\":\"#000000C2\",\"backgroundColor\":\"#E8E8E8\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}}}},\"title\":\"Segmented button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"datasources\":[],\"actions\":{\"click\":[{\"id\":\"56ebc389-f91d-fc25-36ef-0d18329fbeda\",\"name\":\"onClick\",\"icon\":\"more_horiz\",\"type\":\"doNothing\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null,\"openInSeparateDialog\":false,\"openInPopover\":false}]},\"borderRadius\":\"4px\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#E8E8E8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":true,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"},\"executeRpc\":{\"method\":null,\"requestTimeout\":null,\"requestPersistent\":null,\"persistentPollingInterval\":null}},\"leftButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"rightButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"appearance\":{\"layout\":\"squared\",\"autoScale\":true,\"cardBorder\":1,\"cardBorderColor\":\"#305680\",\"leftAppearance\":{\"showLabel\":true,\"label\":\"Traditional\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"rightAppearance\":{\"showLabel\":true,\"label\":\"Hi-Perf\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"selectedStyle\":{\"mainColor\":\"#FFFFFF\",\"backgroundColor\":\"#305680\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}},\"unselectedStyle\":{\"mainColor\":\"#000000C2\",\"backgroundColor\":\"#E8E8E8\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}}}},\"title\":\"Segmented button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"datasources\":[],\"actions\":{\"click\":[{\"id\":\"56ebc389-f91d-fc25-36ef-0d18329fbeda\",\"name\":\"onClick\",\"icon\":\"more_horiz\",\"type\":\"doNothing\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null,\"openInSeparateDialog\":false,\"openInPopover\":false}]},\"borderRadius\":\"4px\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/unread_notifications.json b/application/src/main/data/json/system/widget_types/unread_notifications.json index b6f97724b8..7d699e7018 100644 --- a/application/src/main/data/json/system/widget_types/unread_notifications.json +++ b/application/src/main/data/json/system/widget_types/unread_notifications.json @@ -11,12 +11,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.unreadNotificationWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '400px',\n previewHeight: '300px',\n embedTitlePanel: true\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-unread-notification-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-unread-notification-basic-config", - "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\":\"0\",\"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}\",\"maxNotificationDisplay\":6,\"showCounter\":true,\"counterValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"\"},\"counterValueColor\":\"#fff\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"enableViewAll\":true,\"enableFilter\":true,\"enableMarkAsRead\":true},\"title\":\"Unread notification\",\"dropShadow\":true,\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"#000000\",\"showTitleIcon\":true,\"iconSize\":\"22px\",\"titleIcon\":\"notifications\",\"iconColor\":\"#000000\",\"actions\":{},\"enableFullscreen\":false,\"borderRadius\":\"4px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"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}\",\"maxNotificationDisplay\":6,\"showCounter\":true,\"counterValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"\"},\"counterValueColor\":\"#fff\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"enableViewAll\":true,\"enableFilter\":true,\"enableMarkAsRead\":true},\"title\":\"Unread notification\",\"dropShadow\":true,\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"#000000\",\"showTitleIcon\":true,\"iconSize\":\"22px\",\"titleIcon\":\"notifications\",\"iconColor\":\"#000000\",\"actions\":{},\"enableFullscreen\":false,\"borderRadius\":\"4px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_boolean_timeseries.json b/application/src/main/data/json/system/widget_types/update_boolean_timeseries.json index e338b7af15..8d88f7d168 100644 --- a/application/src/main/data/json/system/widget_types/update_boolean_timeseries.json +++ b/application/src/main/data/json/system/widget_types/update_boolean_timeseries.json @@ -12,10 +12,9 @@ "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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-boolean-attribute-widget-settings", - "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\":{}}" + "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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_device_attribute.json b/application/src/main/data/json/system/widget_types/update_device_attribute.json index 763ba93bc9..0c48a999d4 100644 --- a/application/src/main/data/json/system/widget_types/update_device_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_device_attribute.json @@ -12,10 +12,9 @@ "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-mdc-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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-device-attribute-widget-settings", - "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\":[]}" + "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},\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" }, "tags": [ "command", diff --git a/application/src/main/data/json/system/widget_types/update_double_timeseries.json b/application/src/main/data/json/system/widget_types/update_double_timeseries.json index 20aae30b30..760dd2e932 100644 --- a/application/src/main/data/json/system/widget_types/update_double_timeseries.json +++ b/application/src/main/data/json/system/widget_types/update_double_timeseries.json @@ -12,10 +12,9 @@ "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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-double-attribute-widget-settings", - "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\":\"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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_integer_timeseries.json b/application/src/main/data/json/system/widget_types/update_integer_timeseries.json index 579e233d59..06ef1e46b6 100644 --- a/application/src/main/data/json/system/widget_types/update_integer_timeseries.json +++ b/application/src/main/data/json/system/widget_types/update_integer_timeseries.json @@ -12,10 +12,9 @@ "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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-integer-attribute-widget-settings", - "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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_json_attribute.json b/application/src/main/data/json/system/widget_types/update_json_attribute.json index 95c648204c..f33c05bc6a 100644 --- a/application/src/main/data/json/system/widget_types/update_json_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_json_attribute.json @@ -12,10 +12,9 @@ "templateHtml": "\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": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "", "dataKeySettingsSchema": "{}", "settingsDirective": "tb-update-json-attribute-widget-settings", - "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\":{\"widgetMode\":\"ATTRIBUTE\",\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + "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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetMode\":\"ATTRIBUTE\",\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_location_timeseries.json b/application/src/main/data/json/system/widget_types/update_location_timeseries.json index 9724e8e6b6..1542eed426 100644 --- a/application/src/main/data/json/system/widget_types/update_location_timeseries.json +++ b/application/src/main/data/json/system/widget_types/update_location_timeseries.json @@ -1,9 +1,9 @@ { "fqn": "input_widgets.update_location_timeseries", - "name": "Update location timeseries", + "name": "Update location time series", "deprecated": false, "image": "tb-image;/api/images/system/update_shared_location_attribute_system_widget_image.png", - "description": "Simple form to input new location for pre-defined timeseries keys.", + "description": "Simple form to input new location for pre-defined time series keys.", "descriptor": { "type": "latest", "sizeX": 7.5, @@ -12,10 +12,9 @@ "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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-location-attribute-widget-settings", - "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\":{\"widgetTitle\":\"\",\"showResultMessage\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showGetLocation\":true,\"enableHighAccuracy\":false,\"showLabel\":true,\"latLabel\":\"\",\"lngLabel\":\"\",\"inputFieldsAlignment\":\"column\",\"isLatRequired\":true,\"isLngRequired\":true,\"requiredErrorMessage\":\"\"},\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"showResultMessage\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showGetLocation\":true,\"enableHighAccuracy\":false,\"showLabel\":true,\"latLabel\":\"\",\"lngLabel\":\"\",\"inputFieldsAlignment\":\"column\",\"isLatRequired\":true,\"isLngRequired\":true,\"requiredErrorMessage\":\"\"},\"title\":\"Update location time series\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_multiple_attributes.json b/application/src/main/data/json/system/widget_types/update_multiple_attributes.json index f4aee4f0cf..5c2327d4c5 100644 --- a/application/src/main/data/json/system/widget_types/update_multiple_attributes.json +++ b/application/src/main/data/json/system/widget_types/update_multiple_attributes.json @@ -12,11 +12,9 @@ "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": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-update-multiple-attributes-widget-settings", "dataKeySettingsDirective": "tb-update-multiple-attributes-key-settings", - "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\":{}}" + "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));\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json b/application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json index 83d6d69b66..a0a2a1ebbe 100644 --- a/application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json @@ -12,10 +12,9 @@ "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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-boolean-attribute-widget-settings", - "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\":\"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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_server_date_attribute.json b/application/src/main/data/json/system/widget_types/update_server_date_attribute.json index c7f2c27dab..9629a0d70c 100644 --- a/application/src/main/data/json/system/widget_types/update_server_date_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_date_attribute.json @@ -12,10 +12,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ labelValue }}\n \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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\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.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\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 $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\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\r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \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\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", - "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-date-attribute-widget-settings", - "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\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_server_double_attribute.json b/application/src/main/data/json/system/widget_types/update_server_double_attribute.json index 3baca60720..8e0cb20131 100644 --- a/application/src/main/data/json/system/widget_types/update_server_double_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_double_attribute.json @@ -12,10 +12,9 @@ "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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [$scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-double-attribute-widget-settings", - "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\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server 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\":\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_server_image_attribute.json b/application/src/main/data/json/system/widget_types/update_server_image_attribute.json index 6b6432331c..8e5373ecd0 100644 --- a/application/src/main/data/json/system/widget_types/update_server_image_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_image_attribute.json @@ -12,10 +12,9 @@ "templateHtml": "
\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 settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, 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.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 if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\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\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 if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\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 var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-image-attribute-widget-settings", - "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\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"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\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_server_integer_attribute.json b/application/src/main/data/json/system/widget_types/update_server_integer_attribute.json index f00183c76d..bc4506b462 100644 --- a/application/src/main/data/json/system/widget_types/update_server_integer_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_integer_attribute.json @@ -12,10 +12,9 @@ "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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-integer-attribute-widget-settings", - "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\":{}}" + "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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_server_location_attribute.json b/application/src/main/data/json/system/widget_types/update_server_location_attribute.json index 59cb78339f..97b8d81e71 100644 --- a/application/src/main/data/json/system/widget_types/update_server_location_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_location_attribute.json @@ -12,10 +12,9 @@ "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\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 settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\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 var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng]\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-location-attribute-widget-settings", - "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\":{\"widgetTitle\":\"\",\"showResultMessage\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showGetLocation\":true,\"enableHighAccuracy\":false,\"showLabel\":true,\"latLabel\":\"\",\"lngLabel\":\"\",\"inputFieldsAlignment\":\"column\",\"isLatRequired\":true,\"isLngRequired\":true,\"requiredErrorMessage\":\"\"},\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"showResultMessage\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showGetLocation\":true,\"enableHighAccuracy\":false,\"showLabel\":true,\"latLabel\":\"\",\"lngLabel\":\"\",\"inputFieldsAlignment\":\"column\",\"isLatRequired\":true,\"isLngRequired\":true,\"requiredErrorMessage\":\"\"},\"title\":\"Update server location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_server_string_attribute.json b/application/src/main/data/json/system/widget_types/update_server_string_attribute.json index 8e1eeabfad..b303cf5113 100644 --- a/application/src/main/data/json/system/widget_types/update_server_string_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_string_attribute.json @@ -12,10 +12,9 @@ "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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [];\n if (utils.isDefinedAndNotNull(settings.minLength)) {\n validators.push($scope.validators.minLength(settings.minLength));\n }\n if (utils.isDefinedAndNotNull(settings.maxLength)) {\n validators.push($scope.validators.maxLength(settings.maxLength));\n }\n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\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 var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n 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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-string-attribute-widget-settings", - "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\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"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\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json index d47e9087d2..1c5ca79f40 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json @@ -12,10 +12,9 @@ "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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-boolean-attribute-widget-settings", - "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\":{}}" + "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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_shared_date_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_date_attribute.json index 6f40c1f047..13091ffedf 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_date_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_date_attribute.json @@ -12,10 +12,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ labelValue }}\n \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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\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.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\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 $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\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 \r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-date-attribute-widget-settings", - "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\":{\"showResultMessage\":true,\"isRequired\":true,\"showLabel\":true,\"showTimeInput\":true},\"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\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"isRequired\":true,\"showLabel\":true,\"showTimeInput\":true},\"title\":\"Update shared date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_shared_double_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_double_attribute.json index 1e44faea0e..3a93247ce3 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_double_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_double_attribute.json @@ -12,10 +12,9 @@ "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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n \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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-double-attribute-widget-settings", - "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\":\"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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_shared_image_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_image_attribute.json index 0aa39ac649..83d6f8659e 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_image_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_image_attribute.json @@ -12,10 +12,9 @@ "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 settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, true);\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 if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\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\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 if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\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 var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-image-attribute-widget-settings", - "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\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update shared 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\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update shared image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json index 349540df10..69810272af 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json @@ -12,10 +12,9 @@ "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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-integer-attribute-widget-settings", - "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\":\"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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_shared_location_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_location_attribute.json index 4f15d1f29a..e88e92cf78 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_location_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_location_attribute.json @@ -12,10 +12,9 @@ "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 settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\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 var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng],\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-location-attribute-widget-settings", - "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\":{\"widgetTitle\":\"\",\"showResultMessage\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showGetLocation\":true,\"enableHighAccuracy\":false,\"showLabel\":true,\"latLabel\":\"\",\"lngLabel\":\"\",\"inputFieldsAlignment\":\"column\",\"isLatRequired\":true,\"isLngRequired\":true,\"requiredErrorMessage\":\"\"},\"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\":\"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;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"showResultMessage\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showGetLocation\":true,\"enableHighAccuracy\":false,\"showLabel\":true,\"latLabel\":\"\",\"lngLabel\":\"\",\"inputFieldsAlignment\":\"column\",\"isLatRequired\":true,\"isLngRequired\":true,\"requiredErrorMessage\":\"\"},\"title\":\"Update shared location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json index 135fd74b46..0882d1fc41 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json @@ -12,10 +12,9 @@ "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 settings.isRequired = utils.defaultValue(settings.isRequired, 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 var validators = [];\n if (utils.isDefinedAndNotNull(settings.minLength)) {\n validators.push($scope.validators.minLength(settings.minLength));\n }\n if (utils.isDefinedAndNotNull(settings.maxLength)) {\n validators.push($scope.validators.maxLength(settings.maxLength));\n }\n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\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 var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n 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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-string-attribute-widget-settings", - "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\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/update_string_timeseries.json b/application/src/main/data/json/system/widget_types/update_string_timeseries.json index 93bc4c4be2..40c3c0d7d5 100644 --- a/application/src/main/data/json/system/widget_types/update_string_timeseries.json +++ b/application/src/main/data/json/system/widget_types/update_string_timeseries.json @@ -12,10 +12,9 @@ "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 var validators = [];\n if (utils.isDefinedAndNotNull(settings.minLength)) {\n validators.push($scope.validators.minLength(settings.minLength));\n }\n if (utils.isDefinedAndNotNull(settings.maxLength)) {\n validators.push($scope.validators.maxLength(settings.maxLength));\n }\n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, validators]}\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": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-string-attribute-widget-settings", - "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;\"}]}],\"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},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/uv_index_card.json b/application/src/main/data/json/system/widget_types/uv_index_card.json index 797948b1cb..e0f9f3ea22 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_card.json +++ b/application/src/main/data/json/system/widget_types/uv_index_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#80C32C\"},{\"from\":2,\"to\":5,\"color\":\"#FFA600\"},{\"from\":5,\"to\":7,\"color\":\"#F36900\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json b/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json index 4b3f255401..fc94104bb7 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/uv_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"UV Index\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.ceil(Math.random() * 4 - 2);\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 14) {\\n\\tvalue = 14;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"light_mode\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":2,\"color\":\"#7CC322\"},{\"from\":2,\"to\":5,\"color\":\"#F89E0D\"},{\"from\":5,\"to\":7,\"color\":\"#F77410\"},{\"from\":7,\"to\":10,\"color\":\"#F04022\"},{\"from\":10,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/uv_index_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"UV Index card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/value_card.json b/application/src/main/data/json/system/widget_types/value_card.json index a0cce85fa2..1b539fb360 100644 --- a/application/src/main/data/json/system/widget_types/value_card.json +++ b/application/src/main/data/json/system/widget_types/value_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"constant\",\"color\":\"#5469FF\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"constant\",\"color\":\"#5469FF\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/vertical_bar.json b/application/src/main/data/json/system/widget_types/vertical_bar.json index bd5737cb49..725dcbda62 100644 --- a/application/src/main/data/json/system/widget_types/vertical_bar.json +++ b/application/src/main/data/json/system/widget_types/vertical_bar.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-digital-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-digital-simple-gauge-basic-config", - "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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" + "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;\"}]}],\"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\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/vertical_capsule_tank.json b/application/src/main/data/json/system/widget_types/vertical_capsule_tank.json index 7258e76c85..f1ef3ec8fb 100644 --- a/application/src/main/data/json/system/widget_types/vertical_capsule_tank.json +++ b/application/src/main/data/json/system/widget_types/vertical_capsule_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Capsule\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Capsule\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/vertical_cylinder_tank.json b/application/src/main/data/json/system/widget_types/vertical_cylinder_tank.json index 4432ffc40d..a8d1ae5f6d 100644 --- a/application/src/main/data/json/system/widget_types/vertical_cylinder_tank.json +++ b/application/src/main/data/json/system/widget_types/vertical_cylinder_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Cylinder\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Cylinder\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/vertical_oval_tank.json b/application/src/main/data/json/system/widget_types/vertical_oval_tank.json index fa61324ca7..b461ea7b62 100644 --- a/application/src/main/data/json/system/widget_types/vertical_oval_tank.json +++ b/application/src/main/data/json/system/widget_types/vertical_oval_tank.json @@ -12,12 +12,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.liquidLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.liquidLevelWidget.update();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};\n\nself.onDestroy = function() {\n}\n\nself.actionSources = function() { \n return { \n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false \n } \n };\n}", - "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Oval\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Oval\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", diff --git a/application/src/main/data/json/system/widget_types/vibration_card.json b/application/src/main/data/json/system/widget_types/vibration_card.json index 033da084fb..fd2c4be2a3 100644 --- a/application/src/main/data/json/system/widget_types/vibration_card.json +++ b/application/src/main/data/json/system/widget_types/vibration_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#FFA600\"},{\"from\":1,\"to\":10,\"color\":\"#F36900\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#D81838\"},{\"from\":1000,\"to\":null,\"color\":\"#6F113A\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/vibration_card_with_background.json index f078b93194..3c77ef51b3 100644 --- a/application/src/main/data/json/system/widget_types/vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/vibration_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/vibration_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Vibration\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"let factor = 1000;\\nif (prevValue < 1) {\\n factor = 1;\\n} else if (prevValue < 10) {\\n factor = 10;\\n} else if (prevValue < 100) {\\n factor = 100;\\n}\\nlet value = prevValue + Math.random() * factor;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"vibration\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":28,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":null,\"to\":0.1,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0.1,\"to\":1,\"color\":\"#F89E0D\"},{\"from\":1,\"to\":10,\"color\":\"#F77410\"},{\"from\":10,\"to\":100,\"color\":\"#F04022\"},{\"from\":100,\"to\":1000,\"color\":\"#DE2343\"},{\"from\":1000,\"to\":null,\"color\":\"#791541\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/vibration_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Vibration card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s²\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/visibility_card.json b/application/src/main/data/json/system/widget_types/visibility_card.json index c640c702e9..c30455251a 100644 --- a/application/src/main/data/json/system/widget_types/visibility_card.json +++ b/application/src/main/data/json/system/widget_types/visibility_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#D81838\"},{\"from\":1,\"to\":4,\"color\":\"#FFA600\"},{\"from\":4,\"to\":null,\"color\":\"#80C32C\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/visibility_card_with_background.json b/application/src/main/data/json/system/widget_types/visibility_card_with_background.json index 02e12105b8..4103cf657c 100644 --- a/application/src/main/data/json/system/widget_types/visibility_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/visibility_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/visibility_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Visibility\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 20) {\\n\\tvalue = 20;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"visibility\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":1,\"color\":\"#DE2343\"},{\"from\":1,\"to\":4,\"color\":\"#F89E0D\"},{\"from\":4,\"to\":null,\"color\":\"#7CC322\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/visibility_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Air quality card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"km\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json index 517ecaf1ce..7a00e37826 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Volatile organic compounds card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#80C32C\"},{\"from\":500,\"to\":1000,\"color\":\"#FFA600\"},{\"from\":1000,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Volatile organic compounds card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json index fb5bf45da1..b76fb80d53 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/volatile_organic_compounds_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Volatile organic compounds card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"VOCs\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 500 - 250;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 2000) {\\n\\tvalue = 2000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:molecule\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":32,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":500,\"color\":\"#7CC322\"},{\"from\":500,\"to\":1000,\"color\":\"#F89E0D\"},{\"from\":1000,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/volatile_organic_compounds_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Volatile organic compounds card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"ppb\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "environment", diff --git a/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json b/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json index 78c4fdabf0..5ad343c61e 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json @@ -12,12 +12,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 2,\n singleEntity: true,\n previewWidth: '270px',\n previewHeight: '270px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'direction', label: 'Wind Direction', type: 'timeseries' },\n { name: 'speed', label: 'Wind Speed', type: 'timeseries',\n units: 'm/s', decimals: 1 }];\n }\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-wind-speed-direction-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-wind-speed-direction-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Direction\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 360;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 360) {\\n\\tvalue = 360;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m/s\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"centerValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"centerValueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#5B7EE6\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"ticksColor\":\"rgba(0, 0, 0, 0.12)\",\"directionalNamesElseDegrees\":true,\"majorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"majorTicksColor\":\"rgba(158, 158, 158, 1)\",\"minorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"minorTicksColor\":\"rgba(0, 0, 0, 0.12)\",\"arrowColor\":\"rgba(0, 0, 0, 0.87)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Direction\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 360;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 360) {\\n\\tvalue = 360;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m/s\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"centerValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"centerValueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#5B7EE6\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"ticksColor\":\"rgba(0, 0, 0, 0.12)\",\"directionalNamesElseDegrees\":true,\"majorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"majorTicksColor\":\"rgba(158, 158, 158, 1)\",\"minorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"minorTicksColor\":\"rgba(0, 0, 0, 0.12)\",\"arrowColor\":\"rgba(0, 0, 0, 0.87)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "wind", diff --git a/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json index f96a21781f..2b4f9055c1 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json @@ -12,12 +12,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 2,\n singleEntity: true,\n previewWidth: '270px',\n previewHeight: '270px',\n embedTitlePanel: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'direction', label: 'Wind Direction', type: 'timeseries' },\n { name: 'speed', label: 'Wind Speed', type: 'timeseries',\n units: 'm/s', decimals: 1 }];\n }\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-wind-speed-direction-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-wind-speed-direction-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Direction\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 360;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 360) {\\n\\tvalue = 360;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m/s\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"centerValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"centerValueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"ticksColor\":\"rgba(0, 0, 0, 0.12)\",\"directionalNamesElseDegrees\":true,\"majorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"majorTicksColor\":\"rgba(158, 158, 158, 1)\",\"minorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"minorTicksColor\":\"rgba(0, 0, 0, 0.12)\",\"arrowColor\":\"rgba(0, 0, 0, 0.87)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/wind_speed_and_direction_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Direction\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 360;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 360) {\\n\\tvalue = 360;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m/s\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"centerValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"centerValueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"ticksColor\":\"rgba(0, 0, 0, 0.12)\",\"directionalNamesElseDegrees\":true,\"majorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"majorTicksColor\":\"rgba(158, 158, 158, 1)\",\"minorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"minorTicksColor\":\"rgba(0, 0, 0, 0.12)\",\"arrowColor\":\"rgba(0, 0, 0, 0.87)\",\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/wind_speed_and_direction_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind Speed\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":0,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":true,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:windsock\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "wind", diff --git a/application/src/main/data/json/system/widget_types/wind_speed_card.json b/application/src/main/data/json/system/widget_types/wind_speed_card.json index 0727d4a905..8f3d311d16 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_card.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_card.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#7191EF\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5B7EE6\"},{\"from\":3.4,\"to\":8,\"color\":\"#4B70DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#305AD7\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#234CC7\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#D81838\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", diff --git a/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json index 1fdc2e2ec3..8a37be56e8 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/wind_speed_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 16 - 8;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 26) {\\n\\tvalue = 26;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"square\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"mdi:windsock\",\"iconColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"size\":36,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\"},\"valueColor\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\",\"rangeList\":[{\"from\":0,\"to\":0.2,\"color\":\"#6083EC\"},{\"from\":0.2,\"to\":3.4,\"color\":\"#5579E5\"},{\"from\":3.4,\"to\":8,\"color\":\"#4369DD\"},{\"from\":8,\"to\":10.8,\"color\":\"#2B54CE\"},{\"from\":10.8,\"to\":17.2,\"color\":\"#224AC2\"},{\"from\":17.2,\"to\":24.5,\"color\":\"#F04022\"},{\"from\":24.5,\"to\":null,\"color\":\"#DE2343\"}]},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"image\",\"imageBase64\":\"tb-image;/api/images/system/wind_speed_card_with_background_system_widget_background.png\",\"imageUrl\":null,\"color\":\"#fff\",\"overlay\":{\"enabled\":true,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"autoScale\":true},\"title\":\"Wind speed card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"m/s\",\"decimals\":1,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" }, "tags": [ "weather", 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 index 305dc04961..8773a2d6aa 100644 --- 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 @@ -10,12 +10,12 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 6, + "firstNodeIndex": 2, "nodes": [ { "additionalInfo": { - "layoutX": 822, - "layoutY": 294 + "layoutX": 824, + "layoutY": 156 }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", @@ -30,8 +30,8 @@ }, { "additionalInfo": { - "layoutX": 824, - "layoutY": 221 + "layoutX": 825, + "layoutY": 52 }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", @@ -48,8 +48,8 @@ }, { "additionalInfo": { - "layoutX": 494, - "layoutY": 309 + "layoutX": 347, + "layoutY": 149 }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", @@ -59,8 +59,8 @@ }, { "additionalInfo": { - "layoutX": 824, - "layoutY": 383 + "layoutX": 825, + "layoutY": 266 }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", @@ -72,8 +72,8 @@ }, { "additionalInfo": { - "layoutX": 823, - "layoutY": 444 + "layoutX": 825, + "layoutY": 379 }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", @@ -85,27 +85,14 @@ }, { "additionalInfo": { - "layoutX": 822, - "layoutY": 507 + "layoutX": 825, + "layoutY": 468 }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", "configuration": { "timeoutInSeconds": 60 } - }, - { - "additionalInfo": { - "description": "", - "layoutX": 209, - "layoutY": 307 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - } } ], "connections": [ @@ -133,11 +120,6 @@ "fromIndex": 2, "toIndex": 5, "type": "RPC Request to Device" - }, - { - "fromIndex": 6, - "toIndex": 2, - "type": "Success" } ], "ruleChainConnections": null 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 a988c9d5eb..c48dab1964 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 @@ -9,7 +9,7 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 6, + "firstNodeIndex": 2, "nodes": [ { "additionalInfo": { @@ -92,27 +92,9 @@ "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", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - } } ], "connections": [ - { - "fromIndex": 6, - "toIndex": 2, - "type": "Success" - }, { "fromIndex": 2, "toIndex": 4, diff --git a/application/src/main/data/resources/dashboards/gateways_dashboard.json b/application/src/main/data/resources/dashboards/gateways_dashboard.json index 078f913570..4fe9e180e1 100644 --- a/application/src/main/data/resources/dashboards/gateways_dashboard.json +++ b/application/src/main/data/resources/dashboards/gateways_dashboard.json @@ -11,37 +11,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680624762213, - "endTimeMs": 1680711162213 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -70,7 +39,6 @@ "fontWeight": 600, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -184,7 +152,6 @@ ] } ], - "displayTimewindow": true, "actions": { "actionCellButton": [ { @@ -275,9 +242,9 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "customPretty", - "customHtml": "
\r\n \r\n

Add gateway

\r\n \r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n Name\r\n \r\n \r\n Gateway name is required.\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n", + "customHtml": "
\r\n \r\n

Add gateway

\r\n \r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n Name\r\n \r\n \r\n Gateway name is required.\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n", "customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n", - "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Launch command\");\r\n setTimeout(() => {\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n });\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", + "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required, vm.validators.pattern(/.*\\S.*/)]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Launch command\");\r\n setTimeout(() => {\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n });\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName.trim(),\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", "customResources": [], "openInSeparateDialog": false, "openInPopover": false, @@ -318,37 +285,6 @@ "dataKeys": [] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680685647526, - "endTimeMs": 1680772047526 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -369,7 +305,6 @@ "pageSize": 1024, "noDataDisplayMessage": "", "showLegend": false, - "useDashboardTimewindow": true, "borderRadius": "4px" }, "row": 0, @@ -424,37 +359,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680769601167, - "endTimeMs": 1680856001167 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#FFFFFF01", "color": "rgba(0, 0, 0, 0.87)", @@ -474,8 +378,7 @@ }, "pageSize": 1024, "noDataDisplayMessage": "", - "showLegend": false, - "useDashboardTimewindow": true + "showLegend": false }, "row": 0, "col": 0, @@ -553,8 +456,8 @@ "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "var newValue = value == 'true' ? \"Active\" : \"Inactive\";\nreturn newValue;" + "usePostProcessing": false, + "postFuncBody": null }, { "name": "ALL_ERRORS_COUNT", @@ -571,6 +474,20 @@ "color": "#03a9f4", "settings": {}, "_hash": 0.007837366355011532 + }, + { + "name": "lastConnectTime", + "type": "attribute", + "label": "lastConnectTime", + "color": "#ff9800", + "settings": {} + }, + { + "name": "lastDisconnectTime", + "type": "attribute", + "label": "lastDisconnectTime", + "color": "#673ab7", + "settings": {} } ], "alarmFilterConfig": { @@ -612,37 +529,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1683116949383, - "endTimeMs": 1683203349383 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -650,9 +536,9 @@ "settings": { "useMarkdownTextFunction": true, "markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.", - "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Logs\");\nfunction generateMatHeader(index) {\n if (index !== undefined && index > -1) {\n return ``\n } else {\n return \"\"\n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${ctx.sanitizer.sanitize(1, value)}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\" ? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\nif (data[0].Version) {\n createDataBlock(data[0].Version, \"Gateway Version\", '');\n}\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1] ? data[1].count : 0)} `\n + \" | \" +\n `${(data[2] ? data[2][\"count 2\"] : 0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors ? JSON.parse(data[0].active_connectors).length : 0)} `\n + \" | \" +\n `${(data[0].inactive_connectors ? JSON.parse(data[0].inactive_connectors).length : 0)} `\n , \"Connectors (Enabled | Disabled)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", + "markdownTextFunction": "var blockData = '';\n\nfunction createDataBlock(value, label, dividerStyle, mobile, id) {\n blockData += `\n \n
\n \n ${id ? `` : ``}\n ${label}\n \n ${ctx.sanitizer.sanitize(1, value)}\n
`;\n}\nconst lastConnectTime = Number(data[0].lastConnectTime);\nconst lastDisconnectTime = Number(data[0].lastDisconnectTime);\nconst isActive =\n data[0].Status === 'true' &&\n (!lastDisconnectTime || lastConnectTime > lastDisconnectTime);\ncreateDataBlock(isActive ? \"{{'gateway.active' | translate }}\" : \"{{'gateway.inactive' | translate}}\", \"{{ 'gateway.status-widget.status' | translate }}\", isActive ? \"divider-green\" : \"divider-red\");\ncreateDataBlock(data[0].Name, \"{{ 'gateway.status-widget.gateway-name' | translate }}\", '', ctx.isMobile);\n\nif (data[0].Version) {\n createDataBlock(data[0].Version, \"{{ 'gateway.status-widget.gateway-version' | translate }}\", '');\n}\n\ncreateDataBlock(data[0].Type, \"{{ 'gateway.status-widget.gateway-type' | translate }}\", '');\n\ncreateDataBlock(\n `${(data[1] ? data[1].count : 0)} `\n + \" | \" +\n `${(data[2] ? data[2][\"count 2\"] : 0)} `\n , \"{{ 'gateway.status-widget.devices' | translate }} {{ 'gateway.status-widget.active-inactive' | translate }}\", '');\n \ncreateDataBlock(\n `${(data[0].active_connectors ? JSON.parse(data[0].active_connectors).length : 0)} `\n + \" | \" +\n `${(data[0].inactive_connectors ? JSON.parse(data[0].inactive_connectors).length : 0)} `\n , \"{{ 'gateway.status-widget.connectors' | translate }} {{ 'gateway.status-widget.enabled-disabled' | translate }}\", '', '', 'connectors');\n \ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"{{ 'gateway.status-widget.errors' | translate }}\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', 'logs');\n\nreturn `
${blockData}
`;", "applyDefaultMarkdownStyle": false, - "markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}" + "markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n}\n\n.tb-home-widget-link {\n cursor: pointer;\n width: 100%;\n}\n\n.tb-home-widget-link:hover::after{\n color: inherit;\n}\n \n.tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}" }, "title": "Connectors", "showTitleIcon": false, @@ -667,18 +553,16 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "margin": "0", "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "", "enableDataExport": false, - "displayTimewindow": true, "borderRadius": "4px", "actions": { "elementClick": [ { - "name": "Connectors", + "name": "connectors", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -692,7 +576,7 @@ "id": "ee7216d8-ad7a-20db-1abc-2531e221f24c" }, { - "name": "Logs", + "name": "logs", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -711,7 +595,7 @@ "row": 0, "col": 0, "id": "79f59106-758f-c428-8b93-4341faea705d", - "typeFullFqn": "system.cards.markdown_card" + "typeFullFqn": "system.gateway_widgets.markdown_card" }, "60dcf518-8fc3-3539-8ff8-ce94bda39f3a": { "type": "alarm", @@ -872,9 +756,6 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -1042,7 +923,6 @@ "fontSize": "16px", "fontWeight": 400 }, - "useDashboardTimewindow": true, "showLegend": false, "actions": {}, "datasources": [], @@ -1239,7 +1119,6 @@ "fontSize": "16px", "fontWeight": 400 }, - "useDashboardTimewindow": true, "showLegend": false, "actions": {}, "datasources": [], @@ -1550,7 +1429,6 @@ "fontSize": "16px", "fontWeight": 400 }, - "useDashboardTimewindow": true, "showLegend": false, "actions": {}, "datasources": [], @@ -1584,44 +1462,13 @@ "dataKeys": [] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1685437116892, - "endTimeMs": 1685523516892 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", "padding": "8px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "
\r\n \r\n
", + "markdownTextPattern": "
\r\n \r\n
", "applyDefaultMarkdownStyle": false, "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: center;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n line-height: 36px;\n}" }, @@ -1638,11 +1485,10 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "actions": { "elementClick": [ { - "name": "Launch command", + "name": "launchCommand", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -1671,7 +1517,7 @@ "row": 0, "col": 0, "id": "fb9df382-6ef3-4aa6-bc13-8bf8e300ba19", - "typeFullFqn": "system.cards.markdown_card" + "typeFullFqn": "system.gateway_widgets.markdown_card" }, "61d149e8-b249-5526-e5d7-6ad58413982e": { "type": "latest", @@ -1704,37 +1550,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1685437116892, - "endTimeMs": 1685523516892 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1742,7 +1557,7 @@ "settings": { "useMarkdownTextFunction": true, "markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.", - "markdownTextFunction": "let buttonsHtml = \"\" \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n let disabled = false;\n if (index == 2) {\n disabled = data[0] && data[0].RemoteLoggingLevel ? data[0].RemoteLoggingLevel == \"NONE\" : true;\n } else if (index == 3) {\n const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\n disabled = typeof conf.statistics?.enable === 'boolean' && !conf.statistics.enable;\n } else if (index == 4) {\n const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\n disabled = !conf.remoteShell;\n }\n buttonsHtml += ``\n});\n\nreturn `
${buttonsHtml}
`;", + "markdownTextFunction": "const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\nconst disabledLogsBtn = data[0] && data[0].RemoteLoggingLevel ? data[0].RemoteLoggingLevel == \"NONE\" : true;\nconst disabledStatisticsBtn = typeof conf.statistics?.enable === 'boolean' && !conf.statistics.enable;\nconst disabledRemoteShellBtn = !conf.remoteShell;\nreturn `
\n \n \n \n \n \n \n
`;\n", "applyDefaultMarkdownStyle": false, "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: start;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n line-height: 36px;\n}" }, @@ -1759,11 +1574,10 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "actions": { "elementClick": [ { - "name": "General configuration", + "name": "generalConfiguration", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -1777,7 +1591,7 @@ "id": "337c767b-3217-d3d3-b955-7b0bd0858a1d" }, { - "name": "Connectors configuration", + "name": "connectorsConfiguration", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -1791,7 +1605,7 @@ "id": "00469ea7-f4c4-e39d-a735-c74b6ba6a026" }, { - "name": "Logs", + "name": "logs", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -1805,7 +1619,7 @@ "id": "d8607421-28b4-7c3b-949b-f69c3a46b461" }, { - "name": "Statistics", + "name": "statistics", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -1824,7 +1638,7 @@ "id": "425ba0d8-8e26-18a5-881d-bebe27fb2a7a" }, { - "name": "Remote Shell", + "name": "remoteShell", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -1838,7 +1652,7 @@ "id": "2c4c7dcc-009f-64eb-5f1b-a991df6d25a2" }, { - "name": "RPC", + "name": "rpc", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -1862,7 +1676,7 @@ "row": 0, "col": 0, "id": "61d149e8-b249-5526-e5d7-6ad58413982e", - "typeFullFqn": "system.cards.markdown_card" + "typeFullFqn": "system.gateway_widgets.markdown_card" }, "3d661190-7463-ba61-6793-503c85af67ec": { "type": "latest", @@ -1891,37 +1705,6 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1686652103417, - "endTimeMs": 1686738503417 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1946,8 +1729,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "", @@ -1957,44 +1738,13 @@ "row": 0, "col": 0, "id": "3d661190-7463-ba61-6793-503c85af67ec", - "typeFullFqn": "system.cards.markdown_card" + "typeFullFqn": "system.gateway_widgets.markdown_card" }, "1615bd4e-c0a4-c32c-3706-3c83214cb8d7": { "type": "latest", "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -2023,7 +1773,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -2103,7 +1852,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -2138,37 +1886,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -2197,7 +1914,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -2277,7 +1993,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -2312,37 +2027,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -2371,7 +2055,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -2451,7 +2134,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -2485,37 +2167,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -2544,7 +2195,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -2624,7 +2274,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -2658,37 +2307,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -2717,7 +2335,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -2797,7 +2414,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -2831,37 +2447,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -2890,7 +2475,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -2970,7 +2554,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -3004,37 +2587,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -3063,7 +2615,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -3143,7 +2694,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -3177,37 +2727,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -3236,7 +2755,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -3316,7 +2834,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -3350,37 +2867,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -3409,7 +2895,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -3489,7 +2974,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -3523,37 +3007,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -3582,7 +3035,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -3662,7 +3114,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -3696,37 +3147,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -3755,7 +3175,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -3835,7 +3254,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -3869,37 +3287,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -3928,7 +3315,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -4008,7 +3394,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -4042,37 +3427,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -4101,7 +3455,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -4181,7 +3534,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -4215,37 +3567,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -4274,7 +3595,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -4354,7 +3674,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -4388,37 +3707,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -4447,7 +3735,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -4527,7 +3814,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -4561,37 +3847,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -4620,7 +3875,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -4700,7 +3954,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -4734,37 +3987,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1684327643501, - "endTimeMs": 1684414043501 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -4793,7 +4015,6 @@ "fontWeight": 400, "padding": "5px 10px 5px 10px" }, - "useDashboardTimewindow": false, "showLegend": false, "datasources": [ { @@ -4873,7 +4094,6 @@ } } ], - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetStyle": {}, @@ -4903,7 +4123,7 @@ "typeFullFqn": "system.cards.entities_table" }, "b4194e1e-97af-ed0b-a6a4-70011c71c579": { - "typeFullFqn": "system.cards.markdown_card", + "typeFullFqn": "system.gateway_widgets.markdown_card", "type": "latest", "sizeX": 5, "sizeY": 3.5, @@ -4930,37 +4150,6 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1732109250067, - "endTimeMs": 1732195650067 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -4968,7 +4157,7 @@ "settings": { "useMarkdownTextFunction": true, "markdownTextPattern": "let buttonsHtml = \"\" \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n let disabled = false;\n if (index == 2) {\n disabled = data[0] && data[0].RemoteLoggingLevel ? data[0].RemoteLoggingLevel == \"NONE\" : true;\n } else if (index == 4) {\n const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\n disabled = !conf.remoteShell;\n }\n buttonsHtml += ``\n});\n\nreturn `
${buttonsHtml}
`;", - "markdownTextFunction": "const states = ['storage_statistics', 'machine_statistics', 'custom_statistics'];\nconst currentState = ctx.stateController.getStateId();\nlet buttonsHtml = \"\";\nlet disabled = false;\nconst buttonName = \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n if (states[index] === 'custom_statistics') {\n const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\n disabled = !conf.statistics?.enableCustom;\n }\n buttonsHtml += ``\n});\n\nreturn `
${buttonsHtml}
`;", + "markdownTextFunction": "const currentState = ctx.stateController.getStateId();\nconst conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\nconst disabled = !conf.statistics?.enableCustom;\nreturn `
\n\n\n\n
`;", "applyDefaultMarkdownStyle": false, "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: start;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n line-height: 36px;\r\n}" }, @@ -4985,51 +4174,40 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "actions": { "elementClick": [ { - "name": "Storage", + "name": "storage_statistics", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "storage_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, + "type": "custom", + "customFunction": "widgetContext.stateController.updateState('storage_statistics', widgetContext.stateController.getStateParams(), false);\nwidgetContext.$scope.markdownWidget.onDataUpdated();", "openInSeparateDialog": false, "openInPopover": false, - "id": "fc4c338d-5799-f7dc-9ecb-448e65f8045d" + "id": "56342fb4-e698-1db3-a8f1-34a3b44735c5" }, { - "name": "Machine", + "name": "machine_statistics", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "machine_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, + "type": "custom", + "customFunction": "widgetContext.stateController.updateState('machine_statistics', widgetContext.stateController.getStateParams(), false);\nwidgetContext.$scope.markdownWidget.onDataUpdated();", "openInSeparateDialog": false, "openInPopover": false, - "id": "b35fc758-b21a-f5d3-e968-3572f1bb9ad4" + "id": "dffa7e63-2382-12fa-ef1c-13e162b1acaf" }, { - "name": "Custom", + "name": "custom_statistics", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "custom_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, + "type": "custom", + "customFunction": "widgetContext.stateController.updateState('custom_statistics', widgetContext.stateController.getStateParams(), false);\nwidgetContext.$scope.markdownWidget.onDataUpdated();", "openInSeparateDialog": false, "openInPopover": false, - "id": "fcff099d-8164-08fd-7ffa-e1565772f368" + "id": "f6d267d0-3e3e-d519-bf7f-836fd59fd10f" } ] }, @@ -5045,7 +4223,7 @@ "id": "b4194e1e-97af-ed0b-a6a4-70011c71c579" }, "3a005e49-327c-beaa-8d19-c36b833001f0": { - "typeFullFqn": "system.cards.markdown_card", + "typeFullFqn": "system.gateway_widgets.markdown_card", "type": "latest", "sizeX": 5, "sizeY": 3.5, @@ -5057,37 +4235,6 @@ "dataKeys": [] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733234218432, - "endTimeMs": 1733320618432 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -5191,39 +4338,6 @@ ] } ], - "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "interval": 300000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_MONTH_SO_FAR", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 300000, - "timewindowMs": 86400000, - "fixedTimewindow": { - "startTimeMs": 1733156328824, - "endTimeMs": 1733242728824 - }, - "quickInterval": "CURRENT_MONTH_SO_FAR", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 5000 - } - }, "showTitle": true, "backgroundColor": "rgba(0, 0, 0, 0)", "color": null, @@ -5347,24 +4461,7 @@ "pageSize": 1024, "noDataDisplayMessage": "", "useDashboardTimewindow": true, - "displayTimewindow": false, "decimals": 0, - "timewindowStyle": { - "showIcon": true, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": false - }, "titleColor": "rgba(0, 0, 0, 0.87)", "borderRadius": "5px", "enableDataExport": false @@ -5443,39 +4540,6 @@ ] } ], - "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "interval": 300000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_MONTH_SO_FAR", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 2, - "interval": 7200000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733156705839, - "endTimeMs": 1733243105839 - }, - "quickInterval": "CURRENT_MONTH_SO_FAR", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 5000 - } - }, "showTitle": true, "backgroundColor": "rgba(0, 0, 0, 0)", "color": null, @@ -5599,24 +4663,7 @@ "pageSize": 1024, "noDataDisplayMessage": "", "useDashboardTimewindow": true, - "displayTimewindow": false, "decimals": 0, - "timewindowStyle": { - "showIcon": true, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": false - }, "titleColor": "rgba(0, 0, 0, 0.87)", "borderRadius": "5px", "enableDataExport": false @@ -6811,37 +5858,6 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733240126084, - "endTimeMs": 1733326526084 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -6852,8 +5868,6 @@ }, "title": "HTML Card", "dropShadow": true, - "useDashboardTimewindow": true, - "displayTimewindow": true, "enableFullscreen": false, "margin": "16px 16px 16px 0", "borderRadius": "5px", @@ -7451,7 +6465,7 @@ "id": "7543e668-35b7-bb97-2f14-163399f8d038" }, "7f3ebf9f-d967-44fe-fbe4-4667a2abcf88": { - "typeFullFqn": "system.cards.markdown_card", + "typeFullFqn": "system.gateway_widgets.markdown_card", "type": "latest", "sizeX": 5, "sizeY": 3.5, @@ -7512,37 +6526,6 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733155841882, - "endTimeMs": 1733242241882 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -7567,8 +6550,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "borderRadius": "5px", "widgetCss": "", "pageSize": 1024, @@ -7580,7 +6561,7 @@ "id": "7f3ebf9f-d967-44fe-fbe4-4667a2abcf88" }, "ea75a492-d149-9a53-b45f-840a0ce42a40": { - "typeFullFqn": "system.cards.markdown_card", + "typeFullFqn": "system.gateway_widgets.markdown_card", "type": "latest", "sizeX": 5, "sizeY": 3.5, @@ -7655,37 +6636,6 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733155841882, - "endTimeMs": 1733242241882 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -7710,8 +6660,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "borderRadius": "5px", "widgetCss": "", "pageSize": 1024, @@ -7723,7 +6671,7 @@ "id": "ea75a492-d149-9a53-b45f-840a0ce42a40" }, "13b57ab5-e5ed-701c-4c67-5bb1198e9a53": { - "typeFullFqn": "system.cards.markdown_card", + "typeFullFqn": "system.gateway_widgets.markdown_card", "type": "latest", "sizeX": 5, "sizeY": 3.5, @@ -7798,37 +6746,6 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733155841882, - "endTimeMs": 1733242241882 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -7853,8 +6770,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "borderRadius": "5px", "widgetCss": "", "pageSize": 1024, @@ -7866,7 +6781,7 @@ "id": "13b57ab5-e5ed-701c-4c67-5bb1198e9a53" }, "867613d5-9440-fac1-9156-cd8773df5c97": { - "typeFullFqn": "system.cards.markdown_card", + "typeFullFqn": "system.gateway_widgets.markdown_card", "type": "latest", "sizeX": 5, "sizeY": 3.5, @@ -7878,37 +6793,6 @@ "dataKeys": [] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733234218432, - "endTimeMs": 1733320618432 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -7927,495 +6811,20 @@ "titleTooltip": "", "dropShadow": false, "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "enableDataExport": false - }, - "row": 0, - "col": 0, - "id": "867613d5-9440-fac1-9156-cd8773df5c97" - }, - "56274c38-33fc-fe9d-aa39-ff3e1215276b": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": "", - "entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8", - "dataKeys": [ - { - "name": "general_configuration", - "type": "attribute", - "label": "general_configuration", - "color": "#2196f3", - "settings": {}, - "_hash": 0.5374662835906896 - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1732109250067, - "endTimeMs": 1732195650067 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "let buttonsHtml = \"\" \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n let disabled = false;\n if (index == 2) {\n disabled = data[0] && data[0].RemoteLoggingLevel ? data[0].RemoteLoggingLevel == \"NONE\" : true;\n } else if (index == 4) {\n const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\n disabled = !conf.remoteShell;\n }\n buttonsHtml += ``\n});\n\nreturn `
${buttonsHtml}
`;", - "markdownTextFunction": "const states = ['storage_statistics', 'machine_statistics', 'custom_statistics'];\nconst currentState = ctx.stateController.getStateId();\nlet buttonsHtml = \"\";\nlet disabled = false;\nconst buttonName = \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n if (states[index] === 'custom_statistics') {\n const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\n disabled = !conf.statistics?.enableCustom;\n }\n buttonsHtml += ``\n});\n\nreturn `
${buttonsHtml}
`;", - "applyDefaultMarkdownStyle": false, - "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: start;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n line-height: 36px;\r\n}" - }, - "title": "Markdown/HTML Card", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "actions": { - "elementClick": [ - { - "name": "Storage", - "icon": "more_horiz", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "storage_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "fc4c338d-5799-f7dc-9ecb-448e65f8045d" - }, - { - "name": "Machine", - "icon": "more_horiz", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "machine_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "b35fc758-b21a-f5d3-e968-3572f1bb9ad4" - }, - { - "name": "Custom", - "icon": "more_horiz", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "custom_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "10a5a4b6-71fb-f51c-2836-bb360938127f" - } - ] - }, - "borderRadius": "5px", - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "margin": "16px 16px 0 0", - "enableDataExport": false - }, - "row": 0, - "col": 0, - "id": "56274c38-33fc-fe9d-aa39-ff3e1215276b" - }, - "38063fe5-1bd8-695b-a40b-92c1aed19831": { - "typeFullFqn": "system.gateway_widgets.gateway_status", - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "config": { - "datasources": [ - { - "type": "entity", - "name": "function", - "entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8", - "dataKeys": [ - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#2196f3", - "settings": {}, - "_hash": 0.07932656697286933 - }, - { - "name": "lastDisconnectTime", - "type": "attribute", - "label": "lastDisconnectTime", - "color": "#4caf50", - "settings": {}, - "_hash": 0.06662431625551446 - }, - { - "name": "lastConnectTime", - "type": "attribute", - "label": "lastConnectTime", - "color": "#f44336", - "settings": {}, - "_hash": 0.28485659401606434 - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733240126084, - "endTimeMs": 1733326526084 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "rgb(255, 255, 255)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "", - "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, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "enableFullscreen": false, - "margin": "16px 16px 16px 0", - "borderRadius": "5px", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "enableDataExport": false - }, - "row": 0, - "col": 0, - "id": "38063fe5-1bd8-695b-a40b-92c1aed19831" - }, - "e48e7a66-a30e-2494-b0c0-dd3249944286": { - "typeFullFqn": "system.gateway_widgets.gateway_status", - "type": "latest", - "sizeX": 7.5, - "sizeY": 3, - "config": { - "datasources": [ - { - "type": "entity", - "name": "function", - "entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8", - "dataKeys": [ - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#2196f3", - "settings": {}, - "_hash": 0.07932656697286933 - }, - { - "name": "lastDisconnectTime", - "type": "attribute", - "label": "lastDisconnectTime", - "color": "#4caf50", - "settings": {}, - "_hash": 0.06662431625551446 - }, - { - "name": "lastConnectTime", - "type": "attribute", - "label": "lastConnectTime", - "color": "#f44336", - "settings": {}, - "_hash": 0.28485659401606434 - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1733240126084, - "endTimeMs": 1733326526084 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "rgb(255, 255, 255)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "", - "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, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "enableFullscreen": false, - "margin": "16px 16px 16px 0", - "borderRadius": "5px", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "enableDataExport": false - }, - "row": 0, - "col": 0, - "id": "e48e7a66-a30e-2494-b0c0-dd3249944286" - }, - "3a1f2fe6-7da9-15aa-777d-383e0ac01520": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": "", - "entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8", - "dataKeys": [], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1732109250067, - "endTimeMs": 1732195650067 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "let buttonsHtml = \"\" \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n let disabled = false;\n if (index == 2) {\n disabled = data[0] && data[0].RemoteLoggingLevel ? data[0].RemoteLoggingLevel == \"NONE\" : true;\n } else if (index == 4) {\n const conf = data[0].general_configuration ? JSON.parse(data[0].general_configuration) : {};\n disabled = !conf.remoteShell;\n }\n buttonsHtml += ``\n});\n\nreturn `
${buttonsHtml}
`;", - "markdownTextFunction": "const states = ['storage_statistics', 'machine_statistics', 'custom_statistics'];\nconst currentState = ctx.stateController.getStateId();\nlet buttonsHtml = \"\";\nconst buttonName = \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n buttonsHtml += ``\n});\n\nreturn `
${buttonsHtml}
`;", - "applyDefaultMarkdownStyle": false, - "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: start;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n line-height: 36px;\r\n}" - }, - "title": "Markdown/HTML Card", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "actions": { - "elementClick": [ - { - "name": "Storage", - "icon": "more_horiz", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "storage_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "fc4c338d-5799-f7dc-9ecb-448e65f8045d" - }, - { - "name": "Machine", - "icon": "more_horiz", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "machine_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "b35fc758-b21a-f5d3-e968-3572f1bb9ad4" - }, - { - "name": "Custom", - "icon": "more_horiz", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", - "targetDashboardStateId": "custom_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "fcff099d-8164-08fd-7ffa-e1565772f368" - } - ] + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 }, - "borderRadius": "5px", + "showLegend": false, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "", - "margin": "16px 16px 0 0", "enableDataExport": false }, "row": 0, "col": 0, - "id": "3a1f2fe6-7da9-15aa-777d-383e0ac01520" + "id": "867613d5-9440-fac1-9156-cd8773df5c97" }, "7a34986a-ec7f-4c64-84f9-338cf1626722": { "typeFullFqn": "system.gateway_widgets.gateway_custom_statistics", @@ -8436,11 +6845,6 @@ } } ], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -8485,7 +6889,6 @@ }, "mobileHeight": null, "useDashboardTimewindow": true, - "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "margin": "16px", @@ -8499,6 +6902,146 @@ "row": 0, "col": 0, "id": "7a34986a-ec7f-4c64-84f9-338cf1626722" + }, + "699b8980-7180-3600-ec29-6af50325a2e1": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "showTitle": true, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "4px", + "settings": { + "entitiesTitle": "Devices", + "enableSearch": true, + "enableSelectColumnDisplay": true, + "enableStickyHeader": true, + "enableStickyAction": true, + "reserveSpaceForHiddenAction": "true", + "displayEntityName": true, + "entityNameColumnTitle": "Device Name", + "displayEntityLabel": false, + "displayEntityType": false, + "displayPagination": true, + "defaultPageSize": 10, + "defaultSortOrder": "entityName", + "useRowStyleFunction": false + }, + "title": "Devices", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400, + "padding": "5px 10px 5px 10px" + }, + "showLegend": false, + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4", + "filterId": "774c2598-a4fe-e813-87aa-bf00b78043a3", + "dataKeys": [ + { + "name": "type", + "type": "entityField", + "label": "Device Type", + "color": "#2196f3", + "settings": {}, + "_hash": 0.3129929097366162, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "active", + "type": "attribute", + "label": "Status", + "color": "#4caf50", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "useCellContentFunction": true, + "cellContentFunction": "let cssClass;\r\nswitch (value) {\r\n case \"Active\":\r\n cssClass = \"status status-active\";\r\n break;\r\n case \"Inactive\":\r\n default:\r\n cssClass = \"status status-inactive\";\r\n break;\r\n }\r\n \r\n return `${value}`;", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.5969880627410065, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value == 'true' ? \"Active\": \"Inactive\";" + }, + { + "name": "connectorName", + "type": "attribute", + "label": "Connector Name", + "color": "#f44336", + "settings": {}, + "_hash": 0.012483045440007778, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "connectorType", + "type": "attribute", + "label": "Connector Type", + "color": "#ffc107", + "settings": {}, + "_hash": 0.6004192233378134, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "89e0155f-c2c5-8d17-324c-280f8e11ffab" + } + ] + } + }, + "row": 0, + "col": 0, + "id": "699b8980-7180-3600-ec29-6af50325a2e1", + "typeFullFqn": "system.cards.entities_table" } }, "states": { @@ -8886,7 +7429,7 @@ "mobileOrder": 2, "mobileHeight": null }, - "56274c38-33fc-fe9d-aa39-ff3e1215276b": { + "b4194e1e-97af-ed0b-a6a4-70011c71c579": { "sizeX": 5, "sizeY": 12, "resizable": true, @@ -8895,7 +7438,7 @@ "mobileOrder": 1, "mobileHeight": 3 }, - "38063fe5-1bd8-695b-a40b-92c1aed19831": { + "bc199c75-00da-ca8a-dd0d-16da97a9fa53": { "sizeX": 5, "sizeY": 2, "row": 12, @@ -9521,31 +8064,40 @@ } } }, + "gateway_devices_knx": { + "name": "gateway_devices_knx", + "root": false, + "layouts": { + "main": { + "widgets": { + "699b8980-7180-3600-ec29-6af50325a2e1": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 24, + "margin": 0, + "outerMargin": true, + "backgroundSizeMode": "100%", + "autoFillHeight": false, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "layoutType": "default" + } + } + } + }, "custom_statistics": { "name": "Custom statistics", "root": false, "layouts": { "main": { "widgets": { - "e48e7a66-a30e-2494-b0c0-dd3249944286": { - "sizeX": 4, - "sizeY": 2, - "preserveAspectRatio": false, - "resizable": true, - "row": 12, - "col": 20, - "mobileOrder": 3, - "mobileHeight": 2 - }, - "3a1f2fe6-7da9-15aa-777d-383e0ac01520": { - "sizeX": 4, - "sizeY": 12, - "resizable": true, - "row": 0, - "col": 20, - "mobileOrder": 1, - "mobileHeight": 3 - }, "7a34986a-ec7f-4c64-84f9-338cf1626722": { "sizeX": 20, "sizeY": 14, @@ -9554,6 +8106,21 @@ "col": 0, "resizable": true, "mobileOrder": 2 + }, + "b4194e1e-97af-ed0b-a6a4-70011c71c579": { + "sizeX": 4, + "sizeY": 12, + "resizable": true, + "row": 0, + "col": 20 + }, + "bc199c75-00da-ca8a-dd0d-16da97a9fa53": { + "sizeX": 4, + "sizeY": 2, + "preserveAspectRatio": false, + "resizable": true, + "row": 12, + "col": 20 } }, "gridSettings": { @@ -9573,6 +8140,32 @@ } } } + }, + "gateway_status": { + "name": "gateway_status", + "root": false, + "layouts": { + "main": { + "widgets": { + "bc199c75-00da-ca8a-dd0d-16da97a9fa53": { + "sizeX": 5, + "sizeY": 3, + "preserveAspectRatio": false, + "resizable": true, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "layoutType": "default", + "backgroundColor": "#eeeeee", + "columns": 24, + "margin": 10, + "outerMargin": true, + "backgroundSizeMode": "100%" + } + } + } } }, "entityAliases": { @@ -9992,6 +8585,39 @@ ], "editable": true }, + "774c2598-a4fe-e813-87aa-bf00b78043a3": { + "id": "774c2598-a4fe-e813-87aa-bf00b78043a3", + "filter": "gateway_devices_knx", + "keyFilters": [ + { + "key": { + "type": "ATTRIBUTE", + "key": "connectorType" + }, + "valueType": "STRING", + "predicates": [ + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "knx", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": true, + "label": "", + "autogeneratedLabel": true, + "order": 0 + } + } + ] + } + ], + "editable": true + }, "92a7d208-c143-ea20-5162-1da584532830": { "id": "92a7d208-c143-ea20-5162-1da584532830", "filter": "gateway_devices_bacnet", diff --git a/application/src/main/data/resources/js_extensions/gateway-management-extension.js b/application/src/main/data/resources/js_extensions/gateway-management-extension.js new file mode 100644 index 0000000000..1c76094959 --- /dev/null +++ b/application/src/main/data/resources/js_extensions/gateway-management-extension.js @@ -0,0 +1,17 @@ +System.register(["@angular/core","@angular/material/sort","@angular/material/table","@angular/material/paginator","@shared/public-api","@core/public-api","@angular/common","@ngx-translate/core","@angular/forms","rxjs","rxjs/operators","@angular/material/dialog","tslib","@angular/material/tooltip","@angular/cdk/collections","@ngrx/store","@angular/router","@home/components/public-api","@angular/platform-browser","@home/components/widget/widget.component","@shared/components/dialog/json-object-edit-dialog.component","@shared/import-export/import-export.service","@shared/components/popover.component","@shared/components/popover.service","@shared/decorators/coercion","@angular/material/core"],(function(e){"use strict";var t,n,i,a,r,o,s,l,p,c,d,u,m,h,g,f,y,v,x,b,w,S,C,_,T,I,E,M,k,P,D,O,A,F,R,B,N,L,V,q,G,z,U,j,H,W,$,K,Y,X,Z,Q,J,ee,te,ne,ie,ae,re,oe,se,le,pe,ce,de,ue,me,he,ge,fe,ye,ve,xe,be,we,Se,Ce,_e,Te,Ie,Ee,Me,ke,Pe,De,Oe,Ae,Fe,Re,Be,Ne,Le,Ve,qe,Ge,ze,Ue,je,He,We,$e,Ke,Ye,Xe,Ze,Qe,Je,et,tt,nt,it,at,rt,ot,st;return{setters:[function(e){t=e,n=e.assertInInjectionContext,i=e.inject,a=e.DestroyRef,e.ɵRuntimeError,e.ɵgetOutputDestroyRef,r=e.Injector,o=e.effect,s=e.untracked,e.assertNotInReactiveContext,e.signal,e.computed,l=e.input,p=e.output,c=e.forwardRef,d=e.ChangeDetectionStrategy,u=e.EventEmitter,m=e.booleanAttribute,h=e.ChangeDetectorRef,g=e.SecurityContext,f=e.KeyValueDiffers,y=e.ɵNG_COMP_DEF},function(e){v=e.MatSort},function(e){x=e.MatTableDataSource},function(e){b=e.MatPaginator},function(e){w=e.Direction,S=e.PageLink,C=e.DataKeyType,_=e.SharedModule,T=e,I=e.coerceBoolean,E=e.emptyPageData,M=e.isClientSideTelemetryType,k=e.TelemetrySubscriber,P=e.coerceNumber,D=e.AttributeScope,O=e.helpBaseUrl,A=e.DialogComponent,F=e.defaultLegendConfig,R=e.widgetType,B=e.NULL_UUID,N=e.DatasourceType,L=e.EntityType,V=e.ContentType,q=e.PageComponent,G=e.TbTableDatasource,z=e.HOUR,U=e.DeviceCredentialsType},function(e){j=e.mergeDeep,H=e.isLiteralObject,W=e.isEqual,$=e.WINDOW,K=e,Y=e.deleteNullProperties,X=e.DialogService,Z=e.isNumber,Q=e.isString,J=e.isDefinedAndNotNull,ee=e.formatValue,te=e.deepClone,ne=e.isUndefinedOrNull,ie=e.isObject,ae=e.generateSecret,re=e.camelCase,oe=e.deepTrim},function(e){se=e.CommonModule},function(e){le=e,pe=e.TranslateService,ce=e.TranslateModule},function(e){de=e,ue=e.NG_VALUE_ACCESSOR,me=e.Validators,he=e.NG_VALIDATORS,ge=e.FormBuilder,fe=e.FormControl},function(e){ye=e.Observable,ve=e.ReplaySubject,xe=e.shareReplay,be=e.combineLatest,we=e.Subject,Se=e.fromEvent,Ce=e.BehaviorSubject,_e=e.of,Te=e.zip,Ie=e.forkJoin,Ee=e.merge},function(e){Me=e.takeUntil,ke=e.map,Pe=e.distinctUntilChanged,De=e.debounceTime,Oe=e.filter,Ae=e.tap,Fe=e.catchError,Re=e.publishReplay,Be=e.refCount,Ne=e.take,Le=e.takeWhile,Ve=e.switchMap,qe=e.startWith,Ge=e.mergeMap},function(e){ze=e.MatDialog,Ue=e.MAT_DIALOG_DATA,je=e},function(e){He=e.__decorate,We=e.__extends},function(e){$e=e,Ke=e.MatTooltip},function(e){Ye=e.SelectionModel},function(e){Xe=e},function(e){Ze=e},function(e){Qe=e.calculateAxisSize,Je=e.measureAxisNameSize},function(e){et=e},function(e){tt=e},function(e){nt=e.JsonObjectEditDialogComponent},function(e){it=e},function(e){at=e},function(e){rt=e},function(e){ot=e.coerceBoolean},function(e){st=e.ErrorStateMatcher}],execute:function(){e("getDefaultConfig",Xt);const lt=e("jsonRequired",(e=>e.value?null:{required:!0}));var pt,ct,dt;e("GatewayLogLevel",pt),function(e){e.NONE="NONE",e.CRITICAL="CRITICAL",e.ERROR="ERROR",e.WARNING="WARNING",e.INFO="INFO",e.DEBUG="DEBUG",e.TRACE="TRACE"}(pt||e("GatewayLogLevel",pt={})),e("GatewayVersion",ct),function(e){e.v3_7_3="3.7.3",e.v3_7_2="3.7.2",e.v3_7_0="3.7",e.v3_6_3="3.6.3",e.v3_6_2="3.6.2",e.v3_6_0="3.6",e.v3_5_2="3.5.2",e.Legacy="legacy"}(ct||e("GatewayVersion",ct={})),e("ConnectorType",dt),function(e){e.MQTT="mqtt",e.MODBUS="modbus",e.GRPC="grpc",e.OPCUA="opcua",e.BLE="ble",e.REQUEST="request",e.CAN="can",e.BACNET="bacnet",e.ODBC="odbc",e.REST="rest",e.SNMP="snmp",e.FTP="ftp",e.SOCKET="socket",e.XMPP="xmpp",e.OCPP="ocpp",e.CUSTOM="custom",e.KNX="knx"}(dt||e("ConnectorType",dt={}));const ut=e("GatewayConnectorDefaultTypesTranslatesMap",new Map([[dt.MQTT,"MQTT"],[dt.MODBUS,"MODBUS"],[dt.GRPC,"GRPC"],[dt.OPCUA,"OPCUA"],[dt.BLE,"BLE"],[dt.REQUEST,"REQUEST"],[dt.CAN,"CAN"],[dt.BACNET,"BACNET"],[dt.ODBC,"ODBC"],[dt.REST,"REST"],[dt.SNMP,"SNMP"],[dt.FTP,"FTP"],[dt.SOCKET,"SOCKET"],[dt.XMPP,"XMPP"],[dt.OCPP,"OCPP"],[dt.CUSTOM,"CUSTOM"],[dt.KNX,"KNX"]])),mt=e("ConnectorsTypesByVersion",new Map([[ct.v3_7_0,Object.values(dt)],[ct.Legacy,Object.values(dt).filter((e=>e!==dt.KNX))]]));var ht,gt;e("SocketEncoding",ht),function(e){e.UTF8="utf-8",e.HEX="hex",e.UTF16="utf-16",e.UTF32="utf-32",e.UTF16BE="utf-16-be",e.UTF16LE="utf-16-le",e.UTF32BE="utf-32-be",e.UTF32LE="utf-32-le"}(ht||e("SocketEncoding",ht={})),e("HTTPMethods",gt),function(e){e.DELETE="DELETE",e.GET="GET",e.HEAD="HEAD",e.OPTIONS="OPTIONS",e.PATCH="PATCH",e.POST="POST",e.PUT="PUT"}(gt||e("HTTPMethods",gt={}));var ft,yt={gateway:{active:"Active",address:"Address","address-required":"Address required","add-entry":"Add configuration","add-attribute":"Add attribute","add-attribute-update":"Add attribute update","add-attribute-request":"Add attribute request","add-key":"Add key","add-timeseries":"Add time series","add-mapping":"Add mapping","add-slave":"Add Slave",arguments:"Arguments","add-rpc-method":"Add method","add-rpc-request":"Add request","add-value":"Add argument","advanced-settings":"Advanced settings",application:"Application",bacnet:{"device-timeout":"Discovery timeout","network-number":"Network number","alt-responses-address":"Alternative responses address","apdu-length":"APDU Length","object-name":"Object Name","object-type":{"analog-input":"Analog Input","analog-output":"Analog Output","analog-value":"Analog Value","binary-input":"Binary Input","binary-output":"Binary Output","binary-value":"Binary Value"},"request-type":{label:"Request Type",write:"Write Property",read:"Read Property"},"property-id":{"present-value":"Present Value","status-flags":"Status Flags","cov-increment":"COV Increment","event-state":"Event State","out-of-service":"Out of Service",polarity:"Polarity","priority-array":"Priority Array","relinquish-default":"Relinquish Default","current-command-priority":"Current Command Priority","event-message-texts":"Event Message Texts","event-message-texts-config":"Event Message Texts Config","event-algorithm-inhibit-reference":"Event Algorithm Inhibit Reference","time-delay-normal":"Time Delay Normal"},segmentation:{label:"Segmentation",no:"None",both:"Both",transmit:"Transmit",receive:"Receive"}},baudrate:"Baudrate","buffer-size":"Buffer size","buffer-size-required":"Buffer size is required","buffer-size-range":"Buffer size should be greater than 0",bytesize:"Bytesize",boolean:"Boolean",bit:"Bit","bit-target-type":"Bit target type","delete-value":"Delete value","delete-argument":"Delete argument","delete-rpc-method":"Delete method","delete-rpc-request":"Delete request","delete-attribute-update":"Delete attribute update","delete-attribute-request":"Delete attribute request",advanced:"Advanced","add-device":"Add device","address-filter":"Address filter","address-filter-required":"Address filter is required","advanced-connection-settings":"Advanced connection settings","advanced-configuration-settings":"Advanced configuration settings",attributes:"Attributes","attribute-updates":"Attribute updates","attribute-on-platform":"Attribute on platform","attribute-requests":"Attribute requests","attribute-filter":"Attribute filter","attribute-filter-hint":"Filter for incoming attribute name from platform, supports regular expression.","attribute-filter-required":"Attribute filter required.","attribute-name-expression":"Attribute name expression","attribute-name-expression-required":"Attribute name expression required.","attribute-name-expression-hint":"Hint for Attribute name expression",basic:"Basic","byte-order":"Byte order","word-order":"Word order",broker:{connection:"Connection to broker",name:"Broker name","name-required":"Broker name required.","security-types":{anonymous:"Anonymous",basic:"Basic",certificates:"Certificates"}},certificate:"Certificate","CA-certificate-path":"Path to CA certificate file","path-to-CA-cert-required":"Path to CA certificate file is required.","change-connector-title":"Confirm connector change","change-connector-text":"Switching connectors will discard any unsaved changes. Continue?","checking-device-activity":"Checking device activity",command:"Docker commands","command-copied-message":"Docker command has been copied to clipboard",configuration:"Configuration","add-connector":"Add connector","delete-connector":"Delete connector","connector-add":"Add new connector","connector-enabled":"Enable connector","connector-name":"Connector name",connector:"Connector",converter:"Converter","connector-name-required":"Connector name is required.","connector-type":"Connector type","connector-type-required":"Connector type is required.",connectors:"Connectors","connectors-config":"Connectors configuration","connectors-table-enabled":"Enabled","connectors-table-name":"Name","connectors-table-type":"Type","connectors-table-status":"Status","connectors-table-actions":"Actions","connectors-table-key":"Key","connectors-table-class":"Class","connection-timeout":"Connection timeout (s)","connect-attempt-time":"Connect attempt time (ms)","connect-attempt-count":"Connect attempt count","copy-username":"Copy username","copy-password":"Copy password","copy-client-id":"Copy client ID","connector-created":"Connector created","connector-updated":"Connector updated","create-new-one":"Create new one!","rpc-command-save-template":"Save Template","rpc-command-send":"Send","rpc-command-result":"Response","rpc-command-edit-params":"Edit parameters","gateway-configuration":"General Configuration","docker-label":"Use the following instruction to run IoT Gateway in Docker compose with credentials for selected device","install-docker-compose":"Use the instructions to download, install and setup docker compose",integer:"Integer",inactive:"Inactive",device:"Device",devices:"Devices","device-profile":"Device profile","device-info-settings":"Device info settings","device-info":{"entity-field":"Entity field",source:"Source",expression:"Value / Expression","expression-hint":"Show help",name:"Name","profile-name":"Profile name","device-name-expression":"Device name expression","device-name-expression-required":"Device name expression is required.","device-profile-expression-required":"Device profile expression is required."},"device-name-filter":"Device name filter","device-name-filter-hint":"This field supports regular expressions to filter incoming data by device name.","device-name-filter-required":"Device name filter is required.",details:"Details","delete-mapping-title":"Are you sure you want to delete the mapping related with '{{name}}'?","delete-mapping-description":"Be careful, after the confirmation mapping configuration and all related data will become unrecoverable.","delete-slave-title":"Are you sure you want to delete the server '{{name}}'?","delete-slave-description":"Be careful, after the confirmation server configuration and all related data will become unrecoverable.","delete-device-title":"Are you sure you want to delete the device '{{name}}'?","delete-device-description":"Be careful, after the confirmation the device configuration and all related data will become unrecoverable.",divider:"Divider","download-configuration-file":"Download configuration file","download-docker-compose":"Download docker-compose.yml for your gateway","enable-remote-logging":"Enable remote logging","ellipsis-chips-text":"+ {{count}} more","launch-gateway":"Launch gateway","launch-docker-compose":"Start the gateway using the following command in the terminal from folder with docker-compose.yml file","logs-configuration":"Logs configuration","create-new-gateway":"Create a new gateway","create-new-gateway-text":"Are you sure you want create a new gateway with name: '{{gatewayName}}'?","created-time":"Created time","configuration-delete-dialog-header":"Configurations will be deleted","configuration-delete-dialog-body":"Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below","configuration-delete-dialog-input":"Gateway name","configuration-delete-dialog-input-required":"Gateway name is mandatory","configuration-delete-dialog-confirm":"Turn Off","connector-duplicate-name":"Connector with such name already exists.","connection-type":"Connection type","connector-side":"Connector side","payload-type":"Payload type","platform-side":"Platform side",JSON:"JSON","JSON-hint":"Converter for this payload type processes MQTT messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the message. And regular expressions to get device details from topics.",byte:"Byte",bytes:"Bytes","bytes-hint":"Converter for this payload type designed for binary MQTT payloads, this converter directly interprets binary data to retrieve device names and device profile names, along with attributes and time series, using specific byte positions for data extraction.",custom:"Custom","custom-hint":"This option allows you to use a custom converter for specific data tasks. You need to add your custom converter to the extension folder and enter its class name in the UI settings. Any keys you provide will be sent as configuration to your custom converter.","client-cert-path":"Path to client certificate file","path-to-client-cert-required":"Path to client certificate file is required.","client-id":"Client ID","data-conversion":"Data conversion","data-mapping":"Data mapping","data-mapping-hint":"Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and time series data keys.","opcua-data-mapping-hint":"Data mapping provides the capability to parse and convert the data received from a OPCUA server into specific data keys.",delete:"Delete configuration","delete-attribute":"Delete attribute","delete-key":"Delete key","delete-timeseries":"Delete time series",default:"Default","device-node":"Device node","device-node-required":"Device node required.","device-node-hint":"Path or identifier for device node on OPC UA server. Relative paths from it for attributes and time series can be used.","device-name":"Device name","device-profile-label":"Device profile","device-name-required":"Device name required","device-profile-required":"Device profile required","download-tip":"Download configuration file","drop-file":"Drop file here or",enable:"Enable",encoding:"Encoding","enable-subscription":"Enable subscription",extension:"Extension","extension-hint":"Put your converter classname in the field. Custom converter with such class should be in extension/mqtt folder.","extension-required":"Extension is required.","extension-configuration":"Extension configuration","extension-configuration-hint":"Configuration for converter. These arguments will be passed as a config to convert function.","fill-connector-defaults":"Fill configuration with default values","fill-connector-defaults-hint":"This property allows to fill connector configuration with default values on it's creation.","from-device-request-settings":"Input request parsing","from-device-request-settings-hint":"These fields support JSONPath expressions to extract a name from incoming message.","function-code":"Function code","function-codes":{"read-coils":"01 - Read Coils","read-discrete-inputs":"02 - Read Discrete Inputs","read-multiple-holding-registers":"03 - Read Multiple Holding Registers","read-input-registers":"04 - Read Input Registers","write-single-coil":"05 - Write Single Coil","write-single-holding-register":"06 - Write Single Holding Register","write-multiple-coils":"15 - Write Multiple Coils","write-multiple-holding-registers":"16 - Write Multiple Holding Registers"},"to-device-response-settings":"Output request processing","to-device-response-settings-hint":"For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",gateway:"Gateway","gateway-exists":"Device with same name is already exists.","gateway-status":"Gateway status","gateway-name":"Gateway name","gateway-name-required":"Gateway name is required.","gateway-saved":"Gateway configuration successfully saved.","generate-client-id":"Generate Client ID",grpc:"GRPC","grpc-keep-alive-timeout":"Keep alive timeout (in ms)","grpc-keep-alive-timeout-required":"Keep alive timeout is required","grpc-keep-alive-timeout-min":"Keep alive timeout can not be less then 1","grpc-keep-alive-timeout-pattern":"Keep alive timeout is not valid","grpc-keep-alive":"Keep alive (in ms)","grpc-keep-alive-required":"Keep alive is required","grpc-keep-alive-min":"Keep alive can not be less then 1","grpc-keep-alive-pattern":"Keep alive is not valid","grpc-min-time-between-pings":"Min time between pings (in ms)","grpc-min-time-between-pings-required":"Min time between pings is required","grpc-min-time-between-pings-min":"Min time between pings can not be less then 1","grpc-min-time-between-pings-pattern":"Min time between pings is not valid","grpc-min-ping-interval-without-data":"Min ping interval without data (in ms)","grpc-min-ping-interval-without-data-required":"Min ping interval without data is required","grpc-min-ping-interval-without-data-min":"Min ping interval without data can not be less then 1","grpc-min-ping-interval-without-data-pattern":"Min ping interval without data is not valid","grpc-max-pings-without-data":"Max pings without data","grpc-max-pings-without-data-required":"Max pings without data is required","grpc-max-pings-without-data-min":"Max pings without data can not be less then 1","grpc-max-pings-without-data-pattern":"Max pings without data is not valid",info:"Info",identity:"Identity","inactivity-check-period-seconds":"Inactivity check period (in sec)","inactivity-check-period-seconds-required":"Inactivity check period is required","inactivity-check-period-seconds-min":"Inactivity check period can not be less then 1","inactivity-check-period-seconds-pattern":"Inactivity check period is not valid","inactivity-timeout-seconds":"Inactivity timeout (in sec)","inactivity-timeout-seconds-required":"Inactivity timeout is required","inactivity-timeout-seconds-min":"Inactivity timeout can not be less then 1","inactivity-timeout-seconds-pattern":"Inactivity timeout is not valid","unit-id":"Unit ID",host:"Host","host-required":"Host is required.",holding_registers:"Holding registers",coils_initializer:"Coils initializer",input_registers:"Input registers",discrete_inputs:"Discrete inputs","json-parse":"Not valid JSON.","json-required":"Field cannot be empty.","JSONPath-hint":"Supports constants and JSONPath expressions to extract data.",logs:{logs:"Logs",days:"days",hours:"hours",minutes:"minutes",seconds:"seconds","date-format":"Date format","date-format-required":"Date format required","log-format":"Log format","log-type":"Log type","log-format-required":"Log format required",columns:{general:"General",service:"Service",connection:"Connection",storage:"Storage",extension:"Extension"},remote:"Remote logging","remote-logs":"Remote logs",local:"Local logging",level:"Log level","file-path":"File path","file-path-required":"File path required","saving-period":"Log saving period","saving-period-min":"Log saving period can not be less then 1","saving-period-required":"Log saving period required","backup-count":"Backup count","backup-count-min":"Backup count can not be less then 1","backup-count-required":"Backup count required"},"max-number-of-workers":"Max number of workers","max-number-of-workers-hint":"Maximal number of workers threads for converters \n(The amount of workers changes dynamically, depending on load) \nRecommended amount 50-150.","max-number-of-workers-required":"Max number of workers is required.","max-messages-queue-for-worker":"Max messages queue per worker","max-messages-queue-for-worker-hint":"Maximal messages count that will be in the queue \nfor each converter worker.","max-messages-queue-for-worker-required":"Max messages queue per worker is required.",method:"Method","method-name":"Method name","method-required":"Method name is required.","min-pack-send-delay":"Min pack send delay (in ms)","min-pack-send-delay-required":"Min pack send delay is required","min-pack-send-delay-min":"Min pack send delay can not be less then 10","min-pack-send-delay-pattern":"Min pack send delay is not valid",multiplier:"Multiplier",mode:"Mode","model-name":"Model name",modifier:"Modifier","modifier-invalid":"Modifier is not valid","mqtt-version":"MQTT version",name:"Name","name-required":"Name is required.","network-mask":"Network mask","no-attributes":"No attributes","no-attribute-updates":"No attribute updates","no-attribute-requests":"No attribute requests","no-connectors":"No connectors","no-data":"No configurations","no-gateway-found":"No gateway found.","no-gateway-matching":" '{{item}}' not found.","no-timeseries":"No time series","no-keys":"No keys","no-value":"No arguments","no-rpc-methods":"No RPC methods","no-rpc-requests":"No RPC requests","path-hint":"The path is local to the gateway file system","path-logs":"Path to log files","path-logs-required":"Path is required.",password:"Password","password-required":"Password is required.","permit-without-calls":"Keep alive permit without calls","property-id":"Property ID","poll-period":"Poll period (ms)","poll-period-error":"Poll period should be at least {{min}} (ms).",port:"Port","port-required":"Port is required.","port-limits-error":"Port should be number from {{min}} to {{max}}.","private-key-path":"Path to private key file","path-to-private-key-required":"Path to private key file is required.",parity:"Parity","product-code":"Product code","product-name":"Product name",raw:"Raw",retain:"Retain","retain-hint":"This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.",remote:"Remote configuration","remote-logging-level":"Logging level","remove-entry":"Remove configuration","remote-shell":"Remote shell","remote-configuration":"Remote Configuration","request-expression":"Request expression","request-expression-required":"Request expression is required",retries:"Retries","retries-on-empty":"Retries on empty","retries-on-invalid":"Retries on invalid",response:"Response",rest:{"add-http-header":"Add HTTP header","no-http-headers":"No HTTP headers","delete-http-header":"Delete HTTP header","ssl-verify":"SSL verify",endpoint:"Endpoint","http-methods":"HTTP methods","http-method":"HTTP method","on-error":"On error","on-success":"On success",tries:"Tries","http-headers":"HTTP headers","allow-redirects":"Allow redirects","request-url-expression":"Request URL expression","response-attribute":"Response attribute","response-expected":"Expected","response-type":{default:"Default",const:"Constant",advanced:"Advanced"}},rpc:{rpc:"RPC",title:"{{type}} Connector RPC parameters","templates-title":"Connector RPC Templates",methodFilter:"Method filter","method-name":"Method name",requestTopicExpression:"Request topic expression",responseTopicExpression:"Response topic expression",responseTimeout:"Response timeout",valueExpression:"Value expression",tag:"Tag",type:"Type",functionCode:"Function Code",objectsCount:"Objects Count",address:"Address",method:"Method",requestType:"Request Type",requestTimeout:"Request Timeout",objectType:"Object type",identifier:"Identifier",propertyId:"Property ID",methodRPC:"Method RPC name",withResponse:"With Response",characteristicUUID:"Characteristic UUID",methodProcessing:"Method Processing",nodeID:"Node ID",isExtendedID:"Is Extended ID",isFD:"Is FD",bitrateSwitch:"Bitrate Switch",dataInHEX:"Data In HEX",dataLength:"Data Length",dataByteorder:"Data Byte Order",dataBefore:"Data Before",dataAfter:"Data After",dataExpression:"Data Expression",oid:"OID","add-oid":"Add OID","add-header":"Add header","add-security":"Add security",remove:"Remove",requestFilter:"Request Filter",requestUrlExpression:"Request URL Expression",httpMethod:"HTTP Method",timeout:"Timeout",tries:"Tries",httpHeaders:"HTTP Headers","header-name":"Header name",hint:{"modbus-response-reading":"RPC response will return all subtracted values from all connected devices when the reading functions are selected.","modbus-writing-functions":"RPC will write a filled value to all connected devices when the writing functions are selected.","opc-method":"A filled method name is the OPC-UA method that will processed on the server side (make sure your node has the requested method)."},"security-name":"Security name",value:"Value",security:"Security",responseValueExpression:"Response Value Expression",requestValueExpression:"Request Value Expression",arguments:"Arguments","add-argument":"Add argument","write-property":"Write property","read-property":"Read property","analog-output":"Analog output","analog-input":"Analog input","binary-output":"Binary output","binary-input":"Binary input","binary-value":"Binary value","analog-value":"Analog value",write:"Write",read:"Read",scan:"Scan",oids:"OIDS",set:"Set",multiset:"Multiset",get:"Get","bulk-walk":"Bulk walk",table:"Table","multi-get":"Multiget","get-next":"Get next","bulk-get":"Bulk get",walk:"Walk","save-template":"Save template","template-name":"Template name","template-name-required":"Template name is required.","template-name-duplicate":"Template with such name already exists, it will be updated.",command:"Command",params:"Params","json-value-invalid":"JSON value has an invalid format"},"rpc-methods":"RPC methods","rpc-requests":"RPC requests",request:{"connect-request":"Connect request","disconnect-request":"Disconnect request","attribute-request":"Attribute request","attribute-update":"Attribute update","rpc-connection":"RPC command"},"request-type":"Request type","request-timeout":"Request timeout (ms)","requests-mapping":"Requests mapping","requests-mapping-hint":"MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.","request-topic-expression":"Request topic expression","request-client-certificate":"Request client certificate","request-topic-expression-required":"Request topic expression is required.","response-timeout":"Response timeout","response-timeout-required":"Response timeout is required.","response-timeout-limits-error":"Timeout must be more then {{min}} ms.","response-topic-Qos":"Response topic QoS","response-topic-Qos-hint":"MQTT Quality of Service (QoS) is an agreement between the message sender and receiver that defines the level of delivery guarantee for a specific message.","response-topic-expression":"Response topic expression","response-topic-expression-required":"Response topic expression is required.","response-value-expression":"Response value expression","response-value-expression-required":"Response value expression is required.","vendor-name":"Vendor name","vendor-url":"Vendor URL",value:"Value",values:"Values","value-required":"Value is required.","value-expression":"Value expression","value-expression-required":"Value expression is required.","with-response":"With response","without-response":"Without response",other:"Other",socket:"Socket","save-tip":"Save configuration file","scan-period":"Scan period (ms)","scan-period-error":"Scan period should be at least {{min}} (ms).","sub-check-period":"Subscription check period (ms)","sub-check-period-error":"Subscription check period should be at least {{min}} (ms).",security:"Security","security-policy":"Security policy","security-type":"Security type","security-types":{"access-token":"Access Token","username-password":"Username and Password",tls:"TLS","tls-access-token":"TLS + Access Token","tls-private-key":"TLS + Private Key"},"select-connector":"Select connector to display config","send-change-data":"Send data only on change","send-data-to-platform":"Send data to platform","send-data-on-change":"Send data only on change","send-change-data-hint":"The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",server:"Server","server-hostname":"Server hostname","server-slave":"Server (Slave)","servers-slaves":"Servers (Slaves)","server-port":"Server port","server-url":"Server endpoint url","server-connection":"Server Connection","server-config":"Server configuration","server-slave-config":"Server (Slave) configuration","server-url-required":"Server endpoint url is required.",stopbits:"Stopbits",strict:"Strict",set:"Set","show-map":"Show map",statistics:{entry:"Statistic entry","custom-send-period":"Custom send period (in sec)","custom-send-period-pattern":"Custom send period is not valid","custom-send-period-min":"Custom send period can not be less then 60","custom-send-period-required":"Custom send period is required","create-command":"Create command",attributes:"Attributes","name-already-exists":"Attribute name already exists.",telemetry:"Telemetry","storage-message-count":"Storage message count","messages-from-platform":"Messages from platform","pushed-datapoints":"Pushed datapoints","messages-pulled-from-storage":"Messages pulled from storage","messages-pushed-to-platform":"Messages pushed to platform","messages-sent-to-platform":"Messages sent to platform","process-cpu-usage":"Gateway process CPU usage",memory:"Gateway process memory usage","machine-resources":"Machine resources","free-disk":"Free disk",statistic:"Statistic",statistics:"Statistics","general-statistics":"General statistics","custom-statistics":"Custom statistics","copy-message":"Copy message","statistic-commands-empty":'No configured statistic keys found. You can configure them in "Statistics" tab in general configuration.',"statistics-button":"Go to configuration",commands:"Commands",name:"Time series name","time-series-name-already-exists":"Time series name already exists.","send-period":"Statistic send period (in sec)","send-period-required":"Statistic send period is required","send-period-min":"Statistic send period can not be less then 60","send-period-pattern":"Statistic send period is not valid","check-connectors-configuration":"Check connectors configuration (in sec)",messages:"Messages","max-payload-size-bytes":"Max payload size in bytes","max-payload-size-bytes-required":"Max payload size in bytes is required","max-payload-size-bytes-min":"Max payload size in bytes can not be less then 100","max-payload-size-bytes-pattern":"Max payload size in bytes is not valid","min-pack-size-to-send":"Min packet size to send","min-pack-size-to-send-required":"Min packet size to send is required","min-pack-size-to-send-min":"Min packet size to send can not be less then 100","min-pack-size-to-send-pattern":"Min packet size to send is not valid","no-config-commands-found":"No configuration commands found","delete-command":"Delete command '{{command}}'?","delete-command-data":"All command data will be deleted.","edit-command":"Edit command","change-command-title":"Discard command change","change-command-text":"Cancelling command edit will discard any unsaved changes. Continue?","no-command-found":"No command found","no-commands-matching":"No command matching '{{command}}' were found.","check-connectors-configuration-required":"Check connectors configuration is required","check-connectors-configuration-min":"Check connectors configuration can not be less then 1","check-connectors-configuration-pattern":"Check connectors configuration is not valid","install-cmd":"Install command",add:"Add command",timeout:"Timeout (in sec)","timeout-required":"Timeout is required","timeout-min":"Timeout can not be less then 1","timeout-pattern":"Timeout is not valid","time-series-name-required":"Time series name is required","time-series-name-pattern":"Time series name is not valid",command:"Command","command-required":"Command is required","command-pattern":"Command is not valid",remove:"Remove command"},storage:"Storage","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-read-record-count":"Read record count in storage","storage-read-record-count-min":"Minimum number of records is 1.","storage-read-record-count-pattern":"Number is not valid.","storage-read-record-count-required":"Read record count is required.","storage-max-read-record-count-storage":"Max read record count in storage","storage-max-read-record-count":"Max read record count","storage-max-read-record-count-min":"Minimum number of records is 1.","storage-max-read-record-count-pattern":"Number is not valid.","storage-max-read-record-count-required":"Max read record count is required.","storage-data-folder-path":"Data folder path","storage-data-folder-path-required":"Data folder path is required.","storage-data-folder-path-pattern":"Data folder path path path must start with './' and end with '/', without dots in folder names.","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-path-pattern":"Storage path path must start with './' and end with '/', without dots in folder names.","storage-type":"Storage type","storage-types":{"file-storage":"File storage","memory-storage":"Memory storage",sqlite:"SQLITE"},suffix:{s:"s",ms:"ms"},"report-strategy":{label:"Report strategy","on-change":"On value change","on-report-period":"On report period","on-change-or-report-period":"On value change or report period","report-period":"Report period","on-received":"On received"},"source-type":{msg:"Message",topic:"Topic",const:"Constant",identifier:"Identifier",path:"Path",expression:"Expression",request:"From request"},"workers-settings":"Workers settings",thingsboard:"ThingsBoard",general:"General",timeseries:"Time series",key:"Key",keys:"Keys","key-required":"Key is required.","thingsboard-host":"Platform host","thingsboard-host-required":"Host is required.","thingsboard-port":"Platform 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",timeout:"Timeout","timeout-error":"Timeout should be at least {{min}} (ms).","title-connectors-json":"Connector {{typeName}} configuration",type:"Type","topic-filter":"Topic filter","topic-required":"Topic filter is required.","tls-path-ca-certificate":"Path to CA certificate on gateway","tls-path-client-certificate":"Path to client certificate on gateway","tls-connection":"TLS Connection","master-connections":"Master Connections","method-filter":"Method filter","method-filter-hint":"Regular expression to filter incoming RPC method from platform.","method-filter-required":"Method filter is required.","messages-ttl-check-in-hours":"Messages TTL check in hours","messages-ttl-check-in-hours-required":"Messages TTL check in hours is required.","messages-ttl-check-in-hours-min":"Min number is 1.","messages-ttl-check-in-hours-pattern":"Number is not valid.","messages-ttl-in-days":"Messages TTL in days","messages-ttl-in-days-required":"Messages TTL in days is required.","messages-ttl-in-days-min":"Min number is 1.","messages-ttl-in-days-pattern":"Number is not valid.","size-limit":"Size limit","size-limit-hint":"Maximum size of each SQLite file in megabytes. When exceeded, triggers rotation to a new DB file.","size-limit-required":"Size limit is required.","size-limit-min":"Min size limit is 1.","size-limit-pattern":"Number is not valid.","max-db-amount":"Maximum DB amount","max-db-amount-hint":"Maximum number of rotated DB files to keep on disk. When reached, further writes are dropped until cleanup.","max-db-amount-required":"Maximum DB amount is required.","max-db-amount-min":"Min number is 1.","max-db-amount-pattern":"Number is not valid.","oversize-check-period":"Oversize check period","oversize-check-period-hint":"How frequently (in minutes) to check the current DB’s file size against size_limit.","oversize-check-period-required":"Oversize check period is required.","oversize-check-period-min":"Min number is 1.","oversize-check-period-pattern":"Number is not valid.","writing-batch-size":"Writing batch size","writing-batch-size-hint":"Maximum number of messages collected before writing to the DB during single write request.","writing-batch-size-required":"Writing batch size is required.","writing-batch-size-min":"Min number is 1.","writing-batch-size-pattern":"Number is not valid.","mqtt-qos":"QoS","mqtt-qos-required":"QoS is required","mqtt-qos-range":"QoS values range is from 0 to 1",qos:{"at-most-once":"0 - At most once","at-least-once":"1 - At least once","exactly-once":"2 - Exactly once"},"objects-count":"Objects count","object-id":"Object ID","objects-count-required":"Objects count is required","wait-after-failed-attempts":"Wait after failed attempts (ms)","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",username:"Username","username-required":"Username is required.","unit-id-required":"Unit ID is required.","vendor-id":"Vendor ID","write-coil":"Write Coil","write-coils":"Write Coils","write-register":"Write Register","write-registers":"Write Registers","general-configuration-widget":{"general-configuration":"General configuration","connectors-configuration":"Connectors configuration",logs:"Logs",statistics:"Statistics","remote-shell":"Remote Shell",rpc:"RPC"},"statistics-nav-widget":{"storage-statistics":"Storage","machine-statistics":"Machine","custom-statistics":"Custom"},"status-widget":{status:"Status","gateway-name":"Gateway name","gateway-version":"Gateway version","gateway-type":"Gateway type",devices:"Devices","active-inactive":"(Active | Inactive)",connectors:"Connectors","enabled-disabled":"(Enabled | Disabled)",errors:"Errors"},hints:{"enable-general-statistics":"Enables gateway statistics (machine, storage, connectors).","enable-custom-statistics":"Enables collecting statistics using custom commands.","buffer-size":"Buffer size for received data blocks.",encoding:"Encoding used for writing received string data to storage.",method:"Name for method on a platform.","modbus-master":"Configuration sections for connecting to Modbus servers and reading data from them.","modbus-server":"Configuration section for the Modbus server, storing data and sending updates to the platform when changes occur or at fixed intervals.","remote-configuration":"Enables remote configuration and management of the gateway","remote-shell":"Enables remote control of the operating system with the gateway from the Remote Shell widget",host:"Hostname or IP address of platform server",port:"Port of MQTT service on platform server",token:"Access token for the gateway from platform server","client-id":"MQTT client id for the gateway form platform server",username:"MQTT username for the gateway form platform server",password:"MQTT password for the gateway form platform server","ca-cert":"Path to CA certificate file","date-form":"Date format in log message","object-id-required":"Object ID is required","vendor-id-required":"Vendor ID is required","data-folder":"Path to the folder that will contain data (Relative or Absolute)","log-format":"Log message format","remote-log":"Enables remote logging and logs reading from the gateway","backup-count":"If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",storage:"Provides configuration for saving incoming data before it is sent to the platform","max-file-count":"Maximum number of files that will be created","max-read-count":"Number of messages to retrieve from the storage and send to platform","max-records":"Maximum count of records that will be stored in one file","read-record-count":"Number of messages to retrieve from the storage and send to platform","max-records-count":"Maximum number of data entries in storage before sending to platform","ttl-check-hour":"How often will the Gateway check data for obsolescence","ttl-messages-day":"Maximum number of days that the storage will retain data","username-required-with-password":"Username required if password is specified",commands:"Commands for collecting additional statistic",attribute:"Statistic telemetry key",timeout:"Timeout for command executing",command:"The result of the command execution, will be used as the value for telemetry","check-device-activity":"Enables monitor the activity of each connected device","inactivity-timeout":"Time after whose the gateway will disconnect device","inactivity-period":"Periodicity of device activity check","minimal-pack-delay":"Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)",qos:"Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)","server-port":"Network port on which GRPC server will listen for incoming connections.","grpc-keep-alive-timeout":"Maximum time the server should wait for a keepalive ping response before considering the connection dead.","grpc-keep-alive":"Duration between two successive keepalive ping messages when there is no active RPC call.","grpc-min-time-between-pings":"Minimum amount of time the server should wait between sending keepalive ping messages","grpc-max-pings-without-data":"Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.","grpc-min-ping-interval-without-data":"Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.","permit-without-calls":"Allow server to keep the GRPC connection alive even when there are no active RPC calls.","path-in-os":"Path in gateway os.",memory:"Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",file:"Your data will be stored in separated files and will be saved even after the gateway restart.",sqlite:"Your data will be stored in file based database. And will be saved even after the gateway restart.","opc-timeout":"Timeout in milliseconds for connecting to OPC-UA server.","security-policy":"Security Policy defines the security mechanisms to be applied.","install-cmd":"Packages that will be installed for command executing.","scan-period":"Period in milliseconds to rescan the server.","sub-check-period":"Period to check the subscriptions in the OPC-UA server.","enable-subscription":"If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInMillis.","show-map":"Show nodes on scanning.","method-name":"Name of method on OPC-UA server.",arguments:"Arguments for the method (will be overwritten by arguments from the RPC request).","min-pack-size-to-send":"Minimum package size for sending.","max-payload-size-bytes":"Maximum package size in bytes","poll-period":"Period in milliseconds to read data from nodes.","poll-period-required":"Poll period is required.","report-period-required":"Report period is required.","report-period-range":"Report period must be greater than 100.","timeout-pattern":"Timeout is not valid",rest:{"endpoint-required":"Endpoint is required.","endpoint-pattern":"Endpoint is not valid.","device-name-filter-required":"Device name filter is required.","device-name-filter-pattern":"Device name filter is not valid.","request-url-expression-required":"Request URL expression is required.","request-url-expression-pattern":"Request URL expression is not valid.","value-expression-required":"Value expression is required.","value-expression-pattern":"Value expression is not valid.","method-filter-required":"Method filter is required.","method-filter-pattern":"Method filter is not valid.","scope-type":"Attribute scope on platform.","tries-min":"Request tries number should be greater than 0.","tries-pattern":"Request tries is invalid.","http-methods-required":"HTTP methods are required.",host:"Domain or IP address of the server.",port:"Port on the server for listening clients connection.","ssl-verify":"Enable SSL certificate verification.",cert:"Path to the certificate file.",key:"Path to the private key file",endpoint:"Endpoint path on the server.","http-methods":"HTTP methods allowed for endpoint.","http-method":"Request to external URL HTTP method.",JSON:"Converter for this payload type processes REST messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the request body.",extension:"Custom converter classname. File with this class should be in extension/rest folder.","response-attribute-required":"Response attribute is required.","response-attribute-pattern":"Response attribute is not valid.","update-ssl":"Verify SSL certificate on the external server.","attribute-filter":"Filter for incoming attribute name from platform, support regular expression.","value-expression":"Used to create request body for request to external server. Supports ${deviceName}, ${deviceType}, ${attributeName}, ${attributeValue} variables.","attribute-url-expression":"Used to create request URL for request to external server. Supports ${deviceName}, ${deviceType}, ${attributeName}, ${attributeValue} variables.","method-filter":"Filter for RPC requests from the platform based on the method name. Supports regular expressions.","attribute-name-expression-required":"Attribute name expression is required.","attribute-name-expression-pattern":"Attribute name expression is not valid.","attribute-filter-required":"Attribute filter is required.","attribute-filter-pattern":"Attribute filter is not valid."},socket:{"attribute-on-platform-required":"Attribute on platform is required","attribute-requests-type":"The type of requested attribute can be “shared” or “client.“","with-response":"Boolean flag that specifies whether to send a response back to platform.","key-telemetry":"Name for telemetry on platform.","key-attribute":"Name for attribute on platform."},modbus:{"bit-target-type":"The response type can be either an integer (1/0) or a boolean (True/False).",bit:"Specify the index of the bit to read from the array, or leave it blank to read the entire array.","max-bit":"The bit value must not exceed the objects count.","framer-type":"Type of a framer (Socket, RTU, or ASCII), if needed.",host:"Hostname or IP address of Modbus server.",port:"Modbus server port for connection.","unit-id":"Modbus slave ID.","connection-timeout":"Connection timeout (in seconds) for the Modbus server.","byte-order":"Byte order for reading data.","word-order":"Word order when reading multiple registers.",retries:"Retrying sending data to the master.","retries-on-empty":"Retry sending data to the master if the data is empty.","retries-on-invalid":"Retry sending data to the master if it fails.","poll-period":"Period in milliseconds to check attributes and telemetry on the slave.","connect-attempt-time":"A waiting period in milliseconds before establishing a connection to the master.","connect-attempt-count":"The number of connection attempts made through the gateway.","wait-after-failed-attempts":"A waiting period in milliseconds before attempting to send data to the master.","serial-port":"Serial port for connection.",baudrate:"Baud rate for the serial device.",stopbits:"The number of stop bits sent after each character in a message to indicate the end of the byte.",bytesize:"The number of bits in a byte of serial data. This can be one of 5, 6, 7, or 8.",parity:"The type of checksum used to verify data integrity. Options: (E)ven, (O)dd, (N)one.",strict:"Use inter-character timeout for baudrates ≤ 19200.","objects-count":"Depends on the selected type.",address:"Register address to verify.",key:"Key to be used as the attribute key for the platform instance.",method:"Method name to be used for the platform instance.","data-keys":"For more information about function codes and data types click on help icon",modifier:"The retrieved value will be adjusted (by multiplying or dividing it) based on the specified modifier value."},bacnet:{"object-id":"The gateway object identifier in the BACnet network.","vendor-id":"The gateway vendor identifier in the BACnet network","apdu-length":"Maximal length of the APDU.",segmentation:"Segmentation type for transmitting large BACnet messages.","key-object-id":"Object id in the BACnet device.","property-id":"Property id in the BACnet device.","request-type":"“writeProperty” to write data and “readProperty” to read data.","request-timeout":"Timeout to wait the response from the BACnet device, milliseconds.","device-timeout":"Period of time when the connector will try to discover BACnet devices.","network-number":"Identifier of the network segment."}}}},vt={gateway:{"add-entry":"إضافة تكوين",advanced:"متقدم","checking-device-activity":"فحص نشاط الجهاز",command:"أوامر Docker","command-copied-message":"تم نسخ أمر Docker إلى الحافظة",configuration:"التكوين","connector-add":"إضافة موصل جديد","connector-enabled":"تمكين الموصل","connector-name":"اسم الموصل","connector-name-required":"اسم الموصل مطلوب.","connector-type":"نوع الموصل","connector-type-required":"نوع الموصل مطلوب.",connectors:"الموصلات","connectors-config":"تكوينات الموصلات","connectors-table-enabled":"ممكّن","connectors-table-name":"الاسم","connectors-table-type":"النوع","connectors-table-status":"الحالة","connectors-table-actions":"الإجراءات","connectors-table-key":"المفتاح","connectors-table-class":"الفئة","rpc-command-send":"إرسال","rpc-command-result":"الاستجابة","rpc-command-edit-params":"تحرير المعلمات","gateway-configuration":"تكوين عام","docker-label":"استخدم التعليمات التالية لتشغيل IoT Gateway في Docker compose مع بيانات اعتماد للجهاز المحدد","install-docker-compose":"استخدم التعليمات لتنزيل وتثبيت وإعداد docker compose","download-configuration-file":"تنزيل ملف التكوين","download-docker-compose":"تنزيل docker-compose.yml لبوابتك","launch-gateway":"تشغيل البوابة","launch-docker-compose":"بدء تشغيل البوابة باستخدام الأمر التالي في الطرفية من المجلد الذي يحتوي على ملف docker-compose.yml","create-new-gateway":"إنشاء بوابة جديدة","create-new-gateway-text":"هل أنت متأكد أنك تريد إنشاء بوابة جديدة باسم: '{{gatewayName}}'؟","created-time":"وقت الإنشاء","configuration-delete-dialog-header":"سيتم حذف التكوينات","configuration-delete-dialog-body":"يمكن تعطيل التكوين عن بُعد فقط إذا كان هناك وصول جسدي إلى البوابة. ستتم حذف جميع التكوينات السابقة.

\n لتعطيل التكوين، أدخل اسم البوابة أدناه","configuration-delete-dialog-input":"اسم البوابة","configuration-delete-dialog-input-required":"اسم البوابة إلزامي","configuration-delete-dialog-confirm":"إيقاف التشغيل",delete:"حذف التكوين","download-tip":"تنزيل ملف التكوين","drop-file":"أفلق الملف هنا أو",gateway:"البوابة","gateway-exists":"الجهاز بنفس الاسم موجود بالفعل.","gateway-name":"اسم البوابة","gateway-name-required":"اسم البوابة مطلوب.","gateway-saved":"تم حفظ تكوين البوابة بنجاح.",grpc:"GRPC","grpc-keep-alive-timeout":"مهلة البقاء على قيد الحياة (بالمللي ثانية)","grpc-keep-alive-timeout-required":"مهلة البقاء على قيد الحياة مطلوبة","grpc-keep-alive-timeout-min":"مهلة البقاء على قيد الحياة لا يمكن أن تكون أقل من 1","grpc-keep-alive-timeout-pattern":"مهلة البقاء على قيد الحياة غير صالحة","grpc-keep-alive":"البقاء على قيد الحياة (بالمللي ثانية)","grpc-keep-alive-required":"البقاء على قيد الحياة مطلوب","grpc-keep-alive-min":"البقاء على قيد الحياة لا يمكن أن يكون أقل من 1","grpc-keep-alive-pattern":"البقاء على قيد الحياة غير صالح","grpc-min-time-between-pings":"الحد الأدنى للوقت بين البينغات (بالمللي ثانية)","grpc-min-time-between-pings-required":"الحد الأدنى للوقت بين البينغات مطلوب","grpc-min-time-between-pings-min":"الحد الأدنى للوقت بين البينغات لا يمكن أن يكون أقل من 1","grpc-min-time-between-pings-pattern":"الحد الأدنى للوقت بين البينغات غير صالح","grpc-min-ping-interval-without-data":"الحد الأدنى لفاصل البينغ بدون بيانات (بالمللي ثانية)","grpc-min-ping-interval-without-data-required":"الحد الأدنى لفاصل البينغ بدون بيانات مطلوب","grpc-min-ping-interval-without-data-min":"الحد الأدنى لفاصل البينغ بدون بيانات لا يمكن أن يكون أقل من 1","grpc-min-ping-interval-without-data-pattern":"الحد الأدنى لفاصل البينغ بدون بيانات غير صالح","grpc-max-pings-without-data":"الحد الأقصى لعدد البينغات بدون بيانات","grpc-max-pings-without-data-required":"الحد الأقصى لعدد البينغات بدون بيانات مطلوب","grpc-max-pings-without-data-min":"الحد الأقصى لعدد البينغات بدون بيانات لا يمكن أن يكون أقل من 1","grpc-max-pings-without-data-pattern":"الحد الأقصى لعدد البينغات بدون بيانات غير صالح","inactivity-check-period-seconds":"فترة فحص الخمول (بالثواني)","inactivity-check-period-seconds-required":"فترة فحص الخمول مطلوبة","inactivity-check-period-seconds-min":"فترة فحص الخمول لا يمكن أن تكون أقل من 1","inactivity-check-period-seconds-pattern":"فترة فحص الخمول غير صالحة","inactivity-timeout-seconds":"فترة الخمول (بالثواني)","inactivity-timeout-seconds-required":"فترة الخمول مطلوبة","inactivity-timeout-seconds-min":"فترة الخمول لا يمكن أن تكون أقل من 1","inactivity-timeout-seconds-pattern":"فترة الخمول غير صالحة","json-parse":"JSON غير صالح.","json-required":"الحقل لا يمكن أن يكون فارغًا.",logs:{logs:"السجلات",days:"أيام",hours:"ساعات",minutes:"دقائق",seconds:"ثواني","date-format":"تنسيق التاريخ","date-format-required":"تنسيق التاريخ مطلوب","log-format":"تنسيق السجل","log-type":"نوع السجل","log-format-required":"تنسيق السجل مطلوب",remote:"التسجيل عن بُعد","remote-logs":"السجلات عن بُعد",local:"التسجيل المحلي",level:"مستوى السجل","file-path":"مسار الملف","file-path-required":"مسار الملف مطلوب","saving-period":"فترة حفظ السجل","saving-period-min":"فترة حفظ السجل لا يمكن أن تكون أقل من 1","saving-period-required":"فترة حفظ السجل مطلوبة","backup-count":"عدد النسخ الاحتياطية","backup-count-min":"عدد النسخ الاحتياطية لا يمكن أن يكون أقل من 1","backup-count-required":"عدد النسخ الاحتياطية مطلوب"},"min-pack-send-delay":"الحد الأدنى لتأخير إرسال الحزمة (بالمللي ثانية)","min-pack-send-delay-required":"الحد الأدنى لتأخير إرسال الحزمة مطلوب","min-pack-send-delay-min":"لا يمكن أن يكون الحد الأدنى لتأخير إرسال الحزمة أقل من 0","no-connectors":"لا توجد موصلات","no-data":"لا توجد تكوينات","no-gateway-found":"لم يتم العثور على بوابة.","no-gateway-matching":"'{{item}}' غير موجود.","path-logs":"مسار إلى ملفات السجل","path-logs-required":"المسار مطلوب.","permit-without-calls":"البقاء على الحياة يسمح بدون مكالمات",remote:"التكوين عن بُعد","remote-logging-level":"مستوى التسجيل","remove-entry":"إزالة التكوين","remote-shell":"قشرة عن بُعد","remote-configuration":"التكوين عن بُعد",other:"آخر","save-tip":"حفظ ملف التكوين","security-type":"نوع الأمان","security-types":{"access-token":"رمز الوصول","username-password":"اسم المستخدم وكلمة المرور",tls:"TLS","tls-access-token":"TLS + رمز الوصول","tls-private-key":"TLS + المفتاح الخاص"},"server-port":"منفذ الخادم",statistics:{statistic:"إحصائية",statistics:"الإحصائيات","statistic-commands-empty":"لا تتوفر إحصائيات",commands:"الأوامر","send-period":"فترة إرسال الإحصائيات (بالثواني)","send-period-required":"فترة إرسال الإحصائيات مطلوبة","send-period-min":"لا يمكن أن تكون فترة إرسال الإحصائيات أقل من 60","send-period-pattern":"فترة إرسال الإحصائيات غير صالحة","check-connectors-configuration":"فترة فحص تكوين الموصلات (بالثواني)","check-connectors-configuration-required":"فترة فحص تكوين الموصلات مطلوبة","check-connectors-configuration-min":"لا يمكن أن تكون فترة فحص تكوين الموصلات أقل من 1","check-connectors-configuration-pattern":"فترة فحص تكوين الموصلات غير صالحة",add:"إضافة أمر",timeout:"المهلة","timeout-required":"المهلة مطلوبة","timeout-min":"لا يمكن أن تكون المهلة أقل من 1","timeout-pattern":"المهلة غير صالحة","attribute-name":"اسم السمة","attribute-name-required":"اسم السمة مطلوب",command:"الأمر","command-required":"الأمر مطلوب","command-pattern":"الأمر غير صالح",remove:"إزالة الأمر"},storage:"التخزين","storage-max-file-records":"السجلات القصوى في الملف","storage-max-files":"الحد الأقصى لعدد الملفات","storage-max-files-min":"الحد الأدنى هو 1.","storage-max-files-pattern":"العدد غير صالح.","storage-max-files-required":"العدد مطلوب.","storage-max-records":"السجلات القصوى في التخزين","storage-max-records-min":"الحد الأدنى لعدد السجلات هو 1.","storage-max-records-pattern":"العدد غير صالح.","storage-max-records-required":"السجلات القصوى مطلوبة.","storage-read-record-count":"عدد قراءة السجلات في التخزين","storage-read-record-count-min":"الحد الأدنى لعدد السجلات هو 1.","storage-read-record-count-pattern":"العدد غير صالح.","storage-read-record-count-required":"عدد قراءة السجلات مطلوب.","storage-max-read-record-count":"الحد الأقصى لعدد قراءة السجلات في التخزين","storage-max-read-record-count-min":"الحد الأدنى لعدد السجلات هو 1.","storage-max-read-record-count-pattern":"العدد غير صالح.","storage-max-read-record-count-required":"عدد القراءة القصوى مطلوب.","storage-data-folder-path":"مسار مجلد البيانات","storage-data-folder-path-required":"مسار مجلد البيانات مطلوب.","storage-pack-size":"الحد الأقصى لحجم حزمة الحدث","storage-pack-size-min":"الحد الأدنى هو 1.","storage-pack-size-pattern":"العدد غير صالح.","storage-pack-size-required":"الحجم الأقصى لحزمة الحدث مطلوب.","storage-path":"مسار التخزين","storage-path-required":"مسار التخزين مطلوب.","storage-type":"نوع التخزين","storage-types":{"file-storage":"تخزين الملفات","memory-storage":"تخزين الذاكرة",sqlite:"SQLITE"},thingsboard:"ثينغزبورد",general:"عام","thingsboard-host":"مضيف ثينغزبورد","thingsboard-host-required":"المضيف مطلوب.","thingsboard-port":"منفذ ثينغزبورد","thingsboard-port-max":"الحد الأقصى لرقم المنفذ هو 65535.","thingsboard-port-min":"الحد الأدنى لرقم المنفذ هو 1.","thingsboard-port-pattern":"المنفذ غير صالح.","thingsboard-port-required":"المنفذ مطلوب.",tidy:"ترتيب","tidy-tip":"ترتيب تكوين JSON","title-connectors-json":"تكوين موصل {{typeName}}","tls-path-ca-certificate":"المسار إلى شهادة CA على البوابة","tls-path-client-certificate":"المسار إلى شهادة العميل على البوابة","messages-ttl-check-in-hours":"فحص TTL الرسائل بالساعات","messages-ttl-check-in-hours-required":"يجب تحديد فحص TTL الرسائل بالساعات.","messages-ttl-check-in-hours-min":"الحد الأدنى هو 1.","messages-ttl-check-in-hours-pattern":"الرقم غير صالح.","messages-ttl-in-days":"TTL الرسائل بالأيام","messages-ttl-in-days-required":"يجب تحديد TTL الرسائل بالأيام.","messages-ttl-in-days-min":"الحد الأدنى هو 1.","messages-ttl-in-days-pattern":"الرقم غير صالح.","mqtt-qos":"جودة الخدمة (QoS)","mqtt-qos-required":"جودة الخدمة (QoS) مطلوبة","mqtt-qos-range":"تتراوح قيم جودة الخدمة (QoS) من 0 إلى 1","tls-path-private-key":"المسار إلى المفتاح الخاص على البوابة","toggle-fullscreen":"تبديل وضع ملء الشاشة","transformer-json-config":"تكوين JSON*","update-config":"إضافة/تحديث تكوين JSON",hints:{"remote-configuration":"يمكنك تمكين التكوين وإدارة البوابة عن بُعد","remote-shell":"يمكنك تمكين التحكم البعيد في نظام التشغيل مع البوابة من عنصر واجهة المستخدم قشرة عن بُعد",host:"اسم المضيف أو عنوان IP لخادم ثينغزبورد",port:"منفذ خدمة MQTT على خادم ثينغزبورد",token:"رمز الوصول للبوابة من خادم ثينغزبورد","client-id":"معرف عميل MQTT للبوابة من خادم ثينغزبورد",username:"اسم المستخدم MQTT للبوابة من خادم ثينغزبورد",password:"كلمة المرور MQTT للبوابة من خادم ثينغزبورد","ca-cert":"المسار إلى ملف شهادة CA","date-form":"تنسيق التاريخ في رسالة السجل","data-folder":"المسار إلى المجلد الذي سيحتوي على البيانات (نسبي أو مطلق)","log-format":"تنسيق رسالة السجل","remote-log":"يمكنك تمكين التسجيل البعيد وقراءة السجلات من البوابة","backup-count":"إذا كان عدد النسخ الاحتياطية > 0، عند عملية تدوير، لا يتم الاحتفاظ بأكثر من عدد النسخ الاحتياطية المحددة - يتم حذف الأقدم",storage:"يوفر تكوينًا لحفظ البيانات الواردة قبل إرسالها إلى المنصة","max-file-count":"العدد الأقصى لعدد الملفات التي سيتم إنشاؤها","max-read-count":"عدد الرسائل للحصول عليها من التخزين وإرسالها إلى ثينغزبورد","max-records":"العدد الأقصى للسجلات التي ستخزن في ملف واحد","read-record-count":"عدد الرسائل للحصول عليها من التخزين وإرسالها إلى ثينغزبورد","max-records-count":"العدد الأقصى للبيانات في التخزين قبل إرسالها إلى ثينغزبورد","ttl-check-hour":"كم مرة سيتحقق البوابة من البيانات القديمة","ttl-messages-day":"الحد الأقصى لعدد الأيام التي ستحتفظ فيها التخزين بالبيانات",commands:"الأوامر لجمع الإحصائيات الإضافية",attribute:"مفتاح تلقي الإحصائيات",timeout:"مهلة زمنية لتنفيذ الأمر",command:"سيتم استخدام نتيجة تنفيذ الأمر كقيمة لتلقي الإحصائيات","check-device-activity":"يمكنك تمكين مراقبة نشاط كل جهاز متصل","inactivity-timeout":"الوقت بعد الذي ستفصل البوابة الجهاز","inactivity-period":"تكرار فحص نشاط الجهاز","minimal-pack-delay":"التأخير بين إرسال حزم الرسائل (يؤدي تقليل هذا الإعداد إلى زيادة استخدام وحدة المعالجة المركزية)",qos:"جودة الخدمة في رسائل MQTT (0 - على الأكثر مرة واحدة، 1 - على الأقل مرة واحدة)","server-port":"منفذ الشبكة الذي سيستمع فيه خادم GRPC للاستفسارات الواردة.","grpc-keep-alive-timeout":"الحد الأقصى للوقت الذي يجب أن ينتظره الخادم لاستجابة رسالة الحفاظ على الاتصال قبل اعتبار الاتصال ميتًا.","grpc-keep-alive":"المدة بين رسائل حفظ الاتصال المتعاقبة عند عدم وجود استدعاء RPC نشط.","grpc-min-time-between-pings":"الحد الأدنى للوقت الذي يجب فيه أن ينتظر الخادم بين إرسال رسائل حفظ الاتصال","grpc-max-pings-without-data":"الحد الأقصى لعدد رسائل حفظ الاتصال التي يمكن للخادم إرسالها دون تلقي أي بيانات قبل اعتبار الاتصال ميتًا.","grpc-min-ping-interval-without-data":"الحد الأدنى للوقت الذي يجب فيه أن ينتظر الخادم بين إرسال رسائل حفظ الاتصال عند عدم إرسال أو استلام بيانات.","permit-without-calls":"السماح للخادم بإبقاء اتصال GRPC حيًا حتى عندما لا تكون هناك استدعاءات RPC نشطة."}}},xt={gateway:{"add-entry":"Afegir configuració","connector-add":"Afegir conector","connector-enabled":"Activar conector","connector-name":"Nom conector","connector-name-required":"Cal nom conector.","connector-type":"Tipus conector","connector-type-required":"Cal tipus conector.",connectors:"Configuració de conectors","create-new-gateway":"Crear un gateway nou","create-new-gateway-text":"Crear un nou gateway amb el nom: '{{gatewayName}}'?",delete:"Esborrar configuració","download-tip":"Descarregar fitxer de configuració",gateway:"Gateway","gateway-exists":"Ja existeix un dispositiu amb el mateix nom.","gateway-name":"Nom de Gateway","gateway-name-required":"Cal un nom de gateway.","gateway-saved":"Configuració de gateway gravada satisfactòriament.","json-parse":"JSON no vàlid.","json-required":"El camp no pot ser buit.","no-connectors":"No hi ha conectors","no-data":"No hi ha configuracions","no-gateway-found":"No s'ha trobat cap gateway.","no-gateway-matching":" '{{item}}' no trobat.","path-logs":"Ruta als fitxers de log","path-logs-required":"Cal ruta.",remote:"Configuració remota","remote-logging-level":"Nivel de logging","remove-entry":"Esborrar configuració","save-tip":"Gravar fitxer de configuració","security-type":"Tipus de seguretat","security-types":{"access-token":"Token d'accés",tls:"TLS"},storage:"Grabació","storage-max-file-records":"Número màxim de registres en fitxer","storage-max-files":"Número màxim de fitxers","storage-max-files-min":"El número mínim és 1.","storage-max-files-pattern":"Número no vàlid.","storage-max-files-required":"Cal número.","storage-max-records":"Màxim de registres en el magatzem","storage-max-records-min":"El número mínim és 1.","storage-max-records-pattern":"Número no vàlid.","storage-max-records-required":"Cal número.","storage-pack-size":"Mida màxim de esdeveniments","storage-pack-size-min":"El número mínim és 1.","storage-pack-size-pattern":"Número no vàlid.","storage-pack-size-required":"Cal número.","storage-path":"Ruta de magatzem","storage-path-required":"Cal ruta de magatzem.","storage-type":"Tipus de magatzem","storage-types":{"file-storage":"Magatzem fitxer","memory-storage":"Magatzem en memoria"},thingsboard:"ThingsBoard","thingsboard-host":"Host ThingsBoard","thingsboard-host-required":"Cal Host.","thingsboard-port":"Port ThingsBoard","thingsboard-port-max":"El port màxim és 65535.","thingsboard-port-min":"El port mínim és 1.","thingsboard-port-pattern":"Port no vàlid.","thingsboard-port-required":"Cal port.",tidy:"Endreçat","tidy-tip":"Endreçat JSON","title-connectors-json":"Configuració conector {{typeName}}","tls-path-ca-certificate":"Ruta al certificat CA al gateway","tls-path-client-certificate":"Ruta al certificat client al gateway","tls-path-private-key":"Ruta a la clau privada al gateway","toggle-fullscreen":"Pantalla completa fullscreen","transformer-json-config":"Configuració JSON*","update-config":"Afegir/actualizar configuració JSON"}},bt={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"}},wt={gateway:{"add-entry":"Tilføj konfiguration","connector-add":"Tilføj ny stikforbindelse","connector-enabled":"Aktivér stikforbindelse","connector-name":"Navn på stikforbindelse","connector-name-required":"Navn på stikforbindelse er påkrævet.","connector-type":"Stikforbindelsestype","connector-type-required":"Stikforbindelsestype er påkrævet.",connectors:"Konfiguration af stikforbindelser","create-new-gateway":"Opret en ny gateway","create-new-gateway-text":"",delete:"Slet konfiguration","download-tip":"Download konfigurationsfil",gateway:"Gateway","gateway-exists":"Enhed med samme navn findes allerede.","gateway-name":"Gateway-navn","gateway-name-required":"Gateway-navn er påkrævet.","gateway-saved":"Gateway-konfigurationen blev gemt.","json-parse":"Ikke gyldig JSON.","json-required":"Feltet må ikke være tomt.","no-connectors":"Ingen stikforbindelser","no-data":"Ingen konfigurationer","no-gateway-found":"Ingen gateway fundet.","no-gateway-matching":"","path-logs":"Sti til logfiler","path-logs-required":"Sti er påkrævet.",remote:"Fjernkonfiguration","remote-logging-level":"Logføringsniveau","remove-entry":"Fjern konfiguration","save-tip":"Gem konfigurationsfil","security-type":"Sikkerhedstype","security-types":{"access-token":"Adgangstoken",tls:"TLS"},storage:"Lagring","storage-max-file-records":"Maks. antal poster i fil","storage-max-files":"Maks. antal filer","storage-max-files-min":"Min. antal er 1.","storage-max-files-pattern":"Antal er ikke gyldigt.","storage-max-files-required":"Antal er påkrævet.","storage-max-records":"Maks. antal poster i lagring","storage-max-records-min":"Min. antal poster er 1.","storage-max-records-pattern":"Antal er ikke gyldigt.","storage-max-records-required":"Maks. antal poster er påkrævet.","storage-pack-size":"Maks. antal pakkestørrelse for begivenhed","storage-pack-size-min":"Min. antal er 1.","storage-pack-size-pattern":"Antal er ikke gyldigt.","storage-pack-size-required":"Maks. antal pakkestørrelse for begivenhed er påkrævet.","storage-path":"Lagringssti","storage-path-required":"Lagringssti er påkrævet.","storage-type":"Lagringstype","storage-types":{"file-storage":"Lagring af filter","memory-storage":"Lagring af hukommelse"},thingsboard:"ThingsBoard","thingsboard-host":"ThingsBoard-vært","thingsboard-host-required":"Vært er påkrævet.","thingsboard-port":"ThingsBoard-port","thingsboard-port-max":"Maks. portnummer er 65535.","thingsboard-port-min":"Min. portnummer er 1.","thingsboard-port-pattern":"Port er ikke gyldig.","thingsboard-port-required":"Port er påkrævet.",tidy:"Tidy","tidy-tip":"Tidy konfig. JSON","title-connectors-json":"","tls-path-ca-certificate":"Sti til CA-certifikat på gateway","tls-path-client-certificate":"Sti til klientcertifikat på gateway","tls-path-private-key":"Sti til privat nøgle på gateway","toggle-fullscreen":"Skift til fuld skærm","transformer-json-config":"Konfiguration JSON*","update-config":"Tilføj/opdater konfiguration JSON"}},St={gateway:{"add-entry":"Añadir configuración",advanced:"Avanzado","checking-device-activity":"Probando actividad de dispositivo",command:"Comandos Docker","command-copied-message":"Se han copiado los comandos al portapapeles",configuration:"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:"Conectores","connectors-config":"Configuración de conectores","connectors-table-enabled":"Enabled","connectors-table-name":"Nombre","connectors-table-type":"Tipo","connectors-table-status":"Estado","connectors-table-actions":"Acciones","connectors-table-key":"Clave","connectors-table-class":"Clase","rpc-command-send":"Enviar","rpc-command-result":"Resultado","rpc-command-edit-params":"Editar parametros","gateway-configuration":"Configuración General","create-new-gateway":"Crear un gateway nuevo","create-new-gateway-text":"Crear un nuevo gateway con el nombre: '{{gatewayName}}'?","created-time":"Hora de creación","configuration-delete-dialog-header":"Las configuraciones se borrarán","configuration-delete-dialog-body":"Sólo es posible desactivar la configuración remota, si hay acceso físico al gateway. Se borrarán todas las configuraciones previas.

\nPara desactivar la configuración, introduce el nombre del gateway aquí","configuration-delete-dialog-input":"Nombre Gateway","configuration-delete-dialog-input-required":"Se requiere nombre de gateway","configuration-delete-dialog-confirm":"Desactivar",delete:"Borrar configuración","download-tip":"Descargar fichero de configuración","drop-file":"Arrastra un fichero o",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.",grpc:"GRPC","grpc-keep-alive-timeout":"Timeout Keep alive (en ms)","grpc-keep-alive-timeout-required":"Se requiere Timeout Keep alive","grpc-keep-alive-timeout-min":"El valor no puede ser menor de 1","grpc-keep-alive-timeout-pattern":"El valor no es válido","grpc-keep-alive":"Keep alive (en ms)","grpc-keep-alive-required":"Se requiere keep alive","grpc-keep-alive-min":"El valor no puede ser menor de 1","grpc-keep-alive-pattern":"El valor keep alive no es válido","grpc-min-time-between-pings":"Tiempo mínimo entre pings (en ms)","grpc-min-time-between-pings-required":"Se requiere tiempo mínimo entre pings","grpc-min-time-between-pings-min":"El valor no puede ser menor de 1","grpc-min-time-between-pings-pattern":"El valor de tiempo mínimo entre pings no es válido","grpc-min-ping-interval-without-data":"Intervalo mínimo sin datos (en ms)","grpc-min-ping-interval-without-data-required":"Se requiere intervalo","grpc-min-ping-interval-without-data-min":"El valor no puede ser menor de 1","grpc-min-ping-interval-without-data-pattern":"El valor de intervalo no es válido","grpc-max-pings-without-data":"Intervalo máximo sin datos","grpc-max-pings-without-data-required":"Se requiere intervalo","grpc-max-pings-without-data-min":"El valor no puede ser menor de 1","grpc-max-pings-without-data-pattern":"El valor de intervalo no es válido","inactivity-check-period-seconds":"Periodo de control de inactividad (en segundos)","inactivity-check-period-seconds-required":"Se requiere periodo","inactivity-check-period-seconds-min":"El valor no puede ser menor de 1","inactivity-check-period-seconds-pattern":"El valor del periodo no es válido","inactivity-timeout-seconds":"Timeout de inactividad (en segundos)","inactivity-timeout-seconds-required":"Se requiere timeout de inactividad","inactivity-timeout-seconds-min":"El valor no puede ser menor de 1","inactivity-timeout-seconds-pattern":"El valor de inactividad no es válido","json-parse":"JSON no válido.","json-required":"El campo no puede estar vacío.",logs:{logs:"Registros",days:"días",hours:"horas",minutes:"minutos",seconds:"segundos","date-format":"Formato de fecha","date-format-required":"Se requiere formato de fecha","log-format":"Formato de registro","log-type":"Tipo de registro","log-format-required":"Se requiere tipo de registro",remote:"Registro remoto","remote-logs":"Registro remoto",local:"Registro local",level:"Nivel de registro","file-path":"Ruta de fichero","file-path-required":"Se requiere ruta de fichero","saving-period":"Periodo de guardado de registros","saving-period-min":"El periodo no puede ser menor que 1","saving-period-required":"Se requiere periodo de guardado","backup-count":"Número de backups","backup-count-min":"El número de backups no puede ser menor que 1","backup-count-required":"Se requiere número de backups"},"min-pack-send-delay":"Tiempo de espera, envío de paquetes (en ms)","min-pack-send-delay-required":"Se requiere tiempo de espera","min-pack-send-delay-min":"El tiempo de espera no puede ser menor que 0","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.","permit-without-calls":"Permitir Keep alive si llamadas",remote:"Configuración remota","remote-logging-level":"Nivel de logging","remove-entry":"Borrar configuración","remote-shell":"Consola remota","remote-configuration":"Configuración remota",other:"otros","save-tip":"Grabar fichero de configuración","security-type":"Tipo de seguridad","security-types":{"access-token":"Tóken de acceso","username-password":"Usuario y contraseña",tls:"TLS","tls-access-token":"TLS + Tóken de acceso","tls-private-key":"TLS + Clave privada"},"server-port":"Puerto del servidor",statistics:{statistic:"Estadística",statistics:"Estadísticas","statistic-commands-empty":"No hay estadísticas",commands:"Comandos","send-period":"Periodo de envío de estadísticas (en segundos)","send-period-required":"Se requiere periodo de envío","send-period-min":"El periodo de envío no puede ser menor de 60","send-period-pattern":"El periodo de envío no es válido","check-connectors-configuration":"Revisar configuración de conectores (en segundos)","check-connectors-configuration-required":"Se requiere un valor","check-connectors-configuration-min":"El valor no puede ser menor de 1","check-connectors-configuration-pattern":"La configuración no es válida",add:"Añadir comando",timeout:"Timeout","timeout-required":"Se requiere timeout","timeout-min":"El timeout no puede ser menor de 1","timeout-pattern":"El timeout no es válido","attribute-name":"Nombre de atributo","attribute-name-required":"Se requiere nombre de atributo",command:"Comando","command-required":"Se requiere comando",remove:"Borrar comando"},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-read-record-count":"Leer número de entradas en almacén","storage-read-record-count-min":"El número mínimo de entradas es 1.","storage-read-record-count-pattern":"El número no es válido.","storage-read-record-count-required":"Se requiere número de entradas.","storage-max-read-record-count":"Número máximo de entradas en el almacén","storage-max-read-record-count-min":"El número mínimo es 1.","storage-max-read-record-count-pattern":"El número no es válido","storage-max-read-record-count-required":"Se requiere número máximo de entradas.","storage-data-folder-path":"Ruta de carpeta de datos","storage-data-folder-path-required":"Se requiere ruta.","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 en fichero","memory-storage":"Almacén en memoria",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"General","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","messages-ttl-check-in-hours":"Comprobación de TTL de mensajes en horas","messages-ttl-check-in-hours-required":"Campo requerido.","messages-ttl-check-in-hours-min":"El mínimo es 1.","messages-ttl-check-in-hours-pattern":"El número no es válido.","messages-ttl-in-days":"TTL (Time to live) de mensages en días","messages-ttl-in-days-required":"Se requiere TTL de mensajes.","messages-ttl-in-days-min":"El número mínimo es 1.","messages-ttl-in-days-pattern":"El número no es válido.","mqtt-qos":"QoS","mqtt-qos-required":"Se requiere QoS","mqtt-qos-range":"El rango de valores es desde 0 a 1","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",hints:{"remote-configuration":"Habilita la administración y configuración remota del gateway","remote-shell":"Habilita el control remoto del sistema operativo del gateway desde el widget terminal remoto",host:"Hostname o dirección IP del servidor Thingsboard",port:"Puerto del servicio MQTT en el servidor Thingsboard",token:"Access token para el gateway","client-id":"ID de cliente MQTT para el gateway",username:"Usuario MQTT para el gateway",password:"Contraseña MQTT para el gateway","ca-cert":"Ruta al fichero del certificado CA","date-form":"Formato de fecha en los mensajes de registro","data-folder":"Ruta a la carpeta que contendrá los datos (Relativa o absoluta)","log-format":"Formato de mensajes en registro","remote-log":"Habilita el registro remoto y la posterior lectura desde el gateway","backup-count":"Si el contaje de copias de seguridad es mayor que 0, cuando se realice una renovación, no se conservan más que los archivos de recuento de copias de seguridad, los más antíguos se eliminarán",storage:"Provee la configuración para el grabado de datos entrantes antes de que se envíen a la plataforma","max-file-count":"Número máximo de ficheros que se crearán","max-read-count":"Númeo máximo de mensajes a obtener desde el disco y enviados a la plataforma","max-records":"Número máximo de registros que se guardarán en un solo fichero","read-record-count":"Número de mensages a obtener desde el almacenamiento y enviados a la plataforma","max-records-count":"Número máximo de datos en almacenamiento antes de enviar a la plataforma","ttl-check-hour":"Con qué frecuencia el gateway comprobará si los datos están obsoletos","ttl-messages-day":"Número máximo de días para la retención de datos en el almacén",commands:"Comandos para recoger estadísticas adicionales",attribute:"Clave de telemetría para estadísticas",timeout:"Timeout para la ejecución de comandos",command:"El resultado de la ejecución del comando, se usará como valor para la telemetría","check-device-activity":"Habilita la monitorización de cada uno de los dispositivos conectados","inactivity-timeout":"Tiempo tras que el gateway desconectará el dispositivo","inactivity-period":"Periodo de monitorización de actividad en el dispositivo","minimal-pack-delay":"Tiempo de espera entre envío de paquetes de mensajes (Un valor muy bajo, resultará en un aumento de uso de la CPU en el gateway)",qos:"Quality of Service en los mensajes MQTT (0 - at most once, 1 - at least once)","server-port":"Puerto de red en el cual el servidor GRPC escuchará conexiones entrantes.","grpc-keep-alive-timeout":"Tiempo máximo, el cual el servidor esperara un ping keepalive antes de considerar la conexión terminada.","grpc-keep-alive":"Duración entre dos pings keepalive cuando no haya llamada RPC activa.","grpc-min-time-between-pings":"Mínimo tiempo que el servidor debe esperar entre envíos de mensajes de ping","grpc-max-pings-without-data":"Número máximo de pings keepalive que el servidor puede enviar sin recibir ningún dato antes de considerar la conexión terminada.","grpc-min-ping-interval-without-data":"Mínimo tiempo que el servidor debe esperar entre envíos de ping keepalive cuando no haya ningún dato en envío o recepción.","permit-without-calls":"Permitir al servidor mantener la conexión GRPC abierta, cuando no haya llamadas RPC activas."}}},Ct={gateway:{"add-entry":"설정 추가","connector-add":"새로운 연결자 추가","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"}},_t={gateway:{"add-entry":"Add configuration",advanced:"Advanced","checking-device-activity":"Checking device activity",command:"Docker commands","command-copied-message":"Docker command has been copied to clipboard",configuration:"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","connectors-config":"Connectors configuration","connectors-table-enabled":"Enabled","connectors-table-name":"Name","connectors-table-type":"Type","connectors-table-status":"Status","connectors-table-actions":"Actions","connectors-table-key":"Key","connectors-table-class":"Class","rpc-command-send":"Send","rpc-command-result":"Result","rpc-command-edit-params":"Edit parameters","gateway-configuration":"General Configuration","docker-label":"In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.","create-new-gateway":"Create a new gateway","create-new-gateway-text":"Are you sure you want create a new gateway with name: '{{gatewayName}}'?","created-time":"Created time","configuration-delete-dialog-header":"Configurations will be deleted","configuration-delete-dialog-body":"Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below","configuration-delete-dialog-input":"Gateway name","configuration-delete-dialog-input-required":"Gateway name is mandatory","configuration-delete-dialog-confirm":"Turn Off",delete:"Delete configuration","download-tip":"Download configuration file","drop-file":"Drop file here or",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.",grpc:"GRPC","grpc-keep-alive-timeout":"Keep alive timeout (in ms)","grpc-keep-alive-timeout-required":"Keep alive timeout is required","grpc-keep-alive-timeout-min":"Keep alive timeout can not be less then 1","grpc-keep-alive-timeout-pattern":"Keep alive timeout is not valid","grpc-keep-alive":"Keep alive (in ms)","grpc-keep-alive-required":"Keep alive is required","grpc-keep-alive-min":"Keep alive can not be less then 1","grpc-keep-alive-pattern":"Keep alive is not valid","grpc-min-time-between-pings":"Min time between pings (in ms)","grpc-min-time-between-pings-required":"Min time between pings is required","grpc-min-time-between-pings-min":"Min time between pings can not be less then 1","grpc-min-time-between-pings-pattern":"Min time between pings is not valid","grpc-min-ping-interval-without-data":"Min ping interval without data (in ms)","grpc-min-ping-interval-without-data-required":"Min ping interval without data is required","grpc-min-ping-interval-without-data-min":"Min ping interval without data can not be less then 1","grpc-min-ping-interval-without-data-pattern":"Min ping interval without data is not valid","grpc-max-pings-without-data":"Max pings without data","grpc-max-pings-without-data-required":"Max pings without data is required","grpc-max-pings-without-data-min":"Max pings without data can not be less then 1","grpc-max-pings-without-data-pattern":"Max pings without data is not valid","inactivity-check-period-seconds":"Inactivity check period (in sec)","inactivity-check-period-seconds-required":"Inactivity check period is required","inactivity-check-period-seconds-min":"Inactivity check period can not be less then 1","inactivity-check-period-seconds-pattern":"Inactivity check period is not valid","inactivity-timeout-seconds":"Inactivity timeout (in sec)","inactivity-timeout-seconds-required":"Inactivity timeout is required","inactivity-timeout-seconds-min":"Inactivity timeout can not be less then 1","inactivity-timeout-seconds-pattern":"Inactivity timeout is not valid","json-parse":"Not valid JSON.","json-required":"Field cannot be empty.",logs:{logs:"Logs",days:"days",hours:"hours",minutes:"minutes",seconds:"seconds","date-format":"Date format","date-format-required":"Date format required","log-format":"Log format","log-type":"Log type","log-format-required":"Log format required",remote:"Remote logging","remote-logs":"Remote logs",local:"Local logging",level:"Log level","file-path":"File path","file-path-required":"File path required","saving-period":"Log saving period","saving-period-min":"Log saving period can not be less then 1","saving-period-required":"Log saving period required","backup-count":"Backup count","backup-count-min":"Backup count can not be less then 1","backup-count-required":"Backup count required"},"min-pack-send-delay":"Min pack send delay (in ms)","min-pack-send-delay-required":"Min pack send delay is required","min-pack-send-delay-min":"Min pack send delay can not be less then 0","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.","permit-without-calls":"Keep alive permit without calls",remote:"Remote configuration","remote-logging-level":"Logging level","remove-entry":"Remove configuration","remote-shell":"Remote shell","remote-configuration":"Remote Configuration",other:"Other","save-tip":"Save configuration file","security-type":"Security type","security-types":{"access-token":"Access Token","username-password":"Username and Password",tls:"TLS","tls-access-token":"TLS + Access Token","tls-private-key":"TLS + Private Key"},"server-port":"Server port",statistics:{statistic:"Statistic",statistics:"Statistics","statistic-commands-empty":"No statistics available",commands:"Commands","send-period":"Statistic send period (in sec)","send-period-required":"Statistic send period is required","send-period-min":"Statistic send period can not be less then 60","send-period-pattern":"Statistic send period is not valid","check-connectors-configuration":"Check connectors configuration (in sec)","check-connectors-configuration-required":"Check connectors configuration is required","check-connectors-configuration-min":"Check connectors configuration can not be less then 1","check-connectors-configuration-pattern":"Check connectors configuration is not valid",add:"Add command",timeout:"Timeout","timeout-required":"Timeout is required","timeout-min":"Timeout can not be less then 1","timeout-pattern":"Timeout is not valid","attribute-name":"Attribute name","attribute-name-required":"Attribute name is required",command:"Command","command-required":"Command is required",remove:"Remove command"},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-read-record-count":"Read record count in storage","storage-read-record-count-min":"Minimum number of records is 1.","storage-read-record-count-pattern":"Number is not valid.","storage-read-record-count-required":"Read record count is required.","storage-max-read-record-count":"Max read record count in storage","storage-max-read-record-count-min":"Minimum number of records is 1.","storage-max-read-record-count-pattern":"Number is not valid.","storage-max-read-record-count-required":"Max Read record count is required.","storage-data-folder-path":"Data folder path","storage-data-folder-path-required":"Data folder path 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",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"General","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","messages-ttl-check-in-hours":"Messages TTL check in hours","messages-ttl-check-in-hours-required":"Messages TTL check in hours is required.","messages-ttl-check-in-hours-min":"Min number is 1.","messages-ttl-check-in-hours-pattern":"Number is not valid.","messages-ttl-in-days":"Messages TTL in days","messages-ttl-in-days-required":"Messages TTL in days is required.","messages-ttl-in-days-min":"Min number is 1.","messages-ttl-in-days-pattern":"Number is not valid.","mqtt-qos":"QoS","mqtt-qos-required":"QoS is required","mqtt-qos-range":"QoS values range is from 0 to 1","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",hints:{"remote-configuration":"Enables remote configuration and management of the gateway","remote-shell":"Enables remote control of the operating system with the gateway from the Remote Shell widget",host:"Hostname or IP address of ThingsBoard server",port:"Port of MQTT service on ThingsBoard server",token:"Access token for the gateway from ThingsBoard server","client-id":"MQTT client id for the gateway form ThingsBoard server",username:"MQTT username for the gateway form ThingsBoard server",password:"MQTT password for the gateway form ThingsBoard server","ca-cert":"Path to CA certificate file","date-form":"Date format in log message","data-folder":"Path to folder, that will contains data (Relative or Absolute)","log-format":"Log message format","remote-log":"Enables remote logging and logs reading from the gateway","backup-count":"If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",storage:"Provides configuration for saving incoming data before it is sent to the platform","max-file-count":"Maximum count of file that will be created","max-read-count":"Count of messages to get from storage and send to ThingsBoard","max-records":"Maximum count of records that will be stored in one file","read-record-count":"Count of messages to get from storage and send to ThingsBoard","max-records-count":"Maximum count of data in storage before send to ThingsBoard","ttl-check-hour":"How often will Gateway check data for obsolescence","ttl-messages-day":"Maximum days that storage will save data",commands:"Commands for collecting additional statistic",attribute:"Statistic telemetry key",timeout:"Timeout for command executing",command:"The result of the command execution, will be used as the value for telemetry","check-device-activity":"Enables monitor the activity of each connected device","inactivity-timeout":"Time after whose the gateway will disconnect device","inactivity-period":"Periodicity of device activity check","minimal-pack-delay":"Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)",qos:"Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)","server-port":"Network port on which GRPC server will listen for incoming connections.","grpc-keep-alive-timeout":"Maximum time the server should wait for a keepalive ping response before considering the connection dead.","grpc-keep-alive":"Duration between two successive keepalive ping messages when there is no active RPC call.","grpc-min-time-between-pings":"Minimum amount of time the server should wait between sending keepalive ping messages","grpc-max-pings-without-data":"Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.","grpc-min-ping-interval-without-data":"Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.","permit-without-calls":"Allow server to keep the GRPC connection alive even when there are no active RPC calls."}}},Tt={gateway:{"add-entry":"Configuratie toevoegen","connector-add":"Nieuwe connector toevoegen","connector-enabled":"Connector inschakelen","connector-name":"Naam van de connector","connector-name-required":"De naam van de connector is vereist.","connector-type":"Type aansluiting","connector-type-required":"Het type connector is vereist.",connectors:"Configuratie van connectoren","create-new-gateway":"Een nieuwe gateway maken","create-new-gateway-text":"Weet u zeker dat u een nieuwe gateway wilt maken met de naam: '{{gatewayName}}'?",delete:"Configuratie verwijderen","download-tip":"Configuratiebestand downloaden",gateway:"Gateway","gateway-exists":"Device met dezelfde naam bestaat al.","gateway-name":"Naam van de gateway","gateway-name-required":"De naam van de gateway is vereist.","gateway-saved":"Gatewayconfiguratie succesvol opgeslagen.","json-parse":"Ongeldige JSON.","json-required":"Het veld mag niet leeg zijn.","no-connectors":"Geen connectoren","no-data":"Geen configuraties","no-gateway-found":"Geen gateway gevonden.","no-gateway-matching":"'{{item}}' niet gevonden.","path-logs":"Pad naar logbestanden","path-logs-required":"Pad is vereist.",remote:"Configuratie op afstand","remote-logging-level":"Registratie niveau","remove-entry":"Configuratie verwijderen","save-tip":"Configuratiebestand opslaan","security-type":"Soort beveiliging","security-types":{"access-token":"Toegang tot token",tls:"TLS (TLS)"},storage:"Opslag","storage-max-file-records":"Maximum aantal records in bestand","storage-max-files":"Maximaal aantal bestanden","storage-max-files-min":"Minimum aantal is 1.","storage-max-files-pattern":"Nummer is niet geldig.","storage-max-files-required":"Nummer is vereist.","storage-max-records":"Maximum aantal records in opslag","storage-max-records-min":"Minimum aantal records is 1.","storage-max-records-pattern":"Nummer is niet geldig.","storage-max-records-required":"Maximale records zijn vereist.","storage-pack-size":"Maximale pakketgrootte voor events","storage-pack-size-min":"Minimum aantal is 1.","storage-pack-size-pattern":"Nummer is niet geldig.","storage-pack-size-required":"De maximale pakketgrootte van het event is vereist.","storage-path":"Opslag pad","storage-path-required":"Opslagpad is vereist.","storage-type":"Type opslag","storage-types":{"file-storage":"Opslag van bestanden","memory-storage":"Geheugen opslag"},thingsboard:"Dingen Bord","thingsboard-host":"ThingsBoard-gastheer","thingsboard-host-required":"Server host is vereist.","thingsboard-port":"ThingsBoard-poort","thingsboard-port-max":"Het maximale poortnummer is 65535.","thingsboard-port-min":"Het minimale poortnummer is 1.","thingsboard-port-pattern":"Poort is niet geldig.","thingsboard-port-required":"Poort is vereist.",tidy:"Ordelijk","tidy-tip":"Opgeruimde configuratie JSON","title-connectors-json":"Configuratie van connector {{typeName}}","tls-path-ca-certificate":"Pad naar CA-certificaat op gateway","tls-path-client-certificate":"Pad naar clientcertificaat op gateway","tls-path-private-key":"Pad naar privésleutel op gateway","toggle-fullscreen":"Volledig scherm in- en uitschakelen","transformer-json-config":"Configuratie JSON*","update-config":"Configuratie JSON toevoegen/bijwerken"}},It={gateway:{"add-entry":"Dodaj konfigurację",advanced:"Advanced","checking-device-activity":"Checking device activity",command:"Docker commands","command-copied-message":"Docker command has been copied to clipboard",configuration:"Configuration","connector-add":"Dodaj nowe złącze","connector-enabled":"Włącz złącze","connector-name":"Nazwa złącza","connector-name-required":"Nazwa złącza jest wymagana.","connector-type":"Typ złącza","connector-type-required":"Typ złącza jest wymagany.",connectors:"Konfiguracja złączy","connectors-config":"Connectors configuration","connectors-table-enabled":"Enabled","connectors-table-name":"Name","connectors-table-type":"Type","connectors-table-status":"Status","connectors-table-actions":"Actions","connectors-table-key":"Key","connectors-table-class":"Class","rpc-command-send":"Send","rpc-command-result":"Result","rpc-command-edit-params":"Edit parameters","gateway-configuration":"General Configuration","docker-label":"In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.","create-new-gateway":"Utwórz nowy gateway","create-new-gateway-text":"Czy na pewno chcesz utworzyć nowy gateway o nazwie: '{{gatewayName}}'?","created-time":"Created time","configuration-delete-dialog-header":"Configurations will be deleted","configuration-delete-dialog-body":"Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below","configuration-delete-dialog-input":"Gateway name","configuration-delete-dialog-input-required":"Gateway name is mandatory","configuration-delete-dialog-confirm":"Turn Off",delete:"Usuń konfigurację","download-tip":"Pobierz plik konfiguracyjny","drop-file":"Drop file here or",gateway:"Wejście","gateway-exists":"Urządzenie o tej samej nazwie już istnieje.","gateway-name":"Nazwa Gateway","gateway-name-required":"Nazwa Gateway'a jest wymagana.","gateway-saved":"Konfiguracja Gatewey'a została pomyślnie zapisana.",grpc:"GRPC","grpc-keep-alive-timeout":"Keep alive timeout (in ms)","grpc-keep-alive-timeout-required":"Keep alive timeout is required","grpc-keep-alive-timeout-min":"Keep alive timeout can not be less then 1","grpc-keep-alive-timeout-pattern":"Keep alive timeout is not valid","grpc-keep-alive":"Keep alive (in ms)","grpc-keep-alive-required":"Keep alive is required","grpc-keep-alive-min":"Keep alive can not be less then 1","grpc-keep-alive-pattern":"Keep alive is not valid","grpc-min-time-between-pings":"Min time between pings (in ms)","grpc-min-time-between-pings-required":"Min time between pings is required","grpc-min-time-between-pings-min":"Min time between pings can not be less then 1","grpc-min-time-between-pings-pattern":"Min time between pings is not valid","grpc-min-ping-interval-without-data":"Min ping interval without data (in ms)","grpc-min-ping-interval-without-data-required":"Min ping interval without data is required","grpc-min-ping-interval-without-data-min":"Min ping interval without data can not be less then 1","grpc-min-ping-interval-without-data-pattern":"Min ping interval without data is not valid","grpc-max-pings-without-data":"Max pings without data","grpc-max-pings-without-data-required":"Max pings without data is required","grpc-max-pings-without-data-min":"Max pings without data can not be less then 1","grpc-max-pings-without-data-pattern":"Max pings without data is not valid","inactivity-check-period-seconds":"Inactivity check period (in sec)","inactivity-check-period-seconds-required":"Inactivity check period is required","inactivity-check-period-seconds-min":"Inactivity check period can not be less then 1","inactivity-check-period-seconds-pattern":"Inactivity check period is not valid","inactivity-timeout-seconds":"Inactivity timeout (in sec)","inactivity-timeout-seconds-required":"Inactivity timeout is required","inactivity-timeout-seconds-min":"Inactivity timeout can not be less then 1","inactivity-timeout-seconds-pattern":"Inactivity timeout is not valid","json-parse":"Nieprawidłowy JSON.","json-required":"Pole nie może być puste.",logs:{logs:"Logs",days:"days",hours:"hours",minutes:"minutes",seconds:"seconds","date-format":"Date format","date-format-required":"Date format required","log-format":"Log format","log-type":"Log type","log-format-required":"Log format required",remote:"Remote logging","remote-logs":"Remote logs",local:"Local logging",level:"Log level","file-path":"File path","file-path-required":"File path required","saving-period":"Log saving period","saving-period-min":"Log saving period can not be less then 1","saving-period-required":"Log saving period required","backup-count":"Backup count","backup-count-min":"Backup count can not be less then 1","backup-count-required":"Backup count required"},"min-pack-send-delay":"Min pack send delay (in ms)","min-pack-send-delay-required":"Min pack send delay is required","min-pack-send-delay-min":"Min pack send delay can not be less then 0","no-connectors":"Brak złączy","no-data":"Brak konfiguracji","no-gateway-found":"Nie znaleziono gateway'a.","no-gateway-matching":" '{{item}}' nie znaleziono.","path-logs":"Ścieżka do plików dziennika","path-logs-required":"Ścieżka jest wymagana.","permit-without-calls":"Keep alive permit without calls",remote:"Zdalna konfiguracja","remote-logging-level":"Poziom logowania","remove-entry":"Usuń konfigurację","remote-shell":"Remote shell","remote-configuration":"Remote Configuration",other:"Other","save-tip":"Zapisz plik konfiguracyjny","security-type":"Rodzaj zabezpieczenia","security-types":{"access-token":"Token dostępu","username-password":"Username and Password",tls:"TLS","tls-access-token":"TLS + Access Token","tls-private-key":"TLS + Private Key"},"server-port":"Server port",statistics:{statistic:"Statistic",statistics:"Statistics","statistic-commands-empty":"No statistics available",commands:"Commands","send-period":"Statistic send period (in sec)","send-period-required":"Statistic send period is required","send-period-min":"Statistic send period can not be less then 60","send-period-pattern":"Statistic send period is not valid","check-connectors-configuration":"Check connectors configuration (in sec)","check-connectors-configuration-required":"Check connectors configuration is required","check-connectors-configuration-min":"Check connectors configuration can not be less then 1","check-connectors-configuration-pattern":"Check connectors configuration is not valid",add:"Add command",timeout:"Timeout","timeout-required":"Timeout is required","timeout-min":"Timeout can not be less then 1","timeout-pattern":"Timeout is not valid","attribute-name":"Attribute name","attribute-name-required":"Attribute name is required",command:"Command","command-required":"Command is required",remove:"Remove command"},storage:"Składowanie","storage-max-file-records":"Maksymalna liczba rekordów w pliku","storage-max-files":"Maksymalna liczba plików","storage-max-files-min":"Minimalna liczba to 1.","storage-max-files-pattern":"Numer jest nieprawidłowy.","storage-max-files-required":"Numer jest wymagany.","storage-max-records":"Maksymalna liczba rekordów w pamięci","storage-max-records-min":"Minimalna liczba rekordów to 1.","storage-max-records-pattern":"Numer jest nieprawidłowy.","storage-max-records-required":"Maksymalna liczba rekordów jest wymagana.","storage-read-record-count":"Read record count in storage","storage-read-record-count-min":"Minimum number of records is 1.","storage-read-record-count-pattern":"Number is not valid.","storage-read-record-count-required":"Read record count is required.","storage-max-read-record-count":"Max read record count in storage","storage-max-read-record-count-min":"Minimum number of records is 1.","storage-max-read-record-count-pattern":"Number is not valid.","storage-max-read-record-count-required":"Max Read record count is required.","storage-data-folder-path":"Data folder path","storage-data-folder-path-required":"Data folder path is required.","storage-pack-size":"Maksymalny rozmiar pakietu wydarzeń","storage-pack-size-min":"Minimalna liczba to 1.","storage-pack-size-pattern":"Numer jest nieprawidłowy.","storage-pack-size-required":"Maksymalny rozmiar pakietu wydarzeń jest wymagany.","storage-path":"Ścieżka przechowywania","storage-path-required":"Ścieżka do przechowywania jest wymagana.","storage-type":"Typ składowania","storage-types":{"file-storage":"Nośnik danych","memory-storage":"Przechowywanie pamięci",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"General","thingsboard-host":"Gospodarz ThingsBoard","thingsboard-host-required":"Host jest wymagany.","thingsboard-port":"Port ThingsBoard","thingsboard-port-max":"Maksymalny numer portu to 65535.","thingsboard-port-min":"Minimalny numer portu to 1.","thingsboard-port-pattern":"Port jest nieprawidłowy.","thingsboard-port-required":"Port jest wymagany.",tidy:"Uporządkuj","tidy-tip":"Uporządkowana konfiguracja JSON","title-connectors-json":"Złącze {{typeName}} konfiguracja","tls-path-ca-certificate":"Ścieżka do certyfikatu CA na gateway","tls-path-client-certificate":"Ścieżka do certyfikatu klienta na gateway","messages-ttl-check-in-hours":"Messages TTL check in hours","messages-ttl-check-in-hours-required":"Messages TTL check in hours is required.","messages-ttl-check-in-hours-min":"Min number is 1.","messages-ttl-check-in-hours-pattern":"Number is not valid.","messages-ttl-in-days":"Messages TTL in days","messages-ttl-in-days-required":"Messages TTL in days is required.","messages-ttl-in-days-min":"Min number is 1.","messages-ttl-in-days-pattern":"Number is not valid.","mqtt-qos":"QoS","mqtt-qos-required":"QoS is required","mqtt-qos-range":"QoS values range is from 0 to 1","tls-path-private-key":"Ścieżka do klucza prywatnego na bramce","toggle-fullscreen":"Przełącz tryb pełnoekranowy","transformer-json-config":"Konfiguracja JSON*","update-config":"Dodaj/zaktualizuj konfigurację JSON",hints:{"remote-configuration":"Enables remote configuration and management of the gateway","remote-shell":"Enables remote control of the operating system with the gateway from the Remote Shell widget",host:"Hostname or IP address of ThingsBoard server",port:"Port of MQTT service on ThingsBoard server",token:"Access token for the gateway from ThingsBoard server","client-id":"MQTT client id for the gateway form ThingsBoard server",username:"MQTT username for the gateway form ThingsBoard server",password:"MQTT password for the gateway form ThingsBoard server","ca-cert":"Path to CA certificate file","date-form":"Date format in log message","data-folder":"Path to folder, that will contains data (Relative or Absolute)","log-format":"Log message format","remote-log":"Enables remote logging and logs reading from the gateway","backup-count":"If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",storage:"Provides configuration for saving incoming data before it is sent to the platform","max-file-count":"Maximum count of file that will be created","max-read-count":"Count of messages to get from storage and send to ThingsBoard","max-records":"Maximum count of records that will be stored in one file","read-record-count":"Count of messages to get from storage and send to ThingsBoard","max-records-count":"Maximum count of data in storage before send to ThingsBoard","ttl-check-hour":"How often will Gateway check data for obsolescence","ttl-messages-day":"Maximum days that storage will save data",commands:"Commands for collecting additional statistic",attribute:"Statistic telemetry key",timeout:"Timeout for command executing",command:"The result of the command execution, will be used as the value for telemetry","check-device-activity":"Enables monitor the activity of each connected device","inactivity-timeout":"Time after whose the gateway will disconnect device","inactivity-period":"Periodicity of device activity check","minimal-pack-delay":"Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)",qos:"Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)","server-port":"Network port on which GRPC server will listen for incoming connections.","grpc-keep-alive-timeout":"Maximum time the server should wait for a keepalive ping response before considering the connection dead.","grpc-keep-alive":"Duration between two successive keepalive ping messages when there is no active RPC call.","grpc-min-time-between-pings":"Minimum amount of time the server should wait between sending keepalive ping messages","grpc-max-pings-without-data":"Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.","grpc-min-ping-interval-without-data":"Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.","permit-without-calls":"Allow server to keep the GRPC connection alive even when there are no active RPC calls."}}},Et={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"}},Mt={gateway:{"add-entry":"Dodaj konfiguracijo","connector-add":"Dodaj nov priključek","connector-enabled":"Omogoči priključek","connector-name":"Ime priključka","connector-name-required":"Ime priključka je obvezno.","connector-type":"Vrsta priključka","connector-type-required":"Zahteva se vrsta priključka.",connectors:"Konfiguracija priključkov","create-new-gateway":"Ustvari nov prehod","create-new-gateway-text":"Ali ste prepričani, da želite ustvariti nov prehod z imenom: '{{gatewayName}}'?",delete:"Izbriši konfiguracijo","download-tip":"Prenos konfiguracijske datoteke",gateway:"Prehod","gateway-exists":"Naprava z istim imenom že obstaja.","gateway-name":"Ime prehoda","gateway-name-required":"Ime prehoda je obvezno.","gateway-saved":"Konfiguracija prehoda je uspešno shranjena.","json-parse":"Neveljaven JSON.","json-required":"Polje ne sme biti prazno.","no-connectors":"Ni priključkov","no-data":"Brez konfiguracij","no-gateway-found":"Prehod ni najden.","no-gateway-matching":" '{{item}}' ni mogoče najti.","path-logs":"Pot do dnevniških datotek","path-logs-required":"Pot je obvezna.",remote:"Oddaljena konfiguracija","remote-logging-level":"Raven beleženja","remove-entry":"Odstrani konfiguracijo","save-tip":"Shrani konfiguracijsko datoteko","security-type":"Vrsta zaščite","security-types":{"access-token":"Dostopni žeton",tls:"TLS"},storage:"Shramba","storage-max-file-records":"Največ zapisov v datoteki","storage-max-files":"Največje število datotek","storage-max-files-min":"Najmanjše število je 1.","storage-max-files-pattern":"Številka ni veljavna.","storage-max-files-required":"Številka je obvezna.","storage-max-records":"Največ zapisov v pomnilniku","storage-max-records-min":"Najmanjše število zapisov je 1.","storage-max-records-pattern":"Številka ni veljavna.","storage-max-records-required":"Zahtevan je največ zapisov.","storage-pack-size":"Največja velikost paketa dogodkov","storage-pack-size-min":"Najmanjše število je 1.","storage-pack-size-pattern":"Številka ni veljavna.","storage-pack-size-required":"Zahtevana je največja velikost paketa dogodkov.","storage-path":"Pot pomnilnika","storage-path-required":"Zahtevana je pot do pomnilnika.","storage-type":"Vrsta pomnilnika","storage-types":{"file-storage":"Shramba datotek","memory-storage":"Spomin pomnilnika"},thingsboard:"ThingsBoard","thingsboard-host":"Gostitelj ThingsBoard","thingsboard-host-required":"Potreben je gostitelj.","thingsboard-port":"Vrata ThingsBoard","thingsboard-port-max":"Največja številka vrat je 65535.","thingsboard-port-min":"Najmanjša številka vrat je 1.","thingsboard-port-pattern":"Vrata niso veljavna.","thingsboard-port-required":"Potrebna so vrata.",tidy:"Urejeno","tidy-tip":"Urejena konfiguracija JSON","title-connectors-json":"Konfiguracija konektorja {{typeName}}","tls-path-ca-certificate":"Pot do potrdila CA na prehodu","tls-path-client-certificate":"Pot do potrdila stranke na prehodu","tls-path-private-key":"Pot do zasebnega ključa na prehodu","toggle-fullscreen":"Preklop na celozaslonski način","transformer-json-config":"Konfiguracija JSON *","update-config":"Dodaj / posodobi konfiguracijo JSON"}},kt={gateway:{"add-entry":"Yapılandırma ekle","connector-add":"Yeni bağlayıcı ekle","connector-enabled":"Bağlayıcıyı etkinleştir","connector-name":"Bağlayıcı adı","connector-name-required":"Bağlayıcı adı gerekli.","connector-type":"Bağlayıcı tipi","connector-type-required":"Bağlayıcı türü gerekli.",connectors:"Bağlayıcıların yapılandırması","create-new-gateway":"Yeni bir ağ geçidi oluştur","create-new-gateway-text":"'{{gatewayName}}' adında yeni bir ağ geçidi oluşturmak istediğinizden emin misiniz?",delete:"Yapılandırmayı sil","download-tip":"Yapılandırma dosyasını indirin",gateway:"Ağ geçidi","gateway-exists":"Aynı ada sahip cihaz zaten var.","gateway-name":"Ağ geçidi adı","gateway-name-required":"Ağ geçidi adı gerekli.","gateway-saved":"Ağ geçidi yapılandırması başarıyla kaydedildi.","json-parse":"Geçerli bir JSON değil.","json-required":"Alan boş olamaz.","no-connectors":"Bağlayıcı yok","no-data":"Yapılandırma yok","no-gateway-found":"Ağ geçidi bulunamadı.","no-gateway-matching":" '{{item}}' bulunamadı.","path-logs":"Log dosyaları yolu","path-logs-required":"Log dosyaları dizini gerekli.",remote:"Uzaktan yapılandırma","remote-logging-level":"Loglama seviyesi","remove-entry":"Yapılandırmayı kaldır","save-tip":"Yapılandırma dosyasını kaydet","security-type":"Güvenlik türü","security-types":{"access-token":"Access Token",tls:"TLS"},storage:"Depolama","storage-max-file-records":"Dosyadaki maksimum kayıt","storage-max-files":"Maksimum dosya sayısı","storage-max-files-min":"Minimum sayı 1'dir.","storage-max-files-pattern":"Sayı geçerli değil.","storage-max-files-required":"Sayı gerekli.","storage-max-records":"Depodaki maksimum kayıt","storage-max-records-min":"Minimum kayıt sayısı 1'dir.","storage-max-records-pattern":"Sayı geçerli değil.","storage-max-records-required":"Maksimum kayıt gerekli.","storage-pack-size":"Maksimum etkinlik paketi boyutu","storage-pack-size-min":"Minimum sayı 1'dir.","storage-pack-size-pattern":"Sayı geçerli değil.","storage-pack-size-required":"Maksimum etkinlik paketi boyutu gerekli.","storage-path":"Depolama yolu","storage-path-required":"Depolama yolu gerekli.","storage-type":"Depolama türü","storage-types":{"file-storage":"Dosya depolama","memory-storage":"Bellek depolama"},thingsboard:"ThingsBoard","thingsboard-host":"ThingsBoard host","thingsboard-host-required":"Host gerekli.","thingsboard-port":"ThingsBoard port","thingsboard-port-max":"Maksimum port numarası 65535.","thingsboard-port-min":"Minimum port numarası 1'dir.","thingsboard-port-pattern":"Port geçerli değil.","thingsboard-port-required":"Port gerekli.",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"}},Pt={gateway:{"add-entry":"添加配置",advanced:"高级","checking-device-activity":"检查设备活动",command:"Docker命令","command-copied-message":"Docker命令已复制到剪贴板",configuration:"配置","connector-add":"添加连接器","connector-enabled":"启用连接器","connector-name":"连接器名称","connector-name-required":"连接器名称必填。","connector-type":"连接器类型","connector-type-required":"连接器类型必填。",connectors:"连接器配置","connectors-config":"连接器配置","connectors-table-enabled":"已启用","connectors-table-name":"名称","connectors-table-type":"类型","connectors-table-status":"状态","connectors-table-actions":"操作","connectors-table-key":"键","connectors-table-class":"类","rpc-command-send":"发送","rpc-command-result":"结果","rpc-command-edit-params":"编辑参数","gateway-configuration":"通用配置","create-new-gateway":"创建网关","create-new-gateway-text":"确定要创建名为 '{{gatewayName}}' 的新网关?","created-time":"创建时间","configuration-delete-dialog-header":"配置将被删除","configuration-delete-dialog-body":"只有对网关进行物理访问时,才有可能关闭远程配置。所有先前的配置都将被删除。

\n要关闭配置,请在下面输入网关名称","configuration-delete-dialog-input":"网关名称","configuration-delete-dialog-input-required":"网关名称是必需的","configuration-delete-dialog-confirm":"关闭",delete:"删除配置","download-tip":"下载配置","drop-file":"将文件拖放到此处或",gateway:"网关","gateway-exists":"同名设备已存在。","gateway-name":"网关名称","gateway-name-required":"网关名称必填。","gateway-saved":"已成功保存网关配置。",grpc:"GRPC","grpc-keep-alive-timeout":"保持连接超时(毫秒)","grpc-keep-alive-timeout-required":"需要保持连接超时","grpc-keep-alive-timeout-min":"保持连接超时不能小于1","grpc-keep-alive-timeout-pattern":"保持连接超时无效","grpc-keep-alive":"保持连接(毫秒)","grpc-keep-alive-required":"需要保持连接","grpc-keep-alive-min":"保持连接不能小于1","grpc-keep-alive-pattern":"保持连接无效","grpc-min-time-between-pings":"最小Ping间隔(毫秒)","grpc-min-time-between-pings-required":"需要最小Ping间隔","grpc-min-time-between-pings-min":"最小Ping间隔不能小于1","grpc-min-time-between-pings-pattern":"最小Ping间隔无效","grpc-min-ping-interval-without-data":"无数据时的最小Ping间隔(毫秒)","grpc-min-ping-interval-without-data-required":"需要无数据时的最小Ping间隔","grpc-min-ping-interval-without-data-min":"无数据时的最小Ping间隔不能小于1","grpc-min-ping-interval-without-data-pattern":"无数据时的最小Ping间隔无效","grpc-max-pings-without-data":"无数据时的最大Ping数","grpc-max-pings-without-data-required":"需要无数据时的最大Ping数","grpc-max-pings-without-data-min":"无数据时的最大Ping数不能小于1","grpc-max-pings-without-data-pattern":"无数据时的最大Ping数无效","inactivity-check-period-seconds":"不活跃检查期(秒)","inactivity-check-period-seconds-required":"需要不活跃检查期","inactivity-check-period-seconds-min":"不活跃检查期不能小于1","inactivity-check-period-seconds-pattern":"不活跃检查期无效","inactivity-timeout-seconds":"不活跃超时(秒)","inactivity-timeout-seconds-required":"需要不活跃超时","inactivity-timeout-seconds-min":"不活跃超时不能小于1","inactivity-timeout-seconds-pattern":"不活跃超时无效","json-parse":"无效的JSON。","json-required":"字段不能为空。",logs:{logs:"日志",days:"天",hours:"小时",minutes:"分钟",seconds:"秒","date-format":"日期格式","date-format-required":"需要日期格式","log-format":"日志格式","log-type":"日志类型","log-format-required":"需要日志格式",remote:"远程日志记录","remote-logs":"远程日志",local:"本地日志记录",level:"日志级别","file-path":"文件路径","file-path-required":"需要文件路径","saving-period":"日志保存期限","saving-period-min":"日志保存期限不能小于1","saving-period-required":"需要日志保存期限","backup-count":"备份数量","backup-count-min":"备份数量不能小于1","backup-count-required":"需要备份数量"},"min-pack-send-delay":"最小包发送延迟(毫秒)","min-pack-send-delay-required":"最小包发送延迟是必需的","min-pack-send-delay-min":"最小包发送延迟不能小于0","no-connectors":"无连接器","no-data":"没有配置","no-gateway-found":"未找到网关。","no-gateway-matching":"未找到 '{{item}}' 。","path-logs":"日志文件的路径","path-logs-required":"路径是必需的。","permit-without-calls":"保持连接许可,无需响应",remote:"远程配置","remote-logging-level":"日志记录级别","remove-entry":"删除配置","remote-shell":"远程Shell","remote-configuration":"远程配置",other:"其他","save-tip":"保存配置","security-type":"安全类型","security-types":{"access-token":"访问令牌","username-password":"用户名和密码",tls:"TLS","tls-access-token":"TLS + 访问令牌","tls-private-key":"TLS + 私钥"},"server-port":"服务器端口",statistics:{statistic:"统计信息",statistics:"统计信息","statistic-commands-empty":"无可用统计信息",commands:"命令","send-period":"统计信息发送周期(秒)","send-period-required":"统计信息发送周期是必需的","send-period-min":"统计信息发送周期不能小于60","send-period-pattern":"统计信息发送周期无效","check-connectors-configuration":"检查连接器配置(秒)","check-connectors-configuration-required":"检查连接器配置是必需的","check-connectors-configuration-min":"检查连接器配置不能小于1","check-connectors-configuration-pattern":"检查连接器配置无效",add:"添加命令",timeout:"超时时间","timeout-required":"超时时间是必需的","timeout-min":"超时时间不能小于1","timeout-pattern":"超时时间无效","attribute-name":"属性名称","attribute-name-required":"属性名称是必需的",command:"命令","command-required":"命令是必需的","command-pattern":"命令无效",remove:"删除命令"},storage:"存储","storage-max-file-records":"文件中的最大记录数","storage-max-files":"最大文件数","storage-max-files-min":"最小值为1。","storage-max-files-pattern":"数字无效。","storage-max-files-required":"数字是必需的。","storage-max-records":"存储中的最大记录数","storage-max-records-min":"最小记录数为1。","storage-max-records-pattern":"数字无效。","storage-max-records-required":"最大记录项必填。","storage-read-record-count":"存储中的读取记录数","storage-read-record-count-min":"最小记录数为1。","storage-read-record-count-pattern":"数字不合法。","storage-read-record-count-required":"需要读取记录数。","storage-max-read-record-count":"存储中的最大读取记录数","storage-max-read-record-count-min":"最小记录数为1。","storage-max-read-record-count-pattern":"数字不合法。","storage-max-read-record-count-required":"最大读取记录数必需。","storage-data-folder-path":"数据文件夹路径","storage-data-folder-path-required":"需要数据文件夹路径。","storage-pack-size":"最大事件包大小","storage-pack-size-min":"最小值为1。","storage-pack-size-pattern":"数字无效。","storage-pack-size-required":"最大事件包大小必填。","storage-path":"存储路径","storage-path-required":"存储路径必填。","storage-type":"存储类型","storage-types":{"file-storage":"文件存储","memory-storage":"内存存储",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"常规","thingsboard-host":"ThingsBoard主机","thingsboard-host-required":"主机必填。","thingsboard-port":"ThingsBoard端口","thingsboard-port-max":"最大端口号为65535。","thingsboard-port-min":"最小端口号为1。","thingsboard-port-pattern":"端口无效。","thingsboard-port-required":"端口必填。",tidy:"整理","tidy-tip":"整理配置JSON","title-connectors-json":"连接器 {{typeName}} 配置","tls-path-ca-certificate":"网关上CA证书的路径","tls-path-client-certificate":"网关上客户端证书的路径","messages-ttl-check-in-hours":"消息TTL检查小时数","messages-ttl-check-in-hours-required":"需要提供消息TTL检查小时数。","messages-ttl-check-in-hours-min":"最小值为1。","messages-ttl-check-in-hours-pattern":"数字无效。","messages-ttl-in-days":"消息TTL天数","messages-ttl-in-days-required":"需要提供消息TTL天数。","messages-ttl-in-days-min":"最小值为1。","messages-ttl-in-days-pattern":"数字无效。","mqtt-qos":"QoS","mqtt-qos-required":"需要提供QoS","mqtt-qos-range":"QoS值的范围是从0到1","tls-path-private-key":"网关上私钥的路径","toggle-fullscreen":"切换全屏","transformer-json-config":"配置JSON*","update-config":"添加/更新配置JSON",hints:{"remote-configuration":"启用对网关的远程配置和管理","remote-shell":"通过远程Shell小部件启用对网关操作系统的远程控制",host:"ThingsBoard 主机名或IP地址",port:"ThingsBoard MQTT服务端口",token:"ThingsBoard 网关访问令牌","client-id":"ThingsBoard 网关MQTT客户端ID",username:"ThingsBoard 网关MQTT用户名",password:"ThingsBoard 网关MQTT密码","ca-cert":"CA证书文件的路径","date-form":"日志消息中的日期格式","data-folder":"包含数据的文件夹的路径(相对或绝对路径)","log-format":"日志消息格式","remote-log":"启用对网关的远程日志记录和日志读取","backup-count":"如果备份计数大于0,则在执行轮换时,最多保留备份计数个文件-最旧的文件将被删除",storage:"提供将数据发送到平台之前保存传入数据的配置","max-file-count":"将创建的文件的最大数量","max-read-count":"从存储中获取的消息计数并发送到ThingsBoard","max-records":"一个文件中存储的最大记录数","read-record-count":"从存储中获取的消息计数并发送到ThingsBoard","max-records-count":"在将数据发送到ThingsBoard之前,存储中的最大数据计数","ttl-check-hour":"网关多久检查一次数据是否过时","ttl-messages-day":"存储将保存数据的最大天数",commands:"用于收集附加统计信息的命令",attribute:"统计遥测键",timeout:"命令执行的超时时间",command:"命令执行的结果,将用作遥测的值","check-device-activity":"启用监视每个连接设备的活动","inactivity-timeout":"在此时间后,网关将断开设备的连接","inactivity-period":"设备活动检查的周期","minimal-pack-delay":"发送消息包之间的延迟(减小此设置会导致增加CPU使用率)",qos:"MQTT消息传递的服务质量(0-至多一次,1-至少一次)","server-port":"GRPC服务器侦听传入连接的网络端口","grpc-keep-alive-timeout":"在考虑连接死亡之前,服务器等待keepalive ping响应的最长时间","grpc-keep-alive":"没有活动RPC调用时两个连续keepalive ping消息之间的持续时间","grpc-min-time-between-pings":"服务器在发送keepalive ping消息之间应等待的最小时间量","grpc-max-pings-without-data":"在没有接收到任何数据之前,服务器可以发送的keepalive ping消息的最大数量,然后将连接视为死亡","grpc-min-ping-interval-without-data":"在没有发送或接收数据时,服务器在发送keepalive ping消息之间应等待的最小时间量","permit-without-calls":"允许服务器在没有活动RPC调用时保持GRPC连接活动"},"docker-label":"使用以下指令在 Docker compose 中运行 IoT 网关,并为选定的设备提供凭据","install-docker-compose":"使用以下说明下载、安装和设置 Docker Compose","download-configuration-file":"下载配置文件","download-docker-compose":"下载您的网关的 docker-compose.yml 文件","launch-gateway":"启动网关","launch-docker-compose":"在包含 docker-compose.yml 文件的文件夹中,使用以下命令在终端中启动网关"}},Dt={gateway:{"add-entry":"增加配置","connector-add":"增加新連接器","connector-enabled":"啟用連接器","connector-name":"連接器名稱","connector-name-required":"需要連接器名稱。","connector-type":"連接器類型","connector-type-required":"需要連接器類型。",connectors:"連接器配置","create-new-gateway":"建立新閘道","create-new-gateway-text":"您確定要建立一個名稱為:'{{gatewayName}}'的新閘道嗎?",delete:"刪除配置","download-tip":"下載配置文件",gateway:"閘道","gateway-exists":"同名設備已存在。","gateway-name":"閘道名稱","gateway-name-required":"需要閘道名稱。","gateway-saved":"閘道配置已成功保存。","json-parse":"無效的JSON","json-required":"欄位不能為空。","no-connectors":"無連接器","no-data":"無配置","no-gateway-found":"未找到閘道。","no-gateway-matching":" 未找到'{{item}}'。","path-logs":"日誌文件的路徑","path-logs-required":"需要路徑。",remote:"移除配置","remote-logging-level":"日誌記錄級別","remove-entry":"移除配置","save-tip":"保存配置文件","security-type":"安全類型","security-types":{"access-token":"訪問Token",tls:"TLS"},storage:"貯存","storage-max-file-records":"文件中的最大紀錄","storage-max-files":"最大文件數","storage-max-files-min":"最小數量為1。","storage-max-files-pattern":"號碼無效。","storage-max-files-required":"需要號碼。","storage-max-records":"存儲中的最大紀錄","storage-max-records-min":"最小紀錄數為1。","storage-max-records-pattern":"號碼無效。","storage-max-records-required":"需要最大紀錄數","storage-pack-size":"最大事件包大小","storage-pack-size-min":"最小數量為1。","storage-pack-size-pattern":"號碼無效.","storage-pack-size-required":"需要最大事件包大小","storage-path":"存儲路徑","storage-path-required":"需要存儲路徑。","storage-type":"存儲類型","storage-types":{"file-storage":"文件存儲","memory-storage":"記憶體存儲"},thingsboard:"ThingsBoard","thingsboard-host":"ThingsBoard主機","thingsboard-host-required":"需要主機。","thingsboard-port":"ThingsBoard連接埠","thingsboard-port-max":"最大埠號為 65535。","thingsboard-port-min":"最小埠號為1。","thingsboard-port-pattern":"連接埠無效。","thingsboard-port-required":"需要連接埠。",tidy:"整理","tidy-tip":"整理配置JSON","title-connectors-json":"連接器{{typeName}}配置","tls-path-ca-certificate":"閘道上CA證書的路徑","tls-path-client-certificate":"閘道上用戶端憑據的路徑","tls-path-private-key":"閘道上的私鑰路徑","toggle-fullscreen":"切換全螢幕","transformer-json-config":"配置JSON*","update-config":"增加/更新配置JSON"}};e("AvailableLanguages",ft),function(e){e.English="en_US",e.Arabic="ar_AE",e.Catalan="ca_ES",e.Czech="cs_CZ",e.Danish="da_DK",e.Spanish="es_ES",e.Korean="ko_KR",e.Lithuanian="lt_LT",e.Dutch="nl_BE",e.Polish="pl_PL",e.PortugueseBrazil="pt_BR",e.Slovenian="sl_SI",e.Turkish="tr_TR",e.ChineseSimplified="zh_CN",e.ChineseTraditional="zh_TW"}(ft||e("AvailableLanguages",ft={}));const Ot=new Map([[ft.English,yt],[ft.Arabic,vt],[ft.Catalan,xt],[ft.Czech,bt],[ft.Danish,wt],[ft.Spanish,St],[ft.Korean,Ct],[ft.Lithuanian,_t],[ft.Dutch,Tt],[ft.Polish,It],[ft.PortugueseBrazil,Et],[ft.Slovenian,Mt],[ft.Turkish,kt],[ft.ChineseSimplified,Pt],[ft.ChineseTraditional,Dt]]),At=e("addGatewayLocale",(e=>{const t=ft.English,n=e.currentLang??t,i=e.translations[n]?.gateway?n:t,a=Ot.get(n)?.gateway?n:t,r=[a===t?void 0:Ot.get(t)?.gateway,Ot.get(a)?.gateway,e.translations[i]?.gateway].filter(Ft);if(!r.length)return;const o=1===r.length?r[0]:j({},...r);e.setTranslation(i,{gateway:o},!0)})),Ft=e=>H(e)&&0!==Object.keys(e).length;var Rt={3.6:{socket:{type:"TCP",address:"127.0.0.1",port:5e4,bufferSize:1024},devices:[{address:"*:*",deviceName:"Device Example",deviceType:"default",encoding:"utf-8",telemetry:[{key:"temp",byteFrom:0,byteTo:-1},{key:"hum",byteFrom:0,byteTo:2}],attributes:[{key:"name",byteFrom:0,byteTo:-1},{key:"num",byteFrom:2,byteTo:4}],attributeRequests:[{type:"shared",requestExpressionSource:"constant",attributeNameExpressionSource:"constant",requestExpression:"${[0:3]==atr}",attributeNameExpression:"[3:]"}],attributeUpdates:[{encoding:"utf-16",attributeOnThingsBoard:"sharedName"}],serverSideRpc:[{methodRPC:"rpcMethod1",withResponse:!0,encoding:"utf-8"}]}]},legacy:{type:"TCP",address:"127.0.0.1",port:5e4,bufferSize:1024,devices:[{address:"*:*",deviceName:"Device Example",deviceType:"default",encoding:"utf-8",telemetry:[{key:"temp",byteFrom:0,byteTo:-1},{key:"hum",byteFrom:0,byteTo:2}],attributes:[{key:"name",byteFrom:0,byteTo:-1},{key:"num",byteFrom:2,byteTo:4}],attributeRequests:[{type:"shared",requestExpression:"${[0:3]==atr}",attributeNameExpression:"[3:]"}],attributeUpdates:[{encoding:"utf-16",attributeOnThingsBoard:"sharedName"}],serverSideRpc:[{methodRPC:"rpcMethod1",withResponse:!0,methodProcessing:"write",encoding:"utf-8"}]}]}},Bt={"3.5.2":{broker:{host:"127.0.0.1",port:1883,clientId:"ThingsBoard_gateway",version:5,maxMessageNumberPerWorker:10,maxNumberOfWorkers:100,sendDataOnlyOnChange:!1,security:{type:"anonymous"}},mapping:[{topicFilter:"sensor/data",subscriptionQos:1,converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}",deviceProfileExpressionSource:"message",deviceProfileExpression:"${sensorType}"},sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"${sensorModel}",value:"on"}],timeseries:[{type:"string",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"},{type:"string",key:"combine",value:"${hum}:${temp}"}]}},{topicFilter:"sensor/+/data",subscriptionQos:1,converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"topic",deviceNameExpression:"(?<=sensor/)(.*?)(?=/data)",deviceProfileExpressionSource:"constant",deviceProfileExpression:"Thermometer"},sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"string",key:"humidity",value:"${hum}"}]}},{topicFilter:"sensor/raw_data",subscriptionQos:1,converter:{type:"bytes",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"[0:4]",deviceProfileExpressionSource:"constant",deviceProfileExpression:"default"},sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"raw",key:"rawData",value:"[:]"}],timeseries:[{type:"raw",key:"temp",value:"[4:]"}]}},{topicFilter:"custom/sensors/+",subscriptionQos:1,converter:{type:"custom",extension:"CustomMqttUplinkConverter",cached:!0,extensionConfig:{temperature:2,humidity:2,batteryLevel:1}}}],requestsMapping:{connectRequests:[{topicFilter:"sensor/connect",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}",deviceProfileExpressionSource:"constant",deviceProfileExpression:"Thermometer"}},{topicFilter:"sensor/+/connect",deviceInfo:{deviceNameExpressionSource:"topic",deviceNameExpression:"(?<=sensor/)(.*?)(?=/connect)",deviceProfileExpressionSource:"constant",deviceProfileExpression:"Thermometer"}}],disconnectRequests:[{topicFilter:"sensor/disconnect",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}"}},{topicFilter:"sensor/+/disconnect",deviceInfo:{deviceNameExpressionSource:"topic",deviceNameExpression:"(?<=sensor/)(.*?)(?=/connect)"}}],attributeRequests:[{retain:!1,topicFilter:"v1/devices/me/attributes/request",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}"},attributeNameExpressionSource:"message",attributeNameExpression:"${versionAttribute}, ${pduAttribute}",topicExpression:"devices/${deviceName}/attrs",valueExpression:"${attributeKey}: ${attributeValue}"}],attributeUpdates:[{retain:!0,deviceNameFilter:".*",attributeFilter:"firmwareVersion",topicExpression:"sensor/${deviceName}/${attributeKey}",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{type:"twoWay",deviceNameFilter:".*",methodFilter:"echo",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",responseTopicExpression:"sensor/${deviceName}/response/${methodName}/${requestId}",responseTopicQoS:1,responseTimeout:1e4,valueExpression:"${params}"},{type:"oneWay",deviceNameFilter:".*",methodFilter:"no-reply",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",valueExpression:"${params}"}]}},legacy:{broker:{name:"Default Local Broker",host:"127.0.0.1",port:1883,clientId:"ThingsBoard_gateway",version:5,maxMessageNumberPerWorker:10,maxNumberOfWorkers:100,sendDataOnlyOnChange:!1,security:{type:"basic",username:"user",password:"password"}},mapping:[{topicFilter:"sensor/data",converter:{type:"json",deviceNameJsonExpression:"${serialNumber}",deviceTypeJsonExpression:"${sensorType}",sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"${sensorModel}",value:"on"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"},{type:"string",key:"combine",value:"${hum}:${temp}"}]}},{topicFilter:"sensor/+/data",converter:{type:"json",deviceNameTopicExpression:"(?<=sensor/)(.*?)(?=/data)",deviceTypeTopicExpression:"Thermometer",sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{topicFilter:"sensor/raw_data",converter:{type:"bytes",deviceNameExpression:"[0:4]",deviceTypeExpression:"default",sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"raw",key:"rawData",value:"[:]"}],timeseries:[{type:"raw",key:"temp",value:"[4:]"}]}},{topicFilter:"custom/sensors/+",converter:{type:"custom",extension:"CustomMqttUplinkConverter",cached:!0,"extension-config":{temperatureBytes:2,humidityBytes:2,batteryLevelBytes:1}}}],connectRequests:[{topicFilter:"sensor/connect",deviceNameJsonExpression:"${serialNumber}"},{topicFilter:"sensor/+/connect",deviceNameTopicExpression:"(?<=sensor/)(.*?)(?=/connect)"}],disconnectRequests:[{topicFilter:"sensor/disconnect",deviceNameJsonExpression:"${serialNumber}"},{topicFilter:"sensor/+/disconnect",deviceNameTopicExpression:"(?<=sensor/)(.*?)(?=/disconnect)"}],attributeRequests:[{retain:!1,topicFilter:"v1/devices/me/attributes/request",deviceNameJsonExpression:"${serialNumber}",attributeNameJsonExpression:"${versionAttribute}, ${pduAttribute}",topicExpression:"devices/${deviceName}/attrs",valueExpression:"${attributeKey}: ${attributeValue}"}],attributeUpdates:[{retain:!0,deviceNameFilter:".*",attributeFilter:"firmwareVersion",topicExpression:"sensor/${deviceName}/${attributeKey}",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",responseTopicExpression:"sensor/${deviceName}/response/${methodName}/${requestId}",responseTimeout:1e4,valueExpression:"${params}"},{deviceNameFilter:".*",methodFilter:"no-reply",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",valueExpression:"${params}"}]}},Nt={"3.5.2":{master:{slaves:[{host:"127.0.0.1",port:5021,type:"tcp",method:"socket",timeout:35,byteOrder:"LITTLE",wordOrder:"LITTLE",retries:3,retryOnEmpty:!0,retryOnInvalid:!0,pollPeriod:5e3,unitId:1,deviceName:"Temp Sensor",deviceType:"default",connectAttemptTimeMs:5e3,connectAttemptCount:5,waitAfterFailedAttemptsMs:3e5,reportStrategy:{type:"ON_REPORT_PERIOD",reportPeriod:3e4},attributes:[{tag:"string_read",type:"string",functionCode:4,objectsCount:4,address:1,reportStrategy:{type:"ON_REPORT_PERIOD",reportPeriod:15e3}},{tag:"bits_read",type:"bits",functionCode:4,objectsCount:1,address:5},{tag:"8int_read",type:"8int",functionCode:4,objectsCount:1,address:6},{tag:"16int_read",type:"16int",functionCode:4,objectsCount:1,address:7},{tag:"32int_read_divider",type:"32int",functionCode:4,objectsCount:2,address:8,divider:10},{tag:"8int_read_multiplier",type:"8int",functionCode:4,objectsCount:1,address:10,multiplier:10},{tag:"32int_read",type:"32int",functionCode:4,objectsCount:2,address:11},{tag:"64int_read",type:"64int",functionCode:4,objectsCount:4,address:13}],timeseries:[{tag:"8uint_read",type:"8uint",functionCode:4,objectsCount:1,address:17,reportStrategy:{type:"ON_REPORT_PERIOD",reportPeriod:15e3}},{tag:"16uint_read",type:"16uint",functionCode:4,objectsCount:2,address:18},{tag:"32uint_read",type:"32uint",functionCode:4,objectsCount:4,address:20},{tag:"64uint_read",type:"64uint",functionCode:4,objectsCount:1,address:24},{tag:"16float_read",type:"16float",functionCode:4,objectsCount:1,address:25},{tag:"32float_read",type:"32float",functionCode:4,objectsCount:2,address:26},{tag:"64float_read",type:"64float",functionCode:4,objectsCount:4,address:28}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31},{tag:"getValue",type:"bits",functionCode:1,objectsCount:1,address:31},{tag:"setCPUFanSpeed",type:"32int",functionCode:16,objectsCount:2,address:33},{tag:"getCPULoad",type:"32int",functionCode:4,objectsCount:2,address:35}]}]},slave:{type:"tcp",host:"127.0.0.1",port:5026,method:"socket",deviceName:"Modbus Slave Example",deviceType:"default",pollPeriod:5e3,sendDataToThingsBoard:!1,byteOrder:"LITTLE",wordOrder:"LITTLE",unitId:0,values:{holding_registers:{attributes:[{address:1,type:"string",tag:"sm",objectsCount:1,value:"ON"}],timeseries:[{address:2,type:"8int",tag:"smm",objectsCount:1,value:"12334"}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29,value:1243}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31,value:1}]},coils_initializer:{attributes:[{address:5,type:"8int",tag:"coil",objectsCount:1,value:0}],timeseries:[],attributeUpdates:[],rpc:[]}}}},legacy:{master:{slaves:[{host:"127.0.0.1",port:5021,type:"tcp",method:"socket",timeout:35,byteOrder:"LITTLE",wordOrder:"LITTLE",retries:3,retryOnEmpty:!0,retryOnInvalid:!0,pollPeriod:5e3,unitId:1,deviceName:"Temp Sensor",deviceType:"default",sendDataOnlyOnChange:!0,connectAttemptTimeMs:5e3,connectAttemptCount:5,waitAfterFailedAttemptsMs:3e5,attributes:[{tag:"string_read",type:"string",functionCode:4,objectsCount:4,address:1},{tag:"bits_read",type:"bits",functionCode:4,objectsCount:1,address:5},{tag:"16int_read",type:"16int",functionCode:4,objectsCount:1,address:7},{tag:"32int_read_divider",type:"32int",functionCode:4,objectsCount:2,address:8,divider:10},{tag:"32int_read",type:"32int",functionCode:4,objectsCount:2,address:11},{tag:"64int_read",type:"64int",functionCode:4,objectsCount:4,address:13}],timeseries:[{tag:"16uint_read",type:"16uint",functionCode:4,objectsCount:2,address:18},{tag:"32uint_read",type:"32uint",functionCode:4,objectsCount:4,address:20},{tag:"64uint_read",type:"64uint",functionCode:4,objectsCount:1,address:24},{tag:"16float_read",type:"16float",functionCode:4,objectsCount:1,address:25},{tag:"32float_read",type:"32float",functionCode:4,objectsCount:2,address:26},{tag:"64float_read",type:"64float",functionCode:4,objectsCount:4,address:28}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31},{tag:"getValue",type:"bits",functionCode:1,objectsCount:1,address:31},{tag:"setCPUFanSpeed",type:"32int",functionCode:16,objectsCount:2,address:33},{tag:"getCPULoad",type:"32int",functionCode:4,objectsCount:2,address:35}]}]},slave:{type:"tcp",host:"127.0.0.1",port:5026,method:"socket",deviceName:"Modbus Slave Example",deviceType:"default",pollPeriod:5e3,sendDataToThingsBoard:!1,byteOrder:"LITTLE",wordOrder:"LITTLE",unitId:0,values:{holding_registers:[{attributes:[{address:1,type:"string",tag:"sm",objectsCount:1,value:"ON"}],timeseries:[{address:2,type:"int",tag:"smm",objectsCount:1,value:"12334"}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29,value:1243}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31,value:1}]}],coils_initializer:[{attributes:[{address:5,type:"string",tag:"sm",objectsCount:1,value:"12"}],timeseries:[],attributeUpdates:[],rpc:[]}]}}}},Lt={"3.5.2":{server:{url:"localhost:4840/freeopcua/server/",timeoutInMillis:5e3,scanPeriodInMillis:36e5,pollPeriodInMillis:5e3,enableSubscriptions:!0,subCheckPeriodInMillis:100,showMap:!1,security:"Basic128Rsa15",identity:{type:"anonymous"}},mapping:[{deviceNodePattern:"Root\\.Objects\\.Device1",deviceNodeSource:"path",deviceInfo:{deviceNameExpression:"Device ${Root\\.Objects\\.Device1\\.serialNumber}",deviceNameExpressionSource:"path",deviceProfileExpression:"Device",deviceProfileExpressionSource:"constant"},attributes:[{key:"temperature °C",type:"path",value:"${ns=2;i=5}"}],timeseries:[{key:"humidity",type:"path",value:"${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"},{key:"batteryLevel",type:"path",value:"${Battery\\.batteryLevel}"}],rpc_methods:[{method:"multiply",arguments:[{type:"integer",value:2},{type:"integer",value:4}]}],attributes_updates:[{key:"deviceName",type:"path",value:"Root\\.Objects\\.Device1\\.serialNumber"}]}]},legacy:{server:{name:"OPC-UA Default Server",url:"localhost:4840/freeopcua/server/",timeoutInMillis:5e3,scanPeriodInMillis:5e3,disableSubscriptions:!1,subCheckPeriodInMillis:100,showMap:!1,security:"Basic128Rsa15",identity:{type:"anonymous"},mapping:[{deviceNodePattern:"Root\\.Objects\\.Device1",deviceNamePattern:"Device ${Root\\.Objects\\.Device1\\.serialNumber}",attributes:[{key:"temperature °C",path:"${ns=2;i=5}"}],timeseries:[{key:"humidity",path:"${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"},{key:"batteryLevel",path:"${Battery\\.batteryLevel}"}],rpc_methods:[{method:"multiply",arguments:[2,4]}],attributes_updates:[{attributeOnThingsBoard:"deviceName",attributeOnDevice:"Root\\.Objects\\.Device1\\.serialNumber"}]}]}}},Vt={passiveScanMode:!0,showMap:!1,scanner:{timeout:1e4,deviceName:"Device name"},devices:[{name:"Temperature and humidity sensor",MACAddress:"4C:65:A8:DF:85:C0",pollPeriod:5e5,showMap:!1,timeout:1e4,connectRetry:5,connectRetryInSeconds:0,waitAfterConnectRetries:10,telemetry:[{key:"temperature",method:"notify",characteristicUUID:"226CAA55-6476-4566-7562-66734470666D",valueExpression:"[2]"},{key:"humidity",method:"notify",characteristicUUID:"226CAA55-6476-4566-7562-66734470666D",valueExpression:"[0]"}],attributes:[{key:"name",method:"read",characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",valueExpression:"[0:2]cm [2:]A"},{key:"values",method:"read",characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",valueExpression:"All values: [:]"}],attributeUpdates:[{attributeOnThingsBoard:"sharedName",characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB"}],serverSideRpc:[{methodRPC:"rpcMethod1",withResponse:!0,characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",methodProcessing:"read"},{methodRPC:"rpcMethod2",withResponse:!0,characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",methodProcessing:"write"},{methodRPC:"rpcMethod3",withResponse:!0,methodProcessing:"scan"}]}]},qt={host:"http://127.0.0.1:5000",SSLVerify:!0,security:{type:"basic",username:"user",password:"password"},mapping:[{url:"getdata",httpMethod:"GET",httpHeaders:{ACCEPT:"application/json"},content:{name:"morpheus",job:"leader"},allowRedirects:!0,timeout:.5,scanPeriod:5,converter:{type:"json",deviceNameJsonExpression:"SD8500",deviceTypeJsonExpression:"SD",attributes:[{key:"serialNumber",type:"string",value:"${serial}"}],telemetry:[{key:"Maintainer",type:"string",value:"${Developer}"},{key:"combine",type:"string",value:"${Developer}:${hum}"}]}},{url:"get_info",httpMethod:"GET",httpHeaders:{ACCEPT:"application/json"},allowRedirects:!0,timeout:.5,scanPeriod:100,converter:{type:"custom",deviceNameJsonExpression:"SD8500",deviceTypeJsonExpression:"SD",extension:"CustomRequestUplinkConverter","extension-config":[{key:"Totaliser",type:"float",fromByte:0,toByte:4,byteorder:"big",signed:!0,multiplier:1},{key:"Flow",type:"int",fromByte:4,toByte:6,byteorder:"big",signed:!0,multiplier:.01},{key:"Temperature",type:"int",fromByte:8,toByte:10,byteorder:"big",signed:!0,multiplier:.01},{key:"Pressure",type:"int",fromByte:12,toByte:14,byteorder:"big",signed:!0,multiplier:.01},{key:"deviceStatus",type:"int",byteAddress:15,fromBit:4,toBit:8,byteorder:"big",signed:!1},{key:"OUT2",type:"int",byteAddress:15,fromBit:1,toBit:2,byteorder:"big"},{key:"OUT1",type:"int",byteAddress:15,fromBit:0,toBit:1,byteorder:"big"}]}}],attributeUpdates:[{httpMethod:"POST",httpHeaders:{"CONTENT-TYPE":"application/json"},timeout:.5,tries:3,allowRedirects:!0,deviceNameFilter:"SD.*",attributeFilter:"send_data",requestUrlExpression:"sensor/${deviceName}/${attributeKey}",requestValueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",responseTimeout:1,httpMethod:"GET",requestValueExpression:"${params}",responseValueExpression:"${temp}",timeout:.5,tries:3,httpHeaders:{"Content-Type":"application/json"}},{deviceNameFilter:".*",methodFilter:"no-reply",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",httpMethod:"POST",requestValueExpression:"${params}",httpHeaders:{"Content-Type":"application/json"}}]},Gt={interface:"socketcan",channel:"vcan0",backend:{fd:!0},reconnectPeriod:5,devices:[{name:"Car",sendDataOnlyOnChange:!1,enableUnknownRpc:!0,strictEval:!1,attributes:[{key:"isDriverDoorOpened",nodeId:41,command:"2:2:big:8717",value:"4:1:int",expression:"bool(value & 0b00000100)",polling:{type:"once",dataInHex:"AB CD AB CD"}}],timeseries:[{key:"rpm",nodeId:1918,isExtendedId:!0,command:"2:2:big:48059",value:"4:2:big:int",expression:"value / 4",polling:{type:"always",period:5,dataInHex:"aaaa bbbb aaaa bbbb"}},{key:"milliage",nodeId:1918,isExtendedId:!0,value:"4:2:little:int",expression:"value * 10",polling:{type:"always",period:30,dataInHex:"aa bb cc dd ee ff aa bb"}}],attributeUpdates:[{attributeOnThingsBoard:"softwareVersion",nodeId:64,isExtendedId:!0,dataLength:4,dataExpression:"value + 5",dataByteorder:"little"}],serverSideRpc:[{method:"sendSameData",nodeId:4,isExtendedId:!0,isFd:!0,bitrateSwitch:!0,dataInHex:"aa bb cc dd ee ff aa bb aa bb cc d ee ff"},{method:"setLightLevel",nodeId:5,dataLength:2,dataByteorder:"little",dataBefore:"00AA"},{method:"setSpeed",nodeId:16,dataAfter:"0102",dataExpression:"userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed"}]}]},zt={"3.6.2":{application:{objectName:"TB_gateway",host:"0.0.0.0",port:47808,objectIdentifier:599,maxApduLengthAccepted:1476,segmentationSupported:"segmentedBoth",vendorIdentifier:15},devices:[{deviceInfo:{deviceNameExpression:"BACnet Device ${objectName}",deviceProfileExpression:"default",deviceNameExpressionSource:"expression",deviceProfileExpressionSource:"constant"},host:"192.168.2.110",port:"47808",pollPeriod:1e4,attributes:[{key:"temperature",objectType:"analogOutput",objectId:1,propertyId:"presentValue"}],timeseries:[{key:"state",objectType:"binaryValue",objectId:1,propertyId:"presentValue"}],attributeUpdates:[{key:"brightness",objectType:"analogOutput",objectId:1,propertyId:"presentValue"}],serverSideRpc:[{method:"set_state",requestType:"writeProperty",requestTimeout:1e4,objectType:"binaryOutput",objectId:1,propertyId:"presentValue"},{method:"get_state",requestType:"readProperty",requestTimeout:1e4,objectType:"binaryOutput",objectId:1,propertyId:"presentValue"}]}]},legacy:{general:{objectName:"TB_gateway",address:"0.0.0.0:47808",objectIdentifier:599,maxApduLengthAccepted:1476,segmentationSupported:"segmentedBoth",vendorIdentifier:15},devices:[{deviceName:"BACnet Device ${objectName}",deviceType:"default",address:"192.168.2.110:47808",pollPeriod:1e4,attributes:[{key:"temperature",type:"string",objectId:"analogOutput:1",propertyId:"presentValue"}],timeseries:[{key:"state",type:"bool",objectId:"binaryValue:1",propertyId:"presentValue"}],attributeUpdates:[{key:"brightness",requestType:"writeProperty",objectId:"analogOutput:1",propertyId:"presentValue"}],serverSideRpc:[{method:"set_state",requestType:"writeProperty",requestTimeout:1e4,objectId:"binaryOutput:1",propertyId:"presentValue"},{method:"get_state",requestType:"readProperty",requestTimeout:1e4,objectId:"binaryOutput:1",propertyId:"presentValue"}]}]}},Ut={connection:{str:"Driver={PostgreSQL};Server=localhost;Port=5432;Database=thingsboard;Uid=postgres;Pwd=postgres;",attributes:{autocommit:!0,timeout:0},encoding:"utf-8",decoding:{char:"utf-8",wchar:"utf-8",metadata:"utf-16le"},reconnect:!0,reconnectPeriod:60},pyodbc:{pooling:!1},polling:{query:"SELECT bool_v, str_v, dbl_v, long_v, entity_id, ts FROM ts_kv WHERE ts > ? ORDER BY ts ASC LIMIT 10",period:10,iterator:{column:"ts",query:"SELECT MIN(ts) - 1 FROM ts_kv",persistent:!1}},mapping:{device:{type:"postgres",name:"'ODBC ' + entity_id"},sendDataOnlyOnChange:!1,attributes:"*",timeseries:[{name:"value",value:"[i for i in [str_v, long_v, dbl_v,bool_v] if i is not None][0]"}]},serverSideRpc:{enableUnknownRpc:!1,overrideRpcConfig:!0,methods:["procedureOne",{name:"procedureTwo",args:["One",2,3]}]}},jt={"3.7.2":{server:{host:"127.0.0.1",port:"5000",SSL:!1,security:{cert:"~/ssl/cert.pem",key:"~/ssl/key.pem"}},mapping:[{endpoint:"/my_devices",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"request",deviceNameExpression:"${sensorName}",deviceProfileExpressionSource:"request",deviceProfileExpression:"${sensorType}"},attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"certificateNumber",value:"${certificateNumber}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon1",HTTPMethods:["GET","POST"],security:{type:"anonymous"},converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"constant",deviceNameExpression:"Device 2",deviceProfileExpressionSource:"constant",deviceProfileExpression:"default"},attributes:[{type:"string",key:"model",value:"Model2"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon2",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"custom",deviceInfo:{deviceNameExpressionSource:"constant",deviceNameExpression:"SuperAnonDevice",deviceProfileExpressionSource:"constant",deviceProfileExpression:"default"},extension:"CustomRestUplinkConverter",extensionConfig:{key:"Totaliser",datatype:"float",fromByte:0,toByte:4,byteorder:"big",signed:!0,multiplier:1}}}],requestsMapping:{attributeRequests:[{endpoint:"/sharedAttributes",type:"shared",HTTPMethods:["POST"],security:{type:"anonymous"},timeout:10,deviceNameExpression:"${deviceName}",attributeNameExpression:"${attribute}${attribute1}"}],attributeUpdates:[{HTTPMethod:"POST",SSLVerify:!1,httpHeaders:{"CONTENT-TYPE":"application/json"},security:{type:"anonymous"},timeout:.5,tries:3,allowRedirects:!0,deviceNameFilter:"SN.*",attributeFilter:".*",requestUrlExpression:"http://127.0.0.1:5001/",valueExpression:'{"deviceName":"${deviceName}","${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestUrlExpression:"http://127.0.0.1:5001/${deviceName}",responseTimeout:1,HTTPMethod:"GET",valueExpression:"${params}",timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:"SN.*",methodFilter:"post_attributes",requestUrlExpression:"http://127.0.0.1:5000/my_devices",responseTimeout:1,HTTPMethod:"POST",valueExpression:'{"sensorName":"${deviceName}", "sensorModel":"${params.sensorModel}", "certificateNumber":"${params.certificateNumber}", "temp":"${params.temp}", "hum":"${params.hum}"}',timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:".*",methodFilter:"no-reply",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",HTTPMethod:"POST",valueExpression:"${params}",httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}}]}},legacy:{host:"127.0.0.1",port:"5000",SSL:!1,security:{cert:"~/ssl/cert.pem",key:"~/ssl/key.pem"},mapping:[{endpoint:"/my_devices",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"json",deviceNameExpression:"${sensorName}",deviceTypeExpression:"${sensorType}",attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"certificateNumber",value:"${certificateNumber}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon1",HTTPMethods:["GET","POST"],security:{type:"anonymous"},converter:{type:"json",deviceNameExpression:"Device 2",deviceTypeExpression:"default",attributes:[{type:"string",key:"model",value:"Model2"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon2",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"custom",deviceNameExpression:"SuperAnonDevice",deviceTypeExpression:"default",extension:"CustomRestUplinkConverter","extension-config":{key:"Totaliser",datatype:"float",fromByte:0,toByte:4,byteorder:"big",signed:!0,multiplier:1}}}],attributeRequests:[{endpoint:"/sharedAttributes",type:"shared",HTTPMethods:["POST"],security:{type:"anonymous"},timeout:10,deviceNameExpression:"${deviceName}",attributeNameExpression:"${attribute}${attribute1}"}],attributeUpdates:[{HTTPMethod:"POST",SSLVerify:!1,httpHeaders:{"CONTENT-TYPE":"application/json"},security:{type:"anonymous"},timeout:.5,tries:3,allowRedirects:!0,deviceNameFilter:"SN.*",attributeFilter:".*",requestUrlExpression:"http://127.0.0.1:5001/",valueExpression:'{"deviceName":"${deviceName}","${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestUrlExpression:"http://127.0.0.1:5001/${deviceName}",responseTimeout:1,HTTPMethod:"GET",valueExpression:"${params}",timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:"SN.*",methodFilter:"post_attributes",requestUrlExpression:"http://127.0.0.1:5000/my_devices",responseTimeout:1,HTTPMethod:"POST",valueExpression:'{"sensorName":"${deviceName}", "sensorModel":"${params.sensorModel}", "certificateNumber":"${params.certificateNumber}", "temp":"${params.temp}", "hum":"${params.hum}"}',timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:".*",methodFilter:"no-reply",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",HTTPMethod:"POST",valueExpression:"${params}",httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}}]}},Ht={devices:[{deviceName:"SNMP router",deviceType:"snmp",ip:"snmp.live.gambitcommunications.com",port:161,pollPeriod:5e3,community:"public",attributes:[{key:"ReceivedFromGet",method:"get",oid:"1.3.6.1.2.1.1.1.0",timeout:6},{key:"ReceivedFromMultiGet",method:"multiget",oid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"],timeout:6},{key:"ReceivedFromGetNext",method:"getnext",oid:"1.3.6.1.2.1.1.1.0",timeout:6},{key:"ReceivedFromMultiWalk",method:"multiwalk",oid:["1.3.6.1.2.1.1.1.0","1.3.6.0.1.2.1"]},{key:"ReceivedFromBulkWalk",method:"bulkwalk",oid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"]},{key:"ReceivedFromBulkGet",method:"bulkget",scalarOid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"],repeatingOid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"],maxListSize:10}],telemetry:[{key:"ReceivedFromWalk",community:"private",method:"walk",oid:"1.3.6.1.2.1.1.1.0"},{key:"ReceivedFromTable",method:"table",oid:"1.3.6.1.2.1.1"}],attributeUpdateRequests:[{attributeFilter:"dataToSet",method:"set",oid:"1.3.6.1.2.1.1.1.0"},{attributeFilter:"dataToMultiSet",method:"multiset",mappings:{"1.2.3":"10","2.3.4":"${attribute}"}}],serverSideRpcRequests:[{requestFilter:"setData",method:"set",oid:"1.3.6.1.2.1.1.1.0"},{requestFilter:"multiSetData",method:"multiset"},{requestFilter:"getData",method:"get",oid:"1.3.6.1.2.1.1.1.0"},{requestFilter:"runBulkWalk",method:"bulkwalk",oid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"]}]},{deviceName:"SNMP router",deviceType:"snmp",ip:"127.0.0.1",pollPeriod:5e3,community:"public",converter:"CustomSNMPConverter",attributes:[{key:"ReceivedFromGetWithCustomConverter",method:"get",oid:"1.3.6.1.2.1.1.1.0"}],telemetry:[{key:"ReceivedFromTableWithCustomConverter",method:"table",oid:"1.3.6.1.2.1.1.1.0"}]}]},Wt={host:"0.0.0.0",port:21,TLSSupport:!1,security:{type:"basic",username:"admin",password:"admin"},paths:[{devicePatternName:"asd",devicePatternType:"Device",delimiter:",",path:"fol/*_hello*.txt",readMode:"FULL",maxFileSize:5,pollPeriod:500,txtFileDataView:"SLICED",withSortingFiles:!0,attributes:[{key:"temp",value:"[1:]"},{key:"tmp",value:"[0:1]"}],timeseries:[{type:"int",key:"[0:1]",value:"[0:1]"},{type:"int",key:"temp",value:"[1:]"}]}],attributeUpdates:[{path:"fol/hello.json",deviceNameFilter:".*",writingMode:"WRITE",valueExpression:"{'${attributeKey}':'${attributeValue}'}"}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"read",valueExpression:"${params}"},{deviceNameFilter:".*",methodFilter:"write",valueExpression:"${params}"}]},$t={server:{jid:"gateway@localhost",password:"password",host:"localhost",port:5222,use_ssl:!1,disable_starttls:!1,force_starttls:!0,timeout:1e4,plugins:["xep_0030","xep_0323","xep_0325"]},devices:[{jid:"device@localhost/TMP_1101",deviceNameExpression:"${serialNumber}",deviceTypeExpression:"default",attributes:[{key:"temperature",value:"${temp}"}],timeseries:[{key:"humidity",value:"${hum}"},{key:"combination",value:"${temp}:${hum}"}],attributeUpdates:[{attributeOnThingsBoard:"shared",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{methodRPC:"rpc1",withResponse:!0,valueExpression:"${params}"}]}]},Kt={centralSystem:{name:"Central System",host:"127.0.0.1",port:9e3,connection:{type:"insecure"},security:[{type:"token",tokens:["Bearer ACCESS_TOKEN"]},{type:"basic",credentials:[{username:"admin",password:"admin"}]}]},chargePoints:[{idRegexpPattern:"bidon/hello/CP_1",deviceNameExpression:"${Vendor} ${Model}",deviceTypeExpression:"default",attributes:[{messageTypeFilter:"MeterValues,",key:"temp1",value:"${meter_value[:].sampled_value[:].value}"},{messageTypeFilter:"MeterValues,",key:"vendorId",value:"${connector_id}"}],timeseries:[{messageTypeFilter:"DataTransfer,",key:"temp",value:"${data.temp}"}],attributeUpdates:[{attributeOnThingsBoard:"shared",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{methodRPC:"rpc1",withResponse:!0,valueExpression:"${params}"}]}]};const Yt=e("connectorConfigs",{[dt.MQTT]:Bt,[dt.MODBUS]:Nt,[dt.OPCUA]:Lt,[dt.BLE]:Vt,[dt.REQUEST]:qt,[dt.CAN]:Gt,[dt.BACNET]:zt,[dt.ODBC]:Ut,[dt.REST]:jt,[dt.SNMP]:Ht,[dt.FTP]:Wt,[dt.SOCKET]:Rt,[dt.XMPP]:$t,[dt.OCPP]:Kt,[dt.KNX]:{3.7:{logLevel:"INFO",client:{type:"AUTOMATIC",addressFormat:"LONG",gatewayIP:"127.0.0.1",gatewayPort:3671,autoReconnect:!0,autoReconnectWait:3,gatewaysScanner:{enabled:!0,scanPeriod:3,stopOnFound:!1}},devices:[{deviceInfo:{deviceNameDataType:"string",deviceNameExpressionSource:"expression",deviceNameExpression:"Device ${1/0/5}",deviceProfileDataType:"none",deviceProfileExpressionSource:"constant",deviceProfileNameExpression:"default"},pollPeriod:5e3,attributes:[{type:"temperature",key:"temperature",groupAddress:"1/0/6"}],timeseries:[{type:"humidity",key:"humidity",groupAddress:"1/0/7"}]}],attributeUpdates:[{deviceNameFilter:".*",dataType:"precent_U8",groupAddress:"1/0/9",key:"brightness"}],serverSideRpc:[{requestType:"read",deviceNameFilter:".*",method:"get_name",dataType:"string",groupAddress:"1/0/5"},{requestType:"write",deviceNameFilter:".*",method:"set_name",dataType:"string",groupAddress:"1/0/5"}]}}});function Xt(e){const t=Yt[e];if(!t)throw new Error("No default config found");return t}var Zt;e("ModbusDataType",Zt),function(e){e.STRING="string",e.BYTES="bytes",e.BITS="bits",e.INT8="8int",e.UINT8="8uint",e.INT16="16int",e.UINT16="16uint",e.FLOAT16="16float",e.INT32="32int",e.UINT32="32uint",e.FLOAT32="32float",e.INT64="64int",e.UINT64="64uint",e.FLOAT64="64float"}(Zt||e("ModbusDataType",Zt={}));const Qt=e("ModbusEditableDataTypes",[Zt.BYTES,Zt.BITS,Zt.STRING]);var Jt,en;e("ModbusObjectCountByDataType",Jt),function(e){e[e["8int"]=1]="8int",e[e["8uint"]=1]="8uint",e[e["16int"]=1]="16int",e[e["16uint"]=1]="16uint",e[e["16float"]=1]="16float",e[e["32int"]=2]="32int",e[e["32uint"]=2]="32uint",e[e["32float"]=2]="32float",e[e["64int"]=4]="64int",e[e["64uint"]=4]="64uint",e[e["64float"]=4]="64float"}(Jt||e("ModbusObjectCountByDataType",Jt={})),e("MappingValueType",en),function(e){e.STRING="string",e.INTEGER="integer",e.DOUBLE="double",e.BOOLEAN="boolean"}(en||e("MappingValueType",en={}));const tn=e("mappingValueTypesMap",new Map([[en.STRING,{name:"value.string",icon:"mdi:format-text"}],[en.INTEGER,{name:"value.integer",icon:"mdi:numeric"}],[en.DOUBLE,{name:"value.double",icon:"mdi:numeric"}],[en.BOOLEAN,{name:"value.boolean",icon:"mdi:checkbox-marked-outline"}]])),nn=e("ModbusFunctionCodeTranslationsMap",new Map([[1,"gateway.function-codes.read-coils"],[2,"gateway.function-codes.read-discrete-inputs"],[3,"gateway.function-codes.read-multiple-holding-registers"],[4,"gateway.function-codes.read-input-registers"],[5,"gateway.function-codes.write-single-coil"],[6,"gateway.function-codes.write-single-holding-register"],[15,"gateway.function-codes.write-multiple-coils"],[16,"gateway.function-codes.write-multiple-holding-registers"]]));var an,rn,on;e("ConfigurationModes",an),function(e){e.BASIC="basic",e.ADVANCED="advanced"}(an||e("ConfigurationModes",an={})),e("ReportStrategyType",rn),function(e){e.OnChange="ON_CHANGE",e.OnReportPeriod="ON_REPORT_PERIOD",e.OnChangeOrReportPeriod="ON_CHANGE_OR_REPORT_PERIOD",e.OnReceived="ON_RECEIVED"}(rn||e("ReportStrategyType",rn={})),e("ReportStrategyDefaultValue",on),function(e){e[e.Gateway=6e4]="Gateway",e[e.Connector=6e4]="Connector",e[e.Device=3e4]="Device",e[e.Key=15e3]="Key"}(on||e("ReportStrategyDefaultValue",on={}));const sn=e("ReportStrategyTypeTranslationsMap",new Map([[rn.OnChange,"gateway.report-strategy.on-change"],[rn.OnReportPeriod,"gateway.report-strategy.on-report-period"],[rn.OnChangeOrReportPeriod,"gateway.report-strategy.on-change-or-report-period"],[rn.OnReceived,"gateway.report-strategy.on-received"]])),ln=e("numberInputPattern",new RegExp(/^\d{1,15}(\.\d{1,15})?$/)),pn=e("noLeadTrailSpacesRegex",/^\S+(?: \S+)*$/),cn=e("integerRegex",/^[-+]?\d+$/),dn=e("nonZeroFloat",/^-?(?!0(\.0+)?$)\d+(\.\d+)?$/),un=e("directoryRegex",new RegExp(/^\.\/[^.\/ ]+(?: [^.\/ ]+)*(?:\/[^.\/ ]+(?: [^.\/ ]+)*)*\/$/));var mn;!function(e){e.EXCEPTION="EXCEPTION"}(mn||(mn={}));const hn={...pt,...mn},gn=()=>[10,20,30];function fn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"a",19),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).$implicit,i=t.ɵɵnextContext();return t.ɵɵresetView(i.onTabChanged(n))})),t.ɵɵtext(1),t.ɵɵelementEnd()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("active",i.activeLink.name===e.name),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.name," ")}}function yn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",20),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.created-time")))}function vn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtext(1),t.ɵɵpipe(2,"date"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind2(2,1,e.ts,"yyyy-MM-dd HH:mm:ss")," ")}}function xn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.level")))}function bn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell")(1,"span"),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassMap(i.statusClass(e.status)),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.status)}}function wn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",22),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.message")))}function Sn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵclassMap(i.statusClassMsg(e.status)),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.message," ")}}function Cn(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",23)}function _n(e,n){1&e&&t.ɵɵelement(0,"mat-row",23)}class Tn{constructor(e){this.translate=e,this.displayedColumns=["ts","status","message"],this.gatewayLogLinks=[{name:this.translate.instant("gateway.logs.columns.general"),key:"LOGS"},{name:this.translate.instant("gateway.logs.columns.service"),key:"SERVICE_LOGS"},{name:this.translate.instant("gateway.logs.columns.connection"),key:"CONNECTION_LOGS"},{name:this.translate.instant("gateway.logs.columns.storage"),key:"STORAGE_LOGS"},{key:"EXTENSIONS_LOGS",name:this.translate.instant("gateway.logs.columns.extension")}];const t={property:"ts",direction:w.DESC};this.pageLink=new S(10,0,null,t),this.dataSource=new x([])}ngOnInit(){this.updateWidgetTitle()}ngAfterViewInit(){if(this.dataSource.sort=this.sort,this.dataSource.paginator=this.paginator,this.ctx.defaultSubscription.onTimewindowChangeFunction=e=>(this.ctx.defaultSubscription.options.timeWindowConfig=e,this.ctx.defaultSubscription.updateDataSubscriptions(),e),this.ctx.settings.isConnectorLog&&this.ctx.settings.connectorLogState){const e=this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];this.logLinks=[{key:`${e.key}_LOGS`,name:this.translate.instant("gateway.connector"),filterFn:e=>!e.message.includes("_converter.py")},{key:`${e.key}_converter_LOGS`,name:this.translate.instant("gateway.converter"),filterFn:e=>e.message.includes("_converter.py")}]}else this.logLinks=this.gatewayLogLinks;this.activeLink=this.logLinks[0],this.changeSubscription()}updateWidgetTitle(){if(this.ctx.settings.isConnectorLog&&this.ctx.settings.connectorLogState){const e=this.ctx.widgetConfig.title,t="${connectorName}";if(e.includes(t)){const n=this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];this.ctx.widgetTitle=e.replace(t,n.key)}}}updateData(){if(this.ctx.defaultSubscription.data.length&&this.ctx.defaultSubscription.data[0]){let e=this.ctx.defaultSubscription.data[0].data.map((e=>{const t={ts:e[0],key:this.activeLink.key,message:e[1],status:"INVALID LOG FORMAT"};try{t.message=/\[(.*)/.exec(e[1])[0]}catch(n){t.message=e[1]}try{t.status=e[1].match(/\|(\w+)\|/)[1]}catch(e){t.status="INVALID LOG FORMAT"}return t}));this.activeLink.filterFn&&(e=e.filter((e=>this.activeLink.filterFn(e)))),this.dataSource.data=e}}onTabChanged(e){this.activeLink=e,this.changeSubscription()}statusClass(e){switch(e){case hn.DEBUG:return"status status-debug";case hn.WARNING:return"status status-warning";case hn.ERROR:case hn.EXCEPTION:return"status status-error";default:return"status status-info"}}statusClassMsg(e){if(e===hn.EXCEPTION)return"msg-status-exception"}trackByLogTs(e,t){return t.ts}changeSubscription(){this.ctx.datasources&&this.ctx.datasources[0].entity&&this.ctx.defaultSubscription.options.datasources&&(this.ctx.defaultSubscription.options.datasources[0].dataKeys=[{name:this.activeLink.key,type:C.timeseries,settings:{}}],this.ctx.defaultSubscription.unsubscribe(),this.ctx.defaultSubscription.updateDataSubscriptions(),this.ctx.defaultSubscription.callbacks.onDataUpdated=()=>{this.updateData()})}static{this.ɵfac=function(e){return new(e||Tn)(t.ɵɵdirectiveInject(le.TranslateService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Tn,selectors:[["tb-gateway-logs"]],viewQuery:function(e,n){if(1&e&&(t.ɵɵviewQuery(v,5),t.ɵɵviewQuery(b,5)),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.sort=e.first),t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.paginator=e.first)}},inputs:{ctx:"ctx",dialogRef:"dialogRef"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:23,vars:21,consts:[["tabPanel",""],[1,"flex","h-full","flex-col"],["mat-tab-nav-bar","",3,"tabPanel"],["mat-tab-link","",3,"active","click",4,"ngFor","ngForOf"],[1,"flex-1","overflow-auto"],["mat-table","","matSort","","matSortDisableClear","",3,"dataSource","trackBy","matSortActive","matSortDirection"],["matColumnDef","ts"],["mat-sort-header","","style","width: 20%",4,"matHeaderCellDef"],[4,"matCellDef"],["matColumnDef","status"],["mat-sort-header","","style","width: 10%",4,"matHeaderCellDef"],["matColumnDef","message"],["mat-sort-header","","style","width: 70%",4,"matHeaderCellDef"],[3,"class",4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],["class","mat-row-select",4,"matRowDef","matRowDefColumns"],[1,"no-data-found","flex-1","items-center","justify-center"],[1,"flex-1"],["showFirstLastButtons","",3,"length","pageIndex","pageSize","pageSizeOptions"],["mat-tab-link","",3,"click","active"],["mat-sort-header","",2,"width","20%"],["mat-sort-header","",2,"width","10%"],["mat-sort-header","",2,"width","70%"],[1,"mat-row-select"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"nav",2),t.ɵɵtemplate(2,fn,2,2,"a",3),t.ɵɵelementEnd(),t.ɵɵelement(3,"mat-tab-nav-panel",null,0),t.ɵɵelementStart(5,"div",4)(6,"table",5),t.ɵɵelementContainerStart(7,6),t.ɵɵtemplate(8,yn,3,3,"mat-header-cell",7)(9,vn,3,4,"mat-cell",8),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(10,9),t.ɵɵtemplate(11,xn,3,3,"mat-header-cell",10)(12,bn,3,3,"mat-cell",8),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(13,11),t.ɵɵtemplate(14,wn,3,3,"mat-header-cell",12)(15,Sn,2,3,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(16,Cn,1,0,"mat-header-row",14)(17,_n,1,0,"mat-row",15),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"span",16),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(21,"span",17),t.ɵɵelementEnd(),t.ɵɵelement(22,"mat-paginator",18),t.ɵɵelementEnd()),2&e){const e=t.ɵɵreference(4);t.ɵɵadvance(),t.ɵɵproperty("tabPanel",e),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.logLinks),t.ɵɵadvance(4),t.ɵɵproperty("dataSource",n.dataSource)("trackBy",n.trackByLogTs)("matSortActive",n.pageLink.sortOrder.property)("matSortDirection",n.pageLink.sortDirection()),t.ɵɵadvance(10),t.ɵɵproperty("matHeaderRowDef",n.displayedColumns)("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",n.displayedColumns),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",0!==n.dataSource.data.length),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,18,"attribute.no-telemetry-text")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",0===n.dataSource.data.length),t.ɵɵadvance(),t.ɵɵproperty("length",n.dataSource.data.length)("pageIndex",n.pageLink.page)("pageSize",n.pageLink.pageSize)("pageSizeOptions",t.ɵɵpureFunction0(20,gn))}},dependencies:t.ɵɵgetComponentDepsFactory(Tn,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex;flex-direction:column;overflow-x:auto;padding:0}[_nghost-%COMP%] .status[_ngcontent-%COMP%]{border-radius:20px;font-weight:500;padding:5px 15px}[_nghost-%COMP%] .status-debug[_ngcontent-%COMP%]{color:green;background:#0080001a}[_nghost-%COMP%] .status-warning[_ngcontent-%COMP%]{color:orange;background:#ffa5001a}[_nghost-%COMP%] .status-error[_ngcontent-%COMP%]{color:red;background:#ff00001a}[_nghost-%COMP%] .status-info[_ngcontent-%COMP%]{color:#00f;background:#0000801a}[_nghost-%COMP%] .msg-status-exception[_ngcontent-%COMP%]{color:red}']})}} + /** + * @license Angular v18.2.6 + * (c) 2010-2024 Google LLC. https://angular.io/ + * License: MIT + */ + function In(e){e||(n(In),e=i(a));const t=new ye((t=>e.onDestroy(t.next.bind(t))));return e=>e.pipe(Me(t))}e("GatewayLogsComponent",Tn);function En(e,t){!t?.injector&&n(En);const l=t?.injector??i(r),p=new ve(1),c=o((()=>{let t;try{t=e()}catch(e){return void s((()=>p.error(e)))}s((()=>p.next(t)))}),{injector:l,manualCleanup:!0});return l.get(a).onDestroy((()=>{c.destroy(),p.complete()})),p.asObservable()}const Mn=["commandInput"],kn=(e,t)=>t.attributeOnGateway,Pn=e=>({command:e});function Dn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",10),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.clear())})),t.ɵɵelementStart(2,"mat-icon",11),t.ɵɵtext(3,"close"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"action.clear"))}function On(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",12),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onCreateNewClick(n))})),t.ɵɵelementStart(1,"span",13),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"notification.create-new")))}function An(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵelement(1,"span",14),t.ɵɵpipe(2,"async"),t.ɵɵpipe(3,"highlight"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(3,4,e.attributeOnGateway,t.ɵɵpipeBind1(2,2,i.searchText$)),t.ɵɵsanitizeHtml)}}function Fn(e,n){if(1&e&&(t.ɵɵtext(0),t.ɵɵpipe(1,"async"),t.ɵɵpipe(2,"translate")),2&e){const e=t.ɵɵnextContext(2);t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind2(2,3,"gateway.statistics.no-commands-matching",t.ɵɵpureFunction1(6,Pn,e.truncate.transform(t.ɵɵpipeBind1(1,1,e.searchText$),!0,6,"...")))," ")}}function Rn(e,n){1&e&&(t.ɵɵtext(0),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(1,1,"gateway.statistics.no-command-found")," ")}function Bn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-option",9)(1,"div",15),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(2,"span"),t.ɵɵtemplate(3,Fn,3,8),t.ɵɵpipe(4,"async"),t.ɵɵtemplate(5,Rn,2,3),t.ɵɵelementStart(6,"a",16),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onCreateNewClick(n))})),t.ɵɵtext(7,"gateway.create-new-one"),t.ɵɵelementEnd()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("value",null),t.ɵɵadvance(3),t.ɵɵconditional(t.ɵɵpipeBind1(4,2,e.searchText$)?3:5)}}class Nn{constructor(e,t){this.truncate=e,this.fb=t,this.commands=l(),this.onCreateNewClicked=p(),this.selectStatisticsCommandControl=this.fb.control({}),this.searchText$=this.selectStatisticsCommandControl.valueChanges.pipe(ke((e=>e?"string"==typeof e?e:e?.attributeOnGateway:"")),Pe(),xe(1)),this.filteredCommands$=be([this.searchText$,En(this.commands)]).pipe(De(150),ke((([e,t])=>{const n=t.find((t=>t.attributeOnGateway===e))??null,i=this.selectStatisticsCommandControl.value;return"string"==typeof i&&n?.attributeOnGateway!==e||this.selectStatisticsCommandControl.patchValue(n,{emitEvent:!W(n,i)}),t.filter((t=>t.attributeOnGateway.toLowerCase().includes(e?.toLowerCase()??"")))})),xe(1)),this.onChanges=e=>{},this.selectStatisticsCommandControl.valueChanges.pipe(In()).subscribe((e=>this.onChanges(e)))}registerOnChange(e){this.onChanges=e}registerOnTouched(e){}writeValue(e){this.selectStatisticsCommandControl.patchValue(e)}displayCommandFn(e){return e?e.attributeOnGateway:null}clear(){this.selectStatisticsCommandControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.commandInput.nativeElement.blur(),this.commandInput.nativeElement.focus()}),0)}onCreateNewClick(e){e.stopPropagation(),this.onCreateNewClicked.emit()}static{this.ɵfac=function(e){return new(e||Nn)(t.ɵɵdirectiveInject(T.TruncatePipe),t.ɵɵdirectiveInject(de.UntypedFormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Nn,selectors:[["tb-statistics-commands-autocomplete"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(Mn,7),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.commandInput=e.first)}},inputs:{commands:[1,"commands"]},outputs:{onCreateNewClicked:"onCreateNewClicked"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>Nn)),multi:!0}]),t.ɵɵStandaloneFeature],decls:14,vars:10,consts:[["commandInput",""],["commandAutocomplete","matAutocomplete"],["appearance","outline",1,"mat-block"],["translate",""],["matInput","","type","text",3,"formControl","matAutocomplete"],["type","button","matTooltipPosition","above","matSuffix","","mat-icon-button","","aria-label","Clear",1,"action-button",3,"matTooltip"],["mat-button","","color","primary","matSuffix","",1,"mr-2"],[1,"tb-autocomplete",3,"displayWith"],[3,"value"],[1,"tb-not-found",3,"value"],["type","button","matTooltipPosition","above","matSuffix","","mat-icon-button","","aria-label","Clear",1,"action-button",3,"click","matTooltip"],[1,"material-icons"],["mat-button","","color","primary","matSuffix","",1,"mr-2",3,"click"],[1,"whitespace-nowrap"],[3,"innerHTML"],[1,"tb-not-found-content",3,"click"],["translate","",3,"click"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field",2)(1,"mat-label",3),t.ɵɵtext(2,"gateway.statistics.name"),t.ɵɵelementEnd(),t.ɵɵelement(3,"input",4,0),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,Dn,4,3,"button",5)(7,On,4,3,"button",6),t.ɵɵelementStart(8,"mat-autocomplete",7,1),t.ɵɵrepeaterCreate(10,An,4,7,"mat-option",8,kn,!1,Bn,8,4,"mat-option",9),t.ɵɵpipe(13,"async"),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵreference(9);t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.selectStatisticsCommandControl)("matAutocomplete",e),t.ɵɵattribute("aria-label",t.ɵɵpipeBind1(5,6,"gateway.statistics.command")),t.ɵɵadvance(3),t.ɵɵconditional(n.selectStatisticsCommandControl.value?6:7),t.ɵɵadvance(2),t.ɵɵproperty("displayWith",n.displayCommandFn),t.ɵɵadvance(2),t.ɵɵrepeater(t.ɵɵpipeBind1(13,8,n.filteredCommands$))}},dependencies:t.ɵɵgetComponentDepsFactory(Nn,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%] a[_ngcontent-%COMP%]{border-bottom:none;color:inherit}[_nghost-%COMP%] .action-button[_ngcontent-%COMP%]{opacity:.7}']})}}const Ln=()=>["createdTime","message"];function Vn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",12),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.created-time")))}function qn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",13),t.ɵɵtext(1),t.ɵɵpipe(2,"date"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(2,1,e[0],"yyyy-MM-dd HH:mm:ss"))}}function Gn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",14),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"widgets.gateway.message")," "))}function zn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell")(1,"div",15),t.ɵɵtext(2),t.ɵɵelement(3,"tb-copy-button",16),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",e[1]," "),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(4,3,"gateway.statistics.copy-message")),t.ɵɵproperty("copyText",e[1])}}function Un(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",17)}function jn(e,n){1&e&&t.ɵɵelement(0,"mat-row",17)}class Hn{constructor(){this.data=l([]),this.defaultPageSizes=[10,20,30],this.defaultSortOrder={property:"0",direction:w.DESC},this.pageLink=new S(this.defaultPageSizes[0],0,null,this.defaultSortOrder),this.dataSource=new x([]),o((()=>{this.dataSource.data=this.data()}))}ngAfterViewInit(){this.dataSource.sort=this.sort,this.dataSource.paginator=this.paginator,this.dataSource.sortingDataAccessor=e=>e[Number(this.sort?.active)||0]}static{this.ɵfac=function(e){return new(e||Hn)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Hn,selectors:[["tb-custom-statistics-table"]],viewQuery:function(e,n){if(1&e&&(t.ɵɵviewQuery(v,5),t.ɵɵviewQuery(b,5)),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.sort=e.first),t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.paginator=e.first)}},inputs:{data:[1,"data"]},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:12,vars:13,consts:[[1,"flex","h-full","flex-col"],[1,"flex-1","overflow-auto"],["mat-table","","matSort","","matSortDisableClear","",3,"dataSource","matSortActive","matSortDirection"],["matColumnDef","createdTime"],["mat-sort-header","","class","w-1/5",4,"matHeaderCellDef"],["class","!w-1/5",4,"matCellDef"],["matColumnDef","message"],["mat-sort-header","",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],["class","mat-row-select",4,"matRowDef","matRowDefColumns"],["showFirstLastButtons","",3,"length","pageSize","pageSizeOptions"],["mat-sort-header","",1,"w-1/5"],[1,"!w-1/5"],["mat-sort-header",""],[1,"flex","items-center","justify-between"],["tooltipPosition","above","icon","content_copy",1,"copy-content",3,"copyText","tooltipText"],[1,"mat-row-select"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"table",2),t.ɵɵelementContainerStart(3,3),t.ɵɵtemplate(4,Vn,3,3,"mat-header-cell",4)(5,qn,3,4,"mat-cell",5),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(6,6),t.ɵɵtemplate(7,Gn,3,3,"mat-header-cell",7)(8,zn,5,5,"mat-cell",8),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(9,Un,1,0,"mat-header-row",9)(10,jn,1,0,"mat-row",10),t.ɵɵelementEnd()(),t.ɵɵelement(11,"mat-paginator",11),t.ɵɵelementEnd()),2&e){let e;t.ɵɵadvance(2),t.ɵɵproperty("dataSource",n.dataSource)("matSortActive",n.pageLink.sortOrder.property)("matSortDirection",n.pageLink.sortDirection()),t.ɵɵadvance(7),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(11,Ln))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(12,Ln)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",!n.dataSource.data.length),t.ɵɵproperty("length",null!==(e=null==n.dataSource||null==n.dataSource.data?null:n.dataSource.data.length)&&void 0!==e?e:0)("pageSize",n.defaultPageSizes[0])("pageSizeOptions",n.defaultPageSizes)}},dependencies:t.ɵɵgetComponentDepsFactory(Hn,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .copy-content .mat-icon{padding:4px;font-size:18px}']})}}class Wn{constructor(e,t,n){this.elementRef=e,this.renderer=t,this.tooltip=n,this.tooltipEnabled=!0,this.position="above",this.destroy$=new we}ngOnInit(){this.observeMouseEvents(),this.applyTruncationStyles()}ngAfterViewInit(){this.tooltip.position=this.position}ngOnDestroy(){this.tooltip._isTooltipVisible()&&this.hideTooltip(),this.destroy$.next(),this.destroy$.complete()}observeMouseEvents(){Se(this.elementRef.nativeElement,"mouseenter").pipe(Oe((()=>this.tooltipEnabled)),Oe((()=>this.isOverflown(this.elementRef.nativeElement))),Ae((()=>this.showTooltip())),Me(this.destroy$)).subscribe(),Se(this.elementRef.nativeElement,"mouseleave").pipe(Oe((()=>this.tooltipEnabled)),Oe((()=>this.tooltip._isTooltipVisible())),Ae((()=>this.hideTooltip())),Me(this.destroy$)).subscribe()}applyTruncationStyles(){this.renderer.setStyle(this.elementRef.nativeElement,"white-space","nowrap"),this.renderer.setStyle(this.elementRef.nativeElement,"overflow","hidden"),this.renderer.setStyle(this.elementRef.nativeElement,"text-overflow","ellipsis")}isOverflown(e){return e.clientWidth{this.adjustChips()}),0))}constructor(e,t,n,i){this.el=e,this.renderer=t,this.translate=n,this.window=i,this.destroy$=new we,this.renderer.setStyle(this.el.nativeElement,"max-height","48px"),this.renderer.setStyle(this.el.nativeElement,"overflow","auto"),Se(i,"resize").pipe(Me(this.destroy$)).subscribe((()=>{this.adjustChips()})),this.observeIntersection()}observeIntersection(){this.intersectionObserver=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.adjustChips()}))})),this.intersectionObserver.observe(this.el.nativeElement)}adjustChips(){const e=this.el.nativeElement,t=this.el.nativeElement.querySelector(".ellipsis-chip"),n=parseFloat(this.window.getComputedStyle(t).marginLeft)||0,i=e.querySelectorAll("mat-chip:not(.ellipsis-chip)");if(this.chipsValue.length>1){const a=this.el.nativeElement.querySelector(".ellipsis-text");this.renderer.setStyle(t,"display","inline-flex"),a.innerHTML=this.translate.instant("gateway.ellipsis-chips-text",{count:this.chipsValue.length});const r=e.offsetWidth-(t.offsetWidth+n);let o=0,s=0;i.forEach((e=>{this.renderer.setStyle(e,"display","inline-flex");const t=e.querySelector(".mdc-evolution-chip__text-label");this.applyMaxChipTextWidth(t,r/3),o+(e.offsetWidth+n)<=r&&s_e(E())))).subscribe((e=>{this.attributesSubject.next(e.data),this.pageDataSubject.next(e),a.next(e)})),a}fetchAttributes(e,t,n){return this.getAllAttributes(e,t).pipe(ke((e=>{const t=e.filter((e=>0!==e.lastUpdateTs));return n.filterData(t)})))}getAllAttributes(e,t){if(!this.allAttributes){let n;M.get(t)?(this.telemetrySubscriber=k.createEntityAttributesSubscription(this.telemetryWsService,e,t,this.zone),this.telemetrySubscriber.subscribe(),n=this.telemetrySubscriber.attributeData$()):n=this.attributeService.getEntityAttributes(e,t),this.allAttributes=n.pipe(Re(1),Be())}return this.allAttributes}isAllSelected(){const e=this.selection.selected.length;return this.attributesSubject.pipe(ke((t=>e===t.length)))}isEmpty(){return this.attributesSubject.pipe(ke((e=>!e.length)))}total(){return this.pageDataSubject.pipe(ke((e=>e.totalElements)))}masterToggle(){this.attributesSubject.pipe(Ae((e=>{this.selection.selected.length===e.length?this.selection.clear():e.forEach((e=>{this.selection.select(e)}))})),Ne(1)).subscribe()}}e("AttributeDatasource",Kn);const Yn=()=>({maxWidth:"970px"});function Xn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-expansion-panel",4)(1,"mat-expansion-panel-header",5)(2,"mat-panel-title")(3,"mat-slide-toggle",6),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(4,"mat-label"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementContainer(7,7),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(),n=t.ɵɵreference(5);t.ɵɵproperty("expanded",e.showStrategyControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showStrategyControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,4,"gateway.report-strategy.label")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n)}}function Zn(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",8),t.ɵɵtext(1,"gateway.report-strategy.label"),t.ɵɵelementEnd(),t.ɵɵelementContainer(2,7)),2&e){t.ɵɵnextContext();const e=t.ɵɵreference(5);t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",e)}}function Qn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",16),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.ReportTypeTranslateMap.get(e)))}}function Jn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",19),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.reportStrategyFormGroup.get("reportPeriod").hasError("min")?"gateway.hints.report-period-range":"gateway.hints.report-period-required"))}}function ei(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",17),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",18),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,Jn,3,3,"mat-icon",19),t.ɵɵelementStart(8,"span",20),t.ɵɵtext(9,"gateway.suffix.ms"),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,3,"gateway.report-strategy.report-period")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.reportStrategyFormGroup.get("reportPeriod").hasError("required")||e.reportStrategyFormGroup.get("reportPeriod").hasError("min")&&e.reportStrategyFormGroup.get("reportPeriod").touched?7:-1)}}function ti(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelement(4,"div",11),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",12)(6,"mat-select",13),t.ɵɵtemplate(7,Qn,3,4,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵtemplate(8,ei,10,7,"div",15)),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,5,"gateway.type")," "),t.ɵɵadvance(2),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/report-strategy_fn")("tb-help-popup-style",t.ɵɵpureFunction0(7,Yn)),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.reportStrategyTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.reportStrategyFormGroup.get("type").value!==e.ReportStrategyType.OnChange&&e.reportStrategyFormGroup.get("type").value!==e.ReportStrategyType.OnReceived)}}class ni{constructor(e){this.fb=e,this.isExpansionMode=!1,this.defaultValue=on.Key,this.isConfig=!1,this.reportStrategyTypes=Object.values(rn),this.ReportTypeTranslateMap=sn,this.ReportStrategyType=rn,this.destroy$=new we,this.showStrategyControl=this.fb.control(!1),this.reportStrategyFormGroup=this.fb.group({type:[{value:rn.OnReportPeriod,disabled:!0},[]],reportPeriod:[{value:this.defaultValue,disabled:!0},[me.required,me.min(100)]]}),this.observeStrategyFormChange(),this.observeStrategyToggle()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}writeValue(e){const t=this.isDisabledConfig(e);if(this.isExpansionMode&&this.showStrategyControl.setValue(!t&&!!e,{emitEvent:!1}),t||!e)return this.reportStrategyFormGroup.disable({emitEvent:!1}),void this.reportStrategyFormGroup.setValue(this.getDefaultConfig(),{emitEvent:!1});this.reportStrategyFormGroup.enable({emitEvent:!1});const n=this.getDefaultConfig(),{type:i=n.type,reportPeriod:a=n.reportPeriod}=e??{};this.reportStrategyFormGroup.setValue({type:i,reportPeriod:a},{emitEvent:!1}),this.onTypeChange(i)}validate(){return this.reportStrategyFormGroup.valid||this.reportStrategyFormGroup.disabled?null:{reportStrategyForm:{valid:!1}}}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}observeStrategyFormChange(){this.reportStrategyFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()})),this.reportStrategyFormGroup.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.onTypeChange(e)))}observeStrategyToggle(){this.showStrategyControl.valueChanges.pipe(Me(this.destroy$),Oe((()=>this.isExpansionMode))).subscribe((e=>{if(e)this.reportStrategyFormGroup.enable({emitEvent:!1}),this.onTypeChange(this.reportStrategyFormGroup.get("type").value),this.reportStrategyFormGroup.updateValueAndValidity({emitEvent:!1}),this.onChange(this.reportStrategyFormGroup.value);else{this.reportStrategyFormGroup.disable({emitEvent:!1});const e=this.isConfig?{type:"DISABLED"}:null;this.onChange(e)}}))}onTypeChange(e){const t=this.reportStrategyFormGroup.get("reportPeriod");e===rn.OnChange||e===rn.OnReceived?t.disable({emitEvent:!1}):this.isExpansionMode&&!this.showStrategyControl.value||(t.enable({emitEvent:!1}),this.isConfig&&t.setValue(on.Gateway,{emitEvent:!1}))}getDefaultConfig(){return{type:this.isConfig?rn.OnReceived:rn.OnReportPeriod,reportPeriod:this.isConfig?null:this.defaultValue}}isDisabledConfig(e){return this.isConfig&&("DISABLED"===e?.type||"DISABLED"===e?.type)}static{this.ɵfac=function(e){return new(e||ni)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:ni,selectors:[["tb-report-strategy"]],inputs:{isExpansionMode:"isExpansionMode",defaultValue:"defaultValue",isConfig:"isConfig"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>ni)),multi:!0},{provide:he,useExisting:c((()=>ni)),multi:!0}]),t.ɵɵStandaloneFeature],decls:6,vars:3,consts:[["defaultMode",""],["strategyFields",""],[3,"formGroup"],["class","tb-settings",3,"expanded",4,"ngIf","ngIfElse"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide",3,"click","formControl"],[3,"ngTemplateOutlet"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","flex","items-center","gap-2"],["matSuffix","","tb-help-popup-placement","right",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],[3,"value"],["tbTruncateWithTooltip","",1,"fixed-title-width","tb-required"],["matInput","","type","number","min","100","name","value","formControlName","reportPeriod",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["translate","","matSuffix","",1,"block","pr-2"]],template:function(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,2),t.ɵɵtemplate(1,Xn,8,6,"mat-expansion-panel",3)(2,Zn,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor)(4,ti,9,8,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(3);t.ɵɵproperty("formGroup",n.reportStrategyFormGroup),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isExpansionMode)("ngIfElse",e)}},dependencies:t.ɵɵgetComponentDepsFactory(ni,[se,_,Wn]),encapsulation:2,changeDetection:d.OnPush})}}e("ReportStrategyComponent",ni),He([I()],ni.prototype,"isExpansionMode",void 0),He([P()],ni.prototype,"defaultValue",void 0),He([I()],ni.prototype,"isConfig",void 0);class ii{constructor(e,t){this.attributeService=e,this.cd=t,this.isGatewayActive=!1}ngAfterViewInit(){this.ctx.$scope.gatewayStatus=this,this.loadGatewayState()}loadGatewayState(){this.attributeService.getEntityAttributes(this.deviceId,D.SERVER_SCOPE,["active","lastDisconnectTime","lastConnectTime"]).subscribe((e=>{const t=e.find((e=>"active"===e.key)).value,n=e.find((e=>"lastDisconnectTime"===e.key))?.value,i=e.find((e=>"lastConnectTime"===e.key))?.value;this.isGatewayActive=this.getGatewayStatus(t,i,n),this.cd.detectChanges()}))}getGatewayStatus(e,t,n){return!!e&&(!n||t>n)}onDataUpdated(){const e=this.ctx.defaultSubscription.data,t=e.find((e=>"active"===e.dataKey.name)).data[0][1],n=e.find((e=>"lastDisconnectTime"===e.dataKey.name)).data[0][1],i=e.find((e=>"lastConnectTime"===e.dataKey.name)).data[0][1];this.isGatewayActive=this.getGatewayStatus(t,i,n),this.cd.detectChanges()}static{this.ɵfac=function(e){return new(e||ii)(t.ɵɵdirectiveInject(K.AttributeService),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:ii,selectors:[["tb-gateway-status"]],inputs:{ctx:"ctx",deviceId:"deviceId"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:9,vars:10,consts:[[1,"flex","min-h-10","flex-1","justify-center"],[1,"divider"],[1,"whitespace-nowrap"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-card",0),t.ɵɵelement(1,"div",1),t.ɵɵelementStart(2,"mat-card-header")(3,"mat-card-subtitle",2),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"mat-card-content"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵclassProp("divider-red",!n.isGatewayActive)("divider-green",n.isGatewayActive),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,6,"gateway.gateway-status")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,8,n.isGatewayActive?"gateway.active":"gateway.inactive")))},dependencies:t.ɵɵgetComponentDepsFactory(ii,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex}[_nghost-%COMP%] .divider[_ngcontent-%COMP%]{position:absolute;width:3px;top:12px;border-radius:2px;bottom:4px;left:10px}[_nghost-%COMP%] .divider-green[_ngcontent-%COMP%] .divider[_ngcontent-%COMP%]{border:1px solid rgb(25,128,56);background-color:#198038}[_nghost-%COMP%] .divider-green[_ngcontent-%COMP%] .mat-mdc-card-content[_ngcontent-%COMP%]{color:#198038}[_nghost-%COMP%] .divider-red[_ngcontent-%COMP%] .divider[_ngcontent-%COMP%]{border:1px solid rgb(203,37,48);background-color:#cb2530}[_nghost-%COMP%] .divider-red[_ngcontent-%COMP%] .mat-mdc-card-content[_ngcontent-%COMP%]{color:#cb2530}.mdc-card[_ngcontent-%COMP%]{position:relative;padding-left:10px;box-shadow:none}.mat-mdc-card-subtitle[_ngcontent-%COMP%]{font-weight:400;font-size:12px}.mat-mdc-card-header[_ngcontent-%COMP%]{padding:8px 16px 0}.mat-mdc-card-content[_ngcontent-%COMP%]:last-child{padding-bottom:8px;font-size:16px}'],changeDetection:d.OnPush})}}e("GatewayStatusComponent",ii);class ai{constructor(){this.tooltipText=""}static{this.ɵfac=function(e){return new(e||ai)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:ai,selectors:[["tb-error-icon"]],inputs:{tooltipText:"tooltipText"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:2,vars:1,consts:[["matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",0),t.ɵɵtext(1," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",n.tooltipText)},dependencies:t.ɵɵgetComponentDepsFactory(ai,[_,se]),styles:["[_nghost-%COMP%]{display:flex;width:40px;height:40px;padding:10px}mat-icon[_ngcontent-%COMP%]{font-size:20px}"]})}}var ri,oi;e("ErrorTooltipIconComponent",ai),e("MqttConverterType",ri),function(e){e.JSON="json",e.BYTES="bytes",e.CUSTOM="custom"}(ri||e("MqttConverterType",ri={})),e("MQTTSourceType",oi),function(e){e.MSG="message",e.TOPIC="topic",e.CONST="constant"}(oi||e("MQTTSourceType",oi={}));const si=e("MqttVersions",[{name:3.1,value:3},{name:3.11,value:4},{name:5,value:5}]),li=e("QualityTypeTranslationsMap",new Map([[0,"gateway.qos.at-most-once"],[1,"gateway.qos.at-least-once"],[2,"gateway.qos.exactly-once"]])),pi=e("ConvertorTypeTranslationsMap",new Map([[ri.JSON,"gateway.JSON"],[ri.BYTES,"gateway.bytes"],[ri.CUSTOM,"gateway.custom"]]));var ci;e("RequestType",ci),function(e){e.CONNECT_REQUEST="connectRequests",e.DISCONNECT_REQUEST="disconnectRequests",e.ATTRIBUTE_REQUEST="attributeRequests",e.ATTRIBUTE_UPDATE="attributeUpdates",e.SERVER_SIDE_RPC="serverSideRpc"}(ci||e("RequestType",ci={}));const di=e("RequestTypesTranslationsMap",new Map([[ci.CONNECT_REQUEST,"gateway.request.connect-request"],[ci.DISCONNECT_REQUEST,"gateway.request.disconnect-request"],[ci.ATTRIBUTE_REQUEST,"gateway.request.attribute-request"],[ci.ATTRIBUTE_UPDATE,"gateway.request.attribute-update"],[ci.SERVER_SIDE_RPC,"gateway.request.rpc-connection"]])),ui=e("DataConversionTranslationsMap",new Map([[ri.JSON,"gateway.JSON-hint"],[ri.BYTES,"gateway.bytes-hint"],[ri.CUSTOM,"gateway.custom-hint"]]));var mi,hi;e("SocketType",mi),function(e){e.TCP="TCP",e.UDP="UDP"}(mi||e("SocketType",mi={})),e("SocketValueKey",hi),function(e){e.TIMESERIES="telemetry",e.ATTRIBUTES="attributes",e.ATTRIBUTES_REQUESTS="attributeRequests",e.ATTRIBUTES_UPDATES="attributeUpdates",e.RPC_METHODS="serverSideRpc"}(hi||e("SocketValueKey",hi={}));const gi=e("SocketKeysPanelTitleTranslationsMap",new Map([[hi.ATTRIBUTES,"gateway.attributes"],[hi.TIMESERIES,"gateway.timeseries"],[hi.ATTRIBUTES_REQUESTS,"gateway.attribute-requests"],[hi.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[hi.RPC_METHODS,"gateway.rpc-methods"]]));var fi,yi;e("RequestsType",fi),function(e){e.Shared="shared",e.Client="client"}(fi||e("RequestsType",fi={})),e("ExpressionType",yi),function(e){e.Constant="constant",e.Expression="expression"}(yi||e("ExpressionType",yi={}));const vi=e("SocketKeysAddKeyTranslationsMap",new Map([[hi.ATTRIBUTES,"gateway.add-attribute"],[hi.TIMESERIES,"gateway.add-timeseries"],[hi.ATTRIBUTES_REQUESTS,"gateway.add-attribute-request"],[hi.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[hi.RPC_METHODS,"gateway.add-rpc-method"]])),xi=e("SocketKeysDeleteKeyTranslationsMap",new Map([[hi.ATTRIBUTES,"gateway.delete-attribute"],[hi.TIMESERIES,"gateway.delete-timeseries"],[hi.ATTRIBUTES_REQUESTS,"gateway.delete-attribute-request"],[hi.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[hi.RPC_METHODS,"gateway.delete-rpc-method"]])),bi=e("SocketKeysNoKeysTextTranslationsMap",new Map([[hi.ATTRIBUTES,"gateway.no-attributes"],[hi.TIMESERIES,"gateway.no-timeseries"],[hi.ATTRIBUTES_REQUESTS,"gateway.no-attribute-requests"],[hi.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[hi.RPC_METHODS,"gateway.no-rpc-methods"]]));var wi,Si,Ci,_i;e("RestConverterType",wi),function(e){e.JSON="json",e.CUSTOM="custom"}(wi||e("RestConverterType",wi={})),e("RestSourceType",Si),function(e){e.REQUEST="request",e.CONST="constant"}(Si||e("RestSourceType",Si={})),e("ResponseType",Ci),function(e){e.DEFAULT="default",e.CONST="constant",e.ADVANCED="advanced"}(Ci||e("ResponseType",Ci={})),e("ResponseStatus",_i),function(e){e.OK="OK",e.ERROR="Error"}(_i||e("ResponseStatus",_i={}));const Ti=e("ResponseTypeTranslationsMap",new Map([[Ci.DEFAULT,"gateway.rest.response-type.default"],[Ci.CONST,"gateway.rest.response-type.const"],[Ci.ADVANCED,"gateway.rest.response-type.advanced"]]));var Ii;e("RestRequestType",Ii),function(e){e.ATTRIBUTE_REQUEST="attributeRequests",e.ATTRIBUTE_UPDATE="attributeUpdates",e.SERVER_SIDE_RPC="serverSideRpc"}(Ii||e("RestRequestType",Ii={}));const Ei=e("RestRequestTypesTranslationsMap",new Map([[Ii.ATTRIBUTE_REQUEST,"gateway.request.attribute-request"],[Ii.ATTRIBUTE_UPDATE,"gateway.request.attribute-update"],[Ii.SERVER_SIDE_RPC,"gateway.request.rpc-connection"]]));var Mi;e("RestRequestsScopeType",Mi),function(e){e.Shared="shared",e.Client="client"}(Mi||e("RestRequestsScopeType",Mi={}));const ki=e("RestRequestTypeFieldsMap",new Map([[Ii.ATTRIBUTE_REQUEST,["HTTPMethods","endpoint","type","deviceNameExpression","attributeNameExpression"]],[Ii.ATTRIBUTE_UPDATE,["HTTPMethod","SSLVerify","deviceNameFilter","attributeFilter","requestUrlExpression","valueExpression","httpHeaders","tries","allowRedirects"]],[Ii.SERVER_SIDE_RPC,["HTTPMethod","deviceNameFilter","methodFilter","requestUrlExpression","valueExpression","responseTimeout","httpHeaders","tries"]],["all",["requestType","timeout","security"]]])),Pi=e("RestConvertorTypeTranslationsMap",new Map([[wi.JSON,"gateway.JSON"],[wi.CUSTOM,"gateway.custom"]])),Di=e("RestDataConversionTranslationsMap",new Map([[wi.JSON,"gateway.hints.rest.JSON"],[wi.CUSTOM,"gateway.custom-hint"]]));var Oi;e("PortLimits",Oi),function(e){e[e.MIN=1]="MIN",e[e.MAX=65535]="MAX"}(Oi||e("PortLimits",Oi={}));const Ai=e("GatewayConnectorConfigVersionMap",new Map([[dt.REST,ct.v3_7_2],[dt.KNX,ct.v3_7_0],[dt.BACNET,ct.v3_6_2],[dt.SOCKET,ct.v3_6_0],[dt.MQTT,ct.v3_5_2],[dt.OPCUA,ct.v3_5_2],[dt.MODBUS,ct.v3_5_2]]));var Fi,Ri,Bi,Ni;e("OPCUaSourceType",Fi),function(e){e.PATH="path",e.IDENTIFIER="identifier",e.CONST="constant"}(Fi||e("OPCUaSourceType",Fi={})),e("SecurityType",Ri),function(e){e.ANONYMOUS="anonymous",e.BASIC="basic",e.CERTIFICATES="certificates"}(Ri||e("SecurityType",Ri={})),e("ModeType",Bi),function(e){e.NONE="None",e.SIGN="Sign",e.SIGNANDENCRYPT="SignAndEncrypt"}(Bi||e("ModeType",Bi={})),e("MappingType",Ni),function(e){e.DATA="data",e.REQUESTS="requests",e.OPCUA="OPCua"}(Ni||e("MappingType",Ni={}));const Li=e("MappingTypeTranslationsMap",new Map([[Ni.DATA,"gateway.data-mapping"],[Ni.REQUESTS,"gateway.requests-mapping"],[Ni.OPCUA,"gateway.data-mapping"]]));var Vi;e("SecurityPolicy",Vi),function(e){e.BASIC128="Basic128Rsa15",e.BASIC256="Basic256",e.BASIC256SHA="Basic256Sha256"}(Vi||e("SecurityPolicy",Vi={}));const qi=e("SecurityPolicyTypes",[{value:Vi.BASIC128,name:"Basic128RSA15"},{value:Vi.BASIC256,name:"Basic256"},{value:Vi.BASIC256SHA,name:"Basic256SHA256"}]),Gi=e("SecurityTypeTranslationsMap",new Map([[Ri.ANONYMOUS,"gateway.broker.security-types.anonymous"],[Ri.BASIC,"gateway.broker.security-types.basic"],[Ri.CERTIFICATES,"gateway.broker.security-types.certificates"]])),zi=e("SourceTypeTranslationsMap",new Map([[oi.MSG,"gateway.source-type.msg"],[oi.TOPIC,"gateway.source-type.topic"],[oi.CONST,"gateway.source-type.const"],[Fi.PATH,"gateway.source-type.path"],[Fi.IDENTIFIER,"gateway.source-type.identifier"],[Fi.CONST,"gateway.source-type.const"],[yi.Expression,"gateway.source-type.expression"],[Si.REQUEST,"gateway.source-type.request"]]));var Ui;e("MappingKeysType",Ui),function(e){e.ATTRIBUTES="attributes",e.TIMESERIES="timeseries",e.CUSTOM="extensionConfig",e.RPC_METHODS="rpc_methods",e.ATTRIBUTES_UPDATES="attributes_updates"}(Ui||e("MappingKeysType",Ui={}));const ji=e("MappingKeysPanelTitleTranslationsMap",new Map([[Ui.ATTRIBUTES,"gateway.attributes"],[Ui.TIMESERIES,"gateway.timeseries"],[Ui.CUSTOM,"gateway.keys"],[Ui.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[Ui.RPC_METHODS,"gateway.rpc-methods"]])),Hi=e("MappingKeysAddKeyTranslationsMap",new Map([[Ui.ATTRIBUTES,"gateway.add-attribute"],[Ui.TIMESERIES,"gateway.add-timeseries"],[Ui.CUSTOM,"gateway.add-key"],[Ui.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[Ui.RPC_METHODS,"gateway.add-rpc-method"]])),Wi=e("MappingKeysDeleteKeyTranslationsMap",new Map([[Ui.ATTRIBUTES,"gateway.delete-attribute"],[Ui.TIMESERIES,"gateway.delete-timeseries"],[Ui.CUSTOM,"gateway.delete-key"],[Ui.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[Ui.RPC_METHODS,"gateway.delete-rpc-method"]])),$i=e("MappingKeysNoKeysTextTranslationsMap",new Map([[Ui.ATTRIBUTES,"gateway.no-attributes"],[Ui.TIMESERIES,"gateway.no-timeseries"],[Ui.CUSTOM,"gateway.no-keys"],[Ui.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[Ui.RPC_METHODS,"gateway.no-rpc-methods"]])),Ki=e("QualityTypes",[0,1,2]);var Yi;e("ServerSideRpcType",Yi),function(e){e.WithResponse="twoWay",e.WithoutResponse="oneWay"}(Yi||e("ServerSideRpcType",Yi={}));const Xi=e("HelpLinkByMappingTypeMap",new Map([[Ni.DATA,O+"/docs/iot-gateway/config/mqtt/#section-mapping"],[Ni.OPCUA,O+"/docs/iot-gateway/config/opc-ua/#section-mapping"],[Ni.REQUESTS,O+"/docs/iot-gateway/config/mqtt/#requests-mapping"]])),Zi=e("MappingHintTranslationsMap",new Map([[Ni.DATA,"gateway.data-mapping-hint"],[Ni.OPCUA,"gateway.opcua-data-mapping-hint"],[Ni.REQUESTS,"gateway.requests-mapping-hint"]]));var Qi,Ji,ea,ta,na,ia,aa,ra,oa;e("ServerSideRPCType",Qi),function(e){e.ONE_WAY="oneWay",e.TWO_WAY="twoWay"}(Qi||e("ServerSideRPCType",Qi={})),e("SecurityMode",Ji),function(e){e.basic="basic",e.certificates="certificates",e.extendedCertificates="extendedCertificates"}(Ji||e("SecurityMode",Ji={})),e("ModbusProtocolType",ea),function(e){e.TCP="tcp",e.UDP="udp",e.Serial="serial"}(ea||e("ModbusProtocolType",ea={})),e("ModbusMethodType",ta),function(e){e.SOCKET="socket",e.RTU="rtu"}(ta||e("ModbusMethodType",ta={})),e("ModbusSerialMethodType",na),function(e){e.RTU="rtu",e.ASCII="ascii"}(na||e("ModbusSerialMethodType",na={})),e("ModbusParity",ia),function(e){e.Even="E",e.Odd="O",e.None="N"}(ia||e("ModbusParity",ia={})),e("ModbusOrderType",aa),function(e){e.BIG="BIG",e.LITTLE="LITTLE"}(aa||e("ModbusOrderType",aa={})),e("ModbusRegisterType",ra),function(e){e.HoldingRegisters="holding_registers",e.CoilsInitializer="coils_initializer",e.InputRegisters="input_registers",e.DiscreteInputs="discrete_inputs"}(ra||e("ModbusRegisterType",ra={})),e("ModbusValueKey",oa),function(e){e.ATTRIBUTES="attributes",e.TIMESERIES="timeseries",e.ATTRIBUTES_UPDATES="attributeUpdates",e.RPC_REQUESTS="rpc"}(oa||e("ModbusValueKey",oa={}));const sa=e("ModbusBaudrates",[4800,9600,19200,38400,57600,115200,230400,460800,921600]),la=e("ModbusByteSizes",[5,6,7,8]),pa=e("ModbusDefaultRetries",3),ca=e("ModbusRegisterTranslationsMap",new Map([[ra.HoldingRegisters,"gateway.holding_registers"],[ra.CoilsInitializer,"gateway.coils_initializer"],[ra.InputRegisters,"gateway.input_registers"],[ra.DiscreteInputs,"gateway.discrete_inputs"]]));var da;e("ModbusBitTargetType",da),function(e){e.BooleanType="bool",e.IntegerType="int"}(da||e("ModbusBitTargetType",da={}));const ua=e("ModbusBitTargetTypeTranslationMap",new Map([[da.BooleanType,"gateway.boolean"],[da.IntegerType,"gateway.integer"]])),ma=e("ModbusMethodLabelsMap",new Map([[ta.SOCKET,"Socket"],[ta.RTU,"RTU"],[na.ASCII,"ASCII"]])),ha=e("ModbusProtocolLabelsMap",new Map([[ea.TCP,"TCP"],[ea.UDP,"UDP"],[ea.Serial,"Serial"]])),ga=e("ModbusParityLabelsMap",new Map([[ia.Even,"Even"],[ia.Odd,"Odd"],[ia.None,"None"]])),fa=e("ModbusKeysPanelTitleTranslationsMap",new Map([[oa.ATTRIBUTES,"gateway.attributes"],[oa.TIMESERIES,"gateway.timeseries"],[oa.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[oa.RPC_REQUESTS,"gateway.rpc-requests"]])),ya=e("ModbusKeysAddKeyTranslationsMap",new Map([[oa.ATTRIBUTES,"gateway.add-attribute"],[oa.TIMESERIES,"gateway.add-timeseries"],[oa.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[oa.RPC_REQUESTS,"gateway.add-rpc-request"]])),va=e("ModbusKeysDeleteKeyTranslationsMap",new Map([[oa.ATTRIBUTES,"gateway.delete-attribute"],[oa.TIMESERIES,"gateway.delete-timeseries"],[oa.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[oa.RPC_REQUESTS,"gateway.delete-rpc-request"]])),xa=e("ModbusKeysNoKeysTextTranslationsMap",new Map([[oa.ATTRIBUTES,"gateway.no-attributes"],[oa.TIMESERIES,"gateway.no-timeseries"],[oa.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[oa.RPC_REQUESTS,"gateway.no-rpc-requests"]]));var ba;e("ModifierType",ba),function(e){e.DIVIDER="divider",e.MULTIPLIER="multiplier"}(ba||e("ModifierType",ba={}));const wa=e("ModifierTypesMap",new Map([[ba.DIVIDER,{name:"gateway.divider",icon:"mdi:division"}],[ba.MULTIPLIER,{name:"gateway.multiplier",icon:"mdi:multiplication"}]]));var Sa,Ca;e("DeviceInfoType",Sa),function(e){e.FULL="full",e.PARTIAL="partial"}(Sa||e("DeviceInfoType",Sa={})),e("SegmentationType",Ca),function(e){e.BOTH="segmentedBoth",e.TRANSMIT="segmentedTransmit",e.RECEIVE="segmentedReceive",e.NO="noSegmentation"}(Ca||e("SegmentationType",Ca={}));const _a=e("SegmentationTypeTranslationsMap",new Map([[Ca.BOTH,"gateway.bacnet.segmentation.both"],[Ca.TRANSMIT,"gateway.bacnet.segmentation.transmit"],[Ca.RECEIVE,"gateway.bacnet.segmentation.receive"],[Ca.NO,"gateway.bacnet.segmentation.no"]]));var Ta;e("BacnetDeviceKeysType",Ta),function(e){e.ATTRIBUTES="attributes",e.TIMESERIES="timeseries",e.RPC_METHODS="serverSideRpc",e.ATTRIBUTES_UPDATES="attributeUpdates"}(Ta||e("BacnetDeviceKeysType",Ta={}));const Ia=e("BacnetDeviceKeysPanelTitleTranslationsMap",new Map([[Ta.ATTRIBUTES,"gateway.attributes"],[Ta.TIMESERIES,"gateway.timeseries"],[Ta.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[Ta.RPC_METHODS,"gateway.rpc-methods"]])),Ea=e("BacnetDeviceKeysAddKeyTranslationsMap",new Map([[Ta.ATTRIBUTES,"gateway.add-attribute"],[Ta.TIMESERIES,"gateway.add-timeseries"],[Ta.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[Ta.RPC_METHODS,"gateway.add-rpc-method"]])),Ma=e("BacnetDeviceKeysDeleteKeyTranslationsMap",new Map([[Ta.ATTRIBUTES,"gateway.delete-attribute"],[Ta.TIMESERIES,"gateway.delete-timeseries"],[Ta.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[Ta.RPC_METHODS,"gateway.delete-rpc-method"]])),ka=e("BacnetDeviceKeysNoKeysTextTranslationsMap",new Map([[Ta.ATTRIBUTES,"gateway.no-attributes"],[Ta.TIMESERIES,"gateway.no-timeseries"],[Ta.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[Ta.RPC_METHODS,"gateway.no-rpc-methods"]]));var Pa;e("BacnetKeyObjectType",Pa),function(e){e.analogInput="analogInput",e.analogOutput="analogOutput",e.analogValue="analogValue",e.binaryInput="binaryInput",e.binaryOutput="binaryOutput",e.binaryValue="binaryValue"}(Pa||e("BacnetKeyObjectType",Pa={}));const Da=e("BacnetKeyObjectTypeTranslationsMap",new Map([[Pa.analogInput,"gateway.bacnet.object-type.analog-input"],[Pa.analogOutput,"gateway.bacnet.object-type.analog-output"],[Pa.analogValue,"gateway.bacnet.object-type.analog-value"],[Pa.binaryInput,"gateway.bacnet.object-type.binary-input"],[Pa.binaryOutput,"gateway.bacnet.object-type.binary-output"],[Pa.binaryValue,"gateway.bacnet.object-type.binary-value"]]));var Oa;e("BacnetPropertyId",Oa),function(e){e.presentValue="presentValue",e.statusFlags="statusFlags",e.covIncrement="covIncrement",e.eventState="eventState",e.outOfService="outOfService",e.polarity="polarity",e.priorityArray="priorityArray",e.relinquishDefault="relinquishDefault",e.currentCommandPriority="currentCommandPriority",e.eventMessageTexts="eventMessageTexts",e.eventMessageTextsConfig="eventMessageTextsConfig",e.eventAlgorithmInhibitReference="eventAlgorithmInhibitReference",e.timeDelayNormal="timeDelayNormal"}(Oa||e("BacnetPropertyId",Oa={}));const Aa=e("BacnetPropertyIdByObjectType",new Map([[Pa.analogInput,[Oa.presentValue,Oa.statusFlags,Oa.covIncrement]],[Pa.analogOutput,[Oa.presentValue,Oa.statusFlags,Oa.covIncrement]],[Pa.analogValue,[Oa.presentValue,Oa.statusFlags,Oa.covIncrement]],[Pa.binaryInput,[Oa.presentValue,Oa.statusFlags,Oa.eventState,Oa.outOfService,Oa.polarity]],[Pa.binaryOutput,[Oa.presentValue,Oa.statusFlags,Oa.eventState,Oa.outOfService,Oa.polarity,Oa.priorityArray,Oa.relinquishDefault,Oa.currentCommandPriority,Oa.eventMessageTexts,Oa.eventMessageTextsConfig,Oa.eventAlgorithmInhibitReference,Oa.timeDelayNormal]],[Pa.binaryValue,[Oa.presentValue,Oa.statusFlags,Oa.eventState,Oa.outOfService]]])),Fa=e("BacnetPropertyIdTranslationsMap",new Map([[Oa.presentValue,"gateway.bacnet.property-id.present-value"],[Oa.statusFlags,"gateway.bacnet.property-id.status-flags"],[Oa.covIncrement,"gateway.bacnet.property-id.cov-increment"],[Oa.eventState,"gateway.bacnet.property-id.event-state"],[Oa.outOfService,"gateway.bacnet.property-id.out-of-service"],[Oa.polarity,"gateway.bacnet.property-id.polarity"],[Oa.priorityArray,"gateway.bacnet.property-id.priority-array"],[Oa.relinquishDefault,"gateway.bacnet.property-id.relinquish-default"],[Oa.currentCommandPriority,"gateway.bacnet.property-id.current-command-priority"],[Oa.eventMessageTexts,"gateway.bacnet.property-id.event-message-texts"],[Oa.eventMessageTextsConfig,"gateway.bacnet.property-id.event-message-texts-config"],[Oa.eventAlgorithmInhibitReference,"gateway.bacnet.property-id.event-algorithm-inhibit-reference"],[Oa.timeDelayNormal,"gateway.bacnet.property-id.time-delay-normal"]]));var Ra;e("BacnetRequestType",Ra),function(e){e.Write="writeProperty",e.Read="readProperty"}(Ra||e("BacnetRequestType",Ra={}));const Ba=e("BacnetRequestTypeTranslationsMap",new Map([[Ra.Write,"gateway.bacnet.request-type.write"],[Ra.Read,"gateway.bacnet.request-type.read"]]));class Na{static{this.mqttRequestTypeKeys=Object.values(ci)}static{this.mqttRequestMappingOldFields=["attributeNameJsonExpression","deviceNameJsonExpression","deviceNameTopicExpression","extension-config"]}static{this.mqttRequestMappingNewFields=["attributeNameExpressionSource","responseTopicQoS","extensionConfig"]}static mapMappingToUpgradedVersion(e){return e?.map((({converter:e,topicFilter:t,subscriptionQos:n=1})=>{const i=e.deviceInfo??this.extractConverterDeviceInfo(e),a={...e,deviceInfo:i,extensionConfig:e.extensionConfig||e["extension-config"]||null};return this.cleanUpOldFields(a),{converter:a,topicFilter:t,subscriptionQos:n}}))}static mapRequestsToUpgradedVersion(e){return this.mqttRequestTypeKeys.reduce(((t,n)=>e[n]?(t[n]=e[n].map((e=>{const t=this.mapRequestToUpgradedVersion(e,n);return this.cleanUpOldFields(t),t})),t):t),{})}static mapRequestsToDowngradedVersion(e){return this.mqttRequestTypeKeys.reduce(((t,n)=>e[n]?(t[n]=e[n].map((e=>{n===ci.SERVER_SIDE_RPC&&delete e.type;const{attributeNameExpression:t,deviceInfo:i,...a}=e,r={...a,attributeNameJsonExpression:t||null,deviceNameJsonExpression:i?.deviceNameExpressionSource!==oi.TOPIC?i?.deviceNameExpression:null,deviceNameTopicExpression:i?.deviceNameExpressionSource===oi.TOPIC?i?.deviceNameExpression:null};return this.cleanUpNewFields(r),r})),t):t),{})}static mapMappingToDowngradedVersion(e){return e?.map((e=>{const t=this.mapConverterToDowngradedVersion(e.converter);return this.cleanUpNewFields(t),{converter:t,topicFilter:e.topicFilter}}))}static mapConverterToDowngradedVersion(e){const{deviceInfo:t,...n}=e;return e.type!==ri.BYTES?{...n,deviceNameJsonExpression:t?.deviceNameExpressionSource===oi.MSG?t.deviceNameExpression:null,deviceTypeJsonExpression:t?.deviceProfileExpressionSource===oi.MSG?t.deviceProfileExpression:null,deviceNameTopicExpression:t?.deviceNameExpressionSource!==oi.MSG?t?.deviceNameExpression:null,deviceTypeTopicExpression:t?.deviceProfileExpressionSource!==oi.MSG?t?.deviceProfileExpression:null}:{...n,deviceNameExpression:t.deviceNameExpression,deviceTypeExpression:t.deviceProfileExpression,"extension-config":e.extensionConfig}}static cleanUpOldFields(e){this.mqttRequestMappingOldFields.forEach((t=>delete e[t])),Y(e)}static cleanUpNewFields(e){this.mqttRequestMappingNewFields.forEach((t=>delete e[t])),Y(e)}static getTypeSourceByValue(e){return e.includes("${")?oi.MSG:e.includes("/")?oi.TOPIC:oi.CONST}static extractConverterDeviceInfo(e){const t=e.deviceNameExpression||e.deviceNameJsonExpression||e.deviceNameTopicExpression||null,n=e.deviceNameExpressionSource?e.deviceNameExpressionSource:t?this.getTypeSourceByValue(t):null,i=e.deviceProfileExpression||e.deviceTypeTopicExpression||e.deviceTypeJsonExpression||"default",a=e.deviceProfileExpressionSource?e.deviceProfileExpressionSource:i?this.getTypeSourceByValue(i):null;return t||i?{deviceNameExpression:t,deviceNameExpressionSource:n,deviceProfileExpression:i,deviceProfileExpressionSource:a}:null}static mapRequestToUpgradedVersion(e,t){const n=e.deviceNameJsonExpression||e.deviceNameTopicExpression||null,i=e.deviceTypeTopicExpression||e.deviceTypeJsonExpression||"default",a=i?this.getTypeSourceByValue(i):null,r=e.attributeNameExpressionSource||e.attributeNameJsonExpression||null,o=t===ci.SERVER_SIDE_RPC?1:null,s=t===ci.SERVER_SIDE_RPC?e.responseTopicExpression?Yi.WithResponse:Yi.WithoutResponse:null;return{...e,attributeNameExpression:r,attributeNameExpressionSource:r?this.getTypeSourceByValue(r):null,deviceInfo:e.deviceInfo?e.deviceInfo:n?{deviceNameExpression:n,deviceNameExpressionSource:this.getTypeSourceByValue(n),deviceProfileExpression:i,deviceProfileExpressionSource:a}:null,responseTopicQoS:o,type:s}}}e("MqttVersionMappingUtil",Na);class La{constructor(e,t){this.gatewayVersionIn=e,this.connector=t,this.gatewayVersion=Ya.parseVersion(this.gatewayVersionIn),this.configVersion=Ya.parseVersion(this.connector.configVersion??this.connector.configurationJson.configVersion)}getProcessedByVersion(){return this.isVersionUpdateNeeded()?this.processVersionUpdate():this.connector}processVersionUpdate(){return this.isVersionUpgradeNeeded()?this.getUpgradedVersion():this.isVersionDowngradeNeeded()?this.getDowngradedVersion():this.connector}isVersionUpdateNeeded(){return!!this.gatewayVersion&&this.configVersion!==this.gatewayVersion}isVersionUpgradeNeeded(){const e=Ya.parseVersion(Ai.get(this.connector.type)),t=this.gatewayVersion>=e,n=!this.configVersion||this.configVersion=e&&e>this.gatewayVersion}}e("GatewayConnectorVersionProcessor",La);class Va extends La{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t,this.mqttRequestTypeKeys=Object.values(ci)}getUpgradedVersion(){const{connectRequests:e,disconnectRequests:t,attributeRequests:n,attributeUpdates:i,serverSideRpc:a}=this.connector.configurationJson;let r={...this.connector.configurationJson,requestsMapping:Na.mapRequestsToUpgradedVersion({connectRequests:e,disconnectRequests:t,attributeRequests:n,attributeUpdates:i,serverSideRpc:a}),mapping:Na.mapMappingToUpgradedVersion(this.connector.configurationJson.mapping)};return this.mqttRequestTypeKeys.forEach((e=>{const{[e]:t,...n}=r;r={...n}})),this.cleanUpConfigJson(r),{...this.connector,configurationJson:r,configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const{requestsMapping:e,mapping:t,...n}=this.connector.configurationJson,i=e?Na.mapRequestsToDowngradedVersion(e):{},a=Na.mapMappingToDowngradedVersion(t);return{...this.connector,configurationJson:{...n,...i,mapping:a},configVersion:this.gatewayVersionIn}}cleanUpConfigJson(e){W(e.requestsMapping,{})&&delete e.requestsMapping,W(e.mapping,[])&&delete e.mapping}}e("MqttVersionProcessor",Va);class qa extends La{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{master:e.master?.slaves?Xa.mapMasterToUpgradedVersion(e.master):{slaves:[]},slave:e.slave?Xa.mapSlaveToUpgradedVersion(e.slave):{}},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{...e,slave:e.slave?Xa.mapSlaveToDowngradedVersion(e.slave):{},master:e.master?.slaves?Xa.mapMasterToDowngradedVersion(e.master):{slaves:[]}},configVersion:this.gatewayVersionIn}}}e("ModbusVersionProcessor",qa);class Ga extends La{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson.server;let t;return t="mapping"in this.connector.configurationJson.server?Za.mapMappingToUpgradedVersion(this.connector.configurationJson.server.mapping):"mapping"in this.connector.configurationJson?Za.mapMappingToCurrentVersion(this.connector.configurationJson.mapping):[],{...this.connector,configurationJson:{server:e?Za.mapServerToUpgradedVersion(e):{},mapping:t},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){return{...this.connector,configurationJson:{server:Za.mapServerToDowngradedVersion(this.connector.configurationJson)},configVersion:this.gatewayVersionIn}}}e("OpcVersionProcessor",Ga);class za{constructor(){this.fb=i(ge),this.destroyRef=i(a),this.formGroup=this.initFormGroup(),this.observeValueChanges()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.formGroup.valid?null:{formGroup:{valid:!1}}}writeValue(e){this.onWriteValue(e)}onWriteValue(e){this.formGroup.patchValue(e,{emitEvent:!1})}mapOnChangeValue(e){return e}observeValueChanges(){this.formGroup.valueChanges.pipe(In(this.destroyRef)).subscribe((e=>this.onChange(this.mapOnChangeValue(e))))}static{this.ɵfac=function(e){return new(e||za)}}static{this.ɵdir=t.ɵɵdefineDirective({type:za})}}e("ControlValueAccessorBaseAbstract",za);class Ua extends za{constructor(){super(...arguments),this.withReportStrategy=!0,this.initialized=new u,this.isLegacy=!1,this.fb=i(ge)}get basicFormGroup(){return this.formGroup}ngAfterViewInit(){this.initialized.emit()}onWriteValue(e){this.formGroup.setValue(this.mapConfigToFormValue(e),{emitEvent:!1})}mapOnChangeValue(e){return this.getMappedValue(e)}initFormGroup(){return this.initBasicFormGroup()}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(Ua)))(n||Ua)}})()}static{this.ɵdir=t.ɵɵdefineDirective({type:Ua,inputs:{generalTabContent:"generalTabContent",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},outputs:{initialized:"initialized"},features:[t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature]})}}e("GatewayConnectorBasicConfigDirective",Ua);class ja extends La{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{socket:e?Qa.mapSocketToUpgradedVersion(e):{},devices:e?.devices?Qa.mapDevicesToUpgradedVersion(e.devices):[]},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){return{...this.connector,configurationJson:Qa.mapSocketToDowngradedVersion(this.connector.configurationJson),configVersion:this.gatewayVersionIn}}}e("SocketVersionProcessor",ja);class Ha extends La{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{application:e?.general?Ja.mapApplicationToUpgradedVersion(e.general):{},devices:e?.devices?Ja.mapDevicesToUpgradedVersion(e.devices):[]},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{general:e?.application?Ja.mapApplicationToDowngradedVersion(e.application):{},devices:e?.devices?Ja.mapDevicesToDowngradedVersion(e.devices):[]},configVersion:this.gatewayVersionIn}}}e("BacnetVersionProcessor",Ha);const Wa=["searchInput"];class $a{constructor(){this.withReportStrategy=!0,this.textSearchMode=!1,this.onChange=()=>{},this.translate=i(pe),this.dialog=i(ze),this.dialogService=i(X),this.fb=i(ge),this.cd=i(h),this.destroyRef=i(a),this.textSearch=this.fb.control("",{nonNullable:!0}),this.entityFormArray=this.fb.array([]),this.entityFormArray.valueChanges.pipe(In()).subscribe((e=>{this.updateTableData(e),this.onChange(e)})),this.dataSource=this.getDatasource()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(De(150),Pe(((e,t)=>(e??"")===t.trim())),In(this.destroyRef)).subscribe((e=>this.updateTableData(this.entityFormArray.value,e.trim())))}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){this.entityFormArray.clear(),this.pushDataAsFormArrays(e)}enterFilterMode(){this.textSearchMode=!0,this.cd.detectChanges();const e=this.searchInputField.nativeElement;e.focus(),e.setSelectionRange(0,0)}exitFilterMode(){this.updateTableData(this.entityFormArray.value),this.textSearchMode=!1,this.textSearch.reset()}validate(){return this.entityFormArray.controls.length?null:{devicesFormGroup:{valid:!1}}}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.entityFormArray.push(this.fb.control(e))))}static{this.ɵfac=function(e){return new(e||$a)}}static{this.ɵdir=t.ɵɵdefineDirective({type:$a,viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(Wa,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},features:[t.ɵɵInputTransformsFeature]})}}e("AbstractDevicesConfigTableComponent",$a);class Ka extends La{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t,this.restRequestTypeKeys=Object.values(Ii)}getUpgradedVersion(){const{attributeRequests:e=[],attributeUpdates:t=[],serverSideRpc:n=[],mapping:i=[],...a}=this.connector.configurationJson;let r={server:Ya.cleanUpConfigBaseInfo(a),requestsMapping:{attributeRequests:e,attributeUpdates:t,serverSideRpc:n},mapping:er.mapMappingToUpgradedVersion(i)};return this.restRequestTypeKeys.forEach((e=>{const{[e]:t,...n}=r;r={...n}})),{...this.connector,configurationJson:r,configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const{requestsMapping:e={},mapping:t,server:n,...i}=this.connector.configurationJson,{attributeRequests:a=[],attributeUpdates:r=[],serverSideRpc:o=[]}=e;return{...this.connector,configurationJson:{...i,...n,attributeRequests:a,attributeUpdates:r,serverSideRpc:o,mapping:er.mapMappingToDowngradedVersion(t)},configVersion:this.gatewayVersionIn}}}e("RestVersionProcessor",Ka);class Ya{static getConfig(e,t){switch(e.type){case dt.MQTT:return new Va(t,e).getProcessedByVersion();case dt.OPCUA:return new Ga(t,e).getProcessedByVersion();case dt.MODBUS:return new qa(t,e).getProcessedByVersion();case dt.SOCKET:return new ja(t,e).getProcessedByVersion();case dt.BACNET:return new Ha(t,e).getProcessedByVersion();case dt.REST:return new Ka(t,e).getProcessedByVersion();default:return e}}static parseVersion(e){if(Z(e))return e;if(Q(e)){const[t,n="0",i="0"]=e.split(".");return parseFloat(`${t}.${n}${i.slice(0,1)}`)}return 0}static cleanUpConfigBaseInfo(e){const{name:t,id:n,enableRemoteLogging:i,logLevel:a,reportStrategy:r,configVersion:o,...s}=e;return s}}e("GatewayConnectorVersionMappingUtil",Ya);class Xa{static mapMasterToUpgradedVersion(e){return{slaves:e.slaves.map((e=>{const{sendDataOnlyOnChange:t,...n}=e;return{...n,deviceType:e.deviceType??"default",reportStrategy:t?{type:rn.OnChange}:{type:rn.OnReportPeriod,reportPeriod:e.pollPeriod}}}))}}static mapMasterToDowngradedVersion(e){return{slaves:e.slaves.map((e=>{const{reportStrategy:t,...n}=e;return{...n,sendDataOnlyOnChange:t?.type!==rn.OnReportPeriod}}))}}static mapSlaveToDowngradedVersion(e){if(!e?.values)return e;const t=Object.keys(e.values).reduce(((t,n)=>t={...t,[n]:[e.values[n]]}),{});return{...e,values:t}}static mapSlaveToUpgradedVersion(e){if(!e?.values)return e;const t=Object.keys(e.values).reduce(((t,n)=>t={...t,[n]:this.mapValuesToUpgradedVersion(e.values[n][0]??{})}),{});return{...e,values:t}}static mapValuesToUpgradedVersion(e){return Object.keys(e).reduce(((t,n)=>t={...t,[n]:e[n].map((e=>({...e,type:"int"===e.type?Zt.INT16:e.type})))}),{})}}e("ModbusVersionMappingUtil",Xa);class Za{static mapServerToUpgradedVersion(e){const{mapping:t,disableSubscriptions:n,pollPeriodInMillis:i,...a}=e;return{...a,pollPeriodInMillis:i??5e3,enableSubscriptions:!n}}static mapServerToDowngradedVersion(e){const{mapping:t,server:n}=e,{enableSubscriptions:i,...a}=n??{};return{...a,mapping:t?this.mapMappingToDowngradedVersion(t):[],disableSubscriptions:!i}}static mapMappingToUpgradedVersion(e){return e.map((e=>({deviceNodePattern:e.deviceNodePattern,deviceNodeSource:this.getDeviceNodeSourceByValue(e.deviceNodePattern),deviceInfo:{deviceNameExpression:e.deviceNamePattern,deviceNameExpressionSource:this.getTypeSourceByValue(e.deviceNamePattern),deviceProfileExpression:e.deviceTypePattern??"default",deviceProfileExpressionSource:this.getTypeSourceByValue(e.deviceTypePattern??"default")},attributes:e.attributes?.map((e=>({key:e.key,type:this.getTypeSourceByValue(e.path),value:e.path})))??[],attributes_updates:e.attributes_updates?.map((e=>({key:e.attributeOnThingsBoard,type:this.getTypeSourceByValue(e.attributeOnDevice),value:e.attributeOnDevice})))??[],timeseries:e.timeseries?.map((e=>({key:e.key,type:this.getTypeSourceByValue(e.path),value:e.path})))??[],rpc_methods:e.rpc_methods?.map((e=>({method:e.method,arguments:e.arguments.map((e=>({value:e,type:this.getArgumentType(e)})))})))??[]})))}static mapMappingToCurrentVersion(e){return e.map((e=>({deviceNodePattern:e.deviceNodePattern,deviceNodeSource:this.getDeviceNodeSourceByValue(e.deviceNodePattern),deviceInfo:{deviceNameExpression:e.deviceInfo.deviceNameExpression,deviceNameExpressionSource:this.getTypeSourceByValue(e?.deviceInfo?.deviceNameExpressionSource),deviceProfileExpression:e?.deviceInfo?.deviceProfileExpression??"default",deviceProfileExpressionSource:this.getTypeSourceByValue(e?.deviceInfo?.deviceProfileExpressionSource??"default")},attributes:e.attributes?.map((e=>({key:e.key,type:e.type,value:e?.value})))??[],attributes_updates:e.attributes_updates?.map((e=>({key:e.key,type:e.type,value:e.value})))??[],timeseries:e.timeseries?.map((e=>({key:e.key,type:e.type,value:e.value})))??[],rpc_methods:e.rpc_methods?.map((e=>({method:e.method,arguments:e.arguments.map((e=>({value:e,type:this.getArgumentType(e)})))})))??[]})))}static mapMappingToDowngradedVersion(e){return e.map((e=>({deviceNodePattern:e.deviceNodePattern,deviceNamePattern:e.deviceInfo.deviceNameExpression,deviceTypePattern:e.deviceInfo.deviceProfileExpression,attributes:e.attributes.map((e=>({key:e.key,path:e.value}))),attributes_updates:e.attributes_updates.map((e=>({attributeOnThingsBoard:e.key,attributeOnDevice:e.value}))),timeseries:e.timeseries.map((e=>({key:e.key,path:e.value}))),rpc_methods:e.rpc_methods.map((e=>({method:e.method,arguments:e.arguments.map((e=>e.value))})))})))}static getTypeSourceByValue(e){return e.includes("${")?Fi.IDENTIFIER:e.includes("/")||e.includes("\\")?Fi.PATH:Fi.CONST}static getDeviceNodeSourceByValue(e){return e.includes("${")?Fi.IDENTIFIER:Fi.PATH}static getArgumentType(e){switch(typeof e){case"boolean":return"boolean";case"number":return Number.isInteger(e)?"integer":"float";default:return"string"}}}e("OpcVersionMappingUtil",Za);class Qa{static mapSocketToUpgradedVersion(e){const{devices:t,...n}=e??{};return Ya.cleanUpConfigBaseInfo(n)}static mapSocketToDowngradedVersion(e){const{devices:t,socket:n}=e??{};return{...n,devices:this.mapDevicesToDowngradedVersion(t??[])}}static mapDevicesToUpgradedVersion(e){return e?.map((e=>({...e,attributeRequests:e.attributeRequests?.map((e=>({...e,requestExpressionSource:this.getExpressionSource(e.requestExpression),attributeNameExpressionSource:this.getExpressionSource(e.attributeNameExpression)})))??[]})))??[]}static mapDevicesToDowngradedVersion(e){return e.map((e=>({...e,attributeRequests:e.attributeRequests?.map((({requestExpressionSource:e,attributeNameExpressionSource:t,...n})=>n))??[]})))}static getExpressionSource(e){return e.includes("${")||e.includes("[")?yi.Expression:yi.Constant}}e("SocketVersionMappingUtil",Qa);class Ja{static mapApplicationToUpgradedVersion(e){const{address:t="",...n}=e,[i,a]=t.split(":"),[r,o]=i.split("/");return{host:r,port:a,mask:o,...n}}static mapApplicationToDowngradedVersion(e){const{host:t="",port:n="",mask:i="",...a}=e;return{address:i?`${t}/${i}:${n}`:`${t}:${n}`,...a}}static mapDevicesToUpgradedVersion(e){return e?.map((({address:e="",deviceName:t,deviceType:n,attributes:i,timeseries:a,attributeUpdates:r,serverSideRpc:o,...s})=>({...s,host:e.split(":")[0],port:e.split(":")[1],deviceInfo:{deviceNameExpression:t,deviceProfileExpression:n,deviceNameExpressionSource:this.getExpressionSource(t),deviceProfileExpressionSource:this.getExpressionSource(n)},attributes:this.getUpdateKeys(i),timeseries:this.getUpdateKeys(a),attributeUpdates:this.getUpdateKeys(r),serverSideRpc:this.getUpdateKeys(o)})))??[]}static mapDevicesToDowngradedVersion(e){return e?.map((({port:e,host:t,deviceInfo:n,attributes:i,timeseries:a,attributeUpdates:r,serverSideRpc:o,...s})=>({...s,address:`${t}:${e}`,deviceName:n?.deviceNameExpression,deviceType:n?.deviceProfileExpression,attributes:this.getDowngradedKeys(i),timeseries:this.getDowngradedKeys(a),attributeUpdates:this.getDowngradedKeys(r),serverSideRpc:this.getDowngradedKeys(o)})))??[]}static getExpressionSource(e){return e?.includes("${")||e?.includes("[")?yi.Expression:yi.Constant}static getUpdateKeys(e){return e?.map((({objectId:e="",...t})=>({objectType:e.split(":")[0],objectId:e.split(":")[1],...t})))??[]}static getDowngradedKeys(e){return e?.map((({objectId:e="",objectType:t="",...n})=>({objectId:`${t}:${e}`,...n})))??[]}}e("BacnetVersionMappingUtil",Ja);class er{static mapMappingToUpgradedVersion(e){return e?.map((({converter:e,...t})=>{const{deviceNameExpression:n,deviceTypeExpression:i,...a}=e;return{converter:{deviceInfo:{deviceNameExpressionSource:this.getTypeSourceByValue(n),deviceNameExpression:n,deviceProfileExpressionSource:this.getTypeSourceByValue(i),deviceProfileExpression:i},...a},...t}}))}static mapMappingToDowngradedVersion(e){return e?.map((({converter:e,...t})=>{const{deviceInfo:n,...i}=e;return{converter:{deviceNameExpression:n?.deviceNameExpression,deviceTypeExpression:n?.deviceProfileExpression,...i},...t}}))}static getTypeSourceByValue(e=""){return e.includes("${")?Si.REQUEST:Si.CONST}}e("RestVersionMappingUtil",er);class tr{transform(e,t){const n=Ya.parseVersion(e);return t===dt.MODBUS?n>=Ya.parseVersion(ct.v3_5_2):n>=Ya.parseVersion(ct.v3_6_0)}static{this.ɵfac=function(e){return new(e||tr)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"withReportStrategy",type:tr,pure:!0,standalone:!0})}}function nr(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-required")," "))}function ir(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-pattern")," "))}function ar(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.name-already-exists")," "))}function rr(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-required")," "))}function or(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-pattern")," "))}function sr(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-min")," "))}function lr(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.command-required")," "))}function pr(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.command-pattern")," "))}e("ReportStrategyVersionPipe",tr);class cr extends A{constructor(e,t,n,i,a,r,o,s){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.dialogService=r,this.translate=o,this.destroyRef=s,this.commandHelpLink=O+"/docs/iot-gateway/configuration/#subsection-statistics",this.editCommandFormGroup=this.fb.group({attributeOnGateway:["",[me.required,me.pattern(pn),this.uniqNameRequired()]],command:["",[me.required,me.pattern(/^(?=\S).*\S$/)]],timeout:[10,[me.required,me.min(1),me.pattern(ln),me.pattern(/^[^.\s]+$/)]],installCmd:["",me.pattern(pn)]}),this.editCommandFormGroup.patchValue(this.data.command,{emitEvent:!1})}cancel(){this.confirmConnectorChange().pipe(Le(Boolean),In(this.destroyRef)).subscribe((()=>this.dialogRef.close(null)))}apply(){this.dialogRef.close({current:this.editCommandFormGroup.value,prev:this.data.command})}confirmConnectorChange(){return this.editCommandFormGroup.dirty?this.dialogService.confirm(this.translate.instant("gateway.statistics.change-command-title"),this.translate.instant("gateway.statistics.change-command-text"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0):_e(!0)}uniqNameRequired(){return e=>{const t=e.value?.trim().toLowerCase();return t&&this.data.existingCommands.some((e=>e.toLowerCase()===t))&&t!==this.data.command.attributeOnGateway.toLowerCase()?{duplicateName:{valid:!1}}:null}}static{this.ɵfac=function(e){return new(e||cr)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(K.DialogService),t.ɵɵdirectiveInject(le.TranslateService),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:cr,selectors:[["tb-edit-custom-command-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:64,vars:27,consts:[[1,"edit-command-container",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel","stroked","no-padding-bottom","no-gap","command-container"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["translate",""],["matInput","","formControlName","attributeOnGateway"],["matIconSuffix","",1,"cursor-pointer",3,"matTooltip"],["matInput","","formControlName","timeout","type","number","min","1"],["appearance","outline",1,"mat-block"],["matInput","","formControlName","command"],[1,"tb-settings","pb-4"],["translate","",1,"tb-form-panel-title"],["matInput","","formControlName","installCmd"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"mat-toolbar",1)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",2)(6,"div",3),t.ɵɵelementStart(7,"button",4),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",5),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",6)(11,"div",7)(12,"section",8)(13,"section",9)(14,"mat-form-field",10)(15,"mat-label",11),t.ɵɵtext(16,"gateway.statistics.name"),t.ɵɵelementEnd(),t.ɵɵelement(17,"input",12),t.ɵɵtemplate(18,nr,3,3,"mat-error")(19,ir,3,3,"mat-error")(20,ar,3,3,"mat-error"),t.ɵɵelementStart(21,"mat-icon",13),t.ɵɵpipe(22,"translate"),t.ɵɵtext(23,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(24,"mat-form-field",10)(25,"mat-label",11),t.ɵɵtext(26,"gateway.statistics.timeout"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",14),t.ɵɵtemplate(28,rr,3,3,"mat-error")(29,or,3,3,"mat-error")(30,sr,3,3,"mat-error"),t.ɵɵelementStart(31,"mat-icon",13),t.ɵɵpipe(32,"translate"),t.ɵɵtext(33,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(34,"section")(35,"mat-form-field",15)(36,"mat-label",11),t.ɵɵtext(37,"gateway.statistics.command"),t.ɵɵelementEnd(),t.ɵɵelement(38,"input",16),t.ɵɵtemplate(39,lr,3,3,"mat-error")(40,pr,3,3,"mat-error"),t.ɵɵelementStart(41,"mat-icon",13),t.ɵɵpipe(42,"translate"),t.ɵɵtext(43,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(44,"section")(45,"mat-expansion-panel",17)(46,"mat-expansion-panel-header")(47,"mat-panel-title")(48,"div",18),t.ɵɵtext(49,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(50,"mat-form-field",10)(51,"mat-label",11),t.ɵɵtext(52,"gateway.statistics.install-cmd"),t.ɵɵelementEnd(),t.ɵɵelement(53,"input",19),t.ɵɵelementStart(54,"mat-icon",13),t.ɵɵpipe(55,"translate"),t.ɵɵtext(56,"info_outlined "),t.ɵɵelementEnd()()()()()()(),t.ɵɵelementStart(57,"div",20)(58,"button",21),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(59),t.ɵɵpipe(60,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(61,"button",22),t.ɵɵlistener("click",(function(){return n.apply()})),t.ɵɵtext(62),t.ɵɵpipe(63,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.editCommandFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,13,n.data.titleText)),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.commandHelpLink),t.ɵɵadvance(12),t.ɵɵconditional(n.editCommandFormGroup.get("attributeOnGateway").hasError("required")?18:n.editCommandFormGroup.get("attributeOnGateway").hasError("pattern")?19:n.editCommandFormGroup.get("attributeOnGateway").hasError("duplicateName")?20:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(22,15,"gateway.hints.attribute")),t.ɵɵadvance(7),t.ɵɵconditional(n.editCommandFormGroup.get("timeout").hasError("required")?28:n.editCommandFormGroup.get("timeout").hasError("pattern")?29:n.editCommandFormGroup.get("timeout").hasError("min")?30:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(32,17,"gateway.hints.timeout")),t.ɵɵadvance(8),t.ɵɵconditional(n.editCommandFormGroup.get("command").hasError("required")?39:n.editCommandFormGroup.get("command").hasError("pattern")?40:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(42,19,"gateway.hints.command")),t.ɵɵadvance(13),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(55,21,"gateway.hints.install-cmd")),t.ɵɵadvance(5),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(60,23,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.editCommandFormGroup.invalid||!n.editCommandFormGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(63,25,n.data.buttonText)," "))},dependencies:t.ɵɵgetComponentDepsFactory(cr,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .edit-command-container[_ngcontent-%COMP%]{min-width:40vw;width:50vw}[_nghost-%COMP%] .pointer-event{pointer-events:all}[_nghost-%COMP%] .toggle-group span{padding:0 25px}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{color:#e0e0e0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix:hover{color:#9e9e9e}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex;align-items:center}']})}}var dr=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},ur=new function(){this.browser=new dr,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(ur.wxa=!0,ur.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?ur.worker=!0:"undefined"==typeof navigator||0===navigator.userAgent.indexOf("Node.js")?(ur.node=!0,ur.svgSupported=!0):function(e,t){var n=t.browser,i=e.match(/Firefox\/([\d.]+)/),a=e.match(/MSIE\s([\d.]+)/)||e.match(/Trident\/.+?rv:(([\d.]+))/),r=e.match(/Edge?\/([\d.]+)/),o=/micromessenger/i.test(e);i&&(n.firefox=!0,n.version=i[1]);a&&(n.ie=!0,n.version=a[1]);r&&(n.edge=!0,n.version=r[1],n.newEdge=+r[1].split(".")[0]>18);o&&(n.weChat=!0);t.svgSupported="undefined"!=typeof SVGRect,t.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,t.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),t.domSupported="undefined"!=typeof document;var s=document.documentElement.style;t.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),t.transformSupported=t.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,ur);var mr="sans-serif",hr="12px "+mr;var gr,fr,yr=function(e){var t={};if("undefined"==typeof JSON)return t;for(var n=0;n=0)r=a*e.length;else for(var o=0;o>1)%2;o.style.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",i[s]+":0",a[l]+":0",i[1-s]+":auto",a[1-l]+":auto",""].join("!important;"),e.appendChild(o),n.push(o)}return n}(t,r),s=function(e,t,n){for(var i=n?"invTrans":"trans",a=t[i],r=t.srcCoords,o=[],s=[],l=!0,p=0;p<4;p++){var c=e[p].getBoundingClientRect(),d=2*p,u=c.left,m=c.top;o.push(u,m),l=l&&r&&u===r[d]&&m===r[d+1],s.push(e[p].offsetLeft,e[p].offsetTop)}return l&&a?a:(t.srcCoords=o,t[i]=n?Jo(s,o):Jo(o,s))}(o,r,a);if(s)return s(e,n,i),!0}return!1}function is(e){return"CANVAS"===e.nodeName.toUpperCase()}var as=/([&<>"'])/g,rs={"&":"&","<":"<",">":">",'"':""","'":"'"};function os(e){return null==e?"":(e+"").replace(as,(function(e,t){return rs[t]}))}var ss=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ls=[],ps=ur.browser.firefox&&+ur.browser.version.split(".")[0]<39;function cs(e,t,n,i){return n=n||{},i?ds(e,t,n):ps&&null!=t.layerX&&t.layerX!==t.offsetX?(n.zrX=t.layerX,n.zrY=t.layerY):null!=t.offsetX?(n.zrX=t.offsetX,n.zrY=t.offsetY):ds(e,t,n),n}function ds(e,t,n){if(ur.domSupported&&e.getBoundingClientRect){var i=t.clientX,a=t.clientY;if(is(e)){var r=e.getBoundingClientRect();return n.zrX=i-r.left,void(n.zrY=a-r.top)}if(ns(ls,e,i,a))return n.zrX=ls[0],void(n.zrY=ls[1])}n.zrX=n.zrY=0}function us(e){return e||window.event}function ms(e,t,n){if(null!=(t=us(t)).zrX)return t;var i=t.type;if(i&&i.indexOf("touch")>=0){var a="touchend"!==i?t.targetTouches[0]:t.changedTouches[0];a&&cs(e,a,t,n)}else{cs(e,t,t,n);var r=function(e){var t=e.wheelDelta;if(t)return t;var n=e.deltaX,i=e.deltaY;if(null==n||null==i)return t;return 3*(0!==i?Math.abs(i):Math.abs(n))*(i>0?-1:i<0?1:n>0?-1:1)}(t);t.zrDelta=r?r/120:-(t.detail||0)/3}var o=t.button;return null==t.which&&void 0!==o&&ss.test(t.type)&&(t.which=1&o?1:2&o?3:4&o?2:0),t}function hs(e,t,n,i){e.addEventListener(t,n,i)}var gs=function(e){e.preventDefault(),e.stopPropagation(),e.cancelBubble=!0};function fs(e){return 2===e.which||3===e.which}var ys=function(){function e(){this._track=[]}return e.prototype.recognize=function(e,t,n){return this._doTrack(e,t,n),this._recognize(e)},e.prototype.clear=function(){return this._track.length=0,this},e.prototype._doTrack=function(e,t,n){var i=e.touches;if(i){for(var a={points:[],touches:[],target:t,event:e},r=0,o=i.length;r1&&a&&a.length>1){var o=vs(a)/vs(r);!isFinite(o)&&(o=1),t.pinchScale=o;var s=[((i=a)[0][0]+i[1][0])/2,(i[0][1]+i[1][1])/2];return t.pinchX=s[0],t.pinchY=s[1],{type:"pinch",target:e[0].target,event:t}}}}};function bs(e){return e[0]=1,e[1]=0,e[2]=0,e[3]=1,e[4]=0,e[5]=0,e}function ws(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4],e[5]=t[5],e}function Ss(e,t,n){var i=t[0]*n[0]+t[2]*n[1],a=t[1]*n[0]+t[3]*n[1],r=t[0]*n[2]+t[2]*n[3],o=t[1]*n[2]+t[3]*n[3],s=t[0]*n[4]+t[2]*n[5]+t[4],l=t[1]*n[4]+t[3]*n[5]+t[5];return e[0]=i,e[1]=a,e[2]=r,e[3]=o,e[4]=s,e[5]=l,e}function Cs(e,t,n){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4]+n[0],e[5]=t[5]+n[1],e}function _s(e,t,n,i){void 0===i&&(i=[0,0]);var a=t[0],r=t[2],o=t[4],s=t[1],l=t[3],p=t[5],c=Math.sin(n),d=Math.cos(n);return e[0]=a*d+s*c,e[1]=-a*c+s*d,e[2]=r*d+l*c,e[3]=-r*c+d*l,e[4]=d*(o-i[0])+c*(p-i[1])+i[0],e[5]=d*(p-i[1])-c*(o-i[0])+i[1],e}function Ts(e,t,n){var i=n[0],a=n[1];return e[0]=t[0]*i,e[1]=t[1]*a,e[2]=t[2]*i,e[3]=t[3]*a,e[4]=t[4]*i,e[5]=t[5]*a,e}function Is(e,t){var n=t[0],i=t[2],a=t[4],r=t[1],o=t[3],s=t[5],l=n*o-r*i;return l?(l=1/l,e[0]=o*l,e[1]=-r*l,e[2]=-i*l,e[3]=n*l,e[4]=(i*s-o*a)*l,e[5]=(r*a-n*s)*l,e):null}function Es(e){var t=[1,0,0,1,0,0];return ws(t,e),t}var Ms=function(){function e(e,t){this.x=e||0,this.y=t||0}return e.prototype.copy=function(e){return this.x=e.x,this.y=e.y,this},e.prototype.clone=function(){return new e(this.x,this.y)},e.prototype.set=function(e,t){return this.x=e,this.y=t,this},e.prototype.equal=function(e){return e.x===this.x&&e.y===this.y},e.prototype.add=function(e){return this.x+=e.x,this.y+=e.y,this},e.prototype.scale=function(e){this.x*=e,this.y*=e},e.prototype.scaleAndAdd=function(e,t){this.x+=e.x*t,this.y+=e.y*t},e.prototype.sub=function(e){return this.x-=e.x,this.y-=e.y,this},e.prototype.dot=function(e){return this.x*e.x+this.y*e.y},e.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},e.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},e.prototype.normalize=function(){var e=this.len();return this.x/=e,this.y/=e,this},e.prototype.distance=function(e){var t=this.x-e.x,n=this.y-e.y;return Math.sqrt(t*t+n*n)},e.prototype.distanceSquare=function(e){var t=this.x-e.x,n=this.y-e.y;return t*t+n*n},e.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},e.prototype.transform=function(e){if(e){var t=this.x,n=this.y;return this.x=e[0]*t+e[2]*n+e[4],this.y=e[1]*t+e[3]*n+e[5],this}},e.prototype.toArray=function(e){return e[0]=this.x,e[1]=this.y,e},e.prototype.fromArray=function(e){this.x=e[0],this.y=e[1]},e.set=function(e,t,n){e.x=t,e.y=n},e.copy=function(e,t){e.x=t.x,e.y=t.y},e.len=function(e){return Math.sqrt(e.x*e.x+e.y*e.y)},e.lenSquare=function(e){return e.x*e.x+e.y*e.y},e.dot=function(e,t){return e.x*t.x+e.y*t.y},e.add=function(e,t,n){e.x=t.x+n.x,e.y=t.y+n.y},e.sub=function(e,t,n){e.x=t.x-n.x,e.y=t.y-n.y},e.scale=function(e,t,n){e.x=t.x*n,e.y=t.y*n},e.scaleAndAdd=function(e,t,n,i){e.x=t.x+n.x*i,e.y=t.y+n.y*i},e.lerp=function(e,t,n,i){var a=1-i;e.x=a*t.x+i*n.x,e.y=a*t.y+i*n.y},e}(),ks=Math.min,Ps=Math.max,Ds=new Ms,Os=new Ms,As=new Ms,Fs=new Ms,Rs=new Ms,Bs=new Ms,Ns=function(){function e(e,t,n,i){n<0&&(e+=n,n=-n),i<0&&(t+=i,i=-i),this.x=e,this.y=t,this.width=n,this.height=i}return e.prototype.union=function(e){var t=ks(e.x,this.x),n=ks(e.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=Ps(e.x+e.width,this.x+this.width)-t:this.width=e.width,isFinite(this.y)&&isFinite(this.height)?this.height=Ps(e.y+e.height,this.y+this.height)-n:this.height=e.height,this.x=t,this.y=n},e.prototype.applyTransform=function(t){e.applyTransform(this,this,t)},e.prototype.calculateTransform=function(e){var t=this,n=e.width/t.width,i=e.height/t.height,a=[1,0,0,1,0,0];return Cs(a,a,[-t.x,-t.y]),Ts(a,a,[n,i]),Cs(a,a,[e.x,e.y]),a},e.prototype.intersect=function(t,n){if(!t)return!1;t instanceof e||(t=e.create(t));var i=this,a=i.x,r=i.x+i.width,o=i.y,s=i.y+i.height,l=t.x,p=t.x+t.width,c=t.y,d=t.y+t.height,u=!(rh&&(h=x,gh&&(h=b,y=n.x&&e<=n.x+n.width&&t>=n.y&&t<=n.y+n.height},e.prototype.clone=function(){return new e(this.x,this.y,this.width,this.height)},e.prototype.copy=function(t){e.copy(this,t)},e.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},e.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},e.prototype.isZero=function(){return 0===this.width||0===this.height},e.create=function(t){return new e(t.x,t.y,t.width,t.height)},e.copy=function(e,t){e.x=t.x,e.y=t.y,e.width=t.width,e.height=t.height},e.applyTransform=function(t,n,i){if(i){if(i[1]<1e-5&&i[1]>-1e-5&&i[2]<1e-5&&i[2]>-1e-5){var a=i[0],r=i[3],o=i[4],s=i[5];return t.x=n.x*a+o,t.y=n.y*r+s,t.width=n.width*a,t.height=n.height*r,t.width<0&&(t.x+=t.width,t.width=-t.width),void(t.height<0&&(t.y+=t.height,t.height=-t.height))}Ds.x=As.x=n.x,Ds.y=Fs.y=n.y,Os.x=Fs.x=n.x+n.width,Os.y=As.y=n.y+n.height,Ds.transform(i),Fs.transform(i),Os.transform(i),As.transform(i),t.x=ks(Ds.x,Os.x,As.x,Fs.x),t.y=ks(Ds.y,Os.y,As.y,Fs.y);var l=Ps(Ds.x,Os.x,As.x,Fs.x),p=Ps(Ds.y,Os.y,As.y,Fs.y);t.width=l-t.x,t.height=p-t.y}else t!==n&&e.copy(t,n)},e}(),Ls="silent";function Vs(){gs(this.event)}var qs=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.handler=null,t}return We(t,e),t.prototype.dispose=function(){},t.prototype.setCursor=function(){},t}(Xo),Gs=function(e,t){this.x=e,this.y=t},zs=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],Us=new Ns(0,0,0,0),js=function(e){function t(t,n,i,a,r){var o=e.call(this)||this;return o._hovered=new Gs(0,0),o.storage=t,o.painter=n,o.painterRoot=a,o._pointerSize=r,i=i||new qs,o.proxy=null,o.setHandlerProxy(i),o._draggingMgr=new Yo(o),o}return We(t,e),t.prototype.setHandlerProxy=function(e){this.proxy&&this.proxy.dispose(),e&&(zr(zs,(function(t){e.on&&e.on(t,this[t],this)}),this),e.handler=this),this.proxy=e},t.prototype.mousemove=function(e){var t=e.zrX,n=e.zrY,i=$s(this,t,n),a=this._hovered,r=a.target;r&&!r.__zr&&(r=(a=this.findHover(a.x,a.y)).target);var o=this._hovered=i?new Gs(t,n):this.findHover(t,n),s=o.target,l=this.proxy;l.setCursor&&l.setCursor(s?s.cursor:"default"),r&&s!==r&&this.dispatchToElement(a,"mouseout",e),this.dispatchToElement(o,"mousemove",e),s&&s!==r&&this.dispatchToElement(o,"mouseover",e)},t.prototype.mouseout=function(e){var t=e.zrEventControl;"only_globalout"!==t&&this.dispatchToElement(this._hovered,"mouseout",e),"no_globalout"!==t&&this.trigger("globalout",{type:"globalout",event:e})},t.prototype.resize=function(){this._hovered=new Gs(0,0)},t.prototype.dispatch=function(e,t){var n=this[e];n&&n.call(this,t)},t.prototype.dispose=function(){this.proxy.dispose(),this.storage=null,this.proxy=null,this.painter=null},t.prototype.setCursorStyle=function(e){var t=this.proxy;t.setCursor&&t.setCursor(e)},t.prototype.dispatchToElement=function(e,t,n){var i=(e=e||{}).target;if(!i||!i.silent){for(var a="on"+t,r=function(e,t,n){return{type:e,event:n,target:t.target,topTarget:t.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which,stop:Vs}}(t,e,n);i&&(i[a]&&(r.cancelBubble=!!i[a].call(i,r)),i.trigger(t,r),i=i.__hostTarget?i.__hostTarget:i.parent,!r.cancelBubble););r.cancelBubble||(this.trigger(t,r),this.painter&&this.painter.eachOtherLayer&&this.painter.eachOtherLayer((function(e){"function"==typeof e[a]&&e[a].call(e,r),e.trigger&&e.trigger(t,r)})))}},t.prototype.findHover=function(e,t,n){var i=this.storage.getDisplayList(),a=new Gs(e,t);if(Ws(i,a,e,t,n),this._pointerSize&&!a.target){for(var r=[],o=this._pointerSize,s=o/2,l=new Ns(e-s,t-s,o,o),p=i.length-1;p>=0;p--){var c=i[p];c===n||c.ignore||c.ignoreCoarsePointer||c.parent&&c.parent.ignoreCoarsePointer||(Us.copy(c.getBoundingRect()),c.transform&&Us.applyTransform(c.transform),Us.intersect(l)&&r.push(c))}if(r.length)for(var d=Math.PI/12,u=2*Math.PI,m=0;m=0;r--){var o=e[r],s=void 0;if(o!==a&&!o.ignore&&(s=Hs(o,n,i))&&(!t.topTarget&&(t.topTarget=o),s!==Ls)){t.target=o;break}}}function $s(e,t,n){var i=e.painter;return t<0||t>i.getWidth()||n<0||n>i.getHeight()}zr(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],(function(e){js.prototype[e]=function(t){var n,i,a=t.zrX,r=t.zrY,o=$s(this,a,r);if("mouseup"===e&&o||(i=(n=this.findHover(a,r)).target),"mousedown"===e)this._downEl=i,this._downPoint=[t.zrX,t.zrY],this._upEl=i;else if("mouseup"===e)this._upEl=i;else if("click"===e){if(this._downEl!==this._upEl||!this._downPoint||Go(this._downPoint,[t.zrX,t.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,e,t)}}));function Ks(e,t,n,i){var a=t+1;if(a===n)return 1;if(i(e[a++],e[t])<0){for(;a=0;)a++;return a-t}function Ys(e,t,n,i,a){for(i===t&&i++;i>>1])<0?l=r:s=r+1;var p=i-s;switch(p){case 3:e[s+3]=e[s+2];case 2:e[s+2]=e[s+1];case 1:e[s+1]=e[s];break;default:for(;p>0;)e[s+p]=e[s+p-1],p--}e[s]=o}}function Xs(e,t,n,i,a,r){var o=0,s=0,l=1;if(r(e,t[n+a])>0){for(s=i-a;l0;)o=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),o+=a,l+=a}else{for(s=a+1;ls&&(l=s);var p=o;o=a-l,l=a-p}for(o++;o>>1);r(e,t[n+c])>0?o=c+1:l=c}return l}function Zs(e,t,n,i,a,r){var o=0,s=0,l=1;if(r(e,t[n+a])<0){for(s=a+1;ls&&(l=s);var p=o;o=a-l,l=a-p}else{for(s=i-a;l=0;)o=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),o+=a,l+=a}for(o++;o>>1);r(e,t[n+c])<0?l=c:o=c+1}return l}function Qs(e,t){var n,i,a=7,r=0,o=[];function s(s){var l=n[s],p=i[s],c=n[s+1],d=i[s+1];i[s]=p+d,s===r-3&&(n[s+1]=n[s+2],i[s+1]=i[s+2]),r--;var u=Zs(e[c],e,l,p,0,t);l+=u,0!==(p-=u)&&0!==(d=Xs(e[l+p-1],e,c,d,d-1,t))&&(p<=d?function(n,i,r,s){var l=0;for(l=0;l=7||m>=7);if(h)break;g<0&&(g=0),g+=2}if((a=g)<1&&(a=1),1===i){for(l=0;l=0;l--)e[m+l]=e[u+l];return void(e[d]=o[c])}var h=a;for(;;){var g=0,f=0,y=!1;do{if(t(o[c],e[p])<0){if(e[d--]=e[p--],g++,f=0,0==--i){y=!0;break}}else if(e[d--]=o[c--],f++,g=0,1==--s){y=!0;break}}while((g|f)=0;l--)e[m+l]=e[u+l];if(0===i){y=!0;break}}if(e[d--]=o[c--],1==--s){y=!0;break}if(0!==(f=s-Xs(e[p],o,0,s,s-1,t))){for(s-=f,m=(d-=f)+1,u=(c-=f)+1,l=0;l=7||f>=7);if(y)break;h<0&&(h=0),h+=2}(a=h)<1&&(a=1);if(1===s){for(m=(d-=i)+1,u=(p-=i)+1,l=i-1;l>=0;l--)e[m+l]=e[u+l];e[d]=o[c]}else{if(0===s)throw new Error;for(u=d-(s-1),l=0;l1;){var e=r-2;if(e>=1&&i[e-1]<=i[e]+i[e+1]||e>=2&&i[e-2]<=i[e]+i[e-1])i[e-1]i[e+1])break;s(e)}},forceMergeRuns:function(){for(;r>1;){var e=r-2;e>0&&i[e-1]=32;)t|=1&e,e>>=1;return e+t}(a);do{if((r=Ks(e,n,i,t))s&&(l=s),Ys(e,n,n+l,n+r,t),r=l}o.pushRun(n,r),o.mergeRuns(),a-=r,n+=r}while(0!==a);o.forceMergeRuns()}}}var el=!1;function tl(){el||(el=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function nl(e,t){return e.zlevel===t.zlevel?e.z===t.z?e.z2-t.z2:e.z-t.z:e.zlevel-t.zlevel}var il=function(){function e(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=nl}return e.prototype.traverse=function(e,t){for(var n=0;n0&&(p.__clipPaths=[]),isNaN(p.z)&&(tl(),p.z=0),isNaN(p.z2)&&(tl(),p.z2=0),isNaN(p.zlevel)&&(tl(),p.zlevel=0),this._displayList[this._displayListLen++]=p}var c=e.getDecalElement&&e.getDecalElement();c&&this._updateAndAddDisplayable(c,t,n);var d=e.getTextGuideLine();d&&this._updateAndAddDisplayable(d,t,n);var u=e.getTextContent();u&&this._updateAndAddDisplayable(u,t,n)}},e.prototype.addRoot=function(e){e.__zr&&e.__zr.storage===this||this._roots.push(e)},e.prototype.delRoot=function(e){if(e instanceof Array)for(var t=0,n=e.length;t=0&&this._roots.splice(i,1)}},e.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},e.prototype.getRoots=function(){return this._roots},e.prototype.dispose=function(){this._displayList=null,this._roots=null},e}(),al=ur.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(e){return setTimeout(e,16)},rl={linear:function(e){return e},quadraticIn:function(e){return e*e},quadraticOut:function(e){return e*(2-e)},quadraticInOut:function(e){return(e*=2)<1?.5*e*e:-.5*(--e*(e-2)-1)},cubicIn:function(e){return e*e*e},cubicOut:function(e){return--e*e*e+1},cubicInOut:function(e){return(e*=2)<1?.5*e*e*e:.5*((e-=2)*e*e+2)},quarticIn:function(e){return e*e*e*e},quarticOut:function(e){return 1- --e*e*e*e},quarticInOut:function(e){return(e*=2)<1?.5*e*e*e*e:-.5*((e-=2)*e*e*e-2)},quinticIn:function(e){return e*e*e*e*e},quinticOut:function(e){return--e*e*e*e*e+1},quinticInOut:function(e){return(e*=2)<1?.5*e*e*e*e*e:.5*((e-=2)*e*e*e*e+2)},sinusoidalIn:function(e){return 1-Math.cos(e*Math.PI/2)},sinusoidalOut:function(e){return Math.sin(e*Math.PI/2)},sinusoidalInOut:function(e){return.5*(1-Math.cos(Math.PI*e))},exponentialIn:function(e){return 0===e?0:Math.pow(1024,e-1)},exponentialOut:function(e){return 1===e?1:1-Math.pow(2,-10*e)},exponentialInOut:function(e){return 0===e?0:1===e?1:(e*=2)<1?.5*Math.pow(1024,e-1):.5*(2-Math.pow(2,-10*(e-1)))},circularIn:function(e){return 1-Math.sqrt(1-e*e)},circularOut:function(e){return Math.sqrt(1- --e*e)},circularInOut:function(e){return(e*=2)<1?-.5*(Math.sqrt(1-e*e)-1):.5*(Math.sqrt(1-(e-=2)*e)+1)},elasticIn:function(e){var t,n=.1;return 0===e?0:1===e?1:(!n||n<1?(n=1,t=.1):t=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/.4))},elasticOut:function(e){var t,n=.1;return 0===e?0:1===e?1:(!n||n<1?(n=1,t=.1):t=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*e)*Math.sin((e-t)*(2*Math.PI)/.4)+1)},elasticInOut:function(e){var t,n=.1,i=.4;return 0===e?0:1===e?1:(!n||n<1?(n=1,t=.1):t=i*Math.asin(1/n)/(2*Math.PI),(e*=2)<1?n*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/i)*.5+1)},backIn:function(e){var t=1.70158;return e*e*((t+1)*e-t)},backOut:function(e){var t=1.70158;return--e*e*((t+1)*e+t)+1},backInOut:function(e){var t=2.5949095;return(e*=2)<1?e*e*((t+1)*e-t)*.5:.5*((e-=2)*e*((t+1)*e+t)+2)},bounceIn:function(e){return 1-rl.bounceOut(1-e)},bounceOut:function(e){return e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375},bounceInOut:function(e){return e<.5?.5*rl.bounceIn(2*e):.5*rl.bounceOut(2*e-1)+.5}},ol=Math.pow,sl=Math.sqrt,ll=1e-8,pl=1e-4,cl=sl(3),dl=1/3,ul=Po(),ml=Po(),hl=Po();function gl(e){return e>-1e-8&&ell||e<-1e-8}function yl(e,t,n,i,a){var r=1-a;return r*r*(r*e+3*a*t)+a*a*(a*i+3*r*n)}function vl(e,t,n,i,a){var r=1-a;return 3*(((t-e)*r+2*(n-t)*a)*r+(i-n)*a*a)}function xl(e,t,n,i,a,r){var o=i+3*(t-n)-e,s=3*(n-2*t+e),l=3*(t-e),p=e-a,c=s*s-3*o*l,d=s*l-9*o*p,u=l*l-3*s*p,m=0;if(gl(c)&&gl(d)){if(gl(s))r[0]=0;else(_=-l/s)>=0&&_<=1&&(r[m++]=_)}else{var h=d*d-4*c*u;if(gl(h)){var g=d/c,f=-g/2;(_=-s/o+g)>=0&&_<=1&&(r[m++]=_),f>=0&&f<=1&&(r[m++]=f)}else if(h>0){var y=sl(h),v=c*s+1.5*o*(-d+y),x=c*s+1.5*o*(-d-y);(_=(-s-((v=v<0?-ol(-v,dl):ol(v,dl))+(x=x<0?-ol(-x,dl):ol(x,dl))))/(3*o))>=0&&_<=1&&(r[m++]=_)}else{var b=(2*c*s-3*o*d)/(2*sl(c*c*c)),w=Math.acos(b)/3,S=sl(c),C=Math.cos(w),_=(-s-2*S*C)/(3*o),T=(f=(-s+S*(C+cl*Math.sin(w)))/(3*o),(-s+S*(C-cl*Math.sin(w)))/(3*o));_>=0&&_<=1&&(r[m++]=_),f>=0&&f<=1&&(r[m++]=f),T>=0&&T<=1&&(r[m++]=T)}}return m}function bl(e,t,n,i,a){var r=6*n-12*t+6*e,o=9*t+3*i-3*e-9*n,s=3*t-3*e,l=0;if(gl(o)){if(fl(r))(c=-s/r)>=0&&c<=1&&(a[l++]=c)}else{var p=r*r-4*o*s;if(gl(p))a[0]=-r/(2*o);else if(p>0){var c,d=sl(p),u=(-r-d)/(2*o);(c=(-r+d)/(2*o))>=0&&c<=1&&(a[l++]=c),u>=0&&u<=1&&(a[l++]=u)}}return l}function wl(e,t,n,i,a,r){var o=(t-e)*a+e,s=(n-t)*a+t,l=(i-n)*a+n,p=(s-o)*a+o,c=(l-s)*a+s,d=(c-p)*a+p;r[0]=e,r[1]=o,r[2]=p,r[3]=d,r[4]=d,r[5]=c,r[6]=l,r[7]=i}function Sl(e,t,n,i,a,r,o,s,l,p,c){var d,u,m,h,g,f=.005,y=1/0;ul[0]=l,ul[1]=p;for(var v=0;v<1;v+=.05)ml[0]=yl(e,n,a,o,v),ml[1]=yl(t,i,r,s,v),(h=Uo(ul,ml))=0&&h=0&&f=1?1:xl(0,i,r,1,e,s)&&yl(0,a,o,1,s[0])}}}var Ol=function(){function e(e){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=e.life||1e3,this._delay=e.delay||0,this.loop=e.loop||!1,this.onframe=e.onframe||Mo,this.ondestroy=e.ondestroy||Mo,this.onrestart=e.onrestart||Mo,e.easing&&this.setEasing(e.easing)}return e.prototype.step=function(e,t){if(this._inited||(this._startTime=e+this._delay,this._inited=!0),!this._paused){var n=this._life,i=e-this._startTime-this._pausedTime,a=i/n;a<0&&(a=0),a=Math.min(a,1);var r=this.easingFunc,o=r?r(a):a;if(this.onframe(o),1===a){if(!this.loop)return!0;var s=i%n;this._startTime=e-s,this._pausedTime=0,this.onrestart()}return!1}this._pausedTime+=t},e.prototype.pause=function(){this._paused=!0},e.prototype.resume=function(){this._paused=!1},e.prototype.setEasing=function(e){this.easing=e,this.easingFunc=Zr(e)?e:rl[e]||Dl(e)},e}(),Al=function(e){this.value=e},Fl=function(){function e(){this._len=0}return e.prototype.insert=function(e){var t=new Al(e);return this.insertEntry(t),t},e.prototype.insertEntry=function(e){this.head?(this.tail.next=e,e.prev=this.tail,e.next=null,this.tail=e):this.head=this.tail=e,this._len++},e.prototype.remove=function(e){var t=e.prev,n=e.next;t?t.next=n:this.head=n,n?n.prev=t:this.tail=t,e.next=e.prev=null,this._len--},e.prototype.len=function(){return this._len},e.prototype.clear=function(){this.head=this.tail=null,this._len=0},e}(),Rl=function(){function e(e){this._list=new Fl,this._maxSize=10,this._map={},this._maxSize=e}return e.prototype.put=function(e,t){var n=this._list,i=this._map,a=null;if(null==i[e]){var r=n.len(),o=this._lastRemovedEntry;if(r>=this._maxSize&&r>0){var s=n.head;n.remove(s),delete i[s.key],a=s.value,this._lastRemovedEntry=s}o?o.value=t:o=new Al(t),o.key=e,n.insertEntry(o),i[e]=o}return a},e.prototype.get=function(e){var t=this._map[e],n=this._list;if(null!=t)return t!==n.tail&&(n.remove(t),n.insertEntry(t)),t.value},e.prototype.clear=function(){this._list.clear(),this._map={}},e.prototype.len=function(){return this._list.len()},e}(),Bl={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function Nl(e){return(e=Math.round(e))<0?0:e>255?255:e}function Ll(e){return e<0?0:e>1?1:e}function Vl(e){var t=e;return t.length&&"%"===t.charAt(t.length-1)?Nl(parseFloat(t)/100*255):Nl(parseInt(t,10))}function ql(e){var t=e;return t.length&&"%"===t.charAt(t.length-1)?Ll(parseFloat(t)/100):Ll(parseFloat(t))}function Gl(e,t,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?e+(t-e)*n*6:2*n<1?t:3*n<2?e+(t-e)*(2/3-n)*6:e}function zl(e,t,n){return e+(t-e)*n}function Ul(e,t,n,i,a){return e[0]=t,e[1]=n,e[2]=i,e[3]=a,e}function jl(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e}var Hl=new Rl(20),Wl=null;function $l(e,t){Wl&&jl(Wl,t),Wl=Hl.put(e,Wl||t.slice())}function Kl(e,t){if(e){t=t||[];var n=Hl.get(e);if(n)return jl(t,n);var i=(e+="").replace(/ /g,"").toLowerCase();if(i in Bl)return jl(t,Bl[i]),$l(e,t),t;var a,r=i.length;if("#"===i.charAt(0))return 4===r||5===r?(a=parseInt(i.slice(1,4),16))>=0&&a<=4095?(Ul(t,(3840&a)>>4|(3840&a)>>8,240&a|(240&a)>>4,15&a|(15&a)<<4,5===r?parseInt(i.slice(4),16)/15:1),$l(e,t),t):void Ul(t,0,0,0,1):7===r||9===r?(a=parseInt(i.slice(1,7),16))>=0&&a<=16777215?(Ul(t,(16711680&a)>>16,(65280&a)>>8,255&a,9===r?parseInt(i.slice(7),16)/255:1),$l(e,t),t):void Ul(t,0,0,0,1):void 0;var o=i.indexOf("("),s=i.indexOf(")");if(-1!==o&&s+1===r){var l=i.substr(0,o),p=i.substr(o+1,s-(o+1)).split(","),c=1;switch(l){case"rgba":if(4!==p.length)return 3===p.length?Ul(t,+p[0],+p[1],+p[2],1):Ul(t,0,0,0,1);c=ql(p.pop());case"rgb":return p.length>=3?(Ul(t,Vl(p[0]),Vl(p[1]),Vl(p[2]),3===p.length?c:ql(p[3])),$l(e,t),t):void Ul(t,0,0,0,1);case"hsla":return 4!==p.length?void Ul(t,0,0,0,1):(p[3]=ql(p[3]),Yl(p,t),$l(e,t),t);case"hsl":return 3!==p.length?void Ul(t,0,0,0,1):(Yl(p,t),$l(e,t),t);default:return}}Ul(t,0,0,0,1)}}function Yl(e,t){var n=(parseFloat(e[0])%360+360)%360/360,i=ql(e[1]),a=ql(e[2]),r=a<=.5?a*(i+1):a+i-a*i,o=2*a-r;return Ul(t=t||[],Nl(255*Gl(o,r,n+1/3)),Nl(255*Gl(o,r,n)),Nl(255*Gl(o,r,n-1/3)),1),4===e.length&&(t[3]=e[3]),t}function Xl(e,t){var n=Kl(e);if(n){for(var i=0;i<3;i++)n[i]=t<0?n[i]*(1-t)|0:(255-n[i])*t+n[i]|0,n[i]>255?n[i]=255:n[i]<0&&(n[i]=0);return tp(n,4===n.length?"rgba":"rgb")}}function Zl(e,t,n){if(t&&t.length&&e>=0&&e<=1){n=n||[];var i=e*(t.length-1),a=Math.floor(i),r=Math.ceil(i),o=t[a],s=t[r],l=i-a;return n[0]=Nl(zl(o[0],s[0],l)),n[1]=Nl(zl(o[1],s[1],l)),n[2]=Nl(zl(o[2],s[2],l)),n[3]=Ll(zl(o[3],s[3],l)),n}}function Ql(e,t,n){if(t&&t.length&&e>=0&&e<=1){var i=e*(t.length-1),a=Math.floor(i),r=Math.ceil(i),o=Kl(t[a]),s=Kl(t[r]),l=i-a,p=tp([Nl(zl(o[0],s[0],l)),Nl(zl(o[1],s[1],l)),Nl(zl(o[2],s[2],l)),Ll(zl(o[3],s[3],l))],"rgba");return n?{color:p,leftIndex:a,rightIndex:r,value:i}:p}}function Jl(e,t,n,i){var a=Kl(e);if(e)return a=function(e){if(e){var t,n,i=e[0]/255,a=e[1]/255,r=e[2]/255,o=Math.min(i,a,r),s=Math.max(i,a,r),l=s-o,p=(s+o)/2;if(0===l)t=0,n=0;else{n=p<.5?l/(s+o):l/(2-s-o);var c=((s-i)/6+l/2)/l,d=((s-a)/6+l/2)/l,u=((s-r)/6+l/2)/l;i===s?t=u-d:a===s?t=1/3+c-u:r===s&&(t=2/3+d-c),t<0&&(t+=1),t>1&&(t-=1)}var m=[360*t,n,p];return null!=e[3]&&m.push(e[3]),m}}(a),null!=t&&(a[0]=function(e){return(e=Math.round(e))<0?0:e>360?360:e}(t)),null!=n&&(a[1]=ql(n)),null!=i&&(a[2]=ql(i)),tp(Yl(a),"rgba")}function ep(e,t){var n=Kl(e);if(n&&null!=t)return n[3]=Ll(t),tp(n,"rgba")}function tp(e,t){if(e&&e.length){var n=e[0]+","+e[1]+","+e[2];return"rgba"!==t&&"hsva"!==t&&"hsla"!==t||(n+=","+e[3]),t+"("+n+")"}}function np(e,t){var n=Kl(e);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*t:0}var ip=new Rl(100);function ap(e){if(Qr(e)){var t=ip.get(e);return t||(t=Xl(e,-.1),ip.put(e,t)),t}if(ro(e)){var n=Br({},e);return n.colorStops=Ur(e.colorStops,(function(e){return{offset:e.offset,color:Xl(e.color,-.1)}})),n}return e}var rp=Math.round;function op(e){var t;if(e&&"transparent"!==e){if("string"==typeof e&&e.indexOf("rgba")>-1){var n=Kl(e);n&&(e="rgb("+n[0]+","+n[1]+","+n[2]+")",t=n[3])}}else e="none";return{color:e,opacity:null==t?1:t}}var sp=1e-4;function lp(e){return e-1e-4}function pp(e){return rp(1e3*e)/1e3}function cp(e){return rp(1e4*e)/1e4}var dp={left:"start",right:"end",center:"middle",middle:"middle"};function up(e){return e&&!!e.image}function mp(e){return up(e)||function(e){return e&&!!e.svgElement}(e)}function hp(e){return"linear"===e.type}function gp(e){return"radial"===e.type}function fp(e){return e&&("linear"===e.type||"radial"===e.type)}function yp(e){return"url(#"+e+")"}function vp(e){var t=e.getGlobalScale(),n=Math.max(t[0],t[1]);return Math.max(Math.ceil(Math.log(n)/Math.log(10)),1)}function xp(e){var t=e.x||0,n=e.y||0,i=(e.rotation||0)*ko,a=co(e.scaleX,1),r=co(e.scaleY,1),o=e.skewX||0,s=e.skewY||0,l=[];return(t||n)&&l.push("translate("+t+"px,"+n+"px)"),i&&l.push("rotate("+i+")"),1===a&&1===r||l.push("scale("+a+","+r+")"),(o||s)&&l.push("skew("+rp(o*ko)+"deg, "+rp(s*ko)+"deg)"),l.join(" ")}var bp=ur.hasGlobalWindow&&Zr(window.btoa)?function(e){return window.btoa(unescape(encodeURIComponent(e)))}:"undefined"!=typeof Buffer?function(e){return Buffer.from(e).toString("base64")}:function(e){return null},wp=Array.prototype.slice;function Sp(e,t,n){return(t-e)*n+e}function Cp(e,t,n,i){for(var a=t.length,r=0;ri?t:e,r=Math.min(n,i),o=a[r-1]||{color:[0,0,0,0],offset:0},s=r;so)i.length=o;else for(var s=r;s=1},e.prototype.getAdditiveTrack=function(){return this._additiveTrack},e.prototype.addKeyframe=function(e,t,n){this._needsSort=!0;var i=this.keyframes,a=i.length,r=!1,o=6,s=t;if(Gr(t)){var l=function(e){return Gr(e&&e[0])?2:1}(t);o=l,(1===l&&!eo(t[0])||2===l&&!eo(t[0][0]))&&(r=!0)}else if(eo(t)&&!lo(t))o=0;else if(Qr(t))if(isNaN(+t)){var p=Kl(t);p&&(s=p,o=3)}else o=0;else if(ro(t)){var c=Br({},s);c.colorStops=Ur(t.colorStops,(function(e){return{offset:e.offset,color:Kl(e.color)}})),hp(t)?o=4:gp(t)&&(o=5),s=c}0===a?this.valType=o:o===this.valType&&6!==o||(r=!0),this.discrete=this.discrete||r;var d={time:e,value:s,rawValue:t,percent:0};return n&&(d.easing=n,d.easingFunc=Zr(n)?n:rl[n]||Dl(n)),i.push(d),d},e.prototype.prepare=function(e,t){var n=this.keyframes;this._needsSort&&n.sort((function(e,t){return e.time-t.time}));for(var i=this.valType,a=n.length,r=n[a-1],o=this.discrete,s=Dp(i),l=Pp(i),p=0;p=0&&!(l[n].percent<=t);n--);n=m(n,p-2)}else{for(n=u;nt);n++);n=m(n-1,p-2)}a=l[n+1],i=l[n]}if(i&&a){this._lastFr=n,this._lastFrP=t;var h=a.percent-i.percent,g=0===h?1:m((t-i.percent)/h,1);a.easingFunc&&(g=a.easingFunc(g));var f=r?this._additiveValue:d?Op:e[c];if(!Dp(s)&&!d||f||(f=this._additiveValue=[]),this.discrete)e[c]=g<1?i.rawValue:a.rawValue;else if(Dp(s))1===s?Cp(f,i[o],a[o],g):function(e,t,n,i){for(var a=t.length,r=a&&t[0].length,o=0;o0&&s.addKeyframe(0,Mp(l),i),this._trackKeys.push(o)}s.addKeyframe(e,Mp(t[o]),i)}return this._maxTime=Math.max(this._maxTime,e),this},e.prototype.pause=function(){this._clip.pause(),this._paused=!0},e.prototype.resume=function(){this._clip.resume(),this._paused=!1},e.prototype.isPaused=function(){return!!this._paused},e.prototype.duration=function(e){return this._maxTime=e,this._force=!0,this},e.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var e=this._doneCbs;if(e)for(var t=e.length,n=0;n0)){this._started=1;for(var t=this,n=[],i=this._maxTime||0,a=0;a1){var o=r.pop();a.addKeyframe(o.time,e[i]),a.prepare(this._maxTime,a.getAdditiveTrack())}}}},e}();function Rp(){return(new Date).getTime()}var Bp,Np,Lp=function(e){function t(t){var n=e.call(this)||this;return n._running=!1,n._time=0,n._pausedTime=0,n._pauseStart=0,n._paused=!1,t=t||{},n.stage=t.stage||{},n}return We(t,e),t.prototype.addClip=function(e){e.animation&&this.removeClip(e),this._head?(this._tail.next=e,e.prev=this._tail,e.next=null,this._tail=e):this._head=this._tail=e,e.animation=this},t.prototype.addAnimator=function(e){e.animation=this;var t=e.getClip();t&&this.addClip(t)},t.prototype.removeClip=function(e){if(e.animation){var t=e.prev,n=e.next;t?t.next=n:this._head=n,n?n.prev=t:this._tail=t,e.next=e.prev=e.animation=null}},t.prototype.removeAnimator=function(e){var t=e.getClip();t&&this.removeClip(t),e.animation=null},t.prototype.update=function(e){for(var t=Rp()-this._pausedTime,n=t-this._time,i=this._head;i;){var a=i.next;i.step(t,n)?(i.ondestroy(),this.removeClip(i),i=a):i=a}this._time=t,e||(this.trigger("frame",n),this.stage.update&&this.stage.update())},t.prototype._startLoop=function(){var e=this;this._running=!0,al((function t(){e._running&&(al(t),!e._paused&&e.update())}))},t.prototype.start=function(){this._running||(this._time=Rp(),this._pausedTime=0,this._startLoop())},t.prototype.stop=function(){this._running=!1},t.prototype.pause=function(){this._paused||(this._pauseStart=Rp(),this._paused=!0)},t.prototype.resume=function(){this._paused&&(this._pausedTime+=Rp()-this._pauseStart,this._paused=!1)},t.prototype.clear=function(){for(var e=this._head;e;){var t=e.next;e.prev=e.next=e.animation=null,e=t}this._head=this._tail=null},t.prototype.isFinished=function(){return null==this._head},t.prototype.animate=function(e,t){t=t||{},this.start();var n=new Fp(e,t.loop);return this.addAnimator(n),n},t}(Xo),Vp=ur.domSupported,qp=(Np={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},{mouse:Bp=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],touch:["touchstart","touchend","touchmove"],pointer:Ur(Bp,(function(e){var t=e.replace("mouse","pointer");return Np.hasOwnProperty(t)?t:e}))}),Gp=["mousemove","mouseup"],zp=["pointermove","pointerup"],Up=!1;function jp(e){var t=e.pointerType;return"pen"===t||"touch"===t}function Hp(e){e&&(e.zrByTouch=!0)}function Wp(e,t){for(var n=t,i=!1;n&&9!==n.nodeType&&!(i=n.domBelongToZr||n!==t&&n===e.painterRoot);)n=n.parentNode;return i}var $p=function(e,t){this.stopPropagation=Mo,this.stopImmediatePropagation=Mo,this.preventDefault=Mo,this.type=t.type,this.target=this.currentTarget=e.dom,this.pointerType=t.pointerType,this.clientX=t.clientX,this.clientY=t.clientY},Kp={mousedown:function(e){e=ms(this.dom,e),this.__mayPointerCapture=[e.zrX,e.zrY],this.trigger("mousedown",e)},mousemove:function(e){e=ms(this.dom,e);var t=this.__mayPointerCapture;!t||e.zrX===t[0]&&e.zrY===t[1]||this.__togglePointerCapture(!0),this.trigger("mousemove",e)},mouseup:function(e){e=ms(this.dom,e),this.__togglePointerCapture(!1),this.trigger("mouseup",e)},mouseout:function(e){Wp(this,(e=ms(this.dom,e)).toElement||e.relatedTarget)||(this.__pointerCapturing&&(e.zrEventControl="no_globalout"),this.trigger("mouseout",e))},wheel:function(e){Up=!0,e=ms(this.dom,e),this.trigger("mousewheel",e)},mousewheel:function(e){Up||(e=ms(this.dom,e),this.trigger("mousewheel",e))},touchstart:function(e){Hp(e=ms(this.dom,e)),this.__lastTouchMoment=new Date,this.handler.processGesture(e,"start"),Kp.mousemove.call(this,e),Kp.mousedown.call(this,e)},touchmove:function(e){Hp(e=ms(this.dom,e)),this.handler.processGesture(e,"change"),Kp.mousemove.call(this,e)},touchend:function(e){Hp(e=ms(this.dom,e)),this.handler.processGesture(e,"end"),Kp.mouseup.call(this,e),+new Date-+this.__lastTouchMoment<300&&Kp.click.call(this,e)},pointerdown:function(e){Kp.mousedown.call(this,e)},pointermove:function(e){jp(e)||Kp.mousemove.call(this,e)},pointerup:function(e){Kp.mouseup.call(this,e)},pointerout:function(e){jp(e)||Kp.mouseout.call(this,e)}};zr(["click","dblclick","contextmenu"],(function(e){Kp[e]=function(t){t=ms(this.dom,t),this.trigger(e,t)}}));var Yp={pointermove:function(e){jp(e)||Yp.mousemove.call(this,e)},pointerup:function(e){Yp.mouseup.call(this,e)},mousemove:function(e){this.trigger("mousemove",e)},mouseup:function(e){var t=this.__pointerCapturing;this.__togglePointerCapture(!1),this.trigger("mouseup",e),t&&(e.zrEventControl="only_globalout",this.trigger("mouseout",e))}};function Xp(e,t){var n=t.domHandlers;ur.pointerEventsSupported?zr(qp.pointer,(function(i){Qp(t,i,(function(t){n[i].call(e,t)}))})):(ur.touchEventsSupported&&zr(qp.touch,(function(i){Qp(t,i,(function(a){n[i].call(e,a),function(e){e.touching=!0,null!=e.touchTimer&&(clearTimeout(e.touchTimer),e.touchTimer=null),e.touchTimer=setTimeout((function(){e.touching=!1,e.touchTimer=null}),700)}(t)}))})),zr(qp.mouse,(function(i){Qp(t,i,(function(a){a=us(a),t.touching||n[i].call(e,a)}))})))}function Zp(e,t){function n(n){Qp(t,n,(function(i){i=us(i),Wp(e,i.target)||(i=function(e,t){return ms(e.dom,new $p(e,t),!0)}(e,i),t.domHandlers[n].call(e,i))}),{capture:!0})}ur.pointerEventsSupported?zr(zp,n):ur.touchEventsSupported||zr(Gp,n)}function Qp(e,t,n,i){e.mounted[t]=n,e.listenerOpts[t]=i,hs(e.domTarget,t,n,i)}function Jp(e){var t,n,i,a,r=e.mounted;for(var o in r)r.hasOwnProperty(o)&&(t=e.domTarget,n=o,i=r[o],a=e.listenerOpts[o],t.removeEventListener(n,i,a));e.mounted={}}var ec=function(e,t){this.mounted={},this.listenerOpts={},this.touching=!1,this.domTarget=e,this.domHandlers=t},tc=function(e){function t(t,n){var i=e.call(this)||this;return i.__pointerCapturing=!1,i.dom=t,i.painterRoot=n,i._localHandlerScope=new ec(t,Kp),Vp&&(i._globalHandlerScope=new ec(document,Yp)),Xp(i,i._localHandlerScope),i}return We(t,e),t.prototype.dispose=function(){Jp(this._localHandlerScope),Vp&&Jp(this._globalHandlerScope)},t.prototype.setCursor=function(e){this.dom.style&&(this.dom.style.cursor=e||"default")},t.prototype.__togglePointerCapture=function(e){if(this.__mayPointerCapture=null,Vp&&+this.__pointerCapturing^+e){this.__pointerCapturing=e;var t=this._globalHandlerScope;e?Zp(this,t):Jp(t)}},t}(Xo),nc=1;ur.hasGlobalWindow&&(nc=Math.max(window.devicePixelRatio||window.screen&&window.screen.deviceXDPI/window.screen.logicalXDPI||1,1));var ic=nc,ac="#333",rc="#ccc",oc=bs,sc=5e-5;function lc(e){return e>sc||e<-5e-5}var pc=[],cc=[],dc=[1,0,0,1,0,0],uc=Math.abs,mc=function(){function e(){}return e.prototype.getLocalTransform=function(t){return e.getLocalTransform(this,t)},e.prototype.setPosition=function(e){this.x=e[0],this.y=e[1]},e.prototype.setScale=function(e){this.scaleX=e[0],this.scaleY=e[1]},e.prototype.setSkew=function(e){this.skewX=e[0],this.skewY=e[1]},e.prototype.setOrigin=function(e){this.originX=e[0],this.originY=e[1]},e.prototype.needLocalTransform=function(){return lc(this.rotation)||lc(this.x)||lc(this.y)||lc(this.scaleX-1)||lc(this.scaleY-1)||lc(this.skewX)||lc(this.skewY)},e.prototype.updateTransform=function(){var e=this.parent&&this.parent.transform,t=this.needLocalTransform(),n=this.transform;t||e?(n=n||[1,0,0,1,0,0],t?this.getLocalTransform(n):oc(n),e&&(t?Ss(n,e,n):ws(n,e)),this.transform=n,this._resolveGlobalScaleRatio(n)):n&&(oc(n),this.invTransform=null)},e.prototype._resolveGlobalScaleRatio=function(e){var t=this.globalScaleRatio;if(null!=t&&1!==t){this.getGlobalScale(pc);var n=pc[0]<0?-1:1,i=pc[1]<0?-1:1,a=((pc[0]-n)*t+n)/pc[0]||0,r=((pc[1]-i)*t+i)/pc[1]||0;e[0]*=a,e[1]*=a,e[2]*=r,e[3]*=r}this.invTransform=this.invTransform||[1,0,0,1,0,0],Is(this.invTransform,e)},e.prototype.getComputedTransform=function(){for(var e=this,t=[];e;)t.push(e),e=e.parent;for(;e=t.pop();)e.updateTransform();return this.transform},e.prototype.setLocalTransform=function(e){if(e){var t=e[0]*e[0]+e[1]*e[1],n=e[2]*e[2]+e[3]*e[3],i=Math.atan2(e[1],e[0]),a=Math.PI/2+i-Math.atan2(e[3],e[2]);n=Math.sqrt(n)*Math.cos(a),t=Math.sqrt(t),this.skewX=a,this.skewY=0,this.rotation=-i,this.x=+e[4],this.y=+e[5],this.scaleX=t,this.scaleY=n,this.originX=0,this.originY=0}},e.prototype.decomposeTransform=function(){if(this.transform){var e=this.parent,t=this.transform;e&&e.transform&&(e.invTransform=e.invTransform||[1,0,0,1,0,0],Ss(cc,e.invTransform,t),t=cc);var n=this.originX,i=this.originY;(n||i)&&(dc[4]=n,dc[5]=i,Ss(cc,t,dc),cc[4]-=n,cc[5]-=i,t=cc),this.setLocalTransform(t)}},e.prototype.getGlobalScale=function(e){var t=this.transform;return e=e||[],t?(e[0]=Math.sqrt(t[0]*t[0]+t[1]*t[1]),e[1]=Math.sqrt(t[2]*t[2]+t[3]*t[3]),t[0]<0&&(e[0]=-e[0]),t[3]<0&&(e[1]=-e[1]),e):(e[0]=1,e[1]=1,e)},e.prototype.transformCoordToLocal=function(e,t){var n=[e,t],i=this.invTransform;return i&&Ho(n,n,i),n},e.prototype.transformCoordToGlobal=function(e,t){var n=[e,t],i=this.transform;return i&&Ho(n,n,i),n},e.prototype.getLineScale=function(){var e=this.transform;return e&&uc(e[0]-1)>1e-10&&uc(e[3]-1)>1e-10?Math.sqrt(uc(e[0]*e[3]-e[2]*e[1])):1},e.prototype.copyTransform=function(e){gc(this,e)},e.getLocalTransform=function(e,t){t=t||[];var n=e.originX||0,i=e.originY||0,a=e.scaleX,r=e.scaleY,o=e.anchorX,s=e.anchorY,l=e.rotation||0,p=e.x,c=e.y,d=e.skewX?Math.tan(e.skewX):0,u=e.skewY?Math.tan(-e.skewY):0;if(n||i||o||s){var m=n+o,h=i+s;t[4]=-m*a-d*h*r,t[5]=-h*r-u*m*a}else t[4]=t[5]=0;return t[0]=a,t[3]=r,t[1]=u*a,t[2]=d*r,l&&_s(t,t,l),t[4]+=n+p,t[5]+=i+c,t},e.initDefaultProps=function(){var t=e.prototype;t.scaleX=t.scaleY=t.globalScaleRatio=1,t.x=t.y=t.originX=t.originY=t.skewX=t.skewY=t.rotation=t.anchorX=t.anchorY=0}(),e}(),hc=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function gc(e,t){for(var n=0;n=0?parseFloat(e)/100*t:parseFloat(e):e}function _c(e,t,n){var i=t.position||"inside",a=null!=t.distance?t.distance:5,r=n.height,o=n.width,s=r/2,l=n.x,p=n.y,c="left",d="top";if(i instanceof Array)l+=Cc(i[0],n.width),p+=Cc(i[1],n.height),c=null,d=null;else switch(i){case"left":l-=a,p+=s,c="right",d="middle";break;case"right":l+=a+o,p+=s,d="middle";break;case"top":l+=o/2,p-=a,c="center",d="bottom";break;case"bottom":l+=o/2,p+=r+a,c="center";break;case"inside":l+=o/2,p+=s,c="center",d="middle";break;case"insideLeft":l+=a,p+=s,d="middle";break;case"insideRight":l+=o-a,p+=s,c="right",d="middle";break;case"insideTop":l+=o/2,p+=a,c="center";break;case"insideBottom":l+=o/2,p+=r-a,c="center",d="bottom";break;case"insideTopLeft":l+=a,p+=a;break;case"insideTopRight":l+=o-a,p+=a,c="right";break;case"insideBottomLeft":l+=a,p+=r-a,d="bottom";break;case"insideBottomRight":l+=o-a,p+=r-a,c="right",d="bottom"}return(e=e||{}).x=l,e.y=p,e.align=c,e.verticalAlign=d,e}var Tc="__zr_normal__",Ic=hc.concat(["ignore"]),Ec=jr(hc,(function(e,t){return e[t]=!0,e}),{ignore:!1}),Mc={},kc=new Ns(0,0,0,0),Pc=function(){function e(e){this.id=Dr(),this.animators=[],this.currentStates=[],this.states={},this._init(e)}return e.prototype._init=function(e){this.attr(e)},e.prototype.drift=function(e,t,n){switch(this.draggable){case"horizontal":t=0;break;case"vertical":e=0}var i=this.transform;i||(i=this.transform=[1,0,0,1,0,0]),i[4]+=e,i[5]+=t,this.decomposeTransform(),this.markRedraw()},e.prototype.beforeUpdate=function(){},e.prototype.afterUpdate=function(){},e.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},e.prototype.updateInnerText=function(e){var t=this._textContent;if(t&&(!t.ignore||e)){this.textConfig||(this.textConfig={});var n=this.textConfig,i=n.local,a=t.innerTransformable,r=void 0,o=void 0,s=!1;a.parent=i?this:null;var l=!1;if(a.copyTransform(t),null!=n.position){var p=kc;n.layoutRect?p.copy(n.layoutRect):p.copy(this.getBoundingRect()),i||p.applyTransform(this.transform),this.calculateTextPosition?this.calculateTextPosition(Mc,n,p):_c(Mc,n,p),a.x=Mc.x,a.y=Mc.y,r=Mc.align,o=Mc.verticalAlign;var c=n.origin;if(c&&null!=n.rotation){var d=void 0,u=void 0;"center"===c?(d=.5*p.width,u=.5*p.height):(d=Cc(c[0],p.width),u=Cc(c[1],p.height)),l=!0,a.originX=-a.x+d+(i?0:p.x),a.originY=-a.y+u+(i?0:p.y)}}null!=n.rotation&&(a.rotation=n.rotation);var m=n.offset;m&&(a.x+=m[0],a.y+=m[1],l||(a.originX=-m[0],a.originY=-m[1]));var h=null==n.inside?"string"==typeof n.position&&n.position.indexOf("inside")>=0:n.inside,g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={}),f=void 0,y=void 0,v=void 0;h&&this.canBeInsideText()?(f=n.insideFill,y=n.insideStroke,null!=f&&"auto"!==f||(f=this.getInsideTextFill()),null!=y&&"auto"!==y||(y=this.getInsideTextStroke(f),v=!0)):(f=n.outsideFill,y=n.outsideStroke,null!=f&&"auto"!==f||(f=this.getOutsideFill()),null!=y&&"auto"!==y||(y=this.getOutsideStroke(f),v=!0)),(f=f||"#000")===g.fill&&y===g.stroke&&v===g.autoStroke&&r===g.align&&o===g.verticalAlign||(s=!0,g.fill=f,g.stroke=y,g.autoStroke=v,g.align=r,g.verticalAlign=o,t.setDefaultTextStyle(g)),t.__dirty|=1,s&&t.dirtyStyle(!0)}},e.prototype.canBeInsideText=function(){return!0},e.prototype.getInsideTextFill=function(){return"#fff"},e.prototype.getInsideTextStroke=function(e){return"#000"},e.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?rc:ac},e.prototype.getOutsideStroke=function(e){var t=this.__zr&&this.__zr.getBackgroundColor(),n="string"==typeof t&&Kl(t);n||(n=[255,255,255,1]);for(var i=n[3],a=this.__zr.isDarkMode(),r=0;r<3;r++)n[r]=n[r]*i+(a?0:255)*(1-i);return n[3]=1,tp(n,"rgba")},e.prototype.traverse=function(e,t){},e.prototype.attrKV=function(e,t){"textConfig"===e?this.setTextConfig(t):"textContent"===e?this.setTextContent(t):"clipPath"===e?this.setClipPath(t):"extra"===e?(this.extra=this.extra||{},Br(this.extra,t)):this[e]=t},e.prototype.hide=function(){this.ignore=!0,this.markRedraw()},e.prototype.show=function(){this.ignore=!1,this.markRedraw()},e.prototype.attr=function(e,t){if("string"==typeof e)this.attrKV(e,t);else if(to(e))for(var n=$r(e),i=0;i0},e.prototype.getState=function(e){return this.states[e]},e.prototype.ensureState=function(e){var t=this.states;return t[e]||(t[e]={}),t[e]},e.prototype.clearStates=function(e){this.useState(Tc,!1,e)},e.prototype.useState=function(e,t,n,i){var a=e===Tc;if(this.hasState()||!a){var r=this.currentStates,o=this.stateTransition;if(!(Lr(r,e)>=0)||!t&&1!==r.length){var s;if(this.stateProxy&&!a&&(s=this.stateProxy(e)),s||(s=this.states&&this.states[e]),s||a){a||this.saveCurrentToNormalState(s);var l=!!(s&&s.hoverLayer||i);l&&this._toggleHoverLayerFlag(!0),this._applyStateObj(e,s,this._normalState,t,!n&&!this.__inHover&&o&&o.duration>0,o);var p=this._textContent,c=this._textGuide;return p&&p.useState(e,t,n,l),c&&c.useState(e,t,n,l),a?(this.currentStates=[],this._normalState={}):t?this.currentStates.push(e):this.currentStates=[e],this._updateAnimationTargets(),this.markRedraw(),!l&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2),s}Or("State "+e+" not exists.")}}},e.prototype.useStates=function(e,t,n){if(e.length){var i=[],a=this.currentStates,r=e.length,o=r===a.length;if(o)for(var s=0;s0,m);var h=this._textContent,g=this._textGuide;h&&h.useStates(e,t,d),g&&g.useStates(e,t,d),this._updateAnimationTargets(),this.currentStates=e.slice(),this.markRedraw(),!d&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2)}else this.clearStates()},e.prototype.isSilent=function(){for(var e=this.silent,t=this.parent;!e&&t;){if(t.silent){e=!0;break}t=t.parent}return e},e.prototype._updateAnimationTargets=function(){for(var e=0;e=0){var n=this.currentStates.slice();n.splice(t,1),this.useStates(n)}},e.prototype.replaceState=function(e,t,n){var i=this.currentStates.slice(),a=Lr(i,e),r=Lr(i,t)>=0;a>=0?r?i.splice(a,1):i[a]=t:n&&!r&&i.push(t),this.useStates(i)},e.prototype.toggleState=function(e,t){t?this.useState(e,!0):this.removeState(e)},e.prototype._mergeStates=function(e){for(var t,n={},i=0;i=0&&t.splice(n,1)})),this.animators.push(e),n&&n.animation.addAnimator(e),n&&n.wakeUp()},e.prototype.updateDuringAnimation=function(e){this.markRedraw()},e.prototype.stopAnimation=function(e,t){for(var n=this.animators,i=n.length,a=[],r=0;r0&&n.during&&r[0].during((function(e,t){n.during(t)}));for(var u=0;u0||a.force&&!o.length){var S,C=void 0,_=void 0,T=void 0;if(s){_={},u&&(C={});for(b=0;b=0&&(n.splice(i,0,e),this._doAdd(e))}return this},t.prototype.replace=function(e,t){var n=Lr(this._children,e);return n>=0&&this.replaceAt(t,n),this},t.prototype.replaceAt=function(e,t){var n=this._children,i=n[t];if(e&&e!==this&&e.parent!==this&&e!==i){n[t]=e,i.parent=null;var a=this.__zr;a&&i.removeSelfFromZr(a),this._doAdd(e)}return this},t.prototype._doAdd=function(e){e.parent&&e.parent.remove(e),e.parent=this;var t=this.__zr;t&&t!==e.__zr&&e.addSelfToZr(t),t&&t.refresh()},t.prototype.remove=function(e){var t=this.__zr,n=this._children,i=Lr(n,e);return i<0||(n.splice(i,1),e.parent=null,t&&e.removeSelfFromZr(t),t&&t.refresh()),this},t.prototype.removeAll=function(){for(var e=this._children,t=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},e.prototype.setSleepAfterStill=function(e){this._sleepAfterStill=e},e.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},e.prototype.refreshHover=function(){this._needsRefreshHover=!0},e.prototype.refreshHoverImmediately=function(){this._disposed||(this._needsRefreshHover=!1,this.painter.refreshHover&&"canvas"===this.painter.getType()&&this.painter.refreshHover())},e.prototype.resize=function(e){this._disposed||(e=e||{},this.painter.resize(e.width,e.height),this.handler.resize())},e.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},e.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},e.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},e.prototype.setCursorStyle=function(e){this._disposed||this.handler.setCursorStyle(e)},e.prototype.findHover=function(e,t){if(!this._disposed)return this.handler.findHover(e,t)},e.prototype.on=function(e,t,n){return this._disposed||this.handler.on(e,t,n),this},e.prototype.off=function(e,t){this._disposed||this.handler.off(e,t)},e.prototype.trigger=function(e,t){this._disposed||this.handler.trigger(e,t)},e.prototype.clear=function(){if(!this._disposed){for(var e=this.storage.getRoots(),t=0;t0){if(e<=a)return o;if(e>=r)return s}else{if(e>=a)return o;if(e<=r)return s}else{if(e===a)return o;if(e===r)return s}return(e-a)/l*p+o}function Wc(e,t){switch(e){case"center":case"middle":e="50%";break;case"left":case"top":e="0%";break;case"right":case"bottom":e="100%"}return Qr(e)?(n=e,n.replace(/^\s+|\s+$/g,"")).match(/%$/)?parseFloat(e)/100*t:parseFloat(e):null==e?NaN:+e;var n}function $c(e,t,n){return null==t&&(t=10),t=Math.min(Math.max(0,t),20),e=(+e).toFixed(t),n?e:+e}function Kc(e){return e.sort((function(e,t){return e-t})),e}function Yc(e){if(e=+e,isNaN(e))return 0;if(e>1e-14)for(var t=1,n=0;n<15;n++,t*=10)if(Math.round(e*t)/t===e)return n;return Xc(e)}function Xc(e){var t=e.toString().toLowerCase(),n=t.indexOf("e"),i=n>0?+t.slice(n+1):0,a=n>0?n:t.length,r=t.indexOf("."),o=r<0?0:a-1-r;return Math.max(0,o-i)}function Zc(e,t){var n=Math.log,i=Math.LN10,a=Math.floor(n(e[1]-e[0])/i),r=Math.round(n(Math.abs(t[1]-t[0]))/i),o=Math.min(Math.max(-a+r,0),20);return isFinite(o)?o:20}function Qc(e,t){var n=jr(e,(function(e,t){return e+(isNaN(t)?0:t)}),0);if(0===n)return[];for(var i=Math.pow(10,t),a=Ur(e,(function(e){return(isNaN(e)?0:e)/n*i*100})),r=100*i,o=Ur(a,(function(e){return Math.floor(e)})),s=jr(o,(function(e,t){return e+t}),0),l=Ur(a,(function(e,t){return e-o[t]}));sp&&(p=l[d],c=d);++o[c],l[c]=0,++s}return Ur(o,(function(e){return e/i}))}function Jc(e,t){var n=Math.max(Yc(e),Yc(t)),i=e+t;return n>20?i:$c(i,n)}function ed(e){var t=2*Math.PI;return(e%t+t)%t}function td(e){return e>-1e-4&&e=10&&t++,t}function od(e,t){var n=rd(e),i=Math.pow(10,n),a=e/i;return e=(t?a<1.5?1:a<2.5?2:a<4?3:a<7?5:10:a<1?1:a<2?2:a<3?3:a<5?5:10)*i,n>=-20?+e.toFixed(n<0?-n:0):e}function sd(e){e.sort((function(e,t){return s(e,t,0)?-1:1}));for(var t=-1/0,n=1,i=0;i=0||a&&Lr(a,s)<0)){var l=n.getShallow(s,t);null!=l&&(r[e[o][0]]=l)}}return r}}var Xd=Yd([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),Zd=function(){function e(){}return e.prototype.getAreaStyle=function(e,t){return Xd(this,e,t)},e}(),Qd=new Rl(50);function Jd(e){if("string"==typeof e){var t=Qd.get(e);return t&&t.image}return e}function eu(e,t,n,i,a){if(e){if("string"==typeof e){if(t&&t.__zrImageSrc===e||!n)return t;var r=Qd.get(e),o={hostEl:n,cb:i,cbPayload:a};return r?!nu(t=r.image)&&r.pending.push(o):((t=vr.loadImage(e,tu,tu)).__zrImageSrc=e,Qd.put(e,t.__cachedImgObj={image:t,pending:[o]})),t}return e}return t}function tu(){var e=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var t=0;t=o;l++)s-=o;var p=yc(n,t);return p>s&&(n="",p=0),s=e-p,a.ellipsis=n,a.ellipsisWidth=p,a.contentWidth=s,a.containerWidth=e,a}function ou(e,t){var n=t.containerWidth,i=t.font,a=t.contentWidth;if(!n)return"";var r=yc(e,i);if(r<=n)return e;for(var o=0;;o++){if(r<=a||o>=t.maxIterations){e+=t.ellipsis;break}var s=0===o?su(e,a,t.ascCharWidth,t.cnCharWidth):r>0?Math.floor(e.length*a/r):0;r=yc(e=e.substr(0,s),i)}return""===e&&(e=t.placeholder),e}function su(e,t,n,i){for(var a=0,r=0,o=e.length;r0&&h+i.accumWidth>i.width&&(r=t.split("\n"),d=!0),i.accumWidth=h}else{var g=hu(t,c,i.width,i.breakAll,i.accumWidth);i.accumWidth=g.accumWidth+m,o=g.linesWidths,r=g.lines}}else r=t.split("\n");for(var f=0;f=32&&t<=591||t>=880&&t<=4351||t>=4608&&t<=5119||t>=7680&&t<=8303}(e)||!!uu[e]}function hu(e,t,n,i,a){for(var r=[],o=[],s="",l="",p=0,c=0,d=0;dn:a+c+m>n)?c?(s||l)&&(h?(s||(s=l,l="",c=p=0),r.push(s),o.push(c-p),l+=u,s="",c=p+=m):(l&&(s+=l,l="",p=0),r.push(s),o.push(c),s=u,c=m)):h?(r.push(l),o.push(p),l=u,p=m):(r.push(u),o.push(m)):(c+=m,h?(l+=u,p+=m):(l&&(s+=l,l="",p=0),s+=u))}else l&&(s+=l,c+=p),r.push(s),o.push(c),s="",l="",p=0,c=0}return r.length||s||(s=e,l="",p=0),l&&(s+=l),s&&(r.push(s),o.push(c)),1===r.length&&(c+=a),{accumWidth:c,lines:r,linesWidths:o}}var gu="__zr_style_"+Math.round(10*Math.random()),fu={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},yu={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};fu[gu]=!0;var vu=["z","z2","invisible"],xu=["invisible"],bu=function(e){function t(t){return e.call(this,t)||this}var n;return We(t,e),t.prototype._init=function(t){for(var n=$r(t),i=0;i1e-4)return s[0]=e-n,s[1]=t-i,l[0]=e+n,void(l[1]=t+i);if(Mu[0]=Iu(a)*n+e,Mu[1]=Tu(a)*i+t,ku[0]=Iu(r)*n+e,ku[1]=Tu(r)*i+t,p(s,Mu,ku),c(l,Mu,ku),(a%=Eu)<0&&(a+=Eu),(r%=Eu)<0&&(r+=Eu),a>r&&!o?r+=Eu:aa&&(Pu[0]=Iu(m)*n+e,Pu[1]=Tu(m)*i+t,p(s,Pu,s),c(l,Pu,l))}var Lu={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},Vu=[],qu=[],Gu=[],zu=[],Uu=[],ju=[],Hu=Math.min,Wu=Math.max,$u=Math.cos,Ku=Math.sin,Yu=Math.abs,Xu=Math.PI,Zu=2*Xu,Qu="undefined"!=typeof Float32Array,Ju=[];function em(e){return Math.round(e/Xu*1e8)/1e8%2*Xu}function tm(e,t){var n=em(e[0]);n<0&&(n+=Zu);var i=n-e[0],a=e[1];a+=i,!t&&a-n>=Zu?a=n+Zu:t&&n-a>=Zu?a=n-Zu:!t&&n>a?a=n+(Zu-em(n-a)):t&&n0&&(this._ux=Yu(n/ic/e)||0,this._uy=Yu(n/ic/t)||0)},e.prototype.setDPR=function(e){this.dpr=e},e.prototype.setContext=function(e){this._ctx=e},e.prototype.getContext=function(){return this._ctx},e.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},e.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},e.prototype.moveTo=function(e,t){return this._drawPendingPt(),this.addData(Lu.M,e,t),this._ctx&&this._ctx.moveTo(e,t),this._x0=e,this._y0=t,this._xi=e,this._yi=t,this},e.prototype.lineTo=function(e,t){var n=Yu(e-this._xi),i=Yu(t-this._yi),a=n>this._ux||i>this._uy;if(this.addData(Lu.L,e,t),this._ctx&&a&&this._ctx.lineTo(e,t),a)this._xi=e,this._yi=t,this._pendingPtDist=0;else{var r=n*n+i*i;r>this._pendingPtDist&&(this._pendingPtX=e,this._pendingPtY=t,this._pendingPtDist=r)}return this},e.prototype.bezierCurveTo=function(e,t,n,i,a,r){return this._drawPendingPt(),this.addData(Lu.C,e,t,n,i,a,r),this._ctx&&this._ctx.bezierCurveTo(e,t,n,i,a,r),this._xi=a,this._yi=r,this},e.prototype.quadraticCurveTo=function(e,t,n,i){return this._drawPendingPt(),this.addData(Lu.Q,e,t,n,i),this._ctx&&this._ctx.quadraticCurveTo(e,t,n,i),this._xi=n,this._yi=i,this},e.prototype.arc=function(e,t,n,i,a,r){this._drawPendingPt(),Ju[0]=i,Ju[1]=a,tm(Ju,r),i=Ju[0];var o=(a=Ju[1])-i;return this.addData(Lu.A,e,t,n,n,i,o,0,r?0:1),this._ctx&&this._ctx.arc(e,t,n,i,a,r),this._xi=$u(a)*n+e,this._yi=Ku(a)*n+t,this},e.prototype.arcTo=function(e,t,n,i,a){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(e,t,n,i,a),this},e.prototype.rect=function(e,t,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.rect(e,t,n,i),this.addData(Lu.R,e,t,n,i),this},e.prototype.closePath=function(){this._drawPendingPt(),this.addData(Lu.Z);var e=this._ctx,t=this._x0,n=this._y0;return e&&e.closePath(),this._xi=t,this._yi=n,this},e.prototype.fill=function(e){e&&e.fill(),this.toStatic()},e.prototype.stroke=function(e){e&&e.stroke(),this.toStatic()},e.prototype.len=function(){return this._len},e.prototype.setData=function(e){var t=e.length;this.data&&this.data.length===t||!Qu||(this.data=new Float32Array(t));for(var n=0;np.length&&(this._expandData(),p=this.data);for(var c=0;c0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},e.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var e=[],t=0;t11&&(this.data=new Float32Array(e)))}},e.prototype.getBoundingRect=function(){Gu[0]=Gu[1]=Uu[0]=Uu[1]=Number.MAX_VALUE,zu[0]=zu[1]=ju[0]=ju[1]=-Number.MAX_VALUE;var e,t=this.data,n=0,i=0,a=0,r=0;for(e=0;en||Yu(f)>i||d===t-1)&&(h=Math.sqrt(k*k+f*f),a=g,r=x);break;case Lu.C:var y=e[d++],v=e[d++],x=(g=e[d++],e[d++]),b=e[d++],w=e[d++];h=Cl(a,r,y,v,g,x,b,w,10),a=b,r=w;break;case Lu.Q:h=kl(a,r,y=e[d++],v=e[d++],g=e[d++],x=e[d++],10),a=g,r=x;break;case Lu.A:var S=e[d++],C=e[d++],_=e[d++],T=e[d++],I=e[d++],E=e[d++],M=E+I;d+=1,m&&(o=$u(I)*_+S,s=Ku(I)*T+C),h=Wu(_,T)*Hu(Zu,Math.abs(E)),a=$u(M)*_+S,r=Ku(M)*T+C;break;case Lu.R:o=a=e[d++],s=r=e[d++],h=2*e[d++]+2*e[d++];break;case Lu.Z:var k=o-a;f=s-r;h=Math.sqrt(k*k+f*f),a=o,r=s}h>=0&&(l[c++]=h,p+=h)}return this._pathLen=p,p},e.prototype.rebuildPath=function(e,t){var n,i,a,r,o,s,l,p,c,d,u=this.data,m=this._ux,h=this._uy,g=this._len,f=t<1,y=0,v=0,x=0;if(!f||(this._pathSegLen||this._calculateLength(),l=this._pathSegLen,p=t*this._pathLen))e:for(var b=0;b0&&(e.lineTo(c,d),x=0),w){case Lu.M:n=a=u[b++],i=r=u[b++],e.moveTo(a,r);break;case Lu.L:o=u[b++],s=u[b++];var C=Yu(o-a),_=Yu(s-r);if(C>m||_>h){if(f){if(y+(K=l[v++])>p){var T=(p-y)/K;e.lineTo(a*(1-T)+o*T,r*(1-T)+s*T);break e}y+=K}e.lineTo(o,s),a=o,r=s,x=0}else{var I=C*C+_*_;I>x&&(c=o,d=s,x=I)}break;case Lu.C:var E=u[b++],M=u[b++],k=u[b++],P=u[b++],D=u[b++],O=u[b++];if(f){if(y+(K=l[v++])>p){wl(a,E,k,D,T=(p-y)/K,Vu),wl(r,M,P,O,T,qu),e.bezierCurveTo(Vu[1],qu[1],Vu[2],qu[2],Vu[3],qu[3]);break e}y+=K}e.bezierCurveTo(E,M,k,P,D,O),a=D,r=O;break;case Lu.Q:E=u[b++],M=u[b++],k=u[b++],P=u[b++];if(f){if(y+(K=l[v++])>p){El(a,E,k,T=(p-y)/K,Vu),El(r,M,P,T,qu),e.quadraticCurveTo(Vu[1],qu[1],Vu[2],qu[2]);break e}y+=K}e.quadraticCurveTo(E,M,k,P),a=k,r=P;break;case Lu.A:var A=u[b++],F=u[b++],R=u[b++],B=u[b++],N=u[b++],L=u[b++],V=u[b++],q=!u[b++],G=R>B?R:B,z=Yu(R-B)>.001,U=N+L,j=!1;if(f)y+(K=l[v++])>p&&(U=N+L*(p-y)/K,j=!0),y+=K;if(z&&e.ellipse?e.ellipse(A,F,R,B,V,N,U,q):e.arc(A,F,G,N,U,q),j)break e;S&&(n=$u(N)*R+A,i=Ku(N)*B+F),a=$u(U)*R+A,r=Ku(U)*B+F;break;case Lu.R:n=a=u[b],i=r=u[b+1],o=u[b++],s=u[b++];var H=u[b++],W=u[b++];if(f){if(y+(K=l[v++])>p){var $=p-y;e.moveTo(o,s),e.lineTo(o+Hu($,H),s),($-=H)>0&&e.lineTo(o+H,s+Hu($,W)),($-=W)>0&&e.lineTo(o+Wu(H-$,0),s+W),($-=H)>0&&e.lineTo(o,s+Wu(W-$,0));break e}y+=K}e.rect(o,s,H,W);break;case Lu.Z:if(f){var K;if(y+(K=l[v++])>p){T=(p-y)/K;e.lineTo(a*(1-T)+n*T,r*(1-T)+i*T);break e}y+=K}e.closePath(),a=n,r=i}}},e.prototype.clone=function(){var t=new e,n=this.data;return t.data=n.slice?n.slice():Array.prototype.slice.call(n),t._len=this._len,t},e.CMD=Lu,e.initDefaultProps=function(){var t=e.prototype;t._saveData=!0,t._ux=0,t._uy=0,t._pendingPtDist=0,t._version=0}(),e}();function im(e,t,n,i,a,r,o){if(0===a)return!1;var s=a,l=0;if(o>t+s&&o>i+s||oe+s&&r>n+s||rt+d&&c>i+d&&c>r+d&&c>s+d||ce+d&&p>n+d&&p>a+d&&p>o+d||pt+p&&l>i+p&&l>r+p||le+p&&s>n+p&&s>a+p||sn||c+pa&&(a+=lm);var u=Math.atan2(l,s);return u<0&&(u+=lm),u>=i&&u<=a||u+lm>=i&&u+lm<=a}function cm(e,t,n,i,a,r){if(r>t&&r>i||ra?s:0}var dm=nm.CMD,um=2*Math.PI;var mm=[-1,-1,-1],hm=[-1,-1];function gm(e,t,n,i,a,r,o,s,l,p){if(p>t&&p>i&&p>r&&p>s||p1&&(c=void 0,c=hm[0],hm[0]=hm[1],hm[1]=c),h=yl(t,i,r,s,hm[0]),m>1&&(g=yl(t,i,r,s,hm[1]))),2===m?yt&&s>i&&s>r||s=0&&c<=1&&(a[l++]=c);else{var p=o*o-4*r*s;if(gl(p))(c=-o/(2*r))>=0&&c<=1&&(a[l++]=c);else if(p>0){var c,d=sl(p),u=(-o-d)/(2*r);(c=(-o+d)/(2*r))>=0&&c<=1&&(a[l++]=c),u>=0&&u<=1&&(a[l++]=u)}}return l}(t,i,r,s,mm);if(0===l)return 0;var p=Il(t,i,r);if(p>=0&&p<=1){for(var c=0,d=_l(t,i,r,p),u=0;un||s<-n)return 0;var l=Math.sqrt(n*n-s*s);mm[0]=-l,mm[1]=l;var p=Math.abs(i-a);if(p<1e-4)return 0;if(p>=um-1e-4){i=0,a=um;var c=r?1:-1;return o>=mm[0]+e&&o<=mm[1]+e?c:0}if(i>a){var d=i;i=a,a=d}i<0&&(i+=um,a+=um);for(var u=0,m=0;m<2;m++){var h=mm[m];if(h+e>o){var g=Math.atan2(s,h);c=r?1:-1;g<0&&(g=um+g),(g>=i&&g<=a||g+um>=i&&g+um<=a)&&(g>Math.PI/2&&g<1.5*Math.PI&&(c=-c),u+=c)}}return u}function vm(e,t,n,i,a){for(var r,o,s,l,p=e.data,c=e.len(),d=0,u=0,m=0,h=0,g=0,f=0;f1&&(n||(d+=cm(u,m,h,g,i,a))),v&&(h=u=p[f],g=m=p[f+1]),y){case dm.M:u=h=p[f++],m=g=p[f++];break;case dm.L:if(n){if(im(u,m,p[f],p[f+1],t,i,a))return!0}else d+=cm(u,m,p[f],p[f+1],i,a)||0;u=p[f++],m=p[f++];break;case dm.C:if(n){if(am(u,m,p[f++],p[f++],p[f++],p[f++],p[f],p[f+1],t,i,a))return!0}else d+=gm(u,m,p[f++],p[f++],p[f++],p[f++],p[f],p[f+1],i,a)||0;u=p[f++],m=p[f++];break;case dm.Q:if(n){if(rm(u,m,p[f++],p[f++],p[f],p[f+1],t,i,a))return!0}else d+=fm(u,m,p[f++],p[f++],p[f],p[f+1],i,a)||0;u=p[f++],m=p[f++];break;case dm.A:var x=p[f++],b=p[f++],w=p[f++],S=p[f++],C=p[f++],_=p[f++];f+=1;var T=!!(1-p[f++]);r=Math.cos(C)*w+x,o=Math.sin(C)*S+b,v?(h=r,g=o):d+=cm(u,m,r,o,i,a);var I=(i-x)*S/w+x;if(n){if(pm(x,b,S,C,C+_,T,t,I,a))return!0}else d+=ym(x,b,S,C,C+_,T,I,a);u=Math.cos(C+_)*w+x,m=Math.sin(C+_)*S+b;break;case dm.R:if(h=u=p[f++],g=m=p[f++],r=h+p[f++],o=g+p[f++],n){if(im(h,g,r,g,t,i,a)||im(r,g,r,o,t,i,a)||im(r,o,h,o,t,i,a)||im(h,o,h,g,t,i,a))return!0}else d+=cm(r,g,r,o,i,a),d+=cm(h,o,h,g,i,a);break;case dm.Z:if(n){if(im(u,m,h,g,t,i,a))return!0}else d+=cm(u,m,h,g,i,a);u=h,m=g}}return n||(s=m,l=g,Math.abs(s-l)<1e-4)||(d+=cm(u,m,h,g,i,a)||0),0!==d}var xm=Nr({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},fu),bm={style:Nr({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},yu.style)},wm=hc.concat(["invisible","culling","z","z2","zlevel","parent"]),Sm=function(e){function t(t){return e.call(this,t)||this}var n;return We(t,e),t.prototype.update=function(){var n=this;e.prototype.update.call(this);var i=this.style;if(i.decal){var a=this._decalEl=this._decalEl||new t;a.buildPath===t.prototype.buildPath&&(a.buildPath=function(e){n.buildPath(e,n.shape)}),a.silent=!0;var r=a.style;for(var o in i)r[o]!==i[o]&&(r[o]=i[o]);r.fill=i.fill?i.decal:null,r.decal=null,r.shadowColor=null,i.strokeFirst&&(r.stroke=null);for(var s=0;s.5?ac:t>.2?"#eee":rc}if(e)return rc}return ac},t.prototype.getInsideTextStroke=function(e){var t=this.style.fill;if(Qr(t)){var n=this.__zr;if(!(!n||!n.isDarkMode())===np(e,0)<.4)return t}},t.prototype.buildPath=function(e,t,n){},t.prototype.pathUpdated=function(){this.__dirty&=-5},t.prototype.getUpdatedPathProxy=function(e){return!this.path&&this.createPathProxy(),this.path.beginPath(),this.buildPath(this.path,this.shape,e),this.path},t.prototype.createPathProxy=function(){this.path=new nm(!1)},t.prototype.hasStroke=function(){var e=this.style,t=e.stroke;return!(null==t||"none"===t||!(e.lineWidth>0))},t.prototype.hasFill=function(){var e=this.style.fill;return null!=e&&"none"!==e},t.prototype.getBoundingRect=function(){var e=this._rect,t=this.style,n=!e;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var a=this.path;(i||4&this.__dirty)&&(a.beginPath(),this.buildPath(a,this.shape,!1),this.pathUpdated()),e=a.getBoundingRect()}if(this._rect=e,this.hasStroke()&&this.path&&this.path.len()>0){var r=this._rectStroke||(this._rectStroke=e.clone());if(this.__dirty||n){r.copy(e);var o=t.strokeNoScale?this.getLineScale():1,s=t.lineWidth;if(!this.hasFill()){var l=this.strokeContainThreshold;s=Math.max(s,null==l?4:l)}o>1e-10&&(r.width+=s/o,r.height+=s/o,r.x-=s/o/2,r.y-=s/o/2)}return r}return e},t.prototype.contain=function(e,t){var n=this.transformCoordToLocal(e,t),i=this.getBoundingRect(),a=this.style;if(e=n[0],t=n[1],i.contain(e,t)){var r=this.path;if(this.hasStroke()){var o=a.lineWidth,s=a.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(this.hasFill()||(o=Math.max(o,this.strokeContainThreshold)),function(e,t,n,i){return vm(e,t,!0,n,i)}(r,o/s,e,t)))return!0}if(this.hasFill())return function(e,t,n){return vm(e,0,!1,t,n)}(r,e,t)}return!1},t.prototype.dirtyShape=function(){this.__dirty|=4,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},t.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},t.prototype.animateShape=function(e){return this.animate("shape",e)},t.prototype.updateDuringAnimation=function(e){"style"===e?this.dirtyStyle():"shape"===e?this.dirtyShape():this.markRedraw()},t.prototype.attrKV=function(t,n){"shape"===t?this.setShape(n):e.prototype.attrKV.call(this,t,n)},t.prototype.setShape=function(e,t){var n=this.shape;return n||(n=this.shape={}),"string"==typeof e?n[e]=t:Br(n,e),this.dirtyShape(),this},t.prototype.shapeChanged=function(){return!!(4&this.__dirty)},t.prototype.createStyle=function(e){return To(xm,e)},t.prototype._innerSaveToNormal=function(t){e.prototype._innerSaveToNormal.call(this,t);var n=this._normalState;t.shape&&!n.shape&&(n.shape=Br({},this.shape))},t.prototype._applyStateObj=function(t,n,i,a,r,o){e.prototype._applyStateObj.call(this,t,n,i,a,r,o);var s,l=!(n&&a);if(n&&n.shape?r?a?s=n.shape:(s=Br({},i.shape),Br(s,n.shape)):(s=Br({},a?this.shape:i.shape),Br(s,n.shape)):l&&(s=i.shape),s)if(r){this.shape=Br({},this.shape);for(var p={},c=$r(s),d=0;d0},t.prototype.hasFill=function(){var e=this.style.fill;return null!=e&&"none"!==e},t.prototype.createStyle=function(e){return To(Cm,e)},t.prototype.setBoundingRect=function(e){this._rect=e},t.prototype.getBoundingRect=function(){var e=this.style;if(!this._rect){var t=e.text;null!=t?t+="":t="";var n=xc(t,e.font,e.textAlign,e.textBaseline);if(n.x+=e.x||0,n.y+=e.y||0,this.hasStroke()){var i=e.lineWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect},t.initDefaultProps=void(t.prototype.dirtyRectTolerance=10),t}(bu);_m.prototype.type="tspan";var Tm=Nr({x:0,y:0},fu),Im={style:Nr({x:!0,y:!0,width:!0,height:!0,sx:!0,sy:!0,sWidth:!0,sHeight:!0},yu.style)};var Em=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.createStyle=function(e){return To(Tm,e)},t.prototype._getSize=function(e){var t=this.style,n=t[e];if(null!=n)return n;var i,a=(i=t.image)&&"string"!=typeof i&&i.width&&i.height?t.image:this.__image;if(!a)return 0;var r="width"===e?"height":"width",o=t[r];return null==o?a[e]:a[e]/a[r]*o},t.prototype.getWidth=function(){return this._getSize("width")},t.prototype.getHeight=function(){return this._getSize("height")},t.prototype.getAnimationStyleProps=function(){return Im},t.prototype.getBoundingRect=function(){var e=this.style;return this._rect||(this._rect=new Ns(e.x||0,e.y||0,this.getWidth(),this.getHeight())),this._rect},t}(bu);Em.prototype.type="image";var Mm=Math.round;function km(e,t,n){if(t){var i=t.x1,a=t.x2,r=t.y1,o=t.y2;e.x1=i,e.x2=a,e.y1=r,e.y2=o;var s=n&&n.lineWidth;return s?(Mm(2*i)===Mm(2*a)&&(e.x1=e.x2=Dm(i,s,!0)),Mm(2*r)===Mm(2*o)&&(e.y1=e.y2=Dm(r,s,!0)),e):e}}function Pm(e,t,n){if(t){var i=t.x,a=t.y,r=t.width,o=t.height;e.x=i,e.y=a,e.width=r,e.height=o;var s=n&&n.lineWidth;return s?(e.x=Dm(i,s,!0),e.y=Dm(a,s,!0),e.width=Math.max(Dm(i+r,s,!1)-e.x,0===r?0:1),e.height=Math.max(Dm(a+o,s,!1)-e.y,0===o?0:1),e):e}}function Dm(e,t,n){if(!t)return e;var i=Mm(2*e);return(i+Mm(t))%2==0?i/2:(i+(n?1:-1))/2}var Om=function(){this.x=0,this.y=0,this.width=0,this.height=0},Am={},Fm=function(e){function t(t){return e.call(this,t)||this}return We(t,e),t.prototype.getDefaultShape=function(){return new Om},t.prototype.buildPath=function(e,t){var n,i,a,r;if(this.subPixelOptimize){var o=Pm(Am,t,this.style);n=o.x,i=o.y,a=o.width,r=o.height,o.r=t.r,t=o}else n=t.x,i=t.y,a=t.width,r=t.height;t.r?function(e,t){var n,i,a,r,o,s=t.x,l=t.y,p=t.width,c=t.height,d=t.r;p<0&&(s+=p,p=-p),c<0&&(l+=c,c=-c),"number"==typeof d?n=i=a=r=d:d instanceof Array?1===d.length?n=i=a=r=d[0]:2===d.length?(n=a=d[0],i=r=d[1]):3===d.length?(n=d[0],i=r=d[1],a=d[2]):(n=d[0],i=d[1],a=d[2],r=d[3]):n=i=a=r=0,n+i>p&&(n*=p/(o=n+i),i*=p/o),a+r>p&&(a*=p/(o=a+r),r*=p/o),i+a>c&&(i*=c/(o=i+a),a*=c/o),n+r>c&&(n*=c/(o=n+r),r*=c/o),e.moveTo(s+n,l),e.lineTo(s+p-i,l),0!==i&&e.arc(s+p-i,l+i,i,-Math.PI/2,0),e.lineTo(s+p,l+c-a),0!==a&&e.arc(s+p-a,l+c-a,a,0,Math.PI/2),e.lineTo(s+r,l+c),0!==r&&e.arc(s+r,l+c-r,r,Math.PI/2,Math.PI),e.lineTo(s,l+n),0!==n&&e.arc(s+n,l+n,n,Math.PI,1.5*Math.PI)}(e,t):e.rect(n,i,a,r)},t.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},t}(Sm);Fm.prototype.type="rect";var Rm={fill:"#000"},Bm={style:Nr({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},yu.style)},Nm=function(e){function t(t){var n=e.call(this)||this;return n.type="text",n._children=[],n._defaultStyle=Rm,n.attr(t),n}return We(t,e),t.prototype.childrenRef=function(){return this._children},t.prototype.update=function(){e.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var t=0;tm&&c){var h=Math.floor(m/l);n=n.slice(0,h)}if(e&&o&&null!=d)for(var g=ru(d,r,t.ellipsis,{minChar:t.truncateMinChar,placeholder:t.placeholder}),f=0;f0,T=null!=e.width&&("truncate"===e.overflow||"break"===e.overflow||"breakAll"===e.overflow),I=i.calculatedLineHeight,E=0;El&&du(n,e.substring(l,p),t,s),du(n,i[2],t,s,i[1]),l=iu.lastIndex}lr){w>0?(v.tokens=v.tokens.slice(0,w),f(v,b,x),n.lines=n.lines.slice(0,y+1)):n.lines=n.lines.slice(0,y);break e}var E=S.width,M=null==E||"auto"===E;if("string"==typeof E&&"%"===E.charAt(E.length-1))O.percentWidth=E,c.push(O),O.contentWidth=yc(O.text,T);else{if(M){var k=S.backgroundColor,P=k&&k.image;P&&nu(P=Jd(P))&&(O.width=Math.max(O.width,P.width*I/P.height))}var D=h&&null!=a?a-b:null;null!=D&&D=0&&"right"===(E=x[I]).align;)this._placeToken(E,e,w,h,T,"right",f),S-=E.width,T-=E.width,I--;for(_+=(n-(_-m)-(g-T)-S)/2;C<=I;)E=x[C],this._placeToken(E,e,w,h,_+E.width/2,"center",f),_+=E.width,C++;h+=w}},t.prototype._placeToken=function(e,t,n,i,a,r,o){var s=t.rich[e.styleName]||{};s.text=e.text;var l=e.verticalAlign,p=i+n/2;"top"===l?p=i+e.height/2:"bottom"===l&&(p=i+n-e.height/2),!e.isLineHolder&&Ym(s)&&this._renderBackground(s,t,"right"===r?a-e.width:"center"===r?a-e.width/2:a,p-e.height/2,e.width,e.height);var c=!!s.backgroundColor,d=e.textPadding;d&&(a=$m(a,r,d),p-=e.height/2-d[0]-e.innerHeight/2);var u=this._getOrCreateChild(_m),m=u.createStyle();u.useStyle(m);var h=this._defaultStyle,g=!1,f=0,y=Wm("fill"in s?s.fill:"fill"in t?t.fill:(g=!0,h.fill)),v=Hm("stroke"in s?s.stroke:"stroke"in t?t.stroke:c||o||h.autoStroke&&!g?null:(f=2,h.stroke)),x=s.textShadowBlur>0||t.textShadowBlur>0;m.text=e.text,m.x=a,m.y=p,x&&(m.shadowBlur=s.textShadowBlur||t.textShadowBlur||0,m.shadowColor=s.textShadowColor||t.textShadowColor||"transparent",m.shadowOffsetX=s.textShadowOffsetX||t.textShadowOffsetX||0,m.shadowOffsetY=s.textShadowOffsetY||t.textShadowOffsetY||0),m.textAlign=r,m.textBaseline="middle",m.font=e.font||hr,m.opacity=uo(s.opacity,t.opacity,1),zm(m,s),v&&(m.lineWidth=uo(s.lineWidth,t.lineWidth,f),m.lineDash=co(s.lineDash,t.lineDash),m.lineDashOffset=t.lineDashOffset||0,m.stroke=v),y&&(m.fill=y);var b=e.contentWidth,w=e.contentHeight;u.setBoundingRect(new Ns(bc(m.x,b,m.textAlign),wc(m.y,w,m.textBaseline),b,w))},t.prototype._renderBackground=function(e,t,n,i,a,r){var o,s,l,p=e.backgroundColor,c=e.borderWidth,d=e.borderColor,u=p&&p.image,m=p&&!u,h=e.borderRadius,g=this;if(m||e.lineHeight||c&&d){(o=this._getOrCreateChild(Fm)).useStyle(o.createStyle()),o.style.fill=null;var f=o.shape;f.x=n,f.y=i,f.width=a,f.height=r,f.r=h,o.dirtyShape()}if(m)(l=o.style).fill=p||null,l.fillOpacity=co(e.fillOpacity,1);else if(u){(s=this._getOrCreateChild(Em)).onload=function(){g.dirtyStyle()};var y=s.style;y.image=p.image,y.x=n,y.y=i,y.width=a,y.height=r}c&&d&&((l=o.style).lineWidth=c,l.stroke=d,l.strokeOpacity=co(e.strokeOpacity,1),l.lineDash=e.borderDash,l.lineDashOffset=e.borderDashOffset||0,o.strokeContainThreshold=0,o.hasFill()&&o.hasStroke()&&(l.strokeFirst=!0,l.lineWidth*=2));var v=(o||s).style;v.shadowBlur=e.shadowBlur||0,v.shadowColor=e.shadowColor||"transparent",v.shadowOffsetX=e.shadowOffsetX||0,v.shadowOffsetY=e.shadowOffsetY||0,v.opacity=uo(e.opacity,t.opacity,1)},t.makeFont=function(e){var t="";return Um(e)&&(t=[e.fontStyle,e.fontWeight,Gm(e.fontSize),e.fontFamily||"sans-serif"].join(" ")),t&&fo(t)||e.textFont||e.font},t}(bu),Lm={left:!0,right:1,center:1},Vm={top:1,bottom:1,middle:1},qm=["fontStyle","fontWeight","fontSize","fontFamily"];function Gm(e){return"string"!=typeof e||-1===e.indexOf("px")&&-1===e.indexOf("rem")&&-1===e.indexOf("em")?isNaN(+e)?"12px":e+"px":e}function zm(e,t){for(var n=0;n=0,r=!1;if(e instanceof Sm){var o=eh(e),s=a&&o.selectFill||o.normalFill,l=a&&o.selectStroke||o.normalStroke;if(ch(s)||ch(l)){var p=(i=i||{}).style||{};"inherit"===p.fill?(r=!0,i=Br({},i),(p=Br({},p)).fill=s):!ch(p.fill)&&ch(s)?(r=!0,i=Br({},i),(p=Br({},p)).fill=ap(s)):!ch(p.stroke)&&ch(l)&&(r||(i=Br({},i),p=Br({},p)),p.stroke=ap(l)),i.style=p}}if(i&&null==i.z2){r||(i=Br({},i));var c=e.z2EmphasisLift;i.z2=e.z2+(null!=c?c:ah)}return i}(this,0,t,n);if("blur"===e)return function(e,t,n){var i=Lr(e.currentStates,t)>=0,a=e.style.opacity,r=i?null:function(e,t,n,i){for(var a=e.style,r={},o=0;o0){var r={dataIndex:a,seriesIndex:e.seriesIndex};null!=i&&(r.dataType=i),t.push(r)}}))})),t}function Lh(e,t,n){jh(e,!0),xh(e,Sh),qh(e,t,n)}function Vh(e,t,n,i){i?function(e){jh(e,!1)}(e):Lh(e,t,n)}function qh(e,t,n){var i=Xm(e);null!=t?(i.focus=t,i.blurScope=n):i.focus&&(i.focus=null)}var Gh=["emphasis","blur","select"],zh={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function Uh(e,t,n,i){n=n||"itemStyle";for(var a=0;a1&&(o*=Jh(h),s*=Jh(h));var g=(a===r?-1:1)*Jh((o*o*(s*s)-o*o*(m*m)-s*s*(u*u))/(o*o*(m*m)+s*s*(u*u)))||0,f=g*o*m/s,y=g*-s*u/o,v=(e+n)/2+tg(d)*f-eg(d)*y,x=(t+i)/2+eg(d)*f+tg(d)*y,b=rg([1,0],[(u-f)/o,(m-y)/s]),w=[(u-f)/o,(m-y)/s],S=[(-1*u-f)/o,(-1*m-y)/s],C=rg(w,S);if(ag(w,S)<=-1&&(C=ng),ag(w,S)>=1&&(C=0),C<0){var _=Math.round(C/ng*1e6)/1e6;C=2*ng+_%2*ng}c.addData(p,v,x,o,s,b,C,d,r)}var sg=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,lg=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;var pg=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.applyTransform=function(e){},t}(Sm);function cg(e){return null!=e.setData}function dg(e,t){var n=function(e){var t=new nm;if(!e)return t;var n,i=0,a=0,r=i,o=a,s=nm.CMD,l=e.match(sg);if(!l)return t;for(var p=0;pP*P+D*D&&(_=I,T=E),{cx:_,cy:T,x0:-c,y0:-d,x1:_*(a/w-1),y1:T*(a/w-1)}}function kg(e,t){var n,i=Tg(t.r,0),a=Tg(t.r0||0,0),r=i>0;if(r||a>0){if(r||(i=a,a=0),a>i){var o=i;i=a,a=o}var s=t.startAngle,l=t.endAngle;if(!isNaN(s)&&!isNaN(l)){var p=t.cx,c=t.cy,d=!!t.clockwise,u=Cg(l-s),m=u>vg&&u%vg;if(m>Eg&&(u=m),i>Eg)if(u>vg-Eg)e.moveTo(p+i*bg(s),c+i*xg(s)),e.arc(p,c,i,s,l,!d),a>Eg&&(e.moveTo(p+a*bg(l),c+a*xg(l)),e.arc(p,c,a,l,s,d));else{var h=void 0,g=void 0,f=void 0,y=void 0,v=void 0,x=void 0,b=void 0,w=void 0,S=void 0,C=void 0,_=void 0,T=void 0,I=void 0,E=void 0,M=void 0,k=void 0,P=i*bg(s),D=i*xg(s),O=a*bg(l),A=a*xg(l),F=u>Eg;if(F){var R=t.cornerRadius;R&&(n=function(e){var t;if(Xr(e)){var n=e.length;if(!n)return e;t=1===n?[e[0],e[0],0,0]:2===n?[e[0],e[0],e[1],e[1]]:3===n?e.concat(e[2]):e}else t=[e,e,e,e];return t}(R),h=n[0],g=n[1],f=n[2],y=n[3]);var B=Cg(i-a)/2;if(v=Ig(B,f),x=Ig(B,y),b=Ig(B,h),w=Ig(B,g),_=S=Tg(v,x),T=C=Tg(b,w),(S>Eg||C>Eg)&&(I=i*bg(l),E=i*xg(l),M=a*bg(s),k=a*xg(s),uEg){var j=Ig(f,_),H=Ig(y,_),W=Mg(M,k,P,D,i,j,d),$=Mg(I,E,O,A,i,H,d);e.moveTo(p+W.cx+W.x0,c+W.cy+W.y0),_0&&e.arc(p+W.cx,c+W.cy,j,Sg(W.y0,W.x0),Sg(W.y1,W.x1),!d),e.arc(p,c,i,Sg(W.cy+W.y1,W.cx+W.x1),Sg($.cy+$.y1,$.cx+$.x1),!d),H>0&&e.arc(p+$.cx,c+$.cy,H,Sg($.y1,$.x1),Sg($.y0,$.x0),!d))}else e.moveTo(p+P,c+D),e.arc(p,c,i,s,l,!d);else e.moveTo(p+P,c+D);if(a>Eg&&F)if(T>Eg){j=Ig(h,T),W=Mg(O,A,I,E,a,-(H=Ig(g,T)),d),$=Mg(P,D,M,k,a,-j,d);e.lineTo(p+W.cx+W.x0,c+W.cy+W.y0),T0&&e.arc(p+W.cx,c+W.cy,H,Sg(W.y0,W.x0),Sg(W.y1,W.x1),!d),e.arc(p,c,a,Sg(W.cy+W.y1,W.cx+W.x1),Sg($.cy+$.y1,$.cx+$.x1),d),j>0&&e.arc(p+$.cx,c+$.cy,j,Sg($.y1,$.x1),Sg($.y0,$.x0),!d))}else e.lineTo(p+O,c+A),e.arc(p,c,a,l,s,d);else e.lineTo(p+O,c+A)}else e.moveTo(p,c);e.closePath()}}}var Pg=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0,this.cornerRadius=0},Dg=function(e){function t(t){return e.call(this,t)||this}return We(t,e),t.prototype.getDefaultShape=function(){return new Pg},t.prototype.buildPath=function(e,t){kg(e,t)},t.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},t}(Sm);Dg.prototype.type="sector";var Og=function(){this.cx=0,this.cy=0,this.r=0,this.r0=0},Ag=function(e){function t(t){return e.call(this,t)||this}return We(t,e),t.prototype.getDefaultShape=function(){return new Og},t.prototype.buildPath=function(e,t){var n=t.cx,i=t.cy,a=2*Math.PI;e.moveTo(n+t.r,i),e.arc(n,i,t.r,0,a,!1),e.moveTo(n+t.r0,i),e.arc(n,i,t.r0,0,a,!0)},t}(Sm);function Fg(e,t,n){var i=t.smooth,a=t.points;if(a&&a.length>=2){if(i){var r=function(e,t,n,i){var a,r,o,s,l=[],p=[],c=[],d=[];if(i){o=[1/0,1/0],s=[-1/0,-1/0];for(var u=0,m=e.length;uJg[1]){if(o=!1,a)return o;var p=Math.abs(Jg[0]-Qg[1]),c=Math.abs(Qg[0]-Jg[1]);Math.min(p,c)>i.len()&&(p0){var d={duration:c.duration,delay:c.delay||0,easing:c.easing,done:r,force:!!r||!!o,setToFinal:!p,scope:e,during:o};l?t.animateFrom(n,d):t.animateTo(n,d)}else t.stopAnimation(),!l&&t.attr(n),o&&o(1),r&&r()}function pf(e,t,n,i,a,r){lf("update",e,t,n,i,a,r)}function cf(e,t,n,i,a,r){lf("enter",e,t,n,i,a,r)}function df(e){if(!e.__zr)return!0;for(var t=0;tMath.abs(r[1])?r[0]>0?"right":"left":r[1]>0?"bottom":"top"}function Ff(e){return!e.isGroup}function Rf(e,t,n){if(e&&t){var i,a=(i={},e.traverse((function(e){Ff(e)&&e.anid&&(i[e.anid]=e)})),i);t.traverse((function(e){if(Ff(e)&&e.anid){var t=a[e.anid];if(t){var i=r(e);e.attr(r(t)),pf(e,i,n,Xm(e).dataIndex)}}}))}function r(e){var t={x:e.x,y:e.y,rotation:e.rotation};return function(e){return null!=e.shape}(e)&&(t.shape=Br({},e.shape)),t}}function Bf(e,t){return Ur(e,(function(e){var n=e[0];n=ff(n,t.x),n=yf(n,t.x+t.width);var i=e[1];return i=ff(i,t.y),[n,i=yf(i,t.y+t.height)]}))}function Nf(e,t){var n=ff(e.x,t.x),i=yf(e.x+e.width,t.x+t.width),a=ff(e.y,t.y),r=yf(e.y+e.height,t.y+t.height);if(i>=n&&r>=a)return{x:n,y:a,width:i-n,height:r-a}}function Lf(e,t,n){var i=Br({rectHover:!0},t),a=i.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},e)return 0===e.indexOf("image://")?(a.image=e.slice(8),Nr(a,n),new Em(i)):_f(e.replace("path://",""),i,n,"center")}function Vf(e,t,n,i,a){for(var r=0,o=a[a.length-1];r=-1e-6)return!1;var h=e-a,g=t-r,f=Gf(h,g,p,c)/m;if(f<0||f>1)return!1;var y=Gf(h,g,d,u)/m;return!(y<0||y>1)}function Gf(e,t,n,i){return e*i-n*t}function zf(e){var t=e.itemTooltipOption,n=e.componentModel,i=e.itemName,a=Qr(t)?{formatter:t}:t,r=n.mainType,o=n.componentIndex,s={componentType:r,name:i,$vars:["name"]};s[r+"Index"]=o;var l=e.formatterParamsExtra;l&&zr($r(l),(function(e){Eo(s,e)||(s[e]=l[e],s.$vars.push(e))}));var p=Xm(e.el);p.componentMainType=r,p.componentIndex=o,p.tooltipConfig={name:i,option:Nr({content:i,formatterParams:s},a)}}function Uf(e,t){var n;e.isGroup&&(n=t(e)),n||e.traverse(t)}function jf(e,t){if(e)if(Xr(e))for(var n=0;n-1?Cy:Ty;function ky(e,t){e=e.toUpperCase(),Ey[e]=new xy(t),Iy[e]=t}function Py(e){return Ey[e]}ky(_y,{time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst",custom:"Custom chart",chart:"Chart"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}}),ky(Cy,{time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图",custom:"自定义图表",chart:"图表"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}});var Dy=1e3,Oy=6e4,Ay=36e5,Fy=864e5,Ry=31536e6,By={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}",none:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},Ny="{yyyy}-{MM}-{dd}",Ly={year:"{yyyy}",month:"{yyyy}-{MM}",day:Ny,hour:Ny+" "+By.hour,minute:Ny+" "+By.minute,second:Ny+" "+By.second,millisecond:By.none},Vy=["year","month","day","hour","minute","second","millisecond"],qy=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function Gy(e,t){return"0000".substr(0,t-(e+="").length)+e}function zy(e){switch(e){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return e}}function Uy(e){return e===zy(e)}function jy(e,t,n,i){var a=id(e),r=a[$y(n)](),o=a[Ky(n)]()+1,s=Math.floor((o-1)/3)+1,l=a[Yy(n)](),p=a["get"+(n?"UTC":"")+"Day"](),c=a[Xy(n)](),d=(c-1)%12+1,u=a[Zy(n)](),m=a[Qy(n)](),h=a[Jy(n)](),g=(i instanceof xy?i:Py(i||My)||Ey[Ty]).getModel("time"),f=g.get("month"),y=g.get("monthAbbr"),v=g.get("dayOfWeek"),x=g.get("dayOfWeekAbbr");return(t||"").replace(/{yyyy}/g,r+"").replace(/{yy}/g,Gy(r%100+"",2)).replace(/{Q}/g,s+"").replace(/{MMMM}/g,f[o-1]).replace(/{MMM}/g,y[o-1]).replace(/{MM}/g,Gy(o,2)).replace(/{M}/g,o+"").replace(/{dd}/g,Gy(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,v[p]).replace(/{ee}/g,x[p]).replace(/{e}/g,p+"").replace(/{HH}/g,Gy(c,2)).replace(/{H}/g,c+"").replace(/{hh}/g,Gy(d+"",2)).replace(/{h}/g,d+"").replace(/{mm}/g,Gy(u,2)).replace(/{m}/g,u+"").replace(/{ss}/g,Gy(m,2)).replace(/{s}/g,m+"").replace(/{SSS}/g,Gy(h,3)).replace(/{S}/g,h+"")}function Hy(e,t){var n=id(e),i=n[Ky(t)]()+1,a=n[Yy(t)](),r=n[Xy(t)](),o=n[Zy(t)](),s=n[Qy(t)](),l=0===n[Jy(t)](),p=l&&0===s,c=p&&0===o,d=c&&0===r,u=d&&1===a;return u&&1===i?"year":u?"month":d?"day":c?"hour":p?"minute":l?"second":"millisecond"}function Wy(e,t,n){var i=eo(e)?id(e):e;switch(t=t||Hy(e,n)){case"year":return i[$y(n)]();case"half-year":return i[Ky(n)]()>=6?1:0;case"quarter":return Math.floor((i[Ky(n)]()+1)/4);case"month":return i[Ky(n)]();case"day":return i[Yy(n)]();case"half-day":return i[Xy(n)]()/24;case"hour":return i[Xy(n)]();case"minute":return i[Zy(n)]();case"second":return i[Qy(n)]();case"millisecond":return i[Jy(n)]()}}function $y(e){return e?"getUTCFullYear":"getFullYear"}function Ky(e){return e?"getUTCMonth":"getMonth"}function Yy(e){return e?"getUTCDate":"getDate"}function Xy(e){return e?"getUTCHours":"getHours"}function Zy(e){return e?"getUTCMinutes":"getMinutes"}function Qy(e){return e?"getUTCSeconds":"getSeconds"}function Jy(e){return e?"getUTCMilliseconds":"getMilliseconds"}function ev(e){return e?"setUTCFullYear":"setFullYear"}function tv(e){return e?"setUTCMonth":"setMonth"}function nv(e){return e?"setUTCDate":"setDate"}function iv(e){return e?"setUTCHours":"setHours"}function av(e){return e?"setUTCMinutes":"setMinutes"}function rv(e){return e?"setUTCSeconds":"setSeconds"}function ov(e){return e?"setUTCMilliseconds":"setMilliseconds"}function sv(e){if(!pd(e))return Qr(e)?e:"-";var t=(e+"").split(".");return t[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(t.length>1?"."+t[1]:"")}function lv(e,t){return e=(e||"").toLowerCase().replace(/-(.)/g,(function(e,t){return t.toUpperCase()})),t&&e&&(e=e.charAt(0).toUpperCase()+e.slice(1)),e}var pv=ho;function cv(e,t,n){function i(e){return e&&fo(e)?e:"-"}function a(e){return!(null==e||isNaN(e)||!isFinite(e))}var r="time"===t,o=e instanceof Date;if(r||o){var s=r?id(e):e;if(!isNaN(+s))return jy(s,"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}",n);if(o)return"-"}if("ordinal"===t)return Jr(e)?i(e):eo(e)&&a(e)?e+"":"-";var l=ld(e);return a(l)?sv(l):Jr(e)?i(e):"boolean"==typeof e?e+"":"-"}var dv=["a","b","c","d","e","f","g"],uv=function(e,t){return"{"+e+(null==t?"":t)+"}"};function mv(e,t,n){Xr(t)||(t=[t]);var i=t.length;if(!i)return"";for(var a=t[0].$vars||[],r=0;r':'':{renderMode:r,content:"{"+(n.markerId||"markerX")+"|} ",style:"subItem"===a?{width:4,height:4,borderRadius:2,backgroundColor:i}:{width:10,height:10,borderRadius:5,backgroundColor:i}}:""}function gv(e,t){return t=t||"transparent",Qr(e)?e:to(e)&&e.colorStops&&(e.colorStops[0]||{}).color||t}function fv(e,t){if("_blank"===t||"blank"===t){var n=window.open();n.opener=null,n.location.href=e}else window.open(e,t)}var yv=zr,vv=["left","right","top","bottom","width","height"],xv=[["width","left","right"],["height","top","bottom"]];function bv(e,t,n,i,a){var r=0,o=0;null==i&&(i=1/0),null==a&&(a=1/0);var s=0;t.eachChild((function(l,p){var c,d,u=l.getBoundingRect(),m=t.childAt(p+1),h=m&&m.getBoundingRect();if("horizontal"===e){var g=u.width+(h?-h.x+u.x:0);(c=r+g)>i||l.newline?(r=0,c=g,o+=s+n,s=u.height):s=Math.max(s,u.height)}else{var f=u.height+(h?-h.y+u.y:0);(d=o+f)>a||l.newline?(r+=s+n,o=0,d=f,s=u.width):s=Math.max(s,u.width)}l.newline||(l.x=r,l.y=o,l.markRedraw(),"horizontal"===e?r=c+n:o=d+n)}))}var wv=bv;Yr(bv,"vertical"),Yr(bv,"horizontal");function Sv(e,t,n){n=pv(n||0);var i=t.width,a=t.height,r=Wc(e.left,i),o=Wc(e.top,a),s=Wc(e.right,i),l=Wc(e.bottom,a),p=Wc(e.width,i),c=Wc(e.height,a),d=n[2]+n[0],u=n[1]+n[3],m=e.aspect;switch(isNaN(p)&&(p=i-s-u-r),isNaN(c)&&(c=a-l-d-o),null!=m&&(isNaN(p)&&isNaN(c)&&(m>i/a?p=.8*i:c=.8*a),isNaN(p)&&(p=m*c),isNaN(c)&&(c=p/m)),isNaN(r)&&(r=i-s-p-u),isNaN(o)&&(o=a-l-c-d),e.left||e.right){case"center":r=i/2-p/2-n[3];break;case"right":r=i-p-u}switch(e.top||e.bottom){case"middle":case"center":o=a/2-c/2-n[0];break;case"bottom":o=a-c-d}r=r||0,o=o||0,isNaN(p)&&(p=i-u-r-(s||0)),isNaN(c)&&(c=a-d-o-(l||0));var h=new Ns(r+n[3],o+n[0],p,c);return h.margin=n,h}function Cv(e,t,n,i,a,r){var o,s=!a||!a.hv||a.hv[0],l=!a||!a.hv||a.hv[1],p=a&&a.boundingMode||"all";if((r=r||e).x=e.x,r.y=e.y,!s&&!l)return!1;if("raw"===p)o="group"===e.type?new Ns(0,0,+t.width||0,+t.height||0):e.getBoundingRect();else if(o=e.getBoundingRect(),e.needLocalTransform()){var c=e.getLocalTransform();(o=o.clone()).applyTransform(c)}var d=Sv(Nr({width:o.width,height:o.height},t),n,i),u=s?d.x-o.x:0,m=l?d.y-o.y:0;return"raw"===p?(r.x=u,r.y=m):(r.x+=u,r.y+=m),r===e&&e.markRedraw(),!0}function _v(e){var t=e.layoutMode||e.constructor.layoutMode;return to(t)?t:t?{type:t}:null}function Tv(e,t,n){var i=n&&n.ignoreSize;!Xr(i)&&(i=[i,i]);var a=o(xv[0],0),r=o(xv[1],1);function o(n,a){var r={},o=0,p={},c=0;if(yv(n,(function(t){p[t]=e[t]})),yv(n,(function(e){s(t,e)&&(r[e]=p[e]=t[e]),l(r,e)&&o++,l(p,e)&&c++})),i[a])return l(t,n[1])?p[n[2]]=null:l(t,n[2])&&(p[n[1]]=null),p;if(2!==c&&o){if(o>=2)return r;for(var d=0;d=0;o--)r=Fr(r,n[o],!0);t.defaultOption=r}return t.defaultOption},t.prototype.getReferringComponents=function(e,t){var n=e+"Index",i=e+"Id";return Bd(this.ecModel,e,{index:this.get(n,!0),id:this.get(i,!0)},t)},t.prototype.getBoxLayoutParams=function(){var e=this;return{left:e.get("left"),top:e.get("top"),right:e.get("right"),bottom:e.get("bottom"),width:e.get("width"),height:e.get("height")}},t.prototype.getZLevelKey=function(){return""},t.prototype.setZLevel=function(e){this.option.zlevel=e},t.protoInitialize=function(){var e=t.prototype;e.type="component",e.id="",e.name="",e.mainType="",e.subType="",e.componentIndex=0}(),t}(xy);jd(kv,xy),Kd(kv),function(e){var t={};e.registerSubTypeDefaulter=function(e,n){var i=zd(e);t[i.main]=n},e.determineSubType=function(n,i){var a=i.type;if(!a){var r=zd(n).main;e.hasSubTypes(n)&&t[r]&&(a=t[r](i))}return a}}(kv),function(e,t){function n(e,t){return e[t]||(e[t]={predecessor:[],successor:[]}),e[t]}e.topologicalTravel=function(e,i,a,r){if(e.length){var o=function(e){var i={},a=[];return zr(e,(function(r){var o=n(i,r),s=function(e,t){var n=[];return zr(e,(function(e){Lr(t,e)>=0&&n.push(e)})),n}(o.originalDeps=t(r),e);o.entryCount=s.length,0===o.entryCount&&a.push(r),zr(s,(function(e){Lr(o.predecessor,e)<0&&o.predecessor.push(e);var t=n(i,e);Lr(t.successor,e)<0&&t.successor.push(r)}))})),{graph:i,noEntryList:a}}(i),s=o.graph,l=o.noEntryList,p={};for(zr(e,(function(e){p[e]=!0}));l.length;){var c=l.pop(),d=s[c],u=!!p[c];u&&(a.call(r,c,d.originalDeps.slice()),delete p[c]),zr(d.successor,u?h:m)}zr(p,(function(){var e="";throw new Error(e)}))}function m(e){s[e].entryCount--,0===s[e].entryCount&&l.push(e)}function h(e){p[e]=!0,m(e)}}}(kv,(function(e){var t=[];zr(kv.getClassesByMainType(e),(function(e){t=t.concat(e.dependencies||e.prototype.dependencies||[])})),t=Ur(t,(function(e){return zd(e).main})),"dataset"!==e&&Lr(t,"dataset")<=0&&t.unshift("dataset");return t}));var Pv="";"undefined"!=typeof navigator&&(Pv=navigator.platform||"");var Dv="rgba(0, 0, 0, 0.2)",Ov={darkMode:"auto",colorBy:"series",color:["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],gradientColor:["#f6efa6","#d88273","#bf444c"],aria:{decal:{decals:[{color:Dv,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:Dv,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:Dv,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:Dv,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:Dv,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:Dv,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:Pv.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},Av=Co(["tooltip","label","itemName","itemId","itemGroupId","itemChildGroupId","seriesName"]),Fv="original",Rv="arrayRows",Bv="objectRows",Nv="keyedColumns",Lv="typedArray",Vv="unknown",qv="column",Gv="row",zv=1,Uv=2,jv=3,Hv=Pd();function Wv(e,t,n){var i={},a=Kv(t);if(!a||!e)return i;var r,o,s=[],l=[],p=t.ecModel,c=Hv(p).datasetMap,d=a.uid+"_"+n.seriesLayoutBy;zr(e=e.slice(),(function(t,n){var a=to(t)?t:e[n]={name:t};"ordinal"===a.type&&null==r&&(r=n,o=h(a)),i[a.name]=[]}));var u=c.get(d)||c.set(d,{categoryWayDim:o,valueWayDim:0});function m(e,t,n){for(var i=0;it)return e[i];return e[n-1]}(i,o):n;if((c=c||n)&&c.length){var d=c[l];return a&&(p[a]=d),s.paletteIdx=(l+1)%c.length,d}}var ox="\0_ec_inner";var sx=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.init=function(e,t,n,i,a,r){i=i||{},this.option=null,this._theme=new xy(i),this._locale=new xy(a),this._optionManager=r},t.prototype.setOption=function(e,t,n){var i=cx(t);this._optionManager.setOption(e,n,i),this._resetOption(null,i)},t.prototype.resetOption=function(e,t){return this._resetOption(e,cx(t))},t.prototype._resetOption=function(e,t){var n=!1,i=this._optionManager;if(!e||"recreate"===e){var a=i.mountOption("recreate"===e);0,this.option&&"recreate"!==e?(this.restoreData(),this._mergeOption(a,t)):ex(this,a),n=!0}if("timeline"!==e&&"media"!==e||this.restoreData(),!e||"recreate"===e||"timeline"===e){var r=i.getTimelineOption(this);r&&(n=!0,this._mergeOption(r,t))}if(!e||"recreate"===e||"media"===e){var o=i.getMediaOption(this);o.length&&zr(o,(function(e){n=!0,this._mergeOption(e,t)}),this)}return n},t.prototype.mergeOption=function(e){this._mergeOption(e,null)},t.prototype._mergeOption=function(e,t){var n=this.option,i=this._componentsMap,a=this._componentsCount,r=[],o=Co(),s=t&&t.replaceMergeMainTypeMap;Hv(this).datasetMap=Co(),zr(e,(function(e,t){null!=e&&(kv.hasClass(t)?t&&(r.push(t),o.set(t,!0)):n[t]=null==n[t]?Ar(e):Fr(n[t],e,!0))})),s&&s.each((function(e,t){kv.hasClass(t)&&!o.get(t)&&(r.push(t),o.set(t,!0))})),kv.topologicalTravel(r,kv.getAllClassMainTypes(),(function(t){var r=function(e,t,n){var i=Zv.get(t);if(!i)return n;var a=i(e);return a?n.concat(a):n}(this,t,vd(e[t])),o=i.get(t),l=o?s&&s.get(t)?"replaceMerge":"normalMerge":"replaceAll",p=Cd(o,r,l);(function(e,t,n){zr(e,(function(e){var i=e.newOption;to(i)&&(e.keyInfo.mainType=t,e.keyInfo.subType=function(e,t,n,i){return t.type?t.type:n?n.subType:i.determineSubType(e,t)}(t,i,e.existing,n))}))})(p,t,kv),n[t]=null,i.set(t,null),a.set(t,0);var c,d=[],u=[],m=0;zr(p,(function(e,n){var i=e.existing,a=e.newOption;if(a){var r="series"===t,o=kv.getClass(t,e.keyInfo.subType,!r);if(!o)return;if("tooltip"===t){if(c)return void 0;c=!0}if(i&&i.constructor===o)i.name=e.keyInfo.name,i.mergeOption(a,this),i.optionUpdated(a,!1);else{var s=Br({componentIndex:n},e.keyInfo);Br(i=new o(a,this,this,s),s),e.brandNew&&(i.__requireNewView=!0),i.init(a,this,this),i.optionUpdated(null,!0)}}else i&&(i.mergeOption({},this),i.optionUpdated({},!1));i?(d.push(i.option),u.push(i),m++):(d.push(void 0),u.push(void 0))}),this),n[t]=d,i.set(t,u),a.set(t,m),"series"===t&&Qv(this)}),this),this._seriesIndices||Qv(this)},t.prototype.getOption=function(){var e=Ar(this.option);return zr(e,(function(t,n){if(kv.hasClass(n)){for(var i=vd(t),a=i.length,r=!1,o=a-1;o>=0;o--)i[o]&&!Md(i[o])?r=!0:(i[o]=null,!r&&a--);i.length=a,e[n]=i}})),delete e[ox],e},t.prototype.getTheme=function(){return this._theme},t.prototype.getLocaleModel=function(){return this._locale},t.prototype.setUpdatePayload=function(e){this._payload=e},t.prototype.getUpdatePayload=function(){return this._payload},t.prototype.getComponent=function(e,t){var n=this._componentsMap.get(e);if(n){var i=n[t||0];if(i)return i;if(null==t)for(var a=0;a=t:"max"===n?e<=t:e===t})(i[o],e,r)||(a=!1)}})),a}var vx=zr,xx=to,bx=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function Sx(e){var t=e&&e.itemStyle;if(t)for(var n=0,i=bx.length;n=0;g--){var f=e[g];if(s||(u=f.data.rawIndexOf(f.stackedByDimension,d)),u>=0){var y=f.data.getByRawIndex(f.stackResultDimension,u);if("all"===l||"positive"===l&&y>0||"negative"===l&&y<0||"samesign"===l&&m>=0&&y>0||"samesign"===l&&m<=0&&y<0){m=Jc(m,y),h=y;break}}}return i[0]=m,i[1]=h,i}))}))}var qx,Gx,zx,Ux,jx,Hx=function(e){this.data=e.data||(e.sourceFormat===Nv?{}:[]),this.sourceFormat=e.sourceFormat||Vv,this.seriesLayoutBy=e.seriesLayoutBy||qv,this.startIndex=e.startIndex||0,this.dimensionsDetectedCount=e.dimensionsDetectedCount,this.metaRawOption=e.metaRawOption;var t=this.dimensionsDefine=e.dimensionsDefine;if(t)for(var n=0;np&&(p=m)}s[0]=l,s[1]=p}},i=function(){return this._data?this._data.length/this._dimSize:0};function a(e){for(var t=0;t=0&&(s=r.interpolatedValue[l])}return null!=s?s+"":""})):void 0},e.prototype.getRawValue=function(e,t){return cb(this.getData(t),e)},e.prototype.formatTooltip=function(e,t,n){},e}();function mb(e){var t,n;return to(e)?e.type&&(n=e):t=e,{text:t,frag:n}}function hb(e){return new gb(e)}var gb=function(){function e(e){e=e||{},this._reset=e.reset,this._plan=e.plan,this._count=e.count,this._onDirty=e.onDirty,this._dirty=!0}return e.prototype.perform=function(e){var t,n=this._upstream,i=e&&e.skip;if(this._dirty&&n){var a=this.context;a.data=a.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this),this._plan&&!i&&(t=this._plan(this.context));var r,o=c(this._modBy),s=this._modDataCount||0,l=c(e&&e.modBy),p=e&&e.modDataCount||0;function c(e){return!(e>=1)&&(e=1),e}o===l&&s===p||(t="reset"),(this._dirty||"reset"===t)&&(this._dirty=!1,r=this._doReset(i)),this._modBy=l,this._modDataCount=p;var d=e&&e.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var u=this._dueIndex,m=Math.min(null!=d?this._dueIndex+d:1/0,this._dueEnd);if(!i&&(r||u1&&i>0?s:o}};return r;function o(){return t=e?null:rt},gte:function(e,t){return e>=t}},wb=function(){function e(e,t){if(!eo(t)){var n="";0,hd(n)}this._opFn=bb[e],this._rvalFloat=ld(t)}return e.prototype.evaluate=function(e){return eo(e)?this._opFn(e,this._rvalFloat):this._opFn(ld(e),this._rvalFloat)},e}(),Sb=function(){function e(e,t){var n="desc"===e;this._resultLT=n?1:-1,null==t&&(t=n?"min":"max"),this._incomparable="min"===t?-1/0:1/0}return e.prototype.evaluate=function(e,t){var n=eo(e)?e:ld(e),i=eo(t)?t:ld(t),a=isNaN(n),r=isNaN(i);if(a&&(n=this._incomparable),r&&(i=this._incomparable),a&&r){var o=Qr(e),s=Qr(t);o&&(n=s?e:0),s&&(i=o?t:0)}return ni?-this._resultLT:0},e}(),Cb=function(){function e(e,t){this._rval=t,this._isEQ=e,this._rvalTypeof=typeof t,this._rvalFloat=ld(t)}return e.prototype.evaluate=function(e){var t=e===this._rval;if(!t){var n=typeof e;n===this._rvalTypeof||"number"!==n&&"number"!==this._rvalTypeof||(t=ld(e)===this._rvalFloat)}return this._isEQ?t:!t},e}();function _b(e,t){return"eq"===e||"ne"===e?new Cb("eq"===e,t):Eo(bb,e)?new wb(e,t):null}var Tb=function(){function e(){}return e.prototype.getRawData=function(){throw new Error("not supported")},e.prototype.getRawDataItem=function(e){throw new Error("not supported")},e.prototype.cloneRawData=function(){},e.prototype.getDimensionInfo=function(e){},e.prototype.cloneAllDimensionInfo=function(){},e.prototype.count=function(){},e.prototype.retrieveValue=function(e,t){},e.prototype.retrieveValueFromItem=function(e,t){},e.prototype.convertValue=function(e,t){return yb(e,t)},e}();function Ib(e){var t=e.sourceFormat;if(!Ob(t)){var n="";0,hd(n)}return e.data}function Eb(e){var t=e.sourceFormat,n=e.data;if(!Ob(t)){var i="";0,hd(i)}if(t===Rv){for(var a=[],r=0,o=n.length;r65535?Rb:Bb}function Gb(e,t,n,i,a){var r=Vb[n||"float"];if(a){var o=e[t],s=o&&o.length;if(s!==i){for(var l=new r(i),p=0;pg[1]&&(g[1]=h)}return this._rawCount=this._count=s,{start:o,end:s}},e.prototype._initDataFromProvider=function(e,t,n){for(var i=this._provider,a=this._chunks,r=this._dimensions,o=r.length,s=this._rawExtent,l=Ur(r,(function(e){return e.property})),p=0;pf[1]&&(f[1]=g)}}!i.persistent&&i.clean&&i.clean(),this._rawCount=this._count=t,this._extent=[]},e.prototype.count=function(){return this._count},e.prototype.get=function(e,t){if(!(t>=0&&t=0&&t=this._rawCount||e<0)return-1;if(!this._indices)return e;var t=this._indices,n=t[e];if(null!=n&&ne))return r;a=r-1}}return-1},e.prototype.indicesOfNearest=function(e,t,n){var i=this._chunks[e],a=[];if(!i)return a;null==n&&(n=1/0);for(var r=1/0,o=-1,s=0,l=0,p=this.count();l=0&&o<0)&&(r=d,o=c,s=0),c===o&&(a[s++]=l))}return a.length=s,a},e.prototype.getIndices=function(){var e,t=this._indices;if(t){var n=t.constructor,i=this._count;if(n===Array){e=new n(i);for(var a=0;a=p&&x<=c||isNaN(x))&&(o[s++]=m),m++}u=!0}else if(2===a){h=d[i[0]];var f=d[i[1]],y=e[i[1]][0],v=e[i[1]][1];for(g=0;g=p&&x<=c||isNaN(x))&&(b>=y&&b<=v||isNaN(b))&&(o[s++]=m),m++}u=!0}}if(!u)if(1===a)for(g=0;g=p&&x<=c||isNaN(x))&&(o[s++]=w)}else for(g=0;ge[_][1])&&(S=!1)}S&&(o[s++]=t.getRawIndex(g))}return sf[1]&&(f[1]=g)}}}},e.prototype.lttbDownSample=function(e,t){var n,i,a,r=this.clone([e],!0),o=r._chunks[e],s=this.count(),l=0,p=Math.floor(1/t),c=this.getRawIndex(0),d=new(qb(this._rawCount))(Math.min(2*(Math.ceil(s/p)+2),s));d[l++]=c;for(var u=1;un&&(n=i,a=T)}_>0&&_p-m&&(s=p-m,o.length=s);for(var h=0;hc[1]&&(c[1]=f),d[u++]=y}return a._count=u,a._indices=d,a._updateGetRawIdx(),a},e.prototype.each=function(e,t){if(this._count)for(var n=e.length,i=this._chunks,a=0,r=this.count();ao&&(o=l)}return i=[r,o],this._extent[e]=i,i},e.prototype.getRawDataItem=function(e){var t=this.getRawIndex(e);if(this._provider.persistent)return this._provider.getItem(t);for(var n=[],i=this._chunks,a=0;a=0?this._indices[e]:-1},e.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},e.internalField=function(){function e(e,t,n,i){return yb(e[i],this._dimensions[i])}Ab={arrayRows:e,objectRows:function(e,t,n,i){return yb(e[t],this._dimensions[i])},keyedColumns:e,original:function(e,t,n,i){var a=e&&(null==e.value?e:e.value);return yb(a instanceof Array?a[i]:a,this._dimensions[i])},typedArray:function(e,t,n,i){return e[i]}}}(),e}(),Ub=function(){function e(e){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=e}return e.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},e.prototype._setLocalSource=function(e,t){this._sourceList=e,this._upstreamSignList=t,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},e.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},e.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},e.prototype._createSource=function(){this._setLocalSource([],[]);var e,t,n=this._sourceHost,i=this._getUpstreamSourceManagers(),a=!!i.length;if(Hb(n)){var r=n,o=void 0,s=void 0,l=void 0;if(a){var p=i[0];p.prepareSource(),o=(l=p.getSource()).data,s=l.sourceFormat,t=[p._getVersionSign()]}else s=io(o=r.get("data",!0))?Lv:Fv,t=[];var c=this._getSourceMetaRawOption()||{},d=l&&l.metaRawOption||{},u=co(c.seriesLayoutBy,d.seriesLayoutBy)||null,m=co(c.sourceHeader,d.sourceHeader),h=co(c.dimensions,d.dimensions);e=u!==d.seriesLayoutBy||!!m!=!!d.sourceHeader||h?[$x(o,{seriesLayoutBy:u,sourceHeader:m,dimensions:h},s)]:[]}else{var g=n;if(a){var f=this._applyTransform(i);e=f.sourceList,t=f.upstreamSignList}else{e=[$x(g.get("source",!0),this._getSourceMetaRawOption(),null)],t=[]}}this._setLocalSource(e,t)},e.prototype._applyTransform=function(e){var t,n=this._sourceHost,i=n.get("transform",!0),a=n.get("fromTransformResult",!0);if(null!=a){var r="";1!==e.length&&Wb(r)}var o,s=[],l=[];return zr(e,(function(e){e.prepareSource();var t=e.getSource(a||0),n="";null==a||t||Wb(n),s.push(t),l.push(e._getVersionSign())})),i?t=function(e,t){var n=vd(e),i=n.length,a="";i||hd(a);for(var r=0,o=i;r1||n>0&&!e.noHeader;return zr(e.blocks,(function(e){var n=ew(e);n>=t&&(t=n+ +(i&&(!n||Qb(e)&&!e.noHeader)))})),t}return 0}function tw(e,t,n,i){var a,r=t.noHeader,o=(a=ew(t),{html:Yb[a],richText:Xb[a]}),s=[],l=t.blocks||[];go(!l||Xr(l)),l=l||[];var p=e.orderMode;if(t.sortBlocks&&p){l=l.slice();var c={valueAsc:"asc",valueDesc:"desc"};if(Eo(c,p)){var d=new Sb(c[p],null);l.sort((function(e,t){return d.evaluate(e.sortParam,t.sortParam)}))}else"seriesDesc"===p&&l.reverse()}zr(l,(function(n,a){var r=t.valueFormatter,l=Jb(n)(r?Br(Br({},e),{valueFormatter:r}):e,n,a>0?o.html:0,i);null!=l&&s.push(l)}));var u="richText"===e.renderMode?s.join(o.richText):aw(s.join(""),r?n:o.html);if(r)return u;var m=cv(t.header,"ordinal",e.useUTC),h=Kb(i,e.renderMode).nameStyle;return"richText"===e.renderMode?rw(e,m,h)+o.richText+u:aw('
'+os(m)+"
"+u,n)}function nw(e,t,n,i){var a=e.renderMode,r=t.noName,o=t.noValue,s=!t.markerType,l=t.name,p=e.useUTC,c=t.valueFormatter||e.valueFormatter||function(e){return Ur(e=Xr(e)?e:[e],(function(e,t){return cv(e,Xr(m)?m[t]:m,p)}))};if(!r||!o){var d=s?"":e.markupStyleCreator.makeTooltipMarker(t.markerType,t.markerColor||"#333",a),u=r?"":cv(l,"ordinal",p),m=t.valueType,h=o?[]:c(t.value,t.dataIndex),g=!s||!r,f=!s&&r,y=Kb(i,a),v=y.nameStyle,x=y.valueStyle;return"richText"===a?(s?"":d)+(r?"":rw(e,u,v))+(o?"":function(e,t,n,i,a){var r=[a],o=i?10:20;return n&&r.push({padding:[0,0,0,o],align:"right"}),e.markupStyleCreator.wrapRichTextStyle(Xr(t)?t.join(" "):t,r)}(e,h,g,f,x)):aw((s?"":d)+(r?"":function(e,t,n){return''+os(e)+""}(u,!s,v))+(o?"":function(e,t,n,i){var a=n?"10px":"20px",r=t?"float:right;margin-left:"+a:"";return e=Xr(e)?e:[e],''+Ur(e,(function(e){return os(e)})).join("  ")+""}(h,g,f,x)),n)}}function iw(e,t,n,i,a,r){if(e)return Jb(e)({useUTC:a,renderMode:n,orderMode:i,markupStyleCreator:t,valueFormatter:e.valueFormatter},e,0,r)}function aw(e,t){return'
'+e+'
'}function rw(e,t,n){return e.markupStyleCreator.wrapRichTextStyle(t,n)}function ow(e,t){return gv(e.getData().getItemVisual(t,"style")[e.visualDrawType])}function sw(e,t){var n=e.get("padding");return null!=n?n:"richText"===t?[8,10]:10}var lw=function(){function e(){this.richTextStyles={},this._nextStyleNameId=cd()}return e.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},e.prototype.makeTooltipMarker=function(e,t,n){var i="richText"===n?this._generateStyleName():null,a=hv({color:t,type:e,renderMode:n,markerId:i});return Qr(a)?a:(this.richTextStyles[i]=a.style,a.content)},e.prototype.wrapRichTextStyle=function(e,t){var n={};Xr(t)?zr(t,(function(e){return Br(n,e)})):Br(n,t);var i=this._generateStyleName();return this.richTextStyles[i]=n,"{"+i+"|"+e+"}"},e}();function pw(e){var t,n,i,a,r=e.series,o=e.dataIndex,s=e.multipleSeries,l=r.getData(),p=l.mapDimensionsAll("defaultedTooltip"),c=p.length,d=r.getRawValue(o),u=Xr(d),m=ow(r,o);if(c>1||u&&!c){var h=function(e,t,n,i,a){var r=t.getData(),o=jr(e,(function(e,t,n){var i=r.getDimensionInfo(n);return e||i&&!1!==i.tooltip&&null!=i.displayName}),!1),s=[],l=[],p=[];function c(e,t){var n=r.getDimensionInfo(t);n&&!1!==n.otherDims.tooltip&&(o?p.push(Zb("nameValue",{markerType:"subItem",markerColor:a,name:n.displayName,value:e,valueType:n.type})):(s.push(e),l.push(n.type)))}return i.length?zr(i,(function(e){c(cb(r,n,e),e)})):zr(e,c),{inlineValues:s,inlineValueTypes:l,blocks:p}}(d,r,o,p,m);t=h.inlineValues,n=h.inlineValueTypes,i=h.blocks,a=h.inlineValues[0]}else if(c){var g=l.getDimensionInfo(p[0]);a=t=cb(l,o,p[0]),n=g.type}else a=t=u?d[0]:d;var f=Ed(r),y=f&&r.name||"",v=l.getName(o),x=s?y:v;return Zb("section",{header:y,noHeader:s||!f,sortParam:a,blocks:[Zb("nameValue",{markerType:"item",markerColor:m,name:x,noName:!fo(x),value:t,valueType:n,dataIndex:o})].concat(i||[])})}var cw=Pd();function dw(e,t){return e.getName(t)||e.getId(t)}var uw="__universalTransitionEnabled",mw=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t._selectedDataIndicesMap={},t}return We(t,e),t.prototype.init=function(e,t,n){this.seriesIndex=this.componentIndex,this.dataTask=hb({count:gw,reset:fw}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(e,n),(cw(this).sourceManager=new Ub(this)).prepareSource();var i=this.getInitialData(e,n);vw(i,this),this.dataTask.context.data=i,cw(this).dataBeforeProcessed=i,hw(this),this._initSelectedMapFromData(i)},t.prototype.mergeDefaultAndTheme=function(e,t){var n=_v(this),i=n?Iv(e):{},a=this.subType;kv.hasClass(a)&&(a+="Series"),Fr(e,t.getTheme().get(this.subType)),Fr(e,this.getDefaultOption()),xd(e,"label",["show"]),this.fillDataTextStyle(e.data),n&&Tv(e,i,n)},t.prototype.mergeOption=function(e,t){e=Fr(this.option,e,!0),this.fillDataTextStyle(e.data);var n=_v(this);n&&Tv(this.option,e,n);var i=cw(this).sourceManager;i.dirty(),i.prepareSource();var a=this.getInitialData(e,t);vw(a,this),this.dataTask.dirty(),this.dataTask.context.data=a,cw(this).dataBeforeProcessed=a,hw(this),this._initSelectedMapFromData(a)},t.prototype.fillDataTextStyle=function(e){if(e&&!io(e))for(var t=["show"],n=0;nthis.getShallow("animationThreshold")&&(t=!1),!!t},t.prototype.restoreData=function(){this.dataTask.dirty()},t.prototype.getColorFromPalette=function(e,t,n){var i=this.ecModel,a=ix.prototype.getColorFromPalette.call(this,e,t,n);return a||(a=i.getColorFromPalette(e,t,n)),a},t.prototype.coordDimToDataDim=function(e){return this.getRawData().mapDimensionsAll(e)},t.prototype.getProgressive=function(){return this.get("progressive")},t.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},t.prototype.select=function(e,t){this._innerSelect(this.getData(t),e)},t.prototype.unselect=function(e,t){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,a=this.getData(t);if("series"===i||"all"===n)return this.option.selectedMap={},void(this._selectedDataIndicesMap={});for(var r=0;r=0&&n.push(a)}return n},t.prototype.isSelected=function(e,t){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(t);return("all"===n||n[dw(i,e)])&&!i.getItemModel(e).get(["select","disabled"])},t.prototype.isUniversalTransitionEnabled=function(){if(this[uw])return!0;var e=this.option.universalTransition;return!!e&&(!0===e||e&&e.enabled)},t.prototype._innerSelect=function(e,t){var n,i,a=this.option,r=a.selectedMode,o=t.length;if(r&&o)if("series"===r)a.selectedMap="all";else if("multiple"===r){to(a.selectedMap)||(a.selectedMap={});for(var s=a.selectedMap,l=0;l0&&this._innerSelect(e,t)}},t.registerClass=function(e){return kv.registerClass(e)},t.protoInitialize=function(){var e=t.prototype;e.type="series.__base__",e.seriesIndex=0,e.ignoreStyleOnData=!1,e.hasSymbolVisual=!1,e.defaultSymbol="circle",e.visualStyleAccessPath="itemStyle",e.visualDrawType="fill"}(),t}(kv);function hw(e){var t=e.name;Ed(e)||(e.name=function(e){var t=e.getRawData(),n=t.mapDimensionsAll("seriesName"),i=[];return zr(n,(function(e){var n=t.getDimensionInfo(e);n.displayName&&i.push(n.displayName)})),i.join(" ")}(e)||t)}function gw(e){return e.model.getRawData().count()}function fw(e){var t=e.model;return t.setData(t.getRawData().cloneShallow()),yw}function yw(e,t){t.outputData&&e.end>t.outputData.count()&&t.model.getRawData().cloneShallow(t.outputData)}function vw(e,t){zr(_o(e.CHANGABLE_METHODS,e.DOWNSAMPLE_METHODS),(function(n){e.wrapMethod(n,Yr(xw,t))}))}function xw(e,t){var n=bw(e);return n&&n.setOutputEnd((t||this).count()),t}function bw(e){var t=(e.ecModel||{}).scheduler,n=t&&t.getPipeline(e.uid);if(n){var i=n.currentTask;if(i){var a=i.agentStubMap;a&&(i=a.get(e.uid))}return i}}qr(mw,ub),qr(mw,ix),jd(mw,kv);var ww=function(){function e(){this.group=new Rc,this.uid=wy("viewComponent")}return e.prototype.init=function(e,t){},e.prototype.render=function(e,t,n,i){},e.prototype.dispose=function(e,t){},e.prototype.updateView=function(e,t,n,i){},e.prototype.updateLayout=function(e,t,n,i){},e.prototype.updateVisual=function(e,t,n,i){},e.prototype.toggleBlurSeries=function(e,t,n){},e.prototype.eachRendered=function(e){var t=this.group;t&&t.traverse(e)},e}();function Sw(){var e=Pd();return function(t){var n=e(t),i=t.pipelineContext,a=!!n.large,r=!!n.progressiveRender,o=n.large=!(!i||!i.large),s=n.progressiveRender=!(!i||!i.progressiveRender);return!(a===o&&r===s)&&"reset"}}Ud(ww),Kd(ww);var Cw=Pd(),_w=Sw(),Tw=function(){function e(){this.group=new Rc,this.uid=wy("viewChart"),this.renderTask=hb({plan:Mw,reset:kw}),this.renderTask.context={view:this}}return e.prototype.init=function(e,t){},e.prototype.render=function(e,t,n,i){0},e.prototype.highlight=function(e,t,n,i){var a=e.getData(i&&i.dataType);a&&Ew(a,i,"emphasis")},e.prototype.downplay=function(e,t,n,i){var a=e.getData(i&&i.dataType);a&&Ew(a,i,"normal")},e.prototype.remove=function(e,t){this.group.removeAll()},e.prototype.dispose=function(e,t){},e.prototype.updateView=function(e,t,n,i){this.render(e,t,n,i)},e.prototype.updateLayout=function(e,t,n,i){this.render(e,t,n,i)},e.prototype.updateVisual=function(e,t,n,i){this.render(e,t,n,i)},e.prototype.eachRendered=function(e){jf(this.group,e)},e.markUpdateMethod=function(e,t){Cw(e).updateMethod=t},e.protoInitialize=void(e.prototype.type="chart"),e}();function Iw(e,t,n){e&&Hh(e)&&("emphasis"===t?Th:Ih)(e,n)}function Ew(e,t,n){var i=kd(e,t),a=t&&null!=t.highlightKey?function(e){var t=Jm[e];return null==t&&Qm<=32&&(t=Jm[e]=Qm++),t}(t.highlightKey):null;null!=i?zr(vd(i),(function(t){Iw(e.getItemGraphicEl(t),n,a)})):e.eachItemGraphicEl((function(e){Iw(e,n,a)}))}function Mw(e){return _w(e.model)}function kw(e){var t=e.model,n=e.ecModel,i=e.api,a=e.payload,r=t.pipelineContext.progressiveRender,o=e.view,s=a&&Cw(a).updateMethod,l=r?"incrementalPrepareRender":s&&o[s]?s:"render";return"render"!==l&&o[l](t,n,i,a),Pw[l]}Ud(Tw),Kd(Tw);var Pw={incrementalPrepareRender:{progress:function(e,t){t.view.incrementalRender(e,t.model,t.ecModel,t.api,t.payload)}},render:{forceFirstProgress:!0,progress:function(e,t){t.view.render(t.model,t.ecModel,t.api,t.payload)}}},Dw="\0__throttleOriginMethod",Ow="\0__throttleRate",Aw="\0__throttleType";function Fw(e,t,n){var i,a,r,o,s,l=0,p=0,c=null;function d(){p=(new Date).getTime(),c=null,e.apply(r,o||[])}t=t||0;var u=function(){for(var e=[],u=0;u=0?d():c=setTimeout(d,-a),l=i};return u.clear=function(){c&&(clearTimeout(c),c=null)},u.debounceNextCall=function(e){s=e},u}function Rw(e,t,n,i){var a=e[t];if(a){var r=a[Dw]||a,o=a[Aw];if(a[Ow]!==n||o!==i){if(null==n||!i)return e[t]=r;(a=e[t]=Fw(r,n,"debounce"===i))[Dw]=r,a[Aw]=i,a[Ow]=n}return a}}function Bw(e,t){var n=e[t];n&&n[Dw]&&(n.clear&&n.clear(),e[t]=n[Dw])}var Nw=Pd(),Lw={itemStyle:Yd(fy,!0),lineStyle:Yd(my,!0)},Vw={lineStyle:"stroke",itemStyle:"fill"};function qw(e,t){var n=e.visualStyleMapper||Lw[t];return n||(console.warn("Unknown style type '"+t+"'."),Lw.itemStyle)}function Gw(e,t){var n=e.visualDrawType||Vw[t];return n||(console.warn("Unknown style type '"+t+"'."),"fill")}var zw={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){var n=e.getData(),i=e.visualStyleAccessPath||"itemStyle",a=e.getModel(i),r=qw(e,i)(a),o=a.getShallow("decal");o&&(n.setVisual("decal",o),o.dirty=!0);var s=Gw(e,i),l=r[s],p=Zr(l)?l:null,c="auto"===r.fill||"auto"===r.stroke;if(!r[s]||p||c){var d=e.getColorFromPalette(e.name,null,t.getSeriesCount());r[s]||(r[s]=d,n.setVisual("colorFromPalette",!0)),r.fill="auto"===r.fill||Zr(r.fill)?d:r.fill,r.stroke="auto"===r.stroke||Zr(r.stroke)?d:r.stroke}if(n.setVisual("style",r),n.setVisual("drawType",s),!t.isSeriesFiltered(e)&&p)return n.setVisual("colorFromPalette",!1),{dataEach:function(t,n){var i=e.getDataParams(n),a=Br({},r);a[s]=p(i),t.setItemVisual(n,"style",a)}}}},Uw=new xy,jw={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){if(!e.ignoreStyleOnData&&!t.isSeriesFiltered(e)){var n=e.getData(),i=e.visualStyleAccessPath||"itemStyle",a=qw(e,i),r=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(e,t){var n=e.getRawDataItem(t);if(n&&n[i]){Uw.option=n[i];var o=a(Uw);Br(e.ensureUniqueItemVisual(t,"style"),o),Uw.option.decal&&(e.setItemVisual(t,"decal",Uw.option.decal),Uw.option.decal.dirty=!0),r in o&&e.setItemVisual(t,"colorFromPalette",!1)}}:null}}}},Hw={performRawSeries:!0,overallReset:function(e){var t=Co();e.eachSeries((function(e){var n=e.getColorBy();if(!e.isColorBySeries()){var i=e.type+"-"+n,a=t.get(i);a||(a={},t.set(i,a)),Nw(e).scope=a}})),e.eachSeries((function(t){if(!t.isColorBySeries()&&!e.isSeriesFiltered(t)){var n=t.getRawData(),i={},a=t.getData(),r=Nw(t).scope,o=t.visualStyleAccessPath||"itemStyle",s=Gw(t,o);a.each((function(e){var t=a.getRawIndex(e);i[t]=e})),n.each((function(e){var o=i[e];if(a.getItemVisual(o,"colorFromPalette")){var l=a.ensureUniqueItemVisual(o,"style"),p=n.getName(e)||e+"",c=n.count();l[s]=t.getColorFromPalette(p,r,c)}}))}}))}},Ww=Math.PI;var $w=function(){function e(e,t,n,i){this._stageTaskMap=Co(),this.ecInstance=e,this.api=t,n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice(),this._allHandlers=n.concat(i)}return e.prototype.restoreData=function(e,t){e.restoreData(t),this._stageTaskMap.each((function(e){var t=e.overallTask;t&&t.dirty()}))},e.prototype.getPerformArgs=function(e,t){if(e.__pipeline){var n=this._pipelineMap.get(e.__pipeline.id),i=n.context,a=!t&&n.progressiveEnabled&&(!i||i.progressiveRender)&&e.__idxInPipeline>n.blockIndex?n.step:null,r=i&&i.modDataCount;return{step:a,modBy:null!=r?Math.ceil(r/a):null,modDataCount:r}}},e.prototype.getPipeline=function(e){return this._pipelineMap.get(e)},e.prototype.updateStreamModes=function(e,t){var n=this._pipelineMap.get(e.uid),i=e.getData().count(),a=n.progressiveEnabled&&t.incrementalPrepareRender&&i>=n.threshold,r=e.get("large")&&i>=e.get("largeThreshold"),o="mod"===e.get("progressiveChunkMode")?i:null;e.pipelineContext=n.context={progressiveRender:a,modDataCount:o,large:r}},e.prototype.restorePipelines=function(e){var t=this,n=t._pipelineMap=Co();e.eachSeries((function(e){var i=e.getProgressive(),a=e.uid;n.set(a,{id:a,head:null,tail:null,threshold:e.getProgressiveThreshold(),progressiveEnabled:i&&!(e.preventIncremental&&e.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),t._pipe(e,e.dataTask)}))},e.prototype.prepareStageTasks=function(){var e=this._stageTaskMap,t=this.api.getModel(),n=this.api;zr(this._allHandlers,(function(i){var a=e.get(i.uid)||e.set(i.uid,{}),r="";go(!(i.reset&&i.overallReset),r),i.reset&&this._createSeriesStageTask(i,a,t,n),i.overallReset&&this._createOverallStageTask(i,a,t,n)}),this)},e.prototype.prepareView=function(e,t,n,i){var a=e.renderTask,r=a.context;r.model=t,r.ecModel=n,r.api=i,a.__block=!e.incrementalPrepareRender,this._pipe(t,a)},e.prototype.performDataProcessorTasks=function(e,t){this._performStageTasks(this._dataProcessorHandlers,e,t,{block:!0})},e.prototype.performVisualTasks=function(e,t,n){this._performStageTasks(this._visualHandlers,e,t,n)},e.prototype._performStageTasks=function(e,t,n,i){i=i||{};var a=!1,r=this;function o(e,t){return e.setDirty&&(!e.dirtyMap||e.dirtyMap.get(t.__pipeline.id))}zr(e,(function(e,s){if(!i.visualType||i.visualType===e.visualType){var l=r._stageTaskMap.get(e.uid),p=l.seriesTaskMap,c=l.overallTask;if(c){var d,u=c.agentStubMap;u.each((function(e){o(i,e)&&(e.dirty(),d=!0)})),d&&c.dirty(),r.updatePayload(c,n);var m=r.getPerformArgs(c,i.block);u.each((function(e){e.perform(m)})),c.perform(m)&&(a=!0)}else p&&p.each((function(s,l){o(i,s)&&s.dirty();var p=r.getPerformArgs(s,i.block);p.skip=!e.performRawSeries&&t.isSeriesFiltered(s.context.model),r.updatePayload(s,n),s.perform(p)&&(a=!0)}))}})),this.unfinished=a||this.unfinished},e.prototype.performSeriesTasks=function(e){var t;e.eachSeries((function(e){t=e.dataTask.perform()||t})),this.unfinished=t||this.unfinished},e.prototype.plan=function(){this._pipelineMap.each((function(e){var t=e.tail;do{if(t.__block){e.blockIndex=t.__idxInPipeline;break}t=t.getUpstream()}while(t)}))},e.prototype.updatePayload=function(e,t){"remain"!==t&&(e.context.payload=t)},e.prototype._createSeriesStageTask=function(e,t,n,i){var a=this,r=t.seriesTaskMap,o=t.seriesTaskMap=Co(),s=e.seriesType,l=e.getTargetSeries;function p(t){var s=t.uid,l=o.set(s,r&&r.get(s)||hb({plan:Qw,reset:Jw,count:nS}));l.context={model:t,ecModel:n,api:i,useClearVisual:e.isVisual&&!e.isLayout,plan:e.plan,reset:e.reset,scheduler:a},a._pipe(t,l)}e.createOnAllSeries?n.eachRawSeries(p):s?n.eachRawSeriesByType(s,p):l&&l(n,i).each(p)},e.prototype._createOverallStageTask=function(e,t,n,i){var a=this,r=t.overallTask=t.overallTask||hb({reset:Kw});r.context={ecModel:n,api:i,overallReset:e.overallReset,scheduler:a};var o=r.agentStubMap,s=r.agentStubMap=Co(),l=e.seriesType,p=e.getTargetSeries,c=!0,d=!1,u="";function m(e){var t=e.uid,n=s.set(t,o&&o.get(t)||(d=!0,hb({reset:Yw,onDirty:Zw})));n.context={model:e,overallProgress:c},n.agent=r,n.__block=c,a._pipe(e,n)}go(!e.createOnAllSeries,u),l?n.eachRawSeriesByType(l,m):p?p(n,i).each(m):(c=!1,zr(n.getSeries(),m)),d&&r.dirty()},e.prototype._pipe=function(e,t){var n=e.uid,i=this._pipelineMap.get(n);!i.head&&(i.head=t),i.tail&&i.tail.pipe(t),i.tail=t,t.__idxInPipeline=i.count++,t.__pipeline=i},e.wrapStageHandler=function(e,t){return Zr(e)&&(e={overallReset:e,seriesType:iS(e)}),e.uid=wy("stageHandler"),t&&(e.visualType=t),e},e}();function Kw(e){e.overallReset(e.ecModel,e.api,e.payload)}function Yw(e){return e.overallProgress&&Xw}function Xw(){this.agent.dirty(),this.getDownstream().dirty()}function Zw(){this.agent&&this.agent.dirty()}function Qw(e){return e.plan?e.plan(e.model,e.ecModel,e.api,e.payload):null}function Jw(e){e.useClearVisual&&e.data.clearAllVisual();var t=e.resetDefines=vd(e.reset(e.model,e.ecModel,e.api,e.payload));return t.length>1?Ur(t,(function(e,t){return tS(t)})):eS}var eS=tS(0);function tS(e){return function(t,n){var i=n.data,a=n.resetDefines[e];if(a&&a.dataEach)for(var r=t.start;r0&&c===a.length-p.length){var d=a.slice(0,c);"data"!==d&&(t.mainType=d,t[p.toLowerCase()]=e,s=!0)}}o.hasOwnProperty(a)&&(n[a]=e,s=!0),s||(i[a]=e)}))}return{cptQuery:t,dataQuery:n,otherQuery:i}},e.prototype.filter=function(e,t){var n=this.eventInfo;if(!n)return!0;var i=n.targetEl,a=n.packedEvent,r=n.model,o=n.view;if(!r||!o)return!0;var s=t.cptQuery,l=t.dataQuery;return p(s,r,"mainType")&&p(s,r,"subType")&&p(s,r,"index","componentIndex")&&p(s,r,"name")&&p(s,r,"id")&&p(l,a,"name")&&p(l,a,"dataIndex")&&p(l,a,"dataType")&&(!o.filterForExposedEvent||o.filterForExposedEvent(e,t.otherQuery,i,a));function p(e,t,n,i){return null==e[n]||t[i||n]===e[n]}},e.prototype.afterTrigger=function(){this.eventInfo=null},e}(),fS=["symbol","symbolSize","symbolRotate","symbolOffset"],yS=fS.concat(["symbolKeepAspect"]),vS={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){var n=e.getData();if(e.legendIcon&&n.setVisual("legendIcon",e.legendIcon),e.hasSymbolVisual){for(var i={},a={},r=!1,o=0;o=0&&qS(l)?l:.5,e.createRadialGradient(o,s,0,o,s,l)}(e,t,n):function(e,t,n){var i=null==t.x?0:t.x,a=null==t.x2?1:t.x2,r=null==t.y?0:t.y,o=null==t.y2?0:t.y2;return t.global||(i=i*n.width+n.x,a=a*n.width+n.x,r=r*n.height+n.y,o=o*n.height+n.y),i=qS(i)?i:0,a=qS(a)?a:1,r=qS(r)?r:0,o=qS(o)?o:0,e.createLinearGradient(i,r,a,o)}(e,t,n),a=t.colorStops,r=0;r0&&(t=i.lineDash,n=i.lineWidth,t&&"solid"!==t&&n>0?"dashed"===t?[4*n,2*n]:"dotted"===t?[n]:eo(t)?[t]:Xr(t)?t:null:null),r=i.lineDashOffset;if(a){var o=i.strokeNoScale&&e.getLineScale?e.getLineScale():1;o&&1!==o&&(a=Ur(a,(function(e){return e/o})),r/=o)}return[a,r]}var HS=new nm(!0);function WS(e){var t=e.stroke;return!(null==t||"none"===t||!(e.lineWidth>0))}function $S(e){return"string"==typeof e&&"none"!==e}function KS(e){var t=e.fill;return null!=t&&"none"!==t}function YS(e,t){if(null!=t.fillOpacity&&1!==t.fillOpacity){var n=e.globalAlpha;e.globalAlpha=t.fillOpacity*t.opacity,e.fill(),e.globalAlpha=n}else e.fill()}function XS(e,t){if(null!=t.strokeOpacity&&1!==t.strokeOpacity){var n=e.globalAlpha;e.globalAlpha=t.strokeOpacity*t.opacity,e.stroke(),e.globalAlpha=n}else e.stroke()}function ZS(e,t,n){var i=eu(t.image,t.__image,n);if(nu(i)){var a=e.createPattern(i,t.repeat||"repeat");if("function"==typeof DOMMatrix&&a&&a.setTransform){var r=new DOMMatrix;r.translateSelf(t.x||0,t.y||0),r.rotateSelf(0,0,(t.rotation||0)*ko),r.scaleSelf(t.scaleX||1,t.scaleY||1),a.setTransform(r)}return a}}var QS=["shadowBlur","shadowOffsetX","shadowOffsetY"],JS=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function eC(e,t,n,i,a){var r=!1;if(!i&&t===(n=n||{}))return!1;if(i||t.opacity!==n.opacity){iC(e,a),r=!0;var o=Math.max(Math.min(t.opacity,1),0);e.globalAlpha=isNaN(o)?fu.opacity:o}(i||t.blend!==n.blend)&&(r||(iC(e,a),r=!0),e.globalCompositeOperation=t.blend||fu.blend);for(var s=0;s0&&e.unfinished);e.unfinished||this._zr.flush()}}},t.prototype.getDom=function(){return this._dom},t.prototype.getId=function(){return this.id},t.prototype.getZr=function(){return this._zr},t.prototype.isSSR=function(){return this._ssr},t.prototype.setOption=function(e,t,n){if(!this[bC])if(this._disposed)ZC(this.id);else{var i,a,r;if(to(t)&&(n=t.lazyUpdate,i=t.silent,a=t.replaceMerge,r=t.transition,t=t.notMerge),this[bC]=!0,!this._model||t){var o=new fx(this._api),s=this._theme,l=this._model=new sx;l.scheduler=this._scheduler,l.ssr=this._ssr,l.init(null,null,null,s,this._locale,o)}this._model.setOption(e,{replaceMerge:a},t_);var p={seriesTransition:r,optionChanged:!0};if(n)this[wC]={silent:i,updateParams:p},this[bC]=!1,this.getZr().wakeUp();else{try{MC(this),DC.update.call(this,null,p)}catch(e){throw this[wC]=null,this[bC]=!1,e}this._ssr||this._zr.flush(),this[wC]=null,this[bC]=!1,RC.call(this,i),BC.call(this,i)}}},t.prototype.setTheme=function(){md()},t.prototype.getModel=function(){return this._model},t.prototype.getOption=function(){return this._model&&this._model.getOption()},t.prototype.getWidth=function(){return this._zr.getWidth()},t.prototype.getHeight=function(){return this._zr.getHeight()},t.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||ur.hasGlobalWindow&&window.devicePixelRatio||1},t.prototype.getRenderedCanvas=function(e){return this.renderToCanvas(e)},t.prototype.renderToCanvas=function(e){e=e||{};var t=this._zr.painter;return t.getRenderedCanvas({backgroundColor:e.backgroundColor||this._model.get("backgroundColor"),pixelRatio:e.pixelRatio||this.getDevicePixelRatio()})},t.prototype.renderToSVGString=function(e){e=e||{};var t=this._zr.painter;return t.renderToString({useViewBox:e.useViewBox})},t.prototype.getSvgDataURL=function(){if(ur.svgSupported){var e=this._zr;return zr(e.storage.getDisplayList(),(function(e){e.stopAnimation(null,!0)})),e.painter.toDataURL()}},t.prototype.getDataURL=function(e){if(!this._disposed){var t=(e=e||{}).excludeComponents,n=this._model,i=[],a=this;zr(t,(function(e){n.eachComponent({mainType:e},(function(e){var t=a._componentsMap[e.__viewId];t.group.ignore||(i.push(t),t.group.ignore=!0)}))}));var r="svg"===this._zr.painter.getType()?this.getSvgDataURL():this.renderToCanvas(e).toDataURL("image/"+(e&&e.type||"png"));return zr(i,(function(e){e.group.ignore=!1})),r}ZC(this.id)},t.prototype.getConnectedDataURL=function(e){if(!this._disposed){var t="svg"===e.type,n=this.group,i=Math.min,a=Math.max,r=1/0;if(o_[n]){var o=r,s=r,l=-1/0,p=-1/0,c=[],d=e&&e.pixelRatio||this.getDevicePixelRatio();zr(r_,(function(r,d){if(r.group===n){var u=t?r.getZr().painter.getSvgDom().innerHTML:r.renderToCanvas(Ar(e)),m=r.getDom().getBoundingClientRect();o=i(m.left,o),s=i(m.top,s),l=a(m.right,l),p=a(m.bottom,p),c.push({dom:u,left:m.left,top:m.top})}}));var u=(l*=d)-(o*=d),m=(p*=d)-(s*=d),h=vr.createCanvas(),g=qc(h,{renderer:t?"svg":"canvas"});if(g.resize({width:u,height:m}),t){var f="";return zr(c,(function(e){var t=e.left-o,n=e.top-s;f+=''+e.dom+""})),g.painter.getSvgRoot().innerHTML=f,e.connectedBackgroundColor&&g.painter.setBackgroundColor(e.connectedBackgroundColor),g.refreshImmediately(),g.painter.toDataURL()}return e.connectedBackgroundColor&&g.add(new Fm({shape:{x:0,y:0,width:u,height:m},style:{fill:e.connectedBackgroundColor}})),zr(c,(function(e){var t=new Em({style:{x:e.left*d-o,y:e.top*d-s,image:e.dom}});g.add(t)})),g.refreshImmediately(),h.toDataURL("image/"+(e&&e.type||"png"))}return this.getDataURL(e)}ZC(this.id)},t.prototype.convertToPixel=function(e,t){return OC(this,"convertToPixel",e,t)},t.prototype.convertFromPixel=function(e,t){return OC(this,"convertFromPixel",e,t)},t.prototype.containPixel=function(e,t){var n;if(!this._disposed)return zr(Od(this._model,e),(function(e,i){i.indexOf("Models")>=0&&zr(e,(function(e){var a=e.coordinateSystem;if(a&&a.containPoint)n=n||!!a.containPoint(t);else if("seriesModels"===i){var r=this._chartsMap[e.__viewId];r&&r.containPoint&&(n=n||r.containPoint(t,e))}else 0}),this)}),this),!!n;ZC(this.id)},t.prototype.getVisual=function(e,t){var n=Od(this._model,e,{defaultMainType:"series"}),i=n.seriesModel;var a=i.getData(),r=n.hasOwnProperty("dataIndexInside")?n.dataIndexInside:n.hasOwnProperty("dataIndex")?a.indexOfRawIndex(n.dataIndex):null;return null!=r?bS(a,r,t):wS(a,t)},t.prototype.getViewOfComponentModel=function(e){return this._componentsMap[e.__viewId]},t.prototype.getViewOfSeriesModel=function(e){return this._chartsMap[e.__viewId]},t.prototype._initEvents=function(){var e,t,n,i=this;zr(XC,(function(e){var t=function(t){var n,a=i.getModel(),r=t.target,o="globalout"===e;if(o?n={}:r&&TS(r,(function(e){var t=Xm(e);if(t&&null!=t.dataIndex){var i=t.dataModel||a.getSeriesByIndex(t.seriesIndex);return n=i&&i.getDataParams(t.dataIndex,t.dataType,r)||{},!0}if(t.eventData)return n=Br({},t.eventData),!0}),!0),n){var s=n.componentType,l=n.componentIndex;"markLine"!==s&&"markPoint"!==s&&"markArea"!==s||(s="series",l=n.seriesIndex);var p=s&&null!=l&&a.getComponent(s,l),c=p&&i["series"===p.mainType?"_chartsMap":"_componentsMap"][p.__viewId];0,n.event=t,n.type=e,i._$eventProcessor.eventInfo={targetEl:r,packedEvent:n,model:p,view:c},i.trigger(e,n)}};t.zrEventfulCallAtLast=!0,i._zr.on(e,t,i)})),zr(JC,(function(e,t){i._messageCenter.on(t,(function(e){this.trigger(t,e)}),i)})),zr(["selectchanged"],(function(e){i._messageCenter.on(e,(function(t){this.trigger(e,t)}),i)})),e=this._messageCenter,t=this,n=this._api,e.on("selectchanged",(function(e){var i=n.getModel();e.isFromClick?(_S("map","selectchanged",t,i,e),_S("pie","selectchanged",t,i,e)):"select"===e.fromAction?(_S("map","selected",t,i,e),_S("pie","selected",t,i,e)):"unselect"===e.fromAction&&(_S("map","unselected",t,i,e),_S("pie","unselected",t,i,e))}))},t.prototype.isDisposed=function(){return this._disposed},t.prototype.clear=function(){this._disposed?ZC(this.id):this.setOption({series:[]},!0)},t.prototype.dispose=function(){if(this._disposed)ZC(this.id);else{this._disposed=!0,this.getDom()&&Nd(this.getDom(),l_,"");var e=this,t=e._api,n=e._model;zr(e._componentsViews,(function(e){e.dispose(n,t)})),zr(e._chartsViews,(function(e){e.dispose(n,t)})),e._zr.dispose(),e._dom=e._model=e._chartsMap=e._componentsMap=e._chartsViews=e._componentsViews=e._scheduler=e._api=e._zr=e._throttledZrFlush=e._theme=e._coordSysMgr=e._messageCenter=null,delete r_[e.id]}},t.prototype.resize=function(e){if(!this[bC])if(this._disposed)ZC(this.id);else{this._zr.resize(e);var t=this._model;if(this._loadingFX&&this._loadingFX.resize(),t){var n=t.resetOption("media"),i=e&&e.silent;this[wC]&&(null==i&&(i=this[wC].silent),n=!0,this[wC]=null),this[bC]=!0;try{n&&MC(this),DC.update.call(this,{type:"resize",animation:Br({duration:0},e&&e.animation)})}catch(e){throw this[bC]=!1,e}this[bC]=!1,RC.call(this,i),BC.call(this,i)}}},t.prototype.showLoading=function(e,t){if(this._disposed)ZC(this.id);else if(to(e)&&(t=e,e=""),e=e||"default",this.hideLoading(),a_[e]){var n=a_[e](this._api,t),i=this._zr;this._loadingFX=n,i.add(n)}},t.prototype.hideLoading=function(){this._disposed?ZC(this.id):(this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null)},t.prototype.makeActionFromEvent=function(e){var t=Br({},e);return t.type=JC[e.type],t},t.prototype.dispatchAction=function(e,t){if(this._disposed)ZC(this.id);else if(to(t)||(t={silent:!!t}),QC[e.type]&&this._model)if(this[bC])this._pendingActions.push(e);else{var n=t.silent;FC.call(this,e,n);var i=t.flush;i?this._zr.flush():!1!==i&&ur.browser.weChat&&this._throttledZrFlush(),RC.call(this,n),BC.call(this,n)}},t.prototype.updateLabelLayout=function(){hC.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},t.prototype.appendData=function(e){if(this._disposed)ZC(this.id);else{var t=e.seriesIndex,n=this.getModel().getSeriesByIndex(t);0,n.appendData(e),this._scheduler.unfinished=!0,this.getZr().wakeUp()}},t.internalField=function(){function e(e){e.clearColorPalette(),e.eachSeries((function(e){e.clearColorPalette()}))}function t(e){for(var t=[],n=e.currentStates,i=0;i0?{duration:r,delay:i.get("delay"),easing:i.get("easing")}:null;n.eachRendered((function(e){if(e.states&&e.states.emphasis){if(df(e))return;if(e instanceof Sm&&function(e){var t=eh(e);t.normalFill=e.style.fill,t.normalStroke=e.style.stroke;var n=e.states.select||{};t.selectFill=n.style&&n.style.fill||null,t.selectStroke=n.style&&n.style.stroke||null}(e),e.__dirty){var n=e.prevStates;n&&e.useStates(n)}if(a){e.stateTransition=o;var i=e.getTextContent(),r=e.getTextGuideLine();i&&(i.stateTransition=o),r&&(r.stateTransition=o)}e.__dirty&&t(e)}}))}MC=function(e){var t=e._scheduler;t.restorePipelines(e._model),t.prepareStageTasks(),kC(e,!0),kC(e,!1),t.plan()},kC=function(e,t){for(var n=e._model,i=e._scheduler,a=t?e._componentsViews:e._chartsViews,r=t?e._componentsMap:e._chartsMap,o=e._zr,s=e._api,l=0;lt.get("hoverLayerThreshold")&&!ur.node&&!ur.worker&&t.eachSeries((function(t){if(!t.preventUsingHoverLayer){var n=e._chartsMap[t.__viewId];n.__alive&&n.eachRendered((function(e){e.states.emphasis&&(e.states.emphasis.hoverLayer=!0)}))}}))}(e,t),hC.trigger("series:afterupdate",t,i,s)},jC=function(e){e[SC]=!0,e.getZr().wakeUp()},HC=function(e){e[SC]&&(e.getZr().storage.traverse((function(e){df(e)||t(e)})),e[SC]=!1)},zC=function(e){return new(function(t){function n(){return null!==t&&t.apply(this,arguments)||this}return We(n,t),n.prototype.getCoordinateSystems=function(){return e._coordSysMgr.getCoordinateSystems()},n.prototype.getComponentByElement=function(t){for(;t;){var n=t.__ecComponentInfo;if(null!=n)return e._model.getComponent(n.mainType,n.index);t=t.parent}},n.prototype.enterEmphasis=function(t,n){Th(t,n),jC(e)},n.prototype.leaveEmphasis=function(t,n){Ih(t,n),jC(e)},n.prototype.enterBlur=function(t){Eh(t),jC(e)},n.prototype.leaveBlur=function(t){Mh(t),jC(e)},n.prototype.enterSelect=function(t){kh(t),jC(e)},n.prototype.leaveSelect=function(t){Ph(t),jC(e)},n.prototype.getModel=function(){return e.getModel()},n.prototype.getViewOfComponentModel=function(t){return e.getViewOfComponentModel(t)},n.prototype.getViewOfSeriesModel=function(t){return e.getViewOfSeriesModel(t)},n}(ux))(e)},UC=function(e){function t(e,t){for(var n=0;n=0)){f_.push(n);var r=$w.wrapStageHandler(n,a);r.__prio=t,r.__raw=n,e.push(r)}}function v_(e,t){a_[e]=t}var x_=function(e){var t=(e=Ar(e)).type,n="";t||hd(n);var i=t.split(":");2!==i.length&&hd(n);var a=!1;"echarts"===i[0]&&(t=i[1],a=!0),e.__isBuiltIn=a,Pb.set(t,e)};function b_(e){return null==e?0:e.length||1}function w_(e){return e}g_(yC,zw),g_(vC,jw),g_(vC,Hw),g_(yC,vS),g_(vC,xS),g_(7e3,(function(e,t){e.eachRawSeries((function(n){if(!e.isSeriesFiltered(n)){var i=n.getData();i.hasItemVisual()&&i.each((function(e){var n=i.getItemVisual(e,"decal");n&&(i.ensureUniqueItemVisual(e,"style").decal=cC(n,t))}));var a=i.getVisual("decal");if(a)i.getVisual("style").decal=cC(a,t)}}))})),d_(Lx),u_(900,(function(e){var t=Co();e.eachSeries((function(e){var n=e.get("stack");if(n){var i=t.get(n)||t.set(n,[]),a=e.getData(),r={stackResultDimension:a.getCalculationInfo("stackResultDimension"),stackedOverDimension:a.getCalculationInfo("stackedOverDimension"),stackedDimension:a.getCalculationInfo("stackedDimension"),stackedByDimension:a.getCalculationInfo("stackedByDimension"),isStackedByIndex:a.getCalculationInfo("isStackedByIndex"),data:a,seriesModel:e};if(!r.stackedDimension||!r.isStackedByIndex&&!r.stackedByDimension)return;i.length&&a.setCalculationInfo("stackedOnSeries",i[i.length-1].seriesModel),i.push(r)}})),t.each(Vx)})),v_("default",(function(e,t){Nr(t=t||{},{text:"loading",textColor:"#000",fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255, 255, 255, 0.8)",showSpinner:!0,color:"#5470c6",spinnerRadius:10,lineWidth:5,zlevel:0});var n=new Rc,i=new Fm({style:{fill:t.maskColor},zlevel:t.zlevel,z:1e4});n.add(i);var a,r=new Nm({style:{text:t.text,fill:t.textColor,fontSize:t.fontSize,fontWeight:t.fontWeight,fontStyle:t.fontStyle,fontFamily:t.fontFamily},zlevel:t.zlevel,z:10001}),o=new Fm({style:{fill:"none"},textContent:r,textConfig:{position:"right",distance:10},zlevel:t.zlevel,z:10001});return n.add(o),t.showSpinner&&((a=new $g({shape:{startAngle:-Ww/2,endAngle:-Ww/2+.1,r:t.spinnerRadius},style:{stroke:t.color,lineCap:"round",lineWidth:t.lineWidth},zlevel:t.zlevel,z:10001})).animateShape(!0).when(1e3,{endAngle:3*Ww/2}).start("circularInOut"),a.animateShape(!0).when(1e3,{startAngle:3*Ww/2}).delay(300).start("circularInOut"),n.add(a)),n.resize=function(){var n=r.getBoundingRect().width,s=t.showSpinner?t.spinnerRadius:0,l=(e.getWidth()-2*s-(t.showSpinner&&n?10:0)-n)/2-(t.showSpinner&&n?0:5+n/2)+(t.showSpinner?0:n/2)+(n?0:s),p=e.getHeight()/2;t.showSpinner&&a.setShape({cx:l,cy:p}),o.setShape({x:l-s,y:p-s,width:2*s,height:2*s}),i.setShape({x:0,y:0,width:e.getWidth(),height:e.getHeight()})},n.resize(),n})),h_({type:rh,event:rh,update:rh},Mo),h_({type:oh,event:oh,update:oh},Mo),h_({type:sh,event:sh,update:sh},Mo),h_({type:lh,event:lh,update:lh},Mo),h_({type:ph,event:ph,update:ph},Mo),c_("light",pS),c_("dark",hS);var S_=function(){function e(e,t,n,i,a,r){this._old=e,this._new=t,this._oldKeyGetter=n||w_,this._newKeyGetter=i||w_,this.context=a,this._diffModeMultiple="multiple"===r}return e.prototype.add=function(e){return this._add=e,this},e.prototype.update=function(e){return this._update=e,this},e.prototype.updateManyToOne=function(e){return this._updateManyToOne=e,this},e.prototype.updateOneToMany=function(e){return this._updateOneToMany=e,this},e.prototype.updateManyToMany=function(e){return this._updateManyToMany=e,this},e.prototype.remove=function(e){return this._remove=e,this},e.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},e.prototype._executeOneToOne=function(){var e=this._old,t=this._new,n={},i=new Array(e.length),a=new Array(t.length);this._initIndexMap(e,null,i,"_oldKeyGetter"),this._initIndexMap(t,n,a,"_newKeyGetter");for(var r=0;r1){var p=s.shift();1===s.length&&(n[o]=s[0]),this._update&&this._update(p,r)}else 1===l?(n[o]=null,this._update&&this._update(s,r)):this._remove&&this._remove(r)}this._performRestAdd(a,n)},e.prototype._executeMultiple=function(){var e=this._old,t=this._new,n={},i={},a=[],r=[];this._initIndexMap(e,n,a,"_oldKeyGetter"),this._initIndexMap(t,i,r,"_newKeyGetter");for(var o=0;o1&&1===d)this._updateManyToOne&&this._updateManyToOne(p,l),i[s]=null;else if(1===c&&d>1)this._updateOneToMany&&this._updateOneToMany(p,l),i[s]=null;else if(1===c&&1===d)this._update&&this._update(p,l),i[s]=null;else if(c>1&&d>1)this._updateManyToMany&&this._updateManyToMany(p,l),i[s]=null;else if(c>1)for(var u=0;u1)for(var o=0;o30}var F_,R_,B_,N_,L_,V_,q_,G_=to,z_=Ur,U_="undefined"==typeof Int32Array?Array:Int32Array,j_=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],H_=["_approximateExtent"],W_=function(){function e(e,t){var n;this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","lttbDownSample"];var i=!1;P_(e)?(n=e.dimensions,this._dimOmitted=e.isDimensionOmitted(),this._schema=e):(i=!0,n=e),n=n||["x","y"];for(var a={},r=[],o={},s=!1,l={},p=0;p=t)){var n=this._store.getProvider();this._updateOrdinalMeta();var i=this._nameList,a=this._idList;if(n.getSource().sourceFormat===Fv&&!n.pure)for(var r=[],o=e;o0},e.prototype.ensureUniqueItemVisual=function(e,t){var n=this._itemVisuals,i=n[e];i||(i=n[e]={});var a=i[t];return null==a&&(Xr(a=this.getVisual(t))?a=a.slice():G_(a)&&(a=Br({},a)),i[t]=a),a},e.prototype.setItemVisual=function(e,t,n){var i=this._itemVisuals[e]||{};this._itemVisuals[e]=i,G_(t)?Br(i,t):i[t]=n},e.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},e.prototype.setLayout=function(e,t){G_(e)?Br(this._layout,e):this._layout[e]=t},e.prototype.getLayout=function(e){return this._layout[e]},e.prototype.getItemLayout=function(e){return this._itemLayouts[e]},e.prototype.setItemLayout=function(e,t,n){this._itemLayouts[e]=n?Br(this._itemLayouts[e]||{},t):t},e.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},e.prototype.setItemGraphicEl=function(e,t){var n=this.hostModel&&this.hostModel.seriesIndex;Zm(n,this.dataType,e,t),this._graphicEls[e]=t},e.prototype.getItemGraphicEl=function(e){return this._graphicEls[e]},e.prototype.eachItemGraphicEl=function(e,t){zr(this._graphicEls,(function(n,i){n&&e&&e.call(t,n,i)}))},e.prototype.cloneShallow=function(t){return t||(t=new e(this._schema?this._schema:z_(this.dimensions,this._getDimInfo,this),this.hostModel)),L_(t,this),t._store=this._store,t},e.prototype.wrapMethod=function(e,t){var n=this[e];Zr(n)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(e),this[e]=function(){var e=n.apply(this,arguments);return t.apply(this,[e].concat(mo(arguments)))})},e.internalField=(F_=function(e){var t=e._invertedIndicesMap;zr(t,(function(n,i){var a=e._dimInfos[i],r=a.ordinalMeta,o=a.stack,s=e._store;if(r||o){if(n=t[i]=o?new Array(s.count()):new U_(r.categories.length),r)for(var l=0;l1&&(s+="__ec__"+p),i[t]=s}})),e}();function $_(e,t){Wx(e)||(e=Kx(e));var n=(t=t||{}).coordDimensions||[],i=t.dimensionsDefine||e.dimensionsDefine||[],a=Co(),r=[],o=function(e,t,n,i){var a=Math.max(e.dimensionsDetectedCount||1,t.length,n.length,i||0);return zr(t,(function(e){var t;to(e)&&(t=e.dimsDef)&&(a=Math.max(a,t.length))})),a}(e,n,i,t.dimensionsCount),s=t.canOmitUnusedDimensions&&A_(o),l=i===e.dimensionsDefine,p=l?O_(e):D_(i),c=t.encodeDefine;!c&&t.encodeDefaulter&&(c=t.encodeDefaulter(e,o));for(var d=Co(c),u=new Nb(o),m=0;m0&&(i.name=a+(r-1)),r++,t.set(a,r)}}(r),new k_({source:e,dimensions:r,fullDimensionCount:o,dimensionOmitted:s})}function K_(e,t,n){if(n||t.hasKey(e)){for(var i=0;t.hasKey(e+i);)i++;e+=i}return t.set(e,!0),e}var Y_=function(e){this.coordSysDims=[],this.axisMap=Co(),this.categoryAxisMap=Co(),this.coordSysName=e};var X_={cartesian2d:function(e,t,n,i){var a=e.getReferringComponents("xAxis",Fd).models[0],r=e.getReferringComponents("yAxis",Fd).models[0];t.coordSysDims=["x","y"],n.set("x",a),n.set("y",r),Z_(a)&&(i.set("x",a),t.firstCategoryDimIndex=0),Z_(r)&&(i.set("y",r),null==t.firstCategoryDimIndex&&(t.firstCategoryDimIndex=1))},singleAxis:function(e,t,n,i){var a=e.getReferringComponents("singleAxis",Fd).models[0];t.coordSysDims=["single"],n.set("single",a),Z_(a)&&(i.set("single",a),t.firstCategoryDimIndex=0)},polar:function(e,t,n,i){var a=e.getReferringComponents("polar",Fd).models[0],r=a.findAxisModel("radiusAxis"),o=a.findAxisModel("angleAxis");t.coordSysDims=["radius","angle"],n.set("radius",r),n.set("angle",o),Z_(r)&&(i.set("radius",r),t.firstCategoryDimIndex=0),Z_(o)&&(i.set("angle",o),null==t.firstCategoryDimIndex&&(t.firstCategoryDimIndex=1))},geo:function(e,t,n,i){t.coordSysDims=["lng","lat"]},parallel:function(e,t,n,i){var a=e.ecModel,r=a.getComponent("parallel",e.get("parallelIndex")),o=t.coordSysDims=r.dimensions.slice();zr(r.parallelAxisIndex,(function(e,r){var s=a.getComponent("parallelAxis",e),l=o[r];n.set(l,s),Z_(s)&&(i.set(l,s),null==t.firstCategoryDimIndex&&(t.firstCategoryDimIndex=r))}))}};function Z_(e){return"category"===e.get("type")}function Q_(e,t,n){var i,a,r,o=(n=n||{}).byIndex,s=n.stackedCoordDimension;!function(e){return!P_(e.schema)}(t)?(a=t.schema,i=a.dimensions,r=t.store):i=t;var l,p,c,d,u=!(!e||!e.get("stack"));if(zr(i,(function(e,t){Qr(e)&&(i[t]=e={name:e}),u&&!e.isExtraCoord&&(o||l||!e.ordinalMeta&&!e.stack||(l=e),p||"ordinal"===e.type||"time"===e.type||s&&s!==e.coordDim||(p=e))})),!p||o||l||(o=!0),p){c="__\0ecstackresult_"+e.id,d="__\0ecstackedover_"+e.id,l&&(l.createInvertedIndices=!0);var m=p.coordDim,h=p.type,g=0;zr(i,(function(e){e.coordDim===m&&g++}));var f={name:c,coordDim:m,coordDimIndex:g,type:h,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},y={name:d,coordDim:d,coordDimIndex:g+1,type:h,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};a?(r&&(f.storeDimIndex=r.ensureCalculationDimension(d,h),y.storeDimIndex=r.ensureCalculationDimension(c,h)),a.appendCalculationDimension(f),a.appendCalculationDimension(y)):(i.push(f),i.push(y))}return{stackedDimension:p&&p.name,stackedByDimension:l&&l.name,isStackedByIndex:o,stackedOverDimension:d,stackResultDimension:c}}function J_(e,t){return!!t&&t===e.getCalculationInfo("stackedDimension")}function eT(e,t){return J_(e,t)?e.getCalculationInfo("stackResultDimension"):t}function tT(e,t,n){n=n||{};var i,a=t.getSourceManager(),r=!1;e?(r=!0,i=Kx(e)):r=(i=a.getSource()).sourceFormat===Fv;var o=function(e){var t=e.get("coordinateSystem"),n=new Y_(t),i=X_[t];if(i)return i(e,n,n.axisMap,n.categoryAxisMap),n}(t),s=function(e,t){var n,i=e.get("coordinateSystem"),a=hx.get(i);return t&&t.coordSysDims&&(n=Ur(t.coordSysDims,(function(e){var n={name:e},i=t.axisMap.get(e);if(i){var a=i.get("type");n.type=T_(a)}return n}))),n||(n=a&&(a.getDimensionsInfo?a.getDimensionsInfo():a.dimensions.slice())||["x","y"]),n}(t,o),l=n.useEncodeDefaulter,p=Zr(l)?l:l?Yr(Wv,s,t):null,c=$_(i,{coordDimensions:s,generateCoord:n.generateCoord,encodeDefine:t.getEncode(),encodeDefaulter:p,canOmitUnusedDimensions:!r}),d=function(e,t,n){var i,a;return n&&zr(e,(function(e,r){var o=e.coordDim,s=n.categoryAxisMap.get(o);s&&(null==i&&(i=r),e.ordinalMeta=s.getOrdinalMeta(),t&&(e.createInvertedIndices=!0)),null!=e.otherDims.itemName&&(a=!0)})),a||null==i||(e[i].otherDims.itemName=0),i}(c.dimensions,n.createInvertedIndices,o),u=r?null:a.getSharedDataStore(c),m=Q_(t,{schema:c,store:u}),h=new W_(c,t);h.setCalculationInfo(m);var g=null!=d&&function(e){if(e.sourceFormat===Fv){var t=function(e){var t=0;for(;tt[1]&&(t[1]=e[1])},e.prototype.unionExtentFromData=function(e,t){this.unionExtent(e.getApproximateExtent(t))},e.prototype.getExtent=function(){return this._extent.slice()},e.prototype.setExtent=function(e,t){var n=this._extent;isNaN(e)||(n[0]=e),isNaN(t)||(n[1]=t)},e.prototype.isInExtentRange=function(e){return this._extent[0]<=e&&this._extent[1]>=e},e.prototype.isBlank=function(){return this._isBlank},e.prototype.setBlank=function(e){this._isBlank=e},e}();Kd(nT);var iT=0,aT=function(){function e(e){this.categories=e.categories||[],this._needCollect=e.needCollect,this._deduplication=e.deduplication,this.uid=++iT}return e.createByAxisModel=function(t){var n=t.option,i=n.data,a=i&&Ur(i,rT);return new e({categories:a,needCollect:!a,deduplication:!1!==n.dedplication})},e.prototype.getOrdinal=function(e){return this._getOrCreateMap().get(e)},e.prototype.parseAndCollect=function(e){var t,n=this._needCollect;if(!Qr(e)&&!n)return e;if(n&&!this._deduplication)return t=this.categories.length,this.categories[t]=e,t;var i=this._getOrCreateMap();return null==(t=i.get(e))&&(n?(t=this.categories.length,this.categories[t]=e,i.set(e,t)):t=NaN),t},e.prototype._getOrCreateMap=function(){return this._map||(this._map=Co(this.categories))},e}();function rT(e){return to(e)&&null!=e.value?e.value:e+""}function oT(e){return"interval"===e.type||"log"===e.type}function sT(e,t,n,i){var a={},r=e[1]-e[0],o=a.interval=od(r/t,!0);null!=n&&oi&&(o=a.interval=i);var s=a.intervalPrecision=pT(o);return function(e,t){!isFinite(e[0])&&(e[0]=t[0]),!isFinite(e[1])&&(e[1]=t[1]),cT(e,0,t),cT(e,1,t),e[0]>e[1]&&(e[0]=e[1])}(a.niceTickExtent=[$c(Math.ceil(e[0]/o)*o,s),$c(Math.floor(e[1]/o)*o,s)],e),a}function lT(e){var t=Math.pow(10,rd(e)),n=e/t;return n?2===n?n=3:3===n?n=5:n*=2:n=1,$c(n*t)}function pT(e){return Yc(e)+2}function cT(e,t,n){e[t]=Math.max(Math.min(e[t],n[1]),n[0])}function dT(e,t){return e>=t[0]&&e<=t[1]}function uT(e,t){return t[1]===t[0]?.5:(e-t[0])/(t[1]-t[0])}function mT(e,t){return e*(t[1]-t[0])+t[0]}var hT=function(e){function t(t){var n=e.call(this,t)||this;n.type="ordinal";var i=n.getSetting("ordinalMeta");return i||(i=new aT({})),Xr(i)&&(i=new aT({categories:Ur(i,(function(e){return to(e)?e.value:e}))})),n._ordinalMeta=i,n._extent=n.getSetting("extent")||[0,i.categories.length-1],n}return We(t,e),t.prototype.parse=function(e){return null==e?NaN:Qr(e)?this._ordinalMeta.getOrdinal(e):Math.round(e)},t.prototype.contain=function(e){return dT(e=this.parse(e),this._extent)&&null!=this._ordinalMeta.categories[e]},t.prototype.normalize=function(e){return uT(e=this._getTickNumber(this.parse(e)),this._extent)},t.prototype.scale=function(e){return e=Math.round(mT(e,this._extent)),this.getRawOrdinalNumber(e)},t.prototype.getTicks=function(){for(var e=[],t=this._extent,n=t[0];n<=t[1];)e.push({value:n}),n++;return e},t.prototype.getMinorTicks=function(e){},t.prototype.setSortInfo=function(e){if(null!=e){for(var t=e.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],a=0,r=this._ordinalMeta.categories.length,o=Math.min(r,t.length);a=0&&e=0&&e=e},t.prototype.getOrdinalMeta=function(){return this._ordinalMeta},t.prototype.calcNiceTicks=function(){},t.prototype.calcNiceExtent=function(){},t.type="ordinal",t}(nT);nT.registerClass(hT);var gT=$c,fT=function(e){function t(t){var n=e.call(this,t)||this;n.type="interval",n._interval=0,n._intervalPrecision=2;var i=n.getSetting("ticksGenerator");return Zr(i)&&(n._ticksGenerator=i),n}return We(t,e),t.prototype.parse=function(e){return e},t.prototype.contain=function(e){return dT(e,this._extent)},t.prototype.normalize=function(e){return uT(e,this._extent)},t.prototype.scale=function(e){return mT(e,this._extent)},t.prototype.setExtent=function(e,t){var n=this._extent;isNaN(e)||(n[0]=parseFloat(e)),isNaN(t)||(n[1]=parseFloat(t))},t.prototype.unionExtent=function(e){var t=this._extent;e[0]t[1]&&(t[1]=e[1]),this.setExtent(t[0],t[1])},t.prototype.getInterval=function(){return this._interval},t.prototype.setInterval=function(e){this._interval=e,this._niceExtent=this._extent.slice(),this._intervalPrecision=pT(e)},t.prototype.getTicks=function(e){var t,n=this._interval,i=this._extent,a=this._niceExtent,r=this._intervalPrecision,o=this._ticksGenerator;if(o)try{if(t=o(i,n,a,r))return t}catch(e){}if(t=[],!n)return t;i[0]1e4)return[];var l=t.length?t[t.length-1].value:a[1];return i[1]>l&&(e?t.push({value:gT(l+n,r)}):t.push({value:i[1]})),t},t.prototype.getMinorTicks=function(e){for(var t=this.getTicks(!0),n=[],i=this.getExtent(),a=1;ai[0]&&c0&&(r=null===r?s:Math.min(r,s))}n[i]=r}}return n}(e),n=[];return zr(e,(function(e){var i,a=e.coordinateSystem.getBaseAxis(),r=a.getExtent();if("category"===a.type)i=a.getBandWidth();else if("value"===a.type||"time"===a.type){var o=a.dim+"_"+a.index,s=t[o],l=Math.abs(r[1]-r[0]),p=a.scale.getExtent(),c=Math.abs(p[1]-p[0]);i=s?l/c*s:l}else{var d=e.getData();i=Math.abs(r[1]-r[0])/d.count()}var u=Wc(e.get("barWidth"),i),m=Wc(e.get("barMaxWidth"),i),h=Wc(e.get("barMinWidth")||(kT(e)?.5:1),i),g=e.get("barGap"),f=e.get("barCategoryGap");n.push({bandWidth:i,barWidth:u,barMaxWidth:m,barMinWidth:h,barGap:g,barCategoryGap:f,axisKey:ST(a),stackId:wT(e)})})),TT(n)}function TT(e){var t={};zr(e,(function(e,n){var i=e.axisKey,a=e.bandWidth,r=t[i]||{bandWidth:a,remainedWidth:a,autoWidthCount:0,categoryGap:null,gap:"20%",stacks:{}},o=r.stacks;t[i]=r;var s=e.stackId;o[s]||r.autoWidthCount++,o[s]=o[s]||{width:0,maxWidth:0};var l=e.barWidth;l&&!o[s].width&&(o[s].width=l,l=Math.min(r.remainedWidth,l),r.remainedWidth-=l);var p=e.barMaxWidth;p&&(o[s].maxWidth=p);var c=e.barMinWidth;c&&(o[s].minWidth=c);var d=e.barGap;null!=d&&(r.gap=d);var u=e.barCategoryGap;null!=u&&(r.categoryGap=u)}));var n={};return zr(t,(function(e,t){n[t]={};var i=e.stacks,a=e.bandWidth,r=e.categoryGap;if(null==r){var o=$r(i).length;r=Math.max(35-4*o,15)+"%"}var s=Wc(r,a),l=Wc(e.gap,1),p=e.remainedWidth,c=e.autoWidthCount,d=(p-s)/(c+(c-1)*l);d=Math.max(d,0),zr(i,(function(e){var t=e.maxWidth,n=e.minWidth;if(e.width){i=e.width;t&&(i=Math.min(i,t)),n&&(i=Math.max(i,n)),e.width=i,p-=i+l*i,c--}else{var i=d;t&&ti&&(i=n),i!==d&&(e.width=i,p-=i+l*i,c--)}})),d=(p-s)/(c+(c-1)*l),d=Math.max(d,0);var u,m=0;zr(i,(function(e,t){e.width||(e.width=d),u=e,m+=e.width*(1+l)})),u&&(m-=u.width*l);var h=-m/2;zr(i,(function(e,i){n[t][i]=n[t][i]||{bandWidth:a,offset:h,width:e.width},h+=e.width*(1+l)}))})),n}function IT(e,t){var n=CT(e,t),i=_T(n);zr(n,(function(e){var t=e.getData(),n=e.coordinateSystem.getBaseAxis(),a=wT(e),r=i[ST(n)][a],o=r.offset,s=r.width;t.setLayout({bandWidth:r.bandWidth,offset:o,size:s})}))}function ET(e){return{seriesType:e,plan:Sw(),reset:function(e){if(MT(e)){var t=e.getData(),n=e.coordinateSystem,i=n.getBaseAxis(),a=n.getOtherAxis(i),r=t.getDimensionIndex(t.mapDimension(a.dim)),o=t.getDimensionIndex(t.mapDimension(i.dim)),s=e.get("showBackground",!0),l=t.mapDimension(a.dim),p=t.getCalculationInfo("stackResultDimension"),c=J_(t,l)&&!!t.getCalculationInfo("stackedOnSeries"),d=a.isHorizontal(),u=function(e,t){return t.toGlobalCoord(t.dataToCoord("log"===t.type?1:0))}(0,a),m=kT(e),h=e.get("barMinHeight")||0,g=p&&t.getDimensionIndex(p),f=t.getLayout("size"),y=t.getLayout("offset");return{progress:function(e,t){for(var i,a=e.count,l=m&&xT(3*a),p=m&&s&&xT(3*a),v=m&&xT(a),x=n.master.getRect(),b=d?x.width:x.height,w=t.getStore(),S=0;null!=(i=e.next());){var C=w.get(c?g:r,i),_=w.get(o,i),T=u,I=void 0;c&&(I=+C-w.get(r,i));var E=void 0,M=void 0,k=void 0,P=void 0;if(d){var D=n.dataToPoint([C,_]);if(c)T=n.dataToPoint([I,_])[0];E=T,M=D[1]+y,k=D[0]-T,P=f,Math.abs(k)0)for(var s=0;s=0;--s)if(l[p]){r=l[p];break}r=r||o.none}if(Xr(r)){var c=null==e.level?0:e.level>=0?e.level:r.length+e.level;r=r[c=Math.min(c,r.length-1)]}}return jy(new Date(e.value),r,a,i)}(e,t,n,this.getSetting("locale"),i)},t.prototype.getTicks=function(){var e=this._interval,t=this._extent,n=[];if(!e)return n;n.push({value:t[0],level:0});var i=this.getSetting("useUTC"),a=function(e,t,n,i){var a=1e4,r=qy,o=0;function s(e,t,n,a,r,o,s){for(var l=new Date(t),p=t,c=l[a]();p1&&0===p&&r.unshift({value:r[0].value-u})}}for(p=0;p=i[0]&&y<=i[1]&&d++)}var v=(i[1]-i[0])/t;if(d>1.5*v&&u>v/1.5)break;if(p.push(g),d>v||e===r[m])break}c=[]}}0;var x=Hr(Ur(p,(function(e){return Hr(e,(function(e){return e.value>=i[0]&&e.value<=i[1]&&!e.notAdd}))})),(function(e){return e.length>0})),b=[],w=x.length-1;for(m=0;mn&&(this._approxInterval=n);var r=DT.length,o=Math.min(function(e,t,n,i){for(;n>>1;e[a][1]16?16:e>7.5?7:e>3.5?4:e>1.5?2:1}function AT(e){return(e/=2592e6)>6?6:e>3?3:e>2?2:1}function FT(e){return(e/=Ay)>12?12:e>6?6:e>3.5?4:e>2?2:1}function RT(e,t){return(e/=t?Oy:Dy)>30?30:e>20?20:e>15?15:e>10?10:e>5?5:e>2?2:1}function BT(e){return od(e,!0)}function NT(e,t,n){var i=new Date(e);switch(zy(t)){case"year":case"month":i[tv(n)](0);case"day":i[nv(n)](1);case"hour":i[iv(n)](0);case"minute":i[av(n)](0);case"second":i[rv(n)](0),i[ov(n)](0)}return i.getTime()}nT.registerClass(PT);var LT=nT.prototype,VT=fT.prototype,qT=$c,GT=Math.floor,zT=Math.ceil,UT=Math.pow,jT=Math.log,HT=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type="log",t.base=10,t._originalScale=new fT,t._interval=0,t}return We(t,e),t.prototype.getTicks=function(e){var t=this._originalScale,n=this._extent,i=t.getExtent();return Ur(VT.getTicks.call(this,e),(function(e){var t=e.value,a=$c(UT(this.base,t));return a=t===n[0]&&this._fixMin?$T(a,i[0]):a,{value:a=t===n[1]&&this._fixMax?$T(a,i[1]):a}}),this)},t.prototype.setExtent=function(e,t){var n=jT(this.base);e=jT(Math.max(0,e))/n,t=jT(Math.max(0,t))/n,VT.setExtent.call(this,e,t)},t.prototype.getExtent=function(){var e=this.base,t=LT.getExtent.call(this);t[0]=UT(e,t[0]),t[1]=UT(e,t[1]);var n=this._originalScale.getExtent();return this._fixMin&&(t[0]=$T(t[0],n[0])),this._fixMax&&(t[1]=$T(t[1],n[1])),t},t.prototype.unionExtent=function(e){this._originalScale.unionExtent(e);var t=this.base;e[0]=jT(e[0])/jT(t),e[1]=jT(e[1])/jT(t),LT.unionExtent.call(this,e)},t.prototype.unionExtentFromData=function(e,t){this.unionExtent(e.getApproximateExtent(t))},t.prototype.calcNiceTicks=function(e){e=e||10;var t=this._extent,n=t[1]-t[0];if(!(n===1/0||n<=0)){var i=ad(n);for(e/n*i<=.5&&(i*=10);!isNaN(i)&&Math.abs(i)<1&&Math.abs(i)>0;)i*=10;var a=[$c(zT(t[0]/i)*i),$c(GT(t[1]/i)*i)];this._interval=i,this._niceExtent=a}},t.prototype.calcNiceExtent=function(e){VT.calcNiceExtent.call(this,e),this._fixMin=e.fixMin,this._fixMax=e.fixMax},t.prototype.parse=function(e){return e},t.prototype.contain=function(e){return dT(e=jT(e)/jT(this.base),this._extent)},t.prototype.normalize=function(e){return uT(e=jT(e)/jT(this.base),this._extent)},t.prototype.scale=function(e){return e=mT(e,this._extent),UT(this.base,e)},t.type="log",t}(nT),WT=HT.prototype;function $T(e,t){return qT(e,Yc(t))}WT.getMinorTicks=VT.getMinorTicks,WT.getLabel=VT.getLabel,nT.registerClass(HT);var KT=function(){function e(e,t,n){this._prepareParams(e,t,n)}return e.prototype._prepareParams=function(e,t,n){n[1]0&&s>0&&!l&&(o=0),o<0&&s<0&&!p&&(s=0));var d=this._determinedMin,u=this._determinedMax;return null!=d&&(o=d,l=!0),null!=u&&(s=u,p=!0),{min:o,max:s,minFixed:l,maxFixed:p,isBlank:c}},e.prototype.modifyDataMinMax=function(e,t){this[XT[e]]=t},e.prototype.setDeterminedMinMax=function(e,t){var n=YT[e];this[n]=t},e.prototype.freeze=function(){this.frozen=!0},e}(),YT={min:"_determinedMin",max:"_determinedMax"},XT={min:"_dataMin",max:"_dataMax"};function ZT(e,t,n){var i=e.rawExtentInfo;return i||(i=new KT(e,t,n),e.rawExtentInfo=i,i)}function QT(e,t){return null==t?null:lo(t)?NaN:e.parse(t)}function JT(e,t){var n=e.type,i=ZT(e,t,e.getExtent()).calculate();e.setBlank(i.isBlank);var a=i.min,r=i.max,o=t.ecModel;if(o&&"time"===n){var s=CT("bar",o),l=!1;if(zr(s,(function(e){l=l||e.getBaseAxis()===t.axis})),l){var p=_T(s),c=function(e,t,n,i){var a=n.axis.getExtent(),r=a[1]-a[0],o=function(e,t,n){if(e&&t){var i=e[ST(t)];return null!=i&&null!=n?i[wT(n)]:i}}(i,n.axis);if(void 0===o)return{min:e,max:t};var s=1/0;zr(o,(function(e){s=Math.min(e.offset,s)}));var l=-1/0;zr(o,(function(e){l=Math.max(e.offset+e.width,l)})),s=Math.abs(s),l=Math.abs(l);var p=s+l,c=t-e,d=c/(1-(s+l)/r)-c;return t+=d*(l/p),e-=d*(s/p),{min:e,max:t}}(a,r,t,p);a=c.min,r=c.max}}return{extent:[a,r],fixMin:i.minFixed,fixMax:i.maxFixed}}function eI(e,t){var n=t,i=JT(e,n),a=i.extent,r=n.get("splitNumber");e instanceof HT&&(e.base=n.get("logBase"));var o=e.type,s=n.get("interval"),l="interval"===o||"time"===o;e.setExtent(a[0],a[1]),e.calcNiceExtent({splitNumber:r,fixMin:i.fixMin,fixMax:i.fixMax,minInterval:l?n.get("minInterval"):null,maxInterval:l?n.get("maxInterval"):null}),null!=s&&e.setInterval&&e.setInterval(s)}function tI(e,t){if(t=t||e.get("type"))switch(t){case"category":return new hT({ordinalMeta:e.getOrdinalMeta?e.getOrdinalMeta():e.getCategories(),extent:[1/0,-1/0]});case"time":return new PT({locale:e.ecModel.getLocaleModel(),useUTC:e.ecModel.get("useUTC")});case"value":return new fT({ticksGenerator:e.getTicksGenerator()});default:return new(nT.getClass(t)||fT)}}function nI(e){var t,n,i=e.getLabelModel().get("formatter"),a="category"===e.type?e.scale.getExtent()[0]:null;return"time"===e.scale.type?(n=i,function(t,i){return e.scale.getFormattedLabel(t,i,n)}):Qr(i)?function(t){return function(n){var i=e.scale.getLabel(n);return t.replace("{value}",null!=i?i:"")}}(i):Zr(i)?(t=i,function(n,i){return null!=a&&(i=n.value-a),t(iI(e,n),i,null!=n.level?{level:n.level}:null)}):function(t){return e.scale.getLabel(t)}}function iI(e,t){return"category"===e.type?e.scale.getLabel(t):t.value}function aI(e,t){var n=t*Math.PI/180,i=e.width,a=e.height,r=i*Math.abs(Math.cos(n))+Math.abs(a*Math.sin(n)),o=i*Math.abs(Math.sin(n))+Math.abs(a*Math.cos(n));return new Ns(e.x,e.y,r,o)}function rI(e){var t=e.get("interval");return null==t?"auto":t}function oI(e){return"category"===e.type&&0===rI(e.getLabelModel())}function sI(e,t){var n={};return zr(e.mapDimensionsAll(t),(function(t){n[eT(e,t)]=!0})),$r(n)}var lI=function(){function e(){}return e.prototype.getNeedCrossZero=function(){return!this.option.scale},e.prototype.getCoordSysModel=function(){},e}();var pI=[],cI={registerPreprocessor:d_,registerProcessor:u_,registerPostInit:function(e){m_("afterinit",e)},registerPostUpdate:function(e){m_("afterupdate",e)},registerUpdateLifecycle:m_,registerAction:h_,registerCoordinateSystem:function(e,t){hx.register(e,t)},registerLayout:function(e,t){y_(n_,e,t,1e3,"layout")},registerVisual:g_,registerTransform:x_,registerLoading:v_,registerMap:function(e,t,n){var i=fC("registerMap");i&&i(e,t,n)},registerImpl:function(e,t){gC[e]=t},PRIORITY:xC,ComponentModel:kv,ComponentView:ww,SeriesModel:mw,ChartView:Tw,registerComponentModel:function(e){kv.registerClass(e)},registerComponentView:function(e){ww.registerClass(e)},registerSeriesModel:function(e){mw.registerClass(e)},registerChartView:function(e){Tw.registerClass(e)},registerSubTypeDefaulter:function(e,t){kv.registerSubTypeDefaulter(e,t)},registerPainter:function(e,t){Gc(e,t)}};function dI(e){Xr(e)?zr(e,(function(e){dI(e)})):Lr(pI,e)>=0||(pI.push(e),Zr(e)&&(e={install:e}),e.install(cI))}function uI(e,t){return Math.abs(e-t)<1e-8}function mI(e,t,n){var i=0,a=e[0];if(!a)return!1;for(var r=1;rn&&(e=a,n=o)}if(e)return function(e){for(var t=0,n=0,i=0,a=e.length,r=e[a-1][0],o=e[a-1][1],s=0;s>1^-(1&s),l=l>>1^-(1&l),a=s+=a,r=l+=r,i.push([s/n,l/n])}return i}function _I(e,t){return Ur(Hr((e=function(e){if(!e.UTF8Encoding)return e;var t=e,n=t.UTF8Scale;return null==n&&(n=1024),zr(t.features,(function(e){var t=e.geometry,i=t.encodeOffsets,a=t.coordinates;if(i)switch(t.type){case"LineString":t.coordinates=CI(a,i,n);break;case"Polygon":case"MultiLineString":SI(a,i,n);break;case"MultiPolygon":zr(a,(function(e,t){return SI(e,i[t],n)}))}})),t.UTF8Encoding=!1,t}(e)).features,(function(e){return e.geometry&&e.properties&&e.geometry.coordinates.length>0})),(function(e){var n=e.properties,i=e.geometry,a=[];switch(i.type){case"Polygon":var r=i.coordinates;a.push(new vI(r[0],r.slice(1)));break;case"MultiPolygon":zr(i.coordinates,(function(e){e[0]&&a.push(new vI(e[0],e.slice(1)))}));break;case"LineString":a.push(new xI([i.coordinates]));break;case"MultiLineString":a.push(new xI(i.coordinates))}var o=new bI(n[t||"name"],a,n.cp);return o.properties=n,o}))}var TI=Pd();function II(e){return"category"===e.type?function(e){var t=e.getLabelModel(),n=MI(e,t);return!t.get("show")||e.scale.isBlank()?{labels:[],labelCategoryInterval:n.labelCategoryInterval}:n}(e):function(e){var t=e.scale.getTicks(),n=nI(e);return{labels:Ur(t,(function(t,i){return{level:t.level,formattedLabel:n(t,i),rawLabel:e.scale.getLabel(t),tickValue:t.value}}))}}(e)}function EI(e,t){return"category"===e.type?function(e,t){var n,i,a=kI(e,"ticks"),r=rI(t),o=PI(a,r);if(o)return o;t.get("show")&&!e.scale.isBlank()||(n=[]);if(Zr(r))n=AI(e,r,!0);else if("auto"===r){var s=MI(e,e.getLabelModel());i=s.labelCategoryInterval,n=Ur(s.labels,(function(e){return e.tickValue}))}else n=OI(e,i=r,!0);return DI(a,r,{ticks:n,tickCategoryInterval:i})}(e,t):{ticks:Ur(e.scale.getTicks(),(function(e){return e.value}))}}function MI(e,t){var n,i,a=kI(e,"labels"),r=rI(t),o=PI(a,r);return o||(Zr(r)?n=AI(e,r):(i="auto"===r?function(e){var t=TI(e).autoInterval;return null!=t?t:TI(e).autoInterval=e.calculateCategoryInterval()}(e):r,n=OI(e,i)),DI(a,r,{labels:n,labelCategoryInterval:i}))}function kI(e,t){return TI(e)[t]||(TI(e)[t]=[])}function PI(e,t){for(var n=0;n1&&c/l>2&&(p=Math.round(Math.ceil(p/l)*l));var d=oI(e),u=o.get("showMinLabel")||d,m=o.get("showMaxLabel")||d;u&&p!==r[0]&&g(r[0]);for(var h=p;h<=r[1];h+=l)g(h);function g(e){var t={value:e};s.push(n?e:{formattedLabel:i(t),rawLabel:a.getLabel(t),tickValue:e})}return m&&h-l!==r[1]&&g(r[1]),s}function AI(e,t,n){var i=e.scale,a=nI(e),r=[];return zr(i.getTicks(),(function(e){var o=i.getLabel(e),s=e.value;t(e.value,o)&&r.push(n?s:{formattedLabel:a(e),rawLabel:o,tickValue:s})})),r}var FI=[0,1],RI=function(){function e(e,t,n){this.onBand=!1,this.inverse=!1,this.dim=e,this.scale=t,this._extent=n||[0,0]}return e.prototype.contain=function(e){var t=this._extent,n=Math.min(t[0],t[1]),i=Math.max(t[0],t[1]);return e>=n&&e<=i},e.prototype.containData=function(e){return this.scale.contain(e)},e.prototype.getExtent=function(){return this._extent.slice()},e.prototype.getPixelPrecision=function(e){return Zc(e||this.scale.getExtent(),this._extent)},e.prototype.setExtent=function(e,t){var n=this._extent;n[0]=e,n[1]=t},e.prototype.dataToCoord=function(e,t){var n=this._extent,i=this.scale;return e=i.normalize(e),this.onBand&&"ordinal"===i.type&&BI(n=n.slice(),i.count()),Hc(e,FI,n,t)},e.prototype.coordToData=function(e,t){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&BI(n=n.slice(),i.count());var a=Hc(e,n,FI,t);return this.scale.scale(a)},e.prototype.pointToData=function(e,t){},e.prototype.getTicksCoords=function(e){var t=(e=e||{}).tickModel||this.getTickModel(),n=Ur(EI(this,t).ticks,(function(e){return{coord:this.dataToCoord("ordinal"===this.scale.type?this.scale.getRawOrdinalNumber(e):e),tickValue:e}}),this);return function(e,t,n,i){var a=t.length;if(!e.onBand||n||!a)return;var r,o,s=e.getExtent();if(1===a)t[0].coord=s[0],r=t[1]={coord:s[1]};else{var l=t[a-1].tickValue-t[0].tickValue,p=(t[a-1].coord-t[0].coord)/l;zr(t,(function(e){e.coord-=p/2})),o=1+e.scale.getExtent()[1]-t[a-1].tickValue,r={coord:t[a-1].coord+p*o},t.push(r)}var c=s[0]>s[1];d(t[0].coord,s[0])&&(i?t[0].coord=s[0]:t.shift());i&&d(s[0],t[0].coord)&&t.unshift({coord:s[0]});d(s[1],r.coord)&&(i?r.coord=s[1]:t.pop());i&&d(r.coord,s[1])&&t.push({coord:s[1]});function d(e,t){return e=$c(e),t=$c(t),c?e>t:e0&&e<100||(e=5),Ur(this.scale.getMinorTicks(e),(function(e){return Ur(e,(function(e){return{coord:this.dataToCoord(e),tickValue:e}}),this)}),this)},e.prototype.getViewLabels=function(){return II(this).labels},e.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},e.prototype.getTickModel=function(){return this.model.getModel("axisTick")},e.prototype.getBandWidth=function(){if("time"===this.type){var e=this.model,t=e.get("bandWidthCalculator"),n=void 0;if(Zr(t))try{if(n=t(e))return n}catch(e){}}var i=this._extent,a=this.scale.getExtent(),r=a[1]-a[0]+(this.onBand?1:0);0===r&&(r=1);var o=Math.abs(i[1]-i[0]);return Math.abs(o)/r},e.prototype.calculateCategoryInterval=function(){return function(e){var t=function(e){var t=e.getLabelModel();return{axisRotate:e.getRotate?e.getRotate():e.isHorizontal&&!e.isHorizontal()?90:0,labelRotate:t.get("rotate")||0,font:t.getFont()}}(e),n=nI(e),i=(t.axisRotate-t.labelRotate)/180*Math.PI,a=e.scale,r=a.getExtent(),o=a.count();if(r[1]-r[0]<1)return 0;var s=1;o>40&&(s=Math.max(1,Math.floor(o/40)));for(var l=r[0],p=e.dataToCoord(l+1)-e.dataToCoord(l),c=Math.abs(p*Math.cos(i)),d=Math.abs(p*Math.sin(i)),u=0,m=0;l<=r[1];l+=s){var h,g,f=xc(n({value:l}),t.font,"center","top");h=1.3*f.width,g=1.3*f.height,u=Math.max(u,h,7),m=Math.max(m,g,7)}var y=u/c,v=m/d;isNaN(y)&&(y=1/0),isNaN(v)&&(v=1/0);var x=Math.max(0,Math.floor(Math.min(y,v))),b=TI(e.model),w=e.getExtent(),S=b.lastAutoInterval,C=b.lastTickCount;return null!=S&&null!=C&&Math.abs(S-x)<=1&&Math.abs(C-o)<=1&&S>x&&b.axisExtent0===w[0]&&b.axisExtent1===w[1]?x=S:(b.lastTickCount=o,b.lastAutoInterval=x,b.axisExtent0=w[0],b.axisExtent1=w[1]),x}(this)},e}();function BI(e,t){var n=(e[1]-e[0])/t/2;e[0]+=n,e[1]-=n}var NI=2*Math.PI,LI=nm.CMD,VI=["top","right","bottom","left"];function qI(e,t,n,i,a){var r=n.width,o=n.height;switch(e){case"top":i.set(n.x+r/2,n.y-t),a.set(0,-1);break;case"bottom":i.set(n.x+r/2,n.y+o+t),a.set(0,1);break;case"left":i.set(n.x-t,n.y+o/2),a.set(-1,0);break;case"right":i.set(n.x+r+t,n.y+o/2),a.set(1,0)}}function GI(e,t,n,i,a,r,o,s,l){o-=e,s-=t;var p=Math.sqrt(o*o+s*s),c=(o/=p)*n+e,d=(s/=p)*n+t;if(Math.abs(i-a)%NI<1e-4)return l[0]=c,l[1]=d,p-n;if(r){var u=i;i=sm(a),a=sm(u)}else i=sm(i),a=sm(a);i>a&&(a+=NI);var m=Math.atan2(s,o);if(m<0&&(m+=NI),m>=i&&m<=a||m+NI>=i&&m+NI<=a)return l[0]=c,l[1]=d,p-n;var h=n*Math.cos(i)+e,g=n*Math.sin(i)+t,f=n*Math.cos(a)+e,y=n*Math.sin(a)+t,v=(h-o)*(h-o)+(g-s)*(g-s),x=(f-o)*(f-o)+(y-s)*(y-s);return v0){t=t/180*Math.PI,$I.fromArray(e[0]),KI.fromArray(e[1]),YI.fromArray(e[2]),Ms.sub(XI,$I,KI),Ms.sub(ZI,YI,KI);var n=XI.len(),i=ZI.len();if(!(n<.001||i<.001)){XI.scale(1/n),ZI.scale(1/i);var a=XI.dot(ZI);if(Math.cos(t)1&&Ms.copy(eE,YI),eE.toArray(e[1])}}}}function nE(e,t,n){if(n<=180&&n>0){n=n/180*Math.PI,$I.fromArray(e[0]),KI.fromArray(e[1]),YI.fromArray(e[2]),Ms.sub(XI,KI,$I),Ms.sub(ZI,YI,KI);var i=XI.len(),a=ZI.len();if(!(i<.001||a<.001))if(XI.scale(1/i),ZI.scale(1/a),XI.dot(t)=o)Ms.copy(eE,YI);else{eE.scaleAndAdd(ZI,r/Math.tan(Math.PI/2-s));var l=YI.x!==KI.x?(eE.x-KI.x)/(YI.x-KI.x):(eE.y-KI.y)/(YI.y-KI.y);if(isNaN(l))return;l<0?Ms.copy(eE,KI):l>1&&Ms.copy(eE,YI)}eE.toArray(e[1])}}}function iE(e,t,n,i){var a="normal"===n,r=a?e:e.ensureState(n);r.ignore=t;var o=i.get("smooth");o&&!0===o&&(o=.3),r.shape=r.shape||{},o>0&&(r.shape.smooth=o);var s=i.getModel("lineStyle").getLineStyle();a?e.useStyle(s):r.style=s}function aE(e,t){var n=t.smooth,i=t.points;if(i)if(e.moveTo(i[0][0],i[0][1]),n>0&&i.length>=3){var a=Go(i[0],i[1]),r=Go(i[1],i[2]);if(!a||!r)return e.lineTo(i[1][0],i[1][1]),void e.lineTo(i[2][0],i[2][1]);var o=Math.min(a,r)*n,s=jo([],i[1],i[0],o/a),l=jo([],i[1],i[2],o/r),p=jo([],s,l,.5);e.bezierCurveTo(s[0],s[1],s[0],s[1],p[0],p[1]),e.bezierCurveTo(l[0],l[1],l[0],l[1],i[2][0],i[2][1])}else for(var c=1;c0&&r&&S(-d/o,0,o);var f,y,v=e[0],x=e[o-1];return b(),f<0&&C(-f,.8),y<0&&C(y,.8),b(),w(f,y,1),w(y,f,-1),b(),f<0&&_(-f),y<0&&_(y),p}function b(){f=v.rect[t]-i,y=a-x.rect[t]-x.rect[n]}function w(e,t,n){if(e<0){var i=Math.min(t,-e);if(i>0){S(i*n,0,o);var a=i+e;a<0&&C(-a*n,1)}else C(-e*n,1)}}function S(n,i,a){0!==n&&(p=!0);for(var r=i;r0)for(l=0;l0;l--){S(-(r[l-1]*d),l,o)}}}function _(e){var t=e<0?-1:1;e=Math.abs(e);for(var n=Math.ceil(e/(o-1)),i=0;i0?S(n,0,i+1):S(-n,o-i-1,o),(e-=n)<=0)return}}function pE(e,t,n,i){return lE(e,"y","height",t,n,i)}function cE(e){var t=[];e.sort((function(e,t){return t.priority-e.priority}));var n=new Ns(0,0,0,0);function i(e){if(!e.ignore){var t=e.ensureState("emphasis");null==t.ignore&&(t.ignore=!1)}e.ignore=!0}for(var a=0;a=0&&n.attr(m.oldLayoutSelect),Lr(p,"emphasis")>=0&&n.attr(m.oldLayoutEmphasis)),pf(n,s,t,o)}else if(n.attr(s),!ay(n).valueAnimation){var c=co(n.style.opacity,1);n.style.opacity=0,cf(n,{style:{opacity:c}},t,o)}if(m.oldLayout=s,n.states.select){var d=m.oldLayoutSelect={};yE(d,s,vE),yE(d,n.states.select,vE)}if(n.states.emphasis){var u=m.oldLayoutEmphasis={};yE(u,s,vE),yE(u,n.states.emphasis,vE)}oy(n,o,l,t,t)}if(i&&!i.ignore&&!i.invisible){a=(m=fE(i)).oldLayout;var m,h={points:i.shape.points};a?(i.attr({shape:a}),pf(i,{shape:h},t)):(i.setShape(h),i.style.strokePercent=0,cf(i,{style:{strokePercent:1}},t)),m.oldLayout=h}},e}(),bE=Pd();function wE(e){e.registerUpdateLifecycle("series:beforeupdate",(function(e,t,n){var i=bE(t).labelManager;i||(i=bE(t).labelManager=new xE),i.clearLabels()})),e.registerUpdateLifecycle("series:layoutlabels",(function(e,t,n){var i=bE(t).labelManager;n.updatedSeries.forEach((function(e){i.addLabelsOfSeries(t.getViewOfSeriesModel(e))})),i.updateLayoutConfig(t),i.layout(t),i.processLabelsOverall()}))}dI(wE);var SE=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n}return We(t,e),t.prototype.getInitialData=function(e){return tT(null,this,{useEncodeDefaulter:!0})},t.prototype.getLegendIcon=function(e){var t=new Rc,n=NS("line",0,e.itemHeight/2,e.itemWidth,0,e.lineStyle.stroke,!1);t.add(n),n.setStyle(e.lineStyle);var i=this.getData().getVisual("symbol"),a=this.getData().getVisual("symbolRotate"),r="none"===i?"circle":i,o=.8*e.itemHeight,s=NS(r,(e.itemWidth-o)/2,(e.itemHeight-o)/2,o,o,e.itemStyle.fill);t.add(s),s.setStyle(e.itemStyle);var l="inherit"===e.iconRotate?a:e.iconRotate||0;return s.rotation=l*Math.PI/180,s.setOrigin([e.itemWidth/2,e.itemHeight/2]),r.indexOf("empty")>-1&&(s.style.stroke=s.style.fill,s.style.fill="#fff",s.style.lineWidth=2),t},t.type="series.line",t.dependencies=["grid","polar"],t.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1},t}(mw);function CE(e,t){var n=e.mapDimensionsAll("defaultedLabel"),i=n.length;if(1===i){var a=cb(e,t,n[0]);return null!=a?a+"":null}if(i){for(var r=[],o=0;o=0&&i.push(t[r])}return i.join(" ")}var TE=function(e){function t(t,n,i,a){var r=e.call(this)||this;return r.updateData(t,n,i,a),r}return We(t,e),t.prototype._createSymbol=function(e,t,n,i,a){this.removeAll();var r=NS(e,-1,-1,2,2,null,a);r.attr({z2:100,culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),r.drift=IE,this._symbolType=e,this.add(r)},t.prototype.stopSymbolAnimation=function(e){this.childAt(0).stopAnimation(null,e)},t.prototype.getSymbolType=function(){return this._symbolType},t.prototype.getSymbolPath=function(){return this.childAt(0)},t.prototype.highlight=function(){Th(this.childAt(0))},t.prototype.downplay=function(){Ih(this.childAt(0))},t.prototype.setZ=function(e,t){var n=this.childAt(0);n.zlevel=e,n.z=t},t.prototype.setDraggable=function(e,t){var n=this.childAt(0);n.draggable=e,n.cursor=!t&&e?"move":n.cursor},t.prototype.updateData=function(e,n,i,a){this.silent=!1;var r=e.getItemVisual(n,"symbol")||"circle",o=e.hostModel,s=t.getSymbolSize(e,n),l=r!==this._symbolType,p=a&&a.disableAnimation;if(l){var c=e.getItemVisual(n,"symbolKeepAspect");this._createSymbol(r,e,n,s,c)}else{(u=this.childAt(0)).silent=!1;var d={scaleX:s[0]/2,scaleY:s[1]/2};p?u.attr(d):pf(u,d,o,n),gf(u)}if(this._updateCommon(e,n,s,i,a),l){var u=this.childAt(0);if(!p){d={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:u.style.opacity}};u.scaleX=u.scaleY=0,u.style.opacity=0,cf(u,d,o,n)}}p&&this.childAt(0).stopAnimation("leave")},t.prototype._updateCommon=function(e,t,n,i,a){var r,o,s,l,p,c,d,u,m,h=this.childAt(0),g=e.hostModel;if(i&&(r=i.emphasisItemStyle,o=i.blurItemStyle,s=i.selectItemStyle,l=i.focus,p=i.blurScope,d=i.labelStatesModels,u=i.hoverScale,m=i.cursorStyle,c=i.emphasisDisabled),!i||e.hasItemOption){var f=i&&i.itemModel?i.itemModel:e.getItemModel(t),y=f.getModel("emphasis");r=y.getModel("itemStyle").getItemStyle(),s=f.getModel(["select","itemStyle"]).getItemStyle(),o=f.getModel(["blur","itemStyle"]).getItemStyle(),l=y.get("focus"),p=y.get("blurScope"),c=y.get("disabled"),d=Xf(f),u=y.getShallow("scale"),m=f.getShallow("cursor")}var v=e.getItemVisual(t,"symbolRotate");h.attr("rotation",(v||0)*Math.PI/180||0);var x=VS(e.getItemVisual(t,"symbolOffset"),n);x&&(h.x=x[0],h.y=x[1]),m&&h.attr("cursor",m);var b=e.getItemVisual(t,"style"),w=b.fill;if(h instanceof Em){var S=h.style;h.useStyle(Br({image:S.image,x:S.x,y:S.y,width:S.width,height:S.height},b))}else h.__isEmptyBrush?h.useStyle(Br({},b)):h.useStyle(b),h.style.decal=null,h.setColor(w,a&&a.symbolInnerColor),h.style.strokeNoScale=!0;var C=e.getItemVisual(t,"liftZ"),_=this._z2;null!=C?null==_&&(this._z2=h.z2,h.z2+=C):null!=_&&(h.z2=_,this._z2=null);var T=a&&a.useNameLabel;Yf(h,d,{labelFetcher:g,labelDataIndex:t,defaultText:function(t){return T?e.getName(t):CE(e,t)},inheritColor:w,defaultOpacity:b.opacity}),this._sizeX=n[0]/2,this._sizeY=n[1]/2;var I=h.ensureState("emphasis");I.style=r,h.ensureState("select").style=s,h.ensureState("blur").style=o;var E=null==u||!0===u?Math.max(1.1,3/this._sizeY):isFinite(u)&&u>0?+u:1;I.scaleX=this._sizeX*E,I.scaleY=this._sizeY*E,this.setSymbolScale(1),Vh(this,l,p,c)},t.prototype.setSymbolScale=function(e){this.scaleX=this.scaleY=e},t.prototype.fadeOut=function(e,t,n){var i=this.childAt(0),a=Xm(this).dataIndex,r=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var o=i.getTextContent();o&&uf(o,{style:{opacity:0}},t,{dataIndex:a,removeOpt:r,cb:function(){i.removeTextContent()}})}else i.removeTextContent();uf(i,{style:{opacity:0},scaleX:0,scaleY:0},t,{dataIndex:a,cb:e,removeOpt:r})},t.getSymbolSize=function(e,t){return LS(e.getItemVisual(t,"symbolSize"))},t}(Rc);function IE(e,t){this.parent.drift(e,t)}function EE(e,t,n,i){return t&&!isNaN(t[0])&&!isNaN(t[1])&&!(i.isIgnore&&i.isIgnore(n))&&!(i.clipShape&&!i.clipShape.contain(t[0],t[1]))&&"none"!==e.getItemVisual(n,"symbol")}function ME(e){return null==e||to(e)||(e={isIgnore:e}),e||{}}function kE(e){var t=e.hostModel,n=t.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:t.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:t.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:Xf(t),cursorStyle:t.get("cursor")}}var PE=function(){function e(e){this.group=new Rc,this._SymbolCtor=e||TE}return e.prototype.updateData=function(e,t){this._progressiveEls=null,t=ME(t);var n=this.group,i=e.hostModel,a=this._data,r=this._SymbolCtor,o=t.disableAnimation,s=kE(e),l={disableAnimation:o},p=t.getSymbolPoint||function(t){return e.getItemLayout(t)};a||n.removeAll(),e.diff(a).add((function(i){var a=p(i);if(EE(e,a,i,t)){var o=new r(e,i,s,l);o.setPosition(a),e.setItemGraphicEl(i,o),n.add(o)}})).update((function(c,d){var u=a.getItemGraphicEl(d),m=p(c);if(EE(e,m,c,t)){var h=e.getItemVisual(c,"symbol")||"circle",g=u&&u.getSymbolType&&u.getSymbolType();if(!u||g&&g!==h)n.remove(u),(u=new r(e,c,s,l)).setPosition(m);else{u.updateData(e,c,s,l);var f={x:m[0],y:m[1]};o?u.attr(f):pf(u,f,i)}n.add(u),e.setItemGraphicEl(c,u)}else n.remove(u)})).remove((function(e){var t=a.getItemGraphicEl(e);t&&t.fadeOut((function(){n.remove(t)}),i)})).execute(),this._getSymbolPoint=p,this._data=e},e.prototype.updateLayout=function(){var e=this,t=this._data;t&&t.eachItemGraphicEl((function(t,n){var i=e._getSymbolPoint(n);t.setPosition(i),t.markRedraw()}))},e.prototype.incrementalPrepareUpdate=function(e){this._seriesScope=kE(e),this._data=null,this.group.removeAll()},e.prototype.incrementalUpdate=function(e,t,n){function i(e){e.isGroup||(e.incremental=!0,e.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[],n=ME(n);for(var a=e.start;a0?n=i[0]:i[1]<0&&(n=i[1]);return n}(a,n),o=i.dim,s=a.dim,l=t.mapDimension(s),p=t.mapDimension(o),c="x"===s||"radius"===s?1:0,d=Ur(e.dimensions,(function(e){return t.mapDimension(e)})),u=!1,m=t.getCalculationInfo("stackResultDimension");return J_(t,d[0])&&(u=!0,d[0]=m),J_(t,d[1])&&(u=!0,d[1]=m),{dataDimsForPoint:d,valueStart:r,valueAxisDim:s,baseAxisDim:o,stacked:!!u,valueDim:l,baseDim:p,baseDataOffset:c,stackedOverDimension:t.getCalculationInfo("stackedOverDimension")}}function OE(e,t,n,i){var a=NaN;e.stacked&&(a=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(a)&&(a=e.valueStart);var r=e.baseDataOffset,o=[];return o[r]=n.get(e.baseDim,i),o[1-r]=a,t.dataToPoint(o)}var AE=Math.min,FE=Math.max;function RE(e,t){return isNaN(e)||isNaN(t)}function BE(e,t,n,i,a,r,o,s,l){for(var p,c,d,u,m,h,g=n,f=0;f=a||g<0)break;if(RE(y,v)){if(l){g+=r;continue}break}if(g===n)e[r>0?"moveTo":"lineTo"](y,v),d=y,u=v;else{var x=y-p,b=v-c;if(x*x+b*b<.5){g+=r;continue}if(o>0){for(var w=g+r,S=t[2*w],C=t[2*w+1];S===y&&C===v&&f=i||RE(S,C))m=y,h=v;else{I=S-p,E=C-c;var P=y-p,D=S-y,O=v-c,A=C-v,F=void 0,R=void 0;if("x"===s){var B=I>0?1:-1;m=y-B*(F=Math.abs(P))*o,h=v,M=y+B*(R=Math.abs(D))*o,k=v}else if("y"===s){var N=E>0?1:-1;m=y,h=v-N*(F=Math.abs(O))*o,M=y,k=v+N*(R=Math.abs(A))*o}else F=Math.sqrt(P*P+O*O),m=y-I*o*(1-(T=(R=Math.sqrt(D*D+A*A))/(R+F))),h=v-E*o*(1-T),k=v+E*o*T,M=AE(M=y+I*o*T,FE(S,y)),k=AE(k,FE(C,v)),M=FE(M,AE(S,y)),h=v-(E=(k=FE(k,AE(C,v)))-v)*F/R,m=AE(m=y-(I=M-y)*F/R,FE(p,y)),h=AE(h,FE(c,v)),M=y+(I=y-(m=FE(m,AE(p,y))))*R/F,k=v+(E=v-(h=FE(h,AE(c,v))))*R/F}e.bezierCurveTo(d,u,m,h,y,v),d=M,u=k}else e.lineTo(y,v)}p=y,c=v,g+=r}return f}var NE=function(){this.smooth=0,this.smoothConstraint=!0},LE=function(e){function t(t){var n=e.call(this,t)||this;return n.type="ec-polyline",n}return We(t,e),t.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},t.prototype.getDefaultShape=function(){return new NE},t.prototype.buildPath=function(e,t){var n=t.points,i=0,a=n.length/2;if(t.connectNulls){for(;a>0&&RE(n[2*a-2],n[2*a-1]);a--);for(;i=0){var f=o?(c-i)*g+i:(p-n)*g+n;return o?[e,f]:[f,e]}n=p,i=c;break;case r.C:p=a[l++],c=a[l++],d=a[l++],u=a[l++],m=a[l++],h=a[l++];var y=o?xl(n,p,d,m,e,s):xl(i,c,u,h,e,s);if(y>0)for(var v=0;v=0){f=o?yl(i,c,u,h,x):yl(n,p,d,m,x);return o?[e,f]:[f,e]}}n=m,i=h}}},t}(Sm),VE=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t}(NE),qE=function(e){function t(t){var n=e.call(this,t)||this;return n.type="ec-polygon",n}return We(t,e),t.prototype.getDefaultShape=function(){return new VE},t.prototype.buildPath=function(e,t){var n=t.points,i=t.stackedOnPoints,a=0,r=n.length/2,o=t.smoothMonotone;if(t.connectNulls){for(;r>0&&RE(n[2*r-2],n[2*r-1]);r--);for(;a=0;o--){var s=e.getDimensionInfo(i[o].dimension);if("x"===(a=s&&s.coordDim)||"y"===a){r=i[o];break}}if(r){var l=t.getAxis(a),p=Ur(r.stops,(function(e){return{coord:l.toGlobalCoord(l.dataToCoord(e.value)),color:e.color}})),c=p.length,d=r.outerColors.slice();c&&p[0].coord>p[c-1].coord&&(p.reverse(),d.reverse());var u=function(e,t){var n,i,a=[],r=e.length;function o(e,t,n){var i=e.coord;return{coord:n,color:Ql((n-i)/(t.coord-i),[e.color,t.color])}}for(var s=0;st){i?a.push(o(i,l,t)):n&&a.push(o(n,l,0),o(n,l,t));break}n&&(a.push(o(n,l,0)),n=null),a.push(l),i=l}}return a}(p,"x"===a?n.getWidth():n.getHeight()),m=u.length;if(!m&&c)return p[0].coord<0?d[1]?d[1]:p[c-1].color:d[0]?d[0]:p[0].color;var h=u[0].coord-10,g=u[m-1].coord+10,f=g-h;if(f<.001)return"transparent";zr(u,(function(e){e.offset=(e.coord-h)/f})),u.push({offset:m?u[m-1].offset:.5,color:d[1]||"transparent"}),u.unshift({offset:m?u[0].offset:.5,color:d[0]||"transparent"});var y=new Xg(0,0,0,0,u,!0);return y[a]=h,y[a+"2"]=g,y}}}function ZE(e,t,n){var i=e.get("showAllSymbol"),a="auto"===i;if(!i||a){var r=n.getAxesByScale("ordinal")[0];if(r&&(!a||!function(e,t){var n=e.getExtent(),i=Math.abs(n[1]-n[0])/e.scale.count();isNaN(i)&&(i=0);for(var a=t.count(),r=Math.max(1,Math.round(a/5)),o=0;oi)return!1;return!0}(r,t))){var o=t.mapDimension(r.dim),s={};return zr(r.getViewLabels(),(function(e){var t=r.scale.getRawOrdinalNumber(e.tickValue);s[t]=1})),function(e){return!s.hasOwnProperty(t.get(o,e))}}}}function QE(e,t){return[e[2*t],e[2*t+1]]}function JE(e){if(e.get(["endLabel","show"]))return!0;for(var t=0;t0&&"bolder"===e.get(["emphasis","lineStyle","width"]))&&(m.getState("emphasis").style.lineWidth=+m.style.lineWidth+1);Xm(m).seriesIndex=e.seriesIndex,Vh(m,P,D,O);var A=KE(e.get("smooth")),F=e.get("smoothMonotone");if(m.setShape({smooth:A,smoothMonotone:F,connectNulls:S}),h){var R=o.getCalculationInfo("stackedOnSeries"),B=0;h.useStyle(Nr(l.getAreaStyle(),{fill:E,opacity:.7,lineJoin:"bevel",decal:o.getVisual("style").decal})),R&&(B=KE(R.get("smooth"))),h.setShape({smooth:A,stackedOnSmooth:B,smoothMonotone:F,connectNulls:S}),Uh(h,e,"areaStyle"),Xm(h).seriesIndex=e.seriesIndex,Vh(h,P,D,O)}var N=function(e){i._changePolyState(e)};o.eachItemGraphicEl((function(e){e&&(e.onHoverStateChange=N)})),this._polyline.onHoverStateChange=N,this._data=o,this._coordSys=a,this._stackedOnPoints=b,this._points=p,this._step=I,this._valueOrigin=v,e.get("triggerLineEvent")&&(this.packEventData(e,m),h&&this.packEventData(e,h))},t.prototype.packEventData=function(e,t){Xm(t).eventData={componentType:"series",componentSubType:"line",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"line"}},t.prototype.highlight=function(e,t,n,i){var a=e.getData(),r=kd(a,i);if(this._changePolyState("emphasis"),!(r instanceof Array)&&null!=r&&r>=0){var o=a.getLayout("points"),s=a.getItemGraphicEl(r);if(!s){var l=o[2*r],p=o[2*r+1];if(isNaN(l)||isNaN(p))return;if(this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(l,p))return;var c=e.get("zlevel")||0,d=e.get("z")||0;(s=new TE(a,r)).x=l,s.y=p,s.setZ(c,d);var u=s.getSymbolPath().getTextContent();u&&(u.zlevel=c,u.z=d,u.z2=this._polyline.z2+1),s.__temp=!0,a.setItemGraphicEl(r,s),s.stopSymbolAnimation(!0),this.group.add(s)}s.highlight()}else Tw.prototype.highlight.call(this,e,t,n,i)},t.prototype.downplay=function(e,t,n,i){var a=e.getData(),r=kd(a,i);if(this._changePolyState("normal"),null!=r&&r>=0){var o=a.getItemGraphicEl(r);o&&(o.__temp?(a.setItemGraphicEl(r,null),this.group.remove(o)):o.downplay())}else Tw.prototype.downplay.call(this,e,t,n,i)},t.prototype._changePolyState=function(e){var t=this._polygon;bh(this._polyline,e),t&&bh(t,e)},t.prototype._newPolyline=function(e){var t=this._polyline;return t&&this._lineGroup.remove(t),t=new LE({shape:{points:e},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(t),this._polyline=t,t},t.prototype._newPolygon=function(e,t){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new qE({shape:{points:e,stackedOnPoints:t},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},t.prototype._initSymbolLabelAnimation=function(e,t,n){var i,a,r=t.getBaseAxis(),o=r.inverse;"cartesian2d"===t.type?(i=r.isHorizontal(),a=!1):"polar"===t.type&&(i="angle"===r.dim,a=!0);var s=e.hostModel,l=s.get("animationDuration");Zr(l)&&(l=l(null));var p=s.get("animationDelay")||0,c=Zr(p)?p(null):p;e.eachItemGraphicEl((function(e,r){var s=e;if(s){var d=[e.x,e.y],u=void 0,m=void 0,h=void 0;if(n)if(a){var g=n,f=t.pointToCoord(d);i?(u=g.startAngle,m=g.endAngle,h=-f[1]/180*Math.PI):(u=g.r0,m=g.r,h=f[0])}else{var y=n;i?(u=y.x,m=y.x+y.width,h=e.x):(u=y.y+y.height,m=y.y,h=e.y)}var v=m===u?0:(h-u)/(m-u);o&&(v=1-v);var x=Zr(p)?p(r):l*v+c,b=s.getSymbolPath(),w=b.getTextContent();s.attr({scaleX:0,scaleY:0}),s.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:x}),w&&w.animateFrom({style:{opacity:0}},{duration:300,delay:x}),b.disableLabelAnimation=!0}}))},t.prototype._initOrUpdateEndLabel=function(e,t,n){var i=e.getModel("endLabel");if(JE(e)){var a=e.getData(),r=this._polyline,o=a.getLayout("points");if(!o)return r.removeTextContent(),void(this._endLabel=null);var s=this._endLabel;s||((s=this._endLabel=new Nm({z2:200})).ignoreClip=!0,r.setTextContent(this._endLabel),r.disableLabelAnimation=!0);var l=function(e){for(var t,n,i=e.length/2;i>0&&(t=e[2*i-2],n=e[2*i-1],isNaN(t)||isNaN(n));i--);return i-1}(o);l>=0&&(Yf(r,Xf(e,"endLabel"),{inheritColor:n,labelFetcher:e,labelDataIndex:l,defaultText:function(e,t,n){return null!=n?_E(a,n):CE(a,e)},enableTextSetter:!0},function(e,t){var n=t.getBaseAxis(),i=n.isHorizontal(),a=n.inverse,r=i?a?"right":"left":"center",o=i?"middle":a?"top":"bottom";return{normal:{align:e.get("align")||r,verticalAlign:e.get("verticalAlign")||o}}}(i,t)),r.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},t.prototype._endLabelOnDuring=function(e,t,n,i,a,r,o){var s=this._endLabel,l=this._polyline;if(s){e<1&&null==i.originalX&&(i.originalX=s.x,i.originalY=s.y);var p=n.getLayout("points"),c=n.hostModel,d=c.get("connectNulls"),u=r.get("precision"),m=r.get("distance")||0,h=o.getBaseAxis(),g=h.isHorizontal(),f=h.inverse,y=t.shape,v=f?g?y.x:y.y+y.height:g?y.x+y.width:y.y,x=(g?m:0)*(f?-1:1),b=(g?0:-m)*(f?-1:1),w=g?"x":"y",S=function(e,t,n){for(var i,a,r=e.length/2,o="x"===n?0:1,s=0,l=-1,p=0;p=t||i>=t&&a<=t){l=p;break}s=p,i=a}else i=a;return{range:[s,l],t:(t-i)/(a-i)}}(p,v,w),C=S.range,_=C[1]-C[0],T=void 0;if(_>=1){if(_>1&&!d){var I=QE(p,C[0]);s.attr({x:I[0]+x,y:I[1]+b}),a&&(T=c.getRawValue(C[0]))}else{(I=l.getPointOn(v,w))&&s.attr({x:I[0]+x,y:I[1]+b});var E=c.getRawValue(C[0]),M=c.getRawValue(C[1]);a&&(T=Vd(n,u,E,M,S.t))}i.lastFrameIndex=C[0]}else{var k=1===e||i.lastFrameIndex>0?C[0]:0;I=QE(p,k);a&&(T=c.getRawValue(k)),s.attr({x:I[0]+x,y:I[1]+b})}if(a){var P=ay(s);"function"==typeof P.setLabelText&&P.setLabelText(T)}}},t.prototype._doUpdateAnimation=function(e,t,n,i,a,r,o){var s=this._polyline,l=this._polygon,p=e.hostModel,c=function(e,t,n,i,a,r,o){for(var s=function(e,t){var n=[];return t.diff(e).add((function(e){n.push({cmd:"+",idx:e})})).update((function(e,t){n.push({cmd:"=",idx:t,idx1:e})})).remove((function(e){n.push({cmd:"-",idx:e})})).execute(),n}(e,t),l=[],p=[],c=[],d=[],u=[],m=[],h=[],g=DE(a,t,o),f=e.getLayout("points")||[],y=t.getLayout("points")||[],v=0;v3e3||l&&$E(u,h)>3e3)return s.stopAnimation(),s.setShape({points:m}),void(l&&(l.stopAnimation(),l.setShape({points:m,stackedOnPoints:h})));s.shape.__points=c.current,s.shape.points=d;var g={shape:{points:m}};c.current!==d&&(g.shape.__points=c.next),s.stopAnimation(),pf(s,g,p),l&&(l.setShape({points:d,stackedOnPoints:u}),l.stopAnimation(),pf(l,{shape:{stackedOnPoints:h}},p),s.shape.points!==l.shape.points&&(l.shape.points=s.shape.points));for(var f=[],y=c.status,v=0;vt&&(t=e[n]);return isFinite(t)?t:NaN},min:function(e){for(var t=1/0,n=0;nt&&(t=r,n=a)}return isFinite(n)?n:NaN},nearest:function(e){return e[0]}},aM=function(e){return Math.round(e.length/2)};function rM(e){return{seriesType:e,reset:function(e,t,n){var i=e.getData(),a=e.get("sampling"),r=e.coordinateSystem,o=i.count();if(o>10&&"cartesian2d"===r.type&&a){var s=r.getBaseAxis(),l=r.getOtherAxis(s),p=s.getExtent(),c=n.getDevicePixelRatio(),d=Math.abs(p[1]-p[0])*(c||1),u=Math.round(o/d);if(isFinite(u)&&u>1){"lttb"===a&&e.setData(i.lttbDownSample(i.mapDimension(l.dim),1/u));var m=void 0;Qr(a)?m=iM[a]:Zr(a)&&(m=a),m&&e.setData(i.downSample(i.mapDimension(l.dim),1/u,m,aM))}}}}}function oM(e){e.registerChartView(tM),e.registerSeriesModel(SE),e.registerLayout(nM("line",!0)),e.registerVisual({seriesType:"line",reset:function(e){var t=e.getData(),n=e.getModel("lineStyle").getLineStyle();n&&!n.stroke&&(n.stroke=t.getVisual("style").fill),t.setVisual("legendLineStyle",n)}}),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,rM("line"))}var sM=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.getInitialData=function(e,t){return tT(null,this,{useEncodeDefaulter:!0})},t.prototype.getMarkerPosition=function(e,t,n){var i=this.coordinateSystem;if(i&&i.clampData){var a=i.clampData(e),r=i.dataToPoint(a);if(n)zr(i.getAxes(),(function(e,n){if("category"===e.type&&null!=t){var i=e.getTicksCoords(),o=e.getTickModel().get("alignWithLabel"),s=a[n],l="x1"===t[n]||"y1"===t[n];if(l&&!o&&(s+=1),i.length<2)return;if(2===i.length)return void(r[n]=e.toGlobalCoord(e.getExtent()[l?1:0]));for(var p=void 0,c=void 0,d=1,u=0;us){c=(m+p)/2;break}1===u&&(d=h-i[0].tickValue)}null==c&&(p?p&&(c=i[i.length-1].coord):c=i[0].coord),r[n]=e.toGlobalCoord(c)}}));else{var o=this.getData(),s=o.getLayout("offset"),l=o.getLayout("size"),p=i.getBaseAxis().isHorizontal()?0:1;r[p]+=s+l/2}return r}return[NaN,NaN]},t.type="series.__base_bar__",t.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod"},t}(mw);mw.registerClass(sM);var lM=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.getInitialData=function(){return tT(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},t.prototype.getProgressive=function(){return!!this.get("large")&&this.get("progressive")},t.prototype.getProgressiveThreshold=function(){var e=this.get("progressiveThreshold"),t=this.get("largeThreshold");return t>e&&(e=t),e},t.prototype.brushSelector=function(e,t,n){return n.rect(t.getItemLayout(e))},t.type="series.bar",t.dependencies=["grid","polar"],t.defaultOption=Sy(sM.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:"#212121"}},realtimeSort:!1}),t}(sM),pM=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},cM=function(e){function t(t){var n=e.call(this,t)||this;return n.type="sausage",n}return We(t,e),t.prototype.getDefaultShape=function(){return new pM},t.prototype.buildPath=function(e,t){var n=t.cx,i=t.cy,a=Math.max(t.r0||0,0),r=Math.max(t.r,0),o=.5*(r-a),s=a+o,l=t.startAngle,p=t.endAngle,c=t.clockwise,d=2*Math.PI,u=c?p-lr)return!0;r=p}return!1},t.prototype._isOrderDifferentInView=function(e,t){for(var n=t.scale,i=n.getExtent(),a=Math.max(0,i[0]),r=Math.min(i[1],n.getOrdinalMeta().categories.length-1);a<=r;++a)if(e.ordinalNumbers[a]!==n.getRawOrdinalNumber(a))return!0},t.prototype._updateSortWithinSameData=function(e,t,n,i){if(this._isOrderChangedWithinSameData(e,t,n)){var a=this._dataSort(e,n,t);this._isOrderDifferentInView(a,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:a}))}},t.prototype._dispatchInitSort=function(e,t,n){var i=t.baseAxis,a=this._dataSort(e,i,(function(n){return e.get(e.mapDimension(t.otherAxis.dim),n)}));n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:a})},t.prototype.remove=function(e,t){this._clear(this._model),this._removeOnRenderedListener(t)},t.prototype.dispose=function(e,t){this._removeOnRenderedListener(t)},t.prototype._removeOnRenderedListener=function(e){this._onRendered&&(e.getZr().off("rendered",this._onRendered),this._onRendered=null)},t.prototype._clear=function(e){var t=this.group,n=this._data;e&&e.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl((function(t){hf(t,e,Xm(t).dataIndex)}))):t.removeAll(),this._data=null,this._isFirstFrame=!0},t.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},t.type="bar",t}(Tw),yM={cartesian2d:function(e,t){var n=t.width<0?-1:1,i=t.height<0?-1:1;n<0&&(t.x+=t.width,t.width=-t.width),i<0&&(t.y+=t.height,t.height=-t.height);var a=e.x+e.width,r=e.y+e.height,o=hM(t.x,e.x),s=gM(t.x+t.width,a),l=hM(t.y,e.y),p=gM(t.y+t.height,r),c=sa?s:o,t.y=d&&l>r?p:l,t.width=c?0:s-o,t.height=d?0:p-l,n<0&&(t.x+=t.width,t.width=-t.width),i<0&&(t.y+=t.height,t.height=-t.height),c||d},polar:function(e,t){var n=t.r0<=t.r?1:-1;if(n<0){var i=t.r;t.r=t.r0,t.r0=i}var a=gM(t.r,e.r),r=hM(t.r0,e.r0);t.r=a,t.r0=r;var o=a-r<0;if(n<0){i=t.r;t.r=t.r0,t.r0=i}return o}},vM={cartesian2d:function(e,t,n,i,a,r,o,s,l){var p=new Fm({shape:Br({},i),z2:1});(p.__dataIndex=n,p.name="item",r)&&(p.shape[a?"height":"width"]=0);return p},polar:function(e,t,n,i,a,r,o,s,l){var p=!a&&l?cM:Dg,c=new p({shape:i,z2:1});c.name="item";var d,u,m=TM(a);if(c.calculateTextPosition=(d=m,u=({isRoundCap:p===cM}||{}).isRoundCap,function(e,t,n){var i=t.position;if(!i||i instanceof Array)return _c(e,t,n);var a=d(i),r=null!=t.distance?t.distance:5,o=this.shape,s=o.cx,l=o.cy,p=o.r,c=o.r0,m=(p+c)/2,h=o.startAngle,g=o.endAngle,f=(h+g)/2,y=u?Math.abs(p-c)/2:0,v=Math.cos,x=Math.sin,b=s+p*v(h),w=l+p*x(h),S="left",C="top";switch(a){case"startArc":b=s+(c-r)*v(f),w=l+(c-r)*x(f),S="center",C="top";break;case"insideStartArc":b=s+(c+r)*v(f),w=l+(c+r)*x(f),S="center",C="bottom";break;case"startAngle":b=s+m*v(h)+dM(h,r+y,!1),w=l+m*x(h)+uM(h,r+y,!1),S="right",C="middle";break;case"insideStartAngle":b=s+m*v(h)+dM(h,-r+y,!1),w=l+m*x(h)+uM(h,-r+y,!1),S="left",C="middle";break;case"middle":b=s+m*v(f),w=l+m*x(f),S="center",C="middle";break;case"endArc":b=s+(p+r)*v(f),w=l+(p+r)*x(f),S="center",C="bottom";break;case"insideEndArc":b=s+(p-r)*v(f),w=l+(p-r)*x(f),S="center",C="top";break;case"endAngle":b=s+m*v(g)+dM(g,r+y,!0),w=l+m*x(g)+uM(g,r+y,!0),S="left",C="middle";break;case"insideEndAngle":b=s+m*v(g)+dM(g,-r+y,!0),w=l+m*x(g)+uM(g,-r+y,!0),S="right",C="middle";break;default:return _c(e,t,n)}return(e=e||{}).x=b,e.y=w,e.align=S,e.verticalAlign=C,e}),r){var h=a?"r":"endAngle",g={};c.shape[h]=a?i.r0:i.startAngle,g[h]=i[h],(s?pf:cf)(c,{shape:g},r)}return c}};function xM(e,t,n,i,a,r,o,s){var l,p;r?(p={x:i.x,width:i.width},l={y:i.y,height:i.height}):(p={y:i.y,height:i.height},l={x:i.x,width:i.width}),s||(o?pf:cf)(n,{shape:l},t,a,null),(o?pf:cf)(n,{shape:p},t?e.baseAxis.model:null,a)}function bM(e,t){for(var n=0;n0?1:-1,o=i.height>0?1:-1;return{x:i.x+r*a/2,y:i.y+o*a/2,width:i.width-r*a,height:i.height-o*a}},polar:function(e,t,n){var i=e.getItemLayout(t);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}}};function TM(e){return function(e){var t=e?"Arc":"Angle";return function(e){switch(e){case"start":case"insideStart":case"end":case"insideEnd":return e+t;default:return e}}}(e)}function IM(e,t,n,i,a,r,o,s){var l=t.getItemVisual(n,"style");if(s){if(!r.get("roundCap")){var p=e.shape;Br(p,mM(i.getModel("itemStyle"),p,!0)),e.setShape(p)}}else{var c=i.get(["itemStyle","borderRadius"])||0;e.setShape("r",c)}e.useStyle(l);var d=i.getShallow("cursor");d&&e.attr("cursor",d);var u=s?o?a.r>=a.r0?"endArc":"startArc":a.endAngle>=a.startAngle?"endAngle":"startAngle":o?a.height>=0?"bottom":"top":a.width>=0?"right":"left",m=Xf(i);Yf(e,m,{labelFetcher:r,labelDataIndex:n,defaultText:CE(r.getData(),n),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:u});var h=e.getTextContent();if(s&&h){var g=i.get(["label","position"]);e.textConfig.inside="middle"===g||null,function(e,t,n,i){if(eo(i))e.setTextConfig({rotation:i});else if(Xr(t))e.setTextConfig({rotation:0});else{var a,r=e.shape,o=r.clockwise?r.startAngle:r.endAngle,s=r.clockwise?r.endAngle:r.startAngle,l=(o+s)/2,p=n(t);switch(p){case"startArc":case"insideStartArc":case"middle":case"insideEndArc":case"endArc":a=l;break;case"startAngle":case"insideStartAngle":a=o;break;case"endAngle":case"insideEndAngle":a=s;break;default:return void e.setTextConfig({rotation:0})}var c=1.5*Math.PI-a;"middle"===p&&c>Math.PI/2&&c<1.5*Math.PI&&(c-=Math.PI),e.setTextConfig({rotation:c})}}(e,"outside"===g?u:g,TM(o),i.get(["label","rotate"]))}ry(h,m,r.getRawValue(n),(function(e){return _E(t,e)}));var f=i.getModel(["emphasis"]);Vh(e,f.get("focus"),f.get("blurScope"),f.get("disabled")),Uh(e,i),function(e){return null!=e.startAngle&&null!=e.endAngle&&e.startAngle===e.endAngle}(a)&&(e.style.fill="none",e.style.stroke="none",zr(e.states,(function(e){e.style&&(e.style.fill=e.style.stroke="none")})))}var EM=function(){},MM=function(e){function t(t){var n=e.call(this,t)||this;return n.type="largeBar",n}return We(t,e),t.prototype.getDefaultShape=function(){return new EM},t.prototype.buildPath=function(e,t){for(var n=t.points,i=this.baseDimIdx,a=1-this.baseDimIdx,r=[],o=[],s=this.barWidth,l=0;l=s[0]&&t<=s[0]+l[0]&&n>=s[1]&&n<=s[1]+l[1])return o[c]}return-1}(this,e.offsetX,e.offsetY);Xm(this).dataIndex=t>=0?t:null}),30,!1);function DM(e,t,n){if(jE(n,"cartesian2d")){var i=t,a=n.getArea();return{x:e?i.x:a.x,y:e?a.y:i.y,width:e?i.width:a.width,height:e?a.height:i.height}}var r=t;return{cx:(a=n.getArea()).cx,cy:a.cy,r0:e?a.r0:r.r0,r:e?a.r:r.r,startAngle:e?r.startAngle:0,endAngle:e?r.endAngle:2*Math.PI}}function OM(e){e.registerChartView(fM),e.registerSeriesModel(lM),e.registerLayout(e.PRIORITY.VISUAL.LAYOUT,Yr(IT,"bar")),e.registerLayout(e.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,ET("bar")),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,rM("bar")),e.registerAction({type:"changeAxisOrder",event:"changeAxisOrder",update:"update"},(function(e,t){var n=e.componentType||"series";t.eachComponent({mainType:n,query:e},(function(t){e.sortInfo&&t.axis.setCategorySortInfo(e.sortInfo)}))}))}var AM=2*Math.PI,FM=Math.PI/180;function RM(e,t){return Sv(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()})}function BM(e,t){var n=RM(e,t),i=e.get("center"),a=e.get("radius");Xr(a)||(a=[0,a]);var r,o,s=Wc(n.width,t.getWidth()),l=Wc(n.height,t.getHeight()),p=Math.min(s,l),c=Wc(a[0],p/2),d=Wc(a[1],p/2),u=e.coordinateSystem;if(u){var m=u.dataToPoint(i);r=m[0]||0,o=m[1]||0}else Xr(i)||(i=[i,i]),r=Wc(i[0],s)+n.x,o=Wc(i[1],l)+n.y;return{cx:r,cy:o,r0:c,r:d}}function NM(e,t,n){t.eachSeriesByType(e,(function(e){var t=e.getData(),i=t.mapDimension("value"),a=RM(e,n),r=BM(e,n),o=r.cx,s=r.cy,l=r.r,p=r.r0,c=-e.get("startAngle")*FM,d=e.get("endAngle"),u=e.get("padAngle")*FM;d="auto"===d?c-AM:-d*FM;var m=e.get("minAngle")*FM+u,h=0;t.each(i,(function(e){!isNaN(e)&&h++}));var g=t.getSum(i),f=Math.PI/(g||h)*2,y=e.get("clockwise"),v=e.get("roseType"),x=e.get("stillShowZeroSum"),b=t.getDataExtent(i);b[0]=0;var w=y?1:-1,S=[c,d],C=w*u/2;tm(S,!y),c=S[0],d=S[1];var _=Math.abs(d-c),T=_,I=0,E=c;if(t.setLayout({viewRect:a,r:l}),t.each(i,(function(e,n){var i;if(isNaN(e))t.setItemLayout(n,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:y,cx:o,cy:s,r0:p,r:v?NaN:l});else{(i="area"!==v?0===g&&x?f:e*f:_/h)i?c=r=E+w*i/2:(r=E+C,c=a-C),t.setItemLayout(n,{angle:i,startAngle:r,endAngle:c,clockwise:y,cx:o,cy:s,r0:p,r:v?Hc(e,b,[p,l]):l}),E=a}})),Tn?o:r,c=Math.abs(l.label.y-n);if(c>=p.maxY){var d=l.label.x-t-l.len2*a,u=i+l.len,h=Math.abs(d)e.unconstrainedWidth?null:m:null;i.setStyle("width",h)}var g=i.getBoundingRect();r.width=g.width;var f=(i.style.margin||0)+2.1;r.height=g.height+f,r.y-=(r.height-d)/2}}}function zM(e){return"center"===e.position}function UM(e){var t,n,i=e.getData(),a=[],r=!1,o=(e.get("minShowLabelAngle")||0)*VM,s=i.getLayout("viewRect"),l=i.getLayout("r"),p=s.width,c=s.x,d=s.y,u=s.height;function m(e){e.ignore=!0}i.each((function(e){var s=i.getItemGraphicEl(e),d=s.shape,u=s.getTextContent(),h=s.getTextGuideLine(),g=i.getItemModel(e),f=g.getModel("label"),y=f.get("position")||g.get(["emphasis","label","position"]),v=f.get("distanceToLabelLine"),x=f.get("alignTo"),b=Wc(f.get("edgeDistance"),p),w=f.get("bleedMargin"),S=g.getModel("labelLine"),C=S.get("length");C=Wc(C,p);var _=S.get("length2");if(_=Wc(_,p),Math.abs(d.endAngle-d.startAngle)0?"right":"left":P>0?"left":"right"}var L=Math.PI,V=0,q=f.get("rotate");if(eo(q))V=q*(L/180);else if("center"===y)V=0;else if("radial"===q||!0===q){V=P<0?-k+L:-k}else if("tangential"===q&&"outside"!==y&&"outer"!==y){var G=Math.atan2(P,D);G<0&&(G=2*L+G),D>0&&(G=L+G),V=G-L}if(r=!!V,u.x=T,u.y=I,u.rotation=V,u.setStyle({verticalAlign:"middle"}),O){u.setStyle({align:M});var z=u.states.select;z&&(z.x+=u.x,z.y+=u.y)}else{var U=u.getBoundingRect().clone();U.applyTransform(u.getComputedTransform());var j=(u.style.margin||0)+2.1;U.y-=j/2,U.height+=j,a.push({label:u,labelLine:h,position:y,len:C,len2:_,minTurnAngle:S.get("minTurnAngle"),maxSurfaceAngle:S.get("maxSurfaceAngle"),surfaceNormal:new Ms(P,D),linePoints:E,textAlign:M,labelDistance:v,labelAlignTo:x,edgeDistance:b,bleedMargin:w,rect:U,unconstrainedWidth:U.width,labelStyleWidth:u.style.width})}s.setTextConfig({inside:O})}})),!r&&e.get("avoidLabelOverlap")&&function(e,t,n,i,a,r,o,s){for(var l=[],p=[],c=Number.MAX_VALUE,d=-Number.MAX_VALUE,u=0;u0){for(var l=r.getItemLayout(0),p=1;isNaN(l&&l.startAngle)&&p=n.r0}},t.type="pie",t}(Tw);function WM(e,t,n){t=Xr(t)&&{coordDimensions:t}||Br({encodeDefine:e.getEncode()},t);var i=e.getSource(),a=$_(i,t).dimensions,r=new W_(a,e);return r.initData(i,n),r}var $M=function(){function e(e,t){this._getDataWithEncodedVisual=e,this._getRawData=t}return e.prototype.getAllNames=function(){var e=this._getRawData();return e.mapArray(e.getName)},e.prototype.containName=function(e){return this._getRawData().indexOfName(e)>=0},e.prototype.indexOfName=function(e){return this._getDataWithEncodedVisual().indexOfName(e)},e.prototype.getItemVisual=function(e,t){return this._getDataWithEncodedVisual().getItemVisual(e,t)},e}(),KM=Pd(),YM=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.init=function(t){e.prototype.init.apply(this,arguments),this.legendVisualProvider=new $M(Kr(this.getData,this),Kr(this.getRawData,this)),this._defaultLabelLine(t)},t.prototype.mergeOption=function(){e.prototype.mergeOption.apply(this,arguments)},t.prototype.getInitialData=function(){return WM(this,{coordDimensions:["value"],encodeDefaulter:Yr($v,this)})},t.prototype.getDataParams=function(t){var n=this.getData(),i=KM(n),a=i.seats;if(!a){var r=[];n.each(n.mapDimension("value"),(function(e){r.push(e)})),a=i.seats=Qc(r,n.hostModel.get("percentPrecision"))}var o=e.prototype.getDataParams.call(this,t);return o.percent=a[t]||0,o.$vars.push("percent"),o},t.prototype._defaultLabelLine=function(e){xd(e,"labelLine",["show"]);var t=e.labelLine,n=e.emphasis.labelLine;t.show=t.show&&e.label.show,n.show=n.show&&e.emphasis.label.show},t.type="series.pie",t.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,endAngle:"auto",padAngle:0,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",bleedMargin:10,distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:15,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},t}(mw);function XM(e){e.registerChartView(HM),e.registerSeriesModel(YM),CS("pie",e.registerAction),e.registerLayout(Yr(NM,"pie")),e.registerProcessor(LM("pie")),e.registerProcessor(function(e){return{seriesType:e,reset:function(e,t){var n=e.getData();n.filterSelf((function(e){var t=n.mapDimension("value"),i=n.get(t,e);return!(eo(i)&&!isNaN(i)&&i<0)}))}}}("pie"))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n}We(t,e),t.prototype.getInitialData=function(e,t){return tT(null,this,{useEncodeDefaulter:!0})},t.prototype.getProgressive=function(){var e=this.option.progressive;return null==e?this.option.large?5e3:this.get("progressive"):e},t.prototype.getProgressiveThreshold=function(){var e=this.option.progressiveThreshold;return null==e?this.option.large?1e4:this.get("progressiveThreshold"):e},t.prototype.brushSelector=function(e,t,n){return n.point(t.getItemLayout(e))},t.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},t.type="series.scatter",t.dependencies=["grid","polar","geo","singleAxis","calendar"],t.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:"#212121"}},universalTransition:{divideShape:"clone"}}}(mw);var ZM=function(){},QM=function(e){function t(t){var n=e.call(this,t)||this;return n._off=0,n.hoverDataIdx=-1,n}return We(t,e),t.prototype.getDefaultShape=function(){return new ZM},t.prototype.reset=function(){this.notClear=!1,this._off=0},t.prototype.buildPath=function(e,t){var n,i=t.points,a=t.size,r=this.symbolProxy,o=r.shape,s=e.getContext?e.getContext():e,l=s&&a[0]<4,p=this.softClipShape;if(l)this._ctx=s;else{for(this._ctx=null,n=this._off;n=0;s--){var l=2*s,p=i[l]-r/2,c=i[l+1]-o/2;if(e>=p&&t>=c&&e<=p+r&&t<=c+o)return s}return-1},t.prototype.contain=function(e,t){var n=this.transformCoordToLocal(e,t),i=this.getBoundingRect();return e=n[0],t=n[1],i.contain(e,t)?(this.hoverDataIdx=this.findDataIndex(e,t))>=0:(this.hoverDataIdx=-1,!1)},t.prototype.getBoundingRect=function(){var e=this._rect;if(!e){for(var t=this.shape,n=t.points,i=t.size,a=i[0],r=i[1],o=1/0,s=1/0,l=-1/0,p=-1/0,c=0;c=0&&(l.dataIndex=n+(e.startIndex||0))}))},e.prototype.remove=function(){this._clear()},e.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},e}(),ek=(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.render=function(e,t,n){var i=e.getData();this._updateSymbolDraw(i,e).updateData(i,{clipShape:this._getClipShape(e)}),this._finished=!0},t.prototype.incrementalPrepareRender=function(e,t,n){var i=e.getData();this._updateSymbolDraw(i,e).incrementalPrepareUpdate(i),this._finished=!1},t.prototype.incrementalRender=function(e,t,n){this._symbolDraw.incrementalUpdate(e,t.getData(),{clipShape:this._getClipShape(t)}),this._finished=e.end===t.getData().count()},t.prototype.updateTransform=function(e,t,n){var i=e.getData();if(this.group.dirty(),!this._finished||i.count()>1e4)return{update:!0};var a=nM("").reset(e,t,n);a.progress&&a.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(i)},t.prototype.eachRendered=function(e){this._symbolDraw&&this._symbolDraw.eachRendered(e)},t.prototype._getClipShape=function(e){if(e.get("clip",!0)){var t=e.coordinateSystem;return t&&t.getArea&&t.getArea(.1)}},t.prototype._updateSymbolDraw=function(e,t){var n=this._symbolDraw,i=t.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new JM:new PE,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},t.prototype.remove=function(e,t){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},t.prototype.dispose=function(){},t.type="scatter"}(Tw),function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.type="grid",t.dependencies=["xAxis","yAxis"],t.layoutMode="box",t.defaultOption={show:!1,z:0,left:"10%",top:60,right:"10%",bottom:70,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"},t}(kv)),tk=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",Fd).models[0]},t.type="cartesian2dAxis",t}(kv);qr(tk,lI);var nk={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#6E7079",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#E0E6F1"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.2)","rgba(210,219,238,0.2)"]}}},ik=Fr({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},nk),ak=Fr({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:"#F4F7FD",width:1}}},nk),rk={category:ik,value:ak,time:Fr({splitNumber:6,axisLabel:{showMinLabel:!1,showMaxLabel:!1,rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},ak),log:Nr({logBase:10},ak)},ok={value:1,category:1,time:1,log:1};function sk(e,t,n,i){zr(ok,(function(a,r){var o=Fr(Fr({},rk[r],!0),i,!0),s=function(e){function n(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t+"Axis."+r,n}return We(n,e),n.prototype.mergeDefaultAndTheme=function(e,t){var n=_v(this),i=n?Iv(e):{};Fr(e,t.getTheme().get(r+"Axis")),Fr(e,this.getDefaultOption()),e.type=lk(e),n&&Tv(e,i,n)},n.prototype.optionUpdated=function(){"category"===this.option.type&&(this.__ordinalMeta=aT.createByAxisModel(this))},n.prototype.getCategories=function(e){var t=this.option;if("category"===t.type)return e?t.data:this.__ordinalMeta.categories},n.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},n.prototype.getTicksGenerator=function(){var e=this.option;if("value"===e.type)return e.ticksGenerator},n.type=t+"Axis."+r,n.defaultOption=o,n}(n);e.registerComponentModel(s)})),e.registerSubTypeDefaulter(t+"Axis",lk)}function lk(e){return e.type||(e.data?"category":"value")}var pk=function(){function e(e){this.type="cartesian",this._dimList=[],this._axes={},this.name=e||""}return e.prototype.getAxis=function(e){return this._axes[e]},e.prototype.getAxes=function(){return Ur(this._dimList,(function(e){return this._axes[e]}),this)},e.prototype.getAxesByScale=function(e){return e=e.toLowerCase(),Hr(this.getAxes(),(function(t){return t.scale.type===e}))},e.prototype.addAxis=function(e){var t=e.dim;this._axes[t]=e,this._dimList.push(t)},e}(),ck=["x","y"];function dk(e){return"interval"===e.type||"time"===e.type}var uk=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type="cartesian2d",t.dimensions=ck,t}return We(t,e),t.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var e=this.getAxis("x").scale,t=this.getAxis("y").scale;if(dk(e)&&dk(t)){var n=e.getExtent(),i=t.getExtent(),a=this.dataToPoint([n[0],i[0]]),r=this.dataToPoint([n[1],i[1]]),o=n[1]-n[0],s=i[1]-i[0];if(o&&s){var l=(r[0]-a[0])/o,p=(r[1]-a[1])/s,c=a[0]-n[0]*l,d=a[1]-i[0]*p,u=this._transform=[l,0,0,p,c,d];this._invTransform=Is([],u)}}},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},t.prototype.containPoint=function(e){var t=this.getAxis("x"),n=this.getAxis("y");return t.contain(t.toLocalCoord(e[0]))&&n.contain(n.toLocalCoord(e[1]))},t.prototype.containData=function(e){return this.getAxis("x").containData(e[0])&&this.getAxis("y").containData(e[1])},t.prototype.containZone=function(e,t){var n=this.dataToPoint(e),i=this.dataToPoint(t),a=this.getArea(),r=new Ns(n[0],n[1],i[0]-n[0],i[1]-n[1]);return a.intersect(r)},t.prototype.dataToPoint=function(e,t,n){n=n||[];var i=e[0],a=e[1];if(this._transform&&null!=i&&isFinite(i)&&null!=a&&isFinite(a))return Ho(n,e,this._transform);var r=this.getAxis("x"),o=this.getAxis("y");return n[0]=r.toGlobalCoord(r.dataToCoord(i,t)),n[1]=o.toGlobalCoord(o.dataToCoord(a,t)),n},t.prototype.clampData=function(e,t){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,a=n.getExtent(),r=i.getExtent(),o=n.parse(e[0]),s=i.parse(e[1]);return(t=t||[])[0]=Math.min(Math.max(Math.min(a[0],a[1]),o),Math.max(a[0],a[1])),t[1]=Math.min(Math.max(Math.min(r[0],r[1]),s),Math.max(r[0],r[1])),t},t.prototype.pointToData=function(e,t){var n=[];if(this._invTransform)return Ho(n,e,this._invTransform);var i=this.getAxis("x"),a=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(e[0]),t),n[1]=a.coordToData(a.toLocalCoord(e[1]),t),n},t.prototype.getOtherAxis=function(e){return this.getAxis("x"===e.dim?"y":"x")},t.prototype.getArea=function(e){e=e||0;var t=this.getAxis("x").getGlobalExtent(),n=this.getAxis("y").getGlobalExtent(),i=Math.min(t[0],t[1])-e,a=Math.min(n[0],n[1])-e,r=Math.max(t[0],t[1])-i+e,o=Math.max(n[0],n[1])-a+e;return new Ns(i,a,r,o)},t}(pk),mk=function(e){function t(t,n,i,a,r){var o=e.call(this,t,n,i)||this;return o.index=0,o.type=a||"value",o.position=r||"bottom",o}return We(t,e),t.prototype.isHorizontal=function(){var e=this.position;return"top"===e||"bottom"===e},t.prototype.getGlobalExtent=function(e){var t=this.getExtent();return t[0]=this.toGlobalCoord(t[0]),t[1]=this.toGlobalCoord(t[1]),e&&t[0]>t[1]&&t.reverse(),t},t.prototype.pointToData=function(e,t){return this.coordToData(this.toLocalCoord(e["x"===this.dim?0:1]),t)},t.prototype.setCategorySortInfo=function(e){if("category"!==this.type)return!1;this.model.option.categorySortInfo=e,this.scale.setSortInfo(e)},t}(RI);function hk(e,t,n){n=n||{};var i=e.coordinateSystem,a=t.axis,r={},o=a.getAxesOnZeroOf()[0],s=a.position,l=o?"onZero":s,p=a.dim,c=i.getRect(),d=[c.x,c.x+c.width,c.y,c.y+c.height],u={left:0,right:1,top:0,bottom:1,onZero:2},m=t.get("offset")||0,h="x"===p?[d[2]-m,d[3]+m]:[d[0]-m,d[1]+m];if(o){var g=o.toGlobalCoord(o.dataToCoord(0));h[u.onZero]=Math.max(Math.min(g,h[1]),h[0])}r.position=["y"===p?h[u[l]]:d[0],"x"===p?h[u[l]]:d[3]],r.rotation=Math.PI/2*("x"===p?0:1);r.labelDirection=r.tickDirection=r.nameDirection={top:-1,bottom:1,left:-1,right:1}[s],r.labelOffset=o?h[u[s]]-h[u.onZero]:0,t.get(["axisTick","inside"])&&(r.tickDirection=-r.tickDirection),po(n.labelInside,t.get(["axisLabel","inside"]))&&(r.labelDirection=-r.labelDirection);var f=t.get(["axisLabel","rotate"]);return r.labelRotate="top"===l?-f:f,r.z2=1,r}function gk(e){return"cartesian2d"===e.get("coordinateSystem")}function fk(e){var t={xAxisModel:null,yAxisModel:null};return zr(t,(function(n,i){var a=i.replace(/Model$/,""),r=e.getReferringComponents(a,Fd).models[0];t[i]=r})),t}var yk=Math.log;function vk(e,t,n){var i=fT.prototype,a=i.getTicks.call(n),r=i.getTicks.call(n,!0),o=a.length-1,s=i.getInterval.call(n),l=JT(e,t),p=l.extent,c=l.fixMin,d=l.fixMax;if("log"===e.type){var u=yk(e.base);p=[yk(p[0])/u,yk(p[1])/u]}e.setExtent(p[0],p[1]),e.calcNiceExtent({splitNumber:o,fixMin:c,fixMax:d});var m=i.getExtent.call(e);c&&(p[0]=m[0]),d&&(p[1]=m[1]);var h=i.getInterval.call(e),g=p[0],f=p[1];if(c&&d)h=(f-g)/o;else if(c)for(f=p[0]+h*o;fp[0]&&isFinite(g)&&isFinite(p[0]);)h=lT(h),g=p[1]-h*o;else{e.getTicks().length-1>o&&(h=lT(h));var y=h*o;(g=$c((f=Math.ceil(p[1]/h)*h)-y))<0&&p[0]>=0?(g=0,f=$c(y)):f>0&&p[1]<=0&&(f=0,g=-$c(y))}var v=(a[0].value-r[0].value)/s,x=(a[o].value-r[o].value)/s;i.setExtent.call(e,g+h*v,f+h*x),i.setInterval.call(e,h),(v||x)&&i.setNiceExtent.call(e,g+h,f-h)}var xk=function(){function e(e,t,n){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=ck,this._initCartesian(e,t,n),this.model=e}return e.prototype.getRect=function(){return this._rect},e.prototype.update=function(e,t){var n=this._axesMap;function i(e){var t,n=$r(e),i=n.length;if(i){for(var a=[],r=i-1;r>=0;r--){var o=(l=e[+n[r]]).model,s=l.scale;oT(s)&&o.get("alignTicks")&&null==o.get("interval")&&null==o.getTicksGenerator()?a.push(l):(eI(s,o),oT(s)&&!s.isBlank()&&(t=l))}if(a.length){for(;!t&&a.length;){var l;eI((l=a.pop()).scale,l.model),l.scale.isBlank()||(t=l)}a.length&&t&&zr(a,(function(e){vk(e.scale,e.model,t.scale)}))}}}this._updateScale(e,this.model),i(n.x),i(n.y);var a={};zr(n.x,(function(e){wk(n,"y",e,a)})),zr(n.y,(function(e){wk(n,"x",e,a)})),this.resize(this.model,t)},e.prototype.resize=function(e,t,n){var i=e.getBoxLayoutParams(),a=!n&&e.get("containLabel"),r=Sv(i,{width:t.getWidth(),height:t.getHeight()});this._rect=r;var o=this._axesList;function s(){zr(o,(function(e){var t=e.isHorizontal(),n=t?[0,r.width]:[0,r.height],i=e.inverse?1:0;e.setExtent(n[i],n[1-i]),function(e,t){var n=e.getExtent(),i=n[0]+n[1];e.toGlobalCoord="x"===e.dim?function(e){return e+t}:function(e){return i-e+t},e.toLocalCoord="x"===e.dim?function(e){return e-t}:function(e){return i-e+t}}(e,t?r.x:r.y)}))}s(),a&&(zr(o,(function(e){if(!e.model.get(["axisLabel","inside"])){var t=function(e){var t=e.model,n=e.scale;if(t.get(["axisLabel","show"])&&!n.isBlank()){var i,a,r=n.getExtent();a=n instanceof hT?n.count():(i=n.getTicks()).length;var o,s=e.getLabelModel(),l=nI(e),p=1;a>40&&(p=Math.ceil(a/40));for(var c=0;c0&&i>0||n<0&&i<0)}(e)}var Ck=Math.PI,_k=function(){function e(e,t){this.group=new Rc,this.opt=t,this.axisModel=e,Nr(t,{labelOffset:0,nameDirection:1,tickDirection:1,labelDirection:1,silent:!0,handleAutoShown:function(){return!0}});var n=new Rc({x:t.position[0],y:t.position[1],rotation:t.rotation});n.updateTransform(),this._transformGroup=n}return e.prototype.hasBuilder=function(e){return!!Tk[e]},e.prototype.add=function(e){Tk[e](this.opt,this.axisModel,this.group,this._transformGroup)},e.prototype.getGroup=function(){return this.group},e.innerTextLayout=function(e,t,n){var i,a,r=ed(t-e);return td(r)?(a=n>0?"top":"bottom",i="center"):td(r-Ck)?(a=n>0?"bottom":"top",i="center"):(a="middle",i=r>0&&r0?"right":"left":n>0?"left":"right"),{rotation:r,textAlign:i,textVerticalAlign:a}},e.makeAxisEventDataBase=function(e){var t={componentType:e.mainType,componentIndex:e.componentIndex};return t[e.mainType+"Index"]=e.componentIndex,t},e.isLabelSilent=function(e){var t=e.get("tooltip");return e.get("silent")||!(e.get("triggerEvent")||t&&t.show)},e}(),Tk={axisLine:function(e,t,n,i){var a=t.get(["axisLine","show"]);if("auto"===a&&e.handleAutoShown&&(a=e.handleAutoShown("axisLine")),a){var r=t.axis.getExtent(),o=i.transform,s=[r[0],0],l=[r[1],0],p=s[0]>l[0];o&&(Ho(s,s,o),Ho(l,l,o));var c=Br({lineCap:"round"},t.getModel(["axisLine","lineStyle"]).getLineStyle()),d=new Gg({shape:{x1:s[0],y1:s[1],x2:l[0],y2:l[1]},style:c,strokeContainThreshold:e.strokeContainThreshold||5,silent:!0,z2:1});kf(d.shape,d.style.lineWidth),d.anid="line",n.add(d);var u=t.get(["axisLine","symbol"]);if(null!=u){var m=t.get(["axisLine","symbolSize"]);Qr(u)&&(u=[u,u]),(Qr(m)||eo(m))&&(m=[m,m]);var h=VS(t.get(["axisLine","symbolOffset"])||0,m),g=m[0],f=m[1];zr([{rotate:e.rotation+Math.PI/2,offset:h[0],r:0},{rotate:e.rotation-Math.PI/2,offset:h[1],r:Math.sqrt((s[0]-l[0])*(s[0]-l[0])+(s[1]-l[1])*(s[1]-l[1]))}],(function(t,i){if("none"!==u[i]&&null!=u[i]){var a=NS(u[i],-g/2,-f/2,g,f,c.stroke,!0),r=t.r+t.offset,o=p?l:s;a.attr({rotation:t.rotate,x:o[0]+r*Math.cos(e.rotation),y:o[1]-r*Math.sin(e.rotation),silent:!0,z2:11}),n.add(a)}}))}}},axisTickLabel:function(e,t,n,i){var a=function(e,t,n,i){var a=n.axis,r=n.getModel("axisTick"),o=r.get("show");"auto"===o&&i.handleAutoShown&&(o=i.handleAutoShown("axisTick"));if(!o||a.scale.isBlank())return;for(var s=r.getModel("lineStyle"),l=i.tickDirection*r.get("length"),p=kk(a.getTicksCoords(),t.transform,l,Nr(s.getLineStyle(),{stroke:n.get(["axisLine","lineStyle","color"])}),"ticks"),c=0;cd[1]?-1:1,m=["start"===s?d[0]-u*c:"end"===s?d[1]+u*c:(d[0]+d[1])/2,Mk(s)?e.labelOffset+l*c:0],h=t.get("nameRotate");null!=h&&(h=h*Ck/180),Mk(s)?r=_k.innerTextLayout(e.rotation,null!=h?h:e.rotation,l):(r=function(e,t,n,i){var a,r,o=ed(n-e),s=i[0]>i[1],l="start"===t&&!s||"start"!==t&&s;td(o-Ck/2)?(r=l?"bottom":"top",a="center"):td(o-1.5*Ck)?(r=l?"top":"bottom",a="center"):(r="middle",a=o<1.5*Ck&&o>Ck/2?l?"left":"right":l?"right":"left");return{rotation:o,textAlign:a,textVerticalAlign:r}}(e.rotation,s,h||0,d),null!=(o=e.axisNameAvailableWidth)&&(o=Math.abs(o/Math.sin(r.rotation)),!isFinite(o)&&(o=null)));var g=p.getFont(),f=t.get("nameTruncate",!0)||{},y=f.ellipsis,v=po(e.nameTruncateMaxWidth,f.maxWidth,o),x=new Nm({x:m[0],y:m[1],rotation:r.rotation,silent:_k.isLabelSilent(t),style:Zf(p,{text:a,font:g,overflow:"truncate",width:v,ellipsis:y,fill:p.getTextColor()||t.get(["axisLine","lineStyle","color"]),align:p.get("align")||r.textAlign,verticalAlign:p.get("verticalAlign")||r.textVerticalAlign}),z2:1});if(zf({el:x,componentModel:t,itemName:a}),x.__fullText=a,x.anid="name",t.get("triggerEvent")){var b=_k.makeAxisEventDataBase(t);b.targetType="axisName",b.name=a,Xm(x).eventData=b}i.add(x),x.updateTransform(),n.add(x),x.decomposeTransform()}}};function Ik(e){e&&(e.ignore=!0)}function Ek(e,t){var n=e&&e.getBoundingRect().clone(),i=t&&t.getBoundingRect().clone();if(n&&i){var a=bs([]);return _s(a,a,-e.rotation),n.applyTransform(Ss([],a,e.getLocalTransform())),i.applyTransform(Ss([],a,t.getLocalTransform())),n.intersect(i)}}function Mk(e){return"middle"===e||"center"===e}function kk(e,t,n,i,a){for(var r=[],o=[],s=[],l=0;l=0||e===t}function Ok(e){var t=(e.ecModel.getComponent("axisPointer")||{}).coordSysAxesInfo;return t&&t.axesInfo[Fk(e)]}function Ak(e){return!!e.get(["handle","show"])}function Fk(e){return e.type+"||"+e.id}var Rk={},Bk=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.render=function(t,n,i,a){this.axisPointerClass&&function(e){var t=Ok(e);if(t){var n=t.axisPointerModel,i=t.axis.scale,a=n.option,r=n.get("status"),o=n.get("value");null!=o&&(o=i.parse(o));var s=Ak(n);null==r&&(a.status=s?"show":"hide");var l=i.getExtent().slice();l[0]>l[1]&&l.reverse(),(null==o||o>l[1])&&(o=l[1]),o0&&!d.min?d.min=0:null!=d.min&&d.min<0&&!d.max&&(d.max=0);var u=o;null!=d.color&&(u=Nr({color:d.color},o));var m=Fr(Ar(d),{boundaryGap:e,splitNumber:t,scale:n,axisLine:i,axisTick:a,axisLabel:r,name:d.text,showName:s,nameLocation:"end",nameGap:p,nameTextStyle:u,triggerEvent:c},!1);if(Qr(l)){var h=m.name;m.name=l.replace("{value}",null!=h?h:"")}else Zr(l)&&(m.name=l(m.name,m));var g=new xy(m,null,this.ecModel);return qr(g,lI.prototype),g.mainType="radar",g.componentIndex=this.componentIndex,g}),this);this._indicatorModels=d},t.prototype.getIndicatorModels=function(){return this._indicatorModels},t.type="radar",t.defaultOption={z:0,center:["50%","50%"],radius:"75%",startAngle:90,axisName:{show:!0},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:Fr({lineStyle:{color:"#bbb"}},tP.axisLine),axisLabel:nP(tP.axisLabel,!1),axisTick:nP(tP.axisTick,!1),splitLine:nP(tP.splitLine,!0),splitArea:nP(tP.splitArea,!0),indicator:[]},t}(kv),aP=["axisLine","axisTickLabel","axisName"],rP=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.render=function(e,t,n){this.group.removeAll(),this._buildAxes(e),this._buildSplitLineAndArea(e)},t.prototype._buildAxes=function(e){var t=e.coordinateSystem;zr(Ur(t.getIndicatorAxes(),(function(e){var n=e.model.get("showName")?e.name:"";return new _k(e.model,{axisName:n,position:[t.cx,t.cy],rotation:e.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})})),(function(e){zr(aP,e.add,e),this.group.add(e.getGroup())}),this)},t.prototype._buildSplitLineAndArea=function(e){var t=e.coordinateSystem,n=t.getIndicatorAxes();if(n.length){var i=e.get("shape"),a=e.getModel("splitLine"),r=e.getModel("splitArea"),o=a.getModel("lineStyle"),s=r.getModel("areaStyle"),l=a.get("show"),p=r.get("show"),c=o.get("color"),d=s.get("color"),u=Xr(c)?c:[c],m=Xr(d)?d:[d],h=[],g=[];if("circle"===i)for(var f=n[0].getTicksCoords(),y=t.cx,v=t.cy,x=0;x3?1.4:a>1?1.2:1.1;hP(this,"zoom","zoomOnMouseWheel",e,{scale:i>0?s:1/s,originX:r,originY:o,isAvailableBehavior:null})}if(n){var l=Math.abs(i);hP(this,"scrollMove","moveOnMouseWheel",e,{scrollDelta:(i>0?1:-1)*(l>3?.4:l>1?.15:.05),originX:r,originY:o,isAvailableBehavior:null})}}},t.prototype._pinchHandler=function(e){dP(this._zr,"globalPan")||hP(this,"zoom",null,e,{scale:e.pinchScale>1?1.1:1/1.1,originX:e.pinchX,originY:e.pinchY,isAvailableBehavior:null})},t}(Xo);function hP(e,t,n,i,a){e.pointerChecker&&e.pointerChecker(i,a.originX,a.originY)&&(gs(i.event),gP(e,t,n,i,a))}function gP(e,t,n,i,a){a.isAvailableBehavior=Kr(fP,null,n,i),e.trigger(t,a)}function fP(e,t,n){var i=n[e];return!e||i&&(!Qr(i)||t.event[i+"Key"])}function yP(e,t,n){var i=e.target;i.x+=t,i.y+=n,i.dirty()}function vP(e,t,n,i){var a=e.target,r=e.zoomLimit,o=e.zoom=e.zoom||1;if(o*=t,r){var s=r.min||0,l=r.max||1/0;o=Math.max(Math.min(l,o),s)}var p=o/e.zoom;e.zoom=o,a.x-=(n-a.x)*(p-1),a.y-=(i-a.y)*(p-1),a.scaleX*=p,a.scaleY*=p,a.dirty()}var xP,bP={axisPointer:1,tooltip:1,brush:1};function wP(e,t,n){var i=t.getComponentByElement(e.topTarget),a=i&&i.coordinateSystem;return i&&i!==n&&!bP.hasOwnProperty(i.mainType)&&a&&a.model!==n}function SP(e){Qr(e)&&(e=(new DOMParser).parseFromString(e,"text/xml"));var t=e;for(9===t.nodeType&&(t=t.firstChild);"svg"!==t.nodeName.toLowerCase()||1!==t.nodeType;)t=t.nextSibling;return t}var CP={fill:"fill",stroke:"stroke","stroke-width":"lineWidth",opacity:"opacity","fill-opacity":"fillOpacity","stroke-opacity":"strokeOpacity","stroke-dasharray":"lineDash","stroke-dashoffset":"lineDashOffset","stroke-linecap":"lineCap","stroke-linejoin":"lineJoin","stroke-miterlimit":"miterLimit","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","text-anchor":"textAlign",visibility:"visibility",display:"display"},_P=$r(CP),TP={"alignment-baseline":"textBaseline","stop-color":"stopColor"},IP=$r(TP),EP=function(){function e(){this._defs={},this._root=null}return e.prototype.parse=function(e,t){t=t||{};var n=SP(e);this._defsUsePending=[];var i=new Rc;this._root=i;var a=[],r=n.getAttribute("viewBox")||"",o=parseFloat(n.getAttribute("width")||t.width),s=parseFloat(n.getAttribute("height")||t.height);isNaN(o)&&(o=null),isNaN(s)&&(s=null),AP(n,i,null,!0,!1);for(var l,p,c=n.firstChild;c;)this._parseNode(c,i,a,null,!1,!1),c=c.nextSibling;if(function(e,t){for(var n=0;n=4&&(l={x:parseFloat(d[0]||0),y:parseFloat(d[1]||0),width:parseFloat(d[2]),height:parseFloat(d[3])})}if(l&&null!=o&&null!=s&&(p=zP(l,{x:0,y:0,width:o,height:s}),!t.ignoreViewBox)){var u=i;(i=new Rc).add(u),u.scaleX=u.scaleY=p.scale,u.x=p.x,u.y=p.y}return t.ignoreRootClip||null==o||null==s||i.setClipPath(new Fm({shape:{x:0,y:0,width:o,height:s}})),{root:i,width:o,height:s,viewBoxRect:l,viewBoxTransform:p,named:a}},e.prototype._parseNode=function(e,t,n,i,a,r){var o,s=e.nodeName.toLowerCase(),l=i;if("defs"===s&&(a=!0),"text"===s&&(r=!0),"defs"===s||"switch"===s)o=t;else{if(!a){var p=xP[s];if(p&&Eo(xP,s)){o=p.call(this,e,t);var c=e.getAttribute("name");if(c){var d={name:c,namedFrom:null,svgNodeTagLower:s,el:o};n.push(d),"g"===s&&(l=d)}else i&&n.push({name:i.name,namedFrom:i,svgNodeTagLower:s,el:o});t.add(o)}}var u=MP[s];if(u&&Eo(MP,s)){var m=u.call(this,e),h=e.getAttribute("id");h&&(this._defs[h]=m)}}if(o&&o.isGroup)for(var g=e.firstChild;g;)1===g.nodeType?this._parseNode(g,o,n,l,a,r):3===g.nodeType&&r&&this._parseText(g,o),g=g.nextSibling},e.prototype._parseText=function(e,t){var n=new _m({style:{text:e.textContent},silent:!0,x:this._textX||0,y:this._textY||0});DP(t,n),AP(e,n,this._defsUsePending,!1,!1),function(e,t){var n=t.__selfStyle;if(n){var i=n.textBaseline,a=i;i&&"auto"!==i?"baseline"===i?a="alphabetic":"before-edge"===i||"text-before-edge"===i?a="top":"after-edge"===i||"text-after-edge"===i?a="bottom":"central"!==i&&"mathematical"!==i||(a="middle"):a="alphabetic",e.style.textBaseline=a}var r=t.__inheritedStyle;if(r){var o=r.textAlign,s=o;o&&("middle"===o&&(s="center"),e.style.textAlign=s)}}(n,t);var i=n.style,a=i.fontSize;a&&a<9&&(i.fontSize=9,n.scaleX*=a/9,n.scaleY*=a/9);var r=(i.fontSize||i.fontFamily)&&[i.fontStyle,i.fontWeight,(i.fontSize||12)+"px",i.fontFamily||"sans-serif"].join(" ");i.font=r;var o=n.getBoundingRect();return this._textX+=o.width,t.add(n),n},e.internalField=void(xP={g:function(e,t){var n=new Rc;return DP(t,n),AP(e,n,this._defsUsePending,!1,!1),n},rect:function(e,t){var n=new Fm;return DP(t,n),AP(e,n,this._defsUsePending,!1,!1),n.setShape({x:parseFloat(e.getAttribute("x")||"0"),y:parseFloat(e.getAttribute("y")||"0"),width:parseFloat(e.getAttribute("width")||"0"),height:parseFloat(e.getAttribute("height")||"0")}),n.silent=!0,n},circle:function(e,t){var n=new hg;return DP(t,n),AP(e,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(e.getAttribute("cx")||"0"),cy:parseFloat(e.getAttribute("cy")||"0"),r:parseFloat(e.getAttribute("r")||"0")}),n.silent=!0,n},line:function(e,t){var n=new Gg;return DP(t,n),AP(e,n,this._defsUsePending,!1,!1),n.setShape({x1:parseFloat(e.getAttribute("x1")||"0"),y1:parseFloat(e.getAttribute("y1")||"0"),x2:parseFloat(e.getAttribute("x2")||"0"),y2:parseFloat(e.getAttribute("y2")||"0")}),n.silent=!0,n},ellipse:function(e,t){var n=new fg;return DP(t,n),AP(e,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(e.getAttribute("cx")||"0"),cy:parseFloat(e.getAttribute("cy")||"0"),rx:parseFloat(e.getAttribute("rx")||"0"),ry:parseFloat(e.getAttribute("ry")||"0")}),n.silent=!0,n},polygon:function(e,t){var n,i=e.getAttribute("points");i&&(n=OP(i));var a=new Bg({shape:{points:n||[]},silent:!0});return DP(t,a),AP(e,a,this._defsUsePending,!1,!1),a},polyline:function(e,t){var n,i=e.getAttribute("points");i&&(n=OP(i));var a=new Lg({shape:{points:n||[]},silent:!0});return DP(t,a),AP(e,a,this._defsUsePending,!1,!1),a},image:function(e,t){var n=new Em;return DP(t,n),AP(e,n,this._defsUsePending,!1,!1),n.setStyle({image:e.getAttribute("xlink:href")||e.getAttribute("href"),x:+e.getAttribute("x"),y:+e.getAttribute("y"),width:+e.getAttribute("width"),height:+e.getAttribute("height")}),n.silent=!0,n},text:function(e,t){var n=e.getAttribute("x")||"0",i=e.getAttribute("y")||"0",a=e.getAttribute("dx")||"0",r=e.getAttribute("dy")||"0";this._textX=parseFloat(n)+parseFloat(a),this._textY=parseFloat(i)+parseFloat(r);var o=new Rc;return DP(t,o),AP(e,o,this._defsUsePending,!1,!0),o},tspan:function(e,t){var n=e.getAttribute("x"),i=e.getAttribute("y");null!=n&&(this._textX=parseFloat(n)),null!=i&&(this._textY=parseFloat(i));var a=e.getAttribute("dx")||"0",r=e.getAttribute("dy")||"0",o=new Rc;return DP(t,o),AP(e,o,this._defsUsePending,!1,!0),this._textX+=parseFloat(a),this._textY+=parseFloat(r),o},path:function(e,t){var n=ug(e.getAttribute("d")||"");return DP(t,n),AP(e,n,this._defsUsePending,!1,!1),n.silent=!0,n}}),e}(),MP={lineargradient:function(e){var t=parseInt(e.getAttribute("x1")||"0",10),n=parseInt(e.getAttribute("y1")||"0",10),i=parseInt(e.getAttribute("x2")||"10",10),a=parseInt(e.getAttribute("y2")||"0",10),r=new Xg(t,n,i,a);return kP(e,r),PP(e,r),r},radialgradient:function(e){var t=parseInt(e.getAttribute("cx")||"0",10),n=parseInt(e.getAttribute("cy")||"0",10),i=parseInt(e.getAttribute("r")||"0",10),a=new Zg(t,n,i);return kP(e,a),PP(e,a),a}};function kP(e,t){"userSpaceOnUse"===e.getAttribute("gradientUnits")&&(t.global=!0)}function PP(e,t){for(var n=e.firstChild;n;){if(1===n.nodeType&&"stop"===n.nodeName.toLocaleLowerCase()){var i=n.getAttribute("offset"),a=void 0;a=i&&i.indexOf("%")>0?parseInt(i,10)/100:i?parseFloat(i):0;var r={};GP(n,r,r);var o=r.stopColor||n.getAttribute("stop-color")||"#000000";t.colorStops.push({offset:a,color:o})}n=n.nextSibling}}function DP(e,t){e&&e.__inheritedStyle&&(t.__inheritedStyle||(t.__inheritedStyle={}),Nr(t.__inheritedStyle,e.__inheritedStyle))}function OP(e){for(var t=NP(e),n=[],i=0;i0;r-=2){var o=i[r],s=i[r-1],l=NP(o);switch(a=a||[1,0,0,1,0,0],s){case"translate":Cs(a,a,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":Ts(a,a,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":_s(a,a,-parseFloat(l[0])*VP,[parseFloat(l[1]||"0"),parseFloat(l[2]||"0")]);break;case"skewX":Ss(a,[1,0,Math.tan(parseFloat(l[0])*VP),1,0,0],a);break;case"skewY":Ss(a,[1,Math.tan(parseFloat(l[0])*VP),0,1,0,0],a);break;case"matrix":a[0]=parseFloat(l[0]),a[1]=parseFloat(l[1]),a[2]=parseFloat(l[2]),a[3]=parseFloat(l[3]),a[4]=parseFloat(l[4]),a[5]=parseFloat(l[5])}}t.setLocalTransform(a)}}(e,t),GP(e,o,s),i||function(e,t,n){for(var i=0;i<_P.length;i++){var a=_P[i];null!=(r=e.getAttribute(a))&&(t[CP[a]]=r)}for(i=0;i0,h={api:n,geo:s,mapOrGeoModel:e,data:o,isVisualEncodedByVisualMap:m,isGeo:r,transformInfoRaw:d};"geoJSON"===s.resourceType?this._buildGeoJSON(h):"geoSVG"===s.resourceType&&this._buildSVG(h),this._updateController(e,t,n),this._updateMapSelectHandler(e,l,n,i)},e.prototype._buildGeoJSON=function(e){var t=this._regionsGroupByName=Co(),n=Co(),i=this._regionsGroup,a=e.transformInfoRaw,r=e.mapOrGeoModel,o=e.data,s=e.geo.projection,l=s&&s.stream;function p(e,t){return t&&(e=t(e)),e&&[e[0]*a.scaleX+a.x,e[1]*a.scaleY+a.y]}function c(e){for(var t=[],n=!l&&s&&s.project,i=0;i=0)&&(u=a);var m=o?{normal:{align:"center",verticalAlign:"middle"}}:null;Yf(t,Xf(i),{labelFetcher:u,labelDataIndex:d,defaultText:n},m);var h=t.getTextContent();if(h&&(rD(h).ignore=h.ignore,t.textConfig&&o)){var g=t.getBoundingRect().clone();t.textConfig.layoutRect=g,t.textConfig.position=[(o[0]-g.x)/g.width*100+"%",(o[1]-g.y)/g.height*100+"%"]}t.disableLabelAnimation=!0}else t.removeTextContent(),t.removeTextConfig(),t.disableLabelAnimation=null}function dD(e,t,n,i,a,r){e.data?e.data.setItemGraphicEl(r,t):Xm(t).eventData={componentType:"geo",componentIndex:a.componentIndex,geoIndex:a.componentIndex,name:n,region:i&&i.option||{}}}function uD(e,t,n,i,a){e.data||zf({el:t,componentModel:a,itemName:n,itemTooltipOption:i.get("tooltip")})}function mD(e,t,n,i,a){t.highDownSilentOnTouch=!!a.get("selectedMode");var r=i.getModel("emphasis"),o=r.get("focus");return Vh(t,o,r.get("blurScope"),r.get("disabled")),e.isGeo&&function(e,t,n){var i=Xm(e);i.componentMainType=t.mainType,i.componentIndex=t.componentIndex,i.componentHighDownName=n}(t,a,n),o}function hD(e,t,n){var i,a=[];function r(){i=[]}function o(){i.length&&(a.push(i),i=[])}var s=t({polygonStart:r,polygonEnd:o,lineStart:r,lineEnd:o,point:function(e,t){isFinite(e)&&isFinite(t)&&i.push([e,t])},sphere:function(){}});return!n&&s.polygonStart(),zr(e,(function(e){s.lineStart();for(var t=0;t-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2),n},t.type="series.map",t.dependencies=["geo"],t.layoutMode="box",t.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:"#000"},itemStyle:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{areaColor:"rgba(255,215,0,0.8)"}},select:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{color:"rgba(255,215,0,0.8)"}},nameProperty:"name"}}(mw);var gD=Ho,fD=function(e){function t(t){var n=e.call(this)||this;return n.type="view",n.dimensions=["x","y"],n._roamTransformable=new mc,n._rawTransformable=new mc,n.name=t,n}return We(t,e),t.prototype.setBoundingRect=function(e,t,n,i){return this._rect=new Ns(e,t,n,i),this._rect},t.prototype.getBoundingRect=function(){return this._rect},t.prototype.setViewRect=function(e,t,n,i){this._transformTo(e,t,n,i),this._viewRect=new Ns(e,t,n,i)},t.prototype._transformTo=function(e,t,n,i){var a=this.getBoundingRect(),r=this._rawTransformable;r.transform=a.calculateTransform(new Ns(e,t,n,i));var o=r.parent;r.parent=null,r.decomposeTransform(),r.parent=o,this._updateTransform()},t.prototype.setCenter=function(e,t){e&&(this._center=[Wc(e[0],t.getWidth()),Wc(e[1],t.getHeight())],this._updateCenterAndZoom())},t.prototype.setZoom=function(e){e=e||1;var t=this.zoomLimit;t&&(null!=t.max&&(e=Math.min(t.max,e)),null!=t.min&&(e=Math.max(t.min,e))),this._zoom=e,this._updateCenterAndZoom()},t.prototype.getDefaultCenter=function(){var e=this.getBoundingRect();return[e.x+e.width/2,e.y+e.height/2]},t.prototype.getCenter=function(){return this._center||this.getDefaultCenter()},t.prototype.getZoom=function(){return this._zoom||1},t.prototype.getRoamTransform=function(){return this._roamTransformable.getLocalTransform()},t.prototype._updateCenterAndZoom=function(){var e=this._rawTransformable.getLocalTransform(),t=this._roamTransformable,n=this.getDefaultCenter(),i=this.getCenter(),a=this.getZoom();i=Ho([],i,e),n=Ho([],n,e),t.originX=i[0],t.originY=i[1],t.x=n[0]-i[0],t.y=n[1]-i[1],t.scaleX=t.scaleY=a,this._updateTransform()},t.prototype._updateTransform=function(){var e=this._roamTransformable,t=this._rawTransformable;t.parent=e,e.updateTransform(),t.updateTransform(),ws(this.transform||(this.transform=[]),t.transform||[1,0,0,1,0,0]),this._rawTransform=t.getLocalTransform(),this.invTransform=this.invTransform||[],Is(this.invTransform,this.transform),this.decomposeTransform()},t.prototype.getTransformInfo=function(){var e=this._rawTransformable,t=this._roamTransformable,n=new mc;return n.transform=t.transform,n.decomposeTransform(),{roam:{x:n.x,y:n.y,scaleX:n.scaleX,scaleY:n.scaleY},raw:{x:e.x,y:e.y,scaleX:e.scaleX,scaleY:e.scaleY}}},t.prototype.getViewRect=function(){return this._viewRect},t.prototype.getViewRectAfterRoam=function(){var e=this.getBoundingRect().clone();return e.applyTransform(this.transform),e},t.prototype.dataToPoint=function(e,t,n){var i=t?this._rawTransform:this.transform;return n=n||[],i?gD(n,e,i):Do(n,e)},t.prototype.pointToData=function(e){var t=this.invTransform;return t?gD([],e,t):[e[0],e[1]]},t.prototype.convertToPixel=function(e,t,n){var i=yD(t);return i===this?i.dataToPoint(n):null},t.prototype.convertFromPixel=function(e,t,n){var i=yD(t);return i===this?i.pointToData(n):null},t.prototype.containPoint=function(e){return this.getViewRectAfterRoam().contain(e[0],e[1])},t.dimensions=["x","y"],t}(mc);function yD(e){var t=e.seriesModel;return t?t.coordinateSystem:null}var vD={geoJSON:{aspectScale:.75,invertLongitute:!0},geoSVG:{aspectScale:1,invertLongitute:!1}},xD=["lng","lat"],bD=function(e){function t(t,n,i){var a=e.call(this,t)||this;a.dimensions=xD,a.type="geo",a._nameCoordMap=Co(),a.map=n;var r,o=i.projection,s=eD(n,i.nameMap,i.nameProperty),l=JP(n),p=(a.resourceType=l?l.type:null,a.regions=s.regions),c=vD[l.type];if(a._regionsMap=s.regionsMap,a.regions=s.regions,a.projection=o,o)for(var d=0;d1?(m.width=u,m.height=u/x):(m.height=u,m.width=u*x),m.y=d[1]-m.height/2,m.x=d[0]-m.width/2;else{var w=e.getBoxLayoutParams();w.aspect=x,m=Sv(w,{width:y,height:v})}this.setViewRect(m.x,m.y,m.width,m.height),this.setCenter(e.get("center"),t),this.setZoom(e.get("zoom"))}qr(bD,fD);var CD=function(){function e(){this.dimensions=xD}return e.prototype.create=function(e,t){var n=[];function i(e){return{nameProperty:e.get("nameProperty"),aspectScale:e.get("aspectScale"),projection:e.get("projection")}}e.eachComponent("geo",(function(e,a){var r=e.get("map"),o=new bD(r+a,r,Br({nameMap:e.get("nameMap")},i(e)));o.zoomLimit=e.get("scaleLimit"),n.push(o),e.coordinateSystem=o,o.model=e,o.resize=SD,o.resize(e,t)})),e.eachSeries((function(e){if("geo"===e.get("coordinateSystem")){var t=e.get("geoIndex")||0;e.coordinateSystem=n[t]}}));var a={};return e.eachSeriesByType("map",(function(e){if(!e.getHostGeoModel()){var t=e.getMapType();a[t]=a[t]||[],a[t].push(e)}})),zr(a,(function(e,a){var r=Ur(e,(function(e){return e.get("nameMap")})),o=new bD(a,a,Br({nameMap:Rr(r)},i(e[0])));o.zoomLimit=po.apply(null,Ur(e,(function(e){return e.get("scaleLimit")}))),n.push(o),o.resize=SD,o.resize(e[0],t),zr(e,(function(e){e.coordinateSystem=o,function(e,t){zr(t.get("geoCoord"),(function(t,n){e.addGeoCoord(n,t)}))}(o,e)}))})),n},e.prototype.getFilledRegions=function(e,t,n,i){for(var a=(e||[]).slice(),r=Co(),o=0;ov.x)||(b-=Math.PI);var C=w?"left":"right",_=s.getModel("label"),T=_.get("rotate"),I=T*(Math.PI/180),E=f.getTextContent();E&&(f.setTextConfig({position:_.get("position")||C,rotation:null==T?-b:I,origin:"center"}),E.setStyle("verticalAlign","middle"))}var M=s.get(["emphasis","focus"]),k="relative"===M?_o(o.getAncestorsIndices(),o.getDescendantIndices()):"ancestor"===M?o.getAncestorsIndices():"descendant"===M?o.getDescendantIndices():null;k&&(Xm(n).focus=k),function(e,t,n,i,a,r,o,s){var l=t.getModel(),p=e.get("edgeShape"),c=e.get("layout"),d=e.getOrient(),u=e.get(["lineStyle","curveness"]),m=e.get("edgeForkPosition"),h=l.getModel("lineStyle").getLineStyle(),g=i.__edge;if("curve"===p)t.parentNode&&t.parentNode!==n&&(g||(g=i.__edge=new Hg({shape:AD(c,d,u,a,a)})),pf(g,{shape:AD(c,d,u,r,o)},e));else if("polyline"===p)if("orthogonal"===c){if(t!==n&&t.children&&0!==t.children.length&&!0===t.isExpand){for(var f=t.children,y=[],v=0;vt&&(t=i.height)}this.height=t+1},e.prototype.getNodeById=function(e){if(this.getId()===e)return this;for(var t=0,n=this.children,i=n.length;t=0&&this.hostTree.data.setItemLayout(this.dataIndex,e,t)},e.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},e.prototype.getModel=function(e){if(!(this.dataIndex<0))return this.hostTree.data.getItemModel(this.dataIndex).getModel(e)},e.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},e.prototype.setVisual=function(e,t){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,e,t)},e.prototype.getVisual=function(e){return this.hostTree.data.getItemVisual(this.dataIndex,e)},e.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},e.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},e.prototype.getChildIndex=function(){if(this.parentNode){for(var e=this.parentNode.children,t=0;t=0){var i=n.getData().tree.root,a=e.targetNode;if(Qr(a)&&(a=i.getNodeById(a)),a&&i.contains(a))return{node:a};var r=e.targetNodeId;if(null!=r&&(a=i.getNodeById(r)))return{node:a}}}function WD(e){for(var t=[];e;)(e=e.parentNode)&&t.push(e);return t.reverse()}function $D(e,t){return Lr(WD(e),t)>=0}function KD(e,t){for(var n=[];e;){var i=e.dataIndex;n.push({name:e.name,dataIndex:i,value:t.getRawValue(i)}),e=e.parentNode}return n.reverse(),n}!function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.hasSymbolVisual=!0,t.ignoreStyleOnData=!0,t}We(t,e),t.prototype.getInitialData=function(e){var t={name:e.name,children:e.data},n=e.leaves||{},i=new xy(n,this,this.ecModel),a=jD.createTree(t,this,(function(e){e.wrapMethod("getItemModel",(function(e,t){var n=a.getNodeByDataIndex(t);return n&&n.children.length&&n.isExpand||(e.parentModel=i),e}))}));var r=0;a.eachNode("preorder",(function(e){e.depth>r&&(r=e.depth)}));var o=e.expandAndCollapse&&e.initialTreeDepth>=0?e.initialTreeDepth:r;return a.root.eachNode("preorder",(function(e){var t=e.hostTree.data.getRawDataItem(e.dataIndex);e.isExpand=t&&null!=t.collapsed?!t.collapsed:e.depth<=o})),a.data},t.prototype.getOrient=function(){var e=this.get("orient");return"horizontal"===e?e="LR":"vertical"===e&&(e="TB"),e},t.prototype.setZoom=function(e){this.option.zoom=e},t.prototype.setCenter=function(e){this.option.center=e},t.prototype.formatTooltip=function(e,t,n){for(var i=this.getData().tree,a=i.root.children[0],r=i.getNodeByDataIndex(e),o=r.getValue(),s=r.name;r&&r!==a;)s=r.parentNode.name+"."+s,r=r.parentNode;return Zb("nameValue",{name:s,value:o,noValue:isNaN(o)||null==o})},t.prototype.getDataParams=function(t){var n=e.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(t);return n.treeAncestors=KD(i,this),n.collapsed=!i.isExpand,n},t.type="series.tree",t.layoutMode="box",t.defaultOption={z:2,coordinateSystem:"view",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:"#ccc",width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500}}(mw);function YD(e){var t=e.getData().tree,n={};t.eachNode((function(t){for(var i=t;i&&i.depth>1;)i=i.parentNode;var a=ax(e.ecModel,i.name||i.dataIndex+"",n);t.setVisual("decal",a)}))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.preventUsingHoverLayer=!0,n}We(t,e),t.prototype.getInitialData=function(e,t){var n={name:e.name,children:e.data};XD(n);var i=e.levels||[],a=this.designatedVisualItemStyle={},r=new xy({itemStyle:a},this,t);i=e.levels=function(e,t){var n,i,a=vd(t.get("color")),r=vd(t.get(["aria","decal","decals"]));if(!a)return;e=e||[],zr(e,(function(e){var t=new xy(e),a=t.get("color"),r=t.get("decal");(t.get(["itemStyle","color"])||a&&"none"!==a)&&(n=!0),(t.get(["itemStyle","decal"])||r&&"none"!==r)&&(i=!0)}));var o=e[0]||(e[0]={});n||(o.color=a.slice());!i&&r&&(o.decal=r.slice());return e}(i,t);var o=Ur(i||[],(function(e){return new xy(e,r,t)}),this),s=jD.createTree(n,this,(function(e){e.wrapMethod("getItemModel",(function(e,t){var n=s.getNodeByDataIndex(t),i=n?o[n.depth]:null;return e.parentModel=i||r,e}))}));return s.data},t.prototype.optionUpdated=function(){this.resetViewRoot()},t.prototype.formatTooltip=function(e,t,n){var i=this.getData(),a=this.getRawValue(e);return Zb("nameValue",{name:i.getName(e),value:a})},t.prototype.getDataParams=function(t){var n=e.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(t);return n.treeAncestors=KD(i,this),n.treePathInfo=n.treeAncestors,n},t.prototype.setLayoutInfo=function(e){this.layoutInfo=this.layoutInfo||{},Br(this.layoutInfo,e)},t.prototype.mapIdToIndex=function(e){var t=this._idIndexMap;t||(t=this._idIndexMap=Co(),this._idIndexMapCount=0);var n=t.get(e);return null==n&&t.set(e,n=this._idIndexMapCount++),n},t.prototype.getViewRoot=function(){return this._viewRoot},t.prototype.resetViewRoot=function(e){e?this._viewRoot=e:e=this._viewRoot;var t=this.getRawData().tree.root;e&&(e===t||t.contains(e))||(this._viewRoot=t)},t.prototype.enableAriaDecal=function(){YD(this)},t.type="series.treemap",t.layoutMode="box",t.defaultOption={progressive:0,left:"center",top:"middle",width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{color:"rgba(0,0,0,0.7)",textStyle:{color:"#fff"}},emphasis:{itemStyle:{color:"rgba(0,0,0,0.9)"}}},label:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]}}(mw);function XD(e){var t=0;zr(e.children,(function(e){XD(e);var n=e.value;Xr(n)&&(n=n[0]),t+=n}));var n=e.value;Xr(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=t),n<0&&(n=0),Xr(e.value)?e.value[0]=n:e.value=n}var ZD=function(){function e(e){this.group=new Rc,e.add(this.group)}return e.prototype.render=function(e,t,n,i){var a=e.getModel("breadcrumb"),r=this.group;if(r.removeAll(),a.get("show")&&n){var o=a.getModel("itemStyle"),s=a.getModel("emphasis"),l=o.getModel("textStyle"),p=s.getModel(["itemStyle","textStyle"]),c={pos:{left:a.get("left"),right:a.get("right"),top:a.get("top"),bottom:a.get("bottom")},box:{width:t.getWidth(),height:t.getHeight()},emptyItemWidth:a.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(n,c,l),this._renderContent(e,c,o,s,l,p,i),Cv(r,c.pos,c.box)}},e.prototype._prepare=function(e,t,n){for(var i=e;i;i=i.parentNode){var a=Id(i.getModel().get("name"),""),r=n.getTextRect(a),o=Math.max(r.width+16,t.emptyItemWidth);t.totalWidth+=o+8,t.renderList.push({node:i,text:a,width:o})}},e.prototype._renderContent=function(e,t,n,i,a,r,o){for(var s,l,p,c,d,u,m,h,g,f=0,y=t.emptyItemWidth,v=e.get(["breadcrumb","height"]),x=(s=t.pos,l=t.box,c=l.width,d=l.height,u=Wc(s.left,c),m=Wc(s.top,d),h=Wc(s.right,c),g=Wc(s.bottom,d),(isNaN(u)||isNaN(parseFloat(s.left)))&&(u=0),(isNaN(h)||isNaN(parseFloat(s.right)))&&(h=c),(isNaN(m)||isNaN(parseFloat(s.top)))&&(m=0),(isNaN(g)||isNaN(parseFloat(s.bottom)))&&(g=d),p=pv(p||0),{width:Math.max(h-u-p[1]-p[3],0),height:Math.max(g-m-p[0]-p[2],0)}),b=t.totalWidth,w=t.renderList,S=i.getModel("itemStyle").getItemStyle(),C=w.length-1;C>=0;C--){var _=w[C],T=_.node,I=_.width,E=_.text;b>x.width&&(b-=I-y,I=y,E=null);var M=new Bg({shape:{points:QD(f,0,I,v,C===w.length-1,0===C)},style:Nr(n.getItemStyle(),{lineJoin:"bevel"}),textContent:new Nm({style:Zf(a,{text:E})}),textConfig:{position:"inside"},z2:1e5,onclick:Yr(o,T)});M.disableLabelAnimation=!0,M.getTextContent().ensureState("emphasis").style=Zf(r,{text:E}),M.ensureState("emphasis").style=S,Vh(M,i.get("focus"),i.get("blurScope"),i.get("disabled")),this.group.add(M),JD(M,e,T),f+=I+8}},e.prototype.remove=function(){this.group.removeAll()},e}();function QD(e,t,n,i,a,r){var o=[[a?e:e-5,t],[e+n,t],[e+n,t+i],[a?e:e-5,t+i]];return!r&&o.splice(2,0,[e+n+5,t+i/2]),!a&&o.push([e,t+i/2]),o}function JD(e,t,n){Xm(e).eventData={componentType:"series",componentSubType:"treemap",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&KD(n,t)}}var eO=function(){function e(){this._storage=[],this._elExistsMap={}}return e.prototype.add=function(e,t,n,i,a){return!this._elExistsMap[e.id]&&(this._elExistsMap[e.id]=!0,this._storage.push({el:e,target:t,duration:n,delay:i,easing:a}),!0)},e.prototype.finished=function(e){return this._finishedCallback=e,this},e.prototype.start=function(){for(var e=this,t=this._storage.length,n=function(){--t<=0&&(e._storage.length=0,e._elExistsMap={},e._finishedCallback&&e._finishedCallback())},i=0,a=this._storage.length;i3||Math.abs(e.dy)>3)){var t=this.seriesModel.getData().tree.root;if(!t)return;var n=t.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+e.dx,y:n.y+e.dy,width:n.width,height:n.height}})}},t.prototype._onZoom=function(e){var t=e.originX,n=e.originY;if("animating"!==this._state){var i=this.seriesModel.getData().tree.root;if(!i)return;var a=i.getLayout();if(!a)return;var r=new Ns(a.x,a.y,a.width,a.height),o=this.seriesModel.layoutInfo,s=[1,0,0,1,0,0];Cs(s,s,[-(t-=o.x),-(n-=o.y)]),Ts(s,s,[e.scale,e.scale]),Cs(s,s,[t,n]),r.applyTransform(s),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:r.x,y:r.y,width:r.width,height:r.height}})}},t.prototype._initEvents=function(e){var t=this;e.on("click",(function(e){if("ready"===t._state){var n=t.seriesModel.get("nodeClick",!0);if(n){var i=t.findTarget(e.offsetX,e.offsetY);if(i){var a=i.node;if(a.getLayout().isLeafRoot)t._rootToNode(i);else if("zoomToNode"===n)t._zoomToNode(i);else if("link"===n){var r=a.hostTree.data.getItemModel(a.dataIndex),o=r.get("link",!0),s=r.get("target",!0)||"blank";o&&fv(o,s)}}}}}),this)},t.prototype._renderBreadcrumb=function(e,t,n){var i=this;n||(n=null!=e.get("leafDepth",!0)?{node:e.getViewRoot()}:this.findTarget(t.getWidth()/2,t.getHeight()/2))||(n={node:e.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new ZD(this.group))).render(e,t,n.node,(function(t){"animating"!==i._state&&($D(e.getViewRoot(),t)?i._rootToNode({node:t}):i._zoomToNode({node:t}))}))},t.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage={nodeGroup:[],background:[],content:[]},this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},t.prototype.dispose=function(){this._clearController()},t.prototype._zoomToNode=function(e){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:e.node})},t.prototype._rootToNode=function(e){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:e.node})},t.prototype.findTarget=function(e,t){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},(function(i){var a=this._storage.background[i.getRawIndex()];if(a){var r=a.transformCoordToLocal(e,t),o=a.shape;if(!(o.x<=r[0]&&r[0]<=o.x+o.width&&o.y<=r[1]&&r[1]<=o.y+o.height))return!1;n={node:i,offsetX:r[0],offsetY:r[1]}}}),this),n},t.type="treemap"}(Tw);var lO=zr,pO=to,cO=-1,dO=function(){function e(t){var n=t.mappingMethod,i=t.type,a=this.option=Ar(t);this.type=i,this.mappingMethod=n,this._normalizeData=wO[n];var r=e.visualHandlers[i];this.applyVisual=r.applyVisual,this.getColorMapper=r.getColorMapper,this._normalizedToVisual=r._normalizedToVisual[n],"piecewise"===n?(uO(a),function(e){var t=e.pieceList;e.hasSpecialVisual=!1,zr(t,(function(t,n){t.originIndex=n,null!=t.visual&&(e.hasSpecialVisual=!0)}))}(a)):"category"===n?a.categories?function(e){var t=e.categories,n=e.categoryMap={},i=e.visual;if(lO(t,(function(e,t){n[e]=t})),!Xr(i)){var a=[];to(i)?lO(i,(function(e,t){var i=n[t];a[null!=i?i:cO]=e})):a[-1]=i,i=bO(e,a)}for(var r=t.length-1;r>=0;r--)null==i[r]&&(delete n[t[r]],t.pop())}(a):uO(a,!0):(go("linear"!==n||a.dataExtent),uO(a))}return e.prototype.mapValueToVisual=function(e){var t=this._normalizeData(e);return this._normalizedToVisual(t,e)},e.prototype.getNormalizer=function(){return Kr(this._normalizeData,this)},e.listVisualTypes=function(){return $r(e.visualHandlers)},e.isValidType=function(t){return e.visualHandlers.hasOwnProperty(t)},e.eachVisual=function(e,t,n){to(e)?zr(e,t,n):t.call(n,e)},e.mapVisual=function(t,n,i){var a,r=Xr(t)?[]:to(t)?{}:(a=!0,null);return e.eachVisual(t,(function(e,t){var o=n.call(i,e,t);a?r=o:r[t]=o})),r},e.retrieveVisuals=function(t){var n,i={};return t&&lO(e.visualHandlers,(function(e,a){t.hasOwnProperty(a)&&(i[a]=t[a],n=!0)})),n?i:null},e.prepareVisualTypes=function(e){if(Xr(e))e=e.slice();else{if(!pO(e))return[];var t=[];lO(e,(function(e,n){t.push(n)})),e=t}return e.sort((function(e,t){return"color"===t&&"color"!==e&&0===e.indexOf("color")?1:-1})),e},e.dependsOn=function(e,t){return"color"===t?!(!e||0!==e.indexOf(t)):e===t},e.findPieceIndex=function(e,t,n){for(var i,a=1/0,r=0,o=t.length;ri&&(i=t);var r=i%2?i+2:i+3;a=[];for(var o=0;o0&&(v[0]=-v[0],v[1]=-v[1]);var b=y[0]<0?-1:1;if("start"!==i.__position&&"end"!==i.__position){var w=-Math.atan2(y[1],y[0]);p[0].8?"left":c[0]<-.8?"right":"center",u=c[1]>.8?"top":c[1]<-.8?"bottom":"middle";break;case"start":i.x=-c[0]*h+l[0],i.y=-c[1]*g+l[1],d=c[0]>.8?"right":c[0]<-.8?"left":"center",u=c[1]>.8?"bottom":c[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=h*b+l[0],i.y=l[1]+S,d=y[0]<0?"right":"left",i.originX=-h*b,i.originY=-S;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+S,d="center",i.originY=-S;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-h*b+p[0],i.y=p[1]+S,d=y[0]>=0?"right":"left",i.originX=h*b,i.originY=-S}i.scaleX=i.scaleY=a,i.setStyle({verticalAlign:i.__verticalAlign||u,align:i.__align||d})}}}function C(e,t){var n=e.__specifiedRotation;if(null==n){var i=o.tangentAt(t);e.attr("rotation",(1===t?-1:1)*Math.PI/2-Math.atan2(i[1],i[0]))}else e.attr("rotation",n)}},t}(Rc),YO=function(){function e(e){this.group=new Rc,this._LineCtor=e||KO}return e.prototype.updateData=function(e){var t=this;this._progressiveEls=null;var n=this,i=n.group,a=n._lineData;n._lineData=e,a||i.removeAll();var r=XO(e);e.diff(a).add((function(n){t._doAdd(e,n,r)})).update((function(n,i){t._doUpdate(a,e,i,n,r)})).remove((function(e){i.remove(a.getItemGraphicEl(e))})).execute()},e.prototype.updateLayout=function(){var e=this._lineData;e&&e.eachItemGraphicEl((function(t,n){t.updateLayout(e,n)}),this)},e.prototype.incrementalPrepareUpdate=function(e){this._seriesScope=XO(e),this._lineData=null,this.group.removeAll()},e.prototype.incrementalUpdate=function(e,t){function n(e){e.isGroup||function(e){return e.animators&&e.animators.length>0}(e)||(e.incremental=!0,e.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[];for(var i=e.start;i=0?i+=p:i-=p:h>=0?i-=p:i+=p}return i}function oA(e,t){var n=[],i=El,a=[[],[],[]],r=[[],[]],o=[];t/=2,e.eachEdge((function(e,s){var l=e.getLayout(),p=e.getVisual("fromSymbol"),c=e.getVisual("toSymbol");l.__original||(l.__original=[Oo(l[0]),Oo(l[1])],l[2]&&l.__original.push(Oo(l[2])));var d=l.__original;if(null!=l[2]){if(Do(a[0],d[0]),Do(a[1],d[2]),Do(a[2],d[1]),p&&"none"!==p){var u=OO(e.node1),m=rA(a,d[0],u*t);i(a[0][0],a[1][0],a[2][0],m,n),a[0][0]=n[3],a[1][0]=n[4],i(a[0][1],a[1][1],a[2][1],m,n),a[0][1]=n[3],a[1][1]=n[4]}if(c&&"none"!==c){u=OO(e.node2),m=rA(a,d[1],u*t);i(a[0][0],a[1][0],a[2][0],m,n),a[1][0]=n[1],a[2][0]=n[2],i(a[0][1],a[1][1],a[2][1],m,n),a[1][1]=n[1],a[2][1]=n[2]}Do(l[0],a[0]),Do(l[1],a[2]),Do(l[2],a[1])}else{if(Do(r[0],d[0]),Do(r[1],d[1]),Ro(o,r[1],r[0]),Vo(o,o),p&&"none"!==p){u=OO(e.node1);Fo(r[0],r[0],o,u*t)}if(c&&"none"!==c){u=OO(e.node2);Fo(r[1],r[1],o,-u*t)}Do(l[0],r[0]),Do(l[1],r[1])}}))}function sA(e){return"view"===e.type}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.init=function(e,t){var n=new PE,i=new YO,a=this.group;this._controller=new mP(t.getZr()),this._controllerHost={target:a},a.add(n.group),a.add(i.group),this._symbolDraw=n,this._lineDraw=i,this._firstRender=!0},t.prototype.render=function(e,t,n){var i=this,a=e.coordinateSystem;this._model=e;var r=this._symbolDraw,o=this._lineDraw,s=this.group;if(sA(a)){var l={x:a.x,y:a.y,scaleX:a.scaleX,scaleY:a.scaleY};this._firstRender?s.attr(l):pf(s,l,e)}oA(e.getGraph(),DO(e));var p=e.getData();r.updateData(p);var c=e.getEdgeData();o.updateData(c),this._updateNodeAndLinkScale(),this._updateController(e,t,n),clearTimeout(this._layoutTimeout);var d=e.forceLayout,u=e.get(["force","layoutAnimation"]);d&&this._startForceLayoutIteration(d,u);var m=e.get("layout");p.graph.eachNode((function(t){var n=t.dataIndex,a=t.getGraphicEl(),r=t.getModel();if(a){a.off("drag").off("dragend");var o=r.get("draggable");o&&a.on("drag",(function(r){switch(m){case"force":d.warmUp(),!i._layouting&&i._startForceLayoutIteration(d,u),d.setFixed(n),p.setItemLayout(n,[a.x,a.y]);break;case"circular":p.setItemLayout(n,[a.x,a.y]),t.setLayout({fixed:!0},!0),RO(e,"symbolSize",t,[r.offsetX,r.offsetY]),i.updateLayout(e);break;default:p.setItemLayout(n,[a.x,a.y]),PO(e.getGraph(),e),i.updateLayout(e)}})).on("dragend",(function(){d&&d.setUnfixed(n)})),a.setDraggable(o,!!r.get("cursor")),"adjacency"===r.get(["emphasis","focus"])&&(Xm(a).focus=t.getAdjacentDataIndices())}})),p.graph.eachEdge((function(e){var t=e.getGraphicEl(),n=e.getModel().get(["emphasis","focus"]);t&&"adjacency"===n&&(Xm(t).focus={edge:[e.dataIndex],node:[e.node1.dataIndex,e.node2.dataIndex]})}));var h="circular"===e.get("layout")&&e.get(["circular","rotateLabel"]),g=p.getLayout("cx"),f=p.getLayout("cy");p.graph.eachNode((function(e){NO(e,h,g,f)})),this._firstRender=!1},t.prototype.dispose=function(){this.remove(),this._controller&&this._controller.dispose(),this._controllerHost=null},t.prototype._startForceLayoutIteration=function(e,t){var n=this;!function i(){e.step((function(e){n.updateLayout(n._model),(n._layouting=!e)&&(t?n._layoutTimeout=setTimeout(i,16):i())}))}()},t.prototype._updateController=function(e,t,n){var i=this,a=this._controller,r=this._controllerHost,o=this.group;a.setPointerChecker((function(t,i,a){var r=o.getBoundingRect();return r.applyTransform(o.transform),r.contain(i,a)&&!wP(t,n,e)})),sA(e.coordinateSystem)?(a.enable(e.get("roam")),r.zoomLimit=e.get("scaleLimit"),r.zoom=e.coordinateSystem.getZoom(),a.off("pan").off("zoom").on("pan",(function(t){yP(r,t.dx,t.dy),n.dispatchAction({seriesId:e.id,type:"graphRoam",dx:t.dx,dy:t.dy})})).on("zoom",(function(t){vP(r,t.scale,t.originX,t.originY),n.dispatchAction({seriesId:e.id,type:"graphRoam",zoom:t.scale,originX:t.originX,originY:t.originY}),i._updateNodeAndLinkScale(),oA(e.getGraph(),DO(e)),i._lineDraw.updateLayout(),n.updateLabelLayout()}))):a.disable()},t.prototype._updateNodeAndLinkScale=function(){var e=this._model,t=e.getData(),n=DO(e);t.eachItemGraphicEl((function(e,t){e&&e.setSymbolScale(n)}))},t.prototype.updateLayout=function(e){oA(e.getGraph(),DO(e)),this._symbolDraw.updateLayout(),this._lineDraw.updateLayout()},t.prototype.remove=function(){clearTimeout(this._layoutTimeout),this._layouting=!1,this._layoutTimeout=null,this._symbolDraw&&this._symbolDraw.remove(),this._lineDraw&&this._lineDraw.remove()},t.type="graph"}(Tw);function lA(e){return"_EC_"+e}var pA=function(){function e(e){this.type="graph",this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this._directed=e||!1}return e.prototype.isDirected=function(){return this._directed},e.prototype.addNode=function(e,t){e=null==e?""+t:""+e;var n=this._nodesMap;if(!n[lA(e)]){var i=new cA(e,t);return i.hostGraph=this,this.nodes.push(i),n[lA(e)]=i,i}},e.prototype.getNodeByIndex=function(e){var t=this.data.getRawIndex(e);return this.nodes[t]},e.prototype.getNodeById=function(e){return this._nodesMap[lA(e)]},e.prototype.addEdge=function(e,t,n){var i=this._nodesMap,a=this._edgesMap;if(eo(e)&&(e=this.nodes[e]),eo(t)&&(t=this.nodes[t]),e instanceof cA||(e=i[lA(e)]),t instanceof cA||(t=i[lA(t)]),e&&t){var r=e.id+"-"+t.id,o=new dA(e,t,n);return o.hostGraph=this,this._directed&&(e.outEdges.push(o),t.inEdges.push(o)),e.edges.push(o),e!==t&&t.edges.push(o),this.edges.push(o),a[r]=o,o}},e.prototype.getEdgeByIndex=function(e){var t=this.edgeData.getRawIndex(e);return this.edges[t]},e.prototype.getEdge=function(e,t){e instanceof cA&&(e=e.id),t instanceof cA&&(t=t.id);var n=this._edgesMap;return this._directed?n[e+"-"+t]:n[e+"-"+t]||n[t+"-"+e]},e.prototype.eachNode=function(e,t){for(var n=this.nodes,i=n.length,a=0;a=0&&e.call(t,n[a],a)},e.prototype.eachEdge=function(e,t){for(var n=this.edges,i=n.length,a=0;a=0&&n[a].node1.dataIndex>=0&&n[a].node2.dataIndex>=0&&e.call(t,n[a],a)},e.prototype.breadthFirstTraverse=function(e,t,n,i){if(t instanceof cA||(t=this._nodesMap[lA(t)]),t){for(var a="out"===n?"outEdges":"in"===n?"inEdges":"edges",r=0;r=0&&n.node2.dataIndex>=0}));for(a=0,r=i.length;a=0&&this[e][t].setItemVisual(this.dataIndex,n,i)},getVisual:function(n){return this[e][t].getItemVisual(this.dataIndex,n)},setLayout:function(n,i){this.dataIndex>=0&&this[e][t].setItemLayout(this.dataIndex,n,i)},getLayout:function(){return this[e][t].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[e][t].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[e][t].getRawIndex(this.dataIndex)}}}function mA(e,t,n,i,a){for(var r=new pA(i),o=0;o "+u)),p++)}var m,h=n.get("coordinateSystem");if("cartesian2d"===h||"polar"===h)m=tT(e,n);else{var g=hx.get(h),f=g&&g.dimensions||[];Lr(f,"value")<0&&f.concat(["value"]);var y=$_(e,{coordDimensions:f,encodeDefine:n.getEncode()}).dimensions;(m=new W_(y,n)).initData(e)}var v=new W_(["value"],n);return v.initData(l,s),a&&a(m,v),RD({mainData:m,struct:r,structAttr:"graph",datas:{node:m,edge:v},datasAttr:{node:"data",edge:"edgeData"}}),r.update(),r}qr(cA,uA("hostGraph","data")),qr(dA,uA("hostGraph","edgeData"));!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n}We(t,e),t.prototype.init=function(t){e.prototype.init.apply(this,arguments);var n=this;function i(){return n._categoriesData}this.legendVisualProvider=new $M(i,i),this.fillDataTextStyle(t.edges||t.links),this._updateCategoriesData()},t.prototype.mergeOption=function(t){e.prototype.mergeOption.apply(this,arguments),this.fillDataTextStyle(t.edges||t.links),this._updateCategoriesData()},t.prototype.mergeDefaultAndTheme=function(t){e.prototype.mergeDefaultAndTheme.apply(this,arguments),xd(t,"edgeLabel",["show"])},t.prototype.getInitialData=function(e,t){var n,i=e.edges||e.links||[],a=e.data||e.nodes||[],r=this;if(a&&i){_O(n=this)&&(n.__curvenessList=[],n.__edgeMap={},TO(n));var o=mA(a,i,this,!0,(function(e,t){e.wrapMethod("getItemModel",(function(e){var t=r._categoriesModels[e.getShallow("category")];return t&&(t.parentModel=e.parentModel,e.parentModel=t),e}));var n=xy.prototype.getModel;function i(e,t){var i=n.call(this,e,t);return i.resolveParentPath=a,i}function a(e){if(e&&("label"===e[0]||"label"===e[1])){var t=e.slice();return"label"===e[0]?t[0]="edgeLabel":"label"===e[1]&&(t[1]="edgeLabel"),t}return e}t.wrapMethod("getItemModel",(function(e){return e.resolveParentPath=a,e.getModel=i,e}))}));return zr(o.edges,(function(e){!function(e,t,n,i){if(_O(n)){var a=IO(e,t,n),r=n.__edgeMap,o=r[EO(a)];r[a]&&!o?r[a].isForward=!0:o&&r[a]&&(o.isForward=!0,r[a].isForward=!1),r[a]=r[a]||[],r[a].push(i)}}(e.node1,e.node2,this,e.dataIndex)}),this),o.data}},t.prototype.getGraph=function(){return this.getData().graph},t.prototype.getEdgeData=function(){return this.getGraph().edgeData},t.prototype.getCategoriesData=function(){return this._categoriesData},t.prototype.formatTooltip=function(e,t,n){if("edge"===n){var i=this.getData(),a=this.getDataParams(e,n),r=i.graph.getEdgeByIndex(e),o=i.getName(r.node1.dataIndex),s=i.getName(r.node2.dataIndex),l=[];return null!=o&&l.push(o),null!=s&&l.push(s),Zb("nameValue",{name:l.join(" > "),value:a.value,noValue:null==a.value})}return pw({series:this,dataIndex:e,multipleSeries:t})},t.prototype._updateCategoriesData=function(){var e=Ur(this.option.categories||[],(function(e){return null!=e.value?e:Br({value:0},e)})),t=new W_(["value"],this);t.initData(e),this._categoriesData=t,this._categoriesModels=t.mapArray((function(e){return t.getItemModel(e)}))},t.prototype.setZoom=function(e){this.option.zoom=e},t.prototype.setCenter=function(e){this.option.center=e},t.prototype.isAnimationEnabled=function(){return e.prototype.isAnimationEnabled.call(this)&&!("force"===this.get("layout")&&this.get(["force","layoutAnimation"]))},t.type="series.graph",t.dependencies=["grid","polar","geo","singleAxis","calendar"],t.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:"#aaa",width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}}}(mw);var hA=function(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0},gA=function(e){function t(t){var n=e.call(this,t)||this;return n.type="pointer",n}return We(t,e),t.prototype.getDefaultShape=function(){return new hA},t.prototype.buildPath=function(e,t){var n=Math.cos,i=Math.sin,a=t.r,r=t.width,o=t.angle,s=t.x-n(o)*r*(r>=a/3?1:2),l=t.y-i(o)*r*(r>=a/3?1:2);o=t.angle-Math.PI/2,e.moveTo(s,l),e.lineTo(t.x+n(o)*r,t.y+i(o)*r),e.lineTo(t.x+n(t.angle)*a,t.y+i(t.angle)*a),e.lineTo(t.x-n(o)*r,t.y-i(o)*r),e.lineTo(s,l)},t}(Sm);function fA(e,t){var n=null==e?"":e+"";return t&&(Qr(t)?n=t.replace("{value}",n):Zr(t)&&(n=t(e))),n}(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.render=function(e,t,n){this.group.removeAll();var i=e.get(["axisLine","lineStyle","color"]),a=function(e,t){var n=e.get("center"),i=t.getWidth(),a=t.getHeight(),r=Math.min(i,a);return{cx:Wc(n[0],t.getWidth()),cy:Wc(n[1],t.getHeight()),r:Wc(e.get("radius"),r/2)}}(e,n);this._renderMain(e,t,n,i,a),this._data=e.getData()},t.prototype.dispose=function(){},t.prototype._renderMain=function(e,t,n,i,a){var r=this.group,o=e.get("clockwise"),s=-e.get("startAngle")/180*Math.PI,l=-e.get("endAngle")/180*Math.PI,p=e.getModel("axisLine"),c=p.get("roundCap")?cM:Dg,d=p.get("show"),u=p.getModel("lineStyle"),m=u.get("width"),h=[s,l];tm(h,!o);for(var g=(l=h[1])-(s=h[0]),f=s,y=[],v=0;d&&v=e&&(0===t?0:i[t-1][0])Math.PI/2&&(L+=Math.PI):"tangential"===N?L=-_-Math.PI/2:eo(N)&&(L=N*Math.PI/180),0===L?d.add(new Nm({style:Zf(x,{text:A,x:R,y:B,verticalAlign:c<-.8?"top":c>.8?"bottom":"middle",align:p<-.4?"left":p>.4?"right":"center"},{inheritColor:F}),silent:!0})):d.add(new Nm({style:Zf(x,{text:A,x:R,y:B,verticalAlign:"middle",align:"center"},{inheritColor:F}),silent:!0,originX:R,originY:B,rotation:L}))}if(v.get("show")&&P!==b){O=(O=v.get("distance"))?O+l:l;for(var V=0;V<=w;V++){p=Math.cos(_),c=Math.sin(_);var q=new Gg({shape:{x1:p*(h-O)+u,y1:c*(h-O)+m,x2:p*(h-C-O)+u,y2:c*(h-C-O)+m},silent:!0,style:M});"auto"===M.stroke&&q.setStyle({stroke:i((P+V/w)/b)}),d.add(q),_+=I}_-=I}else _+=T}},t.prototype._renderPointer=function(e,t,n,i,a,r,o,s,l){var p=this.group,c=this._data,d=this._progressEls,u=[],m=e.get(["pointer","show"]),h=e.getModel("progress"),g=h.get("show"),f=e.getData(),y=f.mapDimension("value"),v=+e.get("min"),x=+e.get("max"),b=[v,x],w=[r,o];function S(t,n){var i,r=f.getItemModel(t).getModel("pointer"),o=Wc(r.get("width"),a.r),s=Wc(r.get("length"),a.r),l=e.get(["pointer","icon"]),p=r.get("offsetCenter"),c=Wc(p[0],a.r),d=Wc(p[1],a.r),u=r.get("keepAspect");return(i=l?NS(l,c-o/2,d-s,o,s,null,u):new gA({shape:{angle:-Math.PI/2,width:o,r:s,x:c,y:d}})).rotation=-(n+Math.PI/2),i.x=a.cx,i.y=a.cy,i}function C(e,t){var n=h.get("roundCap")?cM:Dg,i=h.get("overlap"),o=i?h.get("width"):l/f.count(),p=i?a.r-o:a.r-(e+1)*o,c=i?a.r:a.r-e*o,d=new n({shape:{startAngle:r,endAngle:t,cx:a.cx,cy:a.cy,clockwise:s,r0:p,r:c}});return i&&(d.z2=x-f.get(y,e)%x),d}(g||m)&&(f.diff(c).add((function(t){var n=f.get(y,t);if(m){var i=S(t,r);cf(i,{rotation:-((isNaN(+n)?w[0]:Hc(n,b,w,!0))+Math.PI/2)},e),p.add(i),f.setItemGraphicEl(t,i)}if(g){var a=C(t,r),o=h.get("clip");cf(a,{shape:{endAngle:Hc(n,b,w,o)}},e),p.add(a),Zm(e.seriesIndex,f.dataType,t,a),u[t]=a}})).update((function(t,n){var i=f.get(y,t);if(m){var a=c.getItemGraphicEl(n),o=a?a.rotation:r,s=S(t,o);s.rotation=o,pf(s,{rotation:-((isNaN(+i)?w[0]:Hc(i,b,w,!0))+Math.PI/2)},e),p.add(s),f.setItemGraphicEl(t,s)}if(g){var l=d[n],v=C(t,l?l.shape.endAngle:r),x=h.get("clip");pf(v,{shape:{endAngle:Hc(i,b,w,x)}},e),p.add(v),Zm(e.seriesIndex,f.dataType,t,v),u[t]=v}})).execute(),f.each((function(e){var t=f.getItemModel(e),n=t.getModel("emphasis"),a=n.get("focus"),r=n.get("blurScope"),o=n.get("disabled");if(m){var s=f.getItemGraphicEl(e),l=f.getItemVisual(e,"style"),p=l.fill;if(s instanceof Em){var c=s.style;s.useStyle(Br({image:c.image,x:c.x,y:c.y,width:c.width,height:c.height},l))}else s.useStyle(l),"pointer"!==s.type&&s.setColor(p);s.setStyle(t.getModel(["pointer","itemStyle"]).getItemStyle()),"auto"===s.style.fill&&s.setStyle("fill",i(Hc(f.get(y,e),b,[0,1],!0))),s.z2EmphasisLift=0,Uh(s,t),Vh(s,a,r,o)}if(g){var d=u[e];d.useStyle(f.getItemVisual(e,"style")),d.setStyle(t.getModel(["progress","itemStyle"]).getItemStyle()),d.z2EmphasisLift=0,Uh(d,t),Vh(d,a,r,o)}})),this._progressEls=u)},t.prototype._renderAnchor=function(e,t){var n=e.getModel("anchor");if(n.get("show")){var i=n.get("size"),a=n.get("icon"),r=n.get("offsetCenter"),o=n.get("keepAspect"),s=NS(a,t.cx-i/2+Wc(r[0],t.r),t.cy-i/2+Wc(r[1],t.r),i,i,null,o);s.z2=n.get("showAbove")?1:0,s.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(s)}},t.prototype._renderTitleAndDetail=function(e,t,n,i,a){var r=this,o=e.getData(),s=o.mapDimension("value"),l=+e.get("min"),p=+e.get("max"),c=new Rc,d=[],u=[],m=e.isAnimationEnabled(),h=e.get(["pointer","showAbove"]);o.diff(this._data).add((function(e){d[e]=new Nm({silent:!0}),u[e]=new Nm({silent:!0})})).update((function(e,t){d[e]=r._titleEls[t],u[e]=r._detailEls[t]})).execute(),o.each((function(t){var n=o.getItemModel(t),r=o.get(s,t),g=new Rc,f=i(Hc(r,[l,p],[0,1],!0)),y=n.getModel("title");if(y.get("show")){var v=y.get("offsetCenter"),x=a.cx+Wc(v[0],a.r),b=a.cy+Wc(v[1],a.r);(M=d[t]).attr({z2:h?0:2,style:Zf(y,{x:x,y:b,text:o.getName(t),align:"center",verticalAlign:"middle"},{inheritColor:f})}),g.add(M)}var w=n.getModel("detail");if(w.get("show")){var S=w.get("offsetCenter"),C=a.cx+Wc(S[0],a.r),_=a.cy+Wc(S[1],a.r),T=Wc(w.get("width"),a.r),I=Wc(w.get("height"),a.r),E=e.get(["progress","show"])?o.getItemVisual(t,"style").fill:f,M=u[t],k=w.get("formatter");M.attr({z2:h?0:2,style:Zf(w,{x:C,y:_,text:fA(r,k),width:isNaN(T)?null:T,height:isNaN(I)?null:I,align:"center",verticalAlign:"middle"},{inheritColor:E})}),ry(M,{normal:w},r,(function(e){return fA(e,k)})),m&&oy(M,t,o,e,{getFormattedLabel:function(e,t,n,i,a,o){return fA(o?o.interpolatedValue:r,k)}}),g.add(M)}c.add(g)})),this.group.add(c),this._titleEls=d,this._detailEls=u},t.type="gauge"})(Tw),function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.visualStyleAccessPath="itemStyle",n}We(t,e),t.prototype.getInitialData=function(e,t){return WM(this,["value"])},t.type="series.gauge",t.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,"#E6EBF8"]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:"#63677A",width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:"#63677A",width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:"#464646",fontSize:12,rotate:0},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:"#fff",borderWidth:0,borderColor:"#5470c6"}},title:{show:!0,offsetCenter:[0,"20%"],color:"#464646",fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:"rgba(0,0,0,0)",borderWidth:0,borderColor:"#ccc",width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:"#464646",fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}}}(mw);var yA=["itemStyle","opacity"],vA=function(e){function t(t,n){var i=e.call(this)||this,a=i,r=new Lg,o=new Nm;return a.setTextContent(o),i.setTextGuideLine(r),i.updateData(t,n,!0),i}return We(t,e),t.prototype.updateData=function(e,t,n){var i=this,a=e.hostModel,r=e.getItemModel(t),o=e.getItemLayout(t),s=r.getModel("emphasis"),l=r.get(yA);l=null==l?1:l,n||gf(i),i.useStyle(e.getItemVisual(t,"style")),i.style.lineJoin="round",n?(i.setShape({points:o.points}),i.style.opacity=0,cf(i,{style:{opacity:l}},a,t)):pf(i,{style:{opacity:l},shape:{points:o.points}},a,t),Uh(i,r),this._updateLabel(e,t),Vh(this,s.get("focus"),s.get("blurScope"),s.get("disabled"))},t.prototype._updateLabel=function(e,t){var n=this,i=this.getTextGuideLine(),a=n.getTextContent(),r=e.hostModel,o=e.getItemModel(t),s=e.getItemLayout(t).label,l=e.getItemVisual(t,"style"),p=l.fill;Yf(a,Xf(o),{labelFetcher:e.hostModel,labelDataIndex:t,defaultOpacity:l.opacity,defaultText:e.getName(t)},{normal:{align:s.textAlign,verticalAlign:s.verticalAlign}}),n.setTextConfig({local:!0,inside:!!s.inside,insideStroke:p,outsideFill:p});var c=s.linePoints;i.setShape({points:c}),n.textGuideLineConfig={anchor:c?new Ms(c[0][0],c[0][1]):null},pf(a,{style:{x:s.x,y:s.y}},r,t),a.attr({rotation:s.rotation,originX:s.x,originY:s.y,z2:10}),rE(n,oE(o),{stroke:p})},t}(Bg);(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.ignoreLabelLineUpdate=!0,n}We(t,e),t.prototype.render=function(e,t,n){var i=e.getData(),a=this._data,r=this.group;i.diff(a).add((function(e){var t=new vA(i,e);i.setItemGraphicEl(e,t),r.add(t)})).update((function(e,t){var n=a.getItemGraphicEl(t);n.updateData(i,e),r.add(n),i.setItemGraphicEl(e,n)})).remove((function(t){hf(a.getItemGraphicEl(t),e,t)})).execute(),this._data=i},t.prototype.remove=function(){this.group.removeAll(),this._data=null},t.prototype.dispose=function(){},t.type="funnel"})(Tw),function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.init=function(t){e.prototype.init.apply(this,arguments),this.legendVisualProvider=new $M(Kr(this.getData,this),Kr(this.getRawData,this)),this._defaultLabelLine(t)},t.prototype.getInitialData=function(e,t){return WM(this,{coordDimensions:["value"],encodeDefaulter:Yr($v,this)})},t.prototype._defaultLabelLine=function(e){xd(e,"labelLine",["show"]);var t=e.labelLine,n=e.emphasis.labelLine;t.show=t.show&&e.label.show,n.show=n.show&&e.emphasis.label.show},t.prototype.getDataParams=function(t){var n=this.getData(),i=e.prototype.getDataParams.call(this,t),a=n.mapDimension("value"),r=n.getSum(a);return i.percent=r?+(n.get(a,t)/r*100).toFixed(2):0,i.$vars.push("percent"),i},t.type="series.funnel",t.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:60,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}}}(mw);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._dataGroup=new Rc,n._initialized=!1,n}We(t,e),t.prototype.init=function(){this.group.add(this._dataGroup)},t.prototype.render=function(e,t,n,i){this._progressiveEls=null;var a=this._dataGroup,r=e.getData(),o=this._data,s=e.coordinateSystem,l=s.dimensions,p=wA(e);if(r.diff(o).add((function(e){SA(bA(r,a,e,l,s),r,e,p)})).update((function(t,n){var i=o.getItemGraphicEl(n),a=xA(r,t,l,s);r.setItemGraphicEl(t,i),pf(i,{shape:{points:a}},e,t),gf(i),SA(i,r,t,p)})).remove((function(e){var t=o.getItemGraphicEl(e);a.remove(t)})).execute(),!this._initialized){this._initialized=!0;var c=function(e,t,n){var i=e.model,a=e.getRect(),r=new Fm({shape:{x:a.x,y:a.y,width:a.width,height:a.height}}),o="horizontal"===i.get("layout")?"width":"height";return r.setShape(o,0),cf(r,{shape:{width:a.width,height:a.height}},t,n),r}(s,e,(function(){setTimeout((function(){a.removeClipPath()}))}));a.setClipPath(c)}this._data=r},t.prototype.incrementalPrepareRender=function(e,t,n){this._initialized=!0,this._data=null,this._dataGroup.removeAll()},t.prototype.incrementalRender=function(e,t,n){for(var i=t.getData(),a=t.coordinateSystem,r=a.dimensions,o=wA(t),s=this._progressiveEls=[],l=e.start;l5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([e.offsetX,e.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(e){if(!this._mouseDownPoint&&TA(this,"mousemove")){var t=this._model,n=t.coordinateSystem.getSlidedAxisExpandWindow([e.offsetX,e.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(t.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function TA(e,t){var n=e._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===t}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.init=function(){e.prototype.init.apply(this,arguments),this.mergeOption({})},t.prototype.mergeOption=function(e){var t=this.option;e&&Fr(t,e,!0),this._initDimensions()},t.prototype.contains=function(e,t){var n=e.get("parallelIndex");return null!=n&&t.getComponent("parallel",n)===this},t.prototype.setAxisExpand=function(e){zr(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(t){e.hasOwnProperty(t)&&(this.option[t]=e[t])}),this)},t.prototype._initDimensions=function(){var e=this.dimensions=[],t=this.parallelAxisIndex=[];zr(Hr(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(e){return(e.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){e.push("dim"+n.get("dim")),t.push(n.componentIndex)}))},t.type="parallel",t.dependencies=["parallelAxis"],t.layoutMode="box",t.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null}}(kv);var IA=function(e){function t(t,n,i,a,r){var o=e.call(this,t,n,i)||this;return o.type=a||"value",o.axisIndex=r,o}return We(t,e),t.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},t}(RI);function EA(e,t,n,i,a,r){e=e||0;var o=n[1]-n[0];if(null!=a&&(a=kA(a,[0,o])),null!=r&&(r=Math.max(r,null!=a?a:0)),"all"===i){var s=Math.abs(t[1]-t[0]);s=kA(s,[0,o]),a=r=kA(s,[a,r]),i=0}t[0]=kA(t[0],n),t[1]=kA(t[1],n);var l=MA(t,i);t[i]+=e;var p,c=a||0,d=n.slice();return l.sign<0?d[0]+=c:d[1]-=c,t[i]=kA(t[i],d),p=MA(t,i),null!=a&&(p.sign!==l.sign||p.spanr&&(t[1-i]=t[i]+p.sign*r),t}function MA(e,t){var n=e[t]-e[1-t];return{span:Math.abs(n),sign:n>0?-1:n<0?1:t?-1:1}}function kA(e,t){return Math.min(null!=t[1]?t[1]:1/0,Math.max(null!=t[0]?t[0]:-1/0,e))}var PA=zr,DA=Math.min,OA=Math.max,AA=Math.floor,FA=Math.ceil,RA=$c,BA=Math.PI;!function(){function e(e,t,n){this.type="parallel",this._axesMap=Co(),this._axesLayout={},this.dimensions=e.dimensions,this._model=e,this._init(e,t,n)}e.prototype._init=function(e,t,n){var i=e.dimensions,a=e.parallelAxisIndex;PA(i,(function(e,n){var i=a[n],r=t.getComponent("parallelAxis",i),o=this._axesMap.set(e,new IA(e,tI(r),[0,0],r.get("type"),i)),s="category"===o.type;o.onBand=s&&r.get("boundaryGap"),o.inverse=r.get("inverse"),r.axis=o,o.model=r,o.coordinateSystem=r.coordinateSystem=this}),this)},e.prototype.update=function(e,t){this._updateAxesFromSeries(this._model,e)},e.prototype.containPoint=function(e){var t=this._makeLayoutInfo(),n=t.axisBase,i=t.layoutBase,a=t.pixelDimIndex,r=e[1-a],o=e[a];return r>=n&&r<=n+t.axisLength&&o>=i&&o<=i+t.layoutLength},e.prototype.getModel=function(){return this._model},e.prototype._updateAxesFromSeries=function(e,t){t.eachSeries((function(n){if(e.contains(n,t)){var i=n.getData();PA(this.dimensions,(function(e){var t=this._axesMap.get(e);t.scale.unionExtentFromData(i,i.mapDimension(e)),eI(t.scale,t.model)}),this)}}),this)},e.prototype.resize=function(e,t){this._rect=Sv(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()}),this._layoutAxes()},e.prototype.getRect=function(){return this._rect},e.prototype._makeLayoutInfo=function(){var e,t=this._model,n=this._rect,i=["x","y"],a=["width","height"],r=t.get("layout"),o="horizontal"===r?0:1,s=n[a[o]],l=[0,s],p=this.dimensions.length,c=NA(t.get("axisExpandWidth"),l),d=NA(t.get("axisExpandCount")||0,[0,p]),u=t.get("axisExpandable")&&p>3&&p>d&&d>1&&c>0&&s>0,m=t.get("axisExpandWindow");m?(e=NA(m[1]-m[0],l),m[1]=m[0]+e):(e=NA(c*(d-1),l),(m=[c*(t.get("axisExpandCenter")||AA(p/2))-e/2])[1]=m[0]+e);var h=(s-e)/(p-d);h<3&&(h=0);var g=[AA(RA(m[0]/c,1))+1,FA(RA(m[1]/c,1))-1],f=h/c*m[0];return{layout:r,pixelDimIndex:o,layoutBase:n[i[o]],layoutLength:s,axisBase:n[i[1-o]],axisLength:n[a[1-o]],axisExpandable:u,axisExpandWidth:c,axisCollapseWidth:h,axisExpandWindow:m,axisCount:p,winInnerIndices:g,axisExpandWindow0Pos:f}},e.prototype._layoutAxes=function(){var e=this._rect,t=this._axesMap,n=this.dimensions,i=this._makeLayoutInfo(),a=i.layout;t.each((function(e){var t=[0,i.axisLength],n=e.inverse?1:0;e.setExtent(t[n],t[1-n])})),PA(n,(function(t,n){var r=(i.axisExpandable?VA:LA)(n,i),o={horizontal:{x:r.position,y:i.axisLength},vertical:{x:0,y:r.position}},s={horizontal:BA/2,vertical:0},l=[o[a].x+e.x,o[a].y+e.y],p=s[a],c=[1,0,0,1,0,0];_s(c,c,p),Cs(c,c,l),this._axesLayout[t]={position:l,rotation:p,transform:c,axisNameAvailableWidth:r.axisNameAvailableWidth,axisLabelShow:r.axisLabelShow,nameTruncateMaxWidth:r.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}}),this)},e.prototype.getAxis=function(e){return this._axesMap.get(e)},e.prototype.dataToPoint=function(e,t){return this.axisCoordToPoint(this._axesMap.get(t).dataToCoord(e),t)},e.prototype.eachActiveState=function(e,t,n,i){null==n&&(n=0),null==i&&(i=e.count());var a=this._axesMap,r=this.dimensions,o=[],s=[];zr(r,(function(t){o.push(e.mapDimension(t)),s.push(a.get(t).model)}));for(var l=this.hasAxisBrushed(),p=n;pa*(1-c[0])?(l="jump",o=s-a*(1-c[2])):(o=s-a*c[1])>=0&&(o=s-a*(1-c[1]))<=0&&(o=0),(o*=t.axisExpandWidth/p)?EA(o,i,r,"all"):l="none";else{var u=i[1]-i[0];(i=[OA(0,r[1]*s/u-u/2)])[1]=DA(r[1],i[0]+u),i[0]=i[1]-u}return{axisExpandWindow:i,behavior:l}}}();function NA(e,t){return DA(OA(e,t[0]),t[1])}function LA(e,t){var n=t.layoutLength/(t.axisCount-1);return{position:n*e,axisNameAvailableWidth:n,axisLabelShow:!0}}function VA(e,t){var n,i,a=t.layoutLength,r=t.axisExpandWidth,o=t.axisCount,s=t.axisCollapseWidth,l=t.winInnerIndices,p=s,c=!1;return e=0;n--)Kc(t[n])},t.prototype.getActiveState=function(e){var t=this.activeIntervals;if(!t.length)return"normal";if(null==e||isNaN(+e))return"inactive";if(1===t.length){var n=t[0];if(n[0]<=e&&e<=n[1])return"active"}else for(var i=0,a=t.length;i6}(e)||r){if(o&&!r){"single"===s.brushMode&&rF(e);var l=Ar(s);l.brushType=SF(l.brushType,o),l.panelId=o===GA?null:o.panelId,r=e._creatingCover=ZA(e,l),e._covers.push(r)}if(r){var p=TF[SF(e._brushType,o)];r.__brushOption.range=p.getCreatingRange(vF(e,r,e._track)),i&&(QA(e,r),p.updateCommon(e,r)),JA(e,r),a={isEnd:i}}}else i&&"single"===s.brushMode&&s.removeOnClick&&iF(e,t,n)&&rF(e)&&(a={isEnd:i,removeOnClick:!0});return a}function SF(e,t){return"auto"===e?t.defaultBrushType:e}var CF={mousedown:function(e){if(this._dragging)_F(this,e);else if(!e.target||!e.target.draggable){xF(e);var t=this.group.transformCoordToLocal(e.offsetX,e.offsetY);this._creatingCover=null,(this._creatingPanel=iF(this,e,t))&&(this._dragging=!0,this._track=[t.slice()])}},mousemove:function(e){var t=e.offsetX,n=e.offsetY,i=this.group.transformCoordToLocal(t,n);if(function(e,t,n){if(e._brushType&&!function(e,t,n){var i=e._zr;return t<0||t>i.getWidth()||n<0||n>i.getHeight()}(e,t.offsetX,t.offsetY)){var i=e._zr,a=e._covers,r=iF(e,t,n);if(!e._dragging)for(var o=0;o=0&&(r[a[o].depth]=new xy(a[o],this,t));if(i&&n){var s=mA(i,n,this,!0,(function(e,t){e.wrapMethod("getItemModel",(function(e,t){var n=e.parentModel,i=n.getData().getItemLayout(t);if(i){var a=i.depth,r=n.levelModels[a];r&&(e.parentModel=r)}return e})),t.wrapMethod("getItemModel",(function(e,t){var n=e.parentModel,i=n.getGraph().getEdgeByIndex(t).node1.getLayout();if(i){var a=i.depth,r=n.levelModels[a];r&&(e.parentModel=r)}return e}))}));return s.data}},t.prototype.setNodePosition=function(e,t){var n=(this.option.data||this.option.nodes)[e];n.localX=t[0],n.localY=t[1]},t.prototype.getGraph=function(){return this.getData().graph},t.prototype.getEdgeData=function(){return this.getGraph().edgeData},t.prototype.formatTooltip=function(e,t,n){function i(e){return isNaN(e)||null==e}if("edge"===n){var a=this.getDataParams(e,n),r=a.data,o=a.value;return Zb("nameValue",{name:r.source+" -- "+r.target,value:o,noValue:i(o)})}var s=this.getGraph().getNodeByIndex(e).getLayout().value,l=this.getDataParams(e,n).data.name;return Zb("nameValue",{name:null!=l?l+"":null,value:s,noValue:i(s)})},t.prototype.optionUpdated=function(){},t.prototype.getDataParams=function(t,n){var i=e.prototype.getDataParams.call(this,t,n);if(null==i.value&&"node"===n){var a=this.getGraph().getNodeByIndex(t).getLayout().value;i.value=a}return i},t.type="series.sankey",t.defaultOption={z:2,coordinateSystem:"view",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,label:{show:!0,position:"right",fontSize:12},edgeLabel:{show:!1,fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:"#314656",opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:"#212121"}},animationEasing:"linear",animationDuration:1e3}}(mw);var RF=function(){function e(){}return e.prototype.getInitialData=function(e,t){var n,i,a=t.getComponent("xAxis",this.get("xAxisIndex")),r=t.getComponent("yAxis",this.get("yAxisIndex")),o=a.get("type"),s=r.get("type");"category"===o?(e.layout="horizontal",n=a.getOrdinalMeta(),i=!0):"category"===s?(e.layout="vertical",n=r.getOrdinalMeta(),i=!0):e.layout=e.layout||"horizontal";var l=["x","y"],p="horizontal"===e.layout?0:1,c=this._baseAxisDim=l[p],d=l[1-p],u=[a,r],m=u[p].get("type"),h=u[1-p].get("type"),g=e.data;if(g&&i){var f=[];zr(g,(function(e,t){var n;Xr(e)?(n=e.slice(),e.unshift(t)):Xr(e.value)?((n=Br({},e)).value=n.value.slice(),e.value.unshift(t)):n=e,f.push(n)})),e.data=f}var y=this.defaultValueDimensions,v=[{name:c,type:T_(m),ordinalMeta:n,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:d,type:T_(h),dimsDef:y.slice()}];return WM(this,{coordDimensions:v,dimensionsCount:y.length+1,encodeDefaulter:Yr(Wv,v,this)})},e.prototype.getBaseAxis=function(){var e=this._baseAxisDim;return this.ecModel.getComponent(e+"Axis",this.get(e+"AxisIndex")).axis},e}(),BF=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.defaultValueDimensions=[{name:"min",defaultTooltip:!0},{name:"Q1",defaultTooltip:!0},{name:"median",defaultTooltip:!0},{name:"Q3",defaultTooltip:!0},{name:"max",defaultTooltip:!0}],n.visualDrawType="stroke",n}return We(t,e),t.type="series.boxplot",t.dependencies=["xAxis","yAxis","grid"],t.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,boxWidth:[7,50],itemStyle:{color:"#fff",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2,shadowBlur:5,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0,0,0,0.2)"}},animationDuration:800},t}(mw);qr(BF,RF,!0);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.render=function(e,t,n){var i=e.getData(),a=this.group,r=this._data;this._data||a.removeAll();var o="horizontal"===e.get("layout")?1:0;i.diff(r).add((function(e){if(i.hasValue(e)){var t=VF(i.getItemLayout(e),i,e,o,!0);i.setItemGraphicEl(e,t),a.add(t)}})).update((function(e,t){var n=r.getItemGraphicEl(t);if(i.hasValue(e)){var s=i.getItemLayout(e);n?(gf(n),qF(s,n,i,e)):n=VF(s,i,e,o),a.add(n),i.setItemGraphicEl(e,n)}else a.remove(n)})).remove((function(e){var t=r.getItemGraphicEl(e);t&&a.remove(t)})).execute(),this._data=i},t.prototype.remove=function(e){var t=this.group,n=this._data;this._data=null,n&&n.eachItemGraphicEl((function(e){e&&t.remove(e)}))},t.type="boxplot"}(Tw);var NF=function(){},LF=function(e){function t(t){var n=e.call(this,t)||this;return n.type="boxplotBoxPath",n}return We(t,e),t.prototype.getDefaultShape=function(){return new NF},t.prototype.buildPath=function(e,t){var n=t.points,i=0;for(e.moveTo(n[i][0],n[i][1]),i++;i<4;i++)e.lineTo(n[i][0],n[i][1]);for(e.closePath();i0?"borderColor":"borderColor0"])||n.get(["itemStyle",e>0?"color":"color0"]);0===e&&(a=n.get(["itemStyle","borderColorDoji"]));var r=n.getModel("itemStyle").getItemStyle(zF);t.useStyle(r),t.style.fill=null,t.style.stroke=a}var JF=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],n}return We(t,e),t.prototype.getShadowDim=function(){return"open"},t.prototype.brushSelector=function(e,t,n){var i=t.getItemLayout(e);return i&&n.rect(i.brushRect)},t.type="series.candlestick",t.dependencies=["xAxis","yAxis","grid"],t.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderColorDoji:null,borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},t}(mw);qr(JF,RF,!0);var eR=["itemStyle","borderColor"],tR=["itemStyle","borderColor0"],nR=["itemStyle","borderColorDoji"],iR=["itemStyle","color"],aR=["itemStyle","color0"];Sw(),Sw();function rR(e,t,n,i,a,r){return n>i?-1:n0?e.get(a,t-1)<=i?1:-1:1}function oR(e,t){var n=t.rippleEffectColor||t.color;e.eachChild((function(e){e.attr({z:t.z,zlevel:t.zlevel,style:{stroke:"stroke"===t.brushType?n:null,fill:"fill"===t.brushType?n:null}})}))}var sR=function(e){function t(t,n){var i=e.call(this)||this,a=new TE(t,n),r=new Rc;return i.add(a),i.add(r),i.updateData(t,n),i}return We(t,e),t.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},t.prototype.startEffectAnimation=function(e){for(var t=e.symbolType,n=e.color,i=e.rippleNumber,a=this.childAt(1),r=0;r0&&(r=this._getLineLength(i)/l*1e3),r!==this._period||o!==this._loop||s!==this._roundTrip){i.stopAnimation();var c=void 0;c=Zr(p)?p(n):p,i.__t>0&&(c=-r*i.__t),this._animateSymbol(i,r,c,o,s)}this._period=r,this._loop=o,this._roundTrip=s}},t.prototype._animateSymbol=function(e,t,n,i,a){if(t>0){e.__t=0;var r=this,o=e.animate("",i).when(a?2*t:t,{__t:a?2:1}).delay(n).during((function(){r._updateSymbolPosition(e)}));i||o.done((function(){r.remove(e)})),o.start()}},t.prototype._getLineLength=function(e){return Go(e.__p1,e.__cp1)+Go(e.__cp1,e.__p2)},t.prototype._updateAnimationPoints=function(e,t){e.__p1=t[0],e.__p2=t[1],e.__cp1=t[2]||[(t[0][0]+t[1][0])/2,(t[0][1]+t[1][1])/2]},t.prototype.updateData=function(e,t,n){this.childAt(0).updateData(e,t,n),this._updateEffectSymbol(e,t)},t.prototype._updateSymbolPosition=function(e){var t=e.__p1,n=e.__p2,i=e.__cp1,a=e.__t<1?e.__t:2-e.__t,r=[e.x,e.y],o=r.slice(),s=_l,l=Tl;r[0]=s(t[0],i[0],n[0],a),r[1]=s(t[1],i[1],n[1],a);var p=e.__t<1?l(t[0],i[0],n[0],a):l(n[0],i[0],t[0],1-a),c=e.__t<1?l(t[1],i[1],n[1],a):l(n[1],i[1],t[1],1-a);e.rotation=-Math.atan2(c,p)-Math.PI/2,"line"!==this._symbolType&&"rect"!==this._symbolType&&"roundRect"!==this._symbolType||(void 0!==e.__lastT&&e.__lastT=0&&!(i[r]<=t);r--);r=Math.min(r,a-2)}else{for(r=o;rt);r++);r=Math.min(r-1,a-2)}var s=(t-i[r])/(i[r+1]-i[r]),l=n[r],p=n[r+1];e.x=l[0]*(1-s)+s*p[0],e.y=l[1]*(1-s)+s*p[1];var c=e.__t<1?p[0]-l[0]:l[0]-p[0],d=e.__t<1?p[1]-l[1]:l[1]-p[1];e.rotation=-Math.atan2(d,c)-Math.PI/2,this._lastFrame=r,this._lastFramePercent=t,e.ignore=!1}},t}(lR),dR=function(){this.polyline=!1,this.curveness=0,this.segs=[]},uR=function(e){function t(t){var n=e.call(this,t)||this;return n._off=0,n.hoverDataIdx=-1,n}return We(t,e),t.prototype.reset=function(){this.notClear=!1,this._off=0},t.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},t.prototype.getDefaultShape=function(){return new dR},t.prototype.buildPath=function(e,t){var n,i=t.segs,a=t.curveness;if(t.polyline)for(n=this._off;n0){e.moveTo(i[n++],i[n++]);for(var o=1;o0){var d=(s+p)/2-(l-c)*a,u=(l+c)/2-(p-s)*a;e.quadraticCurveTo(d,u,p,c)}else e.lineTo(p,c)}this.incremental&&(this._off=n,this.notClear=!0)},t.prototype.findDataIndex=function(e,t){var n=this.shape,i=n.segs,a=n.curveness,r=this.style.lineWidth;if(n.polyline)for(var o=0,s=0;s0)for(var p=i[s++],c=i[s++],d=1;d0){if(rm(p,c,(p+u)/2-(c-m)*a,(c+m)/2-(u-p)*a,u,m,r,e,t))return o}else if(im(p,c,u,m,r,e,t))return o;o++}return-1},t.prototype.contain=function(e,t){var n=this.transformCoordToLocal(e,t),i=this.getBoundingRect();return e=n[0],t=n[1],i.contain(e,t)?(this.hoverDataIdx=this.findDataIndex(e,t))>=0:(this.hoverDataIdx=-1,!1)},t.prototype.getBoundingRect=function(){var e=this._rect;if(!e){for(var t=this.shape.segs,n=1/0,i=1/0,a=-1/0,r=-1/0,o=0;o0&&(r.dataIndex=n+e.__startIndex)}))},e.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},e}(),hR={seriesType:"lines",plan:Sw(),reset:function(e){var t=e.coordinateSystem;if(t){var n=e.get("polyline"),i=e.pipelineContext.large;return{progress:function(a,r){var o=[];if(i){var s=void 0,l=a.end-a.start;if(n){for(var p=0,c=a.start;c0&&(l||s.configLayer(r,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(o/10+.9,1),0)})),a.updateData(i);var p=e.get("clip",!0)&&UE(e.coordinateSystem,!1,e);p?this.group.setClipPath(p):this.group.removeClipPath(),this._lastZlevel=r,this._finished=!0},t.prototype.incrementalPrepareRender=function(e,t,n){var i=e.getData();this._updateLineDraw(i,e).incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},t.prototype.incrementalRender=function(e,t,n){this._lineDraw.incrementalUpdate(e,t.getData()),this._finished=e.end===t.getData().count()},t.prototype.eachRendered=function(e){this._lineDraw&&this._lineDraw.eachRendered(e)},t.prototype.updateTransform=function(e,t,n){var i=e.getData(),a=e.pipelineContext;if(!this._finished||a.large||a.progressiveRender)return{update:!0};var r=hR.reset(e,t,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._lineDraw.updateLayout(),this._clearLayer(n)},t.prototype._updateLineDraw=function(e,t){var n=this._lineDraw,i=this._showEffect(t),a=!!t.get("polyline"),r=t.pipelineContext.large;return n&&i===this._hasEffet&&a===this._isPolyline&&r===this._isLargeDraw||(n&&n.remove(),n=this._lineDraw=r?new mR:new YO(a?i?cR:pR:i?lR:KO),this._hasEffet=i,this._isPolyline=a,this._isLargeDraw=r),this.group.add(n.group),n},t.prototype._showEffect=function(e){return!!e.get(["effect","show"])},t.prototype._clearLayer=function(e){var t=e.getZr();"svg"===t.painter.getType()||null==this._lastZlevel||t.painter.getLayer(this._lastZlevel).clear(!0)},t.prototype.remove=function(e,t){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(t)},t.prototype.dispose=function(e,t){this.remove(e,t)},t.type="lines"}(Tw),"undefined"==typeof Uint32Array?Array:Uint32Array),fR="undefined"==typeof Float64Array?Array:Float64Array;function yR(e){var t=e.data;t&&t[0]&&t[0][0]&&t[0][0].coord&&(e.data=Ur(t,(function(e){var t={coords:[e[0].coord,e[1].coord]};return e[0].name&&(t.fromName=e[0].name),e[1].name&&(t.toName=e[1].name),Rr([t,e[0],e[1]])})))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.visualStyleAccessPath="lineStyle",n.visualDrawType="stroke",n}We(t,e),t.prototype.init=function(t){t.data=t.data||[],yR(t);var n=this._processFlatCoordsArray(t.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(t.data=new Float32Array(n.count)),e.prototype.init.apply(this,arguments)},t.prototype.mergeOption=function(t){if(yR(t),t.data){var n=this._processFlatCoordsArray(t.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(t.data=new Float32Array(n.count))}e.prototype.mergeOption.apply(this,arguments)},t.prototype.appendData=function(e){var t=this._processFlatCoordsArray(e.data);t.flatCoords&&(this._flatCoords?(this._flatCoords=_o(this._flatCoords,t.flatCoords),this._flatCoordsOffset=_o(this._flatCoordsOffset,t.flatCoordsOffset)):(this._flatCoords=t.flatCoords,this._flatCoordsOffset=t.flatCoordsOffset),e.data=new Float32Array(t.count)),this.getRawData().appendData(e.data)},t.prototype._getCoordsFromItemModel=function(e){var t=this.getData().getItemModel(e),n=t.option instanceof Array?t.option:t.getShallow("coords");return n},t.prototype.getLineCoordsCount=function(e){return this._flatCoordsOffset?this._flatCoordsOffset[2*e+1]:this._getCoordsFromItemModel(e).length},t.prototype.getLineCoords=function(e,t){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[2*e],i=this._flatCoordsOffset[2*e+1],a=0;a ")})},t.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},t.prototype.getProgressive=function(){var e=this.option.progressive;return null==e?this.option.large?1e4:this.get("progressive"):e},t.prototype.getProgressiveThreshold=function(){var e=this.option.progressiveThreshold;return null==e?this.option.large?2e4:this.get("progressiveThreshold"):e},t.prototype.getZLevelKey=function(){var e=this.getModel("effect"),t=e.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:e.get("show")&&t>0?t+"":""},t.type="series.lines",t.dependencies=["grid","polar","geo","calendar"],t.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}}}(mw);var vR=function(){function e(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var e=vr.createCanvas();this.canvas=e}return e.prototype.update=function(e,t,n,i,a,r){var o=this._getBrush(),s=this._getGradient(a,"inRange"),l=this._getGradient(a,"outOfRange"),p=this.pointSize+this.blurSize,c=this.canvas,d=c.getContext("2d"),u=e.length;c.width=t,c.height=n;for(var m=0;m0){var T=r(y)?s:l;y>0&&(y=y*C+S),x[b++]=T[_],x[b++]=T[_+1],x[b++]=T[_+2],x[b++]=T[_+3]*y*256}else b+=4}return d.putImageData(v,0,0),c},e.prototype._getBrush=function(){var e=this._brushCanvas||(this._brushCanvas=vr.createCanvas()),t=this.pointSize+this.blurSize,n=2*t;e.width=n,e.height=n;var i=e.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor="#000",i.beginPath(),i.arc(-t,t,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),e},e.prototype._getGradient=function(e,t){for(var n=this._gradientPixels,i=n[t]||(n[t]=new Uint8ClampedArray(1024)),a=[0,0,0,0],r=0,o=0;o<256;o++)e[t](o/255,!0,a),i[r++]=a[0],i[r++]=a[1],i[r++]=a[2],i[r++]=a[3];return i},e}();function xR(e){var t=e.dimensions;return"lng"===t[0]&&"lat"===t[1]}(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.render=function(e,t,n){var i;t.eachComponent("visualMap",(function(t){t.eachTargetSeries((function(n){n===e&&(i=t)}))})),this._progressiveEls=null,this.group.removeAll();var a=e.coordinateSystem;"cartesian2d"===a.type||"calendar"===a.type?this._renderOnCartesianAndCalendar(e,n,0,e.getData().count()):xR(a)&&this._renderOnGeo(a,e,i,n)},t.prototype.incrementalPrepareRender=function(e,t,n){this.group.removeAll()},t.prototype.incrementalRender=function(e,t,n,i){var a=t.coordinateSystem;a&&(xR(a)?this.render(t,n,i):(this._progressiveEls=[],this._renderOnCartesianAndCalendar(t,i,e.start,e.end,!0)))},t.prototype.eachRendered=function(e){jf(this._progressiveEls||this.group,e)},t.prototype._renderOnCartesianAndCalendar=function(e,t,n,i,a){var r,o,s,l,p=e.coordinateSystem,c=jE(p,"cartesian2d");if(c){var d=p.getAxis("x"),u=p.getAxis("y");0,r=d.getBandWidth()+.5,o=u.getBandWidth()+.5,s=d.scale.getExtent(),l=u.scale.getExtent()}for(var m=this.group,h=e.getData(),g=e.getModel(["emphasis","itemStyle"]).getItemStyle(),f=e.getModel(["blur","itemStyle"]).getItemStyle(),y=e.getModel(["select","itemStyle"]).getItemStyle(),v=e.get(["itemStyle","borderRadius"]),x=Xf(e),b=e.getModel("emphasis"),w=b.get("focus"),S=b.get("blurScope"),C=b.get("disabled"),_=c?[h.mapDimension("x"),h.mapDimension("y"),h.mapDimension("value")]:[h.mapDimension("time"),h.mapDimension("value")],T=n;Ts[1]||kl[1])continue;var P=p.dataToPoint([M,k]);I=new Fm({shape:{x:P[0]-r/2,y:P[1]-o/2,width:r,height:o},style:E})}else{if(isNaN(h.get(_[1],T)))continue;I=new Fm({z2:1,shape:p.dataToRect([h.get(_[0],T)]).contentShape,style:E})}if(h.hasItemOption){var D=h.getItemModel(T),O=D.getModel("emphasis");g=O.getModel("itemStyle").getItemStyle(),f=D.getModel(["blur","itemStyle"]).getItemStyle(),y=D.getModel(["select","itemStyle"]).getItemStyle(),v=D.get(["itemStyle","borderRadius"]),w=O.get("focus"),S=O.get("blurScope"),C=O.get("disabled"),x=Xf(D)}I.shape.r=v;var A=e.getRawValue(T),F="-";A&&null!=A[2]&&(F=A[2]+""),Yf(I,x,{labelFetcher:e,labelDataIndex:T,defaultOpacity:E.opacity,defaultText:F}),I.ensureState("emphasis").style=g,I.ensureState("blur").style=f,I.ensureState("select").style=y,Vh(I,w,S,C),I.incremental=a,a&&(I.states.emphasis.hoverLayer=!0),m.add(I),h.setItemGraphicEl(T,I),this._progressiveEls&&this._progressiveEls.push(I)}},t.prototype._renderOnGeo=function(e,t,n,i){var a=n.targetVisuals.inRange,r=n.targetVisuals.outOfRange,o=t.getData(),s=this._hmLayer||this._hmLayer||new vR;s.blurSize=t.get("blurSize"),s.pointSize=t.get("pointSize"),s.minOpacity=t.get("minOpacity"),s.maxOpacity=t.get("maxOpacity");var l=e.getViewRect().clone(),p=e.getRoamTransform();l.applyTransform(p);var c=Math.max(l.x,0),d=Math.max(l.y,0),u=Math.min(l.width+l.x,i.getWidth()),m=Math.min(l.height+l.y,i.getHeight()),h=u-c,g=m-d,f=[o.mapDimension("lng"),o.mapDimension("lat"),o.mapDimension("value")],y=o.mapArray(f,(function(t,n,i){var a=e.dataToPoint([t,n]);return a[0]-=c,a[1]-=d,a.push(i),a})),v=n.getExtent(),x="visualMap.continuous"===n.type?function(e,t){var n=e[1]-e[0];return t=[(t[0]-e[0])/n,(t[1]-e[0])/n],function(e){return e>=t[0]&&e<=t[1]}}(v,n.option.range):function(e,t,n){var i=e[1]-e[0],a=(t=Ur(t,(function(t){return{interval:[(t.interval[0]-e[0])/i,(t.interval[1]-e[0])/i]}}))).length,r=0;return function(e){var i;for(i=r;i=0;i--){var o;if((o=t[i].interval)[0]<=e&&e<=o[1]){r=i;break}}return i>=0&&i0?1:-1}(n,r,a,i,d),function(e,t,n,i,a,r,o,s,l,p){var c,d=l.valueDim,u=l.categoryDim,m=Math.abs(n[u.wh]),h=e.getItemVisual(t,"symbolSize");c=Xr(h)?h.slice():null==h?["100%","100%"]:[h,h];c[u.index]=Wc(c[u.index],m),c[d.index]=Wc(c[d.index],i?m:Math.abs(r)),p.symbolSize=c;var g=p.symbolScale=[c[0]/s,c[1]/s];g[d.index]*=(l.isHorizontal?-1:1)*o}(e,t,a,r,0,d.boundingLength,d.pxSign,p,i,d),function(e,t,n,i,a){var r=e.get(bR)||0;r&&(SR.attr({scaleX:t[0],scaleY:t[1],rotation:n}),SR.updateTransform(),r/=SR.getLineScale(),r*=t[i.valueDim.index]);a.valueLineWidth=r||0}(n,d.symbolScale,l,i,d);var u=d.symbolSize,m=VS(n.get("symbolOffset"),u);return function(e,t,n,i,a,r,o,s,l,p,c,d){var u=c.categoryDim,m=c.valueDim,h=d.pxSign,g=Math.max(t[m.index]+s,0),f=g;if(i){var y=Math.abs(l),v=po(e.get("symbolMargin"),"15%")+"",x=!1;v.lastIndexOf("!")===v.length-1&&(x=!0,v=v.slice(0,v.length-1));var b=Wc(v,t[m.index]),w=Math.max(g+2*b,0),S=x?0:2*b,C=pd(i),_=C?i:VR((y+S)/w);w=g+2*(b=(y-_*g)/2/(x?_:Math.max(_-1,1))),S=x?0:2*b,C||"fixed"===i||(_=p?VR((Math.abs(p)+S)/w):0),f=_*w-S,d.repeatTimes=_,d.symbolMargin=b}var T=h*(f/2),I=d.pathPosition=[];I[u.index]=n[u.wh]/2,I[m.index]="start"===o?T:"end"===o?l-T:l/2,r&&(I[0]+=r[0],I[1]+=r[1]);var E=d.bundlePosition=[];E[u.index]=n[u.xy],E[m.index]=n[m.xy];var M=d.barRectShape=Br({},n);M[m.wh]=h*Math.max(Math.abs(n[m.wh]),Math.abs(I[m.index]+T)),M[u.wh]=n[u.wh];var k=d.clipShape={};k[u.xy]=-n[u.xy],k[u.wh]=c.ecSize[u.wh],k[m.xy]=0,k[m.wh]=n[m.wh]}(n,u,a,r,0,m,s,d.valueLineWidth,d.boundingLength,d.repeatCutLength,i,d),d}function _R(e,t){return e.toGlobalCoord(e.dataToCoord(e.scale.parse(t)))}function TR(e){var t=e.symbolPatternSize,n=NS(e.symbolType,-t/2,-t/2,t,t);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function IR(e,t,n,i){var a=e.__pictorialBundle,r=n.symbolSize,o=n.valueLineWidth,s=n.pathPosition,l=t.valueDim,p=n.repeatTimes||0,c=0,d=r[t.valueDim.index]+o+2*n.symbolMargin;for(BR(e,(function(e){e.__pictorialAnimationIndex=c,e.__pictorialRepeatTimes=p,c0:i<0)&&(a=p-1-e),t[l.index]=d*(a-p/2+.5)+s[l.index],{x:t[0],y:t[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation}}}function ER(e,t,n,i){var a=e.__pictorialBundle,r=e.__pictorialMainPath;r?NR(r,null,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation},n,i):(r=e.__pictorialMainPath=TR(n),a.add(r),NR(r,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:0,scaleY:0,rotation:n.rotation},{scaleX:n.symbolScale[0],scaleY:n.symbolScale[1]},n,i))}function MR(e,t,n){var i=Br({},t.barRectShape),a=e.__pictorialBarRect;a?NR(a,null,{shape:i},t,n):((a=e.__pictorialBarRect=new Fm({z2:2,shape:i,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}})).disableMorphing=!0,e.add(a))}function kR(e,t,n,i){if(n.symbolClip){var a=e.__pictorialClipPath,r=Br({},n.clipShape),o=t.valueDim,s=n.animationModel,l=n.dataIndex;if(a)pf(a,{shape:r},s,l);else{r[o.wh]=0,a=new Fm({shape:r}),e.__pictorialBundle.setClipPath(a),e.__pictorialClipPath=a;var p={};p[o.wh]=n.clipShape[o.wh],Hf[i?"updateProps":"initProps"](a,{shape:p},s,l)}}}function PR(e,t){var n=e.getItemModel(t);return n.getAnimationDelayParams=DR,n.isAnimationEnabled=OR,n}function DR(e){return{index:e.__pictorialAnimationIndex,count:e.__pictorialRepeatTimes}}function OR(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function AR(e,t,n,i){var a=new Rc,r=new Rc;return a.add(r),a.__pictorialBundle=r,r.x=n.bundlePosition[0],r.y=n.bundlePosition[1],n.symbolRepeat?IR(a,t,n):ER(a,0,n),MR(a,n,i),kR(a,t,n,i),a.__pictorialShapeStr=RR(e,n),a.__pictorialSymbolMeta=n,a}function FR(e,t,n,i){var a=i.__pictorialBarRect;a&&a.removeTextContent();var r=[];BR(i,(function(e){r.push(e)})),i.__pictorialMainPath&&r.push(i.__pictorialMainPath),i.__pictorialClipPath&&(n=null),zr(r,(function(e){uf(e,{scaleX:0,scaleY:0},n,t,(function(){i.parent&&i.parent.remove(i)}))})),e.setItemGraphicEl(t,null)}function RR(e,t){return[e.getItemVisual(t.dataIndex,"symbol")||"none",!!t.symbolRepeat,!!t.symbolClip].join(":")}function BR(e,t,n){zr(e.__pictorialBundle.children(),(function(i){i!==e.__pictorialBarRect&&t.call(n,i)}))}function NR(e,t,n,i,a,r){t&&e.attr(t),i.symbolClip&&!a?n&&e.attr(n):n&&Hf[a?"updateProps":"initProps"](e,n,i.animationModel,i.dataIndex,r)}function LR(e,t,n){var i=n.dataIndex,a=n.itemModel,r=a.getModel("emphasis"),o=r.getModel("itemStyle").getItemStyle(),s=a.getModel(["blur","itemStyle"]).getItemStyle(),l=a.getModel(["select","itemStyle"]).getItemStyle(),p=a.getShallow("cursor"),c=r.get("focus"),d=r.get("blurScope"),u=r.get("scale");BR(e,(function(e){if(e instanceof Em){var t=e.style;e.useStyle(Br({image:t.image,x:t.x,y:t.y,width:t.width,height:t.height},n.style))}else e.useStyle(n.style);var i=e.ensureState("emphasis");i.style=o,u&&(i.scaleX=1.1*e.scaleX,i.scaleY=1.1*e.scaleY),e.ensureState("blur").style=s,e.ensureState("select").style=l,p&&(e.cursor=p),e.z2=n.z2}));var m=t.valueDim.posDesc[+(n.boundingLength>0)],h=e.__pictorialBarRect;h.ignoreClip=!0,Yf(h,Xf(a),{labelFetcher:t.seriesModel,labelDataIndex:i,defaultText:CE(t.seriesModel.getData(),i),inheritColor:n.style.fill,defaultOpacity:n.style.opacity,defaultOutsidePosition:m}),Vh(e,c,d,r.get("disabled"))}function VR(e){var t=Math.round(e);return Math.abs(e-t)<1e-4?t:Math.ceil(e)}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n.defaultSymbol="roundRect",n}We(t,e),t.prototype.getInitialData=function(t){return t.stack=null,e.prototype.getInitialData.apply(this,arguments)},t.type="series.pictorialBar",t.dependencies=["grid"],t.defaultOption=Sy(sM.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",clip:!1,progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:"#212121"}}})}(sM);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._layers=[],n}We(t,e),t.prototype.render=function(e,t,n){var i=e.getData(),a=this,r=this.group,o=e.getLayerSeries(),s=i.getLayout("layoutInfo"),l=s.rect,p=s.boundaryGap;function c(e){return e.name}r.x=0,r.y=l.y+p[0];var d=new S_(this._layersSeries||[],o,c,c),u=[];function m(t,n,s){var l=a._layers;if("remove"!==t){for(var p,c,d=[],m=[],h=o[n].indices,g=0;gT&&!td(E-T)&&E0?(a.virtualPiece?a.virtualPiece.updateData(!1,i,e,t,n):(a.virtualPiece=new qR(i,e,t,n),l.add(a.virtualPiece)),r.piece.off("click"),a.virtualPiece.on("click",(function(e){a._rootToNode(r.parentNode)}))):a.virtualPiece&&(l.remove(a.virtualPiece),a.virtualPiece=null)}(o,s),this._initEvents(),this._oldChildren=c},t.prototype._initEvents=function(){var e=this;this.group.off("click"),this.group.on("click",(function(t){var n=!1;e.seriesModel.getViewRoot().eachNode((function(i){if(!n&&i.piece&&i.piece===t.target){var a=i.getModel().get("nodeClick");if("rootToNode"===a)e._rootToNode(i);else if("link"===a){var r=i.getModel(),o=r.get("link");if(o)fv(o,r.get("target",!0)||"_blank")}n=!0}}))}))},t.prototype._rootToNode=function(e){e!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:GR,from:this.uid,seriesId:this.seriesModel.id,targetNode:e})},t.prototype.containPoint=function(e,t){var n=t.getData().getItemLayout(0);if(n){var i=e[0]-n.cx,a=e[1]-n.cy,r=Math.sqrt(i*i+a*a);return r<=n.r&&r>=n.r0}},t.type="sunburst"})(Tw),function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.ignoreStyleOnData=!0,n}We(t,e),t.prototype.getInitialData=function(e,t){var n={name:e.name,children:e.data};zR(n);var i=this._levelModels=Ur(e.levels||[],(function(e){return new xy(e,this,t)}),this),a=jD.createTree(n,this,(function(e){e.wrapMethod("getItemModel",(function(e,t){var n=a.getNodeByDataIndex(t),r=i[n.depth];return r&&(e.parentModel=r),e}))}));return a.data},t.prototype.optionUpdated=function(){this.resetViewRoot()},t.prototype.getDataParams=function(t){var n=e.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(t);return n.treePathInfo=KD(i,this),n},t.prototype.getLevelModel=function(e){return this._levelModels&&this._levelModels[e.depth]},t.prototype.getViewRoot=function(){return this._viewRoot},t.prototype.resetViewRoot=function(e){e?this._viewRoot=e:e=this._viewRoot;var t=this.getRawData().tree.root;e&&(e===t||t.contains(e))||(this._viewRoot=t)},t.prototype.enableAriaDecal=function(){YD(this)},t.type="series.sunburst",t.defaultOption={z:2,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,stillShowZeroSum:!0,nodeClick:"rootToNode",renderLabelForZeroData:!1,label:{rotate:"radial",show:!0,opacity:1,align:"center",position:"inside",distance:5,silent:!0},itemStyle:{borderWidth:1,borderColor:"white",borderType:"solid",shadowBlur:0,shadowColor:"rgba(0, 0, 0, 0.2)",shadowOffsetX:0,shadowOffsetY:0,opacity:1},emphasis:{focus:"descendant"},blur:{itemStyle:{opacity:.2},label:{opacity:.1}},animationType:"expansion",animationDuration:1e3,animationDurationUpdate:500,data:[],sort:"desc"}}(mw);function zR(e){var t=0;zr(e.children,(function(e){zR(e);var n=e.value;Xr(n)&&(n=n[0]),t+=n}));var n=e.value;Xr(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=t),n<0&&(n=0),Xr(e.value)?e.value[0]=n:e.value=n}Math.PI;var UR={color:"fill",borderColor:"stroke"},jR={symbol:1,symbolSize:1,symbolKeepAspect:1,legendIcon:1,visualMeta:1,liftZ:1,decal:1},HR=Pd(),WR=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.optionUpdated=function(){this.currentZLevel=this.get("zlevel",!0),this.currentZ=this.get("z",!0)},t.prototype.getInitialData=function(e,t){return tT(null,this)},t.prototype.getDataParams=function(t,n,i){var a=e.prototype.getDataParams.call(this,t,n);return i&&(a.info=HR(i).info),a},t.type="series.custom",t.dependencies=["grid","polar","geo","singleAxis","calendar"],t.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,clip:!1},t}(mw);function $R(e,t){return t=t||[0,0],Ur(["x","y"],(function(n,i){var a=this.getAxis(n),r=t[i],o=e[i]/2;return"category"===a.type?a.getBandWidth():Math.abs(a.dataToCoord(r-o)-a.dataToCoord(r+o))}),this)}function KR(e,t){return t=t||[0,0],Ur([0,1],(function(n){var i=t[n],a=e[n]/2,r=[],o=[];return r[n]=i-a,o[n]=i+a,r[1-n]=o[1-n]=t[1-n],Math.abs(this.dataToPoint(r)[n]-this.dataToPoint(o)[n])}),this)}function YR(e,t){var n=this.getAxis(),i=t instanceof Array?t[0]:t,a=(e instanceof Array?e[0]:e)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(i-a)-n.dataToCoord(i+a))}function XR(e,t){return t=t||[0,0],Ur(["Radius","Angle"],(function(n,i){var a=this["get"+n+"Axis"](),r=t[i],o=e[i]/2,s="category"===a.type?a.getBandWidth():Math.abs(a.dataToCoord(r-o)-a.dataToCoord(r+o));return"Angle"===n&&(s=s*Math.PI/180),s}),this)}function ZR(e,t,n,i){return e&&(e.legacy||!1!==e.legacy&&!n&&!i&&"tspan"!==t&&("text"===t||Eo(e,"text")))}function QR(e,t,n){var i,a,r,o=e;if("text"===t)r=o;else{r={},Eo(o,"text")&&(r.text=o.text),Eo(o,"rich")&&(r.rich=o.rich),Eo(o,"textFill")&&(r.fill=o.textFill),Eo(o,"textStroke")&&(r.stroke=o.textStroke),Eo(o,"fontFamily")&&(r.fontFamily=o.fontFamily),Eo(o,"fontSize")&&(r.fontSize=o.fontSize),Eo(o,"fontStyle")&&(r.fontStyle=o.fontStyle),Eo(o,"fontWeight")&&(r.fontWeight=o.fontWeight),a={type:"text",style:r,silent:!0},i={};var s=Eo(o,"textPosition");n?i.position=s?o.textPosition:"inside":s&&(i.position=o.textPosition),Eo(o,"textPosition")&&(i.position=o.textPosition),Eo(o,"textOffset")&&(i.offset=o.textOffset),Eo(o,"textRotation")&&(i.rotation=o.textRotation),Eo(o,"textDistance")&&(i.distance=o.textDistance)}return JR(r,e),zr(r.rich,(function(e){JR(e,e)})),{textConfig:i,textContent:a}}function JR(e,t){t&&(t.font=t.textFont||t.font,Eo(t,"textStrokeWidth")&&(e.lineWidth=t.textStrokeWidth),Eo(t,"textAlign")&&(e.align=t.textAlign),Eo(t,"textVerticalAlign")&&(e.verticalAlign=t.textVerticalAlign),Eo(t,"textLineHeight")&&(e.lineHeight=t.textLineHeight),Eo(t,"textWidth")&&(e.width=t.textWidth),Eo(t,"textHeight")&&(e.height=t.textHeight),Eo(t,"textBackgroundColor")&&(e.backgroundColor=t.textBackgroundColor),Eo(t,"textPadding")&&(e.padding=t.textPadding),Eo(t,"textBorderColor")&&(e.borderColor=t.textBorderColor),Eo(t,"textBorderWidth")&&(e.borderWidth=t.textBorderWidth),Eo(t,"textBorderRadius")&&(e.borderRadius=t.textBorderRadius),Eo(t,"textBoxShadowColor")&&(e.shadowColor=t.textBoxShadowColor),Eo(t,"textBoxShadowBlur")&&(e.shadowBlur=t.textBoxShadowBlur),Eo(t,"textBoxShadowOffsetX")&&(e.shadowOffsetX=t.textBoxShadowOffsetX),Eo(t,"textBoxShadowOffsetY")&&(e.shadowOffsetY=t.textBoxShadowOffsetY))}function eB(e,t,n){var i=e;i.textPosition=i.textPosition||n.position||"inside",null!=n.offset&&(i.textOffset=n.offset),null!=n.rotation&&(i.textRotation=n.rotation),null!=n.distance&&(i.textDistance=n.distance);var a=i.textPosition.indexOf("inside")>=0,r=e.fill||"#000";tB(i,t);var o=null==i.textFill;return a?o&&(i.textFill=n.insideFill||"#fff",!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=r),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(o&&(i.textFill=e.fill||n.outsideFill||"#000"),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=t.text,i.rich=t.rich,zr(t.rich,(function(e){tB(e,e)})),i}function tB(e,t){t&&(Eo(t,"fill")&&(e.textFill=t.fill),Eo(t,"stroke")&&(e.textStroke=t.fill),Eo(t,"lineWidth")&&(e.textStrokeWidth=t.lineWidth),Eo(t,"font")&&(e.font=t.font),Eo(t,"fontStyle")&&(e.fontStyle=t.fontStyle),Eo(t,"fontWeight")&&(e.fontWeight=t.fontWeight),Eo(t,"fontSize")&&(e.fontSize=t.fontSize),Eo(t,"fontFamily")&&(e.fontFamily=t.fontFamily),Eo(t,"align")&&(e.textAlign=t.align),Eo(t,"verticalAlign")&&(e.textVerticalAlign=t.verticalAlign),Eo(t,"lineHeight")&&(e.textLineHeight=t.lineHeight),Eo(t,"width")&&(e.textWidth=t.width),Eo(t,"height")&&(e.textHeight=t.height),Eo(t,"backgroundColor")&&(e.textBackgroundColor=t.backgroundColor),Eo(t,"padding")&&(e.textPadding=t.padding),Eo(t,"borderColor")&&(e.textBorderColor=t.borderColor),Eo(t,"borderWidth")&&(e.textBorderWidth=t.borderWidth),Eo(t,"borderRadius")&&(e.textBorderRadius=t.borderRadius),Eo(t,"shadowColor")&&(e.textBoxShadowColor=t.shadowColor),Eo(t,"shadowBlur")&&(e.textBoxShadowBlur=t.shadowBlur),Eo(t,"shadowOffsetX")&&(e.textBoxShadowOffsetX=t.shadowOffsetX),Eo(t,"shadowOffsetY")&&(e.textBoxShadowOffsetY=t.shadowOffsetY),Eo(t,"textShadowColor")&&(e.textShadowColor=t.textShadowColor),Eo(t,"textShadowBlur")&&(e.textShadowBlur=t.textShadowBlur),Eo(t,"textShadowOffsetX")&&(e.textShadowOffsetX=t.textShadowOffsetX),Eo(t,"textShadowOffsetY")&&(e.textShadowOffsetY=t.textShadowOffsetY))}var nB={position:["x","y"],scale:["scaleX","scaleY"],origin:["originX","originY"]},iB=$r(nB),aB=(jr(hc,(function(e,t){return e[t]=1,e}),{}),hc.join(", "),["","style","shape","extra"]),rB=Pd();function oB(e,t,n,i,a){var r=e+"Animation",o=sf(e,i,a)||{},s=rB(t).userDuring;return o.duration>0&&(o.during=s?Kr(mB,{el:t,userDuring:s}):null,o.setToFinal=!0,o.scope=e),Br(o,n[r]),o}function sB(e,t,n,i){var a=(i=i||{}).dataIndex,r=i.isInit,o=i.clearStyle,s=n.isAnimationEnabled(),l=rB(e),p=t.style;l.userDuring=t.during;var c={},d={};if(function(e,t,n){for(var i=0;i=0)){var d=e.getAnimationStyleProps(),u=d?d.style:null;if(u){!a&&(a=i.style={});var m=$r(n);for(p=0;p0&&e.animateFrom(u,m)}else!function(e,t,n,i,a){if(a){var r=oB("update",e,t,i,n);r.duration>0&&e.animateFrom(a,r)}}(e,t,a||0,n,c);lB(e,t),p?e.dirty():e.markRedraw()}function lB(e,t){for(var n=rB(e).leaveToProps,i=0;i=0){!r&&(r=i[e]={});var u=$r(o);for(c=0;ci[1]&&i.reverse(),{coordSys:{type:"polar",cx:e.cx,cy:e.cy,r:i[1],r0:i[0]},api:{coord:function(i){var a=t.dataToRadius(i[0]),r=n.dataToAngle(i[1]),o=e.coordToPoint([a,r]);return o.push(a,r*Math.PI/180),o},size:Kr(XR,e)}}},calendar:function(e){var t=e.getRect(),n=e.getRangeInfo();return{coordSys:{type:"calendar",x:t.x,y:t.y,width:t.width,height:t.height,cellWidth:e.getCellWidth(),cellHeight:e.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:function(t,n){return e.dataToPoint(t,n)}}}}};function DB(e){return e instanceof Sm}function OB(e){return e instanceof bu}var AB=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.render=function(e,t,n,i){this._progressiveEls=null;var a=this._data,r=e.getData(),o=this.group,s=LB(e,r,t,n);a||o.removeAll(),r.diff(a).add((function(t){qB(n,null,t,s(t,i),e,o,r)})).remove((function(t){var n=a.getItemGraphicEl(t);n&&pB(n,HR(n).option,e)})).update((function(t,l){var p=a.getItemGraphicEl(l);qB(n,p,t,s(t,i),e,o,r)})).execute();var l=e.get("clip",!0)?UE(e.coordinateSystem,!1,e):null;l?o.setClipPath(l):o.removeClipPath(),this._data=r},t.prototype.incrementalPrepareRender=function(e,t,n){this.group.removeAll(),this._data=null},t.prototype.incrementalRender=function(e,t,n,i,a){var r=t.getData(),o=LB(t,r,n,i),s=this._progressiveEls=[];function l(e){e.isGroup||(e.incremental=!0,e.ensureState("emphasis").hoverLayer=!0)}for(var p=e.start;p=0?t.getStore().get(a,n):void 0}var r=t.get(i.name,n),o=i&&i.ordinalMeta;return o?o.categories[r]:r},styleEmphasis:function(n,i){0;null==i&&(i=s);var a=v(i,wB).getItemStyle(),r=x(i,wB),o=Zf(r,null,null,!0,!0);o.text=r.getShallow("show")?uo(e.getFormattedLabel(i,wB),e.getFormattedLabel(i,SB),CE(t,i)):null;var l=Qf(r,null,!0);return w(n,a),a=eB(a,o,l),n&&b(a,n),a.legacy=!0,a},visual:function(e,n){if(null==n&&(n=s),Eo(UR,e)){var i=t.getItemVisual(n,"style");return i?i[UR[e]]:null}if(Eo(jR,e))return t.getItemVisual(n,e)},barLayout:function(e){if("cartesian2d"===r.type){return function(e){var t=[],n=e.axis,i="axis0";if("category"===n.type){for(var a=n.getBandWidth(),r=0;r=d;h--){var g=t.childAt(h);WB(t,g,a)}}(e,d,n,i,a),o>=0?r.replaceAt(d,o):r.add(d),d}function zB(e,t,n){var i,a=HR(e),r=t.type,o=t.shape,s=t.style;return n.isUniversalTransitionEnabled()||null!=r&&r!==a.customGraphicType||"path"===r&&((i=o)&&(Eo(i,"pathData")||Eo(i,"d")))&&XB(o)!==a.customPathData||"image"===r&&Eo(s,"image")&&s.image!==a.customImagePath}function UB(e,t,n){var i=t?jB(e,t):e,a=t?HB(e,i,wB):e.style,r=e.type,o=i?i.textConfig:null,s=e.textContent,l=s?t?jB(s,t):s:null;if(a&&(n.isLegacy||ZR(a,r,!!o,!!l))){n.isLegacy=!0;var p=QR(a,r,!t);!o&&p.textConfig&&(o=p.textConfig),!l&&p.textContent&&(l=p.textContent)}if(!t&&l){var c=l;!c.type&&(c.type="text")}var d=t?n[t]:n.normal;d.cfg=o,d.conOpt=l}function jB(e,t){return t?e?e[t]:null:e}function HB(e,t,n){var i=t&&t.style;return null==i&&n===wB&&e&&(i=e.styleEmphasis),i}function WB(e,t,n){t&&pB(t,HR(e).option,n)}function $B(e,t){var n=e&&e.name;return null!=n?n:"e\0\0"+t}function KB(e,t){var n=this.context,i=null!=e?n.newChildren[e]:null,a=null!=t?n.oldChildren[t]:null;GB(n.api,a,n.dataIndex,i,n.seriesModel,n.group)}function YB(e){var t=this.context,n=t.oldChildren[e];n&&pB(n,HR(n).option,t.seriesModel)}function XB(e){return e&&(e.pathData||e.d)}function ZB(e){e.registerChartView(AB),e.registerSeriesModel(WR)}var QB=Pd(),JB=Ar,eN=Kr,tN=function(){function e(){this._dragging=!1,this.animationThreshold=15}return e.prototype.render=function(e,t,n,i){var a=t.get("value"),r=t.get("status");if(this._axisModel=e,this._axisPointerModel=t,this._api=n,i||this._lastValue!==a||this._lastStatus!==r){this._lastValue=a,this._lastStatus=r;var o=this._group,s=this._handle;if(!r||"hide"===r)return o&&o.hide(),void(s&&s.hide());o&&o.show(),s&&s.show();var l={};this.makeElOption(l,a,e,t,n);var p=l.graphicKey;p!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=p;var c=this._moveAnimation=this.determineAnimation(e,t);if(o){var d=Yr(nN,t,c);this.updatePointerEl(o,l,d),this.updateLabelEl(o,l,d,t)}else o=this._group=new Rc,this.createPointerEl(o,l,e,t),this.createLabelEl(o,l,e,t),n.getZr().add(o);oN(o,t,!0),this._renderHandle(a)}},e.prototype.remove=function(e){this.clear(e)},e.prototype.dispose=function(e){this.clear(e)},e.prototype.determineAnimation=function(e,t){var n=t.get("animation"),i=e.axis,a="category"===i.type,r=t.get("snap");if(!r&&!a)return!1;if("auto"===n||null==n){var o=this.animationThreshold;if(a&&i.getBandWidth()>o)return!0;if(r){var s=Ok(e).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>o}return!1}return!0===n},e.prototype.makeElOption=function(e,t,n,i,a){},e.prototype.createPointerEl=function(e,t,n,i){var a=t.pointer;if(a){var r=QB(e).pointerEl=new Hf[a.type](JB(t.pointer));e.add(r)}},e.prototype.createLabelEl=function(e,t,n,i){if(t.label){var a=QB(e).labelEl=new Nm(JB(t.label));e.add(a),aN(a,i)}},e.prototype.updatePointerEl=function(e,t,n){var i=QB(e).pointerEl;i&&t.pointer&&(i.setStyle(t.pointer.style),n(i,{shape:t.pointer.shape}))},e.prototype.updateLabelEl=function(e,t,n,i){var a=QB(e).labelEl;a&&(a.setStyle(t.label.style),n(a,{x:t.label.x,y:t.label.y}),aN(a,i))},e.prototype._renderHandle=function(e){if(!this._dragging&&this.updateHandleTransform){var t,n=this._axisPointerModel,i=this._api.getZr(),a=this._handle,r=n.getModel("handle"),o=n.get("status");if(!r.get("show")||!o||"hide"===o)return a&&i.remove(a),void(this._handle=null);this._handle||(t=!0,a=this._handle=Lf(r.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(e){gs(e.event)},onmousedown:eN(this._onHandleDragMove,this,0,0),drift:eN(this._onHandleDragMove,this),ondragend:eN(this._onHandleDragEnd,this)}),i.add(a)),oN(a,n,!1),a.setStyle(r.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=r.get("size");Xr(s)||(s=[s,s]),a.scaleX=s[0]/2,a.scaleY=s[1]/2,Rw(this,"_doDispatchAxisPointer",r.get("throttle")||0,"fixRate"),this._moveHandleToValue(e,t)}},e.prototype._moveHandleToValue=function(e,t){nN(this._axisPointerModel,!t&&this._moveAnimation,this._handle,rN(this.getHandleTransform(e,this._axisModel,this._axisPointerModel)))},e.prototype._onHandleDragMove=function(e,t){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(rN(n),[e,t],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(rN(i)),QB(n).lastProp=null,this._doDispatchAxisPointer()}},e.prototype._doDispatchAxisPointer=function(){if(this._handle){var e=this._payloadInfo,t=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:e.cursorPoint[0],y:e.cursorPoint[1],tooltipOption:e.tooltipOption,axesInfo:[{axisDim:t.axis.dim,axisIndex:t.componentIndex}]})}},e.prototype._onHandleDragEnd=function(){if(this._dragging=!1,this._handle){var e=this._axisPointerModel.get("value");this._moveHandleToValue(e),this._api.dispatchAction({type:"hideTip"})}},e.prototype.clear=function(e){this._lastValue=null,this._lastStatus=null;var t=e.getZr(),n=this._group,i=this._handle;t&&n&&(this._lastGraphicKey=null,n&&t.remove(n),i&&t.remove(i),this._group=null,this._handle=null,this._payloadInfo=null),Bw(this,"_doDispatchAxisPointer")},e.prototype.doClear=function(){},e.prototype.buildLabel=function(e,t,n){return{x:e[n=n||0],y:e[1-n],width:t[n],height:t[1-n]}},e}();function nN(e,t,n,i){iN(QB(n).lastProp,i)||(QB(n).lastProp=i,t?pf(n,i,e):(n.stopAnimation(),n.attr(i)))}function iN(e,t){if(to(e)&&to(t)){var n=!0;return zr(t,(function(t,i){n=n&&iN(e[i],t)})),!!n}return e===t}function aN(e,t){e[t.get(["label","show"])?"show":"hide"]()}function rN(e){return{x:e.x||0,y:e.y||0,rotation:e.rotation||0}}function oN(e,t,n){var i=t.get("z"),a=t.get("zlevel");e&&e.traverse((function(e){"group"!==e.type&&(null!=i&&(e.z=i),null!=a&&(e.zlevel=a),e.silent=n)}))}function sN(e){var t,n=e.get("type"),i=e.getModel(n+"Style");return"line"===n?(t=i.getLineStyle()).fill=null:"shadow"===n&&((t=i.getAreaStyle()).stroke=null),t}function lN(e,t,n,i,a){var r=pN(n.get("value"),t.axis,t.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),o=n.getModel("label"),s=pv(o.get("padding")||0),l=o.getFont(),p=xc(r,l),c=a.position,d=p.width+s[1]+s[3],u=p.height+s[0]+s[2],m=a.align;"right"===m&&(c[0]-=d),"center"===m&&(c[0]-=d/2);var h=a.verticalAlign;"bottom"===h&&(c[1]-=u),"middle"===h&&(c[1]-=u/2),function(e,t,n,i){var a=i.getWidth(),r=i.getHeight();e[0]=Math.min(e[0]+t,a)-t,e[1]=Math.min(e[1]+n,r)-n,e[0]=Math.max(e[0],0),e[1]=Math.max(e[1],0)}(c,d,u,i);var g=o.get("backgroundColor");g&&"auto"!==g||(g=t.get(["axisLine","lineStyle","color"])),e.label={x:c[0],y:c[1],style:Zf(o,{text:r,font:l,fill:o.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function pN(e,t,n,i,a){e=t.scale.parse(e);var r=t.scale.getLabel({value:e},{precision:a.precision}),o=a.formatter;if(o){var s={value:iI(t,{value:e}),axisDimension:t.dim,axisIndex:t.index,seriesData:[]};zr(i,(function(e){var t=n.getSeriesByIndex(e.seriesIndex),i=e.dataIndexInside,a=t&&t.getDataParams(i);a&&s.seriesData.push(a)})),Qr(o)?r=o.replace("{value}",r):Zr(o)&&(r=o(s))}return r}function cN(e,t,n){var i=[1,0,0,1,0,0];return _s(i,i,n.rotation),Cs(i,i,n.position),Of([e.dataToCoord(t),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function dN(e,t,n,i,a,r){var o=_k.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=a.get(["label","margin"]),lN(t,i,a,r,{position:cN(i.axis,e,n),align:o.textAlign,verticalAlign:o.textVerticalAlign})}function uN(e,t,n){return{x1:e[n=n||0],y1:e[1-n],x2:t[n],y2:t[1-n]}}function mN(e,t,n){return{x:e[n=n||0],y:e[1-n],width:t[n],height:t[1-n]}}function hN(e,t,n,i,a,r){return{cx:e,cy:t,r0:n,r:i,startAngle:a,endAngle:r,clockwise:!0}}var gN=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.makeElOption=function(e,t,n,i,a){var r=n.axis,o=r.grid,s=i.get("type"),l=fN(o,r).getOtherAxis(r).getGlobalExtent(),p=r.toGlobalCoord(r.dataToCoord(t,!0));if(s&&"none"!==s){var c=sN(i),d=yN[s](r,p,l);d.style=c,e.graphicKey=d.type,e.pointer=d}dN(t,e,hk(o.model,n),n,i,a)},t.prototype.getHandleTransform=function(e,t,n){var i=hk(t.axis.grid.model,t,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var a=cN(t.axis,e,i);return{x:a[0],y:a[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},t.prototype.updateHandleTransform=function(e,t,n,i){var a=n.axis,r=a.grid,o=a.getGlobalExtent(!0),s=fN(r,a).getOtherAxis(a).getGlobalExtent(),l="x"===a.dim?0:1,p=[e.x,e.y];p[l]+=t[l],p[l]=Math.min(o[1],p[l]),p[l]=Math.max(o[0],p[l]);var c=(s[1]+s[0])/2,d=[c,c];d[l]=p[l];return{x:p[0],y:p[1],rotation:e.rotation,cursorPoint:d,tooltipOption:[{verticalAlign:"middle"},{align:"center"}][l]}},t}(tN);function fN(e,t){var n={};return n[t.dim+"AxisIndex"]=t.index,e.getCartesian(n)}var yN={line:function(e,t,n){return{type:"Line",subPixelOptimize:!0,shape:uN([t,n[0]],[t,n[1]],vN(e))}},shadow:function(e,t,n){var i=Math.max(1,e.getBandWidth()),a=n[1]-n[0];return{type:"Rect",shape:mN([t-i/2,n[0]],[i,a],vN(e))}}};function vN(e){return"x"===e.dim?0:1}var xN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="axisPointer",t.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#B9BEC9",width:1,type:"dashed"},shadowStyle:{color:"rgba(210,219,238,0.2)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}},t}(kv),bN=Pd(),wN=zr;function SN(e,t,n){if(!ur.node){var i=t.getZr();bN(i).records||(bN(i).records={}),function(e,t){if(bN(e).initialized)return;function n(n,i){e.on(n,(function(n){var a=function(e){var t={showTip:[],hideTip:[]},n=function(i){var a=t[i.type];a?a.push(i):(i.dispatchAction=n,e.dispatchAction(i))};return{dispatchAction:n,pendings:t}}(t);wN(bN(e).records,(function(e){e&&i(e,n,a.dispatchAction)})),function(e,t){var n,i=e.showTip.length,a=e.hideTip.length;i?n=e.showTip[i-1]:a&&(n=e.hideTip[a-1]);n&&(n.dispatchAction=null,t.dispatchAction(n))}(a.pendings,t)}))}bN(e).initialized=!0,n("click",Yr(_N,"click")),n("mousemove",Yr(_N,"mousemove")),n("globalout",CN)}(i,t),(bN(i).records[e]||(bN(i).records[e]={})).handler=n}}function CN(e,t,n){e.handler("leave",null,n)}function _N(e,t,n,i){t.handler(e,n,i)}function TN(e,t){if(!ur.node){var n=t.getZr();(bN(n).records||{})[e]&&(bN(n).records[e]=null)}}var IN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.render=function(e,t,n){var i=t.getComponent("tooltip"),a=e.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";SN("axisPointer",n,(function(e,t,n){"none"!==a&&("leave"===e||a.indexOf(e)>=0)&&n({type:"updateAxisPointer",currTrigger:e,x:t&&t.offsetX,y:t&&t.offsetY})}))},t.prototype.remove=function(e,t){TN("axisPointer",t)},t.prototype.dispose=function(e,t){TN("axisPointer",t)},t.type="axisPointer",t}(ww);function EN(e,t){var n,i=[],a=e.seriesIndex;if(null==a||!(n=t.getSeriesByIndex(a)))return{point:[]};var r=n.getData(),o=kd(r,e);if(null==o||o<0||Xr(o))return{point:[]};var s=r.getItemGraphicEl(o),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(o)||[];else if(l&&l.dataToPoint)if(e.isStacked){var p=l.getBaseAxis(),c=l.getOtherAxis(p).dim,d=p.dim,u="x"===c||"radius"===c?1:0,m=r.mapDimension(d),h=[];h[u]=r.get(m,o),h[1-u]=r.get(r.getCalculationInfo("stackResultDimension"),o),i=l.dataToPoint(h)||[]}else i=l.dataToPoint(r.getValues(Ur(l.dimensions,(function(e){return r.mapDimension(e)})),o))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),i=[g.x+g.width/2,g.y+g.height/2]}return{point:i,el:s}}var MN=Pd();function kN(e,t,n){var i=e.currTrigger,a=[e.x,e.y],r=e,o=e.dispatchAction||Kr(n.dispatchAction,n),s=t.getComponent("axisPointer").coordSysAxesInfo;if(s){FN(a)&&(a=EN({seriesIndex:r.seriesIndex,dataIndex:r.dataIndex},t).point);var l=FN(a),p=r.axesInfo,c=s.axesInfo,d="leave"===i||FN(a),u={},m={},h={list:[],map:{}},g={showPointer:Yr(DN,m),showTooltip:Yr(ON,h)};zr(s.coordSysMap,(function(e,t){var n=l||e.containPoint(a);zr(s.coordSysAxesInfo[t],(function(e,t){var i=e.axis,r=function(e,t){for(var n=0;n<(e||[]).length;n++){var i=e[n];if(t.axis.dim===i.axisDim&&t.axis.model.componentIndex===i.axisIndex)return i}}(p,e);if(!d&&n&&(!p||r)){var o=r&&r.value;null!=o||l||(o=i.pointToData(a)),null!=o&&PN(e,o,g,!1,u)}}))}));var f={};return zr(c,(function(e,t){var n=e.linkGroup;n&&!m[t]&&zr(n.axesInfo,(function(t,i){var a=m[i];if(t!==e&&a){var r=a.value;n.mapper&&(r=e.axis.scale.parse(n.mapper(r,AN(t),AN(e)))),f[e.key]=r}}))})),zr(f,(function(e,t){PN(c[t],e,g,!0,u)})),function(e,t,n){var i=n.axesInfo=[];zr(t,(function(t,n){var a=t.axisPointerModel.option,r=e[n];r?(!t.useHandle&&(a.status="show"),a.value=r.value,a.seriesDataIndices=(r.payloadBatch||[]).slice()):!t.useHandle&&(a.status="hide"),"show"===a.status&&i.push({axisDim:t.axis.dim,axisIndex:t.axis.model.componentIndex,value:a.value})}))}(m,c,u),function(e,t,n,i){if(FN(t)||!e.list.length)return void i({type:"hideTip"});var a=((e.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:t[0],y:t[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:a.dataIndexInside,dataIndex:a.dataIndex,seriesIndex:a.seriesIndex,dataByCoordSys:e.list})}(h,a,e,o),function(e,t,n){var i=n.getZr(),a="axisPointerLastHighlights",r=MN(i)[a]||{},o=MN(i)[a]={};zr(e,(function(e,t){var n=e.axisPointerModel.option;"show"===n.status&&e.triggerEmphasis&&zr(n.seriesDataIndices,(function(e){var t=e.seriesIndex+" | "+e.dataIndex;o[t]=e}))}));var s=[],l=[];zr(r,(function(e,t){!o[t]&&l.push(e)})),zr(o,(function(e,t){!r[t]&&s.push(e)})),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}(c,0,n),u}}function PN(e,t,n,i,a){var r=e.axis;if(!r.scale.isBlank()&&r.containData(t))if(e.involveSeries){var o=function(e,t){var n=t.axis,i=n.dim,a=e,r=[],o=Number.MAX_VALUE,s=-1;return zr(t.seriesModels,(function(t,l){var p,c,d=t.getData().mapDimensionsAll(i);if(t.getAxisTooltipData){var u=t.getAxisTooltipData(d,e,n);c=u.dataIndices,p=u.nestestValue}else{if(!(c=t.getData().indicesOfNearest(d[0],e,"category"===n.type?.5:null)).length)return;p=t.getData().get(d[0],c[0])}if(null!=p&&isFinite(p)){var m=e-p,h=Math.abs(m);h<=o&&((h=0&&s<0)&&(o=h,s=m,a=p,r.length=0),zr(c,(function(e){r.push({seriesIndex:t.seriesIndex,dataIndexInside:e,dataIndex:t.getData().getRawIndex(e)})})))}})),{payloadBatch:r,snapToValue:a}}(t,e),s=o.payloadBatch,l=o.snapToValue;s[0]&&null==a.seriesIndex&&Br(a,s[0]),!i&&e.snap&&r.containData(l)&&null!=l&&(t=l),n.showPointer(e,t,s),n.showTooltip(e,o,l)}else n.showPointer(e,t)}function DN(e,t,n,i){e[t.key]={value:n,payloadBatch:i}}function ON(e,t,n,i){var a=n.payloadBatch,r=t.axis,o=r.model,s=t.axisPointerModel;if(t.triggerTooltip&&a.length){var l=t.coordSys.model,p=Fk(l),c=e.map[p];c||(c=e.map[p]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},e.list.push(c)),c.dataByAxis.push({axisDim:r.dim,axisIndex:o.componentIndex,axisType:o.type,axisId:o.id,value:i,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:a.slice()})}}function AN(e){var t=e.axis.model,n={},i=n.axisDim=e.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=t.componentIndex,n.axisName=n[i+"AxisName"]=t.name,n.axisId=n[i+"AxisId"]=t.id,n}function FN(e){return!e||null==e[0]||isNaN(e[0])||null==e[1]||isNaN(e[1])}function RN(e){Bk.registerAxisPointerClass("CartesianAxisPointer",gN),e.registerComponentModel(xN),e.registerComponentView(IN),e.registerPreprocessor((function(e){if(e){(!e.axisPointer||0===e.axisPointer.length)&&(e.axisPointer={});var t=e.axisPointer.link;t&&!Xr(t)&&(e.axisPointer.link=[t])}})),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,(function(e,t){e.getComponent("axisPointer").coordSysAxesInfo=Pk(e,t)})),e.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},kN)}function BN(e){dI(Kk),dI(RN)}var NN=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.makeElOption=function(e,t,n,i,a){var r=n.axis;"angle"===r.dim&&(this.animationThreshold=Math.PI/18);var o=r.polar,s=o.getOtherAxis(r).getExtent(),l=r.dataToCoord(t),p=i.get("type");if(p&&"none"!==p){var c=sN(i),d=LN[p](r,o,l,s);d.style=c,e.graphicKey=d.type,e.pointer=d}var u=function(e,t,n,i,a){var r=t.axis,o=r.dataToCoord(e),s=i.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l,p,c,d=i.getRadiusAxis().getExtent();if("radius"===r.dim){var u=[1,0,0,1,0,0];_s(u,u,s),Cs(u,u,[i.cx,i.cy]),l=Of([o,-a],u);var m=t.getModel("axisLabel").get("rotate")||0,h=_k.innerTextLayout(s,m*Math.PI/180,-1);p=h.textAlign,c=h.textVerticalAlign}else{var g=d[1];l=i.coordToPoint([g+a,o]);var f=i.cx,y=i.cy;p=Math.abs(l[0]-f)/g<.3?"center":l[0]>f?"left":"right",c=Math.abs(l[1]-y)/g<.3?"middle":l[1]>y?"top":"bottom"}return{position:l,align:p,verticalAlign:c}}(t,n,0,o,i.get(["label","margin"]));lN(e,n,i,a,u)},t}(tN);var LN={line:function(e,t,n,i){return"angle"===e.dim?{type:"Line",shape:uN(t.coordToPoint([i[0],n]),t.coordToPoint([i[1],n]))}:{type:"Circle",shape:{cx:t.cx,cy:t.cy,r:n}}},shadow:function(e,t,n,i){var a=Math.max(1,e.getBandWidth()),r=Math.PI/180;return"angle"===e.dim?{type:"Sector",shape:hN(t.cx,t.cy,i[0],i[1],(-n-a/2)*r,(a/2-n)*r)}:{type:"Sector",shape:hN(t.cx,t.cy,n-a/2,n+a/2,0,2*Math.PI)}}},VN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.findAxisModel=function(e){var t;return this.ecModel.eachComponent(e,(function(e){e.getCoordSysModel()===this&&(t=e)}),this),t},t.type="polar",t.dependencies=["radiusAxis","angleAxis"],t.defaultOption={z:0,center:["50%","50%"],radius:"80%"},t}(kv),qN=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return We(t,e),t.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",Fd).models[0]},t.type="polarAxis",t}(kv);qr(qN,lI);var GN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="angleAxis",t}(qN),zN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="radiusAxis",t}(qN),UN=function(e){function t(t,n){return e.call(this,"radius",t,n)||this}return We(t,e),t.prototype.pointToData=function(e,t){return this.polar.pointToData(e,t)["radius"===this.dim?0:1]},t}(RI);UN.prototype.dataToRadius=RI.prototype.dataToCoord,UN.prototype.radiusToData=RI.prototype.coordToData;var jN=Pd(),HN=function(e){function t(t,n){return e.call(this,"angle",t,n||[0,360])||this}return We(t,e),t.prototype.pointToData=function(e,t){return this.polar.pointToData(e,t)["radius"===this.dim?0:1]},t.prototype.calculateCategoryInterval=function(){var e=this,t=e.getLabelModel(),n=e.scale,i=n.getExtent(),a=n.count();if(i[1]-i[0]<1)return 0;var r=i[0],o=e.dataToCoord(r+1)-e.dataToCoord(r),s=Math.abs(o),l=xc(null==r?"":r+"",t.getFont(),"center","top"),p=Math.max(l.height,7)/s;isNaN(p)&&(p=1/0);var c=Math.max(0,Math.floor(p)),d=jN(e.model),u=d.lastAutoInterval,m=d.lastTickCount;return null!=u&&null!=m&&Math.abs(u-c)<=1&&Math.abs(m-a)<=1&&u>c?c=u:(d.lastTickCount=a,d.lastAutoInterval=c),c},t}(RI);HN.prototype.dataToAngle=RI.prototype.dataToCoord,HN.prototype.angleToData=RI.prototype.coordToData;var WN=["radius","angle"],$N=function(){function e(e){this.dimensions=WN,this.type="polar",this.cx=0,this.cy=0,this._radiusAxis=new UN,this._angleAxis=new HN,this.axisPointerEnabled=!0,this.name=e||"",this._radiusAxis.polar=this._angleAxis.polar=this}return e.prototype.containPoint=function(e){var t=this.pointToCoord(e);return this._radiusAxis.contain(t[0])&&this._angleAxis.contain(t[1])},e.prototype.containData=function(e){return this._radiusAxis.containData(e[0])&&this._angleAxis.containData(e[1])},e.prototype.getAxis=function(e){return this["_"+e+"Axis"]},e.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},e.prototype.getAxesByScale=function(e){var t=[],n=this._angleAxis,i=this._radiusAxis;return n.scale.type===e&&t.push(n),i.scale.type===e&&t.push(i),t},e.prototype.getAngleAxis=function(){return this._angleAxis},e.prototype.getRadiusAxis=function(){return this._radiusAxis},e.prototype.getOtherAxis=function(e){var t=this._angleAxis;return e===t?this._radiusAxis:t},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},e.prototype.getTooltipAxes=function(e){var t=null!=e&&"auto"!==e?this.getAxis(e):this.getBaseAxis();return{baseAxes:[t],otherAxes:[this.getOtherAxis(t)]}},e.prototype.dataToPoint=function(e,t){return this.coordToPoint([this._radiusAxis.dataToRadius(e[0],t),this._angleAxis.dataToAngle(e[1],t)])},e.prototype.pointToData=function(e,t){var n=this.pointToCoord(e);return[this._radiusAxis.radiusToData(n[0],t),this._angleAxis.angleToData(n[1],t)]},e.prototype.pointToCoord=function(e){var t=e[0]-this.cx,n=e[1]-this.cy,i=this.getAngleAxis(),a=i.getExtent(),r=Math.min(a[0],a[1]),o=Math.max(a[0],a[1]);i.inverse?r=o-360:o=r+360;var s=Math.sqrt(t*t+n*n);t/=s,n/=s;for(var l=Math.atan2(-n,t)/Math.PI*180,p=lo;)l+=360*p;return[s,l]},e.prototype.coordToPoint=function(e){var t=e[0],n=e[1]/180*Math.PI;return[Math.cos(n)*t+this.cx,-Math.sin(n)*t+this.cy]},e.prototype.getArea=function(){var e=this.getAngleAxis(),t=this.getRadiusAxis().getExtent().slice();t[0]>t[1]&&t.reverse();var n=e.getExtent(),i=Math.PI/180;return{cx:this.cx,cy:this.cy,r0:t[0],r:t[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:e.inverse,contain:function(e,t){var n=e-this.cx,i=t-this.cy,a=n*n+i*i-1e-4,r=this.r,o=this.r0;return a<=r*r&&a>=o*o}}},e.prototype.convertToPixel=function(e,t,n){return KN(t)===this?this.dataToPoint(n):null},e.prototype.convertFromPixel=function(e,t,n){return KN(t)===this?this.pointToData(n):null},e}();function KN(e){var t=e.seriesModel,n=e.polarModel;return n&&n.coordinateSystem||t&&t.coordinateSystem}function YN(e,t){var n=this,i=n.getAngleAxis(),a=n.getRadiusAxis();if(i.scale.setExtent(1/0,-1/0),a.scale.setExtent(1/0,-1/0),e.eachSeries((function(e){if(e.coordinateSystem===n){var t=e.getData();zr(sI(t,"radius"),(function(e){a.scale.unionExtentFromData(t,e)})),zr(sI(t,"angle"),(function(e){i.scale.unionExtentFromData(t,e)}))}})),eI(i.scale,i.model),eI(a.scale,a.model),"category"===i.type&&!i.onBand){var r=i.getExtent(),o=360/i.scale.count();i.inverse?r[1]+=o:r[1]-=o,i.setExtent(r[0],r[1])}}function XN(e,t){var n;if(e.type=t.get("type"),e.scale=tI(t),e.onBand=t.get("boundaryGap")&&"category"===e.type,e.inverse=t.get("inverse"),function(e){return"angleAxis"===e.mainType}(t)){e.inverse=e.inverse!==t.get("clockwise");var i=t.get("startAngle"),a=null!==(n=t.get("endAngle"))&&void 0!==n?n:i+(e.inverse?-360:360);e.setExtent(i,a)}t.axis=e,e.model=t}var ZN={dimensions:WN,create:function(e,t){var n=[];return e.eachComponent("polar",(function(e,i){var a=new $N(i+"");a.update=YN;var r=a.getRadiusAxis(),o=a.getAngleAxis(),s=e.findAxisModel("radiusAxis"),l=e.findAxisModel("angleAxis");XN(r,s),XN(o,l),function(e,t,n){var i=t.get("center"),a=n.getWidth(),r=n.getHeight();e.cx=Wc(i[0],a),e.cy=Wc(i[1],r);var o=e.getRadiusAxis(),s=Math.min(a,r)/2,l=t.get("radius");null==l?l=[0,"100%"]:Xr(l)||(l=[0,l]);var p=[Wc(l[0],s),Wc(l[1],s)];o.inverse?o.setExtent(p[1],p[0]):o.setExtent(p[0],p[1])}(a,e,t),n.push(a),e.coordinateSystem=a,a.model=e})),e.eachSeries((function(e){if("polar"===e.get("coordinateSystem")){var t=e.getReferringComponents("polar",Fd).models[0];0,e.coordinateSystem=t.coordinateSystem}})),n}},QN=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function JN(e,t,n){t[1]>t[0]&&(t=t.slice().reverse());var i=e.coordToPoint([t[0],n]),a=e.coordToPoint([t[1],n]);return{x1:i[0],y1:i[1],x2:a[0],y2:a[1]}}function eL(e){return e.getRadiusAxis().inverse?0:1}function tL(e){var t=e[0],n=e[e.length-1];t&&n&&Math.abs(Math.abs(t.coord-n.coord)-360)<1e-4&&e.pop()}var nL=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.axisPointerClass="PolarAxisPointer",n}return We(t,e),t.prototype.render=function(e,t){if(this.group.removeAll(),e.get("show")){var n=e.axis,i=n.polar,a=i.getRadiusAxis().getExtent(),r=n.getTicksCoords(),o=n.getMinorTicksCoords(),s=Ur(n.getViewLabels(),(function(e){e=Ar(e);var t=n.scale,i="ordinal"===t.type?t.getRawOrdinalNumber(e.tickValue):e.tickValue;return e.coord=n.dataToCoord(i),e}));tL(s),tL(r),zr(QN,(function(t){!e.get([t,"show"])||n.scale.isBlank()&&"axisLine"!==t||iL[t](this.group,e,i,r,o,a,s)}),this)}},t.type="angleAxis",t}(Bk),iL={axisLine:function(e,t,n,i,a,r){var o,s=t.getModel(["axisLine","lineStyle"]),l=n.getAngleAxis(),p=Math.PI/180,c=l.getExtent(),d=eL(n),u=d?0:1,m=360===Math.abs(c[1]-c[0])?"Circle":"Arc";(o=0===r[u]?new Hf[m]({shape:{cx:n.cx,cy:n.cy,r:r[d],startAngle:-c[0]*p,endAngle:-c[1]*p,clockwise:l.inverse},style:s.getLineStyle(),z2:1,silent:!0}):new Ag({shape:{cx:n.cx,cy:n.cy,r:r[d],r0:r[u]},style:s.getLineStyle(),z2:1,silent:!0})).style.fill=null,e.add(o)},axisTick:function(e,t,n,i,a,r){var o=t.getModel("axisTick"),s=(o.get("inside")?-1:1)*o.get("length"),l=r[eL(n)],p=Ur(i,(function(e){return new Gg({shape:JN(n,[l,l+s],e.coord)})}));e.add(Ef(p,{style:Nr(o.getModel("lineStyle").getLineStyle(),{stroke:t.get(["axisLine","lineStyle","color"])})}))},minorTick:function(e,t,n,i,a,r){if(a.length){for(var o=t.getModel("axisTick"),s=t.getModel("minorTick"),l=(o.get("inside")?-1:1)*s.get("length"),p=r[eL(n)],c=[],d=0;dh?"left":"right",y=Math.abs(m[1]-g)/u<.3?"middle":m[1]>g?"top":"bottom";if(s&&s[d]){var v=s[d];to(v)&&v.textStyle&&(o=new xy(v.textStyle,l,l.ecModel))}var x=new Nm({silent:_k.isLabelSilent(t),style:Zf(o,{x:m[0],y:m[1],fill:o.getTextColor()||t.get(["axisLine","lineStyle","color"]),text:i.formattedLabel,align:f,verticalAlign:y})});if(e.add(x),c){var b=_k.makeAxisEventDataBase(t);b.targetType="axisLabel",b.value=i.rawLabel,Xm(x).eventData=b}}),this)},splitLine:function(e,t,n,i,a,r){var o=t.getModel("splitLine").getModel("lineStyle"),s=o.get("color"),l=0;s=s instanceof Array?s:[s];for(var p=[],c=0;c=0?"p":"n",I=b;v&&(i[s][_]||(i[s][_]={p:b,n:b}),I=i[s][_][T]);var E=void 0,M=void 0,k=void 0,P=void 0;if("radius"===d.dim){var D=d.dataToCoord(C)-b,O=r.dataToCoord(_);Math.abs(D)=P})}}}))}var dL={startAngle:90,clockwise:!0,splitNumber:12,axisLabel:{rotate:0}},uL={splitNumber:5},mL=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="polar",t}(ww);function hL(e){dI(RN),Bk.registerAxisPointerClass("PolarAxisPointer",NN),e.registerCoordinateSystem("polar",ZN),e.registerComponentModel(VN),e.registerComponentView(mL),sk(e,"angle",GN,dL),sk(e,"radius",zN,uL),e.registerComponentView(nL),e.registerComponentView(oL),e.registerLayout(Yr(cL,"bar"))}function gL(e,t){t=t||{};var n=e.coordinateSystem,i=e.axis,a={},r=i.position,o=i.orient,s=n.getRect(),l=[s.x,s.x+s.width,s.y,s.y+s.height],p={horizontal:{top:l[2],bottom:l[3]},vertical:{left:l[0],right:l[1]}};a.position=["vertical"===o?p.vertical[r]:l[0],"horizontal"===o?p.horizontal[r]:l[3]];a.rotation=Math.PI/2*{horizontal:0,vertical:1}[o];a.labelDirection=a.tickDirection=a.nameDirection={top:-1,bottom:1,right:1,left:-1}[r],e.get(["axisTick","inside"])&&(a.tickDirection=-a.tickDirection),po(t.labelInside,e.get(["axisLabel","inside"]))&&(a.labelDirection=-a.labelDirection);var c=t.rotate;return null==c&&(c=e.get(["axisLabel","rotate"])),a.labelRotation="top"===r?-c:c,a.z2=1,a}var fL=["axisLine","axisTickLabel","axisName"],yL=["splitArea","splitLine"],vL=(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.axisPointerClass="SingleAxisPointer",n}We(t,e),t.prototype.render=function(t,n,i,a){var r=this.group;r.removeAll();var o=this._axisGroup;this._axisGroup=new Rc;var s=gL(t),l=new _k(t,s);zr(fL,l.add,l),r.add(this._axisGroup),r.add(l.getGroup()),zr(yL,(function(e){t.get([e,"show"])&&vL[e](this,this.group,this._axisGroup,t)}),this),Rf(o,this._axisGroup,t),e.prototype.render.call(this,t,n,i,a)},t.prototype.remove=function(){Vk(this)},t.type="singleAxis"}(Bk),{splitLine:function(e,t,n,i){var a=i.axis;if(!a.scale.isBlank()){var r=i.getModel("splitLine"),o=r.getModel("lineStyle"),s=o.get("color");s=s instanceof Array?s:[s];for(var l=o.get("width"),p=i.coordinateSystem.getRect(),c=a.isHorizontal(),d=[],u=0,m=a.getTicksCoords({tickModel:r}),h=[],g=[],f=0;f=t.y&&e[1]<=t.y+t.height:n.contain(n.toLocalCoord(e[1]))&&e[0]>=t.y&&e[0]<=t.y+t.height},e.prototype.pointToData=function(e){var t=this.getAxis();return[t.coordToData(t.toLocalCoord(e["horizontal"===t.orient?0:1]))]},e.prototype.dataToPoint=function(e){var t=this.getAxis(),n=this.getRect(),i=[],a="horizontal"===t.orient?0:1;return e instanceof Array&&(e=e[0]),i[a]=t.toGlobalCoord(t.dataToCoord(+e)),i[1-a]=0===a?n.y+n.height/2:n.x+n.width/2,i},e.prototype.convertToPixel=function(e,t,n){return SL(t)===this?this.dataToPoint(n):null},e.prototype.convertFromPixel=function(e,t,n){return SL(t)===this?this.pointToData(n):null}}();function SL(e){var t=e.seriesModel,n=e.singleAxisModel;return n&&n.coordinateSystem||t&&t.coordinateSystem}var CL=["x","y"],_L=["width","height"],TL=(function(e){function t(){return null!==e&&e.apply(this,arguments)||this}We(t,e),t.prototype.makeElOption=function(e,t,n,i,a){var r=n.axis,o=r.coordinateSystem,s=EL(o,1-IL(r)),l=o.dataToPoint(t)[0],p=i.get("type");if(p&&"none"!==p){var c=sN(i),d=TL[p](r,l,s);d.style=c,e.graphicKey=d.type,e.pointer=d}dN(t,e,gL(n),n,i,a)},t.prototype.getHandleTransform=function(e,t,n){var i=gL(t,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var a=cN(t.axis,e,i);return{x:a[0],y:a[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},t.prototype.updateHandleTransform=function(e,t,n,i){var a=n.axis,r=a.coordinateSystem,o=IL(a),s=EL(r,o),l=[e.x,e.y];l[o]+=t[o],l[o]=Math.min(s[1],l[o]),l[o]=Math.max(s[0],l[o]);var p=EL(r,1-o),c=(p[1]+p[0])/2,d=[c,c];return d[o]=l[o],{x:l[0],y:l[1],rotation:e.rotation,cursorPoint:d,tooltipOption:{verticalAlign:"middle"}}}}(tN),{line:function(e,t,n){return{type:"Line",subPixelOptimize:!0,shape:uN([t,n[0]],[t,n[1]],IL(e))}},shadow:function(e,t,n){var i=e.getBandWidth(),a=n[1]-n[0];return{type:"Rect",shape:mN([t-i/2,n[0]],[i,a],IL(e))}}});function IL(e){return e.isHorizontal()?0:1}function EL(e,t){var n=e.getRect();return[n[CL[t]],n[CL[t]]+n[_L[t]]]}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.type="single"}(ww);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.init=function(t,n,i){var a=Iv(t);e.prototype.init.apply(this,arguments),ML(t,a)},t.prototype.mergeOption=function(t){e.prototype.mergeOption.apply(this,arguments),ML(this.option,t)},t.prototype.getCellSize=function(){return this.option.cellSize},t.type="calendar",t.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{color:"#fff",borderWidth:1,borderColor:"#ccc"},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}}}(kv);function ML(e,t){var n,i=e.cellSize;1===(n=Xr(i)?i:e.cellSize=[i,i]).length&&(n[1]=n[0]);var a=Ur([0,1],(function(e){return function(e,t){return null!=e[xv[t][0]]||null!=e[xv[t][1]]&&null!=e[xv[t][2]]}(t,e)&&(n[e]="auto"),null!=n[e]&&"auto"!==n[e]}));Tv(e,t,{type:"box",ignoreSize:a})}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.render=function(e,t,n){var i=this.group;i.removeAll();var a=e.coordinateSystem,r=a.getRangeInfo(),o=a.getOrient(),s=t.getLocaleModel();this._renderDayRect(e,r,i),this._renderLines(e,r,o,i),this._renderYearText(e,r,o,i),this._renderMonthText(e,s,o,i),this._renderWeekText(e,s,r,o,i)},t.prototype._renderDayRect=function(e,t,n){for(var i=e.coordinateSystem,a=e.getModel("itemStyle").getItemStyle(),r=i.getCellWidth(),o=i.getCellHeight(),s=t.start.time;s<=t.end.time;s=i.getNextNDay(s,1).time){var l=i.dataToRect([s],!1).tl,p=new Fm({shape:{x:l[0],y:l[1],width:r,height:o},cursor:"default",style:a});n.add(p)}},t.prototype._renderLines=function(e,t,n,i){var a=this,r=e.coordinateSystem,o=e.getModel(["splitLine","lineStyle"]).getLineStyle(),s=e.get(["splitLine","show"]),l=o.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var p=t.start,c=0;p.time<=t.end.time;c++){u(p.formatedDate),0===c&&(p=r.getDateInfo(t.start.y+"-"+t.start.m));var d=p.date;d.setMonth(d.getMonth()+1),p=r.getDateInfo(d)}function u(t){a._firstDayOfMonth.push(r.getDateInfo(t)),a._firstDayPoints.push(r.dataToRect([t],!1).tl);var l=a._getLinePointsOfOneWeek(e,t,n);a._tlpoints.push(l[0]),a._blpoints.push(l[l.length-1]),s&&a._drawSplitline(l,o,i)}u(r.getNextNDay(t.end.time,1).formatedDate),s&&this._drawSplitline(a._getEdgesPoints(a._tlpoints,l,n),o,i),s&&this._drawSplitline(a._getEdgesPoints(a._blpoints,l,n),o,i)},t.prototype._getEdgesPoints=function(e,t,n){var i=[e[0].slice(),e[e.length-1].slice()],a="horizontal"===n?0:1;return i[0][a]=i[0][a]-t/2,i[1][a]=i[1][a]+t/2,i},t.prototype._drawSplitline=function(e,t,n){var i=new Lg({z2:20,shape:{points:e},style:t});n.add(i)},t.prototype._getLinePointsOfOneWeek=function(e,t,n){for(var i=e.coordinateSystem,a=i.getDateInfo(t),r=[],o=0;o<7;o++){var s=i.getNextNDay(a.time,o),l=i.dataToRect([s.time],!1);r[2*s.day]=l.tl,r[2*s.day+1]=l["horizontal"===n?"bl":"tr"]}return r},t.prototype._formatterLabel=function(e,t){return Qr(e)&&e?(n=e,zr(t,(function(e,t){n=n.replace("{"+t+"}",i?os(e):e)})),n):Zr(e)?e(t):t.nameMap;var n,i},t.prototype._yearTextPositionControl=function(e,t,n,i,a){var r=t[0],o=t[1],s=["center","bottom"];"bottom"===i?(o+=a,s=["center","top"]):"left"===i?r-=a:"right"===i?(r+=a,s=["center","top"]):o-=a;var l=0;return"left"!==i&&"right"!==i||(l=Math.PI/2),{rotation:l,x:r,y:o,style:{align:s[0],verticalAlign:s[1]}}},t.prototype._renderYearText=function(e,t,n,i){var a=e.getModel("yearLabel");if(a.get("show")){var r=a.get("margin"),o=a.get("position");o||(o="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],l=(s[0][0]+s[1][0])/2,p=(s[0][1]+s[1][1])/2,c="horizontal"===n?0:1,d={top:[l,s[c][1]],bottom:[l,s[1-c][1]],left:[s[1-c][0],p],right:[s[c][0],p]},u=t.start.y;+t.end.y>+t.start.y&&(u=u+"-"+t.end.y);var m=a.get("formatter"),h={start:t.start.y,end:t.end.y,nameMap:u},g=this._formatterLabel(m,h),f=new Nm({z2:30,style:Zf(a,{text:g})});f.attr(this._yearTextPositionControl(f,d[o],n,o,r)),i.add(f)}},t.prototype._monthTextPositionControl=function(e,t,n,i,a){var r="left",o="top",s=e[0],l=e[1];return"horizontal"===n?(l+=a,t&&(r="center"),"start"===i&&(o="bottom")):(s+=a,t&&(o="middle"),"start"===i&&(r="right")),{x:s,y:l,align:r,verticalAlign:o}},t.prototype._renderMonthText=function(e,t,n,i){var a=e.getModel("monthLabel");if(a.get("show")){var r=a.get("nameMap"),o=a.get("margin"),s=a.get("position"),l=a.get("align"),p=[this._tlpoints,this._blpoints];r&&!Qr(r)||(r&&(t=Py(r)||t),r=t.get(["time","monthAbbr"])||[]);var c="start"===s?0:1,d="horizontal"===n?0:1;o="start"===s?-o:o;for(var u="center"===l,m=0;m=i.start.time&&n.timeo.end.time&&e.reverse(),e},e.prototype._getRangeInfo=function(e){var t,n=[this.getDateInfo(e[0]),this.getDateInfo(e[1])];n[0].time>n[1].time&&(t=!0,n.reverse());var i=Math.floor(n[1].time/kL)-Math.floor(n[0].time/kL)+1,a=new Date(n[0].time),r=a.getDate(),o=n[1].date.getDate();a.setDate(r+i-1);var s=a.getDate();if(s!==o)for(var l=a.getTime()-n[1].time>0?1:-1;(s=a.getDate())!==o&&(a.getTime()-n[1].time)*l>0;)i-=l,a.setDate(s-l);var p=Math.floor((i+n[0].day+6)/7),c=t?1-p:p-1;return t&&n.reverse(),{range:[n[0].formatedDate,n[1].formatedDate],start:n[0],end:n[1],allDay:i,weeks:p,nthWeek:c,fweek:n[0].day,lweek:n[1].day}},e.prototype._getDateByWeeksAndDay=function(e,t,n){var i=this._getRangeInfo(n);if(e>i.weeks||0===e&&ti.lweek)return null;var a=7*(e-1)-i.fweek+t,r=new Date(i.start.time);return r.setDate(+i.start.d+a),this.getDateInfo(r)},e.create=function(t,n){var i=[];return t.eachComponent("calendar",(function(a){var r=new e(a,t,n);i.push(r),a.coordinateSystem=r})),t.eachSeries((function(e){"calendar"===e.get("coordinateSystem")&&(e.coordinateSystem=i[e.get("calendarIndex")||0])})),i},e.dimensions=["time","value"]}();function PL(e){var t=e.calendarModel,n=e.seriesModel;return t?t.coordinateSystem:n?n.coordinateSystem:null}function DL(e,t){var n;return zr(t,(function(t){null!=e[t]&&"auto"!==e[t]&&(n=!0)})),n}var OL=["transition","enterFrom","leaveTo"],AL=OL.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function FL(e,t,n){if(n&&(!e[n]&&t[n]&&(e[n]={}),e=e[n],t=t[n]),e&&t)for(var i=n?OL:AL,a=0;a=0;l--){var u,m,h;if(h=null!=(m=Id((u=n[l]).id,null))?a.get(m):null){var g=h.parent,f=(d=BL(g),{}),y=Cv(h,u,g===i?{width:r,height:o}:{width:d.width,height:d.height},null,{hv:u.hv,boundingMode:u.bounding},f);if(!BL(h).isNew&&y){for(var v=u.transition,x={},b=0;b=0)?x[w]=S:h[w]=S}pf(h,x,e,0)}else h.attr(f)}}},t.prototype._clear=function(){var e=this,t=this._elMap;t.each((function(n){VL(n,BL(n).option,t,e._lastGraphicModel)})),this._elMap=Co()},t.prototype.dispose=function(){this._clear()},t.type="graphic"}(ww);function NL(e){var t=Eo(RL,e)?RL[e]:Cf(e);var n=new t({});return BL(n).type=e,n}function LL(e,t,n,i){var a=NL(n);return t.add(a),i.set(e,a),BL(a).id=e,BL(a).isNew=!0,a}function VL(e,t,n,i){e&&e.parent&&("group"===e.type&&e.traverse((function(e){VL(e,t,n,i)})),pB(e,t,i),n.removeKey(BL(e).id))}function qL(e,t,n,i){e.isGroup||zr([["cursor",bu.prototype.cursor],["zlevel",i||0],["z",n||0],["z2",0]],(function(n){var i=n[0];Eo(t,i)?e[i]=co(t[i],n[1]):null==e[i]&&(e[i]=n[1])})),zr($r(t),(function(n){if(0===n.indexOf("on")){var i=t[n];e[n]=Zr(i)?i:null}})),Eo(t,"draggable")&&(e.draggable=t.draggable),null!=t.name&&(e.name=t.name),null!=t.id&&(e.id=t.id)}var GL=["x","y","radius","angle","single"],zL=["cartesian2d","polar","singleAxis"];function UL(e){return e+"Axis"}function jL(e,t){var n,i=Co(),a=[],r=Co();e.eachComponent({mainType:"dataZoom",query:t},(function(e){r.get(e.uid)||s(e)}));do{n=!1,e.eachComponent("dataZoom",o)}while(n);function o(e){!r.get(e.uid)&&function(e){var t=!1;return e.eachTargetAxis((function(e,n){var a=i.get(e);a&&a[n]&&(t=!0)})),t}(e)&&(s(e),n=!0)}function s(e){r.set(e.uid,!0),a.push(e),e.eachTargetAxis((function(e,t){(i.get(e)||i.set(e,[]))[t]=!0}))}return a}function HL(e){var t=e.ecModel,n={infoList:[],infoMap:Co()};return e.eachTargetAxis((function(e,i){var a=t.getComponent(UL(e),i);if(a){var r=a.getCoordSysModel();if(r){var o=r.uid,s=n.infoMap.get(o);s||(s={model:r,axisModels:[]},n.infoList.push(s),n.infoMap.set(o,s)),s.axisModels.push(a)}}})),n}var WL=function(){function e(){this.indexList=[],this.indexMap=[]}return e.prototype.add=function(e){this.indexMap[e]||(this.indexList.push(e),this.indexMap[e]=!0)},e}(),$L=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._autoThrottle=!0,n._noTarget=!0,n._rangePropMode=["percent","percent"],n}return We(t,e),t.prototype.init=function(e,t,n){var i=KL(e);this.settledOption=i,this.mergeDefaultAndTheme(e,n),this._doInit(i)},t.prototype.mergeOption=function(e){var t=KL(e);Fr(this.option,e,!0),Fr(this.settledOption,t,!0),this._doInit(t)},t.prototype._doInit=function(e){var t=this.option;this._setDefaultThrottle(e),this._updateRangeUse(e);var n=this.settledOption;zr([["start","startValue"],["end","endValue"]],(function(e,i){"value"===this._rangePropMode[i]&&(t[e[0]]=n[e[0]]=null)}),this),this._resetTarget()},t.prototype._resetTarget=function(){var e=this.get("orient",!0),t=this._targetAxisInfoMap=Co();this._fillSpecifiedTargetAxis(t)?this._orient=e||this._makeAutoOrientByTargetAxis():(this._orient=e||"horizontal",this._fillAutoTargetAxisByOrient(t,this._orient)),this._noTarget=!0,t.each((function(e){e.indexList.length&&(this._noTarget=!1)}),this)},t.prototype._fillSpecifiedTargetAxis=function(e){var t=!1;return zr(GL,(function(n){var i=this.getReferringComponents(UL(n),Rd);if(i.specified){t=!0;var a=new WL;zr(i.models,(function(e){a.add(e.componentIndex)})),e.set(n,a)}}),this),t},t.prototype._fillAutoTargetAxisByOrient=function(e,t){var n=this.ecModel,i=!0;if(i){var a="vertical"===t?"y":"x";r(n.findComponents({mainType:a+"Axis"}),a)}i&&r(n.findComponents({mainType:"singleAxis",filter:function(e){return e.get("orient",!0)===t}}),"single");function r(t,n){var a=t[0];if(a){var r=new WL;if(r.add(a.componentIndex),e.set(n,r),i=!1,"x"===n||"y"===n){var o=a.getReferringComponents("grid",Fd).models[0];o&&zr(t,(function(e){a.componentIndex!==e.componentIndex&&o===e.getReferringComponents("grid",Fd).models[0]&&r.add(e.componentIndex)}))}}}i&&zr(GL,(function(t){if(i){var a=n.findComponents({mainType:UL(t),filter:function(e){return"category"===e.get("type",!0)}});if(a[0]){var r=new WL;r.add(a[0].componentIndex),e.set(t,r),i=!1}}}),this)},t.prototype._makeAutoOrientByTargetAxis=function(){var e;return this.eachTargetAxis((function(t){!e&&(e=t)}),this),"y"===e?"vertical":"horizontal"},t.prototype._setDefaultThrottle=function(e){if(e.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var t=this.ecModel.option;this.option.throttle=t.animation&&t.animationDurationUpdate>0?100:20}},t.prototype._updateRangeUse=function(e){var t=this._rangePropMode,n=this.get("rangeMode");zr([["start","startValue"],["end","endValue"]],(function(i,a){var r=null!=e[i[0]],o=null!=e[i[1]];r&&!o?t[a]="percent":!r&&o?t[a]="value":n?t[a]=n[a]:r&&(t[a]="percent")}))},t.prototype.noTarget=function(){return this._noTarget},t.prototype.getFirstTargetAxisModel=function(){var e;return this.eachTargetAxis((function(t,n){null==e&&(e=this.ecModel.getComponent(UL(t),n))}),this),e},t.prototype.eachTargetAxis=function(e,t){this._targetAxisInfoMap.each((function(n,i){zr(n.indexList,(function(n){e.call(t,i,n)}))}))},t.prototype.getAxisProxy=function(e,t){var n=this.getAxisModel(e,t);if(n)return n.__dzAxisProxy},t.prototype.getAxisModel=function(e,t){var n=this._targetAxisInfoMap.get(e);if(n&&n.indexMap[t])return this.ecModel.getComponent(UL(e),t)},t.prototype.setRawRange=function(e){var t=this.option,n=this.settledOption;zr([["start","startValue"],["end","endValue"]],(function(i){null==e[i[0]]&&null==e[i[1]]||(t[i[0]]=n[i[0]]=e[i[0]],t[i[1]]=n[i[1]]=e[i[1]])}),this),this._updateRangeUse(e)},t.prototype.setCalculatedRange=function(e){var t=this.option;zr(["start","startValue","end","endValue"],(function(n){t[n]=e[n]}))},t.prototype.getPercentRange=function(){var e=this.findRepresentativeAxisProxy();if(e)return e.getDataPercentWindow()},t.prototype.getValueRange=function(e,t){var n;if(null==e&&null==t){if(n=this.findRepresentativeAxisProxy())return n.getDataValueWindow()}else if(n=this.getAxisProxy(e,t))return n.getDataValueWindow()},t.prototype.findRepresentativeAxisProxy=function(e){if(e)return e.__dzAxisProxy;for(var t,n=this._targetAxisInfoMap.keys(),i=0;i=0}(t)){var n=UL(this._dimName),i=t.getReferringComponents(n,Fd).models[0];i&&this._axisIndex===i.componentIndex&&e.push(t)}}),this),e},e.prototype.getAxisModel=function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},e.prototype.getMinMaxSpan=function(){return Ar(this._minMaxSpan)},e.prototype.calculateDataWindow=function(e){var t,n=this._dataExtent,i=this.getAxisModel().axis.scale,a=this._dataZoomModel.getRangePropMode(),r=[0,100],o=[],s=[];XL(["start","end"],(function(l,p){var c=e[l],d=e[l+"Value"];"percent"===a[p]?(null==c&&(c=r[p]),d=i.parse(Hc(c,r,n))):(t=!0,c=Hc(d=null==d?n[p]:i.parse(d),n,r)),s[p]=null==d||isNaN(d)?n[p]:d,o[p]=null==c||isNaN(c)?r[p]:c})),ZL(s),ZL(o);var l=this._minMaxSpan;function p(e,t,n,a,r){var o=r?"Span":"ValueSpan";EA(0,e,n,"all",l["min"+o],l["max"+o]);for(var s=0;s<2;s++)t[s]=Hc(e[s],n,a,!0),r&&(t[s]=i.parse(t[s]))}return t?p(s,o,n,r,!1):p(o,s,r,n,!0),{valueWindow:s,percentWindow:o}},e.prototype.reset=function(e){if(e===this._dataZoomModel){var t=this.getTargetSeriesModels();this._dataExtent=function(e,t,n){var i=[1/0,-1/0];XL(n,(function(e){!function(e,t,n){t&&zr(sI(t,n),(function(n){var i=t.getApproximateExtent(n);i[0]e[1]&&(e[1]=i[1])}))}(i,e.getData(),t)}));var a=e.getAxisModel(),r=ZT(a.axis.scale,a,i).calculate();return[r.min,r.max]}(this,this._dimName,t),this._updateMinMaxSpan();var n=this.calculateDataWindow(e.settledOption);this._valueWindow=n.valueWindow,this._percentWindow=n.percentWindow,this._setAxisModel()}},e.prototype.filterData=function(e,t){if(e===this._dataZoomModel){var n=this._dimName,i=this.getTargetSeriesModels(),a=e.get("filterMode"),r=this._valueWindow;"none"!==a&&XL(i,(function(e){var t=e.getData(),i=t.mapDimensionsAll(n);if(i.length){if("weakFilter"===a){var o=t.getStore(),s=Ur(i,(function(e){return t.getDimensionIndex(e)}),t);t.filterSelf((function(e){for(var t,n,a,l=0;lr[1];if(c&&!d&&!u)return!0;c&&(a=!0),d&&(t=!0),u&&(n=!0)}return a&&t&&n}))}else XL(i,(function(n){if("empty"===a)e.setData(t=t.map(n,(function(e){return function(e){return e>=r[0]&&e<=r[1]}(e)?e:NaN})));else{var i={};i[n]=r,t.selectRange(i)}}));XL(i,(function(e){t.setApproximateExtent(r,e)}))}}))}},e.prototype._updateMinMaxSpan=function(){var e=this._minMaxSpan={},t=this._dataZoomModel,n=this._dataExtent;XL(["min","max"],(function(i){var a=t.get(i+"Span"),r=t.get(i+"ValueSpan");null!=r&&(r=this.getAxisModel().axis.scale.parse(r)),null!=r?a=Hc(n[0]+r,n,[0,100],!0):null!=a&&(r=Hc(a,[0,100],n,!0)-n[0]),e[i+"Span"]=a,e[i+"ValueSpan"]=r}),this)},e.prototype._setAxisModel=function(){var e=this.getAxisModel(),t=this._percentWindow,n=this._valueWindow;if(t){var i=Zc(n,[0,500]);i=Math.min(i,20);var a=e.axis.scale.rawExtentInfo;0!==t[0]&&a.setDeterminedMinMax("min",+n[0].toFixed(i)),100!==t[1]&&a.setDeterminedMinMax("max",+n[1].toFixed(i)),a.freeze()}},e}();var JL={getTargetSeries:function(e){function t(t){e.eachComponent("dataZoom",(function(n){n.eachTargetAxis((function(i,a){var r=e.getComponent(UL(i),a);t(i,a,r,n)}))}))}t((function(e,t,n,i){n.__dzAxisProxy=null}));var n=[];t((function(t,i,a,r){a.__dzAxisProxy||(a.__dzAxisProxy=new QL(t,i,r,e),n.push(a.__dzAxisProxy))}));var i=Co();return zr(n,(function(e){zr(e.getTargetSeriesModels(),(function(e){i.set(e.uid,e)}))})),i},overallReset:function(e,t){e.eachComponent("dataZoom",(function(e){e.eachTargetAxis((function(t,n){var i=e.getAxisProxy(t,n);i&&i.reset(e)})),e.eachTargetAxis((function(n,i){var a=e.getAxisProxy(n,i);a&&a.filterData(e,t)}))})),e.eachComponent("dataZoom",(function(e){var t=e.findRepresentativeAxisProxy();if(t){var n=t.getDataPercentWindow(),i=t.getDataValueWindow();e.setCalculatedRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]})}}))}};var eV=!1;function tV(e){eV||(eV=!0,e.registerProcessor(e.PRIORITY.PROCESSOR.FILTER,JL),function(e){e.registerAction("dataZoom",(function(e,t){zr(jL(t,e),(function(t){t.setRawRange({start:e.start,end:e.end,startValue:e.startValue,endValue:e.endValue})}))}))}(e),e.registerSubTypeDefaulter("dataZoom",(function(){return"slider"})))}var nV=function(){},iV={};function aV(e){return iV[e]}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.optionUpdated=function(){e.prototype.optionUpdated.apply(this,arguments);var t=this.ecModel;zr(this.option.feature,(function(e,n){var i=aV(n);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(t)),Fr(e,i.defaultOption))}))},t.type="toolbox",t.layoutMode={type:"box",ignoreSize:!0},t.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}},tooltip:{show:!1,position:"bottom"}}}(kv);function rV(e,t){var n=pv(t.get("padding")),i=t.getItemStyle(["color","opacity"]);return i.fill=t.get("backgroundColor"),e=new Fm({shape:{x:e.x-n[3],y:e.y-n[0],width:e.width+n[1]+n[3],height:e.height+n[0]+n[2],r:t.get("borderRadius")},style:i,silent:!0,z2:-1})}!function(e){function t(){return null!==e&&e.apply(this,arguments)||this}We(t,e),t.prototype.render=function(e,t,n,i){var a=this.group;if(a.removeAll(),e.get("show")){var r=+e.get("itemSize"),o="vertical"===e.get("orient"),s=e.get("feature")||{},l=this._features||(this._features={}),p=[];zr(s,(function(e,t){p.push(t)})),new S_(this._featureNames||[],p).add(c).update(c).remove(Yr(c,null)).execute(),this._featureNames=p,function(e,t,n){var i=t.getBoxLayoutParams(),a=t.get("padding"),r={width:n.getWidth(),height:n.getHeight()},o=Sv(i,r,a);wv(t.get("orient"),e,t.get("itemGap"),o.width,o.height),Cv(e,i,r,a)}(a,e,n),a.add(rV(a.getBoundingRect(),e)),o||a.eachChild((function(e){var t=e.__title,i=e.ensureState("emphasis"),o=i.textConfig||(i.textConfig={}),s=e.getTextContent(),l=s&&s.ensureState("emphasis");if(l&&!Zr(l)&&t){var p=l.style||(l.style={}),c=xc(t,Nm.makeFont(p)),d=e.x+a.x,u=!1;e.y+a.y+r+c.height>n.getHeight()&&(o.position="top",u=!0);var m=u?-5-c.height:r+10;d+c.width/2>n.getWidth()?(o.position=["100%",m],p.align="right"):d-c.width/2<0&&(o.position=[0,m],p.align="left")}}))}function c(c,d){var u,m=p[c],h=p[d],g=s[m],f=new xy(g,e,e.ecModel);if(i&&null!=i.newTitle&&i.featureName===m&&(g.title=i.newTitle),m&&!h){if(function(e){return 0===e.indexOf("my")}(m))u={onclick:f.option.onclick,featureName:m};else{var y=aV(m);if(!y)return;u=new y}l[m]=u}else if(!(u=l[h]))return;u.uid=wy("toolbox-feature"),u.model=f,u.ecModel=t,u.api=n;var v=u instanceof nV;m||!h?!f.get("show")||v&&u.unusable?v&&u.remove&&u.remove(t,n):(!function(i,s,l){var p,c,d=i.getModel("iconStyle"),u=i.getModel(["emphasis","iconStyle"]),m=s instanceof nV&&s.getIcons?s.getIcons():i.get("icon"),h=i.get("title")||{};Qr(m)?(p={})[l]=m:p=m;Qr(h)?(c={})[l]=h:c=h;var g=i.iconPaths={};zr(p,(function(l,p){var m=Lf(l,{},{x:-r/2,y:-r/2,width:r,height:r});m.setStyle(d.getItemStyle()),m.ensureState("emphasis").style=u.getItemStyle();var h=new Nm({style:{text:c[p],align:u.get("textAlign"),borderRadius:u.get("textBorderRadius"),padding:u.get("textPadding"),fill:null,font:iy({fontStyle:u.get("textFontStyle"),fontFamily:u.get("textFontFamily"),fontSize:u.get("textFontSize"),fontWeight:u.get("textFontWeight")},t)},ignore:!0});m.setTextContent(h),zf({el:m,componentModel:e,itemName:p,formatterParamsExtra:{title:c[p]}}),m.__title=c[p],m.on("mouseover",(function(){var t=u.getItemStyle(),i=o?null==e.get("right")&&"right"!==e.get("left")?"right":"left":null==e.get("bottom")&&"bottom"!==e.get("top")?"bottom":"top";h.setStyle({fill:u.get("textFill")||t.fill||t.stroke||"#000",backgroundColor:u.get("textBackgroundColor")}),m.setTextConfig({position:u.get("textPosition")||i}),h.ignore=!e.get("showTitle"),n.enterEmphasis(this)})).on("mouseout",(function(){"emphasis"!==i.get(["iconStatus",p])&&n.leaveEmphasis(this),h.hide()})),("emphasis"===i.get(["iconStatus",p])?Th:Ih)(m),a.add(m),m.on("click",Kr(s.onclick,s,t,n,p)),g[p]=m}))}(f,u,m),f.setIconStatus=function(e,t){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[e]=t,i[e]&&("emphasis"===t?Th:Ih)(i[e])},u instanceof nV&&u.render&&u.render(f,t,n,i)):v&&u.dispose&&u.dispose(t,n)}},t.prototype.updateView=function(e,t,n,i){zr(this._features,(function(e){e instanceof nV&&e.updateView&&e.updateView(e.model,t,n,i)}))},t.prototype.remove=function(e,t){zr(this._features,(function(n){n instanceof nV&&n.remove&&n.remove(e,t)})),this.group.removeAll()},t.prototype.dispose=function(e,t){zr(this._features,(function(n){n instanceof nV&&n.dispose&&n.dispose(e,t)}))},t.type="toolbox"}(ww);!function(e){function t(){return null!==e&&e.apply(this,arguments)||this}We(t,e),t.prototype.onclick=function(e,t){var n=this.model,i=n.get("name")||e.get("title.0.text")||"echarts",a="svg"===t.getZr().painter.getType(),r=a?"svg":n.get("type",!0)||"png",o=t.getConnectedDataURL({type:r,backgroundColor:n.get("backgroundColor",!0)||e.get("backgroundColor")||"#fff",connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),s=ur.browser;if(Zr(MouseEvent)&&(s.newEdge||!s.ie&&!s.edge)){var l=document.createElement("a");l.download=i+"."+r,l.target="_blank",l.href=o;var p=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});l.dispatchEvent(p)}else if(window.navigator.msSaveOrOpenBlob||a){var c=o.split(","),d=c[0].indexOf("base64")>-1,u=a?decodeURIComponent(c[1]):c[1];d&&(u=window.atob(u));var m=i+"."+r;if(window.navigator.msSaveOrOpenBlob){for(var h=u.length,g=new Uint8Array(h);h--;)g[h]=u.charCodeAt(h);var f=new Blob([g]);window.navigator.msSaveOrOpenBlob(f,m)}else{var y=document.createElement("iframe");document.body.appendChild(y);var v=y.contentWindow,x=v.document;x.open("image/svg+xml","replace"),x.write(u),x.close(),v.focus(),x.execCommand("SaveAs",!0,m),document.body.removeChild(y)}}else{var b=n.get("lang"),w='',S=window.open();S.document.write(w),S.document.title=i}},t.getDefaultOption=function(e){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:e.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:e.getLocaleModel().get(["toolbox","saveAsImage","lang"])}}}(nV);var oV="__ec_magicType_stack__",sV=[["line","bar"],["stack"]],lV=(function(e){function t(){return null!==e&&e.apply(this,arguments)||this}We(t,e),t.prototype.getIcons=function(){var e=this.model,t=e.get("icon"),n={};return zr(e.get("type"),(function(e){t[e]&&(n[e]=t[e])})),n},t.getDefaultOption=function(e){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:e.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},t.prototype.onclick=function(e,t,n){var i=this.model,a=i.get(["seriesIndex",n]);if(lV[n]){var r,o={series:[]};zr(sV,(function(e){Lr(e,n)>=0&&zr(e,(function(e){i.setIconStatus(e,"normal")}))})),i.setIconStatus(n,"emphasis"),e.eachComponent({mainType:"series",query:null==a?null:{seriesIndex:a}},(function(e){var t=e.subType,a=e.id,r=lV[n](t,a,e,i);r&&(Nr(r,e.option),o.series.push(r));var s=e.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var p=l.dim+"Axis",c=e.getReferringComponents(p,Fd).models[0].componentIndex;o[p]=o[p]||[];for(var d=0;d<=c;d++)o[p][c]=o[p][c]||{};o[p][c].boundaryGap="bar"===n}}}));var s=n;"stack"===n&&(r=Fr({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),t.dispatchAction({type:"changeMagicType",currentType:s,newOption:o,newTitle:r,featureName:"magicType"})}}}(nV),{line:function(e,t,n,i){if("bar"===e)return Fr({id:t,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(e,t,n,i){if("line"===e)return Fr({id:t,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(e,t,n,i){var a=n.get("stack")===oV;if("line"===e||"bar"===e)return i.setIconStatus("stack",a?"normal":"emphasis"),Fr({id:t,stack:a?"":oV},i.get(["option","stack"])||{},!0)}});h_({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},(function(e,t){t.mergeOption(e.newOption)}));var pV=new Array(60).join("-"),cV="\t";function dV(e){return e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var uV=new RegExp("[\t]+","g");function mV(e,t){var n=e.split(new RegExp("\n*"+pV+"\n*","g")),i={series:[]};return zr(n,(function(e,n){if(function(e){if(e.slice(0,e.indexOf("\n")).indexOf(cV)>=0)return!0}(e)){var a=function(e){for(var t=e.split(/\n+/g),n=[],i=Ur(dV(t.shift()).split(uV),(function(e){return{name:e,data:[]}})),a=0;a=0)&&e(a,i._targetInfoList)}))}return e.prototype.setOutputRanges=function(e,t){return this.matchOutputRanges(e,t,(function(e,t,n){if((e.coordRanges||(e.coordRanges=[])).push(t),!e.coordRange){e.coordRange=t;var i=TV[e.brushType](0,n,t);e.__rangeOffset={offset:EV[e.brushType](i.values,e.range,[1,1]),xyMinMax:i.xyMinMax}}})),e},e.prototype.matchOutputRanges=function(e,t,n){zr(e,(function(e){var i=this.findTargetInfo(e,t);i&&!0!==i&&zr(i.coordSyses,(function(i){var a=TV[e.brushType](1,i,e.range,!0);n(e,a.values,i,t)}))}),this)},e.prototype.setInputRanges=function(e,t){zr(e,(function(e){var n,i,a,r,o,s=this.findTargetInfo(e,t);if(e.range=e.range||[],s&&!0!==s){e.panelId=s.panelId;var l=TV[e.brushType](0,s.coordSys,e.coordRange),p=e.__rangeOffset;e.range=p?EV[e.brushType](l.values,p.offset,(n=l.xyMinMax,i=p.xyMinMax,a=kV(n),r=kV(i),o=[a[0]/r[0],a[1]/r[1]],isNaN(o[0])&&(o[0]=1),isNaN(o[1])&&(o[1]=1),o)):l.values}}),this)},e.prototype.makePanelOpts=function(e,t){return Ur(this._targetInfoList,(function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:t?t(n):null,clipPath:EF(i),isTargetByCursor:kF(i,e,n.coordSysModel),getLinearBrushOtherExtent:MF(i)}}))},e.prototype.controlSeries=function(e,t,n){var i=this.findTargetInfo(e,n);return!0===i||i&&Lr(i.coordSyses,t.coordinateSystem)>=0},e.prototype.findTargetInfo=function(e,t){for(var n=this._targetInfoList,i=wV(t,e),a=0;ae[1]&&e.reverse(),e}function wV(e,t){return Od(e,t,{includeMainTypes:vV})}var SV={grid:function(e,t){var n=e.xAxisModels,i=e.yAxisModels,a=e.gridModels,r=Co(),o={},s={};(n||i||a)&&(zr(n,(function(e){var t=e.axis.grid.model;r.set(t.id,t),o[t.id]=!0})),zr(i,(function(e){var t=e.axis.grid.model;r.set(t.id,t),s[t.id]=!0})),zr(a,(function(e){r.set(e.id,e),o[e.id]=!0,s[e.id]=!0})),r.each((function(e){var a=e.coordinateSystem,r=[];zr(a.getCartesians(),(function(e,t){(Lr(n,e.getAxis("x").model)>=0||Lr(i,e.getAxis("y").model)>=0)&&r.push(e)})),t.push({panelId:"grid--"+e.id,gridModel:e,coordSysModel:e,coordSys:r[0],coordSyses:r,getPanelRect:_V.grid,xAxisDeclared:o[e.id],yAxisDeclared:s[e.id]})})))},geo:function(e,t){zr(e.geoModels,(function(e){var n=e.coordinateSystem;t.push({panelId:"geo--"+e.id,geoModel:e,coordSysModel:e,coordSys:n,coordSyses:[n],getPanelRect:_V.geo})}))}},CV=[function(e,t){var n=e.xAxisModel,i=e.yAxisModel,a=e.gridModel;return!a&&n&&(a=n.axis.grid.model),!a&&i&&(a=i.axis.grid.model),a&&a===t.gridModel},function(e,t){var n=e.geoModel;return n&&n===t.geoModel}],_V={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var e=this.coordSys,t=e.getBoundingRect().clone();return t.applyTransform(Df(e)),t}},TV={lineX:Yr(IV,0),lineY:Yr(IV,1),rect:function(e,t,n,i){var a=e?t.pointToData([n[0][0],n[1][0]],i):t.dataToPoint([n[0][0],n[1][0]],i),r=e?t.pointToData([n[0][1],n[1][1]],i):t.dataToPoint([n[0][1],n[1][1]],i),o=[bV([a[0],r[0]]),bV([a[1],r[1]])];return{values:o,xyMinMax:o}},polygon:function(e,t,n,i){var a=[[1/0,-1/0],[1/0,-1/0]];return{values:Ur(n,(function(n){var r=e?t.pointToData(n,i):t.dataToPoint(n,i);return a[0][0]=Math.min(a[0][0],r[0]),a[1][0]=Math.min(a[1][0],r[1]),a[0][1]=Math.max(a[0][1],r[0]),a[1][1]=Math.max(a[1][1],r[1]),r})),xyMinMax:a}}};function IV(e,t,n,i){var a=n.getAxis(["x","y"][e]),r=bV(Ur([0,1],(function(e){return t?a.coordToData(a.toLocalCoord(i[e]),!0):a.toGlobalCoord(a.dataToCoord(i[e]))}))),o=[];return o[e]=r,o[1-e]=[NaN,NaN],{values:r,xyMinMax:o}}var EV={lineX:Yr(MV,0),lineY:Yr(MV,1),rect:function(e,t,n){return[[e[0][0]-n[0]*t[0][0],e[0][1]-n[0]*t[0][1]],[e[1][0]-n[1]*t[1][0],e[1][1]-n[1]*t[1][1]]]},polygon:function(e,t,n){return Ur(e,(function(e,i){return[e[0]-n[0]*t[i][0],e[1]-n[1]*t[i][1]]}))}};function MV(e,t,n,i){return[t[0]-i[e]*n[0],t[1]-i[e]*n[1]]}function kV(e){return e?[e[0][1]-e[0][0],e[1][1]-e[1][0]]:[NaN,NaN]}var PV,DV,OV=zr,AV=yd+"toolbox-dataZoom_",FV=(function(e){function t(){return null!==e&&e.apply(this,arguments)||this}We(t,e),t.prototype.render=function(e,t,n,i){this._brushController||(this._brushController=new XA(n.getZr()),this._brushController.on("brush",Kr(this._onBrush,this)).mount()),function(e,t,n,i,a){var r=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(r="dataZoomSelect"===i.key&&i.dataZoomSelectActive);n._isZoomActive=r,e.setIconStatus("zoom",r?"emphasis":"normal");var o=new xV(RV(e),t,{include:["grid"]}),s=o.makePanelOpts(a,(function(e){return e.xAxisDeclared&&!e.yAxisDeclared?"lineX":!e.xAxisDeclared&&e.yAxisDeclared?"lineY":"rect"}));n._brushController.setPanels(s).enableBrush(!(!r||!s.length)&&{brushType:"auto",brushStyle:e.getModel("brushStyle").getItemStyle()})}(e,t,this,i,n),function(e,t){e.setIconStatus("back",function(e){return yV(e).length}(t)>1?"emphasis":"normal")}(e,t)},t.prototype.onclick=function(e,t,n){FV[n].call(this)},t.prototype.remove=function(e,t){this._brushController&&this._brushController.unmount()},t.prototype.dispose=function(e,t){this._brushController&&this._brushController.dispose()},t.prototype._onBrush=function(e){var t=e.areas;if(e.isEnd&&t.length){var n={},i=this.ecModel;this._brushController.updateCovers([]),new xV(RV(this.model),i,{include:["grid"]}).matchOutputRanges(t,i,(function(e,t,n){if("cartesian2d"===n.type){var i=e.brushType;"rect"===i?(a("x",n,t[0]),a("y",n,t[1])):a({lineX:"x",lineY:"y"}[i],n,t)}})),function(e,t){var n=yV(e);gV(t,(function(t,i){for(var a=n.length-1;a>=0&&!n[a][i];a--);if(a<0){var r=e.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(r){var o=r.getPercentRange();n[0][i]={dataZoomId:i,start:o[0],end:o[1]}}}})),n.push(t)}(i,n),this._dispatchZoomAction(n)}function a(e,t,a){var r=t.getAxis(e),o=r.model,s=function(e,t,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},(function(n){n.getAxisModel(e,t.componentIndex)&&(i=n)})),i}(e,o,i),l=s.findRepresentativeAxisProxy(o);if(l){var p=l.getMinMaxSpan();null==p.minValueSpan&&null==p.maxValueSpan||(a=EA(0,a.slice(),r.scale.getExtent(),0,p.minValueSpan,p.maxValueSpan))}s&&(n[s.id]={dataZoomId:s.id,startValue:a[0],endValue:a[1]})}},t.prototype._dispatchZoomAction=function(e){var t=[];OV(e,(function(e,n){t.push(Ar(e))})),t.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:t})},t.getDefaultOption=function(e){return{show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:e.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:"rgba(210,219,238,0.2)"}}}}(nV),{zoom:function(){var e=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:e})},back:function(){this._dispatchZoomAction(function(e){var t=yV(e),n=t[t.length-1];t.length>1&&t.pop();var i={};return gV(n,(function(e,n){for(var a=t.length-1;a>=0;a--)if(e=t[a][n]){i[n]=e;break}})),i}(this.ecModel))}});function RV(e){var t={xAxisIndex:e.get("xAxisIndex",!0),yAxisIndex:e.get("yAxisIndex",!0),xAxisId:e.get("xAxisId",!0),yAxisId:e.get("yAxisId",!0)};return null==t.xAxisIndex&&null==t.xAxisId&&(t.xAxisIndex="all"),null==t.yAxisIndex&&null==t.yAxisId&&(t.yAxisIndex="all"),t}PV="dataZoom",DV=function(e){var t=e.getComponent("toolbox",0),n=["feature","dataZoom"];if(t&&null!=t.get(n)){var i=t.getModel(n),a=[],r=Od(e,RV(i));return OV(r.xAxisModels,(function(e){return o(e,"xAxis","xAxisIndex")})),OV(r.yAxisModels,(function(e){return o(e,"yAxis","yAxisIndex")})),a}function o(e,t,n){var r=e.componentIndex,o={type:"select",$fromToolbox:!0,filterMode:i.get("filterMode",!0)||"filter",id:AV+t+r};o[n]=r,a.push(o)}},go(null==Zv.get(PV)&&DV),Zv.set(PV,DV);var BV=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="tooltip",t.dependencies=["axisPointer"],t.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"#fff",shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#666",fontSize:14}},t}(kv);function NV(e){var t=e.get("confine");return null!=t?!!t:"richText"===e.get("renderMode")}function LV(e){if(ur.domSupported)for(var t=document.documentElement.style,n=0,i=e.length;n-1?(p+="top:50%",c+="translateY(-50%) rotate("+(o="left"===s?-225:-45)+"deg)"):(p+="left:50%",c+="translateX(-50%) rotate("+(o="top"===s?225:45)+"deg)");var d=o*Math.PI/180,u=l+a,m=u*Math.abs(Math.cos(d))+u*Math.abs(Math.sin(d)),h=t+" solid "+a+"px;";return'
'}(n,i,a)),Qr(e))r.innerHTML=e+o;else if(e){r.innerHTML="",Xr(e)||(e=[e]);for(var s=0;s=0?this._tryShow(n,i):"leave"===t&&this._hide(i))}),this))},t.prototype._keepShow=function(){var e=this._tooltipModel,t=this._ecModel,n=this._api,i=e.get("triggerOn");if(null!=this._lastX&&null!=this._lastY&&"none"!==i&&"click"!==i){var a=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout((function(){!n.isDisposed()&&a.manuallyShowTip(e,t,n,{x:a._lastX,y:a._lastY,dataByCoordSys:a._lastDataByCoordSys})}))}},t.prototype.manuallyShowTip=function(e,t,n,i){if(i.from!==this.uid&&!ur.node&&n.getDom()){var a=tq(i,n);this._ticket="";var r=i.dataByCoordSys,o=function(e,t,n){var i=Ad(e).queryOptionMap,a=i.keys()[0];if(!a||"series"===a)return;var r=Bd(t,a,i.get(a),{useDefault:!1,enableAll:!1,enableNone:!1}),o=r.models[0];if(!o)return;var s,l=n.getViewOfComponentModel(o);if(l.group.traverse((function(t){var n=Xm(t).tooltipConfig;if(n&&n.name===e.name)return s=t,!0})),s)return{componentMainType:a,componentIndex:o.componentIndex,el:s}}(i,t,n);if(o){var s=o.el.getBoundingRect().clone();s.applyTransform(o.el.transform),this._tryShow({offsetX:s.x+s.width/2,offsetY:s.y+s.height/2,target:o.el,position:i.position,positionDefault:"bottom"},a)}else if(i.tooltip&&null!=i.x&&null!=i.y){var l=QV;l.x=i.x,l.y=i.y,l.update(),Xm(l).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:l},a)}else if(r)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:r,tooltipOption:i.tooltipOption},a);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(e,t,n,i))return;var p=EN(i,t),c=p.point[0],d=p.point[1];null!=c&&null!=d&&this._tryShow({offsetX:c,offsetY:d,target:p.el,position:i.position,positionDefault:"bottom"},a)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},a))}},t.prototype.manuallyHideTip=function(e,t,n,i){var a=this._tooltipContent;this._tooltipModel&&a.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,i.from!==this.uid&&this._hide(tq(i,n))},t.prototype._manuallyAxisShowTip=function(e,t,n,i){var a=i.seriesIndex,r=i.dataIndex,o=t.getComponent("axisPointer").coordSysAxesInfo;if(null!=a&&null!=r&&null!=o){var s=t.getSeriesByIndex(a);if(s)if("axis"===eq([s.getData().getItemModel(r),s,(s.coordinateSystem||{}).model],this._tooltipModel).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:a,dataIndex:r,position:i.position}),!0}},t.prototype._tryShow=function(e,t){var n=e.target;if(this._tooltipModel){this._lastX=e.offsetX,this._lastY=e.offsetY;var i=e.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,e);else if(n){var a,r;if("legend"===Xm(n).ssrType)return;this._lastDataByCoordSys=null,TS(n,(function(e){return null!=Xm(e).dataIndex?(a=e,!0):null!=Xm(e).tooltipConfig?(r=e,!0):void 0}),!0),a?this._showSeriesItemTooltip(e,a,t):r?this._showComponentItemTooltip(e,r,t):this._hide(t)}else this._lastDataByCoordSys=null,this._hide(t)}},t.prototype._showOrMove=function(e,t){var n=e.get("showDelay");t=Kr(t,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(t,n):t()},t.prototype._showAxisTooltip=function(e,t){var n=this._ecModel,i=this._tooltipModel,a=[t.offsetX,t.offsetY],r=eq([t.tooltipOption],i),o=this._renderMode,s=[],l=Zb("section",{blocks:[],noHeader:!0}),p=[],c=new lw;zr(e,(function(e){zr(e.dataByAxis,(function(e){var t=n.getComponent(e.axisDim+"Axis",e.axisIndex),a=e.value;if(t&&t.axis&&null!=a){var r=pN(a,t.axis,n,e.seriesDataIndices,e.valueLabelOpt),d=Zb("section",{header:r,noHeader:!fo(r),sortBlocks:!0,blocks:[]});l.blocks.push(d),zr(e.seriesDataIndices,(function(l){var u=n.getSeriesByIndex(l.seriesIndex),m=l.dataIndexInside,h=u.getDataParams(m);if(!(h.dataIndex<0)){h.axisDim=e.axisDim,h.axisIndex=e.axisIndex,h.axisType=e.axisType,h.axisId=e.axisId,h.axisValue=iI(t.axis,{value:a}),h.axisValueLabel=r,h.marker=c.makeTooltipMarker("item",gv(h.color),o);var g=mb(u.formatTooltip(m,!0,null)),f=g.frag;if(f){var y=eq([u],i).get("valueFormatter");d.blocks.push(y?Br({valueFormatter:y},f):f)}g.text&&p.push(g.text),s.push(h)}}))}}))})),l.blocks.reverse(),p.reverse();var d=t.position,u=r.get("order"),m=iw(l,c,o,u,n.get("useUTC"),r.get("textStyle"));m&&p.unshift(m);var h="richText"===o?"\n\n":"
",g=p.join(h);this._showOrMove(r,(function(){this._updateContentNotChangedOnAxis(e,s)?this._updatePosition(r,d,a[0],a[1],this._tooltipContent,s):this._showTooltipContent(r,g,s,Math.random()+"",a[0],a[1],d,null,c)}))},t.prototype._showSeriesItemTooltip=function(e,t,n){var i=this._ecModel,a=Xm(t),r=a.seriesIndex,o=i.getSeriesByIndex(r),s=a.dataModel||o,l=a.dataIndex,p=a.dataType,c=s.getData(p),d=this._renderMode,u=e.positionDefault,m=eq([c.getItemModel(l),s,o&&(o.coordinateSystem||{}).model],this._tooltipModel,u?{position:u}:null),h=m.get("trigger");if(null==h||"item"===h){var g=s.getDataParams(l,p),f=new lw;g.marker=f.makeTooltipMarker("item",gv(g.color),d);var y=mb(s.formatTooltip(l,!1,p)),v=m.get("order"),x=m.get("valueFormatter"),b=y.frag,w=b?iw(x?Br({valueFormatter:x},b):b,f,d,v,i.get("useUTC"),m.get("textStyle")):y.text,S="item_"+s.name+"_"+l;this._showOrMove(m,(function(){this._showTooltipContent(m,w,g,S,e.offsetX,e.offsetY,e.position,e.target,f)})),n({type:"showTip",dataIndexInside:l,dataIndex:c.getRawIndex(l),seriesIndex:r,from:this.uid})}},t.prototype._showComponentItemTooltip=function(e,t,n){var i=Xm(t),a=i.tooltipConfig.option||{};if(Qr(a)){a={content:a,formatter:a}}var r=[a],o=this._ecModel.getComponent(i.componentMainType,i.componentIndex);o&&r.push(o),r.push({formatter:a.content});var s=e.positionDefault,l=eq(r,this._tooltipModel,s?{position:s}:null),p=l.get("content"),c=Math.random()+"",d=new lw;this._showOrMove(l,(function(){var n=Ar(l.get("formatterParams")||{});this._showTooltipContent(l,p,n,c,e.offsetX,e.offsetY,e.position,t,d)})),n({type:"showTip",from:this.uid})},t.prototype._showTooltipContent=function(e,t,n,i,a,r,o,s,l){if(this._ticket="",e.get("showContent")&&e.get("show")){var p=this._tooltipContent;p.setEnterable(e.get("enterable"));var c=e.get("formatter");o=o||e.get("position");var d=t,u=this._getNearestPoint([a,r],n,e.get("trigger"),e.get("borderColor")).color;if(c)if(Qr(c)){var m=e.ecModel.get("useUTC"),h=Xr(n)?n[0]:n;d=c,h&&h.axisType&&h.axisType.indexOf("time")>=0&&(d=jy(h.axisValue,d,m)),d=mv(d,n,!0)}else if(Zr(c)){var g=Kr((function(t,i){t===this._ticket&&(p.setContent(i,l,e,u,o),this._updatePosition(e,o,a,r,p,n,s))}),this);this._ticket=i,d=c(n,i,g)}else d=c;p.setContent(d,l,e,u,o),p.show(e,u),this._updatePosition(e,o,a,r,p,n,s)}},t.prototype._getNearestPoint=function(e,t,n,i){return"axis"===n||Xr(t)?{color:i||("html"===this._renderMode?"#fff":"none")}:Xr(t)?void 0:{color:i||t.color||t.borderColor}},t.prototype._updatePosition=function(e,t,n,i,a,r,o){var s=this._api.getWidth(),l=this._api.getHeight();t=t||e.get("position");var p=a.getSize(),c=e.get("align"),d=e.get("verticalAlign"),u=o&&o.getBoundingRect().clone();if(o&&u.applyTransform(o.transform),Zr(t)&&(t=t([n,i],r,a.el,u,{viewSize:[s,l],contentSize:p.slice()})),Xr(t))n=Wc(t[0],s),i=Wc(t[1],l);else if(to(t)){var m=t;m.width=p[0],m.height=p[1];var h=Sv(m,{width:s,height:l});n=h.x,i=h.y,c=null,d=null}else if(Qr(t)&&o){var g=function(e,t,n,i){var a=n[0],r=n[1],o=Math.ceil(Math.SQRT2*i)+8,s=0,l=0,p=t.width,c=t.height;switch(e){case"inside":s=t.x+p/2-a/2,l=t.y+c/2-r/2;break;case"top":s=t.x+p/2-a/2,l=t.y-r-o;break;case"bottom":s=t.x+p/2-a/2,l=t.y+c+o;break;case"left":s=t.x-a-o,l=t.y+c/2-r/2;break;case"right":s=t.x+p+o,l=t.y+c/2-r/2}return[s,l]}(t,u,p,e.get("borderWidth"));n=g[0],i=g[1]}else{g=function(e,t,n,i,a,r,o){var s=n.getSize(),l=s[0],p=s[1];null!=r&&(e+l+r+2>i?e-=l+r:e+=r);null!=o&&(t+p+o>a?t-=p+o:t+=o);return[e,t]}(n,i,a,s,l,c?null:20,d?null:20);n=g[0],i=g[1]}if(c&&(n-=nq(c)?p[0]/2:"right"===c?p[0]:0),d&&(i-=nq(d)?p[1]/2:"bottom"===d?p[1]:0),NV(e)){g=function(e,t,n,i,a){var r=n.getSize(),o=r[0],s=r[1];return e=Math.min(e+o,i)-o,t=Math.min(t+s,a)-s,e=Math.max(e,0),t=Math.max(t,0),[e,t]}(n,i,a,s,l);n=g[0],i=g[1]}a.moveTo(n,i)},t.prototype._updateContentNotChangedOnAxis=function(e,t){var n=this._lastDataByCoordSys,i=this._cbParamsList,a=!!n&&n.length===e.length;return a&&zr(n,(function(n,r){var o=n.dataByAxis||[],s=(e[r]||{}).dataByAxis||[];(a=a&&o.length===s.length)&&zr(o,(function(e,n){var r=s[n]||{},o=e.seriesDataIndices||[],l=r.seriesDataIndices||[];(a=a&&e.value===r.value&&e.axisType===r.axisType&&e.axisId===r.axisId&&o.length===l.length)&&zr(o,(function(e,t){var n=l[t];a=a&&e.seriesIndex===n.seriesIndex&&e.dataIndex===n.dataIndex})),i&&zr(e.seriesDataIndices,(function(e){var n=e.seriesIndex,r=t[n],o=i[n];r&&o&&o.data!==r.data&&(a=!1)}))}))})),this._lastDataByCoordSys=e,this._cbParamsList=t,!!a},t.prototype._hide=function(e){this._lastDataByCoordSys=null,e({type:"hideTip",from:this.uid})},t.prototype.dispose=function(e,t){!ur.node&&t.getDom()&&(Bw(this,"_updatePosition"),this._tooltipContent.dispose(),TN("itemTooltip",t))},t.type="tooltip",t}(ww);function eq(e,t,n){var i,a=t.ecModel;n?(i=new xy(n,a,a),i=new xy(t.option,i,a)):i=t;for(var r=e.length-1;r>=0;r--){var o=e[r];o&&(o instanceof xy&&(o=o.get("tooltip",!0)),Qr(o)&&(o={formatter:o}),o&&(i=new xy(o,i,a)))}return i}function tq(e,t){return e.dispatchAction||Kr(t.dispatchAction,t)}function nq(e){return"center"===e||"middle"===e}function iq(e){dI(RN),e.registerComponentModel(BV),e.registerComponentView(JV),e.registerAction({type:"showTip",event:"showTip",update:"tooltip:manuallyShowTip"},Mo),e.registerAction({type:"hideTip",event:"hideTip",update:"tooltip:manuallyHideTip"},Mo)}var aq=zr;function rq(e){if(e)for(var t in e)if(e.hasOwnProperty(t))return!0}function oq(e,t,n){var i={};return aq(t,(function(t){var a,r=i[t]=((a=function(){}).prototype.__hidden=a.prototype,new a);aq(e[t],(function(e,i){if(dO.isValidType(i)){var a={type:i,visual:e};n&&n(a,t),r[i]=new dO(a),"opacity"===i&&((a=Ar(a)).type="colorAlpha",r.__hidden.__alphaForOpacity=new dO(a))}}))})),i}function sq(e,t,n){var i;zr(n,(function(e){t.hasOwnProperty(e)&&rq(t[e])&&(i=!0)})),i&&zr(n,(function(n){t.hasOwnProperty(n)&&rq(t[n])?e[n]=Ar(t[n]):delete e[n]}))}lq(0),lq(1);function lq(e){var t=["x","y"],n=["width","height"];return{point:function(t,n,i){if(t){var a=i.range;return pq(t[e],a)}},rect:function(i,a,r){if(i){var o=r.range,s=[i[t[e]],i[t[e]]+i[n[e]]];return s[1]=t&&(e=t-1),e<0&&(e=0)),this.option.currentIndex=e},t.prototype.getCurrentIndex=function(){return this.option.currentIndex},t.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},t.prototype.setPlayState=function(e){this.option.autoPlay=!!e},t.prototype.getPlayState=function(){return!!this.option.autoPlay},t.prototype._initData=function(){var e,t=this.option,n=t.data||[],i=t.axisType,a=this._names=[];"category"===i?(e=[],zr(n,(function(t,n){var i,r=Id(wd(t),"");to(t)?(i=Ar(t)).value=n:i=n,e.push(i),a.push(r)}))):e=n;var r={category:"ordinal",time:"time",value:"number"}[i]||"number";(this._data=new W_([{name:"value",type:r}],this)).initData(e,a)},t.prototype.getData=function(){return this._data},t.prototype.getCategories=function(){if("category"===this.get("axisType"))return this._names.slice()},t.type="timeline",t.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:5,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:"#000"},data:[]},t}(kv),hq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="timeline.slider",t.defaultOption=Sy(mq.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:"#DAE1F5"},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:"#A4B1D7"},itemStyle:{color:"#A4B1D7",borderWidth:1},checkpointStyle:{symbol:"circle",symbolSize:15,color:"#316bf3",borderColor:"#fff",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0, 0, 0, 0.3)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z",stopIcon:"path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z",nextIcon:"M2,18.5A1.52,1.52,0,0,1,.92,18a1.49,1.49,0,0,1,0-2.12L7.81,9.36,1,3.11A1.5,1.5,0,1,1,3,.89l8,7.34a1.48,1.48,0,0,1,.49,1.09,1.51,1.51,0,0,1-.46,1.1L3,18.08A1.5,1.5,0,0,1,2,18.5Z",prevIcon:"M10,.5A1.52,1.52,0,0,1,11.08,1a1.49,1.49,0,0,1,0,2.12L4.19,9.64,11,15.89a1.5,1.5,0,1,1-2,2.22L1,10.77A1.48,1.48,0,0,1,.5,9.68,1.51,1.51,0,0,1,1,8.58L9,.92A1.5,1.5,0,0,1,10,.5Z",prevBtnSize:18,nextBtnSize:18,color:"#A4B1D7",borderColor:"#A4B1D7",borderWidth:1},emphasis:{label:{show:!0,color:"#6f778d"},itemStyle:{color:"#316BF3"},controlStyle:{color:"#316BF3",borderColor:"#316BF3",borderWidth:2}},progress:{lineStyle:{color:"#316BF3"},itemStyle:{color:"#316BF3"},label:{color:"#6f778d"}},data:[]}),t}(mq);qr(hq,ub.prototype);var gq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="timeline",t}(ww),fq=function(e){function t(t,n,i,a){var r=e.call(this,t,n,i)||this;return r.type=a||"value",r}return We(t,e),t.prototype.getLabelModel=function(){return this.model.getModel("label")},t.prototype.isHorizontal=function(){return"horizontal"===this.model.get("orient")},t}(RI),yq=Math.PI,vq=Pd();!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.init=function(e,t){this.api=t},t.prototype.render=function(e,t,n){if(this.model=e,this.api=n,this.ecModel=t,this.group.removeAll(),e.get("show",!0)){var i=this._layout(e,n),a=this._createGroup("_mainGroup"),r=this._createGroup("_labelGroup"),o=this._axis=this._createAxis(i,e);e.formatTooltip=function(e){return Zb("nameValue",{noName:!0,value:o.scale.getLabel({value:e})})},zr(["AxisLine","AxisTick","Control","CurrentPointer"],(function(t){this["_render"+t](i,a,o,e)}),this),this._renderAxisLabel(i,r,o,e),this._position(i,e)}this._doPlayStop(),this._updateTicksStatus()},t.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},t.prototype.dispose=function(){this._clearTimer()},t.prototype._layout=function(e,t){var n,i,a,r,o=e.get(["label","position"]),s=e.get("orient"),l=function(e,t){return Sv(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()},e.get("padding"))}(e,t),p={horizontal:"center",vertical:(n=null==o||"auto"===o?"horizontal"===s?l.y+l.height/2=0||"+"===n?"left":"right"},c={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},d={horizontal:0,vertical:yq/2},u="vertical"===s?l.height:l.width,m=e.getModel("controlStyle"),h=m.get("show",!0),g=h?m.get("itemSize"):0,f=h?m.get("itemGap"):0,y=g+f,v=e.get(["label","rotate"])||0;v=v*yq/180;var x=m.get("position",!0),b=h&&m.get("showPlayBtn",!0),w=h&&m.get("showPrevBtn",!0),S=h&&m.get("showNextBtn",!0),C=0,_=u;"left"===x||"bottom"===x?(b&&(i=[0,0],C+=y),w&&(a=[C,0],C+=y),S&&(r=[_-g,0],_-=y)):(b&&(i=[_-g,0],_-=y),w&&(a=[0,0],C+=y),S&&(r=[_-g,0],_-=y));var T=[C,_];return e.get("inverse")&&T.reverse(),{viewRect:l,mainLength:u,orient:s,rotation:d[s],labelRotation:v,labelPosOpt:n,labelAlign:e.get(["label","align"])||p[s],labelBaseline:e.get(["label","verticalAlign"])||e.get(["label","baseline"])||c[s],playPosition:i,prevBtnPosition:a,nextBtnPosition:r,axisExtent:T,controlSize:g,controlGap:f}},t.prototype._position=function(e,t){var n=this._mainGroup,i=this._labelGroup,a=e.viewRect;if("vertical"===e.orient){var r=[1,0,0,1,0,0],o=a.x,s=a.y+a.height;Cs(r,r,[-o,-s]),_s(r,r,-yq/2),Cs(r,r,[o,s]),(a=a.clone()).applyTransform(r)}var l=f(a),p=f(n.getBoundingRect()),c=f(i.getBoundingRect()),d=[n.x,n.y],u=[i.x,i.y];u[0]=d[0]=l[0][0];var m,h=e.labelPosOpt;null==h||Qr(h)?(y(d,p,l,1,m="+"===h?0:1),y(u,c,l,1,1-m)):(y(d,p,l,1,m=h>=0?0:1),u[1]=d[1]+h);function g(e){e.originX=l[0][0]-e.x,e.originY=l[1][0]-e.y}function f(e){return[[e.x,e.x+e.width],[e.y,e.y+e.height]]}function y(e,t,n,i,a){e[i]+=n[i][a]-t[i][a]}n.setPosition(d),i.setPosition(u),n.rotation=i.rotation=e.rotation,g(n),g(i)},t.prototype._createAxis=function(e,t){var n=t.getData(),i=t.get("axisType"),a=function(e,t){if(t=t||e.get("type"),t)switch(t){case"category":return new hT({ordinalMeta:e.getCategories(),extent:[1/0,-1/0]});case"time":return new PT({locale:e.ecModel.getLocaleModel(),useUTC:e.ecModel.get("useUTC")});default:return new fT}}(t,i);a.getTicks=function(){return n.mapArray(["value"],(function(e){return{value:e}}))};var r=n.getDataExtent("value");a.setExtent(r[0],r[1]),a.calcNiceTicks();var o=new fq("value",a,e.axisExtent,i);return o.model=t,o},t.prototype._createGroup=function(e){var t=this[e]=new Rc;return this.group.add(t),t},t.prototype._renderAxisLine=function(e,t,n,i){var a=n.getExtent();if(i.get(["lineStyle","show"])){var r=new Gg({shape:{x1:a[0],y1:0,x2:a[1],y2:0},style:Br({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});t.add(r);var o=this._progressLine=new Gg({shape:{x1:a[0],x2:this._currentPointer?this._currentPointer.x:a[0],y1:0,y2:0},style:Nr({lineCap:"round",lineWidth:r.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});t.add(o)}},t.prototype._renderAxisTick=function(e,t,n,i){var a=this,r=i.getData(),o=n.scale.getTicks();this._tickSymbols=[],zr(o,(function(e){var o=n.dataToCoord(e.value),s=r.getItemModel(e.value),l=s.getModel("itemStyle"),p=s.getModel(["emphasis","itemStyle"]),c=s.getModel(["progress","itemStyle"]),d={x:o,y:0,onclick:Kr(a._changeTimeline,a,e.value)},u=xq(s,l,t,d);u.ensureState("emphasis").style=p.getItemStyle(),u.ensureState("progress").style=c.getItemStyle(),Lh(u);var m=Xm(u);s.get("tooltip")?(m.dataIndex=e.value,m.dataModel=i):m.dataIndex=m.dataModel=null,a._tickSymbols.push(u)}))},t.prototype._renderAxisLabel=function(e,t,n,i){var a=this;if(n.getLabelModel().get("show")){var r=i.getData(),o=n.getViewLabels();this._tickLabels=[],zr(o,(function(i){var o=i.tickValue,s=r.getItemModel(o),l=s.getModel("label"),p=s.getModel(["emphasis","label"]),c=s.getModel(["progress","label"]),d=n.dataToCoord(i.tickValue),u=new Nm({x:d,y:0,rotation:e.labelRotation-e.rotation,onclick:Kr(a._changeTimeline,a,o),silent:!1,style:Zf(l,{text:i.formattedLabel,align:e.labelAlign,verticalAlign:e.labelBaseline})});u.ensureState("emphasis").style=Zf(p),u.ensureState("progress").style=Zf(c),t.add(u),Lh(u),vq(u).dataIndex=o,a._tickLabels.push(u)}))}},t.prototype._renderControl=function(e,t,n,i){var a=e.controlSize,r=e.rotation,o=i.getModel("controlStyle").getItemStyle(),s=i.getModel(["emphasis","controlStyle"]).getItemStyle(),l=i.getPlayState(),p=i.get("inverse",!0);function c(e,n,l,p){if(e){var c=Cc(co(i.get(["controlStyle",n+"BtnSize"]),a),a),d=function(e,t,n,i){var a=i.style,r=Lf(e.get(["controlStyle",t]),i||{},new Ns(n[0],n[1],n[2],n[3]));a&&r.setStyle(a);return r}(i,n+"Icon",[0,-c/2,c,c],{x:e[0],y:e[1],originX:a/2,originY:0,rotation:p?-r:0,rectHover:!0,style:o,onclick:l});d.ensureState("emphasis").style=s,t.add(d),Lh(d)}}c(e.nextBtnPosition,"next",Kr(this._changeTimeline,this,p?"-":"+")),c(e.prevBtnPosition,"prev",Kr(this._changeTimeline,this,p?"+":"-")),c(e.playPosition,l?"stop":"play",Kr(this._handlePlayClick,this,!l),!0)},t.prototype._renderCurrentPointer=function(e,t,n,i){var a=i.getData(),r=i.getCurrentIndex(),o=a.getItemModel(r).getModel("checkpointStyle"),s=this,l={onCreate:function(e){e.draggable=!0,e.drift=Kr(s._handlePointerDrag,s),e.ondragend=Kr(s._handlePointerDragend,s),bq(e,s._progressLine,r,n,i,!0)},onUpdate:function(e){bq(e,s._progressLine,r,n,i)}};this._currentPointer=xq(o,o,this._mainGroup,{},this._currentPointer,l)},t.prototype._handlePlayClick=function(e){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:e,from:this.uid})},t.prototype._handlePointerDrag=function(e,t,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},t.prototype._handlePointerDragend=function(e){this._pointerChangeTimeline([e.offsetX,e.offsetY],!0)},t.prototype._pointerChangeTimeline=function(e,t){var n=this._toAxisCoord(e)[0],i=Kc(this._axis.getExtent().slice());n>i[1]&&(n=i[1]),n=0&&(o[r]=+o[r].toFixed(d)),[o,c]}var Eq={min:Yr(Iq,"min"),max:Yr(Iq,"max"),average:Yr(Iq,"average"),median:Yr(Iq,"median")};function Mq(e,t){if(t){var n=e.getData(),i=e.coordinateSystem,a=i&&i.dimensions;if(!function(e){return!isNaN(parseFloat(e.x))&&!isNaN(parseFloat(e.y))}(t)&&!Xr(t.coord)&&Xr(a)){var r=kq(t,n,i,e);if((t=Ar(t)).type&&Eq[t.type]&&r.baseAxis&&r.valueAxis){var o=Lr(a,r.baseAxis.dim),s=Lr(a,r.valueAxis.dim),l=Eq[t.type](n,r.baseDataDim,r.valueDataDim,o,s);t.coord=l[0],t.value=l[1]}else t.coord=[null!=t.xAxis?t.xAxis:t.radiusAxis,null!=t.yAxis?t.yAxis:t.angleAxis]}if(null!=t.coord&&Xr(a))for(var p=t.coord,c=0;c<2;c++)Eq[p[c]]&&(p[c]=Oq(n,n.mapDimension(a[c]),p[c]));else t.coord=[];return t}}function kq(e,t,n,i){var a={};return null!=e.valueIndex||null!=e.valueDim?(a.valueDataDim=null!=e.valueIndex?t.getDimension(e.valueIndex):e.valueDim,a.valueAxis=n.getAxis(function(e,t){var n=e.getData().getDimensionInfo(t);return n&&n.coordDim}(i,a.valueDataDim)),a.baseAxis=n.getOtherAxis(a.valueAxis),a.baseDataDim=t.mapDimension(a.baseAxis.dim)):(a.baseAxis=i.getBaseAxis(),a.valueAxis=n.getOtherAxis(a.baseAxis),a.baseDataDim=t.mapDimension(a.baseAxis.dim),a.valueDataDim=t.mapDimension(a.valueAxis.dim)),a}function Pq(e,t){return!(e&&e.containData&&t.coord&&!Tq(t))||e.containData(t.coord)}function Dq(e,t){return e?function(e,n,i,a){return yb(a<2?e.coord&&e.coord[a]:e.value,t[a])}:function(e,n,i,a){return yb(e.value,t[a])}}function Oq(e,t,n){if("average"===n){var i=0,a=0;return e.each(t,(function(e,t){isNaN(e)||(i+=e,a++)})),i/a}return"median"===n?e.getMedian(t):e.getDataExtent(t)["max"===n?1:0]}var Aq=Pd(),Fq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.init=function(){this.markerGroupMap=Co()},t.prototype.render=function(e,t,n){var i=this,a=this.markerGroupMap;a.each((function(e){Aq(e).keep=!1})),t.eachSeries((function(e){var a=_q.getMarkerModelFromSeries(e,i.type);a&&i.renderSeries(e,a,t,n)})),a.each((function(e){!Aq(e).keep&&i.group.remove(e.group)}))},t.prototype.markKeep=function(e){Aq(e).keep=!0},t.prototype.toggleBlurSeries=function(e,t){var n=this;zr(e,(function(e){var i=_q.getMarkerModelFromSeries(e,n.type);i&&i.getData().eachItemGraphicEl((function(e){e&&(t?Eh(e):Mh(e))}))}))},t.type="marker",t}(ww);function Rq(e,t,n){var i=t.coordinateSystem;e.each((function(a){var r,o=e.getItemModel(a),s=Wc(o.get("x"),n.getWidth()),l=Wc(o.get("y"),n.getHeight());if(isNaN(s)||isNaN(l)){if(t.getMarkerPosition)r=t.getMarkerPosition(e.getValues(e.dimensions,a));else if(i){var p=e.get(i.dimensions[0],a),c=e.get(i.dimensions[1],a);r=i.dataToPoint([p,c])}}else r=[s,l];isNaN(s)||(r[0]=s),isNaN(l)||(r[1]=l),e.setItemLayout(a,r)}))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.updateTransform=function(e,t,n){t.eachSeries((function(e){var t=_q.getMarkerModelFromSeries(e,"markPoint");t&&(Rq(t.getData(),e,n),this.markerGroupMap.get(e.id).updateLayout())}),this)},t.prototype.renderSeries=function(e,t,n,i){var a=e.coordinateSystem,r=e.id,o=e.getData(),s=this.markerGroupMap,l=s.get(r)||s.set(r,new PE),p=function(e,t,n){var i;i=e?Ur(e&&e.dimensions,(function(e){return Br(Br({},t.getData().getDimensionInfo(t.getData().mapDimension(e))||{}),{name:e,ordinalMeta:null})})):[{name:"value",type:"float"}];var a=new W_(i,n),r=Ur(n.get("data"),Yr(Mq,t));e&&(r=Hr(r,Yr(Pq,e)));var o=Dq(!!e,i);return a.initData(r,null,o),a}(a,e,t);t.setData(p),Rq(t.getData(),e,i),p.each((function(e){var n=p.getItemModel(e),i=n.getShallow("symbol"),a=n.getShallow("symbolSize"),r=n.getShallow("symbolRotate"),s=n.getShallow("symbolOffset"),l=n.getShallow("symbolKeepAspect");if(Zr(i)||Zr(a)||Zr(r)||Zr(s)){var c=t.getRawValue(e),d=t.getDataParams(e);Zr(i)&&(i=i(c,d)),Zr(a)&&(a=a(c,d)),Zr(r)&&(r=r(c,d)),Zr(s)&&(s=s(c,d))}var u=n.getModel("itemStyle").getItemStyle(),m=wS(o,"color");u.fill||(u.fill=m),p.setItemVisual(e,{symbol:i,symbolSize:a,symbolRotate:r,symbolOffset:s,symbolKeepAspect:l,style:u})})),l.updateData(p),this.group.add(l.group),p.eachItemGraphicEl((function(e){e.traverse((function(e){Xm(e).dataModel=t}))})),this.markKeep(l),l.group.silent=t.get("silent")||e.get("silent")},t.type="markPoint"}(Fq);var Bq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.createMarkerModelFromSeries=function(e,n,i){return new t(e,n,i)},t.type="markLine",t.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},t}(_q),Nq=Pd(),Lq=function(e,t,n,i){var a,r=e.getData();if(Xr(i))a=i;else{var o=i.type;if("min"===o||"max"===o||"average"===o||"median"===o||null!=i.xAxis||null!=i.yAxis){var s=void 0,l=void 0;if(null!=i.yAxis||null!=i.xAxis)s=t.getAxis(null!=i.yAxis?"y":"x"),l=po(i.yAxis,i.xAxis);else{var p=kq(i,r,t,e);s=p.valueAxis,l=Oq(r,eT(r,p.valueDataDim),o)}var c="x"===s.dim?0:1,d=1-c,u=Ar(i),m={coord:[]};u.type=null,u.coord=[],u.coord[d]=-1/0,m.coord[d]=1/0;var h=n.get("precision");h>=0&&eo(l)&&(l=+l.toFixed(Math.min(h,20))),u.coord[c]=m.coord[c]=l,a=[u,m,{type:o,valueIndex:i.valueIndex,value:l}]}else a=[]}var g=[Mq(e,a[0]),Mq(e,a[1]),Br({},a[2])];return g[2].type=g[2].type||null,Fr(g[2],g[0]),Fr(g[2],g[1]),g};function Vq(e){return!isNaN(e)&&!isFinite(e)}function qq(e,t,n,i){var a=1-e,r=i.dimensions[e];return Vq(t[a])&&Vq(n[a])&&t[e]===n[e]&&i.getAxis(r).containData(t[e])}function Gq(e,t){if("cartesian2d"===e.type){var n=t[0].coord,i=t[1].coord;if(n&&i&&(qq(1,n,i,e)||qq(0,n,i,e)))return!0}return Pq(e,t[0])&&Pq(e,t[1])}function zq(e,t,n,i,a){var r,o=i.coordinateSystem,s=e.getItemModel(t),l=Wc(s.get("x"),a.getWidth()),p=Wc(s.get("y"),a.getHeight());if(isNaN(l)||isNaN(p)){if(i.getMarkerPosition)r=i.getMarkerPosition(e.getValues(e.dimensions,t));else{var c=o.dimensions,d=e.get(c[0],t),u=e.get(c[1],t);r=o.dataToPoint([d,u])}if(jE(o,"cartesian2d")){var m=o.getAxis("x"),h=o.getAxis("y");c=o.dimensions;Vq(e.get(c[0],t))?r[0]=m.toGlobalCoord(m.getExtent()[n?0:1]):Vq(e.get(c[1],t))&&(r[1]=h.toGlobalCoord(h.getExtent()[n?0:1]))}isNaN(l)||(r[0]=l),isNaN(p)||(r[1]=p)}else r=[l,p];e.setItemLayout(t,r)}var Uq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.updateTransform=function(e,t,n){t.eachSeries((function(e){var t=_q.getMarkerModelFromSeries(e,"markLine");if(t){var i=t.getData(),a=Nq(t).from,r=Nq(t).to;a.each((function(t){zq(a,t,!0,e,n),zq(r,t,!1,e,n)})),i.each((function(e){i.setItemLayout(e,[a.getItemLayout(e),r.getItemLayout(e)])})),this.markerGroupMap.get(e.id).updateLayout()}}),this)},t.prototype.renderSeries=function(e,t,n,i){var a=e.coordinateSystem,r=e.id,o=e.getData(),s=this.markerGroupMap,l=s.get(r)||s.set(r,new YO);this.group.add(l.group);var p=function(e,t,n){var i;i=e?Ur(e&&e.dimensions,(function(e){return Br(Br({},t.getData().getDimensionInfo(t.getData().mapDimension(e))||{}),{name:e,ordinalMeta:null})})):[{name:"value",type:"float"}];var a=new W_(i,n),r=new W_(i,n),o=new W_([],n),s=Ur(n.get("data"),Yr(Lq,t,e,n));e&&(s=Hr(s,Yr(Gq,e)));var l=Dq(!!e,i);return a.initData(Ur(s,(function(e){return e[0]})),null,l),r.initData(Ur(s,(function(e){return e[1]})),null,l),o.initData(Ur(s,(function(e){return e[2]}))),o.hasItemOption=!0,{from:a,to:r,line:o}}(a,e,t),c=p.from,d=p.to,u=p.line;Nq(t).from=c,Nq(t).to=d,t.setData(u);var m=t.get("symbol"),h=t.get("symbolSize"),g=t.get("symbolRotate"),f=t.get("symbolOffset");function y(t,n,a){var r=t.getItemModel(n);zq(t,n,a,e,i);var s=r.getModel("itemStyle").getItemStyle();null==s.fill&&(s.fill=wS(o,"color")),t.setItemVisual(n,{symbolKeepAspect:r.get("symbolKeepAspect"),symbolOffset:co(r.get("symbolOffset",!0),f[a?0:1]),symbolRotate:co(r.get("symbolRotate",!0),g[a?0:1]),symbolSize:co(r.get("symbolSize"),h[a?0:1]),symbol:co(r.get("symbol",!0),m[a?0:1]),style:s})}Xr(m)||(m=[m,m]),Xr(h)||(h=[h,h]),Xr(g)||(g=[g,g]),Xr(f)||(f=[f,f]),p.from.each((function(e){y(c,e,!0),y(d,e,!1)})),u.each((function(e){var t=u.getItemModel(e).getModel("lineStyle").getLineStyle();u.setItemLayout(e,[c.getItemLayout(e),d.getItemLayout(e)]),null==t.stroke&&(t.stroke=c.getItemVisual(e,"style").fill),u.setItemVisual(e,{fromSymbolKeepAspect:c.getItemVisual(e,"symbolKeepAspect"),fromSymbolOffset:c.getItemVisual(e,"symbolOffset"),fromSymbolRotate:c.getItemVisual(e,"symbolRotate"),fromSymbolSize:c.getItemVisual(e,"symbolSize"),fromSymbol:c.getItemVisual(e,"symbol"),toSymbolKeepAspect:d.getItemVisual(e,"symbolKeepAspect"),toSymbolOffset:d.getItemVisual(e,"symbolOffset"),toSymbolRotate:d.getItemVisual(e,"symbolRotate"),toSymbolSize:d.getItemVisual(e,"symbolSize"),toSymbol:d.getItemVisual(e,"symbol"),style:t})})),l.updateData(u),p.line.eachItemGraphicEl((function(e){Xm(e).dataModel=t,e.traverse((function(e){Xm(e).dataModel=t}))})),this.markKeep(l),l.group.silent=t.get("silent")||e.get("silent")},t.type="markLine",t}(Fq);function jq(e){e.registerComponentModel(Bq),e.registerComponentView(Uq),e.registerPreprocessor((function(e){wq(e.series,"markLine")&&(e.markLine=e.markLine||{})}))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.createMarkerModelFromSeries=function(e,n,i){return new t(e,n,i)},t.type="markArea",t.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}}}(_q);var Hq=Pd(),Wq=function(e,t,n,i){var a=i[0],r=i[1];if(a&&r){var o=Mq(e,a),s=Mq(e,r),l=o.coord,p=s.coord;l[0]=po(l[0],-1/0),l[1]=po(l[1],-1/0),p[0]=po(p[0],1/0),p[1]=po(p[1],1/0);var c=Rr([{},o,s]);return c.coord=[o.coord,s.coord],c.x0=o.x,c.y0=o.y,c.x1=s.x,c.y1=s.y,c}};function $q(e){return!isNaN(e)&&!isFinite(e)}function Kq(e,t,n,i){var a=1-e;return $q(t[a])&&$q(n[a])}function Yq(e,t){var n=t.coord[0],i=t.coord[1],a={coord:n,x:t.x0,y:t.y0},r={coord:i,x:t.x1,y:t.y1};return jE(e,"cartesian2d")?!(!n||!i||!Kq(1,n,i)&&!Kq(0,n,i))||function(e,t,n){return!(e&&e.containZone&&t.coord&&n.coord&&!Tq(t)&&!Tq(n))||e.containZone(t.coord,n.coord)}(e,a,r):Pq(e,a)||Pq(e,r)}function Xq(e,t,n,i,a){var r,o=i.coordinateSystem,s=e.getItemModel(t),l=Wc(s.get(n[0]),a.getWidth()),p=Wc(s.get(n[1]),a.getHeight());if(isNaN(l)||isNaN(p)){if(i.getMarkerPosition){var c=e.getValues(["x0","y0"],t),d=e.getValues(["x1","y1"],t),u=o.clampData(c),m=o.clampData(d),h=[];"x0"===n[0]?h[0]=u[0]>m[0]?d[0]:c[0]:h[0]=u[0]>m[0]?c[0]:d[0],"y0"===n[1]?h[1]=u[1]>m[1]?d[1]:c[1]:h[1]=u[1]>m[1]?c[1]:d[1],r=i.getMarkerPosition(h,n,!0)}else{var g=[v=e.get(n[0],t),x=e.get(n[1],t)];o.clampData&&o.clampData(g,g),r=o.dataToPoint(g,!0)}if(jE(o,"cartesian2d")){var f=o.getAxis("x"),y=o.getAxis("y"),v=e.get(n[0],t),x=e.get(n[1],t);$q(v)?r[0]=f.toGlobalCoord(f.getExtent()["x0"===n[0]?0:1]):$q(x)&&(r[1]=y.toGlobalCoord(y.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(r[0]=l),isNaN(p)||(r[1]=p)}else r=[l,p];return r}var Zq=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]];!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}We(t,e),t.prototype.updateTransform=function(e,t,n){t.eachSeries((function(e){var t=_q.getMarkerModelFromSeries(e,"markArea");if(t){var i=t.getData();i.each((function(t){var a=Ur(Zq,(function(a){return Xq(i,t,a,e,n)}));i.setItemLayout(t,a),i.getItemGraphicEl(t).setShape("points",a)}))}}),this)},t.prototype.renderSeries=function(e,t,n,i){var a=e.coordinateSystem,r=e.id,o=e.getData(),s=this.markerGroupMap,l=s.get(r)||s.set(r,{group:new Rc});this.group.add(l.group),this.markKeep(l);var p=function(e,t,n){var i,a,r=["x0","y0","x1","y1"];if(e){var o=Ur(e&&e.dimensions,(function(e){var n=t.getData();return Br(Br({},n.getDimensionInfo(n.mapDimension(e))||{}),{name:e,ordinalMeta:null})}));a=Ur(r,(function(e,t){return{name:e,type:o[t%2].type}})),i=new W_(a,n)}else i=new W_(a=[{name:"value",type:"float"}],n);var s=Ur(n.get("data"),Yr(Wq,t,e,n));e&&(s=Hr(s,Yr(Yq,e)));var l=e?function(e,t,n,i){return yb(e.coord[Math.floor(i/2)][i%2],a[i])}:function(e,t,n,i){return yb(e.value,a[i])};return i.initData(s,null,l),i.hasItemOption=!0,i}(a,e,t);t.setData(p),p.each((function(t){var n=Ur(Zq,(function(n){return Xq(p,t,n,e,i)})),r=a.getAxis("x").scale,s=a.getAxis("y").scale,l=r.getExtent(),c=s.getExtent(),d=[r.parse(p.get("x0",t)),r.parse(p.get("x1",t))],u=[s.parse(p.get("y0",t)),s.parse(p.get("y1",t))];Kc(d),Kc(u);var m=!!(l[0]>d[1]||l[1]u[1]||c[1]=0},t.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},t.type="legend.plain",t.dependencies=["series"],t.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},t}(kv),Jq=Yr,eG=zr,tG=Rc,nG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.newlineDisabled=!1,n}return We(t,e),t.prototype.init=function(){this.group.add(this._contentGroup=new tG),this.group.add(this._selectorGroup=new tG),this._isFirstRender=!0},t.prototype.getContentGroup=function(){return this._contentGroup},t.prototype.getSelectorGroup=function(){return this._selectorGroup},t.prototype.render=function(e,t,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),e.get("show",!0)){var a=e.get("align"),r=e.get("orient");a&&"auto"!==a||(a="right"===e.get("left")&&"vertical"===r?"right":"left");var o=e.get("selector",!0),s=e.get("selectorPosition",!0);!o||s&&"auto"!==s||(s="horizontal"===r?"end":"start"),this.renderInner(a,e,t,n,o,r,s);var l=e.getBoxLayoutParams(),p={width:n.getWidth(),height:n.getHeight()},c=e.get("padding"),d=Sv(l,p,c),u=this.layoutInner(e,a,d,i,o,s),m=Sv(Nr({width:u.width,height:u.height},l),p,c);this.group.x=m.x-u.x,this.group.y=m.y-u.y,this.group.markRedraw(),this.group.add(this._backgroundEl=rV(u,e))}},t.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},t.prototype.renderInner=function(e,t,n,i,a,r,o){var s=this.getContentGroup(),l=Co(),p=t.get("selectedMode"),c=[];n.eachRawSeries((function(e){!e.get("legendHoverLink")&&c.push(e.id)})),eG(t.getData(),(function(a,r){var o=a.get("name");if(!this.newlineDisabled&&(""===o||"\n"===o)){var d=new tG;return d.newline=!0,void s.add(d)}var u=n.getSeriesByName(o)[0];if(!l.get(o)){if(u){var m=u.getData(),h=m.getVisual("legendLineStyle")||{},g=m.getVisual("legendIcon"),f=m.getVisual("style"),y=this._createItem(u,o,r,a,t,e,h,f,g,p,i);y.on("click",Jq(iG,o,null,i,c)).on("mouseover",Jq(rG,u.name,null,i,c)).on("mouseout",Jq(oG,u.name,null,i,c)),n.ssr&&y.eachChild((function(e){var t=Xm(e);t.seriesIndex=u.seriesIndex,t.dataIndex=r,t.ssrType="legend"})),l.set(o,!0)}else n.eachRawSeries((function(s){if(!l.get(o)&&s.legendVisualProvider){var d=s.legendVisualProvider;if(!d.containName(o))return;var u=d.indexOfName(o),m=d.getItemVisual(u,"style"),h=d.getItemVisual(u,"legendIcon"),g=Kl(m.fill);g&&0===g[3]&&(g[3]=.2,m=Br(Br({},m),{fill:tp(g,"rgba")}));var f=this._createItem(s,o,r,a,t,e,{},m,h,p,i);f.on("click",Jq(iG,null,o,i,c)).on("mouseover",Jq(rG,null,o,i,c)).on("mouseout",Jq(oG,null,o,i,c)),n.ssr&&f.eachChild((function(e){var t=Xm(e);t.seriesIndex=s.seriesIndex,t.dataIndex=r,t.ssrType="legend"})),l.set(o,!0)}}),this);0}}),this),a&&this._createSelector(a,t,i,r,o)},t.prototype._createSelector=function(e,t,n,i,a){var r=this.getSelectorGroup();eG(e,(function(e){var i=e.type,a=new Nm({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect"})}});r.add(a),Yf(a,{normal:t.getModel("selectorLabel"),emphasis:t.getModel(["emphasis","selectorLabel"])},{defaultText:e.title}),Lh(a)}))},t.prototype._createItem=function(e,t,n,i,a,r,o,s,l,p,c){var d=e.visualDrawType,u=a.get("itemWidth"),m=a.get("itemHeight"),h=a.isSelected(t),g=i.get("symbolRotate"),f=i.get("symbolKeepAspect"),y=i.get("icon"),v=function(e,t,n,i,a,r,o){function s(e,t){"auto"===e.lineWidth&&(e.lineWidth=t.lineWidth>0?2:0),eG(e,(function(n,i){"inherit"===e[i]&&(e[i]=t[i])}))}var l=t.getModel("itemStyle"),p=l.getItemStyle(),c=0===e.lastIndexOf("empty",0)?"fill":"stroke",d=l.getShallow("decal");p.decal=d&&"inherit"!==d?cC(d,o):i.decal,"inherit"===p.fill&&(p.fill=i[a]);"inherit"===p.stroke&&(p.stroke=i[c]);"inherit"===p.opacity&&(p.opacity=("fill"===a?i:n).opacity);s(p,i);var u=t.getModel("lineStyle"),m=u.getLineStyle();if(s(m,n),"auto"===p.fill&&(p.fill=i.fill),"auto"===p.stroke&&(p.stroke=i.fill),"auto"===m.stroke&&(m.stroke=i.fill),!r){var h=t.get("inactiveBorderWidth"),g=p[c];p.lineWidth="auto"===h?i.lineWidth>0&&g?2:0:p.lineWidth,p.fill=t.get("inactiveColor"),p.stroke=t.get("inactiveBorderColor"),m.stroke=u.get("inactiveColor"),m.lineWidth=u.get("inactiveWidth")}return{itemStyle:p,lineStyle:m}}(l=y||l||"roundRect",i,o,s,d,h,c),x=new tG,b=i.getModel("textStyle");if(!Zr(e.getLegendIcon)||y&&"inherit"!==y){var w="inherit"===y&&e.getData().getVisual("symbol")?"inherit"===g?e.getData().getVisual("symbolRotate"):g:0;x.add(function(e){var t=e.icon||"roundRect",n=NS(t,0,0,e.itemWidth,e.itemHeight,e.itemStyle.fill,e.symbolKeepAspect);n.setStyle(e.itemStyle),n.rotation=(e.iconRotate||0)*Math.PI/180,n.setOrigin([e.itemWidth/2,e.itemHeight/2]),t.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2);return n}({itemWidth:u,itemHeight:m,icon:l,iconRotate:w,itemStyle:v.itemStyle,lineStyle:v.lineStyle,symbolKeepAspect:f}))}else x.add(e.getLegendIcon({itemWidth:u,itemHeight:m,icon:l,iconRotate:g,itemStyle:v.itemStyle,lineStyle:v.lineStyle,symbolKeepAspect:f}));var S="left"===r?u+5:-5,C=r,_=a.get("formatter"),T=t;Qr(_)&&_?T=_.replace("{name}",null!=t?t:""):Zr(_)&&(T=_(t));var I=h?b.getTextColor():i.get("inactiveColor");x.add(new Nm({style:Zf(b,{text:T,x:S,y:m/2,fill:I,align:C,verticalAlign:"middle"},{inheritColor:I})}));var E=new Fm({shape:x.getBoundingRect(),style:{fill:"transparent"}}),M=i.getModel("tooltip");return M.get("show")&&zf({el:E,componentModel:a,itemName:t,itemTooltipOption:M.option}),x.add(E),x.eachChild((function(e){e.silent=!0})),E.silent=!p,this.getContentGroup().add(x),Lh(x),x.__legendDataIndex=n,x},t.prototype.layoutInner=function(e,t,n,i,a,r){var o=this.getContentGroup(),s=this.getSelectorGroup();wv(e.get("orient"),o,e.get("itemGap"),n.width,n.height);var l=o.getBoundingRect(),p=[-l.x,-l.y];if(s.markRedraw(),o.markRedraw(),a){wv("horizontal",s,e.get("selectorItemGap",!0));var c=s.getBoundingRect(),d=[-c.x,-c.y],u=e.get("selectorButtonGap",!0),m=e.getOrient().index,h=0===m?"width":"height",g=0===m?"height":"width",f=0===m?"y":"x";"end"===r?d[m]+=l[h]+u:p[m]+=c[h]+u,d[1-m]+=l[g]/2-c[g]/2,s.x=d[0],s.y=d[1],o.x=p[0],o.y=p[1];var y={x:0,y:0};return y[h]=l[h]+u+c[h],y[g]=Math.max(l[g],c[g]),y[f]=Math.min(0,c[f]+d[1-m]),y}return o.x=p[0],o.y=p[1],this.group.getBoundingRect()},t.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},t.type="legend.plain",t}(ww);function iG(e,t,n,i){oG(e,t,n,i),n.dispatchAction({type:"legendToggleSelect",name:null!=e?e:t}),rG(e,t,n,i)}function aG(e){for(var t,n=e.getZr().storage.getDisplayList(),i=0,a=n.length;in[a],h=[-d.x,-d.y];t||(h[i]=l[s]);var g=[0,0],f=[-u.x,-u.y],y=co(e.get("pageButtonGap",!0),e.get("itemGap",!0));m&&("end"===e.get("pageButtonPosition",!0)?f[i]+=n[a]-u[a]:g[i]+=u[a]+y);f[1-i]+=d[r]/2-u[r]/2,l.setPosition(h),p.setPosition(g),c.setPosition(f);var v={x:0,y:0};if(v[a]=m?n[a]:d[a],v[r]=Math.max(d[r],u[r]),v[o]=Math.min(0,u[o]+f[1-i]),p.__rectSize=n[a],m){var x={x:0,y:0};x[a]=Math.max(n[a]-u[a]-y,0),x[r]=v[r],p.setClipPath(new Fm({shape:x})),p.__rectSize=x[a]}else c.eachChild((function(e){e.attr({invisible:!0,silent:!0})}));var b=this._getPageInfo(e);return null!=b.pageIndex&&pf(l,{x:b.contentPosition[0],y:b.contentPosition[1]},m?e:null),this._updatePageInfoView(e,b),v},t.prototype._pageGo=function(e,t,n){var i=this._getPageInfo(t)[e];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:t.id})},t.prototype._updatePageInfoView=function(e,t){var n=this._controllerGroup;zr(["pagePrev","pageNext"],(function(i){var a=null!=t[i+"DataIndex"],r=n.childOfName(i);r&&(r.setStyle("fill",a?e.get("pageIconColor",!0):e.get("pageIconInactiveColor",!0)),r.cursor=a?"pointer":"default")}));var i=n.childOfName("pageText"),a=e.get("pageFormatter"),r=t.pageIndex,o=null!=r?r+1:0,s=t.pageCount;i&&a&&i.setStyle("text",Qr(a)?a.replace("{current}",null==o?"":o+"").replace("{total}",null==s?"":s+""):a({current:o,total:s}))},t.prototype._getPageInfo=function(e){var t=e.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,a=e.getOrient().index,r=pG[a],o=cG[a],s=this._findTargetItemIndex(t),l=n.children(),p=l[s],c=l.length,d=c?1:0,u={contentPosition:[n.x,n.y],pageCount:d,pageIndex:d-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!p)return u;var m=v(p);u.contentPosition[a]=-m.s;for(var h=s+1,g=m,f=m,y=null;h<=c;++h)(!(y=v(l[h]))&&f.e>g.s+i||y&&!x(y,g.s))&&(g=f.i>g.i?f:y)&&(null==u.pageNextDataIndex&&(u.pageNextDataIndex=g.i),++u.pageCount),f=y;for(h=s-1,g=m,f=m,y=null;h>=-1;--h)(y=v(l[h]))&&x(f,y.s)||!(g.i=t&&e.s<=t+i}},t.prototype._findTargetItemIndex=function(e){return this._showController?(this.getContentGroup().eachChild((function(i,a){var r=i.__legendDataIndex;null==n&&null!=r&&(n=a),r===e&&(t=a)})),null!=t?t:n):0;var t,n},t.type="legend.scroll"}(nG);var dG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="dataZoom.inside",t.defaultOption=Sy($L.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),t}($L),uG=Pd();function mG(e,t){if(t){e.removeKey(t.model.uid);var n=t.controller;n&&n.dispose()}}function hG(e,t){e.isDisposed()||e.dispatchAction({type:"dataZoom",animation:{easing:"cubicOut",duration:100},batch:t})}function gG(e,t,n,i){return e.coordinateSystem.containPoint([n,i])}function fG(e){e.registerProcessor(e.PRIORITY.PROCESSOR.FILTER,(function(e,t){var n=uG(t),i=n.coordSysRecordMap||(n.coordSysRecordMap=Co());i.each((function(e){e.dataZoomInfoMap=null})),e.eachComponent({mainType:"dataZoom",subType:"inside"},(function(e){zr(HL(e).infoList,(function(n){var a=n.model.uid,r=i.get(a)||i.set(a,function(e,t){var n={model:t,containsPoint:Yr(gG,t),dispatchAction:Yr(hG,e),dataZoomInfoMap:null,controller:null},i=n.controller=new mP(e.getZr());return zr(["pan","zoom","scrollMove"],(function(e){i.on(e,(function(t){var i=[];n.dataZoomInfoMap.each((function(a){if(t.isAvailableBehavior(a.model.option)){var r=(a.getRange||{})[e],o=r&&r(a.dzReferCoordSysInfo,n.model.mainType,n.controller,t);!a.model.get("disabled",!0)&&o&&i.push({dataZoomId:a.model.id,start:o[0],end:o[1]})}})),i.length&&n.dispatchAction(i)}))})),n}(t,n.model));(r.dataZoomInfoMap||(r.dataZoomInfoMap=Co())).set(e.uid,{dzReferCoordSysInfo:n,model:e,getRange:null})}))})),i.each((function(e){var t,n=e.controller,a=e.dataZoomInfoMap;if(a){var r=a.keys()[0];null!=r&&(t=a.get(r))}if(t){var o=function(e){var t,n="type_",i={type_true:2,type_move:1,type_false:0,type_undefined:-1},a=!0;return e.each((function(e){var r=e.model,o=!r.get("disabled",!0)&&(!r.get("zoomLock",!0)||"move");i[n+o]>i[n+t]&&(t=o),a=a&&r.get("preventDefaultMouseMove",!0)})),{controlType:t,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!a}}}(a);n.enable(o.controlType,o.opt),n.setPointerChecker(e.containsPoint),Rw(e,"dispatchAction",t.model.get("throttle",!0),"fixRate")}else mG(i,e)}))}))}var yG=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type="dataZoom.inside",t}return We(t,e),t.prototype.render=function(t,n,i){e.prototype.render.apply(this,arguments),t.noTarget()?this._clear():(this.range=t.getPercentRange(),function(e,t,n){uG(e).coordSysRecordMap.each((function(e){var i=e.dataZoomInfoMap.get(t.uid);i&&(i.getRange=n)}))}(i,t,{pan:Kr(vG.pan,this),zoom:Kr(vG.zoom,this),scrollMove:Kr(vG.scrollMove,this)}))},t.prototype.dispose=function(){this._clear(),e.prototype.dispose.apply(this,arguments)},t.prototype._clear=function(){!function(e,t){for(var n=uG(e).coordSysRecordMap,i=n.keys(),a=0;a0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(r[1]-r[0])+r[0],p=Math.max(1/i.scale,0);r[0]=(r[0]-l)*p+l,r[1]=(r[1]-l)*p+l;var c=this.dataZoomModel.findRepresentativeAxisProxy();if(c){var d=c.getMinMaxSpan();if(EA(0,r,[0,100],0,d.minSpan,d.maxSpan),this.range=r,a[0]!==r[0]||a[1]!==r[1])return r}}},pan:xG((function(e,t,n,i,a,r){var o=bG[i]([r.oldX,r.oldY],[r.newX,r.newY],t,a,n);return o.signal*(e[1]-e[0])*o.pixel/o.pixelLength})),scrollMove:xG((function(e,t,n,i,a,r){return bG[i]([0,0],[r.scrollDelta,r.scrollDelta],t,a,n).signal*(e[1]-e[0])*r.scrollDelta}))};function xG(e){return function(t,n,i,a){var r=this.range,o=r.slice(),s=t.axisModels[0];if(s)return EA(e(o,s,t,n,i,a),o,[0,100],"all"),this.range=o,r[0]!==o[0]||r[1]!==o[1]?o:void 0}}var bG={grid:function(e,t,n,i,a){var r=n.axis,o={},s=a.model.coordinateSystem.getRect();return e=e||[0,0],"x"===r.dim?(o.pixel=t[0]-e[0],o.pixelLength=s.width,o.pixelStart=s.x,o.signal=r.inverse?1:-1):(o.pixel=t[1]-e[1],o.pixelLength=s.height,o.pixelStart=s.y,o.signal=r.inverse?-1:1),o},polar:function(e,t,n,i,a){var r=n.axis,o={},s=a.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),p=s.getAngleAxis().getExtent();return e=e?s.pointToCoord(e):[0,0],t=s.pointToCoord(t),"radiusAxis"===n.mainType?(o.pixel=t[0]-e[0],o.pixelLength=l[1]-l[0],o.pixelStart=l[0],o.signal=r.inverse?1:-1):(o.pixel=t[1]-e[1],o.pixelLength=p[1]-p[0],o.pixelStart=p[0],o.signal=r.inverse?-1:1),o},singleAxis:function(e,t,n,i,a){var r=n.axis,o=a.model.coordinateSystem.getRect(),s={};return e=e||[0,0],"horizontal"===r.orient?(s.pixel=t[0]-e[0],s.pixelLength=o.width,s.pixelStart=o.x,s.signal=r.inverse?1:-1):(s.pixel=t[1]-e[1],s.pixelLength=o.height,s.pixelStart=o.y,s.signal=r.inverse?-1:1),s}};function wG(e){tV(e),e.registerComponentModel(dG),e.registerComponentView(yG),fG(e)}var SG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.type="dataZoom.slider",t.layoutMode="box",t.defaultOption=Sy($L.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:"#d2dbee",borderRadius:3,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#d2dbee",width:.5},areaStyle:{color:"#d2dbee",opacity:.2}},selectedDataBackground:{lineStyle:{color:"#8fb0f7",width:.5},areaStyle:{color:"#8fb0f7",opacity:.2}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:"#fff",borderColor:"#ACB8D1"},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:"#D2DBEE",opacity:.7},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#6E7079"},brushSelect:!0,brushStyle:{color:"rgba(135,175,274,0.15)"},emphasis:{handleStyle:{borderColor:"#8FB0F7"},moveHandleStyle:{color:"#8FB0F7"}}}),t}($L),CG=Fm,_G="horizontal",TG="vertical",IG=["line","bar","candlestick","scatter","custom"],EG={easing:"cubicOut",duration:100,delay:0},MG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._displayables={},n}return We(t,e),t.prototype.init=function(e,t){this.api=t,this._onBrush=Kr(this._onBrush,this),this._onBrushEnd=Kr(this._onBrushEnd,this)},t.prototype.render=function(t,n,i,a){if(e.prototype.render.apply(this,arguments),Rw(this,"_dispatchZoomAction",t.get("throttle"),"fixRate"),this._orient=t.getOrient(),!1!==t.get("show")){if(t.noTarget())return this._clear(),void this.group.removeAll();a&&"dataZoom"===a.type&&a.from===this.uid||this._buildView(),this._updateView()}else this.group.removeAll()},t.prototype.dispose=function(){this._clear(),e.prototype.dispose.apply(this,arguments)},t.prototype._clear=function(){Bw(this,"_dispatchZoomAction");var e=this.api.getZr();e.off("mousemove",this._onBrush),e.off("mouseup",this._onBrushEnd)},t.prototype._buildView=function(){var e=this.group;e.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var t=this._displayables.sliderGroup=new Rc;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),e.add(t),this._positionGroup()},t.prototype._resetLocation=function(){var e=this.dataZoomModel,t=this.api,n=e.get("brushSelect")?7:0,i=this._findCoordRect(),a={width:t.getWidth(),height:t.getHeight()},r=this._orient===_G?{right:a.width-i.x-i.width,top:a.height-30-7-n,width:i.width,height:30}:{right:7,top:i.y,width:30,height:i.height},o=Iv(e.option);zr(["right","top","width","height"],(function(e){"ph"===o[e]&&(o[e]=r[e])}));var s=Sv(o,a);this._location={x:s.x,y:s.y},this._size=[s.width,s.height],this._orient===TG&&this._size.reverse()},t.prototype._positionGroup=function(){var e=this.group,t=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),a=i&&i.get("inverse"),r=this._displayables.sliderGroup,o=(this._dataShadowInfo||{}).otherAxisInverse;r.attr(n!==_G||a?n===_G&&a?{scaleY:o?1:-1,scaleX:-1}:n!==TG||a?{scaleY:o?-1:1,scaleX:-1,rotation:Math.PI/2}:{scaleY:o?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:o?1:-1,scaleX:1});var s=e.getBoundingRect([r]);e.x=t.x-s.x,e.y=t.y-s.y,e.markRedraw()},t.prototype._getViewExtent=function(){return[0,this._size[0]]},t.prototype._renderBackground=function(){var e=this.dataZoomModel,t=this._size,n=this._displayables.sliderGroup,i=e.get("brushSelect");n.add(new CG({silent:!0,shape:{x:0,y:0,width:t[0],height:t[1]},style:{fill:e.get("backgroundColor")},z2:-40}));var a=new CG({shape:{x:0,y:0,width:t[0],height:t[1]},style:{fill:"transparent"},z2:0,onclick:Kr(this._onClickPanel,this)}),r=this.api.getZr();i?(a.on("mousedown",this._onBrushStart,this),a.cursor="crosshair",r.on("mousemove",this._onBrush),r.on("mouseup",this._onBrushEnd)):(r.off("mousemove",this._onBrush),r.off("mouseup",this._onBrushEnd)),n.add(a)},t.prototype._renderDataShadow=function(){var e=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],e){var t=this._size,n=this._shadowSize||[],i=e.series,a=i.getRawData(),r=i.getShadowDim&&i.getShadowDim(),o=r&&a.getDimensionInfo(r)?i.getShadowDim():e.otherDim;if(null!=o){var s=this._shadowPolygonPts,l=this._shadowPolylinePts;if(a!==this._shadowData||o!==this._shadowDim||t[0]!==n[0]||t[1]!==n[1]){var p=a.getDataExtent(o),c=.3*(p[1]-p[0]);p=[p[0]-c,p[1]+c];var d,u=[0,t[1]],m=[0,t[0]],h=[[t[0],0],[0,0]],g=[],f=m[1]/(a.count()-1),y=0,v=Math.round(a.count()/t[0]);a.each([o],(function(e,t){if(v>0&&t%v)y+=f;else{var n=null==e||isNaN(e)||""===e,i=n?0:Hc(e,p,u,!0);n&&!d&&t?(h.push([h[h.length-1][0],0]),g.push([g[g.length-1][0],0])):!n&&d&&(h.push([y,0]),g.push([y,0])),h.push([y,i]),g.push([y,i]),y+=f,d=n}})),s=this._shadowPolygonPts=h,l=this._shadowPolylinePts=g}this._shadowData=a,this._shadowDim=o,this._shadowSize=[t[0],t[1]];for(var x=this.dataZoomModel,b=0;b<3;b++){var w=S(1===b);this._displayables.sliderGroup.add(w),this._displayables.dataShadowSegs.push(w)}}}function S(e){var t=x.getModel(e?"selectedDataBackground":"dataBackground"),n=new Rc,i=new Bg({shape:{points:s},segmentIgnoreThreshold:1,style:t.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),a=new Lg({shape:{points:l},segmentIgnoreThreshold:1,style:t.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return n.add(i),n.add(a),n}},t.prototype._prepareDataShadowInfo=function(){var e=this.dataZoomModel,t=e.get("showDataShadow");if(!1!==t){var n,i=this.ecModel;return e.eachTargetAxis((function(a,r){var o=e.getAxisProxy(a,r);o&&zr(o.getTargetSeriesModels(),(function(e){if(!(n||!0!==t&&Lr(IG,e.get("type"))<0)){var o,s=i.getComponent(UL(a),r).axis,l=function(e){var t={x:"y",y:"x",radius:"angle",angle:"radius"};return t[e]}(a),p=e.coordinateSystem;null!=l&&p.getOtherAxis&&(o=p.getOtherAxis(s).inverse),l=e.getData().mapDimension(l),n={thisAxis:s,series:e,thisDim:a,otherDim:l,otherAxisInverse:o}}}),this)}),this),n}},t.prototype._renderHandle=function(){var e=this.group,t=this._displayables,n=t.handles=[null,null],i=t.handleLabels=[null,null],a=this._displayables.sliderGroup,r=this._size,o=this.dataZoomModel,s=this.api,l=o.get("borderRadius")||0,p=o.get("brushSelect"),c=t.filler=new CG({silent:p,style:{fill:o.get("fillerColor")},textConfig:{position:"inside"}});a.add(c),a.add(new CG({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:r[0],height:r[1],r:l},style:{stroke:o.get("dataBackgroundColor")||o.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}})),zr([0,1],(function(t){var r=o.get("handleIcon");!FS[r]&&r.indexOf("path://")<0&&r.indexOf("image://")<0&&(r="path://"+r);var s=NS(r,-1,0,2,2,null,!0);s.attr({cursor:kG(this._orient),draggable:!0,drift:Kr(this._onDragMove,this,t),ondragend:Kr(this._onDragEnd,this),onmouseover:Kr(this._showDataInfo,this,!0),onmouseout:Kr(this._showDataInfo,this,!1),z2:5});var l=s.getBoundingRect(),p=o.get("handleSize");this._handleHeight=Wc(p,this._size[1]),this._handleWidth=l.width/l.height*this._handleHeight,s.setStyle(o.getModel("handleStyle").getItemStyle()),s.style.strokeNoScale=!0,s.rectHover=!0,s.ensureState("emphasis").style=o.getModel(["emphasis","handleStyle"]).getItemStyle(),Lh(s);var c=o.get("handleColor");null!=c&&(s.style.fill=c),a.add(n[t]=s);var d=o.getModel("textStyle");e.add(i[t]=new Nm({silent:!0,invisible:!0,style:Zf(d,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:d.getTextColor(),font:d.getFont()}),z2:10}))}),this);var d=c;if(p){var u=Wc(o.get("moveHandleSize"),r[1]),m=t.moveHandle=new Fm({style:o.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:r[1]-.5,height:u}}),h=.8*u,g=t.moveHandleIcon=NS(o.get("moveHandleIcon"),-h/2,-h/2,h,h,"#fff",!0);g.silent=!0,g.y=r[1]+u/2-.5,m.ensureState("emphasis").style=o.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var f=Math.min(r[1]/2,Math.max(u,10));(d=t.moveZone=new Fm({invisible:!0,shape:{y:r[1]-f,height:u+f}})).on("mouseover",(function(){s.enterEmphasis(m)})).on("mouseout",(function(){s.leaveEmphasis(m)})),a.add(m),a.add(g),a.add(d)}d.attr({draggable:!0,cursor:kG(this._orient),drift:Kr(this._onDragMove,this,"all"),ondragstart:Kr(this._showDataInfo,this,!0),ondragend:Kr(this._onDragEnd,this),onmouseover:Kr(this._showDataInfo,this,!0),onmouseout:Kr(this._showDataInfo,this,!1)})},t.prototype._resetInterval=function(){var e=this._range=this.dataZoomModel.getPercentRange(),t=this._getViewExtent();this._handleEnds=[Hc(e[0],[0,100],t,!0),Hc(e[1],[0,100],t,!0)]},t.prototype._updateInterval=function(e,t){var n=this.dataZoomModel,i=this._handleEnds,a=this._getViewExtent(),r=n.findRepresentativeAxisProxy();if(r){var o=r.getMinMaxSpan(),s=[0,100];EA(t,i,a,n.get("zoomLock")?"all":e,null!=o.minSpan?Hc(o.minSpan,s,a,!0):null,null!=o.maxSpan?Hc(o.maxSpan,s,a,!0):null);var l=this._range,p=this._range=Kc([Hc(i[0],a,s,!0),Hc(i[1],a,s,!0)]);return!l||l[0]!==p[0]||l[1]!==p[1]}return!1},t.prototype._updateView=function(e){var t=this._displayables,n=this._handleEnds,i=Kc(n.slice()),a=this._size;zr([0,1],(function(e){var i=t.handles[e],r=this._handleHeight;i.attr({scaleX:r/2,scaleY:r/2,x:n[e]+(e?-1:1),y:a[1]/2-r/2})}),this),t.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:a[1]});var r={x:i[0],width:i[1]-i[0]};t.moveHandle&&(t.moveHandle.setShape(r),t.moveZone.setShape(r),t.moveZone.getBoundingRect(),t.moveHandleIcon&&t.moveHandleIcon.attr("x",r.x+r.width/2));for(var o=t.dataShadowSegs,s=[0,i[0],i[1],a[0]],l=0;lt[0]||n[1]<0||n[1]>t[1])){var i=this._handleEnds,a=(i[0]+i[1])/2,r=this._updateInterval("all",n[0]-a);this._updateView(),r&&this._dispatchZoomAction(!1)}},t.prototype._onBrushStart=function(e){var t=e.offsetX,n=e.offsetY;this._brushStart=new Ms(t,n),this._brushing=!0,this._brushStartTime=+new Date},t.prototype._onBrushEnd=function(e){if(this._brushing){var t=this._displayables.brushRect;if(this._brushing=!1,t){t.attr("ignore",!0);var n=t.shape;if(!(+new Date-this._brushStartTime<200&&Math.abs(n.width)<5)){var i=this._getViewExtent(),a=[0,100];this._range=Kc([Hc(n.x,i,a,!0),Hc(n.x+n.width,i,a,!0)]),this._handleEnds=[n.x,n.x+n.width],this._updateView(),this._dispatchZoomAction(!1)}}}},t.prototype._onBrush=function(e){this._brushing&&(gs(e.event),this._updateBrushRect(e.offsetX,e.offsetY))},t.prototype._updateBrushRect=function(e,t){var n=this._displayables,i=this.dataZoomModel,a=n.brushRect;a||(a=n.brushRect=new CG({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(a)),a.attr("ignore",!1);var r=this._brushStart,o=this._displayables.sliderGroup,s=o.transformCoordToLocal(e,t),l=o.transformCoordToLocal(r.x,r.y),p=this._size;s[0]=Math.max(Math.min(p[0],s[0]),0),a.setShape({x:l[0],y:0,width:s[0]-l[0],height:p[1]})},t.prototype._dispatchZoomAction=function(e){var t=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:e?EG:null,start:t[0],end:t[1]})},t.prototype._findCoordRect=function(){var e,t=HL(this.dataZoomModel).infoList;if(!e&&t.length){var n=t[0].model.coordinateSystem;e=n.getRect&&n.getRect()}if(!e){var i=this.api.getWidth(),a=this.api.getHeight();e={x:.2*i,y:.2*a,width:.6*i,height:.6*a}}return e},t.type="dataZoom.slider",t}(YL);function kG(e){return"vertical"===e?"ns-resize":"ew-resize"}function PG(e){e.registerComponentModel(SG),e.registerComponentView(MG),tV(e)}function DG(e){dI(wG),dI(PG)}var OG=function(e,t,n){var i=Ar((AG[e]||{})[t]);return n&&Xr(i)?i[i.length-1]:i},AG={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},FG=dO.mapVisual,RG=dO.eachVisual,BG=Xr,NG=zr,LG=Kc,VG=Hc,qG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.stateList=["inRange","outOfRange"],n.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],n.layoutMode={type:"box",ignoreSize:!0},n.dataBound=[-1/0,1/0],n.targetVisuals={},n.controllerVisuals={},n}return We(t,e),t.prototype.init=function(e,t,n){this.mergeDefaultAndTheme(e,n)},t.prototype.optionUpdated=function(e,t){var n=this.option;!t&&sq(n,e,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},t.prototype.resetVisual=function(e){var t=this.stateList;e=Kr(e,this),this.controllerVisuals=oq(this.option.controller,t,e),this.targetVisuals=oq(this.option.target,t,e)},t.prototype.getItemSymbol=function(){return null},t.prototype.getTargetSeriesIndices=function(){var e=this.option.seriesIndex,t=[];return null==e||"all"===e?this.ecModel.eachSeries((function(e,n){t.push(n)})):t=vd(e),t},t.prototype.eachTargetSeries=function(e,t){zr(this.getTargetSeriesIndices(),(function(n){var i=this.ecModel.getSeriesByIndex(n);i&&e.call(t,i)}),this)},t.prototype.isTargetSeries=function(e){var t=!1;return this.eachTargetSeries((function(n){n===e&&(t=!0)})),t},t.prototype.formatValueText=function(e,t,n){var i,a=this.option,r=a.precision,o=this.dataBound,s=a.formatter;n=n||["<",">"],Xr(e)&&(e=e.slice(),i=!0);var l=t?e:i?[p(e[0]),p(e[1])]:p(e);return Qr(s)?s.replace("{value}",i?l[0]:l).replace("{value2}",i?l[1]:l):Zr(s)?i?s(e[0],e[1]):s(e):i?e[0]===o[0]?n[0]+" "+l[1]:e[1]===o[1]?n[1]+" "+l[0]:l[0]+" - "+l[1]:l;function p(e){return e===o[0]?"min":e===o[1]?"max":(+e).toFixed(Math.min(r,20))}},t.prototype.resetExtent=function(){var e=this.option,t=LG([e.min,e.max]);this._dataExtent=t},t.prototype.getDataDimensionIndex=function(e){var t=this.option.dimension;if(null!=t)return e.getDimensionIndex(t);for(var n=e.dimensions,i=n.length-1;i>=0;i--){var a=n[i],r=e.getDimensionInfo(a);if(!r.isCalculationCoord)return r.storeDimIndex}},t.prototype.getExtent=function(){return this._dataExtent.slice()},t.prototype.completeVisualOption=function(){var e=this.ecModel,t=this.option,n={inRange:t.inRange,outOfRange:t.outOfRange},i=t.target||(t.target={}),a=t.controller||(t.controller={});Fr(i,n),Fr(a,n);var r=this.isCategory();function o(n){BG(t.color)&&!n.inRange&&(n.inRange={color:t.color.slice().reverse()}),n.inRange=n.inRange||{color:e.get("gradientColor")}}o.call(this,i),o.call(this,a),function(e,t,n){var i=e[t],a=e[n];i&&!a&&(a=e[n]={},NG(i,(function(e,t){if(dO.isValidType(t)){var n=OG(t,"inactive",r);null!=n&&(a[t]=n,"color"!==t||a.hasOwnProperty("opacity")||a.hasOwnProperty("colorAlpha")||(a.opacity=[0,0]))}})))}.call(this,i,"inRange","outOfRange"),function(e){var t=(e.inRange||{}).symbol||(e.outOfRange||{}).symbol,n=(e.inRange||{}).symbolSize||(e.outOfRange||{}).symbolSize,i=this.get("inactiveColor"),a=this.getItemSymbol()||"roundRect";NG(this.stateList,(function(o){var s=this.itemSize,l=e[o];l||(l=e[o]={color:r?i:[i]}),null==l.symbol&&(l.symbol=t&&Ar(t)||(r?a:[a])),null==l.symbolSize&&(l.symbolSize=n&&Ar(n)||(r?s[0]:[s[0],s[0]])),l.symbol=FG(l.symbol,(function(e){return"none"===e?a:e}));var p=l.symbolSize;if(null!=p){var c=-1/0;RG(p,(function(e){e>c&&(c=e)})),l.symbolSize=FG(p,(function(e){return VG(e,[0,c],[0,s[0]],!0)}))}}),this)}.call(this,a)},t.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},t.prototype.isCategory=function(){return!!this.option.categories},t.prototype.setSelected=function(e){},t.prototype.getSelected=function(){return null},t.prototype.getValueState=function(e){return null},t.prototype.getVisualMeta=function(e){return null},t.type="visualMap",t.dependencies=["series"],t.defaultOption={show:!0,z:4,seriesIndex:"all",min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,textStyle:{color:"#333"}},t}(kv),GG=[20,140],zG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.optionUpdated=function(t,n){e.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual((function(e){e.mappingMethod="linear",e.dataExtent=this.getExtent()})),this._resetRange()},t.prototype.resetItemSize=function(){e.prototype.resetItemSize.apply(this,arguments);var t=this.itemSize;(null==t[0]||isNaN(t[0]))&&(t[0]=GG[0]),(null==t[1]||isNaN(t[1]))&&(t[1]=GG[1])},t.prototype._resetRange=function(){var e=this.getExtent(),t=this.option.range;!t||t.auto?(e.auto=1,this.option.range=e):Xr(t)&&(t[0]>t[1]&&t.reverse(),t[0]=Math.max(t[0],e[0]),t[1]=Math.min(t[1],e[1]))},t.prototype.completeVisualOption=function(){e.prototype.completeVisualOption.apply(this,arguments),zr(this.stateList,(function(e){var t=this.option.controller[e].symbolSize;t&&t[0]!==t[1]&&(t[0]=t[1]/3)}),this)},t.prototype.setSelected=function(e){this.option.range=e.slice(),this._resetRange()},t.prototype.getSelected=function(){var e=this.getExtent(),t=Kc((this.get("range")||[]).slice());return t[0]>e[1]&&(t[0]=e[1]),t[1]>e[1]&&(t[1]=e[1]),t[0]=n[1]||e<=t[1])?"inRange":"outOfRange"},t.prototype.findTargetDataIndices=function(e){var t=[];return this.eachTargetSeries((function(n){var i=[],a=n.getData();a.each(this.getDataDimensionIndex(a),(function(t,n){e[0]<=t&&t<=e[1]&&i.push(n)}),this),t.push({seriesId:n.id,dataIndex:i})}),this),t},t.prototype.getVisualMeta=function(e){var t=UG(this,"outOfRange",this.getExtent()),n=UG(this,"inRange",this.option.range.slice()),i=[];function a(t,n){i.push({value:t,color:e(t,n)})}for(var r=0,o=0,s=n.length,l=t.length;oe[1])break;n.push({color:this.getControllerVisual(r,"color",t),offset:a/100})}return n.push({color:this.getControllerVisual(e[1],"color",t),offset:1}),n},t.prototype._createBarPoints=function(e,t){var n=this.visualMapModel.itemSize;return[[n[0]-t[0],e[0]],[n[0],e[0]],[n[0],e[1]],[n[0]-t[1],e[1]]]},t.prototype._createBarGroup=function(e){var t=this._orient,n=this.visualMapModel.get("inverse");return new Rc("horizontal"!==t||n?"horizontal"===t&&n?{scaleX:"bottom"===e?-1:1,rotation:-Math.PI/2}:"vertical"!==t||n?{scaleX:"left"===e?1:-1}:{scaleX:"left"===e?1:-1,scaleY:-1}:{scaleX:"bottom"===e?1:-1,rotation:Math.PI/2})},t.prototype._updateHandle=function(e,t){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,a=n.handleThumbs,r=n.handleLabels,o=i.itemSize,s=i.getExtent();YG([0,1],(function(l){var p=a[l];p.setStyle("fill",t.handlesColor[l]),p.y=e[l];var c=KG(e[l],[0,o[1]],s,!0),d=this.getControllerVisual(c,"symbolSize");p.scaleX=p.scaleY=d/o[0],p.x=o[0]-d/2;var u=Of(n.handleLabelPoints[l],Df(p,this.group));r[l].setStyle({x:u[0],y:u[1],text:i.formatValueText(this._dataInterval[l]),verticalAlign:"middle",align:"vertical"===this._orient?this._applyTransform("left",n.mainGroup):"center"})}),this)}},t.prototype._showIndicator=function(e,t,n,i){var a=this.visualMapModel,r=a.getExtent(),o=a.itemSize,s=[0,o[1]],l=this._shapes,p=l.indicator;if(p){p.attr("invisible",!1);var c=this.getControllerVisual(e,"color",{convertOpacityToAlpha:!0}),d=this.getControllerVisual(e,"symbolSize"),u=KG(e,r,s,!0),m=o[0]-d/2,h={x:p.x,y:p.y};p.y=u,p.x=m;var g=Of(l.indicatorLabelPoint,Df(p,this.group)),f=l.indicatorLabel;f.attr("invisible",!1);var y=this._applyTransform("left",l.mainGroup),v="horizontal"===this._orient;f.setStyle({text:(n||"")+a.formatValueText(t),verticalAlign:v?y:"middle",align:v?"center":y});var x={x:m,y:u,style:{fill:c}},b={style:{x:g[0],y:g[1]}};if(a.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var w={duration:100,easing:"cubicInOut",additive:!0};p.x=h.x,p.y=h.y,p.animateTo(x,w),f.animateTo(b,w)}else p.attr(x),f.attr(b);this._firstShowIndicator=!1;var S=this._shapes.handleLabels;if(S)for(var C=0;Ca[1]&&(p[1]=1/0),t&&(p[0]===-1/0?this._showIndicator(l,p[1],"< ",o):p[1]===1/0?this._showIndicator(l,p[0],"> ",o):this._showIndicator(l,l,"≈ ",o));var c=this._hoverLinkDataIndices,d=[];(t||ez(n))&&(d=this._hoverLinkDataIndices=n.findTargetDataIndices(p));var u=function(e,t){var n={},i={};return a(e||[],n),a(t||[],i,n),[r(n),r(i)];function a(e,t,n){for(var i=0,a=e.length;i=0&&(a.dimension=r,i.push(a))}})),e.getData().setVisual("visualMeta",i)}}];function rz(e,t,n,i){for(var a=t.targetVisuals[i],r=dO.prepareVisualTypes(a),o={color:wS(e.getData(),"color")},s=0,l=r.length;s0:e.splitNumber>0)&&!e.calculable?"piecewise":"continuous"})),e.registerAction(nz,iz),zr(az,(function(t){e.registerVisual(e.PRIORITY.VISUAL.COMPONENT,t)})),e.registerPreprocessor(sz))}function dz(e){e.registerComponentModel(zG),e.registerComponentView(QG),cz(e)}var uz=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._pieceList=[],n}return We(t,e),t.prototype.optionUpdated=function(t,n){e.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var i=this._mode=this._determineMode();this._pieceList=[],mz[this._mode].call(this,this._pieceList),this._resetSelected(t,n);var a=this.option.categories;this.resetVisual((function(e,t){"categories"===i?(e.mappingMethod="category",e.categories=Ar(a)):(e.dataExtent=this.getExtent(),e.mappingMethod="piecewise",e.pieceList=Ur(this._pieceList,(function(e){return e=Ar(e),"inRange"!==t&&(e.visual=null),e})))}))},t.prototype.completeVisualOption=function(){var t=this.option,n={},i=dO.listVisualTypes(),a=this.isCategory();function r(e,t,n){return e&&e[t]&&e[t].hasOwnProperty(n)}zr(t.pieces,(function(e){zr(i,(function(t){e.hasOwnProperty(t)&&(n[t]=1)}))})),zr(n,(function(e,n){var i=!1;zr(this.stateList,(function(e){i=i||r(t,e,n)||r(t.target,e,n)}),this),!i&&zr(this.stateList,(function(e){(t[e]||(t[e]={}))[n]=OG(n,"inRange"===e?"active":"inactive",a)}))}),this),e.prototype.completeVisualOption.apply(this,arguments)},t.prototype._resetSelected=function(e,t){var n=this.option,i=this._pieceList,a=(t?n:e).selected||{};if(n.selected=a,zr(i,(function(e,t){var n=this.getSelectedMapKey(e);a.hasOwnProperty(n)||(a[n]=!0)}),this),"single"===n.selectedMode){var r=!1;zr(i,(function(e,t){var n=this.getSelectedMapKey(e);a[n]&&(r?a[n]=!1:r=!0)}),this)}},t.prototype.getItemSymbol=function(){return this.get("itemSymbol")},t.prototype.getSelectedMapKey=function(e){return"categories"===this._mode?e.value+"":e.index+""},t.prototype.getPieceList=function(){return this._pieceList},t.prototype._determineMode=function(){var e=this.option;return e.pieces&&e.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},t.prototype.setSelected=function(e){this.option.selected=Ar(e)},t.prototype.getValueState=function(e){var t=dO.findPieceIndex(e,this._pieceList);return null!=t&&this.option.selected[this.getSelectedMapKey(this._pieceList[t])]?"inRange":"outOfRange"},t.prototype.findTargetDataIndices=function(e){var t=[],n=this._pieceList;return this.eachTargetSeries((function(i){var a=[],r=i.getData();r.each(this.getDataDimensionIndex(r),(function(t,i){dO.findPieceIndex(t,n)===e&&a.push(i)}),this),t.push({seriesId:i.id,dataIndex:a})}),this),t},t.prototype.getRepresentValue=function(e){var t;if(this.isCategory())t=e.value;else if(null!=e.value)t=e.value;else{var n=e.interval||[];t=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return t},t.prototype.getVisualMeta=function(e){if(!this.isCategory()){var t=[],n=["",""],i=this,a=this._pieceList.slice();if(a.length){var r=a[0].interval[0];r!==-1/0&&a.unshift({interval:[-1/0,r]}),(r=a[a.length-1].interval[1])!==1/0&&a.push({interval:[r,1/0]})}else a.push({interval:[-1/0,1/0]});var o=-1/0;return zr(a,(function(e){var t=e.interval;t&&(t[0]>o&&s([o,t[0]],"outOfRange"),s(t.slice()),o=t[1])}),this),{stops:t,outerColors:n}}function s(a,r){var o=i.getRepresentValue({interval:a});r||(r=i.getValueState(o));var s=e(o,r);a[0]===-1/0?n[0]=s:a[1]===1/0?n[1]=s:t.push({value:a[0],color:s},{value:a[1],color:s})}},t.type="visualMap.piecewise",t.defaultOption=Sy(qG.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),t}(qG),mz={splitNumber:function(e){var t=this.option,n=Math.min(t.precision,20),i=this.getExtent(),a=t.splitNumber;a=Math.max(parseInt(a,10),1),t.splitNumber=a;for(var r=(i[1]-i[0])/a;+r.toFixed(n)!==r&&n<5;)n++;t.precision=n,r=+r.toFixed(n),t.minOpen&&e.push({interval:[-1/0,i[0]],close:[0,0]});for(var o=0,s=i[0];o","≥"][t[0]]];e.text=e.text||this.formatValueText(null!=e.value?e.value:e.interval,!1,n)}),this)}};function hz(e,t){var n=e.inverse;("vertical"===e.orient?!n:n)&&t.reverse()}var gz=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return We(t,e),t.prototype.doRender=function(){var e=this.group;e.removeAll();var t=this.visualMapModel,n=t.get("textGap"),i=t.textStyleModel,a=i.getFont(),r=i.getTextColor(),o=this._getItemAlign(),s=t.itemSize,l=this._getViewData(),p=l.endsText,c=po(t.get("showLabel",!0),!p);p&&this._renderEndsText(e,p[0],s,c,o),zr(l.viewPieceList,(function(i){var l=i.piece,p=new Rc;p.onclick=Kr(this._onItemClick,this,l),this._enableHoverLink(p,i.indexInModelPieceList);var d=t.getRepresentValue(l);if(this._createItemSymbol(p,d,[0,0,s[0],s[1]]),c){var u=this.visualMapModel.getValueState(d);p.add(new Nm({style:{x:"right"===o?-n:s[0]+n,y:s[1]/2,text:l.text,verticalAlign:"middle",align:o,font:a,fill:r,opacity:"outOfRange"===u?.5:1}}))}e.add(p)}),this),p&&this._renderEndsText(e,p[1],s,c,o),wv(t.get("orient"),e,t.get("itemGap")),this.renderBackground(e),this.positionGroup(e)},t.prototype._enableHoverLink=function(e,t){var n=this;e.on("mouseover",(function(){return i("highlight")})).on("mouseout",(function(){return i("downplay")}));var i=function(e){var i=n.visualMapModel;i.option.hoverLink&&n.api.dispatchAction({type:e,batch:$G(i.findTargetDataIndices(t),i)})}},t.prototype._getItemAlign=function(){var e=this.visualMapModel,t=e.option;if("vertical"===t.orient)return WG(e,this.api,e.itemSize);var n=t.align;return n&&"auto"!==n||(n="left"),n},t.prototype._renderEndsText=function(e,t,n,i,a){if(t){var r=new Rc,o=this.visualMapModel.textStyleModel;r.add(new Nm({style:Zf(o,{x:i?"right"===a?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?a:"center",text:t})})),e.add(r)}},t.prototype._getViewData=function(){var e=this.visualMapModel,t=Ur(e.getPieceList(),(function(e,t){return{piece:e,indexInModelPieceList:t}})),n=e.get("text"),i=e.get("orient"),a=e.get("inverse");return("horizontal"===i?a:!a)?t.reverse():n&&(n=n.slice().reverse()),{viewPieceList:t,endsText:n}},t.prototype._createItemSymbol=function(e,t,n){e.add(NS(this.getControllerVisual(t,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(t,"color")))},t.prototype._onItemClick=function(e){var t=this.visualMapModel,n=t.option,i=n.selectedMode;if(i){var a=Ar(n.selected),r=t.getSelectedMapKey(e);"single"===i||!0===i?(a[r]=!0,zr(a,(function(e,t){a[t]=t===r}))):a[r]=!a[r],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:a})}},t.type="visualMap.piecewise",t}(jG);function fz(e){e.registerComponentModel(uz),e.registerComponentView(gz),cz(e)}function yz(e){dI(dz),dI(fz)}Pd();var vz={value:"eq","<":"lt","<=":"lte",">":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},xz=function(){function e(e){if(null==(this._condVal=Qr(e)?new RegExp(e):so(e)?e:null)){var t="";0,hd(t)}}return e.prototype.evaluate=function(e){var t=typeof e;return Qr(t)?this._condVal.test(e):!!eo(t)&&this._condVal.test(e+"")},e}(),bz=function(){function e(){}return e.prototype.evaluate=function(){return this.value},e}(),wz=function(){function e(){}return e.prototype.evaluate=function(){for(var e=this.children,t=0;t=Dz:-l>=Dz),u=l>0?l%Dz:l%Dz+Dz,m=!1;m=!!d||!lp(c)&&u>=Pz==!!p;var h=e+n*kz(r),g=t+i*Mz(r);this._start&&this._add("M",h,g);var f=Math.round(a*Oz);if(d){var y=1/this._p,v=(p?1:-1)*(Dz-y);this._add("A",n,i,f,1,+p,e+n*kz(r+v),t+i*Mz(r+v)),y>.01&&this._add("A",n,i,f,0,+p,h,g)}else{var x=e+n*kz(o),b=t+i*Mz(o);this._add("A",n,i,f,+m,+p,x,b)}},e.prototype.rect=function(e,t,n,i){this._add("M",e,t),this._add("l",n,0),this._add("l",0,i),this._add("l",-n,0),this._add("Z")},e.prototype.closePath=function(){this._d.length>0&&this._add("Z")},e.prototype._add=function(e,t,n,i,a,r,o,s,l){for(var p=[],c=this._p,d=1;d"}(a,r)+("style"!==a?os(o):o||"")+(i?""+n+Ur(i,(function(t){return e(t)})).join(n)+n:"")+("")}(e)}function Hz(e){return{zrId:e,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssStyleCache:{},cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function Wz(e,t,n,i){return Uz("svg","root",{width:e,height:t,xmlns:Vz,"xmlns:xlink":qz,version:"1.1",baseProfile:"full",viewBox:!!i&&"0 0 "+e+" "+t},n)}var $z=0;function Kz(){return $z++}var Yz={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},Xz="transform-origin";function Zz(e,t,n){var i=Br({},e.shape);Br(i,t),e.buildPath(n,i);var a=new Az;return a.reset(vp(e)),n.rebuildPath(a,1),a.generateStr(),a.getStr()}function Qz(e,t){var n=t.originX,i=t.originY;(n||i)&&(e[Xz]=n+"px "+i+"px")}var Jz={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function eU(e,t){var n=t.zrId+"-ani-"+t.cssAnimIdx++;return t.cssAnims[n]=e,n}function tU(e){return Qr(e)?Yz[e]?"cubic-bezier("+Yz[e]+")":Dl(e)?e:"":""}function nU(e,t,n,i){var a=e.animators,r=a.length,o=[];if(e instanceof Kg){var s=function(e,t,n){var i,a,r=e.shape.paths,o={};if(zr(r,(function(e){var t=Hz(n.zrId);t.animation=!0,nU(e,{},t,!0);var r=t.cssAnims,s=t.cssNodes,l=$r(r),p=l.length;if(p){var c=r[a=l[p-1]];for(var d in c){var u=c[d];o[d]=o[d]||{d:""},o[d].d+=u.d||""}for(var m in s){var h=s[m].animation;h.indexOf(a)>=0&&(i=h)}}})),i){t.d=!1;var s=eU(o,n);return i.replace(a,s)}}(e,t,n);if(s)o.push(s);else if(!r)return}else if(!r)return;for(var l={},p=0;p0})).length)return eU(c,n)+" "+a[0]+" both"}for(var f in l){(s=g(l[f]))&&o.push(s)}if(o.length){var y=n.zrId+"-cls-"+Kz();n.cssNodes["."+y]={animation:o.join(",")},t.class=y}}function iU(e,t,n,i){var a=JSON.stringify(e),r=n.cssStyleCache[a];r||(r=n.zrId+"-cls-"+Kz(),n.cssStyleCache[a]=r,n.cssNodes["."+r+(i?":hover":"")]=e),t.class=t.class?t.class+" "+r:r}var aU=Math.round;function rU(e){return e&&Qr(e.src)}function oU(e){return e&&Zr(e.toDataURL)}function sU(e,t,n,i){Lz((function(a,r){var o="fill"===a||"stroke"===a;o&&fp(r)?vU(t,e,a,i):o&&mp(r)?xU(n,e,a,i):e[a]=o&&"none"===r?"transparent":r}),t,n,!1),function(e,t,n){var i=e.style;if(function(e){return e&&(e.shadowBlur||e.shadowOffsetX||e.shadowOffsetY)}(i)){var a=function(e){var t=e.style,n=e.getGlobalScale();return[t.shadowColor,(t.shadowBlur||0).toFixed(2),(t.shadowOffsetX||0).toFixed(2),(t.shadowOffsetY||0).toFixed(2),n[0],n[1]].join(",")}(e),r=n.shadowCache,o=r[a];if(!o){var s=e.getGlobalScale(),l=s[0],p=s[1];if(!l||!p)return;var c=i.shadowOffsetX||0,d=i.shadowOffsetY||0,u=i.shadowBlur,m=op(i.shadowColor),h=m.opacity,g=m.color,f=u/2/l+" "+u/2/p;o=n.zrId+"-s"+n.shadowIdx++,n.defs[o]=Uz("filter",o,{id:o,x:"-100%",y:"-100%",width:"300%",height:"300%"},[Uz("feDropShadow","",{dx:c/l,dy:d/p,stdDeviation:f,"flood-color":g,"flood-opacity":h})]),r[a]=o}t.filter=yp(o)}}(n,e,i)}function lU(e,t){var n=zc(t);n&&(n.each((function(t,n){null!=t&&(e[(Gz+n).toLowerCase()]=t+"")})),t.isSilent()&&(e[Gz+"silent"]="true"))}function pU(e){return lp(e[0]-1)&&lp(e[1])&&lp(e[2])&&lp(e[3]-1)}function cU(e,t,n){if(t&&(!function(e){return lp(e[4])&&lp(e[5])}(t)||!pU(t))){var i=n?10:1e4;e.transform=pU(t)?"translate("+aU(t[4]*i)/i+" "+aU(t[5]*i)/i+")":function(e){return"matrix("+pp(e[0])+","+pp(e[1])+","+pp(e[2])+","+pp(e[3])+","+cp(e[4])+","+cp(e[5])+")"}(t)}}function dU(e,t,n){for(var i=e.points,a=[],r=0;r=0&&o||r;s&&(a=ap(s))}var l=i.lineWidth;l&&(l/=!i.strokeNoScale&&e.transform?e.transform[0]:1);var p={cursor:"pointer"};a&&(p.fill=a),i.stroke&&(p.stroke=i.stroke),l&&(p["stroke-width"]=l),iU(p,t,n,!0)}}(e,r,t),Uz(s,e.id+"",r)}function yU(e,t){return e instanceof Sm?fU(e,t):e instanceof Em?function(e,t){var n=e.style,i=n.image;if(i&&!Qr(i)&&(rU(i)?i=i.src:oU(i)&&(i=i.toDataURL())),i){var a=n.x||0,r=n.y||0,o={href:i,width:n.width,height:n.height};return a&&(o.x=a),r&&(o.y=r),cU(o,e.transform),sU(o,n,e,t),lU(o,e),t.animation&&nU(e,o,t),Uz("image",e.id+"",o)}}(e,t):e instanceof _m?function(e,t){var n=e.style,i=n.text;if(null!=i&&(i+=""),i&&!isNaN(n.x)&&!isNaN(n.y)){var a=n.font||hr,r=n.x||0,o=function(e,t,n){return"top"===n?e+=t/2:"bottom"===n&&(e-=t/2),e}(n.y||0,Sc(a),n.textBaseline),s={"dominant-baseline":"central","text-anchor":dp[n.textAlign]||n.textAlign};if(Um(n)){var l="",p=n.fontStyle,c=Gm(n.fontSize);if(!parseFloat(c))return;var d=n.fontFamily||mr,u=n.fontWeight;l+="font-size:"+c+";font-family:"+d+";",p&&"normal"!==p&&(l+="font-style:"+p+";"),u&&"normal"!==u&&(l+="font-weight:"+u+";"),s.style=l}else s.style="font: "+a;return i.match(/\s/)&&(s["xml:space"]="preserve"),r&&(s.x=r),o&&(s.y=o),cU(s,e.transform),sU(s,n,e,t),lU(s,e),t.animation&&nU(e,s,t),Uz("text",e.id+"",s,void 0,i)}}(e,t):void 0}function vU(e,t,n,i){var a,r=e[n],o={gradientUnits:r.global?"userSpaceOnUse":"objectBoundingBox"};if(hp(r))a="linearGradient",o.x1=r.x,o.y1=r.y,o.x2=r.x2,o.y2=r.y2;else{if(!gp(r))return void 0;a="radialGradient",o.cx=co(r.x,.5),o.cy=co(r.y,.5),o.r=co(r.r,.5)}for(var s=r.colorStops,l=[],p=0,c=s.length;pl?FU(e,null==n[d+1]?null:n[d+1].elm,n,s,d):RU(e,t,o,l))}(n,i,a):PU(a)?(PU(e.text)&&EU(n,""),FU(n,null,a,0,a.length-1)):PU(i)?RU(n,i,0,i.length-1):PU(e.text)&&EU(n,""):e.text!==t.text&&(PU(i)&&RU(n,i,0,i.length-1),EU(n,t.text)))}var LU=0,VU=function(){function e(e,t,n){if(this.type="svg",this.refreshHover=qU("refreshHover"),this.configLayer=qU("configLayer"),this.storage=t,this._opts=n=Br({},n),this.root=e,this._id="zr"+LU++,this._oldVNode=Wz(n.width,n.height),e&&!n.ssr){var i=this._viewport=document.createElement("div");i.style.cssText="position:relative;overflow:hidden";var a=this._svgDom=this._oldVNode.elm=zz("svg");BU(null,this._oldVNode),i.appendChild(a),e.appendChild(i)}this.resize(n.width,n.height)}return e.prototype.getType=function(){return this.type},e.prototype.getViewportRoot=function(){return this._viewport},e.prototype.getViewportRootOffset=function(){var e=this.getViewportRoot();if(e)return{offsetLeft:e.offsetLeft||0,offsetTop:e.offsetTop||0}},e.prototype.getSvgDom=function(){return this._svgDom},e.prototype.refresh=function(){if(this.root){var e=this.renderToVNode({willUpdate:!0});e.attrs.style="position:absolute;left:0;top:0;user-select:none",function(e,t){if(OU(e,t))NU(e,t);else{var n=e.elm,i=TU(n);AU(t),null!==i&&(SU(i,t.elm,IU(n)),RU(i,[e],0,0))}}(this._oldVNode,e),this._oldVNode=e}},e.prototype.renderOneToVNode=function(e){return yU(e,Hz(this._id))},e.prototype.renderToVNode=function(e){e=e||{};var t=this.storage.getDisplayList(!0),n=this._width,i=this._height,a=Hz(this._id);a.animation=e.animation,a.willUpdate=e.willUpdate,a.compress=e.compress,a.emphasis=e.emphasis;var r=[],o=this._bgVNode=function(e,t,n,i){var a;if(n&&"none"!==n)if(a=Uz("rect","bg",{width:e,height:t,x:"0",y:"0"}),fp(n))vU({fill:n},a.attrs,"fill",i);else if(mp(n))xU({style:{fill:n},dirty:Mo,getBoundingRect:function(){return{width:e,height:t}}},a.attrs,"fill",i);else{var r=op(n),o=r.color,s=r.opacity;a.attrs.fill=o,s<1&&(a.attrs["fill-opacity"]=s)}return a}(n,i,this._backgroundColor,a);o&&r.push(o);var s=e.compress?null:this._mainVNode=Uz("g","main",{},[]);this._paintList(t,a,s?s.children:r),s&&r.push(s);var l=Ur($r(a.defs),(function(e){return a.defs[e]}));if(l.length&&r.push(Uz("defs","defs",{},l)),e.animation){var p=function(e,t,n){var i=(n=n||{}).newline?"\n":"",a=" {"+i,r=i+"}",o=Ur($r(e),(function(t){return t+a+Ur($r(e[t]),(function(n){return n+":"+e[t][n]+";"})).join(i)+r})).join(i),s=Ur($r(t),(function(e){return"@keyframes "+e+a+Ur($r(t[e]),(function(n){return n+a+Ur($r(t[e][n]),(function(i){var a=t[e][n][i];return"d"===i&&(a='path("'+a+'")'),i+":"+a+";"})).join(i)+r})).join(i)+r})).join(i);return o||s?[""].join(i):""}(a.cssNodes,a.cssAnims,{newline:!0});if(p){var c=Uz("style","stl",{},[],p);r.push(c)}}return Wz(n,i,r,e.useViewBox)},e.prototype.renderToString=function(e){return e=e||{},jz(this.renderToVNode({animation:co(e.cssAnimation,!0),emphasis:co(e.cssEmphasis,!0),willUpdate:!1,compress:!0,useViewBox:co(e.useViewBox,!0)}),{newline:!0})},e.prototype.setBackgroundColor=function(e){this._backgroundColor=e},e.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},e.prototype._paintList=function(e,t,n){for(var i,a,r=e.length,o=[],s=0,l=0,p=0;p=0&&(!d||!a||d[h]!==a[h]);h--);for(var g=m-1;g>h;g--)i=o[--s-1];for(var f=h+1;f=o)}}for(var c=this.__startIndex;c15)break}n.prevElClipPaths&&d.restore()};if(m)if(0===m.length)s=l.__endIndex;else for(var b=u.dpr,w=0;w0&&e>i[0]){for(s=0;se);s++);o=n[i[s]]}if(i.splice(s+1,0,e),n[e]=t,!t.virtual)if(o){var l=o.dom;l.nextSibling?r.insertBefore(t.dom,l.nextSibling):r.appendChild(t.dom)}else r.firstChild?r.insertBefore(t.dom,r.firstChild):r.appendChild(t.dom);t.painter||(t.painter=this)}},e.prototype.eachLayer=function(e,t){for(var n=this._zlevelList,i=0;i0?WU:0),this._needsManuallyCompositing),p.__builtin__||Or("ZLevel "+l+" has been used by unkown layer "+p.id),p!==r&&(p.__used=!0,p.__startIndex!==a&&(p.__dirty=!0),p.__startIndex=a,p.incremental?p.__drawIndex=-1:p.__drawIndex=a,t(a),r=p),1&s.__dirty&&!s.__inHover&&(p.__dirty=!0,p.incremental&&p.__drawIndex<0&&(p.__drawIndex=a))}t(a),this.eachBuiltinLayer((function(e,t){!e.__used&&e.getElementCount()>0&&(e.__dirty=!0,e.__startIndex=e.__endIndex=e.__drawIndex=0),e.__dirty&&e.__drawIndex<0&&(e.__drawIndex=e.__startIndex)}))},e.prototype.clear=function(){return this.eachBuiltinLayer(this._clearLayer),this},e.prototype._clearLayer=function(e){e.clear()},e.prototype.setBackgroundColor=function(e){this._backgroundColor=e,zr(this._layers,(function(e){e.setUnpainted()}))},e.prototype.configLayer=function(e,t){if(t){var n=this._layerConfig;n[e]?Fr(n[e],t,!0):n[e]=t;for(var i=0;i({legendKey:e,left:!0}),ZU=e=>({background:e});function QU(e,n){1&e&&t.ɵɵelementContainer(0)}function JU(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,QU,1,0,"ng-container",8),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e.widgetTitlePanel)}}function ej(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Min")))}function tj(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Max")))}function nj(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Avg")))}function ij(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Total")))}function aj(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Latest")))}function rj(e,n){1&e&&t.ɵɵelementContainer(0)}function oj(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].min,"html"),t.ɵɵsanitizeHtml)}}function sj(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].max,"html"),t.ɵɵsanitizeHtml)}}function lj(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].avg,"html"),t.ɵɵsanitizeHtml)}}function pj(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].total,"html"),t.ɵɵsanitizeHtml)}}function cj(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].latest,"html"),t.ɵɵsanitizeHtml)}}function dj(e,n){if(1&e&&(t.ɵɵelementStart(0,"tr")(1,"th"),t.ɵɵtemplate(2,rj,1,0,"ng-container",14),t.ɵɵelementEnd(),t.ɵɵtemplate(3,oj,2,4,"td",15)(4,sj,2,4,"td",15)(5,lj,2,4,"td",15)(6,pj,2,4,"td",15)(7,cj,2,4,"td",15),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2),a=t.ɵɵreference(8);t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",a)("ngTemplateOutletContext",t.ɵɵpureFunction1(7,XU,e)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showMin),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showMax),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showAvg),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showTotal),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showLatest)}}function uj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"table",10)(2,"thead")(3,"tr"),t.ɵɵelement(4,"th"),t.ɵɵtemplate(5,ej,3,3,"th",11)(6,tj,3,3,"th",11)(7,nj,3,3,"th",11)(8,ij,3,3,"th",11)(9,aj,3,3,"th",11),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"tbody"),t.ɵɵtemplate(11,dj,8,9,"tr",12),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵproperty("ngIf",!0===e.legendConfig.showMin),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showMax),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showAvg),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showTotal),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showLatest),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",e.legendKeys)}}function mj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",18),t.ɵɵelement(2,"div",19)(3,"div",20),t.ɵɵpipe(4,"safe"),t.ɵɵelementEnd()()),2&e){const e=n.legendKey,i=n.left;t.ɵɵclassProp("left",i),t.ɵɵadvance(2),t.ɵɵstyleMap(t.ɵɵpureFunction1(8,ZU,e.dataKey.color)),t.ɵɵadvance(),t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(4,5,e.dataKey.label,"html"),t.ɵɵsanitizeHtml)}}class hj{constructor(e,t,n){this.renderer=e,this.sanitizer=t,this.widgetComponent=n,this.updateXAxisTimeWindow=(e,t)=>{e.min=t.minTime,e.max=t.maxTime}}ngOnInit(){this.initEchart(),this.initLegend()}ngAfterViewInit(){this.myChart=function(e,t,n){var i=!(n&&n.ssr);if(i){var a=p_(e);if(a)return a}var r=new KC(e,t,n);return r.id="ec_"+s_++,r_[r.id]=r,i&&Nd(e,l_,r.id),UC(r),hC.trigger("afterinit",r),r}(this.echartContainer.nativeElement,null,{renderer:"svg"}),this.initResize(),this.xAxis=this.setupXAxis(),this.yAxis=this.setupYAxis(),this.option={...this.setupAnimationSettings(),formatter:e=>this.setupTooltipElement(e),backgroundColor:"transparent",darkMode:!1,tooltip:{show:!0,trigger:"axis",confine:!0,padding:[8,12],appendTo:"body",textStyle:{fontFamily:"Roboto",fontSize:12,fontWeight:"normal",lineHeight:16}},grid:[{backgroundColor:null,borderColor:"#ccc",borderWidth:1,bottom:45,left:5,right:5,show:!1,top:10}],xAxis:[this.xAxis],yAxis:[this.yAxis],series:this.setupChartLines(),dataZoom:[{type:"inside",disabled:!1,realtime:!0,filterMode:"none"},{type:"slider",show:!0,showDetail:!1,realtime:!0,filterMode:"none",bottom:5}]},this.myChart.setOption(this.option),this.updateAxisOffset(!1)}onDataUpdated(){const e=[];this.onResize(),this.updateXAxisTimeWindow(this.xAxis,this.ctx.defaultSubscription.timeWindow);for(const t in this.ctx.data){e[t]=[];for(const[n,i]of this.ctx.data[t].data)e[t].push({name:n,value:[n,i]})}const t=[];for(const n of e)t.push({data:n});this.option.series=t,this.myChart.setOption(this.option),this.updateAxisOffset()}updateAxisOffset(e=!0){const t=Qe(this.myChart,this.yAxis.mainType,this.yAxis.id),n=Je(this.myChart,this.yAxis.mainType,this.yAxis.id,this.yAxis.name),i=Qe(this.myChart,this.xAxis.mainType,this.xAxis.id),a=t+n,r=i+Je(this.myChart,this.yAxis.mainType,this.yAxis.id,this.yAxis.name)+35;this.option.grid[0].left===a&&this.option.grid[0].bottom===r||(this.option.grid[0].left=a,this.yAxis.nameGap=t,this.option.grid[0].bottom=r,this.xAxis.nameGap=i,this.myChart.setOption(this.option,{replaceMerge:["yAxis","xAxis","grid"],lazyUpdate:e}))}initEchart(){dI([iq,BN,yz,DG,jq,hL,lP,oM,OM,XM,pP,ZB,wE,KU,GU])}initLegend(){this.showLegend=this.ctx.settings.showLegend,this.showLegend&&(this.legendConfig=this.ctx.settings.legendConfig,this.legendData=this.ctx.defaultSubscription.legendData,this.legendKeys=this.legendData.keys,this.legendClass=`legend-${this.legendConfig.position}`,this.legendConfig.sortDataKeys?this.legendKeys=this.legendData.keys.sort(((e,t)=>e.dataKey.label.localeCompare(t.dataKey.label))):this.legendKeys=this.legendData.keys)}initResize(){this.shapeResize$=new ResizeObserver((()=>{this.onResize()})),this.shapeResize$.observe(this.echartContainer.nativeElement)}onResize(){this.myChart.resize()}setupTooltipElement(e){const t=this.renderer.createElement("div");if(this.renderer.setStyle(t,"display","flex"),this.renderer.setStyle(t,"flex-direction","column"),this.renderer.setStyle(t,"align-items","flex-start"),this.renderer.setStyle(t,"gap","16px"),e.length){const n=this.renderer.createElement("div");this.renderer.setStyle(n,"display","flex"),this.renderer.setStyle(n,"flex-direction","column"),this.renderer.setStyle(n,"align-items","flex-start"),this.renderer.setStyle(n,"gap","4px"),this.renderer.appendChild(n,this.setTooltipDate(e));for(const[t,i]of e.entries())this.renderer.appendChild(n,this.constructTooltipSeriesElement(i,t));this.renderer.appendChild(t,n)}return t}constructTooltipSeriesElement(e,t){const n=this.renderer.createElement("div");this.renderer.setStyle(n,"display","flex"),this.renderer.setStyle(n,"flex-direction","row"),this.renderer.setStyle(n,"align-items","center"),this.renderer.setStyle(n,"align-self","stretch"),this.renderer.setStyle(n,"gap","12px");const i=this.renderer.createElement("div");this.renderer.setStyle(i,"display","flex"),this.renderer.setStyle(i,"align-items","center"),this.renderer.setStyle(i,"gap","8px"),this.renderer.appendChild(n,i);const a=this.renderer.createElement("div");this.renderer.setStyle(a,"width","8px"),this.renderer.setStyle(a,"height","8px"),this.renderer.setStyle(a,"border-radius","50%"),this.renderer.setStyle(a,"background",e.color),this.renderer.appendChild(i,a);const r=this.renderer.createElement("div");this.renderer.setProperty(r,"innerHTML",this.sanitizer.sanitize(g.HTML,e.seriesName)),this.renderer.setStyle(r,"font-family","Roboto"),this.renderer.setStyle(r,"font-size","12px"),this.renderer.setStyle(r,"font-style","normal"),this.renderer.setStyle(r,"font-weight",400),this.renderer.setStyle(r,"line-height","16px"),this.renderer.setStyle(r,"color","rgba(0, 0, 0, 0.76)"),this.renderer.appendChild(i,r);const o=J(this.ctx.data[t].dataKey.decimals)?this.ctx.data[t].dataKey.decimals:this.ctx.decimals,s=J(this.ctx.data[t].dataKey.units)?this.ctx.data[t].dataKey.units:this.ctx.units,l=ee(e.value[1],o,s,!1),p=this.renderer.createElement("div");return this.renderer.setProperty(p,"innerHTML",this.sanitizer.sanitize(g.HTML,l)),this.renderer.setStyle(p,"flex","1"),this.renderer.setStyle(p,"text-align","end"),this.renderer.setStyle(p,"font-family","Roboto"),this.renderer.setStyle(p,"font-size","12px"),this.renderer.setStyle(p,"font-style","normal"),this.renderer.setStyle(p,"font-weight",500),this.renderer.setStyle(p,"line-height","16px"),this.renderer.setStyle(p,"color","rgba(0, 0, 0, 0.76)"),this.renderer.appendChild(n,p),n}setTooltipDate(e){const t=this.renderer.createElement("div");return this.renderer.appendChild(t,this.renderer.createText(new Date(e[0].value[0]).toLocaleString("en-GB"))),this.renderer.setStyle(t,"font-family","Roboto"),this.renderer.setStyle(t,"font-size","11px"),this.renderer.setStyle(t,"font-style","normal"),this.renderer.setStyle(t,"font-weight","400"),this.renderer.setStyle(t,"line-height","16px"),this.renderer.setStyle(t,"color","rgba(0, 0, 0, 0.76)"),t}setupAnimationSettings(){return{animation:!0,animationDelay:0,animationDelayUpdate:0,animationDuration:500,animationDurationUpdate:300,animationEasing:"cubicOut",animationEasingUpdate:"cubicOut",animationThreshold:2e3}}setupChartLines(){const e=[];for(const[t,n]of this.ctx.datasources[0].dataKeys.entries())e.push({id:t,name:n.label,type:"line",showSymbol:!1,smooth:!1,step:!1,stackStrategy:"all",data:[],lineStyle:{color:n.color},itemStyle:{color:n.color}});return e}setupYAxis(){return{type:"value",position:"left",mainType:"yAxis",id:"yAxis",offset:0,nameLocation:"middle",nameRotate:90,alignTicks:!0,scale:!0,show:!0,axisLabel:{color:"rgba(0, 0, 0, 0.54)",fontFamily:"Roboto",fontSize:12,fontStyle:"normal",fontWeight:400,show:!0,formatter:e=>ee(e,this.ctx.decimals,this.ctx.units,!1)},splitLine:{show:!0},axisLine:{show:!0,lineStyle:{color:"rgba(0, 0, 0, 0.54)"}},axisTick:{lineStyle:{color:"rgba(0, 0, 0, 0.54)"},show:!0},nameTextStyle:{color:"rgba(0, 0, 0, 0.54)",fontFamily:"Roboto",fontSize:12,fontStyle:"normal",fontWeight:600}}}setupXAxis(){return{id:"xAxis",mainType:"xAxis",show:!0,type:"time",position:"bottom",offset:0,nameLocation:"middle",max:this.ctx.defaultSubscription.timeWindow.maxTime,min:this.ctx.defaultSubscription.timeWindow.minTime,nameTextStyle:{color:"rgba(0, 0, 0, 0.54)",fontStyle:"normal",fontWeight:600,fontFamily:"Roboto",fontSize:12},axisPointer:{shadowStyle:{color:"rgba(210,219,238,0.2)"}},splitLine:{show:!0},axisTick:{show:!0,lineStyle:{color:"rgba(0, 0, 0, 0.54)"}},axisLine:{onZero:!1,show:!0,lineStyle:{color:"rgba(0, 0, 0, 0.54)"}},axisLabel:{color:"rgba(0, 0, 0, 0.54)",fontFamily:"Roboto",fontSize:10,fontStyle:"normal",fontWeight:400,show:!0,hideOverlap:!0}}}static{this.ɵfac=function(e){return new(e||hj)(t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(et.DomSanitizer),t.ɵɵdirectiveInject(tt.WidgetComponent))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:hj,selectors:[["tb-gateway-statistics-chart"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(YU,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.echartContainer=e.first)}},inputs:{ctx:"ctx",widgetTitlePanel:"widgetTitlePanel"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:9,vars:4,consts:[["echartContainer",""],["legendItem",""],[1,"tb-time-series-chart-panel"],[1,"tb-time-series-chart-overlay"],[4,"ngIf"],[1,"tb-time-series-chart-content"],[1,"tb-time-series-chart-shape"],["class","tb-time-series-chart-legend",4,"ngIf"],[4,"ngTemplateOutlet"],[1,"tb-time-series-chart-legend"],[1,"tb-time-series-chart-legend-table","vertical"],["class","tb-time-series-chart-legend-type-label right legend legend-row-color",4,"ngIf"],[4,"ngFor","ngForOf"],[1,"tb-time-series-chart-legend-type-label","right","legend","legend-row-color"],[4,"ngTemplateOutlet","ngTemplateOutletContext"],["class","tb-time-series-chart-legend-value legend",3,"innerHTML",4,"ngIf"],[1,"tb-time-series-chart-legend-value","legend",3,"innerHTML"],[1,"tb-time-series-chart-legend-item"],[1,"tb-time-series-chart-legend-item-label"],[1,"tb-time-series-chart-legend-item-label-circle"],[1,"legend","legend-label-color",3,"innerHTML"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",2),t.ɵɵelement(1,"div",3),t.ɵɵtemplate(2,JU,2,1,"ng-container",4),t.ɵɵelementStart(3,"div",5),t.ɵɵelement(4,"div",6,0),t.ɵɵtemplate(6,uj,12,6,"div",7)(7,mj,5,10,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.widgetComponent.dashboardWidget.showWidgetTitlePanel),t.ɵɵadvance(),t.ɵɵclassMap(n.legendClass),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.showLegend))},dependencies:t.ɵɵgetComponentDepsFactory(hj,[se,_]),styles:['@charset "UTF-8";.tb-time-series-chart-panel[_ngcontent-%COMP%]{width:100%;height:100%;position:relative;display:flex;flex-direction:column;gap:8px;padding:12px}.tb-time-series-chart-panel[_ngcontent-%COMP%] > div[_ngcontent-%COMP%]:not(.tb-time-series-chart-overlay){z-index:1}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-overlay[_ngcontent-%COMP%]{position:absolute;inset:12px}.tb-time-series-chart-panel[_ngcontent-%COMP%] div.tb-widget-title[_ngcontent-%COMP%]{padding:0}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%]{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;gap:8px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%]{flex-direction:column-reverse}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-right[_ngcontent-%COMP%]{flex-direction:row}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-left[_ngcontent-%COMP%]{flex-direction:row-reverse}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-shape[_ngcontent-%COMP%]{flex:1;min-width:0;min-height:0;display:flex;align-items:center;justify-content:center}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-right[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-left[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]{display:inline-grid;grid-auto-flow:column;grid-template-rows:repeat(auto-fit,minmax(16px,min-content));max-width:calc(25% - 8px);height:fit-content;max-height:100%}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-bottom[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]{align-self:center}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%] .tb-time-series-chart-legend.tb-simple-legend[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-bottom[_ngcontent-%COMP%] .tb-time-series-chart-legend.tb-simple-legend[_ngcontent-%COMP%]{justify-content:center}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]:not(.tb-simple-legend), .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-bottom[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]:not(.tb-simple-legend){width:100%}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]{display:flex;align-items:flex-start;align-self:stretch;column-gap:16px;row-gap:8px;flex-wrap:wrap;overflow:auto;width:fit-content;max-width:100%;max-height:calc(35% - 8px)}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%]{border-spacing:0;table-layout:fixed}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table.vertical[_ngcontent-%COMP%]{width:100%;table-layout:auto}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table.vertical[_ngcontent-%COMP%] tbody[_ngcontent-%COMP%] th[_ngcontent-%COMP%]{width:95%}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] th[_ngcontent-%COMP%]:not(:last-child), .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] td[_ngcontent-%COMP%]:not(:last-child){padding-right:16px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] thead[_ngcontent-%COMP%] tr[_ngcontent-%COMP%] th[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] thead[_ngcontent-%COMP%] tr[_ngcontent-%COMP%] td[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] tbody[_ngcontent-%COMP%] tr[_ngcontent-%COMP%]:not(:last-child) th[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] tbody[_ngcontent-%COMP%] tr[_ngcontent-%COMP%]:not(:last-child) td[_ngcontent-%COMP%]{padding-bottom:8px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%]{align-items:flex-end}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] .tb-time-series-chart-legend-item.left[_ngcontent-%COMP%]{align-items:flex-start}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%]{display:flex;flex-direction:column;justify-content:center;align-items:flex-start;-webkit-user-select:none;user-select:none}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%] .tb-time-series-chart-legend-item-label[_ngcontent-%COMP%]{display:flex;align-items:center;gap:4px;color:#ccc;white-space:nowrap;cursor:pointer}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%] .tb-time-series-chart-legend-item-label[_ngcontent-%COMP%] .tb-time-series-chart-legend-item-label-circle[_ngcontent-%COMP%]{width:8px;height:8px;border-radius:50%;background-color:#ccc}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-type-label[_ngcontent-%COMP%]{white-space:nowrap;text-align:left}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-type-label.right[_ngcontent-%COMP%]{text-align:right}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-value[_ngcontent-%COMP%]{white-space:nowrap;text-align:right}.tb-time-series-chart-panel[_ngcontent-%COMP%] .legend[_ngcontent-%COMP%]{font-size:12px;font-style:normal;font-weight:500;letter-spacing:normal;line-height:16px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .legend.legend-row-color[_ngcontent-%COMP%]{color:#00000061}.tb-time-series-chart-panel[_ngcontent-%COMP%] .legend.legend-label-color[_ngcontent-%COMP%]{color:#000}']})}}const gj=["statisticChart"];function fj(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",12),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.openEditCommandDialog())})),t.ɵɵelementStart(2,"mat-icon",13),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",12),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.onDeleteClick())})),t.ɵɵelementStart(6,"mat-icon",13),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function yj(e,n){if(1&e&&t.ɵɵelement(0,"tb-gateway-statistics-chart",14,0),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("ctx",e.ctx)}}function vj(e,n){if(1&e&&t.ɵɵelement(0,"tb-custom-statistics-table",15),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("data",e.subscriptionData)}}function xj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10),t.ɵɵtemplate(1,yj,2,1,"tb-gateway-statistics-chart",14)(2,vj,1,1,"tb-custom-statistics-table",15),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵconditional(e.isNumericData?1:2)}}function bj(e,n){1&e&&(t.ɵɵelementStart(0,"div",11),t.ɵɵelement(1,"div",16),t.ɵɵelementStart(2,"div",17),t.ɵɵtext(3,"attribute.no-telemetry-text"),t.ɵɵelementEnd()())}class wj{constructor(e,t,n,i,a,r){this.fb=e,this.attributeService=t,this.destroyRef=n,this.dialog=i,this.dialogService=a,this.utils=r,this.subscriptionData=[],this.statisticForm=this.fb.group({command:[]}),this.isNumericData=!1,this.commands=[],this.subscribed=!1,this.dataTypeDefined=!1,this.subscriptionOptions={callbacks:{onDataUpdated:()=>this.ctx.ngZone.run((()=>{this.isDataOnlyNumbers(),this.isNumericData&&this.statisticChart?.onDataUpdated()})),onDataUpdateError:(e,t)=>this.ctx.ngZone.run((()=>{this.onDataUpdateError(t)}))},useDashboardTimewindow:!1,legendConfig:F(R.timeseries)},this.statisticForm.get("command").valueChanges.pipe(In()).subscribe((e=>{this.subscribed=!1,this.subscriptionInfo&&e?.attributeOnGateway&&this.createSubscription(this.ctx.defaultSubscription.datasources[0].entity,e.attributeOnGateway)}))}ngAfterViewInit(){if(this.ctx.defaultSubscription.datasources.length){const e=this.ctx.defaultSubscription.datasources[0].entity;if(e.id.id===B)return;this.getGatewayGeneralConfig().pipe(In(this.destroyRef)).subscribe((t=>{this.commands=t?.statistics.commands.reverse()??[],this.commands.length&&(this.statisticForm.get("command").setValue(this.commands[0]),this.createSubscription(e,this.commands[0].attributeOnGateway))}))}}openEditCommandDialog(){const e=this.statisticForm.get("command").value,t="string"==typeof e||!e,n="string"==typeof e?{attributeOnGateway:e}:e;let i;this.dialog.open(cr,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{titleText:t?"gateway.statistics.create-command":"gateway.statistics.edit-command",buttonText:t?"action.add":"action.apply",command:n,existingCommands:this.commands.map((e=>e.attributeOnGateway))}}).afterClosed().pipe(Ve((e=>Te(this.getGatewayGeneralConfig(),_e(e)))),Ve((([e,t])=>(this.commands=[...e?.statistics.commands.filter((e=>e.attributeOnGateway!==t?.prev?.attributeOnGateway))??[],...t?.current?[{...t.current}]:[]],i=t?.current,this.updateStatisticsCommands(e,this.commands)))),In(this.destroyRef)).subscribe((()=>{i&&this.statisticForm.get("command").patchValue(i)}))}onDeleteClick(){const e=this.statisticForm.get("command").value.attributeOnGateway;this.dialogService.confirm(this.ctx.translate.instant("gateway.statistics.delete-command",{command:e}),this.ctx.translate.instant("gateway.statistics.delete-command-data"),this.ctx.translate.instant("action.cancel"),this.ctx.translate.instant("action.confirm")).pipe(Le(Boolean),Ve((()=>this.getGatewayGeneralConfig())),Ve((t=>(this.commands=[...t.statistics.commands.filter((t=>t.attributeOnGateway!==e))],this.updateStatisticsCommands(t,this.commands)))),In(this.destroyRef)).subscribe()}getGatewayGeneralConfig(){const e=this.ctx.defaultSubscription.datasources[0].entity;return e.id.id===B?_e(null):this.attributeService.getEntityAttributes(e.id,D.SHARED_SCOPE,["general_configuration"]).pipe(ke((e=>e[0]?.value)))}updateStatisticsCommands(e,t){const n=this.ctx.defaultSubscription.datasources[0].entity;return n.id.id!==B&&e?this.attributeService.saveEntityAttributes(n.id,D.SHARED_SCOPE,[{key:"general_configuration",value:{...e,statistics:{...e.statistics,commands:t}}}]):_e(null)}createSubscription(e,t){const n=[{type:N.entity,entityType:L.DEVICE,entityId:e.id.id,entityName:e.name,timeseries:[]}];n[0].timeseries=[{name:t,label:t,settings:{}}],this.subscriptionInfo=n,this.changeSubscription(n)}onDataUpdateError(e){const t=this.utils.parseException(e);let n=t.name;t.message&&(n+=": "+t.message),console.error(n)}isDataOnlyNumbers(){this.subscriptionData=this.ctx.defaultSubscription.data[0]?.data??[],this.subscriptionData.length&&!this.dataTypeDefined&&(this.isNumericData=this.subscriptionData.every((e=>!isNaN(+e[1]))),this.dataTypeDefined=!0),this.ctx.detectChanges()}changeSubscription(e){this.ctx.defaultSubscription?.unsubscribe(),this.ctx.datasources[0].entity&&this.ctx.subscriptionApi.createSubscriptionFromInfo(R.timeseries,e,this.subscriptionOptions,!1,!0).pipe(In(this.destroyRef)).subscribe((e=>{this.dataTypeDefined=!1,this.ctx.defaultSubscription=e,this.ctx.settings.showLegend=!1,this.ctx.data=e.data,this.ctx.datasources=e.datasources,this.isDataOnlyNumbers(),this.subscribed=!0}))}static{this.ɵfac=function(e){return new(e||wj)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(K.AttributeService),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(K.DialogService),t.ɵɵdirectiveInject(K.UtilsService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:wj,selectors:[["tb-gateway-statistics"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(gj,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.statisticChart=e.first)}},inputs:{ctx:"ctx"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:18,vars:13,consts:[["statisticChart",""],[1,"flex","flex-1","flex-col","max-h-full"],[1,"entry-container","flex","items-center"],[1,"tb-form-panel","stroked","w-full","flex-1",3,"formGroup"],["translate","",1,"tb-form-panel-title"],[1,"entry-container","flex","w-full","gap-2"],["formControlName","command",1,"flex-1",3,"onCreateNewClicked","commands"],["appearance","outline",1,"flex-1"],["matInput","","disabled","",3,"tbTruncateWithTooltip","value"],[1,"actions-container","flex","flex-col","p-2","min-w-16"],[1,"chart-box","flex","flex-1","flex-col","overflow-auto"],[1,"tb-no-data-available","h-full"],["type","button","matSuffix","","mat-icon-button","","aria-label","Edit","matTooltipPosition","above",1,"action-button",3,"click","matTooltip"],[1,"material-icons"],[1,"flex-1",3,"ctx"],[1,"h-full","flex-1",3,"data"],[1,"tb-no-data-bg"],["translate","",1,"tb-no-data-text"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"div",2)(2,"div",3)(3,"div",4),t.ɵɵtext(4,"gateway.statistics.entry"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",5)(6,"tb-statistics-commands-autocomplete",6),t.ɵɵlistener("onCreateNewClicked",(function(){return n.openEditCommandDialog()})),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-form-field",7)(8,"mat-label"),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(11,"input",8),t.ɵɵpipe(12,"translate"),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(14,"div",9),t.ɵɵtemplate(15,fj,8,6),t.ɵɵelementEnd()(),t.ɵɵtemplate(16,xj,3,1,"div",10)(17,bj,4,0,"div",11),t.ɵɵelementEnd()),2&e){let e,i,a,r;t.ɵɵadvance(2),t.ɵɵproperty("formGroup",n.statisticForm),t.ɵɵadvance(4),t.ɵɵproperty("commands",n.commands),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(10,7,"gateway.statistics.command")),t.ɵɵadvance(2),t.ɵɵproperty("tbTruncateWithTooltip",null!==(e=null==(e=n.statisticForm.get("command").value)?null:e.command)&&void 0!==e?e:t.ɵɵpipeBind1(12,9,"gateway.statistics.no-config-commands-found"))("value",null!==(i=null==(i=n.statisticForm.get("command").value)?null:i.command)&&void 0!==i?i:t.ɵɵpipeBind1(13,11,"gateway.statistics.no-config-commands-found")),t.ɵɵadvance(4),t.ɵɵconditional(null!=(a=n.statisticForm.get("command").value)&&a.attributeOnGateway?15:-1),t.ɵɵadvance(),t.ɵɵconditional(null!=(r=n.statisticForm.get("command").value)&&r.attributeOnGateway&&n.subscriptionData.length&&n.subscribed?16:17)}},dependencies:t.ɵɵgetComponentDepsFactory(wj,[se,_,Nn,Hn,hj]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;padding:4px;display:flex;flex-direction:column}[_nghost-%COMP%] .action-button[_ngcontent-%COMP%]{opacity:.7}@media screen and (max-width: 599px){[_nghost-%COMP%] .entry-container[_ngcontent-%COMP%]{flex-direction:column}[_nghost-%COMP%] .actions-container[_ngcontent-%COMP%]{flex-direction:row}}']})}}var Sj;e("GatewayStatisticsComponent",wj),e("BACnetRequestTypes",Sj),function(e){e.WriteProperty="writeProperty",e.ReadProperty="readProperty"}(Sj||e("BACnetRequestTypes",Sj={}));const Cj=e("BACnetRequestTypesTranslates",new Map([[Sj.WriteProperty,"gateway.rpc.write-property"],[Sj.ReadProperty,"gateway.rpc.read-property"]]));var _j;e("BACnetObjectTypes",_j),function(e){e.BinaryInput="binaryInput",e.BinaryOutput="binaryOutput",e.AnalogInput="analogInput",e.AnalogOutput="analogOutput",e.BinaryValue="binaryValue",e.AnalogValue="analogValue"}(_j||e("BACnetObjectTypes",_j={}));const Tj=e("BACnetObjectTypesTranslates",new Map([[_j.AnalogOutput,"gateway.rpc.analog-output"],[_j.AnalogInput,"gateway.rpc.analog-input"],[_j.BinaryOutput,"gateway.rpc.binary-output"],[_j.BinaryInput,"gateway.rpc.binary-input"],[_j.BinaryValue,"gateway.rpc.binary-value"],[_j.AnalogValue,"gateway.rpc.analog-value"]]));var Ij;e("BLEMethods",Ij),function(e){e.WRITE="write",e.READ="read",e.SCAN="scan"}(Ij||e("BLEMethods",Ij={}));const Ej=e("BLEMethodsTranslates",new Map([[Ij.WRITE,"gateway.rpc.write"],[Ij.READ,"gateway.rpc.read"],[Ij.SCAN,"gateway.rpc.scan"]]));var Mj,kj;e("CANByteOrders",Mj),function(e){e.LITTLE="LITTLE",e.BIG="BIG"}(Mj||e("CANByteOrders",Mj={})),e("SocketMethodProcessings",kj),function(e){e.WRITE="write",e.READ="read"}(kj||e("SocketMethodProcessings",kj={}));const Pj=e("SocketMethodProcessingsTranslates",new Map([[kj.WRITE,"gateway.rpc.write"],[kj.READ,"gateway.rpc.read"]]));var Dj;e("SNMPMethods",Dj),function(e){e.SET="set",e.MULTISET="multiset",e.GET="get",e.BULKWALK="bulkwalk",e.TABLE="table",e.MULTIGET="multiget",e.GETNEXT="getnext",e.BULKGET="bulkget",e.WALKS="walk"}(Dj||e("SNMPMethods",Dj={}));const Oj=e("SNMPMethodsTranslations",new Map([[Dj.SET,"gateway.rpc.set"],[Dj.MULTISET,"gateway.rpc.multiset"],[Dj.GET,"gateway.rpc.get"],[Dj.BULKWALK,"gateway.rpc.bulk-walk"],[Dj.TABLE,"gateway.rpc.table"],[Dj.MULTIGET,"gateway.rpc.multi-get"],[Dj.GETNEXT,"gateway.rpc.get-next"],[Dj.BULKGET,"gateway.rpc.bulk-get"],[Dj.WALKS,"gateway.rpc.walk"]]));var Aj,Fj;e("SocketEncodings",Aj),function(e){e.UTF_8="utf-8"}(Aj||e("SocketEncodings",Aj={})),e("RestSecurityType",Fj),function(e){e.ANONYMOUS="anonymous",e.BASIC="basic"}(Fj||e("RestSecurityType",Fj={}));const Rj=e("RestSecurityTypeTranslationsMap",new Map([[Fj.ANONYMOUS,"gateway.broker.security-types.anonymous"],[Fj.BASIC,"gateway.broker.security-types.basic"]]));class Bj{transform(e){return e.map((e=>(e?.value??e).toString())).join(", ")}static{this.ɵfac=function(e){return new(e||Bj)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"getRpcTemplateArrayView",type:Bj,pure:!0,standalone:!0})}}e("RpcTemplateArrayViewPipe",Bj);class Nj{constructor(){this.differs=i(f),this.keyValues=[]}transform(e){if(!e||!(e instanceof Map)&&"object"!=typeof e)return null;this.differ??=this.differs.find(e).create();const t=this.differ.diff(e);return t&&(this.keyValues=[],t.forEachItem((e=>{J(e.currentValue)&&this.keyValues.push(this.makeKeyValuePair(e.key,e.currentValue))}))),this.keyValues}makeKeyValuePair(e,t){return{key:e,value:t}}static{this.ɵfac=function(e){return new(e||Nj)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"keyValueIsNotEmpty",type:Nj,pure:!1,standalone:!0})}}e("KeyValueIsNotEmptyPipe",Nj);const Lj=e=>({$implicit:e,innerValue:!1}),Vj=e=>({"padding-left":e}),qj=(e,t)=>({"boolean-true":e,"boolean-false":t}),Gj=e=>({$implicit:e,innerValue:!0});function zj(e,n){if(1&e&&t.ɵɵelementContainer(0,13),2&e){const e=n.$implicit;t.ɵɵnextContext();const i=t.ɵɵreference(15);t.ɵɵproperty("ngTemplateOutlet",i)("ngTemplateOutletContext",t.ɵɵpureFunction1(2,Lj,e))}}function Uj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",19),t.ɵɵtext(1),t.ɵɵpipe(2,"getRpcTemplateArrayView"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,e.value)," ")}}function jj(e,n){if(1&e&&t.ɵɵelementContainer(0,20),2&e){t.ɵɵnextContext();const e=t.ɵɵreference(12);t.ɵɵproperty("ngTemplateOutlet",e)}}function Hj(e,n){if(1&e&&t.ɵɵelementContainer(0,20),2&e){t.ɵɵnextContext(2);const e=t.ɵɵreference(10);t.ɵɵproperty("ngTemplateOutlet",e)}}function Wj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div"),t.ɵɵtemplate(1,Hj,1,1,"ng-container",21),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵreference(8),i=t.ɵɵnextContext().$implicit,a=t.ɵɵnextContext();t.ɵɵclassMap(t.ɵɵpureFunction2(4,qj,!0===e.value,!1===e.value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",i.type===a.ConnectorType.SNMP&&"method"===e.key)("ngIfElse",n)}}function $j(e,n){if(1&e&&t.ɵɵtext(0),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵtextInterpolate(e.value)}}function Kj(e,n){if(1&e&&(t.ɵɵtext(0),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵtextInterpolate(t.ɵɵpipeBind1(1,1,n.SNMPMethodsTranslations.get(e.value)))}}function Yj(e,n){if(1&e&&t.ɵɵelementContainer(0,13),2&e){const e=n.$implicit;t.ɵɵnextContext(3);const i=t.ɵɵreference(15);t.ɵɵproperty("ngTemplateOutlet",i)("ngTemplateOutletContext",t.ɵɵpureFunction1(2,Gj,e))}}function Xj(e,n){if(1&e&&(t.ɵɵtemplate(0,Yj,1,4,"ng-container",12),t.ɵɵpipe(1,"keyvalue")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("ngForOf",t.ɵɵpipeBind2(1,1,e.value,n.originalOrder))}}function Zj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",14)(1,"div",15),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(4,Uj,3,3,"div",16)(5,jj,1,1,"ng-container",17)(6,Wj,2,7,"div",18)(7,$j,1,1,"ng-template",null,1,t.ɵɵtemplateRefExtractor)(9,Kj,2,3,"ng-template",null,2,t.ɵɵtemplateRefExtractor)(11,Xj,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=n.innerValue,a=t.ɵɵnextContext(2);t.ɵɵstyleMap(t.ɵɵpureFunction1(10,Vj,i?"16px":"0")),t.ɵɵclassMap(a.getRpcParamsRowClasses(e.value)),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",i?e.key:t.ɵɵpipeBind1(3,8,"gateway.rpc."+e.key)," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",a.isArray(e.value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",a.isObject(e.value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!a.isObject(e.value)&&!a.isArray(e.value))}}function Qj(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-expansion-panel",6)(1,"mat-expansion-panel-header")(2,"mat-panel-title",7)(3,"span",8),t.ɵɵtext(4),t.ɵɵelementEnd()(),t.ɵɵelementStart(5,"mat-panel-description")(6,"button",9),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteTemplate(n,i))})),t.ɵɵelementStart(7,"mat-icon",10),t.ɵɵtext(8,"delete"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",11),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.applyTemplate(n,i))})),t.ɵɵelementStart(10,"mat-icon",10),t.ɵɵtext(11,"play_arrow"),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(12,zj,1,4,"ng-container",12),t.ɵɵpipe(13,"keyValueIsNotEmpty"),t.ɵɵtemplate(14,Zj,13,12,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()}if(2&e){const e=n.$implicit;t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",e.name),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.name),t.ɵɵadvance(8),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(13,3,e.config))}}class Jj{constructor(e){this.attributeService=e,this.saveTemplate=new u,this.useTemplate=new u,this.ConnectorType=dt,this.originalOrder=()=>0,this.isObject=e=>H(e),this.isArray=e=>Array.isArray(e),this.SNMPMethodsTranslations=Oj}applyTemplate(e,t){e.stopPropagation(),this.useTemplate.emit(t)}deleteTemplate(e,t){e.stopPropagation();const n=this.rpcTemplates.findIndex((e=>e.name==t.name));this.rpcTemplates.splice(n,1);const i=`${this.connectorType}_template`;this.attributeService.saveEntityAttributes({id:this.ctx.defaultSubscription.targetDeviceId,entityType:L.DEVICE},D.SERVER_SCOPE,[{key:i,value:this.rpcTemplates}]).subscribe((()=>{}))}getRpcParamsRowClasses(e){return this.isObject(e)?"flex-col":"flex-row justify-between items-center"}static{this.ɵfac=function(e){return new(e||Jj)(t.ɵɵdirectiveInject(K.AttributeService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Jj,selectors:[["tb-gateway-service-rpc-connector-templates"]],inputs:{connectorType:"connectorType",ctx:"ctx",rpcTemplates:"rpcTemplates"},outputs:{saveTemplate:"saveTemplate",useTemplate:"useTemplate"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:4,vars:4,consts:[["RPCTemplateRef",""],["value",""],["SNMPMethod",""],["RPCObjectRow",""],[1,"mat-subtitle-1","title"],["hideToggle","",4,"ngFor","ngForOf"],["hideToggle",""],[1,"template-name"],["matTooltipPosition","above",3,"matTooltip"],["mat-icon-button","","matTooltip","Delete",3,"click"],[1,"material-icons"],["mat-icon-button","","matTooltip","Use",3,"click"],[3,"ngTemplateOutlet","ngTemplateOutletContext",4,"ngFor","ngForOf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"rpc-params-row","flex"],[1,"template-key"],["tbTruncateWithTooltip","","class","array-value",4,"ngIf"],[3,"ngTemplateOutlet",4,"ngIf"],[3,"class",4,"ngIf"],["tbTruncateWithTooltip","",1,"array-value"],[3,"ngTemplateOutlet"],[3,"ngTemplateOutlet",4,"ngIf","ngIfElse"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",4),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(3,Qj,16,5,"mat-expansion-panel",5)),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,"gateway.rpc.templates-title")),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",n.rpcTemplates))},dependencies:t.ɵɵgetComponentDepsFactory(Jj,[se,_,Bj,Nj]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex;flex-direction:column;padding:0}[_nghost-%COMP%] .title[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .template-key[_ngcontent-%COMP%]{color:#00000061;height:32px;line-height:32px}[_nghost-%COMP%] .boolean-true[_ngcontent-%COMP%], [_nghost-%COMP%] .boolean-false[_ngcontent-%COMP%]{border-radius:3px;height:32px;line-height:32px;padding:0 12px;width:fit-content;font-size:14px;text-transform:capitalize}[_nghost-%COMP%] .boolean-false[_ngcontent-%COMP%]{color:#d12730;background-color:#d1273014}[_nghost-%COMP%] .boolean-true[_ngcontent-%COMP%]{color:#198038;background-color:#19803814}[_nghost-%COMP%] mat-expansion-panel[_ngcontent-%COMP%]{margin-top:10px;overflow:visible}[_nghost-%COMP%] .mat-expansion-panel-header-description[_ngcontent-%COMP%]{flex-direction:row-reverse;align-items:center;margin-right:0;flex:0}[_nghost-%COMP%] .mat-expansion-panel-header-description[_ngcontent-%COMP%] > mat-icon[_ngcontent-%COMP%]{margin-left:15px;color:#00000061}[_nghost-%COMP%] .mat-expansion-panel-header[_ngcontent-%COMP%]{padding:0 0 0 12px}[_nghost-%COMP%] .mat-expansion-panel-header.mat-expansion-panel-header.mat-expanded[_ngcontent-%COMP%]{height:48px}[_nghost-%COMP%] .mat-expansion-panel-header[_ngcontent-%COMP%] .mat-content.mat-content-hide-toggle[_ngcontent-%COMP%]{margin-right:0}[_nghost-%COMP%] .rpc-params-row[_ngcontent-%COMP%]{overflow:hidden;white-space:nowrap}[_nghost-%COMP%] .rpc-params-row[_ngcontent-%COMP%] [_ngcontent-%COMP%]:not(:first-child){white-space:pre;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .template-name[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis;display:block}[_nghost-%COMP%] .mat-content{align-items:center}[_nghost-%COMP%] .mat-expansion-panel-header-title[_ngcontent-%COMP%]{flex:1;margin:0}[_nghost-%COMP%] .array-value[_ngcontent-%COMP%]{margin-left:10px}']})}}function eH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rpc.template-name-required")," "))}function tH(e,n){1&e&&(t.ɵɵelementStart(0,"div",12),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rpc.template-name-duplicate")," "))}e("GatewayServiceRPCConnectorTemplatesComponent",Jj);class nH extends A{constructor(e,t,n,i,a){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.config=this.data.config,this.templates=this.data.templates,this.templateNameCtrl=this.fb.control("",[me.required])}validateDuplicateName(e){const t=e.value.trim();return!!this.templates.find((e=>e.name===t))}close(){this.dialogRef.close()}save(){this.templateNameCtrl.setValue(this.templateNameCtrl.value.trim()),this.templateNameCtrl.valid&&this.dialogRef.close(this.templateNameCtrl.value)}static{this.ɵfac=function(e){return new(e||nH)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:nH,selectors:[["tb-gateway-service-rpc-connector-template-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:20,vars:10,consts:[["color","primary",1,"justify-between"],["translate",""],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"mat-content","flex","flex-col",2,"width","600px"],[1,"mat-block","tb-value-type",2,"flex-grow","0"],["matInput","","required","",3,"formControl"],[4,"ngIf"],["class","mat-mdc-form-field-error","style","margin-top: -15px; padding-left: 10px; font-size: 14px;",4,"ngIf"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"mat-mdc-form-field-error",2,"margin-top","-15px","padding-left","10px","font-size","14px"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-toolbar",0)(1,"h2",1),t.ɵɵtext(2,"gateway.rpc.save-template"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"button",2),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵelementStart(4,"mat-icon",3),t.ɵɵtext(5,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(6,"div",4)(7,"mat-form-field",5)(8,"mat-label",1),t.ɵɵtext(9,"gateway.rpc.template-name"),t.ɵɵelementEnd(),t.ɵɵelement(10,"input",6),t.ɵɵtemplate(11,eH,3,3,"mat-error",7),t.ɵɵelementEnd(),t.ɵɵtemplate(12,tH,3,3,"div",8),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",9)(14,"button",10),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵlistener("click",(function(){return n.save()})),t.ɵɵtext(18),t.ɵɵpipe(19,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(10),t.ɵɵproperty("formControl",n.templateNameCtrl),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.templateNameCtrl.hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.validateDuplicateName(n.templateNameCtrl)),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(16,6,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",!n.templateNameCtrl.valid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(19,8,"action.save")," "))},dependencies:t.ɵɵgetComponentDepsFactory(nH,[se,_]),encapsulation:2})}}function iH(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",6),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SecurityTypeTranslationsMap.get(e))," ")}}function aH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.username-required"))}function rH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.password-required"))}function oH(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",7)(2,"div",8),t.ɵɵtext(3,"gateway.username"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",9)(5,"mat-form-field",10),t.ɵɵelement(6,"input",11),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,aH,3,3,"mat-icon",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",7)(10,"div",8),t.ɵɵtext(11,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"div",9)(13,"mat-form-field",10),t.ɵɵelement(14,"input",13),t.ɵɵpipe(15,"translate"),t.ɵɵtemplate(16,rH,3,3,"mat-icon",12),t.ɵɵelementStart(17,"div",14),t.ɵɵelement(18,"tb-toggle-password",15),t.ɵɵelementEnd()()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,6,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("username").hasError("required")&&e.securityFormGroup.get("username").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("password").hasError("required")&&e.securityFormGroup.get("password").touched),t.ɵɵadvance(),t.ɵɵclassProp("hide-toggle",e.securityFormGroup.get("password").hasError("required"))}}e("GatewayServiceRPCConnectorTemplateDialogComponent",nH);class sH{constructor(e){this.fb=e,this.BrokerSecurityType=Fj,this.securityTypes=Object.values(Fj),this.SecurityTypeTranslationsMap=Rj,this.destroy$=new we,this.propagateChange=e=>{},this.securityFormGroup=this.fb.group({type:[Fj.ANONYMOUS,[]],username:["",[me.required,me.pattern(pn)]],password:["",[me.required,me.pattern(pn)]]}),this.observeSecurityForm()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}writeValue(e){e.type||(e.type=Fj.ANONYMOUS),this.securityFormGroup.reset(e),this.updateView(e)}validate(){return this.securityFormGroup.valid?null:{securityForm:{valid:!1}}}updateView(e){this.propagateChange(e)}updateValidators(e){e===Fj.BASIC?(this.securityFormGroup.get("username").enable({emitEvent:!1}),this.securityFormGroup.get("password").enable({emitEvent:!1})):(this.securityFormGroup.get("username").disable({emitEvent:!1}),this.securityFormGroup.get("password").disable({emitEvent:!1}))}observeSecurityForm(){this.securityFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.updateView(e))),this.securityFormGroup.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.updateValidators(e)))}static{this.ɵfac=function(e){return new(e||sH)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:sH,selectors:[["tb-rest-connector-security"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>sH)),multi:!0},{provide:he,useExisting:c((()=>sH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:7,vars:3,consts:[[1,"tb-form-row","space-between","same-padding","tb-flex","column",3,"formGroup"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fields-label"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],[4,"ngIf"],[3,"value"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["translate","",1,"fixed-title-width"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","username",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","password","name","value","formControlName","password",3,"placeholder"],["matSuffix","",1,"tb-flex","no-gap","align-center","fill-height"],[1,"tb-flex","align-center","fill-height"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.security"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"tb-toggle-select",3),t.ɵɵtemplate(5,iH,3,4,"tb-toggle-option",4),t.ɵɵelementEnd()(),t.ɵɵtemplate(6,oH,19,10,"ng-container",5),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.securityFormGroup),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.securityTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.securityFormGroup.get("type").value===n.BrokerSecurityType.BASIC))},dependencies:t.ɵɵgetComponentDepsFactory(sH,[_,se]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block;margin-bottom:10px}[_nghost-%COMP%] .fields-label[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .hide-toggle[_ngcontent-%COMP%]{display:none}'],changeDetection:d.OnPush})}}e("RestConnectorSecurityComponent",sH);const lH=e=>({type:e});function pH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.bACnetRequestTypesTranslates.get(e))," ")}}function cH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.bACnetObjectTypesTranslates.get(e))," ")}}function dH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",9),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",10)(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-select",11),t.ɵɵtemplate(10,pH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field")(12,"mat-label"),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",13),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",14)(17,"mat-form-field",15)(18,"mat-label"),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-select",16),t.ɵɵtemplate(22,cH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(23,"mat-form-field",15)(24,"mat-label"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",17),t.ɵɵelementEnd()(),t.ɵɵelementStart(28,"mat-form-field",10)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",18),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,8,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,10,"gateway.rpc.requestType")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.bACnetRequestTypes),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,12,"gateway.rpc.requestTimeout")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,14,"gateway.rpc.objectType")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.bACnetObjectTypes),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(26,16,"gateway.rpc.identifier")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,18,"gateway.rpc.propertyId"))}}function uH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.bLEMethodsTranslates.get(e))," ")}}function mH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",20),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",21),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-form-field",10)(11,"mat-label"),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"mat-select",22),t.ɵɵtemplate(15,uH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(16,"mat-slide-toggle",23),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,5,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,7,"gateway.rpc.characteristicUUID")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,9,"gateway.rpc.methodProcessing")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.bLEMethods),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,11,"gateway.rpc.withResponse")," ")}}function hH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e)," ")}}function gH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",24),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",25),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-slide-toggle",26),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-slide-toggle",27),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"mat-slide-toggle",28),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",14)(20,"mat-form-field",15)(21,"mat-label"),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(24,"input",29),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",15)(26,"mat-label"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"mat-select",30),t.ɵɵtemplate(30,hH,3,4,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(31,"div",14)(32,"mat-form-field",15)(33,"mat-label"),t.ɵɵtext(34),t.ɵɵpipe(35,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(36,"input",31),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"mat-form-field",15)(38,"mat-label"),t.ɵɵtext(39),t.ɵɵpipe(40,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(41,"input",32),t.ɵɵelementEnd()(),t.ɵɵelementStart(42,"mat-form-field")(43,"mat-label"),t.ɵɵtext(44),t.ɵɵpipe(45,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(46,"input",33),t.ɵɵelementEnd(),t.ɵɵelementStart(47,"mat-form-field")(48,"mat-label"),t.ɵɵtext(49),t.ɵɵpipe(50,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(51,"input",34),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,12,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,14,"gateway.rpc.nodeID")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,16,"gateway.rpc.isExtendedID")," "),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,18,"gateway.rpc.isFD")," "),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,20,"gateway.rpc.bitrateSwitch")," "),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(23,22,"gateway.rpc.dataLength")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(28,24,"gateway.rpc.dataByteorder")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.cANByteOrders),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(35,26,"gateway.rpc.dataBefore")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(40,28,"gateway.rpc.dataAfter")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(45,30,"gateway.rpc.dataInHEX")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(50,32,"gateway.rpc.dataExpression"))}}function fH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",35),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",36),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,2,"gateway.rpc.methodFilter")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,4,"gateway.rpc.valueExpression")))}function yH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",37),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",36),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-slide-toggle",38),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,3,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,5,"gateway.rpc.valueExpression")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,7,"gateway.rpc.withResponse")," "))}function vH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",37),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",36),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-slide-toggle",38),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,3,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,5,"gateway.rpc.valueExpression")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,7,"gateway.rpc.withResponse")," "))}function xH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SNMPMethodsTranslations.get(e))," ")}}function bH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",45)(1,"mat-form-field",46),t.ɵɵelement(2,"input",47),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-icon",48),t.ɵɵpipe(4,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext(3);return t.ɵɵresetView(i.removeSNMPoid(n))})),t.ɵɵtext(5,"delete "),t.ɵɵelementEnd()()}if(2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵproperty("formControl",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(4,2,"gateway.rpc.remove"))}}function wH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",39),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",10)(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-select",40),t.ɵɵtemplate(10,xH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-slide-toggle",38),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"fieldset",41)(15,"span",42),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(18,bH,6,4,"div",43),t.ɵɵelementStart(19,"button",44),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.addSNMPoid())})),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,7,"gateway.rpc.requestFilter")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,9,"gateway.rpc.method")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.sNMPMethods),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,11,"gateway.rpc.withResponse")," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(17,13,"gateway.rpc.oids"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",e.getFormArrayControls("oid")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(21,15,"gateway.rpc.add-oid")," ")}}function SH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function CH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",59),t.ɵɵelementContainerStart(1,63),t.ɵɵelementStart(2,"mat-form-field",64),t.ɵɵelement(3,"input",65),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",64),t.ɵɵelement(5,"input",66),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-icon",67),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext(4);return t.ɵɵresetView(i.removeHTTPHeader(n))})),t.ɵɵtext(8,"delete "),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()}if(2&e){const e=n.index;t.ɵɵadvance(),t.ɵɵproperty("formGroupName",e),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,2,"gateway.rpc.remove"))}}function _H(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",58)(1,"div",59)(2,"span",60),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"span",60),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(8,"span",61),t.ɵɵelementEnd(),t.ɵɵelement(9,"mat-divider"),t.ɵɵtemplate(10,CH,9,4,"div",62),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rpc.header-name")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(7,5,"gateway.rpc.value")),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.getFormArrayControls("httpHeaders"))}}function TH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",49),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",14)(6,"mat-form-field",50)(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-select",51),t.ɵɵtemplate(11,SH,2,2,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",15)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",52),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",14)(18,"mat-form-field",15)(19,"mat-label"),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(22,"input",53),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",15)(24,"mat-label"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",54),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-form-field",15)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",55),t.ɵɵelementEnd()(),t.ɵɵelementStart(33,"mat-form-field")(34,"mat-label"),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(37,"input",36),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"fieldset",56)(39,"span",42),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(42,_H,11,7,"div",57),t.ɵɵelementStart(43,"button",44),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.addHTTPHeader())})),t.ɵɵtext(44),t.ɵɵpipe(45,"translate"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,11,"gateway.rpc.methodFilter")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,13,"gateway.rpc.httpMethod")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.hTTPMethods),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,15,"gateway.rpc.requestUrlExpression")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(21,17,"gateway.rpc.responseTimeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(26,19,"gateway.rpc.timeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,21,"gateway.rpc.tries")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,23,"gateway.rpc.valueExpression")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,25,"gateway.rpc.httpHeaders")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.getFormArrayControls("httpHeaders").length),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(45,27,"gateway.rpc.add-header")," ")}}function IH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function EH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",59),t.ɵɵelementContainerStart(1,63),t.ɵɵelementStart(2,"mat-form-field",64),t.ɵɵelement(3,"input",73),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",64),t.ɵɵelement(6,"input",74),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-icon",67),t.ɵɵpipe(8,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext(4);return t.ɵɵresetView(i.removeHTTPHeader(n))})),t.ɵɵtext(9,"delete "),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()}if(2&e){const e=n.index;t.ɵɵadvance(),t.ɵɵproperty("formGroupName",e),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(4,3,"gateway.rpc.set")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,5,"gateway.rpc.remove"))}}function MH(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",58)(1,"div",59)(2,"span",60),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"span",60),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(8,"span",61),t.ɵɵelementEnd(),t.ɵɵelement(9,"mat-divider"),t.ɵɵtemplate(10,EH,10,7,"div",62),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rpc.header-name")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(7,5,"gateway.rpc.value")),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.getFormArrayControls("httpHeaders"))}}function kH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",68),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",59)(6,"mat-form-field",50)(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-select",51),t.ɵɵtemplate(11,IH,2,2,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",15)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",52),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",59)(18,"mat-form-field",15)(19,"mat-label"),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(22,"input",53),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",15)(24,"mat-label"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",69),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-form-field",15)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",70),t.ɵɵelementEnd()(),t.ɵɵelementStart(33,"mat-form-field")(34,"mat-label"),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(37,"input",71),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"mat-form-field")(39,"mat-label"),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(42,"input",72),t.ɵɵelementEnd(),t.ɵɵelementStart(43,"fieldset",56)(44,"span",42),t.ɵɵtext(45),t.ɵɵpipe(46,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(47,MH,11,7,"div",57),t.ɵɵelementStart(48,"button",44),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.addHTTPHeader())})),t.ɵɵtext(49),t.ɵɵpipe(50,"translate"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,12,"gateway.rpc.methodFilter")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,14,"gateway.rpc.httpMethod")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.hTTPMethods),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,16,"gateway.rpc.requestUrlExpression")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(21,18,"gateway.rpc.responseTimeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(26,20,"gateway.rpc.timeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,22,"gateway.rpc.tries")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,24,"gateway.rpc.requestValueExpression")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,26,"gateway.rpc.responseValueExpression")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(46,28,"gateway.rpc.httpHeaders")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.getFormArrayControls("httpHeaders").length),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(50,30,"gateway.rpc.add-header")," ")}}function PH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rpc.json-value-invalid")," "))}function DH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",75),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",76),t.ɵɵelementStart(10,"mat-icon",77),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext(2);return t.ɵɵresetView(i.openEditJSONDialog(n))})),t.ɵɵtext(12,"edit "),t.ɵɵelementEnd(),t.ɵɵtemplate(13,PH,3,3,"mat-error",78),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,4,"gateway.statistics.command")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,6,"widget-config.datasource-parameters")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,8,"gateway.rpc-command-edit-params")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.commandForm.get("params").hasError("invalidJSON"))}}function OH(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,6),t.ɵɵtemplate(1,dH,33,20,"ng-template",7)(2,mH,19,13,"ng-template",7)(3,gH,52,34,"ng-template",7)(4,fH,10,6,"ng-template",7)(5,yH,13,9,"ng-template",7)(6,vH,13,9,"ng-template",7)(7,wH,22,17,"ng-template",7)(8,TH,46,29,"ng-template",7)(9,kH,51,32,"ng-template",7)(10,DH,14,10,"ng-template",8),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("ngSwitch",e.connectorType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.BACNET),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.BLE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.CAN),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.FTP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.OCPP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.XMPP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.SNMP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.REST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.REQUEST)}}class AH{constructor(e,t){this.fb=e,this.dialog=t,this.sendCommand=new u,this.saveTemplate=new u,this.ConnectorType=dt,this.bACnetRequestTypes=Object.values(Sj),this.bACnetObjectTypes=Object.values(_j),this.bLEMethods=Object.values(Ij),this.cANByteOrders=Object.values(Mj),this.sNMPMethods=Object.values(Dj),this.hTTPMethods=Object.values(gt),this.bACnetRequestTypesTranslates=Cj,this.bACnetObjectTypesTranslates=Tj,this.bLEMethodsTranslates=Ej,this.SNMPMethodsTranslations=Oj,this.gatewayConnectorDefaultTypesTranslates=ut,this.urlPattern=/^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/,this.numbersOnlyPattern=/^[0-9]*$/,this.hexOnlyPattern=/^[0-9A-Fa-f ]+$/,this.propagateChange=e=>{},this.destroy$=new we}ngOnInit(){this.commandForm=this.connectorParamsFormGroupByType(this.connectorType),this.observeFormChanges()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}connectorParamsFormGroupByType(e){let t;switch(e){case dt.BACNET:t=this.fb.group({method:[null,[me.required,me.pattern(pn)]],requestType:[null,[me.required,me.pattern(pn)]],requestTimeout:[null,[me.required,me.min(10),me.pattern(this.numbersOnlyPattern)]],objectType:[null,[]],identifier:[null,[me.required,me.min(1),me.pattern(this.numbersOnlyPattern)]],propertyId:[null,[me.required,me.pattern(pn)]]});break;case dt.BLE:t=this.fb.group({methodRPC:[null,[me.required,me.pattern(pn)]],characteristicUUID:["00002A00-0000-1000-8000-00805F9B34FB",[me.required,me.pattern(pn)]],methodProcessing:[null,[me.required]],withResponse:[!1,[]]});break;case dt.CAN:t=this.fb.group({method:[null,[me.required,me.pattern(pn)]],nodeID:[null,[me.required,me.min(0),me.pattern(this.numbersOnlyPattern)]],isExtendedID:[!1,[]],isFD:[!1,[]],bitrateSwitch:[!1,[]],dataLength:[null,[me.min(1),me.pattern(this.numbersOnlyPattern)]],dataByteorder:[null,[]],dataBefore:[null,[me.pattern(pn),me.pattern(this.hexOnlyPattern)]],dataAfter:[null,[me.pattern(pn),me.pattern(this.hexOnlyPattern)]],dataInHEX:[null,[me.pattern(pn),me.pattern(this.hexOnlyPattern)]],dataExpression:[null,[me.pattern(pn)]]});break;case dt.FTP:t=this.fb.group({methodFilter:[null,[me.required,me.pattern(pn)]],valueExpression:[null,[me.required,me.pattern(pn)]]});break;case dt.OCPP:case dt.XMPP:t=this.fb.group({methodRPC:[null,[me.required,me.pattern(pn)]],valueExpression:[null,[me.required,me.pattern(pn)]],withResponse:[!1,[]]});break;case dt.SNMP:t=this.fb.group({requestFilter:[null,[me.required,me.pattern(pn)]],method:[null,[me.required]],withResponse:[!1,[]],oid:this.fb.array([],[me.required])});break;case dt.REST:t=this.fb.group({methodFilter:[null,[me.required,me.pattern(pn)]],httpMethod:[null,[me.required]],requestUrlExpression:[null,[me.required,me.pattern(this.urlPattern)]],responseTimeout:[null,[me.required,me.min(10),me.pattern(this.numbersOnlyPattern)]],timeout:[null,[me.required,me.min(10),me.pattern(this.numbersOnlyPattern)]],tries:[null,[me.required,me.min(1),me.pattern(this.numbersOnlyPattern)]],valueExpression:[null,[me.required,me.pattern(pn)]],httpHeaders:this.fb.array([]),security:[{},[me.required]]});break;case dt.REQUEST:t=this.fb.group({methodFilter:[null,[me.required,me.pattern(pn)]],httpMethod:[null,[me.required]],requestUrlExpression:[null,[me.required,me.pattern(this.urlPattern)]],responseTimeout:[null,[me.required,me.min(10),me.pattern(this.numbersOnlyPattern)]],timeout:[null,[me.required,me.min(10),me.pattern(this.numbersOnlyPattern)]],tries:[null,[me.required,me.min(1),me.pattern(this.numbersOnlyPattern)]],requestValueExpression:[null,[me.required,me.pattern(pn)]],responseValueExpression:[null,[me.pattern(pn)]],httpHeaders:this.fb.array([])});break;default:t=this.fb.group({command:[null,[me.required,me.pattern(pn)]],params:[{},[lt]]})}return t}addSNMPoid(e=null){const t=this.commandForm.get("oid");t&&t.push(this.fb.control(e,[me.required,me.pattern(pn)]),{emitEvent:!1})}removeSNMPoid(e){this.commandForm.get("oid").removeAt(e)}addHTTPHeader(e={headerName:null,value:null}){const t=this.commandForm.get("httpHeaders"),n=this.fb.group({headerName:[e.headerName,[me.required,me.pattern(pn)]],value:[e.value,[me.required,me.pattern(pn)]]});t&&t.push(n,{emitEvent:!1})}removeHTTPHeader(e){this.commandForm.get("httpHeaders").removeAt(e)}getFormArrayControls(e){return this.commandForm.get(e).controls}openEditJSONDialog(e){e&&e.stopPropagation(),this.dialog.open(nt,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{jsonValue:this.commandForm.get("params").value,required:!0}}).afterClosed().subscribe((e=>{e&&this.commandForm.get("params").setValue(e)}))}save(){this.saveTemplate.emit()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}clearFromArrayByName(e){const t=this.commandForm.get(e);for(;0!==t.length;)t.removeAt(0)}writeValue(e){if("object"==typeof e){switch(e=te(e),this.connectorType){case dt.SNMP:this.clearFromArrayByName("oid"),e.oid.forEach((e=>{this.addSNMPoid(e)})),delete e.oid;break;case dt.REQUEST:case dt.REST:this.clearFromArrayByName("httpHeaders"),e.httpHeaders&&Object.entries(e.httpHeaders).forEach((e=>{this.addHTTPHeader({headerName:e[0],value:e[1]})})),delete e.httpHeaders}this.commandForm.patchValue(e,{onlySelf:!1})}}observeFormChanges(){this.commandForm.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.connectorType!==dt.REST&&this.connectorType!==dt.REQUEST||(e.httpHeaders=e.httpHeaders.reduce(((e,t)=>(e[t.headerName]=t.value,e)),{})),this.commandForm.valid&&this.propagateChange({...this.commandForm.value,...e})}))}static{this.ɵfac=function(e){return new(e||AH)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(je.MatDialog))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:AH,selectors:[["tb-gateway-service-rpc-connector"]],inputs:{connectorType:"connectorType"},outputs:{sendCommand:"sendCommand",saveTemplate:"saveTemplate"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>AH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:12,vars:16,consts:[[1,"command-form","flex","flex-col",3,"formGroup"],[1,"mat-subtitle-1","title"],[3,"ngIf"],[1,"template-actions","flex","flex-row","justify-end","gap-2.5"],["mat-raised-button","",3,"click","disabled"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"ngSwitch"],[3,"ngSwitchCase"],["ngSwitchDefault",""],["matInput","","formControlName","method","placeholder","set_state"],[1,"mat-block"],["formControlName","requestType"],[3,"value",4,"ngFor","ngForOf"],["matInput","","formControlName","requestTimeout","type","number","min","10","step","1","placeholder","1000"],[1,"flex","flex-1","flex-row","gap-2.5"],[1,"flex-1"],["formControlName","objectType"],["matInput","","formControlName","identifier","type","number","min","1","step","1","placeholder","1"],["matInput","","formControlName","propertyId","placeholder","presentValue"],[3,"value"],["matInput","","formControlName","methodRPC","placeholder","rpcMethod1"],["matInput","","formControlName","characteristicUUID","placeholder","00002A00-0000-1000-8000-00805F9B34FB"],["formControlName","methodProcessing"],["formControlName","withResponse",1,"mat-slide"],["matInput","","formControlName","method","placeholder","sendSameData"],["matInput","","formControlName","nodeID","type","number","placeholder","4","min","0","step","1"],["formControlName","isExtendedID",1,"mat-slide","margin"],["formControlName","isFD",1,"mat-slide","margin"],["formControlName","bitrateSwitch",1,"mat-slide","margin"],["matInput","","formControlName","dataLength","type","number","placeholder","2","min","1","step","1"],["formControlName","dataByteorder"],["matInput","","formControlName","dataBefore","placeholder","00AA"],["matInput","","formControlName","dataAfter","placeholder","0102"],["matInput","","formControlName","dataInHEX","placeholder","aa bb cc dd ee ff aa bb aa bb cc d ee ff"],["matInput","","formControlName","dataExpression","placeholder","userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed"],["matInput","","formControlName","methodFilter","placeholder","read"],["matInput","","formControlName","valueExpression","placeholder","${params}"],["matInput","","formControlName","methodRPC","placeholder","rpc1"],["formControlName","withResponse",1,"mat-slide","margin"],["matInput","","formControlName","requestFilter","placeholder","setData"],["formControlName","method"],["formArrayName","oid",1,"fields","flex","flex-col","gap-2.5","border"],[1,"fields-label"],["class","flex flex-1 flex-row items-center justify-center gap-2.5",4,"ngFor","ngForOf"],["mat-raised-button","",1,"self-start",3,"click"],[1,"flex","flex-1","flex-row","items-center","justify-center","gap-2.5"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","flex-1"],["matInput","","required","",3,"formControl"],[1,"flex-[1_1_30px]",2,"cursor","pointer","max-width","30px","min-width","30px",3,"click","matTooltip"],["matInput","","formControlName","methodFilter","placeholder","post_attributes"],[1,"max-w-4/12","flex-[1_1_33%]"],["formControlName","httpMethod"],["matInput","","formControlName","requestUrlExpression","placeholder","http://127.0.0.1:5000/my_devices"],["matInput","","formControlName","responseTimeout","type","number","step","1","min","10","placeholder","10"],["matInput","","formControlName","timeout","type","number","step","1","min","10","placeholder","1000"],["matInput","","formControlName","tries","type","number","step","1","min","1","placeholder","3"],["formArrayName","httpHeaders",1,"fields","flex","flex-col","gap-2.5","border"],["class","flex flex-col gap-2.5 border",4,"ngIf"],[1,"flex","flex-col","gap-2.5","border"],[1,"flex","flex-row","items-center","justify-center","gap-2.5"],[1,"title","flex-1"],[2,"width","30px"],["class","flex flex-row items-center justify-center gap-2.5",4,"ngFor","ngForOf"],[3,"formGroupName"],["appearance","outline",1,"flex-1"],["matInput","","formControlName","headerName"],["matInput","","formControlName","value","placeholder","application/json"],[2,"cursor","pointer","width","30px",3,"click","matTooltip"],["matInput","","formControlName","methodFilter","placeholder","echo"],["matInput","","formControlName","timeout","type","number","step","1","min","10","placeholder","10"],["matInput","","formControlName","tries","type","number","step","1","min","1","placeholder","1"],["matInput","","formControlName","requestValueExpression","placeholder","${params}"],["matInput","","formControlName","responseValueExpression","placeholder","${temp}"],["matInput","","formControlName","headerName",3,"placeholder"],["matInput","","formControlName","value"],["matInput","","formControlName","command"],["matInput","","formControlName","params","type","JSON","tb-json-to-string",""],["aria-hidden","false","aria-label","help-icon","matIconSuffix","",1,"material-icons-outlined",2,"cursor","pointer",3,"click","matTooltip"],[4,"ngIf"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(4,OH,11,10,"ng-template",2),t.ɵɵelementStart(5,"div",3)(6,"button",4),t.ɵɵlistener("click",(function(){return n.save()})),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"button",5),t.ɵɵlistener("click",(function(){return n.sendCommand.emit()})),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.commandForm),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(3,7,"gateway.rpc.title",t.ɵɵpureFunction1(14,lH,n.gatewayConnectorDefaultTypesTranslates.get(n.connectorType)))),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.connectorType),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.commandForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(8,10,"gateway.rpc-command-save-template")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.commandForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,12,"gateway.rpc-command-send")," "))},dependencies:t.ɵɵgetComponentDepsFactory(AH,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex;flex-direction:column;padding:0}[_nghost-%COMP%] .title[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%]{flex-wrap:nowrap}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%] > button[_ngcontent-%COMP%]{margin-top:10px}[_nghost-%COMP%] .mat-mdc-slide-toggle.margin[_ngcontent-%COMP%]{margin-bottom:10px;margin-left:10px}[_nghost-%COMP%] .fields[_ngcontent-%COMP%] .fields-label[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .border[_ngcontent-%COMP%]{padding:16px;margin-bottom:10px;box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;border:solid 1px #e0e0e0;border-radius:4px}[_nghost-%COMP%] .border[_ngcontent-%COMP%] .title[_ngcontent-%COMP%]{color:#0000008a}[_nghost-%COMP%] .border[_ngcontent-%COMP%] .mat-icon[_ngcontent-%COMP%]{color:#00000061}[_nghost-%COMP%] .border[_ngcontent-%COMP%] .mat-divider[_ngcontent-%COMP%]{margin-left:-16px;margin-right:-16px;margin-bottom:16px}']})}}function FH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",11),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function RH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",11),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.ModbusFunctionCodeTranslationsMap.get(e)))}}function BH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",12),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-required"))}function NH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",12),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function LH(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",13)(1,"mat-form-field",3)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",14),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,NH,3,3,"mat-icon",8),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rpc.value")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.rpcParametersFormGroup.get("value").hasError("required")&&e.rpcParametersFormGroup.get("value").touched)}}class VH{constructor(e){this.fb=e,this.ModbusEditableDataTypes=Qt,this.ModbusFunctionCodeTranslationsMap=nn,this.modbusDataTypes=Object.values(Zt),this.writeFunctionCodes=[5,6,15,16],this.defaultFunctionCodes=[3,4,6,16],this.readFunctionCodes=[1,2,3,4],this.bitsFunctionCodes=[...this.readFunctionCodes,...this.writeFunctionCodes],this.destroy$=new we,this.rpcParametersFormGroup=this.fb.group({type:[Zt.BYTES,[me.required]],functionCode:[this.defaultFunctionCodes[0],[me.required]],value:[{value:"",disabled:!0},[me.required,me.pattern(pn)]],address:[null,[me.required]],objectsCount:[1,[me.required]]}),this.updateFunctionCodes(this.rpcParametersFormGroup.get("type").value),this.observeValueChanges(),this.observeKeyDataType(),this.observeFunctionCode()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.rpcParametersFormGroup.valid?null:{rpcParametersFormGroup:{valid:!1}}}writeValue(e){this.rpcParametersFormGroup.patchValue(e,{emitEvent:!1})}observeValueChanges(){this.rpcParametersFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}observeKeyDataType(){this.rpcParametersFormGroup.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.ModbusEditableDataTypes.includes(e)||this.rpcParametersFormGroup.get("objectsCount").patchValue(Jt[e],{emitEvent:!1}),this.updateFunctionCodes(e)}))}observeFunctionCode(){this.rpcParametersFormGroup.get("functionCode").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.updateValueEnabling(e)))}updateValueEnabling(e){this.writeFunctionCodes.includes(e)?this.rpcParametersFormGroup.get("value").enable({emitEvent:!1}):(this.rpcParametersFormGroup.get("value").setValue(null),this.rpcParametersFormGroup.get("value").disable({emitEvent:!1}))}updateFunctionCodes(e){this.functionCodes=e===Zt.BITS?this.bitsFunctionCodes:this.defaultFunctionCodes,this.functionCodes.includes(this.rpcParametersFormGroup.get("functionCode").value)||this.rpcParametersFormGroup.get("functionCode").patchValue(this.functionCodes[0],{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||VH)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:VH,selectors:[["tb-gateway-modbus-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>VH)),multi:!0},{provide:he,useExisting:c((()=>VH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:35,vars:30,consts:[[3,"formGroup"],[1,"tb-form-hint","tb-primary-fill","no-padding-top","hint-container"],[1,"flex","flex-1","flex-row","gap-2.5"],[1,"flex-1"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["formControlName","functionCode"],["matInput","","type","number","min","0","max","50000","name","value","formControlName","address",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","number","min","1","max","50000","name","value","formControlName","objectsCount",3,"placeholder","readonly"],["class","flex",4,"ngIf"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[1,"flex"],["matInput","","name","value","formControlName","value",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelement(4,"br"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",2)(8,"mat-form-field",3)(9,"mat-label"),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-select",4),t.ɵɵtemplate(13,FH,2,2,"mat-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(14,"mat-form-field",3)(15,"mat-label"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"mat-select",6),t.ɵɵtemplate(19,RH,3,4,"mat-option",5),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",2)(21,"mat-form-field",3)(22,"mat-label"),t.ɵɵtext(23),t.ɵɵpipe(24,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(25,"input",7),t.ɵɵpipe(26,"translate"),t.ɵɵtemplate(27,BH,3,3,"mat-icon",8),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-form-field",3)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",9),t.ɵɵpipe(33,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(34,LH,8,7,"div",10),t.ɵɵelementContainerEnd()),2&e&&(t.ɵɵproperty("formGroup",n.rpcParametersFormGroup),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,14,"gateway.rpc.hint.modbus-response-reading"),""),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,16,"gateway.rpc.hint.modbus-writing-functions")," "),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(11,18,"gateway.rpc.type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.modbusDataTypes),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(17,20,"gateway.rpc.functionCode")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.functionCodes),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(24,22,"gateway.rpc.address")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(26,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.rpcParametersFormGroup.get("address").hasError("required")&&n.rpcParametersFormGroup.get("address").touched),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,26,"gateway.rpc.objectsCount")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(33,28,"gateway.set")),t.ɵɵproperty("readonly",!n.ModbusEditableDataTypes.includes(n.rpcParametersFormGroup.get("type").value)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.writeFunctionCodes.includes(n.rpcParametersFormGroup.get("functionCode").value)))},dependencies:t.ɵɵgetComponentDepsFactory(VH,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .hint-container[_ngcontent-%COMP%]{margin-bottom:12px}'],changeDetection:d.OnPush})}}function qH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",6),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rpc.responseTopicExpression")))}function GH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",7),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rpc.responseTimeout")))}class zH{constructor(e){this.fb=e,this.onChange=e=>{},this.onTouched=()=>{},this.destroy$=new we,this.rpcParametersFormGroup=this.fb.group({methodFilter:[null,[me.required,me.pattern(pn)]],requestTopicExpression:[null,[me.required,me.pattern(pn)]],responseTopicExpression:[{value:null,disabled:!0},[me.required,me.pattern(pn)]],responseTimeout:[{value:null,disabled:!0},[me.min(10),me.pattern(cn)]],valueExpression:[null,[me.required,me.pattern(pn)]],withResponse:[!1,[]]}),this.observeValueChanges(),this.observeWithResponse()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.rpcParametersFormGroup.valid?null:{rpcParametersFormGroup:{valid:!1}}}writeValue(e){this.rpcParametersFormGroup.patchValue(e,{emitEvent:!1}),this.toggleResponseFields(e.withResponse)}observeValueChanges(){this.rpcParametersFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}observeWithResponse(){this.rpcParametersFormGroup.get("withResponse").valueChanges.pipe(Ae((e=>this.toggleResponseFields(e))),Me(this.destroy$)).subscribe()}toggleResponseFields(e){const t=this.rpcParametersFormGroup.get("responseTopicExpression"),n=this.rpcParametersFormGroup.get("responseTimeout");e?(t.enable(),n.enable()):(t.disable(),n.disable())}static{this.ɵfac=function(e){return new(e||zH)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:zH,selectors:[["tb-gateway-mqtt-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>zH)),multi:!0},{provide:he,useExisting:c((()=>zH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:21,vars:15,consts:[[3,"formGroup"],["matInput","","formControlName","methodFilter","placeholder","echo"],["matInput","","formControlName","requestTopicExpression","placeholder","sensor/${deviceName}/request/${methodName}/${requestId}"],["formControlName","withResponse",1,"margin",3,"click"],[4,"ngIf"],["matInput","","formControlName","valueExpression","placeholder","${params}"],["matInput","","formControlName","responseTopicExpression","placeholder","sensor/${deviceName}/response/${methodName}/${requestId}"],["matInput","","formControlName","responseTimeout","type","number","placeholder","10000","min","10","step","1"]],template:function(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"mat-form-field")(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",1),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field")(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(10,"input",2),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-slide-toggle",3),t.ɵɵlistener("click",(function(e){return e.stopPropagation()})),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(14,qH,5,3,"mat-form-field",4)(15,GH,5,3,"mat-form-field",4),t.ɵɵelementStart(16,"mat-form-field")(17,"mat-label"),t.ɵɵtext(18),t.ɵɵpipe(19,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(20,"input",5),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e){let e,i;t.ɵɵproperty("formGroup",n.rpcParametersFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,7,"gateway.rpc.method-name")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,9,"gateway.rpc.requestTopicExpression")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,11,"gateway.rpc.withResponse")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",null==(e=n.rpcParametersFormGroup.get("withResponse"))?null:e.value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",null==(i=n.rpcParametersFormGroup.get("withResponse"))?null:i.value),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(19,13,"gateway.rpc.valueExpression"))}},dependencies:t.ɵɵgetComponentDepsFactory(zH,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:flex;flex-direction:column}[_nghost-%COMP%] .mat-mdc-slide-toggle.margin[_ngcontent-%COMP%]{margin-bottom:10px;margin-left:10px}'],changeDetection:d.OnPush})}}function UH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",26),t.ɵɵelement(1,"mat-icon",27),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("svgIcon",i.valueTypes.get(e).icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,i.valueTypes.get(e).name))}}function jH(e,n){1&e&&(t.ɵɵelement(0,"input",28),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function HH(e,n){1&e&&(t.ɵɵelement(0,"input",29),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function WH(e,n){1&e&&(t.ɵɵelement(0,"input",30),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function $H(e,n){1&e&&(t.ɵɵelementStart(0,"mat-select",31)(1,"mat-option",26),t.ɵɵtext(2,"true"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-option",26),t.ɵɵtext(4,"false"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵproperty("value",!0),t.ɵɵadvance(2),t.ɵɵproperty("value",!1))}function KH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",32),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function YH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",8)(1,"div",9)(2,"div",10),t.ɵɵtext(3,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",11)(5,"mat-form-field",12)(6,"mat-select",13)(7,"mat-select-trigger")(8,"div",14),t.ɵɵelement(9,"mat-icon",15),t.ɵɵelementStart(10,"span"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(13,UH,5,5,"mat-option",16),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(14,"div",17)(15,"div",10),t.ɵɵtext(16,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"mat-form-field",18),t.ɵɵelementContainerStart(18,19),t.ɵɵtemplate(19,jH,2,3,"input",20)(20,HH,2,3,"input",21)(21,WH,2,3,"input",22)(22,$H,5,2,"mat-select",23),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(23,KH,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(24,"button",25),t.ɵɵpipe(25,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext();return t.ɵɵresetView(i.removeArgument(n))})),t.ɵɵelementStart(26,"mat-icon"),t.ɵɵtext(27,"delete"),t.ɵɵelementEnd()()()}if(2&e){let e,i;const a=n.$implicit,r=t.ɵɵnextContext();t.ɵɵproperty("formGroup",a),t.ɵɵadvance(9),t.ɵɵproperty("svgIcon",null==(e=r.valueTypes.get(a.get("type").value))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,11,null==(i=r.valueTypes.get(a.get("type").value))?null:i.name)),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",r.valueTypeKeys),t.ɵɵadvance(5),t.ɵɵproperty("ngSwitch",a.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.STRING),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.INTEGER),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.DOUBLE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.BOOLEAN),t.ɵɵadvance(),t.ɵɵproperty("ngIf",a.get(a.get("type").value+"Value").hasError("required")&&a.get(a.get("type").value+"Value").touched),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(25,13,"gateway.rpc.remove"))}}class XH{constructor(e,t){this.fb=e,this.cdr=t,this.valueTypeKeys=Object.values(en),this.MappingValueType=en,this.valueTypes=tn,this.onChange=e=>{},this.onTouched=()=>{},this.destroy$=new we,this.rpcParametersFormGroup=this.fb.group({method:[null,[me.required,me.pattern(pn)]],arguments:this.fb.array([])}),this.observeValueChanges()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.rpcParametersFormGroup.valid?null:{rpcParametersFormGroup:{valid:!1}}}writeValue(e){this.clearArguments(),e.arguments?.map((({type:e,value:t})=>({type:e,[e+"Value"]:t}))).forEach((e=>this.addArgument(e))),this.cdr.markForCheck(),this.rpcParametersFormGroup.get("method").patchValue(e.method)}observeValueChanges(){this.rpcParametersFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{const t=e.arguments.map((({type:e,...t})=>({type:e,value:t[e+"Value"]})));this.onChange({method:e.method,arguments:t}),this.onTouched()}))}removeArgument(e){this.rpcParametersFormGroup.get("arguments").removeAt(e)}addArgument(e={}){const t=this.fb.group({type:[e.type??en.STRING],stringValue:[e.stringValue??{value:"",disabled:!(W(e,{})||e.stringValue)},[me.required,me.pattern(pn)]],integerValue:[{value:e.integerValue??0,disabled:!J(e.integerValue)},[me.required,me.pattern(cn)]],doubleValue:[{value:e.doubleValue??0,disabled:!J(e.doubleValue)},[me.required]],booleanValue:[{value:e.booleanValue??!1,disabled:!J(e.booleanValue)},[me.required]]});this.observeTypeChange(t),this.rpcParametersFormGroup.get("arguments").push(t,{emitEvent:!1})}clearArguments(){const e=this.rpcParametersFormGroup.get("arguments");for(;0!==e.length;)e.removeAt(0)}observeTypeChange(e){e.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((t=>{e.disable({emitEvent:!1}),e.get("type").enable({emitEvent:!1}),e.get(t+"Value").enable({emitEvent:!1})}))}static{this.ɵfac=function(e){return new(e||XH)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:XH,selectors:[["tb-gateway-opc-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>XH)),multi:!0},{provide:he,useExisting:c((()=>XH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:18,vars:14,consts:[[3,"formGroup"],[1,"tb-form-hint","tb-primary-fill","tb-flex","no-padding-top","hint-container"],[1,"tb-flex"],["matInput","","formControlName","method","placeholder","multiply"],["formArrayName","arguments",1,"tb-form-panel","stroked","arguments-container"],[1,"fields-label"],["class","flex flex-1 items-center justify-center gap-2.5",3,"formGroup",4,"ngFor","ngForOf"],["mat-raised-button","",1,"self-start",3,"click"],[1,"flex","flex-1","items-center","justify-center","gap-2.5",3,"formGroup"],[1,"tb-form-row","column-xs","type-container","items-center","justify-between"],["translate","",1,"tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","type"],[1,"tb-flex","align-center"],[1,"tb-mat-18",3,"svgIcon"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-row","column-xs","value-container","item-center","justify-between"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","tb-suffix-absolute","flex"],[3,"ngSwitch"],["matInput","","required","","formControlName","stringValue",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder",4,"ngSwitchCase"],["formControlName","booleanValue",4,"ngSwitchCase"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["mat-icon-button","","matTooltipPosition","above",1,"tb-box-button",3,"click","matTooltip"],[3,"value"],[1,"tb-mat-20",3,"svgIcon"],["matInput","","required","","formControlName","stringValue",3,"placeholder"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder"],["formControlName","booleanValue"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",2)(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(8,"input",3),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"fieldset",4)(10,"strong")(11,"span",5),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(14,YH,28,15,"div",6),t.ɵɵelementStart(15,"button",7),t.ɵɵlistener("click",(function(){return n.addArgument()})),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e&&(t.ɵɵproperty("formGroup",n.rpcParametersFormGroup),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,6,"gateway.rpc.hint.opc-method")," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(7,8,"gateway.rpc.method")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,10,"gateway.rpc.arguments")),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",n.rpcParametersFormGroup.get("arguments").controls),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,12,"gateway.rpc.add-argument")," "))},dependencies:t.ɵɵgetComponentDepsFactory(XH,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .arguments-container[_ngcontent-%COMP%]{margin-bottom:10px}[_nghost-%COMP%] .type-container[_ngcontent-%COMP%]{width:40%}[_nghost-%COMP%] .value-container[_ngcontent-%COMP%]{width:50%}[_nghost-%COMP%] .hint-container[_ngcontent-%COMP%]{margin-bottom:12px}'],changeDetection:d.OnPush})}}function ZH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SocketMethodProcessingsTranslates.get(e))," ")}}function QH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}class JH extends za{constructor(){super(...arguments),this.SocketMethodProcessingsTranslates=Pj,this.socketMethodProcessings=Object.values(kj),this.socketEncoding=Object.values(ht)}initFormGroup(){return this.fb.group({methodRPC:[null,[me.required,me.pattern(pn)]],methodProcessing:[kj.WRITE,[me.required]],encoding:[Aj.UTF_8,[me.required,me.pattern(pn)]],withResponse:[!1,[]]})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(JH)))(n||JH)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:JH,selectors:[["tb-gateway-socket-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>JH)),multi:!0},{provide:he,useExisting:c((()=>JH)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:21,vars:15,consts:[[3,"formGroup"],[1,"w-full"],["matInput","","formControlName","methodRPC","placeholder","rpcMethod1"],[1,"mat-block"],["formControlName","methodProcessing"],[3,"value",4,"ngFor","ngForOf"],["formControlName","encoding"],["formControlName","withResponse",1,"mat-slide","margin"],[3,"value"]],template:function(e,n){1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"mat-form-field",1)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",2),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",3)(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-select",4),t.ɵɵtemplate(11,ZH,3,4,"mat-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",3)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"mat-select",6),t.ɵɵtemplate(17,QH,2,2,"mat-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"mat-slide-toggle",7),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e&&(t.ɵɵproperty("formGroup",n.formGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,7,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,9,"gateway.rpc.methodProcessing")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.socketMethodProcessings),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,11,"gateway.encoding")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(20,13,"gateway.rpc.withResponse")," "))},dependencies:t.ɵɵgetComponentDepsFactory(JH,[se,_]),encapsulation:2,changeDetection:d.OnPush})}}const eW=e=>({border:e}),tW=e=>({type:e});function nW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",15),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function iW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-min")," "))}function aW(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"mat-form-field")(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-select",9),t.ɵɵtemplate(6,nW,2,2,"mat-option",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"mat-form-field",11)(8,"mat-label"),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(11,"input",12),t.ɵɵtemplate(12,iW,3,3,"mat-error",13),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"button",14),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.sendCommand())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,6,"gateway.statistics.command")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.RPCCommands),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(10,8,"gateway.statistics.timeout")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.commandForm.get("time").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("disabled",e.commandForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,10,"gateway.rpc-command-send")," ")}}function rW(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-service-rpc-connector",17),t.ɵɵlistener("sendCommand",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.sendCommand())}))("saveTemplate",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.saveTemplate())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("connectorType",e.connectorType)}}function oW(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-modbus-rpc-parameters",24)}function sW(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-mqtt-rpc-parameters",24)}function lW(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-opc-rpc-parameters",24)}function pW(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-socket-rpc-parameters",24)}function cW(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",18)(1,"div",19),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(4,20),t.ɵɵtemplate(5,oW,1,0,"tb-gateway-modbus-rpc-parameters",21)(6,sW,1,0,"tb-gateway-mqtt-rpc-parameters",21)(7,lW,1,0,"tb-gateway-opc-rpc-parameters",21)(8,pW,1,0,"tb-gateway-socket-rpc-parameters",21),t.ɵɵelementContainerEnd(),t.ɵɵelementStart(9,"div",22)(10,"button",23),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.saveTemplate())})),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"button",14),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.sendCommand())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(3,10,"gateway.rpc.title",t.ɵɵpureFunction1(17,tW,e.gatewayConnectorDefaultTypesTranslates.get(e.connectorType)))),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",e.connectorType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MODBUS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MQTT),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.OPCUA),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.SOCKET),t.ɵɵadvance(2),t.ɵɵproperty("disabled",e.commandForm.get("params").invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,13,"gateway.rpc-command-save-template")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",e.commandForm.get("params").invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,15,"gateway.rpc-command-send")," ")}}function dW(e,n){if(1&e&&t.ɵɵtemplate(0,rW,1,1,"tb-gateway-service-rpc-connector",16)(1,cW,16,19,"ng-template",null,1,t.ɵɵtemplateRefExtractor),2&e){const e=t.ɵɵreference(2),n=t.ɵɵnextContext();t.ɵɵproperty("ngIf",!n.typesWithUpdatedParams.has(n.connectorType))("ngIfElse",e)}}function uW(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",25)(1,"mat-icon",26),t.ɵɵtext(2,"schedule"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"span"),t.ɵɵtext(4),t.ɵɵpipe(5,"date"),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(5,1,e.resultTime,"yyyy/MM/dd HH:mm:ss"))}}function mW(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-service-rpc-connector-templates",27),t.ɵɵlistener("useTemplate",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.useTemplate(n))})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("rpcTemplates",e.templates)("ctx",e.ctx)("connectorType",e.connectorType)}}class hW{constructor(e,t,n,i,a){this.fb=e,this.dialog=t,this.utils=n,this.cd=i,this.attributeService=a,this.contentTypes=V,this.RPCCommands=["Ping","Stats","Devices","Update","Version","Restart","Reboot"],this.templates=[],this.ConnectorType=dt,this.gatewayConnectorDefaultTypesTranslates=ut,this.typesWithUpdatedParams=new Set([dt.MQTT,dt.OPCUA,dt.MODBUS,dt.SOCKET]),this.modbusReadFunctionCodes=[1,2,3,4],this.subscriptionOptions={callbacks:{onDataUpdated:()=>this.ctx.ngZone.run((()=>{this.updateTemplates()})),onDataUpdateError:(e,t)=>this.ctx.ngZone.run((()=>{this.onDataUpdateError(t)})),dataLoading:()=>{}}},this.commandForm=this.fb.group({command:[null,[me.required]],time:[60,[me.required,me.min(1)]],params:["{}",[lt]],result:[null]})}ngOnInit(){if(this.isConnector=this.ctx.settings.isConnector,this.isConnector){this.connectorType=this.ctx.stateController.getStateParams().connector_rpc.value.type;const e=[{type:N.entity,entityType:L.DEVICE,entityId:this.ctx.defaultSubscription.targetDeviceId,entityName:"Connector",attributes:[{name:`${this.connectorType}_template`}]}];this.ctx.subscriptionApi.createSubscriptionFromInfo(R.latest,e,this.subscriptionOptions,!1,!0).subscribe((e=>{this.subscription=e}))}else this.commandForm.get("command").setValue(this.RPCCommands[0])}sendCommand(e){this.resultTime=null;const t=e||this.commandForm.value,n=this.isConnector?`${this.connectorType}_`:"gateway_",i=this.isConnector?this.getCommandFromParamsByType(t.params):t.command.toLowerCase(),a=this.ctx.stateController.getStateParams().connector_rpc?.value.configurationJson.id,r=a?{...t.params,connectorId:a}:t.params;this.ctx.controlApi.sendTwoWayCommand(n+i,r,t.time).subscribe({next:e=>{this.resultTime=(new Date).getTime(),this.commandForm.get("result").setValue(JSON.stringify(e))},error:e=>{this.resultTime=(new Date).getTime(),console.error(e),this.commandForm.get("result").setValue(JSON.stringify(e.error))}})}getCommandFromParamsByType(e){switch(this.connectorType){case dt.MQTT:case dt.FTP:case dt.SNMP:case dt.REST:case dt.REQUEST:return e.methodFilter;case dt.MODBUS:return this.modbusReadFunctionCodes.includes(e.functionCode)?"get":"set";case dt.BACNET:case dt.CAN:case dt.OPCUA:return e.method;case dt.BLE:case dt.OCPP:case dt.SOCKET:case dt.XMPP:return e.methodRPC;default:return e.command}}saveTemplate(){this.dialog.open(nH,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{config:this.commandForm.value.params,templates:this.templates}}).afterClosed().subscribe((e=>{if(e){const t={name:e,config:this.commandForm.value.params,type:this.connectorType},n=this.templates,i=n.findIndex((e=>e.name==t.name));i>-1&&n.splice(i,1),n.push(t);const a=`${this.connectorType}_template`;this.attributeService.saveEntityAttributes({id:this.ctx.defaultSubscription.targetDeviceId,entityType:L.DEVICE},D.SERVER_SCOPE,[{key:a,value:n}]).subscribe((()=>{this.cd.detectChanges()}))}}))}useTemplate(e){this.commandForm.get("params").patchValue(e.config)}updateTemplates(){this.templates=this.subscription.data[0].data[0][1].length?JSON.parse(this.subscription.data[0].data[0][1]):[],this.cd.detectChanges()}onDataUpdateError(e){const t=this.utils.parseException(e);let n=t.name;t.message&&(n+=": "+t.message),console.error(n)}static{this.ɵfac=function(e){return new(e||hW)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(K.UtilsService),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(K.AttributeService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:hW,selectors:[["tb-gateway-service-rpc"]],inputs:{ctx:"ctx",dialogRef:"dialogRef"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:12,vars:14,consts:[["connectorForm",""],["updatedParameters",""],[1,"flex","flex-1","flex-col"],[1,"command-form","flex","flex-row","gap-2.5","lt-sm:flex-col",3,"formGroup"],[4,"ngIf","ngIfElse"],[1,"result-block",3,"formGroup"],["class","result-time flex flex-1 flex-row items-center justify-center",4,"ngIf"],["readonly","true","formControlName","result",3,"contentType"],["class","border",3,"rpcTemplates","ctx","connectorType","useTemplate",4,"ngIf"],["formControlName","command"],[3,"value",4,"ngFor","ngForOf"],[1,"flex-1"],["matInput","","formControlName","time","type","number","min","1"],[4,"ngIf"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["formControlName","params",3,"connectorType","sendCommand","saveTemplate",4,"ngIf","ngIfElse"],["formControlName","params",3,"sendCommand","saveTemplate","connectorType"],[1,"rpc-parameters","flex","flex-col"],[1,"mat-subtitle-1","tb-form-panel-title"],[3,"ngSwitch"],["formControlName","params",4,"ngSwitchCase"],[1,"fex-row","template-actions","flex","flex-1","items-center","justify-end","gap-2.5"],["mat-raised-button","",3,"click","disabled"],["formControlName","params"],[1,"result-time","flex","flex-1","flex-row","items-center","justify-center"],[1,"material-icons"],[1,"border",3,"useTemplate","rpcTemplates","ctx","connectorType"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",2)(1,"div",3),t.ɵɵtemplate(2,aW,16,12,"ng-container",4)(3,dW,3,2,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"section",5)(6,"span"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,uW,6,4,"div",6),t.ɵɵelementEnd(),t.ɵɵelement(10,"tb-json-content",7),t.ɵɵelementEnd()(),t.ɵɵtemplate(11,mW,1,3,"tb-gateway-service-rpc-connector-templates",8)),2&e){const e=t.ɵɵreference(4);t.ɵɵclassMap(t.ɵɵpureFunction1(12,eW,n.isConnector)),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.commandForm),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.isConnector)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵproperty("formGroup",n.commandForm),t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(8,10,"gateway.rpc-command-result")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.resultTime),t.ɵɵadvance(),t.ɵɵproperty("contentType",n.contentTypes.JSON),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isConnector)}},dependencies:t.ɵɵgetComponentDepsFactory(hW,[se,_,AH,VH,zH,XH,Jj,JH]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;overflow:auto;display:flex;flex-direction:row;padding:0 5px}[_nghost-%COMP%] > *[_ngcontent-%COMP%]{height:100%;overflow:auto}[_nghost-%COMP%] > tb-gateway-service-rpc-connector-templates[_ngcontent-%COMP%]:last-child{margin-left:10px}[_nghost-%COMP%] tb-gateway-service-rpc-connector-templates[_ngcontent-%COMP%]{flex:1 1 30%;max-width:30%}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%]{flex-wrap:nowrap;padding:0 5px 5px}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%] > button[_ngcontent-%COMP%]{margin-top:10px}[_nghost-%COMP%] .rpc-parameters[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%]{padding:0 5px;display:flex;flex-direction:column;flex:1}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] > span[_ngcontent-%COMP%]{font-weight:600;position:relative;font-size:14px;margin-bottom:10px}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] > span[_ngcontent-%COMP%] .result-time[_ngcontent-%COMP%]{font-weight:400;font-size:14px;line-height:32px;position:absolute;left:0;top:25px;z-index:5;color:#0000008a}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] > span[_ngcontent-%COMP%] .result-time[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{padding-left:10px}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] tb-json-content[_ngcontent-%COMP%]{flex:1}[_nghost-%COMP%] .border[_ngcontent-%COMP%]{padding:16px;box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;border:solid 1px #e0e0e0;border-radius:4px}']})}}function gW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.configuration-delete-dialog-input-required")," "))}e("GatewayServiceRPCComponent",hW);class fW extends A{constructor(e,t,n,i,a){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.gatewayName=this.data.gatewayName,this.gatewayControl=this.fb.control("")}close(){this.dialogRef.close()}turnOff(){this.dialogRef.close(!0)}static{this.ɵfac=function(e){return new(e||fW)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:fW,selectors:[["tb-gateway-remote-configuration-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:24,vars:14,consts:[["color","warn"],["translate",""],[1,"flex-1"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"mat-content","flex-col",2,"max-width","600px"],[3,"innerHTML"],[1,"mat-block","tb-value-type",2,"flex-grow","0"],["matInput","","required","",3,"formControl"],[4,"ngIf"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","warn","type","button","cdkFocusInitial","",3,"click"],["mat-button","","color","warn","type","button",3,"click","disabled"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-toolbar",0)(1,"mat-icon"),t.ɵɵtext(2,"warning"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"h2",1),t.ɵɵtext(4,"gateway.configuration-delete-dialog-header"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",2),t.ɵɵelementStart(6,"button",3),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵelementStart(7,"mat-icon",4),t.ɵɵtext(8,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",5),t.ɵɵelement(10,"span",6),t.ɵɵpipe(11,"translate"),t.ɵɵelementStart(12,"mat-form-field",7)(13,"mat-label",1),t.ɵɵtext(14,"gateway.configuration-delete-dialog-input"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",8),t.ɵɵtemplate(16,gW,3,3,"mat-error",9),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",10)(18,"button",11),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"button",12),t.ɵɵlistener("click",(function(){return n.turnOff()})),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(10),t.ɵɵpropertyInterpolate2("innerHTML","",t.ɵɵpipeBind1(11,8,"gateway.configuration-delete-dialog-body")," ",n.gatewayName,"",t.ɵɵsanitizeHtml),t.ɵɵadvance(5),t.ɵɵproperty("formControl",n.gatewayControl),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayControl.hasError("required")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(20,10,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.gatewayControl.value!==n.gatewayName),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(23,12,"gateway.configuration-delete-dialog-confirm")," "))},dependencies:t.ɵɵgetComponentDepsFactory(fW,[se,_]),encapsulation:2})}}var yW;e("GatewayRemoteConfigurationDialogComponent",fW),function(e){e.tls="tls",e.accessToken="accessToken"}(yW||(yW={}));const vW="configuration_drafts",xW="RemoteLoggingLevel",bW=new Map([[yW.tls,"gateway.security-types.tls"],[yW.accessToken,"gateway.security-types.access-token"]]);var wW,SW;!function(e){e.none="NONE",e.critical="CRITICAL",e.error="ERROR",e.warning="WARNING",e.info="INFO",e.debug="DEBUG"}(wW||(wW={})),function(e){e.memory="memory",e.file="file"}(SW||(SW={}));const CW=new Map([[SW.memory,"gateway.storage-types.memory-storage"],[SW.file,"gateway.storage-types.file-storage"]]);var _W;!function(e){e.mqtt="MQTT",e.modbus="Modbus",e.opcua="OPC-UA",e.ble="BLE",e.request="Request",e.can="CAN",e.bacnet="BACnet",e.custom="Custom"}(_W||(_W={}));const TW={config:{},name:"",configType:null,enabled:!1};function IW(e){return JSON.stringify(e.value)===JSON.stringify({})?{validJSON:!0}:null}function EW(e){return e.replace("_","").replace("-","").replace(/^\s+|\s+/g,"").toLowerCase()+".json"}function MW(e,t){return'[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"'.replace(/{ERROR}/g,e).replace(/{.\/logs\/}/g,t)}function kW(e){return{id:e,entityType:L.DEVICE}}function PW(e){const t={};return Object.prototype.hasOwnProperty.call(e,"thingsboard")&&(t.host=e.thingsboard.host,t.port=e.thingsboard.port,t.remoteConfiguration=e.thingsboard.remoteConfiguration,Object.prototype.hasOwnProperty.call(e.thingsboard.security,yW.accessToken)?(t.securityType=yW.accessToken,t.accessToken=e.thingsboard.security.accessToken):(t.securityType=yW.tls,t.caCertPath=e.thingsboard.security.caCert,t.privateKeyPath=e.thingsboard.security.privateKey,t.certPath=e.thingsboard.security.cert)),Object.prototype.hasOwnProperty.call(e,"storage")&&Object.prototype.hasOwnProperty.call(e.storage,"type")&&(e.storage.type===SW.memory?(t.storageType=SW.memory,t.readRecordsCount=e.storage.read_records_count,t.maxRecordsCount=e.storage.max_records_count):e.storage.type===SW.file&&(t.storageType=SW.file,t.dataFolderPath=e.storage.data_folder_path,t.maxFilesCount=e.storage.max_file_count,t.readRecordsCount=e.storage.read_records_count,t.maxRecordsCount=e.storage.max_records_count)),t}function DW(e){const t={};for(const n of e)n.enabled||(t[n.name]={connector:n.configType,config:n.config});return t}function OW(e){const t={thingsboard:AW(e)};return function(e,t){for(const n of t)if(n.enabled){const t=n.configType;Array.isArray(e[t])||(e[t]=[]);const i={name:n.name,config:n.config};e[t].push(i)}}(t,e.connectors),t}function AW(e){let t;t=e.securityType===yW.accessToken?{accessToken:e.accessToken}:{caCert:e.caCertPath,privateKey:e.privateKeyPath,cert:e.certPath};const n={host:e.host,remoteConfiguration:e.remoteConfiguration,port:e.port,security:t};let i;i=e.storageType===SW.memory?{type:SW.memory,read_records_count:e.readRecordsCount,max_records_count:e.maxRecordsCount}:{type:SW.file,data_folder_path:e.dataFolderPath,max_file_count:e.maxFilesCount,max_read_records_count:e.readRecordsCount,max_records_per_file:e.maxRecordsCount};const a=[];for(const t of e.connectors)if(t.enabled){const e={configuration:EW(t.name),name:t.name,type:t.configType};a.push(e)}return{thingsboard:n,connectors:a,storage:i,logs:window.btoa(MW(e.remoteLoggingLevel,e.remoteLoggingPathToLogs))}}const FW=["formContainer"],RW=(e,t,n)=>({"gap-1.25":e,"flex-row":t,"flex-col":n}),BW=(e,t,n)=>({"gap-1.25":e,"flex-row justify-end item-center":t,"flex-col justify-evenly item-center":n});function NW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e.value.toString())," ")}}function LW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-host-required "),t.ɵɵelementEnd())}function VW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-required "),t.ɵɵelementEnd())}function qW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-min "),t.ɵɵelementEnd())}function GW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-max "),t.ɵɵelementEnd())}function zW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-pattern "),t.ɵɵelementEnd())}function UW(e,n){1&e&&(t.ɵɵelementStart(0,"div",16)(1,"mat-form-field")(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",30),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field")(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(10,"input",31),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field")(12,"mat-label"),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",32),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.tls-path-ca-certificate")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,5,"gateway.tls-path-private-key")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,7,"gateway.tls-path-client-certificate")))}function jW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function HW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.path-logs-required "),t.ɵɵelementEnd())}function WW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e.value.toString())," ")}}function $W(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-pack-size-required "),t.ɵɵelementEnd())}function KW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-pack-size-min "),t.ɵɵelementEnd())}function YW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-pack-size-pattern "),t.ɵɵelementEnd())}function XW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-records-required "),t.ɵɵelementEnd())}function ZW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-records-min "),t.ɵɵelementEnd())}function QW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-records-pattern "),t.ɵɵelementEnd())}function JW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-files-required "),t.ɵɵelementEnd())}function e$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-files-min "),t.ɵɵelementEnd())}function t$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-files-pattern "),t.ɵɵelementEnd())}function n$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-path-required "),t.ɵɵelementEnd())}function i$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",5)(1,"mat-form-field",8)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",33),t.ɵɵtemplate(6,JW,2,0,"mat-error",10)(7,e$,2,0,"mat-error",10)(8,t$,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-form-field",8)(10,"mat-label"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",34),t.ɵɵtemplate(14,n$,2,0,"mat-error",10),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵclassMap(t.ɵɵpureFunction3(12,RW,e.layoutGap,e.alignment,!e.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,8,"gateway.storage-max-files")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("maxFilesCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("maxFilesCount").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("maxFilesCount").hasError("pattern")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,10,"gateway.storage-path")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("dataFolderPath").hasError("required"))}}function a$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function r$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.connector-type-required "),t.ɵɵelementEnd())}function o$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.connector-name-required "),t.ɵɵelementEnd())}function s$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"section",35)(1,"div",36)(2,"div",37),t.ɵɵelement(3,"mat-slide-toggle",38),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",39)(5,"mat-form-field",8)(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-select",40),t.ɵɵlistener("selectionChange",(function(){const n=t.ɵɵrestoreView(e).$implicit,i=t.ɵɵnextContext();return t.ɵɵresetView(i.changeConnectorType(n))})),t.ɵɵtemplate(10,a$,2,2,"mat-option",7),t.ɵɵelementEnd(),t.ɵɵtemplate(11,r$,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",8)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"input",41),t.ɵɵlistener("blur",(function(){const n=t.ɵɵrestoreView(e),i=n.$implicit,a=n.index,r=t.ɵɵnextContext();return t.ɵɵresetView(r.changeConnectorName(i,a))})),t.ɵɵelementEnd(),t.ɵɵtemplate(17,o$,2,0,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",42)(19,"button",43),t.ɵɵpipe(20,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e),a=i.$implicit,r=i.index,o=t.ɵɵnextContext();return t.ɵɵresetView(o.openConfigDialog(n,r,a.get("config").value,a.get("name").value))})),t.ɵɵelementStart(21,"mat-icon"),t.ɵɵtext(22,"more_horiz"),t.ɵɵelementEnd()(),t.ɵɵelementStart(23,"button",43),t.ɵɵpipe(24,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext();return t.ɵɵresetView(i.removeConnector(n))})),t.ɵɵelementStart(25,"mat-icon"),t.ɵɵtext(26,"close"),t.ɵɵelementEnd()()()()()}if(2&e){const e=n.$implicit,i=n.index,a=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("formGroupName",i),t.ɵɵadvance(3),t.ɵɵclassMap(t.ɵɵpureFunction3(24,RW,a.layoutGap,a.alignment,!a.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,16,"gateway.connector-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",a.connectorTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("configType").hasError("required")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,18,"gateway.connector-name")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.get("name").hasError("required")),t.ɵɵadvance(),t.ɵɵclassMap(t.ɵɵpureFunction3(28,BW,a.layoutGap,a.alignment,!a.alignment)),t.ɵɵadvance(),t.ɵɵclassProp("mat-warn",e.get("config").invalid),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(20,20,"gateway.update-config")),t.ɵɵproperty("disabled",a.isReadOnlyForm),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(24,22,"gateway.delete")),t.ɵɵproperty("disabled",a.isReadOnlyForm)}}function l$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",44),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.exportConfig())})),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,3,"gateway.download-tip")),t.ɵɵproperty("disabled",!e.gatewayConfigurationGroup.dirty||e.gatewayConfigurationGroup.invalid),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,5,"action.download")," ")}}function p$(e,n){if(1&e&&(t.ɵɵelementStart(0,"button",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,3,"gateway.save-tip")),t.ɵɵproperty("disabled",!e.gatewayConfigurationGroup.dirty||e.gatewayConfigurationGroup.invalid),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,5,"action.save")," ")}}class c$ extends q{constructor(e,t,n,i,a,r,o,s,l,p,c){super(e),this.store=e,this.elementRef=t,this.utils=n,this.ngZone=i,this.fb=a,this.window=r,this.dialog=o,this.translate=s,this.deviceService=l,this.attributeService=p,this.importExport=c,this.alignment=!0,this.layoutGap=!0,this.securityTypes=bW,this.gatewayLogLevels=Object.keys(wW).map((e=>wW[e])),this.connectorTypes=Object.keys(_W),this.storageTypes=CW,this.toastTargetId="gateway-configuration-widget"+this.utils.guid(),this.isReadOnlyForm=!1}get connectors(){return this.gatewayConfigurationGroup.get("connectors")}ngOnInit(){this.initWidgetSettings(this.ctx.settings),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(){this.formResize$&&this.formResize$.disconnect(),this.subscribeGateway$.unsubscribe(),this.subscribeStorageType$.unsubscribe()}initWidgetSettings(e){let t;t=e.gatewayTitle&&e.gatewayTitle.length?this.utils.customTranslation(e.gatewayTitle,e.gatewayTitle):this.translate.instant("gateway.gateway"),this.ctx.widgetTitle=t,this.isReadOnlyForm=!!e.readOnly&&e.readOnly,this.archiveFileName=e.archiveFileName?.length?e.archiveFileName:"gatewayConfiguration",this.gatewayType=e.gatewayType?.length?e.gatewayType:"Gateway",this.gatewayNameExists=this.utils.customTranslation(e.gatewayNameExists,e.gatewayNameExists)||this.translate.instant("gateway.gateway-exists"),this.successfulSaved=this.utils.customTranslation(e.successfulSave,e.successfulSave)||this.translate.instant("gateway.gateway-saved"),this.updateWidgetDisplaying()}resize(){this.ngZone.run((()=>{this.updateWidgetDisplaying(),this.ctx.detectChanges()}))}updateWidgetDisplaying(){this.ctx.$container&&this.ctx.$container[0].offsetWidth<=425?(this.layoutGap=!1,this.alignment=!1):(this.layoutGap=!0,this.alignment=!0)}saveAttribute(e,t,n){const i=this.gatewayConfigurationGroup.get("gateway").value,a={key:e,value:t};return this.attributeService.saveEntityAttributes(kW(i),n,[a])}createConnector(e=TW){this.connectors.push(this.fb.group({enabled:[e.enabled],configType:[e.configType,[me.required]],name:[e.name,[me.required]],config:[e.config,[me.nullValidator,IW]]}))}getFormField(e){return this.gatewayConfigurationGroup.get(e)}buildForm(){this.gatewayConfigurationGroup=this.fb.group({gateway:[null,[]],accessToken:[null,[me.required]],securityType:[yW.accessToken],host:[this.window.location.hostname,[me.required]],port:[1883,[me.required,me.min(1),me.max(65535),me.pattern(/^-?[0-9]+$/)]],remoteConfiguration:[!0],caCertPath:["/etc/thingsboard-gateway/ca.pem"],privateKeyPath:["/etc/thingsboard-gateway/privateKey.pem"],certPath:["/etc/thingsboard-gateway/certificate.pem"],remoteLoggingLevel:[wW.debug],remoteLoggingPathToLogs:["./logs/",[me.required]],storageType:[SW.memory],readRecordsCount:[100,[me.required,me.min(1),me.pattern(/^-?[0-9]+$/)]],maxRecordsCount:[1e4,[me.required,me.min(1),me.pattern(/^-?[0-9]+$/)]],maxFilesCount:[5,[me.required,me.min(1),me.pattern(/^-?[0-9]+$/)]],dataFolderPath:["./data/",[me.required]],connectors:this.fb.array([])}),this.isReadOnlyForm&&this.gatewayConfigurationGroup.disable({emitEvent:!1}),this.subscribeStorageType$=this.getFormField("storageType").valueChanges.subscribe((e=>{e===SW.memory?(this.getFormField("maxFilesCount").disable(),this.getFormField("dataFolderPath").disable()):(this.getFormField("maxFilesCount").enable(),this.getFormField("dataFolderPath").enable())})),this.subscribeGateway$=this.getFormField("gateway").valueChanges.subscribe((e=>{null!==e?Ie([this.deviceService.getDeviceCredentials(e).pipe(Ae((e=>{this.getFormField("accessToken").patchValue(e.credentialsId)}))),...this.getAttributes(e)]).subscribe((()=>{this.gatewayConfigurationGroup.markAsPristine(),this.ctx.detectChanges()})):this.getFormField("accessToken").patchValue("")}))}gatewayExist(){this.ctx.showErrorToast(this.gatewayNameExists,"top","left",this.toastTargetId)}exportConfig(){const e=this.gatewayConfigurationGroup.value,t={};var n,i,a;t["tb_gateway.yaml"]=function(e){let t;t="thingsboard:\n",t+=" host: "+e.host+"\n",t+=" remoteConfiguration: "+e.remoteConfiguration+"\n",t+=" port: "+e.port+"\n",t+=" security:\n",e.securityType===yW.accessToken?t+=" access-token: "+e.accessToken+"\n":(t+=" ca_cert: "+e.caCertPath+"\n",t+=" privateKey: "+e.privateKeyPath+"\n",t+=" cert: "+e.certPath+"\n"),t+="storage:\n",e.storageType===SW.memory?(t+=" type: memory\n",t+=" read_records_count: "+e.readRecordsCount+"\n",t+=" max_records_count: "+e.maxRecordsCount+"\n"):(t+=" type: file\n",t+=" data_folder_path: "+e.dataFolderPath+"\n",t+=" max_file_count: "+e.maxFilesCount+"\n",t+=" max_read_records_count: "+e.readRecordsCount+"\n",t+=" max_records_per_file: "+e.maxRecordsCount+"\n"),t+="connectors:\n";for(const n of e.connectors)n.enabled&&(t+=" -\n",t+=" name: "+n.name+"\n",t+=" type: "+n.configType+"\n",t+=" configuration: "+EW(n.name)+"\n");return t}(e),function(e,t){for(const n of t)n.enabled&&(e[EW(n.name)]=JSON.stringify(n.config))}(t,e.connectors),n=t,i=e.remoteLoggingLevel,a=e.remoteLoggingPathToLogs,n["logs.conf"]=MW(i,a),this.importExport.exportJSZip(t,this.archiveFileName),this.saveAttribute(xW,this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),D.SHARED_SCOPE)}addNewConnector(){this.createConnector()}removeConnector(e){e>-1&&(this.connectors.removeAt(e),this.connectors.markAsDirty())}openConfigDialog(e,t,n,i){e&&(e.stopPropagation(),e.preventDefault()),this.dialog.open(nt,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{jsonValue:n,required:!0,title:this.translate.instant("gateway.title-connectors-json",{typeName:i})}}).afterClosed().subscribe((e=>{e&&(this.connectors.at(t).get("config").patchValue(e),this.ctx.detectChanges())}))}createConnectorName(e,t,n=0){const i=n?t+n:t;return-1===e.findIndex((e=>e.name===i))?i:this.createConnectorName(e,t,++n)}validateConnectorName(e,t,n,i=0){for(let a=0;a{this.ctx.showSuccessToast(this.successfulSaved,2e3,"top","left",this.toastTargetId),this.gatewayConfigurationGroup.markAsPristine()}))}getAttributes(e){const t=[];return t.push(Ie([this.getAttribute("current_configuration",D.CLIENT_SCOPE,e),this.getAttribute(vW,D.SERVER_SCOPE,e)]).pipe(Ae((([e,t])=>{this.setFormGatewaySettings(e),this.setFormConnectorsDraft(t),this.isReadOnlyForm&&this.gatewayConfigurationGroup.disable({emitEvent:!1})})))),t.push(this.getAttribute(xW,D.SHARED_SCOPE,e).pipe(Ae((e=>this.processLoggingLevel(e))))),t}getAttribute(e,t,n){return this.attributeService.getEntityAttributes(kW(n),t,[e])}setFormGatewaySettings(e){if(this.connectors.clear(),e.length>0){const t=JSON.parse(window.atob(e[0].value));for(const e of Object.keys(t)){const n=t[e];if("thingsboard"===e)null!==n&&Object.keys(n).length>0&&this.gatewayConfigurationGroup.patchValue(PW(n));else for(const t of Object.keys(n)){let i="No name";Object.prototype.hasOwnProperty.call(n[t],"name")&&(i=n[t].name);const a={enabled:!0,configType:e,config:n[t].config,name:i};this.createConnector(a)}}}}setFormConnectorsDraft(e){if(e.length>0){const t=JSON.parse(window.atob(e[0].value));for(const e of Object.keys(t)){const n={enabled:!1,configType:t[e].connector,config:t[e].config,name:e};this.createConnector(n)}}}processLoggingLevel(e){let t=wW.debug;e.length>0&&wW[e[0].value.toLowerCase()]&&(t=wW[e[0].value.toLowerCase()]),this.getFormField("remoteLoggingLevel").patchValue(t)}static{this.ɵfac=function(e){return new(e||c$)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(t.ElementRef),t.ɵɵdirectiveInject(K.UtilsService),t.ɵɵdirectiveInject(t.NgZone),t.ɵɵdirectiveInject(de.UntypedFormBuilder),t.ɵɵdirectiveInject($),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(le.TranslateService),t.ɵɵdirectiveInject(K.DeviceService),t.ɵɵdirectiveInject(K.AttributeService),t.ɵɵdirectiveInject(it.ImportExportService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:c$,selectors:[["tb-gateway-form"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(FW,7),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.formContainerRef=e.first)}},inputs:{ctx:"ctx",isStateForm:"isStateForm"},standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:104,vars:104,consts:[["formContainer",""],["tb-toast","",1,"gateway-form",3,"ngSubmit","formGroup","toastTarget"],["multi","true",1,"mat-body-2"],[1,"tb-panel-title"],["formControlName","gateway","required","",3,"gatewayNameExist","deviceName","isStateForm","newGatewayType"],[1,"flex"],["formControlName","securityType"],[3,"value",4,"ngFor","ngForOf"],[1,"flex-1"],["matInput","","type","text","formControlName","host"],["translate","",4,"ngIf"],["matInput","","type","number","formControlName","port"],["class","flex flex-col",4,"ngIf"],["formControlName","remoteConfiguration"],["formControlName","remoteLoggingLevel"],["matInput","","type","text","formControlName","remoteLoggingPathToLogs"],[1,"flex","flex-col"],["formControlName","storageType"],["matInput","","type","number","formControlName","readRecordsCount"],["matInput","","type","number","formControlName","maxRecordsCount"],["class","flex",3,"class",4,"ngIf"],[1,"gateway-config","flex","flex-col"],["formArrayName","connectors",4,"ngFor","ngForOf"],[1,"no-data-found","items-center","justify-center"],["mat-raised-button","","type","button","matTooltipPosition","above",3,"click","matTooltip"],[1,"form-action-buttons","flex","flex-row","items-center","justify-end"],["mat-raised-button","","color","primary","type","button",3,"disabled","matTooltip","click",4,"ngIf"],["mat-raised-button","","color","primary","type","submit",3,"disabled","matTooltip",4,"ngIf"],[3,"value"],["translate",""],["matInput","","type","text","formControlName","caCertPath"],["matInput","","type","text","formControlName","privateKeyPath"],["matInput","","type","text","formControlName","certPath"],["matInput","","type","number","formControlName","maxFilesCount"],["matInput","","type","text","formControlName","dataFolderPath"],["formArrayName","connectors"],[1,"flex","flex-row","items-stretch","justify-between","gap-2",3,"formGroupName"],[1,"flex","flex-col","justify-center"],["formControlName","enabled"],[1,"flex-full","flex"],["formControlName","configType",3,"selectionChange"],["matInput","","type","text","formControlName","name",3,"blur"],[1,"action-buttons","flex"],["mat-icon-button","","matTooltipPosition","above",3,"click","disabled","matTooltip"],["mat-raised-button","","color","primary","type","button",3,"click","disabled","matTooltip"],["mat-raised-button","","color","primary","type","submit",3,"disabled","matTooltip"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"form",1,0),t.ɵɵlistener("ngSubmit",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.save())})),t.ɵɵelementStart(2,"mat-accordion",2)(3,"mat-expansion-panel")(4,"mat-expansion-panel-header")(5,"mat-panel-title")(6,"div",3),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵpipe(9,"uppercase"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"tb-entity-gateway-select",4),t.ɵɵlistener("gatewayNameExist",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.gatewayExist())})),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",5)(12,"mat-label"),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-select",6),t.ɵɵtemplate(16,NW,3,4,"mat-option",7),t.ɵɵpipe(17,"keyvalue"),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",5)(19,"mat-form-field",8)(20,"mat-label"),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(23,"input",9),t.ɵɵtemplate(24,LW,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",8)(26,"mat-label"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(29,"input",11),t.ɵɵtemplate(30,VW,2,0,"mat-error",10)(31,qW,2,0,"mat-error",10)(32,GW,2,0,"mat-error",10)(33,zW,2,0,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵtemplate(34,UW,16,9,"div",12),t.ɵɵelementStart(35,"mat-checkbox",13),t.ɵɵtext(36),t.ɵɵpipe(37,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"div",5)(39,"mat-form-field",8)(40,"mat-label"),t.ɵɵtext(41),t.ɵɵpipe(42,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(43,"mat-select",14),t.ɵɵtemplate(44,jW,2,2,"mat-option",7),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"mat-form-field",8)(46,"mat-label"),t.ɵɵtext(47),t.ɵɵpipe(48,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(49,"input",15),t.ɵɵtemplate(50,HW,2,0,"mat-error",10),t.ɵɵelementEnd()()(),t.ɵɵelementStart(51,"mat-expansion-panel")(52,"mat-expansion-panel-header")(53,"mat-panel-title")(54,"div",3),t.ɵɵtext(55),t.ɵɵpipe(56,"translate"),t.ɵɵpipe(57,"uppercase"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(58,"div",16)(59,"mat-form-field")(60,"mat-label"),t.ɵɵtext(61),t.ɵɵpipe(62,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(63,"mat-select",17),t.ɵɵtemplate(64,WW,3,4,"mat-option",7),t.ɵɵpipe(65,"keyvalue"),t.ɵɵelementEnd()(),t.ɵɵelementStart(66,"div",5)(67,"mat-form-field",8)(68,"mat-label"),t.ɵɵtext(69),t.ɵɵpipe(70,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(71,"input",18),t.ɵɵtemplate(72,$W,2,0,"mat-error",10)(73,KW,2,0,"mat-error",10)(74,YW,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(75,"mat-form-field",8)(76,"mat-label"),t.ɵɵtext(77),t.ɵɵpipe(78,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(79,"input",19),t.ɵɵtemplate(80,XW,2,0,"mat-error",10)(81,ZW,2,0,"mat-error",10)(82,QW,2,0,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵtemplate(83,i$,15,16,"div",20),t.ɵɵelementEnd()(),t.ɵɵelementStart(84,"mat-expansion-panel")(85,"mat-expansion-panel-header")(86,"mat-panel-title")(87,"div",3),t.ɵɵtext(88),t.ɵɵpipe(89,"translate"),t.ɵɵpipe(90,"uppercase"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(91,"div",21),t.ɵɵtemplate(92,s$,27,32,"section",22),t.ɵɵelementStart(93,"span",23),t.ɵɵtext(94),t.ɵɵpipe(95,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(96,"div")(97,"button",24),t.ɵɵpipe(98,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addNewConnector())})),t.ɵɵtext(99),t.ɵɵpipe(100,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(101,"section",25),t.ɵɵtemplate(102,l$,4,7,"button",26)(103,p$,4,7,"button",27),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("toastTarget",n.toastTargetId),t.ɵɵproperty("formGroup",n.gatewayConfigurationGroup),t.ɵɵadvance(7),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,54,t.ɵɵpipeBind1(8,52,"gateway.thingsboard"))),t.ɵɵadvance(3),t.ɵɵproperty("deviceName",n.deviceNameForm)("isStateForm",n.isStateForm)("newGatewayType",n.gatewayType),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,56,"gateway.security-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(17,58,n.securityTypes)),t.ɵɵadvance(2),t.ɵɵclassMap(t.ɵɵpureFunction3(92,RW,n.layoutGap,n.alignment,!n.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(22,60,"gateway.thingsboard-host")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("host").hasError("required")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(28,62,"gateway.thingsboard-port")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("max")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("pattern")),t.ɵɵadvance(),t.ɵɵproperty("ngIf","tls"===n.gatewayConfigurationGroup.get("securityType").value),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(37,64,"gateway.remote")),t.ɵɵadvance(2),t.ɵɵclassMap(t.ɵɵpureFunction3(96,RW,n.layoutGap,n.alignment,!n.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(42,66,"gateway.remote-logging-level")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.gatewayLogLevels),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(48,68,"gateway.path-logs")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("remoteLoggingPathToLogs").hasError("required")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(57,72,t.ɵɵpipeBind1(56,70,"gateway.storage"))),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(62,74,"gateway.storage-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(65,76,n.storageTypes)),t.ɵɵadvance(2),t.ɵɵclassMap(t.ɵɵpureFunction3(100,RW,n.layoutGap,n.alignment,!n.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(70,78,"gateway.storage-pack-size")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("readRecordsCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("readRecordsCount").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("readRecordsCount").hasError("pattern")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(78,80,"file"!==n.gatewayConfigurationGroup.get("storageType").value?"gateway.storage-max-records":"gateway.storage-max-file-records")," "),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("maxRecordsCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("maxRecordsCount").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("maxRecordsCount").hasError("pattern")),t.ɵɵadvance(),t.ɵɵproperty("ngIf","file"===n.gatewayConfigurationGroup.get("storageType").value),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(90,84,t.ɵɵpipeBind1(89,82,"gateway.connectors-config"))),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",n.connectors.controls),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.connectors.length),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(95,86,"gateway.no-connectors")),t.ɵɵadvance(3),t.ɵɵclassProp("!hidden",n.isReadOnlyForm),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(98,88,"gateway.connector-add")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(100,90,"action.add")," "),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.isReadOnlyForm),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.gatewayConfigurationGroup.get("remoteConfiguration").value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("remoteConfiguration").value))},dependencies:t.ɵɵgetComponentDepsFactory(c$,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .gateway-form[_ngcontent-%COMP%]{height:100%;padding:5px;background-color:transparent;overflow-y:auto;overflow-x:hidden}[_nghost-%COMP%] .gateway-form[_ngcontent-%COMP%] .form-action-buttons[_ngcontent-%COMP%]{padding-top:8px}[_nghost-%COMP%] .gateway-form[_ngcontent-%COMP%] .gateway-config[_ngcontent-%COMP%] .no-data-found[_ngcontent-%COMP%]{position:relative;display:flex;height:40px}']})}}e("GatewayFormComponent",c$);class d${transform(e,t){return Ya.parseVersion(e)>=Ya.parseVersion(Ai.get(t))}static{this.ɵfac=function(e){return new(e||d$)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"isLatestVersionConfig",type:d$,pure:!0,standalone:!0})}}class u${constructor(e){this.translate=e}transform(e){return e.hasError("required")?this.translate.instant("gateway.port-required"):e.hasError("min")||e.hasError("max")?this.translate.instant("gateway.port-limits-error",{min:Oi.MIN,max:Oi.MAX}):""}static{this.ɵfac=function(e){return new(e||u$)(t.ɵɵdirectiveInject(le.TranslateService,16))}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"getGatewayPortTooltip",type:u$,pure:!0,standalone:!0})}}class m${transform(e,t,n,i){switch(e){case dt.OPCUA:return this.getOpcConnectorHelpLink(t,n);case dt.MQTT:return this.getMqttConnectorHelpLink(t,n,i);case dt.BACNET:return this.getBacnetConnectorHelpLink(t,n);case dt.REST:return this.getRestConnectorHelpLink(n)}}getOpcConnectorHelpLink(e,t){if(t!==Fi.CONST)return`widget/lib/gateway/${e}-${t}_fn`}getMqttConnectorHelpLink(e,t,n){if(t!==oi.CONST)return n?e!==Ui.ATTRIBUTES&&e!==Ui.TIMESERIES||n!==ri.JSON?`widget/lib/gateway/mqtt-${n}-expression_fn`:"widget/lib/gateway/mqtt-json-key-expression_fn":"widget/lib/gateway/mqtt-expression_fn"}getBacnetConnectorHelpLink(e,t){if(t!==Fi.CONST)return`widget/lib/gateway/bacnet-device-${e}-${t}_fn`}getRestConnectorHelpLink(e){if(e!==Si.CONST)return"widget/lib/gateway/rest-json_fn"}static{this.ɵfac=function(e){return new(e||m$)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"getConnectorMappingHelpLink",type:m$,pure:!0,standalone:!0})}}function h$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.gatewayConnectorDefaultTypesTranslatesMap.get(e)," ")}}function g$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",22),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.connectorForm.get("name").hasError("duplicateName")?"gateway.connector-duplicate-name":"gateway.name-required"))}}function f$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"div",9),t.ɵɵtext(2,"gateway.connectors-table-class"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",10),t.ɵɵelement(4,"input",23),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,1,"gateway.set")))}function y$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"div",9),t.ɵɵtext(2,"gateway.connectors-table-key"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",10),t.ɵɵelement(4,"input",24),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,1,"gateway.set")))}function v$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function x$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"mat-slide-toggle",25)(2,"mat-label",26),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.fill-connector-defaults-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.fill-connector-defaults")," "))}function b$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"mat-slide-toggle",27)(2,"mat-label",26),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.send-change-data-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.send-change-data")," "))}class w$ extends A{constructor(e,t,n,i,a,r){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.isLatestVersionConfig=r,this.connectorType=dt,this.gatewayConnectorDefaultTypesTranslatesMap=ut,this.gatewayLogLevel=Object.values(pt),this.submitted=!1,this.destroy$=new we,this.updateConnectorTypesByVersion(),this.connectorForm=this.fb.group({type:[dt.MQTT,[]],name:["",[me.required,this.uniqNameRequired(),me.pattern(pn)]],logLevel:[pt.INFO,[]],useDefaults:[!0,[]],sendDataOnlyOnChange:[!1,[]],class:["",[]],key:["auto",[]]})}ngOnInit(){this.observeTypeChange()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}helpLinkId(){return O+"/docs/iot-gateway/configuration/"}cancel(){this.dialogRef.close(null)}add(){this.submitted=!0;const e=this.connectorForm.getRawValue();if(e.useDefaults){const t=Xt(e.type),n=this.data.gatewayVersion;n&&(e.configVersion=n),e.configurationJson=(this.isLatestVersionConfig.transform(n,e.type)?t[Ai.get(e.type)]:t[ct.Legacy])??t,this.connectorForm.valid&&this.dialogRef.close(e)}else this.connectorForm.valid&&this.dialogRef.close(e)}uniqNameRequired(){return e=>{const t=e.value.trim().toLowerCase();return this.data.dataSourceData.some((({value:{name:e}})=>e.toLowerCase()===t))?{duplicateName:{valid:!1}}:null}}updateConnectorTypesByVersion(){const e=mt.keys();for(let t of e)if(t===ct.Legacy||Ya.parseVersion(this.data.gatewayVersion)>=Ya.parseVersion(t)){this.connectorTypes=mt.get(t);break}}observeTypeChange(){this.connectorForm.get("type").valueChanges.pipe(Ae((e=>{const t=this.connectorForm.get("useDefaults");e===dt.GRPC||e===dt.CUSTOM?t.setValue(!1):t.value||t.setValue(!0)})),Me(this.destroy$)).subscribe()}static{this.ɵfac=function(e){return new(e||w$)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(d$))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:w$,selectors:[["tb-add-connector-dialog"]],standalone:!0,features:[t.ɵɵProvidersFeature([d$]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:43,vars:25,consts:[[1,"add-connector",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","autocomplete","off","name","value","formControlName","name",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf"],["formControlName","logLevel"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","name","value","formControlName","class",3,"placeholder"],["matInput","","name","value","formControlName","key",3,"placeholder"],["formControlName","useDefaults",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"mat-toolbar",1)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",2)(6,"div",3),t.ɵɵelementStart(7,"button",4),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",5),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",6)(11,"div",7)(12,"div",8)(13,"div",9),t.ɵɵtext(14,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-form-field",10)(16,"mat-select",11),t.ɵɵtemplate(17,h$,2,2,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(18,"div",8)(19,"div",13),t.ɵɵtext(20,"gateway.name"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",10),t.ɵɵelement(22,"input",14),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,g$,3,3,"mat-icon",15),t.ɵɵelementEnd()(),t.ɵɵtemplate(25,f$,6,3,"div",16)(26,y$,6,3,"div",16),t.ɵɵelementStart(27,"div",8)(28,"div",9),t.ɵɵtext(29,"gateway.remote-logging-level"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",10)(31,"mat-select",17),t.ɵɵtemplate(32,v$,2,2,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵtemplate(33,x$,6,6,"div",16)(34,b$,6,6,"div",16),t.ɵɵpipe(35,"withReportStrategy"),t.ɵɵelementEnd()(),t.ɵɵelementStart(36,"div",18)(37,"button",19),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(38),t.ɵɵpipe(39,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(40,"button",20),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(41),t.ɵɵpipe(42,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.connectorForm),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,14,"gateway.add-connector")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.helpLinkId()),t.ɵɵadvance(11),t.ɵɵproperty("ngForOf",n.connectorTypes),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,16,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.connectorForm.get("name").hasError("required")&&n.connectorForm.get("name").touched||n.connectorForm.get("name").hasError("duplicateName")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value===n.connectorType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value===n.connectorType.GRPC),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.gatewayLogLevel),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value!==n.connectorType.GRPC&&n.connectorForm.get("type").value!==n.connectorType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value===n.connectorType.MQTT&&!t.ɵɵpipeBind2(35,18,n.data.gatewayVersion,n.connectorType.MQTT)),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(39,21,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.connectorForm.invalid||!n.connectorForm.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(42,23,"action.add")," "))},dependencies:t.ɵɵgetComponentDepsFactory(w$,[se,_,tr]),styles:['@charset "UTF-8";[_nghost-%COMP%] .add-connector[_ngcontent-%COMP%]{min-width:400px;width:500px}']})}}e("AddConnectorDialogComponent",w$);const S$=()=>({maxWidth:"970px"});function C$(e,n){1&e&&(t.ɵɵelementStart(0,"div",4),t.ɵɵtext(1,"gateway.device-info.source"),t.ɵɵelementEnd())}function _$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function T$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"mat-form-field",18)(2,"mat-select",19),t.ɵɵtemplate(3,_$,3,4,"mat-option",20),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.sourceTypes)}}function I$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",22),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-info.device-name-expression-required"))}function E$(e,n){if(1&e&&(t.ɵɵelement(0,"div",23),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,e.connectorType,"name-field",e.mappingFormGroup.get("deviceNameExpressionSource").value,e.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,S$))}}function M$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function k$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"mat-form-field",18)(2,"mat-select",26),t.ɵɵtemplate(3,M$,3,4,"mat-option",20),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.sourceTypes)}}function P$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",22),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-info.device-profile-expression-required"))}function D$(e,n){if(1&e&&(t.ɵɵelement(0,"div",23),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,e.connectorType,"profile-name",e.mappingFormGroup.get("deviceProfileExpressionSource").value,e.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,S$))}}function O$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",24)(1,"div",9),t.ɵɵtext(2,"gateway.device-info.profile-name"),t.ɵɵelementEnd(),t.ɵɵtemplate(3,k$,4,1,"div",10),t.ɵɵelementStart(4,"div",11)(5,"mat-form-field",12),t.ɵɵelement(6,"input",25),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,P$,3,3,"mat-icon",14)(9,D$,2,8,"div",15),t.ɵɵpipe(10,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.useSource),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,4,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingFormGroup.get("deviceProfileExpression").hasError("required")&&e.mappingFormGroup.get("deviceProfileExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(10,6,e.connectorType,"profile-name",e.mappingFormGroup.get("deviceProfileExpressionSource").value,e.convertorType))}}class A$ extends q{get deviceInfoType(){return this.deviceInfoTypeValue}set deviceInfoType(e){this.deviceInfoTypeValue!==e&&(this.deviceInfoTypeValue=e)}constructor(e,t,n,i){super(e),this.store=e,this.translate=t,this.dialog=n,this.fb=i,this.SourceTypeTranslationsMap=zi,this.DeviceInfoType=Sa,this.useSource=!0,this.required=!1,this.connectorType=dt.MQTT,this.sourceTypes=Object.values(oi),this.destroy$=new we,this.propagateChange=e=>{}}ngOnInit(){this.mappingFormGroup=this.fb.group({deviceNameExpression:["",this.required?[me.required,me.pattern(pn)]:[me.pattern(pn)]]}),this.useSource&&this.mappingFormGroup.addControl("deviceNameExpressionSource",this.fb.control(this.sourceTypes[0],[])),this.deviceInfoType===Sa.FULL&&(this.useSource&&this.mappingFormGroup.addControl("deviceProfileExpressionSource",this.fb.control(this.sourceTypes[0],[])),this.mappingFormGroup.addControl("deviceProfileExpression",this.fb.control("",this.required?[me.required,me.pattern(pn)]:[me.pattern(pn)]))),this.mappingFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.updateView(e)}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}writeValue(e){this.mappingFormGroup.patchValue(e,{emitEvent:!1})}validate(){return this.mappingFormGroup.valid?null:{mappingForm:{valid:!1}}}updateView(e){this.propagateChange(e)}static{this.ɵfac=function(e){return new(e||A$)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(le.TranslateService),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:A$,selectors:[["tb-device-info-table"]],inputs:{useSource:"useSource",required:"required",connectorType:"connectorType",convertorType:"convertorType",sourceTypes:"sourceTypes",deviceInfoType:"deviceInfoType"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>A$)),multi:!0},{provide:he,useExisting:c((()=>A$)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:23,vars:18,consts:[[1,"tb-form-panel","stroked",3,"formGroup"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-table","no-padding","no-gap"],[1,"tb-form-table-header"],["translate","",1,"tb-form-table-header-cell","w-1/5"],["class","tb-form-table-header-cell w-1/5","translate","",4,"ngIf"],["translate","",1,"tb-form-table-header-cell","w-1/2"],[1,"tb-form-table-body","no-gap"],[1,"tb-form-table-row","tb-form-row","no-border","same-padding","pt-4"],["translate","",1,"tb-required","w-1/5"],["class","no-gap w-1/5",4,"ngIf"],[1,"tb-form-table-row-cell","tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","deviceNameExpression",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style",4,"ngIf"],["class","tb-form-table-row tb-form-row no-border same-padding pb-4",4,"ngIf"],[1,"no-gap","w-1/5"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","!w-full"],["formControlName","deviceNameExpressionSource"],[3,"value",4,"ngFor","ngForOf"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"tb-form-table-row","tb-form-row","no-border","same-padding","pb-4"],["matInput","","name","value","formControlName","deviceProfileExpression",3,"placeholder"],["formControlName","deviceProfileExpressionSource"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2,"device.device"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",2)(4,"div",3)(5,"div",4),t.ɵɵtext(6,"gateway.device-info.entity-field"),t.ɵɵelementEnd(),t.ɵɵtemplate(7,C$,2,0,"div",5),t.ɵɵelementStart(8,"div",6),t.ɵɵtext(9," gateway.device-info.expression "),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",7)(11,"div",8)(12,"div",9),t.ɵɵtext(13,"gateway.device-info.name"),t.ɵɵelementEnd(),t.ɵɵtemplate(14,T$,4,1,"div",10),t.ɵɵelementStart(15,"div",11)(16,"mat-form-field",12),t.ɵɵelement(17,"input",13),t.ɵɵpipe(18,"translate"),t.ɵɵtemplate(19,I$,3,3,"mat-icon",14)(20,E$,2,8,"div",15),t.ɵɵpipe(21,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(22,O$,11,11,"div",16),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingFormGroup),t.ɵɵadvance(),t.ɵɵclassProp("tb-required",n.required),t.ɵɵadvance(6),t.ɵɵproperty("ngIf",n.useSource),t.ɵɵadvance(4),t.ɵɵclassProp("pb-4",n.deviceInfoType!==n.DeviceInfoType.FULL),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.useSource),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(18,11,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.mappingFormGroup.get("deviceNameExpression").hasError("required")&&n.mappingFormGroup.get("deviceNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(21,13,n.connectorType,"name-field",n.mappingFormGroup.get("deviceNameExpressionSource").value,n.convertorType)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceInfoType===n.DeviceInfoType.FULL))},dependencies:t.ɵɵgetComponentDepsFactory(A$,[se,_,m$]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}'],changeDetection:d.OnPush})}}He([I()],A$.prototype,"useSource",void 0),He([I()],A$.prototype,"required",void 0);const F$=()=>({maxWidth:"970px"});function R$(e,n){if(1&e&&t.ɵɵelement(0,"mat-icon",19),2&e){let e;const n=t.ɵɵnextContext();t.ɵɵproperty("svgIcon",null==(e=n.valueTypes.get(n.valueTypeFormGroup.get("type").value))?null:e.icon)}}function B$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵelement(1,"mat-icon",22),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){let e,i;const a=n.$implicit,r=t.ɵɵnextContext(2);t.ɵɵproperty("value",a),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("svgIcon",null==(e=r.valueTypes.get(a))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,null==(i=r.valueTypes.get(a))?null:i.name))}}function N$(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,B$,5,5,"mat-option",20),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.valueTypeKeys)}}function L$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-option",23)(1,"span"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.raw")))}function V$(e,n){1&e&&(t.ɵɵelement(0,"input",24),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function q$(e,n){1&e&&(t.ɵɵelement(0,"input",25),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function G$(e,n){1&e&&(t.ɵɵelement(0,"input",26),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function z$(e,n){1&e&&(t.ɵɵelement(0,"input",27),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function U$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-select",28)(1,"mat-option",21),t.ɵɵtext(2,"true"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-option",21),t.ɵɵtext(4,"false"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵproperty("value",!0),t.ɵɵadvance(2),t.ɵɵproperty("value",!1))}function j$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",29),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function H$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",30),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("tb-help-popup",e.helpLink)("tb-help-popup-style",t.ɵɵpureFunction0(2,F$))}}class W${constructor(e){this.fb=e,this.valueTypeKeys=Object.values(en),this.valueTypes=tn,this.MappingValueType=en,this.destroy$=new we,this.onChange=e=>{},this.valueTypeFormGroup=this.fb.group({type:[en.STRING],stringValue:[{value:"",disabled:this.rawData},[me.required,me.pattern(pn)]],integerValue:[{value:0,disabled:!0},[me.required,me.pattern(cn)]],doubleValue:[{value:0,disabled:!0},[me.required]],booleanValue:[{value:!1,disabled:!0},[me.required]],rawValue:[{value:"",disabled:!this.rawData},[me.required,me.pattern(pn)]]}),this.valueTypeFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((({type:e,...t})=>{this.onChange({type:e,value:t[e+"Value"]})})),this.observeTypeChange()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}observeTypeChange(){this.valueTypeFormGroup.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.toggleTypeInputs(e)))}toggleTypeInputs(e){this.valueTypeFormGroup.disable({emitEvent:!1}),this.valueTypeFormGroup.get("type").enable({emitEvent:!1}),this.valueTypeFormGroup.get(e+"Value").enable({emitEvent:!1})}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){const t=this.getValueType(e?.value),n={stringValue:"",rawValue:"",integerValue:0,doubleValue:0,booleanValue:!1,type:t};n[t+"Value"]=e?.value,this.toggleTypeInputs(t),this.valueTypeFormGroup.patchValue(n,{emitEvent:!1})}validate(){return this.valueTypeFormGroup.valid?null:{valueTypeFormGroup:{valid:!1}}}getValueType(e){if(this.rawData)return"raw";switch(typeof e){case"boolean":return en.BOOLEAN;case"number":return Number.isInteger(e)?en.INTEGER:en.DOUBLE;default:return en.STRING}}static{this.ɵfac=function(e){return new(e||W$)(t.ɵɵdirectiveInject(de.UntypedFormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:W$,selectors:[["tb-type-value-field"]],inputs:{rawData:"rawData",helpLink:"helpLink"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>W$)),multi:!0},{provide:he,useExisting:c((()=>W$)),multi:!0}]),t.ɵɵStandaloneFeature],decls:29,vars:17,consts:[["raw",""],[3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","type"],[1,"tb-flex","align-center"],["class","tb-mat-18",3,"svgIcon",4,"ngIf"],[4,"ngIf","ngIfElse"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","flex","tb-suffix-absolute"],[3,"ngSwitch"],["matInput","","required","","formControlName","stringValue",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","rawValue",3,"placeholder",4,"ngSwitchCase"],["formControlName","booleanValue",4,"ngSwitchCase"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style","click",4,"ngIf"],[1,"tb-mat-18",3,"svgIcon"],[3,"value",4,"ngFor","ngForOf"],[3,"value"],[1,"tb-mat-20",3,"svgIcon"],["value","raw"],["matInput","","required","","formControlName","stringValue",3,"placeholder"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder"],["matInput","","required","","formControlName","rawValue",3,"placeholder"],["formControlName","booleanValue"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"click","tb-help-popup","tb-help-popup-style"]],template:function(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,1),t.ɵɵelementStart(1,"div",2)(2,"div",3),t.ɵɵtext(3,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",4)(5,"mat-form-field",5)(6,"mat-select",6)(7,"mat-select-trigger")(8,"div",7),t.ɵɵtemplate(9,R$,1,1,"mat-icon",8),t.ɵɵelementStart(10,"span"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(14,N$,2,1,"ng-container",9)(15,L$,4,3,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(17,"div",2)(18,"div",3),t.ɵɵtext(19,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(20,"mat-form-field",10),t.ɵɵelementContainerStart(21,11),t.ɵɵtemplate(22,V$,2,3,"input",12)(23,q$,2,3,"input",13)(24,G$,2,3,"input",14)(25,z$,2,3,"input",15)(26,U$,5,2,"mat-select",16),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(27,j$,3,3,"mat-icon",17)(28,H$,1,3,"div",18),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e){let e,i;const a=t.ɵɵreference(16);t.ɵɵproperty("formGroup",n.valueTypeFormGroup),t.ɵɵadvance(9),t.ɵɵproperty("ngIf",!n.rawData),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,13,null==(e=n.valueTypes.get(n.valueTypeFormGroup.get("type").value))?null:e.name)||t.ɵɵpipeBind1(13,15,"gateway.raw")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",!n.rawData)("ngIfElse",a),t.ɵɵadvance(7),t.ɵɵproperty("ngSwitch",n.valueTypeFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.STRING),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.INTEGER),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.DOUBLE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase","raw"),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.BOOLEAN),t.ɵɵadvance(),t.ɵɵproperty("ngIf",(null==(i=n.valueTypeFormGroup.get(n.valueTypeFormGroup.get("type").value))?null:i.hasError("required"))&&n.valueTypeFormGroup.get(n.valueTypeFormGroup.get("type").value).touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.helpLink)}},dependencies:t.ɵɵgetComponentDepsFactory(W$,[_,se]),styles:['@charset "UTF-8";[_nghost-%COMP%]{gap:16px;display:grid;width:100%}']})}}function $$(e,n){if(1&e&&t.ɵɵelement(0,"tb-type-value-field",14),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("helpLink",e.helpLink)}}function K$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",6)(1,"div",7),t.ɵɵelementContainerStart(2,8),t.ɵɵelementStart(3,"mat-expansion-panel",9)(4,"mat-expansion-panel-header",10)(5,"mat-panel-title")(6,"div",11),t.ɵɵtext(7),t.ɵɵelementEnd()()(),t.ɵɵtemplate(8,$$,1,1,"ng-template",12),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"button",13),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"delete"),t.ɵɵelementEnd()()()}if(2&e){let e;const i=n.$implicit,a=n.last;t.ɵɵadvance(2),t.ɵɵproperty("formGroup",i),t.ɵɵadvance(),t.ɵɵproperty("expanded",a),t.ɵɵadvance(4),t.ɵɵtextInterpolate(null!==(e=null==(e=i.get("typeValue").value)?null:e.value)&&void 0!==e?e:""),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(10,4,"gateway.delete-argument"))}}function Y$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4),t.ɵɵtemplate(1,K$,13,6,"div",5),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.valueListFormArray.controls)("ngForTrackBy",e.trackByKey)}}function X$(e,n){1&e&&(t.ɵɵelementStart(0,"div",15)(1,"span",16),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate("gateway.no-value"))}He([I()],W$.prototype,"rawData",void 0);class Z${constructor(e){this.fb=e,this.destroy$=new we,this.onChange=e=>{}}ngOnInit(){this.valueListFormArray=this.fb.array([]),this.valueListFormArray.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e.map((({typeValue:e})=>({...e}))))}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}trackByKey(e,t){return t}addKey(){const e=this.fb.group({typeValue:[]});this.valueListFormArray.push(e)}deleteKey(e,t){e&&e.stopPropagation(),this.valueListFormArray.removeAt(t),this.valueListFormArray.markAsDirty()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){for(const t of e){const e={typeValue:[t]},n=this.fb.group(e);this.valueListFormArray.push(n)}}validate(){return this.valueListFormArray.valid?null:{valueListForm:{valid:!1}}}static{this.ɵfac=function(e){return new(e||Z$)(t.ɵɵdirectiveInject(de.UntypedFormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Z$,selectors:[["tb-type-value-panel"]],inputs:{helpLink:"helpLink"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>Z$)),multi:!0},{provide:he,useExisting:c((()=>Z$)),multi:!0}]),t.ɵɵStandaloneFeature],decls:8,vars:5,consts:[["noKeys",""],[1,"tb-form-panel","no-border","no-padding"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],["tbTruncateWithTooltip","",1,"title-container"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],["formControlName","typeValue",3,"helpLink"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",1),t.ɵɵtemplate(1,Y$,2,2,"div",2),t.ɵɵelementStart(2,"div")(3,"button",3),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(6,X$,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor)}if(2&e){const e=t.ɵɵreference(7);t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.valueListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,3,"gateway.add-value")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(Z$,[se,_,W$]),styles:['@charset "UTF-8";[_nghost-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw}[_nghost-%COMP%] .key-panel[_ngcontent-%COMP%]{height:250px;overflow:auto}[_nghost-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}']})}}const Q$=()=>({maxWidth:"970px"});function J$(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",19),t.ɵɵtext(2),t.ɵɵelementEnd(),t.ɵɵtext(3),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",e.get("key").value," "),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",":","  ")}}function eK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function tK(e,n){if(1&e&&(t.ɵɵelement(0,"div",41),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext(3).$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,n.connectorType,n.keysType,e.get("type").value,n.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,Q$))}}function nK(e,n){if(1&e&&t.ɵɵelement(0,"mat-icon",44),2&e){let e;const n=t.ɵɵnextContext(4).$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("svgIcon",null==(e=i.valueTypes.get(n.get("type").value))?null:e.icon)}}function iK(e,n){if(1&e&&(t.ɵɵelementStart(0,"span"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){let e;const n=t.ɵɵnextContext(4).$implicit,i=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,null!==(e=null==(e=i.valueTypes.get(n.get("type").value))?null:e.name)&&void 0!==e?e:i.valueTypes.get(n.get("type").value))," ")}}function aK(e,n){1&e&&(t.ɵɵelementStart(0,"span"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.raw")))}function rK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-select-trigger")(1,"div",42),t.ɵɵtemplate(2,nK,1,1,"mat-icon",43)(3,iK,3,3,"span",36)(4,aK,3,3,"ng-template",null,2,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()),2&e){let e;const n=t.ɵɵreference(5),i=t.ɵɵnextContext(3).$implicit,a=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("ngIf",null==(e=a.valueTypes.get(i.get("type").value))?null:e.icon),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!a.rawData)("ngIfElse",n)}}function oK(e,n){if(1&e&&t.ɵɵelement(0,"mat-icon",48),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(6);t.ɵɵpropertyInterpolate("svgIcon",n.valueTypes.get(e).icon)}}function sK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",46),t.ɵɵtemplate(1,oK,1,1,"mat-icon",47),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){let e,i;const a=n.$implicit,r=t.ɵɵnextContext(6);t.ɵɵproperty("value",a),t.ɵɵadvance(),t.ɵɵproperty("ngIf",null==(e=r.valueTypes.get(a))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,3,null!==(i=null==(i=r.valueTypes.get(a))?null:i.name)&&void 0!==i?i:r.valueTypes.get(a))," ")}}function lK(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,sK,5,5,"mat-option",45),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(5);t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.valueTypeKeys)}}function pK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-option",46)(1,"span"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("value","raw"),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,2,"gateway.raw")))}function cK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function dK(e,n){if(1&e&&(t.ɵɵelement(0,"div",41),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext(3).$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,n.connectorType,n.keysType,e.get("type").value,n.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,Q$))}}function uK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"div",24)(2,"div",25),t.ɵɵtext(3,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",26)(5,"div",27),t.ɵɵpipe(6,"translate"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-form-field",28),t.ɵɵelement(10,"input",29),t.ɵɵpipe(11,"translate"),t.ɵɵtemplate(12,eK,3,3,"mat-icon",30)(13,tK,2,8,"div",31),t.ɵɵpipe(14,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",24)(16,"div",25),t.ɵɵtext(17,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"div",32)(19,"div",33),t.ɵɵtext(20,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",34)(22,"mat-select",35),t.ɵɵtemplate(23,rK,6,3,"mat-select-trigger",18)(24,lK,2,1,"ng-container",36)(25,pK,4,4,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()(),t.ɵɵelementStart(27,"div",37)(28,"div",27),t.ɵɵpipe(29,"translate"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(32,"mat-form-field",38),t.ɵɵelement(33,"input",39),t.ɵɵpipe(34,"translate"),t.ɵɵtemplate(35,cK,3,3,"mat-icon",30)(36,dK,2,8,"div",31),t.ɵɵpipe(37,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵreference(26),n=t.ɵɵnextContext(2).$implicit,i=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,13,"gateway.JSONPath-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(8,15,"gateway.key")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(11,17,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.get("key").hasError("required")&&n.get("key").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(14,19,i.connectorType,i.keysType,n.get("type").value,i.convertorType)),t.ɵɵadvance(10),t.ɵɵproperty("ngIf",!i.rawData),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!i.rawData)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(29,24,"gateway.JSONPath-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(31,26,"gateway.value")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(34,28,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.get("value").hasError("required")&&n.get("value").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(37,30,i.connectorType,i.keysType,n.get("type").value,i.convertorType))}}function mK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function hK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function gK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"div",26)(2,"div",33),t.ɵɵtext(3,"gateway.key"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",28),t.ɵɵelement(5,"input",29),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,mK,3,3,"mat-icon",30),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",26)(9,"div",33),t.ɵɵtext(10,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",49),t.ɵɵelement(12,"input",39),t.ɵɵpipe(13,"translate"),t.ɵɵtemplate(14,hK,3,3,"mat-icon",30),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,4,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("key").hasError("required")&&e.get("key").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(13,6,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("value").hasError("required")&&e.get("value").touched)}}function fK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-required"))}function yK(e,n){1&e&&t.ɵɵelement(0,"tb-type-value-panel",54)}function vK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"div",26)(2,"div",27),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",28),t.ɵɵelement(7,"input",50),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,fK,3,3,"mat-icon",30),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",14)(11,"mat-expansion-panel",51)(12,"mat-expansion-panel-header",52)(13,"mat-panel-title")(14,"div",53),t.ɵɵpipe(15,"translate"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(18,yK,1,0,"ng-template",20),t.ɵɵelementEnd()()()),2&e){let e;const n=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,7,"gateway.hints.method-name")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,9,"gateway.method-name")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,11,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.get("method").hasError("required")&&n.get("method").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(15,13,"gateway.hints.arguments")),t.ɵɵadvance(2),t.ɵɵtextInterpolate2(" ",t.ɵɵpipeBind1(17,15,"gateway.arguments"),""," ("+(null==(e=n.get("arguments").value)?null:e.length)+")"," ")}}function xK(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",55),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Key)}}function bK(e,n){if(1&e&&t.ɵɵtemplate(0,uK,38,35,"div",22)(1,gK,15,8,"div",22)(2,vK,19,17,"div",22)(3,xK,1,2,"tb-report-strategy",23),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("ngIf",e.keysType!==e.MappingKeysType.CUSTOM&&e.keysType!==e.MappingKeysType.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.MappingKeysType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.MappingKeysType.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.withReportStrategy&&(e.keysType===e.MappingKeysType.ATTRIBUTES||e.keysType===e.MappingKeysType.TIMESERIES))}}function wK(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",13)(1,"div",14),t.ɵɵelementContainerStart(2,15),t.ɵɵelementStart(3,"mat-expansion-panel",16)(4,"mat-expansion-panel-header",17)(5,"mat-panel-title"),t.ɵɵtemplate(6,J$,4,2,"ng-container",18),t.ɵɵelementStart(7,"div",19),t.ɵɵtext(8),t.ɵɵelementEnd()()(),t.ɵɵtemplate(9,bK,4,4,"ng-template",20),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",21),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.last,a=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",a.keysType!==a.MappingKeysType.RPC_METHODS),t.ɵɵadvance(2),t.ɵɵtextInterpolate(a.valueTitle(e)),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,a.deleteKeyTitle))}}function SK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",11),t.ɵɵtemplate(1,wK,14,7,"div",12),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.keysListFormArray.controls)("ngForTrackBy",e.trackByKey)}}function CK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",56)(1,"span",57),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class _K extends q{constructor(e,t,n){super(n),this.fb=e,this.popover=t,this.store=n,this.valueTypeEnum=en,this.valueTypes=tn,this.valueTypeKeys=Object.values(en),this.rawData=!1,this.withReportStrategy=!0,this.keysDataApplied=new u,this.MappingKeysType=Ui,this.ReportStrategyDefaultValue=on,this.ConnectorType=dt,this.errorText=""}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}trackByKey(e,t){return t}addKey(){let e;e=this.keysType===Ui.RPC_METHODS?this.fb.group({method:["",[me.required]],arguments:[[],[]]}):this.keysType===Ui.CUSTOM?this.fb.group({key:["",[me.required,me.pattern(pn)]],value:["",[me.required,me.pattern(pn)]]}):this.fb.group({key:["",[me.required,me.pattern(pn)]],type:[this.rawData?"raw":this.valueTypeKeys[0]],value:["",[me.required,me.pattern(pn)]],reportStrategy:[{value:null,disabled:this.isReportStrategyDisabled()}]}),this.keysListFormArray.push(e)}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){let e=this.keysListFormArray.value.map((({reportStrategy:e,...t})=>({...t,...e&&{reportStrategy:e}})));if(this.keysType===Ui.CUSTOM){e={};for(let t of this.keysListFormArray.value)e[t.key]=t.value}this.keysDataApplied.emit(e)}prepareKeysFormArray(e){const t=[];return e&&(this.keysType===Ui.CUSTOM&&(e=Object.keys(e).map((t=>({key:t,value:e[t],type:""})))),e.forEach((e=>{let n;if(this.keysType===Ui.RPC_METHODS)n=this.fb.group({method:[e.method,[me.required]],arguments:[[...e.arguments],[]]});else if(this.keysType===Ui.CUSTOM){const{key:t,value:i}=e;n=this.fb.group({key:[t,[me.required,me.pattern(pn)]],value:[i,[me.required,me.pattern(pn)]]})}else{const{key:t,value:i,type:a,reportStrategy:r}=e;n=this.fb.group({key:[t,[me.required,me.pattern(pn)]],type:[a],value:[i,[me.required,me.pattern(pn)]],reportStrategy:[{value:r,disabled:this.isReportStrategyDisabled()}]})}t.push(n)}))),this.fb.array(t)}valueTitle(e){const t=this.keysType===Ui.RPC_METHODS?e.get("method").value:e.get("value").value;return J(t)?"object"==typeof t?JSON.stringify(t):t:""}isReportStrategyDisabled(){return!(this.withReportStrategy&&(this.keysType===Ui.ATTRIBUTES||this.keysType===Ui.TIMESERIES))}static{this.ɵfac=function(e){return new(e||_K)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverComponent),t.ɵɵdirectiveInject(Xe.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:_K,selectors:[["tb-mapping-data-keys-panel"]],inputs:{panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keys:"keys",keysType:"keysType",connectorType:"connectorType",convertorType:"convertorType",sourceType:"sourceType",valueTypeEnum:"valueTypeEnum",valueTypes:"valueTypes",valueTypeKeys:"valueTypeKeys",rawData:"rawData",withReportStrategy:"withReportStrategy"},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:16,consts:[["noKeys",""],["rawOption",""],["rawText",""],[1,"tb-mapping-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex","flex-row","flex-wrap"],[4,"ngIf"],["tbTruncateWithTooltip","",1,"title-container"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],["class","tb-form-panel no-border no-padding",4,"ngIf"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue",4,"ngIf"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style",4,"ngIf"],[1,"tb-form-row"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","type"],[4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs","flex","items-center","justify-between"],["appearance","outline","subscriptSizing","dynamic",1,"no-gap","flex","flex-1"],["matInput","","required","","formControlName","value",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"tb-flex","align-center"],["class","tb-mat-18",3,"svgIcon",4,"ngIf"],[1,"tb-mat-18",3,"svgIcon"],[3,"value",4,"ngFor","ngForOf"],[3,"value"],["class","tb-mat-20",3,"svgIcon",4,"ngIf"],[1,"tb-mat-20",3,"svgIcon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","tb-suffix-absolute","flex"],["matInput","","name","value","formControlName","method",3,"placeholder"],[1,"tb-settings"],[1,"flex","flex-wrap"],[1,"title-container",3,"tb-hint-tooltip-icon"],["formControlName","arguments"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"div",5),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,SK,2,2,"div",6),t.ɵɵelementStart(6,"div")(7,"button",7),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(10,CK,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(12,"div",8)(13,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",10),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.applyKeysData())})),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(11);t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,8,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")",""),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.keysListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,10,n.addKeyTitle)," "),t.ɵɵadvance(6),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,12,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,14,"action.apply")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(_K,[se,_,ni,Z$,m$]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:500px;overflow:auto}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] tb-value-input[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}']})}}He([I()],_K.prototype,"rawData",void 0),He([I()],_K.prototype,"withReportStrategy",void 0);const TK=()=>({maxWidth:"970px"}),IK=(e,t)=>[e,t];function EK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.topic-required"))}function MK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.QualityTranslationsMap.get(e))," ")}}function kK(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.ConvertorTypeTranslationsMap.get(e))," ")}}function PK(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",41),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("convertorType",e.ConvertorTypeEnum.JSON)("deviceInfoType",e.DeviceInfoType.FULL)}}function DK(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",42),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("deviceInfoType",e.DeviceInfoType.FULL)("convertorType",e.ConvertorTypeEnum.BYTES)("sourceTypes",t.ɵɵpureFunction2(3,IK,e.sourceTypesEnum.MSG,e.sourceTypesEnum.CONST))}}function OK(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",37),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function AK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function FK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function RK(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",14)(1,"div",31)(2,"div",32),t.ɵɵtext(3,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",43)(5,"mat-chip-listbox",44),t.ɵɵtemplate(6,AK,2,1,"mat-chip",45),t.ɵɵelementStart(7,"mat-chip",46),t.ɵɵelement(8,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",48,0),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(10),a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES))})),t.ɵɵelementStart(12,"tb-icon",49),t.ɵɵtext(13,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(14,"div",31)(15,"div",32),t.ɵɵtext(16,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"div",43)(18,"mat-chip-listbox",44),t.ɵɵtemplate(19,FK,2,1,"mat-chip",45),t.ɵɵelementStart(20,"mat-chip",46),t.ɵɵelement(21,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"button",48,1),t.ɵɵpipe(24,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(23),a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.TIMESERIES))})),t.ɵɵelementStart(25,"tb-icon",49),t.ɵɵtext(26,"edit"),t.ɵɵelementEnd()()()()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",e.converterAttributes),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.converterAttributes),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,6,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.converterTelemetry),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.converterTelemetry),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(24,8,"action.edit"))}}function BK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.extension-required"))}function NK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function LK(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",14)(1,"div",21)(2,"div",50),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",23),t.ɵɵelement(7,"input",51),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,BK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",52)(11,"div",35),t.ɵɵtext(12,"gateway.extension-configuration"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",15),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",31)(17,"div",32),t.ɵɵtext(18,"gateway.keys"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",43)(20,"mat-chip-listbox",44),t.ɵɵtemplate(21,NK,2,1,"mat-chip",45),t.ɵɵelementStart(22,"mat-chip",46),t.ɵɵelement(23,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(24,"button",48,2),t.ɵɵpipe(26,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(25),a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.CUSTOM))})),t.ɵɵelementStart(27,"tb-icon",49),t.ɵɵtext(28,"edit"),t.ɵɵelementEnd()()()()()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,8,"gateway.extension-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,10,"gateway.extension")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,12,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("converter.custom.extension").hasError("required")&&e.mappingForm.get("converter.custom.extension").touched),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,14,"gateway.extension-configuration-hint")),t.ɵɵadvance(6),t.ɵɵproperty("tbEllipsisChipList",e.customKeys),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.customKeys),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(26,16,"action.edit"))}}function VK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",22),t.ɵɵtext(2,"gateway.topic-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",23),t.ɵɵelement(4,"input",24),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,EK,3,3,"mat-icon",25),t.ɵɵelement(7,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",21)(9,"div",27),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",23)(14,"mat-select",28),t.ɵɵtemplate(15,MK,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementContainerStart(16,30),t.ɵɵelementStart(17,"div",31)(18,"div",32),t.ɵɵtext(19,"gateway.payload-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(20,"tb-toggle-select",33),t.ɵɵtemplate(21,kK,3,4,"tb-toggle-option",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"div",34)(23,"div",35),t.ɵɵtext(24,"gateway.data-conversion"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"div",15),t.ɵɵtext(26),t.ɵɵpipe(27,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(28,36),t.ɵɵtemplate(29,PK,1,2,"ng-template",17)(30,DK,1,6,"ng-template",17)(31,OK,1,2,"tb-report-strategy",37)(32,RK,27,10,"div",38)(33,LK,29,18,"div",38),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,16,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("topicFilter").hasError("required")&&e.mappingForm.get("topicFilter").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/topic-filter_fn")("tb-help-popup-style",t.ɵɵpureFunction0(24,TK)),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,18,"gateway.response-topic-Qos-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,20,"gateway.mqtt-qos")," "),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.qualityTypes),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",e.convertorTypes),t.ɵɵadvance(5),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(27,22,e.DataConversionTranslationsMap.get(e.converterType))," "),t.ɵɵadvance(2),t.ɵɵproperty("formGroupName",e.converterType)("ngSwitch",e.converterType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConvertorTypeEnum.JSON),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConvertorTypeEnum.BYTES),t.ɵɵadvance(),t.ɵɵconditional(e.data.withReportStrategy?31:-1),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.converterType===e.ConvertorTypeEnum.BYTES||e.converterType===e.ConvertorTypeEnum.JSON),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.converterType===e.ConvertorTypeEnum.CUSTOM)}}function qK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.RequestTypesTranslationsMap.get(e))," ")}}function GK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.topic-required"))}function zK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",22),t.ɵɵtext(2,"gateway.topic-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",23),t.ɵɵelement(4,"input",57),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,GK,3,3,"mat-icon",25),t.ɵɵelement(7,"div",26),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,5,"gateway.set")),t.ɵɵproperty("formControl",e.mappingForm.get("requestValue").get(e.requestMappingType).get("topicFilter")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue").get(e.requestMappingType).get("topicFilter").hasError("required")&&e.mappingForm.get("requestValue").get(e.requestMappingType).get("topicFilter").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/topic-filter_fn")("tb-help-popup-style",t.ɵɵpureFunction0(7,TK))}}function UK(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",58),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("deviceInfoType",e.DeviceInfoType.FULL)}}function jK(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",58),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("deviceInfoType",e.DeviceInfoType.PARTIAL)}}function HK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function WK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-info.device-name-expression-required"))}function $K(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function KK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.attribute-name-expression-required"))}function YK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-value-expression-required"))}function XK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-topic-expression-required"))}function ZK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",34)(1,"div",59),t.ɵɵtext(2,"gateway.from-device-request-settings"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",60),t.ɵɵtext(4," gateway.from-device-request-settings-hint "),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",61)(6,"div",62)(7,"div",63),t.ɵɵtext(8,"gateway.device-info.device-name-expression"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",64)(10,"div",65)(11,"mat-form-field",23)(12,"mat-select",66),t.ɵɵtemplate(13,HK,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(14,"mat-form-field",23),t.ɵɵelement(15,"input",67),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,WK,3,3,"mat-icon",25),t.ɵɵelement(18,"div",26),t.ɵɵelementEnd()()(),t.ɵɵelementStart(19,"div",21)(20,"div",22),t.ɵɵtext(21,"gateway.attribute-name-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"div",64)(23,"div",65)(24,"mat-form-field",23)(25,"mat-select",68),t.ɵɵtemplate(26,$K,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(27,"mat-form-field",23),t.ɵɵelement(28,"input",69),t.ɵɵpipe(29,"translate"),t.ɵɵtemplate(30,KK,3,3,"mat-icon",25),t.ɵɵelement(31,"div",26),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(32,"div",34)(33,"div",59),t.ɵɵtext(34,"gateway.to-device-response-settings"),t.ɵɵelementEnd(),t.ɵɵelementStart(35,"div",60),t.ɵɵtext(36," gateway.to-device-response-settings-hint "),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"div",21)(38,"div",22),t.ɵɵtext(39,"gateway.response-value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(40,"mat-form-field",23),t.ɵɵelement(41,"input",70),t.ɵɵpipe(42,"translate"),t.ɵɵtemplate(43,YK,3,3,"mat-icon",25),t.ɵɵelement(44,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"div",21)(46,"div",22),t.ɵɵtext(47,"gateway.response-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(48,"mat-form-field",23),t.ɵɵelement(49,"input",71),t.ɵɵpipe(50,"translate"),t.ɵɵtemplate(51,XK,3,3,"mat-icon",25),t.ɵɵelement(52,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(53,"div",72)(54,"mat-slide-toggle",73)(55,"mat-label",74),t.ɵɵpipe(56,"translate"),t.ɵɵtext(57),t.ɵɵpipe(58,"translate"),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(13),t.ɵɵproperty("ngForOf",e.sourceTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.deviceInfo.deviceNameExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.deviceInfo.deviceNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(32,TK)),t.ɵɵadvance(8),t.ɵɵproperty("ngForOf",e.sourceTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(29,22,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.attributeNameExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.attributeNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(33,TK)),t.ɵɵadvance(10),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(42,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.valueExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.valueExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(34,TK)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(50,26,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.topicExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.topicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(35,TK)),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(56,28,"gateway.retain-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(58,30,"gateway.retain")," ")}}function QK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-filter-required"))}function JK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.attribute-filter-required"))}function eY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-value-expression-required"))}function tY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-topic-expression-required"))}function nY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",50),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",23),t.ɵɵelement(6,"input",75),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,QK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",21)(10,"div",50),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"mat-form-field",23),t.ɵɵelement(15,"input",76),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,JK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",21)(19,"div",22),t.ɵɵtext(20,"gateway.response-value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",23),t.ɵɵelement(22,"input",70),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,eY,3,3,"mat-icon",25),t.ɵɵelement(25,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"div",21)(27,"div",22),t.ɵɵtext(28,"gateway.response-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"mat-form-field",23),t.ɵɵelement(30,"input",71),t.ɵɵpipe(31,"translate"),t.ɵɵtemplate(32,tY,3,3,"mat-icon",25),t.ɵɵelement(33,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"div",72)(35,"mat-slide-toggle",73)(36,"mat-label",74),t.ɵɵpipe(37,"translate"),t.ɵɵtext(38),t.ɵɵpipe(39,"translate"),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,18,"gateway.device-name-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,20,"gateway.device-name-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,22,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.deviceNameFilter").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.deviceNameFilter").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,24,"gateway.attribute-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,26,"gateway.attribute-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,28,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.attributeFilter").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.attributeFilter").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,30,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.valueExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.valueExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(38,TK)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(31,32,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.topicExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.topicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(39,TK)),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(37,34,"gateway.retain-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(39,36,"gateway.retain")," ")}}function iY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-filter-required"))}function aY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-filter-required"))}function rY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.request-topic-expression-required"))}function oY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-expression-required"))}function sY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-topic-expression-required"))}function lY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(4);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.QualityTranslationsMap.get(e))," ")}}function pY(e,n){if(1&e&&t.ɵɵelement(0,"tb-error-icon",84),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("tooltipText",e.responseTimeoutErrorTooltip)}}function cY(e,n){1&e&&(t.ɵɵelementStart(0,"span",85),t.ɵɵtext(1,"gateway.suffix.ms"),t.ɵɵelementEnd())}function dY(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",21)(2,"div",22),t.ɵɵtext(3,"gateway.response-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",23),t.ɵɵelement(5,"input",81),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,sY,3,3,"mat-icon",25),t.ɵɵelement(8,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",21)(10,"div",27),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"mat-form-field",23)(15,"mat-select",82),t.ɵɵtemplate(16,lY,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(17,"div",21)(18,"div",22),t.ɵɵtext(19,"gateway.response-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(20,"mat-form-field",23),t.ɵɵelement(21,"input",83),t.ɵɵpipe(22,"translate"),t.ɵɵtemplate(23,pY,1,1,"tb-error-icon",84)(24,cY,2,0,"span",85),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.responseTopicExpression").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.responseTopicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(17,TK)),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,11,"gateway.response-topic-Qos-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,13,"gateway.response-topic-Qos")," "),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.qualityTypes),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(22,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional((e.mappingForm.get("requestValue.serverSideRpc.responseTimeout").hasError("required")||e.mappingForm.get("requestValue.serverSideRpc.responseTimeout").hasError("min"))&&e.mappingForm.get("requestValue.serverSideRpc.responseTimeout").touched?23:24)}}function uY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",77)(1,"tb-toggle-select",33)(2,"tb-toggle-option",40),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-option",40),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",21)(9,"div",50),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",23),t.ɵɵelement(14,"input",75),t.ɵɵpipe(15,"translate"),t.ɵɵtemplate(16,iY,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",21)(18,"div",50),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",23),t.ɵɵelement(23,"input",78),t.ɵɵpipe(24,"translate"),t.ɵɵtemplate(25,aY,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"div",21)(27,"div",22),t.ɵɵtext(28,"gateway.request-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"mat-form-field",23),t.ɵɵelement(30,"input",79),t.ɵɵpipe(31,"translate"),t.ɵɵtemplate(32,rY,3,3,"mat-icon",25),t.ɵɵelement(33,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"div",21)(35,"div",22),t.ɵɵtext(36,"gateway.value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"mat-form-field",23),t.ɵɵelement(38,"input",70),t.ɵɵpipe(39,"translate"),t.ɵɵtemplate(40,oY,3,3,"mat-icon",25),t.ɵɵelement(41,"div",26),t.ɵɵelementEnd()(),t.ɵɵtemplate(42,dY,25,18,"ng-container",80)),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("value",e.ServerSideRPCType.TWO_WAY),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,21,"gateway.with-response")," "),t.ɵɵadvance(2),t.ɵɵproperty("value",e.ServerSideRPCType.ONE_WAY),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,23,"gateway.without-response")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,25,"gateway.device-name-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,27,"gateway.device-name-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,29,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.deviceNameFilter").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.deviceNameFilter").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(19,31,"gateway.method-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(21,33,"gateway.method-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(24,35,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.methodFilter").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.methodFilter").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(31,37,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.requestTopicExpression").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.requestTopicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(41,TK)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(39,39,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.valueExpression").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.valueExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(42,TK)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.type").value===e.ServerSideRPCType.TWO_WAY)}}function mY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",32),t.ɵɵtext(2,"gateway.request-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",23)(4,"mat-select",53),t.ɵɵtemplate(5,qK,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementContainerStart(6,54)(7,55),t.ɵɵtemplate(8,zK,8,8,"div",56)(9,UK,1,1,"ng-template",17)(10,jK,1,1,"ng-template",17)(11,ZK,59,36,"ng-template",17)(12,nY,40,40,"ng-template",17)(13,uY,43,43,"ng-template",17),t.ɵɵelementContainerEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.requestTypes),t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e.mappingForm.get("requestValue").get(e.requestMappingType))("ngSwitch",e.requestMappingType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.requestMappingType===e.RequestTypeEnum.ATTRIBUTE_REQUEST||e.requestMappingType===e.RequestTypeEnum.CONNECT_REQUEST||e.requestMappingType===e.RequestTypeEnum.DISCONNECT_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.CONNECT_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.DISCONNECT_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.ATTRIBUTE_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.ATTRIBUTE_UPDATE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.SERVER_SIDE_RPC)}}function hY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function gY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-node-required"))}function fY(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",37),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function yY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function vY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function xY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function bY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function wY(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",86)(1,"div",87)(2,"div",88),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"div",89)(7,"mat-form-field",90)(8,"mat-select",91),t.ɵɵtemplate(9,hY,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"mat-form-field",90),t.ɵɵelement(11,"input",92),t.ɵɵpipe(12,"translate"),t.ɵɵtemplate(13,gY,3,3,"mat-icon",25),t.ɵɵelement(14,"div",26),t.ɵɵpipe(15,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()(),t.ɵɵelement(16,"tb-device-info-table",93),t.ɵɵtemplate(17,fY,1,2,"tb-report-strategy",37),t.ɵɵelementStart(18,"div",31)(19,"div",32),t.ɵɵtext(20,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",43)(22,"mat-chip-listbox",44),t.ɵɵtemplate(23,yY,2,1,"mat-chip",45),t.ɵɵelementStart(24,"mat-chip",46),t.ɵɵelement(25,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"button",48,3),t.ɵɵpipe(28,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(27),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES))})),t.ɵɵelementStart(29,"tb-icon",49),t.ɵɵtext(30,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(31,"div",31)(32,"div",32),t.ɵɵtext(33,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"div",43)(35,"mat-chip-listbox",44),t.ɵɵtemplate(36,vY,2,1,"mat-chip",45),t.ɵɵelementStart(37,"mat-chip",46),t.ɵɵelement(38,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(39,"button",48,4),t.ɵɵpipe(41,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(40),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.TIMESERIES))})),t.ɵɵelementStart(42,"tb-icon",49),t.ɵɵtext(43,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(44,"div",31)(45,"div",32),t.ɵɵtext(46,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(47,"div",43)(48,"mat-chip-listbox",44),t.ɵɵtemplate(49,xY,2,1,"mat-chip",45),t.ɵɵelementStart(50,"mat-chip",46),t.ɵɵelement(51,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(52,"button",48,5),t.ɵɵpipe(54,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(53),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES_UPDATES))})),t.ɵɵelementStart(55,"tb-icon",49),t.ɵɵtext(56,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(57,"div",31)(58,"div",32),t.ɵɵtext(59,"gateway.rpc-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(60,"div",43)(61,"mat-chip-listbox",44),t.ɵɵtemplate(62,bY,2,1,"mat-chip",45),t.ɵɵelementStart(63,"mat-chip",46),t.ɵɵelement(64,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(65,"button",48,6),t.ɵɵpipe(67,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(66),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.RPC_METHODS))})),t.ɵɵelementStart(68,"tb-icon",49),t.ɵɵtext(69,"edit"),t.ɵɵelementEnd()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,23,"gateway.device-node-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,25,"gateway.device-node")," "),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",t.ɵɵpureFunction2(41,IK,e.OPCUaSourceTypesEnum.PATH,e.OPCUaSourceTypesEnum.IDENTIFIER)),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(12,27,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("deviceNodePattern").hasError("required")&&e.mappingForm.get("deviceNodePattern").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind3(15,29,e.ConnectorType.OPCUA,"device-node",e.mappingForm.get("deviceNodeSource").value))("tb-help-popup-style",t.ɵɵpureFunction0(44,TK)),t.ɵɵadvance(2),t.ɵɵproperty("connectorType",e.ConnectorType.OPCUA)("sourceTypes",e.OPCUaSourceTypes)("deviceInfoType",e.DeviceInfoType.FULL),t.ɵɵadvance(),t.ɵɵconditional(e.data.withReportStrategy?17:-1),t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",e.opcAttributes),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcAttributes),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(28,33,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.opcTelemetry),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcTelemetry),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(41,35,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.opcAttributesUpdates),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcAttributesUpdates),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(54,37,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.opcRpcMethods),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcRpcMethods),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(67,39,"action.edit"))}}class SY extends A{constructor(e,t,n,i,a,r,o,s,l){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.translate=l,this.MappingType=Ni,this.qualityTypes=Ki,this.QualityTranslationsMap=li,this.convertorTypes=Object.values(ri),this.ConvertorTypeEnum=ri,this.ConvertorTypeTranslationsMap=pi,this.sourceTypes=Object.values(oi),this.OPCUaSourceTypes=Object.values(Fi),this.OPCUaSourceTypesEnum=Fi,this.sourceTypesEnum=oi,this.SourceTypeTranslationsMap=zi,this.requestTypes=Object.values(ci),this.RequestTypeEnum=ci,this.RequestTypesTranslationsMap=di,this.DeviceInfoType=Sa,this.ServerSideRPCType=Qi,this.MappingKeysType=Ui,this.MappingHintTranslationsMap=Zi,this.MappingTypeTranslationsMap=Li,this.DataConversionTranslationsMap=ui,this.HelpLinkByMappingTypeMap=Xi,this.ConnectorType=dt,this.ReportStrategyDefaultValue=on,this.keysPopupClosed=!0,this.destroy$=new we,this.createMappingForm()}get converterAttributes(){if(this.converterType)return this.mappingForm.get("converter").get(this.converterType).value.attributes.map((e=>e.key))}get converterTelemetry(){if(this.converterType)return this.mappingForm.get("converter").get(this.converterType).value.timeseries.map((e=>e.key))}get opcAttributes(){return this.mappingForm.get("attributes").value?.map((e=>e.key))||[]}get opcTelemetry(){return this.mappingForm.get("timeseries").value?.map((e=>e.key))||[]}get opcRpcMethods(){return this.mappingForm.get("rpc_methods").value?.map((e=>e.method))||[]}get opcAttributesUpdates(){return this.mappingForm.get("attributes_updates")?.value?.map((e=>e.key))||[]}get converterType(){return this.mappingForm.get("converter")?.get("type").value}get customKeys(){return Object.keys(this.mappingForm.get("converter").get("custom").value.extensionConfig)}get requestMappingType(){return this.mappingForm.get("requestType").value}get responseTimeoutErrorTooltip(){const e=this.mappingForm.get("requestValue.serverSideRpc.responseTimeout");return e.hasError("required")?this.translate.instant("gateway.response-timeout-required"):e.hasError("min")?this.translate.instant("gateway.response-timeout-limits-error",{min:1}):""}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}createMappingForm(){switch(this.data.mappingType){case Ni.DATA:this.mappingForm=this.fb.group({}),this.createDataMappingForm();break;case Ni.REQUESTS:this.mappingForm=this.fb.group({}),this.createRequestMappingForm();break;case Ni.OPCUA:this.createOPCUAMappingForm()}}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){this.mappingForm.valid&&this.dialogRef.close(this.prepareMappingData())}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))this.popoverService.hidePopover(i);else{const e=(this.data.mappingType!==Ni.OPCUA?this.mappingForm.get("converter").get(this.converterType):this.mappingForm).get(n),t={keys:e.value,keysType:n,rawData:this.mappingForm.get("converter.type")?.value===ri.BYTES,panelTitle:ji.get(n),addKeyTitle:Hi.get(n),deleteKeyTitle:Wi.get(n),noKeysText:$i.get(n),withReportStrategy:this.data.withReportStrategy,connectorType:this.data.mappingType===Ni.OPCUA?dt.OPCUA:dt.MQTT,convertorType:this.converterType};this.data.mappingType===Ni.OPCUA&&(t.valueTypeKeys=Object.values(Fi),t.valueTypeEnum=Fi,t.valueTypes=zi,t.sourceType=this.mappingForm.get("deviceNodeSource").value),this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,_K,"leftBottom",!1,null,t,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(Me(this.destroy$)).subscribe((t=>{this.popoverComponent.hide(),e.patchValue(t),e.markAsDirty()})),this.popoverComponent.tbHideStart.pipe(Me(this.destroy$)).subscribe((()=>{this.keysPopupClosed=!0}))}}prepareMappingData(){const e=this.mappingForm.value;switch(Y(e),this.data.mappingType){case Ni.DATA:const{converter:t,topicFilter:n,subscriptionQos:i}=e,{reportStrategy:a,...r}=t[t.type];return{topicFilter:n,subscriptionQos:i,...a?{reportStrategy:a}:{},converter:{type:t.type,...r}};case Ni.REQUESTS:return{requestType:e.requestType,requestValue:e.requestValue[e.requestType]};default:return e}}getFormValueData(){if(this.data.value&&Object.keys(this.data.value).length)switch(this.data.mappingType){case Ni.DATA:const{converter:e,topicFilter:t,subscriptionQos:n,reportStrategy:i}=this.data.value;return{topicFilter:t,subscriptionQos:n,converter:{type:e.type,[e.type]:{...e,...i?{reportStrategy:i}:{}}}};case Ni.REQUESTS:return{requestType:this.data.value.requestType,requestValue:{[this.data.value.requestType]:this.data.value.requestValue}};default:return this.data.value}}createDataMappingForm(){this.mappingForm.addControl("topicFilter",this.fb.control("",[me.required,me.pattern(pn)])),this.mappingForm.addControl("subscriptionQos",this.fb.control(0)),this.mappingForm.addControl("converter",this.fb.group({type:[ri.JSON,[]],json:this.fb.group({deviceInfo:[{},[]],attributes:[[],[]],timeseries:[[],[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),bytes:this.fb.group({deviceInfo:[{},[]],attributes:[[],[]],timeseries:[[],[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),custom:this.fb.group({extension:["",[me.required,me.pattern(pn)]],extensionConfig:[{},[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]})})),this.mappingForm.patchValue(this.getFormValueData()),this.mappingForm.get("converter.type").valueChanges.pipe(qe(this.mappingForm.get("converter.type").value),Me(this.destroy$)).subscribe((e=>{const t=this.mappingForm.get("converter");t.get("json").disable({emitEvent:!1}),t.get("bytes").disable({emitEvent:!1}),t.get("custom").disable({emitEvent:!1}),t.get(e).enable({emitEvent:!1})}))}createRequestMappingForm(){this.mappingForm.addControl("requestType",this.fb.control(ci.CONNECT_REQUEST,[])),this.mappingForm.addControl("requestValue",this.fb.group({connectRequests:this.fb.group({topicFilter:["",[me.required,me.pattern(pn)]],deviceInfo:[{},[]]}),disconnectRequests:this.fb.group({topicFilter:["",[me.required,me.pattern(pn)]],deviceInfo:[{},[]]}),attributeRequests:this.fb.group({topicFilter:["",[me.required,me.pattern(pn)]],deviceInfo:this.fb.group({deviceNameExpressionSource:[oi.MSG,[]],deviceNameExpression:["",[me.required]]}),attributeNameExpressionSource:[oi.MSG,[]],attributeNameExpression:["",[me.required,me.pattern(pn)]],topicExpression:["",[me.required,me.pattern(pn)]],valueExpression:["",[me.required,me.pattern(pn)]],retain:[!1,[]]}),attributeUpdates:this.fb.group({deviceNameFilter:["",[me.required,me.pattern(pn)]],attributeFilter:["",[me.required,me.pattern(pn)]],topicExpression:["",[me.required,me.pattern(pn)]],valueExpression:["",[me.required,me.pattern(pn)]],retain:[!0,[]]}),serverSideRpc:this.fb.group({type:[Qi.TWO_WAY,[]],deviceNameFilter:["",[me.required,me.pattern(pn)]],methodFilter:["",[me.required,me.pattern(pn)]],requestTopicExpression:["",[me.required,me.pattern(pn)]],responseTopicExpression:["",[me.required,me.pattern(pn)]],valueExpression:["",[me.required,me.pattern(pn)]],responseTopicQoS:[0,[]],responseTimeout:[1e4,[me.required,me.min(1)]]}),reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]})),this.mappingForm.get("requestType").valueChanges.pipe(qe(this.mappingForm.get("requestType").value),Me(this.destroy$)).subscribe((e=>{const t=this.mappingForm.get("requestValue");t.get("connectRequests").disable({emitEvent:!1}),t.get("disconnectRequests").disable({emitEvent:!1}),t.get("attributeRequests").disable({emitEvent:!1}),t.get("attributeUpdates").disable({emitEvent:!1}),t.get("serverSideRpc").disable({emitEvent:!1}),t.get(e).enable()})),this.mappingForm.get("requestValue.serverSideRpc.type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{const t=this.mappingForm.get("requestValue.serverSideRpc");e===Qi.ONE_WAY?(t.get("responseTopicExpression").disable({emitEvent:!1}),t.get("responseTopicQoS").disable({emitEvent:!1}),t.get("responseTimeout").disable({emitEvent:!1})):(t.get("responseTopicExpression").enable({emitEvent:!1}),t.get("responseTopicQoS").enable({emitEvent:!1}),t.get("responseTimeout").enable({emitEvent:!1}))})),this.mappingForm.patchValue(this.getFormValueData())}createOPCUAMappingForm(){this.mappingForm=this.fb.group({deviceNodeSource:[Fi.PATH,[]],deviceNodePattern:["",[me.required]],deviceInfo:[{},[]],attributes:[[],[]],timeseries:[[],[]],rpc_methods:[[],[]],attributes_updates:[[],[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),this.mappingForm.patchValue(this.getFormValueData())}static{this.ɵfac=function(e){return new(e||SY)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(rt.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(le.TranslateService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:SY,selectors:[["tb-mapping-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:26,vars:19,consts:[["attributesButton",""],["telemetryButton",""],["keysButton",""],["opcAttributesButton",""],["opcTelemetryButton",""],["attributesUpdatesButton",""],["rpcMethodsButton",""],[1,"key-mapping",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-hint","tb-primary-fill"],[3,"ngSwitch"],[3,"ngSwitchCase"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex"],["matInput","","name","value","formControlName","topicFilter",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","subscriptionQos"],[3,"value",4,"ngFor","ngForOf"],["formGroupName","converter"],[1,"tb-form-row","space-between","tb-flex"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[3,"formGroupName","ngSwitch"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],["class","tb-form-panel no-border no-padding",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["formControlName","deviceInfo","required","true",3,"convertorType","deviceInfoType"],["formControlName","deviceInfo","required","true",3,"deviceInfoType","convertorType","sourceTypes"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[4,"ngFor","ngForOf"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","name","value","formControlName","extension",3,"placeholder"],[1,"tb-form-row","space-between","same-padding","tb-flex","column"],["formControlName","requestType"],["formGroupName","requestValue"],[3,"formGroup","ngSwitch"],["class","tb-form-row column-xs",4,"ngIf"],["matInput","","name","value",3,"formControl","placeholder"],["formControlName","deviceInfo","required","true",3,"deviceInfoType"],["translate","",1,"tb-form-panel-title","tb-required"],["translate","",1,"tb-form-hint","tb-primary-fill"],["formGroupName","deviceInfo",1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-flex","no-flex","align-center"],["translate","",1,"tb-required"],[1,"flex","flex-1","gap-2"],[1,"flex","w-1/4"],["formControlName","deviceNameExpressionSource"],["matInput","","name","value","formControlName","deviceNameExpression",3,"placeholder"],["formControlName","attributeNameExpressionSource"],["matInput","","name","value","formControlName","attributeNameExpression",3,"placeholder"],["matInput","","name","value","formControlName","valueExpression",3,"placeholder"],["matInput","","name","value","formControlName","topicExpression",3,"placeholder"],[1,"tb-form-row"],["formControlName","retain",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["matInput","","name","value","formControlName","deviceNameFilter",3,"placeholder"],["matInput","","name","value","formControlName","attributeFilter",3,"placeholder"],[1,"tb-flex","row","center","align-center","no-gap","fill-width"],["matInput","","name","value","formControlName","methodFilter",3,"placeholder"],["matInput","","name","value","formControlName","requestTopicExpression",3,"placeholder"],[4,"ngIf"],["matInput","","name","value","formControlName","responseTopicExpression",3,"placeholder"],["formControlName","responseTopicQoS"],["matInput","","name","value","type","number","min","1","formControlName","responseTimeout",3,"placeholder"],["matSuffix","",3,"tooltipText"],["translate","","matSuffix","",1,"block","pr-2"],[1,"device-node-row","tb-form-row","column-xs","pr-4"],["translate","",1,"tb-flex","no-flex","align-center","w-1/5"],[1,"tb-required",3,"tb-hint-tooltip-icon"],[1,"flex","w-1/5"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","deviceNodeSource"],["matInput","","name","value","formControlName","deviceNodePattern",3,"placeholder"],["formControlName","deviceInfo","required","true",3,"connectorType","sourceTypes","deviceInfoType"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",7)(1,"mat-toolbar",8)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",9)(6,"div",10),t.ɵɵelementStart(7,"button",11),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",12),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",13)(11,"div",14)(12,"div",15),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(15,16),t.ɵɵtemplate(16,VK,34,25,"ng-template",17)(17,mY,14,9,"ng-template",17)(18,wY,70,45,"ng-template",17),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()(),t.ɵɵelementStart(19,"div",18)(20,"button",19),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"button",20),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingForm),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,11,n.MappingTypeTranslationsMap.get(null==n.data?null:n.data.mappingType))),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.HelpLinkByMappingTypeMap.get(n.data.mappingType)),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,13,n.MappingHintTranslationsMap.get(null==n.data?null:n.data.mappingType))," "),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",n.data.mappingType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingType.DATA),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingType.REQUESTS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingType.OPCUA),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(22,15,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.mappingForm.invalid||!n.mappingForm.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(25,17,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(SY,[se,_,m$,$n,A$,ai,ni]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:grid;height:100%}[_nghost-%COMP%] .key-mapping[_ngcontent-%COMP%]{max-width:900px;display:flex;flex-direction:column}[_nghost-%COMP%] .key-mapping[_ngcontent-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{min-height:64px}[_nghost-%COMP%] .key-mapping[_ngcontent-%COMP%] tb-toggle-select[_ngcontent-%COMP%]{padding:4px 0}[_nghost-%COMP%] .mat-mdc-dialog-content[_ngcontent-%COMP%]{height:670px}[_nghost-%COMP%] .ellipsis-chips-container[_ngcontent-%COMP%]{max-width:70%}[_nghost-%COMP%] .key-mapping .mat-mdc-chip-listbox .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center;flex-wrap:nowrap}[_nghost-%COMP%] .tb-form-row .fixed-title-width{min-width:40px;width:35%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-form-row .mat-mdc-form-field{width:0}[_nghost-%COMP%] .device-node-row.tb-form-row{gap:12px}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}']})}}e("MappingDialogComponent",SY);const CY=["searchInput"],_Y=()=>({minWidth:"96px",textAlign:"center"});function TY(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",24)(2,"span",25),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",9),t.ɵɵelementStart(6,"button",11),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageMapping(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",11),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,e.mappingTypeTranslationsMap.get(e.mappingType))),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search"))}}function IY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-header-cell",29),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext();t.ɵɵclassProp("request-column",n.mappingType===n.mappingTypeEnum.REQUESTS),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,3,e.title)," ")}}function EY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext().$implicit,a=t.ɵɵnextContext();t.ɵɵclassProp("request-column",a.mappingType===a.mappingTypeEnum.REQUESTS),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e[i.def]," ")}}function MY(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,26),t.ɵɵtemplate(1,IY,3,5,"mat-header-cell",27)(2,EY,2,3,"mat-cell",28),t.ɵɵelementContainerEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("matColumnDef",e.def)}}function kY(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",31)}function PY(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",11),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageMapping(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",11),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteMapping(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function DY(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,PY,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",32),t.ɵɵelementContainer(4,33),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",34)(6,"button",35),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",36),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",37,2),t.ɵɵelementContainer(11,33),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,_Y)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function OY(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",38)}function AY(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class FY{set mappingType(e){this.mappingTypeValue!==e&&(this.mappingTypeValue=e)}get mappingType(){return this.mappingTypeValue}constructor(e,t,n,i){this.translate=e,this.dialog=t,this.dialogService=n,this.fb=i,this.required=!1,this.withReportStrategy=!0,this.mappingTypeTranslationsMap=Li,this.mappingTypeEnum=Ni,this.displayedColumns=[],this.mappingColumns=[],this.textSearchMode=!1,this.hidePageSize=!1,this.activeValue=!1,this.dirtyValue=!1,this.textSearch=this.fb.control("",{nonNullable:!0}),this.onChange=()=>{},this.onTouched=()=>{},this.destroy$=new we,this.mappingFormGroup=this.fb.array([]),this.dirtyValue=!this.activeValue,this.dataSource=new RY}ngOnInit(){this.setMappingColumns(),this.displayedColumns.push(...this.mappingColumns.map((e=>e.def)),"actions"),this.mappingFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.updateTableData(e),this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(De(150),Pe(((e,t)=>(e??"")===t.trim())),Me(this.destroy$)).subscribe((e=>{const t=e.trim();this.updateTableData(this.mappingFormGroup.value,t.trim())}))}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){this.mappingFormGroup.clear(),this.pushDataAsFormArrays(e)}validate(){return!this.required||this.mappingFormGroup.controls.length?null:{mappingFormGroup:{valid:!1}}}enterFilterMode(){this.textSearchMode=!0,setTimeout((()=>{this.searchInputField.nativeElement.focus(),this.searchInputField.nativeElement.setSelectionRange(0,0)}),10)}exitFilterMode(){this.updateTableData(this.mappingFormGroup.value),this.textSearchMode=!1,this.textSearch.reset()}manageMapping(e,t){e&&e.stopPropagation();const n=J(t)?this.mappingFormGroup.at(t).value:{};this.dialog.open(SY,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{mappingType:this.mappingType,value:n,buttonTitle:ne(t)?"action.add":"action.apply",withReportStrategy:this.withReportStrategy}}).afterClosed().pipe(Ne(1),Me(this.destroy$)).subscribe((e=>{e&&(J(t)?this.mappingFormGroup.at(t).patchValue(e):this.pushDataAsFormArrays([e]),this.mappingFormGroup.markAsDirty())}))}updateTableData(e,t){let n=e.map((e=>this.getMappingValue(e)));t&&(n=n.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(n)}deleteMapping(e,t){e&&e.stopPropagation();const n=this.mappingFormGroup.controls[t].value,i=n.deviceInfo?.deviceNameExpression??n.topicFilter??this.getRequestDetails(n);this.dialogService.confirm(this.translate.instant("gateway.delete-mapping-title",{name:i}),this.translate.instant("gateway.delete-mapping-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).subscribe((e=>{e&&(this.mappingFormGroup.removeAt(t),this.mappingFormGroup.markAsDirty())}))}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.mappingFormGroup.push(this.fb.control(e))))}getMappingValue(e){switch(this.mappingType){case Ni.DATA:const t=pi.get(e.converter?.type);return{topicFilter:e.topicFilter,QoS:e.subscriptionQos,converter:t?this.translate.instant(t):""};case Ni.REQUESTS:return{requestType:e.requestType,type:this.translate.instant(di.get(e.requestType)),details:this.getRequestDetails(e)};case Ni.OPCUA:const n=e.deviceInfo?.deviceNameExpression,i=e.deviceInfo?.deviceProfileExpression,{deviceNodePattern:a}=e;return{deviceNodePattern:a,deviceNamePattern:n,deviceProfileExpression:i};default:return{}}}getRequestDetails(e){let t;return t=e.requestType===ci.ATTRIBUTE_UPDATE?e.requestValue.attributeFilter:e.requestType===ci.SERVER_SIDE_RPC?e.requestValue.methodFilter:e.requestValue.topicFilter,t}setMappingColumns(){switch(this.mappingType){case Ni.DATA:this.mappingColumns.push({def:"topicFilter",title:"gateway.topic-filter"},{def:"QoS",title:"gateway.mqtt-qos"},{def:"converter",title:"gateway.payload-type"});break;case Ni.REQUESTS:this.mappingColumns.push({def:"type",title:"gateway.type"},{def:"details",title:"gateway.details"});break;case Ni.OPCUA:this.mappingColumns.push({def:"deviceNodePattern",title:"gateway.device-node"},{def:"deviceNamePattern",title:"gateway.device-name"},{def:"deviceProfileExpression",title:"gateway.device-profile"})}}static{this.ɵfac=function(e){return new(e||FY)(t.ɵɵdirectiveInject(le.TranslateService),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(K.DialogService),t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:FY,selectors:[["tb-mapping-table"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(CY,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{required:"required",withReportStrategy:"withReportStrategy",mappingType:"mappingType"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>FY)),multi:!0},{provide:he,useExisting:c((()=>FY)),multi:!0}]),t.ɵɵStandaloneFeature],decls:40,vars:33,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-mapping-table","tb-absolute-fill"],[1,"tb-mapping-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef",4,"ngFor","ngForOf"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-mapping-table-title"],[3,"matColumnDef"],["class","table-value-column",3,"request-column",4,"matHeaderCellDef"],["tbTruncateWithTooltip","","class","table-value-column",3,"request-column",4,"matCellDef"],[1,"table-value-column"],["tbTruncateWithTooltip","",1,"table-value-column"],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,TY,14,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"div",7)(7,"button",8),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-icon"),t.ɵɵtext(10,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",9)(12,"mat-label"),t.ɵɵtext(13," "),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",10,0),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵpipe(18,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(19,"mat-icon"),t.ɵɵtext(20,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(21,"div",12)(22,"table",13),t.ɵɵtemplate(23,MY,3,1,"ng-container",14),t.ɵɵelementContainerStart(24,15),t.ɵɵtemplate(25,kY,1,0,"mat-header-cell",16)(26,DY,12,6,"mat-cell",17),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(27,OY,1,0,"mat-header-row",18)(28,AY,1,0,"mat-row",19),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"section",20),t.ɵɵpipe(30,"async"),t.ɵɵelementStart(31,"button",21),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageMapping(i))})),t.ɵɵelementStart(32,"mat-icon",22),t.ɵɵtext(33,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"span"),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(37,"span",23),t.ɵɵpipe(38,"async"),t.ɵɵtext(39," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(4,19,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,21,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,23,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,25,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.mappingColumns),t.ɵɵadvance(4),t.ɵɵproperty("matHeaderRowDef",n.displayedColumns)("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",n.displayedColumns),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(30,27,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,29,"gateway.add-mapping")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(38,31,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(FY,[se,_,Wn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%]{width:100%;height:100%;background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content.tb-outlined-border[_ngcontent-%COMP%]{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;border:solid 1px #e0e0e0;border-radius:4px}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .tb-mapping-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:21%}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column.request-column[_ngcontent-%COMP%]{width:35%}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .ellipsis[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-mapping-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}e("MappingTableComponent",FY),He([I()],FY.prototype,"required",void 0),He([I()],FY.prototype,"withReportStrategy",void 0);let RY=class extends G{constructor(){super()}};function BY(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",7),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SecurityTypeTranslationsMap.get(e))," ")}}function NY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",18),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.username-required"))}function LY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",8)(1,"div",9),t.ɵɵtext(2,"gateway.username"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",10)(4,"mat-form-field",11),t.ɵɵelement(5,"input",12),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,NY,3,3,"mat-icon",13),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",8)(9,"div",14),t.ɵɵtext(10,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"div",10)(12,"mat-form-field",11),t.ɵɵelement(13,"input",15),t.ɵɵpipe(14,"translate"),t.ɵɵelementStart(15,"div",16),t.ɵɵelement(16,"tb-toggle-password",17),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,3,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("username").hasError("required")&&e.securityFormGroup.get("username").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,5,"gateway.set"))}}function VY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",7),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function qY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",18),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.username-required"))}function GY(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",8)(2,"div",14),t.ɵɵtext(3,"gateway.mode"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",10)(5,"mat-form-field",11)(6,"mat-select",25),t.ɵɵtemplate(7,VY,2,2,"mat-option",4),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(8,"div",8)(9,"div",14),t.ɵɵtext(10,"gateway.username"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"div",10)(12,"mat-form-field",11),t.ɵɵelement(13,"input",12),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,qY,3,3,"mat-icon",13),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",8)(17,"div",14),t.ɵɵtext(18,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",10)(20,"mat-form-field",11),t.ɵɵelement(21,"input",15),t.ɵɵpipe(22,"translate"),t.ɵɵelementStart(23,"div",16),t.ɵɵelement(24,"tb-toggle-password",17),t.ɵɵelementEnd()()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",e.modeTypes),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,4,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("username").hasError("required")&&e.securityFormGroup.get("username").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(22,6,"gateway.set"))}}function zY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",8)(4,"div",20),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",10)(8,"mat-form-field",11),t.ɵɵelement(9,"input",21),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(11,"div",8)(12,"div",20),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"div",10)(16,"mat-form-field",11),t.ɵɵelement(17,"input",22),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(19,"div",8)(20,"div",20),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"div",10)(24,"mat-form-field",11),t.ɵɵelement(25,"input",23),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(27,GY,25,8,"ng-container",24)),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,8,"gateway.path-hint")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,10,"gateway.CA-certificate-path")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(10,12,"gateway.set")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,14,"gateway.private-key-path")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(18,16,"gateway.set")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(22,18,"gateway.client-cert-path")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(26,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mode()===e.SecurityMode.extendedCertificates)}}e("MappingDatasource",RY);class UY{constructor(e,t){this.fb=e,this.cdr=t,this.title="gateway.security",this.mode=l(Ji.certificates),this.BrokerSecurityType=Ri,this.SecurityMode=Ji,this.securityTypes=Object.values(Ri),this.modeTypes=Object.values(Bi),this.SecurityTypeTranslationsMap=Gi,this.destroy$=new we,o((()=>{this.mode()===Ji.basic&&(this.securityTypes=this.securityTypes.filter((e=>e!==Ri.CERTIFICATES)))}))}ngOnInit(){this.securityFormGroup=this.fb.group({type:[Ri.ANONYMOUS,[]],username:["",[me.required,me.pattern(pn)]],password:["",[me.pattern(pn)]],pathToCACert:["",[me.pattern(pn)]],pathToPrivateKey:["",[me.pattern(pn)]],pathToClientCert:["",[me.pattern(pn)]]}),this.mode()===Ji.extendedCertificates&&this.securityFormGroup.addControl("mode",this.fb.control(Bi.NONE,[])),this.securityFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{Y(e),this.onChange(e),this.onTouched()})),this.securityFormGroup.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.updateValidators(e)))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}writeValue(e){if(e)e.type||(e.type=Ri.ANONYMOUS),this.updateValidators(e.type),this.securityFormGroup.reset(e,{emitEvent:!1});else{const e={type:Ri.ANONYMOUS};this.securityFormGroup.reset(e,{emitEvent:!1})}this.cdr.markForCheck()}validate(){return this.securityFormGroup.get("type").value!==Ri.BASIC||this.securityFormGroup.valid?null:{securityForm:{valid:!1}}}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}updateValidators(e){if(e)if(this.securityFormGroup.get("username").disable({emitEvent:!1}),this.securityFormGroup.get("password").disable({emitEvent:!1}),this.securityFormGroup.get("pathToCACert").disable({emitEvent:!1}),this.securityFormGroup.get("pathToPrivateKey").disable({emitEvent:!1}),this.securityFormGroup.get("pathToClientCert").disable({emitEvent:!1}),this.securityFormGroup.get("mode")?.disable({emitEvent:!1}),e===Ri.BASIC)this.securityFormGroup.get("username").enable({emitEvent:!1}),this.securityFormGroup.get("password").enable({emitEvent:!1});else if(e===Ri.CERTIFICATES&&(this.securityFormGroup.get("pathToCACert").enable({emitEvent:!1}),this.securityFormGroup.get("pathToPrivateKey").enable({emitEvent:!1}),this.securityFormGroup.get("pathToClientCert").enable({emitEvent:!1}),this.mode()===Ji.extendedCertificates)){const e=this.securityFormGroup.get("mode");e&&!e.value&&e.setValue(Bi.NONE,{emitEvent:!1}),e?.enable({emitEvent:!1}),this.securityFormGroup.get("username").enable({emitEvent:!1}),this.securityFormGroup.get("password").enable({emitEvent:!1})}}static{this.ɵfac=function(e){return new(e||UY)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:UY,selectors:[["tb-security-config"]],inputs:{title:"title",mode:[1,"mode"]},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>UY)),multi:!0},{provide:he,useExisting:c((()=>UY)),multi:!0}]),t.ɵɵStandaloneFeature],decls:10,vars:8,consts:[[1,"tb-form-row","space-between","same-padding","tb-flex","column",3,"formGroup"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],[1,"fixed-title-width","tb-required"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],[3,"ngSwitch"],[3,"ngSwitchCase"],[3,"value"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","username",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width"],["matInput","","type","password","name","value","formControlName","password",3,"placeholder"],["matSuffix","",1,"tb-flex","no-gap","align-center","fill-height"],[1,"tb-flex","align-center","fill-height"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[1,"tb-form-hint","tb-primary-fill"],["tbTruncateWithTooltip","",1,"fixed-title-width"],["matInput","","name","value","formControlName","pathToCACert",3,"placeholder"],["matInput","","name","value","formControlName","pathToPrivateKey",3,"placeholder"],["matInput","","name","value","formControlName","pathToClientCert",3,"placeholder"],[4,"ngIf"],["formControlName","mode"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-select",3),t.ɵɵtemplate(6,BY,3,4,"tb-toggle-option",4),t.ɵɵelementEnd()(),t.ɵɵelementContainerStart(7,5),t.ɵɵtemplate(8,LY,17,7,"ng-template",6)(9,zY,28,22,"ng-template",6),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.securityFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,6,n.title)),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.securityTypes),t.ɵɵadvance(),t.ɵɵproperty("ngSwitch",n.securityFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.BrokerSecurityType.BASIC),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.BrokerSecurityType.CERTIFICATES))},dependencies:t.ɵɵgetComponentDepsFactory(UY,[se,_,Wn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}'],changeDetection:d.OnPush})}}e("SecurityConfigComponent",UY);const jY=()=>({min:1e3}),HY=()=>({min:50}),WY=()=>({min:100});function $Y(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.server-url-required"))}function KY(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",9),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.timeout-error",t.ɵɵpureFunction0(4,jY)))}function YY(e,n){1&e&&(t.ɵɵelementStart(0,"span",10),t.ɵɵtext(1,"gateway.suffix.ms"),t.ɵɵelementEnd())}function XY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",22),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.value),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.name)}}function ZY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind2(1,1,"gateway.scan-period-error",t.ɵɵpureFunction0(4,jY)))}function QY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind2(1,1,"gateway.poll-period-error",t.ɵɵpureFunction0(4,HY)))}function JY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"div",6),t.ɵɵpipe(2,"translate"),t.ɵɵelementStart(3,"div",7),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"mat-form-field",3),t.ɵɵelement(7,"input",23),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,QY,3,5,"mat-icon",5),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,4,"gateway.hints.poll-period")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,6,"gateway.poll-period")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.serverConfigFormGroup.get("pollPeriodInMillis").hasError("required")||e.serverConfigFormGroup.get("pollPeriodInMillis").hasError("min"))&&e.serverConfigFormGroup.get("pollPeriodInMillis").touched)}}function eX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind2(1,1,"gateway.sub-check-period-error",t.ɵɵpureFunction0(4,WY)))}class tX{constructor(e){this.fb=e,this.hideNewFields=!1,this.securityPolicyTypes=qi,this.SecurityMode=Ji,this.destroy$=new we,this.serverConfigFormGroup=this.fb.group({url:["",[me.required,me.pattern(pn)]],timeoutInMillis:[1e3,[me.required,me.min(1e3)]],scanPeriodInMillis:[z,[me.required,me.min(1e3)]],pollPeriodInMillis:[5e3,[me.required,me.min(50)]],enableSubscriptions:[!0,[]],subCheckPeriodInMillis:[100,[me.required,me.min(100)]],showMap:[!1,[]],security:[Vi.BASIC128,[]],identity:[]}),this.serverConfigFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}ngAfterViewInit(){this.hideNewFields&&this.serverConfigFormGroup.get("pollPeriodInMillis").disable({emitEvent:!1})}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.serverConfigFormGroup.valid?null:{serverConfigFormGroup:{valid:!1}}}writeValue(e){const{timeoutInMillis:t=1e3,scanPeriodInMillis:n=z,pollPeriodInMillis:i=5e3,enableSubscriptions:a=!0,subCheckPeriodInMillis:r=100,showMap:o=!1,security:s=Vi.BASIC128,identity:l={}}=e;this.serverConfigFormGroup.reset({...e,timeoutInMillis:t,scanPeriodInMillis:n,pollPeriodInMillis:i,enableSubscriptions:a,subCheckPeriodInMillis:r,showMap:o,security:s,identity:l},{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||tX)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:tX,selectors:[["tb-opc-server-config"]],inputs:{hideNewFields:"hideNewFields"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>tX)),multi:!0},{provide:he,useExisting:c((()=>tX)),multi:!0}]),t.ɵɵStandaloneFeature],decls:63,vars:56,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],["tbTruncateWithTooltip","","translate","",1,"fixed-title-width"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","url",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],[1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip",""],["matInput","","type","number","min","1000","name","value","formControlName","timeoutInMillis",3,"placeholder"],["matSuffix","",3,"tooltipText"],["translate","","matSuffix","",1,"block","pr-2"],["formControlName","security"],[3,"value",4,"ngFor","ngForOf"],["matInput","","type","number","min","1000","name","value","formControlName","scanPeriodInMillis",3,"placeholder"],["class","tb-form-row column-xs",4,"ngIf"],["matInput","","type","number","min","100","name","value","formControlName","subCheckPeriodInMillis",3,"placeholder"],[1,"tb-form-row"],["formControlName","enableSubscriptions",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","showMap",1,"mat-slide"],["formControlName","identity",3,"mode"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["matInput","","type","number","min","50","name","value","formControlName","pollPeriodInMillis",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.server-url"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",3),t.ɵɵelement(5,"input",4),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,$Y,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",1)(9,"div",6),t.ɵɵpipe(10,"translate"),t.ɵɵelementStart(11,"div",7),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(14,"mat-form-field",3),t.ɵɵelement(15,"input",8),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,KY,2,5,"tb-error-icon",9)(18,YY,2,0,"span",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(19,"div",1)(20,"div",6),t.ɵɵpipe(21,"translate"),t.ɵɵelementStart(22,"div",7),t.ɵɵtext(23),t.ɵɵpipe(24,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(25,"mat-form-field",3)(26,"mat-select",11),t.ɵɵtemplate(27,XY,2,2,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(28,"div",1)(29,"div",6),t.ɵɵpipe(30,"translate"),t.ɵɵelementStart(31,"div",7),t.ɵɵtext(32),t.ɵɵpipe(33,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"mat-form-field",3),t.ɵɵelement(35,"input",13),t.ɵɵpipe(36,"translate"),t.ɵɵtemplate(37,ZY,3,5,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵtemplate(38,JY,10,10,"div",14),t.ɵɵelementStart(39,"div",1)(40,"div",6),t.ɵɵpipe(41,"translate"),t.ɵɵelementStart(42,"div",7),t.ɵɵtext(43),t.ɵɵpipe(44,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"mat-form-field",3),t.ɵɵelement(46,"input",15),t.ɵɵpipe(47,"translate"),t.ɵɵtemplate(48,eX,3,5,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(49,"div",16)(50,"mat-slide-toggle",17)(51,"mat-label",18),t.ɵɵpipe(52,"translate"),t.ɵɵelementStart(53,"div",7),t.ɵɵtext(54),t.ɵɵpipe(55,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(56,"div",16)(57,"mat-slide-toggle",19)(58,"mat-label",18),t.ɵɵpipe(59,"translate"),t.ɵɵtext(60),t.ɵɵpipe(61,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelement(62,"tb-security-config",20),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.serverConfigFormGroup),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.serverConfigFormGroup.get("url").hasError("required")&&n.serverConfigFormGroup.get("url").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,26,"gateway.hints.opc-timeout")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,28,"gateway.timeout")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,30,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional((n.serverConfigFormGroup.get("timeoutInMillis").hasError("required")||n.serverConfigFormGroup.get("timeoutInMillis").hasError("min"))&&n.serverConfigFormGroup.get("timeoutInMillis").touched?17:18),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(21,32,"gateway.hints.security-policy")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(24,34,"gateway.security-policy")),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",n.securityPolicyTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(30,36,"gateway.hints.scan-period")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(33,38,"gateway.scan-period")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,40,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.serverConfigFormGroup.get("scanPeriodInMillis").hasError("required")||n.serverConfigFormGroup.get("scanPeriodInMillis").hasError("min"))&&n.serverConfigFormGroup.get("scanPeriodInMillis").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.hideNewFields),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(41,42,"gateway.hints.sub-check-period")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(44,44,"gateway.sub-check-period")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(47,46,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.serverConfigFormGroup.get("subCheckPeriodInMillis").hasError("required")||n.serverConfigFormGroup.get("subCheckPeriodInMillis").hasError("min"))&&n.serverConfigFormGroup.get("subCheckPeriodInMillis").touched),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(52,48,"gateway.hints.enable-subscription")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(55,50,"gateway.enable-subscription")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(59,52,"gateway.hints.show-map")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(61,54,"gateway.show-map")," "),t.ɵɵadvance(2),t.ɵɵproperty("mode",n.SecurityMode.extendedCertificates))},dependencies:t.ɵɵgetComponentDepsFactory(tX,[se,_,UY,Wn,ai]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}'],changeDetection:d.OnPush})}}e("OpcServerConfigComponent",tX),He([I()],tX.prototype,"hideNewFields",void 0);class nX extends Ua{constructor(){super(...arguments),this.withReportStrategy=!0,this.mappingTypes=Ni,this.isLegacy=!0}initBasicFormGroup(){return this.fb.group({mapping:[],server:[]})}mapConfigToFormValue(e){let t;return t=e?.server&&"mapping"in e?.server?Za.mapMappingToUpgradedVersion(e.server.mapping):e&&"mapping"in e?Za.mapMappingToCurrentVersion(e.mapping):[],{server:e.server?Za.mapServerToUpgradedVersion(e.server):{},mapping:t}}getMappedValue(e){return{server:Za.mapServerToDowngradedVersion(e)}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(nX)))(n||nX)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:nX,selectors:[["tb-opc-ua-legacy-basic-config"]],inputs:{withReportStrategy:"withReportStrategy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>nX)),multi:!0},{provide:he,useExisting:c((()=>nX)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server",3,"hideNewFields"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"required","withReportStrategy","mappingType"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-opc-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,11,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,13,"gateway.server"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("hideNewFields",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,15,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("required",!0)("withReportStrategy",n.withReportStrategy)("mappingType",n.mappingTypes.OPCUA))},dependencies:t.ɵɵgetComponentDepsFactory(nX,[se,_,FY,tX]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("OpcUaLegacyBasicConfigComponent",nX),He([I()],nX.prototype,"withReportStrategy",void 0);class iX extends Ua{constructor(){super(...arguments),this.withReportStrategy=!0,this.mappingTypes=Ni,this.isLegacy=!1}initBasicFormGroup(){return this.fb.group({mapping:[],server:[]})}mapConfigToFormValue(e){return{server:e.server??{},mapping:e.mapping??[]}}getMappedValue(e){return{server:e.server,mapping:e.mapping}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(iX)))(n||iX)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:iX,selectors:[["tb-opc-ua-basic-config"]],inputs:{withReportStrategy:"withReportStrategy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>iX)),multi:!0},{provide:he,useExisting:c((()=>iX)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server",3,"hideNewFields"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"required","withReportStrategy","mappingType"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-opc-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,11,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,13,"gateway.server"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("hideNewFields",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,15,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("required",!0)("withReportStrategy",n.withReportStrategy)("mappingType",n.mappingTypes.OPCUA))},dependencies:t.ɵɵgetComponentDepsFactory(iX,[se,_,UY,FY,tX]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("OpcUaBasicConfigComponent",iX),He([I()],iX.prototype,"withReportStrategy",void 0);class aX extends Ua{constructor(){super(...arguments),this.withReportStrategy=!0,this.MappingType=Ni}initBasicFormGroup(){return this.fb.group({mapping:[],requestsMapping:[],broker:[],workers:[]})}getRequestDataArray(e){const t=[];return ie(e)&&Object.keys(e).forEach((n=>{for(const i of e[n])t.push({requestType:n,requestValue:i})})),t}getRequestDataObject(e){return e.reduce(((e,{requestType:t,requestValue:n})=>(e[t].push(n),e)),{connectRequests:[],disconnectRequests:[],attributeRequests:[],attributeUpdates:[],serverSideRpc:[]})}getBrokerMappedValue(e,t){return{...e,maxNumberOfWorkers:t.maxNumberOfWorkers??100,maxMessageNumberPerWorker:t.maxMessageNumberPerWorker??10}}writeValue(e){this.basicFormGroup.setValue(this.mapConfigToFormValue(e),{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(aX)))(n||aX)}})()}static{this.ɵdir=t.ɵɵdefineDirective({type:aX,inputs:{withReportStrategy:"withReportStrategy"},features:[t.ɵɵInheritDefinitionFeature]})}}function rX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",8),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.max-number-of-workers-required"))}function oX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",8),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.max-messages-queue-for-worker-required"))}e("MqttBasicConfigDirective",aX),He([I()],aX.prototype,"withReportStrategy",void 0);class sX{constructor(e){this.fb=e,this.destroy$=new we,this.workersConfigFormGroup=this.fb.group({maxNumberOfWorkers:[100,[me.required,me.min(1)]],maxMessageNumberPerWorker:[10,[me.required,me.min(1)]]}),this.workersConfigFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){const{maxNumberOfWorkers:t,maxMessageNumberPerWorker:n}=e;this.workersConfigFormGroup.reset({maxNumberOfWorkers:t||100,maxMessageNumberPerWorker:n||10},{emitEvent:!1})}validate(){return this.workersConfigFormGroup.valid?null:{workersConfigFormGroup:{valid:!1}}}static{this.ɵfac=function(e){return new(e||sX)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:sX,selectors:[["tb-workers-config-control"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>sX)),multi:!0},{provide:he,useExisting:c((()=>sX)),multi:!0}]),t.ɵɵStandaloneFeature],decls:21,vars:21,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",2,"width","50%",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip",""],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","type","number","min","1","formControlName","maxNumberOfWorkers",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","name","value","type","number","min","1","formControlName","maxMessageNumberPerWorker",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"div",3),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"mat-form-field",4),t.ɵɵelement(8,"input",5),t.ɵɵpipe(9,"translate"),t.ɵɵtemplate(10,rX,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"div",1)(12,"div",2),t.ɵɵpipe(13,"translate"),t.ɵɵelementStart(14,"div",3),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"mat-form-field",4),t.ɵɵelement(18,"input",7),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,oX,3,3,"mat-icon",6),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.workersConfigFormGroup),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,9,"gateway.max-number-of-workers-hint")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,11,"gateway.max-number-of-workers")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(9,13,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.workersConfigFormGroup.get("maxNumberOfWorkers").hasError("min")||n.workersConfigFormGroup.get("maxNumberOfWorkers").hasError("required")&&n.workersConfigFormGroup.get("maxNumberOfWorkers").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(13,15,"gateway.max-messages-queue-for-worker-hint")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(16,17,"gateway.max-messages-queue-for-worker")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,19,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.workersConfigFormGroup.get("maxMessageNumberPerWorker").hasError("min")||n.workersConfigFormGroup.get("maxMessageNumberPerWorker").hasError("required")&&n.workersConfigFormGroup.get("maxMessageNumberPerWorker").touched))},dependencies:t.ɵɵgetComponentDepsFactory(sX,[se,_,Wn]),encapsulation:2,changeDetection:d.OnPush})}}function lX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",13),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function pX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",13),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.brokerConfigFormGroup.get("port")))}}function cX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",14),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.value),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.name)}}function dX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",15),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("clientId"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.generate-client-id"))}e("WorkersConfigControlComponent",sX);class uX{constructor(e,t){this.fb=e,this.cdr=t,this.mqttVersions=si,this.portLimits=Oi,this.destroy$=new we,this.brokerConfigFormGroup=this.fb.group({host:["",[me.required,me.pattern(pn)]],port:[null,[me.required,me.min(Oi.MIN),me.max(Oi.MAX)]],version:[5,[]],clientId:["tb_gw_"+ae(5),[me.pattern(pn)]],security:[]}),this.brokerConfigFormGroup.valueChanges.subscribe((e=>{this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}generate(e){this.brokerConfigFormGroup.get(e)?.patchValue("tb_gw_"+ae(5))}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){const{version:t=5,clientId:n=`tb_gw_${ae(5)}`,security:i={}}=e;this.brokerConfigFormGroup.reset({...e,version:t,clientId:n,security:i},{emitEvent:!1}),this.cdr.markForCheck()}validate(){return this.brokerConfigFormGroup.valid?null:{brokerConfigFormGroup:{valid:!1}}}static{this.ɵfac=function(e){return new(e||uX)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:uX,selectors:[["tb-broker-config-control"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>uX)),multi:!0},{provide:he,useExisting:c((()=>uX)),multi:!0}]),t.ɵɵStandaloneFeature],decls:29,vars:16,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["translate","",1,"fixed-title-width"],["formControlName","version"],[3,"value",4,"ngFor","ngForOf"],["matInput","","name","value","formControlName","clientId",3,"placeholder"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"matTooltip","click",4,"ngIf"],["formControlName","security"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"click","matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",3),t.ɵɵelement(5,"input",4),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,lX,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",1)(9,"div",2),t.ɵɵtext(10,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",3),t.ɵɵelement(12,"input",6),t.ɵɵpipe(13,"translate"),t.ɵɵtemplate(14,pX,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"div",1)(16,"div",7),t.ɵɵtext(17,"gateway.mqtt-version"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"mat-form-field",3)(19,"mat-select",8),t.ɵɵtemplate(20,cX,2,2,"mat-option",9),t.ɵɵelementEnd()()(),t.ɵɵelementStart(21,"div",1)(22,"div",7),t.ɵɵtext(23,"gateway.client-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(24,"mat-form-field",3),t.ɵɵelement(25,"input",10),t.ɵɵpipe(26,"translate"),t.ɵɵtemplate(27,dX,4,3,"button",11),t.ɵɵelementEnd()(),t.ɵɵelement(28,"tb-security-config",12),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.brokerConfigFormGroup),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,10,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.brokerConfigFormGroup.get("host").hasError("required")&&n.brokerConfigFormGroup.get("host").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(13,12,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.brokerConfigFormGroup.get("port").hasError("required")||n.brokerConfigFormGroup.get("port").hasError("min")||n.brokerConfigFormGroup.get("port").hasError("max"))&&n.brokerConfigFormGroup.get("port").touched),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.mqttVersions),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(26,14,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",!n.brokerConfigFormGroup.get("clientId").value))},dependencies:t.ɵɵgetComponentDepsFactory(uX,[se,_,UY,u$]),encapsulation:2,changeDetection:d.OnPush})}}e("BrokerConfigControlComponent",uX);class mX extends aX{mapConfigToFormValue(e){const{broker:t,mapping:n=[],requestsMapping:i}=e;return{workers:t&&(t.maxNumberOfWorkers||t.maxMessageNumberPerWorker)?{maxNumberOfWorkers:t.maxNumberOfWorkers,maxMessageNumberPerWorker:t.maxMessageNumberPerWorker}:{},mapping:n??[],broker:t??{},requestsMapping:this.getRequestDataArray(i)}}getMappedValue(e){const{broker:t,workers:n,mapping:i,requestsMapping:a}=e||{};return{broker:this.getBrokerMappedValue(t,n),mapping:i,requestsMapping:a?.length?this.getRequestDataObject(a):{}}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(mX)))(n||mX)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:mX,selectors:[["tb-mqtt-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>mX)),multi:!0},{provide:he,useExisting:c((()=>mX)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:24,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","broker"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"withReportStrategy","required","mappingType"],["formControlName","requestsMapping",3,"withReportStrategy","mappingType"],[1,"tb-form-panel","no-border","no-padding"],["formControlName","workers"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-broker-config-control",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-tab",1),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"div",4),t.ɵɵelement(14,"tb-mapping-table",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"mat-tab",1),t.ɵɵpipe(16,"translate"),t.ɵɵelementStart(17,"div",7),t.ɵɵelement(18,"tb-workers-config-control",8),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,14,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,16,"gateway.broker.connection"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,18,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("required",!0)("mappingType",n.MappingType.DATA),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(12,20,"gateway.requests-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("mappingType",n.MappingType.REQUESTS),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(16,22,"gateway.workers-settings")))},dependencies:t.ɵɵgetComponentDepsFactory(mX,[se,_,sX,uX,FY]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("MqttBasicConfigComponent",mX);class hX extends aX{mapConfigToFormValue(e){const{broker:t,mapping:n=[],connectRequests:i=[],disconnectRequests:a=[],attributeRequests:r=[],attributeUpdates:o=[],serverSideRpc:s=[]}=e,l=Na.mapRequestsToUpgradedVersion({connectRequests:i,disconnectRequests:a,attributeRequests:r,attributeUpdates:o,serverSideRpc:s});return{workers:t&&(t.maxNumberOfWorkers||t.maxMessageNumberPerWorker)?{maxNumberOfWorkers:t.maxNumberOfWorkers,maxMessageNumberPerWorker:t.maxMessageNumberPerWorker}:{},mapping:Na.mapMappingToUpgradedVersion(n)||[],broker:t||{},requestsMapping:this.getRequestDataArray(l)}}getMappedValue(e){const{broker:t,workers:n,mapping:i,requestsMapping:a}=e||{},r=a?.length?this.getRequestDataObject(a):{};return{broker:this.getBrokerMappedValue(t,n),mapping:Na.mapMappingToDowngradedVersion(i),...Na.mapRequestsToDowngradedVersion(r)}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(hX)))(n||hX)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:hX,selectors:[["tb-mqtt-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>hX)),multi:!0},{provide:he,useExisting:c((()=>hX)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:24,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","broker"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"withReportStrategy","required","mappingType"],["formControlName","requestsMapping",3,"withReportStrategy","mappingType"],[1,"tb-form-panel","no-border","no-padding"],["formControlName","workers"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-broker-config-control",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-tab",1),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"div",4),t.ɵɵelement(14,"tb-mapping-table",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"mat-tab",1),t.ɵɵpipe(16,"translate"),t.ɵɵelementStart(17,"div",7),t.ɵɵelement(18,"tb-workers-config-control",8),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,14,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,16,"gateway.broker.connection"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,18,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("required",!0)("mappingType",n.MappingType.DATA),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(12,20,"gateway.requests-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("mappingType",n.MappingType.REQUESTS),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(16,22,"gateway.workers-settings")))},dependencies:t.ɵɵgetComponentDepsFactory(hX,[se,_,sX,uX,FY]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("MqttLegacyBasicConfigComponent",hX);class gX extends A{constructor(e,t,n,i,a){super(t,n,a),this.fb=e,this.store=t,this.router=n,this.data=i,this.dialogRef=a,this.portLimits=Oi,this.modbusProtocolTypes=Object.values(ea),this.modbusMethodTypes=Object.values(ta),this.modbusSerialMethodTypes=Object.values(na),this.modbusParities=Object.values(ia),this.modbusByteSizes=la,this.modbusBaudrates=sa,this.modbusOrderType=Object.values(aa),this.ModbusProtocolType=ea,this.ModbusParityLabelsMap=ga,this.ModbusProtocolLabelsMap=ha,this.ModbusMethodLabelsMap=ma,this.ReportStrategyDefaultValue=on,this.modbusHelpLink=O+"/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters",this.serialSpecificControlKeys=["serialPort","baudrate","stopbits","bytesize","parity","strict"],this.tcpUdpSpecificControlKeys=["port","security","host"],this.destroy$=new we,this.showSecurityControl=this.fb.control(!1),this.initializeSlaveFormGroup(),this.updateSlaveFormGroup(),this.updateControlsEnabling(this.data.value.type),this.observeTypeChange(),this.observeShowSecurity(),this.showSecurityControl.patchValue(!!this.data.value.security&&!W(this.data.value.security,{}))}get protocolType(){return this.slaveConfigFormGroup.get("type").value}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}cancel(){this.dialogRef.close(null)}add(){this.slaveConfigFormGroup.valid&&this.dialogRef.close(this.getSlaveResultData())}initializeSlaveFormGroup(){this.slaveConfigFormGroup=this.fb.group({type:[ea.TCP],host:["",[me.required,me.pattern(pn)]],port:[null,[me.required,me.min(Oi.MIN),me.max(Oi.MAX)]],serialPort:["",[me.required,me.pattern(pn)]],method:[ta.SOCKET,[me.required]],baudrate:[this.modbusBaudrates[0]],stopbits:[1],bytesize:[la[0]],parity:[ia.None],strict:[!0],unitId:[null,[me.required]],deviceName:["",[me.required,me.pattern(pn)]],deviceType:["",[me.required,me.pattern(pn)]],timeout:[35],byteOrder:[aa.BIG],wordOrder:[aa.BIG],retries:[pa],retryOnEmpty:[!0],retryOnInvalid:[!0],pollPeriod:[1e3,[me.required]],connectAttemptTimeMs:[500,[me.required]],connectAttemptCount:[5,[me.required]],waitAfterFailedAttemptsMs:[3e4,[me.required]],values:[{}],security:[{}]}),this.addFieldsToFormGroup()}updateSlaveFormGroup(){this.slaveConfigFormGroup.patchValue({...this.data.value,port:this.data.value.type===ea.Serial?null:this.data.value.port,serialPort:this.data.value.type===ea.Serial?this.data.value.port:"",retries:!1===this.data.value.retries?0:Z(this.data.value.retries)?this.data.value.retries:pa,values:{attributes:this.data.value.attributes??[],timeseries:this.data.value.timeseries??[],attributeUpdates:this.data.value.attributeUpdates??[],rpc:this.data.value.rpc??[]}})}observeTypeChange(){this.slaveConfigFormGroup.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.updateControlsEnabling(e),this.updateMethodType(e)}))}updateMethodType(e){this.slaveConfigFormGroup.get("method").value!==ta.RTU&&this.slaveConfigFormGroup.get("method").patchValue(e===ea.Serial?na.ASCII:ta.SOCKET,{emitEvent:!1})}updateControlsEnabling(e){const[t,n]=e===ea.Serial?[this.serialSpecificControlKeys,this.tcpUdpSpecificControlKeys]:[this.tcpUdpSpecificControlKeys,this.serialSpecificControlKeys];t.forEach((e=>this.slaveConfigFormGroup.get(e)?.enable({emitEvent:!1}))),n.forEach((e=>this.slaveConfigFormGroup.get(e)?.disable({emitEvent:!1}))),this.updateSecurityEnabling(this.showSecurityControl.value)}observeShowSecurity(){this.showSecurityControl.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.updateSecurityEnabling(e)))}updateSecurityEnabling(e){e&&this.protocolType!==ea.Serial?this.slaveConfigFormGroup.get("security").enable({emitEvent:!1}):this.slaveConfigFormGroup.get("security").disable({emitEvent:!1})}static{this.ɵfac=function(e){return new(e||gX)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef))}}static{this.ɵdir=t.ɵɵdefineDirective({type:gX,features:[t.ɵɵInheritDefinitionFeature]})}}e("ModbusSlaveDialogAbstract",gX);const fX=()=>({maxWidth:"970px"});function yX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate3(" ",e.get("tag").value,"",": ","",e.get("value").value," ")}}function vX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",22)(1,"div",23),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"span",24),t.ɵɵtext(5),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"div",25),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"span",26),t.ɵɵtext(10),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"div",25),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementStart(14,"span",26),t.ɵɵtext(15),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(3,6,n.keysType===n.ModbusValueKey.RPC_REQUESTS?"gateway.method":"gateway.key"),": "),t.ɵɵadvance(3),t.ɵɵtextInterpolate(e.get("tag").value),t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(8,8,"gateway.address"),": "),t.ɵɵadvance(3),t.ɵɵtextInterpolate(e.get("address").value),t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(13,10,"gateway.type"),": "),t.ɵɵadvance(3),t.ɵɵtextInterpolate(e.get("type").value)}}function xX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",46),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.keysType===e.ModbusValueKey.RPC_REQUESTS?"gateway.method-required":"gateway.key-required"))}}function bX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",47),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function wX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",47),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(5);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.ModbusFunctionCodeTranslationsMap.get(e))," ")}}function SX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",31)(1,"div",36),t.ɵɵtext(2,"gateway.function-code"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",33)(4,"mat-select",48),t.ɵɵtemplate(5,wX,3,4,"mat-option",38),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.functionCodesMap.get(e.get("id").value)||n.defaultFunctionCodes)}}function CX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",46),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.objects-count-required"))}function _X(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",46),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.modbus.max-bit"))}function TX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",31)(1,"div",49),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.bit"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",50),t.ɵɵelement(5,"input",51),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,_X,3,3,"mat-icon",35),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(3).$implicit;t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.bit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("bit").hasError("max")&&e.get("bit").touched)}}function IX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",47),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(6);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.BitTargetTypeTranslationMap.get(e)))}}function EX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",31)(1,"div",49),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.bit-target-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",52)(5,"mat-form-field",53)(6,"mat-select",54),t.ɵɵtemplate(7,IX,3,4,"mat-option",38),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(5);t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,2,"gateway.hints.modbus.bit-target-type")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",e.bitTargetTypes)}}function MX(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,TX,8,7,"div",39)(2,EX,8,4,"div",39),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("objectsCount").value>1),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.hideNewFields)}}function kX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",46),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-required"))}function PX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",47),t.ɵɵelement(1,"mat-icon",63),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(5);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("svgIcon",i.ModifierTypesMap.get(e).icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,i.ModifierTypesMap.get(e).name))}}function DX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",46),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.modifier-invalid"))}function OX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",55)(1,"mat-expansion-panel",16)(2,"mat-expansion-panel-header",17)(3,"mat-panel-title")(4,"mat-slide-toggle",56),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label",57),t.ɵɵpipe(6,"translate"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(9,"div",52)(10,"div",58)(11,"div",36),t.ɵɵtext(12,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",53)(14,"mat-select",59)(15,"mat-select-trigger")(16,"div",60),t.ɵɵelement(17,"mat-icon",61),t.ɵɵelementStart(18,"span"),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(21,PX,5,5,"mat-option",38),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(22,"div",31)(23,"div",36),t.ɵɵtext(24,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",50),t.ɵɵelement(26,"input",62),t.ɵɵpipe(27,"translate"),t.ɵɵtemplate(28,DX,3,3,"mat-icon",35),t.ɵɵelementEnd()()()()}if(2&e){let e,n;const i=t.ɵɵnextContext(2).$implicit,a=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("expanded",a.enableModifiersControlMap.get(i.get("id").value).value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",a.enableModifiersControlMap.get(i.get("id").value)),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,9,"gateway.hints.modbus.modifier")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(8,11,"gateway.modifier")," "),t.ɵɵadvance(10),t.ɵɵproperty("svgIcon",null==(e=a.ModifierTypesMap.get(i.get("modifierType").value))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,13,null==(n=a.ModifierTypesMap.get(i.get("modifierType").value))?null:n.name)),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",a.modifierTypes),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(27,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",i.get("modifierValue").hasError("pattern")&&i.get("modifierValue").touched)}}function AX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",46),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function FX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",31)(1,"div",64),t.ɵɵtext(2,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",33),t.ɵɵelement(4,"input",65),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,AX,3,3,"mat-icon",35),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,2,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("value").hasError("required")&&e.get("value").touched)}}function RX(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",66),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("defaultValue",e.ReportStrategyDefaultValue.Key)("isExpansionMode",!0)}}function BX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",27),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelement(3,"div",28),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",29)(5,"div",30),t.ɵɵtext(6,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",31)(8,"div",32),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",33),t.ɵɵelement(13,"input",34),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,xX,3,3,"mat-icon",35),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",29)(17,"div",30),t.ɵɵtext(18,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",31)(20,"div",36),t.ɵɵtext(21," gateway.type "),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",33)(23,"mat-select",37),t.ɵɵtemplate(24,bX,2,2,"mat-option",38),t.ɵɵelementEnd()()(),t.ɵɵtemplate(25,SX,6,1,"div",39),t.ɵɵelementStart(26,"div",31)(27,"div",40),t.ɵɵpipe(28,"translate"),t.ɵɵtext(29,"gateway.objects-count"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",33),t.ɵɵelement(31,"input",41),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,CX,3,3,"mat-icon",35),t.ɵɵelementEnd()(),t.ɵɵtemplate(34,MX,3,2,"ng-container",42),t.ɵɵelementStart(35,"div",31)(36,"div",40),t.ɵɵpipe(37,"translate"),t.ɵɵtext(38,"gateway.address"),t.ɵɵelementEnd(),t.ɵɵelementStart(39,"mat-form-field",33),t.ɵɵelement(40,"input",43),t.ɵɵpipe(41,"translate"),t.ɵɵtemplate(42,kX,3,3,"mat-icon",35),t.ɵɵelementEnd()(),t.ɵɵtemplate(43,OX,29,17,"div",44)(44,FX,7,4,"div",39)(45,RX,1,2,"tb-report-strategy",45),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,20,"gateway.hints.modbus.data-keys")," "),t.ɵɵadvance(2),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/modbus-functions-data-types_fn")("tb-help-popup-style",t.ɵɵpureFunction0(36,fX)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(9,22,n.keysType===n.ModbusValueKey.RPC_REQUESTS?"gateway.hints.modbus.method":"gateway.hints.modbus.key")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,24,n.keysType===n.ModbusValueKey.RPC_REQUESTS?"gateway.method-name":"gateway.key")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,26,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("tag").hasError("required")&&e.get("tag").touched),t.ɵɵadvance(9),t.ɵɵproperty("ngForOf",n.modbusDataTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.withFunctionCode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(28,28,"gateway.hints.modbus.objects-count")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,30,"gateway.set")),t.ɵɵproperty("readonly",!n.ModbusEditableDataTypes.includes(e.get("type").value)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("objectsCount").hasError("required")&&e.get("objectsCount").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("type").value===n.ModbusDataType.BITS),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(37,32,"gateway.hints.modbus.address")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(41,34,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("address").hasError("required")&&e.get("address").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.showModifiersMap.get(e.get("id").value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isMaster),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.withReportStrategy)}}function NX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",13)(1,"div",14),t.ɵɵelementContainerStart(2,15),t.ɵɵelementStart(3,"mat-expansion-panel",16)(4,"mat-expansion-panel-header",17)(5,"mat-panel-title"),t.ɵɵtemplate(6,yX,2,3,"div",18)(7,vX,16,12,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()(),t.ɵɵtemplate(9,BX,46,37,"ng-template",19),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",20),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.last,a=t.ɵɵreference(8),r=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",r.isMaster)("ngIfElse",a),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,r.deleteKeyTitle))}}function LX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",11),t.ɵɵtemplate(1,NX,14,7,"div",12),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.keysListFormArray.controls)("ngForTrackBy",e.trackByControlId)}}function VX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",67)(1,"span",68),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class qX{constructor(e,t){this.fb=e,this.popover=t,this.isMaster=!1,this.hideNewFields=!1,this.keysDataApplied=new u,this.withFunctionCode=!0,this.withReportStrategy=!0,this.enableModifiersControlMap=new Map,this.showModifiersMap=new Map,this.functionCodesMap=new Map,this.defaultFunctionCodes=[],this.modbusDataTypes=Object.values(Zt),this.modifierTypes=Object.values(ba),this.bitTargetTypes=Object.values(da),this.BitTargetTypeTranslationMap=ua,this.ModbusEditableDataTypes=Qt,this.ModbusFunctionCodeTranslationsMap=nn,this.ModifierTypesMap=wa,this.ReportStrategyDefaultValue=on,this.ModbusDataType=Zt,this.ModbusValueKey=oa,this.destroy$=new we,this.defaultReadFunctionCodes=[3,4],this.bitsReadFunctionCodes=[1,2],this.defaultWriteFunctionCodes=[6,16],this.bitsWriteFunctionCodes=[5,15]}ngOnInit(){this.withFunctionCode=!this.isMaster||this.keysType!==oa.ATTRIBUTES&&this.keysType!==oa.TIMESERIES,this.withReportStrategy=!(this.isMaster||this.keysType!==oa.ATTRIBUTES&&this.keysType!==oa.TIMESERIES||this.hideNewFields),this.keysListFormArray=this.prepareKeysFormArray(this.values),this.defaultFunctionCodes=this.getDefaultFunctionCodes()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}trackByControlId(e,t){return t.value.id}addKey(){const e=ae(5),t=this.fb.group({tag:["",[me.required,me.pattern(pn)]],value:[{value:"",disabled:!this.isMaster},[me.required,me.pattern(pn)]],type:[Zt.BYTES,[me.required]],address:[null,[me.required]],objectsCount:[1,[me.required]],functionCode:[{value:this.getDefaultFunctionCodes()[0],disabled:!this.withFunctionCode},[me.required]],reportStrategy:[{value:null,disabled:!this.withReportStrategy}],modifierType:[{value:ba.MULTIPLIER,disabled:!0}],modifierValue:[{value:1,disabled:!0},[me.pattern(dn)]],bit:[{value:null,disabled:!0}],bitTargetType:[{value:da.IntegerType,disabled:!0}],id:[{value:e,disabled:!0}]});this.showModifiersMap.set(e,!1),this.enableModifiersControlMap.set(e,this.fb.control(!1)),this.observeKeyDataType(t),this.observeObjectsCount(t),this.observeEnableModifier(t),this.keysListFormArray.push(t)}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover.hide()}applyKeysData(){this.keysDataApplied.emit(this.getFormValue())}getFormValue(){return this.mapKeysWithModifier(this.withReportStrategy?this.cleanUpEmptyStrategies(this.keysListFormArray.value):this.keysListFormArray.value)}cleanUpEmptyStrategies(e){return e.map((e=>{const{reportStrategy:t,...n}=e;return t?e:n}))}mapKeysWithModifier(e){return e.map(((e,t)=>{if(this.showModifiersMap.get(this.keysListFormArray.controls[t].get("id").value)){const{modifierType:t,modifierValue:n,...i}=e;return t?{...i,[t]:n}:i}return e}))}prepareKeysFormArray(e){const t=[];return e&&e.forEach((e=>{const n=this.createDataKeyFormGroup(e);this.observeKeyDataType(n),this.observeObjectsCount(n),this.observeEnableModifier(n),this.functionCodesMap.set(n.get("id").value,this.getFunctionCodes(e.type)),t.push(n)})),this.fb.array(t)}createDataKeyFormGroup(e){const{tag:t,value:n,type:i,address:a,objectsCount:r,functionCode:o,multiplier:s,divider:l,reportStrategy:p,bit:c,bitTargetType:d}=e,u=ae(5),m=this.shouldShowModifier(i);return this.showModifiersMap.set(u,m),this.enableModifiersControlMap.set(u,this.fb.control((s||l)&&m)),this.fb.group({tag:[t,[me.required,me.pattern(pn)]],value:[{value:n,disabled:!this.isMaster},[me.required,me.pattern(pn)]],type:[i,[me.required]],address:[a,[me.required]],objectsCount:[r,[me.required]],functionCode:[{value:o,disabled:!this.withFunctionCode},[me.required]],modifierType:[{value:l?ba.DIVIDER:ba.MULTIPLIER,disabled:!this.enableModifiersControlMap.get(u).value}],bit:[{value:c,disabled:i!==Zt.BITS||r<2},[me.max(r-1)]],bitTargetType:[{value:d??da.IntegerType,disabled:i!==Zt.BITS||this.hideNewFields}],modifierValue:[{value:s??l??1,disabled:!this.enableModifiersControlMap.get(u).value},[me.pattern(dn)]],id:[{value:u,disabled:!0}],reportStrategy:[{value:p,disabled:!this.withReportStrategy}]})}shouldShowModifier(e){return!(this.isMaster||this.keysType!==oa.ATTRIBUTES&&this.keysType!==oa.TIMESERIES||this.ModbusEditableDataTypes.includes(e))}observeKeyDataType(e){e.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((t=>{this.ModbusEditableDataTypes.includes(t)||e.get("objectsCount").patchValue(Jt[t],{emitEvent:!1}),this.toggleBitsFields(e);const n=this.shouldShowModifier(t);this.showModifiersMap.set(e.get("id").value,n),this.updateFunctionCodes(e,t)}))}observeObjectsCount(e){e.get("objectsCount").valueChanges.pipe(Oe((()=>e.get("type").value===Zt.BITS)),Me(this.destroy$)).subscribe((()=>this.toggleBitsFields(e)))}toggleBitsFields(e){const{objectsCount:t,type:n,bit:i,bitTargetType:a}=e.controls,r=n.value===Zt.BITS,o=t.value>1;r&&o?(i.enable({emitEvent:!1}),i.setValidators(me.max(t.value-1))):i.disable({emitEvent:!1}),i.updateValueAndValidity({emitEvent:!1}),a[r?"enable":"disable"]({emitEvent:!1})}observeEnableModifier(e){this.enableModifiersControlMap.get(e.get("id").value).valueChanges.pipe(Me(this.destroy$)).subscribe((t=>this.toggleModifierControls(e,t)))}toggleModifierControls(e,t){const n=e.get("modifierType"),i=e.get("modifierValue");n[t?"enable":"disable"]({emitEvent:!1}),i[t?"enable":"disable"]({emitEvent:!1}),n.markAsDirty(),i.markAsDirty()}updateFunctionCodes(e,t){const n=this.getFunctionCodes(t);this.functionCodesMap.set(e.get("id").value,n),n.includes(e.get("functionCode").value)||e.get("functionCode").patchValue(n[0],{emitEvent:!1})}getFunctionCodes(e){const t=[...e===Zt.BITS?this.bitsWriteFunctionCodes:[],...this.defaultWriteFunctionCodes];if(this.keysType===oa.ATTRIBUTES_UPDATES)return t.sort(((e,t)=>e-t));const n=[...this.defaultReadFunctionCodes];return e===Zt.BITS&&n.push(...this.bitsReadFunctionCodes),this.keysType===oa.RPC_REQUESTS&&n.push(...t),n.sort(((e,t)=>e-t))}getDefaultFunctionCodes(){return this.keysType===oa.ATTRIBUTES_UPDATES?this.defaultWriteFunctionCodes:this.keysType===oa.RPC_REQUESTS?[...this.defaultReadFunctionCodes,...this.defaultWriteFunctionCodes]:this.defaultReadFunctionCodes}static{this.ɵfac=function(e){return new(e||qX)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverComponent))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:qX,selectors:[["tb-modbus-data-keys-panel"]],inputs:{isMaster:"isMaster",hideNewFields:"hideNewFields",panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keysType:"keysType",values:"values"},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:19,vars:16,consts:[["noKeys",""],["tagName",""],[1,"tb-modbus-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],[1,"flex","justify-between","pt-4"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],["class","title-container","tbTruncateWithTooltip","",4,"ngIf","ngIfElse"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],["tbTruncateWithTooltip","",1,"title-container"],[1,"tb-flex"],[1,"title-container","tb-flex"],["tbTruncateWithTooltip","",1,"key-label"],[1,"title-container"],[1,"key-label"],[1,"tb-form-hint","tb-primary-fill","tb-flex","align-center"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","tag",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","1","max","50000","name","value","formControlName","objectsCount",3,"placeholder","readonly"],[4,"ngIf"],["matInput","","type","number","min","0","max","65535","name","value","formControlName","address",3,"placeholder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],["class","stroked tb-form-panel","formControlName","reportStrategy",3,"defaultValue","isExpansionMode",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["formControlName","functionCode"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","tb-suffix-absolute","flex"],["matInput","","formControlName","bit","step","1","type","number","min","0",3,"placeholder"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","bitTargetType"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"mat-slide",3,"click","formControl"],[3,"tb-hint-tooltip-icon"],[1,"tb-form-row","column-xs","w-full"],["formControlName","modifierType"],[1,"tb-flex","align-center"],[1,"tb-mat-18",3,"svgIcon"],["matInput","","required","","formControlName","modifierValue","step","0.1","type","number",3,"placeholder"],[1,"tb-mat-20",3,"svgIcon"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","value",3,"placeholder"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"defaultValue","isExpansionMode"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"div",3)(2,"div",4),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,LX,2,2,"div",5),t.ɵɵelementEnd(),t.ɵɵtemplate(6,VX,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd(),t.ɵɵelementStart(8,"div",6)(9,"button",7),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"div",8)(13,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",10),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.applyKeysData())})),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(7);t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,8,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")",""),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.keysListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(5),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,10,n.addKeyTitle)," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,12,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,14,"action.apply")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(qX,[se,_,ni,Wn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{width:180px}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .key-label[_ngcontent-%COMP%]{font-weight:400}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:calc(100vh - 112px);max-height:500px;overflow:auto}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}']})}}e("ModbusDataKeysPanelComponent",qX),He([I()],qX.prototype,"isMaster",void 0),He([I()],qX.prototype,"hideNewFields",void 0);const GX=()=>({$implicit:null}),zX=e=>({$implicit:e});function UX(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",7),t.ɵɵelementContainer(2,8),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(),n=t.ɵɵreference(4);t.ɵɵadvance(),t.ɵɵproperty("formGroup",e.valuesFormGroup),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",n)("ngTemplateOutletContext",t.ɵɵpureFunction0(3,GX))}}function jX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-tab",11),t.ɵɵpipe(1,"translate"),t.ɵɵelementStart(2,"div",7),t.ɵɵelementContainer(3,8),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2),a=t.ɵɵreference(4);t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(1,4,i.ModbusValuesTranslationsMap.get(e))),t.ɵɵadvance(2),t.ɵɵproperty("formGroup",i.valuesFormGroup.get(e)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",a)("ngTemplateOutletContext",t.ɵɵpureFunction1(6,zX,e))}}function HX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-tab-group",9),t.ɵɵtemplate(1,jX,4,8,"mat-tab",10),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("formGroup",e.valuesFormGroup),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.modbusRegisterTypes)}}function WX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function $X(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function KX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function YX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function XX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",12)(1,"div",13),t.ɵɵtext(2,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",14)(4,"mat-chip-listbox",15),t.ɵɵtemplate(5,WX,2,1,"mat-chip",16),t.ɵɵelementStart(6,"mat-chip",17),t.ɵɵelement(7,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"button",19,2),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(9),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.ATTRIBUTES,i))})),t.ɵɵelementStart(11,"tb-icon",20),t.ɵɵtext(12,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(13,"div",12)(14,"div",13),t.ɵɵtext(15,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",14)(17,"mat-chip-listbox",15),t.ɵɵtemplate(18,$X,2,1,"mat-chip",16),t.ɵɵelementStart(19,"mat-chip",17),t.ɵɵelement(20,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(21,"button",19,3),t.ɵɵpipe(23,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(22),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.TIMESERIES,i))})),t.ɵɵelementStart(24,"tb-icon",20),t.ɵɵtext(25,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(26,"div",12)(27,"div",13),t.ɵɵtext(28,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"div",14)(30,"mat-chip-listbox",15),t.ɵɵtemplate(31,KX,2,1,"mat-chip",16),t.ɵɵelementStart(32,"mat-chip",17),t.ɵɵelement(33,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"button",19,4),t.ɵɵpipe(36,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(35),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.ATTRIBUTES_UPDATES,i))})),t.ɵɵelementStart(37,"tb-icon",20),t.ɵɵtext(38,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(39,"div",12)(40,"div",13),t.ɵɵtext(41,"gateway.rpc-requests"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"div",14)(43,"mat-chip-listbox",15),t.ɵɵtemplate(44,YX,2,1,"mat-chip",16),t.ɵɵelementStart(45,"mat-chip",17),t.ɵɵelement(46,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(47,"button",19,5),t.ɵɵpipe(49,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(48),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.RPC_REQUESTS,i))})),t.ɵɵelementStart(50,"tb-icon",20),t.ɵɵtext(51,"edit"),t.ɵɵelementEnd()()()()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,16,"action.edit")),t.ɵɵproperty("disabled",i.disabled),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.TIMESERIES,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.TIMESERIES,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(23,18,"action.edit")),t.ɵɵproperty("disabled",i.disabled),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES_UPDATES,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES_UPDATES,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(36,20,"action.edit")),t.ɵɵproperty("disabled",i.disabled),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.RPC_REQUESTS,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.RPC_REQUESTS,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(49,22,"action.edit")),t.ɵɵproperty("disabled",i.disabled)}}class ZX{constructor(e,t,n,i,a,r){this.fb=e,this.popoverService=t,this.renderer=n,this.viewContainerRef=i,this.cdr=a,this.destroyRef=r,this.singleMode=!1,this.hideNewFields=!1,this.disabled=!1,this.modbusRegisterTypes=Object.values(ra),this.modbusValueKeys=Object.values(oa),this.ModbusValuesTranslationsMap=ca,this.ModbusValueKey=oa}ngOnInit(){this.initializeValuesFormGroup(),this.observeValuesChanges()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){if(this.singleMode)this.valuesFormGroup.setValue(this.getSingleRegisterState(e),{emitEvent:!1});else{const{holding_registers:t,coils_initializer:n,input_registers:i,discrete_inputs:a}=e;this.valuesFormGroup.setValue({holding_registers:this.getSingleRegisterState(t),coils_initializer:this.getSingleRegisterState(n),input_registers:this.getSingleRegisterState(i),discrete_inputs:this.getSingleRegisterState(a)},{emitEvent:!1})}this.cdr.markForCheck()}validate(){return this.valuesFormGroup.valid?null:{valuesFormGroup:{valid:!1}}}setDisabledState(e){this.disabled=e,this.cdr.markForCheck()}getValueGroup(e,t){return t?this.valuesFormGroup.get(t).get(e):this.valuesFormGroup.get(e)}manageKeys(e,t,n,i){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const a=t._elementRef.nativeElement;if(this.popoverService.hasPopover(a))return void this.popoverService.hidePopover(a);const r=this.getValueGroup(n,i),o={values:r.value,isMaster:!this.singleMode,keysType:n,panelTitle:fa.get(n),addKeyTitle:ya.get(n),deleteKeyTitle:va.get(n),noKeysText:xa.get(n),hideNewFields:this.hideNewFields};this.popoverComponent=this.popoverService.displayPopover(a,this.renderer,this.viewContainerRef,qX,"leftTop",!1,null,o,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(In(this.destroyRef)).subscribe((e=>{this.popoverComponent.hide(),r.patchValue(e),r.markAsDirty(),this.cdr.markForCheck()}))}initializeValuesFormGroup(){const e=()=>this.fb.group(this.modbusValueKeys.reduce(((e,t)=>(e[t]=this.fb.control([[],[]]),e)),{}));this.singleMode?this.valuesFormGroup=e():this.valuesFormGroup=this.fb.group(this.modbusRegisterTypes.reduce(((t,n)=>(t[n]=e(),t)),{}))}observeValuesChanges(){this.valuesFormGroup.valueChanges.pipe(In(this.destroyRef)).subscribe((e=>{this.onChange(e),this.onTouched()}))}getSingleRegisterState(e){return{attributes:e?.attributes??[],timeseries:e?.timeseries??[],attributeUpdates:e?.attributeUpdates??[],rpc:e?.rpc??[]}}static{this.ɵfac=function(e){return new(e||ZX)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(rt.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:ZX,selectors:[["tb-modbus-values"]],inputs:{singleMode:"singleMode",hideNewFields:"hideNewFields"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>ZX)),multi:!0},{provide:he,useExisting:c((()=>ZX)),multi:!0}]),t.ɵɵStandaloneFeature],decls:5,vars:2,consts:[["multipleView",""],["singleView",""],["attributesButton",""],["telemetryButton",""],["attributesUpdatesButton",""],["rpcRequestsButton",""],[4,"ngIf","ngIfElse"],[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"formGroup"],[3,"label",4,"ngFor","ngForOf"],[3,"label"],[1,"tb-form-row","space-between","tb-flex"],["translate","",1,"fixed-title-width"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[4,"ngFor","ngForOf"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","disabled","matTooltip"],["matButtonIcon",""]],template:function(e,n){if(1&e&&t.ɵɵtemplate(0,UX,3,4,"ng-container",6)(1,HX,2,2,"ng-template",null,0,t.ɵɵtemplateRefExtractor)(3,XX,52,24,"ng-template",null,1,t.ɵɵtemplateRefExtractor),2&e){const e=t.ɵɵreference(2);t.ɵɵproperty("ngIf",n.singleMode)("ngIfElse",e)}},dependencies:t.ɵɵgetComponentDepsFactory(ZX,[se,_,$n]),styles:['@charset "UTF-8";[_nghost-%COMP%] .ellipsis-chips-container{max-width:70%}[_nghost-%COMP%] .mat-mdc-tab-body-wrapper{min-height:320px} .mdc-evolution-chip-set__chips{align-items:center}'],changeDetection:d.OnPush})}}function QX(e,n){1&e&&(t.ɵɵelementStart(0,"div",2)(1,"div",10),t.ɵɵtext(2,"gateway.server-hostname"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",5)(4,"mat-form-field",6),t.ɵɵelement(5,"input",16),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,1,"gateway.set")))}function JX(e,n){1&e&&(t.ɵɵelementStart(0,"div",17)(1,"mat-slide-toggle",18)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.request-client-certificate")," "))}e("ModbusValuesComponent",ZX),He([I()],ZX.prototype,"singleMode",void 0),He([I()],ZX.prototype,"hideNewFields",void 0);class eZ{constructor(e,t){this.fb=e,this.cdr=t,this.isMaster=!1,this.disabled=!1,this.destroy$=new we,this.securityConfigFormGroup=this.fb.group({certfile:["",[me.required,me.pattern(pn)]],keyfile:["",[me.required,me.pattern(pn)]],password:["",[me.required,me.pattern(pn)]],server_hostname:["",[me.pattern(pn)]],reqclicert:[{value:!1,disabled:!0}]}),this.observeValueChanges()}ngOnChanges(){this.updateMasterEnabling()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}setDisabledState(e){this.disabled=e,this.disabled?this.securityConfigFormGroup.disable({emitEvent:!1}):this.securityConfigFormGroup.enable({emitEvent:!1}),this.updateMasterEnabling(),this.cdr.markForCheck()}validate(){return this.securityConfigFormGroup.valid?null:{securityConfigFormGroup:{valid:!1}}}writeValue(e){const{certfile:t,password:n,keyfile:i,server_hostname:a}=e,r={certfile:t??"",password:n??"",keyfile:i??"",server_hostname:a??"",reqclicert:!!e.reqclicert};this.securityConfigFormGroup.reset(r,{emitEvent:!1})}updateMasterEnabling(){this.isMaster?(this.disabled||this.securityConfigFormGroup.get("reqclicert").enable({emitEvent:!1}),this.securityConfigFormGroup.get("server_hostname").disable({emitEvent:!1})):(this.disabled||this.securityConfigFormGroup.get("server_hostname").enable({emitEvent:!1}),this.securityConfigFormGroup.get("reqclicert").disable({emitEvent:!1}))}observeValueChanges(){this.securityConfigFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}static{this.ɵfac=function(e){return new(e||eZ)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:eZ,selectors:[["tb-modbus-security-config"]],inputs:{isMaster:"isMaster"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>eZ)),multi:!0},{provide:he,useExisting:c((()=>eZ)),multi:!0}]),t.ɵɵNgOnChangesFeature,t.ɵɵStandaloneFeature],decls:33,vars:21,consts:[[1,"tb-form-panel","no-border","no-padding",3,"formGroup"],[1,"tb-form-hint","tb-primary-fill"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["tbTruncateWithTooltip","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","required","","formControlName","certfile",3,"placeholder"],[1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["matInput","","name","value","required","","formControlName","keyfile",3,"placeholder"],["translate","",1,"fixed-title-width"],["matInput","","type","password","required","","name","value","formControlName","password",3,"placeholder"],["matSuffix","",1,"tb-flex","no-gap","align-center","fill-height"],[1,"tb-flex","align-center","fill-height"],["class","tb-form-row space-between tb-flex fill-width",4,"ngIf"],["class","tb-form-row",4,"ngIf"],["matInput","","name","value","formControlName","server_hostname",3,"placeholder"],[1,"tb-form-row"],["formControlName","reqclicert",1,"mat-slide"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",2)(5,"div",3),t.ɵɵpipe(6,"translate"),t.ɵɵelementStart(7,"span",4),t.ɵɵtext(8,"gateway.client-cert-path"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",5)(10,"mat-form-field",6),t.ɵɵelement(11,"input",7),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(13,"div",2)(14,"div",8),t.ɵɵpipe(15,"translate"),t.ɵɵelementStart(16,"span",4),t.ɵɵtext(17,"gateway.private-key-path"),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",5)(19,"mat-form-field",6),t.ɵɵelement(20,"input",9),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"div",2)(23,"div",10),t.ɵɵtext(24,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"div",5)(26,"mat-form-field",6),t.ɵɵelement(27,"input",11),t.ɵɵpipe(28,"translate"),t.ɵɵelementStart(29,"div",12),t.ɵɵelement(30,"tb-toggle-password",13),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(31,QX,7,3,"div",14)(32,JX,5,3,"div",15),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.securityConfigFormGroup),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,9,"gateway.hints.path-in-os")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,11,"gateway.hints.ca-cert")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(12,13,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(15,15,"gateway.private-key-path")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,17,"gateway.set")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,19,"gateway.set")),t.ɵɵadvance(4),t.ɵɵproperty("ngIf",!n.isMaster),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isMaster))},dependencies:t.ɵɵgetComponentDepsFactory(eZ,[se,_,Wn]),encapsulation:2,changeDetection:d.OnPush})}}function tZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusProtocolLabelsMap.get(e))}}function nZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function iZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",53),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,nZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("host").hasError("required")&&e.slaveConfigFormGroup.get("host").touched)}}function aZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.slaveConfigFormGroup.get("port")))}}function rZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",55),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,aZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.hints.modbus.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",e.portLimits.MIN),t.ɵɵpropertyInterpolate("max",e.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.slaveConfigFormGroup.get("port").hasError("required")||e.slaveConfigFormGroup.get("port").hasError("min")||e.slaveConfigFormGroup.get("port").hasError("max"))&&e.slaveConfigFormGroup.get("port").touched)}}function oZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.port-required"))}function sZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",56),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,oZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.serial-port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("serialPort").hasError("required")&&e.slaveConfigFormGroup.get("serialPort").touched)}}function lZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusMethodLabelsMap.get(e))}}function pZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function cZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function dZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusParityLabelsMap.get(e))}}function uZ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",17)(2,"div",18),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4,"gateway.baudrate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",19)(6,"mat-select",57),t.ɵɵtemplate(7,pZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",17)(9,"div",18),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11,"gateway.bytesize"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",19)(13,"mat-select",58),t.ɵɵtemplate(14,cZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",17)(16,"div",18),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"gateway.stopbits"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"mat-form-field",19),t.ɵɵelement(20,"input",59),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"div",17)(23,"div",18),t.ɵɵpipe(24,"translate"),t.ɵɵtext(25,"gateway.parity"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",19)(27,"mat-select",60),t.ɵɵtemplate(28,dZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(29,"div",36)(30,"mat-slide-toggle",61)(31,"mat-label",39),t.ɵɵpipe(32,"translate"),t.ɵɵtext(33),t.ɵɵpipe(34,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.modbus.baudrate")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusBaudrates),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,12,"gateway.hints.modbus.bytesize")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusByteSizes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(17,14,"gateway.hints.modbus.stopbits")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,16,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(24,18,"gateway.hints.modbus.parity")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusParities),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(32,20,"gateway.hints.modbus.strict")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(34,22,"gateway.strict")," ")}}function mZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.unit-id-required"))}function hZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function gZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function fZ(e,n){1&e&&(t.ɵɵelementStart(0,"div",36)(1,"mat-slide-toggle",62)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.send-data-on-change")," "))}function yZ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",63),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("defaultValue",e.ReportStrategyDefaultValue.Device)("isExpansionMode",!0)}}function vZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function xZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function bZ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",64)(1,"mat-expansion-panel",65)(2,"mat-expansion-panel-header",66)(3,"mat-panel-title")(4,"mat-slide-toggle",67),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(8,"tb-modbus-security-config",68),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("expanded",e.showSecurityControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showSecurityControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,3,"gateway.tls-connection")," ")}}e("ModbusSecurityConfigComponent",eZ),He([I()],eZ.prototype,"isMaster",void 0);class wZ extends gX{constructor(e,t,n,i,a){super(e,t,n,i,a),this.fb=e,this.store=t,this.router=n,this.data=i,this.dialogRef=a}getSlaveResultData(){const{values:e,type:t,serialPort:n,...i}=this.slaveConfigFormGroup.value,a={...i,type:t,...e};return t===ea.Serial&&(a.port=n),a.reportStrategy||delete a.reportStrategy,Y(a),a}addFieldsToFormGroup(){this.slaveConfigFormGroup.addControl("reportStrategy",this.fb.control(null))}static{this.ɵfac=function(e){return new(e||wZ)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:wZ,selectors:[["tb-modbus-slave-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:142,vars:97,consts:[["serialPort",""],["reportStrategy",""],[1,"slaves-config-container"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"tb-form-panel",3,"formGroup"],[1,"stroked","tb-form-panel"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","method"],[4,"ngIf"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","unitId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],["class","tb-form-row",4,"ngIf","ngIfElse"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["formControlName","byteOrder"],["formControlName","wordOrder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],[1,"tb-form-row"],["matInput","","type","number","min","0","name","retries","formControlName","retries",3,"placeholder"],["formControlName","retryOnEmpty",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","retryOnInvalid",1,"mat-slide"],[1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","100","name","value","formControlName","pollPeriod",3,"placeholder"],["translate","",1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","500","name","value","formControlName","connectAttemptTimeMs",3,"placeholder"],["matInput","","type","number","min","1","name","value","formControlName","connectAttemptCount",3,"placeholder"],["matInput","","type","number","min","30000","name","value","formControlName","waitAfterFailedAttemptsMs",3,"placeholder"],["formControlName","values",3,"singleMode","hideNewFields"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matInput","","name","value","formControlName","serialPort",3,"placeholder"],["formControlName","baudrate"],["formControlName","bytesize"],["matInput","","type","number","min","0","name","value","formControlName","stopbits",3,"placeholder"],["formControlName","parity"],["formControlName","strict",1,"mat-slide"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"defaultValue","isExpansionMode"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide","justify-start",3,"click","formControl"],["formControlName","security",1,"security-config"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"mat-toolbar",3)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",4)(6,"div",5),t.ɵɵelementStart(7,"button",6),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",7),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",8)(11,"div",9)(12,"div",10)(13,"div",11)(14,"div",12),t.ɵɵtext(15,"gateway.server-connection"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"tb-toggle-select",13),t.ɵɵtemplate(17,tZ,2,2,"tb-toggle-option",14),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",10),t.ɵɵtemplate(19,iZ,8,7,"div",15)(20,rZ,8,9,"div",16)(21,sZ,8,7,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(23,"div",17)(24,"div",18),t.ɵɵpipe(25,"translate"),t.ɵɵtext(26," gateway.method "),t.ɵɵelementEnd(),t.ɵɵelementStart(27,"mat-form-field",19)(28,"mat-select",20),t.ɵɵtemplate(29,lZ,2,2,"mat-option",14),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(30,uZ,35,24,"ng-container",21),t.ɵɵelementStart(31,"div",17)(32,"div",22),t.ɵɵpipe(33,"translate"),t.ɵɵtext(34,"gateway.unit-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(35,"mat-form-field",19),t.ɵɵelement(36,"input",23),t.ɵɵpipe(37,"translate"),t.ɵɵtemplate(38,mZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(39,"div",17)(40,"div",25),t.ɵɵtext(41,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"mat-form-field",19),t.ɵɵelement(43,"input",26),t.ɵɵpipe(44,"translate"),t.ɵɵtemplate(45,hZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"div",17)(47,"div",25),t.ɵɵtext(48,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(49,"mat-form-field",19),t.ɵɵelement(50,"input",27),t.ɵɵpipe(51,"translate"),t.ɵɵtemplate(52,gZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵtemplate(53,fZ,5,3,"div",28)(54,yZ,1,2,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(56,"div",29)(57,"mat-expansion-panel",30)(58,"mat-expansion-panel-header")(59,"mat-panel-title")(60,"div",31),t.ɵɵtext(61,"gateway.advanced-connection-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(62,"div",10)(63,"div",17)(64,"div",18),t.ɵɵpipe(65,"translate"),t.ɵɵtext(66,"gateway.connection-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(67,"mat-form-field",19),t.ɵɵelement(68,"input",32),t.ɵɵpipe(69,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(70,"div",17)(71,"div",18),t.ɵɵpipe(72,"translate"),t.ɵɵtext(73,"gateway.byte-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(74,"mat-form-field",19)(75,"mat-select",33),t.ɵɵtemplate(76,vZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(77,"div",17)(78,"div",18),t.ɵɵpipe(79,"translate"),t.ɵɵtext(80,"gateway.word-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(81,"mat-form-field",19)(82,"mat-select",34),t.ɵɵtemplate(83,xZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵtemplate(84,bZ,9,5,"div",35),t.ɵɵelementStart(85,"div",36)(86,"div",18),t.ɵɵpipe(87,"translate"),t.ɵɵtext(88,"gateway.retries"),t.ɵɵelementEnd(),t.ɵɵelementStart(89,"mat-form-field",19),t.ɵɵelement(90,"input",37),t.ɵɵpipe(91,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(92,"div",36)(93,"mat-slide-toggle",38)(94,"mat-label",39),t.ɵɵpipe(95,"translate"),t.ɵɵtext(96),t.ɵɵpipe(97,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(98,"div",36)(99,"mat-slide-toggle",40)(100,"mat-label",39),t.ɵɵpipe(101,"translate"),t.ɵɵtext(102),t.ɵɵpipe(103,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(104,"div",17)(105,"div",41),t.ɵɵpipe(106,"translate"),t.ɵɵelementStart(107,"span",42),t.ɵɵtext(108," gateway.poll-period "),t.ɵɵelementEnd()(),t.ɵɵelementStart(109,"mat-form-field",19),t.ɵɵelement(110,"input",43),t.ɵɵpipe(111,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(112,"div",17)(113,"div",44),t.ɵɵpipe(114,"translate"),t.ɵɵtext(115,"gateway.connect-attempt-time"),t.ɵɵelementEnd(),t.ɵɵelementStart(116,"mat-form-field",19),t.ɵɵelement(117,"input",45),t.ɵɵpipe(118,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(119,"div",17)(120,"div",44),t.ɵɵpipe(121,"translate"),t.ɵɵtext(122,"gateway.connect-attempt-count"),t.ɵɵelementEnd(),t.ɵɵelementStart(123,"mat-form-field",19),t.ɵɵelement(124,"input",46),t.ɵɵpipe(125,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(126,"div",17)(127,"div",44),t.ɵɵpipe(128,"translate"),t.ɵɵtext(129,"gateway.wait-after-failed-attempts"),t.ɵɵelementEnd(),t.ɵɵelementStart(130,"mat-form-field",19),t.ɵɵelement(131,"input",47),t.ɵɵpipe(132,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(133,"div",29),t.ɵɵelement(134,"tb-modbus-values",48),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(135,"div",49)(136,"button",50),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(137),t.ɵɵpipe(138,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(139,"button",51),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(140),t.ɵɵpipe(141,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(22),i=t.ɵɵreference(55);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,45,"gateway.server-slave")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.modbusHelpLink),t.ɵɵadvance(4),t.ɵɵproperty("formGroup",n.slaveConfigFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",n.modbusProtocolTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(25,47,"gateway.hints.modbus.framer-type")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.protocolType===n.ModbusProtocolType.Serial?n.modbusSerialMethodTypes:n.modbusMethodTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType===n.ModbusProtocolType.Serial),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(33,49,"gateway.hints.modbus.unit-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(37,51,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("unitId").hasError("required")&&n.slaveConfigFormGroup.get("unitId").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(44,53,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceName").hasError("required")&&n.slaveConfigFormGroup.get("deviceName").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(51,55,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceType").hasError("required")&&n.slaveConfigFormGroup.get("deviceType").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.data.hideNewFields)("ngIfElse",i),t.ɵɵadvance(11),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(65,57,"gateway.hints.modbus.connection-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(69,59,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(72,61,"gateway.hints.modbus.byte-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(79,63,"gateway.hints.modbus.word-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(87,65,"gateway.hints.modbus.retries")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(91,67,"gateway.set")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(95,69,"gateway.hints.modbus.retries-on-empty")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(97,71,"gateway.retries-on-empty")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(101,73,"gateway.hints.modbus.retries-on-invalid")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(103,75,"gateway.retries-on-invalid")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(106,77,"gateway.hints.modbus.poll-period")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(111,79,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(114,81,"gateway.hints.modbus.connect-attempt-time")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(118,83,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(121,85,"gateway.hints.modbus.connect-attempt-count")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(125,87,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(128,89,"gateway.hints.modbus.wait-after-failed-attempts")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(132,91,"gateway.set")),t.ɵɵadvance(3),t.ɵɵproperty("singleMode",!0)("hideNewFields",n.data.hideNewFields),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(138,93,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.slaveConfigFormGroup.invalid||!n.slaveConfigFormGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(141,95,n.data.buttonTitle)," ")}},dependencies:t.ɵɵgetComponentDepsFactory(wZ,[se,_,ZX,eZ,u$,ni,Wn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .slaves-config-container[_ngcontent-%COMP%]{width:80vw;max-width:900px}[_nghost-%COMP%] .slave-name-label[_ngcontent-%COMP%]{margin-right:16px;color:#000000de}[_nghost-%COMP%] .fixed-title-width-260[_ngcontent-%COMP%]{min-width:260px}[_nghost-%COMP%] .security-config .fixed-title-width{min-width:230px}'],changeDetection:d.OnPush})}}function SZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusProtocolLabelsMap.get(e))}}function CZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function _Z(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",53),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,CZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("host").hasError("required")&&e.slaveConfigFormGroup.get("host").touched)}}function TZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.slaveConfigFormGroup.get("port")))}}function IZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",55),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,TZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.hints.modbus.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",e.portLimits.MIN),t.ɵɵpropertyInterpolate("max",e.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.slaveConfigFormGroup.get("port").hasError("required")||e.slaveConfigFormGroup.get("port").hasError("min")||e.slaveConfigFormGroup.get("port").hasError("max"))&&e.slaveConfigFormGroup.get("port").touched)}}function EZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.port-required"))}function MZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",56),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,EZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.serial-port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("serialPort").hasError("required")&&e.slaveConfigFormGroup.get("serialPort").touched)}}function kZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusMethodLabelsMap.get(e))}}function PZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function DZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function OZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusParityLabelsMap.get(e))}}function AZ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",17)(2,"div",18),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4,"gateway.baudrate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",19)(6,"mat-select",57),t.ɵɵtemplate(7,PZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",17)(9,"div",18),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11,"gateway.bytesize"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",19)(13,"mat-select",58),t.ɵɵtemplate(14,DZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",17)(16,"div",18),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"gateway.stopbits"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"mat-form-field",19),t.ɵɵelement(20,"input",59),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"div",17)(23,"div",18),t.ɵɵpipe(24,"translate"),t.ɵɵtext(25,"gateway.parity"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",19)(27,"mat-select",60),t.ɵɵtemplate(28,OZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(29,"div",36)(30,"mat-slide-toggle",61)(31,"mat-label",39),t.ɵɵpipe(32,"translate"),t.ɵɵtext(33),t.ɵɵpipe(34,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.modbus.baudrate")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusBaudrates),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,12,"gateway.hints.modbus.bytesize")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusByteSizes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(17,14,"gateway.hints.modbus.stopbits")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,16,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(24,18,"gateway.hints.modbus.parity")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusParities),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(32,20,"gateway.hints.modbus.strict")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(34,22,"gateway.strict")," ")}}function FZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.unit-id-required"))}function RZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function BZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function NZ(e,n){1&e&&(t.ɵɵelementStart(0,"div",36)(1,"mat-slide-toggle",62)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.send-data-on-change")," "))}function LZ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",63),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("defaultValue",e.ReportStrategyDefaultValue.Device)("isExpansionMode",!0)}}function VZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function qZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function GZ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",64)(1,"mat-expansion-panel",65)(2,"mat-expansion-panel-header",66)(3,"mat-panel-title")(4,"mat-slide-toggle",67),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(8,"tb-modbus-security-config",68),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("expanded",e.showSecurityControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showSecurityControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,3,"gateway.tls-connection")," ")}}e("ModbusSlaveDialogComponent",wZ);class zZ extends gX{constructor(e,t,n,i,a){super(e,t,n,i,a),this.fb=e,this.store=t,this.router=n,this.data=i,this.dialogRef=a}getSlaveResultData(){const{values:e,type:t,serialPort:n,...i}=this.slaveConfigFormGroup.value,a={...i,type:t,...e};return t===ea.Serial&&(a.port=n),Y(a),a}addFieldsToFormGroup(){this.slaveConfigFormGroup.addControl("sendDataOnlyOnChange",this.fb.control(!1))}static{this.ɵfac=function(e){return new(e||zZ)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:zZ,selectors:[["tb-modbus-legacy-slave-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:142,vars:97,consts:[["serialPort",""],["reportStrategy",""],[1,"slaves-config-container"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"tb-form-panel",3,"formGroup"],[1,"stroked","tb-form-panel"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","method"],[4,"ngIf"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","unitId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],["class","tb-form-row",4,"ngIf","ngIfElse"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["formControlName","byteOrder"],["formControlName","wordOrder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],[1,"tb-form-row"],["matInput","","type","number","min","0","name","retries","formControlName","retries",3,"placeholder"],["formControlName","retryOnEmpty",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","retryOnInvalid",1,"mat-slide"],[1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","100","name","value","formControlName","pollPeriod",3,"placeholder"],["translate","",1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","500","name","value","formControlName","connectAttemptTimeMs",3,"placeholder"],["matInput","","type","number","min","1","name","value","formControlName","connectAttemptCount",3,"placeholder"],["matInput","","type","number","min","30000","name","value","formControlName","waitAfterFailedAttemptsMs",3,"placeholder"],["formControlName","values",3,"singleMode","hideNewFields"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matInput","","name","value","formControlName","serialPort",3,"placeholder"],["formControlName","baudrate"],["formControlName","bytesize"],["matInput","","type","number","min","0","name","value","formControlName","stopbits",3,"placeholder"],["formControlName","parity"],["formControlName","strict",1,"mat-slide"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"defaultValue","isExpansionMode"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide","justify-start",3,"click","formControl"],["formControlName","security",1,"security-config"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"mat-toolbar",3)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",4)(6,"div",5),t.ɵɵelementStart(7,"button",6),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",7),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",8)(11,"div",9)(12,"div",10)(13,"div",11)(14,"div",12),t.ɵɵtext(15,"gateway.server-connection"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"tb-toggle-select",13),t.ɵɵtemplate(17,SZ,2,2,"tb-toggle-option",14),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",10),t.ɵɵtemplate(19,_Z,8,7,"div",15)(20,IZ,8,9,"div",16)(21,MZ,8,7,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(23,"div",17)(24,"div",18),t.ɵɵpipe(25,"translate"),t.ɵɵtext(26," gateway.method "),t.ɵɵelementEnd(),t.ɵɵelementStart(27,"mat-form-field",19)(28,"mat-select",20),t.ɵɵtemplate(29,kZ,2,2,"mat-option",14),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(30,AZ,35,24,"ng-container",21),t.ɵɵelementStart(31,"div",17)(32,"div",22),t.ɵɵpipe(33,"translate"),t.ɵɵtext(34,"gateway.unit-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(35,"mat-form-field",19),t.ɵɵelement(36,"input",23),t.ɵɵpipe(37,"translate"),t.ɵɵtemplate(38,FZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(39,"div",17)(40,"div",25),t.ɵɵtext(41,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"mat-form-field",19),t.ɵɵelement(43,"input",26),t.ɵɵpipe(44,"translate"),t.ɵɵtemplate(45,RZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"div",17)(47,"div",25),t.ɵɵtext(48,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(49,"mat-form-field",19),t.ɵɵelement(50,"input",27),t.ɵɵpipe(51,"translate"),t.ɵɵtemplate(52,BZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵtemplate(53,NZ,5,3,"div",28)(54,LZ,1,2,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(56,"div",29)(57,"mat-expansion-panel",30)(58,"mat-expansion-panel-header")(59,"mat-panel-title")(60,"div",31),t.ɵɵtext(61,"gateway.advanced-connection-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(62,"div",10)(63,"div",17)(64,"div",18),t.ɵɵpipe(65,"translate"),t.ɵɵtext(66,"gateway.connection-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(67,"mat-form-field",19),t.ɵɵelement(68,"input",32),t.ɵɵpipe(69,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(70,"div",17)(71,"div",18),t.ɵɵpipe(72,"translate"),t.ɵɵtext(73,"gateway.byte-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(74,"mat-form-field",19)(75,"mat-select",33),t.ɵɵtemplate(76,VZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(77,"div",17)(78,"div",18),t.ɵɵpipe(79,"translate"),t.ɵɵtext(80,"gateway.word-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(81,"mat-form-field",19)(82,"mat-select",34),t.ɵɵtemplate(83,qZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵtemplate(84,GZ,9,5,"div",35),t.ɵɵelementStart(85,"div",36)(86,"div",18),t.ɵɵpipe(87,"translate"),t.ɵɵtext(88,"gateway.retries"),t.ɵɵelementEnd(),t.ɵɵelementStart(89,"mat-form-field",19),t.ɵɵelement(90,"input",37),t.ɵɵpipe(91,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(92,"div",36)(93,"mat-slide-toggle",38)(94,"mat-label",39),t.ɵɵpipe(95,"translate"),t.ɵɵtext(96),t.ɵɵpipe(97,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(98,"div",36)(99,"mat-slide-toggle",40)(100,"mat-label",39),t.ɵɵpipe(101,"translate"),t.ɵɵtext(102),t.ɵɵpipe(103,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(104,"div",17)(105,"div",41),t.ɵɵpipe(106,"translate"),t.ɵɵelementStart(107,"span",42),t.ɵɵtext(108," gateway.poll-period "),t.ɵɵelementEnd()(),t.ɵɵelementStart(109,"mat-form-field",19),t.ɵɵelement(110,"input",43),t.ɵɵpipe(111,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(112,"div",17)(113,"div",44),t.ɵɵpipe(114,"translate"),t.ɵɵtext(115,"gateway.connect-attempt-time"),t.ɵɵelementEnd(),t.ɵɵelementStart(116,"mat-form-field",19),t.ɵɵelement(117,"input",45),t.ɵɵpipe(118,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(119,"div",17)(120,"div",44),t.ɵɵpipe(121,"translate"),t.ɵɵtext(122,"gateway.connect-attempt-count"),t.ɵɵelementEnd(),t.ɵɵelementStart(123,"mat-form-field",19),t.ɵɵelement(124,"input",46),t.ɵɵpipe(125,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(126,"div",17)(127,"div",44),t.ɵɵpipe(128,"translate"),t.ɵɵtext(129,"gateway.wait-after-failed-attempts"),t.ɵɵelementEnd(),t.ɵɵelementStart(130,"mat-form-field",19),t.ɵɵelement(131,"input",47),t.ɵɵpipe(132,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(133,"div",29),t.ɵɵelement(134,"tb-modbus-values",48),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(135,"div",49)(136,"button",50),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(137),t.ɵɵpipe(138,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(139,"button",51),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(140),t.ɵɵpipe(141,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(22),i=t.ɵɵreference(55);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,45,"gateway.server-slave")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.modbusHelpLink),t.ɵɵadvance(4),t.ɵɵproperty("formGroup",n.slaveConfigFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",n.modbusProtocolTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(25,47,"gateway.hints.modbus.framer-type")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.protocolType===n.ModbusProtocolType.Serial?n.modbusSerialMethodTypes:n.modbusMethodTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType===n.ModbusProtocolType.Serial),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(33,49,"gateway.hints.modbus.unit-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(37,51,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("unitId").hasError("required")&&n.slaveConfigFormGroup.get("unitId").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(44,53,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceName").hasError("required")&&n.slaveConfigFormGroup.get("deviceName").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(51,55,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceType").hasError("required")&&n.slaveConfigFormGroup.get("deviceType").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.data.hideNewFields)("ngIfElse",i),t.ɵɵadvance(11),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(65,57,"gateway.hints.modbus.connection-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(69,59,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(72,61,"gateway.hints.modbus.byte-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(79,63,"gateway.hints.modbus.word-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(87,65,"gateway.hints.modbus.retries")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(91,67,"gateway.set")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(95,69,"gateway.hints.modbus.retries-on-empty")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(97,71,"gateway.retries-on-empty")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(101,73,"gateway.hints.modbus.retries-on-invalid")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(103,75,"gateway.retries-on-invalid")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(106,77,"gateway.hints.modbus.poll-period")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(111,79,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(114,81,"gateway.hints.modbus.connect-attempt-time")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(118,83,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(121,85,"gateway.hints.modbus.connect-attempt-count")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(125,87,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(128,89,"gateway.hints.modbus.wait-after-failed-attempts")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(132,91,"gateway.set")),t.ɵɵadvance(3),t.ɵɵproperty("singleMode",!0)("hideNewFields",n.data.hideNewFields),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(138,93,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.slaveConfigFormGroup.invalid||!n.slaveConfigFormGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(141,95,n.data.buttonTitle)," ")}},dependencies:t.ɵɵgetComponentDepsFactory(zZ,[se,_,ZX,eZ,u$,ni]),styles:['@charset "UTF-8";[_nghost-%COMP%] .slaves-config-container[_ngcontent-%COMP%]{width:80vw;max-width:900px}[_nghost-%COMP%] .slave-name-label[_ngcontent-%COMP%]{margin-right:16px;color:#000000de}[_nghost-%COMP%] .fixed-title-width-260[_ngcontent-%COMP%]{min-width:260px}[_nghost-%COMP%] .security-config .fixed-title-width{min-width:230px}'],changeDetection:d.OnPush})}}function UZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusProtocolLabelsMap.get(e))}}function jZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function HZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",14),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",39),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,jZ,3,3,"mat-icon",16),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("host").hasError("required")&&e.slaveConfigFormGroup.get("host").touched)}}function WZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.slaveConfigFormGroup.get("port")))}}function $Z(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",14),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",41),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,WZ,3,3,"mat-icon",16),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.hints.modbus.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",e.portLimits.MIN),t.ɵɵpropertyInterpolate("max",e.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.slaveConfigFormGroup.get("port").hasError("required")||e.slaveConfigFormGroup.get("port").hasError("min")||e.slaveConfigFormGroup.get("port").hasError("max"))&&e.slaveConfigFormGroup.get("port").touched)}}function KZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.port-required"))}function YZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",14),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",42),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,KZ,3,3,"mat-icon",16),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.serial-port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("port").hasError("required")&&e.slaveConfigFormGroup.get("port").touched)}}function XZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusMethodLabelsMap.get(e))}}function ZZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.unit-id-required"))}function QZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function JZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function eQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function tQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",11),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.baudrate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12)(5,"mat-select",43),t.ɵɵtemplate(6,eQ,2,2,"mat-option",6),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,2,"gateway.hints.modbus.baudrate")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusBaudrates)}}function nQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function iQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function aQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",44)(1,"mat-expansion-panel",45)(2,"mat-expansion-panel-header",46)(3,"mat-panel-title")(4,"mat-slide-toggle",47),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(8,"tb-modbus-security-config",48),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("expanded",e.showSecurityControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showSecurityControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,3,"gateway.tls-connection")," ")}}e("ModbusLegacySlaveDialogComponent",zZ);class rQ{constructor(e){this.fb=e,this.ModbusProtocolLabelsMap=ha,this.ModbusMethodLabelsMap=ma,this.portLimits=Oi,this.modbusProtocolTypes=Object.values(ea),this.modbusMethodTypes=Object.values(ta),this.modbusSerialMethodTypes=Object.values(na),this.modbusOrderType=Object.values(aa),this.ModbusProtocolType=ea,this.modbusBaudrates=sa,this.isSlaveEnabled=!1,this.serialSpecificControlKeys=["serialPort","baudrate"],this.tcpUdpSpecificControlKeys=["port","security","host"],this.destroy$=new we,this.showSecurityControl=this.fb.control(!1),this.slaveConfigFormGroup=this.fb.group({type:[ea.TCP],host:["",[me.required,me.pattern(pn)]],port:[null,[me.required,me.min(Oi.MIN),me.max(Oi.MAX)]],serialPort:["",[me.required,me.pattern(pn)]],method:[ta.SOCKET],unitId:[null,[me.required]],baudrate:[this.modbusBaudrates[0]],deviceName:["",[me.required,me.pattern(pn)]],deviceType:["",[me.required,me.pattern(pn)]],pollPeriod:[1e3,[me.required]],sendDataToThingsBoard:[!1],byteOrder:[aa.BIG],wordOrder:[aa.BIG],security:[],identity:this.fb.group({vendorName:["",[me.pattern(pn)]],productCode:["",[me.pattern(pn)]],vendorUrl:["",[me.pattern(pn)]],productName:["",[me.pattern(pn)]],modelName:["",[me.pattern(pn)]]}),values:[]}),this.observeValueChanges(),this.observeTypeChange(),this.observeShowSecurity()}get protocolType(){return this.slaveConfigFormGroup.get("type").value}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.slaveConfigFormGroup.valid?null:{slaveConfigFormGroup:{valid:!1}}}writeValue(e){this.showSecurityControl.patchValue(!!e.security&&!W(e.security,{})),this.updateSlaveConfig(e)}setDisabledState(e){this.isSlaveEnabled=!e,this.updateFormEnableState()}observeValueChanges(){this.slaveConfigFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{e.type===ea.Serial&&(e.port=e.serialPort,delete e.serialPort),this.onChange(e),this.onTouched()}))}observeTypeChange(){this.slaveConfigFormGroup.get("type").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.updateFormEnableState(),this.updateMethodType(e)}))}updateMethodType(e){this.slaveConfigFormGroup.get("method").value!==ta.RTU&&this.slaveConfigFormGroup.get("method").patchValue(e===ea.Serial?na.ASCII:ta.SOCKET,{emitEvent:!1})}updateFormEnableState(){this.isSlaveEnabled?(this.slaveConfigFormGroup.enable({emitEvent:!1}),this.showSecurityControl.enable({emitEvent:!1})):(this.slaveConfigFormGroup.disable({emitEvent:!1}),this.showSecurityControl.disable({emitEvent:!1})),this.updateEnablingByProtocol(),this.updateSecurityEnable(this.showSecurityControl.value)}observeShowSecurity(){this.showSecurityControl.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>this.updateSecurityEnable(e)))}updateSecurityEnable(e){e&&this.isSlaveEnabled&&this.protocolType!==ea.Serial?this.slaveConfigFormGroup.get("security").enable({emitEvent:!1}):this.slaveConfigFormGroup.get("security").disable({emitEvent:!1})}updateEnablingByProtocol(){const e=this.protocolType===ea.Serial,t=e?this.serialSpecificControlKeys:this.tcpUdpSpecificControlKeys,n=e?this.tcpUdpSpecificControlKeys:this.serialSpecificControlKeys;this.isSlaveEnabled&&t.forEach((e=>this.slaveConfigFormGroup.get(e)?.enable({emitEvent:!1}))),n.forEach((e=>this.slaveConfigFormGroup.get(e)?.disable({emitEvent:!1})))}updateSlaveConfig(e){const{vendorName:t="",productCode:n="",vendorUrl:i="",productName:a="",modelName:r=""}=e.identity??{},o={vendorName:t,productCode:n,vendorUrl:i,productName:a,modelName:r},{type:s=ea.TCP,method:l=ta.RTU,unitId:p=0,deviceName:c="",deviceType:d="",pollPeriod:u=1e3,sendDataToThingsBoard:m=!1,byteOrder:h=aa.BIG,wordOrder:g=aa.BIG,security:f={},values:y={},baudrate:v=this.modbusBaudrates[0],host:x="",port:b=null}=e,w={type:s,method:l,unitId:p,deviceName:c,deviceType:d,pollPeriod:u,sendDataToThingsBoard:!!m,byteOrder:h,wordOrder:g,security:f,identity:o,values:y,baudrate:v,host:s===ea.Serial?"":x,port:s===ea.Serial?null:b,serialPort:s===ea.Serial?b:""};this.slaveConfigFormGroup.setValue(w,{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||rQ)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:rQ,selectors:[["tb-modbus-slave-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>rQ)),multi:!0},{provide:he,useExisting:c((()=>rQ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:112,vars:59,consts:[["serialPort",""],[1,"slave-container",3,"formGroup"],[1,"slave-content","tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-panel","no-border","no-padding","padding-top"],["class","tb-form-row column-xs",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","method"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","unitId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","100","name","value","formControlName","pollPeriod",3,"placeholder"],[1,"tb-form-row"],["formControlName","sendDataToThingsBoard",1,"mat-slide"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],["formControlName","byteOrder"],["formControlName","wordOrder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],[3,"formGroup"],["matInput","","name","value","formControlName","vendorName",3,"placeholder"],["matInput","","name","value","formControlName","productCode",3,"placeholder"],["matInput","","name","value","formControlName","vendorUrl",3,"placeholder"],["matInput","","name","value","formControlName","productName",3,"placeholder"],["matInput","","name","value","formControlName","modelName",3,"placeholder"],["formControlName","values"],[3,"value"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matInput","","name","value","formControlName","serialPort",3,"placeholder"],["formControlName","baudrate"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide",3,"click","formControl"],["formControlName","security"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"div",2)(2,"div",3)(3,"div",4),t.ɵɵtext(4,"gateway.server-slave-config"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-select",5),t.ɵɵtemplate(6,UZ,2,2,"tb-toggle-option",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",7),t.ɵɵtemplate(8,HZ,8,7,"div",8)(9,$Z,8,9,"div",9)(10,YZ,8,7,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(12,"div",10)(13,"div",11),t.ɵɵpipe(14,"translate"),t.ɵɵtext(15," gateway.method "),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"mat-form-field",12)(17,"mat-select",13),t.ɵɵtemplate(18,XZ,2,2,"mat-option",6),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(19,"div",10)(20,"div",14),t.ɵɵpipe(21,"translate"),t.ɵɵtext(22,"gateway.unit-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",12),t.ɵɵelement(24,"input",15),t.ɵɵpipe(25,"translate"),t.ɵɵtemplate(26,ZZ,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(27,"div",10)(28,"div",17),t.ɵɵtext(29,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",12),t.ɵɵelement(31,"input",18),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,QZ,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"div",10)(35,"div",17),t.ɵɵtext(36,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"mat-form-field",12),t.ɵɵelement(38,"input",19),t.ɵɵpipe(39,"translate"),t.ɵɵtemplate(40,JZ,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(41,"div",10)(42,"div",20),t.ɵɵpipe(43,"translate"),t.ɵɵelementStart(44,"span",21),t.ɵɵtext(45," gateway.poll-period "),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"mat-form-field",12),t.ɵɵelement(47,"input",22),t.ɵɵpipe(48,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(49,tQ,7,4,"div",8),t.ɵɵelementStart(50,"div",23)(51,"mat-slide-toggle",24)(52,"mat-label"),t.ɵɵtext(53),t.ɵɵpipe(54,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(55,"div",25)(56,"mat-expansion-panel",26)(57,"mat-expansion-panel-header")(58,"mat-panel-title")(59,"div",27),t.ɵɵtext(60,"gateway.advanced-connection-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(61,"div",7)(62,"div",10)(63,"div",11),t.ɵɵpipe(64,"translate"),t.ɵɵtext(65,"gateway.byte-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(66,"mat-form-field",12)(67,"mat-select",28),t.ɵɵtemplate(68,nQ,2,2,"mat-option",6),t.ɵɵelementEnd()()(),t.ɵɵelementStart(69,"div",10)(70,"div",11),t.ɵɵpipe(71,"translate"),t.ɵɵtext(72,"gateway.word-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(73,"mat-form-field",12)(74,"mat-select",29),t.ɵɵtemplate(75,iQ,2,2,"mat-option",6),t.ɵɵelementEnd()()(),t.ɵɵtemplate(76,aQ,9,5,"div",30),t.ɵɵelementContainerStart(77,31),t.ɵɵelementStart(78,"div",10)(79,"div",4),t.ɵɵtext(80,"gateway.vendor-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(81,"mat-form-field",12),t.ɵɵelement(82,"input",32),t.ɵɵpipe(83,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(84,"div",10)(85,"div",4),t.ɵɵtext(86,"gateway.product-code"),t.ɵɵelementEnd(),t.ɵɵelementStart(87,"mat-form-field",12),t.ɵɵelement(88,"input",33),t.ɵɵpipe(89,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(90,"div",10)(91,"div",4),t.ɵɵtext(92,"gateway.vendor-url"),t.ɵɵelementEnd(),t.ɵɵelementStart(93,"mat-form-field",12),t.ɵɵelement(94,"input",34),t.ɵɵpipe(95,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(96,"div",10)(97,"div",4),t.ɵɵtext(98,"gateway.product-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(99,"mat-form-field",12),t.ɵɵelement(100,"input",35),t.ɵɵpipe(101,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(102,"div",10)(103,"div",4),t.ɵɵtext(104,"gateway.model-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(105,"mat-form-field",12),t.ɵɵelement(106,"input",36),t.ɵɵpipe(107,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()(),t.ɵɵelementStart(108,"div",25)(109,"div",27),t.ɵɵtext(110,"gateway.values"),t.ɵɵelementEnd(),t.ɵɵelement(111,"tb-modbus-values",37),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵreference(11);t.ɵɵproperty("formGroup",n.slaveConfigFormGroup),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.modbusProtocolTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(14,29,"gateway.hints.modbus.framer-type")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.protocolType===n.ModbusProtocolType.Serial?n.modbusSerialMethodTypes:n.modbusMethodTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(21,31,"gateway.hints.modbus.unit-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(25,33,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("unitId").hasError("required")&&n.slaveConfigFormGroup.get("unitId").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,35,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceName").hasError("required")&&n.slaveConfigFormGroup.get("deviceName").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(39,37,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceType").hasError("required")&&n.slaveConfigFormGroup.get("deviceType").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(43,39,"gateway.hints.modbus.poll-period")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(48,41,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType===n.ModbusProtocolType.Serial),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(54,43,"gateway.send-data-to-platform")," "),t.ɵɵadvance(10),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(64,45,"gateway.hints.modbus.byte-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(71,47,"gateway.hints.modbus.word-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.slaveConfigFormGroup.get("identity")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(83,49,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(89,51,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(95,53,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(101,55,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(107,57,"gateway.set"))}},dependencies:t.ɵɵgetComponentDepsFactory(rQ,[se,_,ZX,eZ,u$,Wn]),encapsulation:2,changeDetection:d.OnPush})}}e("ModbusSlaveConfigComponent",rQ);const oQ=["searchInput"],sQ=()=>["deviceName","info","unitId","type","actions"],lQ=()=>({minWidth:"96px",textAlign:"center"});function pQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",9)(1,"div",28)(2,"span",29),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",11),t.ɵɵelementStart(6,"button",13),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageSlave(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",13),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.servers-slaves")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search")))}function cQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.device-name")))}function dQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.deviceName)}}function uQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.info")," "))}function mQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){let e;const i=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(null!==(e=i.host)&&void 0!==e?e:i.port)}}function hQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.unit-id")," "))}function gQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.unitId)}}function fQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30)(1,"div"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.type")))}function yQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.ModbusProtocolLabelsMap.get(e.type)," ")}}function vQ(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",32)}function xQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageSlave(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",13),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteSlave(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function bQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,xQ,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",33),t.ɵɵelementContainer(4,34),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",35)(6,"button",36),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",37),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",38,2),t.ɵɵelementContainer(11,34),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,lQ)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function wQ(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",39)}function SQ(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class CQ{constructor(e,t,n,i,a){this.translate=e,this.dialog=t,this.dialogService=n,this.fb=i,this.cdr=a,this.isLegacy=!1,this.textSearchMode=!1,this.textSearch=this.fb.control("",{nonNullable:!0}),this.ModbusProtocolLabelsMap=ha,this.onChange=()=>{},this.onTouched=()=>{},this.destroy$=new we,this.masterFormGroup=this.fb.group({slaves:this.fb.array([])}),this.dataSource=new _Q}get slaves(){return this.masterFormGroup.get("slaves")}ngOnInit(){this.masterFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.updateTableData(e.slaves),this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(De(150),Pe(((e,t)=>(e??"")===t.trim())),Me(this.destroy$)).subscribe((e=>this.updateTableData(this.slaves.value,e.trim())))}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){this.slaves.clear(),this.pushDataAsFormArrays(e.slaves)}enterFilterMode(){this.textSearchMode=!0,this.cdr.detectChanges();const e=this.searchInputField.nativeElement;e.focus(),e.setSelectionRange(0,0)}exitFilterMode(){this.updateTableData(this.slaves.value),this.textSearchMode=!1,this.textSearch.reset()}manageSlave(e,t){e&&e.stopPropagation();const n=J(t),i=n?this.slaves.at(t).value:{};this.getSlaveDialog(i,n?"action.apply":"action.add").afterClosed().pipe(Ne(1),Me(this.destroy$)).subscribe((e=>{e&&(n?this.slaves.at(t).patchValue(e):this.slaves.push(this.fb.control(e)),this.masterFormGroup.markAsDirty())}))}getSlaveDialog(e,t){return this.isLegacy?this.dialog.open(zZ,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,hideNewFields:!0,buttonTitle:t}}):this.dialog.open(wZ,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,hideNewFields:!1}})}deleteSlave(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-slave-title",{name:this.slaves.controls[t].value.deviceName}),this.translate.instant("gateway.delete-slave-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(Ne(1),Me(this.destroy$)).subscribe((e=>{e&&(this.slaves.removeAt(t),this.masterFormGroup.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.slaves.push(this.fb.control(e))))}static{this.ɵfac=function(e){return new(e||CQ)(t.ɵɵdirectiveInject(le.TranslateService),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(K.DialogService),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:CQ,selectors:[["tb-modbus-master-table"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(oQ,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{isLegacy:"isLegacy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>CQ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:55,vars:41,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-master-table","tb-absolute-fill"],[1,"tb-form-panel","no-border","no-padding","padding-top","hint-container"],["tbTruncateWithTooltip","",1,"tb-form-hint","tb-primary-fill","tb-flex"],[1,"tb-master-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","table-value-column",4,"matHeaderCellDef"],["class","table-value-column",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-master-table-title"],[1,"table-value-column"],["tbTruncateWithTooltip",""],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"div",5),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(5,"div",6)(6,"mat-toolbar",7),t.ɵɵtemplate(7,pQ,14,9,"div",8),t.ɵɵpipe(8,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-toolbar",7)(10,"div",9)(11,"button",10),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"mat-icon"),t.ɵɵtext(14,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"mat-form-field",11)(16,"mat-label"),t.ɵɵtext(17," "),t.ɵɵelementEnd(),t.ɵɵelement(18,"input",12,0),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"button",13),t.ɵɵpipe(22,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(23,"mat-icon"),t.ɵɵtext(24,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(25,"div",14)(26,"table",15),t.ɵɵelementContainerStart(27,16),t.ɵɵtemplate(28,cQ,4,3,"mat-header-cell",17)(29,dQ,3,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(30,16),t.ɵɵtemplate(31,uQ,3,3,"mat-header-cell",17)(32,mQ,3,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(33,16),t.ɵɵtemplate(34,hQ,3,3,"mat-header-cell",17)(35,gQ,3,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(36,16),t.ɵɵtemplate(37,fQ,4,3,"mat-header-cell",17)(38,yQ,2,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(39,19),t.ɵɵtemplate(40,vQ,1,0,"mat-header-cell",20)(41,bQ,12,6,"mat-cell",21),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(42,wQ,1,0,"mat-header-row",22)(43,SQ,1,0,"mat-row",23),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"section",24),t.ɵɵpipe(45,"async"),t.ɵɵelementStart(46,"button",25),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageSlave(i))})),t.ɵɵelementStart(47,"mat-icon",26),t.ɵɵtext(48,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(49,"span"),t.ɵɵtext(50),t.ɵɵpipe(51,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(52,"span",27),t.ɵɵpipe(53,"async"),t.ɵɵtext(54," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,23,"gateway.hints.modbus-master")),t.ɵɵadvance(3),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(8,25,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(12,27,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(20,29,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(22,31,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","deviceName"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","info"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","unitId"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","type"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(39,sQ))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(40,sQ)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(45,33,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(51,35,"gateway.add-slave")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(53,37,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(CQ,[se,_,Wn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%]{width:100%;height:calc(100% - 60px);background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .tb-master-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:15%}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-master-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] .hint-container[_ngcontent-%COMP%]{z-index:1000}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}e("ModbusMasterTableComponent",CQ),He([ot()],CQ.prototype,"isLegacy",void 0);class _Q extends G{constructor(){super()}}e("SlavesDatasource",_Q);class TQ extends Ua{constructor(){super(),this.enableSlaveControl=new fe(!1),this.enableSlaveControl.valueChanges.pipe(In()).subscribe((e=>{this.updateSlaveEnabling(e),this.basicFormGroup.get("slave").updateValueAndValidity({emitEvent:!!this.onChange})}))}writeValue(e){super.writeValue(e),this.onEnableSlaveControl(e)}validate(){const{master:e,slave:t}=this.basicFormGroup.value,n=!e?.slaves?.length&&(W(t,{})||!t);return!this.basicFormGroup.valid||n?{basicFormGroup:{valid:!1}}:null}initBasicFormGroup(){return this.fb.group({master:[],slave:[]})}updateSlaveEnabling(e){e?this.basicFormGroup.get("slave").enable({emitEvent:!1}):this.basicFormGroup.get("slave").disable({emitEvent:!1})}onEnableSlaveControl(e){this.enableSlaveControl.setValue(!!e.slave&&!W(e.slave,{}))}static{this.ɵfac=function(e){return new(e||TQ)}}static{this.ɵdir=t.ɵɵdefineDirective({type:TQ,features:[t.ɵɵInheritDefinitionFeature]})}}e("ModbusBasicConfigDirective",TQ);class IQ extends TQ{constructor(){super(...arguments),this.isLegacy=!1}mapConfigToFormValue({master:e,slave:t}){return{master:e?.slaves?e:{slaves:[]},slave:t??{}}}getMappedValue(e){return{master:e.master,slave:e.slave}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(IQ)))(n||IQ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:IQ,selectors:[["tb-modbus-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>IQ)),multi:!0},{provide:he,useExisting:c((()=>IQ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:19,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","master",3,"isLegacy"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-form-hint","tb-primary-fill","tb-flex","center"],[1,"tb-form-row"],[1,"mat-slide",3,"formControl"],["formControlName","slave"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-modbus-master-table",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4)(10,"div",5),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",6)(14,"mat-slide-toggle",7)(15,"mat-label"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(18,"tb-modbus-slave-config",8),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(5,11,"gateway.master-connections")),t.ɵɵadvance(2),t.ɵɵproperty("isLegacy",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(8,13,"gateway.server-config")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,15,"gateway.hints.modbus-server")),t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.enableSlaveControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,17,"gateway.enable")," "))},dependencies:t.ɵɵgetComponentDepsFactory(IQ,[se,_,rQ,CQ,$n]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}'],changeDetection:d.OnPush})}}e("ModbusBasicConfigComponent",IQ);class EQ extends TQ{constructor(){super(...arguments),this.isLegacy=!0}mapConfigToFormValue(e){return{master:e.master?.slaves?e.master:{slaves:[]},slave:e.slave?Xa.mapSlaveToUpgradedVersion(e.slave):{}}}getMappedValue(e){return{master:e.master,slave:e.slave?Xa.mapSlaveToDowngradedVersion(e.slave):{}}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(EQ)))(n||EQ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:EQ,selectors:[["tb-modbus-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>EQ)),multi:!0},{provide:he,useExisting:c((()=>EQ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:19,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","master",3,"isLegacy"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-form-hint","tb-primary-fill","tb-flex","center"],[1,"tb-form-row"],[1,"mat-slide",3,"formControl"],["formControlName","slave"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-modbus-master-table",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4)(10,"div",5),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",6)(14,"mat-slide-toggle",7)(15,"mat-label"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(18,"tb-modbus-slave-config",8),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(5,11,"gateway.master-connections")),t.ɵɵadvance(2),t.ɵɵproperty("isLegacy",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(8,13,"gateway.server-config")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,15,"gateway.hints.modbus-server")),t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.enableSlaveControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,17,"gateway.enable")," "))},dependencies:t.ɵɵgetComponentDepsFactory(EQ,[se,_,rQ,CQ,$n]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}'],changeDetection:d.OnPush})}}e("ModbusLegacyBasicConfigComponent",EQ);const MQ=()=>({maxWidth:"970px"});function kQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",20),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.get("key").value," ")}}function PQ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtext(1),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate(e.get("methodRPC").value)}}function DQ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtext(1),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate(e.get("attributeOnThingsBoard").value)}}function OQ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtext(1),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate2(" ",e.get("requestExpression").value+" - ","",e.get("attributeNameExpression").value," ")}}function AQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21),t.ɵɵtemplate(1,PQ,2,1,"ng-container",22)(2,DQ,2,1,"ng-container",22)(3,OQ,2,2,"ng-container",22),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("ngSwitch",e.keysType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.SocketValueKey.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.SocketValueKey.ATTRIBUTES_UPDATES),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.SocketValueKey.ATTRIBUTES_REQUESTS)}}function FQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function RQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function BQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function NQ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",42),2&e){const e=t.ɵɵnextContext(5);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Key)}}function LQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",24)(2,"div",25),t.ɵɵtext(3,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",26)(5,"div",27),t.ɵɵpipe(6,"translate"),t.ɵɵpipe(7,"translate"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"div",28)(11,"mat-form-field",29),t.ɵɵelement(12,"input",30),t.ɵɵpipe(13,"translate"),t.ɵɵtemplate(14,FQ,3,3,"mat-icon",31),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(15,"div",24)(16,"div",25),t.ɵɵtext(17,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"div",26)(19,"div",32)(20,"span",33),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(23,"div",34),t.ɵɵelementEnd(),t.ɵɵelementStart(24,"label",35),t.ɵɵtext(25,"from"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",36),t.ɵɵelement(27,"input",37),t.ɵɵpipe(28,"translate"),t.ɵɵtemplate(29,RQ,3,3,"mat-icon",31),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"label",38),t.ɵɵtext(31,"to"),t.ɵɵelementEnd(),t.ɵɵelementStart(32,"mat-form-field",36),t.ɵɵelement(33,"input",39),t.ɵɵpipe(34,"translate"),t.ɵɵtemplate(35,BQ,3,3,"mat-icon",31),t.ɵɵelementEnd()(),t.ɵɵtemplate(36,NQ,1,2,"tb-report-strategy",40),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",n.keysType===n.SocketValueKey.ATTRIBUTES?t.ɵɵpipeBind1(6,12,"gateway.hints.socket.key-attribute"):t.ɵɵpipeBind1(7,14,"gateway.hints.socket.key-telemetry")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,16,"gateway.key")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(13,18,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("key").hasError("required")&&e.get("key").touched),t.ɵɵadvance(7),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(22,20,"gateway.byte")),t.ɵɵadvance(2),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/byte_fn")("tb-help-popup-style",t.ɵɵpureFunction0(26,MQ)),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,22,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("byteFrom").hasError("required")&&e.get("byteFrom").touched),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(34,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("byteTo").hasError("required")&&e.get("byteTo").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.withReportStrategy&&(n.keysType===n.SocketValueKey.ATTRIBUTES||n.keysType===n.SocketValueKey.TIMESERIES))}}function VQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-required"))}function qQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}function GQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",26)(2,"div",43),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",28)(6,"mat-form-field",29),t.ɵɵelement(7,"input",44),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,VQ,3,3,"mat-icon",31),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",26)(11,"div",45),t.ɵɵpipe(12,"translate"),t.ɵɵtext(13," gateway.encoding "),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",28)(15,"mat-form-field",29)(16,"mat-select",46),t.ɵɵtemplate(17,qQ,2,2,"mat-option",47),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(18,"div",48)(19,"mat-slide-toggle",49),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(20,"mat-label",50),t.ɵɵpipe(21,"translate"),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd()()()()}if(2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,7,"gateway.method-name")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("methodRPC").hasError("required")&&e.get("methodRPC").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(12,11,"gateway.hints.encoding")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(21,13,"gateway.hints.socket.with-response")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(23,15,"gateway.rpc.withResponse")," ")}}function zQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e)," ")}}function UQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e)," ")}}function jQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.request-expression-required"))}function HQ(e,n){1&e&&t.ɵɵelement(0,"div",34),2&e&&t.ɵɵproperty("tb-help-popup","widget/lib/gateway/request-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(2,MQ))}function WQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e)," ")}}function $Q(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.attribute-name-expression-required"))}function KQ(e,n){1&e&&t.ɵɵelement(0,"div",34),2&e&&t.ɵɵproperty("tb-help-popup","widget/lib/gateway/attribute-name-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(2,MQ))}function YQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",26)(2,"div",45),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4," gateway.type "),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",28)(6,"mat-form-field",29)(7,"mat-select",52),t.ɵɵtemplate(8,zQ,3,4,"mat-option",47),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(9,"div",26)(10,"div",43),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",53)(14,"mat-form-field",29)(15,"mat-select",54),t.ɵɵtemplate(16,UQ,3,4,"mat-option",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"mat-form-field",29),t.ɵɵelement(18,"input",55),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,jQ,3,3,"mat-icon",31)(21,HQ,1,3,"div",56),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"div",26)(23,"div",43),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"div",53)(27,"mat-form-field",29)(28,"mat-select",57),t.ɵɵtemplate(29,WQ,3,4,"mat-option",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(30,"mat-form-field",29),t.ɵɵelement(31,"input",58),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,$Q,3,3,"mat-icon",31)(34,KQ,1,3,"div",56),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,12,"gateway.hints.socket.attribute-requests-type")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.requestsType),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,14,"gateway.request-expression")," "),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.expressionType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,16,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("requestExpression").hasError("required")&&e.get("requestExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("requestExpressionSource").value===n.ExpressionType.Expression),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(25,18,"gateway.attribute-name-expression")," "),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.expressionType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("attributeNameExpression").hasError("required")&&e.get("attributeNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("attributeNameExpressionSource").value===n.ExpressionType.Expression)}}function XQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}function ZQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.socket.attribute-on-platform-required"))}function QQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",26)(2,"div",45),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4," gateway.encoding "),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",28)(6,"mat-form-field",29)(7,"mat-select",46),t.ɵɵtemplate(8,XQ,2,2,"mat-option",47),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(9,"div",26)(10,"div",43),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",28)(14,"mat-form-field",29),t.ɵɵelement(15,"input",59),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,ZQ,3,3,"mat-icon",31),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,5,"gateway.hints.encoding")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,7,"gateway.attribute-on-platform")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("attributeOnThingsBoard").hasError("required")&&e.get("attributeOnThingsBoard").touched)}}function JQ(e,n){if(1&e&&t.ɵɵtemplate(0,LQ,37,27,"div",23)(1,GQ,24,17,"div",23)(2,YQ,35,22,"div",23)(3,QQ,18,11,"div",23),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.TIMESERIES||e.keysType===e.SocketValueKey.ATTRIBUTES),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.ATTRIBUTES_REQUESTS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.ATTRIBUTES_UPDATES)}}function eJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",12)(1,"div",13),t.ɵɵelementContainerStart(2,14),t.ɵɵelementStart(3,"mat-expansion-panel",15)(4,"mat-expansion-panel-header",16)(5,"mat-panel-title"),t.ɵɵtemplate(6,kQ,2,1,"div",17)(7,AQ,4,4,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()(),t.ɵɵtemplate(9,JQ,4,4,"ng-template",18),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",19),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.last,a=t.ɵɵreference(8),r=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",r.keysType===r.SocketValueKey.TIMESERIES||r.keysType===r.SocketValueKey.ATTRIBUTES)("ngIfElse",a),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,r.deleteKeyTitle))}}function tJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10),t.ɵɵtemplate(1,eJ,14,7,"div",11),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.keysListFormArray.controls)("ngForTrackBy",e.trackByKey)}}function nJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",60)(1,"span",61),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class iJ extends q{constructor(e,t,n){super(n),this.fb=e,this.popover=t,this.store=n,this.withReportStrategy=!0,this.keysDataApplied=new u,this.SocketValueKey=hi,this.socketEncoding=Object.values(ht),this.requestsType=Object.values(fi),this.expressionType=Object.values(yi),this.ExpressionType=yi,this.ReportStrategyDefaultValue=on}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}trackByKey(e,t){return t}addKey(){let e;e=this.keysType===hi.RPC_METHODS?this.fb.group({methodRPC:["",[me.required]],encoding:[ht.UTF16,[me.required]],withResponse:[!0]}):this.keysType===hi.ATTRIBUTES_UPDATES?this.fb.group({encoding:[ht.UTF16,[me.required]],attributeOnThingsBoard:["",[me.required]]}):this.keysType===hi.ATTRIBUTES_REQUESTS?this.fb.group({type:[fi.Shared],requestExpressionSource:[yi.Constant],attributeNameExpressionSource:[yi.Constant],requestExpression:["",[me.required]],attributeNameExpression:["",[me.required]]}):this.fb.group({key:["",[me.required,me.pattern(pn)]],byteFrom:[0,[me.required]],byteTo:[0,[me.required]],reportStrategy:[{value:null,disabled:this.isReportStrategyDisabled()}]}),this.keysListFormArray.push(e)}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){let e=this.keysListFormArray.value.map((({reportStrategy:e,...t})=>({...t,...e&&{reportStrategy:e}})));this.keysDataApplied.emit(e)}prepareKeysFormArray(e){const t=[];return e?.forEach((e=>{let n;if(this.keysType===hi.RPC_METHODS){const t=e;n=this.fb.group({methodRPC:[t.methodRPC,[me.required]],encoding:[t.encoding,[me.required]],withResponse:[t.withResponse]})}else if(this.keysType===hi.ATTRIBUTES_REQUESTS){const t=e;n=this.fb.group({type:[t.type??fi.Shared],requestExpressionSource:[t.requestExpressionSource??yi.Constant],attributeNameExpressionSource:[t.attributeNameExpressionSource??yi.Constant],requestExpression:[t.requestExpression,[me.required]],attributeNameExpression:[t.attributeNameExpression,[me.required]]})}else if(this.keysType===hi.ATTRIBUTES_UPDATES)n=this.fb.group({encoding:[e.encoding??ht.UTF16],attributeOnThingsBoard:[e.attributeOnThingsBoard,[me.required]]});else{const{key:t,byteFrom:i,byteTo:a,reportStrategy:r}=e;n=this.fb.group({key:[t,[me.required,me.pattern(pn)]],byteFrom:[i??0,[me.required]],byteTo:[a??0,[me.required]],reportStrategy:[{value:r,disabled:this.isReportStrategyDisabled()}]})}t.push(n)})),this.fb.array(t)}isReportStrategyDisabled(){return!(this.withReportStrategy&&(this.keysType===hi.ATTRIBUTES||this.keysType===hi.TIMESERIES))}static{this.ɵfac=function(e){return new(e||iJ)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverComponent),t.ɵɵdirectiveInject(Xe.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:iJ,selectors:[["tb-device-data-keys-panel"]],inputs:{panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keys:"keys",keysType:"keysType",withReportStrategy:"withReportStrategy"},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:16,consts:[["noKeys",""],["valueTitle",""],[1,"tb-device-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],["class","title-container",4,"ngIf","ngIfElse"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"title-container"],[1,"title-container",3,"ngSwitch"],[4,"ngSwitchCase"],["class","tb-form-panel no-border no-padding",4,"ngIf"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],[1,"fixed-title-width","tb-flex","align-center"],[1,"tb-required"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["for","byteFrom",1,"tb-small-label"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","flex-1"],["matInput","","required","","formControlName","byteFrom","type","number","id","byteFrom",3,"placeholder"],["for","byteTo",1,"tb-small-label"],["matInput","","required","","formControlName","byteTo","type","number","id","byteTo",3,"placeholder"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue"],[1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","methodRPC",3,"placeholder"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","encoding"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-row"],["formControlName","withResponse",1,"mat-slide",3,"click"],[3,"tb-hint-tooltip-icon"],[3,"value"],["formControlName","type"],[1,"tb-flex"],["formControlName","requestExpressionSource"],["matInput","","name","value","formControlName","requestExpression",3,"placeholder"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style",4,"ngIf"],["formControlName","attributeNameExpressionSource"],["matInput","","name","value","formControlName","attributeNameExpression",3,"placeholder"],["matInput","","name","value","formControlName","attributeOnThingsBoard",3,"placeholder"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"div",3)(2,"div",4),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,tJ,2,2,"div",5),t.ɵɵelementStart(6,"div")(7,"button",6),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(10,nJ,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(12,"div",7)(13,"button",8),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.applyKeysData())})),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(11);t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,8,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")"," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.keysListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,10,n.addKeyTitle)," "),t.ɵɵadvance(6),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,12,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,14,"action.apply")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(iJ,[se,_,ni]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:500px;overflow:auto}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-small-label[_ngcontent-%COMP%]{font-size:16px;padding-right:0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}'],changeDetection:d.OnPush})}}e("DeviceDataKeysPanelComponent",iJ),He([I()],iJ.prototype,"withReportStrategy",void 0);const aJ=()=>({maxWidth:"970px"});function rJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-filter-required"))}function oJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function sJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function lJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",41),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}function pJ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",27),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function cJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function dJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function uJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.type," ")}}function mJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.encoding," ")}}function hJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.methodRPC," ")}}class gJ extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.destroyRef=l,this.cdr=p,this.deviceFormGroup=this.fb.group({address:["",[me.required,me.pattern(pn)]],deviceName:["",[me.required,me.pattern(pn)]],deviceType:["",[me.required,me.pattern(pn)]],encoding:[ht.UTF8],telemetry:[[]],attributes:[[]],attributeRequests:[[]],attributeUpdates:[[]],serverSideRpc:[[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),this.keysPopupClosed=!0,this.SocketValueKey=hi,this.socketDeviceHelpLink=O+"/docs/iot-gateway/config/socket/#device-subsection",this.socketEncoding=Object.values(ht),this.ReportStrategyDefaultValue=on,this.deviceFormGroup.patchValue(this.data.value,{emitEvent:!1})}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.deviceFormGroup.valid){const e=this.deviceFormGroup.value;Y(e),this.dialogRef.close(e)}}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))return void this.popoverService.hidePopover(i);const a=this.deviceFormGroup.get(n),r={keys:a.value,keysType:n,panelTitle:gi.get(n),addKeyTitle:vi.get(n),deleteKeyTitle:xi.get(n),noKeysText:bi.get(n),withReportStrategy:this.data.withReportStrategy};this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,iJ,"leftBottom",!1,null,r,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(In(this.destroyRef)).subscribe((e=>{this.popoverComponent.hide(),a.patchValue(e),a.markAsDirty(),this.cdr.markForCheck()})),this.popoverComponent.tbHideStart.pipe(In(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}static{this.ɵfac=function(e){return new(e||gJ)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(rt.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:gJ,selectors:[["tb-device-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:119,vars:57,consts:[["socketTelemetryButton",""],["attributesButton",""],["attributeRequestsButton",""],["attributesUpdatesButton",""],["rpcMethodsButton",""],[1,"dialog-mapping",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width"],["translate","",1,"tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","address",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","encoding"],[3,"value",4,"ngFor","ngForOf"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],[1,"tb-form-row","space-between","tb-flex"],["translate","",1,"fixed-title-width"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[4,"ngFor","ngForOf"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",5)(1,"mat-toolbar",6)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",7)(6,"div",8),t.ɵɵelementStart(7,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",10),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",11)(11,"div",12)(12,"div",13)(13,"div",14)(14,"div",15),t.ɵɵtext(15," gateway.address-filter "),t.ɵɵelementEnd()(),t.ɵɵelementStart(16,"div",16)(17,"mat-form-field",17),t.ɵɵelement(18,"input",18),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,rJ,3,3,"mat-icon",19),t.ɵɵelement(21,"div",20),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"div",13)(23,"div",21),t.ɵɵtext(24,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"div",16)(26,"mat-form-field",17),t.ɵɵelement(27,"input",22),t.ɵɵpipe(28,"translate"),t.ɵɵtemplate(29,oJ,3,3,"mat-icon",19),t.ɵɵelementEnd()()(),t.ɵɵelementStart(30,"div",13)(31,"div",21),t.ɵɵtext(32,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"div",16)(34,"mat-form-field",17),t.ɵɵelement(35,"input",23),t.ɵɵpipe(36,"translate"),t.ɵɵtemplate(37,sJ,3,3,"mat-icon",19),t.ɵɵelementEnd()()(),t.ɵɵelementStart(38,"div",13)(39,"div",24),t.ɵɵpipe(40,"translate"),t.ɵɵtext(41," gateway.encoding "),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"div",16)(43,"mat-form-field",17)(44,"mat-select",25),t.ɵɵtemplate(45,lJ,2,2,"mat-option",26),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(46,pJ,1,2,"tb-report-strategy",27),t.ɵɵelementStart(47,"div",28)(48,"div",29),t.ɵɵtext(49,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(50,"div",30)(51,"mat-chip-listbox",31),t.ɵɵtemplate(52,cJ,2,1,"mat-chip",32),t.ɵɵelementStart(53,"mat-chip",33),t.ɵɵelement(54,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(55,"button",35,0),t.ɵɵpipe(57,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(56);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.TIMESERIES))})),t.ɵɵelementStart(58,"tb-icon",36),t.ɵɵtext(59,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(60,"div",28)(61,"div",29),t.ɵɵtext(62,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(63,"div",30)(64,"mat-chip-listbox",31),t.ɵɵtemplate(65,dJ,2,1,"mat-chip",32),t.ɵɵelementStart(66,"mat-chip",33),t.ɵɵelement(67,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(68,"button",35,1),t.ɵɵpipe(70,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(69);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.ATTRIBUTES))})),t.ɵɵelementStart(71,"tb-icon",36),t.ɵɵtext(72,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(73,"div",28)(74,"div",29),t.ɵɵtext(75,"gateway.attribute-requests"),t.ɵɵelementEnd(),t.ɵɵelementStart(76,"div",30)(77,"mat-chip-listbox",31),t.ɵɵtemplate(78,uJ,2,1,"mat-chip",32),t.ɵɵelementStart(79,"mat-chip",33),t.ɵɵelement(80,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(81,"button",35,2),t.ɵɵpipe(83,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(82);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.ATTRIBUTES_REQUESTS))})),t.ɵɵelementStart(84,"tb-icon",36),t.ɵɵtext(85,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(86,"div",28)(87,"div",29),t.ɵɵtext(88,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(89,"div",30)(90,"mat-chip-listbox",31),t.ɵɵtemplate(91,mJ,2,1,"mat-chip",32),t.ɵɵelementStart(92,"mat-chip",33),t.ɵɵelement(93,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(94,"button",35,3),t.ɵɵpipe(96,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(95);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.ATTRIBUTES_UPDATES))})),t.ɵɵelementStart(97,"tb-icon",36),t.ɵɵtext(98,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(99,"div",28)(100,"div",29),t.ɵɵtext(101,"gateway.rpc-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(102,"div",30)(103,"mat-chip-listbox",31),t.ɵɵtemplate(104,hJ,2,1,"mat-chip",32),t.ɵɵelementStart(105,"mat-chip",33),t.ɵɵelement(106,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(107,"button",35,4),t.ɵɵpipe(109,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(108);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.RPC_METHODS))})),t.ɵɵelementStart(110,"tb-icon",36),t.ɵɵtext(111,"edit"),t.ɵɵelementEnd()()()()()(),t.ɵɵelementStart(112,"div",37)(113,"button",38),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(114),t.ɵɵpipe(115,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(116,"button",39),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(117),t.ɵɵpipe(118,"translate"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵproperty("formGroup",n.deviceFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,32,"gateway.device")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.socketDeviceHelpLink),t.ɵɵadvance(12),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,34,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceFormGroup.get("address").hasError("required")&&n.deviceFormGroup.get("address").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/address-filter_fn")("tb-help-popup-style",t.ɵɵpureFunction0(56,aJ)),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,36,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceFormGroup.get("deviceName").hasError("required")&&n.deviceFormGroup.get("deviceName").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,38,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceFormGroup.get("deviceType").hasError("required")&&n.deviceFormGroup.get("deviceType").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(40,40,"gateway.hints.encoding")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(),t.ɵɵconditional(n.data.withReportStrategy?46:-1),t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("telemetry").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("telemetry").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(57,42,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(70,44,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributeRequests").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("attributeRequests").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(83,46,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(96,48,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(109,50,"action.edit")),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(115,52,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.deviceFormGroup.invalid||!n.deviceFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(118,54,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(gJ,[se,_,$n,ni]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:grid;height:100%}[_nghost-%COMP%] .tb-form-panel[_ngcontent-%COMP%]{width:77vw;max-width:800px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%]{max-width:900px;display:flex;flex-direction:column}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{min-height:64px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] tb-toggle-select[_ngcontent-%COMP%]{padding:4px 0}[_nghost-%COMP%] .mat-mdc-dialog-content[_ngcontent-%COMP%]{height:670px}[_nghost-%COMP%] .ellipsis-chips-container[_ngcontent-%COMP%]{max-width:70%}[_nghost-%COMP%] .dialog-mapping .mat-mdc-chip-listbox .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center}[_nghost-%COMP%] .tb-form-row .fixed-title-width{min-width:40px;width:35%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center}[_nghost-%COMP%] .tb-form-row .mat-mdc-form-field{width:0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}[_nghost-%COMP%] .device-config{gap:12px;padding-left:10px;padding-right:10px}[_nghost-%COMP%] .device-node-pattern-field{flex-basis:3%}'],changeDetection:d.OnPush})}}e("DeviceDialogComponent",gJ);const fJ=["searchInput"],yJ=()=>["address","deviceName","actions"],vJ=()=>({minWidth:"96px",textAlign:"center"});function xJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",26)(2,"span",27),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",9),t.ɵɵelementStart(6,"button",11),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageDevices(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",11),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.devices")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search")))}function bJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.address-filter")," "))}function wJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.address)}}function SJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.device-name")))}function CJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.deviceName)}}function _J(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",30)}function TJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",11),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageDevices(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",11),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteDevice(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function IJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,TJ,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",31),t.ɵɵelementContainer(4,32),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",33)(6,"button",34),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",35),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",36,2),t.ɵɵelementContainer(11,32),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,vJ)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function EJ(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",37)}function MJ(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class kJ{constructor(e,t,n,i,a){this.translate=e,this.dialog=t,this.dialogService=n,this.fb=i,this.cdr=a,this.withReportStrategy=!0,this.textSearchMode=!1,this.textSearch=this.fb.control("",{nonNullable:!0}),this.onChange=()=>{},this.destroy$=new we,this.devicesFormGroup=this.fb.array([]),this.dataSource=new PJ}ngOnInit(){this.devicesFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.updateTableData(e),this.onChange(e)}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(De(150),Pe(((e,t)=>(e??"")===t.trim())),Me(this.destroy$)).subscribe((e=>this.updateTableData(this.devicesFormGroup.value,e.trim())))}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){this.devicesFormGroup.clear(),this.pushDataAsFormArrays(e)}enterFilterMode(){this.textSearchMode=!0,this.cdr.detectChanges();const e=this.searchInputField.nativeElement;e.focus(),e.setSelectionRange(0,0)}exitFilterMode(){this.updateTableData(this.devicesFormGroup.value),this.textSearchMode=!1,this.textSearch.reset()}manageDevices(e,t){e&&e.stopPropagation();const n=J(t),i=n?this.devicesFormGroup.at(t).value:{};this.getDeviceDialog(i,n?"action.apply":"action.add").afterClosed().pipe(Ne(1),Me(this.destroy$)).subscribe((e=>{e&&(n?this.devicesFormGroup.at(t).patchValue(e):this.devicesFormGroup.push(this.fb.control(e)),this.devicesFormGroup.markAsDirty())}))}validate(){return this.devicesFormGroup.controls.length?null:{devicesFormGroup:{valid:!1}}}getDeviceDialog(e,t){return this.dialog.open(gJ,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,withReportStrategy:this.withReportStrategy}})}deleteDevice(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-device-title",{name:this.devicesFormGroup.controls[t].value.deviceName}),this.translate.instant("gateway.delete-device-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(Ne(1),Me(this.destroy$)).subscribe((e=>{e&&(this.devicesFormGroup.removeAt(t),this.devicesFormGroup.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.devicesFormGroup.push(this.fb.control(e))))}static{this.ɵfac=function(e){return new(e||kJ)(t.ɵɵdirectiveInject(le.TranslateService),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(K.DialogService),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:kJ,selectors:[["tb-devices-config-table"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(fJ,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{withReportStrategy:"withReportStrategy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>kJ)),multi:!0},{provide:he,useExisting:c((()=>kJ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:45,vars:36,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-device-table","tb-absolute-fill"],[1,"tb-device-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","table-value-column",4,"matHeaderCellDef"],["class","table-value-column",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-device-table-title"],[1,"table-value-column"],["tbTruncateWithTooltip",""],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,xJ,14,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"div",7)(7,"button",8),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-icon"),t.ɵɵtext(10,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",9)(12,"mat-label"),t.ɵɵtext(13," "),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",10,0),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵpipe(18,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(19,"mat-icon"),t.ɵɵtext(20,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(21,"div",12)(22,"table",13),t.ɵɵelementContainerStart(23,14),t.ɵɵtemplate(24,bJ,3,3,"mat-header-cell",15)(25,wJ,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(26,14),t.ɵɵtemplate(27,SJ,4,3,"mat-header-cell",15)(28,CJ,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(29,17),t.ɵɵtemplate(30,_J,1,0,"mat-header-cell",18)(31,IJ,12,6,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(32,EJ,1,0,"mat-header-row",20)(33,MJ,1,0,"mat-row",21),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"section",22),t.ɵɵpipe(35,"async"),t.ɵɵelementStart(36,"button",23),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageDevices(i))})),t.ɵɵelementStart(37,"mat-icon",24),t.ɵɵtext(38,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(39,"span"),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(42,"span",25),t.ɵɵpipe(43,"async"),t.ɵɵtext(44," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(4,20,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,22,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,24,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,26,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","address"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","deviceName"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(34,yJ))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(35,yJ)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(35,28,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,30,"gateway.add-device")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(43,32,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(kJ,[se,_,Wn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%]{width:100%;height:100%;background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:35%}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}e("DevicesConfigTableComponent",kJ),He([I()],kJ.prototype,"withReportStrategy",void 0);let PJ=class extends G{constructor(){super()}};function DJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",14),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function OJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",15),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-required"))}function AJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",15),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.socketConfigFormGroup.get("port")))}}function FJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",15),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.socketConfigFormGroup.get("bufferSize").hasError("min")?"gateway.buffer-size-range":"gateway.buffer-size-required"))}}e("DevicesDatasource",PJ);class RJ{constructor(e){this.fb=e,this.portLimits=Oi,this.socketTypes=Object.values(mi),this.onChange=e=>{},this.destroy$=new we,this.socketConfigFormGroup=this.fb.group({address:["",[me.required,me.pattern(pn)]],type:[mi.TCP],port:[5e4,[me.required,me.min(Oi.MIN),me.max(Oi.MAX)]],bufferSize:[1024,[me.required,me.min(1),me.pattern(pn)]]}),this.socketConfigFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e)}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){const{address:t="",type:n=mi.TCP,port:i=5e4,bufferSize:a=1024}=e??{};this.socketConfigFormGroup.reset({address:t,type:n,port:i,bufferSize:a})}validate(){return this.socketConfigFormGroup.valid?null:{socketConfigFormGroup:{valid:!1}}}static{this.ɵfac=function(e){return new(e||RJ)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:RJ,selectors:[["tb-socket-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>RJ)),multi:!0},{provide:he,useExisting:c((()=>RJ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:34,vars:25,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width"],["tbTruncateWithTooltip",""],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","address",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","name","value","min","1","formControlName","bufferSize",3,"placeholder"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2)(3,"div",3),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"tb-toggle-select",4),t.ɵɵtemplate(7,DJ,2,2,"tb-toggle-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",1)(9,"div",6),t.ɵɵtext(10,"gateway.address"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"div",7)(12,"mat-form-field",8),t.ɵɵelement(13,"input",9),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,OJ,3,3,"mat-icon",10),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",1)(17,"div",6),t.ɵɵtext(18,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",7)(20,"mat-form-field",8),t.ɵɵelement(21,"input",11),t.ɵɵpipe(22,"translate"),t.ɵɵtemplate(23,AJ,3,3,"mat-icon",10),t.ɵɵelementEnd()()(),t.ɵɵelementStart(24,"div",1)(25,"div",12),t.ɵɵpipe(26,"translate"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"div",7)(30,"mat-form-field",8),t.ɵɵelement(31,"input",13),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,FJ,3,3,"mat-icon",10),t.ɵɵelementEnd()()()()),2&e&&(t.ɵɵproperty("formGroup",n.socketConfigFormGroup),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,13,"gateway.connection-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.socketTypes),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.socketConfigFormGroup.get("address").hasError("required")&&n.socketConfigFormGroup.get("address").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(22,17,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.socketConfigFormGroup.get("port").hasError("required")||n.socketConfigFormGroup.get("port").hasError("min")||n.socketConfigFormGroup.get("port").hasError("max"))&&n.socketConfigFormGroup.get("port").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(26,19,"gateway.hints.buffer-size")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(28,21,"gateway.buffer-size")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,23,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.socketConfigFormGroup.get("bufferSize").hasError("required")||n.socketConfigFormGroup.get("bufferSize").hasError("min")&&n.socketConfigFormGroup.get("bufferSize").touched))},dependencies:t.ɵɵgetComponentDepsFactory(RJ,[se,_,u$,Wn]),encapsulation:2,changeDetection:d.OnPush})}}e("SocketConfigComponent",RJ);class BJ extends Ua{constructor(){super(...arguments),this.isLegacy=!1}getMappedValue(e){return e}initBasicFormGroup(){return this.fb.group({socket:[],devices:[]})}mapConfigToFormValue(e){return{socket:e.socket??{},devices:e.devices??[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(BJ)))(n||BJ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:BJ,selectors:[["tb-socket-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>BJ)),multi:!0},{provide:he,useExisting:c((()=>BJ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:10,vars:14,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","socket"],["formControlName","devices",3,"withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-socket-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-devices-config-table",4),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,8,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,10,"gateway.socket"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,12,"gateway.devices"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("withReportStrategy",!n.isLegacy))},dependencies:t.ɵɵgetComponentDepsFactory(BJ,[se,_,RJ,kJ]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("SocketBasicConfigComponent",BJ);class NJ extends Ua{constructor(){super(...arguments),this.isLegacy=!0}getMappedValue(e){return Qa.mapSocketToDowngradedVersion(e)}initBasicFormGroup(){return this.fb.group({socket:[],devices:[]})}mapConfigToFormValue(e){return{socket:Qa.mapSocketToUpgradedVersion(e),devices:e?.devices?Qa.mapDevicesToUpgradedVersion(e.devices):[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(NJ)))(n||NJ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:NJ,selectors:[["tb-socket-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>NJ)),multi:!0},{provide:he,useExisting:c((()=>NJ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:10,vars:14,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","socket"],["formControlName","devices",3,"withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-socket-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-devices-config-table",4),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,8,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,10,"gateway.socket"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,12,"gateway.devices"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("withReportStrategy",!n.isLegacy))},dependencies:t.ɵɵgetComponentDepsFactory(NJ,[se,_,RJ,kJ]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}function LJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.name-required"))}function VJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function qJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.applicationConfigFormGroup.get("port")))}}function GJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.object-id-required"))}function zJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.vendor-id-required"))}function UJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SegmentationTypeTranslationsMap.get(e))," ")}}e("SocketLegacyBasicConfigComponent",NJ);class jJ extends za{constructor(){super(...arguments),this.segmentationTypes=Object.values(Ca),this.SegmentationTypeTranslationsMap=_a,this.portLimits=Oi}get applicationConfigFormGroup(){return this.formGroup}initFormGroup(){return this.fb.group({objectName:["",[me.required,me.pattern(pn)]],host:["",[me.required,me.pattern(pn)]],port:[null,[me.required,me.min(Oi.MIN),me.max(Oi.MAX)]],mask:[""],objectIdentifier:[null,[me.required]],vendorIdentifier:[null,[me.required]],maxApduLengthAccepted:[],segmentationSupported:[Ca.BOTH],networkNumber:[],deviceDiscoveryTimeoutInSec:[]})}mapOnChangeValue(e){return Y(e),e}onWriteValue(e){const{maxApduLengthAccepted:t=1476,segmentationSupported:n=Ca.BOTH,networkNumber:i=3,deviceDiscoveryTimeoutInSec:a=5,...r}=e;this.formGroup.reset({...r,maxApduLengthAccepted:t,segmentationSupported:n,networkNumber:i,deviceDiscoveryTimeoutInSec:a},{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(jJ)))(n||jJ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:jJ,selectors:[["tb-bacnet-application-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>jJ)),multi:!0},{provide:he,useExisting:c((()=>jJ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:83,vars:53,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","autocomplete","off","name","value","formControlName","objectName",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["translate","",1,"fixed-title-width"],["matInput","","name","value","formControlName","mask",3,"placeholder"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","objectIdentifier",3,"placeholder"],["matInput","","type","number","min","0","name","value","formControlName","vendorIdentifier",3,"placeholder"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-panel","no-border","no-padding","padding-top"],["matInput","","type","number","min","0","name","value","formControlName","maxApduLengthAccepted",3,"placeholder"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","segmentationSupported"],[3,"value"],["matInput","","type","number","min","0","name","value","formControlName","networkNumber",3,"placeholder"],["matInput","","type","number","min","0","name","value","formControlName","deviceDiscoveryTimeoutInSec",3,"placeholder"],["translate","","matSuffix","",1,"block","pr-2"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.bacnet.object-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",3)(5,"mat-form-field",4),t.ɵɵelement(6,"input",5),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,LJ,3,3,"mat-icon",6),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",1)(10,"div",2),t.ɵɵtext(11,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",4),t.ɵɵelement(13,"input",7),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,VJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(16,"div",1)(17,"div",2),t.ɵɵtext(18,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"mat-form-field",4),t.ɵɵelement(20,"input",8),t.ɵɵpipe(21,"translate"),t.ɵɵtemplate(22,qJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(23,"div",1)(24,"div",9),t.ɵɵtext(25,"gateway.network-mask"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",4),t.ɵɵelement(27,"input",10),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(29,"div",1)(30,"div",11),t.ɵɵpipe(31,"translate"),t.ɵɵtext(32,"gateway.object-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"mat-form-field",4),t.ɵɵelement(34,"input",12),t.ɵɵpipe(35,"translate"),t.ɵɵtemplate(36,GJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(37,"div",1)(38,"div",11),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40,"gateway.vendor-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(41,"mat-form-field",4),t.ɵɵelement(42,"input",13),t.ɵɵpipe(43,"translate"),t.ɵɵtemplate(44,zJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"div",14)(46,"mat-expansion-panel",15)(47,"mat-expansion-panel-header")(48,"mat-panel-title")(49,"div",16),t.ɵɵtext(50,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(51,"div",17)(52,"div",1)(53,"div",11),t.ɵɵpipe(54,"translate"),t.ɵɵtext(55,"gateway.bacnet.apdu-length"),t.ɵɵelementEnd(),t.ɵɵelementStart(56,"mat-form-field",4),t.ɵɵelement(57,"input",18),t.ɵɵpipe(58,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(59,"div",1)(60,"div",19),t.ɵɵpipe(61,"translate"),t.ɵɵtext(62,"gateway.bacnet.segmentation.label"),t.ɵɵelementEnd(),t.ɵɵelementStart(63,"mat-form-field",4)(64,"mat-select",20),t.ɵɵrepeaterCreate(65,UJ,3,4,"mat-option",21,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()(),t.ɵɵelementStart(67,"div",1)(68,"div",19),t.ɵɵpipe(69,"translate"),t.ɵɵtext(70,"gateway.bacnet.network-number"),t.ɵɵelementEnd(),t.ɵɵelementStart(71,"mat-form-field",4),t.ɵɵelement(72,"input",22),t.ɵɵpipe(73,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(74,"div",1)(75,"div",19),t.ɵɵpipe(76,"translate"),t.ɵɵtext(77,"gateway.bacnet.device-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(78,"mat-form-field",4),t.ɵɵelement(79,"input",23),t.ɵɵpipe(80,"translate"),t.ɵɵelementStart(81,"span",24),t.ɵɵtext(82,"gateway.suffix.s"),t.ɵɵelementEnd()()()()()()()),2&e&&(t.ɵɵproperty("formGroup",n.applicationConfigFormGroup),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,23,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("objectName").hasError("required")&&n.applicationConfigFormGroup.get("objectName").touched?8:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,25,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("host").hasError("required")&&n.applicationConfigFormGroup.get("host").touched?15:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,27,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional((n.applicationConfigFormGroup.get("port").hasError("required")||n.applicationConfigFormGroup.get("port").hasError("min")||n.applicationConfigFormGroup.get("port").hasError("max"))&&n.applicationConfigFormGroup.get("port").touched?22:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,29,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(31,31,"gateway.hints.bacnet.object-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(35,33,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("objectIdentifier").hasError("required")&&n.applicationConfigFormGroup.get("objectIdentifier").touched?36:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(39,35,"gateway.hints.bacnet.vendor-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(43,37,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("vendorIdentifier").hasError("required")&&n.applicationConfigFormGroup.get("vendorIdentifier").touched?44:-1),t.ɵɵadvance(9),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(54,39,"gateway.hints.bacnet.apdu-length")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(58,41,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(61,43,"gateway.hints.bacnet.segmentation")),t.ɵɵadvance(5),t.ɵɵrepeater(n.segmentationTypes),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(69,45,"gateway.hints.bacnet.network-number")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(73,47,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(76,49,"gateway.hints.bacnet.device-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(80,51,"gateway.set")))},dependencies:t.ɵɵgetComponentDepsFactory(jJ,[se,_,u$]),encapsulation:2,changeDetection:d.OnPush})}}function HJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",11),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function WJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",14),t.ɵɵpipe(1,"translate"),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",15)(6,"mat-form-field",6),t.ɵɵelement(7,"input",16),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,HJ,3,3,"mat-icon",11),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",e.keyType===e.BacnetDeviceKeysType.TIMESERIES?t.ɵɵpipeBind1(1,4,"gateway.hints.socket.key-telemetry"):t.ɵɵpipeBind1(2,6,"gateway.hints.socket.key-attribute")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,8,"gateway.key")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,10,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.formGroup.get("key").hasError("required")&&e.formGroup.get("key").touched?9:-1)}}function $J(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",11),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-required"))}function KJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",14),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",15)(5,"mat-form-field",6),t.ɵɵelement(6,"input",17),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,$J,3,3,"mat-icon",11),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(1,4,"gateway.hints.method")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,6,"gateway.method")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.formGroup.get("method").hasError("required")&&e.formGroup.get("method").touched?8:-1)}}function YJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.BacnetRequestTypeTranslationsMap.get(e))," ")}}function XJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",18)(2,"div",14),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"tb-toggle-select",19),t.ɵɵrepeaterCreate(7,YJ,3,4,"tb-toggle-option",8,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.hints.bacnet.request-type")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,4,"gateway.bacnet.request-type.label")),t.ɵɵadvance(3),t.ɵɵrepeater(e.requestTypes)}}function ZJ(e,n){1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",4),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.request-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",6),t.ɵɵelement(5,"input",20),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,2,"gateway.hints.bacnet.request-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,4,"gateway.set")))}function QJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.BacnetKeyObjectTypeTranslationsMap.get(e))," ")}}function JJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",11),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.object-id-required"))}function e1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.BacnetPropertyIdTranslationsMap.get(e))," ")}}function t1(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",13),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Key)}}class n1 extends za{constructor(){super(...arguments),this.withReportStrategy=!0,this.propertyIds=Aa.get(Pa.analogOutput),this.objectTypes=Object.values(Pa),this.requestTypes=Object.values(Ra),this.ReportStrategyDefaultValue=on,this.BacnetDeviceKeysType=Ta,this.BacnetKeyObjectTypeTranslationsMap=Da,this.BacnetPropertyIdTranslationsMap=Fa,this.BacnetRequestTypeTranslationsMap=Ba}ngOnInit(){this.formGroup=this.initKeyFormGroup(),this.observeValueChanges(),this.observeObjectType()}isReportStrategyDisabled(){return!(this.withReportStrategy&&(this.keyType===Ta.ATTRIBUTES||this.keyType===Ta.TIMESERIES))}initKeyFormGroup(){return this.fb.group({key:[{value:"",disabled:this.keyType===Ta.RPC_METHODS},[me.required,me.pattern(pn)]],method:[{value:"",disabled:this.keyType!==Ta.RPC_METHODS},[me.required,me.pattern(pn)]],objectType:[Pa.analogOutput],objectId:[0,[me.required]],propertyId:[Oa.presentValue],requestTimeout:[{value:0,disabled:this.keyType!==Ta.RPC_METHODS}],requestType:[{value:Ra.Write,disabled:this.keyType!==Ta.RPC_METHODS}],reportStrategy:[{value:null,disabled:this.isReportStrategyDisabled()}]})}observeObjectType(){this.formGroup.get("objectType").valueChanges.pipe(In(this.destroyRef)).subscribe((e=>{this.propertyIds=Aa.get(e),this.propertyIds.includes(this.formGroup.get("propertyId").value)||this.formGroup.get("propertyId").patchValue(this.propertyIds[0],{emitEvent:!1})}))}initFormGroup(){return this.fb.group({})}mapOnChangeValue({reportStrategy:e,...t}){return e?{...t,reportStrategy:e}:t}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(n1)))(n||n1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:n1,selectors:[["tb-bacnet-device-data-key"]],inputs:{keyType:"keyType",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>n1)),multi:!0},{provide:he,useExisting:c((()=>n1)),multi:!0}]),t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:35,vars:15,consts:[[1,"tb-form-panel","no-border","no-padding",3,"formGroup"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap","raw-value-option"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","objectType"],[3,"value"],[1,"tb-form-table-row-cell","tb-flex","no-gap"],["matInput","","type","number","min","0","name","value","formControlName","objectId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["formControlName","propertyId"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matInput","","name","value","formControlName","method",3,"placeholder"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["formControlName","requestType","appearance","fill"],["matInput","","type","number","min","0","name","value","formControlName","requestTimeout",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",3),t.ɵɵtemplate(5,WJ,10,12)(6,KJ,9,10),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",1)(8,"div",2),t.ɵɵtext(9,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵtemplate(10,XJ,9,6,"div",3)(11,ZJ,7,6,"div",3),t.ɵɵelementStart(12,"div",3)(13,"div",4),t.ɵɵpipe(14,"translate"),t.ɵɵtext(15,"gateway.object-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",5)(17,"mat-form-field",6)(18,"mat-select",7),t.ɵɵrepeaterCreate(19,QJ,3,4,"mat-option",8,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()()(),t.ɵɵelementStart(21,"div",9)(22,"mat-form-field",6),t.ɵɵelement(23,"input",10),t.ɵɵpipe(24,"translate"),t.ɵɵtemplate(25,JJ,3,3,"mat-icon",11),t.ɵɵelementEnd()()(),t.ɵɵelementStart(26,"div",3)(27,"div",4),t.ɵɵpipe(28,"translate"),t.ɵɵtext(29,"gateway.property-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",6)(31,"mat-select",12),t.ɵɵrepeaterCreate(32,e1,3,4,"mat-option",8,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()()(),t.ɵɵtemplate(34,t1,1,2,"tb-report-strategy",13),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.formGroup),t.ɵɵadvance(5),t.ɵɵconditional(n.keyType!==n.BacnetDeviceKeysType.RPC_METHODS?5:6),t.ɵɵadvance(5),t.ɵɵconditional(n.keyType===n.BacnetDeviceKeysType.RPC_METHODS?10:-1),t.ɵɵadvance(),t.ɵɵconditional(n.keyType===n.BacnetDeviceKeysType.RPC_METHODS?11:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(14,9,"gateway.hints.bacnet.key-object-id")),t.ɵɵadvance(6),t.ɵɵrepeater(n.objectTypes),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(24,11,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.formGroup.get("objectId").hasError("required")&&n.formGroup.get("objectId").touched?25:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(28,13,"gateway.hints.bacnet.property-id")),t.ɵɵadvance(5),t.ɵɵrepeater(n.propertyIds),t.ɵɵadvance(2),t.ɵɵconditional(n.isReportStrategyDisabled()?-1:34))},dependencies:t.ɵɵgetComponentDepsFactory(n1,[se,_,ni]),encapsulation:2,changeDetection:d.OnPush})}}function i1(e,n){if(1&e&&t.ɵɵelement(0,"tb-bacnet-device-data-key",17),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("formControl",e)("keyType",n.keysType)("withReportStrategy",n.withReportStrategy)}}function a1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵelementContainerStart(2,11),t.ɵɵelementStart(3,"mat-expansion-panel",12)(4,"mat-expansion-panel-header",13)(5,"mat-panel-title")(6,"div",14),t.ɵɵtext(7),t.ɵɵelementEnd()()(),t.ɵɵtemplate(8,i1,1,3,"ng-template",15),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"button",16),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"delete"),t.ɵɵelementEnd()()()}if(2&e){let e;const i=n.$implicit,a=n.$index,r=n.$count,o=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",i),t.ɵɵadvance(),t.ɵɵproperty("expanded",a===r-1),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",null!==(e=null==i.value?null:i.value.key)&&void 0!==e?e:null==i.value?null:i.value.method," "),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(10,4,o.deleteKeyTitle))}}function r1(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3),t.ɵɵrepeaterCreate(1,a1,13,6,"div",9,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵrepeater(e.keysListFormArray.controls)}}function o1(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"span",18),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class s1 extends q{constructor(e,t,n){super(n),this.fb=e,this.popover=t,this.store=n,this.withReportStrategy=!0,this.keysDataApplied=p(),this.ReportStrategyDefaultValue=on}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}addKey(){this.keysListFormArray.push(this.fb.control({}))}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){this.keysDataApplied.emit(this.keysListFormArray.value)}prepareKeysFormArray(e){const t=[];return e?.forEach((e=>{t.push(this.fb.control(e))})),this.fb.array(t)}static{this.ɵfac=function(e){return new(e||s1)(t.ɵɵdirectiveInject(de.UntypedFormBuilder),t.ɵɵdirectiveInject(at.TbPopoverComponent),t.ɵɵdirectiveInject(Xe.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:s1,selectors:[["tb-bacnet-device-data-keys-panel"]],inputs:{panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keys:"keys",keysType:"keysType",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:18,vars:15,consts:[[1,"tb-device-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],[1,"tb-form-panel","no-border","no-padding","key-panel"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"title-container"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[3,"formControl","keyType","withReportStrategy"],["translate","",1,"tb-prompt"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,r1,3,0,"div",3)(6,o1,3,1,"div",4),t.ɵɵelementStart(7,"div")(8,"button",5),t.ɵɵlistener("click",(function(){return n.addKey()})),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(11,"div",6)(12,"button",7),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"button",8),t.ɵɵlistener("click",(function(){return n.applyKeysData()})),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,7,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")"," "),t.ɵɵadvance(2),t.ɵɵconditional(n.keysListFormArray.controls.length?5:6),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(10,9,n.addKeyTitle)," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,11,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,13,"action.apply")," "))},dependencies:t.ɵɵgetComponentDepsFactory(s1,[se,_,n1]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:500px;overflow:auto}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-small-label[_ngcontent-%COMP%]{font-size:16px;padding-right:0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}'],changeDetection:d.OnPush})}}function l1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function p1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.port-required"))}function c1(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",12)(1,"mat-expansion-panel",35)(2,"mat-expansion-panel-header")(3,"mat-panel-title")(4,"div",36),t.ɵɵtext(5,"gateway.advanced-configuration-settings"),t.ɵɵelementEnd()()(),t.ɵɵelement(6,"tb-string-items-list",37),t.ɵɵpipe(7,"translate"),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd()()),2&e){let e;const n=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(7,2,"gateway.bacnet.alt-responses-address")),t.ɵɵpropertyInterpolate("placeholder",null!=(e=n.deviceFormGroup.get("altResponsesAddresses").value)&&e.length?"":t.ɵɵpipeBind1(8,4,"gateway.address"))}}function d1(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",21),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function u1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.poll-period-required"))}function m1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function h1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function g1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function f1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.method," ")}}class y1 extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.cdr=l,this.destroyRef=p,this.deviceFormGroup=this.fb.group({host:["",[me.required,me.pattern(pn)]],port:["",[me.required]],networkMask:[""],deviceInfo:[],altResponsesAddresses:[{value:[],disabled:this.data.hideNewFields}],pollPeriod:[1e4,[me.required,me.min(0)]],timeseries:[[]],attributes:[[]],attributeUpdates:[[]],serverSideRpc:[[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),this.keysPopupClosed=!0,this.BacnetDeviceKeysType=Ta,this.DeviceInfoType=Sa,this.deviceHelpLink=O+"/docs/iot-gateway/config/bacnet/#device-object-settings",this.sourceTypes=Object.values(yi),this.ConnectorType=dt,this.ReportStrategyDefaultValue=on,this.deviceFormGroup.patchValue(this.data.value,{emitEvent:!1})}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.deviceFormGroup.valid){const{altResponsesAddresses:e,reportStrategy:t,...n}=this.deviceFormGroup.value;this.dialogRef.close({altResponsesAddresses:e??[],...t?{reportStrategy:t}:{},...n})}}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))return void this.popoverService.hidePopover(i);const a=this.deviceFormGroup.get(n),r={keys:a.value,keysType:n,panelTitle:Ia.get(n),addKeyTitle:Ea.get(n),deleteKeyTitle:Ma.get(n),noKeysText:ka.get(n),withReportStrategy:this.data.withReportStrategy};this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,s1,"leftBottom",!1,null,r,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.subscribe((e=>{this.popoverComponent.hide(),a.patchValue(e),a.markAsDirty(),this.cdr.markForCheck()})),this.popoverComponent.tbHideStart.pipe(In(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}static{this.ɵfac=function(e){return new(e||y1)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(rt.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:y1,selectors:[["tb-bacnet-device-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:106,vars:48,consts:[["attributesButton",""],["socketTelemetryButton",""],["attributesUpdatesButton",""],["rpcMethodsButton",""],[1,"dialog-mapping",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","name","value","formControlName","port",3,"placeholder"],["translate","",1,"fixed-title-width"],["matInput","","name","value","formControlName","networkMask",3,"placeholder"],["formControlName","deviceInfo","required","true",3,"deviceInfoType","sourceTypes","connectorType"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],[1,"fixed-title-width","tb-required"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","0","name","value","formControlName","pollPeriod",3,"placeholder"],[1,"tb-form-row","space-between","tb-flex"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[1,"tb-settings","chips-panel","w-full"],["translate","",1,"tb-form-panel-title"],["editable","","floatLabel","always","formControlName","altResponsesAddresses",1,"chips-list",3,"label","placeholder"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",4)(1,"mat-toolbar",5)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",6)(6,"div",7),t.ɵɵelementStart(7,"button",8),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",9),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",10)(11,"div",11)(12,"div",12)(13,"div",13),t.ɵɵtext(14,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-form-field",14),t.ɵɵelement(16,"input",15),t.ɵɵpipe(17,"translate"),t.ɵɵtemplate(18,l1,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(19,"div",12)(20,"div",13),t.ɵɵtext(21,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",14),t.ɵɵelement(23,"input",17),t.ɵɵpipe(24,"translate"),t.ɵɵtemplate(25,p1,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"div",12)(27,"div",18),t.ɵɵtext(28,"gateway.network-mask"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"mat-form-field",14),t.ɵɵelement(30,"input",19),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(32,c1,9,6,"div",12),t.ɵɵelement(33,"tb-device-info-table",20),t.ɵɵtemplate(34,d1,1,2,"tb-report-strategy",21),t.ɵɵelementStart(35,"div",12)(36,"div",22)(37,"span",23),t.ɵɵtext(38,"gateway.poll-period"),t.ɵɵelementEnd()(),t.ɵɵelementStart(39,"mat-form-field",14),t.ɵɵelement(40,"input",24),t.ɵɵpipe(41,"translate"),t.ɵɵtemplate(42,u1,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(43,"div",25)(44,"div",18),t.ɵɵtext(45,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(46,"div",26)(47,"mat-chip-listbox",27),t.ɵɵrepeaterCreate(48,m1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(50,"mat-chip",28),t.ɵɵelement(51,"label",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(52,"button",30,0),t.ɵɵpipe(54,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(53);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.ATTRIBUTES))})),t.ɵɵelementStart(55,"tb-icon",31),t.ɵɵtext(56,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(57,"div",25)(58,"div",18),t.ɵɵtext(59,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(60,"div",26)(61,"mat-chip-listbox",27),t.ɵɵrepeaterCreate(62,h1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(64,"mat-chip",28),t.ɵɵelement(65,"label",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(66,"button",30,1),t.ɵɵpipe(68,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(67);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.TIMESERIES))})),t.ɵɵelementStart(69,"tb-icon",31),t.ɵɵtext(70,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(71,"div",25)(72,"div",18),t.ɵɵtext(73,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(74,"div",26)(75,"mat-chip-listbox",27),t.ɵɵrepeaterCreate(76,g1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(78,"mat-chip",28),t.ɵɵelement(79,"label",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(80,"button",30,2),t.ɵɵpipe(82,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(81);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.ATTRIBUTES_UPDATES))})),t.ɵɵelementStart(83,"tb-icon",31),t.ɵɵtext(84,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(85,"div",25)(86,"div",18),t.ɵɵtext(87,"gateway.rpc-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(88,"div",26)(89,"mat-chip-listbox",27),t.ɵɵrepeaterCreate(90,f1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(92,"mat-chip",28),t.ɵɵelement(93,"label",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(94,"button",30,3),t.ɵɵpipe(96,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(95);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.RPC_METHODS))})),t.ɵɵelementStart(97,"tb-icon",31),t.ɵɵtext(98,"edit"),t.ɵɵelementEnd()()()()()(),t.ɵɵelementStart(99,"div",32)(100,"button",33),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(101),t.ɵɵpipe(102,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(103,"button",34),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(104),t.ɵɵpipe(105,"translate"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵproperty("formGroup",n.deviceFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,26,"gateway.device")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.deviceHelpLink),t.ɵɵadvance(10),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(17,28,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.deviceFormGroup.get("host").hasError("required")&&n.deviceFormGroup.get("host").touched?18:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(24,30,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.deviceFormGroup.get("port").hasError("required")&&n.deviceFormGroup.get("port").touched?25:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(31,32,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.data.hideNewFields?-1:32),t.ɵɵadvance(),t.ɵɵproperty("deviceInfoType",n.DeviceInfoType.FULL)("sourceTypes",n.sourceTypes)("connectorType",n.ConnectorType.BACNET),t.ɵɵadvance(),t.ɵɵconditional(n.data.withReportStrategy?34:-1),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(41,34,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.deviceFormGroup.get("pollPeriod").hasError("required")&&n.deviceFormGroup.get("pollPeriod").touched?42:-1),t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(54,36,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("timeseries").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("timeseries").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(68,38,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(82,40,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(96,42,"action.edit")),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(102,44,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.deviceFormGroup.invalid||!n.deviceFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(105,46,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(y1,[se,_,$n,Wn,A$,ni]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:grid;height:100%}[_nghost-%COMP%] .tb-form-panel[_ngcontent-%COMP%]{width:77vw;max-width:800px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%]{max-width:900px;display:flex;flex-direction:column}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{min-height:64px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] tb-toggle-select[_ngcontent-%COMP%]{padding:4px 0}[_nghost-%COMP%] .mat-mdc-dialog-content[_ngcontent-%COMP%]{height:670px}[_nghost-%COMP%] .ellipsis-chips-container[_ngcontent-%COMP%]{max-width:70%}[_nghost-%COMP%] .chips-panel[_ngcontent-%COMP%]{padding:6px 6px 6px 0}[_nghost-%COMP%] .dialog-mapping .mat-mdc-chip-listbox .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center}[_nghost-%COMP%] .tb-form-row .fixed-title-width{min-width:40px;width:35%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center}[_nghost-%COMP%] .tb-form-row .mat-mdc-form-field{width:0}[_nghost-%COMP%] .tb-form-row .chips-list .mat-mdc-form-field{width:100%}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}[_nghost-%COMP%] .device-config{gap:12px;padding-left:10px;padding-right:10px}[_nghost-%COMP%] .device-node-pattern-field{flex-basis:3%}'],changeDetection:d.OnPush})}}const v1=()=>["deviceName","host","port","actions"],x1=()=>({minWidth:"96px",textAlign:"center"});function b1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",26)(2,"span",27),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",9),t.ɵɵelementStart(6,"button",11),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageDevices(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",11),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.devices")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search")))}function w1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.device-name")))}function S1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(null==e.deviceInfo?null:e.deviceInfo.deviceNameExpression)}}function C1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.host")," "))}function _1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.host)}}function T1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.port")," "))}function I1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.port)}}function E1(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",30)}function M1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",11),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageDevices(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",11),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteDevice(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function k1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,M1,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",31),t.ɵɵelementContainer(4,32),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",33)(6,"button",34),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",35),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",36,2),t.ɵɵelementContainer(11,32),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,x1)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function P1(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",37)}function D1(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class O1 extends $a{constructor(){super(...arguments),this.hideNewFields=!1}getDatasource(){return new A1}manageDevices(e,t){e&&e.stopPropagation();const n=J(t),i=n?this.entityFormArray.at(t).value:{};this.getDeviceDialog(i,n?"action.apply":"action.add").afterClosed().pipe(Ne(1),In(this.destroyRef)).subscribe((e=>{e&&(n?this.entityFormArray.at(t).patchValue(e):this.entityFormArray.push(this.fb.control(e)),this.entityFormArray.markAsDirty())}))}deleteDevice(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-device-title",{name:this.entityFormArray.controls[t].value.deviceInfo?.deviceNameExpression}),this.translate.instant("gateway.delete-device-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(Ne(1),In(this.destroyRef)).subscribe((e=>{e&&(this.entityFormArray.removeAt(t),this.entityFormArray.markAsDirty())}))}getDeviceDialog(e,t){return this.dialog.open(y1,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,withReportStrategy:this.withReportStrategy,hideNewFields:this.hideNewFields}})}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())||e.deviceNameExpression?.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(O1)))(n||O1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:O1,selectors:[["tb-bacnet-devices-config-table"]],inputs:{hideNewFields:[2,"hideNewFields","hideNewFields",m]},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>O1)),multi:!0},{provide:he,useExisting:c((()=>O1)),multi:!0}]),t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:48,vars:37,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-device-table","tb-absolute-fill"],[1,"tb-device-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","table-value-column",4,"matHeaderCellDef"],["class","table-value-column",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-device-table-title"],[1,"table-value-column"],["tbTruncateWithTooltip",""],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,b1,14,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"div",7)(7,"button",8),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-icon"),t.ɵɵtext(10,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",9)(12,"mat-label"),t.ɵɵtext(13," "),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",10,0),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵpipe(18,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(19,"mat-icon"),t.ɵɵtext(20,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(21,"div",12)(22,"table",13),t.ɵɵelementContainerStart(23,14),t.ɵɵtemplate(24,w1,4,3,"mat-header-cell",15)(25,S1,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(26,14),t.ɵɵtemplate(27,C1,3,3,"mat-header-cell",15)(28,_1,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(29,14),t.ɵɵtemplate(30,T1,3,3,"mat-header-cell",15)(31,I1,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(32,17),t.ɵɵtemplate(33,E1,1,0,"mat-header-cell",18)(34,k1,12,6,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(35,P1,1,0,"mat-header-row",20)(36,D1,1,0,"mat-row",21),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"section",22),t.ɵɵpipe(38,"async"),t.ɵɵelementStart(39,"button",23),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageDevices(i))})),t.ɵɵelementStart(40,"mat-icon",24),t.ɵɵtext(41,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"span"),t.ɵɵtext(43),t.ɵɵpipe(44,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(45,"span",25),t.ɵɵpipe(46,"async"),t.ɵɵtext(47," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(4,21,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,23,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,25,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,27,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","deviceName"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","host"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","port"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(35,v1))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(36,v1)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(38,29,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(44,31,"gateway.add-device")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(46,33,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(O1,[se,_,Wn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%]{width:100%;height:100%;background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:21%}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}class A1 extends G{constructor(){super()}}class F1 extends Ua{initBasicFormGroup(){return this.fb.group({application:[],devices:[]})}mapConfigToFormValue(e){return{application:e.application??{},devices:e.devices??[]}}getMappedValue(e){return{application:e.application,devices:e.devices??[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(F1)))(n||F1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:F1,selectors:[["tb-bacnet-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>F1)),multi:!0},{provide:he,useExisting:c((()=>F1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:15,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","application"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","devices",3,"hideNewFields","withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-bacnet-application-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-bacnet-devices-config-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.application"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.devices"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("hideNewFields",n.isLegacy)("withReportStrategy",n.withReportStrategy))},dependencies:t.ɵɵgetComponentDepsFactory(F1,[se,_,jJ,O1]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}class R1 extends Ua{constructor(){super(...arguments),this.isLegacy=!0}initBasicFormGroup(){return this.fb.group({application:[],devices:[]})}mapConfigToFormValue(e){return{application:e.general?Ja.mapApplicationToUpgradedVersion(e.general):{},devices:e.devices?.length?Ja.mapDevicesToUpgradedVersion(e.devices):[]}}getMappedValue(e){return{general:e.application?Ja.mapApplicationToDowngradedVersion(e.application):{},devices:e.devices?.length?Ja.mapDevicesToDowngradedVersion(e.devices):[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(R1)))(n||R1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:R1,selectors:[["tb-bacnet-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>R1)),multi:!0},{provide:he,useExisting:c((()=>R1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:15,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","application"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","devices",3,"hideNewFields","withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-bacnet-application-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-bacnet-devices-config-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.application"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.devices"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("hideNewFields",n.isLegacy)("withReportStrategy",n.withReportStrategy))},dependencies:t.ɵɵgetComponentDepsFactory(R1,[se,_,jJ,O1]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}function B1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",5),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function N1(e,n){if(1&e&&(t.ɵɵelement(0,"tb-error-icon",8),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵpipe(2,"translate")),2&e){t.ɵɵnextContext();const e=t.ɵɵreadContextLet(16);t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(2,3,t.ɵɵpipeBind1(1,1,e)))}}class L1 extends za{constructor(){super(...arguments),this.portLimits=Oi}get serverConfigFormGroup(){return this.formGroup}initFormGroup(){return this.fb.group({host:["",[me.required,me.pattern(pn)]],port:[null,[me.required,me.min(Oi.MIN),me.max(Oi.MAX)]],SSL:[!1],security:this.fb.group({cert:[""],key:[""]})})}mapOnChangeValue(e){return Y(e),e}onWriteValue(e){this.formGroup.reset(e,{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(L1)))(n||L1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:L1,selectors:[["tb-rest-server-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>L1)),multi:!0},{provide:he,useExisting:c((()=>L1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:46,vars:43,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[1,"tb-form-row","column-xs"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matSuffix","",3,"tooltipText"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["formControlName","SSL",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],[3,"formGroup"],["tbTruncateWithTooltip","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","cert",3,"placeholder"],["matInput","","name","value","formControlName","key",3,"placeholder"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",3),t.ɵɵelement(6,"input",4),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,B1,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",6)(10,"div",2),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",3),t.ɵɵelement(14,"input",7),t.ɵɵpipe(15,"translate"),t.ɵɵdeclareLet(16),t.ɵɵtemplate(17,N1,3,5,"tb-error-icon",8),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",9)(19,"div",10),t.ɵɵtext(20,"gateway.security"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",11)(22,"mat-slide-toggle",12)(23,"mat-label",13),t.ɵɵpipe(24,"translate"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerStart(27,14),t.ɵɵelementStart(28,"div",11)(29,"div",15),t.ɵɵpipe(30,"translate"),t.ɵɵtext(31),t.ɵɵpipe(32,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"div",16)(34,"mat-form-field",3),t.ɵɵelement(35,"input",17),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(37,"div",11)(38,"div",15),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"div",16)(43,"mat-form-field",3),t.ɵɵelement(44,"input",18),t.ɵɵpipe(45,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()),2&e){t.ɵɵproperty("formGroup",n.serverConfigFormGroup),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,18,"gateway.hints.rest.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.serverConfigFormGroup.get("host").hasError("required")&&n.serverConfigFormGroup.get("host").touched?8:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,22,"gateway.hints.rest.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,24,"gateway.set")),t.ɵɵadvance(2);const e=t.ɵɵstoreLet(n.serverConfigFormGroup.get("port"));t.ɵɵadvance(),t.ɵɵconditional((e.hasError("required")||e.hasError("min")||e.hasError("max"))&&e.touched?17:-1),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(24,27,"gateway.hints.rest.ssl-verify")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(26,29,"gateway.rest.ssl-verify")," "),t.ɵɵadvance(2),t.ɵɵproperty("formGroup",n.serverConfigFormGroup.get("security")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(30,31,"gateway.hints.rest.cert")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(32,33,"gateway.certificate")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,35,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(39,37,"gateway.hints.rest.key")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,39,"gateway.key")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(45,41,"gateway.set"))}},dependencies:t.ɵɵgetComponentDepsFactory(L1,[se,_,u$,Wn,ai]),encapsulation:2,changeDetection:d.OnPush})}}const V1=()=>({min:1});function q1(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",4),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.ResponseTypeTranslationsMap.get(e)))}}function G1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",4),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function z1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",4),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function U1(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,7),t.ɵɵelementStart(1,"div",8)(2,"div",9),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",10)(6,"mat-form-field",11)(7,"mat-select",12),t.ɵɵrepeaterCreate(8,G1,2,2,"mat-option",4,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(10,"div",8)(11,"div",9),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",10)(15,"mat-form-field",11)(16,"mat-select",13),t.ɵɵrepeaterCreate(17,z1,2,2,"mat-option",4,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("formGroup",e.responseConfigFormGroup.get(e.ResponseType.CONST)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rest.on-success")),t.ɵɵadvance(5),t.ɵɵrepeater(e.responseStatuses),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,5,"gateway.rest.on-error")),t.ɵɵadvance(5),t.ɵɵrepeater(e.responseStatuses)}}function j1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.response-timeout-required"))}function H1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,V1)))}function W1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function $1(e,n){1&e&&(t.ɵɵelementStart(0,"span",18),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function K1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.response-attribute-required"))}function Y1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.response-attribute-pattern"))}function X1(e,n){if(1&e&&(t.ɵɵdeclareLet(0),t.ɵɵelementContainerStart(1,7),t.ɵɵelementStart(2,"div",8)(3,"mat-slide-toggle",14)(4,"mat-label"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(7,"div",8)(8,"div",15),t.ɵɵtext(9,"gateway.timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-form-field",11),t.ɵɵelement(11,"input",16),t.ɵɵpipe(12,"translate"),t.ɵɵdeclareLet(13),t.ɵɵtemplate(14,j1,2,3,"tb-error-icon",17)(15,H1,2,5,"tb-error-icon",17)(16,W1,2,3,"tb-error-icon",17)(17,$1,2,0,"span",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",8)(19,"div",15),t.ɵɵtext(20,"gateway.rest.response-attribute"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",11),t.ɵɵelement(22,"input",19),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,K1,2,3,"tb-error-icon",17)(25,Y1,2,3,"tb-error-icon",17),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(),n=e.responseConfigFormGroup.get(e.ResponseType.ADVANCED);t.ɵɵadvance(),t.ɵɵproperty("formGroup",n),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,6,"gateway.rest.response-expected")," "),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(12,8,"gateway.set"));const i=n.get("timeout").touched;t.ɵɵadvance(3),t.ɵɵconditional(n.get("timeout").hasError("required")&&i?14:n.get("timeout").hasError("min")&&i?15:n.get("timeout").hasError("pattern")&&i?16:17),t.ɵɵadvance(8),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,10,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.get("responseAttribute").hasError("required")&&n.get("responseAttribute").touched?24:n.get("responseAttribute").hasError("pattern")&&n.get("responseAttribute").touched?25:-1)}}class Z1 extends za{get responseConfigFormGroup(){return this.formGroup}constructor(){super(),this.ResponseTypeTranslationsMap=Ti,this.ResponseType=Ci,this.responseTypes=Object.values(Ci),this.responseStatuses=Object.values(_i),this.observeIsExpected()}initFormGroup(){return this.fb.group({type:[Ci.DEFAULT],[Ci.CONST]:this.fb.group({successResponse:[_i.OK],unsuccessfulResponse:[_i.ERROR]}),[Ci.ADVANCED]:this.fb.group({responseExpected:[!0],timeout:[null,[me.required,me.min(.001),me.pattern(ln)]],responseAttribute:["",[me.required,me.pattern(pn)]]})})}mapOnChangeValue({type:e,...t}){return{type:e,...t[e]??{}}}onWriteValue(e){const{type:t=Ci.DEFAULT,...n}=e??{};this.toggleIsExpected(n.responseExpected,t),this.responseConfigFormGroup.patchValue({type:t,[t]:n},{emitEvent:!1})}toggleIsExpected(e,t){const n=e&&t===Ci.ADVANCED;this.responseConfigFormGroup.get(Ci.ADVANCED).get("timeout")[n?"enable":"disable"]({emitEvent:!1}),this.responseConfigFormGroup.get(Ci.ADVANCED).get("responseAttribute")[n?"enable":"disable"]({emitEvent:!1})}observeIsExpected(){Ee(this.responseConfigFormGroup.get("type").valueChanges,this.responseConfigFormGroup.get(Ci.ADVANCED).get("responseExpected").valueChanges).pipe(In()).subscribe((()=>this.toggleIsExpected(this.responseConfigFormGroup.get(Ci.ADVANCED).get("responseExpected").value,this.responseConfigFormGroup.get("type").value)))}static{this.ɵfac=function(e){return new(e||Z1)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Z1,selectors:[["tb-rest-response-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>Z1)),multi:!0},{provide:he,useExisting:c((()=>Z1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:7,consts:[[1,"tb-form-row","space-between","same-padding","tb-flex","column","size-full",3,"formGroup"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],[1,"fixed-title-width","tb-required"],["formControlName","type","appearance","fill"],[3,"value"],[3,"ngSwitch"],[3,"ngSwitchCase"],[3,"formGroup"],[1,"tb-form-row"],[1,"fixed-title-width"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","successResponse"],["formControlName","unsuccessfulResponse"],["formControlName","responseExpected",1,"mat-slide"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["matSuffix","",3,"tooltipText"],["translate","","matSuffix","",1,"block","pr-2"],["matInput","","type","text","name","value","formControlName","responseAttribute",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-select",3),t.ɵɵrepeaterCreate(6,q1,3,4,"tb-toggle-option",4,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()(),t.ɵɵelementContainerStart(8,5),t.ɵɵtemplate(9,U1,19,7,"ng-template",6)(10,X1,26,12,"ng-template",6),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.responseConfigFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,5,"gateway.response")),t.ɵɵadvance(3),t.ɵɵrepeater(n.responseTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",n.responseConfigFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.ResponseType.CONST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.ResponseType.ADVANCED))},dependencies:t.ɵɵgetComponentDepsFactory(Z1,[se,_,ai]),encapsulation:2,changeDetection:d.OnPush})}}function Q1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",15),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.endpoint-required"))}function J1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",17),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function e0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",15),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.http-methods-required"))}function t0(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",17),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.RestConvertorTypeTranslationsMap.get(e)))}}function n0(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",27),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function i0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",40),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function a0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",40),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function r0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",28)(1,"div",20)(2,"div",21),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",33)(6,"mat-chip-listbox",34),t.ɵɵrepeaterCreate(7,i0,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(9,"mat-chip",35),t.ɵɵelement(10,"label",36),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"button",37,0),t.ɵɵpipe(13,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(12),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES))})),t.ɵɵelementStart(14,"tb-icon",38),t.ɵɵtext(15,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(16,"div",20)(17,"div",39),t.ɵɵtext(18,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",33)(20,"mat-chip-listbox",34),t.ɵɵrepeaterCreate(21,a0,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(23,"mat-chip",35),t.ɵɵelement(24,"label",36),t.ɵɵelementEnd()(),t.ɵɵelementStart(25,"button",37,1),t.ɵɵpipe(27,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(26),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.TIMESERIES))})),t.ɵɵelementStart(28,"tb-icon",38),t.ɵɵtext(29,"edit"),t.ɵɵelementEnd()()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,5,"gateway.attributes")),t.ɵɵadvance(3),t.ɵɵproperty("tbEllipsisChipList",e.converterAttributes),t.ɵɵadvance(),t.ɵɵrepeater(e.converterAttributes),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(13,7,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.converterTelemetry),t.ɵɵadvance(),t.ɵɵrepeater(e.converterTelemetry),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(27,9,"action.edit"))}}function o0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",15),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.extension-required"))}function s0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",40),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function l0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",28)(1,"div",11)(2,"div",12),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",41),t.ɵɵelement(7,"input",42),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,o0,2,3,"tb-error-icon",15),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",43)(11,"div",24),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",25),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"div",20)(18,"div",21),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",44)(22,"mat-chip-listbox",34),t.ɵɵtemplate(23,s0,3,1,"mat-chip",45),t.ɵɵelementStart(24,"mat-chip",35),t.ɵɵelement(25,"label",36),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"button",37,2),t.ɵɵpipe(28,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(27),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.CUSTOM))})),t.ɵɵelementStart(29,"tb-icon",38),t.ɵɵtext(30,"edit"),t.ɵɵelementEnd()()()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.rest.extension")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,12,"gateway.extension")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,14,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("converter.extension").hasError("required")&&e.mappingFormGroup.get("converter.extension").touched?9:-1),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,16,"gateway.extension-configuration")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(16,18,"gateway.extension-configuration-hint")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,20,"gateway.keys")),t.ɵɵadvance(3),t.ɵɵproperty("tbEllipsisChipList",e.customKeys),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.customKeys),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(28,22,"action.edit"))}}class p0 extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.destroyRef=l,this.cd=p,this.mappingFormGroup=this.fb.group({endpoint:["",[me.required,me.pattern(pn)]],HTTPMethods:[[gt.POST],[me.required]],security:[{type:Ri.ANONYMOUS}],converter:this.fb.group({type:[wi.JSON,[]],deviceInfo:[],reportStrategy:[],attributes:[[]],timeseries:[[]],extension:["",[me.required,me.pattern(pn)]],extensionConfig:[]}),response:[]}),this.keysPopupClosed=!0,this.helpLink=`${O}/docs/iot-gateway/config/rest/#mapping-section`,this.httpMethods=Object.values(gt),this.converterTypes=Object.values(wi),this.restSourceTypes=Object.values(Si),this.SecurityMode=Ji,this.MappingKeysType=Ui,this.DeviceInfoType=Sa,this.RestConvertorTypeTranslationsMap=Pi,this.RestDataConversionTranslationsMap=Di,this.RestConverterType=wi,this.ConnectorType=dt,this.ReportStrategyDefaultValue=on,this.setInitialFormValue(),this.observeConverterTypeChange(),this.toggleConverterFieldsByType(this.converterType)}get converterType(){return this.mappingFormGroup.get("converter")?.get("type").value}get converterAttributes(){if(this.converterType)return this.mappingFormGroup.get("converter").value.attributes.map((e=>e.key))}get converterTelemetry(){if(this.converterType)return this.mappingFormGroup.get("converter").value.timeseries.map((e=>e.key))}get customKeys(){return Object.keys(this.mappingFormGroup.get("converter").value.extensionConfig??{})}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.mappingFormGroup.valid){const{converter:e,...t}=this.mappingFormGroup.value,{reportStrategy:n,...i}=e,a={...t,reportStrategy:n,converter:i};Y(a),this.dialogRef.close(a)}}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))this.popoverService.hidePopover(i);else{const e=this.mappingFormGroup.get("converter").get(n),t={keys:e.value,keysType:n,panelTitle:ji.get(n),addKeyTitle:Hi.get(n),deleteKeyTitle:Wi.get(n),noKeysText:$i.get(n),withReportStrategy:this.data.withReportStrategy,convertorType:this.converterType,connectorType:dt.REST};this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,_K,"leftBottom",!1,null,t,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(In(this.destroyRef)).subscribe((t=>{this.popoverComponent.hide(),e.patchValue(t),e.markAsDirty(),this.cd.markForCheck()})),this.popoverComponent.tbHideStart.pipe(In(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}}setInitialFormValue(){const{converter:e,reportStrategy:t,...n}=this.data.value;this.mappingFormGroup.patchValue({...n,converter:{...e,...t?{reportStrategy:t}:{}}},{emitEvent:!1})}observeConverterTypeChange(){this.mappingFormGroup.get("converter").get("type").valueChanges.pipe(In(this.destroyRef)).subscribe((e=>this.toggleConverterFieldsByType(e)))}toggleConverterFieldsByType(e){const t=e===wi.JSON;this.mappingFormGroup.get("converter").get("attributes")[t?"enable":"disable"]({emitEvent:!1}),this.mappingFormGroup.get("converter").get("timeseries")[t?"enable":"disable"]({emitEvent:!1}),this.mappingFormGroup.get("converter").get("extension")[t?"disable":"enable"]({emitEvent:!1}),this.mappingFormGroup.get("converter").get("extensionConfig")[t?"disable":"enable"]({emitEvent:!1})}static{this.ɵfac=function(e){return new(e||p0)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(rt.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:p0,selectors:[["tb-rest-mapping-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:59,vars:45,consts:[["attributesButton",""],["telemetryButton",""],["keysButton",""],[1,"h-full","w-[77vw]","max-w-3xl",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","endpoint",3,"placeholder"],["matSuffix","",3,"tooltipText"],["formControlName","HTTPMethods","multiple",""],[3,"value"],["formControlName","security",3,"mode"],["formGroupName","converter"],[1,"tb-form-row","space-between","tb-flex"],[1,"fixed-title-width"],["formControlName","type","appearance","fill"],[1,"tb-form-panel","stroked"],[1,"tb-form-panel-title"],[1,"tb-form-hint","tb-primary-fill"],["formControlName","deviceInfo","required","true",1,"device-info",3,"sourceTypes","convertorType","deviceInfoType","connectorType"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],[1,"tb-form-panel","no-border","no-padding","w-full"],["formControlName","response"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[1,"tb-flex","max-w-70%"],[1,"tb-flex","gw-chip-list",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["translate","",1,"fixed-title-width"],["tbTruncateWithTooltip",""],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex"],["matInput","","name","value","formControlName","extension",3,"placeholder"],[1,"tb-form-row","space-between","same-padding","tb-flex","column"],[1,"tb-flex","ellipsis-chips-container"],[4,"ngFor","ngForOf"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",3)(1,"mat-toolbar",4)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",5)(6,"div",6),t.ɵɵelementStart(7,"button",7),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",8),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",9)(11,"div",10)(12,"div",11)(13,"div",12),t.ɵɵpipe(14,"translate"),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"mat-form-field",13),t.ɵɵelement(18,"input",14),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,Q1,2,3,"tb-error-icon",15),t.ɵɵelementEnd()(),t.ɵɵelementStart(21,"div",11)(22,"div",12),t.ɵɵpipe(23,"translate"),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",13)(27,"mat-select",16),t.ɵɵrepeaterCreate(28,J1,2,2,"mat-option",17,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd(),t.ɵɵtemplate(30,e0,2,3,"tb-error-icon",15),t.ɵɵelementEnd()(),t.ɵɵelement(31,"tb-security-config",18),t.ɵɵelementContainerStart(32,19),t.ɵɵelementStart(33,"div",20)(34,"div",21),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"tb-toggle-select",22),t.ɵɵrepeaterCreate(38,t0,3,4,"tb-toggle-option",17,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()(),t.ɵɵelementStart(40,"div",23)(41,"div",24),t.ɵɵtext(42),t.ɵɵpipe(43,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"div",25),t.ɵɵtext(45),t.ɵɵpipe(46,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(47,"tb-device-info-table",26),t.ɵɵtemplate(48,n0,1,2,"tb-report-strategy",27)(49,r0,30,11,"div",28)(50,l0,31,24,"div",28),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelement(51,"tb-rest-response-config",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(52,"div",30)(53,"button",31),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(54),t.ɵɵpipe(55,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(56,"button",32),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(57),t.ɵɵpipe(58,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,23,"gateway.data-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.helpLink),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(14,25,"gateway.hints.rest.endpoint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(16,27,"gateway.rest.endpoint")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,29,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.mappingFormGroup.get("endpoint").hasError("required")&&n.mappingFormGroup.get("endpoint").touched?20:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(23,31,"gateway.hints.rest.http-methods")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(25,33,"gateway.rest.http-methods")),t.ɵɵadvance(4),t.ɵɵrepeater(n.httpMethods),t.ɵɵadvance(2),t.ɵɵconditional(n.mappingFormGroup.get("HTTPMethods").hasError("required")&&n.mappingFormGroup.get("HTTPMethods").touched?30:-1),t.ɵɵadvance(),t.ɵɵproperty("mode",n.SecurityMode.basic),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,35,"gateway.payload-type")),t.ɵɵadvance(3),t.ɵɵrepeater(n.converterTypes),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(43,37,"gateway.data-conversion")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(46,39,n.RestDataConversionTranslationsMap.get(n.converterType))," "),t.ɵɵadvance(2),t.ɵɵproperty("sourceTypes",n.restSourceTypes)("convertorType",n.RestConverterType.JSON)("deviceInfoType",n.DeviceInfoType.FULL)("connectorType",n.ConnectorType.REST),t.ɵɵadvance(),t.ɵɵconditional(n.data.withReportStrategy?48:-1),t.ɵɵadvance(),t.ɵɵconditional(n.converterType===n.RestConverterType.JSON?49:50),t.ɵɵadvance(5),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(55,41,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.mappingFormGroup.invalid||!n.mappingFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(58,43,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(p0,[se,_,$n,A$,UY,Z1,ai,ni]),styles:['@charset "UTF-8";[_nghost-%COMP%] .device-info .fixed-title-width{min-width:0}'],changeDetection:d.OnPush})}}const c0=()=>["endpoint","httpMethods","securityType","actions"];function d0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",6)(1,"div",29),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"span",8),t.ɵɵelementStart(5,"button",10),t.ɵɵpipe(6,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageMapping(n))})),t.ɵɵelementStart(7,"mat-icon"),t.ɵɵtext(8,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",10),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,3,"gateway.data-mapping")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(6,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,7,"action.search")))}function u0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rest.endpoint")))}function m0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",32)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.endpoint)}}function h0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",33),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rest.http-methods")," "))}function g0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",38),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function f0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",34)(1,"mat-chip-listbox",35),t.ɵɵrepeaterCreate(2,g0,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(4,"mat-chip",36),t.ɵɵelement(5,"label",37),t.ɵɵelementEnd()()()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵproperty("tbEllipsisChipList",e.HTTPMethods),t.ɵɵadvance(),t.ɵɵrepeater(e.HTTPMethods)}}function y0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.security-type")," "))}function v0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",39)(1,"div",31),t.ɵɵtext(2),t.ɵɵpipe(3,"titlecase"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,null==e.security?null:e.security.type))}}function x0(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",40)}function b0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",10),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageMapping(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",10),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteMapping(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function w0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,b0,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",41),t.ɵɵelementContainer(4,42),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",43)(6,"button",44),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",45),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",46,2),t.ɵɵelementContainer(11,42),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(4),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function S0(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",47)}function C0(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class _0 extends $a{getDatasource(){return new T0}manageMapping(e,t){e&&e.stopPropagation();const n=J(t),i=n?this.entityFormArray.at(t).value:{};this.getMappingDialog(i,n?"action.apply":"action.add").afterClosed().pipe(In(this.destroyRef)).subscribe((e=>{e&&(n?this.entityFormArray.at(t).patchValue(e):this.entityFormArray.push(this.fb.control(e)),this.entityFormArray.markAsDirty())}))}deleteMapping(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-mapping-title",{name:this.entityFormArray.at(t).value.endpoint}),this.translate.instant("gateway.delete-mapping-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(In(this.destroyRef)).subscribe((e=>{e&&(this.entityFormArray.removeAt(t),this.entityFormArray.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())||e.type?.toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}getMappingDialog(e,t){return this.dialog.open(p0,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,withReportStrategy:this.withReportStrategy}})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(_0)))(n||_0)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:_0,selectors:[["tb-rest-mapping-table"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>_0)),multi:!0},{provide:he,useExisting:c((()=>_0)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:47,vars:37,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-absolute-fill","size-full"],[1,"flex","size-full","flex-col"],[1,"gw-table-toolbar","mat-mdc-table-toolbar"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"gw-connector-table"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","!w-1/5",4,"matHeaderCellDef"],["class","w-1/5",4,"matCellDef"],["class","w-1/3 !pl-0",4,"matHeaderCellDef"],["class","w-1/3",4,"matCellDef"],["class","w-1/6",4,"matHeaderCellDef"],["class","w-1/6",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"!w-1/5"],["tbTruncateWithTooltip",""],[1,"w-1/5"],[1,"w-1/3","!pl-0"],[1,"w-1/3"],[1,"tb-flex",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text","http-method-label","font-medium"],["tbTruncateWithTooltip","",1,"http-method-label","font-medium"],[1,"w-1/6"],[1,"w-12"],[1,"lt-lg:!hidden","flex","min-w-24","flex-1","flex-row","items-stretch","justify-end","text-center"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,d0,13,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"button",7),t.ɵɵpipe(7,"translate"),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"mat-form-field",8)(11,"mat-label"),t.ɵɵtext(12," "),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",9,0),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",10),t.ɵɵpipe(17,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(18,"mat-icon"),t.ɵɵtext(19,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",11)(21,"table",12),t.ɵɵelementContainerStart(22,13),t.ɵɵtemplate(23,u0,4,3,"mat-header-cell",14)(24,m0,3,1,"mat-cell",15),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(25,13),t.ɵɵtemplate(26,h0,3,3,"mat-header-cell",16)(27,f0,6,1,"mat-cell",17),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(28,13),t.ɵɵtemplate(29,y0,3,3,"mat-header-cell",18)(30,v0,4,3,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(31,20),t.ɵɵtemplate(32,x0,1,0,"mat-header-cell",21)(33,w0,12,3,"mat-cell",22),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(34,S0,1,0,"mat-header-row",23)(35,C0,1,0,"mat-row",24),t.ɵɵelementEnd(),t.ɵɵelementStart(36,"section",25),t.ɵɵpipe(37,"async"),t.ɵɵelementStart(38,"button",26),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageMapping(i))})),t.ɵɵelementStart(39,"mat-icon",27),t.ɵɵtext(40,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(41,"span"),t.ɵɵtext(42),t.ɵɵpipe(43,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(44,"span",28),t.ɵɵpipe(45,"async"),t.ɵɵtext(46," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵconditional(!1===t.ɵɵpipeBind1(4,21,n.dataSource.isEmpty())?3:-1),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,23,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,25,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,27,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","endpoint"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","httpMethods"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","securityType"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(35,c0))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(36,c0)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(37,29,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(43,31,"gateway.add-mapping")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(45,33,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(_0,[se,_,Wn,$n]),styles:[".http-method-label[_ngcontent-%COMP%]{font-size:12px}"],changeDetection:d.OnPush})}}let T0=class extends G{constructor(){super()}};function I0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",13),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" : ",e.value.value," ")}}function E0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function M0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function k0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",16)(1,"div",17),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",18)(5,"mat-form-field",19),t.ɵɵelement(6,"input",20),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,E0,3,3,"mat-icon",21),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",16)(10,"div",22),t.ɵɵtext(11,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",19),t.ɵɵelement(13,"input",23),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,M0,3,3,"mat-icon",21),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,5,"gateway.key")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.get("key").hasError("required")&&e.get("key").touched?8:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.get("value").hasError("required")&&e.get("value").touched?15:-1)}}function P0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵelementContainerStart(2,11),t.ɵɵelementStart(3,"mat-expansion-panel",12)(4,"mat-expansion-panel-header")(5,"mat-panel-title")(6,"div",13),t.ɵɵtext(7),t.ɵɵelementEnd(),t.ɵɵtemplate(8,I0,2,1,"div",13),t.ɵɵelementEnd()(),t.ɵɵtemplate(9,k0,16,11,"ng-template",14),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",15),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.$index,a=n.$count;t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i===a-1),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",null==e.value?null:e.value.key," "),t.ɵɵadvance(),t.ɵɵconditional(null!=e.value&&e.value.value?8:-1),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,"gateway.rest.delete-http-header"))}}function D0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3),t.ɵɵrepeaterCreate(1,P0,14,7,"div",9,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵrepeater(e.keysListFormArray.controls)}}function O0(e,n){1&e&&(t.ɵɵelementStart(0,"div",4)(1,"span",24),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rest.no-http-headers")))}class A0 extends q{constructor(e,t){super(t),this.fb=e,this.store=t,this.withReportStrategy=!0,this.keysDataApplied=p(),this.ReportStrategyDefaultValue=on}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}addKey(){this.keysListFormArray.push(this.fb.group({key:["",[me.required,me.pattern(pn)]],value:["",[me.required,me.pattern(pn)]]}))}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){this.keysDataApplied.emit(this.keysListFormArray.value.reduce(((e,{key:t,value:n})=>({...e,[t]:n})),{}))}prepareKeysFormArray(e){const t=[];return Object.keys(e)?.forEach((n=>{t.push(this.fb.group({key:[n,[me.required,me.pattern(pn)]],value:[e[n],[me.required,me.pattern(pn)]]}))})),this.fb.array(t)}static{this.ɵfac=function(e){return new(e||A0)(t.ɵɵdirectiveInject(de.UntypedFormBuilder),t.ɵɵdirectiveInject(Xe.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:A0,selectors:[["tb-rest-http-headers-panel"]],inputs:{keys:"keys",popover:"popover",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:18,vars:15,consts:[[1,"w-[77vw]","max-w-2xl"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],[1,"tb-form-panel","no-border","no-padding","h-[500px]"],[1,"tb-flex","no-flex","center","align-center","h-[500px]"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],["tbTruncateWithTooltip","",1,"max-w-[11vw]"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",1,"gw-delete-icon-button",3,"click","matTooltip"],[1,"tb-form-row"],[1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","type","text","name","value","formControlName","value",3,"placeholder"],[1,"tb-prompt"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,D0,3,0,"div",3)(6,O0,4,3,"div",4),t.ɵɵelementStart(7,"div")(8,"button",5),t.ɵɵlistener("click",(function(){return n.addKey()})),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(11,"div",6)(12,"button",7),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"button",8),t.ɵɵlistener("click",(function(){return n.applyKeysData()})),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,7,"gateway.rest.http-headers"),""," ("+n.keysListFormArray.controls.length+")"," "),t.ɵɵadvance(2),t.ɵɵconditional(n.keysListFormArray.controls.length?5:6),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(10,9,"gateway.rest.add-http-header")," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,11,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,13,"action.apply")," "))},dependencies:t.ɵɵgetComponentDepsFactory(A0,[se,_]),encapsulation:2,changeDetection:d.OnPush})}}const F0=()=>({min:1}),R0=()=>({maxWidth:"970px"});function B0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.RestRequestTypesTranslationsMap.get(e)))}}function N0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.endpoint-required"))}function L0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.endpoint-pattern"))}function V0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function q0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.http-methods-required"))}function G0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,e))}}function z0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.rest.endpoint"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",21),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,N0,2,3,"tb-error-icon",22)(8,L0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",9)(10,"div",19),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"gateway.rest.http-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",20)(14,"mat-select",23),t.ɵɵrepeaterCreate(15,V0,2,2,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd(),t.ɵɵtemplate(17,q0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",9)(19,"div",24),t.ɵɵpipe(20,"translate"),t.ɵɵtext(21,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",20)(23,"mat-select",25),t.ɵɵrepeaterCreate(24,G0,3,4,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,6,"gateway.hints.rest.endpoint")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("endpoint").hasError("required")&&e.mappingFormGroup.get("endpoint").touched?7:e.mappingFormGroup.get("endpoint").hasError("pattern")&&e.mappingFormGroup.get("endpoint").touched?8:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,10,"gateway.hints.rest.http-methods")),t.ɵɵadvance(5),t.ɵɵrepeater(e.httpMethods),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("HTTPMethods").hasError("required")&&e.mappingFormGroup.get("HTTPMethods").touched?17:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(20,12,"gateway.hints.rest.scope-type")),t.ɵɵadvance(5),t.ɵɵrepeater(e.requestsScopeType)}}function U0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-required"))}function j0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-pattern"))}function H0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.device-name-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",26),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,U0,2,3,"tb-error-icon",22)(8,j0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.device-name-filter-hint")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("deviceNameFilter").hasError("required")&&e.mappingFormGroup.get("deviceNameFilter").touched?7:e.mappingFormGroup.get("deviceNameFilter").hasError("pattern")&&e.mappingFormGroup.get("deviceNameFilter").touched?8:-1)}}function W0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.method-filter-required"))}function $0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.method-filter-pattern"))}function K0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.method-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",27),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,W0,2,3,"tb-error-icon",22)(8,$0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.rest.method-filter")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("methodFilter").hasError("required")&&e.mappingFormGroup.get("methodFilter").touched?7:e.mappingFormGroup.get("methodFilter").hasError("pattern")&&e.mappingFormGroup.get("methodFilter").touched?8:-1)}}function Y0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-filter-required"))}function X0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-filter-pattern"))}function Z0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.attribute-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",28),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,Y0,2,3,"tb-error-icon",22)(8,X0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.rest.attribute-filter")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("attributeFilter").hasError("required")&&e.mappingFormGroup.get("attributeFilter").touched?7:e.mappingFormGroup.get("attributeFilter").hasError("pattern")&&e.mappingFormGroup.get("attributeFilter").touched?8:-1)}}function Q0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.request-url-expression-required"))}function J0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.request-url-expression-pattern"))}function e2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function t2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.value-expression-required"))}function n2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.value-expression-pattern"))}function i2(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",29)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.rest.request-url-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",30),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,Q0,2,3,"tb-error-icon",22)(8,J0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",9)(10,"div",19),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"gateway.rest.http-method"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",20)(14,"mat-select",31),t.ɵɵrepeaterCreate(15,e2,2,2,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()(),t.ɵɵelementStart(17,"div",9)(18,"div",19),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"gateway.value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",20),t.ɵɵelement(22,"input",32),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,t2,2,3,"tb-error-icon",22)(25,n2,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,7,"gateway.hints.rest.attribute-url-expression")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("requestUrlExpression").hasError("required")&&e.mappingFormGroup.get("requestUrlExpression").touched?7:e.mappingFormGroup.get("requestUrlExpression").hasError("pattern")&&e.mappingFormGroup.get("requestUrlExpression").touched?8:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,11,"gateway.hints.rest.http-method")),t.ɵɵadvance(5),t.ɵɵrepeater(e.httpMethods),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(19,13,"gateway.hints.rest.value-expression")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("valueExpression").hasError("required")&&e.mappingFormGroup.get("valueExpression").touched?24:e.mappingFormGroup.get("valueExpression").hasError("pattern")&&e.mappingFormGroup.get("valueExpression").touched?25:-1)}}function a2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,F0)))}function r2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function o2(e,n){1&e&&(t.ɵɵelementStart(0,"span",34),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function s2(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵtext(2,"gateway.response-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",20),t.ɵɵelement(4,"input",33),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,a2,2,5,"tb-error-icon",22)(7,r2,2,3,"tb-error-icon",22)(8,o2,2,0,"span",34),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,2,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("responseTimeout").hasError("min")&&e.mappingFormGroup.get("responseTimeout").touched?6:e.mappingFormGroup.get("responseTimeout").hasError("pattern")&&e.mappingFormGroup.get("responseTimeout").touched?7:8)}}function l2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-required"))}function p2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-pattern"))}function c2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-name-expression-required"))}function d2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-name-expression-pattern"))}function u2(e,n){1&e&&t.ɵɵelement(0,"div",38),2&e&&t.ɵɵproperty("tb-help-popup","widget/lib/gateway/rest-json_fn")("tb-help-popup-style",t.ɵɵpureFunction0(2,R0))}function m2(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.device-name-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",35),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,l2,2,3,"tb-error-icon",22)(8,p2,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",9)(10,"div",36),t.ɵɵtext(11,"gateway.attribute-name-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",20),t.ɵɵelement(13,"input",37),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,c2,2,3,"tb-error-icon",22)(16,d2,2,3,"tb-error-icon",22)(17,u2,1,3,"div",38),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.device-name-filter-hint")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("deviceNameExpression").hasError("required")&&e.mappingFormGroup.get("deviceNameExpression").touched?7:e.mappingFormGroup.get("deviceNameExpression").hasError("pattern")&&e.mappingFormGroup.get("deviceNameExpression").touched?8:-1),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("attributeNameExpression").hasError("required")&&e.mappingFormGroup.get("attributeNameExpression").touched?15:e.mappingFormGroup.get("attributeNameExpression").hasError("pattern")&&e.mappingFormGroup.get("attributeNameExpression").touched?16:17)}}function h2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,F0)))}function g2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function f2(e,n){1&e&&(t.ɵɵelementStart(0,"span",34),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function y2(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵtext(2,"gateway.timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",20),t.ɵɵelement(4,"input",39),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,h2,2,5,"tb-error-icon",22)(7,g2,2,3,"tb-error-icon",22)(8,f2,2,0,"span",34),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,2,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("timeout").hasError("min")&&e.mappingFormGroup.get("timeout").touched?6:e.mappingFormGroup.get("timeout").hasError("pattern")&&e.mappingFormGroup.get("timeout").touched?7:8)}}function v2(e,n){1&e&&(t.ɵɵelementStart(0,"div",14)(1,"mat-slide-toggle",40)(2,"mat-label",41),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.hints.rest.update-ssl")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.rest.ssl-verify")," "))}function x2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",53),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.value)}}function b2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,F0)))}function w2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function S2(e,n){1&e&&(t.ɵɵelementStart(0,"span",34),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function C2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.tries-min"))}function _2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.tries-pattern"))}function T2(e,n){1&e&&(t.ɵɵelementStart(0,"div",14)(1,"mat-slide-toggle",54)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.rest.allow-redirects")," "))}function I2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",8)(1,"mat-expansion-panel",42)(2,"mat-expansion-panel-header")(3,"mat-panel-title",43)(4,"div",44),t.ɵɵtext(5,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(6,"div",45)(7,"div",10),t.ɵɵtext(8,"gateway.rest.http-headers"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"div",46)(10,"mat-chip-listbox",47),t.ɵɵpipe(11,"keyvalue"),t.ɵɵrepeaterCreate(12,x2,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵpipe(14,"keyvalue"),t.ɵɵelementStart(15,"mat-chip",48),t.ɵɵelement(16,"label",49),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"button",50,0),t.ɵɵpipe(19,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(18),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i))})),t.ɵɵelementStart(20,"tb-icon",51),t.ɵɵtext(21,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(22,"div",9)(23,"div",10),t.ɵɵtext(24,"gateway.timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",20),t.ɵɵelement(26,"input",39),t.ɵɵpipe(27,"translate"),t.ɵɵtemplate(28,b2,2,5,"tb-error-icon",22)(29,w2,2,3,"tb-error-icon",22)(30,S2,2,0,"span",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(31,"div",9)(32,"div",10),t.ɵɵtext(33,"gateway.rest.tries"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"mat-form-field",20),t.ɵɵelement(35,"input",52),t.ɵɵpipe(36,"translate"),t.ɵɵtemplate(37,C2,2,3,"tb-error-icon",22)(38,_2,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵtemplate(39,T2,5,3,"div",14),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(10),t.ɵɵproperty("tbEllipsisChipList",t.ɵɵpipeBind1(11,7,e.mappingFormGroup.get("httpHeaders").value)),t.ɵɵadvance(2),t.ɵɵrepeater(t.ɵɵpipeBind1(14,9,e.mappingFormGroup.get("httpHeaders").value)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,11,"action.edit")),t.ɵɵadvance(9),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(27,13,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("timeout").hasError("min")&&e.mappingFormGroup.get("timeout").touched?28:e.mappingFormGroup.get("timeout").hasError("pattern")&&e.mappingFormGroup.get("timeout").touched?29:30),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("tries").hasError("min")&&e.mappingFormGroup.get("tries").touched?37:e.mappingFormGroup.get("tries").hasError("pattern")&&e.mappingFormGroup.get("tries").touched?38:-1),t.ɵɵadvance(2),t.ɵɵconditional(e.requestType===e.RestRequestType.ATTRIBUTE_UPDATE?39:-1)}}class E2 extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.cdr=l,this.destroyRef=p,this.requestTypes=Object.values(Ii),this.requestsScopeType=Object.values(Mi),this.httpMethods=Object.values(gt),this.helpLink=`${O}/docs/iot-gateway/config/rest/#attribute-request-section`,this.RestRequestTypesTranslationsMap=Ei,this.SecurityMode=Ji,this.RestRequestType=Ii,this.mappingFormGroup=this.fb.group({requestType:[Ii.ATTRIBUTE_REQUEST],endpoint:["",[me.required,me.pattern(pn)]],HTTPMethods:[[gt.POST],[me.required]],HTTPMethod:[gt.POST,[me.required]],type:[Mi.Shared],security:[{type:Ri.ANONYMOUS}],timeout:[null,[me.min(.001),me.pattern(ln)]],deviceNameExpression:["",[me.required,me.pattern(pn)]],attributeNameExpression:["",[me.required,me.pattern(pn)]],SSLVerify:[!1],deviceNameFilter:["",[me.required,me.pattern(pn)]],methodFilter:["",[me.required,me.pattern(pn)]],attributeFilter:["",[me.required,me.pattern(pn)]],requestUrlExpression:[this.data.defaultRequestUrl,[me.required,me.pattern(pn)]],valueExpression:["",[me.required,me.pattern(pn)]],responseTimeout:[null,[me.min(.001),me.pattern(ln)]],tries:[null,[me.min(1),me.pattern(ln)]],allowRedirects:[!1],httpHeaders:[{}]}),this.keysPopupClosed=!0,this.mappingFormGroup.patchValue(this.data.value,{emitEvent:!1}),this.observeRequestTypeChange(),this.toggleFieldsByRequestType(this.mappingFormGroup.get("requestType").value)}get requestType(){return this.mappingFormGroup.get("requestType").value}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.mappingFormGroup.valid){const{requestType:e,...t}=this.mappingFormGroup.value;Y(t),this.dialogRef.close({requestType:e,requestValue:t})}}manageKeys(e,t){e.stopPropagation();const n=t._elementRef.nativeElement;if(this.popoverService.hasPopover(n))return void this.popoverService.hidePopover(n);this.popoverService.hasPopover(n)&&this.popoverService.hidePopover(n),this.keysPopupClosed=!1;const i=this.popoverService.displayPopover(n,this.renderer,this.viewContainerRef,A0,"leftBottom",!1,null,{keys:this.mappingFormGroup.get("httpHeaders").value,withReportStrategy:this.data.withReportStrategy},{},{},{},!0);i.tbComponentRef.instance.popover=i,i.tbComponentRef.instance.keysDataApplied.subscribe((e=>{i.hide(),this.mappingFormGroup.get("httpHeaders").patchValue(e),this.mappingFormGroup.get("httpHeaders").markAsDirty(),this.cdr.markForCheck()})),i.tbHideStart.pipe(In(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}observeRequestTypeChange(){this.mappingFormGroup.get("requestType").valueChanges.pipe(Pe(),In(this.destroyRef)).subscribe((e=>this.toggleFieldsByRequestType(e)))}toggleFieldsByRequestType(e){this.mappingFormGroup.disable({emitEvent:!1}),ki.get("all").forEach((e=>this.mappingFormGroup.get(e).enable({emitEvent:!1}))),ki.get(e).forEach((e=>this.mappingFormGroup.get(e).enable({emitEvent:!1})))}static{this.ɵfac=function(e){return new(e||E2)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(Ze.Router),t.ɵɵdirectiveInject(Ue),t.ɵɵdirectiveInject(je.MatDialogRef),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(rt.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:E2,selectors:[["tb-rest-requests-mapping-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:37,vars:23,consts:[["button",""],[1,"h-full","w-[77vw]","max-w-3xl",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row"],["translate","",1,"fixed-title-width"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex"],["formControlName","requestType"],[3,"value"],[1,"tb-form-row","tb-flex","fill-width"],["formControlName","security",3,"mode"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","type","text","name","value","formControlName","endpoint",3,"placeholder"],["matSuffix","",3,"tooltipText"],["formControlName","HTTPMethods","multiple",""],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","type"],["matInput","","type","text","name","value","formControlName","deviceNameFilter",3,"placeholder"],["matInput","","type","text","name","value","formControlName","methodFilter",3,"placeholder"],["matInput","","type","text","name","value","formControlName","attributeFilter",3,"placeholder"],[1,"tb-form-row","request-url-row"],["matInput","","type","text","name","value","formControlName","requestUrlExpression",3,"placeholder"],["formControlName","HTTPMethod"],["matInput","","type","text","name","value","formControlName","valueExpression",3,"placeholder"],["matInput","","type","number","min","0","name","value","formControlName","responseTimeout",3,"placeholder"],["translate","","matSuffix","",1,"block","pr-2"],["matInput","","type","text","name","value","formControlName","deviceNameExpression",3,"placeholder"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","type","text","name","value","formControlName","attributeNameExpression",3,"placeholder"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["formControlName","SSLVerify",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],[1,"tb-settings"],[1,"justify-end"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","space-between","tb-flex"],[1,"tb-flex","gw-chip-list"],[1,"tb-flex",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["matInput","","type","number","min","0","name","value","formControlName","tries",3,"placeholder"],["tbTruncateWithTooltip",""],["formControlName","allowRedirects",1,"mat-slide"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",1)(1,"mat-toolbar",2)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",3)(6,"div",4),t.ɵɵelementStart(7,"button",5),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",6),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",7)(11,"div",8)(12,"div",9)(13,"div",10),t.ɵɵtext(14,"gateway.request-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-form-field",11)(16,"mat-select",12),t.ɵɵrepeaterCreate(17,B0,3,4,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()(),t.ɵɵtemplate(19,z0,26,14)(20,H0,9,7,"div",9)(21,K0,9,7,"div",9)(22,Z0,9,7,"div",9)(23,i2,26,17)(24,s2,9,4,"div",9)(25,m2,18,11)(26,y2,9,4,"div",9)(27,v2,6,6,"div",14),t.ɵɵelement(28,"tb-security-config",15),t.ɵɵtemplate(29,I2,40,17,"div",8),t.ɵɵelementEnd()(),t.ɵɵelementStart(30,"div",16)(31,"button",17),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(32),t.ɵɵpipe(33,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"button",18),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,17,"gateway.requests-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.helpLink),t.ɵɵadvance(11),t.ɵɵrepeater(n.requestTypes),t.ɵɵadvance(2),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_REQUEST?19:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType!==n.RestRequestType.ATTRIBUTE_REQUEST?20:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.SERVER_SIDE_RPC?21:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_UPDATE?22:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType!==n.RestRequestType.ATTRIBUTE_REQUEST?23:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.SERVER_SIDE_RPC?24:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_REQUEST?25:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_REQUEST?26:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_UPDATE?27:-1),t.ɵɵadvance(),t.ɵɵproperty("mode",n.SecurityMode.basic),t.ɵɵadvance(),t.ɵɵconditional(n.requestType!==n.RestRequestType.ATTRIBUTE_REQUEST?29:-1),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(33,19,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.mappingFormGroup.invalid||!n.mappingFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(36,21,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(E2,[se,_,$n,UY,ai]),styles:["[_nghost-%COMP%] .tb-form-row.request-url-row[_ngcontent-%COMP%]{gap:6px}"],changeDetection:d.OnPush})}}const M2=()=>["type","details","actions"],k2=()=>({});function P2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",6)(1,"div",26),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"span",8),t.ɵɵelementStart(5,"button",10),t.ɵɵpipe(6,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageMapping(n))})),t.ɵɵelementStart(7,"mat-icon"),t.ɵɵtext(8,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",10),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,3,"gateway.requests-mapping")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(6,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,7,"action.search")))}function D2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",27)(1,"div",28),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.type")))}function O2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",29)(1,"div",28),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,i.RestRequestTypesTranslationsMap.get(e.requestType)))}}function A2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.details")," "))}function F2(e,n){if(1&e&&t.ɵɵtext(0),2&e){let e;const n=t.ɵɵnextContext().$implicit,i=t.ɵɵnextContext();t.ɵɵtextInterpolate1(" ",i.Object.values(null!==(e=n.requestValue.httpHeaders)&&void 0!==e?e:t.ɵɵpureFunction0(1,k2)).join(", ")," ")}}function R2(e,n){if(1&e&&t.ɵɵtext(0),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵtextInterpolate1(" ",e.requestValue.methodFilter," ")}}function B2(e,n){if(1&e&&t.ɵɵtext(0),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵtextInterpolate1(" ",e.requestValue.endpoint," ")}}function N2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",29)(1,"div",28),t.ɵɵtemplate(2,F2,1,2)(3,R2,1,1)(4,B2,1,1),t.ɵɵelementEnd()()),2&e){let e;const i=n.$implicit;t.ɵɵadvance(2),t.ɵɵconditional("attributeUpdates"===(e=i.requestType)?2:"serverSideRpc"===e?3:4)}}function L2(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",31)}function V2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",10),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageMapping(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",10),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteMapping(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function q2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,V2,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",32),t.ɵɵelementContainer(4,33),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",34)(6,"button",35),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",36),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",37,2),t.ɵɵelementContainer(11,33),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(4),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function G2(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",38)}function z2(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class U2 extends $a{constructor(){super(...arguments),this.Object=Object,this.RestRequestTypesTranslationsMap=Ei}getDatasource(){return new j2}manageMapping(e,t){e&&e.stopPropagation();const n=J(t),{requestType:i=Ii.ATTRIBUTE_REQUEST,requestValue:a={}}=n?this.entityFormArray.at(t).value:{};this.getMappingDialog({requestType:i,...a},n?"action.apply":"action.add").afterClosed().pipe(In(this.destroyRef)).subscribe((e=>{e&&(n?this.entityFormArray.at(t).patchValue(e):this.entityFormArray.push(this.fb.control(e)),this.entityFormArray.markAsDirty())}))}deleteMapping(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-mapping-title",{name:this.getRequestDetails(this.entityFormArray.at(t).value)}),this.translate.instant("gateway.delete-mapping-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(In(this.destroyRef)).subscribe((e=>{e&&(this.entityFormArray.removeAt(t),this.entityFormArray.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>{const n=this.getRequestDetails(e);return Object.values(e).some((e=>Q(e)&&this.translate.instant(Ei.get(e)).toLowerCase().includes(t.toLowerCase())||n?.toLowerCase().includes(t.toLowerCase())))}))),this.dataSource.loadData(e)}getRequestDetails(e){let t;switch(e.requestType){case"attributeUpdates":t=Object.values(e.requestValue.httpHeaders).join(", ");break;case"serverSideRpc":t=e.requestValue.methodFilter;break;default:t=e.requestValue.endpoint}return t}validate(){return null}getMappingDialog(e,t){return this.dialog.open(E2,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,defaultRequestUrl:this.defaultRequestUrl,withReportStrategy:this.withReportStrategy}})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(U2)))(n||U2)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:U2,selectors:[["tb-rest-request-mapping-table"]],inputs:{defaultRequestUrl:"defaultRequestUrl"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>U2)),multi:!0},{provide:he,useExisting:c((()=>U2)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:44,vars:36,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-absolute-fill","size-full"],[1,"flex","size-full","flex-col"],[1,"gw-table-toolbar","mat-mdc-table-toolbar"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"gw-connector-table"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","!w-1/3",4,"matHeaderCellDef"],["class","w-1/3",4,"matCellDef"],["class","w-1/3 !pl-0",4,"matHeaderCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"!w-1/3"],["tbTruncateWithTooltip",""],[1,"w-1/3"],[1,"w-1/3","!pl-0"],[1,"w-12"],[1,"lt-lg:!hidden","flex","min-w-24","flex-1","flex-row","items-stretch","justify-end","text-center"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,P2,13,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"button",7),t.ɵɵpipe(7,"translate"),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"mat-form-field",8)(11,"mat-label"),t.ɵɵtext(12," "),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",9,0),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",10),t.ɵɵpipe(17,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(18,"mat-icon"),t.ɵɵtext(19,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",11)(21,"table",12),t.ɵɵelementContainerStart(22,13),t.ɵɵtemplate(23,D2,4,3,"mat-header-cell",14)(24,O2,4,3,"mat-cell",15),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(25,13),t.ɵɵtemplate(26,A2,3,3,"mat-header-cell",16)(27,N2,5,1,"mat-cell",15),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(28,17),t.ɵɵtemplate(29,L2,1,0,"mat-header-cell",18)(30,q2,12,3,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(31,G2,1,0,"mat-header-row",20)(32,z2,1,0,"mat-row",21),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"section",22),t.ɵɵpipe(34,"async"),t.ɵɵelementStart(35,"button",23),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageMapping(i))})),t.ɵɵelementStart(36,"mat-icon",24),t.ɵɵtext(37,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"span"),t.ɵɵtext(39),t.ɵɵpipe(40,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(41,"span",25),t.ɵɵpipe(42,"async"),t.ɵɵtext(43," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵconditional(!1===t.ɵɵpipeBind1(4,20,n.dataSource.isEmpty())?3:-1),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,22,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,24,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,26,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","type"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","details"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(34,M2))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(35,M2)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(34,28,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(40,30,"gateway.add-mapping")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(42,32,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(U2,[se,_,Wn]),encapsulation:2,changeDetection:d.OnPush})}}class j2 extends G{constructor(){super()}}class H2 extends Ua{initBasicFormGroup(){return this.fb.group({server:[],mapping:[],requestsMapping:[]})}getRequestDataArray(e){const t=[];return ie(e)&&Object.keys(e).forEach((n=>{for(const i of e[n])t.push({requestType:n,requestValue:i})})),t}getRequestDataObject(e){return e.reduce(((e,{requestType:t,requestValue:n})=>(e[t]?.push(n),e)),{attributeRequests:[],attributeUpdates:[],serverSideRpc:[]})}updateDefaultUrl({host:e,port:t,SSL:n}){this.defaultRequestUrl=e&&t?`${n?"https":"http"}//${e}:${t}/`:document.location.origin}writeValue(e){this.basicFormGroup.setValue(this.mapConfigToFormValue(e),{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(H2)))(n||H2)}})()}static{this.ɵdir=t.ɵɵdefineDirective({type:H2,features:[t.ɵɵInheritDefinitionFeature]})}}class W2 extends H2{mapConfigToFormValue(e){return{server:e.server??{},mapping:e.mapping??[],requestsMapping:this.getRequestDataArray(e.requestsMapping)}}getMappedValue(e){return this.updateDefaultUrl(e?.server??{}),{server:e.server??{},mapping:e.mapping??[],requestsMapping:this.getRequestDataObject(e.requestsMapping??[])}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(W2)))(n||W2)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:W2,selectors:[["tb-rest-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>W2)),multi:!0},{provide:he,useExisting:c((()=>W2)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:13,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server"],["formControlName","mapping"],["formControlName","requestsMapping",3,"defaultRequestUrl"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-rest-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-rest-mapping-table",4),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-tab",1),t.ɵɵpipe(11,"translate"),t.ɵɵelement(12,"tb-rest-request-mapping-table",5),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.server"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(11,15,"gateway.requests-mapping")),t.ɵɵadvance(2),t.ɵɵproperty("defaultRequestUrl",n.defaultRequestUrl))},dependencies:t.ɵɵgetComponentDepsFactory(W2,[se,_,L1,_0,U2]),encapsulation:2,changeDetection:d.OnPush})}}class $2 extends H2{mapConfigToFormValue({attributeRequests:e=[],attributeUpdates:t=[],serverSideRpc:n=[],mapping:i=[],...a}){return{server:{...a??{}},mapping:er.mapMappingToUpgradedVersion(i),requestsMapping:this.getRequestDataArray({attributeRequests:e,attributeUpdates:t,serverSideRpc:n})}}getMappedValue({requestsMapping:e,mapping:t=[],server:n={}}){this.updateDefaultUrl(n);const{attributeRequests:i=[],attributeUpdates:a=[],serverSideRpc:r=[]}=this.getRequestDataObject(e);return{...n,mapping:er.mapMappingToDowngradedVersion(t),attributeRequests:i,attributeUpdates:a,serverSideRpc:r}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory($2)))(n||$2)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:$2,selectors:[["tb-rest-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>$2)),multi:!0},{provide:he,useExisting:c((()=>$2)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:13,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server"],["formControlName","mapping"],["formControlName","requestsMapping",3,"defaultRequestUrl"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-rest-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-rest-mapping-table",4),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-tab",1),t.ɵɵpipe(11,"translate"),t.ɵɵelement(12,"tb-rest-request-mapping-table",5),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.server"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(11,15,"gateway.requests-mapping")),t.ɵɵadvance(2),t.ɵɵproperty("defaultRequestUrl",n.defaultRequestUrl))},dependencies:t.ɵɵgetComponentDepsFactory($2,[se,_,L1,_0,U2]),encapsulation:2,changeDetection:d.OnPush})}}const K2=(e,t)=>({hasErrors:e,noErrors:t}),Y2=()=>({minWidth:"144px",maxWidth:"144px",textAlign:"center"}),X2=()=>({minWidth:"144px",maxWidth:"144px",width:"144px",textAlign:"center"}),Z2=e=>({"tb-current-entity":e});function Q2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",32),t.ɵɵpipe(1,"translate"),t.ɵɵpipe(2,"async"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onAddConnector(n))})),t.ɵɵelementStart(3,"mat-icon"),t.ɵɵtext(4,"add"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.add")),t.ɵɵproperty("disabled",t.ɵɵpipeBind1(2,4,e.isLoading$))}}function J2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"section",33)(1,"button",34),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onAddConnector(n))})),t.ɵɵelementStart(2,"mat-icon",35),t.ɵɵtext(3,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"span"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,1,"gateway.add-connector")))}function e3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",36),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-enabled")," "))}function t3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell")(1,"mat-slide-toggle",37),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return n.stopPropagation(),t.ɵɵresetView(a.onEnableConnector(i))})),t.ɵɵelementEnd()()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("checked",i.activeConnectors.includes(e.key))}}function n3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",38),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-name"),""))}function i3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function a3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-type")," "))}function r3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",40),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.returnType(e)," ")}}function o3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.configuration")," "))}function s3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",40)(1,"div",41),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassMap(i.isConnectorSynced(e)?"status-sync":"status-unsync"),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.isConnectorSynced(e)?"sync":"out of sync"," ")}}function l3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-status")," "))}function p3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell",40)(1,"span",42),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorLogs(i,n))})),t.ɵɵelementEnd()()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassMap(t.ɵɵpureFunction2(3,K2,+i.getErrorsCount(e)>0,0==+i.getErrorsCount(e)||""===i.getErrorsCount(e))),t.ɵɵpropertyInterpolate("matTooltip","Errors: "+i.getErrorsCount(e))}}function c3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell"),t.ɵɵelement(1,"div",43),t.ɵɵelementStart(2,"div",44),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,Y2)),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.connectors-table-actions")))}function d3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell")(1,"div",45)(2,"button",46),t.ɵɵpipe(3,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorRpc(i,n))})),t.ɵɵelementStart(4,"mat-icon"),t.ɵɵtext(5,"private_connectivity"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"button",47),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorLogs(i,n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"list"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",47),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteConnector(i,n))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(14,"div",48)(15,"button",49),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(16,"mat-icon",50),t.ɵɵtext(17,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"mat-menu",51,1)(20,"button",46),t.ɵɵpipe(21,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorRpc(i,n))})),t.ɵɵelementStart(22,"mat-icon"),t.ɵɵtext(23,"private_connectivity"),t.ɵɵelementEnd()(),t.ɵɵelementStart(24,"button",47),t.ɵɵpipe(25,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorLogs(i,n))})),t.ɵɵelementStart(26,"mat-icon"),t.ɵɵtext(27,"list"),t.ɵɵelementEnd()(),t.ɵɵelementStart(28,"button",47),t.ɵɵpipe(29,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteConnector(i,n))})),t.ɵɵelementStart(30,"mat-icon"),t.ɵɵtext(31,"delete"),t.ɵɵelementEnd()()()()()}if(2&e){const e=n.$implicit,i=t.ɵɵreference(19);t.ɵɵadvance(),t.ɵɵstyleMap(t.ɵɵpureFunction0(23,X2)),t.ɵɵadvance(),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(3,11,"gateway.rpc.rpc"))("disabled",!e.value.configurationJson.id),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(7,13,"gateway.logs.logs")),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,15,"gateway.delete-connector")),t.ɵɵadvance(5),t.ɵɵproperty("matMenuTriggerFor",i),t.ɵɵadvance(5),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(21,17,"gateway.rpc.rpc"))("disabled",!e.value.configurationJson.id),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(25,19,"gateway.logs.logs")),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(29,21,"gateway.delete-connector"))}}function u3(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",52)}function m3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-row",53),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.selectConnector(n,i))})),t.ɵɵelementEnd()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵclassMap(t.ɵɵpureFunction1(2,Z2,i.isSameConnector(e)))}}function h3(e,n){if(1&e&&(t.ɵɵelementStart(0,"span",54),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate1("v",e.connectorForm.get("configVersion").value,"")}}function g3(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-select",55)(1,"tb-toggle-option",56),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"tb-toggle-option",56),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("value",e.ConnectorConfigurationModes.BASIC),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,4,"gateway.basic")," "),t.ɵɵadvance(2),t.ɵɵproperty("value",e.ConnectorConfigurationModes.ADVANCED),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,6,"gateway.advanced")," ")}}function f3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-mqtt-basic-config",64),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function y3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-mqtt-legacy-basic-config",64),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function v3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,f3,2,4,"tb-mqtt-basic-config",63),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,y3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.MQTT))("ngIfElse",e)}}function x3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-opc-ua-basic-config",64),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function b3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-opc-ua-legacy-basic-config",64),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function w3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,x3,2,4,"tb-opc-ua-basic-config",63),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,b3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.OPCUA))("ngIfElse",e)}}function S3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-modbus-basic-config",66),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function C3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-modbus-legacy-basic-config",66),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function _3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,S3,1,1,"tb-modbus-basic-config",65),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,C3,1,1,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.MODBUS))("ngIfElse",e)}}function T3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-socket-basic-config",66),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function I3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-socket-legacy-basic-config",66),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function E3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,T3,1,1,"tb-socket-basic-config",65),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,I3,1,1,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.SOCKET))("ngIfElse",e)}}function M3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-bacnet-basic-config",64),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function k3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-bacnet-legacy-basic-config",64),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function P3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,M3,2,4,"tb-bacnet-basic-config",63),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,k3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.BACNET))("ngIfElse",e)}}function D3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-rest-basic-config",68),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function O3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-rest-legacy-basic-config",68),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function A3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,D3,2,4,"tb-rest-basic-config",67),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,O3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.REST))("ngIfElse",e)}}function F3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0)(1,61),t.ɵɵtemplate(2,v3,5,5,"ng-container",62)(3,w3,5,5,"ng-container",62)(4,_3,5,5,"ng-container",62)(5,E3,5,5,"ng-container",62)(6,P3,5,5,"ng-container",62)(7,A3,5,5,"ng-container",62),t.ɵɵelementContainerEnd()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("ngSwitch",e.initialConnector.type),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MQTT),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.OPCUA),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MODBUS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.SOCKET),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.BACNET),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.REST)}}function R3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-tab-group")(1,"mat-tab",69),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,70),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",69),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-json-object-edit",71),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()),2&e){t.ɵɵnextContext(2);const e=t.ɵɵreference(41);t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,6,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,8,"gateway.configuration"),"*"),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(7,10,"gateway.configuration")),t.ɵɵproperty("fillHeight",!0)}}function B3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"section",57),t.ɵɵtemplate(1,F3,8,7,"ng-container",58)(2,R3,8,12,"ng-template",null,2,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(4,"div",59)(5,"button",60),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.onSaveConnector())})),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()}if(2&e){let e;const n=t.ɵɵreference(3),i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngIf",(null==(e=i.connectorForm.get("mode"))?null:e.value)===i.ConnectorConfigurationModes.BASIC)("ngIfElse",n),t.ɵɵadvance(4),t.ɵɵproperty("disabled",!i.connectorForm.dirty||i.connectorForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,4,"action.save")," ")}}function N3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",88),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.connectorForm.get("name").hasError("duplicateName")?"gateway.connector-duplicate-name":"gateway.name-required"))}}function L3(e,n){1&e&&(t.ɵɵelementStart(0,"div",73)(1,"div",84),t.ɵɵtext(2,"gateway.connectors-table-class"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",75)(4,"mat-form-field",76),t.ɵɵelement(5,"input",89),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,1,"gateway.set")))}function V3(e,n){1&e&&(t.ɵɵelementStart(0,"div",73)(1,"div",84),t.ɵɵtext(2,"gateway.connectors-table-key"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",75)(4,"mat-form-field",76),t.ɵɵelement(5,"input",90),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,1,"gateway.set")))}function q3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",56),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function G3(e,n){1&e&&(t.ɵɵelementStart(0,"div",73)(1,"mat-slide-toggle",91)(2,"mat-label",92),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.send-change-data-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.send-change-data")," "))}function z3(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",93),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Connector)}}function U3(e,n){if(1&e&&(t.ɵɵelementStart(0,"section",72)(1,"div",73)(2,"div",74),t.ɵɵtext(3,"gateway.name"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",75)(5,"mat-form-field",76),t.ɵɵelement(6,"input",77),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,N3,3,3,"mat-icon",78),t.ɵɵelementEnd()()(),t.ɵɵtemplate(9,L3,7,3,"div",79)(10,V3,7,3,"div",79),t.ɵɵelementStart(11,"div",80)(12,"div",81),t.ɵɵtext(13,"gateway.logs-configuration"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",82)(15,"mat-slide-toggle",83)(16,"mat-label"),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(19,"div",73)(20,"div",84),t.ɵɵtext(21,"gateway.remote-logging-level"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"div",75)(23,"mat-form-field",76)(24,"mat-select",85),t.ɵɵtemplate(25,q3,2,2,"mat-option",86),t.ɵɵelementEnd()()()()(),t.ɵɵtemplate(26,G3,6,6,"div",79),t.ɵɵpipe(27,"withReportStrategy"),t.ɵɵtemplate(28,z3,1,2,"tb-report-strategy",87),t.ɵɵpipe(29,"withReportStrategy"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("formGroup",e.connectorForm),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.connectorForm.get("name").hasError("required")&&e.connectorForm.get("name").touched||e.connectorForm.get("name").hasError("duplicateName")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.connectorForm.get("type").value===e.ConnectorType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.connectorForm.get("type").value===e.ConnectorType.GRPC),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,11,"gateway.enable-remote-logging")," "),t.ɵɵadvance(8),t.ɵɵproperty("ngForOf",e.gatewayLogLevel),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.connectorForm.get("type").value===e.ConnectorType.MQTT&&!t.ɵɵpipeBind2(27,13,e.connectorForm.get("configVersion").value,e.ConnectorType.MQTT)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(29,16,e.connectorForm.get("configVersion").value,e.connectorForm.get("type").value))}}class j3{isErrorState(e){return e&&e.invalid}}e("ForceErrorStateMatcher",j3);class H3 extends q{constructor(e,t,n,i,a,r,o,s,l,p,c){super(e),this.store=e,this.fb=t,this.translate=n,this.attributeService=i,this.dialogService=a,this.dialog=r,this.telemetryWsService=o,this.zone=s,this.utils=l,this.withReportStrategy=p,this.cd=c,this.ConnectorType=dt,this.allowBasicConfig=new Set([dt.MQTT,dt.OPCUA,dt.MODBUS,dt.SOCKET,dt.BACNET,dt.REST]),this.gatewayLogLevel=Object.values(pt),this.displayedColumns=["enabled","key","type","syncStatus","errors","actions"],this.GatewayConnectorTypesTranslatesMap=ut,this.ConnectorConfigurationModes=an,this.ReportStrategyDefaultValue=on,this.basicConfigInitSubject=new we,this.activeData=[],this.inactiveData=[],this.sharedAttributeData=[],this.subscriptionOptions={callbacks:{onDataUpdated:()=>this.ctx.ngZone.run((()=>{this.onErrorsUpdated()})),onDataUpdateError:(e,t)=>this.ctx.ngZone.run((()=>{this.onDataUpdateError(t)}))}},this.destroy$=new we,this.attributeUpdateSubject=new we,this.initDataSources(),this.initConnectorForm(),this.observeAttributeChange()}ngAfterViewInit(){this.dataSource.sort=this.sort,this.dataSource.sortingDataAccessor=this.getSortingDataAccessor(),this.ctx.$scope.gatewayConnectors=this,this.loadConnectors(),this.loadGatewayState(),this.observeModeChange()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}onSaveConnector(){this.saveConnector(this.getUpdatedConnectorData(this.connectorForm.value),!1)}saveConnector(e,t=!0){const n=t||this.activeConnectors.includes(this.initialConnector.name)?D.SHARED_SCOPE:D.SERVER_SCOPE;Ie(this.getEntityAttributeTasks(e,n)).pipe(Ne(1)).subscribe((n=>{this.showToast(t?this.translate.instant("gateway.connector-created"):this.translate.instant("gateway.connector-updated")),this.initialConnector=e,this.updateData(!0),this.connectorForm.markAsPristine()}))}getEntityAttributeTasks(e,t){const n=[],i=[{key:e.name,value:e}],a=[],r=!this.activeConnectors.includes(e.name)&&t===D.SHARED_SCOPE||!this.inactiveConnectors.includes(e.name)&&t===D.SERVER_SCOPE,o=this.initialConnector&&this.initialConnector.name!==e.name;return o&&(a.push({key:this.initialConnector.name}),this.removeConnectorFromList(this.initialConnector.name,!0),this.removeConnectorFromList(this.initialConnector.name,!1)),r&&(t===D.SHARED_SCOPE?this.activeConnectors.push(e.name):this.inactiveConnectors.push(e.name)),(o||r)&&n.push(this.getSaveEntityAttributesTask(t)),n.push(this.attributeService.saveEntityAttributes(this.device,t,i)),a.length&&n.push(this.attributeService.deleteEntityAttributes(this.device,t,a)),n}getSaveEntityAttributesTask(e){const t=e===D.SHARED_SCOPE?"active_connectors":"inactive_connectors",n=e===D.SHARED_SCOPE?this.activeConnectors:this.inactiveConnectors;return this.attributeService.saveEntityAttributes(this.device,e,[{key:t,value:n}])}removeConnectorFromList(e,t){const n=t?this.activeConnectors:this.inactiveConnectors,i=n.indexOf(e);-1!==i&&n.splice(i,1)}getUpdatedConnectorData(e){const t={...e};return t.configuration=`${re(t.name)}.json`,delete t.basicConfig,t.type!==dt.GRPC&&delete t.key,t.type!==dt.CUSTOM&&delete t.class,this.allowBasicConfig.has(t.type)||delete t.mode,this.withReportStrategy.transform(t.configVersion,t.type)&&(t.configurationJson.reportStrategy=t.reportStrategy,ne(t.reportStrategy)&&delete t.reportStrategy,ne(t.configurationJson.reportStrategy)&&delete t.configurationJson.reportStrategy),this.gatewayVersion&&!t.configVersion&&(t.configVersion=this.gatewayVersion),t.ts=Date.now(),t}updateData(e=!1){this.pageLink.sortOrder.property=this.sort.active,this.pageLink.sortOrder.direction=w[this.sort.direction.toUpperCase()],this.attributeDataSource.loadAttributes(this.device,D.CLIENT_SCOPE,this.pageLink,e).subscribe((e=>{this.activeData=e.data.filter((e=>this.activeConnectors.includes(e.key))),this.combineData(),this.generateSubscription(),this.setClientData(e)})),this.inactiveConnectorsDataSource.loadAttributes(this.device,D.SHARED_SCOPE,this.pageLink,e).subscribe((e=>{this.sharedAttributeData=e.data.filter((e=>this.activeConnectors.includes(e.key))),this.combineData()})),this.serverDataSource.loadAttributes(this.device,D.SERVER_SCOPE,this.pageLink,e).subscribe((e=>{this.inactiveData=e.data.filter((e=>this.inactiveConnectors.includes(e.key))),this.combineData()}))}isConnectorSynced(e){const t=e.value;if(!t.ts||e.skipSync||!this.isGatewayActive)return!1;if(-1===this.activeData.findIndex((e=>("string"==typeof e.value?JSON.parse(e.value):e.value).name===t.name)))return!1;return-1!==this.sharedAttributeData.findIndex((e=>{const n=e.value,i=n.name===t.name,a=W(n.configurationJson,{})&&i,r=this.hasSameConfig(n.configurationJson,t.configurationJson),o=n.ts&&n.ts<=t.ts;return i&&o&&(r||a)}))}hasSameConfig(e,t){const{name:n,id:i,enableRemoteLogging:a,logLevel:r,reportStrategy:o,configVersion:s,...l}=e,{name:p,id:c,enableRemoteLogging:d,logLevel:u,reportStrategy:m,configVersion:h,...g}=t;return W(l,g)}combineData(){const e=[...this.activeData,...this.inactiveData,...this.sharedAttributeData].reduce(((e,t)=>{const n=e.findIndex((e=>e.key===t.key));return-1===n?e.push(t):t.lastUpdateTs>e[n].lastUpdateTs&&!this.isConnectorSynced(e[n])&&(e[n]={...t,skipSync:!0}),e}),[]);this.dataSource.data=e.map((e=>({...e,value:"string"==typeof e.value?JSON.parse(e.value):e.value})))}clearOutConnectorForm(){this.initialConnector=null,this.connectorForm.setValue({mode:an.BASIC,name:"",type:dt.MQTT,sendDataOnlyOnChange:!1,enableRemoteLogging:!1,logLevel:pt.INFO,key:"auto",class:"",configuration:"",configurationJson:{},basicConfig:{},configVersion:"",reportStrategy:[{value:{},disabled:!0}]},{emitEvent:!1}),this.connectorForm.markAsPristine()}selectConnector(e,t){e&&e.stopPropagation();const n=t.value;n?.name!==this.initialConnector?.name&&this.confirmConnectorChange().subscribe((e=>{e&&this.setFormValue(n)}))}isSameConnector(e){if(!this.initialConnector)return!1;const t=e.value;return this.initialConnector.name===t.name}showToast(e){this.store.dispatch({type:"[Notification] Show",notification:{message:e,type:"success",duration:1e3,verticalPosition:"top",horizontalPosition:"left",target:"dashboardRoot",forceDismiss:!0}})}returnType(e){const t=e.value;return this.GatewayConnectorTypesTranslatesMap.get(t.type)}deleteConnector(e,t){t?.stopPropagation();const n=`Delete connector "${e.key}"?`;this.dialogService.confirm(n,"All connector data will be deleted.","Cancel","Delete").pipe(Ne(1),Ve((t=>{if(!t)return;const n=[],i=this.activeConnectors.includes(e.value?.name)?D.SHARED_SCOPE:D.SERVER_SCOPE;return n.push(this.attributeService.deleteEntityAttributes(this.device,i,[e])),this.removeConnectorFromList(e.key,!0),this.removeConnectorFromList(e.key,!1),n.push(this.getSaveEntityAttributesTask(i)),Ie(n)}))).subscribe((()=>{this.initialConnector&&this.initialConnector.name!==e.key||(this.clearOutConnectorForm(),this.cd.detectChanges()),this.updateData(!0)}))}connectorLogs(e,t){t&&t.stopPropagation();const n=te(this.ctx.stateController.getStateParams());n.connector_logs=e,n.targetEntityParamName="connector_logs",this.ctx.stateController.openState("connector_logs",n)}connectorRpc(e,t){t&&t.stopPropagation();const n=te(this.ctx.stateController.getStateParams());n.connector_rpc=e,n.targetEntityParamName="connector_rpc",this.ctx.stateController.openState("connector_rpc",n)}onEnableConnector(e){e.value.ts=(new Date).getTime(),this.updateActiveConnectorKeys(e.key),this.attributeUpdateSubject.next(e)}getErrorsCount(e){const t=e.key,n=this.subscription&&this.subscription.data.find((e=>e&&e.dataKey.name===`${t}_ERRORS_COUNT`));return n&&this.activeConnectors.includes(t)?n.data[0][1]||0:"Inactive"}onAddConnector(e){e?.stopPropagation(),this.confirmConnectorChange().pipe(Ne(1),Oe(Boolean),Ve((()=>this.openAddConnectorDialog())),Oe(Boolean)).subscribe((e=>this.addConnector(e)))}addConnector(e){e.configurationJson||(e.configurationJson={}),this.gatewayVersion&&!e.configVersion&&(e.configVersion=this.gatewayVersion),e.basicConfig=e.configurationJson,this.initialConnector=e;const t=this.connectorForm.get("type").value;this.setInitialConnectorValues(e),this.saveConnector(this.getUpdatedConnectorData(e)),t!==e.type&&this.allowBasicConfig.has(e.type)?this.basicConfigInitSubject.pipe(Ne(1)).subscribe((()=>{this.patchConnectorBasicConfig(e.basicConfig)})):this.patchConnectorBasicConfig(e.basicConfig)}setInitialConnectorValues(e){const{basicConfig:t,mode:n,enableRemoteLogging:i,...a}=e;this.toggleReportStrategy(e),this.connectorForm.get("mode").setValue(this.allowBasicConfig.has(e.type)?e.mode??an.BASIC:null,{emitEvent:!1}),this.connectorForm.get("enableRemoteLogging").setValue(i,{emitEvent:!1}),this.connectorForm.patchValue(a,{emitEvent:!1})}openAddConnectorDialog(){return this.ctx.ngZone.run((()=>this.dialog.open(w$,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{dataSourceData:this.dataSource.data,gatewayVersion:this.gatewayVersion}}).afterClosed()))}uniqNameRequired(){return e=>{const t=e.value?.trim().toLowerCase(),n=this.dataSource.data.some((e=>e.value.name.toLowerCase()===t)),i=this.initialConnector?.name.toLowerCase()===t;return n&&!i?{duplicateName:{valid:!1}}:null}}initDataSources(){const e={property:"key",direction:w.ASC};this.pageLink=new S(1e3,0,null,e),this.attributeDataSource=new Kn(this.attributeService,this.telemetryWsService,this.zone,this.translate),this.inactiveConnectorsDataSource=new Kn(this.attributeService,this.telemetryWsService,this.zone,this.translate),this.serverDataSource=new Kn(this.attributeService,this.telemetryWsService,this.zone,this.translate),this.dataSource=new x([])}initConnectorForm(){this.connectorForm=this.fb.group({mode:[an.BASIC],name:["",[me.required,this.uniqNameRequired(),me.pattern(pn)]],type:["",[me.required]],enableRemoteLogging:[!1],logLevel:["",[me.required]],sendDataOnlyOnChange:[!1],key:["auto"],class:[""],configuration:[""],configurationJson:[{},[me.required]],basicConfig:[{}],configVersion:[""],reportStrategy:[{value:{},disabled:!0}]})}getSortingDataAccessor(){return(e,t)=>{switch(t){case"syncStatus":return this.isConnectorSynced(e)?1:0;case"enabled":return this.activeConnectors.includes(e.key)?1:0;case"errors":const n=this.getErrorsCount(e);return"string"==typeof n?this.sort.direction.toUpperCase()===w.DESC?-1:1/0:n;default:return e[t]||e.value[t]}}}loadConnectors(){this.device&&this.device.id!==B&&Ie([this.attributeService.getEntityAttributes(this.device,D.SHARED_SCOPE,["active_connectors"]),this.attributeService.getEntityAttributes(this.device,D.SERVER_SCOPE,["inactive_connectors"]),this.attributeService.getEntityAttributes(this.device,D.CLIENT_SCOPE,["Version"])]).pipe(Me(this.destroy$)).subscribe((e=>{this.activeConnectors=this.parseConnectors(e[0]),this.inactiveConnectors=this.parseConnectors(e[1]),this.gatewayVersion=e[2][0]?.value,this.updateData(!0)}))}loadGatewayState(){this.attributeService.getEntityAttributes(this.device,D.SERVER_SCOPE).pipe(Me(this.destroy$)).subscribe((e=>{const t=e.find((e=>"active"===e.key)).value,n=e.find((e=>"lastDisconnectTime"===e.key))?.value,i=e.find((e=>"lastConnectTime"===e.key))?.value;this.isGatewayActive=this.getGatewayStatus(t,i,n)}))}parseConnectors(e){const t=e?.[0]?.value||[];return Q(t)?JSON.parse(t):t}observeModeChange(){this.connectorForm.get("mode").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{e===an.BASIC&&this.patchConnectorBasicConfig(this.connectorForm.get("configurationJson").value)}))}observeAttributeChange(){this.attributeUpdateSubject.pipe(De(300),Ae((e=>this.executeAttributeUpdates(e))),Me(this.destroy$)).subscribe()}updateActiveConnectorKeys(e){if(this.activeConnectors.includes(e)){const t=this.activeConnectors.indexOf(e);-1!==t&&this.activeConnectors.splice(t,1),this.inactiveConnectors.push(e)}else{const t=this.inactiveConnectors.indexOf(e);-1!==t&&this.inactiveConnectors.splice(t,1),this.activeConnectors.push(e)}}executeAttributeUpdates(e){Ie(this.getAttributeExecutionTasks(e)).pipe(Ne(1),Ae((()=>this.updateData(!0))),Me(this.destroy$)).subscribe()}getAttributeExecutionTasks(e){const t=this.activeConnectors.includes(e.key),n=t?D.SERVER_SCOPE:D.SHARED_SCOPE,i=t?D.SHARED_SCOPE:D.SERVER_SCOPE;return[this.attributeService.saveEntityAttributes(this.device,D.SHARED_SCOPE,[{key:"active_connectors",value:this.activeConnectors}]),this.attributeService.saveEntityAttributes(this.device,D.SERVER_SCOPE,[{key:"inactive_connectors",value:this.inactiveConnectors}]),this.attributeService.deleteEntityAttributes(this.device,n,[e]),this.attributeService.saveEntityAttributes(this.device,i,[e])]}onDataUpdateError(e){const t=this.utils.parseException(e);let n=t.name;t.message&&(n+=": "+t.message),console.error(n)}onErrorsUpdated(){this.cd.detectChanges()}onDataUpdated(){const e=this.ctx.defaultSubscription.data,t=e.find((e=>"active"===e.dataKey.name)).data[0][1],n=e.find((e=>"lastDisconnectTime"===e.dataKey.name)).data[0][1],i=e.find((e=>"lastConnectTime"===e.dataKey.name)).data[0][1];this.isGatewayActive=this.getGatewayStatus(t,i,n),this.cd.detectChanges()}getGatewayStatus(e,t,n){return!!e&&(!n||t>n)}generateSubscription(){if(this.subscription&&this.subscription.unsubscribe(),this.device){const e=[{type:N.entity,entityType:L.DEVICE,entityId:this.device.id,entityName:"Gateway",timeseries:[]}];this.dataSource.data.forEach((t=>{e[0].timeseries.push({name:`${t.key}_ERRORS_COUNT`,label:`${t.key}_ERRORS_COUNT`})})),this.ctx.subscriptionApi.createSubscriptionFromInfo(R.latest,e,this.subscriptionOptions,!1,!0).subscribe((e=>{this.subscription=e}))}}createBasicConfigWatcher(){this.basicConfigSub&&this.basicConfigSub.unsubscribe(),this.basicConfigSub=this.connectorForm.get("basicConfig").valueChanges.pipe(Oe((()=>!!this.initialConnector)),Me(this.destroy$)).subscribe((e=>{const t=this.connectorForm.get("configurationJson"),n=this.connectorForm.get("type").value,i=this.connectorForm.get("mode").value;if(!W(e,t?.value)&&this.allowBasicConfig.has(n)&&i===an.BASIC){const n={...t.value,...e};this.connectorForm.get("configurationJson").patchValue(n,{emitEvent:!1})}}))}createJsonConfigWatcher(){this.jsonConfigSub&&this.jsonConfigSub.unsubscribe(),this.jsonConfigSub=this.connectorForm.get("configurationJson").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{const t=this.connectorForm.get("basicConfig"),n=this.connectorForm.get("type").value,i=this.connectorForm.get("mode").value;!W(e,t?.value)&&this.allowBasicConfig.has(n)&&i===an.ADVANCED&&this.connectorForm.get("basicConfig").patchValue(e,{emitEvent:!1})}))}confirmConnectorChange(){return this.initialConnector&&this.connectorForm.dirty?this.dialogService.confirm(this.translate.instant("gateway.change-connector-title"),this.translate.instant("gateway.change-connector-text"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0):_e(!0)}setFormValue(e){this.connectorForm.disabled&&this.connectorForm.enable();const t=Ya.getConfig({configuration:"",key:"auto",configurationJson:{},...e},this.gatewayVersion);this.gatewayVersion&&!t.configVersion&&(t.configVersion=this.gatewayVersion),t.basicConfig=t.configurationJson,this.initialConnector=t,this.updateConnector(t)}updateConnector(e){this.jsonConfigSub?.unsubscribe(),this.allowBasicConfig.has(e.type)?this.updateBasicConfigConnector(e):(this.setInitialConnectorValues(e),this.connectorForm.markAsPristine(),this.createJsonConfigWatcher())}updateBasicConfigConnector(e){this.basicConfigSub?.unsubscribe();const t=this.connectorForm.get("type").value;this.setInitialConnectorValues(e),t!==e.type&&this.allowBasicConfig.has(e.type)&&e.mode!==an.ADVANCED?this.basicConfigInitSubject.asObservable().pipe(Ne(1)).subscribe((()=>{this.patchConnectorBasicConfig(e.basicConfig)})):this.patchConnectorBasicConfig(e.basicConfig)}patchConnectorBasicConfig(e){this.connectorForm.get("basicConfig").patchValue(e,{emitEvent:!1}),this.connectorForm.markAsPristine(),this.createBasicConfigWatcher(),this.createJsonConfigWatcher()}toggleReportStrategy(e){const t=this.connectorForm.get("reportStrategy"),n=this.connectorForm.get("sendDataOnlyOnChange");this.connectorForm.get("reportStrategy").reset(e.reportStrategy,{emitEvent:!1}),this.withReportStrategy.transform(e.configVersion,e.type)?(t.enable({emitEvent:!1}),n.disable({emitEvent:!1})):(t.disable({emitEvent:!1}),e.type===dt.MQTT&&n.enable({emitEvent:!1}))}setClientData(e){if(this.initialConnector){const t=e.data.find((e=>e.key===this.initialConnector.name));t&&(t.value="string"==typeof t.value?JSON.parse(t.value):t.value,this.isConnectorSynced(t)&&t.value.configurationJson&&this.setFormValue({...t.value,mode:this.connectorForm.get("mode").value??t.value.mode}))}}static{this.ɵfac=function(e){return new(e||H3)(t.ɵɵdirectiveInject(Xe.Store),t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(le.TranslateService),t.ɵɵdirectiveInject(K.AttributeService),t.ɵɵdirectiveInject(K.DialogService),t.ɵɵdirectiveInject(je.MatDialog),t.ɵɵdirectiveInject(K.TelemetryWebsocketService),t.ɵɵdirectiveInject(t.NgZone),t.ɵɵdirectiveInject(K.UtilsService),t.ɵɵdirectiveInject(tr),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:H3,selectors:[["tb-gateway-connector"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(v,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.sort=e.first)}},inputs:{ctx:"ctx",device:"device"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:st,useClass:j3},tr]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:42,vars:21,consts:[["generalTabContent",""],["cellActionsMenu","matMenu"],["defaultConfig",""],["legacy",""],[1,"connector-container","tb-form-panel","no-border"],[1,"table-section","tb-form-panel","no-padding","section-container","flex"],[1,"mat-mdc-table-toolbar","justify-between"],["mat-icon-button","","matTooltipPosition","above",3,"disabled","matTooltip","click",4,"ngIf"],[1,"table-container"],["class","mat-headline-5 tb-absolute-fill tb-add-new items-center justify-center",4,"ngIf"],["mat-table","","matSort","","matSortDisableClear","",3,"dataSource","matSortActive","matSortDirection"],["matColumnDef","enabled","sticky",""],["style","width: 60px;min-width: 60px;",4,"matHeaderCellDef"],[4,"matCellDef"],["matColumnDef","key"],["mat-sort-header","","style","width: 40%",4,"matHeaderCellDef"],["matColumnDef","type"],["mat-sort-header","","style","width: 30%",4,"matHeaderCellDef"],["style","text-transform: uppercase",4,"matCellDef"],["matColumnDef","syncStatus"],["matColumnDef","errors"],["matColumnDef","actions","stickyEnd",""],[4,"matHeaderCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],["class","mat-row-select",3,"class","click",4,"matRowDef","matRowDefColumns"],[1,"tb-form-panel","section-container","flex",3,"formGroup"],[1,"tb-form-panel-title","tb-flex","no-flex","space-between","align-center"],[1,"tb-form-panel-title"],["class","version-placeholder",4,"ngIf"],["formControlName","mode","appearance","fill",4,"ngIf"],["translate","",1,"no-data-found","items-center","justify-center"],["class","tb-form-panel section-container no-border no-padding tb-flex space-between",4,"ngIf"],["mat-icon-button","","matTooltipPosition","above",3,"click","disabled","matTooltip"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],[2,"width","60px","min-width","60px"],[3,"click","checked"],["mat-sort-header","",2,"width","40%"],["mat-sort-header","",2,"width","30%"],[2,"text-transform","uppercase"],[1,"status"],["matTooltipPosition","above",1,"dot",3,"click","matTooltip"],[1,"gt-md:!hidden",2,"width","48px","min-width","48px","max-width","48px"],[1,"lt-lg:!hidden"],[1,"lt-md:!hidden","flex-row","justify-end"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip","disabled"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"gt-sm:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"],[1,"mat-row-select",3,"click"],[1,"version-placeholder"],["formControlName","mode","appearance","fill"],[3,"value"],[1,"tb-form-panel","section-container","no-border","no-padding","tb-flex","space-between"],[4,"ngIf","ngIfElse"],[1,"flex","justify-end"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[3,"ngSwitch"],[4,"ngSwitchCase"],["formControlName","basicConfig",3,"generalTabContent","withReportStrategy","initialized",4,"ngIf","ngIfElse"],["formControlName","basicConfig",3,"initialized","generalTabContent","withReportStrategy"],["formControlName","basicConfig",3,"generalTabContent","initialized",4,"ngIf","ngIfElse"],["formControlName","basicConfig",3,"initialized","generalTabContent"],["class","h-full","formControlName","basicConfig",3,"generalTabContent","withReportStrategy","initialized",4,"ngIf","ngIfElse"],["formControlName","basicConfig",1,"h-full",3,"initialized","generalTabContent","withReportStrategy"],[3,"label"],[3,"ngTemplateOutlet"],["jsonRequired","","formControlName","configurationJson",1,"configuration-json",3,"fillHeight","label"],[1,"tb-form-panel","no-border","no-padding","padding-top","section-container","flex",3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","autocomplete","off","name","value","formControlName","name",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row"],["formControlName","enableRemoteLogging",1,"mat-slide"],["translate","",1,"fixed-title-width"],["formControlName","logLevel"],[3,"value",4,"ngFor","ngForOf"],["class","stroked tb-form-panel","formControlName","reportStrategy",3,"isExpansionMode","defaultValue",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","name","value","formControlName","class",3,"placeholder"],["matInput","","name","value","formControlName","key",3,"placeholder"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",4)(1,"section",5)(2,"mat-toolbar",6)(3,"h2"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(6,Q2,5,6,"button",7),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",8),t.ɵɵtemplate(8,J2,7,3,"section",9),t.ɵɵelementStart(9,"table",10),t.ɵɵelementContainerStart(10,11),t.ɵɵtemplate(11,e3,3,3,"mat-header-cell",12)(12,t3,2,1,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(13,14),t.ɵɵtemplate(14,n3,3,3,"mat-header-cell",15)(15,i3,2,1,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(16,16),t.ɵɵtemplate(17,a3,3,3,"mat-header-cell",17)(18,r3,2,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(19,19),t.ɵɵtemplate(20,o3,3,3,"mat-header-cell",17)(21,s3,3,3,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(22,20),t.ɵɵtemplate(23,l3,3,3,"mat-header-cell",17)(24,p3,2,6,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(25,21),t.ɵɵtemplate(26,c3,5,6,"mat-header-cell",22)(27,d3,32,24,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(28,u3,1,0,"mat-header-row",23)(29,m3,1,4,"mat-row",24),t.ɵɵelementEnd()()(),t.ɵɵelementStart(30,"section",25)(31,"div",26)(32,"div",27),t.ɵɵtext(33),t.ɵɵpipe(34,"translate"),t.ɵɵtemplate(35,h3,2,1,"span",28),t.ɵɵelementEnd(),t.ɵɵtemplate(36,g3,7,8,"tb-toggle-select",29),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"span",30),t.ɵɵtext(38," gateway.select-connector "),t.ɵɵelementEnd(),t.ɵɵtemplate(39,B3,8,6,"section",31),t.ɵɵelementEnd()(),t.ɵɵtemplate(40,U3,30,19,"ng-template",null,0,t.ɵɵtemplateRefExtractor)),2&e&&(t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,17,"gateway.connectors")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",null==n.dataSource||null==n.dataSource.data?null:n.dataSource.data.length),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",!(null!=n.dataSource&&(null!=n.dataSource.data&&n.dataSource.data.length))),t.ɵɵadvance(),t.ɵɵproperty("dataSource",n.dataSource)("matSortActive",n.pageLink.sortOrder.property)("matSortDirection",n.pageLink.sortDirection()),t.ɵɵadvance(19),t.ɵɵproperty("matHeaderRowDef",n.displayedColumns)("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",n.displayedColumns),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.connectorForm),t.ɵɵadvance(3),t.ɵɵtextInterpolate2(" ",null!=n.initialConnector&&n.initialConnector.type?n.GatewayConnectorTypesTranslatesMap.get(n.initialConnector.type):""," ",t.ɵɵpipeBind1(34,19,"gateway.configuration")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.connectorForm.get("configVersion").value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.initialConnector&&n.allowBasicConfig.has(n.initialConnector.type)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.initialConnector),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.initialConnector))},dependencies:t.ɵɵgetComponentDepsFactory(H3,[se,_,tr,d$,mX,hX,iX,nX,IQ,EQ,BJ,NJ,ni,F1,R1,$2,W2]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block;overflow-x:auto;padding:0}[_nghost-%COMP%] .version-placeholder[_ngcontent-%COMP%]{color:gray;font-size:12px}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%]{height:100%;width:100%;flex-direction:row}@media screen and (max-width: 1279px){[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%]{flex-direction:column}}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] > section[_ngcontent-%COMP%]:not(.table-section){max-width:unset}@media screen and (min-width: 1280px){[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] > section[_ngcontent-%COMP%]:not(.table-section){max-width:50%}}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .table-section[_ngcontent-%COMP%]{min-height:35vh;overflow:hidden}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .table-section[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .flex[_ngcontent-%COMP%]{flex:1}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .input-container[_ngcontent-%COMP%]{height:auto}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .section-container[_ngcontent-%COMP%]{background-color:#fff}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{background:transparent;color:#000000de!important}[_nghost-%COMP%] .mat-mdc-slide-toggle[_ngcontent-%COMP%]{margin:0 8px}[_nghost-%COMP%] .status[_ngcontent-%COMP%]{text-align:center;border-radius:16px;font-weight:500;width:fit-content;padding:5px 15px}[_nghost-%COMP%] .status-sync[_ngcontent-%COMP%]{background:#1980380f;color:#198038}[_nghost-%COMP%] .status-unsync[_ngcontent-%COMP%]{background:#cb25300f;color:#cb2530}[_nghost-%COMP%] mat-row[_ngcontent-%COMP%]{cursor:pointer}[_nghost-%COMP%] .dot[_ngcontent-%COMP%]{height:12px;width:12px;background-color:#bbb;border-radius:50%;display:inline-block}[_nghost-%COMP%] .hasErrors[_ngcontent-%COMP%]{background-color:#cb2530}[_nghost-%COMP%] .noErrors[_ngcontent-%COMP%]{background-color:#198038}[_nghost-%COMP%] .connector-container .mat-mdc-tab-group, [_nghost-%COMP%] .connector-container .mat-mdc-tab-body-wrapper{height:100%}[_nghost-%COMP%] .connector-container .mat-mdc-tab-body.mat-mdc-tab-body-active{position:absolute}[_nghost-%COMP%] .connector-container .tb-form-row .fixed-title-width{min-width:120px;width:30%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .connector-container .tb-add-new{display:flex;z-index:999;pointer-events:none;background-color:#fff}[_nghost-%COMP%] .connector-container .tb-add-new button.connector{height:auto;padding-right:12px;font-size:20px;border-style:dashed;border-width:2px;border-radius:8px;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;color:#00000061}@media screen and (min-width: 960px){[_nghost-%COMP%] .configuration-json .ace_tooltip{transform:translate(-250px,-120px)}}']})}}e("GatewayConnectorComponent",H3);class W3{constructor(e){this.deviceService=e}download(e){e&&e.stopPropagation(),this.deviceId&&this.deviceService.downloadGatewayDockerComposeFile(this.deviceId).subscribe((()=>{}))}static{this.ɵfac=function(e){return new(e||W3)(t.ɵɵdirectiveInject(K.DeviceService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:W3,selectors:[["tb-gateway-command"]],inputs:{deviceId:"deviceId"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:32,vars:9,consts:[["mat-dialog-content","",1,"tb-form-panel","no-border",2,"padding","16px 16px 8px"],[1,"tb-no-data-text"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","no-border","no-padding","space-between"],["translate","",1,"tb-no-data-text","tb-commands-hint"],["mat-stroked-button","","color","primary","href","https://docs.docker.com/compose/install/","target","_blank"],["mat-stroked-button","","color","primary",3,"click"],["usePlainMarkdown","","containerClass","start-code","data","\n ```bash\n docker compose up\n {:copy-code}\n ```\n "]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",2)(5,"div",3),t.ɵɵtext(6,"device.connectivity.install-necessary-client-tools"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",4)(8,"div",5),t.ɵɵtext(9,"gateway.install-docker-compose"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"a",6)(11,"mat-icon"),t.ɵɵtext(12,"description"),t.ɵɵelementEnd(),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",2)(16,"div",3),t.ɵɵtext(17,"gateway.download-configuration-file"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"div",4)(19,"div",5),t.ɵɵtext(20,"gateway.download-docker-compose"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"button",7),t.ɵɵlistener("click",(function(e){return n.download(e)})),t.ɵɵelementStart(22,"mat-icon"),t.ɵɵtext(23,"download"),t.ɵɵelementEnd(),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(26,"div",2)(27,"div",3),t.ɵɵtext(28,"gateway.launch-gateway"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"div",5),t.ɵɵtext(30,"gateway.launch-docker-compose"),t.ɵɵelementEnd(),t.ɵɵelement(31,"tb-markdown",8),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,3,"gateway.docker-label")),t.ɵɵadvance(11),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,5,"common.documentation")," "),t.ɵɵadvance(11),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(25,7,"action.download")," "))},dependencies:t.ɵɵgetComponentDepsFactory(W3,[se,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-commands-hint[_ngcontent-%COMP%]{color:inherit;font-weight:400;flex:1}[_nghost-%COMP%] .tb-markdown-view .start-code .code-wrapper{padding:0}[_nghost-%COMP%] .tb-markdown-view .start-code .code-wrapper pre[class*=language-]{margin:0;background:#f3f6fa;border-color:#305680;padding-right:38px;overflow:scroll;padding-bottom:4px;min-height:42px;scrollbar-width:thin}[_nghost-%COMP%] .tb-markdown-view .start-code .code-wrapper pre[class*=language-]::-webkit-scrollbar{width:4px;height:4px}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn{right:-2px}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn p{color:#305680}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn p, [_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn div{background-color:#f3f6fa}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn div img{display:none}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn div:after{content:"";position:initial;display:block;width:18px;height:18px;background:#305680;mask-image:url(/assets/copy-code-icon.svg);-webkit-mask-image:url(/assets/copy-code-icon.svg);mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}']})}}var $3,K3,Y3;e("DeviceGatewayCommandComponent",W3),e("GatewayBasicConfigTab",$3),function(e){e[e.general=0]="general",e[e.logs=1]="logs",e[e.storage=2]="storage",e[e.grpc=3]="grpc",e[e.statistics=4]="statistics",e[e.other=5]="other"}($3||e("GatewayBasicConfigTab",$3={})),e("StorageTypes",K3),function(e){e.MEMORY="memory",e.FILE="file",e.SQLITE="sqlite"}(K3||e("StorageTypes",K3={})),e("LocalLogsConfigs",Y3),function(e){e.service="service",e.connector="connector",e.converter="converter",e.tb_connection="tb_connection",e.storage="storage",e.extension="extension"}(Y3||e("LocalLogsConfigs",Y3={}));const X3=e("LocalLogsConfigTranslateMap",new Map([[Y3.service,"Service"],[Y3.connector,"Connector"],[Y3.converter,"Converter"],[Y3.tb_connection,"TB Connection"],[Y3.storage,"Storage"],[Y3.extension,"Extension"]])),Z3=e("StorageTypesTranslationMap",new Map([[K3.MEMORY,"gateway.storage-types.memory-storage"],[K3.FILE,"gateway.storage-types.file-storage"],[K3.SQLITE,"gateway.storage-types.sqlite"]]));var Q3;e("LogSavingPeriod",Q3),function(e){e.days="D",e.hours="H",e.minutes="M",e.seconds="S"}(Q3||e("LogSavingPeriod",Q3={}));const J3=e("LogSavingPeriodTranslations",new Map([[Q3.days,"gateway.logs.days"],[Q3.hours,"gateway.logs.hours"],[Q3.minutes,"gateway.logs.minutes"],[Q3.seconds,"gateway.logs.seconds"]]));var e4;e("SecurityTypes",e4),function(e){e.ACCESS_TOKEN="accessToken",e.USERNAME_PASSWORD="usernamePassword",e.TLS_ACCESS_TOKEN="tlsAccessToken",e.TLS_PRIVATE_KEY="tlsPrivateKey"}(e4||e("SecurityTypes",e4={}));const t4=e("SecurityTypesTranslationsMap",new Map([[e4.ACCESS_TOKEN,"gateway.security-types.access-token"],[e4.USERNAME_PASSWORD,"gateway.security-types.username-password"],[e4.TLS_ACCESS_TOKEN,"gateway.security-types.tls-access-token"]])),n4=e("logsHandlerClass","thingsboard_gateway.tb_utility.tb_rotating_file_handler.TimedRotatingFileHandler"),i4=e("logsLegacyHandlerClass","thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler");function a4(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",10),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.storageTypesTranslationMap.get(e))," ")}}function r4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-read-record-count-required")," "))}function o4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-read-record-count-min")," "))}function s4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-read-record-count-pattern")," "))}function l4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-required")," "))}function p4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-min")," "))}function c4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-pattern")," "))}function d4(e,n){if(1&e&&(t.ɵɵelementStart(0,"section",11)(1,"mat-form-field",12)(2,"mat-label",13),t.ɵɵtext(3,"gateway.storage-read-record-count"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",14),t.ɵɵtemplate(5,r4,3,3,"mat-error",15)(6,o4,3,3,"mat-error",15)(7,s4,3,3,"mat-error",15),t.ɵɵelementStart(8,"mat-icon",16),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",12)(12,"mat-label",13),t.ɵɵtext(13,"gateway.storage-max-records"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",17),t.ɵɵtemplate(15,l4,3,3,"mat-error",15)(16,p4,3,3,"mat-error",15)(17,c4,3,3,"mat-error",15),t.ɵɵelementStart(18,"mat-icon",16),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"info_outlined "),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵproperty("ngIf",e.storageFormGroup.get("read_records_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("read_records_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("read_records_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,8,"gateway.hints.read-record-count")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,10,"gateway.hints.max-records-count"))}}function u4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-data-folder-path-required")," "))}function m4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-data-folder-path-pattern")," "))}function h4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-files-required")," "))}function g4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-files-min")," "))}function f4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-files-pattern")," "))}function y4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-required")," "))}function v4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-min")," "))}function x4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-pattern")," "))}function b4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-required")," "))}function w4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-min")," "))}function S4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-pattern")," "))}function C4(e,n){if(1&e&&(t.ɵɵelementStart(0,"section")(1,"div",18)(2,"mat-form-field",12)(3,"mat-label",13),t.ɵɵtext(4,"gateway.storage-data-folder-path"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",19),t.ɵɵtemplate(6,u4,3,3,"mat-error",15)(7,m4,3,3,"mat-error",15),t.ɵɵelementStart(8,"mat-icon",20),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",12)(12,"mat-label",13),t.ɵɵtext(13,"gateway.storage-max-files"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",21),t.ɵɵtemplate(15,h4,3,3,"mat-error",15)(16,g4,3,3,"mat-error",15)(17,f4,3,3,"mat-error",15),t.ɵɵelementStart(18,"mat-icon",16),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(21,"div",11)(22,"mat-form-field",12)(23,"mat-label",13),t.ɵɵtext(24,"gateway.storage-max-read-record-count-storage"),t.ɵɵelementEnd(),t.ɵɵelement(25,"input",22),t.ɵɵtemplate(26,y4,3,3,"mat-error",15)(27,v4,3,3,"mat-error",15)(28,x4,3,3,"mat-error",15),t.ɵɵelementStart(29,"mat-icon",16),t.ɵɵpipe(30,"translate"),t.ɵɵtext(31,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(32,"mat-form-field",12)(33,"mat-label",13),t.ɵɵtext(34,"gateway.storage-max-records"),t.ɵɵelementEnd(),t.ɵɵelement(35,"input",23),t.ɵɵtemplate(36,b4,3,3,"mat-error",15)(37,w4,3,3,"mat-error",15)(38,S4,3,3,"mat-error",15),t.ɵɵelementStart(39,"mat-icon",16),t.ɵɵpipe(40,"translate"),t.ɵɵtext(41,"info_outlined "),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵproperty("ngIf",e.storageFormGroup.get("data_folder_path").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("data_folder_path").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,15,"gateway.hints.data-folder")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_file_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_file_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_file_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,17,"gateway.hints.max-file-count")),t.ɵɵadvance(8),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(30,19,"gateway.hints.max-read-count")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(40,21,"gateway.hints.max-records"))}}function _4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-path-required")," "))}function T4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-path-pattern")," "))}function I4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-check-in-hours-required")," "))}function E4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-check-in-hours-min")," "))}function M4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-check-in-hours-pattern")," "))}function k4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-in-days-required")," "))}function P4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-in-days-min")," "))}function D4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-in-days-pattern")," "))}function O4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-required")," "))}function A4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-min")," "))}function F4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-pattern")," "))}function R4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.size-limit-required")," "))}function B4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.size-limit-min")," "))}function N4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.size-limit-pattern")," "))}function L4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.max-db-amount-required")," "))}function V4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.max-db-amount-min")," "))}function q4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.max-db-amount-pattern")," "))}function G4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.oversize-check-period-required")," "))}function z4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.oversize-check-period-min")," "))}function U4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.oversize-check-period-pattern")," "))}function j4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.writing-batch-size-required")," "))}function H4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.writing-batch-size-min")," "))}function W4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.writing-batch-size-pattern")," "))}function $4(e,n){if(1&e&&(t.ɵɵelementStart(0,"section")(1,"div",24)(2,"mat-form-field",25)(3,"mat-label",13),t.ɵɵtext(4,"gateway.storage-path"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",26),t.ɵɵtemplate(6,_4,3,3,"mat-error",15)(7,T4,3,3,"mat-error",15),t.ɵɵelementStart(8,"mat-icon",16),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",12)(12,"mat-label",13),t.ɵɵtext(13,"gateway.messages-ttl-check-in-hours"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",27),t.ɵɵtemplate(15,I4,3,3,"mat-error",15)(16,E4,3,3,"mat-error",15)(17,M4,3,3,"mat-error",15),t.ɵɵelementStart(18,"mat-icon",16),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(21,"div",11)(22,"mat-form-field",12)(23,"mat-label",13),t.ɵɵtext(24,"gateway.messages-ttl-in-days"),t.ɵɵelementEnd(),t.ɵɵelement(25,"input",28),t.ɵɵtemplate(26,k4,3,3,"mat-error",15)(27,P4,3,3,"mat-error",15)(28,D4,3,3,"mat-error",15),t.ɵɵelementStart(29,"mat-icon",29),t.ɵɵpipe(30,"translate"),t.ɵɵtext(31,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(32,"mat-form-field",12)(33,"mat-label",13),t.ɵɵtext(34,"gateway.storage-max-read-record-count"),t.ɵɵelementEnd(),t.ɵɵelement(35,"input",22),t.ɵɵtemplate(36,O4,3,3,"mat-error",15)(37,A4,3,3,"mat-error",15)(38,F4,3,3,"mat-error",15),t.ɵɵelementStart(39,"mat-icon",29),t.ɵɵpipe(40,"translate"),t.ɵɵtext(41,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(42,"div",11)(43,"mat-form-field",12)(44,"mat-label",13),t.ɵɵtext(45,"gateway.size-limit"),t.ɵɵelementEnd(),t.ɵɵelement(46,"input",30),t.ɵɵtemplate(47,R4,3,3,"mat-error",15)(48,B4,3,3,"mat-error",15)(49,N4,3,3,"mat-error",15),t.ɵɵelementStart(50,"mat-icon",29),t.ɵɵpipe(51,"translate"),t.ɵɵtext(52,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(53,"mat-form-field",12)(54,"mat-label",13),t.ɵɵtext(55,"gateway.max-db-amount"),t.ɵɵelementEnd(),t.ɵɵelement(56,"input",31),t.ɵɵtemplate(57,L4,3,3,"mat-error",15)(58,V4,3,3,"mat-error",15)(59,q4,3,3,"mat-error",15),t.ɵɵelementStart(60,"mat-icon",29),t.ɵɵpipe(61,"translate"),t.ɵɵtext(62,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(63,"div",11)(64,"mat-form-field",12)(65,"mat-label",13),t.ɵɵtext(66,"gateway.oversize-check-period"),t.ɵɵelementEnd(),t.ɵɵelement(67,"input",32),t.ɵɵtemplate(68,G4,3,3,"mat-error",15)(69,z4,3,3,"mat-error",15)(70,U4,3,3,"mat-error",15),t.ɵɵelementStart(71,"mat-icon",29),t.ɵɵpipe(72,"translate"),t.ɵɵtext(73,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(74,"mat-form-field",12)(75,"mat-label",13),t.ɵɵtext(76,"gateway.writing-batch-size"),t.ɵɵelementEnd(),t.ɵɵelement(77,"input",33),t.ɵɵtemplate(78,j4,3,3,"mat-error",15)(79,H4,3,3,"mat-error",15)(80,W4,3,3,"mat-error",15),t.ɵɵelementStart(81,"mat-icon",29),t.ɵɵpipe(82,"translate"),t.ɵɵtext(83,"info_outlined "),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵproperty("ngIf",e.storageFormGroup.get("data_file_path").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("data_file_path").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,31,"gateway.hints.data-folder")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_check_in_hours").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_check_in_hours").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_check_in_hours").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,33,"gateway.hints.ttl-check-hour")),t.ɵɵadvance(8),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_in_days").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_in_days").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_in_days").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(30,35,"gateway.hints.ttl-messages-day")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(40,37,"gateway.hints.max-read-count")),t.ɵɵadvance(8),t.ɵɵproperty("ngIf",e.storageFormGroup.get("size_limit").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("size_limit").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("size_limit").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(51,39,"gateway.size-limit-hint")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_db_amount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_db_amount").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_db_amount").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(61,41,"gateway.max-db-amount-hint")),t.ɵɵadvance(8),t.ɵɵproperty("ngIf",e.storageFormGroup.get("oversize_check_period").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("oversize_check_period").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("oversize_check_period").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(72,43,"gateway.oversize-check-period-hint")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("writing_batch_size").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("writing_batch_size").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("writing_batch_size").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(82,45,"gateway.writing-batch-size-hint"))}}class K4{constructor(e){this.fb=e,this.initialized=new u,this.StorageTypes=K3,this.storageTypes=Object.values(K3),this.storageTypesTranslationMap=Z3,this.onChange=()=>{},this.numberValidators=[me.min(1),me.pattern(ln)],this.requiredNumberValidators=[me.required,...this.numberValidators],this.requiredDirValidators=[me.required,me.pattern(un)],this.storageConfig={[K3.MEMORY]:{read_records_count:this.requiredNumberValidators,max_records_count:this.requiredNumberValidators},[K3.FILE]:{data_folder_path:this.requiredDirValidators,max_file_count:this.requiredNumberValidators,max_read_records_count:this.requiredNumberValidators,max_records_per_file:this.requiredNumberValidators},[K3.SQLITE]:{data_file_path:this.requiredDirValidators,messages_ttl_check_in_hours:this.requiredNumberValidators,messages_ttl_in_days:this.requiredNumberValidators,max_read_records_count:this.requiredNumberValidators,size_limit:this.numberValidators,max_db_amount:this.numberValidators,oversize_check_period:this.numberValidators,writing_batch_size:this.numberValidators}},this.storageFormGroup=this.initStorageFormGroup(),this.observeStorageTypeChanges(),this.storageFormGroup.valueChanges.pipe(In()).subscribe((e=>{this.onChange(e)}))}ngAfterViewInit(){this.updateValidators(this.storageFormGroup.get("type").value),this.initialized.emit({storage:this.storageFormGroup.value})}writeValue(e){J(e)&&(this.storageFormGroup.patchValue(e,{emitEvent:!1}),this.updateValidators(e.type))}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.storageFormGroup.valid?null:{storageFormGroup:{valid:!1}}}initStorageFormGroup(){return this.fb.group({type:[K3.MEMORY,[me.required]],read_records_count:[100],max_records_count:[1e5],data_folder_path:["./data/"],max_file_count:[10],max_read_records_count:[10],max_records_per_file:[1e4],data_file_path:["./data/"],messages_ttl_check_in_hours:[1],messages_ttl_in_days:[7],size_limit:[1024],max_db_amount:[10],oversize_check_period:[1],writing_batch_size:[1e3]})}observeStorageTypeChanges(){this.storageFormGroup.get("type").valueChanges.pipe(In()).subscribe((e=>{this.updateValidators(e)}))}updateValidators(e){const t=this.storageConfig[e];Object.keys(this.storageFormGroup.controls).forEach((e=>{if("type"===e)return;const n=this.storageFormGroup.get(e),i=t[e];i?(n.enable({emitEvent:!1}),n.setValidators(i),n.updateValueAndValidity({emitEvent:!1}),n.markAsTouched({emitEvent:!1})):(n.disable({emitEvent:!1}),n.clearValidators(),n.updateValueAndValidity({emitEvent:!1}),n.markAsUntouched({emitEvent:!1}))}))}static{this.ɵfac=function(e){return new(e||K4)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:K4,selectors:[["tb-gateway-storage-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>K4)),multi:!0},{provide:he,useExisting:c((()=>K4)),multi:!0}]),t.ɵɵStandaloneFeature],decls:15,vars:9,consts:[[1,"mat-content","mat-padding","flex","w-full","flex-col","gap-2",3,"formGroup"],[1,"tb-form-panel","no-padding-bottom","w-full"],["translate","",1,"tb-form-panel-title"],["translate","",1,"tb-form-panel-hint"],["formControlName","type",1,"flex"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-panel-hint"],[3,"ngSwitch"],["class","tb-form-row no-border no-padding tb-standard-fields column-xs",4,"ngSwitchCase"],[4,"ngSwitchCase"],[3,"value"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["translate",""],["type","number","matInput","","formControlName","read_records_count"],[4,"ngIf"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["type","number","matInput","","formControlName","max_records_count"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs","mb-2"],["matInput","","formControlName","data_folder_path"],["aria-hidden","false","aria-label","help-icon","matSuffix","",1,"mat-form-field-infix","pointer-event","suffix-icon",2,"cursor","pointer",3,"matTooltip"],["matInput","","type","number","formControlName","max_file_count"],["matInput","","type","number","formControlName","max_read_records_count"],["matInput","","type","number","formControlName","max_records_per_file"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs","align-start","mb-2"],["appearance","outline","subscriptSizing","dynamic",1,"flex"],["matInput","","formControlName","data_file_path"],["matInput","","type","number","formControlName","messages_ttl_check_in_hours"],["matInput","","type","number","formControlName","messages_ttl_in_days"],["matIconSuffix","",1,"cursor-pointer",3,"matTooltip"],["matInput","","type","number","formControlName","size_limit"],["matInput","","type","number","formControlName","max_db_amount"],["matInput","","type","number","formControlName","oversize_check_period"],["matInput","","type","number","formControlName","writing_batch_size"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.storage"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",3),t.ɵɵtext(5,"gateway.hints.storage"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"tb-toggle-select",4),t.ɵɵtemplate(7,a4,3,4,"tb-toggle-option",5),t.ɵɵelementEnd(),t.ɵɵelementStart(8,"div",6),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(11,7),t.ɵɵtemplate(12,d4,21,12,"section",8)(13,C4,42,23,"section",9)(14,$4,84,47,"section",9),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.storageFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",n.storageTypes),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(10,7,"gateway.hints."+n.storageFormGroup.get("type").value)),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",n.storageFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.StorageTypes.MEMORY),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.StorageTypes.FILE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.StorageTypes.SQLITE))},dependencies:t.ɵɵgetComponentDepsFactory(K4,[se,_]),encapsulation:2})}}function Y4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-required")," "))}function X4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-min")," "))}function Z4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-max")," "))}function Q4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-pattern")," "))}function J4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-timeout-required")," "))}function e5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-timeout-min")," "))}function t5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-timeout-pattern")," "))}function n5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-required")," "))}function i5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-min")," "))}function a5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-pattern")," "))}function r5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-time-between-pings-required")," "))}function o5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-time-between-pings-min")," "))}function s5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-time-between-pings-pattern")," "))}function l5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-max-pings-without-data-required")," "))}function p5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-max-pings-without-data-min")," "))}function c5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-max-pings-without-data-pattern")," "))}function d5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-ping-interval-without-data-required")," "))}function u5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-ping-interval-without-data-min")," "))}function m5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-ping-interval-without-data-pattern")," "))}class h5{constructor(e){this.fb=e,this.initialized=new u,this.onChange=()=>{},this.grpcFormGroup=this.initGrpcFormGroup(),this.grpcFormGroup.valueChanges.pipe(In()).subscribe((e=>{this.onChange(e)})),this.grpcFormGroup.get("enabled").valueChanges.pipe(In()).subscribe((e=>{this.toggleRpcFields(e)}))}ngAfterViewInit(){this.initialized.emit({grpc:this.grpcFormGroup.value})}writeValue(e){e&&this.toggleRpcFields(e.enabled),this.grpcFormGroup.patchValue(e,{emitEvent:!1})}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.grpcFormGroup.valid?null:{grpcFormGroup:{valid:!1}}}toggleRpcFields(e){const t=this.grpcFormGroup;e?(t.get("serverPort").enable({emitEvent:!1}),t.get("keepAliveTimeMs").enable({emitEvent:!1}),t.get("keepAliveTimeoutMs").enable({emitEvent:!1}),t.get("keepalivePermitWithoutCalls").enable({emitEvent:!1}),t.get("maxPingsWithoutData").enable({emitEvent:!1}),t.get("minTimeBetweenPingsMs").enable({emitEvent:!1}),t.get("minPingIntervalWithoutDataMs").enable({emitEvent:!1})):(t.get("serverPort").disable({emitEvent:!1}),t.get("keepAliveTimeMs").disable({emitEvent:!1}),t.get("keepAliveTimeoutMs").disable({emitEvent:!1}),t.get("keepalivePermitWithoutCalls").disable({emitEvent:!1}),t.get("maxPingsWithoutData").disable({emitEvent:!1}),t.get("minTimeBetweenPingsMs").disable({emitEvent:!1}),t.get("minPingIntervalWithoutDataMs").disable({emitEvent:!1}))}initGrpcFormGroup(){return this.fb.group({enabled:[!1],serverPort:[9595,[me.required,me.min(1),me.max(65535),me.pattern(ln)]],keepAliveTimeMs:[1e4,[me.required,me.min(1),me.pattern(ln)]],keepAliveTimeoutMs:[5e3,[me.required,me.min(1),me.pattern(ln)]],keepalivePermitWithoutCalls:[!0],maxPingsWithoutData:[0,[me.required,me.min(0),me.pattern(ln)]],minTimeBetweenPingsMs:[1e4,[me.required,me.min(1),me.pattern(ln)]],minPingIntervalWithoutDataMs:[5e3,[me.required,me.min(1),me.pattern(ln)]]})}static{this.ɵfac=function(e){return new(e||h5)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:h5,selectors:[["tb-gateway-grpc-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>h5)),multi:!0},{provide:he,useExisting:c((()=>h5)),multi:!0}]),t.ɵɵStandaloneFeature],decls:75,vars:47,consts:[[1,"mat-content","mat-padding","flex","flex-col","gap-2",3,"formGroup"],[1,"tb-form-panel","no-padding-bottom","w-full"],["color","primary","formControlName","enabled",1,"mat-slide"],[1,"tb-form-row","no-border","no-padding",3,"tb-hint-tooltip-icon"],["color","primary","formControlName","keepalivePermitWithoutCalls",1,"mat-slide"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["translate",""],["matInput","","formControlName","serverPort","type","number","min","0"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],[4,"ngIf"],["matInput","","formControlName","keepAliveTimeoutMs","type","number","min","0"],["matInput","","formControlName","keepAliveTimeMs","type","number","min","0"],["matInput","","formControlName","minTimeBetweenPingsMs","type","number","min","0"],["matInput","","formControlName","maxPingsWithoutData","type","number","min","0"],["matInput","","formControlName","minPingIntervalWithoutDataMs","type","number","min","0"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"mat-slide-toggle",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",3),t.ɵɵpipe(6,"translate"),t.ɵɵelementStart(7,"mat-slide-toggle",4),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"section")(11,"section",5)(12,"mat-form-field",6)(13,"mat-label",7),t.ɵɵtext(14,"gateway.server-port"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",8),t.ɵɵelementStart(16,"mat-icon",9),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(19,Y4,3,3,"mat-error",10)(20,X4,3,3,"mat-error",10)(21,Z4,3,3,"mat-error",10)(22,Q4,3,3,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",6)(24,"mat-label",7),t.ɵɵtext(25,"gateway.grpc-keep-alive-timeout"),t.ɵɵelementEnd(),t.ɵɵelement(26,"input",11),t.ɵɵelementStart(27,"mat-icon",9),t.ɵɵpipe(28,"translate"),t.ɵɵtext(29,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(30,J4,3,3,"mat-error",10)(31,e5,3,3,"mat-error",10)(32,t5,3,3,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(33,"section",5)(34,"mat-form-field",6)(35,"mat-label",7),t.ɵɵtext(36,"gateway.grpc-keep-alive"),t.ɵɵelementEnd(),t.ɵɵelement(37,"input",12),t.ɵɵelementStart(38,"mat-icon",9),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(41,n5,3,3,"mat-error",10)(42,i5,3,3,"mat-error",10)(43,a5,3,3,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"mat-form-field",6)(45,"mat-label",7),t.ɵɵtext(46,"gateway.grpc-min-time-between-pings"),t.ɵɵelementEnd(),t.ɵɵelement(47,"input",13),t.ɵɵelementStart(48,"mat-icon",9),t.ɵɵpipe(49,"translate"),t.ɵɵtext(50,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(51,r5,3,3,"mat-error",10)(52,o5,3,3,"mat-error",10)(53,s5,3,3,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(54,"section",5)(55,"mat-form-field",6)(56,"mat-label",7),t.ɵɵtext(57,"gateway.grpc-max-pings-without-data"),t.ɵɵelementEnd(),t.ɵɵelement(58,"input",14),t.ɵɵelementStart(59,"mat-icon",9),t.ɵɵpipe(60,"translate"),t.ɵɵtext(61,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(62,l5,3,3,"mat-error",10)(63,p5,3,3,"mat-error",10)(64,c5,3,3,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(65,"mat-form-field",6)(66,"mat-label",7),t.ɵɵtext(67,"gateway.grpc-min-ping-interval-without-data"),t.ɵɵelementEnd(),t.ɵɵelement(68,"input",15),t.ɵɵelementStart(69,"mat-icon",9),t.ɵɵpipe(70,"translate"),t.ɵɵtext(71,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(72,d5,3,3,"mat-error",10)(73,u5,3,3,"mat-error",10)(74,m5,3,3,"mat-error",10),t.ɵɵelementEnd()()()()()),2&e&&(t.ɵɵproperty("formGroup",n.grpcFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,29,"gateway.grpc")," "),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,31,"gateway.hints.permit-without-calls")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,33,"gateway.permit-without-calls")," "),t.ɵɵadvance(8),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,35,"gateway.hints.server-port")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("max")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("pattern")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(28,37,"gateway.hints.grpc-keep-alive-timeout")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeoutMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeoutMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeoutMs").hasError("pattern")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(39,39,"gateway.hints.grpc-keep-alive")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeMs").hasError("pattern")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(49,41,"gateway.hints.grpc-min-time-between-pings")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minTimeBetweenPingsMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minTimeBetweenPingsMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minTimeBetweenPingsMs").hasError("pattern")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(60,43,"gateway.hints.grpc-max-pings-without-data")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("maxPingsWithoutData").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("maxPingsWithoutData").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("maxPingsWithoutData").hasError("pattern")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(70,45,"gateway.hints.grpc-min-ping-interval-without-data")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minPingIntervalWithoutDataMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minPingIntervalWithoutDataMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minPingIntervalWithoutDataMs").hasError("pattern")))},dependencies:t.ɵɵgetComponentDepsFactory(h5,[se,_]),encapsulation:2})}}function g5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.date-format-required")," "))}function f5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.log-format-required")," "))}function y5(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",29),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function v5(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",30),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.localLogsConfigTranslateMap.get(e))}}function x5(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",29),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function b5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.file-path-required")," "))}function w5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.saving-period-required")," "))}function S5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.saving-period-min")," "))}function C5(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",29),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e.value)," ")}}function _5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.backup-count-required")," "))}function T5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.backup-count-min")," "))}class I5{constructor(e){this.fb=e,this.initialized=new u,this.logSavingPeriods=J3,this.localLogsConfigs=Object.keys(Y3),this.localLogsConfigTranslateMap=X3,this.gatewayLogLevel=Object.values(pt),this.remoteLogLevel=Object.values(pt).filter((e=>e!==pt.NONE)),this.onChange=()=>{},this.logsFormGroup=this.initLogsFormGroup(),this.showRemoteLogsControl=this.fb.control(!1),this.logsFormGroup.valueChanges.pipe(In()).subscribe((e=>{this.onChange(e)})),this.logSelector=this.fb.control(Y3.service);for(const e of Object.keys(Y3))this.addLocalLogConfig(e,{});this.showRemoteLogsControl.valueChanges.pipe(In()).subscribe((e=>this.logsFormGroup.get("logLevel")[e?"enable":"disable"]()))}ngAfterViewInit(){this.initialized.emit({logs:this.logsFormGroup.value})}writeValue(e){this.logsFormGroup.patchValue(e,{emitEvent:!1}),this.updateRemoteLogs(e?.logLevel??pt.NONE)}registerOnChange(e){this.onChange=e}registerOnTouched(e){}getLogFormGroup(e){return this.logsFormGroup.get(`local.${e}`)}validate(){return this.logsFormGroup.valid?null:{logsFormGroup:{valid:!1}}}initLogsFormGroup(){return this.fb.group({dateFormat:["%Y-%m-%d %H:%M:%S",[me.required,me.pattern(/^[^\s].*[^\s]$/)]],logFormat:["%(asctime)s.%(msecs)03d - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s",[me.required,me.pattern(/^[^\s].*[^\s]$/)]],type:["remote",[me.required]],logLevel:[{value:pt.INFO,disabled:!0}],local:this.fb.group({})})}addLocalLogConfig(e,t){const n=this.logsFormGroup.get("local"),i=this.fb.group({logLevel:[t.logLevel||pt.INFO,[me.required]],filePath:[t.filePath||"./logs",[me.required]],backupCount:[t.backupCount||7,[me.required,me.min(0)]],savingTime:[t.savingTime||3,[me.required,me.min(0)]],savingPeriod:[t.savingPeriod||Q3.days,[me.required]]});n.addControl(e,i,{emitEvent:!1})}updateRemoteLogs(e){const t=e&&e!==pt.NONE;this.showRemoteLogsControl.patchValue(t,{emitEvent:!1}),this.logsFormGroup.get("logLevel")[t?"enable":"disable"]({emitEvent:!1}),this.logsFormGroup.get("logLevel").patchValue(e===pt.NONE?pt.INFO:e,{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||I5)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:I5,selectors:[["tb-gateway-logs-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>I5)),multi:!0},{provide:he,useExisting:c((()=>I5)),multi:!0}]),t.ɵɵStandaloneFeature],decls:72,vars:33,consts:[[1,"mat-content","mat-padding","flex","flex-col","gap-2",3,"formGroup"],[1,"tb-form-panel","no-padding-bottom"],[1,"flex","flex-col"],["appearance","outline"],["translate",""],["matInput","","formControlName","dateFormat"],[4,"ngIf"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["matInput","","formControlName","logFormat","rows","2"],[1,"tb-form-panel"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide",3,"click","formControl"],[3,"tb-hint-tooltip-icon"],["formControlName","logLevel"],[3,"value",4,"ngFor","ngForOf"],["formGroupName","local",1,"tb-form-panel","no-padding-bottom"],["translate","",1,"tb-form-panel-title"],[1,"toggle-group",3,"formControl"],["class","first-capital",3,"value",4,"ngFor","ngForOf"],[3,"formGroup"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["matInput","","formControlName","filePath"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","saving-period"],["matInput","","formControlName","savingTime","type","number","min","0"],["appearance","outline","hideRequiredMarker","",2,"min-width","110px","width","30%"],["formControlName","savingPeriod"],["matInput","","formControlName","backupCount","type","number","min","0"],[3,"value"],[1,"first-capital",3,"value"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2)(3,"mat-form-field",3)(4,"mat-label",4),t.ɵɵtext(5,"gateway.logs.date-format"),t.ɵɵelementEnd(),t.ɵɵelement(6,"input",5),t.ɵɵtemplate(7,g5,3,3,"mat-error",6),t.ɵɵelementStart(8,"mat-icon",7),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",3)(12,"mat-label",4),t.ɵɵtext(13,"gateway.logs.log-format"),t.ɵɵelementEnd(),t.ɵɵelement(14,"textarea",8),t.ɵɵtemplate(15,f5,3,3,"mat-error",6),t.ɵɵelementStart(16,"mat-icon",7),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"info_outlined "),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(19,"div",9)(20,"mat-expansion-panel",10)(21,"mat-expansion-panel-header",11)(22,"mat-panel-title")(23,"mat-slide-toggle",12),t.ɵɵlistener("click",(function(e){return e.stopPropagation()})),t.ɵɵelementStart(24,"mat-label")(25,"div",13),t.ɵɵpipe(26,"translate"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(29,"mat-form-field",3)(30,"mat-label",4),t.ɵɵtext(31,"gateway.logs.level"),t.ɵɵelementEnd(),t.ɵɵelementStart(32,"mat-select",14),t.ɵɵtemplate(33,y5,2,2,"mat-option",15),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(34,"div",16)(35,"div",17),t.ɵɵtext(36,"gateway.logs.local"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"tb-toggle-select",18),t.ɵɵtemplate(38,v5,2,2,"tb-toggle-option",19),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(39,20),t.ɵɵelementStart(40,"div",21)(41,"mat-form-field",22)(42,"mat-label",4),t.ɵɵtext(43,"gateway.logs.level"),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"mat-select",14),t.ɵɵtemplate(45,x5,2,2,"mat-option",15),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"mat-form-field",22)(47,"mat-label",4),t.ɵɵtext(48,"gateway.logs.file-path"),t.ɵɵelementEnd(),t.ɵɵelement(49,"input",23),t.ɵɵtemplate(50,b5,3,3,"mat-error",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(51,"div",21)(52,"div",24)(53,"mat-form-field",22)(54,"mat-label",4),t.ɵɵtext(55,"gateway.logs.saving-period"),t.ɵɵelementEnd(),t.ɵɵelement(56,"input",25),t.ɵɵtemplate(57,w5,3,3,"mat-error",6)(58,S5,3,3,"mat-error",6),t.ɵɵelementEnd(),t.ɵɵelementStart(59,"mat-form-field",26)(60,"mat-select",27),t.ɵɵtemplate(61,C5,3,4,"mat-option",15),t.ɵɵpipe(62,"keyvalue"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(63,"mat-form-field",22)(64,"mat-label",4),t.ɵɵtext(65,"gateway.logs.backup-count"),t.ɵɵelementEnd(),t.ɵɵelement(66,"input",28),t.ɵɵtemplate(67,_5,3,3,"mat-error",6)(68,T5,3,3,"mat-error",6),t.ɵɵelementStart(69,"mat-icon",7),t.ɵɵpipe(70,"translate"),t.ɵɵtext(71,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.logsFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.logsFormGroup.get("dateFormat").hasError("required")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,21,"gateway.hints.date-form")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.logsFormGroup.get("logFormat").hasError("required")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,23,"gateway.hints.log-format")),t.ɵɵadvance(4),t.ɵɵproperty("expanded",n.showRemoteLogsControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.showRemoteLogsControl),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(26,25,"gateway.hints.remote-log")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(28,27,"gateway.logs.remote")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.remoteLogLevel),t.ɵɵadvance(4),t.ɵɵproperty("formControl",n.logSelector),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.localLogsConfigs),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.getLogFormGroup(n.logSelector.value)),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.gatewayLogLevel),t.ɵɵadvance(5),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".filePath").hasError("required")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".savingTime").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".savingTime").hasError("min")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(62,29,n.logSavingPeriods)),t.ɵɵadvance(6),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".backupCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".backupCount").hasError("min")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(70,31,"gateway.hints.backup-count")))},dependencies:t.ɵɵgetComponentDepsFactory(I5,[se,_]),encapsulation:2})}}function E5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"security.clientId-required")," "))}function M5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",6),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"gateway.copy-client-id")),t.ɵɵproperty("copyText",e.usernameFormGroup.get("clientId").value)}}function k5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("clientId"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-client-id"))}function P5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"security.username-required")," "))}function D5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",6),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"gateway.copy-username")),t.ɵɵproperty("copyText",e.usernameFormGroup.get("username").value)}}function O5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("username"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-user-name"))}function A5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",6),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"gateway.copy-password")),t.ɵɵproperty("copyText",e.usernameFormGroup.get("password").value)}}function F5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("password"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-password"))}function R5(e,n){1&e&&(t.ɵɵelement(0,"tb-error",14),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("error",t.ɵɵpipeBind1(1,1,"device.client-id-or-user-name-necessary"))}function B5(e,n){1&e&&(t.ɵɵelement(0,"tb-error",14),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("error",t.ɵɵpipeBind1(1,1,"gateway.hints.username-required-with-password"))}class N5{constructor(e){this.fb=e,this.onChange=()=>{},this.initForm(),this.usernameFormGroup.valueChanges.pipe(In()).subscribe((e=>this.onChange(e)))}writeValue(e){this.usernameFormGroup.patchValue(e,{emitEvent:!1})}registerOnChange(e){this.onChange=e}registerOnTouched(e){}setDisabledState(e){e?this.usernameFormGroup.disable({emitEvent:!1}):this.usernameFormGroup.enable({emitEvent:!1})}validate(){return this.usernameFormGroup.valid?null:{usernameFormGroup:{valid:!1}}}initForm(){this.usernameFormGroup=this.createSecurityFormGroup()}createSecurityFormGroup(){return this.fb.group({clientId:[null,[me.pattern(/^[^.\s]+$/)]],username:[null,[me.pattern(/^[^.\s]+$/)]],password:[null,[me.pattern(/^[^.\s]+$/)]]},{validators:[this.atLeastOneRequired,this.usernameRequired]})}atLeastOneRequired(e){const t=e.get("clientId").value,n=e.get("username").value;return t||n?null:{atLeastOneRequired:!0}}usernameRequired(e){const t=e.get("username").value,n=e.get("password").value;return!t&&n?{usernameRequired:!0}:null}generate(e){this.usernameFormGroup.get(e).patchValue(ae(20))}static{this.ɵfac=function(e){return new(e||N5)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:N5,selectors:[["tb-gateway-username-configuration"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>N5)),multi:!0},{provide:he,useExisting:c((()=>N5)),multi:!0}]),t.ɵɵStandaloneFeature],decls:33,vars:17,consts:[[3,"formGroup"],[1,"xs:flex-col","no-border","no-padding","tb-standard-fields","flex","gap-2"],["appearance","outline",1,"flex","flex-1"],["translate",""],["matInput","","formControlName","clientId"],[4,"ngIf"],["matSuffix","","miniButton","false","tooltipPosition","above","icon","content_copy",3,"copyText","tooltipText"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"matTooltip"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["matInput","","formControlName","username"],["appearance","outline","subscriptSizing","dynamic",2,"width","100%"],["matInput","","formControlName","password"],["class","block",3,"error",4,"ngIf"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"click","matTooltip"],[1,"block",3,"error"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"section",0)(1,"div",1)(2,"mat-form-field",2)(3,"mat-label",3),t.ɵɵtext(4,"security.clientId"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",4),t.ɵɵtemplate(6,E5,3,3,"mat-error",5)(7,M5,2,4,"tb-copy-button",6)(8,k5,4,3,"button",7),t.ɵɵelementStart(9,"mat-icon",8),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",2)(13,"mat-label",3),t.ɵɵtext(14,"security.username"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",9),t.ɵɵtemplate(16,P5,3,3,"mat-error",5)(17,D5,2,4,"tb-copy-button",6)(18,O5,4,3,"button",7),t.ɵɵelementStart(19,"mat-icon",8),t.ɵɵpipe(20,"translate"),t.ɵɵtext(21,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"mat-form-field",10)(23,"mat-label",3),t.ɵɵtext(24,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelement(25,"input",11),t.ɵɵtemplate(26,A5,2,4,"tb-copy-button",6)(27,F5,4,3,"button",7),t.ɵɵelementStart(28,"mat-icon",8),t.ɵɵpipe(29,"translate"),t.ɵɵtext(30,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵtemplate(31,R5,2,3,"tb-error",12)(32,B5,2,3,"tb-error",12)),2&e&&(t.ɵɵproperty("formGroup",n.usernameFormGroup),t.ɵɵadvance(6),t.ɵɵproperty("ngIf",n.usernameFormGroup.get("clientId").hasError("required")),t.ɵɵadvance(),t.ɵɵconditional(n.usernameFormGroup.get("clientId").value?7:8),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,11,"gateway.hints.client-id")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.usernameFormGroup.get("username").hasError("required")),t.ɵɵadvance(),t.ɵɵconditional(n.usernameFormGroup.get("username").value?17:18),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(20,13,"gateway.hints.username")),t.ɵɵadvance(7),t.ɵɵconditional(n.usernameFormGroup.get("password").value?26:27),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(29,15,"gateway.hints.password")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.usernameFormGroup.hasError("atLeastOneRequired")&&n.usernameFormGroup.touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.usernameFormGroup.hasError("usernameRequired")&&n.usernameFormGroup.touched))},dependencies:t.ɵɵgetComponentDepsFactory(N5,[se,_]),encapsulation:2})}}class L5{constructor(e,t){this.deviceService=e,this.destroyRef=t,this.initialCredentialsSubject=new Ce(null)}get initialCredentials(){return this.initialCredentialsSubject.value}get initialCredentials$(){return this.initialCredentialsSubject.asObservable()}updateCredentials(e){let t={};switch(e.type){case e4.USERNAME_PASSWORD:this.shouldUpdateCredentials(e)&&(t=this.generateMqttCredentials(e));break;case e4.ACCESS_TOKEN:case e4.TLS_ACCESS_TOKEN:this.shouldUpdateAccessToken(e)&&(t={credentialsType:U.ACCESS_TOKEN,credentialsId:e.accessToken,credentialsValue:null})}return this.initialCredentialsSubject.next({...this.initialCredentials,...t}),Object.keys(t).length?this.deviceService.saveDeviceCredentials(this.initialCredentials):_e(null)}setInitialCredentials(e){this.deviceService.getDeviceCredentials(e.id).pipe(In(this.destroyRef)).subscribe((e=>{this.initialCredentialsSubject.next({...e,version:null})}))}shouldUpdateSecurityConfig(e){switch(e.type){case e4.USERNAME_PASSWORD:return this.shouldUpdateCredentials(e);case e4.ACCESS_TOKEN:case e4.TLS_ACCESS_TOKEN:return this.shouldUpdateAccessToken(e)}}credentialsToSecurityConfig(e){const t=e.credentialsType===U.MQTT_BASIC?e4.USERNAME_PASSWORD:e4.ACCESS_TOKEN;if(e.credentialsType!==U.MQTT_BASIC)return{type:t,accessToken:e.credentialsId};if(e.credentialsValue){const{clientId:n,userName:i,password:a}=JSON.parse(e.credentialsValue);return{type:t,clientId:n,username:i,password:a}}}shouldUpdateCredentials(e){if(this.initialCredentials.credentialsType!==U.MQTT_BASIC)return!0;const t=JSON.parse(this.initialCredentials.credentialsValue);return!(t.clientId===e.clientId&&t.userName===e.username&&t.password===e.password)}shouldUpdateAccessToken(e){return this.initialCredentials.credentialsType!==U.ACCESS_TOKEN||this.initialCredentials.credentialsId!==e.accessToken}generateMqttCredentials(e){const{clientId:t,username:n,password:i}=e,a={...t&&{clientId:t},...n&&{userName:n},...i&&{password:i}};return{credentialsType:U.MQTT_BASIC,credentialsValue:JSON.stringify(a)}}static{this.ɵfac=function(e){return new(e||L5)(t.ɵɵinject(K.DeviceService),t.ɵɵinject(t.DestroyRef))}}static{this.ɵprov=t.ɵɵdefineInjectable({token:L5,factory:L5.ɵfac})}}function V5(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e.value)," ")}}function q5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"security.access-token-required")," "))}function G5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",13),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext(2);t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"device.copy-access-token")),t.ɵɵproperty("copyText",e.securityFormGroup.get("accessToken").value)}}function z5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",16),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.generateAccessToken())})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-access-token"))}function U5(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field",9)(1,"mat-label",10),t.ɵɵtext(2,"security.access-token"),t.ɵɵelementEnd(),t.ɵɵelement(3,"input",11),t.ɵɵtemplate(4,q5,3,3,"mat-error",12)(5,G5,2,4,"tb-copy-button",13)(6,z5,4,3,"button",14),t.ɵɵelementStart(7,"mat-icon",15),t.ɵɵpipe(8,"translate"),t.ɵɵtext(9,"info_outlined "),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵproperty("ngIf",e.securityFormGroup.get("accessToken").hasError("required")),t.ɵɵadvance(),t.ɵɵconditional(e.securityFormGroup.get("accessToken").value?5:6),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,3,"gateway.hints.token"))}}function j5(e,n){1&e&&(t.ɵɵelement(0,"tb-file-input",17),t.ɵɵpipe(1,"translate"),t.ɵɵpipe(2,"translate"),t.ɵɵpipe(3,"translate")),2&e&&(t.ɵɵpropertyInterpolate("hint",t.ɵɵpipeBind1(1,5,"gateway.hints.ca-cert")),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,7,"security.ca-cert")),t.ɵɵpropertyInterpolate("dropLabel",t.ɵɵpipeBind1(3,9,"gateway.drop-file")),t.ɵɵproperty("allowedExtensions","pem,cert,key")("accept",".pem, application/pem,.cert, application/cert, .key,application/key"))}class H5{constructor(e,t,n){this.fb=e,this.cd=t,this.gatewayCredentialsService=n,this.initialized=new u,this.securityTypes=t4,this.onChange=()=>{},this.securityFormGroup=this.createSecurityFormGroup(),this.setupFormListeners()}ngAfterViewInit(){const{usernamePassword:e,...t}=this.securityFormGroup.value;this.initialized.emit({thingsboard:{security:e?{...t,...e}:t}})}writeValue(e){e?this.updateFormBySecurityConfig(e):this.updateFormBySecurityConfig(this.gatewayCredentialsService.credentialsToSecurityConfig(this.gatewayCredentialsService.initialCredentials))}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.securityFormGroup.valid?null:{securityFormGroup:{valid:!1}}}updateFormBySecurityConfig(e){const{clientId:t,username:n,password:i,...a}=e??{};a?.type===e4.USERNAME_PASSWORD?this.securityFormGroup.patchValue({...a,usernamePassword:{clientId:t,username:n,password:i}},{emitEvent:!1}):this.securityFormGroup.patchValue(a,{emitEvent:!1}),this.toggleBySecurityType(this.securityFormGroup.get("type").value)}createSecurityFormGroup(){return this.fb.group({type:[e4.ACCESS_TOKEN,[me.required]],accessToken:[null,[me.required,me.pattern(/^[^.\s]+$/)]],caCert:[null,[me.required]],usernamePassword:[]})}setupFormListeners(){this.securityFormGroup.valueChanges.pipe(In()).subscribe((({usernamePassword:e,...t})=>{this.onChange(e?{...t,...e}:t)})),this.securityFormGroup.get("type").valueChanges.pipe(In()).subscribe((e=>{this.toggleBySecurityType(e)})),this.securityFormGroup.get("caCert").valueChanges.pipe(In()).subscribe((()=>this.cd.detectChanges()))}toggleBySecurityType(e){switch(this.securityFormGroup.disable({emitEvent:!1}),this.securityFormGroup.get("type").enable({emitEvent:!1}),e){case e4.ACCESS_TOKEN:this.securityFormGroup.get("accessToken").enable({emitEvent:!1});break;case e4.TLS_PRIVATE_KEY:this.securityFormGroup.get("caCert").enable({emitEvent:!1});break;case e4.TLS_ACCESS_TOKEN:this.securityFormGroup.get("accessToken").enable({emitEvent:!1}),this.securityFormGroup.get("caCert").enable({emitEvent:!1});break;case e4.USERNAME_PASSWORD:this.securityFormGroup.get("usernamePassword").enable({emitEvent:!1})}}generateAccessToken(){this.securityFormGroup.get("accessToken").patchValue(ae(20))}static{this.ɵfac=function(e){return new(e||H5)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(L5))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:H5,selectors:[["tb-gateway-security-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>H5)),multi:!0},{provide:he,useExisting:c((()=>H5)),multi:!0}]),t.ɵɵStandaloneFeature],decls:10,vars:8,consts:[[1,"tb-form-panel"],["translate","",1,"tb-form-panel-title"],[3,"formGroup"],["formControlName","type",1,"toggle-group","flex"],[3,"value",4,"ngFor","ngForOf"],["appearance","outline",4,"ngIf"],["formControlName","usernamePassword"],["formControlName","caCert",3,"hint","label","allowedExtensions","accept","dropLabel",4,"ngIf"],[3,"value"],["appearance","outline"],["translate",""],["matInput","","formControlName","accessToken"],[4,"ngIf"],["matSuffix","","miniButton","false","tooltipPosition","above","icon","content_copy",3,"copyText","tooltipText"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"matTooltip"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"click","matTooltip"],["formControlName","caCert",3,"hint","label","allowedExtensions","accept","dropLabel"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2,"security.security"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(3,2),t.ɵɵelementStart(4,"tb-toggle-select",3),t.ɵɵtemplate(5,V5,3,4,"tb-toggle-option",4),t.ɵɵpipe(6,"keyvalue"),t.ɵɵelementEnd(),t.ɵɵtemplate(7,U5,10,5,"mat-form-field",5),t.ɵɵelement(8,"tb-gateway-username-configuration",6),t.ɵɵtemplate(9,j5,4,11,"tb-file-input",7),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(3),t.ɵɵproperty("formGroup",n.securityFormGroup),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(6,6,n.securityTypes)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.securityFormGroup.get("type").value.toLowerCase().includes("accesstoken")),t.ɵɵadvance(),t.ɵɵclassProp("hidden","usernamePassword"!==n.securityFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.securityFormGroup.get("type").value.toLowerCase().includes("tls")))},dependencies:t.ɵɵgetComponentDepsFactory(H5,[se,_,N5]),encapsulation:2})}}const W5=["configGroup"];function $5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-host-required")))}function K5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-required")))}function Y5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-min")))}function X5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-max")))}function Z5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-pattern")))}function Q5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-security-configuration",20),t.ɵɵlistener("initialized",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext(2);return t.ɵɵresetView(i.onInitialized(n))})),t.ɵɵelementEnd()}}function J5(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",21),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("isExpansionMode",!0)("isConfig",!0)("defaultValue",e.ReportStrategyDefaultValue.Gateway)}}function e6(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",7)(1,"div",8)(2,"div",9),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"mat-slide-toggle",10),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",9),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-slide-toggle",11),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"div",12)(13,"mat-form-field",13)(14,"mat-label",14),t.ɵɵtext(15,"gateway.thingsboard-host"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",15),t.ɵɵelementStart(17,"mat-icon",16),t.ɵɵpipe(18,"translate"),t.ɵɵtext(19,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(20,$5,3,3,"mat-error"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",13)(22,"mat-label",14),t.ɵɵtext(23,"gateway.thingsboard-port"),t.ɵɵelementEnd(),t.ɵɵelement(24,"input",17),t.ɵɵtemplate(25,K5,3,3,"mat-error")(26,Y5,3,3,"mat-error")(27,X5,3,3,"mat-error")(28,Z5,3,3,"mat-error"),t.ɵɵelementStart(29,"mat-icon",16),t.ɵɵpipe(30,"translate"),t.ɵɵtext(31,"info_outlined "),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(32,Q5,1,0,"tb-gateway-security-configuration",18),t.ɵɵpipe(33,"async"),t.ɵɵtemplate(34,J5,1,3,"tb-report-strategy",19),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.remote-configuration")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,12,"gateway.remote-configuration")," "),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(8,14,"gateway.hints.remote-shell")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,16,"gateway.remote-shell")," "),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,18,"gateway.hints.host")),t.ɵɵadvance(3),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.host").hasError("required")?20:-1),t.ɵɵadvance(5),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.port").hasError("required")?25:e.basicFormGroup.get("thingsboard.port").hasError("min")?26:e.basicFormGroup.get("thingsboard.port").hasError("max")?27:e.basicFormGroup.get("thingsboard.port").hasError("pattern")?28:-1),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(30,20,"gateway.hints.port")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",t.ɵɵpipeBind1(33,22,e.initialCredentials$)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.withReportStrategy)}}function t6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-slide-toggle",23)(1,"mat-label",33),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(5,"mat-slide-toggle",34)(6,"mat-label",33),t.ɵɵpipe(7,"translate"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,4,"gateway.hints.enable-general-statistics")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,6,"gateway.statistics.general-statistics")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(7,8,"gateway.hints.enable-custom-statistics")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,10,"gateway.statistics.custom-statistics")," "))}function n6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-slide-toggle",23),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.statistics")," "))}function i6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.send-period-required")))}function a6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.send-period-min")))}function r6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.send-period-pattern")))}function o6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.custom-send-period-required")))}function s6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.custom-send-period-min")))}function l6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.custom-send-period-pattern")))}function p6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-required")))}function c6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-pattern")," "))}function d6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-already-exists")))}function u6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-required")))}function m6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-min")))}function h6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-pattern")))}function g6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.command-required")))}function f6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.command-pattern")))}function y6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",35)(1,"section",36)(2,"section",37)(3,"mat-form-field",38)(4,"mat-label",14),t.ɵɵtext(5,"gateway.statistics.name"),t.ɵɵelementEnd(),t.ɵɵelement(6,"input",39),t.ɵɵtemplate(7,p6,3,3,"mat-error")(8,c6,3,3,"mat-error")(9,d6,3,3,"mat-error"),t.ɵɵelementStart(10,"mat-icon",16),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(13,"mat-form-field",38)(14,"mat-label",14),t.ɵɵtext(15,"gateway.statistics.timeout"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",40),t.ɵɵtemplate(17,u6,3,3,"mat-error")(18,m6,3,3,"mat-error")(19,h6,3,3,"mat-error"),t.ɵɵelementStart(20,"mat-icon",16),t.ɵɵpipe(21,"translate"),t.ɵɵtext(22,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(23,"section")(24,"mat-form-field",41)(25,"mat-label",14),t.ɵɵtext(26,"gateway.statistics.command"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",42),t.ɵɵtemplate(28,g6,3,3,"mat-error")(29,f6,3,3,"mat-error"),t.ɵɵelementStart(30,"mat-icon",16),t.ɵɵpipe(31,"translate"),t.ɵɵtext(32,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(33,"section")(34,"mat-expansion-panel",43)(35,"mat-expansion-panel-header")(36,"mat-panel-title")(37,"div",28),t.ɵɵtext(38,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(39,"mat-form-field",38)(40,"mat-label",14),t.ɵɵtext(41,"gateway.statistics.install-cmd"),t.ɵɵelementEnd(),t.ɵɵelement(42,"input",44),t.ɵɵelementStart(43,"mat-icon",45),t.ɵɵpipe(44,"translate"),t.ɵɵtext(45,"info_outlined "),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(46,"button",46),t.ɵɵpipe(47,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.removeCommandControl(i,n))})),t.ɵɵelementStart(48,"mat-icon"),t.ɵɵtext(49,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.index,a=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("formGroupName",i),t.ɵɵadvance(6),t.ɵɵconditional(e.get("attributeOnGateway").hasError("required")?7:e.get("attributeOnGateway").hasError("pattern")?8:e.get("attributeOnGateway").hasError("duplicateName")?9:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,10,"gateway.hints.attribute")),t.ɵɵadvance(7),t.ɵɵconditional(e.get("timeout").hasError("required")?17:e.get("timeout").hasError("min")?18:e.get("timeout").hasError("pattern")?19:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(21,12,"gateway.hints.timeout")),t.ɵɵadvance(8),t.ɵɵconditional(e.get("command").hasError("required")?28:e.get("command").hasError("pattern")?29:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(31,14,"gateway.hints.command")),t.ɵɵadvance(13),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(44,16,"gateway.hints.install-cmd")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(47,18,"gateway.statistics.remove")),t.ɵɵproperty("disabled",!a.basicFormGroup.get("thingsboard.remoteConfiguration").value)}}function v6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",22),t.ɵɵtemplate(2,t6,10,12)(3,n6,3,3,"mat-slide-toggle",23),t.ɵɵelementStart(4,"mat-form-field",24)(5,"mat-label",14),t.ɵɵtext(6,"gateway.statistics.send-period"),t.ɵɵelementEnd(),t.ɵɵelement(7,"input",25),t.ɵɵtemplate(8,i6,3,3,"mat-error")(9,a6,3,3,"mat-error")(10,r6,3,3,"mat-error"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",24)(12,"mat-label",14),t.ɵɵtext(13,"gateway.statistics.custom-send-period"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",26),t.ɵɵtemplate(15,o6,3,3,"mat-error")(16,s6,3,3,"mat-error")(17,l6,3,3,"mat-error"),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",27)(19,"div",28),t.ɵɵtext(20,"gateway.statistics.commands"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",29),t.ɵɵtext(22,"gateway.hints.commands"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(23,30),t.ɵɵtemplate(24,y6,50,20,"div",31),t.ɵɵelementStart(25,"button",32),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.addCommand())})),t.ɵɵtext(26),t.ɵɵpipe(27,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵconditional(e.hasUpdatedStatistics?2:3),t.ɵɵadvance(6),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.statistics.statsSendPeriodInSeconds").hasError("required")?8:e.basicFormGroup.get("thingsboard.statistics.statsSendPeriodInSeconds").hasError("min")?9:e.basicFormGroup.get("thingsboard.statistics.statsSendPeriodInSeconds").hasError("pattern")?10:-1),t.ɵɵadvance(7),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.statistics.customStatsSendPeriodInSeconds").hasError("required")?15:e.basicFormGroup.get("thingsboard.statistics.customStatsSendPeriodInSeconds").hasError("min")?16:e.basicFormGroup.get("thingsboard.statistics.customStatsSendPeriodInSeconds").hasError("pattern")?17:-1),t.ɵɵadvance(9),t.ɵɵproperty("ngForOf",e.commandFormArray().controls),t.ɵɵadvance(),t.ɵɵproperty("disabled",!e.basicFormGroup.get("thingsboard.remoteConfiguration").value),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(27,6,"gateway.statistics.add")," ")}}function x6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-timeout-seconds-required")))}function b6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-timeout-seconds-min")))}function w6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-timeout-seconds-pattern")))}function S6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-check-period-seconds-required")))}function C6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-check-period-seconds-min")))}function _6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-check-period-seconds-pattern")))}function T6(e,n){if(1&e&&(t.ɵɵelementStart(0,"section",37)(1,"mat-form-field",38)(2,"mat-label",14),t.ɵɵtext(3,"gateway.inactivity-timeout-seconds"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",56),t.ɵɵtemplate(5,x6,3,3,"mat-error")(6,b6,3,3,"mat-error")(7,w6,3,3,"mat-error"),t.ɵɵelementStart(8,"mat-icon",16),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",38)(12,"mat-label",14),t.ɵɵtext(13,"gateway.inactivity-check-period-seconds"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",57),t.ɵɵtemplate(15,S6,3,3,"mat-error")(16,C6,3,3,"mat-error")(17,_6,3,3,"mat-error"),t.ɵɵelementStart(18,"mat-icon",16),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"info_outlined "),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds").hasError("required")?5:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds").hasError("min")?6:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds").hasError("pattern")?7:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,4,"gateway.hints.inactivity-timeout")),t.ɵɵadvance(7),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds").hasError("required")?15:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds").hasError("min")?16:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds").hasError("pattern")?17:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,6,"gateway.hints.inactivity-period"))}}function I6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.min-pack-send-delay-required")))}function E6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.min-pack-send-delay-min")))}function M6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.min-pack-send-delay-pattern")))}function k6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.mqtt-qos-required")))}function P6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.mqtt-qos-range")))}function D6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.mqtt-qos-range")))}function O6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.check-connectors-configuration-required")))}function A6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.check-connectors-configuration-min")))}function F6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.check-connectors-configuration-pattern")))}function R6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.max-payload-size-bytes-required")))}function B6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.max-payload-size-bytes-min")))}function N6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.max-payload-size-bytes-pattern")))}function L6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.min-pack-size-to-send-required")))}function V6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.min-pack-size-to-send-min")))}function q6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.min-pack-size-to-send-pattern")))}function G6(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",7)(1,"div",47)(2,"div",9),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"mat-slide-toggle",48),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(7,T6,21,8,"section",49),t.ɵɵelementEnd(),t.ɵɵelementStart(8,"div",8)(9,"div",28),t.ɵɵtext(10,"gateway.advanced"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"section",37)(12,"mat-form-field",38)(13,"mat-label",14),t.ɵɵtext(14,"gateway.min-pack-send-delay"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",50),t.ɵɵtemplate(16,I6,3,3,"mat-error")(17,E6,3,3,"mat-error")(18,M6,3,3,"mat-error"),t.ɵɵelementStart(19,"mat-icon",16),t.ɵɵpipe(20,"translate"),t.ɵɵtext(21,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"mat-form-field",38)(23,"mat-label",14),t.ɵɵtext(24,"gateway.mqtt-qos"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-select",51)(26,"mat-option",52),t.ɵɵtext(27,"0"),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-option",52),t.ɵɵtext(29,"1"),t.ɵɵelementEnd()(),t.ɵɵtemplate(30,k6,3,3,"mat-error")(31,P6,3,3,"mat-error")(32,D6,3,3,"mat-error"),t.ɵɵelementStart(33,"mat-icon",16),t.ɵɵpipe(34,"translate"),t.ɵɵtext(35,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(36,"section",37)(37,"mat-form-field",38)(38,"mat-label",14),t.ɵɵtext(39,"gateway.statistics.check-connectors-configuration"),t.ɵɵelementEnd(),t.ɵɵelement(40,"input",53),t.ɵɵtemplate(41,O6,3,3,"mat-error")(42,A6,3,3,"mat-error")(43,F6,3,3,"mat-error"),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"mat-form-field",38)(45,"mat-label",14),t.ɵɵtext(46,"gateway.statistics.max-payload-size-bytes"),t.ɵɵelementEnd(),t.ɵɵelement(47,"input",54),t.ɵɵtemplate(48,R6,3,3,"mat-error")(49,B6,3,3,"mat-error")(50,N6,3,3,"mat-error"),t.ɵɵelementStart(51,"mat-icon",16),t.ɵɵpipe(52,"translate"),t.ɵɵtext(53,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(54,"section",37)(55,"mat-form-field",38)(56,"mat-label",14),t.ɵɵtext(57,"gateway.statistics.min-pack-size-to-send"),t.ɵɵelementEnd(),t.ɵɵelement(58,"input",55),t.ɵɵtemplate(59,L6,3,3,"mat-error")(60,V6,3,3,"mat-error")(61,q6,3,3,"mat-error"),t.ɵɵelementStart(62,"mat-icon",16),t.ɵɵpipe(63,"translate"),t.ɵɵtext(64,"info_outlined "),t.ɵɵelementEnd()()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassProp("no-padding-bottom",e.basicFormGroup.get("thingsboard.checkingDeviceActivity.checkDeviceInactivity").value),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,16,"gateway.hints.check-device-activity")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,18,"gateway.checking-device-activity")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.basicFormGroup.get("thingsboard.checkingDeviceActivity.checkDeviceInactivity").value),t.ɵɵadvance(9),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.minPackSendDelayMS").hasError("required")?16:e.basicFormGroup.get("thingsboard.minPackSendDelayMS").hasError("min")?17:e.basicFormGroup.get("thingsboard.minPackSendDelayMS").hasError("pattern")?18:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(20,20,"gateway.hints.minimal-pack-delay")),t.ɵɵadvance(7),t.ɵɵproperty("value",0),t.ɵɵadvance(2),t.ɵɵproperty("value",1),t.ɵɵadvance(2),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.qos").hasError("required")?30:e.basicFormGroup.get("thingsboard.qos").hasError("min")?31:e.basicFormGroup.get("thingsboard.qos").hasError("max")?32:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(34,22,"gateway.hints.qos")),t.ɵɵadvance(8),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.checkConnectorsConfigurationInSeconds").hasError("required")?41:e.basicFormGroup.get("thingsboard.checkConnectorsConfigurationInSeconds").hasError("min")?42:e.basicFormGroup.get("thingsboard.checkConnectorsConfigurationInSeconds").hasError("pattern")?43:-1),t.ɵɵadvance(7),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.maxPayloadSizeBytes").hasError("required")?48:e.basicFormGroup.get("thingsboard.maxPayloadSizeBytes").hasError("min")?49:e.basicFormGroup.get("thingsboard.maxPayloadSizeBytes").hasError("pattern")?50:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(52,24,"gateway.hints.max-payload-size-bytes")),t.ɵɵadvance(8),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.minPackSizeToSend").hasError("required")?59:e.basicFormGroup.get("thingsboard.minPackSizeToSend").hasError("min")?60:e.basicFormGroup.get("thingsboard.minPackSizeToSend").hasError("pattern")?61:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(63,26,"gateway.hints.min-pack-size-to-send"))}}class z6{constructor(e,t,n,i,a){this.fb=e,this.deviceService=t,this.gatewayCredentialsService=n,this.destroyRef=i,this.dialog=a,this.gatewayVersion=ct.Legacy,this.dialogMode=!1,this.withReportStrategy=!1,this.initialized=new u,this.ReportStrategyDefaultValue=on,this.initialCredentials$=this.gatewayCredentialsService.initialCredentials$,this.onChange=()=>{},this.destroy$=new we,this.initBasicFormGroup(),this.observeFormChanges(),this.basicFormGroup.valueChanges.pipe(Me(this.destroy$)).subscribe((()=>{this.onChange(this.basicFormGroup.getRawValue())}))}ngOnChanges(e){e.withReportStrategy&&!e.withReportStrategy.firstChange&&this.withReportStrategy&&this.basicFormGroup.get("thingsboard.reportStrategy").enable({emitEvent:!1}),e.gatewayVersion?.previousValue!==e.gatewayVersion.currentValue&&this.onVersionChange()}ngAfterViewInit(){this.defaultTab&&(this.configGroup.selectedIndex=$3[this.defaultTab])}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){this.basicFormGroup.patchValue(e,{emitEvent:!1});const t=this.basicFormGroup.get("thingsboard.reportStrategy"),n=e?.thingsboard?.reportStrategy;"DISABLED"===n?.type?t.disable({emitEvent:!1}):this.withReportStrategy&&t.enable({emitEvent:!1});const i=e?.thingsboard?.statistics?.commands??[];this.commandFormArray().clear({emitEvent:!1}),i.forEach((e=>this.addCommand(e,!1)))}validate(){return this.basicFormGroup.valid?null:{basicFormGroup:{valid:!1}}}commandFormArray(){return this.basicFormGroup.get("thingsboard.statistics.commands")}removeCommandControl(e,t){""!==t.pointerType&&(this.commandFormArray().removeAt(e),this.basicFormGroup.markAsDirty())}openConfigurationConfirmDialog(){this.deviceService.getDevice(this.device.id).pipe(Me(this.destroy$)).subscribe((e=>{this.dialog.open(fW,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{gatewayName:e.name}}).afterClosed().pipe(Ne(1)).subscribe((e=>{e||this.basicFormGroup.get("thingsboard.remoteConfiguration").setValue(!0,{emitEvent:!1})}))}))}addCommand(e,t=!0){const{attributeOnGateway:n=null,command:i=null,timeout:a=10,installCmd:r=""}=e||{},o=this.fb.group({attributeOnGateway:[n,[me.required,me.pattern(pn),this.uniqNameRequired()]],command:[i,[me.required,me.pattern(/^(?=\S).*\S$/)]],timeout:[a,[me.required,me.min(1),me.pattern(ln),me.pattern(/^[^.\s]+$/)]],installCmd:[r,me.pattern(pn)]});this.commandFormArray().push(o,{emitEvent:t})}uniqNameRequired(){return e=>{const t=e.value?.trim().toLowerCase();return e.dirty&&t&&this.commandFormArray().value.some((e=>e.attributeOnGateway?.toLowerCase()===t))?{duplicateName:{valid:!1}}:null}}onInitialized(e){this.basicFormGroup.patchValue(e,{emitEvent:!1}),this.initialized.emit(this.basicFormGroup.value)}initBasicFormGroup(){this.basicFormGroup=this.fb.group({thingsboard:this.initThingsboardFormGroup(),storage:[],grpc:[],connectors:this.fb.array([]),logs:[]})}initThingsboardFormGroup(){return this.fb.group({host:[window.location.hostname,[me.required,me.pattern(/^[^\s]+$/)]],port:[1883,[me.required,me.min(1),me.max(65535),me.pattern(ln)]],remoteShell:[!1],remoteConfiguration:[!0],checkConnectorsConfigurationInSeconds:[60,[me.required,me.min(1),me.pattern(ln)]],statistics:this.fb.group({enable:[!0],enableCustom:[{value:!1,disabled:!0}],statsSendPeriodInSeconds:[3600,[me.required,me.min(60),me.pattern(ln)]],customStatsSendPeriodInSeconds:[3600,[me.required,me.min(60),me.pattern(ln)]],commands:this.fb.array([])}),maxPayloadSizeBytes:[8196,[me.required,me.min(100),me.pattern(ln)]],minPackSendDelayMS:[50,[me.required,me.min(10),me.pattern(ln)]],minPackSizeToSend:[500,[me.required,me.min(100),me.pattern(ln)]],handleDeviceRenaming:[!0],checkingDeviceActivity:this.initCheckingDeviceActivityFormGroup(),security:[],qos:[1],reportStrategy:[null]})}initCheckingDeviceActivityFormGroup(){return this.fb.group({checkDeviceInactivity:[!1],inactivityTimeoutSeconds:[300,[me.min(1),me.pattern(ln)]],inactivityCheckPeriodSeconds:[10,[me.min(1),me.pattern(ln)]]})}onVersionChange(){if(this.hasUpdatedStatistics=Ya.parseVersion(this.gatewayVersion)>=Ya.parseVersion(ct.v3_7_3),this.hasUpdatedStatistics){const e=this.basicFormGroup.get("thingsboard.statistics.enableCustom");e.enable({emitEvent:!1}),this.basicFormGroup.get("thingsboard.statistics.enable").valueChanges.pipe(In(this.destroyRef)).subscribe((t=>{t||e.patchValue(!1,{emitEvent:!1}),e[t?"enable":"disable"]({emitEvent:!1})}))}}observeFormChanges(){this.observeRemoteConfigurationChanges(),this.observeDeviceActivityChanges()}observeRemoteConfigurationChanges(){this.basicFormGroup.get("thingsboard.remoteConfiguration").valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{e||this.openConfigurationConfirmDialog()}))}observeDeviceActivityChanges(){const e=this.basicFormGroup.get("thingsboard.checkingDeviceActivity");e.get("checkDeviceInactivity").valueChanges.pipe(Me(this.destroy$)).subscribe((t=>{e.updateValueAndValidity();const n=[me.min(1),me.required,me.pattern(ln)];t?(e.get("inactivityTimeoutSeconds").setValidators(n),e.get("inactivityCheckPeriodSeconds").setValidators(n)):(e.get("inactivityTimeoutSeconds").clearValidators(),e.get("inactivityCheckPeriodSeconds").clearValidators()),e.get("inactivityTimeoutSeconds").updateValueAndValidity({emitEvent:!1}),e.get("inactivityCheckPeriodSeconds").updateValueAndValidity({emitEvent:!1})}))}static{this.ɵfac=function(e){return new(e||z6)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(K.DeviceService),t.ɵɵdirectiveInject(L5),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(je.MatDialog))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:z6,selectors:[["tb-gateway-basic-configuration"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(W5,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.configGroup=e.first)}},inputs:{device:"device",defaultTab:"defaultTab",gatewayVersion:"gatewayVersion",dialogMode:"dialogMode",withReportStrategy:"withReportStrategy"},outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>z6)),multi:!0},{provide:he,useExisting:c((()=>z6)),multi:!0}]),t.ɵɵNgOnChangesFeature,t.ɵɵStandaloneFeature],decls:20,vars:21,consts:[["configGroup",""],[1,"tab-group-block",3,"formGroup"],[3,"label"],["matTabContent",""],["formControlName","logs",1,"configuration-block",3,"initialized"],["formControlName","storage",3,"initialized"],["formControlName","grpc",3,"initialized"],["formGroupName","thingsboard",1,"mat-content","mat-padding","configuration-block"],[1,"tb-form-panel","no-padding-bottom"],[1,"tb-form-row","no-border","no-padding",3,"tb-hint-tooltip-icon"],["color","primary","formControlName","remoteConfiguration",1,"mat-slide"],["color","primary","formControlName","remoteShell",1,"mat-slide"],[1,"no-border","no-padding","tb-standard-fields","xs:flex-col","flex","gap-2"],["appearance","outline",1,"flex","flex-1"],["translate",""],["matInput","","formControlName","host"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["matInput","","formControlName","port","type","number","min","0"],["formControlName","security",3,"initialized",4,"ngIf"],["class","tb-form-panel","formControlName","reportStrategy",3,"isExpansionMode","isConfig","defaultValue",4,"ngIf"],["formControlName","security",3,"initialized"],["formControlName","reportStrategy",1,"tb-form-panel",3,"isExpansionMode","isConfig","defaultValue"],["formGroupName","statistics",1,"tb-form-panel","no-padding-bottom"],["color","primary","formControlName","enable",1,"mat-slide"],["appearance","outline"],["matInput","","formControlName","statsSendPeriodInSeconds","type","number","min","60"],["matInput","","formControlName","customStatsSendPeriodInSeconds","type","number","min","60"],[1,"tb-form-panel"],["translate","",1,"tb-form-panel-title"],["translate","",1,"tb-form-panel-hint"],["formGroupName","statistics"],["formArrayName","commands","class","statistics-container flex flex-row",4,"ngFor","ngForOf"],["mat-stroked-button","","color","primary","type","button",2,"width","fit-content",3,"click","disabled"],[3,"tb-hint-tooltip-icon"],["color","primary","formControlName","enableCustom",1,"mat-slide"],["formArrayName","commands",1,"statistics-container","flex","flex-row"],[1,"tb-form-panel","stroked","no-padding-bottom","no-gap","command-container",3,"formGroupName"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["matInput","","formControlName","attributeOnGateway"],["matInput","","formControlName","timeout","type","number","min","0"],["appearance","outline",1,"mat-block"],["matInput","","formControlName","command"],[1,"tb-settings","pb-4"],["matInput","","formControlName","installCmd"],["matIconSuffix","",1,"cursor-pointer",3,"matTooltip"],["mat-icon-button","","matTooltipPosition","above",1,"tb-box-button",3,"click","disabled","matTooltip"],["formGroupName","checkingDeviceActivity",1,"tb-form-panel"],["color","primary","formControlName","checkDeviceInactivity",1,"mat-slide"],["class","tb-form-row no-border no-padding tb-standard-fields column-xs",4,"ngIf"],["matInput","","formControlName","minPackSendDelayMS","type","number","min","0"],["formControlName","qos"],[3,"value"],["matInput","","formControlName","checkConnectorsConfigurationInSeconds","type","number","min","0"],["matInput","","formControlName","maxPayloadSizeBytes","type","number","min","0"],["matInput","","formControlName","minPackSizeToSend","type","number","min","0"],["matInput","","formControlName","inactivityTimeoutSeconds","type","number","min","0"],["matInput","","type","number","min","0","formControlName","inactivityCheckPeriodSeconds"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-tab-group",1,0)(2,"mat-tab",2),t.ɵɵpipe(3,"translate"),t.ɵɵtemplate(4,e6,35,24,"ng-template",3),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-tab",2),t.ɵɵpipe(6,"translate"),t.ɵɵelementStart(7,"tb-gateway-logs-configuration",4),t.ɵɵlistener("initialized",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.onInitialized(i))})),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"mat-tab",2),t.ɵɵpipe(9,"translate"),t.ɵɵelementStart(10,"tb-gateway-storage-configuration",5),t.ɵɵlistener("initialized",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.onInitialized(i))})),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-tab",2),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"tb-gateway-grpc-configuration",6),t.ɵɵlistener("initialized",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.onInitialized(i))})),t.ɵɵelementEnd()(),t.ɵɵelementStart(14,"mat-tab",2),t.ɵɵpipe(15,"translate"),t.ɵɵtemplate(16,v6,28,8,"ng-template",3),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"mat-tab",2),t.ɵɵpipe(18,"translate"),t.ɵɵtemplate(19,G6,65,28,"ng-template",3),t.ɵɵelementEnd()()}2&e&&(t.ɵɵclassProp("dialog-mode",n.dialogMode),t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(3,9,"gateway.general")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(6,11,"gateway.logs.logs")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(9,13,"gateway.storage")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(12,15,"gateway.grpc")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(15,17,"gateway.statistics.statistics")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(18,19,"gateway.other")))},dependencies:t.ɵɵgetComponentDepsFactory(z6,[se,_,ni,K4,h5,I5,H5]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:grid;grid-template-rows:min-content minmax(auto,1fr) min-content}[_nghost-%COMP%] .configuration-block[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:16px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{grid-row:1;background:transparent;color:#000000de!important}[_nghost-%COMP%] .tab-group-block[_ngcontent-%COMP%]{min-width:0;height:100%;min-height:0;grid-row:2}[_nghost-%COMP%] .toggle-group[_ngcontent-%COMP%]{margin-right:auto}[_nghost-%COMP%] .first-capital[_ngcontent-%COMP%]{text-transform:capitalize}[_nghost-%COMP%] textarea[_ngcontent-%COMP%]{resize:none}[_nghost-%COMP%] .saving-period[_ngcontent-%COMP%]{flex:1}[_nghost-%COMP%] .statistics-container[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] .statistics-container[_ngcontent-%COMP%] .command-container[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] mat-form-field[_ngcontent-%COMP%] mat-error[_ngcontent-%COMP%]{display:none!important}[_nghost-%COMP%] mat-form-field[_ngcontent-%COMP%] mat-error[_ngcontent-%COMP%]:first-child{display:block!important}[_nghost-%COMP%] .pointer-event{pointer-events:all}[_nghost-%COMP%] .toggle-group span{padding:0 25px}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{color:#e0e0e0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix:hover{color:#9e9e9e}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex;align-items:center}']})}}e("GatewayBasicConfigurationComponent",z6),He([I()],z6.prototype,"dialogMode",void 0),He([I()],z6.prototype,"withReportStrategy",void 0);class U6{constructor(e){this.fb=e,this.destroy$=new we,this.advancedFormControl=this.fb.control(""),this.advancedFormControl.valueChanges.pipe(Me(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){this.advancedFormControl.reset(e,{emitEvent:!1})}validate(){return this.advancedFormControl.valid?null:{advancedFormControl:{valid:!1}}}static{this.ɵfac=function(e){return new(e||U6)(t.ɵɵdirectiveInject(de.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:U6,selectors:[["tb-gateway-advanced-configuration"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ue,useExisting:c((()=>U6)),multi:!0},{provide:he,useExisting:c((()=>U6)),multi:!0}]),t.ɵɵStandaloneFeature],decls:2,vars:4,consts:[["fillHeight","true","jsonRequired","",1,"flex","h-full","flex-col","p-2",3,"label","formControl"]],template:function(e,n){1&e&&(t.ɵɵelement(0,"tb-json-object-edit",0),t.ɵɵpipe(1,"translate")),2&e&&(t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(1,2,"gateway.configuration")),t.ɵɵproperty("formControl",n.advancedFormControl))},dependencies:t.ɵɵgetComponentDepsFactory(U6,[se,_]),encapsulation:2})}}function j6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",14),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(1,"mat-icon",15),t.ɵɵtext(2,"close"),t.ɵɵelementEnd()()}}function H6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-basic-configuration",16),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onInitialized(n))})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("device",e.device)("defaultTab",e.defaultTab)("gatewayVersion",e.gatewayVersion)("dialogMode",!!e.dialogRef)("withReportStrategy",t.ɵɵpipeBind1(1,5,e.gatewayVersion))}}function W6(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-advanced-configuration",10)}function $6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",17),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.cancel())})),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()}2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"action.cancel")," "))}e("GatewayAdvancedConfigurationComponent",U6);class K6{constructor(e,t,n,i,a){this.fb=e,this.attributeService=t,this.cd=n,this.gatewayCredentialsService=i,this.destroyRef=a,this.ConfigurationModes=an,this.gatewayConfigAttributeKeys=["general_configuration","grpc_configuration","logs_configuration","storage_configuration","RemoteLoggingLevel","mode"],this.useUpdatedLogs=!1,this.gatewayConfigGroup=this.fb.group({basicConfig:[],advancedConfig:[],mode:[an.BASIC]}),this.observeAlignConfigs()}ngAfterViewInit(){this.fetchConfigAttribute(this.device)}saveConfig(){const{mode:e,advancedConfig:t}=oe(this.removeEmpty(this.gatewayConfigGroup.value)),n={mode:e,...t};n.thingsboard.statistics.commands=Object.values(n.thingsboard.statistics.commands??[]),n.thingsboard.reportStrategy||(n.thingsboard.reportStrategy={type:"DISABLED"});const i=this.generateAttributes(n);this.attributeService.saveEntityAttributes(this.device,D.SHARED_SCOPE,i).pipe(Ve((e=>this.gatewayCredentialsService.updateCredentials(n.thingsboard.security))),In(this.destroyRef)).subscribe((()=>{this.dialogRef?this.dialogRef.close():(this.gatewayConfigGroup.markAsPristine(),this.cd.detectChanges())}))}onInitialized(e){this.gatewayConfigGroup.get("basicConfig").patchValue(e,{emitEvent:!1}),this.gatewayConfigGroup.get("advancedConfig").patchValue(e,{emitEvent:!1})}observeAlignConfigs(){this.gatewayConfigGroup.get("basicConfig").valueChanges.pipe(In(this.destroyRef)).subscribe((e=>{const t=this.gatewayConfigGroup.get("advancedConfig");W(t.value,e)||this.gatewayConfigGroup.get("mode").value!==an.BASIC||t.patchValue(e,{emitEvent:!1})})),this.gatewayConfigGroup.get("advancedConfig").valueChanges.pipe(In(this.destroyRef)).subscribe((e=>{const t=this.gatewayConfigGroup.get("basicConfig");W(t.value,e)||this.gatewayConfigGroup.get("mode").value!==an.ADVANCED||t.patchValue(e,{emitEvent:!1})}))}generateAttributes(e){const t=[],n=(e,n)=>{t.push({key:e,value:n})},i=(e,t)=>{t={...t,ts:(new Date).getTime()},n(e,t)};return n("RemoteLoggingLevel",e.logs?.logLevel??pt.NONE),delete e.connectors,n("logs_configuration",this.generateLogsFile(e.logs)),i("grpc_configuration",e.grpc),i("storage_configuration",e.storage),i("general_configuration",e.thingsboard),n("mode",e.mode),t}cancel(){this.dialogRef&&this.dialogRef.close()}removeEmpty(e){return Object.fromEntries(Object.entries(e).filter((([e,t])=>null!=t)).map((([e,t])=>[e,t===Object(t)?this.removeEmpty(t):t])))}generateLogsFile(e){const t={version:1,disable_existing_loggers:!1,formatters:{LogFormatter:{class:"logging.Formatter",format:e.logFormat,datefmt:e.dateFormat}},handlers:{consoleHandler:{class:"logging.StreamHandler",formatter:"LogFormatter",level:0,stream:"ext://sys.stdout"}},loggers:{...this.useUpdatedLogs?{}:{database:{handlers:["consoleHandler"],level:"DEBUG",propagate:!1}}},root:{level:"ERROR",handlers:["consoleHandler"]},ts:(new Date).getTime()};return this.addLocalLoggers(t,e?.local),t}addLocalLoggers(e,t){if(t)for(const n of Object.keys(t))e.handlers[n+"Handler"]=this.createHandlerObj(t[n],n),e.loggers[n]=this.createLoggerObj(t[n],n)}createHandlerObj(e,t){return{class:this.useUpdatedLogs?n4:i4,formatter:"LogFormatter",filename:`${e.filePath}/${t}.log`,backupCount:e.backupCount,interval:e.savingTime,when:e.savingPeriod,encoding:"utf-8"}}createLoggerObj(e,t){return{handlers:[`${t}Handler`,"consoleHandler"],level:e.logLevel,propagate:!1}}fetchConfigAttribute(e){e.id!==B&&this.attributeService.getEntityAttributes(e,D.CLIENT_SCOPE).pipe(Ge((t=>t.length?_e(t):this.attributeService.getEntityAttributes(e,D.SHARED_SCOPE,this.gatewayConfigAttributeKeys))),In(this.destroyRef)).subscribe((e=>{this.gatewayVersion=e.find((e=>"Version"===e.key))?.value,this.useUpdatedLogs=Ya.parseVersion(this.gatewayVersion??ct.Legacy)>=Ya.parseVersion(ct.v3_6_3),this.updateConfigs(e),this.cd.detectChanges()}))}updateConfigs(e){let t={},n=pt.NONE;this.gatewayCredentialsService.setInitialCredentials(this.device),e.forEach((e=>{switch(e.key){case"general_configuration":e.value&&(t={...t,thingsboard:e.value});break;case"grpc_configuration":e.value&&(t={...t,grpc:e.value});break;case"logs_configuration":e.value&&(t={...t,logs:this.logsToObj(e.value)});break;case"storage_configuration":e.value&&(t={...t,storage:e.value});break;case"mode":t={...t,mode:e.value??an.BASIC};break;case"RemoteLoggingLevel":e.value&&(n=e.value)}})),t.logs&&(t={...t,logs:{...t.logs,logLevel:n}}),t.thingsboard?.security?this.gatewayCredentialsService.initialCredentials$.pipe(Oe(Boolean),Ne(1),In(this.destroyRef)).subscribe((e=>{this.gatewayCredentialsService.shouldUpdateSecurityConfig(t.thingsboard.security)&&(t.thingsboard.security=this.gatewayCredentialsService.credentialsToSecurityConfig(e)),this.gatewayConfigGroup.get("basicConfig").patchValue(t,{emitEvent:!1}),this.gatewayConfigGroup.get("advancedConfig").patchValue(t,{emitEvent:!1})})):(this.gatewayConfigGroup.get("basicConfig").patchValue(t,{emitEvent:!1}),this.gatewayConfigGroup.get("advancedConfig").patchValue(t,{emitEvent:!1}))}logsToObj(e){const{format:t,datefmt:n}=e.formatters.LogFormatter;return{local:Object.keys(Y3).reduce(((t,n)=>{const i=e.handlers[`${n}Handler`]||{},a=e.loggers[n]||{};return t[n]={logLevel:a.level||pt.INFO,filePath:i.filename?.split(`/${n}`)[0]||"./logs",backupCount:i.backupCount||7,savingTime:i.interval||3,savingPeriod:i.when||Q3.days},t}),{}),logFormat:t,dateFormat:n}}static{this.ɵfac=function(e){return new(e||K6)(t.ɵɵdirectiveInject(de.FormBuilder),t.ɵɵdirectiveInject(K.AttributeService),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(L5),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:K6,selectors:[["tb-gateway-configuration"]],inputs:{device:"device",defaultTab:"defaultTab",dialogRef:"dialogRef"},standalone:!0,features:[t.ɵɵProvidersFeature([L5]),t.ɵɵStandaloneFeature],decls:24,vars:24,consts:[[1,"flex","size-full","max-w-full","flex-col",3,"formGroup"],["color","primary"],[1,"tb-flex","space-between","align-center","h-16"],["tbTruncateWithTooltip",""],[1,"flex","items-center"],["formControlName","mode",3,"appearance"],[3,"value"],["mat-icon-button","","type","button",3,"click",4,"ngIf"],[1,"content-wrapper","flex-1"],["formControlName","basicConfig",3,"device","defaultTab","gatewayVersion","dialogMode","withReportStrategy"],["formControlName","advancedConfig"],[1,"flex","h-full","items-center","justify-end","gap-2","pr-4"],["mat-button","","color","primary","type","button"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["formControlName","basicConfig",3,"initialized","device","defaultTab","gatewayVersion","dialogMode","withReportStrategy"],["mat-button","","color","primary","type","button",3,"click"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"mat-toolbar",1)(2,"div",2)(3,"h2")(4,"div",3),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",4)(8,"tb-toggle-select",5)(9,"tb-toggle-option",6),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"tb-toggle-option",6),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(15,j6,3,0,"button",7),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",8),t.ɵɵtemplate(17,H6,2,7,"tb-gateway-basic-configuration",9)(18,W6,1,0,"tb-gateway-advanced-configuration",10),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",11),t.ɵɵtemplate(20,$6,3,3,"button",12),t.ɵɵelementStart(21,"button",13),t.ɵɵlistener("click",(function(){return n.saveConfig()})),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.gatewayConfigGroup),t.ɵɵadvance(),t.ɵɵclassProp("page-header",!n.dialogRef),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,16,"gateway.gateway-configuration")),t.ɵɵadvance(3),t.ɵɵclassProp("dialog-toggle",!!n.dialogRef),t.ɵɵpropertyInterpolate("appearance",n.dialogRef?"stroked":"fill"),t.ɵɵadvance(),t.ɵɵproperty("value",n.ConfigurationModes.BASIC),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,18,"gateway.basic")," "),t.ɵɵadvance(2),t.ɵɵproperty("value",n.ConfigurationModes.ADVANCED),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,20,"gateway.advanced")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.dialogRef),t.ɵɵadvance(2),t.ɵɵconditional(n.gatewayConfigGroup.get("mode").value===n.ConfigurationModes.BASIC?17:18),t.ɵɵadvance(3),t.ɵɵconditional(n.dialogRef?20:-1),t.ɵɵadvance(),t.ɵɵproperty("disabled",n.gatewayConfigGroup.invalid||!n.gatewayConfigGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(23,22,"action.save")," "))},dependencies:t.ɵɵgetComponentDepsFactory(K6,[se,_,tr,z6,U6]),styles:['@charset "UTF-8";[_nghost-%COMP%]{overflow:hidden;padding:0!important;max-height:75vh;height:75vh;width:800px;max-width:80vw}@media screen and (max-width: 599px){[_nghost-%COMP%]{max-height:100%;height:100%;width:100%;max-width:100%}}[_nghost-%COMP%] .page-header.mat-toolbar[_ngcontent-%COMP%]{background:transparent;color:#000000de!important}[_nghost-%COMP%] .content-wrapper[_ngcontent-%COMP%]{min-height:calc(100% - 116px)}.dialog-toggle[_ngcontent-%COMP%] .mat-button-toggle-button{color:#ffffffbf}']})}}e("GatewayConfigurationComponent",K6);class Y6{constructor(){}static{this.ɵfac=function(e){return new(e||Y6)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Y6,selectors:[["tb-lib-styles-entry"]],standalone:!0,features:[t.ɵɵStandaloneFeature],decls:0,vars:0,template:function(e,t){},styles:['@charset "UTF-8";.gw-delete-icon-button{color:#0000008a}.gw-connector-table .mat-mdc-table{table-layout:fixed}.gw-chip-list .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center}.gw-table-toolbar.mat-mdc-table-toolbar.mat-mdc-table-toolbar{padding:0 8px}.see-example{display:flex}.tb-default .h-16{height:4rem}.tb-default .h-\\[500px\\]{height:500px}.tb-default .max-h-full{max-height:100%}.tb-default .min-h-10{min-height:2.5rem}.tb-default .\\!w-1\\/5{width:20%!important}.tb-default .w-1\\/2{width:50%}.tb-default .w-12{width:3rem}.tb-default .w-\\[77vw\\]{width:77vw}.tb-default .min-w-16{min-width:4rem}.tb-default .min-w-24{min-width:6rem}.tb-default .max-w-2xl{max-width:42rem}.tb-default .max-w-3xl{max-width:48rem}.tb-default .max-w-4\\/12{max-width:33.333333%}.tb-default .max-w-\\[11vw\\]{max-width:11vw}.tb-default .max-w-full{max-width:100%}.tb-default .flex-\\[1_1_30px\\]{flex:1 1 30px}.tb-default .flex-\\[1_1_33\\%\\]{flex:1 1 33%}.tb-default .flex-grow{flex-grow:1}.tb-default .gap-1\\.25{gap:.3125rem}.tb-default .p-1{padding:.25rem}.tb-default .\\!pl-0{padding-left:0!important}.tb-default .pr-4{padding-right:1rem}.tb-default .pt-4{padding-top:1rem}.tb-default .text-center{text-align:center}@media (max-width: 599px){.tb-default .lt-sm\\:flex-col{flex-direction:column}}\n'],encapsulation:2})}}const X6=(e,t)=>{const n=e[y];if(n.styles?.length){const e=n.styles[0];let i=document.getElementById(t);if(!i){i=document.createElement("style"),i.id=t;(document.head||document.getElementsByTagName("head")[0]).appendChild(i)}i.innerHTML=e}};class Z6{constructor(e){this.translate=e,At(e),this.translate.onLangChange.pipe(In(),Oe((e=>e.lang!==ft.English))).subscribe((()=>{At(e)})),(e=>{X6(Y6,e)})("tb-gateway-css")}static{this.ɵfac=function(e){return new(e||Z6)(t.ɵɵinject(le.TranslateService))}}static{this.ɵmod=t.ɵɵdefineNgModule({type:Z6})}static{this.ɵinj=t.ɵɵdefineInjector({imports:[se,_,ce,wj,hW,c$,K6,Tn,W3,H3]})}}e("GatewayExtensionModule",Z6),("undefined"==typeof ngJitMode||ngJitMode)&&t.ɵɵsetNgModuleScope(Z6,{imports:[se,_,ce,wj,hW,c$,K6,Tn,W3,H3]})}}}));//# sourceMappingURL=gateway-management-extension.js.map diff --git a/application/src/main/data/resources/js_modules/gateway-management-extension.js b/application/src/main/data/resources/js_modules/gateway-management-extension.js deleted file mode 100644 index 183f475e63..0000000000 --- a/application/src/main/data/resources/js_modules/gateway-management-extension.js +++ /dev/null @@ -1,17 +0,0 @@ -System.register(["@angular/core","@angular/material/sort","@angular/material/table","@angular/material/paginator","@shared/public-api","@angular/common","@angular/forms","rxjs","rxjs/operators","@core/public-api","@angular/material/dialog","tslib","@angular/material/tooltip","@ngx-translate/core","@angular/cdk/collections","@ngrx/store","@angular/router","@home/components/public-api","@angular/platform-browser","@home/components/widget/widget.component","@shared/components/dialog/json-object-edit-dialog.component","@shared/import-export/import-export.service","@shared/components/popover.component","@shared/components/popover.service","@shared/decorators/coercion","@angular/material/core"],(function(e){"use strict";var t,n,i,a,r,o,s,l,p,c,d,u,m,h,g,f,y,v,x,b,w,S,C,_,T,I,E,M,k,P,D,O,A,F,R,B,N,L,V,q,G,z,U,j,H,W,$,K,Y,X,Z,Q,J,ee,te,ne,ie,ae,re,oe,se,le,pe,ce,de,ue,me,he,ge,fe,ye,ve,xe,be,we,Se,Ce,_e,Te,Ie,Ee,Me,ke,Pe,De,Oe,Ae,Fe,Re,Be,Ne,Le,Ve,qe,Ge,ze,Ue,je,He,We,$e,Ke,Ye,Xe,Ze,Qe,Je,et,tt,nt,it,at,rt,ot;return{setters:[function(e){t=e,n=e.assertInInjectionContext,i=e.inject,a=e.DestroyRef,e.ɵRuntimeError,e.ɵgetOutputDestroyRef,r=e.Injector,o=e.effect,s=e.untracked,e.assertNotInReactiveContext,e.signal,e.computed,l=e.input,p=e.output,c=e.forwardRef,d=e.ChangeDetectionStrategy,u=e.EventEmitter,m=e.booleanAttribute,h=e.ChangeDetectorRef,g=e.SecurityContext,f=e.KeyValueDiffers,y=e.ɵNG_COMP_DEF},function(e){v=e.MatSort},function(e){x=e.MatTableDataSource},function(e){b=e.MatPaginator},function(e){w=e.Direction,S=e.PageLink,C=e.DataKeyType,_=e.SharedModule,T=e,I=e.coerceBoolean,E=e.emptyPageData,M=e.isClientSideTelemetryType,k=e.TelemetrySubscriber,P=e.coerceNumber,D=e.AttributeScope,O=e.helpBaseUrl,A=e.DialogComponent,F=e.defaultLegendConfig,R=e.widgetType,B=e.NULL_UUID,N=e.DatasourceType,L=e.EntityType,V=e.ContentType,q=e.PageComponent,G=e.TbTableDatasource,z=e.HOUR,U=e.DeviceCredentialsType},function(e){j=e.CommonModule},function(e){H=e,W=e.NG_VALUE_ACCESSOR,$=e.Validators,K=e.NG_VALIDATORS,Y=e.FormBuilder,X=e.FormControl},function(e){Z=e.Observable,Q=e.ReplaySubject,J=e.shareReplay,ee=e.combineLatest,te=e.Subject,ne=e.fromEvent,ie=e.BehaviorSubject,ae=e.of,re=e.zip,oe=e.forkJoin,se=e.merge},function(e){le=e.takeUntil,pe=e.map,ce=e.distinctUntilChanged,de=e.debounceTime,ue=e.filter,me=e.tap,he=e.catchError,ge=e.publishReplay,fe=e.refCount,ye=e.take,ve=e.takeWhile,xe=e.switchMap,be=e.startWith,we=e.mergeMap},function(e){Se=e.isEqual,Ce=e.WINDOW,_e=e,Te=e.deleteNullProperties,Ie=e.DialogService,Ee=e.isNumber,Me=e.isString,ke=e.isDefinedAndNotNull,Pe=e.formatValue,De=e.isLiteralObject,Oe=e.deepClone,Ae=e.isUndefinedOrNull,Fe=e.isObject,Re=e.generateSecret,Be=e.camelCase,Ne=e.deepTrim},function(e){Le=e.MatDialog,Ve=e.MAT_DIALOG_DATA,qe=e},function(e){Ge=e.__decorate,ze=e.__extends},function(e){Ue=e,je=e.MatTooltip},function(e){He=e,We=e.TranslateService,$e=e.TranslateModule},function(e){Ke=e.SelectionModel},function(e){Ye=e},function(e){Xe=e},function(e){Ze=e.calculateAxisSize,Qe=e.measureAxisNameSize},function(e){Je=e},function(e){et=e},function(e){tt=e.JsonObjectEditDialogComponent},function(e){nt=e},function(e){it=e},function(e){at=e},function(e){rt=e.coerceBoolean},function(e){ot=e.ErrorStateMatcher}],execute:function(){e("getDefaultConfig",Ht);const st=e("jsonRequired",(e=>e.value?null:{required:!0}));var lt,pt,ct;e("GatewayLogLevel",lt),function(e){e.NONE="NONE",e.CRITICAL="CRITICAL",e.ERROR="ERROR",e.WARNING="WARNING",e.INFO="INFO",e.DEBUG="DEBUG",e.TRACE="TRACE"}(lt||e("GatewayLogLevel",lt={})),e("GatewayVersion",pt),function(e){e.v3_7_3="3.7.3",e.v3_7_2="3.7.2",e.v3_7_0="3.7",e.v3_6_3="3.6.3",e.v3_6_2="3.6.2",e.v3_6_0="3.6",e.v3_5_2="3.5.2",e.Legacy="legacy"}(pt||e("GatewayVersion",pt={})),e("ConnectorType",ct),function(e){e.MQTT="mqtt",e.MODBUS="modbus",e.GRPC="grpc",e.OPCUA="opcua",e.BLE="ble",e.REQUEST="request",e.CAN="can",e.BACNET="bacnet",e.ODBC="odbc",e.REST="rest",e.SNMP="snmp",e.FTP="ftp",e.SOCKET="socket",e.XMPP="xmpp",e.OCPP="ocpp",e.CUSTOM="custom",e.KNX="knx"}(ct||e("ConnectorType",ct={}));const dt=e("GatewayConnectorDefaultTypesTranslatesMap",new Map([[ct.MQTT,"MQTT"],[ct.MODBUS,"MODBUS"],[ct.GRPC,"GRPC"],[ct.OPCUA,"OPCUA"],[ct.BLE,"BLE"],[ct.REQUEST,"REQUEST"],[ct.CAN,"CAN"],[ct.BACNET,"BACNET"],[ct.ODBC,"ODBC"],[ct.REST,"REST"],[ct.SNMP,"SNMP"],[ct.FTP,"FTP"],[ct.SOCKET,"SOCKET"],[ct.XMPP,"XMPP"],[ct.OCPP,"OCPP"],[ct.CUSTOM,"CUSTOM"],[ct.KNX,"KNX"]])),ut=e("ConnectorsTypesByVersion",new Map([[pt.v3_7_0,Object.values(ct)],[pt.Legacy,Object.values(ct).filter((e=>e!==ct.KNX))]]));var mt,ht;e("SocketEncoding",mt),function(e){e.UTF8="utf-8",e.HEX="hex",e.UTF16="utf-16",e.UTF32="utf-32",e.UTF16BE="utf-16-be",e.UTF16LE="utf-16-le",e.UTF32BE="utf-32-be",e.UTF32LE="utf-32-le"}(mt||e("SocketEncoding",mt={})),e("HTTPMethods",ht),function(e){e.DELETE="DELETE",e.GET="GET",e.HEAD="HEAD",e.OPTIONS="OPTIONS",e.PATCH="PATCH",e.POST="POST",e.PUT="PUT"}(ht||e("HTTPMethods",ht={}));var gt={gateway:{active:"Active",address:"Address","address-required":"Address required","add-entry":"Add configuration","add-attribute":"Add attribute","add-attribute-update":"Add attribute update","add-attribute-request":"Add attribute request","add-key":"Add key","add-timeseries":"Add time series","add-mapping":"Add mapping","add-slave":"Add Slave",arguments:"Arguments","add-rpc-method":"Add method","add-rpc-request":"Add request","add-value":"Add argument","advanced-settings":"Advanced settings",application:"Application",bacnet:{"device-timeout":"Discovery timeout","network-number":"Network number","alt-responses-address":"Alternative responses address","apdu-length":"APDU Length","object-name":"Object Name","object-type":{"analog-input":"Analog Input","analog-output":"Analog Output","analog-value":"Analog Value","binary-input":"Binary Input","binary-output":"Binary Output","binary-value":"Binary Value"},"request-type":{label:"Request Type",write:"Write Property",read:"Read Property"},"property-id":{"present-value":"Present Value","status-flags":"Status Flags","cov-increment":"COV Increment","event-state":"Event State","out-of-service":"Out of Service",polarity:"Polarity","priority-array":"Priority Array","relinquish-default":"Relinquish Default","current-command-priority":"Current Command Priority","event-message-texts":"Event Message Texts","event-message-texts-config":"Event Message Texts Config","event-algorithm-inhibit-reference":"Event Algorithm Inhibit Reference","time-delay-normal":"Time Delay Normal"},segmentation:{label:"Segmentation",no:"None",both:"Both",transmit:"Transmit",receive:"Receive"}},baudrate:"Baudrate","buffer-size":"Buffer size","buffer-size-required":"Buffer size is required","buffer-size-range":"Buffer size should be greater than 0",bytesize:"Bytesize",boolean:"Boolean",bit:"Bit","bit-target-type":"Bit target type","delete-value":"Delete value","delete-argument":"Delete argument","delete-rpc-method":"Delete method","delete-rpc-request":"Delete request","delete-attribute-update":"Delete attribute update","delete-attribute-request":"Delete attribute request",advanced:"Advanced","add-device":"Add device","address-filter":"Address filter","address-filter-required":"Address filter is required","advanced-connection-settings":"Advanced connection settings","advanced-configuration-settings":"Advanced configuration settings",attributes:"Attributes","attribute-updates":"Attribute updates","attribute-on-platform":"Attribute on platform","attribute-requests":"Attribute requests","attribute-filter":"Attribute filter","attribute-filter-hint":"Filter for incoming attribute name from platform, supports regular expression.","attribute-filter-required":"Attribute filter required.","attribute-name-expression":"Attribute name expression","attribute-name-expression-required":"Attribute name expression required.","attribute-name-expression-hint":"Hint for Attribute name expression",basic:"Basic","byte-order":"Byte order","word-order":"Word order",broker:{connection:"Connection to broker",name:"Broker name","name-required":"Broker name required.","security-types":{anonymous:"Anonymous",basic:"Basic",certificates:"Certificates"}},certificate:"Certificate","CA-certificate-path":"Path to CA certificate file","path-to-CA-cert-required":"Path to CA certificate file is required.","change-connector-title":"Confirm connector change","change-connector-text":"Switching connectors will discard any unsaved changes. Continue?","checking-device-activity":"Checking device activity",command:"Docker commands","command-copied-message":"Docker command has been copied to clipboard",configuration:"Configuration","add-connector":"Add connector","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","connectors-config":"Connectors configuration","connectors-table-enabled":"Enabled","connectors-table-name":"Name","connectors-table-type":"Type","connectors-table-status":"Status","connectors-table-actions":"Actions","connectors-table-key":"Key","connectors-table-class":"Class","connection-timeout":"Connection timeout (s)","connect-attempt-time":"Connect attempt time (ms)","connect-attempt-count":"Connect attempt count","copy-username":"Copy username","copy-password":"Copy password","copy-client-id":"Copy client ID","connector-created":"Connector created","connector-updated":"Connector updated","create-new-one":"Create new one!","rpc-command-save-template":"Save Template","rpc-command-send":"Send","rpc-command-result":"Response","rpc-command-edit-params":"Edit parameters","gateway-configuration":"General Configuration","docker-label":"Use the following instruction to run IoT Gateway in Docker compose with credentials for selected device","install-docker-compose":"Use the instructions to download, install and setup docker compose",integer:"Integer",inactive:"Inactive",device:"Device",devices:"Devices","device-profile":"Device profile","device-info-settings":"Device info settings","device-info":{"entity-field":"Entity field",source:"Source",expression:"Value / Expression","expression-hint":"Show help",name:"Name","profile-name":"Profile name","device-name-expression":"Device name expression","device-name-expression-required":"Device name expression is required.","device-profile-expression-required":"Device profile expression is required."},"device-name-filter":"Device name filter","device-name-filter-hint":"This field supports regular expressions to filter incoming data by device name.","device-name-filter-required":"Device name filter is required.",details:"Details","delete-mapping-title":"Are you sure you want to delete the mapping related with '{{name}}'?","delete-mapping-description":"Be careful, after the confirmation mapping configuration and all related data will become unrecoverable.","delete-slave-title":"Are you sure you want to delete the server '{{name}}'?","delete-slave-description":"Be careful, after the confirmation server configuration and all related data will become unrecoverable.","delete-device-title":"Are you sure you want to delete the device '{{name}}'?","delete-device-description":"Be careful, after the confirmation the device configuration and all related data will become unrecoverable.",divider:"Divider","download-configuration-file":"Download configuration file","download-docker-compose":"Download docker-compose.yml for your gateway","enable-remote-logging":"Enable remote logging","ellipsis-chips-text":"+ {{count}} more","launch-gateway":"Launch gateway","launch-docker-compose":"Start the gateway using the following command in the terminal from folder with docker-compose.yml file","logs-configuration":"Logs configuration","create-new-gateway":"Create a new gateway","create-new-gateway-text":"Are you sure you want create a new gateway with name: '{{gatewayName}}'?","created-time":"Created time","configuration-delete-dialog-header":"Configurations will be deleted","configuration-delete-dialog-body":"Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below","configuration-delete-dialog-input":"Gateway name","configuration-delete-dialog-input-required":"Gateway name is mandatory","configuration-delete-dialog-confirm":"Turn Off","connector-duplicate-name":"Connector with such name already exists.","connection-type":"Connection type","connector-side":"Connector side","payload-type":"Payload type","platform-side":"Platform side",JSON:"JSON","JSON-hint":"Converter for this payload type processes MQTT messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the message. And regular expressions to get device details from topics.",byte:"Byte",bytes:"Bytes","bytes-hint":"Converter for this payload type designed for binary MQTT payloads, this converter directly interprets binary data to retrieve device names and device profile names, along with attributes and time series, using specific byte positions for data extraction.",custom:"Custom","custom-hint":"This option allows you to use a custom converter for specific data tasks. You need to add your custom converter to the extension folder and enter its class name in the UI settings. Any keys you provide will be sent as configuration to your custom converter.","client-cert-path":"Path to client certificate file","path-to-client-cert-required":"Path to client certificate file is required.","client-id":"Client ID","data-conversion":"Data conversion","data-mapping":"Data mapping","data-mapping-hint":"Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and time series data keys.","opcua-data-mapping-hint":"Data mapping provides the capability to parse and convert the data received from a OPCUA server into specific data keys.",delete:"Delete configuration","delete-attribute":"Delete attribute","delete-key":"Delete key","delete-timeseries":"Delete time series",default:"Default","device-node":"Device node","device-node-required":"Device node required.","device-node-hint":"Path or identifier for device node on OPC UA server. Relative paths from it for attributes and time series can be used.","device-name":"Device name","device-profile-label":"Device profile","device-name-required":"Device name required","device-profile-required":"Device profile required","download-tip":"Download configuration file","drop-file":"Drop file here or",enable:"Enable",encoding:"Encoding","enable-subscription":"Enable subscription",extension:"Extension","extension-hint":"Put your converter classname in the field. Custom converter with such class should be in extension/mqtt folder.","extension-required":"Extension is required.","extension-configuration":"Extension configuration","extension-configuration-hint":"Configuration for converter. These arguments will be passed as a config to convert function.","fill-connector-defaults":"Fill configuration with default values","fill-connector-defaults-hint":"This property allows to fill connector configuration with default values on it's creation.","from-device-request-settings":"Input request parsing","from-device-request-settings-hint":"These fields support JSONPath expressions to extract a name from incoming message.","function-code":"Function code","function-codes":{"read-coils":"01 - Read Coils","read-discrete-inputs":"02 - Read Discrete Inputs","read-multiple-holding-registers":"03 - Read Multiple Holding Registers","read-input-registers":"04 - Read Input Registers","write-single-coil":"05 - Write Single Coil","write-single-holding-register":"06 - Write Single Holding Register","write-multiple-coils":"15 - Write Multiple Coils","write-multiple-holding-registers":"16 - Write Multiple Holding Registers"},"to-device-response-settings":"Output request processing","to-device-response-settings-hint":"For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",gateway:"Gateway","gateway-exists":"Device with same name is already exists.","gateway-status":"Gateway status","gateway-name":"Gateway name","gateway-name-required":"Gateway name is required.","gateway-saved":"Gateway configuration successfully saved.","generate-client-id":"Generate Client ID",grpc:"GRPC","grpc-keep-alive-timeout":"Keep alive timeout (in ms)","grpc-keep-alive-timeout-required":"Keep alive timeout is required","grpc-keep-alive-timeout-min":"Keep alive timeout can not be less then 1","grpc-keep-alive-timeout-pattern":"Keep alive timeout is not valid","grpc-keep-alive":"Keep alive (in ms)","grpc-keep-alive-required":"Keep alive is required","grpc-keep-alive-min":"Keep alive can not be less then 1","grpc-keep-alive-pattern":"Keep alive is not valid","grpc-min-time-between-pings":"Min time between pings (in ms)","grpc-min-time-between-pings-required":"Min time between pings is required","grpc-min-time-between-pings-min":"Min time between pings can not be less then 1","grpc-min-time-between-pings-pattern":"Min time between pings is not valid","grpc-min-ping-interval-without-data":"Min ping interval without data (in ms)","grpc-min-ping-interval-without-data-required":"Min ping interval without data is required","grpc-min-ping-interval-without-data-min":"Min ping interval without data can not be less then 1","grpc-min-ping-interval-without-data-pattern":"Min ping interval without data is not valid","grpc-max-pings-without-data":"Max pings without data","grpc-max-pings-without-data-required":"Max pings without data is required","grpc-max-pings-without-data-min":"Max pings without data can not be less then 1","grpc-max-pings-without-data-pattern":"Max pings without data is not valid",info:"Info",identity:"Identity","inactivity-check-period-seconds":"Inactivity check period (in sec)","inactivity-check-period-seconds-required":"Inactivity check period is required","inactivity-check-period-seconds-min":"Inactivity check period can not be less then 1","inactivity-check-period-seconds-pattern":"Inactivity check period is not valid","inactivity-timeout-seconds":"Inactivity timeout (in sec)","inactivity-timeout-seconds-required":"Inactivity timeout is required","inactivity-timeout-seconds-min":"Inactivity timeout can not be less then 1","inactivity-timeout-seconds-pattern":"Inactivity timeout is not valid","unit-id":"Unit ID",host:"Host","host-required":"Host is required.",holding_registers:"Holding registers",coils_initializer:"Coils initializer",input_registers:"Input registers",discrete_inputs:"Discrete inputs","json-parse":"Not valid JSON.","json-required":"Field cannot be empty.","JSONPath-hint":"Supports constants and JSONPath expressions to extract data.",logs:{logs:"Logs",days:"days",hours:"hours",minutes:"minutes",seconds:"seconds","date-format":"Date format","date-format-required":"Date format required","log-format":"Log format","log-type":"Log type","log-format-required":"Log format required",remote:"Remote logging","remote-logs":"Remote logs",local:"Local logging",level:"Log level","file-path":"File path","file-path-required":"File path required","saving-period":"Log saving period","saving-period-min":"Log saving period can not be less then 1","saving-period-required":"Log saving period required","backup-count":"Backup count","backup-count-min":"Backup count can not be less then 1","backup-count-required":"Backup count required"},"max-number-of-workers":"Max number of workers","max-number-of-workers-hint":"Maximal number of workers threads for converters \n(The amount of workers changes dynamically, depending on load) \nRecommended amount 50-150.","max-number-of-workers-required":"Max number of workers is required.","max-messages-queue-for-worker":"Max messages queue per worker","max-messages-queue-for-worker-hint":"Maximal messages count that will be in the queue \nfor each converter worker.","max-messages-queue-for-worker-required":"Max messages queue per worker is required.",method:"Method","method-name":"Method name","method-required":"Method name is required.","min-pack-send-delay":"Min pack send delay (in ms)","min-pack-send-delay-required":"Min pack send delay is required","min-pack-send-delay-min":"Min pack send delay can not be less then 10","min-pack-send-delay-pattern":"Min pack send delay is not valid",multiplier:"Multiplier",mode:"Mode","model-name":"Model name",modifier:"Modifier","modifier-invalid":"Modifier is not valid","mqtt-version":"MQTT version",name:"Name","name-required":"Name is required.","network-mask":"Network mask","no-attributes":"No attributes","no-attribute-updates":"No attribute updates","no-attribute-requests":"No attribute requests","no-connectors":"No connectors","no-data":"No configurations","no-gateway-found":"No gateway found.","no-gateway-matching":" '{{item}}' not found.","no-timeseries":"No time series","no-keys":"No keys","no-value":"No arguments","no-rpc-methods":"No RPC methods","no-rpc-requests":"No RPC requests","path-hint":"The path is local to the gateway file system","path-logs":"Path to log files","path-logs-required":"Path is required.",password:"Password","password-required":"Password is required.","permit-without-calls":"Keep alive permit without calls","property-id":"Property ID","poll-period":"Poll period (ms)","poll-period-error":"Poll period should be at least {{min}} (ms).",port:"Port","port-required":"Port is required.","port-limits-error":"Port should be number from {{min}} to {{max}}.","private-key-path":"Path to private key file","path-to-private-key-required":"Path to private key file is required.",parity:"Parity","product-code":"Product code","product-name":"Product name",raw:"Raw",retain:"Retain","retain-hint":"This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.",remote:"Remote configuration","remote-logging-level":"Logging level","remove-entry":"Remove configuration","remote-shell":"Remote shell","remote-configuration":"Remote Configuration","request-expression":"Request expression","request-expression-required":"Request expression is required",retries:"Retries","retries-on-empty":"Retries on empty","retries-on-invalid":"Retries on invalid",response:"Response",rest:{"add-http-header":"Add HTTP header","no-http-headers":"No HTTP headers","delete-http-header":"Delete HTTP header","ssl-verify":"SSL verify",endpoint:"Endpoint","http-methods":"HTTP methods","http-method":"HTTP method","on-error":"On error","on-success":"On success",tries:"Tries","http-headers":"HTTP headers","allow-redirects":"Allow redirects","request-url-expression":"Request URL expression","response-attribute":"Response attribute","response-expected":"Expected","response-type":{default:"Default",const:"Constant",advanced:"Advanced"}},rpc:{title:"{{type}} Connector RPC parameters","templates-title":"Connector RPC Templates",methodFilter:"Method filter","method-name":"Method name",requestTopicExpression:"Request topic expression",responseTopicExpression:"Response topic expression",responseTimeout:"Response timeout",valueExpression:"Value expression",tag:"Tag",type:"Type",functionCode:"Function Code",objectsCount:"Objects Count",address:"Address",method:"Method",requestType:"Request Type",requestTimeout:"Request Timeout",objectType:"Object type",identifier:"Identifier",propertyId:"Property ID",methodRPC:"Method RPC name",withResponse:"With Response",characteristicUUID:"Characteristic UUID",methodProcessing:"Method Processing",nodeID:"Node ID",isExtendedID:"Is Extended ID",isFD:"Is FD",bitrateSwitch:"Bitrate Switch",dataInHEX:"Data In HEX",dataLength:"Data Length",dataByteorder:"Data Byte Order",dataBefore:"Data Before",dataAfter:"Data After",dataExpression:"Data Expression",oid:"OID","add-oid":"Add OID","add-header":"Add header","add-security":"Add security",remove:"Remove",requestFilter:"Request Filter",requestUrlExpression:"Request URL Expression",httpMethod:"HTTP Method",timeout:"Timeout",tries:"Tries",httpHeaders:"HTTP Headers","header-name":"Header name",hint:{"modbus-response-reading":"RPC response will return all subtracted values from all connected devices when the reading functions are selected.","modbus-writing-functions":"RPC will write a filled value to all connected devices when the writing functions are selected.","opc-method":"A filled method name is the OPC-UA method that will processed on the server side (make sure your node has the requested method)."},"security-name":"Security name",value:"Value",security:"Security",responseValueExpression:"Response Value Expression",requestValueExpression:"Request Value Expression",arguments:"Arguments","add-argument":"Add argument","write-property":"Write property","read-property":"Read property","analog-output":"Analog output","analog-input":"Analog input","binary-output":"Binary output","binary-input":"Binary input","binary-value":"Binary value","analog-value":"Analog value",write:"Write",read:"Read",scan:"Scan",oids:"OIDS",set:"Set",multiset:"Multiset",get:"Get","bulk-walk":"Bulk walk",table:"Table","multi-get":"Multiget","get-next":"Get next","bulk-get":"Bulk get",walk:"Walk","save-template":"Save template","template-name":"Template name","template-name-required":"Template name is required.","template-name-duplicate":"Template with such name already exists, it will be updated.",command:"Command",params:"Params","json-value-invalid":"JSON value has an invalid format"},"rpc-methods":"RPC methods","rpc-requests":"RPC requests",request:{"connect-request":"Connect request","disconnect-request":"Disconnect request","attribute-request":"Attribute request","attribute-update":"Attribute update","rpc-connection":"RPC command"},"request-type":"Request type","request-timeout":"Request timeout (ms)","requests-mapping":"Requests mapping","requests-mapping-hint":"MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.","request-topic-expression":"Request topic expression","request-client-certificate":"Request client certificate","request-topic-expression-required":"Request topic expression is required.","response-timeout":"Response timeout","response-timeout-required":"Response timeout is required.","response-timeout-limits-error":"Timeout must be more then {{min}} ms.","response-topic-Qos":"Response topic QoS","response-topic-Qos-hint":"MQTT Quality of Service (QoS) is an agreement between the message sender and receiver that defines the level of delivery guarantee for a specific message.","response-topic-expression":"Response topic expression","response-topic-expression-required":"Response topic expression is required.","response-value-expression":"Response value expression","response-value-expression-required":"Response value expression is required.","vendor-name":"Vendor name","vendor-url":"Vendor URL",value:"Value",values:"Values","value-required":"Value is required.","value-expression":"Value expression","value-expression-required":"Value expression is required.","with-response":"With response","without-response":"Without response",other:"Other",socket:"Socket","save-tip":"Save configuration file","scan-period":"Scan period (ms)","scan-period-error":"Scan period should be at least {{min}} (ms).","sub-check-period":"Subscription check period (ms)","sub-check-period-error":"Subscription check period should be at least {{min}} (ms).",security:"Security","security-policy":"Security policy","security-type":"Security type","security-types":{"access-token":"Access Token","username-password":"Username and Password",tls:"TLS","tls-access-token":"TLS + Access Token","tls-private-key":"TLS + Private Key"},"select-connector":"Select connector to display config","send-change-data":"Send data only on change","send-data-to-platform":"Send data to platform","send-data-on-change":"Send data only on change","send-change-data-hint":"The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",server:"Server","server-hostname":"Server hostname","server-slave":"Server (Slave)","servers-slaves":"Servers (Slaves)","server-port":"Server port","server-url":"Server endpoint url","server-connection":"Server Connection","server-config":"Server configuration","server-slave-config":"Server (Slave) configuration","server-url-required":"Server endpoint url is required.",stopbits:"Stopbits",strict:"Strict",set:"Set","show-map":"Show map",statistics:{entry:"Statistic entry","custom-send-period":"Custom send period (in sec)","custom-send-period-pattern":"Custom send period is not valid","custom-send-period-min":"Custom send period can not be less then 60","custom-send-period-required":"Custom send period is required","create-command":"Create command",attributes:"Attributes","name-already-exists":"Attribute name already exists.",telemetry:"Telemetry","storage-message-count":"Storage message count","messages-from-platform":"Messages from platform","pushed-datapoints":"Pushed datapoints","messages-pulled-from-storage":"Messages pulled from storage","messages-pushed-to-platform":"Messages pushed to platform","messages-sent-to-platform":"Messages sent to platform","process-cpu-usage":"Gateway process CPU usage",memory:"Gateway process memory usage","machine-resources":"Machine resources","free-disk":"Free disk",statistic:"Statistic",statistics:"Statistics","general-statistics":"General statistics","custom-statistics":"Custom statistics","copy-message":"Copy message","statistic-commands-empty":'No configured statistic keys found. You can configure them in "Statistics" tab in general configuration.',"statistics-button":"Go to configuration",commands:"Commands",name:"Time series name","time-series-name-already-exists":"Time series name already exists.","send-period":"Statistic send period (in sec)","send-period-required":"Statistic send period is required","send-period-min":"Statistic send period can not be less then 60","send-period-pattern":"Statistic send period is not valid","check-connectors-configuration":"Check connectors configuration (in sec)",messages:"Messages","max-payload-size-bytes":"Max payload size in bytes","max-payload-size-bytes-required":"Max payload size in bytes is required","max-payload-size-bytes-min":"Max payload size in bytes can not be less then 100","max-payload-size-bytes-pattern":"Max payload size in bytes is not valid","min-pack-size-to-send":"Min packet size to send","min-pack-size-to-send-required":"Min packet size to send is required","min-pack-size-to-send-min":"Min packet size to send can not be less then 100","min-pack-size-to-send-pattern":"Min packet size to send is not valid","no-config-commands-found":"No configuration commands found","delete-command":"Delete command '{{command}}'?","delete-command-data":"All command data will be deleted.","edit-command":"Edit command","change-command-title":"Discard command change","change-command-text":"Cancelling command edit will discard any unsaved changes. Continue?","no-command-found":"No command found","no-commands-matching":"No command matching '{{command}}' were found.","check-connectors-configuration-required":"Check connectors configuration is required","check-connectors-configuration-min":"Check connectors configuration can not be less then 1","check-connectors-configuration-pattern":"Check connectors configuration is not valid","install-cmd":"Install command",add:"Add command",timeout:"Timeout (in sec)","timeout-required":"Timeout is required","timeout-min":"Timeout can not be less then 1","timeout-pattern":"Timeout is not valid","time-series-name-required":"Time series name is required","time-series-name-pattern":"Time series name is not valid",command:"Command","command-required":"Command is required","command-pattern":"Command is not valid",remove:"Remove command"},storage:"Storage","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-read-record-count":"Read record count in storage","storage-read-record-count-min":"Minimum number of records is 1.","storage-read-record-count-pattern":"Number is not valid.","storage-read-record-count-required":"Read record count is required.","storage-max-read-record-count":"Max read record count in storage","storage-max-read-record-count-min":"Minimum number of records is 1.","storage-max-read-record-count-pattern":"Number is not valid.","storage-max-read-record-count-required":"Max Read record count is required.","storage-data-folder-path":"Data folder path","storage-data-folder-path-required":"Data folder path 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",sqlite:"SQLITE"},suffix:{s:"s",ms:"ms"},"report-strategy":{label:"Report strategy","on-change":"On value change","on-report-period":"On report period","on-change-or-report-period":"On value change or report period","report-period":"Report period","on-received":"On received"},"source-type":{msg:"Message",topic:"Topic",const:"Constant",identifier:"Identifier",path:"Path",expression:"Expression",request:"From request"},"workers-settings":"Workers settings",thingsboard:"ThingsBoard",general:"General",timeseries:"Time series",key:"Key",keys:"Keys","key-required":"Key is required.","thingsboard-host":"Platform host","thingsboard-host-required":"Host is required.","thingsboard-port":"Platform 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",timeout:"Timeout","timeout-error":"Timeout should be at least {{min}} (ms).","title-connectors-json":"Connector {{typeName}} configuration",type:"Type","topic-filter":"Topic filter","topic-required":"Topic filter is required.","tls-path-ca-certificate":"Path to CA certificate on gateway","tls-path-client-certificate":"Path to client certificate on gateway","tls-connection":"TLS Connection","master-connections":"Master Connections","method-filter":"Method filter","method-filter-hint":"Regular expression to filter incoming RPC method from platform.","method-filter-required":"Method filter is required.","messages-ttl-check-in-hours":"Messages TTL check in hours","messages-ttl-check-in-hours-required":"Messages TTL check in hours is required.","messages-ttl-check-in-hours-min":"Min number is 1.","messages-ttl-check-in-hours-pattern":"Number is not valid.","messages-ttl-in-days":"Messages TTL in days","messages-ttl-in-days-required":"Messages TTL in days is required.","messages-ttl-in-days-min":"Min number is 1.","messages-ttl-in-days-pattern":"Number is not valid.","mqtt-qos":"QoS","mqtt-qos-required":"QoS is required","mqtt-qos-range":"QoS values range is from 0 to 1",qos:{"at-most-once":"0 - At most once","at-least-once":"1 - At least once","exactly-once":"2 - Exactly once"},"objects-count":"Objects count","object-id":"Object ID","objects-count-required":"Objects count is required","wait-after-failed-attempts":"Wait after failed attempts (ms)","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",username:"Username","username-required":"Username is required.","unit-id-required":"Unit ID is required.","vendor-id":"Vendor ID","write-coil":"Write Coil","write-coils":"Write Coils","write-register":"Write Register","write-registers":"Write Registers",hints:{"enable-general-statistics":"Enables gateway statistics (machine, storage, connectors).","enable-custom-statistics":"Enables collecting statistics using custom commands.","buffer-size":"Buffer size for received data blocks.",encoding:"Encoding used for writing received string data to storage.",method:"Name for method on a platform.","modbus-master":"Configuration sections for connecting to Modbus servers and reading data from them.","modbus-server":"Configuration section for the Modbus server, storing data and sending updates to the platform when changes occur or at fixed intervals.","remote-configuration":"Enables remote configuration and management of the gateway","remote-shell":"Enables remote control of the operating system with the gateway from the Remote Shell widget",host:"Hostname or IP address of platform server",port:"Port of MQTT service on platform server",token:"Access token for the gateway from platform server","client-id":"MQTT client id for the gateway form platform server",username:"MQTT username for the gateway form platform server",password:"MQTT password for the gateway form platform server","ca-cert":"Path to CA certificate file","date-form":"Date format in log message","object-id-required":"Object ID is required","vendor-id-required":"Vendor ID is required","data-folder":"Path to the folder that will contain data (Relative or Absolute)","log-format":"Log message format","remote-log":"Enables remote logging and logs reading from the gateway","backup-count":"If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",storage:"Provides configuration for saving incoming data before it is sent to the platform","max-file-count":"Maximum number of files that will be created","max-read-count":"Number of messages to retrieve from the storage and send to platform","max-records":"Maximum count of records that will be stored in one file","read-record-count":"Number of messages to retrieve from the storage and send to platform","max-records-count":"Maximum number of data entries in storage before sending to platform","ttl-check-hour":"How often will the Gateway check data for obsolescence","ttl-messages-day":"Maximum number of days that the storage will retain data","username-required-with-password":"Username required if password is specified",commands:"Commands for collecting additional statistic",attribute:"Statistic telemetry key",timeout:"Timeout for command executing",command:"The result of the command execution, will be used as the value for telemetry","check-device-activity":"Enables monitor the activity of each connected device","inactivity-timeout":"Time after whose the gateway will disconnect device","inactivity-period":"Periodicity of device activity check","minimal-pack-delay":"Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)",qos:"Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)","server-port":"Network port on which GRPC server will listen for incoming connections.","grpc-keep-alive-timeout":"Maximum time the server should wait for a keepalive ping response before considering the connection dead.","grpc-keep-alive":"Duration between two successive keepalive ping messages when there is no active RPC call.","grpc-min-time-between-pings":"Minimum amount of time the server should wait between sending keepalive ping messages","grpc-max-pings-without-data":"Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.","grpc-min-ping-interval-without-data":"Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.","permit-without-calls":"Allow server to keep the GRPC connection alive even when there are no active RPC calls.","path-in-os":"Path in gateway os.",memory:"Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",file:"Your data will be stored in separated files and will be saved even after the gateway restart.",sqlite:"Your data will be stored in file based database. And will be saved even after the gateway restart.","opc-timeout":"Timeout in milliseconds for connecting to OPC-UA server.","security-policy":"Security Policy defines the security mechanisms to be applied.","install-cmd":"Packages that will be installed for command executing.","scan-period":"Period in milliseconds to rescan the server.","sub-check-period":"Period to check the subscriptions in the OPC-UA server.","enable-subscription":"If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInMillis.","show-map":"Show nodes on scanning.","method-name":"Name of method on OPC-UA server.",arguments:"Arguments for the method (will be overwritten by arguments from the RPC request).","min-pack-size-to-send":"Minimum package size for sending.","max-payload-size-bytes":"Maximum package size in bytes","poll-period":"Period in milliseconds to read data from nodes.","poll-period-required":"Poll period is required.","report-period-required":"Report period is required.","report-period-range":"Report period must be greater than 100.","timeout-pattern":"Timeout is not valid",rest:{"endpoint-required":"Endpoint is required.","endpoint-pattern":"Endpoint is not valid.","device-name-filter-required":"Device name filter is required.","device-name-filter-pattern":"Device name filter is not valid.","request-url-expression-required":"Request URL expression is required.","request-url-expression-pattern":"Request URL expression is not valid.","value-expression-required":"Value expression is required.","value-expression-pattern":"Value expression is not valid.","method-filter-required":"Method filter is required.","method-filter-pattern":"Method filter is not valid.","scope-type":"Attribute scope on platform.","tries-min":"Request tries number should be greater than 0.","tries-pattern":"Request tries is invalid.","http-methods-required":"HTTP methods are required.",host:"Domain or IP address of the server.",port:"Port on the server for listening clients connection.","ssl-verify":"Enable SSL certificate verification.",cert:"Path to the certificate file.",key:"Path to the private key file",endpoint:"Endpoint path on the server.","http-methods":"HTTP methods allowed for endpoint.","http-method":"Request to external URL HTTP method.",JSON:"Converter for this payload type processes REST messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the request body.",extension:"Custom converter classname. File with this class should be in extension/rest folder.","response-attribute-required":"Response attribute is required.","response-attribute-pattern":"Response attribute is not valid.","update-ssl":"Verify SSL certificate on the external server.","attribute-filter":"Filter for incoming attribute name from platform, support regular expression.","value-expression":"Used to create request body for request to external server. Supports ${deviceName}, ${deviceType}, ${attributeName}, ${attributeValue} variables.","attribute-url-expression":"Used to create request URL for request to external server. Supports ${deviceName}, ${deviceType}, ${attributeName}, ${attributeValue} variables.","method-filter":"Filter for RPC requests from the platform based on the method name. Supports regular expressions.","attribute-name-expression-required":"Attribute name expression is required.","attribute-name-expression-pattern":"Attribute name expression is not valid.","attribute-filter-required":"Attribute filter is required.","attribute-filter-pattern":"Attribute filter is not valid."},socket:{"attribute-on-platform-required":"Attribute on platform is required","attribute-requests-type":"The type of requested attribute can be “shared” or “client.“","with-response":"Boolean flag that specifies whether to send a response back to platform.","key-telemetry":"Name for telemetry on platform.","key-attribute":"Name for attribute on platform."},modbus:{"bit-target-type":"The response type can be either an integer (1/0) or a boolean (True/False).",bit:"Specify the index of the bit to read from the array, or leave it blank to read the entire array.","max-bit":"The bit value must not exceed the objects count.","framer-type":"Type of a framer (Socket, RTU, or ASCII), if needed.",host:"Hostname or IP address of Modbus server.",port:"Modbus server port for connection.","unit-id":"Modbus slave ID.","connection-timeout":"Connection timeout (in seconds) for the Modbus server.","byte-order":"Byte order for reading data.","word-order":"Word order when reading multiple registers.",retries:"Retrying data transmission to the master. Acceptable values: true or false.","retries-on-empty":"Retry sending data to the master if the data is empty.","retries-on-invalid":"Retry sending data to the master if it fails.","poll-period":"Period in milliseconds to check attributes and telemetry on the slave.","connect-attempt-time":"A waiting period in milliseconds before establishing a connection to the master.","connect-attempt-count":"The number of connection attempts made through the gateway.","wait-after-failed-attempts":"A waiting period in milliseconds before attempting to send data to the master.","serial-port":"Serial port for connection.",baudrate:"Baud rate for the serial device.",stopbits:"The number of stop bits sent after each character in a message to indicate the end of the byte.",bytesize:"The number of bits in a byte of serial data. This can be one of 5, 6, 7, or 8.",parity:"The type of checksum used to verify data integrity. Options: (E)ven, (O)dd, (N)one.",strict:"Use inter-character timeout for baudrates ≤ 19200.","objects-count":"Depends on the selected type.",address:"Register address to verify.",key:"Key to be used as the attribute key for the platform instance.",method:"Method name to be used for the platform instance.","data-keys":"For more information about function codes and data types click on help icon",modifier:"The retrieved value will be adjusted (by multiplying or dividing it) based on the specified modifier value."},bacnet:{"object-id":"The gateway object identifier in the BACnet network.","vendor-id":"The gateway vendor identifier in the BACnet network","apdu-length":"Maximal length of the APDU.",segmentation:"Segmentation type for transmitting large BACnet messages.","key-object-id":"Object id in the BACnet device.","property-id":"Property id in the BACnet device.","request-type":"“writeProperty” to write data and “readProperty” to read data.","request-timeout":"Timeout to wait the response from the BACnet device, milliseconds.","device-timeout":"Period of time when the connector will try to discover BACnet devices.","network-number":"Identifier of the network segment."}}}},ft={"add-entry":"إضافة تكوين",advanced:"متقدم","checking-device-activity":"فحص نشاط الجهاز",command:"أوامر Docker","command-copied-message":"تم نسخ أمر Docker إلى الحافظة",configuration:"التكوين","connector-add":"إضافة موصل جديد","connector-enabled":"تمكين الموصل","connector-name":"اسم الموصل","connector-name-required":"اسم الموصل مطلوب.","connector-type":"نوع الموصل","connector-type-required":"نوع الموصل مطلوب.",connectors:"الموصلات","connectors-config":"تكوينات الموصلات","connectors-table-enabled":"ممكّن","connectors-table-name":"الاسم","connectors-table-type":"النوع","connectors-table-status":"الحالة","connectors-table-actions":"الإجراءات","connectors-table-key":"المفتاح","connectors-table-class":"الفئة","rpc-command-send":"إرسال","rpc-command-result":"الاستجابة","rpc-command-edit-params":"تحرير المعلمات","gateway-configuration":"تكوين عام","docker-label":"استخدم التعليمات التالية لتشغيل IoT Gateway في Docker compose مع بيانات اعتماد للجهاز المحدد","install-docker-compose":"استخدم التعليمات لتنزيل وتثبيت وإعداد docker compose","download-configuration-file":"تنزيل ملف التكوين","download-docker-compose":"تنزيل docker-compose.yml لبوابتك","launch-gateway":"تشغيل البوابة","launch-docker-compose":"بدء تشغيل البوابة باستخدام الأمر التالي في الطرفية من المجلد الذي يحتوي على ملف docker-compose.yml","create-new-gateway":"إنشاء بوابة جديدة","create-new-gateway-text":"هل أنت متأكد أنك تريد إنشاء بوابة جديدة باسم: '{{gatewayName}}'؟","created-time":"وقت الإنشاء","configuration-delete-dialog-header":"سيتم حذف التكوينات","configuration-delete-dialog-body":"يمكن تعطيل التكوين عن بُعد فقط إذا كان هناك وصول جسدي إلى البوابة. ستتم حذف جميع التكوينات السابقة.

\n لتعطيل التكوين، أدخل اسم البوابة أدناه","configuration-delete-dialog-input":"اسم البوابة","configuration-delete-dialog-input-required":"اسم البوابة إلزامي","configuration-delete-dialog-confirm":"إيقاف التشغيل",delete:"حذف التكوين","download-tip":"تنزيل ملف التكوين","drop-file":"أفلق الملف هنا أو",gateway:"البوابة","gateway-exists":"الجهاز بنفس الاسم موجود بالفعل.","gateway-name":"اسم البوابة","gateway-name-required":"اسم البوابة مطلوب.","gateway-saved":"تم حفظ تكوين البوابة بنجاح.",grpc:"GRPC","grpc-keep-alive-timeout":"مهلة البقاء على قيد الحياة (بالمللي ثانية)","grpc-keep-alive-timeout-required":"مهلة البقاء على قيد الحياة مطلوبة","grpc-keep-alive-timeout-min":"مهلة البقاء على قيد الحياة لا يمكن أن تكون أقل من 1","grpc-keep-alive-timeout-pattern":"مهلة البقاء على قيد الحياة غير صالحة","grpc-keep-alive":"البقاء على قيد الحياة (بالمللي ثانية)","grpc-keep-alive-required":"البقاء على قيد الحياة مطلوب","grpc-keep-alive-min":"البقاء على قيد الحياة لا يمكن أن يكون أقل من 1","grpc-keep-alive-pattern":"البقاء على قيد الحياة غير صالح","grpc-min-time-between-pings":"الحد الأدنى للوقت بين البينغات (بالمللي ثانية)","grpc-min-time-between-pings-required":"الحد الأدنى للوقت بين البينغات مطلوب","grpc-min-time-between-pings-min":"الحد الأدنى للوقت بين البينغات لا يمكن أن يكون أقل من 1","grpc-min-time-between-pings-pattern":"الحد الأدنى للوقت بين البينغات غير صالح","grpc-min-ping-interval-without-data":"الحد الأدنى لفاصل البينغ بدون بيانات (بالمللي ثانية)","grpc-min-ping-interval-without-data-required":"الحد الأدنى لفاصل البينغ بدون بيانات مطلوب","grpc-min-ping-interval-without-data-min":"الحد الأدنى لفاصل البينغ بدون بيانات لا يمكن أن يكون أقل من 1","grpc-min-ping-interval-without-data-pattern":"الحد الأدنى لفاصل البينغ بدون بيانات غير صالح","grpc-max-pings-without-data":"الحد الأقصى لعدد البينغات بدون بيانات","grpc-max-pings-without-data-required":"الحد الأقصى لعدد البينغات بدون بيانات مطلوب","grpc-max-pings-without-data-min":"الحد الأقصى لعدد البينغات بدون بيانات لا يمكن أن يكون أقل من 1","grpc-max-pings-without-data-pattern":"الحد الأقصى لعدد البينغات بدون بيانات غير صالح","inactivity-check-period-seconds":"فترة فحص الخمول (بالثواني)","inactivity-check-period-seconds-required":"فترة فحص الخمول مطلوبة","inactivity-check-period-seconds-min":"فترة فحص الخمول لا يمكن أن تكون أقل من 1","inactivity-check-period-seconds-pattern":"فترة فحص الخمول غير صالحة","inactivity-timeout-seconds":"فترة الخمول (بالثواني)","inactivity-timeout-seconds-required":"فترة الخمول مطلوبة","inactivity-timeout-seconds-min":"فترة الخمول لا يمكن أن تكون أقل من 1","inactivity-timeout-seconds-pattern":"فترة الخمول غير صالحة","json-parse":"JSON غير صالح.","json-required":"الحقل لا يمكن أن يكون فارغًا.",logs:{logs:"السجلات",days:"أيام",hours:"ساعات",minutes:"دقائق",seconds:"ثواني","date-format":"تنسيق التاريخ","date-format-required":"تنسيق التاريخ مطلوب","log-format":"تنسيق السجل","log-type":"نوع السجل","log-format-required":"تنسيق السجل مطلوب",remote:"التسجيل عن بُعد","remote-logs":"السجلات عن بُعد",local:"التسجيل المحلي",level:"مستوى السجل","file-path":"مسار الملف","file-path-required":"مسار الملف مطلوب","saving-period":"فترة حفظ السجل","saving-period-min":"فترة حفظ السجل لا يمكن أن تكون أقل من 1","saving-period-required":"فترة حفظ السجل مطلوبة","backup-count":"عدد النسخ الاحتياطية","backup-count-min":"عدد النسخ الاحتياطية لا يمكن أن يكون أقل من 1","backup-count-required":"عدد النسخ الاحتياطية مطلوب"},"min-pack-send-delay":"الحد الأدنى لتأخير إرسال الحزمة (بالمللي ثانية)","min-pack-send-delay-required":"الحد الأدنى لتأخير إرسال الحزمة مطلوب","min-pack-send-delay-min":"لا يمكن أن يكون الحد الأدنى لتأخير إرسال الحزمة أقل من 0","no-connectors":"لا توجد موصلات","no-data":"لا توجد تكوينات","no-gateway-found":"لم يتم العثور على بوابة.","no-gateway-matching":"'{{item}}' غير موجود.","path-logs":"مسار إلى ملفات السجل","path-logs-required":"المسار مطلوب.","permit-without-calls":"البقاء على الحياة يسمح بدون مكالمات",remote:"التكوين عن بُعد","remote-logging-level":"مستوى التسجيل","remove-entry":"إزالة التكوين","remote-shell":"قشرة عن بُعد","remote-configuration":"التكوين عن بُعد",other:"آخر","save-tip":"حفظ ملف التكوين","security-type":"نوع الأمان","security-types":{"access-token":"رمز الوصول","username-password":"اسم المستخدم وكلمة المرور",tls:"TLS","tls-access-token":"TLS + رمز الوصول","tls-private-key":"TLS + المفتاح الخاص"},"server-port":"منفذ الخادم",statistics:{statistic:"إحصائية",statistics:"الإحصائيات","statistic-commands-empty":"لا تتوفر إحصائيات",commands:"الأوامر","send-period":"فترة إرسال الإحصائيات (بالثواني)","send-period-required":"فترة إرسال الإحصائيات مطلوبة","send-period-min":"لا يمكن أن تكون فترة إرسال الإحصائيات أقل من 60","send-period-pattern":"فترة إرسال الإحصائيات غير صالحة","check-connectors-configuration":"فترة فحص تكوين الموصلات (بالثواني)","check-connectors-configuration-required":"فترة فحص تكوين الموصلات مطلوبة","check-connectors-configuration-min":"لا يمكن أن تكون فترة فحص تكوين الموصلات أقل من 1","check-connectors-configuration-pattern":"فترة فحص تكوين الموصلات غير صالحة",add:"إضافة أمر",timeout:"المهلة","timeout-required":"المهلة مطلوبة","timeout-min":"لا يمكن أن تكون المهلة أقل من 1","timeout-pattern":"المهلة غير صالحة","attribute-name":"اسم السمة","attribute-name-required":"اسم السمة مطلوب",command:"الأمر","command-required":"الأمر مطلوب","command-pattern":"الأمر غير صالح",remove:"إزالة الأمر"},storage:"التخزين","storage-max-file-records":"السجلات القصوى في الملف","storage-max-files":"الحد الأقصى لعدد الملفات","storage-max-files-min":"الحد الأدنى هو 1.","storage-max-files-pattern":"العدد غير صالح.","storage-max-files-required":"العدد مطلوب.","storage-max-records":"السجلات القصوى في التخزين","storage-max-records-min":"الحد الأدنى لعدد السجلات هو 1.","storage-max-records-pattern":"العدد غير صالح.","storage-max-records-required":"السجلات القصوى مطلوبة.","storage-read-record-count":"عدد قراءة السجلات في التخزين","storage-read-record-count-min":"الحد الأدنى لعدد السجلات هو 1.","storage-read-record-count-pattern":"العدد غير صالح.","storage-read-record-count-required":"عدد قراءة السجلات مطلوب.","storage-max-read-record-count":"الحد الأقصى لعدد قراءة السجلات في التخزين","storage-max-read-record-count-min":"الحد الأدنى لعدد السجلات هو 1.","storage-max-read-record-count-pattern":"العدد غير صالح.","storage-max-read-record-count-required":"عدد القراءة القصوى مطلوب.","storage-data-folder-path":"مسار مجلد البيانات","storage-data-folder-path-required":"مسار مجلد البيانات مطلوب.","storage-pack-size":"الحد الأقصى لحجم حزمة الحدث","storage-pack-size-min":"الحد الأدنى هو 1.","storage-pack-size-pattern":"العدد غير صالح.","storage-pack-size-required":"الحجم الأقصى لحزمة الحدث مطلوب.","storage-path":"مسار التخزين","storage-path-required":"مسار التخزين مطلوب.","storage-type":"نوع التخزين","storage-types":{"file-storage":"تخزين الملفات","memory-storage":"تخزين الذاكرة",sqlite:"SQLITE"},thingsboard:"ثينغزبورد",general:"عام","thingsboard-host":"مضيف ثينغزبورد","thingsboard-host-required":"المضيف مطلوب.","thingsboard-port":"منفذ ثينغزبورد","thingsboard-port-max":"الحد الأقصى لرقم المنفذ هو 65535.","thingsboard-port-min":"الحد الأدنى لرقم المنفذ هو 1.","thingsboard-port-pattern":"المنفذ غير صالح.","thingsboard-port-required":"المنفذ مطلوب.",tidy:"ترتيب","tidy-tip":"ترتيب تكوين JSON","title-connectors-json":"تكوين موصل {{typeName}}","tls-path-ca-certificate":"المسار إلى شهادة CA على البوابة","tls-path-client-certificate":"المسار إلى شهادة العميل على البوابة","messages-ttl-check-in-hours":"فحص TTL الرسائل بالساعات","messages-ttl-check-in-hours-required":"يجب تحديد فحص TTL الرسائل بالساعات.","messages-ttl-check-in-hours-min":"الحد الأدنى هو 1.","messages-ttl-check-in-hours-pattern":"الرقم غير صالح.","messages-ttl-in-days":"TTL الرسائل بالأيام","messages-ttl-in-days-required":"يجب تحديد TTL الرسائل بالأيام.","messages-ttl-in-days-min":"الحد الأدنى هو 1.","messages-ttl-in-days-pattern":"الرقم غير صالح.","mqtt-qos":"جودة الخدمة (QoS)","mqtt-qos-required":"جودة الخدمة (QoS) مطلوبة","mqtt-qos-range":"تتراوح قيم جودة الخدمة (QoS) من 0 إلى 1","tls-path-private-key":"المسار إلى المفتاح الخاص على البوابة","toggle-fullscreen":"تبديل وضع ملء الشاشة","transformer-json-config":"تكوين JSON*","update-config":"إضافة/تحديث تكوين JSON",hints:{"remote-configuration":"يمكنك تمكين التكوين وإدارة البوابة عن بُعد","remote-shell":"يمكنك تمكين التحكم البعيد في نظام التشغيل مع البوابة من عنصر واجهة المستخدم قشرة عن بُعد",host:"اسم المضيف أو عنوان IP لخادم ثينغزبورد",port:"منفذ خدمة MQTT على خادم ثينغزبورد",token:"رمز الوصول للبوابة من خادم ثينغزبورد","client-id":"معرف عميل MQTT للبوابة من خادم ثينغزبورد",username:"اسم المستخدم MQTT للبوابة من خادم ثينغزبورد",password:"كلمة المرور MQTT للبوابة من خادم ثينغزبورد","ca-cert":"المسار إلى ملف شهادة CA","date-form":"تنسيق التاريخ في رسالة السجل","data-folder":"المسار إلى المجلد الذي سيحتوي على البيانات (نسبي أو مطلق)","log-format":"تنسيق رسالة السجل","remote-log":"يمكنك تمكين التسجيل البعيد وقراءة السجلات من البوابة","backup-count":"إذا كان عدد النسخ الاحتياطية > 0، عند عملية تدوير، لا يتم الاحتفاظ بأكثر من عدد النسخ الاحتياطية المحددة - يتم حذف الأقدم",storage:"يوفر تكوينًا لحفظ البيانات الواردة قبل إرسالها إلى المنصة","max-file-count":"العدد الأقصى لعدد الملفات التي سيتم إنشاؤها","max-read-count":"عدد الرسائل للحصول عليها من التخزين وإرسالها إلى ثينغزبورد","max-records":"العدد الأقصى للسجلات التي ستخزن في ملف واحد","read-record-count":"عدد الرسائل للحصول عليها من التخزين وإرسالها إلى ثينغزبورد","max-records-count":"العدد الأقصى للبيانات في التخزين قبل إرسالها إلى ثينغزبورد","ttl-check-hour":"كم مرة سيتحقق البوابة من البيانات القديمة","ttl-messages-day":"الحد الأقصى لعدد الأيام التي ستحتفظ فيها التخزين بالبيانات",commands:"الأوامر لجمع الإحصائيات الإضافية",attribute:"مفتاح تلقي الإحصائيات",timeout:"مهلة زمنية لتنفيذ الأمر",command:"سيتم استخدام نتيجة تنفيذ الأمر كقيمة لتلقي الإحصائيات","check-device-activity":"يمكنك تمكين مراقبة نشاط كل جهاز متصل","inactivity-timeout":"الوقت بعد الذي ستفصل البوابة الجهاز","inactivity-period":"تكرار فحص نشاط الجهاز","minimal-pack-delay":"التأخير بين إرسال حزم الرسائل (يؤدي تقليل هذا الإعداد إلى زيادة استخدام وحدة المعالجة المركزية)",qos:"جودة الخدمة في رسائل MQTT (0 - على الأكثر مرة واحدة، 1 - على الأقل مرة واحدة)","server-port":"منفذ الشبكة الذي سيستمع فيه خادم GRPC للاستفسارات الواردة.","grpc-keep-alive-timeout":"الحد الأقصى للوقت الذي يجب أن ينتظره الخادم لاستجابة رسالة الحفاظ على الاتصال قبل اعتبار الاتصال ميتًا.","grpc-keep-alive":"المدة بين رسائل حفظ الاتصال المتعاقبة عند عدم وجود استدعاء RPC نشط.","grpc-min-time-between-pings":"الحد الأدنى للوقت الذي يجب فيه أن ينتظر الخادم بين إرسال رسائل حفظ الاتصال","grpc-max-pings-without-data":"الحد الأقصى لعدد رسائل حفظ الاتصال التي يمكن للخادم إرسالها دون تلقي أي بيانات قبل اعتبار الاتصال ميتًا.","grpc-min-ping-interval-without-data":"الحد الأدنى للوقت الذي يجب فيه أن ينتظر الخادم بين إرسال رسائل حفظ الاتصال عند عدم إرسال أو استلام بيانات.","permit-without-calls":"السماح للخادم بإبقاء اتصال GRPC حيًا حتى عندما لا تكون هناك استدعاءات RPC نشطة."}},yt={"add-entry":"Afegir configuració","connector-add":"Afegir conector","connector-enabled":"Activar conector","connector-name":"Nom conector","connector-name-required":"Cal nom conector.","connector-type":"Tipus conector","connector-type-required":"Cal tipus conector.",connectors:"Configuració de conectors","create-new-gateway":"Crear un gateway nou","create-new-gateway-text":"Crear un nou gateway amb el nom: '{{gatewayName}}'?",delete:"Esborrar configuració","download-tip":"Descarregar fitxer de configuració",gateway:"Gateway","gateway-exists":"Ja existeix un dispositiu amb el mateix nom.","gateway-name":"Nom de Gateway","gateway-name-required":"Cal un nom de gateway.","gateway-saved":"Configuració de gateway gravada satisfactòriament.","json-parse":"JSON no vàlid.","json-required":"El camp no pot ser buit.","no-connectors":"No hi ha conectors","no-data":"No hi ha configuracions","no-gateway-found":"No s'ha trobat cap gateway.","no-gateway-matching":" '{{item}}' no trobat.","path-logs":"Ruta als fitxers de log","path-logs-required":"Cal ruta.",remote:"Configuració remota","remote-logging-level":"Nivel de logging","remove-entry":"Esborrar configuració","save-tip":"Gravar fitxer de configuració","security-type":"Tipus de seguretat","security-types":{"access-token":"Token d'accés",tls:"TLS"},storage:"Grabació","storage-max-file-records":"Número màxim de registres en fitxer","storage-max-files":"Número màxim de fitxers","storage-max-files-min":"El número mínim és 1.","storage-max-files-pattern":"Número no vàlid.","storage-max-files-required":"Cal número.","storage-max-records":"Màxim de registres en el magatzem","storage-max-records-min":"El número mínim és 1.","storage-max-records-pattern":"Número no vàlid.","storage-max-records-required":"Cal número.","storage-pack-size":"Mida màxim de esdeveniments","storage-pack-size-min":"El número mínim és 1.","storage-pack-size-pattern":"Número no vàlid.","storage-pack-size-required":"Cal número.","storage-path":"Ruta de magatzem","storage-path-required":"Cal ruta de magatzem.","storage-type":"Tipus de magatzem","storage-types":{"file-storage":"Magatzem fitxer","memory-storage":"Magatzem en memoria"},thingsboard:"ThingsBoard","thingsboard-host":"Host ThingsBoard","thingsboard-host-required":"Cal Host.","thingsboard-port":"Port ThingsBoard","thingsboard-port-max":"El port màxim és 65535.","thingsboard-port-min":"El port mínim és 1.","thingsboard-port-pattern":"Port no vàlid.","thingsboard-port-required":"Cal port.",tidy:"Endreçat","tidy-tip":"Endreçat JSON","title-connectors-json":"Configuració conector {{typeName}}","tls-path-ca-certificate":"Ruta al certificat CA al gateway","tls-path-client-certificate":"Ruta al certificat client al gateway","tls-path-private-key":"Ruta a la clau privada al gateway","toggle-fullscreen":"Pantalla completa fullscreen","transformer-json-config":"Configuració JSON*","update-config":"Afegir/actualizar configuració JSON"},vt={"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"},xt={"add-entry":"Tilføj konfiguration","connector-add":"Tilføj ny stikforbindelse","connector-enabled":"Aktivér stikforbindelse","connector-name":"Navn på stikforbindelse","connector-name-required":"Navn på stikforbindelse er påkrævet.","connector-type":"Stikforbindelsestype","connector-type-required":"Stikforbindelsestype er påkrævet.",connectors:"Konfiguration af stikforbindelser","create-new-gateway":"Opret en ny gateway","create-new-gateway-text":"",delete:"Slet konfiguration","download-tip":"Download konfigurationsfil",gateway:"Gateway","gateway-exists":"Enhed med samme navn findes allerede.","gateway-name":"Gateway-navn","gateway-name-required":"Gateway-navn er påkrævet.","gateway-saved":"Gateway-konfigurationen blev gemt.","json-parse":"Ikke gyldig JSON.","json-required":"Feltet må ikke være tomt.","no-connectors":"Ingen stikforbindelser","no-data":"Ingen konfigurationer","no-gateway-found":"Ingen gateway fundet.","no-gateway-matching":"","path-logs":"Sti til logfiler","path-logs-required":"Sti er påkrævet.",remote:"Fjernkonfiguration","remote-logging-level":"Logføringsniveau","remove-entry":"Fjern konfiguration","save-tip":"Gem konfigurationsfil","security-type":"Sikkerhedstype","security-types":{"access-token":"Adgangstoken",tls:"TLS"},storage:"Lagring","storage-max-file-records":"Maks. antal poster i fil","storage-max-files":"Maks. antal filer","storage-max-files-min":"Min. antal er 1.","storage-max-files-pattern":"Antal er ikke gyldigt.","storage-max-files-required":"Antal er påkrævet.","storage-max-records":"Maks. antal poster i lagring","storage-max-records-min":"Min. antal poster er 1.","storage-max-records-pattern":"Antal er ikke gyldigt.","storage-max-records-required":"Maks. antal poster er påkrævet.","storage-pack-size":"Maks. antal pakkestørrelse for begivenhed","storage-pack-size-min":"Min. antal er 1.","storage-pack-size-pattern":"Antal er ikke gyldigt.","storage-pack-size-required":"Maks. antal pakkestørrelse for begivenhed er påkrævet.","storage-path":"Lagringssti","storage-path-required":"Lagringssti er påkrævet.","storage-type":"Lagringstype","storage-types":{"file-storage":"Lagring af filter","memory-storage":"Lagring af hukommelse"},thingsboard:"ThingsBoard","thingsboard-host":"ThingsBoard-vært","thingsboard-host-required":"Vært er påkrævet.","thingsboard-port":"ThingsBoard-port","thingsboard-port-max":"Maks. portnummer er 65535.","thingsboard-port-min":"Min. portnummer er 1.","thingsboard-port-pattern":"Port er ikke gyldig.","thingsboard-port-required":"Port er påkrævet.",tidy:"Tidy","tidy-tip":"Tidy konfig. JSON","title-connectors-json":"","tls-path-ca-certificate":"Sti til CA-certifikat på gateway","tls-path-client-certificate":"Sti til klientcertifikat på gateway","tls-path-private-key":"Sti til privat nøgle på gateway","toggle-fullscreen":"Skift til fuld skærm","transformer-json-config":"Konfiguration JSON*","update-config":"Tilføj/opdater konfiguration JSON"},bt={"add-entry":"Añadir configuración",advanced:"Avanzado","checking-device-activity":"Probando actividad de dispositivo",command:"Comandos Docker","command-copied-message":"Se han copiado los comandos al portapapeles",configuration:"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:"Conectores","connectors-config":"Configuración de conectores","connectors-table-enabled":"Enabled","connectors-table-name":"Nombre","connectors-table-type":"Tipo","connectors-table-status":"Estado","connectors-table-actions":"Acciones","connectors-table-key":"Clave","connectors-table-class":"Clase","rpc-command-send":"Enviar","rpc-command-result":"Resultado","rpc-command-edit-params":"Editar parametros","gateway-configuration":"Configuración General","create-new-gateway":"Crear un gateway nuevo","create-new-gateway-text":"Crear un nuevo gateway con el nombre: '{{gatewayName}}'?","created-time":"Hora de creación","configuration-delete-dialog-header":"Las configuraciones se borrarán","configuration-delete-dialog-body":"Sólo es posible desactivar la configuración remota, si hay acceso físico al gateway. Se borrarán todas las configuraciones previas.

\nPara desactivar la configuración, introduce el nombre del gateway aquí","configuration-delete-dialog-input":"Nombre Gateway","configuration-delete-dialog-input-required":"Se requiere nombre de gateway","configuration-delete-dialog-confirm":"Desactivar",delete:"Borrar configuración","download-tip":"Descargar fichero de configuración","drop-file":"Arrastra un fichero o",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.",grpc:"GRPC","grpc-keep-alive-timeout":"Timeout Keep alive (en ms)","grpc-keep-alive-timeout-required":"Se requiere Timeout Keep alive","grpc-keep-alive-timeout-min":"El valor no puede ser menor de 1","grpc-keep-alive-timeout-pattern":"El valor no es válido","grpc-keep-alive":"Keep alive (en ms)","grpc-keep-alive-required":"Se requiere keep alive","grpc-keep-alive-min":"El valor no puede ser menor de 1","grpc-keep-alive-pattern":"El valor keep alive no es válido","grpc-min-time-between-pings":"Tiempo mínimo entre pings (en ms)","grpc-min-time-between-pings-required":"Se requiere tiempo mínimo entre pings","grpc-min-time-between-pings-min":"El valor no puede ser menor de 1","grpc-min-time-between-pings-pattern":"El valor de tiempo mínimo entre pings no es válido","grpc-min-ping-interval-without-data":"Intervalo mínimo sin datos (en ms)","grpc-min-ping-interval-without-data-required":"Se requiere intervalo","grpc-min-ping-interval-without-data-min":"El valor no puede ser menor de 1","grpc-min-ping-interval-without-data-pattern":"El valor de intervalo no es válido","grpc-max-pings-without-data":"Intervalo máximo sin datos","grpc-max-pings-without-data-required":"Se requiere intervalo","grpc-max-pings-without-data-min":"El valor no puede ser menor de 1","grpc-max-pings-without-data-pattern":"El valor de intervalo no es válido","inactivity-check-period-seconds":"Periodo de control de inactividad (en segundos)","inactivity-check-period-seconds-required":"Se requiere periodo","inactivity-check-period-seconds-min":"El valor no puede ser menor de 1","inactivity-check-period-seconds-pattern":"El valor del periodo no es válido","inactivity-timeout-seconds":"Timeout de inactividad (en segundos)","inactivity-timeout-seconds-required":"Se requiere timeout de inactividad","inactivity-timeout-seconds-min":"El valor no puede ser menor de 1","inactivity-timeout-seconds-pattern":"El valor de inactividad no es válido","json-parse":"JSON no válido.","json-required":"El campo no puede estar vacío.",logs:{logs:"Registros",days:"días",hours:"horas",minutes:"minutos",seconds:"segundos","date-format":"Formato de fecha","date-format-required":"Se requiere formato de fecha","log-format":"Formato de registro","log-type":"Tipo de registro","log-format-required":"Se requiere tipo de registro",remote:"Registro remoto","remote-logs":"Registro remoto",local:"Registro local",level:"Nivel de registro","file-path":"Ruta de fichero","file-path-required":"Se requiere ruta de fichero","saving-period":"Periodo de guardado de registros","saving-period-min":"El periodo no puede ser menor que 1","saving-period-required":"Se requiere periodo de guardado","backup-count":"Número de backups","backup-count-min":"El número de backups no puede ser menor que 1","backup-count-required":"Se requiere número de backups"},"min-pack-send-delay":"Tiempo de espera, envío de paquetes (en ms)","min-pack-send-delay-required":"Se requiere tiempo de espera","min-pack-send-delay-min":"El tiempo de espera no puede ser menor que 0","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.","permit-without-calls":"Permitir Keep alive si llamadas",remote:"Configuración remota","remote-logging-level":"Nivel de logging","remove-entry":"Borrar configuración","remote-shell":"Consola remota","remote-configuration":"Configuración remota",other:"otros","save-tip":"Grabar fichero de configuración","security-type":"Tipo de seguridad","security-types":{"access-token":"Tóken de acceso","username-password":"Usuario y contraseña",tls:"TLS","tls-access-token":"TLS + Tóken de acceso","tls-private-key":"TLS + Clave privada"},"server-port":"Puerto del servidor",statistics:{statistic:"Estadística",statistics:"Estadísticas","statistic-commands-empty":"No hay estadísticas",commands:"Comandos","send-period":"Periodo de envío de estadísticas (en segundos)","send-period-required":"Se requiere periodo de envío","send-period-min":"El periodo de envío no puede ser menor de 60","send-period-pattern":"El periodo de envío no es válido","check-connectors-configuration":"Revisar configuración de conectores (en segundos)","check-connectors-configuration-required":"Se requiere un valor","check-connectors-configuration-min":"El valor no puede ser menor de 1","check-connectors-configuration-pattern":"La configuración no es válida",add:"Añadir comando",timeout:"Timeout","timeout-required":"Se requiere timeout","timeout-min":"El timeout no puede ser menor de 1","timeout-pattern":"El timeout no es válido","attribute-name":"Nombre de atributo","attribute-name-required":"Se requiere nombre de atributo",command:"Comando","command-required":"Se requiere comando",remove:"Borrar comando"},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-read-record-count":"Leer número de entradas en almacén","storage-read-record-count-min":"El número mínimo de entradas es 1.","storage-read-record-count-pattern":"El número no es válido.","storage-read-record-count-required":"Se requiere número de entradas.","storage-max-read-record-count":"Número máximo de entradas en el almacén","storage-max-read-record-count-min":"El número mínimo es 1.","storage-max-read-record-count-pattern":"El número no es válido","storage-max-read-record-count-required":"Se requiere número máximo de entradas.","storage-data-folder-path":"Ruta de carpeta de datos","storage-data-folder-path-required":"Se requiere ruta.","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 en fichero","memory-storage":"Almacén en memoria",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"General","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","messages-ttl-check-in-hours":"Comprobación de TTL de mensajes en horas","messages-ttl-check-in-hours-required":"Campo requerido.","messages-ttl-check-in-hours-min":"El mínimo es 1.","messages-ttl-check-in-hours-pattern":"El número no es válido.","messages-ttl-in-days":"TTL (Time to live) de mensages en días","messages-ttl-in-days-required":"Se requiere TTL de mensajes.","messages-ttl-in-days-min":"El número mínimo es 1.","messages-ttl-in-days-pattern":"El número no es válido.","mqtt-qos":"QoS","mqtt-qos-required":"Se requiere QoS","mqtt-qos-range":"El rango de valores es desde 0 a 1","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",hints:{"remote-configuration":"Habilita la administración y configuración remota del gateway","remote-shell":"Habilita el control remoto del sistema operativo del gateway desde el widget terminal remoto",host:"Hostname o dirección IP del servidor Thingsboard",port:"Puerto del servicio MQTT en el servidor Thingsboard",token:"Access token para el gateway","client-id":"ID de cliente MQTT para el gateway",username:"Usuario MQTT para el gateway",password:"Contraseña MQTT para el gateway","ca-cert":"Ruta al fichero del certificado CA","date-form":"Formato de fecha en los mensajes de registro","data-folder":"Ruta a la carpeta que contendrá los datos (Relativa o absoluta)","log-format":"Formato de mensajes en registro","remote-log":"Habilita el registro remoto y la posterior lectura desde el gateway","backup-count":"Si el contaje de copias de seguridad es mayor que 0, cuando se realice una renovación, no se conservan más que los archivos de recuento de copias de seguridad, los más antíguos se eliminarán",storage:"Provee la configuración para el grabado de datos entrantes antes de que se envíen a la plataforma","max-file-count":"Número máximo de ficheros que se crearán","max-read-count":"Númeo máximo de mensajes a obtener desde el disco y enviados a la plataforma","max-records":"Número máximo de registros que se guardarán en un solo fichero","read-record-count":"Número de mensages a obtener desde el almacenamiento y enviados a la plataforma","max-records-count":"Número máximo de datos en almacenamiento antes de enviar a la plataforma","ttl-check-hour":"Con qué frecuencia el gateway comprobará si los datos están obsoletos","ttl-messages-day":"Número máximo de días para la retención de datos en el almacén",commands:"Comandos para recoger estadísticas adicionales",attribute:"Clave de telemetría para estadísticas",timeout:"Timeout para la ejecución de comandos",command:"El resultado de la ejecución del comando, se usará como valor para la telemetría","check-device-activity":"Habilita la monitorización de cada uno de los dispositivos conectados","inactivity-timeout":"Tiempo tras que el gateway desconectará el dispositivo","inactivity-period":"Periodo de monitorización de actividad en el dispositivo","minimal-pack-delay":"Tiempo de espera entre envío de paquetes de mensajes (Un valor muy bajo, resultará en un aumento de uso de la CPU en el gateway)",qos:"Quality of Service en los mensajes MQTT (0 - at most once, 1 - at least once)","server-port":"Puerto de red en el cual el servidor GRPC escuchará conexiones entrantes.","grpc-keep-alive-timeout":"Tiempo máximo, el cual el servidor esperara un ping keepalive antes de considerar la conexión terminada.","grpc-keep-alive":"Duración entre dos pings keepalive cuando no haya llamada RPC activa.","grpc-min-time-between-pings":"Mínimo tiempo que el servidor debe esperar entre envíos de mensajes de ping","grpc-max-pings-without-data":"Número máximo de pings keepalive que el servidor puede enviar sin recibir ningún dato antes de considerar la conexión terminada.","grpc-min-ping-interval-without-data":"Mínimo tiempo que el servidor debe esperar entre envíos de ping keepalive cuando no haya ningún dato en envío o recepción.","permit-without-calls":"Permitir al servidor mantener la conexión GRPC abierta, cuando no haya llamadas RPC activas."}},wt={"add-entry":"설정 추가","connector-add":"새로운 연결자 추가","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"},St={"add-entry":"Add configuration",advanced:"Advanced","checking-device-activity":"Checking device activity",command:"Docker commands","command-copied-message":"Docker command has been copied to clipboard",configuration:"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","connectors-config":"Connectors configuration","connectors-table-enabled":"Enabled","connectors-table-name":"Name","connectors-table-type":"Type","connectors-table-status":"Status","connectors-table-actions":"Actions","connectors-table-key":"Key","connectors-table-class":"Class","rpc-command-send":"Send","rpc-command-result":"Result","rpc-command-edit-params":"Edit parameters","gateway-configuration":"General Configuration","docker-label":"In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.","create-new-gateway":"Create a new gateway","create-new-gateway-text":"Are you sure you want create a new gateway with name: '{{gatewayName}}'?","created-time":"Created time","configuration-delete-dialog-header":"Configurations will be deleted","configuration-delete-dialog-body":"Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below","configuration-delete-dialog-input":"Gateway name","configuration-delete-dialog-input-required":"Gateway name is mandatory","configuration-delete-dialog-confirm":"Turn Off",delete:"Delete configuration","download-tip":"Download configuration file","drop-file":"Drop file here or",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.",grpc:"GRPC","grpc-keep-alive-timeout":"Keep alive timeout (in ms)","grpc-keep-alive-timeout-required":"Keep alive timeout is required","grpc-keep-alive-timeout-min":"Keep alive timeout can not be less then 1","grpc-keep-alive-timeout-pattern":"Keep alive timeout is not valid","grpc-keep-alive":"Keep alive (in ms)","grpc-keep-alive-required":"Keep alive is required","grpc-keep-alive-min":"Keep alive can not be less then 1","grpc-keep-alive-pattern":"Keep alive is not valid","grpc-min-time-between-pings":"Min time between pings (in ms)","grpc-min-time-between-pings-required":"Min time between pings is required","grpc-min-time-between-pings-min":"Min time between pings can not be less then 1","grpc-min-time-between-pings-pattern":"Min time between pings is not valid","grpc-min-ping-interval-without-data":"Min ping interval without data (in ms)","grpc-min-ping-interval-without-data-required":"Min ping interval without data is required","grpc-min-ping-interval-without-data-min":"Min ping interval without data can not be less then 1","grpc-min-ping-interval-without-data-pattern":"Min ping interval without data is not valid","grpc-max-pings-without-data":"Max pings without data","grpc-max-pings-without-data-required":"Max pings without data is required","grpc-max-pings-without-data-min":"Max pings without data can not be less then 1","grpc-max-pings-without-data-pattern":"Max pings without data is not valid","inactivity-check-period-seconds":"Inactivity check period (in sec)","inactivity-check-period-seconds-required":"Inactivity check period is required","inactivity-check-period-seconds-min":"Inactivity check period can not be less then 1","inactivity-check-period-seconds-pattern":"Inactivity check period is not valid","inactivity-timeout-seconds":"Inactivity timeout (in sec)","inactivity-timeout-seconds-required":"Inactivity timeout is required","inactivity-timeout-seconds-min":"Inactivity timeout can not be less then 1","inactivity-timeout-seconds-pattern":"Inactivity timeout is not valid","json-parse":"Not valid JSON.","json-required":"Field cannot be empty.",logs:{logs:"Logs",days:"days",hours:"hours",minutes:"minutes",seconds:"seconds","date-format":"Date format","date-format-required":"Date format required","log-format":"Log format","log-type":"Log type","log-format-required":"Log format required",remote:"Remote logging","remote-logs":"Remote logs",local:"Local logging",level:"Log level","file-path":"File path","file-path-required":"File path required","saving-period":"Log saving period","saving-period-min":"Log saving period can not be less then 1","saving-period-required":"Log saving period required","backup-count":"Backup count","backup-count-min":"Backup count can not be less then 1","backup-count-required":"Backup count required"},"min-pack-send-delay":"Min pack send delay (in ms)","min-pack-send-delay-required":"Min pack send delay is required","min-pack-send-delay-min":"Min pack send delay can not be less then 0","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.","permit-without-calls":"Keep alive permit without calls",remote:"Remote configuration","remote-logging-level":"Logging level","remove-entry":"Remove configuration","remote-shell":"Remote shell","remote-configuration":"Remote Configuration",other:"Other","save-tip":"Save configuration file","security-type":"Security type","security-types":{"access-token":"Access Token","username-password":"Username and Password",tls:"TLS","tls-access-token":"TLS + Access Token","tls-private-key":"TLS + Private Key"},"server-port":"Server port",statistics:{statistic:"Statistic",statistics:"Statistics","statistic-commands-empty":"No statistics available",commands:"Commands","send-period":"Statistic send period (in sec)","send-period-required":"Statistic send period is required","send-period-min":"Statistic send period can not be less then 60","send-period-pattern":"Statistic send period is not valid","check-connectors-configuration":"Check connectors configuration (in sec)","check-connectors-configuration-required":"Check connectors configuration is required","check-connectors-configuration-min":"Check connectors configuration can not be less then 1","check-connectors-configuration-pattern":"Check connectors configuration is not valid",add:"Add command",timeout:"Timeout","timeout-required":"Timeout is required","timeout-min":"Timeout can not be less then 1","timeout-pattern":"Timeout is not valid","attribute-name":"Attribute name","attribute-name-required":"Attribute name is required",command:"Command","command-required":"Command is required",remove:"Remove command"},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-read-record-count":"Read record count in storage","storage-read-record-count-min":"Minimum number of records is 1.","storage-read-record-count-pattern":"Number is not valid.","storage-read-record-count-required":"Read record count is required.","storage-max-read-record-count":"Max read record count in storage","storage-max-read-record-count-min":"Minimum number of records is 1.","storage-max-read-record-count-pattern":"Number is not valid.","storage-max-read-record-count-required":"Max Read record count is required.","storage-data-folder-path":"Data folder path","storage-data-folder-path-required":"Data folder path 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",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"General","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","messages-ttl-check-in-hours":"Messages TTL check in hours","messages-ttl-check-in-hours-required":"Messages TTL check in hours is required.","messages-ttl-check-in-hours-min":"Min number is 1.","messages-ttl-check-in-hours-pattern":"Number is not valid.","messages-ttl-in-days":"Messages TTL in days","messages-ttl-in-days-required":"Messages TTL in days is required.","messages-ttl-in-days-min":"Min number is 1.","messages-ttl-in-days-pattern":"Number is not valid.","mqtt-qos":"QoS","mqtt-qos-required":"QoS is required","mqtt-qos-range":"QoS values range is from 0 to 1","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",hints:{"remote-configuration":"Enables remote configuration and management of the gateway","remote-shell":"Enables remote control of the operating system with the gateway from the Remote Shell widget",host:"Hostname or IP address of ThingsBoard server",port:"Port of MQTT service on ThingsBoard server",token:"Access token for the gateway from ThingsBoard server","client-id":"MQTT client id for the gateway form ThingsBoard server",username:"MQTT username for the gateway form ThingsBoard server",password:"MQTT password for the gateway form ThingsBoard server","ca-cert":"Path to CA certificate file","date-form":"Date format in log message","data-folder":"Path to folder, that will contains data (Relative or Absolute)","log-format":"Log message format","remote-log":"Enables remote logging and logs reading from the gateway","backup-count":"If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",storage:"Provides configuration for saving incoming data before it is sent to the platform","max-file-count":"Maximum count of file that will be created","max-read-count":"Count of messages to get from storage and send to ThingsBoard","max-records":"Maximum count of records that will be stored in one file","read-record-count":"Count of messages to get from storage and send to ThingsBoard","max-records-count":"Maximum count of data in storage before send to ThingsBoard","ttl-check-hour":"How often will Gateway check data for obsolescence","ttl-messages-day":"Maximum days that storage will save data",commands:"Commands for collecting additional statistic",attribute:"Statistic telemetry key",timeout:"Timeout for command executing",command:"The result of the command execution, will be used as the value for telemetry","check-device-activity":"Enables monitor the activity of each connected device","inactivity-timeout":"Time after whose the gateway will disconnect device","inactivity-period":"Periodicity of device activity check","minimal-pack-delay":"Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)",qos:"Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)","server-port":"Network port on which GRPC server will listen for incoming connections.","grpc-keep-alive-timeout":"Maximum time the server should wait for a keepalive ping response before considering the connection dead.","grpc-keep-alive":"Duration between two successive keepalive ping messages when there is no active RPC call.","grpc-min-time-between-pings":"Minimum amount of time the server should wait between sending keepalive ping messages","grpc-max-pings-without-data":"Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.","grpc-min-ping-interval-without-data":"Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.","permit-without-calls":"Allow server to keep the GRPC connection alive even when there are no active RPC calls."}},Ct={"add-entry":"Configuratie toevoegen","connector-add":"Nieuwe connector toevoegen","connector-enabled":"Connector inschakelen","connector-name":"Naam van de connector","connector-name-required":"De naam van de connector is vereist.","connector-type":"Type aansluiting","connector-type-required":"Het type connector is vereist.",connectors:"Configuratie van connectoren","create-new-gateway":"Een nieuwe gateway maken","create-new-gateway-text":"Weet u zeker dat u een nieuwe gateway wilt maken met de naam: '{{gatewayName}}'?",delete:"Configuratie verwijderen","download-tip":"Configuratiebestand downloaden",gateway:"Gateway","gateway-exists":"Device met dezelfde naam bestaat al.","gateway-name":"Naam van de gateway","gateway-name-required":"De naam van de gateway is vereist.","gateway-saved":"Gatewayconfiguratie succesvol opgeslagen.","json-parse":"Ongeldige JSON.","json-required":"Het veld mag niet leeg zijn.","no-connectors":"Geen connectoren","no-data":"Geen configuraties","no-gateway-found":"Geen gateway gevonden.","no-gateway-matching":"'{{item}}' niet gevonden.","path-logs":"Pad naar logbestanden","path-logs-required":"Pad is vereist.",remote:"Configuratie op afstand","remote-logging-level":"Registratie niveau","remove-entry":"Configuratie verwijderen","save-tip":"Configuratiebestand opslaan","security-type":"Soort beveiliging","security-types":{"access-token":"Toegang tot token",tls:"TLS (TLS)"},storage:"Opslag","storage-max-file-records":"Maximum aantal records in bestand","storage-max-files":"Maximaal aantal bestanden","storage-max-files-min":"Minimum aantal is 1.","storage-max-files-pattern":"Nummer is niet geldig.","storage-max-files-required":"Nummer is vereist.","storage-max-records":"Maximum aantal records in opslag","storage-max-records-min":"Minimum aantal records is 1.","storage-max-records-pattern":"Nummer is niet geldig.","storage-max-records-required":"Maximale records zijn vereist.","storage-pack-size":"Maximale pakketgrootte voor events","storage-pack-size-min":"Minimum aantal is 1.","storage-pack-size-pattern":"Nummer is niet geldig.","storage-pack-size-required":"De maximale pakketgrootte van het event is vereist.","storage-path":"Opslag pad","storage-path-required":"Opslagpad is vereist.","storage-type":"Type opslag","storage-types":{"file-storage":"Opslag van bestanden","memory-storage":"Geheugen opslag"},thingsboard:"Dingen Bord","thingsboard-host":"ThingsBoard-gastheer","thingsboard-host-required":"Server host is vereist.","thingsboard-port":"ThingsBoard-poort","thingsboard-port-max":"Het maximale poortnummer is 65535.","thingsboard-port-min":"Het minimale poortnummer is 1.","thingsboard-port-pattern":"Poort is niet geldig.","thingsboard-port-required":"Poort is vereist.",tidy:"Ordelijk","tidy-tip":"Opgeruimde configuratie JSON","title-connectors-json":"Configuratie van connector {{typeName}}","tls-path-ca-certificate":"Pad naar CA-certificaat op gateway","tls-path-client-certificate":"Pad naar clientcertificaat op gateway","tls-path-private-key":"Pad naar privésleutel op gateway","toggle-fullscreen":"Volledig scherm in- en uitschakelen","transformer-json-config":"Configuratie JSON*","update-config":"Configuratie JSON toevoegen/bijwerken"},_t={"add-entry":"Dodaj konfigurację",advanced:"Advanced","checking-device-activity":"Checking device activity",command:"Docker commands","command-copied-message":"Docker command has been copied to clipboard",configuration:"Configuration","connector-add":"Dodaj nowe złącze","connector-enabled":"Włącz złącze","connector-name":"Nazwa złącza","connector-name-required":"Nazwa złącza jest wymagana.","connector-type":"Typ złącza","connector-type-required":"Typ złącza jest wymagany.",connectors:"Konfiguracja złączy","connectors-config":"Connectors configuration","connectors-table-enabled":"Enabled","connectors-table-name":"Name","connectors-table-type":"Type","connectors-table-status":"Status","connectors-table-actions":"Actions","connectors-table-key":"Key","connectors-table-class":"Class","rpc-command-send":"Send","rpc-command-result":"Result","rpc-command-edit-params":"Edit parameters","gateway-configuration":"General Configuration","docker-label":"In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.","create-new-gateway":"Utwórz nowy gateway","create-new-gateway-text":"Czy na pewno chcesz utworzyć nowy gateway o nazwie: '{{gatewayName}}'?","created-time":"Created time","configuration-delete-dialog-header":"Configurations will be deleted","configuration-delete-dialog-body":"Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below","configuration-delete-dialog-input":"Gateway name","configuration-delete-dialog-input-required":"Gateway name is mandatory","configuration-delete-dialog-confirm":"Turn Off",delete:"Usuń konfigurację","download-tip":"Pobierz plik konfiguracyjny","drop-file":"Drop file here or",gateway:"Wejście","gateway-exists":"Urządzenie o tej samej nazwie już istnieje.","gateway-name":"Nazwa Gateway","gateway-name-required":"Nazwa Gateway'a jest wymagana.","gateway-saved":"Konfiguracja Gatewey'a została pomyślnie zapisana.",grpc:"GRPC","grpc-keep-alive-timeout":"Keep alive timeout (in ms)","grpc-keep-alive-timeout-required":"Keep alive timeout is required","grpc-keep-alive-timeout-min":"Keep alive timeout can not be less then 1","grpc-keep-alive-timeout-pattern":"Keep alive timeout is not valid","grpc-keep-alive":"Keep alive (in ms)","grpc-keep-alive-required":"Keep alive is required","grpc-keep-alive-min":"Keep alive can not be less then 1","grpc-keep-alive-pattern":"Keep alive is not valid","grpc-min-time-between-pings":"Min time between pings (in ms)","grpc-min-time-between-pings-required":"Min time between pings is required","grpc-min-time-between-pings-min":"Min time between pings can not be less then 1","grpc-min-time-between-pings-pattern":"Min time between pings is not valid","grpc-min-ping-interval-without-data":"Min ping interval without data (in ms)","grpc-min-ping-interval-without-data-required":"Min ping interval without data is required","grpc-min-ping-interval-without-data-min":"Min ping interval without data can not be less then 1","grpc-min-ping-interval-without-data-pattern":"Min ping interval without data is not valid","grpc-max-pings-without-data":"Max pings without data","grpc-max-pings-without-data-required":"Max pings without data is required","grpc-max-pings-without-data-min":"Max pings without data can not be less then 1","grpc-max-pings-without-data-pattern":"Max pings without data is not valid","inactivity-check-period-seconds":"Inactivity check period (in sec)","inactivity-check-period-seconds-required":"Inactivity check period is required","inactivity-check-period-seconds-min":"Inactivity check period can not be less then 1","inactivity-check-period-seconds-pattern":"Inactivity check period is not valid","inactivity-timeout-seconds":"Inactivity timeout (in sec)","inactivity-timeout-seconds-required":"Inactivity timeout is required","inactivity-timeout-seconds-min":"Inactivity timeout can not be less then 1","inactivity-timeout-seconds-pattern":"Inactivity timeout is not valid","json-parse":"Nieprawidłowy JSON.","json-required":"Pole nie może być puste.",logs:{logs:"Logs",days:"days",hours:"hours",minutes:"minutes",seconds:"seconds","date-format":"Date format","date-format-required":"Date format required","log-format":"Log format","log-type":"Log type","log-format-required":"Log format required",remote:"Remote logging","remote-logs":"Remote logs",local:"Local logging",level:"Log level","file-path":"File path","file-path-required":"File path required","saving-period":"Log saving period","saving-period-min":"Log saving period can not be less then 1","saving-period-required":"Log saving period required","backup-count":"Backup count","backup-count-min":"Backup count can not be less then 1","backup-count-required":"Backup count required"},"min-pack-send-delay":"Min pack send delay (in ms)","min-pack-send-delay-required":"Min pack send delay is required","min-pack-send-delay-min":"Min pack send delay can not be less then 0","no-connectors":"Brak złączy","no-data":"Brak konfiguracji","no-gateway-found":"Nie znaleziono gateway'a.","no-gateway-matching":" '{{item}}' nie znaleziono.","path-logs":"Ścieżka do plików dziennika","path-logs-required":"Ścieżka jest wymagana.","permit-without-calls":"Keep alive permit without calls",remote:"Zdalna konfiguracja","remote-logging-level":"Poziom logowania","remove-entry":"Usuń konfigurację","remote-shell":"Remote shell","remote-configuration":"Remote Configuration",other:"Other","save-tip":"Zapisz plik konfiguracyjny","security-type":"Rodzaj zabezpieczenia","security-types":{"access-token":"Token dostępu","username-password":"Username and Password",tls:"TLS","tls-access-token":"TLS + Access Token","tls-private-key":"TLS + Private Key"},"server-port":"Server port",statistics:{statistic:"Statistic",statistics:"Statistics","statistic-commands-empty":"No statistics available",commands:"Commands","send-period":"Statistic send period (in sec)","send-period-required":"Statistic send period is required","send-period-min":"Statistic send period can not be less then 60","send-period-pattern":"Statistic send period is not valid","check-connectors-configuration":"Check connectors configuration (in sec)","check-connectors-configuration-required":"Check connectors configuration is required","check-connectors-configuration-min":"Check connectors configuration can not be less then 1","check-connectors-configuration-pattern":"Check connectors configuration is not valid",add:"Add command",timeout:"Timeout","timeout-required":"Timeout is required","timeout-min":"Timeout can not be less then 1","timeout-pattern":"Timeout is not valid","attribute-name":"Attribute name","attribute-name-required":"Attribute name is required",command:"Command","command-required":"Command is required",remove:"Remove command"},storage:"Składowanie","storage-max-file-records":"Maksymalna liczba rekordów w pliku","storage-max-files":"Maksymalna liczba plików","storage-max-files-min":"Minimalna liczba to 1.","storage-max-files-pattern":"Numer jest nieprawidłowy.","storage-max-files-required":"Numer jest wymagany.","storage-max-records":"Maksymalna liczba rekordów w pamięci","storage-max-records-min":"Minimalna liczba rekordów to 1.","storage-max-records-pattern":"Numer jest nieprawidłowy.","storage-max-records-required":"Maksymalna liczba rekordów jest wymagana.","storage-read-record-count":"Read record count in storage","storage-read-record-count-min":"Minimum number of records is 1.","storage-read-record-count-pattern":"Number is not valid.","storage-read-record-count-required":"Read record count is required.","storage-max-read-record-count":"Max read record count in storage","storage-max-read-record-count-min":"Minimum number of records is 1.","storage-max-read-record-count-pattern":"Number is not valid.","storage-max-read-record-count-required":"Max Read record count is required.","storage-data-folder-path":"Data folder path","storage-data-folder-path-required":"Data folder path is required.","storage-pack-size":"Maksymalny rozmiar pakietu wydarzeń","storage-pack-size-min":"Minimalna liczba to 1.","storage-pack-size-pattern":"Numer jest nieprawidłowy.","storage-pack-size-required":"Maksymalny rozmiar pakietu wydarzeń jest wymagany.","storage-path":"Ścieżka przechowywania","storage-path-required":"Ścieżka do przechowywania jest wymagana.","storage-type":"Typ składowania","storage-types":{"file-storage":"Nośnik danych","memory-storage":"Przechowywanie pamięci",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"General","thingsboard-host":"Gospodarz ThingsBoard","thingsboard-host-required":"Host jest wymagany.","thingsboard-port":"Port ThingsBoard","thingsboard-port-max":"Maksymalny numer portu to 65535.","thingsboard-port-min":"Minimalny numer portu to 1.","thingsboard-port-pattern":"Port jest nieprawidłowy.","thingsboard-port-required":"Port jest wymagany.",tidy:"Uporządkuj","tidy-tip":"Uporządkowana konfiguracja JSON","title-connectors-json":"Złącze {{typeName}} konfiguracja","tls-path-ca-certificate":"Ścieżka do certyfikatu CA na gateway","tls-path-client-certificate":"Ścieżka do certyfikatu klienta na gateway","messages-ttl-check-in-hours":"Messages TTL check in hours","messages-ttl-check-in-hours-required":"Messages TTL check in hours is required.","messages-ttl-check-in-hours-min":"Min number is 1.","messages-ttl-check-in-hours-pattern":"Number is not valid.","messages-ttl-in-days":"Messages TTL in days","messages-ttl-in-days-required":"Messages TTL in days is required.","messages-ttl-in-days-min":"Min number is 1.","messages-ttl-in-days-pattern":"Number is not valid.","mqtt-qos":"QoS","mqtt-qos-required":"QoS is required","mqtt-qos-range":"QoS values range is from 0 to 1","tls-path-private-key":"Ścieżka do klucza prywatnego na bramce","toggle-fullscreen":"Przełącz tryb pełnoekranowy","transformer-json-config":"Konfiguracja JSON*","update-config":"Dodaj/zaktualizuj konfigurację JSON",hints:{"remote-configuration":"Enables remote configuration and management of the gateway","remote-shell":"Enables remote control of the operating system with the gateway from the Remote Shell widget",host:"Hostname or IP address of ThingsBoard server",port:"Port of MQTT service on ThingsBoard server",token:"Access token for the gateway from ThingsBoard server","client-id":"MQTT client id for the gateway form ThingsBoard server",username:"MQTT username for the gateway form ThingsBoard server",password:"MQTT password for the gateway form ThingsBoard server","ca-cert":"Path to CA certificate file","date-form":"Date format in log message","data-folder":"Path to folder, that will contains data (Relative or Absolute)","log-format":"Log message format","remote-log":"Enables remote logging and logs reading from the gateway","backup-count":"If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",storage:"Provides configuration for saving incoming data before it is sent to the platform","max-file-count":"Maximum count of file that will be created","max-read-count":"Count of messages to get from storage and send to ThingsBoard","max-records":"Maximum count of records that will be stored in one file","read-record-count":"Count of messages to get from storage and send to ThingsBoard","max-records-count":"Maximum count of data in storage before send to ThingsBoard","ttl-check-hour":"How often will Gateway check data for obsolescence","ttl-messages-day":"Maximum days that storage will save data",commands:"Commands for collecting additional statistic",attribute:"Statistic telemetry key",timeout:"Timeout for command executing",command:"The result of the command execution, will be used as the value for telemetry","check-device-activity":"Enables monitor the activity of each connected device","inactivity-timeout":"Time after whose the gateway will disconnect device","inactivity-period":"Periodicity of device activity check","minimal-pack-delay":"Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)",qos:"Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)","server-port":"Network port on which GRPC server will listen for incoming connections.","grpc-keep-alive-timeout":"Maximum time the server should wait for a keepalive ping response before considering the connection dead.","grpc-keep-alive":"Duration between two successive keepalive ping messages when there is no active RPC call.","grpc-min-time-between-pings":"Minimum amount of time the server should wait between sending keepalive ping messages","grpc-max-pings-without-data":"Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.","grpc-min-ping-interval-without-data":"Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.","permit-without-calls":"Allow server to keep the GRPC connection alive even when there are no active RPC calls."}},Tt={"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"},It={"add-entry":"Dodaj konfiguracijo","connector-add":"Dodaj nov priključek","connector-enabled":"Omogoči priključek","connector-name":"Ime priključka","connector-name-required":"Ime priključka je obvezno.","connector-type":"Vrsta priključka","connector-type-required":"Zahteva se vrsta priključka.",connectors:"Konfiguracija priključkov","create-new-gateway":"Ustvari nov prehod","create-new-gateway-text":"Ali ste prepričani, da želite ustvariti nov prehod z imenom: '{{gatewayName}}'?",delete:"Izbriši konfiguracijo","download-tip":"Prenos konfiguracijske datoteke",gateway:"Prehod","gateway-exists":"Naprava z istim imenom že obstaja.","gateway-name":"Ime prehoda","gateway-name-required":"Ime prehoda je obvezno.","gateway-saved":"Konfiguracija prehoda je uspešno shranjena.","json-parse":"Neveljaven JSON.","json-required":"Polje ne sme biti prazno.","no-connectors":"Ni priključkov","no-data":"Brez konfiguracij","no-gateway-found":"Prehod ni najden.","no-gateway-matching":" '{{item}}' ni mogoče najti.","path-logs":"Pot do dnevniških datotek","path-logs-required":"Pot je obvezna.",remote:"Oddaljena konfiguracija","remote-logging-level":"Raven beleženja","remove-entry":"Odstrani konfiguracijo","save-tip":"Shrani konfiguracijsko datoteko","security-type":"Vrsta zaščite","security-types":{"access-token":"Dostopni žeton",tls:"TLS"},storage:"Shramba","storage-max-file-records":"Največ zapisov v datoteki","storage-max-files":"Največje število datotek","storage-max-files-min":"Najmanjše število je 1.","storage-max-files-pattern":"Številka ni veljavna.","storage-max-files-required":"Številka je obvezna.","storage-max-records":"Največ zapisov v pomnilniku","storage-max-records-min":"Najmanjše število zapisov je 1.","storage-max-records-pattern":"Številka ni veljavna.","storage-max-records-required":"Zahtevan je največ zapisov.","storage-pack-size":"Največja velikost paketa dogodkov","storage-pack-size-min":"Najmanjše število je 1.","storage-pack-size-pattern":"Številka ni veljavna.","storage-pack-size-required":"Zahtevana je največja velikost paketa dogodkov.","storage-path":"Pot pomnilnika","storage-path-required":"Zahtevana je pot do pomnilnika.","storage-type":"Vrsta pomnilnika","storage-types":{"file-storage":"Shramba datotek","memory-storage":"Spomin pomnilnika"},thingsboard:"ThingsBoard","thingsboard-host":"Gostitelj ThingsBoard","thingsboard-host-required":"Potreben je gostitelj.","thingsboard-port":"Vrata ThingsBoard","thingsboard-port-max":"Največja številka vrat je 65535.","thingsboard-port-min":"Najmanjša številka vrat je 1.","thingsboard-port-pattern":"Vrata niso veljavna.","thingsboard-port-required":"Potrebna so vrata.",tidy:"Urejeno","tidy-tip":"Urejena konfiguracija JSON","title-connectors-json":"Konfiguracija konektorja {{typeName}}","tls-path-ca-certificate":"Pot do potrdila CA na prehodu","tls-path-client-certificate":"Pot do potrdila stranke na prehodu","tls-path-private-key":"Pot do zasebnega ključa na prehodu","toggle-fullscreen":"Preklop na celozaslonski način","transformer-json-config":"Konfiguracija JSON *","update-config":"Dodaj / posodobi konfiguracijo JSON"},Et={"add-entry":"Yapılandırma ekle","connector-add":"Yeni bağlayıcı ekle","connector-enabled":"Bağlayıcıyı etkinleştir","connector-name":"Bağlayıcı adı","connector-name-required":"Bağlayıcı adı gerekli.","connector-type":"Bağlayıcı tipi","connector-type-required":"Bağlayıcı türü gerekli.",connectors:"Bağlayıcıların yapılandırması","create-new-gateway":"Yeni bir ağ geçidi oluştur","create-new-gateway-text":"'{{gatewayName}}' adında yeni bir ağ geçidi oluşturmak istediğinizden emin misiniz?",delete:"Yapılandırmayı sil","download-tip":"Yapılandırma dosyasını indirin",gateway:"Ağ geçidi","gateway-exists":"Aynı ada sahip cihaz zaten var.","gateway-name":"Ağ geçidi adı","gateway-name-required":"Ağ geçidi adı gerekli.","gateway-saved":"Ağ geçidi yapılandırması başarıyla kaydedildi.","json-parse":"Geçerli bir JSON değil.","json-required":"Alan boş olamaz.","no-connectors":"Bağlayıcı yok","no-data":"Yapılandırma yok","no-gateway-found":"Ağ geçidi bulunamadı.","no-gateway-matching":" '{{item}}' bulunamadı.","path-logs":"Log dosyaları yolu","path-logs-required":"Log dosyaları dizini gerekli.",remote:"Uzaktan yapılandırma","remote-logging-level":"Loglama seviyesi","remove-entry":"Yapılandırmayı kaldır","save-tip":"Yapılandırma dosyasını kaydet","security-type":"Güvenlik türü","security-types":{"access-token":"Access Token",tls:"TLS"},storage:"Depolama","storage-max-file-records":"Dosyadaki maksimum kayıt","storage-max-files":"Maksimum dosya sayısı","storage-max-files-min":"Minimum sayı 1'dir.","storage-max-files-pattern":"Sayı geçerli değil.","storage-max-files-required":"Sayı gerekli.","storage-max-records":"Depodaki maksimum kayıt","storage-max-records-min":"Minimum kayıt sayısı 1'dir.","storage-max-records-pattern":"Sayı geçerli değil.","storage-max-records-required":"Maksimum kayıt gerekli.","storage-pack-size":"Maksimum etkinlik paketi boyutu","storage-pack-size-min":"Minimum sayı 1'dir.","storage-pack-size-pattern":"Sayı geçerli değil.","storage-pack-size-required":"Maksimum etkinlik paketi boyutu gerekli.","storage-path":"Depolama yolu","storage-path-required":"Depolama yolu gerekli.","storage-type":"Depolama türü","storage-types":{"file-storage":"Dosya depolama","memory-storage":"Bellek depolama"},thingsboard:"ThingsBoard","thingsboard-host":"ThingsBoard host","thingsboard-host-required":"Host gerekli.","thingsboard-port":"ThingsBoard port","thingsboard-port-max":"Maksimum port numarası 65535.","thingsboard-port-min":"Minimum port numarası 1'dir.","thingsboard-port-pattern":"Port geçerli değil.","thingsboard-port-required":"Port gerekli.",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"},Mt={"add-entry":"添加配置",advanced:"高级","checking-device-activity":"检查设备活动",command:"Docker命令","command-copied-message":"Docker命令已复制到剪贴板",configuration:"配置","connector-add":"添加连接器","connector-enabled":"启用连接器","connector-name":"连接器名称","connector-name-required":"连接器名称必填。","connector-type":"连接器类型","connector-type-required":"连接器类型必填。",connectors:"连接器配置","connectors-config":"连接器配置","connectors-table-enabled":"已启用","connectors-table-name":"名称","connectors-table-type":"类型","connectors-table-status":"状态","connectors-table-actions":"操作","connectors-table-key":"键","connectors-table-class":"类","rpc-command-send":"发送","rpc-command-result":"结果","rpc-command-edit-params":"编辑参数","gateway-configuration":"通用配置","create-new-gateway":"创建网关","create-new-gateway-text":"确定要创建名为 '{{gatewayName}}' 的新网关?","created-time":"创建时间","configuration-delete-dialog-header":"配置将被删除","configuration-delete-dialog-body":"只有对网关进行物理访问时,才有可能关闭远程配置。所有先前的配置都将被删除。

\n要关闭配置,请在下面输入网关名称","configuration-delete-dialog-input":"网关名称","configuration-delete-dialog-input-required":"网关名称是必需的","configuration-delete-dialog-confirm":"关闭",delete:"删除配置","download-tip":"下载配置","drop-file":"将文件拖放到此处或",gateway:"网关","gateway-exists":"同名设备已存在。","gateway-name":"网关名称","gateway-name-required":"网关名称必填。","gateway-saved":"已成功保存网关配置。",grpc:"GRPC","grpc-keep-alive-timeout":"保持连接超时(毫秒)","grpc-keep-alive-timeout-required":"需要保持连接超时","grpc-keep-alive-timeout-min":"保持连接超时不能小于1","grpc-keep-alive-timeout-pattern":"保持连接超时无效","grpc-keep-alive":"保持连接(毫秒)","grpc-keep-alive-required":"需要保持连接","grpc-keep-alive-min":"保持连接不能小于1","grpc-keep-alive-pattern":"保持连接无效","grpc-min-time-between-pings":"最小Ping间隔(毫秒)","grpc-min-time-between-pings-required":"需要最小Ping间隔","grpc-min-time-between-pings-min":"最小Ping间隔不能小于1","grpc-min-time-between-pings-pattern":"最小Ping间隔无效","grpc-min-ping-interval-without-data":"无数据时的最小Ping间隔(毫秒)","grpc-min-ping-interval-without-data-required":"需要无数据时的最小Ping间隔","grpc-min-ping-interval-without-data-min":"无数据时的最小Ping间隔不能小于1","grpc-min-ping-interval-without-data-pattern":"无数据时的最小Ping间隔无效","grpc-max-pings-without-data":"无数据时的最大Ping数","grpc-max-pings-without-data-required":"需要无数据时的最大Ping数","grpc-max-pings-without-data-min":"无数据时的最大Ping数不能小于1","grpc-max-pings-without-data-pattern":"无数据时的最大Ping数无效","inactivity-check-period-seconds":"不活跃检查期(秒)","inactivity-check-period-seconds-required":"需要不活跃检查期","inactivity-check-period-seconds-min":"不活跃检查期不能小于1","inactivity-check-period-seconds-pattern":"不活跃检查期无效","inactivity-timeout-seconds":"不活跃超时(秒)","inactivity-timeout-seconds-required":"需要不活跃超时","inactivity-timeout-seconds-min":"不活跃超时不能小于1","inactivity-timeout-seconds-pattern":"不活跃超时无效","json-parse":"无效的JSON。","json-required":"字段不能为空。",logs:{logs:"日志",days:"天",hours:"小时",minutes:"分钟",seconds:"秒","date-format":"日期格式","date-format-required":"需要日期格式","log-format":"日志格式","log-type":"日志类型","log-format-required":"需要日志格式",remote:"远程日志记录","remote-logs":"远程日志",local:"本地日志记录",level:"日志级别","file-path":"文件路径","file-path-required":"需要文件路径","saving-period":"日志保存期限","saving-period-min":"日志保存期限不能小于1","saving-period-required":"需要日志保存期限","backup-count":"备份数量","backup-count-min":"备份数量不能小于1","backup-count-required":"需要备份数量"},"min-pack-send-delay":"最小包发送延迟(毫秒)","min-pack-send-delay-required":"最小包发送延迟是必需的","min-pack-send-delay-min":"最小包发送延迟不能小于0","no-connectors":"无连接器","no-data":"没有配置","no-gateway-found":"未找到网关。","no-gateway-matching":"未找到 '{{item}}' 。","path-logs":"日志文件的路径","path-logs-required":"路径是必需的。","permit-without-calls":"保持连接许可,无需响应",remote:"远程配置","remote-logging-level":"日志记录级别","remove-entry":"删除配置","remote-shell":"远程Shell","remote-configuration":"远程配置",other:"其他","save-tip":"保存配置","security-type":"安全类型","security-types":{"access-token":"访问令牌","username-password":"用户名和密码",tls:"TLS","tls-access-token":"TLS + 访问令牌","tls-private-key":"TLS + 私钥"},"server-port":"服务器端口",statistics:{statistic:"统计信息",statistics:"统计信息","statistic-commands-empty":"无可用统计信息",commands:"命令","send-period":"统计信息发送周期(秒)","send-period-required":"统计信息发送周期是必需的","send-period-min":"统计信息发送周期不能小于60","send-period-pattern":"统计信息发送周期无效","check-connectors-configuration":"检查连接器配置(秒)","check-connectors-configuration-required":"检查连接器配置是必需的","check-connectors-configuration-min":"检查连接器配置不能小于1","check-connectors-configuration-pattern":"检查连接器配置无效",add:"添加命令",timeout:"超时时间","timeout-required":"超时时间是必需的","timeout-min":"超时时间不能小于1","timeout-pattern":"超时时间无效","attribute-name":"属性名称","attribute-name-required":"属性名称是必需的",command:"命令","command-required":"命令是必需的","command-pattern":"命令无效",remove:"删除命令"},storage:"存储","storage-max-file-records":"文件中的最大记录数","storage-max-files":"最大文件数","storage-max-files-min":"最小值为1。","storage-max-files-pattern":"数字无效。","storage-max-files-required":"数字是必需的。","storage-max-records":"存储中的最大记录数","storage-max-records-min":"最小记录数为1。","storage-max-records-pattern":"数字无效。","storage-max-records-required":"最大记录项必填。","storage-read-record-count":"存储中的读取记录数","storage-read-record-count-min":"最小记录数为1。","storage-read-record-count-pattern":"数字不合法。","storage-read-record-count-required":"需要读取记录数。","storage-max-read-record-count":"存储中的最大读取记录数","storage-max-read-record-count-min":"最小记录数为1。","storage-max-read-record-count-pattern":"数字不合法。","storage-max-read-record-count-required":"最大读取记录数必需。","storage-data-folder-path":"数据文件夹路径","storage-data-folder-path-required":"需要数据文件夹路径。","storage-pack-size":"最大事件包大小","storage-pack-size-min":"最小值为1。","storage-pack-size-pattern":"数字无效。","storage-pack-size-required":"最大事件包大小必填。","storage-path":"存储路径","storage-path-required":"存储路径必填。","storage-type":"存储类型","storage-types":{"file-storage":"文件存储","memory-storage":"内存存储",sqlite:"SQLITE"},thingsboard:"ThingsBoard",general:"常规","thingsboard-host":"ThingsBoard主机","thingsboard-host-required":"主机必填。","thingsboard-port":"ThingsBoard端口","thingsboard-port-max":"最大端口号为65535。","thingsboard-port-min":"最小端口号为1。","thingsboard-port-pattern":"端口无效。","thingsboard-port-required":"端口必填。",tidy:"整理","tidy-tip":"整理配置JSON","title-connectors-json":"连接器 {{typeName}} 配置","tls-path-ca-certificate":"网关上CA证书的路径","tls-path-client-certificate":"网关上客户端证书的路径","messages-ttl-check-in-hours":"消息TTL检查小时数","messages-ttl-check-in-hours-required":"需要提供消息TTL检查小时数。","messages-ttl-check-in-hours-min":"最小值为1。","messages-ttl-check-in-hours-pattern":"数字无效。","messages-ttl-in-days":"消息TTL天数","messages-ttl-in-days-required":"需要提供消息TTL天数。","messages-ttl-in-days-min":"最小值为1。","messages-ttl-in-days-pattern":"数字无效。","mqtt-qos":"QoS","mqtt-qos-required":"需要提供QoS","mqtt-qos-range":"QoS值的范围是从0到1","tls-path-private-key":"网关上私钥的路径","toggle-fullscreen":"切换全屏","transformer-json-config":"配置JSON*","update-config":"添加/更新配置JSON",hints:{"remote-configuration":"启用对网关的远程配置和管理","remote-shell":"通过远程Shell小部件启用对网关操作系统的远程控制",host:"ThingsBoard 主机名或IP地址",port:"ThingsBoard MQTT服务端口",token:"ThingsBoard 网关访问令牌","client-id":"ThingsBoard 网关MQTT客户端ID",username:"ThingsBoard 网关MQTT用户名",password:"ThingsBoard 网关MQTT密码","ca-cert":"CA证书文件的路径","date-form":"日志消息中的日期格式","data-folder":"包含数据的文件夹的路径(相对或绝对路径)","log-format":"日志消息格式","remote-log":"启用对网关的远程日志记录和日志读取","backup-count":"如果备份计数大于0,则在执行轮换时,最多保留备份计数个文件-最旧的文件将被删除",storage:"提供将数据发送到平台之前保存传入数据的配置","max-file-count":"将创建的文件的最大数量","max-read-count":"从存储中获取的消息计数并发送到ThingsBoard","max-records":"一个文件中存储的最大记录数","read-record-count":"从存储中获取的消息计数并发送到ThingsBoard","max-records-count":"在将数据发送到ThingsBoard之前,存储中的最大数据计数","ttl-check-hour":"网关多久检查一次数据是否过时","ttl-messages-day":"存储将保存数据的最大天数",commands:"用于收集附加统计信息的命令",attribute:"统计遥测键",timeout:"命令执行的超时时间",command:"命令执行的结果,将用作遥测的值","check-device-activity":"启用监视每个连接设备的活动","inactivity-timeout":"在此时间后,网关将断开设备的连接","inactivity-period":"设备活动检查的周期","minimal-pack-delay":"发送消息包之间的延迟(减小此设置会导致增加CPU使用率)",qos:"MQTT消息传递的服务质量(0-至多一次,1-至少一次)","server-port":"GRPC服务器侦听传入连接的网络端口","grpc-keep-alive-timeout":"在考虑连接死亡之前,服务器等待keepalive ping响应的最长时间","grpc-keep-alive":"没有活动RPC调用时两个连续keepalive ping消息之间的持续时间","grpc-min-time-between-pings":"服务器在发送keepalive ping消息之间应等待的最小时间量","grpc-max-pings-without-data":"在没有接收到任何数据之前,服务器可以发送的keepalive ping消息的最大数量,然后将连接视为死亡","grpc-min-ping-interval-without-data":"在没有发送或接收数据时,服务器在发送keepalive ping消息之间应等待的最小时间量","permit-without-calls":"允许服务器在没有活动RPC调用时保持GRPC连接活动"},"docker-label":"使用以下指令在 Docker compose 中运行 IoT 网关,并为选定的设备提供凭据","install-docker-compose":"使用以下说明下载、安装和设置 Docker Compose","download-configuration-file":"下载配置文件","download-docker-compose":"下载您的网关的 docker-compose.yml 文件","launch-gateway":"启动网关","launch-docker-compose":"在包含 docker-compose.yml 文件的文件夹中,使用以下命令在终端中启动网关"},kt={"add-entry":"增加配置","connector-add":"增加新連接器","connector-enabled":"啟用連接器","connector-name":"連接器名稱","connector-name-required":"需要連接器名稱。","connector-type":"連接器類型","connector-type-required":"需要連接器類型。",connectors:"連接器配置","create-new-gateway":"建立新閘道","create-new-gateway-text":"您確定要建立一個名稱為:'{{gatewayName}}'的新閘道嗎?",delete:"刪除配置","download-tip":"下載配置文件",gateway:"閘道","gateway-exists":"同名設備已存在。","gateway-name":"閘道名稱","gateway-name-required":"需要閘道名稱。","gateway-saved":"閘道配置已成功保存。","json-parse":"無效的JSON","json-required":"欄位不能為空。","no-connectors":"無連接器","no-data":"無配置","no-gateway-found":"未找到閘道。","no-gateway-matching":" 未找到'{{item}}'。","path-logs":"日誌文件的路徑","path-logs-required":"需要路徑。",remote:"移除配置","remote-logging-level":"日誌記錄級別","remove-entry":"移除配置","save-tip":"保存配置文件","security-type":"安全類型","security-types":{"access-token":"訪問Token",tls:"TLS"},storage:"貯存","storage-max-file-records":"文件中的最大紀錄","storage-max-files":"最大文件數","storage-max-files-min":"最小數量為1。","storage-max-files-pattern":"號碼無效。","storage-max-files-required":"需要號碼。","storage-max-records":"存儲中的最大紀錄","storage-max-records-min":"最小紀錄數為1。","storage-max-records-pattern":"號碼無效。","storage-max-records-required":"需要最大紀錄數","storage-pack-size":"最大事件包大小","storage-pack-size-min":"最小數量為1。","storage-pack-size-pattern":"號碼無效.","storage-pack-size-required":"需要最大事件包大小","storage-path":"存儲路徑","storage-path-required":"需要存儲路徑。","storage-type":"存儲類型","storage-types":{"file-storage":"文件存儲","memory-storage":"記憶體存儲"},thingsboard:"ThingsBoard","thingsboard-host":"ThingsBoard主機","thingsboard-host-required":"需要主機。","thingsboard-port":"ThingsBoard連接埠","thingsboard-port-max":"最大埠號為 65535。","thingsboard-port-min":"最小埠號為1。","thingsboard-port-pattern":"連接埠無效。","thingsboard-port-required":"需要連接埠。",tidy:"整理","tidy-tip":"整理配置JSON","title-connectors-json":"連接器{{typeName}}配置","tls-path-ca-certificate":"閘道上CA證書的路徑","tls-path-client-certificate":"閘道上用戶端憑據的路徑","tls-path-private-key":"閘道上的私鑰路徑","toggle-fullscreen":"切換全螢幕","transformer-json-config":"配置JSON*","update-config":"增加/更新配置JSON"};var Pt={3.6:{socket:{type:"TCP",address:"127.0.0.1",port:5e4,bufferSize:1024},devices:[{address:"*:*",deviceName:"Device Example",deviceType:"default",encoding:"utf-8",telemetry:[{key:"temp",byteFrom:0,byteTo:-1},{key:"hum",byteFrom:0,byteTo:2}],attributes:[{key:"name",byteFrom:0,byteTo:-1},{key:"num",byteFrom:2,byteTo:4}],attributeRequests:[{type:"shared",requestExpressionSource:"constant",attributeNameExpressionSource:"constant",requestExpression:"${[0:3]==atr}",attributeNameExpression:"[3:]"}],attributeUpdates:[{encoding:"utf-16",attributeOnThingsBoard:"sharedName"}],serverSideRpc:[{methodRPC:"rpcMethod1",withResponse:!0,encoding:"utf-8"}]}]},legacy:{type:"TCP",address:"127.0.0.1",port:5e4,bufferSize:1024,devices:[{address:"*:*",deviceName:"Device Example",deviceType:"default",encoding:"utf-8",telemetry:[{key:"temp",byteFrom:0,byteTo:-1},{key:"hum",byteFrom:0,byteTo:2}],attributes:[{key:"name",byteFrom:0,byteTo:-1},{key:"num",byteFrom:2,byteTo:4}],attributeRequests:[{type:"shared",requestExpression:"${[0:3]==atr}",attributeNameExpression:"[3:]"}],attributeUpdates:[{encoding:"utf-16",attributeOnThingsBoard:"sharedName"}],serverSideRpc:[{methodRPC:"rpcMethod1",withResponse:!0,methodProcessing:"write",encoding:"utf-8"}]}]}},Dt={"3.5.2":{broker:{host:"127.0.0.1",port:1883,clientId:"ThingsBoard_gateway",version:5,maxMessageNumberPerWorker:10,maxNumberOfWorkers:100,sendDataOnlyOnChange:!1,security:{type:"anonymous"}},mapping:[{topicFilter:"sensor/data",subscriptionQos:1,converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}",deviceProfileExpressionSource:"message",deviceProfileExpression:"${sensorType}"},sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"${sensorModel}",value:"on"}],timeseries:[{type:"string",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"},{type:"string",key:"combine",value:"${hum}:${temp}"}]}},{topicFilter:"sensor/+/data",subscriptionQos:1,converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"topic",deviceNameExpression:"(?<=sensor/)(.*?)(?=/data)",deviceProfileExpressionSource:"constant",deviceProfileExpression:"Thermometer"},sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"string",key:"humidity",value:"${hum}"}]}},{topicFilter:"sensor/raw_data",subscriptionQos:1,converter:{type:"bytes",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"[0:4]",deviceProfileExpressionSource:"constant",deviceProfileExpression:"default"},sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"raw",key:"rawData",value:"[:]"}],timeseries:[{type:"raw",key:"temp",value:"[4:]"}]}},{topicFilter:"custom/sensors/+",subscriptionQos:1,converter:{type:"custom",extension:"CustomMqttUplinkConverter",cached:!0,extensionConfig:{temperature:2,humidity:2,batteryLevel:1}}}],requestsMapping:{connectRequests:[{topicFilter:"sensor/connect",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}",deviceProfileExpressionSource:"constant",deviceProfileExpression:"Thermometer"}},{topicFilter:"sensor/+/connect",deviceInfo:{deviceNameExpressionSource:"topic",deviceNameExpression:"(?<=sensor/)(.*?)(?=/connect)",deviceProfileExpressionSource:"constant",deviceProfileExpression:"Thermometer"}}],disconnectRequests:[{topicFilter:"sensor/disconnect",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}"}},{topicFilter:"sensor/+/disconnect",deviceInfo:{deviceNameExpressionSource:"topic",deviceNameExpression:"(?<=sensor/)(.*?)(?=/connect)"}}],attributeRequests:[{retain:!1,topicFilter:"v1/devices/me/attributes/request",deviceInfo:{deviceNameExpressionSource:"message",deviceNameExpression:"${serialNumber}"},attributeNameExpressionSource:"message",attributeNameExpression:"${versionAttribute}, ${pduAttribute}",topicExpression:"devices/${deviceName}/attrs",valueExpression:"${attributeKey}: ${attributeValue}"}],attributeUpdates:[{retain:!0,deviceNameFilter:".*",attributeFilter:"firmwareVersion",topicExpression:"sensor/${deviceName}/${attributeKey}",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{type:"twoWay",deviceNameFilter:".*",methodFilter:"echo",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",responseTopicExpression:"sensor/${deviceName}/response/${methodName}/${requestId}",responseTopicQoS:1,responseTimeout:1e4,valueExpression:"${params}"},{type:"oneWay",deviceNameFilter:".*",methodFilter:"no-reply",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",valueExpression:"${params}"}]}},legacy:{broker:{name:"Default Local Broker",host:"127.0.0.1",port:1883,clientId:"ThingsBoard_gateway",version:5,maxMessageNumberPerWorker:10,maxNumberOfWorkers:100,sendDataOnlyOnChange:!1,security:{type:"basic",username:"user",password:"password"}},mapping:[{topicFilter:"sensor/data",converter:{type:"json",deviceNameJsonExpression:"${serialNumber}",deviceTypeJsonExpression:"${sensorType}",sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"${sensorModel}",value:"on"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"},{type:"string",key:"combine",value:"${hum}:${temp}"}]}},{topicFilter:"sensor/+/data",converter:{type:"json",deviceNameTopicExpression:"(?<=sensor/)(.*?)(?=/data)",deviceTypeTopicExpression:"Thermometer",sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"string",key:"model",value:"${sensorModel}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{topicFilter:"sensor/raw_data",converter:{type:"bytes",deviceNameExpression:"[0:4]",deviceTypeExpression:"default",sendDataOnlyOnChange:!1,timeout:6e4,attributes:[{type:"raw",key:"rawData",value:"[:]"}],timeseries:[{type:"raw",key:"temp",value:"[4:]"}]}},{topicFilter:"custom/sensors/+",converter:{type:"custom",extension:"CustomMqttUplinkConverter",cached:!0,"extension-config":{temperatureBytes:2,humidityBytes:2,batteryLevelBytes:1}}}],connectRequests:[{topicFilter:"sensor/connect",deviceNameJsonExpression:"${serialNumber}"},{topicFilter:"sensor/+/connect",deviceNameTopicExpression:"(?<=sensor/)(.*?)(?=/connect)"}],disconnectRequests:[{topicFilter:"sensor/disconnect",deviceNameJsonExpression:"${serialNumber}"},{topicFilter:"sensor/+/disconnect",deviceNameTopicExpression:"(?<=sensor/)(.*?)(?=/disconnect)"}],attributeRequests:[{retain:!1,topicFilter:"v1/devices/me/attributes/request",deviceNameJsonExpression:"${serialNumber}",attributeNameJsonExpression:"${versionAttribute}, ${pduAttribute}",topicExpression:"devices/${deviceName}/attrs",valueExpression:"${attributeKey}: ${attributeValue}"}],attributeUpdates:[{retain:!0,deviceNameFilter:".*",attributeFilter:"firmwareVersion",topicExpression:"sensor/${deviceName}/${attributeKey}",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",responseTopicExpression:"sensor/${deviceName}/response/${methodName}/${requestId}",responseTimeout:1e4,valueExpression:"${params}"},{deviceNameFilter:".*",methodFilter:"no-reply",requestTopicExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",valueExpression:"${params}"}]}},Ot={"3.5.2":{master:{slaves:[{host:"127.0.0.1",port:5021,type:"tcp",method:"socket",timeout:35,byteOrder:"LITTLE",wordOrder:"LITTLE",retries:!0,retryOnEmpty:!0,retryOnInvalid:!0,pollPeriod:5e3,unitId:1,deviceName:"Temp Sensor",deviceType:"default",connectAttemptTimeMs:5e3,connectAttemptCount:5,waitAfterFailedAttemptsMs:3e5,reportStrategy:{type:"ON_REPORT_PERIOD",reportPeriod:3e4},attributes:[{tag:"string_read",type:"string",functionCode:4,objectsCount:4,address:1,reportStrategy:{type:"ON_REPORT_PERIOD",reportPeriod:15e3}},{tag:"bits_read",type:"bits",functionCode:4,objectsCount:1,address:5},{tag:"8int_read",type:"8int",functionCode:4,objectsCount:1,address:6},{tag:"16int_read",type:"16int",functionCode:4,objectsCount:1,address:7},{tag:"32int_read_divider",type:"32int",functionCode:4,objectsCount:2,address:8,divider:10},{tag:"8int_read_multiplier",type:"8int",functionCode:4,objectsCount:1,address:10,multiplier:10},{tag:"32int_read",type:"32int",functionCode:4,objectsCount:2,address:11},{tag:"64int_read",type:"64int",functionCode:4,objectsCount:4,address:13}],timeseries:[{tag:"8uint_read",type:"8uint",functionCode:4,objectsCount:1,address:17,reportStrategy:{type:"ON_REPORT_PERIOD",reportPeriod:15e3}},{tag:"16uint_read",type:"16uint",functionCode:4,objectsCount:2,address:18},{tag:"32uint_read",type:"32uint",functionCode:4,objectsCount:4,address:20},{tag:"64uint_read",type:"64uint",functionCode:4,objectsCount:1,address:24},{tag:"16float_read",type:"16float",functionCode:4,objectsCount:1,address:25},{tag:"32float_read",type:"32float",functionCode:4,objectsCount:2,address:26},{tag:"64float_read",type:"64float",functionCode:4,objectsCount:4,address:28}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31},{tag:"getValue",type:"bits",functionCode:1,objectsCount:1,address:31},{tag:"setCPUFanSpeed",type:"32int",functionCode:16,objectsCount:2,address:33},{tag:"getCPULoad",type:"32int",functionCode:4,objectsCount:2,address:35}]}]},slave:{type:"tcp",host:"127.0.0.1",port:5026,method:"socket",deviceName:"Modbus Slave Example",deviceType:"default",pollPeriod:5e3,sendDataToThingsBoard:!1,byteOrder:"LITTLE",wordOrder:"LITTLE",unitId:0,values:{holding_registers:{attributes:[{address:1,type:"string",tag:"sm",objectsCount:1,value:"ON"}],timeseries:[{address:2,type:"8int",tag:"smm",objectsCount:1,value:"12334"}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29,value:1243}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31,value:1}]},coils_initializer:{attributes:[{address:5,type:"8int",tag:"coil",objectsCount:1,value:0}],timeseries:[],attributeUpdates:[],rpc:[]}}}},legacy:{master:{slaves:[{host:"127.0.0.1",port:5021,type:"tcp",method:"socket",timeout:35,byteOrder:"LITTLE",wordOrder:"LITTLE",retries:!0,retryOnEmpty:!0,retryOnInvalid:!0,pollPeriod:5e3,unitId:1,deviceName:"Temp Sensor",deviceType:"default",sendDataOnlyOnChange:!0,connectAttemptTimeMs:5e3,connectAttemptCount:5,waitAfterFailedAttemptsMs:3e5,attributes:[{tag:"string_read",type:"string",functionCode:4,objectsCount:4,address:1},{tag:"bits_read",type:"bits",functionCode:4,objectsCount:1,address:5},{tag:"16int_read",type:"16int",functionCode:4,objectsCount:1,address:7},{tag:"32int_read_divider",type:"32int",functionCode:4,objectsCount:2,address:8,divider:10},{tag:"32int_read",type:"32int",functionCode:4,objectsCount:2,address:11},{tag:"64int_read",type:"64int",functionCode:4,objectsCount:4,address:13}],timeseries:[{tag:"16uint_read",type:"16uint",functionCode:4,objectsCount:2,address:18},{tag:"32uint_read",type:"32uint",functionCode:4,objectsCount:4,address:20},{tag:"64uint_read",type:"64uint",functionCode:4,objectsCount:1,address:24},{tag:"16float_read",type:"16float",functionCode:4,objectsCount:1,address:25},{tag:"32float_read",type:"32float",functionCode:4,objectsCount:2,address:26},{tag:"64float_read",type:"64float",functionCode:4,objectsCount:4,address:28}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31},{tag:"getValue",type:"bits",functionCode:1,objectsCount:1,address:31},{tag:"setCPUFanSpeed",type:"32int",functionCode:16,objectsCount:2,address:33},{tag:"getCPULoad",type:"32int",functionCode:4,objectsCount:2,address:35}]}]},slave:{type:"tcp",host:"127.0.0.1",port:5026,method:"socket",deviceName:"Modbus Slave Example",deviceType:"default",pollPeriod:5e3,sendDataToThingsBoard:!1,byteOrder:"LITTLE",wordOrder:"LITTLE",unitId:0,values:{holding_registers:[{attributes:[{address:1,type:"string",tag:"sm",objectsCount:1,value:"ON"}],timeseries:[{address:2,type:"int",tag:"smm",objectsCount:1,value:"12334"}],attributeUpdates:[{tag:"shared_attribute_write",type:"32int",functionCode:6,objectsCount:2,address:29,value:1243}],rpc:[{tag:"setValue",type:"bits",functionCode:5,objectsCount:1,address:31,value:1}]}],coils_initializer:[{attributes:[{address:5,type:"string",tag:"sm",objectsCount:1,value:"12"}],timeseries:[],attributeUpdates:[],rpc:[]}]}}}},At={"3.5.2":{server:{url:"localhost:4840/freeopcua/server/",timeoutInMillis:5e3,scanPeriodInMillis:36e5,pollPeriodInMillis:5e3,enableSubscriptions:!0,subCheckPeriodInMillis:100,showMap:!1,security:"Basic128Rsa15",identity:{type:"anonymous"}},mapping:[{deviceNodePattern:"Root\\.Objects\\.Device1",deviceNodeSource:"path",deviceInfo:{deviceNameExpression:"Device ${Root\\.Objects\\.Device1\\.serialNumber}",deviceNameExpressionSource:"path",deviceProfileExpression:"Device",deviceProfileExpressionSource:"constant"},attributes:[{key:"temperature °C",type:"path",value:"${ns=2;i=5}"}],timeseries:[{key:"humidity",type:"path",value:"${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"},{key:"batteryLevel",type:"path",value:"${Battery\\.batteryLevel}"}],rpc_methods:[{method:"multiply",arguments:[{type:"integer",value:2},{type:"integer",value:4}]}],attributes_updates:[{key:"deviceName",type:"path",value:"Root\\.Objects\\.Device1\\.serialNumber"}]}]},legacy:{server:{name:"OPC-UA Default Server",url:"localhost:4840/freeopcua/server/",timeoutInMillis:5e3,scanPeriodInMillis:5e3,disableSubscriptions:!1,subCheckPeriodInMillis:100,showMap:!1,security:"Basic128Rsa15",identity:{type:"anonymous"},mapping:[{deviceNodePattern:"Root\\.Objects\\.Device1",deviceNamePattern:"Device ${Root\\.Objects\\.Device1\\.serialNumber}",attributes:[{key:"temperature °C",path:"${ns=2;i=5}"}],timeseries:[{key:"humidity",path:"${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"},{key:"batteryLevel",path:"${Battery\\.batteryLevel}"}],rpc_methods:[{method:"multiply",arguments:[2,4]}],attributes_updates:[{attributeOnThingsBoard:"deviceName",attributeOnDevice:"Root\\.Objects\\.Device1\\.serialNumber"}]}]}}},Ft={passiveScanMode:!0,showMap:!1,scanner:{timeout:1e4,deviceName:"Device name"},devices:[{name:"Temperature and humidity sensor",MACAddress:"4C:65:A8:DF:85:C0",pollPeriod:5e5,showMap:!1,timeout:1e4,connectRetry:5,connectRetryInSeconds:0,waitAfterConnectRetries:10,telemetry:[{key:"temperature",method:"notify",characteristicUUID:"226CAA55-6476-4566-7562-66734470666D",valueExpression:"[2]"},{key:"humidity",method:"notify",characteristicUUID:"226CAA55-6476-4566-7562-66734470666D",valueExpression:"[0]"}],attributes:[{key:"name",method:"read",characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",valueExpression:"[0:2]cm [2:]A"},{key:"values",method:"read",characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",valueExpression:"All values: [:]"}],attributeUpdates:[{attributeOnThingsBoard:"sharedName",characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB"}],serverSideRpc:[{methodRPC:"rpcMethod1",withResponse:!0,characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",methodProcessing:"read"},{methodRPC:"rpcMethod2",withResponse:!0,characteristicUUID:"00002A00-0000-1000-8000-00805F9B34FB",methodProcessing:"write"},{methodRPC:"rpcMethod3",withResponse:!0,methodProcessing:"scan"}]}]},Rt={host:"http://127.0.0.1:5000",SSLVerify:!0,security:{type:"basic",username:"user",password:"password"},mapping:[{url:"getdata",httpMethod:"GET",httpHeaders:{ACCEPT:"application/json"},content:{name:"morpheus",job:"leader"},allowRedirects:!0,timeout:.5,scanPeriod:5,converter:{type:"json",deviceNameJsonExpression:"SD8500",deviceTypeJsonExpression:"SD",attributes:[{key:"serialNumber",type:"string",value:"${serial}"}],telemetry:[{key:"Maintainer",type:"string",value:"${Developer}"},{key:"combine",type:"string",value:"${Developer}:${hum}"}]}},{url:"get_info",httpMethod:"GET",httpHeaders:{ACCEPT:"application/json"},allowRedirects:!0,timeout:.5,scanPeriod:100,converter:{type:"custom",deviceNameJsonExpression:"SD8500",deviceTypeJsonExpression:"SD",extension:"CustomRequestUplinkConverter","extension-config":[{key:"Totaliser",type:"float",fromByte:0,toByte:4,byteorder:"big",signed:!0,multiplier:1},{key:"Flow",type:"int",fromByte:4,toByte:6,byteorder:"big",signed:!0,multiplier:.01},{key:"Temperature",type:"int",fromByte:8,toByte:10,byteorder:"big",signed:!0,multiplier:.01},{key:"Pressure",type:"int",fromByte:12,toByte:14,byteorder:"big",signed:!0,multiplier:.01},{key:"deviceStatus",type:"int",byteAddress:15,fromBit:4,toBit:8,byteorder:"big",signed:!1},{key:"OUT2",type:"int",byteAddress:15,fromBit:1,toBit:2,byteorder:"big"},{key:"OUT1",type:"int",byteAddress:15,fromBit:0,toBit:1,byteorder:"big"}]}}],attributeUpdates:[{httpMethod:"POST",httpHeaders:{"CONTENT-TYPE":"application/json"},timeout:.5,tries:3,allowRedirects:!0,deviceNameFilter:"SD.*",attributeFilter:"send_data",requestUrlExpression:"sensor/${deviceName}/${attributeKey}",requestValueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",responseTimeout:1,httpMethod:"GET",requestValueExpression:"${params}",responseValueExpression:"${temp}",timeout:.5,tries:3,httpHeaders:{"Content-Type":"application/json"}},{deviceNameFilter:".*",methodFilter:"no-reply",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",httpMethod:"POST",requestValueExpression:"${params}",httpHeaders:{"Content-Type":"application/json"}}]},Bt={interface:"socketcan",channel:"vcan0",backend:{fd:!0},reconnectPeriod:5,devices:[{name:"Car",sendDataOnlyOnChange:!1,enableUnknownRpc:!0,strictEval:!1,attributes:[{key:"isDriverDoorOpened",nodeId:41,command:"2:2:big:8717",value:"4:1:int",expression:"bool(value & 0b00000100)",polling:{type:"once",dataInHex:"AB CD AB CD"}}],timeseries:[{key:"rpm",nodeId:1918,isExtendedId:!0,command:"2:2:big:48059",value:"4:2:big:int",expression:"value / 4",polling:{type:"always",period:5,dataInHex:"aaaa bbbb aaaa bbbb"}},{key:"milliage",nodeId:1918,isExtendedId:!0,value:"4:2:little:int",expression:"value * 10",polling:{type:"always",period:30,dataInHex:"aa bb cc dd ee ff aa bb"}}],attributeUpdates:[{attributeOnThingsBoard:"softwareVersion",nodeId:64,isExtendedId:!0,dataLength:4,dataExpression:"value + 5",dataByteorder:"little"}],serverSideRpc:[{method:"sendSameData",nodeId:4,isExtendedId:!0,isFd:!0,bitrateSwitch:!0,dataInHex:"aa bb cc dd ee ff aa bb aa bb cc d ee ff"},{method:"setLightLevel",nodeId:5,dataLength:2,dataByteorder:"little",dataBefore:"00AA"},{method:"setSpeed",nodeId:16,dataAfter:"0102",dataExpression:"userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed"}]}]},Nt={"3.6.2":{application:{objectName:"TB_gateway",host:"0.0.0.0",port:"47808",objectIdentifier:599,maxApduLengthAccepted:1476,segmentationSupported:"segmentedBoth",vendorIdentifier:15},devices:[{deviceInfo:{deviceNameExpression:"BACnet Device ${objectName}",deviceProfileExpression:"default",deviceNameExpressionSource:"expression",deviceProfileExpressionSource:"constant"},host:"192.168.2.110",port:"47808",pollPeriod:1e4,attributes:[{key:"temperature",objectType:"analogOutput",objectId:"1",propertyId:"presentValue"}],timeseries:[{key:"state",objectType:"binaryValue",objectId:"1",propertyId:"presentValue"}],attributeUpdates:[{key:"brightness",objectType:"analogOutput",objectId:"1",propertyId:"presentValue"}],serverSideRpc:[{method:"set_state",requestType:"writeProperty",requestTimeout:1e4,objectType:"binaryOutput",objectId:"1",propertyId:"presentValue"},{method:"get_state",requestType:"readProperty",requestTimeout:1e4,objectType:"binaryOutput",objectId:"1",propertyId:"presentValue"}]}]},legacy:{general:{objectName:"TB_gateway",address:"0.0.0.0:47808",objectIdentifier:599,maxApduLengthAccepted:1476,segmentationSupported:"segmentedBoth",vendorIdentifier:15},devices:[{deviceName:"BACnet Device ${objectName}",deviceType:"default",address:"192.168.2.110:47808",pollPeriod:1e4,attributes:[{key:"temperature",type:"string",objectId:"analogOutput:1",propertyId:"presentValue"}],timeseries:[{key:"state",type:"bool",objectId:"binaryValue:1",propertyId:"presentValue"}],attributeUpdates:[{key:"brightness",requestType:"writeProperty",objectId:"analogOutput:1",propertyId:"presentValue"}],serverSideRpc:[{method:"set_state",requestType:"writeProperty",requestTimeout:1e4,objectId:"binaryOutput:1",propertyId:"presentValue"},{method:"get_state",requestType:"readProperty",requestTimeout:1e4,objectId:"binaryOutput:1",propertyId:"presentValue"}]}]}},Lt={connection:{str:"Driver={PostgreSQL};Server=localhost;Port=5432;Database=thingsboard;Uid=postgres;Pwd=postgres;",attributes:{autocommit:!0,timeout:0},encoding:"utf-8",decoding:{char:"utf-8",wchar:"utf-8",metadata:"utf-16le"},reconnect:!0,reconnectPeriod:60},pyodbc:{pooling:!1},polling:{query:"SELECT bool_v, str_v, dbl_v, long_v, entity_id, ts FROM ts_kv WHERE ts > ? ORDER BY ts ASC LIMIT 10",period:10,iterator:{column:"ts",query:"SELECT MIN(ts) - 1 FROM ts_kv",persistent:!1}},mapping:{device:{type:"postgres",name:"'ODBC ' + entity_id"},sendDataOnlyOnChange:!1,attributes:"*",timeseries:[{name:"value",value:"[i for i in [str_v, long_v, dbl_v,bool_v] if i is not None][0]"}]},serverSideRpc:{enableUnknownRpc:!1,overrideRpcConfig:!0,methods:["procedureOne",{name:"procedureTwo",args:["One",2,3]}]}},Vt={"3.7.2":{server:{host:"127.0.0.1",port:"5000",SSL:!1,security:{cert:"~/ssl/cert.pem",key:"~/ssl/key.pem"}},mapping:[{endpoint:"/my_devices",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"request",deviceNameExpression:"${sensorName}",deviceProfileExpressionSource:"request",deviceProfileExpression:"${sensorType}"},attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"certificateNumber",value:"${certificateNumber}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon1",HTTPMethods:["GET","POST"],security:{type:"anonymous"},converter:{type:"json",deviceInfo:{deviceNameExpressionSource:"constant",deviceNameExpression:"Device 2",deviceProfileExpressionSource:"constant",deviceProfileExpression:"default"},attributes:[{type:"string",key:"model",value:"Model2"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon2",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"custom",deviceInfo:{deviceNameExpressionSource:"constant",deviceNameExpression:"SuperAnonDevice",deviceProfileExpressionSource:"constant",deviceProfileExpression:"default"},extension:"CustomRestUplinkConverter",extensionConfig:{key:"Totaliser",datatype:"float",fromByte:0,toByte:4,byteorder:"big",signed:!0,multiplier:1}}}],requestsMapping:{attributeRequests:[{endpoint:"/sharedAttributes",type:"shared",HTTPMethods:["POST"],security:{type:"anonymous"},timeout:10,deviceNameExpression:"${deviceName}",attributeNameExpression:"${attribute}${attribute1}"}],attributeUpdates:[{HTTPMethod:"POST",SSLVerify:!1,httpHeaders:{"CONTENT-TYPE":"application/json"},security:{type:"anonymous"},timeout:.5,tries:3,allowRedirects:!0,deviceNameFilter:"SN.*",attributeFilter:".*",requestUrlExpression:"http://127.0.0.1:5001/",valueExpression:'{"deviceName":"${deviceName}","${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestUrlExpression:"http://127.0.0.1:5001/${deviceName}",responseTimeout:1,HTTPMethod:"GET",valueExpression:"${params}",timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:"SN.*",methodFilter:"post_attributes",requestUrlExpression:"http://127.0.0.1:5000/my_devices",responseTimeout:1,HTTPMethod:"POST",valueExpression:'{"sensorName":"${deviceName}", "sensorModel":"${params.sensorModel}", "certificateNumber":"${params.certificateNumber}", "temp":"${params.temp}", "hum":"${params.hum}"}',timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:".*",methodFilter:"no-reply",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",HTTPMethod:"POST",valueExpression:"${params}",httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}}]}},legacy:{host:"127.0.0.1",port:"5000",SSL:!1,security:{cert:"~/ssl/cert.pem",key:"~/ssl/key.pem"},mapping:[{endpoint:"/my_devices",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"json",deviceNameExpression:"${sensorName}",deviceTypeExpression:"${sensorType}",attributes:[{type:"string",key:"model",value:"${sensorModel}"},{type:"string",key:"certificateNumber",value:"${certificateNumber}"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon1",HTTPMethods:["GET","POST"],security:{type:"anonymous"},converter:{type:"json",deviceNameExpression:"Device 2",deviceTypeExpression:"default",attributes:[{type:"string",key:"model",value:"Model2"}],timeseries:[{type:"double",key:"temperature",value:"${temp}"},{type:"double",key:"humidity",value:"${hum}"}]}},{endpoint:"/anon2",HTTPMethods:["POST"],security:{type:"anonymous"},converter:{type:"custom",deviceNameExpression:"SuperAnonDevice",deviceTypeExpression:"default",extension:"CustomRestUplinkConverter","extension-config":{key:"Totaliser",datatype:"float",fromByte:0,toByte:4,byteorder:"big",signed:!0,multiplier:1}}}],attributeRequests:[{endpoint:"/sharedAttributes",type:"shared",HTTPMethods:["POST"],security:{type:"anonymous"},timeout:10,deviceNameExpression:"${deviceName}",attributeNameExpression:"${attribute}${attribute1}"}],attributeUpdates:[{HTTPMethod:"POST",SSLVerify:!1,httpHeaders:{"CONTENT-TYPE":"application/json"},security:{type:"anonymous"},timeout:.5,tries:3,allowRedirects:!0,deviceNameFilter:"SN.*",attributeFilter:".*",requestUrlExpression:"http://127.0.0.1:5001/",valueExpression:'{"deviceName":"${deviceName}","${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"echo",requestUrlExpression:"http://127.0.0.1:5001/${deviceName}",responseTimeout:1,HTTPMethod:"GET",valueExpression:"${params}",timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:"SN.*",methodFilter:"post_attributes",requestUrlExpression:"http://127.0.0.1:5000/my_devices",responseTimeout:1,HTTPMethod:"POST",valueExpression:'{"sensorName":"${deviceName}", "sensorModel":"${params.sensorModel}", "certificateNumber":"${params.certificateNumber}", "temp":"${params.temp}", "hum":"${params.hum}"}',timeout:10,tries:3,httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}},{deviceNameFilter:".*",methodFilter:"no-reply",requestUrlExpression:"sensor/${deviceName}/request/${methodName}/${requestId}",HTTPMethod:"POST",valueExpression:"${params}",httpHeaders:{"Content-Type":"application/json"},security:{type:"anonymous"}}]}},qt={devices:[{deviceName:"SNMP router",deviceType:"snmp",ip:"snmp.live.gambitcommunications.com",port:161,pollPeriod:5e3,community:"public",attributes:[{key:"ReceivedFromGet",method:"get",oid:"1.3.6.1.2.1.1.1.0",timeout:6},{key:"ReceivedFromMultiGet",method:"multiget",oid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"],timeout:6},{key:"ReceivedFromGetNext",method:"getnext",oid:"1.3.6.1.2.1.1.1.0",timeout:6},{key:"ReceivedFromMultiWalk",method:"multiwalk",oid:["1.3.6.1.2.1.1.1.0","1.3.6.0.1.2.1"]},{key:"ReceivedFromBulkWalk",method:"bulkwalk",oid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"]},{key:"ReceivedFromBulkGet",method:"bulkget",scalarOid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"],repeatingOid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"],maxListSize:10}],telemetry:[{key:"ReceivedFromWalk",community:"private",method:"walk",oid:"1.3.6.1.2.1.1.1.0"},{key:"ReceivedFromTable",method:"table",oid:"1.3.6.1.2.1.1"}],attributeUpdateRequests:[{attributeFilter:"dataToSet",method:"set",oid:"1.3.6.1.2.1.1.1.0"},{attributeFilter:"dataToMultiSet",method:"multiset",mappings:{"1.2.3":"10","2.3.4":"${attribute}"}}],serverSideRpcRequests:[{requestFilter:"setData",method:"set",oid:"1.3.6.1.2.1.1.1.0"},{requestFilter:"multiSetData",method:"multiset"},{requestFilter:"getData",method:"get",oid:"1.3.6.1.2.1.1.1.0"},{requestFilter:"runBulkWalk",method:"bulkwalk",oid:["1.3.6.1.2.1.1.1.0","1.3.6.1.2.1.1.2.0"]}]},{deviceName:"SNMP router",deviceType:"snmp",ip:"127.0.0.1",pollPeriod:5e3,community:"public",converter:"CustomSNMPConverter",attributes:[{key:"ReceivedFromGetWithCustomConverter",method:"get",oid:"1.3.6.1.2.1.1.1.0"}],telemetry:[{key:"ReceivedFromTableWithCustomConverter",method:"table",oid:"1.3.6.1.2.1.1.1.0"}]}]},Gt={host:"0.0.0.0",port:21,TLSSupport:!1,security:{type:"basic",username:"admin",password:"admin"},paths:[{devicePatternName:"asd",devicePatternType:"Device",delimiter:",",path:"fol/*_hello*.txt",readMode:"FULL",maxFileSize:5,pollPeriod:500,txtFileDataView:"SLICED",withSortingFiles:!0,attributes:[{key:"temp",value:"[1:]"},{key:"tmp",value:"[0:1]"}],timeseries:[{type:"int",key:"[0:1]",value:"[0:1]"},{type:"int",key:"temp",value:"[1:]"}]}],attributeUpdates:[{path:"fol/hello.json",deviceNameFilter:".*",writingMode:"WRITE",valueExpression:"{'${attributeKey}':'${attributeValue}'}"}],serverSideRpc:[{deviceNameFilter:".*",methodFilter:"read",valueExpression:"${params}"},{deviceNameFilter:".*",methodFilter:"write",valueExpression:"${params}"}]},zt={server:{jid:"gateway@localhost",password:"password",host:"localhost",port:5222,use_ssl:!1,disable_starttls:!1,force_starttls:!0,timeout:1e4,plugins:["xep_0030","xep_0323","xep_0325"]},devices:[{jid:"device@localhost/TMP_1101",deviceNameExpression:"${serialNumber}",deviceTypeExpression:"default",attributes:[{key:"temperature",value:"${temp}"}],timeseries:[{key:"humidity",value:"${hum}"},{key:"combination",value:"${temp}:${hum}"}],attributeUpdates:[{attributeOnThingsBoard:"shared",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{methodRPC:"rpc1",withResponse:!0,valueExpression:"${params}"}]}]},Ut={centralSystem:{name:"Central System",host:"127.0.0.1",port:9e3,connection:{type:"insecure"},security:[{type:"token",tokens:["Bearer ACCESS_TOKEN"]},{type:"basic",credentials:[{username:"admin",password:"admin"}]}]},chargePoints:[{idRegexpPattern:"bidon/hello/CP_1",deviceNameExpression:"${Vendor} ${Model}",deviceTypeExpression:"default",attributes:[{messageTypeFilter:"MeterValues,",key:"temp1",value:"${meter_value[:].sampled_value[:].value}"},{messageTypeFilter:"MeterValues,",key:"vendorId",value:"${connector_id}"}],timeseries:[{messageTypeFilter:"DataTransfer,",key:"temp",value:"${data.temp}"}],attributeUpdates:[{attributeOnThingsBoard:"shared",valueExpression:'{"${attributeKey}":"${attributeValue}"}'}],serverSideRpc:[{methodRPC:"rpc1",withResponse:!0,valueExpression:"${params}"}]}]};const jt=e("connectorConfigs",{[ct.MQTT]:Dt,[ct.MODBUS]:Ot,[ct.OPCUA]:At,[ct.BLE]:Ft,[ct.REQUEST]:Rt,[ct.CAN]:Bt,[ct.BACNET]:Nt,[ct.ODBC]:Lt,[ct.REST]:Vt,[ct.SNMP]:qt,[ct.FTP]:Gt,[ct.SOCKET]:Pt,[ct.XMPP]:zt,[ct.OCPP]:Ut,[ct.KNX]:{3.7:{logLevel:"INFO",client:{type:"AUTOMATIC",addressFormat:"LONG",gatewayIP:"127.0.0.1",gatewayPort:3671,autoReconnect:!0,autoReconnectWait:3,gatewaysScanner:{enabled:!0,scanPeriod:3,stopOnFound:!1}},devices:[{deviceInfo:{deviceNameDataType:"string",deviceNameExpressionSource:"expression",deviceNameExpression:"Device ${1/0/5}",deviceProfileDataType:"none",deviceProfileExpressionSource:"constant",deviceProfileNameExpression:"default"},pollPeriod:5e3,attributes:[{type:"temperature",key:"temperature",groupAddress:"1/0/6"}],timeseries:[{type:"humidity",key:"humidity",groupAddress:"1/0/7"}]}],attributeUpdates:[{deviceNameFilter:".*",dataType:"precent_U8",groupAddress:"1/0/9",key:"brightness"}],serverSideRpc:[{requestType:"read",deviceNameFilter:".*",method:"get_name",dataType:"string",groupAddress:"1/0/5"},{requestType:"write",deviceNameFilter:".*",method:"set_name",dataType:"string",groupAddress:"1/0/5"}]}}});function Ht(e){const t=jt[e];if(!t)throw new Error("No default config found");return t}var Wt;e("ModbusDataType",Wt),function(e){e.STRING="string",e.BYTES="bytes",e.BITS="bits",e.INT8="8int",e.UINT8="8uint",e.INT16="16int",e.UINT16="16uint",e.FLOAT16="16float",e.INT32="32int",e.UINT32="32uint",e.FLOAT32="32float",e.INT64="64int",e.UINT64="64uint",e.FLOAT64="64float"}(Wt||e("ModbusDataType",Wt={}));const $t=e("ModbusEditableDataTypes",[Wt.BYTES,Wt.BITS,Wt.STRING]);var Kt,Yt;e("ModbusObjectCountByDataType",Kt),function(e){e[e["8int"]=1]="8int",e[e["8uint"]=1]="8uint",e[e["16int"]=1]="16int",e[e["16uint"]=1]="16uint",e[e["16float"]=1]="16float",e[e["32int"]=2]="32int",e[e["32uint"]=2]="32uint",e[e["32float"]=2]="32float",e[e["64int"]=4]="64int",e[e["64uint"]=4]="64uint",e[e["64float"]=4]="64float"}(Kt||e("ModbusObjectCountByDataType",Kt={})),e("MappingValueType",Yt),function(e){e.STRING="string",e.INTEGER="integer",e.DOUBLE="double",e.BOOLEAN="boolean"}(Yt||e("MappingValueType",Yt={}));const Xt=e("mappingValueTypesMap",new Map([[Yt.STRING,{name:"value.string",icon:"mdi:format-text"}],[Yt.INTEGER,{name:"value.integer",icon:"mdi:numeric"}],[Yt.DOUBLE,{name:"value.double",icon:"mdi:numeric"}],[Yt.BOOLEAN,{name:"value.boolean",icon:"mdi:checkbox-marked-outline"}]])),Zt=e("ModbusFunctionCodeTranslationsMap",new Map([[1,"gateway.function-codes.read-coils"],[2,"gateway.function-codes.read-discrete-inputs"],[3,"gateway.function-codes.read-multiple-holding-registers"],[4,"gateway.function-codes.read-input-registers"],[5,"gateway.function-codes.write-single-coil"],[6,"gateway.function-codes.write-single-holding-register"],[15,"gateway.function-codes.write-multiple-coils"],[16,"gateway.function-codes.write-multiple-holding-registers"]]));var Qt,Jt,en;e("ConfigurationModes",Qt),function(e){e.BASIC="basic",e.ADVANCED="advanced"}(Qt||e("ConfigurationModes",Qt={})),e("ReportStrategyType",Jt),function(e){e.OnChange="ON_CHANGE",e.OnReportPeriod="ON_REPORT_PERIOD",e.OnChangeOrReportPeriod="ON_CHANGE_OR_REPORT_PERIOD",e.OnReceived="ON_RECEIVED"}(Jt||e("ReportStrategyType",Jt={})),e("ReportStrategyDefaultValue",en),function(e){e[e.Gateway=6e4]="Gateway",e[e.Connector=6e4]="Connector",e[e.Device=3e4]="Device",e[e.Key=15e3]="Key"}(en||e("ReportStrategyDefaultValue",en={}));const tn=e("ReportStrategyTypeTranslationsMap",new Map([[Jt.OnChange,"gateway.report-strategy.on-change"],[Jt.OnReportPeriod,"gateway.report-strategy.on-report-period"],[Jt.OnChangeOrReportPeriod,"gateway.report-strategy.on-change-or-report-period"],[Jt.OnReceived,"gateway.report-strategy.on-received"]])),nn=e("numberInputPattern",new RegExp(/^\d{1,15}(\.\d{1,15})?$/)),an=e("noLeadTrailSpacesRegex",/^\S+(?: \S+)*$/),rn=e("integerRegex",/^[-+]?\d+$/),on=e("nonZeroFloat",/^-?(?!0(\.0+)?$)\d+(\.\d+)?$/);var sn;!function(e){e.EXCEPTION="EXCEPTION"}(sn||(sn={}));const ln={...lt,...sn},pn=()=>[10,20,30];function cn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"a",19),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).$implicit,i=t.ɵɵnextContext();return t.ɵɵresetView(i.onTabChanged(n))})),t.ɵɵtext(1),t.ɵɵelementEnd()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("active",i.activeLink.name===e.name),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.name," ")}}function dn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",20),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.created-time")))}function un(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtext(1),t.ɵɵpipe(2,"date"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind2(2,1,e.ts,"yyyy-MM-dd HH:mm:ss")," ")}}function mn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.level")))}function hn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell")(1,"span"),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassMap(i.statusClass(e.status)),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.status)}}function gn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",22),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.message")))}function fn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵclassMap(i.statusClassMsg(e.status)),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.message," ")}}function yn(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",23)}function vn(e,n){1&e&&t.ɵɵelement(0,"mat-row",23)}class xn{constructor(){this.displayedColumns=["ts","status","message"],this.gatewayLogLinks=[{name:"General",key:"LOGS"},{name:"Service",key:"SERVICE_LOGS"},{name:"Connection",key:"CONNECTION_LOGS"},{name:"Storage",key:"STORAGE_LOGS"},{key:"EXTENSIONS_LOGS",name:"Extension"}];const e={property:"ts",direction:w.DESC};this.pageLink=new S(10,0,null,e),this.dataSource=new x([])}ngOnInit(){this.updateWidgetTitle()}ngAfterViewInit(){if(this.dataSource.sort=this.sort,this.dataSource.paginator=this.paginator,this.ctx.defaultSubscription.onTimewindowChangeFunction=e=>(this.ctx.defaultSubscription.options.timeWindowConfig=e,this.ctx.defaultSubscription.updateDataSubscriptions(),e),this.ctx.settings.isConnectorLog&&this.ctx.settings.connectorLogState){const e=this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];this.logLinks=[{key:`${e.key}_LOGS`,name:"Connector",filterFn:e=>!e.message.includes("_converter.py")},{key:`${e.key}_converter_LOGS`,name:"Converter",filterFn:e=>e.message.includes("_converter.py")}]}else this.logLinks=this.gatewayLogLinks;this.activeLink=this.logLinks[0],this.changeSubscription()}updateWidgetTitle(){if(this.ctx.settings.isConnectorLog&&this.ctx.settings.connectorLogState){const e=this.ctx.widgetConfig.title,t="${connectorName}";if(e.includes(t)){const n=this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];this.ctx.widgetTitle=e.replace(t,n.key)}}}updateData(){if(this.ctx.defaultSubscription.data.length&&this.ctx.defaultSubscription.data[0]){let e=this.ctx.defaultSubscription.data[0].data.map((e=>{const t={ts:e[0],key:this.activeLink.key,message:e[1],status:"INVALID LOG FORMAT"};try{t.message=/\[(.*)/.exec(e[1])[0]}catch(n){t.message=e[1]}try{t.status=e[1].match(/\|(\w+)\|/)[1]}catch(e){t.status="INVALID LOG FORMAT"}return t}));this.activeLink.filterFn&&(e=e.filter((e=>this.activeLink.filterFn(e)))),this.dataSource.data=e}}onTabChanged(e){this.activeLink=e,this.changeSubscription()}statusClass(e){switch(e){case ln.DEBUG:return"status status-debug";case ln.WARNING:return"status status-warning";case ln.ERROR:case ln.EXCEPTION:return"status status-error";default:return"status status-info"}}statusClassMsg(e){if(e===ln.EXCEPTION)return"msg-status-exception"}trackByLogTs(e,t){return t.ts}changeSubscription(){this.ctx.datasources&&this.ctx.datasources[0].entity&&this.ctx.defaultSubscription.options.datasources&&(this.ctx.defaultSubscription.options.datasources[0].dataKeys=[{name:this.activeLink.key,type:C.timeseries,settings:{}}],this.ctx.defaultSubscription.unsubscribe(),this.ctx.defaultSubscription.updateDataSubscriptions(),this.ctx.defaultSubscription.callbacks.onDataUpdated=()=>{this.updateData()})}static{this.ɵfac=function(e){return new(e||xn)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:xn,selectors:[["tb-gateway-logs"]],viewQuery:function(e,n){if(1&e&&(t.ɵɵviewQuery(v,5),t.ɵɵviewQuery(b,5)),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.sort=e.first),t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.paginator=e.first)}},inputs:{ctx:"ctx",dialogRef:"dialogRef"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:23,vars:21,consts:[["tabPanel",""],[1,"flex","h-full","flex-col"],["mat-tab-nav-bar","",3,"tabPanel"],["mat-tab-link","",3,"active","click",4,"ngFor","ngForOf"],[1,"flex-1","overflow-auto"],["mat-table","","matSort","","matSortDisableClear","",3,"dataSource","trackBy","matSortActive","matSortDirection"],["matColumnDef","ts"],["mat-sort-header","","style","width: 20%",4,"matHeaderCellDef"],[4,"matCellDef"],["matColumnDef","status"],["mat-sort-header","","style","width: 10%",4,"matHeaderCellDef"],["matColumnDef","message"],["mat-sort-header","","style","width: 70%",4,"matHeaderCellDef"],[3,"class",4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],["class","mat-row-select",4,"matRowDef","matRowDefColumns"],[1,"no-data-found","flex-1","items-center","justify-center"],[1,"flex-1"],["showFirstLastButtons","",3,"length","pageIndex","pageSize","pageSizeOptions"],["mat-tab-link","",3,"click","active"],["mat-sort-header","",2,"width","20%"],["mat-sort-header","",2,"width","10%"],["mat-sort-header","",2,"width","70%"],[1,"mat-row-select"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"nav",2),t.ɵɵtemplate(2,cn,2,2,"a",3),t.ɵɵelementEnd(),t.ɵɵelement(3,"mat-tab-nav-panel",null,0),t.ɵɵelementStart(5,"div",4)(6,"table",5),t.ɵɵelementContainerStart(7,6),t.ɵɵtemplate(8,dn,3,3,"mat-header-cell",7)(9,un,3,4,"mat-cell",8),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(10,9),t.ɵɵtemplate(11,mn,3,3,"mat-header-cell",10)(12,hn,3,3,"mat-cell",8),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(13,11),t.ɵɵtemplate(14,gn,3,3,"mat-header-cell",12)(15,fn,2,3,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(16,yn,1,0,"mat-header-row",14)(17,vn,1,0,"mat-row",15),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"span",16),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(21,"span",17),t.ɵɵelementEnd(),t.ɵɵelement(22,"mat-paginator",18),t.ɵɵelementEnd()),2&e){const e=t.ɵɵreference(4);t.ɵɵadvance(),t.ɵɵproperty("tabPanel",e),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.logLinks),t.ɵɵadvance(4),t.ɵɵproperty("dataSource",n.dataSource)("trackBy",n.trackByLogTs)("matSortActive",n.pageLink.sortOrder.property)("matSortDirection",n.pageLink.sortDirection()),t.ɵɵadvance(10),t.ɵɵproperty("matHeaderRowDef",n.displayedColumns)("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",n.displayedColumns),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",0!==n.dataSource.data.length),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,18,"attribute.no-telemetry-text")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",0===n.dataSource.data.length),t.ɵɵadvance(),t.ɵɵproperty("length",n.dataSource.data.length)("pageIndex",n.pageLink.page)("pageSize",n.pageLink.pageSize)("pageSizeOptions",t.ɵɵpureFunction0(20,pn))}},dependencies:t.ɵɵgetComponentDepsFactory(xn,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex;flex-direction:column;overflow-x:auto;padding:0}[_nghost-%COMP%] .status[_ngcontent-%COMP%]{border-radius:20px;font-weight:500;padding:5px 15px}[_nghost-%COMP%] .status-debug[_ngcontent-%COMP%]{color:green;background:#0080001a}[_nghost-%COMP%] .status-warning[_ngcontent-%COMP%]{color:orange;background:#ffa5001a}[_nghost-%COMP%] .status-error[_ngcontent-%COMP%]{color:red;background:#ff00001a}[_nghost-%COMP%] .status-info[_ngcontent-%COMP%]{color:#00f;background:#0000801a}[_nghost-%COMP%] .msg-status-exception[_ngcontent-%COMP%]{color:red}']})}} -/** - * @license Angular v18.2.6 - * (c) 2010-2024 Google LLC. https://angular.io/ - * License: MIT - */ -function bn(e){e||(n(bn),e=i(a));const t=new Z((t=>e.onDestroy(t.next.bind(t))));return e=>e.pipe(le(t))}e("GatewayLogsComponent",xn);function wn(e,t){!t?.injector&&n(wn);const l=t?.injector??i(r),p=new Q(1),c=o((()=>{let t;try{t=e()}catch(e){return void s((()=>p.error(e)))}s((()=>p.next(t)))}),{injector:l,manualCleanup:!0});return l.get(a).onDestroy((()=>{c.destroy(),p.complete()})),p.asObservable()}const Sn=["commandInput"],Cn=(e,t)=>t.attributeOnGateway,_n=e=>({command:e});function Tn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",10),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.clear())})),t.ɵɵelementStart(2,"mat-icon",11),t.ɵɵtext(3,"close"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"action.clear"))}function In(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",12),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onCreateNewClick(n))})),t.ɵɵelementStart(1,"span",13),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"notification.create-new")))}function En(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵelement(1,"span",14),t.ɵɵpipe(2,"async"),t.ɵɵpipe(3,"highlight"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(3,4,e.attributeOnGateway,t.ɵɵpipeBind1(2,2,i.searchText$)),t.ɵɵsanitizeHtml)}}function Mn(e,n){if(1&e&&(t.ɵɵtext(0),t.ɵɵpipe(1,"async"),t.ɵɵpipe(2,"translate")),2&e){const e=t.ɵɵnextContext(2);t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind2(2,3,"gateway.statistics.no-commands-matching",t.ɵɵpureFunction1(6,_n,e.truncate.transform(t.ɵɵpipeBind1(1,1,e.searchText$),!0,6,"...")))," ")}}function kn(e,n){1&e&&(t.ɵɵtext(0),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(1,1,"gateway.statistics.no-command-found")," ")}function Pn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-option",9)(1,"div",15),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(2,"span"),t.ɵɵtemplate(3,Mn,3,8),t.ɵɵpipe(4,"async"),t.ɵɵtemplate(5,kn,2,3),t.ɵɵelementStart(6,"a",16),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onCreateNewClick(n))})),t.ɵɵtext(7,"gateway.create-new-one"),t.ɵɵelementEnd()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("value",null),t.ɵɵadvance(3),t.ɵɵconditional(t.ɵɵpipeBind1(4,2,e.searchText$)?3:5)}}class Dn{constructor(e,t){this.truncate=e,this.fb=t,this.commands=l(),this.onCreateNewClicked=p(),this.selectStatisticsCommandControl=this.fb.control({}),this.searchText$=this.selectStatisticsCommandControl.valueChanges.pipe(pe((e=>e?"string"==typeof e?e:e?.attributeOnGateway:"")),ce(),J(1)),this.filteredCommands$=ee([this.searchText$,wn(this.commands)]).pipe(de(150),pe((([e,t])=>{const n=t.find((t=>t.attributeOnGateway===e))??null,i=this.selectStatisticsCommandControl.value;return"string"==typeof i&&n?.attributeOnGateway!==e||this.selectStatisticsCommandControl.patchValue(n,{emitEvent:!Se(n,i)}),t.filter((t=>t.attributeOnGateway.toLowerCase().includes(e?.toLowerCase()??"")))})),J(1)),this.onChanges=e=>{},this.selectStatisticsCommandControl.valueChanges.pipe(bn()).subscribe((e=>this.onChanges(e)))}registerOnChange(e){this.onChanges=e}registerOnTouched(e){}writeValue(e){this.selectStatisticsCommandControl.patchValue(e)}displayCommandFn(e){return e?e.attributeOnGateway:null}clear(){this.selectStatisticsCommandControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.commandInput.nativeElement.blur(),this.commandInput.nativeElement.focus()}),0)}onCreateNewClick(e){e.stopPropagation(),this.onCreateNewClicked.emit()}static{this.ɵfac=function(e){return new(e||Dn)(t.ɵɵdirectiveInject(T.TruncatePipe),t.ɵɵdirectiveInject(H.UntypedFormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Dn,selectors:[["tb-statistics-commands-autocomplete"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(Sn,7),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.commandInput=e.first)}},inputs:{commands:[1,"commands"]},outputs:{onCreateNewClicked:"onCreateNewClicked"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>Dn)),multi:!0}]),t.ɵɵStandaloneFeature],decls:14,vars:10,consts:[["commandInput",""],["commandAutocomplete","matAutocomplete"],["appearance","outline",1,"mat-block"],["translate",""],["matInput","","type","text",3,"formControl","matAutocomplete"],["type","button","matTooltipPosition","above","matSuffix","","mat-icon-button","","aria-label","Clear",1,"action-button",3,"matTooltip"],["mat-button","","color","primary","matSuffix","",1,"mr-2"],[1,"tb-autocomplete",3,"displayWith"],[3,"value"],[1,"tb-not-found",3,"value"],["type","button","matTooltipPosition","above","matSuffix","","mat-icon-button","","aria-label","Clear",1,"action-button",3,"click","matTooltip"],[1,"material-icons"],["mat-button","","color","primary","matSuffix","",1,"mr-2",3,"click"],[1,"whitespace-nowrap"],[3,"innerHTML"],[1,"tb-not-found-content",3,"click"],["translate","",3,"click"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field",2)(1,"mat-label",3),t.ɵɵtext(2,"gateway.statistics.name"),t.ɵɵelementEnd(),t.ɵɵelement(3,"input",4,0),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,Tn,4,3,"button",5)(7,In,4,3,"button",6),t.ɵɵelementStart(8,"mat-autocomplete",7,1),t.ɵɵrepeaterCreate(10,En,4,7,"mat-option",8,Cn,!1,Pn,8,4,"mat-option",9),t.ɵɵpipe(13,"async"),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵreference(9);t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.selectStatisticsCommandControl)("matAutocomplete",e),t.ɵɵattribute("aria-label",t.ɵɵpipeBind1(5,6,"gateway.statistics.command")),t.ɵɵadvance(3),t.ɵɵconditional(n.selectStatisticsCommandControl.value?6:7),t.ɵɵadvance(2),t.ɵɵproperty("displayWith",n.displayCommandFn),t.ɵɵadvance(2),t.ɵɵrepeater(t.ɵɵpipeBind1(13,8,n.filteredCommands$))}},dependencies:t.ɵɵgetComponentDepsFactory(Dn,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%] a[_ngcontent-%COMP%]{border-bottom:none;color:inherit}[_nghost-%COMP%] .action-button[_ngcontent-%COMP%]{opacity:.7}']})}}const On=()=>["createdTime","message"];function An(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",12),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"widgets.gateway.created-time")))}function Fn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",13),t.ɵɵtext(1),t.ɵɵpipe(2,"date"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(2,1,e[0],"yyyy-MM-dd HH:mm:ss"))}}function Rn(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",14),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"widgets.gateway.message")," "))}function Bn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell")(1,"div",15),t.ɵɵtext(2),t.ɵɵelement(3,"tb-copy-button",16),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",e[1]," "),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(4,3,"gateway.statistics.copy-message")),t.ɵɵproperty("copyText",e[1])}}function Nn(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",17)}function Ln(e,n){1&e&&t.ɵɵelement(0,"mat-row",17)}class Vn{constructor(){this.data=l([]),this.defaultPageSizes=[10,20,30],this.defaultSortOrder={property:"0",direction:w.DESC},this.pageLink=new S(this.defaultPageSizes[0],0,null,this.defaultSortOrder),this.dataSource=new x([]),o((()=>{this.dataSource.data=this.data()}))}ngAfterViewInit(){this.dataSource.sort=this.sort,this.dataSource.paginator=this.paginator,this.dataSource.sortingDataAccessor=e=>e[Number(this.sort?.active)||0]}static{this.ɵfac=function(e){return new(e||Vn)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Vn,selectors:[["tb-custom-statistics-table"]],viewQuery:function(e,n){if(1&e&&(t.ɵɵviewQuery(v,5),t.ɵɵviewQuery(b,5)),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.sort=e.first),t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.paginator=e.first)}},inputs:{data:[1,"data"]},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:12,vars:13,consts:[[1,"flex","h-full","flex-col"],[1,"flex-1","overflow-auto"],["mat-table","","matSort","","matSortDisableClear","",3,"dataSource","matSortActive","matSortDirection"],["matColumnDef","createdTime"],["mat-sort-header","","class","w-1/5",4,"matHeaderCellDef"],["class","!w-1/5",4,"matCellDef"],["matColumnDef","message"],["mat-sort-header","",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],["class","mat-row-select",4,"matRowDef","matRowDefColumns"],["showFirstLastButtons","",3,"length","pageSize","pageSizeOptions"],["mat-sort-header","",1,"w-1/5"],[1,"!w-1/5"],["mat-sort-header",""],[1,"flex","items-center","justify-between"],["tooltipPosition","above","icon","content_copy",1,"copy-content",3,"copyText","tooltipText"],[1,"mat-row-select"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"table",2),t.ɵɵelementContainerStart(3,3),t.ɵɵtemplate(4,An,3,3,"mat-header-cell",4)(5,Fn,3,4,"mat-cell",5),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(6,6),t.ɵɵtemplate(7,Rn,3,3,"mat-header-cell",7)(8,Bn,5,5,"mat-cell",8),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(9,Nn,1,0,"mat-header-row",9)(10,Ln,1,0,"mat-row",10),t.ɵɵelementEnd()(),t.ɵɵelement(11,"mat-paginator",11),t.ɵɵelementEnd()),2&e){let e;t.ɵɵadvance(2),t.ɵɵproperty("dataSource",n.dataSource)("matSortActive",n.pageLink.sortOrder.property)("matSortDirection",n.pageLink.sortDirection()),t.ɵɵadvance(7),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(11,On))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(12,On)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",!n.dataSource.data.length),t.ɵɵproperty("length",null!==(e=null==n.dataSource||null==n.dataSource.data?null:n.dataSource.data.length)&&void 0!==e?e:0)("pageSize",n.defaultPageSizes[0])("pageSizeOptions",n.defaultPageSizes)}},dependencies:t.ɵɵgetComponentDepsFactory(Vn,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .copy-content .mat-icon{padding:4px;font-size:18px}']})}}class qn{constructor(e,t,n){this.elementRef=e,this.renderer=t,this.tooltip=n,this.tooltipEnabled=!0,this.position="above",this.destroy$=new te}ngOnInit(){this.observeMouseEvents(),this.applyTruncationStyles()}ngAfterViewInit(){this.tooltip.position=this.position}ngOnDestroy(){this.tooltip._isTooltipVisible()&&this.hideTooltip(),this.destroy$.next(),this.destroy$.complete()}observeMouseEvents(){ne(this.elementRef.nativeElement,"mouseenter").pipe(ue((()=>this.tooltipEnabled)),ue((()=>this.isOverflown(this.elementRef.nativeElement))),me((()=>this.showTooltip())),le(this.destroy$)).subscribe(),ne(this.elementRef.nativeElement,"mouseleave").pipe(ue((()=>this.tooltipEnabled)),ue((()=>this.tooltip._isTooltipVisible())),me((()=>this.hideTooltip())),le(this.destroy$)).subscribe()}applyTruncationStyles(){this.renderer.setStyle(this.elementRef.nativeElement,"white-space","nowrap"),this.renderer.setStyle(this.elementRef.nativeElement,"overflow","hidden"),this.renderer.setStyle(this.elementRef.nativeElement,"text-overflow","ellipsis")}isOverflown(e){return e.clientWidth{this.adjustChips()}),0))}constructor(e,t,n,i){this.el=e,this.renderer=t,this.translate=n,this.window=i,this.destroy$=new te,this.renderer.setStyle(this.el.nativeElement,"max-height","48px"),this.renderer.setStyle(this.el.nativeElement,"overflow","auto"),ne(i,"resize").pipe(le(this.destroy$)).subscribe((()=>{this.adjustChips()})),this.observeIntersection()}observeIntersection(){this.intersectionObserver=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.adjustChips()}))})),this.intersectionObserver.observe(this.el.nativeElement)}adjustChips(){const e=this.el.nativeElement,t=this.el.nativeElement.querySelector(".ellipsis-chip"),n=parseFloat(this.window.getComputedStyle(t).marginLeft)||0,i=e.querySelectorAll("mat-chip:not(.ellipsis-chip)");if(this.chipsValue.length>1){const a=this.el.nativeElement.querySelector(".ellipsis-text");this.renderer.setStyle(t,"display","inline-flex"),a.innerHTML=this.translate.instant("gateway.ellipsis-chips-text",{count:this.chipsValue.length});const r=e.offsetWidth-(t.offsetWidth+n);let o=0,s=0;i.forEach((e=>{this.renderer.setStyle(e,"display","inline-flex");const t=e.querySelector(".mdc-evolution-chip__text-label");this.applyMaxChipTextWidth(t,r/3),o+(e.offsetWidth+n)<=r&&sae(E())))).subscribe((e=>{this.attributesSubject.next(e.data),this.pageDataSubject.next(e),a.next(e)})),a}fetchAttributes(e,t,n){return this.getAllAttributes(e,t).pipe(pe((e=>{const t=e.filter((e=>0!==e.lastUpdateTs));return n.filterData(t)})))}getAllAttributes(e,t){if(!this.allAttributes){let n;M.get(t)?(this.telemetrySubscriber=k.createEntityAttributesSubscription(this.telemetryWsService,e,t,this.zone),this.telemetrySubscriber.subscribe(),n=this.telemetrySubscriber.attributeData$()):n=this.attributeService.getEntityAttributes(e,t),this.allAttributes=n.pipe(ge(1),fe())}return this.allAttributes}isAllSelected(){const e=this.selection.selected.length;return this.attributesSubject.pipe(pe((t=>e===t.length)))}isEmpty(){return this.attributesSubject.pipe(pe((e=>!e.length)))}total(){return this.pageDataSubject.pipe(pe((e=>e.totalElements)))}masterToggle(){this.attributesSubject.pipe(me((e=>{this.selection.selected.length===e.length?this.selection.clear():e.forEach((e=>{this.selection.select(e)}))})),ye(1)).subscribe()}}e("AttributeDatasource",zn);const Un=()=>({maxWidth:"970px"});function jn(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-expansion-panel",4)(1,"mat-expansion-panel-header",5)(2,"mat-panel-title")(3,"mat-slide-toggle",6),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(4,"mat-label"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementContainer(7,7),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(),n=t.ɵɵreference(5);t.ɵɵproperty("expanded",e.showStrategyControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showStrategyControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,4,"gateway.report-strategy.label")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n)}}function Hn(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",8),t.ɵɵtext(1,"gateway.report-strategy.label"),t.ɵɵelementEnd(),t.ɵɵelementContainer(2,7)),2&e){t.ɵɵnextContext();const e=t.ɵɵreference(5);t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",e)}}function Wn(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",16),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.ReportTypeTranslateMap.get(e)))}}function $n(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",19),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.reportStrategyFormGroup.get("reportPeriod").hasError("min")?"gateway.hints.report-period-range":"gateway.hints.report-period-required"))}}function Kn(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",17),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",18),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,$n,3,3,"mat-icon",19),t.ɵɵelementStart(8,"span",20),t.ɵɵtext(9,"gateway.suffix.ms"),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,3,"gateway.report-strategy.report-period")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.reportStrategyFormGroup.get("reportPeriod").hasError("required")||e.reportStrategyFormGroup.get("reportPeriod").hasError("min")&&e.reportStrategyFormGroup.get("reportPeriod").touched?7:-1)}}function Yn(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelement(4,"div",11),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",12)(6,"mat-select",13),t.ɵɵtemplate(7,Wn,3,4,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵtemplate(8,Kn,10,7,"div",15)),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,5,"gateway.type")," "),t.ɵɵadvance(2),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/report-strategy_fn")("tb-help-popup-style",t.ɵɵpureFunction0(7,Un)),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.reportStrategyTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.reportStrategyFormGroup.get("type").value!==e.ReportStrategyType.OnChange&&e.reportStrategyFormGroup.get("type").value!==e.ReportStrategyType.OnReceived)}}class Xn{constructor(e){this.fb=e,this.isExpansionMode=!1,this.defaultValue=en.Key,this.reportStrategyTypes=Object.values(Jt),this.ReportTypeTranslateMap=tn,this.ReportStrategyType=Jt,this.destroy$=new te,this.showStrategyControl=this.fb.control(!1),this.reportStrategyFormGroup=this.fb.group({type:[{value:Jt.OnReportPeriod,disabled:!0},[]],reportPeriod:[{value:this.defaultValue,disabled:!0},[$.required,$.min(100)]]}),this.observeStrategyFormChange(),this.observeStrategyToggle()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}writeValue(e){this.isExpansionMode&&this.showStrategyControl.setValue(!!e,{emitEvent:!1}),e&&this.reportStrategyFormGroup.enable({emitEvent:!1});const{type:t=Jt.OnReportPeriod,reportPeriod:n=this.defaultValue}=e??{};this.reportStrategyFormGroup.setValue({type:t,reportPeriod:n},{emitEvent:!1}),this.onTypeChange(t)}validate(){return this.reportStrategyFormGroup.valid||this.reportStrategyFormGroup.disabled?null:{reportStrategyForm:{valid:!1}}}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}observeStrategyFormChange(){this.reportStrategyFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()})),this.reportStrategyFormGroup.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.onTypeChange(e)))}observeStrategyToggle(){this.showStrategyControl.valueChanges.pipe(le(this.destroy$),ue((()=>this.isExpansionMode))).subscribe((e=>{e?(this.reportStrategyFormGroup.enable({emitEvent:!1}),this.reportStrategyFormGroup.get("reportPeriod").addValidators($.required),this.onChange(this.reportStrategyFormGroup.value)):(this.reportStrategyFormGroup.disable({emitEvent:!1}),this.reportStrategyFormGroup.get("reportPeriod").removeValidators($.required),this.onChange(null)),this.reportStrategyFormGroup.updateValueAndValidity({emitEvent:!1})}))}onTypeChange(e){const t=this.reportStrategyFormGroup.get("reportPeriod");e===Jt.OnChange||e===Jt.OnReceived?t.disable({emitEvent:!1}):this.isExpansionMode&&!this.showStrategyControl.value||t.enable({emitEvent:!1})}static{this.ɵfac=function(e){return new(e||Xn)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Xn,selectors:[["tb-report-strategy"]],inputs:{isExpansionMode:"isExpansionMode",defaultValue:"defaultValue"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>Xn)),multi:!0},{provide:K,useExisting:c((()=>Xn)),multi:!0}]),t.ɵɵStandaloneFeature],decls:6,vars:3,consts:[["defaultMode",""],["strategyFields",""],[3,"formGroup"],["class","tb-settings",3,"expanded",4,"ngIf","ngIfElse"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide",3,"click","formControl"],[3,"ngTemplateOutlet"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","flex","items-center","gap-2"],["matSuffix","","tb-help-popup-placement","right",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],[3,"value"],["tbTruncateWithTooltip","",1,"fixed-title-width","tb-required"],["matInput","","type","number","min","100","name","value","formControlName","reportPeriod",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["translate","","matSuffix","",1,"block","pr-2"]],template:function(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,2),t.ɵɵtemplate(1,jn,8,6,"mat-expansion-panel",3)(2,Hn,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor)(4,Yn,9,8,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(3);t.ɵɵproperty("formGroup",n.reportStrategyFormGroup),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isExpansionMode)("ngIfElse",e)}},dependencies:t.ɵɵgetComponentDepsFactory(Xn,[j,_,qn]),encapsulation:2,changeDetection:d.OnPush})}}e("ReportStrategyComponent",Xn),Ge([I()],Xn.prototype,"isExpansionMode",void 0),Ge([P()],Xn.prototype,"defaultValue",void 0);class Zn{constructor(e,t){this.attributeService=e,this.cd=t,this.isGatewayActive=!1}ngAfterViewInit(){this.ctx.$scope.gatewayStatus=this,this.loadGatewayState()}loadGatewayState(){this.attributeService.getEntityAttributes(this.deviceId,D.SERVER_SCOPE,["active","lastDisconnectTime","lastConnectTime"]).subscribe((e=>{const t=e.find((e=>"active"===e.key)).value,n=e.find((e=>"lastDisconnectTime"===e.key))?.value,i=e.find((e=>"lastConnectTime"===e.key))?.value;this.isGatewayActive=this.getGatewayStatus(t,i,n),this.cd.detectChanges()}))}getGatewayStatus(e,t,n){return!!e&&(!n||t>n)}onDataUpdated(){const e=this.ctx.defaultSubscription.data,t=e.find((e=>"active"===e.dataKey.name)).data[0][1],n=e.find((e=>"lastDisconnectTime"===e.dataKey.name)).data[0][1],i=e.find((e=>"lastConnectTime"===e.dataKey.name)).data[0][1];this.isGatewayActive=this.getGatewayStatus(t,i,n),this.cd.detectChanges()}static{this.ɵfac=function(e){return new(e||Zn)(t.ɵɵdirectiveInject(_e.AttributeService),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Zn,selectors:[["tb-gateway-status"]],inputs:{ctx:"ctx",deviceId:"deviceId"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:9,vars:10,consts:[[1,"flex","min-h-10","flex-1","justify-center"],[1,"divider"],[1,"whitespace-nowrap"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-card",0),t.ɵɵelement(1,"div",1),t.ɵɵelementStart(2,"mat-card-header")(3,"mat-card-subtitle",2),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"mat-card-content"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵclassProp("divider-red",!n.isGatewayActive)("divider-green",n.isGatewayActive),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,6,"gateway.gateway-status")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,8,n.isGatewayActive?"gateway.active":"gateway.inactive")))},dependencies:t.ɵɵgetComponentDepsFactory(Zn,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex}[_nghost-%COMP%] .divider[_ngcontent-%COMP%]{position:absolute;width:3px;top:12px;border-radius:2px;bottom:4px;left:10px}[_nghost-%COMP%] .divider-green[_ngcontent-%COMP%] .divider[_ngcontent-%COMP%]{border:1px solid rgb(25,128,56);background-color:#198038}[_nghost-%COMP%] .divider-green[_ngcontent-%COMP%] .mat-mdc-card-content[_ngcontent-%COMP%]{color:#198038}[_nghost-%COMP%] .divider-red[_ngcontent-%COMP%] .divider[_ngcontent-%COMP%]{border:1px solid rgb(203,37,48);background-color:#cb2530}[_nghost-%COMP%] .divider-red[_ngcontent-%COMP%] .mat-mdc-card-content[_ngcontent-%COMP%]{color:#cb2530}.mdc-card[_ngcontent-%COMP%]{position:relative;padding-left:10px;box-shadow:none}.mat-mdc-card-subtitle[_ngcontent-%COMP%]{font-weight:400;font-size:12px}.mat-mdc-card-header[_ngcontent-%COMP%]{padding:8px 16px 0}.mat-mdc-card-content[_ngcontent-%COMP%]:last-child{padding-bottom:8px;font-size:16px}'],changeDetection:d.OnPush})}}e("GatewayStatusComponent",Zn);class Qn{constructor(){this.tooltipText=""}static{this.ɵfac=function(e){return new(e||Qn)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Qn,selectors:[["tb-error-icon"]],inputs:{tooltipText:"tooltipText"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:2,vars:1,consts:[["matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",0),t.ɵɵtext(1," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",n.tooltipText)},dependencies:t.ɵɵgetComponentDepsFactory(Qn,[_,j]),styles:["[_nghost-%COMP%]{display:flex;width:40px;height:40px;padding:10px}mat-icon[_ngcontent-%COMP%]{font-size:20px}"]})}}var Jn,ei;e("ErrorTooltipIconComponent",Qn),e("MqttConverterType",Jn),function(e){e.JSON="json",e.BYTES="bytes",e.CUSTOM="custom"}(Jn||e("MqttConverterType",Jn={})),e("MQTTSourceType",ei),function(e){e.MSG="message",e.TOPIC="topic",e.CONST="constant"}(ei||e("MQTTSourceType",ei={}));const ti=e("MqttVersions",[{name:3.1,value:3},{name:3.11,value:4},{name:5,value:5}]),ni=e("QualityTypeTranslationsMap",new Map([[0,"gateway.qos.at-most-once"],[1,"gateway.qos.at-least-once"],[2,"gateway.qos.exactly-once"]])),ii=e("ConvertorTypeTranslationsMap",new Map([[Jn.JSON,"gateway.JSON"],[Jn.BYTES,"gateway.bytes"],[Jn.CUSTOM,"gateway.custom"]]));var ai;e("RequestType",ai),function(e){e.CONNECT_REQUEST="connectRequests",e.DISCONNECT_REQUEST="disconnectRequests",e.ATTRIBUTE_REQUEST="attributeRequests",e.ATTRIBUTE_UPDATE="attributeUpdates",e.SERVER_SIDE_RPC="serverSideRpc"}(ai||e("RequestType",ai={}));const ri=e("RequestTypesTranslationsMap",new Map([[ai.CONNECT_REQUEST,"gateway.request.connect-request"],[ai.DISCONNECT_REQUEST,"gateway.request.disconnect-request"],[ai.ATTRIBUTE_REQUEST,"gateway.request.attribute-request"],[ai.ATTRIBUTE_UPDATE,"gateway.request.attribute-update"],[ai.SERVER_SIDE_RPC,"gateway.request.rpc-connection"]])),oi=e("DataConversionTranslationsMap",new Map([[Jn.JSON,"gateway.JSON-hint"],[Jn.BYTES,"gateway.bytes-hint"],[Jn.CUSTOM,"gateway.custom-hint"]]));var si,li;e("SocketType",si),function(e){e.TCP="TCP",e.UDP="UDP"}(si||e("SocketType",si={})),e("SocketValueKey",li),function(e){e.TIMESERIES="telemetry",e.ATTRIBUTES="attributes",e.ATTRIBUTES_REQUESTS="attributeRequests",e.ATTRIBUTES_UPDATES="attributeUpdates",e.RPC_METHODS="serverSideRpc"}(li||e("SocketValueKey",li={}));const pi=e("SocketKeysPanelTitleTranslationsMap",new Map([[li.ATTRIBUTES,"gateway.attributes"],[li.TIMESERIES,"gateway.timeseries"],[li.ATTRIBUTES_REQUESTS,"gateway.attribute-requests"],[li.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[li.RPC_METHODS,"gateway.rpc-methods"]]));var ci,di;e("RequestsType",ci),function(e){e.Shared="shared",e.Client="client"}(ci||e("RequestsType",ci={})),e("ExpressionType",di),function(e){e.Constant="constant",e.Expression="expression"}(di||e("ExpressionType",di={}));const ui=e("SocketKeysAddKeyTranslationsMap",new Map([[li.ATTRIBUTES,"gateway.add-attribute"],[li.TIMESERIES,"gateway.add-timeseries"],[li.ATTRIBUTES_REQUESTS,"gateway.add-attribute-request"],[li.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[li.RPC_METHODS,"gateway.add-rpc-method"]])),mi=e("SocketKeysDeleteKeyTranslationsMap",new Map([[li.ATTRIBUTES,"gateway.delete-attribute"],[li.TIMESERIES,"gateway.delete-timeseries"],[li.ATTRIBUTES_REQUESTS,"gateway.delete-attribute-request"],[li.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[li.RPC_METHODS,"gateway.delete-rpc-method"]])),hi=e("SocketKeysNoKeysTextTranslationsMap",new Map([[li.ATTRIBUTES,"gateway.no-attributes"],[li.TIMESERIES,"gateway.no-timeseries"],[li.ATTRIBUTES_REQUESTS,"gateway.no-attribute-requests"],[li.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[li.RPC_METHODS,"gateway.no-rpc-methods"]]));var gi,fi,yi,vi;e("RestConverterType",gi),function(e){e.JSON="json",e.CUSTOM="custom"}(gi||e("RestConverterType",gi={})),e("RestSourceType",fi),function(e){e.REQUEST="request",e.CONST="constant"}(fi||e("RestSourceType",fi={})),e("ResponseType",yi),function(e){e.DEFAULT="default",e.CONST="constant",e.ADVANCED="advanced"}(yi||e("ResponseType",yi={})),e("ResponseStatus",vi),function(e){e.OK="OK",e.ERROR="Error"}(vi||e("ResponseStatus",vi={}));const xi=e("ResponseTypeTranslationsMap",new Map([[yi.DEFAULT,"gateway.rest.response-type.default"],[yi.CONST,"gateway.rest.response-type.const"],[yi.ADVANCED,"gateway.rest.response-type.advanced"]]));var bi;e("RestRequestType",bi),function(e){e.ATTRIBUTE_REQUEST="attributeRequests",e.ATTRIBUTE_UPDATE="attributeUpdates",e.SERVER_SIDE_RPC="serverSideRpc"}(bi||e("RestRequestType",bi={}));const wi=e("RestRequestTypesTranslationsMap",new Map([[bi.ATTRIBUTE_REQUEST,"gateway.request.attribute-request"],[bi.ATTRIBUTE_UPDATE,"gateway.request.attribute-update"],[bi.SERVER_SIDE_RPC,"gateway.request.rpc-connection"]]));var Si;e("RestRequestsScopeType",Si),function(e){e.Shared="shared",e.Client="client"}(Si||e("RestRequestsScopeType",Si={}));const Ci=e("RestRequestTypeFieldsMap",new Map([[bi.ATTRIBUTE_REQUEST,["HTTPMethods","endpoint","type","deviceNameExpression","attributeNameExpression"]],[bi.ATTRIBUTE_UPDATE,["HTTPMethod","SSLVerify","deviceNameFilter","attributeFilter","requestUrlExpression","valueExpression","httpHeaders","tries","allowRedirects"]],[bi.SERVER_SIDE_RPC,["HTTPMethod","deviceNameFilter","methodFilter","requestUrlExpression","valueExpression","responseTimeout","httpHeaders","tries"]],["all",["requestType","timeout","security"]]])),_i=e("RestConvertorTypeTranslationsMap",new Map([[gi.JSON,"gateway.JSON"],[gi.CUSTOM,"gateway.custom"]])),Ti=e("RestDataConversionTranslationsMap",new Map([[gi.JSON,"gateway.hints.rest.JSON"],[gi.CUSTOM,"gateway.custom-hint"]]));var Ii;e("PortLimits",Ii),function(e){e[e.MIN=1]="MIN",e[e.MAX=65535]="MAX"}(Ii||e("PortLimits",Ii={}));const Ei=e("GatewayConnectorConfigVersionMap",new Map([[ct.REST,pt.v3_7_2],[ct.KNX,pt.v3_7_0],[ct.BACNET,pt.v3_6_2],[ct.SOCKET,pt.v3_6_0],[ct.MQTT,pt.v3_5_2],[ct.OPCUA,pt.v3_5_2],[ct.MODBUS,pt.v3_5_2]]));var Mi,ki,Pi,Di;e("OPCUaSourceType",Mi),function(e){e.PATH="path",e.IDENTIFIER="identifier",e.CONST="constant"}(Mi||e("OPCUaSourceType",Mi={})),e("SecurityType",ki),function(e){e.ANONYMOUS="anonymous",e.BASIC="basic",e.CERTIFICATES="certificates"}(ki||e("SecurityType",ki={})),e("ModeType",Pi),function(e){e.NONE="None",e.SIGN="Sign",e.SIGNANDENCRYPT="SignAndEncrypt"}(Pi||e("ModeType",Pi={})),e("MappingType",Di),function(e){e.DATA="data",e.REQUESTS="requests",e.OPCUA="OPCua"}(Di||e("MappingType",Di={}));const Oi=e("MappingTypeTranslationsMap",new Map([[Di.DATA,"gateway.data-mapping"],[Di.REQUESTS,"gateway.requests-mapping"],[Di.OPCUA,"gateway.data-mapping"]]));var Ai;e("SecurityPolicy",Ai),function(e){e.BASIC128="Basic128Rsa15",e.BASIC256="Basic256",e.BASIC256SHA="Basic256Sha256"}(Ai||e("SecurityPolicy",Ai={}));const Fi=e("SecurityPolicyTypes",[{value:Ai.BASIC128,name:"Basic128RSA15"},{value:Ai.BASIC256,name:"Basic256"},{value:Ai.BASIC256SHA,name:"Basic256SHA256"}]),Ri=e("SecurityTypeTranslationsMap",new Map([[ki.ANONYMOUS,"gateway.broker.security-types.anonymous"],[ki.BASIC,"gateway.broker.security-types.basic"],[ki.CERTIFICATES,"gateway.broker.security-types.certificates"]])),Bi=e("SourceTypeTranslationsMap",new Map([[ei.MSG,"gateway.source-type.msg"],[ei.TOPIC,"gateway.source-type.topic"],[ei.CONST,"gateway.source-type.const"],[Mi.PATH,"gateway.source-type.path"],[Mi.IDENTIFIER,"gateway.source-type.identifier"],[Mi.CONST,"gateway.source-type.const"],[di.Expression,"gateway.source-type.expression"],[fi.REQUEST,"gateway.source-type.request"]]));var Ni;e("MappingKeysType",Ni),function(e){e.ATTRIBUTES="attributes",e.TIMESERIES="timeseries",e.CUSTOM="extensionConfig",e.RPC_METHODS="rpc_methods",e.ATTRIBUTES_UPDATES="attributes_updates"}(Ni||e("MappingKeysType",Ni={}));const Li=e("MappingKeysPanelTitleTranslationsMap",new Map([[Ni.ATTRIBUTES,"gateway.attributes"],[Ni.TIMESERIES,"gateway.timeseries"],[Ni.CUSTOM,"gateway.keys"],[Ni.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[Ni.RPC_METHODS,"gateway.rpc-methods"]])),Vi=e("MappingKeysAddKeyTranslationsMap",new Map([[Ni.ATTRIBUTES,"gateway.add-attribute"],[Ni.TIMESERIES,"gateway.add-timeseries"],[Ni.CUSTOM,"gateway.add-key"],[Ni.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[Ni.RPC_METHODS,"gateway.add-rpc-method"]])),qi=e("MappingKeysDeleteKeyTranslationsMap",new Map([[Ni.ATTRIBUTES,"gateway.delete-attribute"],[Ni.TIMESERIES,"gateway.delete-timeseries"],[Ni.CUSTOM,"gateway.delete-key"],[Ni.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[Ni.RPC_METHODS,"gateway.delete-rpc-method"]])),Gi=e("MappingKeysNoKeysTextTranslationsMap",new Map([[Ni.ATTRIBUTES,"gateway.no-attributes"],[Ni.TIMESERIES,"gateway.no-timeseries"],[Ni.CUSTOM,"gateway.no-keys"],[Ni.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[Ni.RPC_METHODS,"gateway.no-rpc-methods"]])),zi=e("QualityTypes",[0,1,2]);var Ui;e("ServerSideRpcType",Ui),function(e){e.WithResponse="twoWay",e.WithoutResponse="oneWay"}(Ui||e("ServerSideRpcType",Ui={}));const ji=e("HelpLinkByMappingTypeMap",new Map([[Di.DATA,O+"/docs/iot-gateway/config/mqtt/#section-mapping"],[Di.OPCUA,O+"/docs/iot-gateway/config/opc-ua/#section-mapping"],[Di.REQUESTS,O+"/docs/iot-gateway/config/mqtt/#requests-mapping"]])),Hi=e("MappingHintTranslationsMap",new Map([[Di.DATA,"gateway.data-mapping-hint"],[Di.OPCUA,"gateway.opcua-data-mapping-hint"],[Di.REQUESTS,"gateway.requests-mapping-hint"]]));var Wi,$i,Ki,Yi,Xi,Zi,Qi,Ji,ea;e("ServerSideRPCType",Wi),function(e){e.ONE_WAY="oneWay",e.TWO_WAY="twoWay"}(Wi||e("ServerSideRPCType",Wi={})),e("SecurityMode",$i),function(e){e.basic="basic",e.certificates="certificates",e.extendedCertificates="extendedCertificates"}($i||e("SecurityMode",$i={})),e("ModbusProtocolType",Ki),function(e){e.TCP="tcp",e.UDP="udp",e.Serial="serial"}(Ki||e("ModbusProtocolType",Ki={})),e("ModbusMethodType",Yi),function(e){e.SOCKET="socket",e.RTU="rtu"}(Yi||e("ModbusMethodType",Yi={})),e("ModbusSerialMethodType",Xi),function(e){e.RTU="rtu",e.ASCII="ascii"}(Xi||e("ModbusSerialMethodType",Xi={})),e("ModbusParity",Zi),function(e){e.Even="E",e.Odd="O",e.None="N"}(Zi||e("ModbusParity",Zi={})),e("ModbusOrderType",Qi),function(e){e.BIG="BIG",e.LITTLE="LITTLE"}(Qi||e("ModbusOrderType",Qi={})),e("ModbusRegisterType",Ji),function(e){e.HoldingRegisters="holding_registers",e.CoilsInitializer="coils_initializer",e.InputRegisters="input_registers",e.DiscreteInputs="discrete_inputs"}(Ji||e("ModbusRegisterType",Ji={})),e("ModbusValueKey",ea),function(e){e.ATTRIBUTES="attributes",e.TIMESERIES="timeseries",e.ATTRIBUTES_UPDATES="attributeUpdates",e.RPC_REQUESTS="rpc"}(ea||e("ModbusValueKey",ea={}));const ta=e("ModbusBaudrates",[4800,9600,19200,38400,57600,115200,230400,460800,921600]),na=e("ModbusByteSizes",[5,6,7,8]),ia=e("ModbusRegisterTranslationsMap",new Map([[Ji.HoldingRegisters,"gateway.holding_registers"],[Ji.CoilsInitializer,"gateway.coils_initializer"],[Ji.InputRegisters,"gateway.input_registers"],[Ji.DiscreteInputs,"gateway.discrete_inputs"]]));var aa;e("ModbusBitTargetType",aa),function(e){e.BooleanType="bool",e.IntegerType="int"}(aa||e("ModbusBitTargetType",aa={}));const ra=e("ModbusBitTargetTypeTranslationMap",new Map([[aa.BooleanType,"gateway.boolean"],[aa.IntegerType,"gateway.integer"]])),oa=e("ModbusMethodLabelsMap",new Map([[Yi.SOCKET,"Socket"],[Yi.RTU,"RTU"],[Xi.ASCII,"ASCII"]])),sa=e("ModbusProtocolLabelsMap",new Map([[Ki.TCP,"TCP"],[Ki.UDP,"UDP"],[Ki.Serial,"Serial"]])),la=e("ModbusParityLabelsMap",new Map([[Zi.Even,"Even"],[Zi.Odd,"Odd"],[Zi.None,"None"]])),pa=e("ModbusKeysPanelTitleTranslationsMap",new Map([[ea.ATTRIBUTES,"gateway.attributes"],[ea.TIMESERIES,"gateway.timeseries"],[ea.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[ea.RPC_REQUESTS,"gateway.rpc-requests"]])),ca=e("ModbusKeysAddKeyTranslationsMap",new Map([[ea.ATTRIBUTES,"gateway.add-attribute"],[ea.TIMESERIES,"gateway.add-timeseries"],[ea.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[ea.RPC_REQUESTS,"gateway.add-rpc-request"]])),da=e("ModbusKeysDeleteKeyTranslationsMap",new Map([[ea.ATTRIBUTES,"gateway.delete-attribute"],[ea.TIMESERIES,"gateway.delete-timeseries"],[ea.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[ea.RPC_REQUESTS,"gateway.delete-rpc-request"]])),ua=e("ModbusKeysNoKeysTextTranslationsMap",new Map([[ea.ATTRIBUTES,"gateway.no-attributes"],[ea.TIMESERIES,"gateway.no-timeseries"],[ea.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[ea.RPC_REQUESTS,"gateway.no-rpc-requests"]]));var ma;e("ModifierType",ma),function(e){e.DIVIDER="divider",e.MULTIPLIER="multiplier"}(ma||e("ModifierType",ma={}));const ha=e("ModifierTypesMap",new Map([[ma.DIVIDER,{name:"gateway.divider",icon:"mdi:division"}],[ma.MULTIPLIER,{name:"gateway.multiplier",icon:"mdi:multiplication"}]]));var ga,fa;e("DeviceInfoType",ga),function(e){e.FULL="full",e.PARTIAL="partial"}(ga||e("DeviceInfoType",ga={})),e("SegmentationType",fa),function(e){e.BOTH="segmentedBoth",e.TRANSMIT="segmentedTransmit",e.RECEIVE="segmentedReceive",e.NO="noSegmentation"}(fa||e("SegmentationType",fa={}));const ya=e("SegmentationTypeTranslationsMap",new Map([[fa.BOTH,"gateway.bacnet.segmentation.both"],[fa.TRANSMIT,"gateway.bacnet.segmentation.transmit"],[fa.RECEIVE,"gateway.bacnet.segmentation.receive"],[fa.NO,"gateway.bacnet.segmentation.no"]]));var va;e("BacnetDeviceKeysType",va),function(e){e.ATTRIBUTES="attributes",e.TIMESERIES="timeseries",e.RPC_METHODS="serverSideRpc",e.ATTRIBUTES_UPDATES="attributeUpdates"}(va||e("BacnetDeviceKeysType",va={}));const xa=e("BacnetDeviceKeysPanelTitleTranslationsMap",new Map([[va.ATTRIBUTES,"gateway.attributes"],[va.TIMESERIES,"gateway.timeseries"],[va.ATTRIBUTES_UPDATES,"gateway.attribute-updates"],[va.RPC_METHODS,"gateway.rpc-methods"]])),ba=e("BacnetDeviceKeysAddKeyTranslationsMap",new Map([[va.ATTRIBUTES,"gateway.add-attribute"],[va.TIMESERIES,"gateway.add-timeseries"],[va.ATTRIBUTES_UPDATES,"gateway.add-attribute-update"],[va.RPC_METHODS,"gateway.add-rpc-method"]])),wa=e("BacnetDeviceKeysDeleteKeyTranslationsMap",new Map([[va.ATTRIBUTES,"gateway.delete-attribute"],[va.TIMESERIES,"gateway.delete-timeseries"],[va.ATTRIBUTES_UPDATES,"gateway.delete-attribute-update"],[va.RPC_METHODS,"gateway.delete-rpc-method"]])),Sa=e("BacnetDeviceKeysNoKeysTextTranslationsMap",new Map([[va.ATTRIBUTES,"gateway.no-attributes"],[va.TIMESERIES,"gateway.no-timeseries"],[va.ATTRIBUTES_UPDATES,"gateway.no-attribute-updates"],[va.RPC_METHODS,"gateway.no-rpc-methods"]]));var Ca;e("BacnetKeyObjectType",Ca),function(e){e.analogInput="analogInput",e.analogOutput="analogOutput",e.analogValue="analogValue",e.binaryInput="binaryInput",e.binaryOutput="binaryOutput",e.binaryValue="binaryValue"}(Ca||e("BacnetKeyObjectType",Ca={}));const _a=e("BacnetKeyObjectTypeTranslationsMap",new Map([[Ca.analogInput,"gateway.bacnet.object-type.analog-input"],[Ca.analogOutput,"gateway.bacnet.object-type.analog-output"],[Ca.analogValue,"gateway.bacnet.object-type.analog-value"],[Ca.binaryInput,"gateway.bacnet.object-type.binary-input"],[Ca.binaryOutput,"gateway.bacnet.object-type.binary-output"],[Ca.binaryValue,"gateway.bacnet.object-type.binary-value"]]));var Ta;e("BacnetPropertyId",Ta),function(e){e.presentValue="presentValue",e.statusFlags="statusFlags",e.covIncrement="covIncrement",e.eventState="eventState",e.outOfService="outOfService",e.polarity="polarity",e.priorityArray="priorityArray",e.relinquishDefault="relinquishDefault",e.currentCommandPriority="currentCommandPriority",e.eventMessageTexts="eventMessageTexts",e.eventMessageTextsConfig="eventMessageTextsConfig",e.eventAlgorithmInhibitReference="eventAlgorithmInhibitReference",e.timeDelayNormal="timeDelayNormal"}(Ta||e("BacnetPropertyId",Ta={}));const Ia=e("BacnetPropertyIdByObjectType",new Map([[Ca.analogInput,[Ta.presentValue,Ta.statusFlags,Ta.covIncrement]],[Ca.analogOutput,[Ta.presentValue,Ta.statusFlags,Ta.covIncrement]],[Ca.analogValue,[Ta.presentValue,Ta.statusFlags,Ta.covIncrement]],[Ca.binaryInput,[Ta.presentValue,Ta.statusFlags,Ta.eventState,Ta.outOfService,Ta.polarity]],[Ca.binaryOutput,[Ta.presentValue,Ta.statusFlags,Ta.eventState,Ta.outOfService,Ta.polarity,Ta.priorityArray,Ta.relinquishDefault,Ta.currentCommandPriority,Ta.eventMessageTexts,Ta.eventMessageTextsConfig,Ta.eventAlgorithmInhibitReference,Ta.timeDelayNormal]],[Ca.binaryValue,[Ta.presentValue,Ta.statusFlags,Ta.eventState,Ta.outOfService]]])),Ea=e("BacnetPropertyIdTranslationsMap",new Map([[Ta.presentValue,"gateway.bacnet.property-id.present-value"],[Ta.statusFlags,"gateway.bacnet.property-id.status-flags"],[Ta.covIncrement,"gateway.bacnet.property-id.cov-increment"],[Ta.eventState,"gateway.bacnet.property-id.event-state"],[Ta.outOfService,"gateway.bacnet.property-id.out-of-service"],[Ta.polarity,"gateway.bacnet.property-id.polarity"],[Ta.priorityArray,"gateway.bacnet.property-id.priority-array"],[Ta.relinquishDefault,"gateway.bacnet.property-id.relinquish-default"],[Ta.currentCommandPriority,"gateway.bacnet.property-id.current-command-priority"],[Ta.eventMessageTexts,"gateway.bacnet.property-id.event-message-texts"],[Ta.eventMessageTextsConfig,"gateway.bacnet.property-id.event-message-texts-config"],[Ta.eventAlgorithmInhibitReference,"gateway.bacnet.property-id.event-algorithm-inhibit-reference"],[Ta.timeDelayNormal,"gateway.bacnet.property-id.time-delay-normal"]]));var Ma;e("BacnetRequestType",Ma),function(e){e.Write="writeProperty",e.Read="readProperty"}(Ma||e("BacnetRequestType",Ma={}));const ka=e("BacnetRequestTypeTranslationsMap",new Map([[Ma.Write,"gateway.bacnet.request-type.write"],[Ma.Read,"gateway.bacnet.request-type.read"]]));class Pa{static{this.mqttRequestTypeKeys=Object.values(ai)}static{this.mqttRequestMappingOldFields=["attributeNameJsonExpression","deviceNameJsonExpression","deviceNameTopicExpression","extension-config"]}static{this.mqttRequestMappingNewFields=["attributeNameExpressionSource","responseTopicQoS","extensionConfig"]}static mapMappingToUpgradedVersion(e){return e?.map((({converter:e,topicFilter:t,subscriptionQos:n=1})=>{const i=e.deviceInfo??this.extractConverterDeviceInfo(e),a={...e,deviceInfo:i,extensionConfig:e.extensionConfig||e["extension-config"]||null};return this.cleanUpOldFields(a),{converter:a,topicFilter:t,subscriptionQos:n}}))}static mapRequestsToUpgradedVersion(e){return this.mqttRequestTypeKeys.reduce(((t,n)=>e[n]?(t[n]=e[n].map((e=>{const t=this.mapRequestToUpgradedVersion(e,n);return this.cleanUpOldFields(t),t})),t):t),{})}static mapRequestsToDowngradedVersion(e){return this.mqttRequestTypeKeys.reduce(((t,n)=>e[n]?(t[n]=e[n].map((e=>{n===ai.SERVER_SIDE_RPC&&delete e.type;const{attributeNameExpression:t,deviceInfo:i,...a}=e,r={...a,attributeNameJsonExpression:t||null,deviceNameJsonExpression:i?.deviceNameExpressionSource!==ei.TOPIC?i?.deviceNameExpression:null,deviceNameTopicExpression:i?.deviceNameExpressionSource===ei.TOPIC?i?.deviceNameExpression:null};return this.cleanUpNewFields(r),r})),t):t),{})}static mapMappingToDowngradedVersion(e){return e?.map((e=>{const t=this.mapConverterToDowngradedVersion(e.converter);return this.cleanUpNewFields(t),{converter:t,topicFilter:e.topicFilter}}))}static mapConverterToDowngradedVersion(e){const{deviceInfo:t,...n}=e;return e.type!==Jn.BYTES?{...n,deviceNameJsonExpression:t?.deviceNameExpressionSource===ei.MSG?t.deviceNameExpression:null,deviceTypeJsonExpression:t?.deviceProfileExpressionSource===ei.MSG?t.deviceProfileExpression:null,deviceNameTopicExpression:t?.deviceNameExpressionSource!==ei.MSG?t?.deviceNameExpression:null,deviceTypeTopicExpression:t?.deviceProfileExpressionSource!==ei.MSG?t?.deviceProfileExpression:null}:{...n,deviceNameExpression:t.deviceNameExpression,deviceTypeExpression:t.deviceProfileExpression,"extension-config":e.extensionConfig}}static cleanUpOldFields(e){this.mqttRequestMappingOldFields.forEach((t=>delete e[t])),Te(e)}static cleanUpNewFields(e){this.mqttRequestMappingNewFields.forEach((t=>delete e[t])),Te(e)}static getTypeSourceByValue(e){return e.includes("${")?ei.MSG:e.includes("/")?ei.TOPIC:ei.CONST}static extractConverterDeviceInfo(e){const t=e.deviceNameExpression||e.deviceNameJsonExpression||e.deviceNameTopicExpression||null,n=e.deviceNameExpressionSource?e.deviceNameExpressionSource:t?this.getTypeSourceByValue(t):null,i=e.deviceProfileExpression||e.deviceTypeTopicExpression||e.deviceTypeJsonExpression||"default",a=e.deviceProfileExpressionSource?e.deviceProfileExpressionSource:i?this.getTypeSourceByValue(i):null;return t||i?{deviceNameExpression:t,deviceNameExpressionSource:n,deviceProfileExpression:i,deviceProfileExpressionSource:a}:null}static mapRequestToUpgradedVersion(e,t){const n=e.deviceNameJsonExpression||e.deviceNameTopicExpression||null,i=e.deviceTypeTopicExpression||e.deviceTypeJsonExpression||"default",a=i?this.getTypeSourceByValue(i):null,r=e.attributeNameExpressionSource||e.attributeNameJsonExpression||null,o=t===ai.SERVER_SIDE_RPC?1:null,s=t===ai.SERVER_SIDE_RPC?e.responseTopicExpression?Ui.WithResponse:Ui.WithoutResponse:null;return{...e,attributeNameExpression:r,attributeNameExpressionSource:r?this.getTypeSourceByValue(r):null,deviceInfo:e.deviceInfo?e.deviceInfo:n?{deviceNameExpression:n,deviceNameExpressionSource:this.getTypeSourceByValue(n),deviceProfileExpression:i,deviceProfileExpressionSource:a}:null,responseTopicQoS:o,type:s}}}e("MqttVersionMappingUtil",Pa);class Da{constructor(e,t){this.gatewayVersionIn=e,this.connector=t,this.gatewayVersion=za.parseVersion(this.gatewayVersionIn),this.configVersion=za.parseVersion(this.connector.configVersion??this.connector.configurationJson.configVersion)}getProcessedByVersion(){return this.isVersionUpdateNeeded()?this.processVersionUpdate():this.connector}processVersionUpdate(){return this.isVersionUpgradeNeeded()?this.getUpgradedVersion():this.isVersionDowngradeNeeded()?this.getDowngradedVersion():this.connector}isVersionUpdateNeeded(){return!!this.gatewayVersion&&this.configVersion!==this.gatewayVersion}isVersionUpgradeNeeded(){const e=za.parseVersion(Ei.get(this.connector.type)),t=this.gatewayVersion>=e,n=!this.configVersion||this.configVersion=e&&e>this.gatewayVersion}}e("GatewayConnectorVersionProcessor",Da);class Oa extends Da{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t,this.mqttRequestTypeKeys=Object.values(ai)}getUpgradedVersion(){const{connectRequests:e,disconnectRequests:t,attributeRequests:n,attributeUpdates:i,serverSideRpc:a}=this.connector.configurationJson;let r={...this.connector.configurationJson,requestsMapping:Pa.mapRequestsToUpgradedVersion({connectRequests:e,disconnectRequests:t,attributeRequests:n,attributeUpdates:i,serverSideRpc:a}),mapping:Pa.mapMappingToUpgradedVersion(this.connector.configurationJson.mapping)};return this.mqttRequestTypeKeys.forEach((e=>{const{[e]:t,...n}=r;r={...n}})),this.cleanUpConfigJson(r),{...this.connector,configurationJson:r,configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const{requestsMapping:e,mapping:t,...n}=this.connector.configurationJson,i=e?Pa.mapRequestsToDowngradedVersion(e):{},a=Pa.mapMappingToDowngradedVersion(t);return{...this.connector,configurationJson:{...n,...i,mapping:a},configVersion:this.gatewayVersionIn}}cleanUpConfigJson(e){Se(e.requestsMapping,{})&&delete e.requestsMapping,Se(e.mapping,[])&&delete e.mapping}}e("MqttVersionProcessor",Oa);class Aa extends Da{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{master:e.master?.slaves?Ua.mapMasterToUpgradedVersion(e.master):{slaves:[]},slave:e.slave?Ua.mapSlaveToUpgradedVersion(e.slave):{}},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{...e,slave:e.slave?Ua.mapSlaveToDowngradedVersion(e.slave):{},master:e.master?.slaves?Ua.mapMasterToDowngradedVersion(e.master):{slaves:[]}},configVersion:this.gatewayVersionIn}}}e("ModbusVersionProcessor",Aa);class Fa extends Da{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson.server;return{...this.connector,configurationJson:{server:e?ja.mapServerToUpgradedVersion(e):{},mapping:e?.mapping?ja.mapMappingToUpgradedVersion(e.mapping):[]},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){return{...this.connector,configurationJson:{server:ja.mapServerToDowngradedVersion(this.connector.configurationJson)},configVersion:this.gatewayVersionIn}}}e("OpcVersionProcessor",Fa);class Ra{constructor(){this.fb=i(Y),this.destroyRef=i(a),this.formGroup=this.initFormGroup(),this.observeValueChanges()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.formGroup.valid?null:{formGroup:{valid:!1}}}writeValue(e){this.onWriteValue(e)}onWriteValue(e){this.formGroup.patchValue(e,{emitEvent:!1})}mapOnChangeValue(e){return e}observeValueChanges(){this.formGroup.valueChanges.pipe(bn(this.destroyRef)).subscribe((e=>this.onChange(this.mapOnChangeValue(e))))}static{this.ɵfac=function(e){return new(e||Ra)}}static{this.ɵdir=t.ɵɵdefineDirective({type:Ra})}}e("ControlValueAccessorBaseAbstract",Ra);class Ba extends Ra{constructor(){super(...arguments),this.withReportStrategy=!0,this.initialized=new u,this.isLegacy=!1,this.fb=i(Y)}get basicFormGroup(){return this.formGroup}ngAfterViewInit(){this.initialized.emit()}onWriteValue(e){this.formGroup.setValue(this.mapConfigToFormValue(e),{emitEvent:!1})}mapOnChangeValue(e){return this.getMappedValue(e)}initFormGroup(){return this.initBasicFormGroup()}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(Ba)))(n||Ba)}})()}static{this.ɵdir=t.ɵɵdefineDirective({type:Ba,inputs:{generalTabContent:"generalTabContent",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},outputs:{initialized:"initialized"},features:[t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature]})}}e("GatewayConnectorBasicConfigDirective",Ba);class Na extends Da{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{socket:e?Ha.mapSocketToUpgradedVersion(e):{},devices:e?.devices?Ha.mapDevicesToUpgradedVersion(e.devices):[]},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){return{...this.connector,configurationJson:Ha.mapSocketToDowngradedVersion(this.connector.configurationJson),configVersion:this.gatewayVersionIn}}}e("SocketVersionProcessor",Na);class La extends Da{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t}getUpgradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{application:e?.general?Wa.mapApplicationToUpgradedVersion(e.general):{},devices:e?.devices?Wa.mapDevicesToUpgradedVersion(e.devices):[]},configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const e=this.connector.configurationJson;return{...this.connector,configurationJson:{general:e?.application?Wa.mapApplicationToDowngradedVersion(e.application):{},devices:e?.devices?Wa.mapDevicesToDowngradedVersion(e.devices):[]},configVersion:this.gatewayVersionIn}}}e("BacnetVersionProcessor",La);const Va=["searchInput"];class qa{constructor(){this.withReportStrategy=!0,this.textSearchMode=!1,this.onChange=()=>{},this.translate=i(We),this.dialog=i(Le),this.dialogService=i(Ie),this.fb=i(Y),this.cd=i(h),this.destroyRef=i(a),this.textSearch=this.fb.control("",{nonNullable:!0}),this.entityFormArray=this.fb.array([]),this.entityFormArray.valueChanges.pipe(bn()).subscribe((e=>{this.updateTableData(e),this.onChange(e)})),this.dataSource=this.getDatasource()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(de(150),ce(((e,t)=>(e??"")===t.trim())),bn(this.destroyRef)).subscribe((e=>this.updateTableData(this.entityFormArray.value,e.trim())))}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){this.entityFormArray.clear(),this.pushDataAsFormArrays(e)}enterFilterMode(){this.textSearchMode=!0,this.cd.detectChanges();const e=this.searchInputField.nativeElement;e.focus(),e.setSelectionRange(0,0)}exitFilterMode(){this.updateTableData(this.entityFormArray.value),this.textSearchMode=!1,this.textSearch.reset()}validate(){return this.entityFormArray.controls.length?null:{devicesFormGroup:{valid:!1}}}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.entityFormArray.push(this.fb.control(e))))}static{this.ɵfac=function(e){return new(e||qa)}}static{this.ɵdir=t.ɵɵdefineDirective({type:qa,viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(Va,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},features:[t.ɵɵInputTransformsFeature]})}}e("AbstractDevicesConfigTableComponent",qa);class Ga extends Da{constructor(e,t){super(e,t),this.gatewayVersionIn=e,this.connector=t,this.restRequestTypeKeys=Object.values(bi)}getUpgradedVersion(){const{attributeRequests:e=[],attributeUpdates:t=[],serverSideRpc:n=[],mapping:i=[],...a}=this.connector.configurationJson;let r={server:za.cleanUpConfigBaseInfo(a),requestsMapping:{attributeRequests:e,attributeUpdates:t,serverSideRpc:n},mapping:$a.mapMappingToUpgradedVersion(i)};return this.restRequestTypeKeys.forEach((e=>{const{[e]:t,...n}=r;r={...n}})),{...this.connector,configurationJson:r,configVersion:this.gatewayVersionIn}}getDowngradedVersion(){const{requestsMapping:e={},mapping:t,server:n,...i}=this.connector.configurationJson,{attributeRequests:a=[],attributeUpdates:r=[],serverSideRpc:o=[]}=e;return{...this.connector,configurationJson:{...i,...n,attributeRequests:a,attributeUpdates:r,serverSideRpc:o,mapping:$a.mapMappingToDowngradedVersion(t)},configVersion:this.gatewayVersionIn}}}e("RestVersionProcessor",Ga);class za{static getConfig(e,t){switch(e.type){case ct.MQTT:return new Oa(t,e).getProcessedByVersion();case ct.OPCUA:return new Fa(t,e).getProcessedByVersion();case ct.MODBUS:return new Aa(t,e).getProcessedByVersion();case ct.SOCKET:return new Na(t,e).getProcessedByVersion();case ct.BACNET:return new La(t,e).getProcessedByVersion();case ct.REST:return new Ga(t,e).getProcessedByVersion();default:return e}}static parseVersion(e){if(Ee(e))return e;if(Me(e)){const[t,n="0",i="0"]=e.split(".");return parseFloat(`${t}.${n}${i.slice(0,1)}`)}return 0}static cleanUpConfigBaseInfo(e){const{name:t,id:n,enableRemoteLogging:i,logLevel:a,reportStrategy:r,configVersion:o,...s}=e;return s}}e("GatewayConnectorVersionMappingUtil",za);class Ua{static mapMasterToUpgradedVersion(e){return{slaves:e.slaves.map((e=>{const{sendDataOnlyOnChange:t,...n}=e;return{...n,deviceType:e.deviceType??"default",reportStrategy:t?{type:Jt.OnChange}:{type:Jt.OnReportPeriod,reportPeriod:e.pollPeriod}}}))}}static mapMasterToDowngradedVersion(e){return{slaves:e.slaves.map((e=>{const{reportStrategy:t,...n}=e;return{...n,sendDataOnlyOnChange:t?.type!==Jt.OnReportPeriod}}))}}static mapSlaveToDowngradedVersion(e){if(!e?.values)return e;const t=Object.keys(e.values).reduce(((t,n)=>t={...t,[n]:[e.values[n]]}),{});return{...e,values:t}}static mapSlaveToUpgradedVersion(e){if(!e?.values)return e;const t=Object.keys(e.values).reduce(((t,n)=>t={...t,[n]:this.mapValuesToUpgradedVersion(e.values[n][0]??{})}),{});return{...e,values:t}}static mapValuesToUpgradedVersion(e){return Object.keys(e).reduce(((t,n)=>t={...t,[n]:e[n].map((e=>({...e,type:"int"===e.type?Wt.INT16:e.type})))}),{})}}e("ModbusVersionMappingUtil",Ua);class ja{static mapServerToUpgradedVersion(e){const{mapping:t,disableSubscriptions:n,pollPeriodInMillis:i,...a}=e;return{...a,pollPeriodInMillis:i??5e3,enableSubscriptions:!n}}static mapServerToDowngradedVersion(e){const{mapping:t,server:n}=e,{enableSubscriptions:i,...a}=n??{};return{...a,mapping:t?this.mapMappingToDowngradedVersion(t):[],disableSubscriptions:!i}}static mapMappingToUpgradedVersion(e){return e.map((e=>({deviceNodePattern:e.deviceNodePattern,deviceNodeSource:this.getDeviceNodeSourceByValue(e.deviceNodePattern),deviceInfo:{deviceNameExpression:e.deviceNamePattern,deviceNameExpressionSource:this.getTypeSourceByValue(e.deviceNamePattern),deviceProfileExpression:e.deviceTypePattern??"default",deviceProfileExpressionSource:this.getTypeSourceByValue(e.deviceTypePattern??"default")},attributes:e.attributes?.map((e=>({key:e.key,type:this.getTypeSourceByValue(e.path),value:e.path})))??[],attributes_updates:e.attributes_updates?.map((e=>({key:e.attributeOnThingsBoard,type:this.getTypeSourceByValue(e.attributeOnDevice),value:e.attributeOnDevice})))??[],timeseries:e.timeseries?.map((e=>({key:e.key,type:this.getTypeSourceByValue(e.path),value:e.path})))??[],rpc_methods:e.rpc_methods?.map((e=>({method:e.method,arguments:e.arguments.map((e=>({value:e,type:this.getArgumentType(e)})))})))??[]})))}static mapMappingToDowngradedVersion(e){return e.map((e=>({deviceNodePattern:e.deviceNodePattern,deviceNamePattern:e.deviceInfo.deviceNameExpression,deviceTypePattern:e.deviceInfo.deviceProfileExpression,attributes:e.attributes.map((e=>({key:e.key,path:e.value}))),attributes_updates:e.attributes_updates.map((e=>({attributeOnThingsBoard:e.key,attributeOnDevice:e.value}))),timeseries:e.timeseries.map((e=>({key:e.key,path:e.value}))),rpc_methods:e.rpc_methods.map((e=>({method:e.method,arguments:e.arguments.map((e=>e.value))})))})))}static getTypeSourceByValue(e){return e.includes("${")?Mi.IDENTIFIER:e.includes("/")||e.includes("\\")?Mi.PATH:Mi.CONST}static getDeviceNodeSourceByValue(e){return e.includes("${")?Mi.IDENTIFIER:Mi.PATH}static getArgumentType(e){switch(typeof e){case"boolean":return"boolean";case"number":return Number.isInteger(e)?"integer":"float";default:return"string"}}}e("OpcVersionMappingUtil",ja);class Ha{static mapSocketToUpgradedVersion(e){const{devices:t,...n}=e??{};return za.cleanUpConfigBaseInfo(n)}static mapSocketToDowngradedVersion(e){const{devices:t,socket:n}=e??{};return{...n,devices:this.mapDevicesToDowngradedVersion(t??[])}}static mapDevicesToUpgradedVersion(e){return e?.map((e=>({...e,attributeRequests:e.attributeRequests?.map((e=>({...e,requestExpressionSource:this.getExpressionSource(e.requestExpression),attributeNameExpressionSource:this.getExpressionSource(e.attributeNameExpression)})))??[]})))??[]}static mapDevicesToDowngradedVersion(e){return e.map((e=>({...e,attributeRequests:e.attributeRequests?.map((({requestExpressionSource:e,attributeNameExpressionSource:t,...n})=>n))??[]})))}static getExpressionSource(e){return e.includes("${")||e.includes("[")?di.Expression:di.Constant}}e("SocketVersionMappingUtil",Ha);class Wa{static mapApplicationToUpgradedVersion(e){const{address:t="",...n}=e,[i,a]=t.split(":"),[r,o]=i.split("/");return{host:r,port:a,mask:o,...n}}static mapApplicationToDowngradedVersion(e){const{host:t="",port:n="",mask:i="",...a}=e;return{address:i?`${t}/${i}:${n}`:`${t}:${n}`,...a}}static mapDevicesToUpgradedVersion(e){return e?.map((({address:e="",deviceName:t,deviceType:n,attributes:i,timeseries:a,attributeUpdates:r,serverSideRpc:o,...s})=>({...s,host:e.split(":")[0],port:e.split(":")[1],deviceInfo:{deviceNameExpression:t,deviceProfileExpression:n,deviceNameExpressionSource:this.getExpressionSource(t),deviceProfileExpressionSource:this.getExpressionSource(n)},attributes:this.getUpdateKeys(i),timeseries:this.getUpdateKeys(a),attributeUpdates:this.getUpdateKeys(r),serverSideRpc:this.getUpdateKeys(o)})))??[]}static mapDevicesToDowngradedVersion(e){return e?.map((({port:e,host:t,deviceInfo:n,attributes:i,timeseries:a,attributeUpdates:r,serverSideRpc:o,...s})=>({...s,address:`${t}:${e}`,deviceName:n?.deviceNameExpression,deviceType:n?.deviceProfileExpression,attributes:this.getDowngradedKeys(i),timeseries:this.getDowngradedKeys(a),attributeUpdates:this.getDowngradedKeys(r),serverSideRpc:this.getDowngradedKeys(o)})))??[]}static getExpressionSource(e){return e.includes("${")||e.includes("[")?di.Expression:di.Constant}static getUpdateKeys(e){return e?.map((({objectId:e="",...t})=>({objectType:e.split(":")[0],objectId:e.split(":")[1],...t})))??[]}static getDowngradedKeys(e){return e?.map((({objectId:e="",objectType:t="",...n})=>({objectId:`${t}:${e}`,...n})))??[]}}e("BacnetVersionMappingUtil",Wa);class $a{static mapMappingToUpgradedVersion(e){return e?.map((({converter:e,...t})=>{const{deviceNameExpression:n,deviceTypeExpression:i,...a}=e;return{converter:{deviceInfo:{deviceNameExpressionSource:this.getTypeSourceByValue(n),deviceNameExpression:n,deviceProfileExpressionSource:this.getTypeSourceByValue(i),deviceProfileExpression:i},...a},...t}}))}static mapMappingToDowngradedVersion(e){return e?.map((({converter:e,...t})=>{const{deviceInfo:n,...i}=e;return{converter:{deviceNameExpression:n?.deviceNameExpression,deviceTypeExpression:n?.deviceProfileExpression,...i},...t}}))}static getTypeSourceByValue(e=""){return e.includes("${")?fi.REQUEST:fi.CONST}}e("RestVersionMappingUtil",$a);class Ka{transform(e,t){const n=za.parseVersion(e);return t===ct.MODBUS?n>=za.parseVersion(pt.v3_5_2):n>=za.parseVersion(pt.v3_6_0)}static{this.ɵfac=function(e){return new(e||Ka)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"withReportStrategy",type:Ka,pure:!0,standalone:!0})}}function Ya(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-required")," "))}function Xa(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-pattern")," "))}function Za(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.name-already-exists")," "))}function Qa(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-required")," "))}function Ja(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-pattern")," "))}function er(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-min")," "))}function tr(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.command-required")," "))}function nr(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.command-pattern")," "))}e("ReportStrategyVersionPipe",Ka);class ir extends A{constructor(e,t,n,i,a,r,o,s){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.dialogService=r,this.translate=o,this.destroyRef=s,this.commandHelpLink=O+"/docs/iot-gateway/configuration/#subsection-statistics",this.editCommandFormGroup=this.fb.group({attributeOnGateway:["",[$.required,$.pattern(an),this.uniqNameRequired()]],command:["",[$.required,$.pattern(/^(?=\S).*\S$/)]],timeout:[10,[$.required,$.min(1),$.pattern(nn),$.pattern(/^[^.\s]+$/)]],installCmd:["",$.pattern(an)]}),this.editCommandFormGroup.patchValue(this.data.command,{emitEvent:!1})}cancel(){this.confirmConnectorChange().pipe(ve(Boolean),bn(this.destroyRef)).subscribe((()=>this.dialogRef.close(null)))}apply(){this.dialogRef.close({current:this.editCommandFormGroup.value,prev:this.data.command})}confirmConnectorChange(){return this.editCommandFormGroup.dirty?this.dialogService.confirm(this.translate.instant("gateway.statistics.change-command-title"),this.translate.instant("gateway.statistics.change-command-text"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0):ae(!0)}uniqNameRequired(){return e=>{const t=e.value?.trim().toLowerCase(),n=t&&this.data.existingCommands.some((e=>e.toLowerCase()===t))&&t!==this.data.command.attributeOnGateway.toLowerCase();return n?{duplicateName:{valid:!1}}:null}}static{this.ɵfac=function(e){return new(e||ir)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(_e.DialogService),t.ɵɵdirectiveInject(He.TranslateService),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:ir,selectors:[["tb-edit-custom-command-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:64,vars:27,consts:[[1,"edit-command-container",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel","stroked","no-padding-bottom","no-gap","command-container"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["translate",""],["matInput","","formControlName","attributeOnGateway"],["matIconSuffix","",1,"cursor-pointer",3,"matTooltip"],["matInput","","formControlName","timeout","type","number","min","1"],["appearance","outline",1,"mat-block"],["matInput","","formControlName","command"],[1,"tb-settings","pb-4"],["translate","",1,"tb-form-panel-title"],["matInput","","formControlName","installCmd"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"mat-toolbar",1)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",2)(6,"div",3),t.ɵɵelementStart(7,"button",4),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",5),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",6)(11,"div",7)(12,"section",8)(13,"section",9)(14,"mat-form-field",10)(15,"mat-label",11),t.ɵɵtext(16,"gateway.statistics.name"),t.ɵɵelementEnd(),t.ɵɵelement(17,"input",12),t.ɵɵtemplate(18,Ya,3,3,"mat-error")(19,Xa,3,3,"mat-error")(20,Za,3,3,"mat-error"),t.ɵɵelementStart(21,"mat-icon",13),t.ɵɵpipe(22,"translate"),t.ɵɵtext(23,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(24,"mat-form-field",10)(25,"mat-label",11),t.ɵɵtext(26,"gateway.statistics.timeout"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",14),t.ɵɵtemplate(28,Qa,3,3,"mat-error")(29,Ja,3,3,"mat-error")(30,er,3,3,"mat-error"),t.ɵɵelementStart(31,"mat-icon",13),t.ɵɵpipe(32,"translate"),t.ɵɵtext(33,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(34,"section")(35,"mat-form-field",15)(36,"mat-label",11),t.ɵɵtext(37,"gateway.statistics.command"),t.ɵɵelementEnd(),t.ɵɵelement(38,"input",16),t.ɵɵtemplate(39,tr,3,3,"mat-error")(40,nr,3,3,"mat-error"),t.ɵɵelementStart(41,"mat-icon",13),t.ɵɵpipe(42,"translate"),t.ɵɵtext(43,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(44,"section")(45,"mat-expansion-panel",17)(46,"mat-expansion-panel-header")(47,"mat-panel-title")(48,"div",18),t.ɵɵtext(49,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(50,"mat-form-field",10)(51,"mat-label",11),t.ɵɵtext(52,"gateway.statistics.install-cmd"),t.ɵɵelementEnd(),t.ɵɵelement(53,"input",19),t.ɵɵelementStart(54,"mat-icon",13),t.ɵɵpipe(55,"translate"),t.ɵɵtext(56,"info_outlined "),t.ɵɵelementEnd()()()()()()(),t.ɵɵelementStart(57,"div",20)(58,"button",21),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(59),t.ɵɵpipe(60,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(61,"button",22),t.ɵɵlistener("click",(function(){return n.apply()})),t.ɵɵtext(62),t.ɵɵpipe(63,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.editCommandFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,13,n.data.titleText)),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.commandHelpLink),t.ɵɵadvance(12),t.ɵɵconditional(n.editCommandFormGroup.get("attributeOnGateway").hasError("required")?18:n.editCommandFormGroup.get("attributeOnGateway").hasError("pattern")?19:n.editCommandFormGroup.get("attributeOnGateway").hasError("duplicateName")?20:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(22,15,"gateway.hints.attribute")),t.ɵɵadvance(7),t.ɵɵconditional(n.editCommandFormGroup.get("timeout").hasError("required")?28:n.editCommandFormGroup.get("timeout").hasError("pattern")?29:n.editCommandFormGroup.get("timeout").hasError("min")?30:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(32,17,"gateway.hints.timeout")),t.ɵɵadvance(8),t.ɵɵconditional(n.editCommandFormGroup.get("command").hasError("required")?39:n.editCommandFormGroup.get("command").hasError("pattern")?40:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(42,19,"gateway.hints.command")),t.ɵɵadvance(13),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(55,21,"gateway.hints.install-cmd")),t.ɵɵadvance(5),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(60,23,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.editCommandFormGroup.invalid||!n.editCommandFormGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(63,25,n.data.buttonText)," "))},dependencies:t.ɵɵgetComponentDepsFactory(ir,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .edit-command-container[_ngcontent-%COMP%]{min-width:40vw;width:50vw}[_nghost-%COMP%] .pointer-event{pointer-events:all}[_nghost-%COMP%] .toggle-group span{padding:0 25px}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{color:#e0e0e0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix:hover{color:#9e9e9e}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex;align-items:center}']})}}var ar=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},rr=new function(){this.browser=new ar,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(rr.wxa=!0,rr.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?rr.worker=!0:"undefined"==typeof navigator||0===navigator.userAgent.indexOf("Node.js")?(rr.node=!0,rr.svgSupported=!0):function(e,t){var n=t.browser,i=e.match(/Firefox\/([\d.]+)/),a=e.match(/MSIE\s([\d.]+)/)||e.match(/Trident\/.+?rv:(([\d.]+))/),r=e.match(/Edge?\/([\d.]+)/),o=/micromessenger/i.test(e);i&&(n.firefox=!0,n.version=i[1]);a&&(n.ie=!0,n.version=a[1]);r&&(n.edge=!0,n.version=r[1],n.newEdge=+r[1].split(".")[0]>18);o&&(n.weChat=!0);t.svgSupported="undefined"!=typeof SVGRect,t.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,t.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),t.domSupported="undefined"!=typeof document;var s=document.documentElement.style;t.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),t.transformSupported=t.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,rr);var or="sans-serif",sr="12px "+or;var lr,pr,cr=function(e){var t={};if("undefined"==typeof JSON)return t;for(var n=0;n=0)r=a*e.length;else for(var o=0;o>1)%2;o.style.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",i[s]+":0",a[l]+":0",i[1-s]+":auto",a[1-l]+":auto",""].join("!important;"),e.appendChild(o),n.push(o)}return n}(t,r),s=function(e,t,n){for(var i=n?"invTrans":"trans",a=t[i],r=t.srcCoords,o=[],s=[],l=!0,p=0;p<4;p++){var c=e[p].getBoundingClientRect(),d=2*p,u=c.left,m=c.top;o.push(u,m),l=l&&r&&u===r[d]&&m===r[d+1],s.push(e[p].offsetLeft,e[p].offsetTop)}return l&&a?a:(t.srcCoords=o,t[i]=n?Wo(s,o):Wo(o,s))}(o,r,a);if(s)return s(e,n,i),!0}return!1}function Xo(e){return"CANVAS"===e.nodeName.toUpperCase()}var Zo=/([&<>"'])/g,Qo={"&":"&","<":"<",">":">",'"':""","'":"'"};function Jo(e){return null==e?"":(e+"").replace(Zo,(function(e,t){return Qo[t]}))}var es=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ts=[],ns=rr.browser.firefox&&+rr.browser.version.split(".")[0]<39;function is(e,t,n,i){return n=n||{},i?as(e,t,n):ns&&null!=t.layerX&&t.layerX!==t.offsetX?(n.zrX=t.layerX,n.zrY=t.layerY):null!=t.offsetX?(n.zrX=t.offsetX,n.zrY=t.offsetY):as(e,t,n),n}function as(e,t,n){if(rr.domSupported&&e.getBoundingClientRect){var i=t.clientX,a=t.clientY;if(Xo(e)){var r=e.getBoundingClientRect();return n.zrX=i-r.left,void(n.zrY=a-r.top)}if(Yo(ts,e,i,a))return n.zrX=ts[0],void(n.zrY=ts[1])}n.zrX=n.zrY=0}function rs(e){return e||window.event}function os(e,t,n){if(null!=(t=rs(t)).zrX)return t;var i=t.type;if(i&&i.indexOf("touch")>=0){var a="touchend"!==i?t.targetTouches[0]:t.changedTouches[0];a&&is(e,a,t,n)}else{is(e,t,t,n);var r=function(e){var t=e.wheelDelta;if(t)return t;var n=e.deltaX,i=e.deltaY;if(null==n||null==i)return t;return 3*(0!==i?Math.abs(i):Math.abs(n))*(i>0?-1:i<0?1:n>0?-1:1)}(t);t.zrDelta=r?r/120:-(t.detail||0)/3}var o=t.button;return null==t.which&&void 0!==o&&es.test(t.type)&&(t.which=1&o?1:2&o?3:4&o?2:0),t}function ss(e,t,n,i){e.addEventListener(t,n,i)}var ls=function(e){e.preventDefault(),e.stopPropagation(),e.cancelBubble=!0};function ps(e){return 2===e.which||3===e.which}var cs=function(){function e(){this._track=[]}return e.prototype.recognize=function(e,t,n){return this._doTrack(e,t,n),this._recognize(e)},e.prototype.clear=function(){return this._track.length=0,this},e.prototype._doTrack=function(e,t,n){var i=e.touches;if(i){for(var a={points:[],touches:[],target:t,event:e},r=0,o=i.length;r1&&a&&a.length>1){var o=ds(a)/ds(r);!isFinite(o)&&(o=1),t.pinchScale=o;var s=[((i=a)[0][0]+i[1][0])/2,(i[0][1]+i[1][1])/2];return t.pinchX=s[0],t.pinchY=s[1],{type:"pinch",target:e[0].target,event:t}}}}};function ms(e){return e[0]=1,e[1]=0,e[2]=0,e[3]=1,e[4]=0,e[5]=0,e}function hs(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4],e[5]=t[5],e}function gs(e,t,n){var i=t[0]*n[0]+t[2]*n[1],a=t[1]*n[0]+t[3]*n[1],r=t[0]*n[2]+t[2]*n[3],o=t[1]*n[2]+t[3]*n[3],s=t[0]*n[4]+t[2]*n[5]+t[4],l=t[1]*n[4]+t[3]*n[5]+t[5];return e[0]=i,e[1]=a,e[2]=r,e[3]=o,e[4]=s,e[5]=l,e}function fs(e,t,n){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4]+n[0],e[5]=t[5]+n[1],e}function ys(e,t,n,i){void 0===i&&(i=[0,0]);var a=t[0],r=t[2],o=t[4],s=t[1],l=t[3],p=t[5],c=Math.sin(n),d=Math.cos(n);return e[0]=a*d+s*c,e[1]=-a*c+s*d,e[2]=r*d+l*c,e[3]=-r*c+d*l,e[4]=d*(o-i[0])+c*(p-i[1])+i[0],e[5]=d*(p-i[1])-c*(o-i[0])+i[1],e}function vs(e,t,n){var i=n[0],a=n[1];return e[0]=t[0]*i,e[1]=t[1]*a,e[2]=t[2]*i,e[3]=t[3]*a,e[4]=t[4]*i,e[5]=t[5]*a,e}function xs(e,t){var n=t[0],i=t[2],a=t[4],r=t[1],o=t[3],s=t[5],l=n*o-r*i;return l?(l=1/l,e[0]=o*l,e[1]=-r*l,e[2]=-i*l,e[3]=n*l,e[4]=(i*s-o*a)*l,e[5]=(r*a-n*s)*l,e):null}function bs(e){var t=[1,0,0,1,0,0];return hs(t,e),t}var ws=function(){function e(e,t){this.x=e||0,this.y=t||0}return e.prototype.copy=function(e){return this.x=e.x,this.y=e.y,this},e.prototype.clone=function(){return new e(this.x,this.y)},e.prototype.set=function(e,t){return this.x=e,this.y=t,this},e.prototype.equal=function(e){return e.x===this.x&&e.y===this.y},e.prototype.add=function(e){return this.x+=e.x,this.y+=e.y,this},e.prototype.scale=function(e){this.x*=e,this.y*=e},e.prototype.scaleAndAdd=function(e,t){this.x+=e.x*t,this.y+=e.y*t},e.prototype.sub=function(e){return this.x-=e.x,this.y-=e.y,this},e.prototype.dot=function(e){return this.x*e.x+this.y*e.y},e.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},e.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},e.prototype.normalize=function(){var e=this.len();return this.x/=e,this.y/=e,this},e.prototype.distance=function(e){var t=this.x-e.x,n=this.y-e.y;return Math.sqrt(t*t+n*n)},e.prototype.distanceSquare=function(e){var t=this.x-e.x,n=this.y-e.y;return t*t+n*n},e.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},e.prototype.transform=function(e){if(e){var t=this.x,n=this.y;return this.x=e[0]*t+e[2]*n+e[4],this.y=e[1]*t+e[3]*n+e[5],this}},e.prototype.toArray=function(e){return e[0]=this.x,e[1]=this.y,e},e.prototype.fromArray=function(e){this.x=e[0],this.y=e[1]},e.set=function(e,t,n){e.x=t,e.y=n},e.copy=function(e,t){e.x=t.x,e.y=t.y},e.len=function(e){return Math.sqrt(e.x*e.x+e.y*e.y)},e.lenSquare=function(e){return e.x*e.x+e.y*e.y},e.dot=function(e,t){return e.x*t.x+e.y*t.y},e.add=function(e,t,n){e.x=t.x+n.x,e.y=t.y+n.y},e.sub=function(e,t,n){e.x=t.x-n.x,e.y=t.y-n.y},e.scale=function(e,t,n){e.x=t.x*n,e.y=t.y*n},e.scaleAndAdd=function(e,t,n,i){e.x=t.x+n.x*i,e.y=t.y+n.y*i},e.lerp=function(e,t,n,i){var a=1-i;e.x=a*t.x+i*n.x,e.y=a*t.y+i*n.y},e}(),Ss=Math.min,Cs=Math.max,_s=new ws,Ts=new ws,Is=new ws,Es=new ws,Ms=new ws,ks=new ws,Ps=function(){function e(e,t,n,i){n<0&&(e+=n,n=-n),i<0&&(t+=i,i=-i),this.x=e,this.y=t,this.width=n,this.height=i}return e.prototype.union=function(e){var t=Ss(e.x,this.x),n=Ss(e.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=Cs(e.x+e.width,this.x+this.width)-t:this.width=e.width,isFinite(this.y)&&isFinite(this.height)?this.height=Cs(e.y+e.height,this.y+this.height)-n:this.height=e.height,this.x=t,this.y=n},e.prototype.applyTransform=function(t){e.applyTransform(this,this,t)},e.prototype.calculateTransform=function(e){var t=this,n=e.width/t.width,i=e.height/t.height,a=[1,0,0,1,0,0];return fs(a,a,[-t.x,-t.y]),vs(a,a,[n,i]),fs(a,a,[e.x,e.y]),a},e.prototype.intersect=function(t,n){if(!t)return!1;t instanceof e||(t=e.create(t));var i=this,a=i.x,r=i.x+i.width,o=i.y,s=i.y+i.height,l=t.x,p=t.x+t.width,c=t.y,d=t.y+t.height,u=!(rh&&(h=x,gh&&(h=b,y=n.x&&e<=n.x+n.width&&t>=n.y&&t<=n.y+n.height},e.prototype.clone=function(){return new e(this.x,this.y,this.width,this.height)},e.prototype.copy=function(t){e.copy(this,t)},e.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},e.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},e.prototype.isZero=function(){return 0===this.width||0===this.height},e.create=function(t){return new e(t.x,t.y,t.width,t.height)},e.copy=function(e,t){e.x=t.x,e.y=t.y,e.width=t.width,e.height=t.height},e.applyTransform=function(t,n,i){if(i){if(i[1]<1e-5&&i[1]>-1e-5&&i[2]<1e-5&&i[2]>-1e-5){var a=i[0],r=i[3],o=i[4],s=i[5];return t.x=n.x*a+o,t.y=n.y*r+s,t.width=n.width*a,t.height=n.height*r,t.width<0&&(t.x+=t.width,t.width=-t.width),void(t.height<0&&(t.y+=t.height,t.height=-t.height))}_s.x=Is.x=n.x,_s.y=Es.y=n.y,Ts.x=Es.x=n.x+n.width,Ts.y=Is.y=n.y+n.height,_s.transform(i),Es.transform(i),Ts.transform(i),Is.transform(i),t.x=Ss(_s.x,Ts.x,Is.x,Es.x),t.y=Ss(_s.y,Ts.y,Is.y,Es.y);var l=Cs(_s.x,Ts.x,Is.x,Es.x),p=Cs(_s.y,Ts.y,Is.y,Es.y);t.width=l-t.x,t.height=p-t.y}else t!==n&&e.copy(t,n)},e}(),Ds="silent";function Os(){ls(this.event)}var As=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.handler=null,t}return ze(t,e),t.prototype.dispose=function(){},t.prototype.setCursor=function(){},t}(Uo),Fs=function(e,t){this.x=e,this.y=t},Rs=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],Bs=new Ps(0,0,0,0),Ns=function(e){function t(t,n,i,a,r){var o=e.call(this)||this;return o._hovered=new Fs(0,0),o.storage=t,o.painter=n,o.painterRoot=a,o._pointerSize=r,i=i||new As,o.proxy=null,o.setHandlerProxy(i),o._draggingMgr=new zo(o),o}return ze(t,e),t.prototype.setHandlerProxy=function(e){this.proxy&&this.proxy.dispose(),e&&(Rr(Rs,(function(t){e.on&&e.on(t,this[t],this)}),this),e.handler=this),this.proxy=e},t.prototype.mousemove=function(e){var t=e.zrX,n=e.zrY,i=qs(this,t,n),a=this._hovered,r=a.target;r&&!r.__zr&&(r=(a=this.findHover(a.x,a.y)).target);var o=this._hovered=i?new Fs(t,n):this.findHover(t,n),s=o.target,l=this.proxy;l.setCursor&&l.setCursor(s?s.cursor:"default"),r&&s!==r&&this.dispatchToElement(a,"mouseout",e),this.dispatchToElement(o,"mousemove",e),s&&s!==r&&this.dispatchToElement(o,"mouseover",e)},t.prototype.mouseout=function(e){var t=e.zrEventControl;"only_globalout"!==t&&this.dispatchToElement(this._hovered,"mouseout",e),"no_globalout"!==t&&this.trigger("globalout",{type:"globalout",event:e})},t.prototype.resize=function(){this._hovered=new Fs(0,0)},t.prototype.dispatch=function(e,t){var n=this[e];n&&n.call(this,t)},t.prototype.dispose=function(){this.proxy.dispose(),this.storage=null,this.proxy=null,this.painter=null},t.prototype.setCursorStyle=function(e){var t=this.proxy;t.setCursor&&t.setCursor(e)},t.prototype.dispatchToElement=function(e,t,n){var i=(e=e||{}).target;if(!i||!i.silent){for(var a="on"+t,r=function(e,t,n){return{type:e,event:n,target:t.target,topTarget:t.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which,stop:Os}}(t,e,n);i&&(i[a]&&(r.cancelBubble=!!i[a].call(i,r)),i.trigger(t,r),i=i.__hostTarget?i.__hostTarget:i.parent,!r.cancelBubble););r.cancelBubble||(this.trigger(t,r),this.painter&&this.painter.eachOtherLayer&&this.painter.eachOtherLayer((function(e){"function"==typeof e[a]&&e[a].call(e,r),e.trigger&&e.trigger(t,r)})))}},t.prototype.findHover=function(e,t,n){var i=this.storage.getDisplayList(),a=new Fs(e,t);if(Vs(i,a,e,t,n),this._pointerSize&&!a.target){for(var r=[],o=this._pointerSize,s=o/2,l=new Ps(e-s,t-s,o,o),p=i.length-1;p>=0;p--){var c=i[p];c===n||c.ignore||c.ignoreCoarsePointer||c.parent&&c.parent.ignoreCoarsePointer||(Bs.copy(c.getBoundingRect()),c.transform&&Bs.applyTransform(c.transform),Bs.intersect(l)&&r.push(c))}if(r.length)for(var d=Math.PI/12,u=2*Math.PI,m=0;m=0;r--){var o=e[r],s=void 0;if(o!==a&&!o.ignore&&(s=Ls(o,n,i))&&(!t.topTarget&&(t.topTarget=o),s!==Ds)){t.target=o;break}}}function qs(e,t,n){var i=e.painter;return t<0||t>i.getWidth()||n<0||n>i.getHeight()}Rr(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],(function(e){Ns.prototype[e]=function(t){var n,i,a=t.zrX,r=t.zrY,o=qs(this,a,r);if("mouseup"===e&&o||(i=(n=this.findHover(a,r)).target),"mousedown"===e)this._downEl=i,this._downPoint=[t.zrX,t.zrY],this._upEl=i;else if("mouseup"===e)this._upEl=i;else if("click"===e){if(this._downEl!==this._upEl||!this._downPoint||Fo(this._downPoint,[t.zrX,t.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,e,t)}}));function Gs(e,t,n,i){var a=t+1;if(a===n)return 1;if(i(e[a++],e[t])<0){for(;a=0;)a++;return a-t}function zs(e,t,n,i,a){for(i===t&&i++;i>>1])<0?l=r:s=r+1;var p=i-s;switch(p){case 3:e[s+3]=e[s+2];case 2:e[s+2]=e[s+1];case 1:e[s+1]=e[s];break;default:for(;p>0;)e[s+p]=e[s+p-1],p--}e[s]=o}}function Us(e,t,n,i,a,r){var o=0,s=0,l=1;if(r(e,t[n+a])>0){for(s=i-a;l0;)o=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),o+=a,l+=a}else{for(s=a+1;ls&&(l=s);var p=o;o=a-l,l=a-p}for(o++;o>>1);r(e,t[n+c])>0?o=c+1:l=c}return l}function js(e,t,n,i,a,r){var o=0,s=0,l=1;if(r(e,t[n+a])<0){for(s=a+1;ls&&(l=s);var p=o;o=a-l,l=a-p}else{for(s=i-a;l=0;)o=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),o+=a,l+=a}for(o++;o>>1);r(e,t[n+c])<0?l=c:o=c+1}return l}function Hs(e,t){var n,i,a=7,r=0,o=[];function s(s){var l=n[s],p=i[s],c=n[s+1],d=i[s+1];i[s]=p+d,s===r-3&&(n[s+1]=n[s+2],i[s+1]=i[s+2]),r--;var u=js(e[c],e,l,p,0,t);l+=u,0!==(p-=u)&&0!==(d=Us(e[l+p-1],e,c,d,d-1,t))&&(p<=d?function(n,i,r,s){var l=0;for(l=0;l=7||m>=7);if(h)break;g<0&&(g=0),g+=2}if((a=g)<1&&(a=1),1===i){for(l=0;l=0;l--)e[m+l]=e[u+l];return void(e[d]=o[c])}var h=a;for(;;){var g=0,f=0,y=!1;do{if(t(o[c],e[p])<0){if(e[d--]=e[p--],g++,f=0,0==--i){y=!0;break}}else if(e[d--]=o[c--],f++,g=0,1==--s){y=!0;break}}while((g|f)=0;l--)e[m+l]=e[u+l];if(0===i){y=!0;break}}if(e[d--]=o[c--],1==--s){y=!0;break}if(0!==(f=s-Us(e[p],o,0,s,s-1,t))){for(s-=f,m=(d-=f)+1,u=(c-=f)+1,l=0;l=7||f>=7);if(y)break;h<0&&(h=0),h+=2}(a=h)<1&&(a=1);if(1===s){for(m=(d-=i)+1,u=(p-=i)+1,l=i-1;l>=0;l--)e[m+l]=e[u+l];e[d]=o[c]}else{if(0===s)throw new Error;for(u=d-(s-1),l=0;l1;){var e=r-2;if(e>=1&&i[e-1]<=i[e]+i[e+1]||e>=2&&i[e-2]<=i[e]+i[e-1])i[e-1]i[e+1])break;s(e)}},forceMergeRuns:function(){for(;r>1;){var e=r-2;e>0&&i[e-1]=32;)t|=1&e,e>>=1;return e+t}(a);do{if((r=Gs(e,n,i,t))s&&(l=s),zs(e,n,n+l,n+r,t),r=l}o.pushRun(n,r),o.mergeRuns(),a-=r,n+=r}while(0!==a);o.forceMergeRuns()}}}var $s=!1;function Ks(){$s||($s=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function Ys(e,t){return e.zlevel===t.zlevel?e.z===t.z?e.z2-t.z2:e.z-t.z:e.zlevel-t.zlevel}var Xs=function(){function e(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=Ys}return e.prototype.traverse=function(e,t){for(var n=0;n0&&(p.__clipPaths=[]),isNaN(p.z)&&(Ks(),p.z=0),isNaN(p.z2)&&(Ks(),p.z2=0),isNaN(p.zlevel)&&(Ks(),p.zlevel=0),this._displayList[this._displayListLen++]=p}var c=e.getDecalElement&&e.getDecalElement();c&&this._updateAndAddDisplayable(c,t,n);var d=e.getTextGuideLine();d&&this._updateAndAddDisplayable(d,t,n);var u=e.getTextContent();u&&this._updateAndAddDisplayable(u,t,n)}},e.prototype.addRoot=function(e){e.__zr&&e.__zr.storage===this||this._roots.push(e)},e.prototype.delRoot=function(e){if(e instanceof Array)for(var t=0,n=e.length;t=0&&this._roots.splice(i,1)}},e.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},e.prototype.getRoots=function(){return this._roots},e.prototype.dispose=function(){this._displayList=null,this._roots=null},e}(),Zs=rr.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(e){return setTimeout(e,16)},Qs={linear:function(e){return e},quadraticIn:function(e){return e*e},quadraticOut:function(e){return e*(2-e)},quadraticInOut:function(e){return(e*=2)<1?.5*e*e:-.5*(--e*(e-2)-1)},cubicIn:function(e){return e*e*e},cubicOut:function(e){return--e*e*e+1},cubicInOut:function(e){return(e*=2)<1?.5*e*e*e:.5*((e-=2)*e*e+2)},quarticIn:function(e){return e*e*e*e},quarticOut:function(e){return 1- --e*e*e*e},quarticInOut:function(e){return(e*=2)<1?.5*e*e*e*e:-.5*((e-=2)*e*e*e-2)},quinticIn:function(e){return e*e*e*e*e},quinticOut:function(e){return--e*e*e*e*e+1},quinticInOut:function(e){return(e*=2)<1?.5*e*e*e*e*e:.5*((e-=2)*e*e*e*e+2)},sinusoidalIn:function(e){return 1-Math.cos(e*Math.PI/2)},sinusoidalOut:function(e){return Math.sin(e*Math.PI/2)},sinusoidalInOut:function(e){return.5*(1-Math.cos(Math.PI*e))},exponentialIn:function(e){return 0===e?0:Math.pow(1024,e-1)},exponentialOut:function(e){return 1===e?1:1-Math.pow(2,-10*e)},exponentialInOut:function(e){return 0===e?0:1===e?1:(e*=2)<1?.5*Math.pow(1024,e-1):.5*(2-Math.pow(2,-10*(e-1)))},circularIn:function(e){return 1-Math.sqrt(1-e*e)},circularOut:function(e){return Math.sqrt(1- --e*e)},circularInOut:function(e){return(e*=2)<1?-.5*(Math.sqrt(1-e*e)-1):.5*(Math.sqrt(1-(e-=2)*e)+1)},elasticIn:function(e){var t,n=.1;return 0===e?0:1===e?1:(!n||n<1?(n=1,t=.1):t=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/.4))},elasticOut:function(e){var t,n=.1;return 0===e?0:1===e?1:(!n||n<1?(n=1,t=.1):t=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*e)*Math.sin((e-t)*(2*Math.PI)/.4)+1)},elasticInOut:function(e){var t,n=.1,i=.4;return 0===e?0:1===e?1:(!n||n<1?(n=1,t=.1):t=i*Math.asin(1/n)/(2*Math.PI),(e*=2)<1?n*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/i)*.5+1)},backIn:function(e){var t=1.70158;return e*e*((t+1)*e-t)},backOut:function(e){var t=1.70158;return--e*e*((t+1)*e+t)+1},backInOut:function(e){var t=2.5949095;return(e*=2)<1?e*e*((t+1)*e-t)*.5:.5*((e-=2)*e*((t+1)*e+t)+2)},bounceIn:function(e){return 1-Qs.bounceOut(1-e)},bounceOut:function(e){return e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375},bounceInOut:function(e){return e<.5?.5*Qs.bounceIn(2*e):.5*Qs.bounceOut(2*e-1)+.5}},Js=Math.pow,el=Math.sqrt,tl=1e-8,nl=1e-4,il=el(3),al=1/3,rl=Co(),ol=Co(),sl=Co();function ll(e){return e>-1e-8&&etl||e<-1e-8}function cl(e,t,n,i,a){var r=1-a;return r*r*(r*e+3*a*t)+a*a*(a*i+3*r*n)}function dl(e,t,n,i,a){var r=1-a;return 3*(((t-e)*r+2*(n-t)*a)*r+(i-n)*a*a)}function ul(e,t,n,i,a,r){var o=i+3*(t-n)-e,s=3*(n-2*t+e),l=3*(t-e),p=e-a,c=s*s-3*o*l,d=s*l-9*o*p,u=l*l-3*s*p,m=0;if(ll(c)&&ll(d)){if(ll(s))r[0]=0;else(_=-l/s)>=0&&_<=1&&(r[m++]=_)}else{var h=d*d-4*c*u;if(ll(h)){var g=d/c,f=-g/2;(_=-s/o+g)>=0&&_<=1&&(r[m++]=_),f>=0&&f<=1&&(r[m++]=f)}else if(h>0){var y=el(h),v=c*s+1.5*o*(-d+y),x=c*s+1.5*o*(-d-y);(_=(-s-((v=v<0?-Js(-v,al):Js(v,al))+(x=x<0?-Js(-x,al):Js(x,al))))/(3*o))>=0&&_<=1&&(r[m++]=_)}else{var b=(2*c*s-3*o*d)/(2*el(c*c*c)),w=Math.acos(b)/3,S=el(c),C=Math.cos(w),_=(-s-2*S*C)/(3*o),T=(f=(-s+S*(C+il*Math.sin(w)))/(3*o),(-s+S*(C-il*Math.sin(w)))/(3*o));_>=0&&_<=1&&(r[m++]=_),f>=0&&f<=1&&(r[m++]=f),T>=0&&T<=1&&(r[m++]=T)}}return m}function ml(e,t,n,i,a){var r=6*n-12*t+6*e,o=9*t+3*i-3*e-9*n,s=3*t-3*e,l=0;if(ll(o)){if(pl(r))(c=-s/r)>=0&&c<=1&&(a[l++]=c)}else{var p=r*r-4*o*s;if(ll(p))a[0]=-r/(2*o);else if(p>0){var c,d=el(p),u=(-r-d)/(2*o);(c=(-r+d)/(2*o))>=0&&c<=1&&(a[l++]=c),u>=0&&u<=1&&(a[l++]=u)}}return l}function hl(e,t,n,i,a,r){var o=(t-e)*a+e,s=(n-t)*a+t,l=(i-n)*a+n,p=(s-o)*a+o,c=(l-s)*a+s,d=(c-p)*a+p;r[0]=e,r[1]=o,r[2]=p,r[3]=d,r[4]=d,r[5]=c,r[6]=l,r[7]=i}function gl(e,t,n,i,a,r,o,s,l,p,c){var d,u,m,h,g,f=.005,y=1/0;rl[0]=l,rl[1]=p;for(var v=0;v<1;v+=.05)ol[0]=cl(e,n,a,o,v),ol[1]=cl(t,i,r,s,v),(h=Bo(rl,ol))=0&&h=0&&f=1?1:ul(0,i,r,1,e,s)&&cl(0,a,o,1,s[0])}}}var Tl=function(){function e(e){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=e.life||1e3,this._delay=e.delay||0,this.loop=e.loop||!1,this.onframe=e.onframe||wo,this.ondestroy=e.ondestroy||wo,this.onrestart=e.onrestart||wo,e.easing&&this.setEasing(e.easing)}return e.prototype.step=function(e,t){if(this._inited||(this._startTime=e+this._delay,this._inited=!0),!this._paused){var n=this._life,i=e-this._startTime-this._pausedTime,a=i/n;a<0&&(a=0),a=Math.min(a,1);var r=this.easingFunc,o=r?r(a):a;if(this.onframe(o),1===a){if(!this.loop)return!0;var s=i%n;this._startTime=e-s,this._pausedTime=0,this.onrestart()}return!1}this._pausedTime+=t},e.prototype.pause=function(){this._paused=!0},e.prototype.resume=function(){this._paused=!1},e.prototype.setEasing=function(e){this.easing=e,this.easingFunc=jr(e)?e:Qs[e]||_l(e)},e}(),Il=function(e){this.value=e},El=function(){function e(){this._len=0}return e.prototype.insert=function(e){var t=new Il(e);return this.insertEntry(t),t},e.prototype.insertEntry=function(e){this.head?(this.tail.next=e,e.prev=this.tail,e.next=null,this.tail=e):this.head=this.tail=e,this._len++},e.prototype.remove=function(e){var t=e.prev,n=e.next;t?t.next=n:this.head=n,n?n.prev=t:this.tail=t,e.next=e.prev=null,this._len--},e.prototype.len=function(){return this._len},e.prototype.clear=function(){this.head=this.tail=null,this._len=0},e}(),Ml=function(){function e(e){this._list=new El,this._maxSize=10,this._map={},this._maxSize=e}return e.prototype.put=function(e,t){var n=this._list,i=this._map,a=null;if(null==i[e]){var r=n.len(),o=this._lastRemovedEntry;if(r>=this._maxSize&&r>0){var s=n.head;n.remove(s),delete i[s.key],a=s.value,this._lastRemovedEntry=s}o?o.value=t:o=new Il(t),o.key=e,n.insertEntry(o),i[e]=o}return a},e.prototype.get=function(e){var t=this._map[e],n=this._list;if(null!=t)return t!==n.tail&&(n.remove(t),n.insertEntry(t)),t.value},e.prototype.clear=function(){this._list.clear(),this._map={}},e.prototype.len=function(){return this._list.len()},e}(),kl={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function Pl(e){return(e=Math.round(e))<0?0:e>255?255:e}function Dl(e){return e<0?0:e>1?1:e}function Ol(e){var t=e;return t.length&&"%"===t.charAt(t.length-1)?Pl(parseFloat(t)/100*255):Pl(parseInt(t,10))}function Al(e){var t=e;return t.length&&"%"===t.charAt(t.length-1)?Dl(parseFloat(t)/100):Dl(parseFloat(t))}function Fl(e,t,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?e+(t-e)*n*6:2*n<1?t:3*n<2?e+(t-e)*(2/3-n)*6:e}function Rl(e,t,n){return e+(t-e)*n}function Bl(e,t,n,i,a){return e[0]=t,e[1]=n,e[2]=i,e[3]=a,e}function Nl(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e}var Ll=new Ml(20),Vl=null;function ql(e,t){Vl&&Nl(Vl,t),Vl=Ll.put(e,Vl||t.slice())}function Gl(e,t){if(e){t=t||[];var n=Ll.get(e);if(n)return Nl(t,n);var i=(e+="").replace(/ /g,"").toLowerCase();if(i in kl)return Nl(t,kl[i]),ql(e,t),t;var a,r=i.length;if("#"===i.charAt(0))return 4===r||5===r?(a=parseInt(i.slice(1,4),16))>=0&&a<=4095?(Bl(t,(3840&a)>>4|(3840&a)>>8,240&a|(240&a)>>4,15&a|(15&a)<<4,5===r?parseInt(i.slice(4),16)/15:1),ql(e,t),t):void Bl(t,0,0,0,1):7===r||9===r?(a=parseInt(i.slice(1,7),16))>=0&&a<=16777215?(Bl(t,(16711680&a)>>16,(65280&a)>>8,255&a,9===r?parseInt(i.slice(7),16)/255:1),ql(e,t),t):void Bl(t,0,0,0,1):void 0;var o=i.indexOf("("),s=i.indexOf(")");if(-1!==o&&s+1===r){var l=i.substr(0,o),p=i.substr(o+1,s-(o+1)).split(","),c=1;switch(l){case"rgba":if(4!==p.length)return 3===p.length?Bl(t,+p[0],+p[1],+p[2],1):Bl(t,0,0,0,1);c=Al(p.pop());case"rgb":return p.length>=3?(Bl(t,Ol(p[0]),Ol(p[1]),Ol(p[2]),3===p.length?c:Al(p[3])),ql(e,t),t):void Bl(t,0,0,0,1);case"hsla":return 4!==p.length?void Bl(t,0,0,0,1):(p[3]=Al(p[3]),zl(p,t),ql(e,t),t);case"hsl":return 3!==p.length?void Bl(t,0,0,0,1):(zl(p,t),ql(e,t),t);default:return}}Bl(t,0,0,0,1)}}function zl(e,t){var n=(parseFloat(e[0])%360+360)%360/360,i=Al(e[1]),a=Al(e[2]),r=a<=.5?a*(i+1):a+i-a*i,o=2*a-r;return Bl(t=t||[],Pl(255*Fl(o,r,n+1/3)),Pl(255*Fl(o,r,n)),Pl(255*Fl(o,r,n-1/3)),1),4===e.length&&(t[3]=e[3]),t}function Ul(e,t){var n=Gl(e);if(n){for(var i=0;i<3;i++)n[i]=t<0?n[i]*(1-t)|0:(255-n[i])*t+n[i]|0,n[i]>255?n[i]=255:n[i]<0&&(n[i]=0);return Kl(n,4===n.length?"rgba":"rgb")}}function jl(e,t,n){if(t&&t.length&&e>=0&&e<=1){n=n||[];var i=e*(t.length-1),a=Math.floor(i),r=Math.ceil(i),o=t[a],s=t[r],l=i-a;return n[0]=Pl(Rl(o[0],s[0],l)),n[1]=Pl(Rl(o[1],s[1],l)),n[2]=Pl(Rl(o[2],s[2],l)),n[3]=Dl(Rl(o[3],s[3],l)),n}}function Hl(e,t,n){if(t&&t.length&&e>=0&&e<=1){var i=e*(t.length-1),a=Math.floor(i),r=Math.ceil(i),o=Gl(t[a]),s=Gl(t[r]),l=i-a,p=Kl([Pl(Rl(o[0],s[0],l)),Pl(Rl(o[1],s[1],l)),Pl(Rl(o[2],s[2],l)),Dl(Rl(o[3],s[3],l))],"rgba");return n?{color:p,leftIndex:a,rightIndex:r,value:i}:p}}function Wl(e,t,n,i){var a=Gl(e);if(e)return a=function(e){if(e){var t,n,i=e[0]/255,a=e[1]/255,r=e[2]/255,o=Math.min(i,a,r),s=Math.max(i,a,r),l=s-o,p=(s+o)/2;if(0===l)t=0,n=0;else{n=p<.5?l/(s+o):l/(2-s-o);var c=((s-i)/6+l/2)/l,d=((s-a)/6+l/2)/l,u=((s-r)/6+l/2)/l;i===s?t=u-d:a===s?t=1/3+c-u:r===s&&(t=2/3+d-c),t<0&&(t+=1),t>1&&(t-=1)}var m=[360*t,n,p];return null!=e[3]&&m.push(e[3]),m}}(a),null!=t&&(a[0]=function(e){return(e=Math.round(e))<0?0:e>360?360:e}(t)),null!=n&&(a[1]=Al(n)),null!=i&&(a[2]=Al(i)),Kl(zl(a),"rgba")}function $l(e,t){var n=Gl(e);if(n&&null!=t)return n[3]=Dl(t),Kl(n,"rgba")}function Kl(e,t){if(e&&e.length){var n=e[0]+","+e[1]+","+e[2];return"rgba"!==t&&"hsva"!==t&&"hsla"!==t||(n+=","+e[3]),t+"("+n+")"}}function Yl(e,t){var n=Gl(e);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*t:0}var Xl=new Ml(100);function Zl(e){if(Hr(e)){var t=Xl.get(e);return t||(t=Ul(e,-.1),Xl.put(e,t)),t}if(Qr(e)){var n=kr({},e);return n.colorStops=Br(e.colorStops,(function(e){return{offset:e.offset,color:Ul(e.color,-.1)}})),n}return e}var Ql=Math.round;function Jl(e){var t;if(e&&"transparent"!==e){if("string"==typeof e&&e.indexOf("rgba")>-1){var n=Gl(e);n&&(e="rgb("+n[0]+","+n[1]+","+n[2]+")",t=n[3])}}else e="none";return{color:e,opacity:null==t?1:t}}var ep=1e-4;function tp(e){return e-1e-4}function np(e){return Ql(1e3*e)/1e3}function ip(e){return Ql(1e4*e)/1e4}var ap={left:"start",right:"end",center:"middle",middle:"middle"};function rp(e){return e&&!!e.image}function op(e){return rp(e)||function(e){return e&&!!e.svgElement}(e)}function sp(e){return"linear"===e.type}function lp(e){return"radial"===e.type}function pp(e){return e&&("linear"===e.type||"radial"===e.type)}function cp(e){return"url(#"+e+")"}function dp(e){var t=e.getGlobalScale(),n=Math.max(t[0],t[1]);return Math.max(Math.ceil(Math.log(n)/Math.log(10)),1)}function up(e){var t=e.x||0,n=e.y||0,i=(e.rotation||0)*So,a=io(e.scaleX,1),r=io(e.scaleY,1),o=e.skewX||0,s=e.skewY||0,l=[];return(t||n)&&l.push("translate("+t+"px,"+n+"px)"),i&&l.push("rotate("+i+")"),1===a&&1===r||l.push("scale("+a+","+r+")"),(o||s)&&l.push("skew("+Ql(o*So)+"deg, "+Ql(s*So)+"deg)"),l.join(" ")}var mp=rr.hasGlobalWindow&&jr(window.btoa)?function(e){return window.btoa(unescape(encodeURIComponent(e)))}:"undefined"!=typeof Buffer?function(e){return Buffer.from(e).toString("base64")}:function(e){return null},hp=Array.prototype.slice;function gp(e,t,n){return(t-e)*n+e}function fp(e,t,n,i){for(var a=t.length,r=0;ri?t:e,r=Math.min(n,i),o=a[r-1]||{color:[0,0,0,0],offset:0},s=r;so)i.length=o;else for(var s=r;s=1},e.prototype.getAdditiveTrack=function(){return this._additiveTrack},e.prototype.addKeyframe=function(e,t,n){this._needsSort=!0;var i=this.keyframes,a=i.length,r=!1,o=6,s=t;if(Fr(t)){var l=function(e){return Fr(e&&e[0])?2:1}(t);o=l,(1===l&&!$r(t[0])||2===l&&!$r(t[0][0]))&&(r=!0)}else if($r(t)&&!to(t))o=0;else if(Hr(t))if(isNaN(+t)){var p=Gl(t);p&&(s=p,o=3)}else o=0;else if(Qr(t)){var c=kr({},s);c.colorStops=Br(t.colorStops,(function(e){return{offset:e.offset,color:Gl(e.color)}})),sp(t)?o=4:lp(t)&&(o=5),s=c}0===a?this.valType=o:o===this.valType&&6!==o||(r=!0),this.discrete=this.discrete||r;var d={time:e,value:s,rawValue:t,percent:0};return n&&(d.easing=n,d.easingFunc=jr(n)?n:Qs[n]||_l(n)),i.push(d),d},e.prototype.prepare=function(e,t){var n=this.keyframes;this._needsSort&&n.sort((function(e,t){return e.time-t.time}));for(var i=this.valType,a=n.length,r=n[a-1],o=this.discrete,s=_p(i),l=Cp(i),p=0;p=0&&!(l[n].percent<=t);n--);n=m(n,p-2)}else{for(n=u;nt);n++);n=m(n-1,p-2)}a=l[n+1],i=l[n]}if(i&&a){this._lastFr=n,this._lastFrP=t;var h=a.percent-i.percent,g=0===h?1:m((t-i.percent)/h,1);a.easingFunc&&(g=a.easingFunc(g));var f=r?this._additiveValue:d?Tp:e[c];if(!_p(s)&&!d||f||(f=this._additiveValue=[]),this.discrete)e[c]=g<1?i.rawValue:a.rawValue;else if(_p(s))1===s?fp(f,i[o],a[o],g):function(e,t,n,i){for(var a=t.length,r=a&&t[0].length,o=0;o0&&s.addKeyframe(0,wp(l),i),this._trackKeys.push(o)}s.addKeyframe(e,wp(t[o]),i)}return this._maxTime=Math.max(this._maxTime,e),this},e.prototype.pause=function(){this._clip.pause(),this._paused=!0},e.prototype.resume=function(){this._clip.resume(),this._paused=!1},e.prototype.isPaused=function(){return!!this._paused},e.prototype.duration=function(e){return this._maxTime=e,this._force=!0,this},e.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var e=this._doneCbs;if(e)for(var t=e.length,n=0;n0)){this._started=1;for(var t=this,n=[],i=this._maxTime||0,a=0;a1){var o=r.pop();a.addKeyframe(o.time,e[i]),a.prepare(this._maxTime,a.getAdditiveTrack())}}}},e}();function Mp(){return(new Date).getTime()}var kp,Pp,Dp=function(e){function t(t){var n=e.call(this)||this;return n._running=!1,n._time=0,n._pausedTime=0,n._pauseStart=0,n._paused=!1,t=t||{},n.stage=t.stage||{},n}return ze(t,e),t.prototype.addClip=function(e){e.animation&&this.removeClip(e),this._head?(this._tail.next=e,e.prev=this._tail,e.next=null,this._tail=e):this._head=this._tail=e,e.animation=this},t.prototype.addAnimator=function(e){e.animation=this;var t=e.getClip();t&&this.addClip(t)},t.prototype.removeClip=function(e){if(e.animation){var t=e.prev,n=e.next;t?t.next=n:this._head=n,n?n.prev=t:this._tail=t,e.next=e.prev=e.animation=null}},t.prototype.removeAnimator=function(e){var t=e.getClip();t&&this.removeClip(t),e.animation=null},t.prototype.update=function(e){for(var t=Mp()-this._pausedTime,n=t-this._time,i=this._head;i;){var a=i.next;i.step(t,n)?(i.ondestroy(),this.removeClip(i),i=a):i=a}this._time=t,e||(this.trigger("frame",n),this.stage.update&&this.stage.update())},t.prototype._startLoop=function(){var e=this;this._running=!0,Zs((function t(){e._running&&(Zs(t),!e._paused&&e.update())}))},t.prototype.start=function(){this._running||(this._time=Mp(),this._pausedTime=0,this._startLoop())},t.prototype.stop=function(){this._running=!1},t.prototype.pause=function(){this._paused||(this._pauseStart=Mp(),this._paused=!0)},t.prototype.resume=function(){this._paused&&(this._pausedTime+=Mp()-this._pauseStart,this._paused=!1)},t.prototype.clear=function(){for(var e=this._head;e;){var t=e.next;e.prev=e.next=e.animation=null,e=t}this._head=this._tail=null},t.prototype.isFinished=function(){return null==this._head},t.prototype.animate=function(e,t){t=t||{},this.start();var n=new Ep(e,t.loop);return this.addAnimator(n),n},t}(Uo),Op=rr.domSupported,Ap=(Pp={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},{mouse:kp=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],touch:["touchstart","touchend","touchmove"],pointer:Br(kp,(function(e){var t=e.replace("mouse","pointer");return Pp.hasOwnProperty(t)?t:e}))}),Fp=["mousemove","mouseup"],Rp=["pointermove","pointerup"],Bp=!1;function Np(e){var t=e.pointerType;return"pen"===t||"touch"===t}function Lp(e){e&&(e.zrByTouch=!0)}function Vp(e,t){for(var n=t,i=!1;n&&9!==n.nodeType&&!(i=n.domBelongToZr||n!==t&&n===e.painterRoot);)n=n.parentNode;return i}var qp=function(e,t){this.stopPropagation=wo,this.stopImmediatePropagation=wo,this.preventDefault=wo,this.type=t.type,this.target=this.currentTarget=e.dom,this.pointerType=t.pointerType,this.clientX=t.clientX,this.clientY=t.clientY},Gp={mousedown:function(e){e=os(this.dom,e),this.__mayPointerCapture=[e.zrX,e.zrY],this.trigger("mousedown",e)},mousemove:function(e){e=os(this.dom,e);var t=this.__mayPointerCapture;!t||e.zrX===t[0]&&e.zrY===t[1]||this.__togglePointerCapture(!0),this.trigger("mousemove",e)},mouseup:function(e){e=os(this.dom,e),this.__togglePointerCapture(!1),this.trigger("mouseup",e)},mouseout:function(e){Vp(this,(e=os(this.dom,e)).toElement||e.relatedTarget)||(this.__pointerCapturing&&(e.zrEventControl="no_globalout"),this.trigger("mouseout",e))},wheel:function(e){Bp=!0,e=os(this.dom,e),this.trigger("mousewheel",e)},mousewheel:function(e){Bp||(e=os(this.dom,e),this.trigger("mousewheel",e))},touchstart:function(e){Lp(e=os(this.dom,e)),this.__lastTouchMoment=new Date,this.handler.processGesture(e,"start"),Gp.mousemove.call(this,e),Gp.mousedown.call(this,e)},touchmove:function(e){Lp(e=os(this.dom,e)),this.handler.processGesture(e,"change"),Gp.mousemove.call(this,e)},touchend:function(e){Lp(e=os(this.dom,e)),this.handler.processGesture(e,"end"),Gp.mouseup.call(this,e),+new Date-+this.__lastTouchMoment<300&&Gp.click.call(this,e)},pointerdown:function(e){Gp.mousedown.call(this,e)},pointermove:function(e){Np(e)||Gp.mousemove.call(this,e)},pointerup:function(e){Gp.mouseup.call(this,e)},pointerout:function(e){Np(e)||Gp.mouseout.call(this,e)}};Rr(["click","dblclick","contextmenu"],(function(e){Gp[e]=function(t){t=os(this.dom,t),this.trigger(e,t)}}));var zp={pointermove:function(e){Np(e)||zp.mousemove.call(this,e)},pointerup:function(e){zp.mouseup.call(this,e)},mousemove:function(e){this.trigger("mousemove",e)},mouseup:function(e){var t=this.__pointerCapturing;this.__togglePointerCapture(!1),this.trigger("mouseup",e),t&&(e.zrEventControl="only_globalout",this.trigger("mouseout",e))}};function Up(e,t){var n=t.domHandlers;rr.pointerEventsSupported?Rr(Ap.pointer,(function(i){Hp(t,i,(function(t){n[i].call(e,t)}))})):(rr.touchEventsSupported&&Rr(Ap.touch,(function(i){Hp(t,i,(function(a){n[i].call(e,a),function(e){e.touching=!0,null!=e.touchTimer&&(clearTimeout(e.touchTimer),e.touchTimer=null),e.touchTimer=setTimeout((function(){e.touching=!1,e.touchTimer=null}),700)}(t)}))})),Rr(Ap.mouse,(function(i){Hp(t,i,(function(a){a=rs(a),t.touching||n[i].call(e,a)}))})))}function jp(e,t){function n(n){Hp(t,n,(function(i){i=rs(i),Vp(e,i.target)||(i=function(e,t){return os(e.dom,new qp(e,t),!0)}(e,i),t.domHandlers[n].call(e,i))}),{capture:!0})}rr.pointerEventsSupported?Rr(Rp,n):rr.touchEventsSupported||Rr(Fp,n)}function Hp(e,t,n,i){e.mounted[t]=n,e.listenerOpts[t]=i,ss(e.domTarget,t,n,i)}function Wp(e){var t,n,i,a,r=e.mounted;for(var o in r)r.hasOwnProperty(o)&&(t=e.domTarget,n=o,i=r[o],a=e.listenerOpts[o],t.removeEventListener(n,i,a));e.mounted={}}var $p=function(e,t){this.mounted={},this.listenerOpts={},this.touching=!1,this.domTarget=e,this.domHandlers=t},Kp=function(e){function t(t,n){var i=e.call(this)||this;return i.__pointerCapturing=!1,i.dom=t,i.painterRoot=n,i._localHandlerScope=new $p(t,Gp),Op&&(i._globalHandlerScope=new $p(document,zp)),Up(i,i._localHandlerScope),i}return ze(t,e),t.prototype.dispose=function(){Wp(this._localHandlerScope),Op&&Wp(this._globalHandlerScope)},t.prototype.setCursor=function(e){this.dom.style&&(this.dom.style.cursor=e||"default")},t.prototype.__togglePointerCapture=function(e){if(this.__mayPointerCapture=null,Op&&+this.__pointerCapturing^+e){this.__pointerCapturing=e;var t=this._globalHandlerScope;e?jp(this,t):Wp(t)}},t}(Uo),Yp=1;rr.hasGlobalWindow&&(Yp=Math.max(window.devicePixelRatio||window.screen&&window.screen.deviceXDPI/window.screen.logicalXDPI||1,1));var Xp=Yp,Zp="#333",Qp="#ccc",Jp=ms,ec=5e-5;function tc(e){return e>ec||e<-5e-5}var nc=[],ic=[],ac=[1,0,0,1,0,0],rc=Math.abs,oc=function(){function e(){}return e.prototype.getLocalTransform=function(t){return e.getLocalTransform(this,t)},e.prototype.setPosition=function(e){this.x=e[0],this.y=e[1]},e.prototype.setScale=function(e){this.scaleX=e[0],this.scaleY=e[1]},e.prototype.setSkew=function(e){this.skewX=e[0],this.skewY=e[1]},e.prototype.setOrigin=function(e){this.originX=e[0],this.originY=e[1]},e.prototype.needLocalTransform=function(){return tc(this.rotation)||tc(this.x)||tc(this.y)||tc(this.scaleX-1)||tc(this.scaleY-1)||tc(this.skewX)||tc(this.skewY)},e.prototype.updateTransform=function(){var e=this.parent&&this.parent.transform,t=this.needLocalTransform(),n=this.transform;t||e?(n=n||[1,0,0,1,0,0],t?this.getLocalTransform(n):Jp(n),e&&(t?gs(n,e,n):hs(n,e)),this.transform=n,this._resolveGlobalScaleRatio(n)):n&&(Jp(n),this.invTransform=null)},e.prototype._resolveGlobalScaleRatio=function(e){var t=this.globalScaleRatio;if(null!=t&&1!==t){this.getGlobalScale(nc);var n=nc[0]<0?-1:1,i=nc[1]<0?-1:1,a=((nc[0]-n)*t+n)/nc[0]||0,r=((nc[1]-i)*t+i)/nc[1]||0;e[0]*=a,e[1]*=a,e[2]*=r,e[3]*=r}this.invTransform=this.invTransform||[1,0,0,1,0,0],xs(this.invTransform,e)},e.prototype.getComputedTransform=function(){for(var e=this,t=[];e;)t.push(e),e=e.parent;for(;e=t.pop();)e.updateTransform();return this.transform},e.prototype.setLocalTransform=function(e){if(e){var t=e[0]*e[0]+e[1]*e[1],n=e[2]*e[2]+e[3]*e[3],i=Math.atan2(e[1],e[0]),a=Math.PI/2+i-Math.atan2(e[3],e[2]);n=Math.sqrt(n)*Math.cos(a),t=Math.sqrt(t),this.skewX=a,this.skewY=0,this.rotation=-i,this.x=+e[4],this.y=+e[5],this.scaleX=t,this.scaleY=n,this.originX=0,this.originY=0}},e.prototype.decomposeTransform=function(){if(this.transform){var e=this.parent,t=this.transform;e&&e.transform&&(e.invTransform=e.invTransform||[1,0,0,1,0,0],gs(ic,e.invTransform,t),t=ic);var n=this.originX,i=this.originY;(n||i)&&(ac[4]=n,ac[5]=i,gs(ic,t,ac),ic[4]-=n,ic[5]-=i,t=ic),this.setLocalTransform(t)}},e.prototype.getGlobalScale=function(e){var t=this.transform;return e=e||[],t?(e[0]=Math.sqrt(t[0]*t[0]+t[1]*t[1]),e[1]=Math.sqrt(t[2]*t[2]+t[3]*t[3]),t[0]<0&&(e[0]=-e[0]),t[3]<0&&(e[1]=-e[1]),e):(e[0]=1,e[1]=1,e)},e.prototype.transformCoordToLocal=function(e,t){var n=[e,t],i=this.invTransform;return i&&Lo(n,n,i),n},e.prototype.transformCoordToGlobal=function(e,t){var n=[e,t],i=this.transform;return i&&Lo(n,n,i),n},e.prototype.getLineScale=function(){var e=this.transform;return e&&rc(e[0]-1)>1e-10&&rc(e[3]-1)>1e-10?Math.sqrt(rc(e[0]*e[3]-e[2]*e[1])):1},e.prototype.copyTransform=function(e){lc(this,e)},e.getLocalTransform=function(e,t){t=t||[];var n=e.originX||0,i=e.originY||0,a=e.scaleX,r=e.scaleY,o=e.anchorX,s=e.anchorY,l=e.rotation||0,p=e.x,c=e.y,d=e.skewX?Math.tan(e.skewX):0,u=e.skewY?Math.tan(-e.skewY):0;if(n||i||o||s){var m=n+o,h=i+s;t[4]=-m*a-d*h*r,t[5]=-h*r-u*m*a}else t[4]=t[5]=0;return t[0]=a,t[3]=r,t[1]=u*a,t[2]=d*r,l&&ys(t,t,l),t[4]+=n+p,t[5]+=i+c,t},e.initDefaultProps=function(){var t=e.prototype;t.scaleX=t.scaleY=t.globalScaleRatio=1,t.x=t.y=t.originX=t.originY=t.skewX=t.skewY=t.rotation=t.anchorX=t.anchorY=0}(),e}(),sc=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function lc(e,t){for(var n=0;n=0?parseFloat(e)/100*t:parseFloat(e):e}function yc(e,t,n){var i=t.position||"inside",a=null!=t.distance?t.distance:5,r=n.height,o=n.width,s=r/2,l=n.x,p=n.y,c="left",d="top";if(i instanceof Array)l+=fc(i[0],n.width),p+=fc(i[1],n.height),c=null,d=null;else switch(i){case"left":l-=a,p+=s,c="right",d="middle";break;case"right":l+=a+o,p+=s,d="middle";break;case"top":l+=o/2,p-=a,c="center",d="bottom";break;case"bottom":l+=o/2,p+=r+a,c="center";break;case"inside":l+=o/2,p+=s,c="center",d="middle";break;case"insideLeft":l+=a,p+=s,d="middle";break;case"insideRight":l+=o-a,p+=s,c="right",d="middle";break;case"insideTop":l+=o/2,p+=a,c="center";break;case"insideBottom":l+=o/2,p+=r-a,c="center",d="bottom";break;case"insideTopLeft":l+=a,p+=a;break;case"insideTopRight":l+=o-a,p+=a,c="right";break;case"insideBottomLeft":l+=a,p+=r-a,d="bottom";break;case"insideBottomRight":l+=o-a,p+=r-a,c="right",d="bottom"}return(e=e||{}).x=l,e.y=p,e.align=c,e.verticalAlign=d,e}var vc="__zr_normal__",xc=sc.concat(["ignore"]),bc=Nr(sc,(function(e,t){return e[t]=!0,e}),{ignore:!1}),wc={},Sc=new Ps(0,0,0,0),Cc=function(){function e(e){this.id=_r(),this.animators=[],this.currentStates=[],this.states={},this._init(e)}return e.prototype._init=function(e){this.attr(e)},e.prototype.drift=function(e,t,n){switch(this.draggable){case"horizontal":t=0;break;case"vertical":e=0}var i=this.transform;i||(i=this.transform=[1,0,0,1,0,0]),i[4]+=e,i[5]+=t,this.decomposeTransform(),this.markRedraw()},e.prototype.beforeUpdate=function(){},e.prototype.afterUpdate=function(){},e.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},e.prototype.updateInnerText=function(e){var t=this._textContent;if(t&&(!t.ignore||e)){this.textConfig||(this.textConfig={});var n=this.textConfig,i=n.local,a=t.innerTransformable,r=void 0,o=void 0,s=!1;a.parent=i?this:null;var l=!1;if(a.copyTransform(t),null!=n.position){var p=Sc;n.layoutRect?p.copy(n.layoutRect):p.copy(this.getBoundingRect()),i||p.applyTransform(this.transform),this.calculateTextPosition?this.calculateTextPosition(wc,n,p):yc(wc,n,p),a.x=wc.x,a.y=wc.y,r=wc.align,o=wc.verticalAlign;var c=n.origin;if(c&&null!=n.rotation){var d=void 0,u=void 0;"center"===c?(d=.5*p.width,u=.5*p.height):(d=fc(c[0],p.width),u=fc(c[1],p.height)),l=!0,a.originX=-a.x+d+(i?0:p.x),a.originY=-a.y+u+(i?0:p.y)}}null!=n.rotation&&(a.rotation=n.rotation);var m=n.offset;m&&(a.x+=m[0],a.y+=m[1],l||(a.originX=-m[0],a.originY=-m[1]));var h=null==n.inside?"string"==typeof n.position&&n.position.indexOf("inside")>=0:n.inside,g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={}),f=void 0,y=void 0,v=void 0;h&&this.canBeInsideText()?(f=n.insideFill,y=n.insideStroke,null!=f&&"auto"!==f||(f=this.getInsideTextFill()),null!=y&&"auto"!==y||(y=this.getInsideTextStroke(f),v=!0)):(f=n.outsideFill,y=n.outsideStroke,null!=f&&"auto"!==f||(f=this.getOutsideFill()),null!=y&&"auto"!==y||(y=this.getOutsideStroke(f),v=!0)),(f=f||"#000")===g.fill&&y===g.stroke&&v===g.autoStroke&&r===g.align&&o===g.verticalAlign||(s=!0,g.fill=f,g.stroke=y,g.autoStroke=v,g.align=r,g.verticalAlign=o,t.setDefaultTextStyle(g)),t.__dirty|=1,s&&t.dirtyStyle(!0)}},e.prototype.canBeInsideText=function(){return!0},e.prototype.getInsideTextFill=function(){return"#fff"},e.prototype.getInsideTextStroke=function(e){return"#000"},e.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?Qp:Zp},e.prototype.getOutsideStroke=function(e){var t=this.__zr&&this.__zr.getBackgroundColor(),n="string"==typeof t&&Gl(t);n||(n=[255,255,255,1]);for(var i=n[3],a=this.__zr.isDarkMode(),r=0;r<3;r++)n[r]=n[r]*i+(a?0:255)*(1-i);return n[3]=1,Kl(n,"rgba")},e.prototype.traverse=function(e,t){},e.prototype.attrKV=function(e,t){"textConfig"===e?this.setTextConfig(t):"textContent"===e?this.setTextContent(t):"clipPath"===e?this.setClipPath(t):"extra"===e?(this.extra=this.extra||{},kr(this.extra,t)):this[e]=t},e.prototype.hide=function(){this.ignore=!0,this.markRedraw()},e.prototype.show=function(){this.ignore=!1,this.markRedraw()},e.prototype.attr=function(e,t){if("string"==typeof e)this.attrKV(e,t);else if(Kr(e))for(var n=qr(e),i=0;i0},e.prototype.getState=function(e){return this.states[e]},e.prototype.ensureState=function(e){var t=this.states;return t[e]||(t[e]={}),t[e]},e.prototype.clearStates=function(e){this.useState(vc,!1,e)},e.prototype.useState=function(e,t,n,i){var a=e===vc;if(this.hasState()||!a){var r=this.currentStates,o=this.stateTransition;if(!(Dr(r,e)>=0)||!t&&1!==r.length){var s;if(this.stateProxy&&!a&&(s=this.stateProxy(e)),s||(s=this.states&&this.states[e]),s||a){a||this.saveCurrentToNormalState(s);var l=!!(s&&s.hoverLayer||i);l&&this._toggleHoverLayerFlag(!0),this._applyStateObj(e,s,this._normalState,t,!n&&!this.__inHover&&o&&o.duration>0,o);var p=this._textContent,c=this._textGuide;return p&&p.useState(e,t,n,l),c&&c.useState(e,t,n,l),a?(this.currentStates=[],this._normalState={}):t?this.currentStates.push(e):this.currentStates=[e],this._updateAnimationTargets(),this.markRedraw(),!l&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2),s}Tr("State "+e+" not exists.")}}},e.prototype.useStates=function(e,t,n){if(e.length){var i=[],a=this.currentStates,r=e.length,o=r===a.length;if(o)for(var s=0;s0,m);var h=this._textContent,g=this._textGuide;h&&h.useStates(e,t,d),g&&g.useStates(e,t,d),this._updateAnimationTargets(),this.currentStates=e.slice(),this.markRedraw(),!d&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2)}else this.clearStates()},e.prototype.isSilent=function(){for(var e=this.silent,t=this.parent;!e&&t;){if(t.silent){e=!0;break}t=t.parent}return e},e.prototype._updateAnimationTargets=function(){for(var e=0;e=0){var n=this.currentStates.slice();n.splice(t,1),this.useStates(n)}},e.prototype.replaceState=function(e,t,n){var i=this.currentStates.slice(),a=Dr(i,e),r=Dr(i,t)>=0;a>=0?r?i.splice(a,1):i[a]=t:n&&!r&&i.push(t),this.useStates(i)},e.prototype.toggleState=function(e,t){t?this.useState(e,!0):this.removeState(e)},e.prototype._mergeStates=function(e){for(var t,n={},i=0;i=0&&t.splice(n,1)})),this.animators.push(e),n&&n.animation.addAnimator(e),n&&n.wakeUp()},e.prototype.updateDuringAnimation=function(e){this.markRedraw()},e.prototype.stopAnimation=function(e,t){for(var n=this.animators,i=n.length,a=[],r=0;r0&&n.during&&r[0].during((function(e,t){n.during(t)}));for(var u=0;u0||a.force&&!o.length){var S,C=void 0,_=void 0,T=void 0;if(s){_={},u&&(C={});for(b=0;b=0&&(n.splice(i,0,e),this._doAdd(e))}return this},t.prototype.replace=function(e,t){var n=Dr(this._children,e);return n>=0&&this.replaceAt(t,n),this},t.prototype.replaceAt=function(e,t){var n=this._children,i=n[t];if(e&&e!==this&&e.parent!==this&&e!==i){n[t]=e,i.parent=null;var a=this.__zr;a&&i.removeSelfFromZr(a),this._doAdd(e)}return this},t.prototype._doAdd=function(e){e.parent&&e.parent.remove(e),e.parent=this;var t=this.__zr;t&&t!==e.__zr&&e.addSelfToZr(t),t&&t.refresh()},t.prototype.remove=function(e){var t=this.__zr,n=this._children,i=Dr(n,e);return i<0||(n.splice(i,1),e.parent=null,t&&e.removeSelfFromZr(t),t&&t.refresh()),this},t.prototype.removeAll=function(){for(var e=this._children,t=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},e.prototype.setSleepAfterStill=function(e){this._sleepAfterStill=e},e.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},e.prototype.refreshHover=function(){this._needsRefreshHover=!0},e.prototype.refreshHoverImmediately=function(){this._disposed||(this._needsRefreshHover=!1,this.painter.refreshHover&&"canvas"===this.painter.getType()&&this.painter.refreshHover())},e.prototype.resize=function(e){this._disposed||(e=e||{},this.painter.resize(e.width,e.height),this.handler.resize())},e.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},e.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},e.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},e.prototype.setCursorStyle=function(e){this._disposed||this.handler.setCursorStyle(e)},e.prototype.findHover=function(e,t){if(!this._disposed)return this.handler.findHover(e,t)},e.prototype.on=function(e,t,n){return this._disposed||this.handler.on(e,t,n),this},e.prototype.off=function(e,t){this._disposed||this.handler.off(e,t)},e.prototype.trigger=function(e,t){this._disposed||this.handler.trigger(e,t)},e.prototype.clear=function(){if(!this._disposed){for(var e=this.storage.getRoots(),t=0;t0){if(e<=a)return o;if(e>=r)return s}else{if(e>=a)return o;if(e<=r)return s}else{if(e===a)return o;if(e===r)return s}return(e-a)/l*p+o}function Vc(e,t){switch(e){case"center":case"middle":e="50%";break;case"left":case"top":e="0%";break;case"right":case"bottom":e="100%"}return Hr(e)?(n=e,n.replace(/^\s+|\s+$/g,"")).match(/%$/)?parseFloat(e)/100*t:parseFloat(e):null==e?NaN:+e;var n}function qc(e,t,n){return null==t&&(t=10),t=Math.min(Math.max(0,t),20),e=(+e).toFixed(t),n?e:+e}function Gc(e){return e.sort((function(e,t){return e-t})),e}function zc(e){if(e=+e,isNaN(e))return 0;if(e>1e-14)for(var t=1,n=0;n<15;n++,t*=10)if(Math.round(e*t)/t===e)return n;return Uc(e)}function Uc(e){var t=e.toString().toLowerCase(),n=t.indexOf("e"),i=n>0?+t.slice(n+1):0,a=n>0?n:t.length,r=t.indexOf("."),o=r<0?0:a-1-r;return Math.max(0,o-i)}function jc(e,t){var n=Math.log,i=Math.LN10,a=Math.floor(n(e[1]-e[0])/i),r=Math.round(n(Math.abs(t[1]-t[0]))/i),o=Math.min(Math.max(-a+r,0),20);return isFinite(o)?o:20}function Hc(e,t){var n=Nr(e,(function(e,t){return e+(isNaN(t)?0:t)}),0);if(0===n)return[];for(var i=Math.pow(10,t),a=Br(e,(function(e){return(isNaN(e)?0:e)/n*i*100})),r=100*i,o=Br(a,(function(e){return Math.floor(e)})),s=Nr(o,(function(e,t){return e+t}),0),l=Br(a,(function(e,t){return e-o[t]}));sp&&(p=l[d],c=d);++o[c],l[c]=0,++s}return Br(o,(function(e){return e/i}))}function Wc(e,t){var n=Math.max(zc(e),zc(t)),i=e+t;return n>20?i:qc(i,n)}function $c(e){var t=2*Math.PI;return(e%t+t)%t}function Kc(e){return e>-1e-4&&e=10&&t++,t}function Jc(e,t){var n=Qc(e),i=Math.pow(10,n),a=e/i;return e=(t?a<1.5?1:a<2.5?2:a<4?3:a<7?5:10:a<1?1:a<2?2:a<3?3:a<5?5:10)*i,n>=-20?+e.toFixed(n<0?-n:0):e}function ed(e){e.sort((function(e,t){return s(e,t,0)?-1:1}));for(var t=-1/0,n=1,i=0;i=0||a&&Dr(a,s)<0)){var l=n.getShallow(s,t);null!=l&&(r[e[o][0]]=l)}}return r}}var Ud=zd([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),jd=function(){function e(){}return e.prototype.getAreaStyle=function(e,t){return Ud(this,e,t)},e}(),Hd=new Ml(50);function Wd(e){if("string"==typeof e){var t=Hd.get(e);return t&&t.image}return e}function $d(e,t,n,i,a){if(e){if("string"==typeof e){if(t&&t.__zrImageSrc===e||!n)return t;var r=Hd.get(e),o={hostEl:n,cb:i,cbPayload:a};return r?!Yd(t=r.image)&&r.pending.push(o):((t=dr.loadImage(e,Kd,Kd)).__zrImageSrc=e,Hd.put(e,t.__cachedImgObj={image:t,pending:[o]})),t}return e}return t}function Kd(){var e=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var t=0;t=o;l++)s-=o;var p=cc(n,t);return p>s&&(n="",p=0),s=e-p,a.ellipsis=n,a.ellipsisWidth=p,a.contentWidth=s,a.containerWidth=e,a}function Jd(e,t){var n=t.containerWidth,i=t.font,a=t.contentWidth;if(!n)return"";var r=cc(e,i);if(r<=n)return e;for(var o=0;;o++){if(r<=a||o>=t.maxIterations){e+=t.ellipsis;break}var s=0===o?eu(e,a,t.ascCharWidth,t.cnCharWidth):r>0?Math.floor(e.length*a/r):0;r=cc(e=e.substr(0,s),i)}return""===e&&(e=t.placeholder),e}function eu(e,t,n,i){for(var a=0,r=0,o=e.length;r0&&h+i.accumWidth>i.width&&(r=t.split("\n"),d=!0),i.accumWidth=h}else{var g=su(t,c,i.width,i.breakAll,i.accumWidth);i.accumWidth=g.accumWidth+m,o=g.linesWidths,r=g.lines}}else r=t.split("\n");for(var f=0;f=32&&t<=591||t>=880&&t<=4351||t>=4608&&t<=5119||t>=7680&&t<=8303}(e)||!!ru[e]}function su(e,t,n,i,a){for(var r=[],o=[],s="",l="",p=0,c=0,d=0;dn:a+c+m>n)?c?(s||l)&&(h?(s||(s=l,l="",c=p=0),r.push(s),o.push(c-p),l+=u,s="",c=p+=m):(l&&(s+=l,l="",p=0),r.push(s),o.push(c),s=u,c=m)):h?(r.push(l),o.push(p),l=u,p=m):(r.push(u),o.push(m)):(c+=m,h?(l+=u,p+=m):(l&&(s+=l,l="",p=0),s+=u))}else l&&(s+=l,c+=p),r.push(s),o.push(c),s="",l="",p=0,c=0}return r.length||s||(s=e,l="",p=0),l&&(s+=l),s&&(r.push(s),o.push(c)),1===r.length&&(c+=a),{accumWidth:c,lines:r,linesWidths:o}}var lu="__zr_style_"+Math.round(10*Math.random()),pu={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},cu={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};pu[lu]=!0;var du=["z","z2","invisible"],uu=["invisible"],mu=function(e){function t(t){return e.call(this,t)||this}var n;return ze(t,e),t.prototype._init=function(t){for(var n=qr(t),i=0;i1e-4)return s[0]=e-n,s[1]=t-i,l[0]=e+n,void(l[1]=t+i);if(wu[0]=xu(a)*n+e,wu[1]=vu(a)*i+t,Su[0]=xu(r)*n+e,Su[1]=vu(r)*i+t,p(s,wu,Su),c(l,wu,Su),(a%=bu)<0&&(a+=bu),(r%=bu)<0&&(r+=bu),a>r&&!o?r+=bu:aa&&(Cu[0]=xu(m)*n+e,Cu[1]=vu(m)*i+t,p(s,Cu,s),c(l,Cu,l))}var Du={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},Ou=[],Au=[],Fu=[],Ru=[],Bu=[],Nu=[],Lu=Math.min,Vu=Math.max,qu=Math.cos,Gu=Math.sin,zu=Math.abs,Uu=Math.PI,ju=2*Uu,Hu="undefined"!=typeof Float32Array,Wu=[];function $u(e){return Math.round(e/Uu*1e8)/1e8%2*Uu}function Ku(e,t){var n=$u(e[0]);n<0&&(n+=ju);var i=n-e[0],a=e[1];a+=i,!t&&a-n>=ju?a=n+ju:t&&n-a>=ju?a=n-ju:!t&&n>a?a=n+(ju-$u(n-a)):t&&n0&&(this._ux=zu(n/Xp/e)||0,this._uy=zu(n/Xp/t)||0)},e.prototype.setDPR=function(e){this.dpr=e},e.prototype.setContext=function(e){this._ctx=e},e.prototype.getContext=function(){return this._ctx},e.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},e.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},e.prototype.moveTo=function(e,t){return this._drawPendingPt(),this.addData(Du.M,e,t),this._ctx&&this._ctx.moveTo(e,t),this._x0=e,this._y0=t,this._xi=e,this._yi=t,this},e.prototype.lineTo=function(e,t){var n=zu(e-this._xi),i=zu(t-this._yi),a=n>this._ux||i>this._uy;if(this.addData(Du.L,e,t),this._ctx&&a&&this._ctx.lineTo(e,t),a)this._xi=e,this._yi=t,this._pendingPtDist=0;else{var r=n*n+i*i;r>this._pendingPtDist&&(this._pendingPtX=e,this._pendingPtY=t,this._pendingPtDist=r)}return this},e.prototype.bezierCurveTo=function(e,t,n,i,a,r){return this._drawPendingPt(),this.addData(Du.C,e,t,n,i,a,r),this._ctx&&this._ctx.bezierCurveTo(e,t,n,i,a,r),this._xi=a,this._yi=r,this},e.prototype.quadraticCurveTo=function(e,t,n,i){return this._drawPendingPt(),this.addData(Du.Q,e,t,n,i),this._ctx&&this._ctx.quadraticCurveTo(e,t,n,i),this._xi=n,this._yi=i,this},e.prototype.arc=function(e,t,n,i,a,r){this._drawPendingPt(),Wu[0]=i,Wu[1]=a,Ku(Wu,r),i=Wu[0];var o=(a=Wu[1])-i;return this.addData(Du.A,e,t,n,n,i,o,0,r?0:1),this._ctx&&this._ctx.arc(e,t,n,i,a,r),this._xi=qu(a)*n+e,this._yi=Gu(a)*n+t,this},e.prototype.arcTo=function(e,t,n,i,a){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(e,t,n,i,a),this},e.prototype.rect=function(e,t,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.rect(e,t,n,i),this.addData(Du.R,e,t,n,i),this},e.prototype.closePath=function(){this._drawPendingPt(),this.addData(Du.Z);var e=this._ctx,t=this._x0,n=this._y0;return e&&e.closePath(),this._xi=t,this._yi=n,this},e.prototype.fill=function(e){e&&e.fill(),this.toStatic()},e.prototype.stroke=function(e){e&&e.stroke(),this.toStatic()},e.prototype.len=function(){return this._len},e.prototype.setData=function(e){var t=e.length;this.data&&this.data.length===t||!Hu||(this.data=new Float32Array(t));for(var n=0;np.length&&(this._expandData(),p=this.data);for(var c=0;c0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},e.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var e=[],t=0;t11&&(this.data=new Float32Array(e)))}},e.prototype.getBoundingRect=function(){Fu[0]=Fu[1]=Bu[0]=Bu[1]=Number.MAX_VALUE,Ru[0]=Ru[1]=Nu[0]=Nu[1]=-Number.MAX_VALUE;var e,t=this.data,n=0,i=0,a=0,r=0;for(e=0;en||zu(f)>i||d===t-1)&&(h=Math.sqrt(k*k+f*f),a=g,r=x);break;case Du.C:var y=e[d++],v=e[d++],x=(g=e[d++],e[d++]),b=e[d++],w=e[d++];h=fl(a,r,y,v,g,x,b,w,10),a=b,r=w;break;case Du.Q:h=Sl(a,r,y=e[d++],v=e[d++],g=e[d++],x=e[d++],10),a=g,r=x;break;case Du.A:var S=e[d++],C=e[d++],_=e[d++],T=e[d++],I=e[d++],E=e[d++],M=E+I;d+=1,m&&(o=qu(I)*_+S,s=Gu(I)*T+C),h=Vu(_,T)*Lu(ju,Math.abs(E)),a=qu(M)*_+S,r=Gu(M)*T+C;break;case Du.R:o=a=e[d++],s=r=e[d++],h=2*e[d++]+2*e[d++];break;case Du.Z:var k=o-a;f=s-r;h=Math.sqrt(k*k+f*f),a=o,r=s}h>=0&&(l[c++]=h,p+=h)}return this._pathLen=p,p},e.prototype.rebuildPath=function(e,t){var n,i,a,r,o,s,l,p,c,d,u=this.data,m=this._ux,h=this._uy,g=this._len,f=t<1,y=0,v=0,x=0;if(!f||(this._pathSegLen||this._calculateLength(),l=this._pathSegLen,p=t*this._pathLen))e:for(var b=0;b0&&(e.lineTo(c,d),x=0),w){case Du.M:n=a=u[b++],i=r=u[b++],e.moveTo(a,r);break;case Du.L:o=u[b++],s=u[b++];var C=zu(o-a),_=zu(s-r);if(C>m||_>h){if(f){if(y+(K=l[v++])>p){var T=(p-y)/K;e.lineTo(a*(1-T)+o*T,r*(1-T)+s*T);break e}y+=K}e.lineTo(o,s),a=o,r=s,x=0}else{var I=C*C+_*_;I>x&&(c=o,d=s,x=I)}break;case Du.C:var E=u[b++],M=u[b++],k=u[b++],P=u[b++],D=u[b++],O=u[b++];if(f){if(y+(K=l[v++])>p){hl(a,E,k,D,T=(p-y)/K,Ou),hl(r,M,P,O,T,Au),e.bezierCurveTo(Ou[1],Au[1],Ou[2],Au[2],Ou[3],Au[3]);break e}y+=K}e.bezierCurveTo(E,M,k,P,D,O),a=D,r=O;break;case Du.Q:E=u[b++],M=u[b++],k=u[b++],P=u[b++];if(f){if(y+(K=l[v++])>p){bl(a,E,k,T=(p-y)/K,Ou),bl(r,M,P,T,Au),e.quadraticCurveTo(Ou[1],Au[1],Ou[2],Au[2]);break e}y+=K}e.quadraticCurveTo(E,M,k,P),a=k,r=P;break;case Du.A:var A=u[b++],F=u[b++],R=u[b++],B=u[b++],N=u[b++],L=u[b++],V=u[b++],q=!u[b++],G=R>B?R:B,z=zu(R-B)>.001,U=N+L,j=!1;if(f)y+(K=l[v++])>p&&(U=N+L*(p-y)/K,j=!0),y+=K;if(z&&e.ellipse?e.ellipse(A,F,R,B,V,N,U,q):e.arc(A,F,G,N,U,q),j)break e;S&&(n=qu(N)*R+A,i=Gu(N)*B+F),a=qu(U)*R+A,r=Gu(U)*B+F;break;case Du.R:n=a=u[b],i=r=u[b+1],o=u[b++],s=u[b++];var H=u[b++],W=u[b++];if(f){if(y+(K=l[v++])>p){var $=p-y;e.moveTo(o,s),e.lineTo(o+Lu($,H),s),($-=H)>0&&e.lineTo(o+H,s+Lu($,W)),($-=W)>0&&e.lineTo(o+Vu(H-$,0),s+W),($-=H)>0&&e.lineTo(o,s+Vu(W-$,0));break e}y+=K}e.rect(o,s,H,W);break;case Du.Z:if(f){var K;if(y+(K=l[v++])>p){T=(p-y)/K;e.lineTo(a*(1-T)+n*T,r*(1-T)+i*T);break e}y+=K}e.closePath(),a=n,r=i}}},e.prototype.clone=function(){var t=new e,n=this.data;return t.data=n.slice?n.slice():Array.prototype.slice.call(n),t._len=this._len,t},e.CMD=Du,e.initDefaultProps=function(){var t=e.prototype;t._saveData=!0,t._ux=0,t._uy=0,t._pendingPtDist=0,t._version=0}(),e}();function Xu(e,t,n,i,a,r,o){if(0===a)return!1;var s=a,l=0;if(o>t+s&&o>i+s||oe+s&&r>n+s||rt+d&&c>i+d&&c>r+d&&c>s+d||ce+d&&p>n+d&&p>a+d&&p>o+d||pt+p&&l>i+p&&l>r+p||le+p&&s>n+p&&s>a+p||sn||c+pa&&(a+=tm);var u=Math.atan2(l,s);return u<0&&(u+=tm),u>=i&&u<=a||u+tm>=i&&u+tm<=a}function im(e,t,n,i,a,r){if(r>t&&r>i||ra?s:0}var am=Yu.CMD,rm=2*Math.PI;var om=[-1,-1,-1],sm=[-1,-1];function lm(e,t,n,i,a,r,o,s,l,p){if(p>t&&p>i&&p>r&&p>s||p1&&(c=void 0,c=sm[0],sm[0]=sm[1],sm[1]=c),h=cl(t,i,r,s,sm[0]),m>1&&(g=cl(t,i,r,s,sm[1]))),2===m?yt&&s>i&&s>r||s=0&&c<=1&&(a[l++]=c);else{var p=o*o-4*r*s;if(ll(p))(c=-o/(2*r))>=0&&c<=1&&(a[l++]=c);else if(p>0){var c,d=el(p),u=(-o-d)/(2*r);(c=(-o+d)/(2*r))>=0&&c<=1&&(a[l++]=c),u>=0&&u<=1&&(a[l++]=u)}}return l}(t,i,r,s,om);if(0===l)return 0;var p=xl(t,i,r);if(p>=0&&p<=1){for(var c=0,d=yl(t,i,r,p),u=0;un||s<-n)return 0;var l=Math.sqrt(n*n-s*s);om[0]=-l,om[1]=l;var p=Math.abs(i-a);if(p<1e-4)return 0;if(p>=rm-1e-4){i=0,a=rm;var c=r?1:-1;return o>=om[0]+e&&o<=om[1]+e?c:0}if(i>a){var d=i;i=a,a=d}i<0&&(i+=rm,a+=rm);for(var u=0,m=0;m<2;m++){var h=om[m];if(h+e>o){var g=Math.atan2(s,h);c=r?1:-1;g<0&&(g=rm+g),(g>=i&&g<=a||g+rm>=i&&g+rm<=a)&&(g>Math.PI/2&&g<1.5*Math.PI&&(c=-c),u+=c)}}return u}function dm(e,t,n,i,a){for(var r,o,s,l,p=e.data,c=e.len(),d=0,u=0,m=0,h=0,g=0,f=0;f1&&(n||(d+=im(u,m,h,g,i,a))),v&&(h=u=p[f],g=m=p[f+1]),y){case am.M:u=h=p[f++],m=g=p[f++];break;case am.L:if(n){if(Xu(u,m,p[f],p[f+1],t,i,a))return!0}else d+=im(u,m,p[f],p[f+1],i,a)||0;u=p[f++],m=p[f++];break;case am.C:if(n){if(Zu(u,m,p[f++],p[f++],p[f++],p[f++],p[f],p[f+1],t,i,a))return!0}else d+=lm(u,m,p[f++],p[f++],p[f++],p[f++],p[f],p[f+1],i,a)||0;u=p[f++],m=p[f++];break;case am.Q:if(n){if(Qu(u,m,p[f++],p[f++],p[f],p[f+1],t,i,a))return!0}else d+=pm(u,m,p[f++],p[f++],p[f],p[f+1],i,a)||0;u=p[f++],m=p[f++];break;case am.A:var x=p[f++],b=p[f++],w=p[f++],S=p[f++],C=p[f++],_=p[f++];f+=1;var T=!!(1-p[f++]);r=Math.cos(C)*w+x,o=Math.sin(C)*S+b,v?(h=r,g=o):d+=im(u,m,r,o,i,a);var I=(i-x)*S/w+x;if(n){if(nm(x,b,S,C,C+_,T,t,I,a))return!0}else d+=cm(x,b,S,C,C+_,T,I,a);u=Math.cos(C+_)*w+x,m=Math.sin(C+_)*S+b;break;case am.R:if(h=u=p[f++],g=m=p[f++],r=h+p[f++],o=g+p[f++],n){if(Xu(h,g,r,g,t,i,a)||Xu(r,g,r,o,t,i,a)||Xu(r,o,h,o,t,i,a)||Xu(h,o,h,g,t,i,a))return!0}else d+=im(r,g,r,o,i,a),d+=im(h,o,h,g,i,a);break;case am.Z:if(n){if(Xu(u,m,h,g,t,i,a))return!0}else d+=im(u,m,h,g,i,a);u=h,m=g}}return n||(s=m,l=g,Math.abs(s-l)<1e-4)||(d+=im(u,m,h,g,i,a)||0),0!==d}var um=Pr({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},pu),mm={style:Pr({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},cu.style)},hm=sc.concat(["invisible","culling","z","z2","zlevel","parent"]),gm=function(e){function t(t){return e.call(this,t)||this}var n;return ze(t,e),t.prototype.update=function(){var n=this;e.prototype.update.call(this);var i=this.style;if(i.decal){var a=this._decalEl=this._decalEl||new t;a.buildPath===t.prototype.buildPath&&(a.buildPath=function(e){n.buildPath(e,n.shape)}),a.silent=!0;var r=a.style;for(var o in i)r[o]!==i[o]&&(r[o]=i[o]);r.fill=i.fill?i.decal:null,r.decal=null,r.shadowColor=null,i.strokeFirst&&(r.stroke=null);for(var s=0;s.5?Zp:t>.2?"#eee":Qp}if(e)return Qp}return Zp},t.prototype.getInsideTextStroke=function(e){var t=this.style.fill;if(Hr(t)){var n=this.__zr;if(!(!n||!n.isDarkMode())===Yl(e,0)<.4)return t}},t.prototype.buildPath=function(e,t,n){},t.prototype.pathUpdated=function(){this.__dirty&=-5},t.prototype.getUpdatedPathProxy=function(e){return!this.path&&this.createPathProxy(),this.path.beginPath(),this.buildPath(this.path,this.shape,e),this.path},t.prototype.createPathProxy=function(){this.path=new Yu(!1)},t.prototype.hasStroke=function(){var e=this.style,t=e.stroke;return!(null==t||"none"===t||!(e.lineWidth>0))},t.prototype.hasFill=function(){var e=this.style.fill;return null!=e&&"none"!==e},t.prototype.getBoundingRect=function(){var e=this._rect,t=this.style,n=!e;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var a=this.path;(i||4&this.__dirty)&&(a.beginPath(),this.buildPath(a,this.shape,!1),this.pathUpdated()),e=a.getBoundingRect()}if(this._rect=e,this.hasStroke()&&this.path&&this.path.len()>0){var r=this._rectStroke||(this._rectStroke=e.clone());if(this.__dirty||n){r.copy(e);var o=t.strokeNoScale?this.getLineScale():1,s=t.lineWidth;if(!this.hasFill()){var l=this.strokeContainThreshold;s=Math.max(s,null==l?4:l)}o>1e-10&&(r.width+=s/o,r.height+=s/o,r.x-=s/o/2,r.y-=s/o/2)}return r}return e},t.prototype.contain=function(e,t){var n=this.transformCoordToLocal(e,t),i=this.getBoundingRect(),a=this.style;if(e=n[0],t=n[1],i.contain(e,t)){var r=this.path;if(this.hasStroke()){var o=a.lineWidth,s=a.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(this.hasFill()||(o=Math.max(o,this.strokeContainThreshold)),function(e,t,n,i){return dm(e,t,!0,n,i)}(r,o/s,e,t)))return!0}if(this.hasFill())return function(e,t,n){return dm(e,0,!1,t,n)}(r,e,t)}return!1},t.prototype.dirtyShape=function(){this.__dirty|=4,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},t.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},t.prototype.animateShape=function(e){return this.animate("shape",e)},t.prototype.updateDuringAnimation=function(e){"style"===e?this.dirtyStyle():"shape"===e?this.dirtyShape():this.markRedraw()},t.prototype.attrKV=function(t,n){"shape"===t?this.setShape(n):e.prototype.attrKV.call(this,t,n)},t.prototype.setShape=function(e,t){var n=this.shape;return n||(n=this.shape={}),"string"==typeof e?n[e]=t:kr(n,e),this.dirtyShape(),this},t.prototype.shapeChanged=function(){return!!(4&this.__dirty)},t.prototype.createStyle=function(e){return vo(um,e)},t.prototype._innerSaveToNormal=function(t){e.prototype._innerSaveToNormal.call(this,t);var n=this._normalState;t.shape&&!n.shape&&(n.shape=kr({},this.shape))},t.prototype._applyStateObj=function(t,n,i,a,r,o){e.prototype._applyStateObj.call(this,t,n,i,a,r,o);var s,l=!(n&&a);if(n&&n.shape?r?a?s=n.shape:(s=kr({},i.shape),kr(s,n.shape)):(s=kr({},a?this.shape:i.shape),kr(s,n.shape)):l&&(s=i.shape),s)if(r){this.shape=kr({},this.shape);for(var p={},c=qr(s),d=0;d0},t.prototype.hasFill=function(){var e=this.style.fill;return null!=e&&"none"!==e},t.prototype.createStyle=function(e){return vo(fm,e)},t.prototype.setBoundingRect=function(e){this._rect=e},t.prototype.getBoundingRect=function(){var e=this.style;if(!this._rect){var t=e.text;null!=t?t+="":t="";var n=uc(t,e.font,e.textAlign,e.textBaseline);if(n.x+=e.x||0,n.y+=e.y||0,this.hasStroke()){var i=e.lineWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect},t.initDefaultProps=void(t.prototype.dirtyRectTolerance=10),t}(mu);ym.prototype.type="tspan";var vm=Pr({x:0,y:0},pu),xm={style:Pr({x:!0,y:!0,width:!0,height:!0,sx:!0,sy:!0,sWidth:!0,sHeight:!0},cu.style)};var bm=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.createStyle=function(e){return vo(vm,e)},t.prototype._getSize=function(e){var t=this.style,n=t[e];if(null!=n)return n;var i,a=(i=t.image)&&"string"!=typeof i&&i.width&&i.height?t.image:this.__image;if(!a)return 0;var r="width"===e?"height":"width",o=t[r];return null==o?a[e]:a[e]/a[r]*o},t.prototype.getWidth=function(){return this._getSize("width")},t.prototype.getHeight=function(){return this._getSize("height")},t.prototype.getAnimationStyleProps=function(){return xm},t.prototype.getBoundingRect=function(){var e=this.style;return this._rect||(this._rect=new Ps(e.x||0,e.y||0,this.getWidth(),this.getHeight())),this._rect},t}(mu);bm.prototype.type="image";var wm=Math.round;function Sm(e,t,n){if(t){var i=t.x1,a=t.x2,r=t.y1,o=t.y2;e.x1=i,e.x2=a,e.y1=r,e.y2=o;var s=n&&n.lineWidth;return s?(wm(2*i)===wm(2*a)&&(e.x1=e.x2=_m(i,s,!0)),wm(2*r)===wm(2*o)&&(e.y1=e.y2=_m(r,s,!0)),e):e}}function Cm(e,t,n){if(t){var i=t.x,a=t.y,r=t.width,o=t.height;e.x=i,e.y=a,e.width=r,e.height=o;var s=n&&n.lineWidth;return s?(e.x=_m(i,s,!0),e.y=_m(a,s,!0),e.width=Math.max(_m(i+r,s,!1)-e.x,0===r?0:1),e.height=Math.max(_m(a+o,s,!1)-e.y,0===o?0:1),e):e}}function _m(e,t,n){if(!t)return e;var i=wm(2*e);return(i+wm(t))%2==0?i/2:(i+(n?1:-1))/2}var Tm=function(){this.x=0,this.y=0,this.width=0,this.height=0},Im={},Em=function(e){function t(t){return e.call(this,t)||this}return ze(t,e),t.prototype.getDefaultShape=function(){return new Tm},t.prototype.buildPath=function(e,t){var n,i,a,r;if(this.subPixelOptimize){var o=Cm(Im,t,this.style);n=o.x,i=o.y,a=o.width,r=o.height,o.r=t.r,t=o}else n=t.x,i=t.y,a=t.width,r=t.height;t.r?function(e,t){var n,i,a,r,o,s=t.x,l=t.y,p=t.width,c=t.height,d=t.r;p<0&&(s+=p,p=-p),c<0&&(l+=c,c=-c),"number"==typeof d?n=i=a=r=d:d instanceof Array?1===d.length?n=i=a=r=d[0]:2===d.length?(n=a=d[0],i=r=d[1]):3===d.length?(n=d[0],i=r=d[1],a=d[2]):(n=d[0],i=d[1],a=d[2],r=d[3]):n=i=a=r=0,n+i>p&&(n*=p/(o=n+i),i*=p/o),a+r>p&&(a*=p/(o=a+r),r*=p/o),i+a>c&&(i*=c/(o=i+a),a*=c/o),n+r>c&&(n*=c/(o=n+r),r*=c/o),e.moveTo(s+n,l),e.lineTo(s+p-i,l),0!==i&&e.arc(s+p-i,l+i,i,-Math.PI/2,0),e.lineTo(s+p,l+c-a),0!==a&&e.arc(s+p-a,l+c-a,a,0,Math.PI/2),e.lineTo(s+r,l+c),0!==r&&e.arc(s+r,l+c-r,r,Math.PI/2,Math.PI),e.lineTo(s,l+n),0!==n&&e.arc(s+n,l+n,n,Math.PI,1.5*Math.PI)}(e,t):e.rect(n,i,a,r)},t.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},t}(gm);Em.prototype.type="rect";var Mm={fill:"#000"},km={style:Pr({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},cu.style)},Pm=function(e){function t(t){var n=e.call(this)||this;return n.type="text",n._children=[],n._defaultStyle=Mm,n.attr(t),n}return ze(t,e),t.prototype.childrenRef=function(){return this._children},t.prototype.update=function(){e.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var t=0;tm&&c){var h=Math.floor(m/l);n=n.slice(0,h)}if(e&&o&&null!=d)for(var g=Qd(d,r,t.ellipsis,{minChar:t.truncateMinChar,placeholder:t.placeholder}),f=0;f0,T=null!=e.width&&("truncate"===e.overflow||"break"===e.overflow||"breakAll"===e.overflow),I=i.calculatedLineHeight,E=0;El&&au(n,e.substring(l,p),t,s),au(n,i[2],t,s,i[1]),l=Xd.lastIndex}lr){w>0?(v.tokens=v.tokens.slice(0,w),f(v,b,x),n.lines=n.lines.slice(0,y+1)):n.lines=n.lines.slice(0,y);break e}var E=S.width,M=null==E||"auto"===E;if("string"==typeof E&&"%"===E.charAt(E.length-1))O.percentWidth=E,c.push(O),O.contentWidth=cc(O.text,T);else{if(M){var k=S.backgroundColor,P=k&&k.image;P&&Yd(P=Wd(P))&&(O.width=Math.max(O.width,P.width*I/P.height))}var D=h&&null!=a?a-b:null;null!=D&&D=0&&"right"===(E=x[I]).align;)this._placeToken(E,e,w,h,T,"right",f),S-=E.width,T-=E.width,I--;for(_+=(n-(_-m)-(g-T)-S)/2;C<=I;)E=x[C],this._placeToken(E,e,w,h,_+E.width/2,"center",f),_+=E.width,C++;h+=w}},t.prototype._placeToken=function(e,t,n,i,a,r,o){var s=t.rich[e.styleName]||{};s.text=e.text;var l=e.verticalAlign,p=i+n/2;"top"===l?p=i+e.height/2:"bottom"===l&&(p=i+n-e.height/2),!e.isLineHolder&&zm(s)&&this._renderBackground(s,t,"right"===r?a-e.width:"center"===r?a-e.width/2:a,p-e.height/2,e.width,e.height);var c=!!s.backgroundColor,d=e.textPadding;d&&(a=qm(a,r,d),p-=e.height/2-d[0]-e.innerHeight/2);var u=this._getOrCreateChild(ym),m=u.createStyle();u.useStyle(m);var h=this._defaultStyle,g=!1,f=0,y=Vm("fill"in s?s.fill:"fill"in t?t.fill:(g=!0,h.fill)),v=Lm("stroke"in s?s.stroke:"stroke"in t?t.stroke:c||o||h.autoStroke&&!g?null:(f=2,h.stroke)),x=s.textShadowBlur>0||t.textShadowBlur>0;m.text=e.text,m.x=a,m.y=p,x&&(m.shadowBlur=s.textShadowBlur||t.textShadowBlur||0,m.shadowColor=s.textShadowColor||t.textShadowColor||"transparent",m.shadowOffsetX=s.textShadowOffsetX||t.textShadowOffsetX||0,m.shadowOffsetY=s.textShadowOffsetY||t.textShadowOffsetY||0),m.textAlign=r,m.textBaseline="middle",m.font=e.font||sr,m.opacity=ao(s.opacity,t.opacity,1),Rm(m,s),v&&(m.lineWidth=ao(s.lineWidth,t.lineWidth,f),m.lineDash=io(s.lineDash,t.lineDash),m.lineDashOffset=t.lineDashOffset||0,m.stroke=v),y&&(m.fill=y);var b=e.contentWidth,w=e.contentHeight;u.setBoundingRect(new Ps(mc(m.x,b,m.textAlign),hc(m.y,w,m.textBaseline),b,w))},t.prototype._renderBackground=function(e,t,n,i,a,r){var o,s,l,p=e.backgroundColor,c=e.borderWidth,d=e.borderColor,u=p&&p.image,m=p&&!u,h=e.borderRadius,g=this;if(m||e.lineHeight||c&&d){(o=this._getOrCreateChild(Em)).useStyle(o.createStyle()),o.style.fill=null;var f=o.shape;f.x=n,f.y=i,f.width=a,f.height=r,f.r=h,o.dirtyShape()}if(m)(l=o.style).fill=p||null,l.fillOpacity=io(e.fillOpacity,1);else if(u){(s=this._getOrCreateChild(bm)).onload=function(){g.dirtyStyle()};var y=s.style;y.image=p.image,y.x=n,y.y=i,y.width=a,y.height=r}c&&d&&((l=o.style).lineWidth=c,l.stroke=d,l.strokeOpacity=io(e.strokeOpacity,1),l.lineDash=e.borderDash,l.lineDashOffset=e.borderDashOffset||0,o.strokeContainThreshold=0,o.hasFill()&&o.hasStroke()&&(l.strokeFirst=!0,l.lineWidth*=2));var v=(o||s).style;v.shadowBlur=e.shadowBlur||0,v.shadowColor=e.shadowColor||"transparent",v.shadowOffsetX=e.shadowOffsetX||0,v.shadowOffsetY=e.shadowOffsetY||0,v.opacity=ao(e.opacity,t.opacity,1)},t.makeFont=function(e){var t="";return Bm(e)&&(t=[e.fontStyle,e.fontWeight,Fm(e.fontSize),e.fontFamily||"sans-serif"].join(" ")),t&&lo(t)||e.textFont||e.font},t}(mu),Dm={left:!0,right:1,center:1},Om={top:1,bottom:1,middle:1},Am=["fontStyle","fontWeight","fontSize","fontFamily"];function Fm(e){return"string"!=typeof e||-1===e.indexOf("px")&&-1===e.indexOf("rem")&&-1===e.indexOf("em")?isNaN(+e)?"12px":e+"px":e}function Rm(e,t){for(var n=0;n=0,r=!1;if(e instanceof gm){var o=$m(e),s=a&&o.selectFill||o.normalFill,l=a&&o.selectStroke||o.normalStroke;if(ih(s)||ih(l)){var p=(i=i||{}).style||{};"inherit"===p.fill?(r=!0,i=kr({},i),(p=kr({},p)).fill=s):!ih(p.fill)&&ih(s)?(r=!0,i=kr({},i),(p=kr({},p)).fill=Zl(s)):!ih(p.stroke)&&ih(l)&&(r||(i=kr({},i),p=kr({},p)),p.stroke=Zl(l)),i.style=p}}if(i&&null==i.z2){r||(i=kr({},i));var c=e.z2EmphasisLift;i.z2=e.z2+(null!=c?c:Zm)}return i}(this,0,t,n);if("blur"===e)return function(e,t,n){var i=Dr(e.currentStates,t)>=0,a=e.style.opacity,r=i?null:function(e,t,n,i){for(var a=e.style,r={},o=0;o0){var r={dataIndex:a,seriesIndex:e.seriesIndex};null!=i&&(r.dataType=i),t.push(r)}}))})),t}function Dh(e,t,n){Nh(e,!0),uh(e,gh),Ah(e,t,n)}function Oh(e,t,n,i){i?function(e){Nh(e,!1)}(e):Dh(e,t,n)}function Ah(e,t,n){var i=Um(e);null!=t?(i.focus=t,i.blurScope=n):i.focus&&(i.focus=null)}var Fh=["emphasis","blur","select"],Rh={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function Bh(e,t,n,i){n=n||"itemStyle";for(var a=0;a1&&(o*=Wh(h),s*=Wh(h));var g=(a===r?-1:1)*Wh((o*o*(s*s)-o*o*(m*m)-s*s*(u*u))/(o*o*(m*m)+s*s*(u*u)))||0,f=g*o*m/s,y=g*-s*u/o,v=(e+n)/2+Kh(d)*f-$h(d)*y,x=(t+i)/2+$h(d)*f+Kh(d)*y,b=Qh([1,0],[(u-f)/o,(m-y)/s]),w=[(u-f)/o,(m-y)/s],S=[(-1*u-f)/o,(-1*m-y)/s],C=Qh(w,S);if(Zh(w,S)<=-1&&(C=Yh),Zh(w,S)>=1&&(C=0),C<0){var _=Math.round(C/Yh*1e6)/1e6;C=2*Yh+_%2*Yh}c.addData(p,v,x,o,s,b,C,d,r)}var eg=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,tg=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;var ng=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.applyTransform=function(e){},t}(gm);function ig(e){return null!=e.setData}function ag(e,t){var n=function(e){var t=new Yu;if(!e)return t;var n,i=0,a=0,r=i,o=a,s=Yu.CMD,l=e.match(eg);if(!l)return t;for(var p=0;pP*P+D*D&&(_=I,T=E),{cx:_,cy:T,x0:-c,y0:-d,x1:_*(a/w-1),y1:T*(a/w-1)}}function Sg(e,t){var n,i=vg(t.r,0),a=vg(t.r0||0,0),r=i>0;if(r||a>0){if(r||(i=a,a=0),a>i){var o=i;i=a,a=o}var s=t.startAngle,l=t.endAngle;if(!isNaN(s)&&!isNaN(l)){var p=t.cx,c=t.cy,d=!!t.clockwise,u=fg(l-s),m=u>dg&&u%dg;if(m>bg&&(u=m),i>bg)if(u>dg-bg)e.moveTo(p+i*mg(s),c+i*ug(s)),e.arc(p,c,i,s,l,!d),a>bg&&(e.moveTo(p+a*mg(l),c+a*ug(l)),e.arc(p,c,a,l,s,d));else{var h=void 0,g=void 0,f=void 0,y=void 0,v=void 0,x=void 0,b=void 0,w=void 0,S=void 0,C=void 0,_=void 0,T=void 0,I=void 0,E=void 0,M=void 0,k=void 0,P=i*mg(s),D=i*ug(s),O=a*mg(l),A=a*ug(l),F=u>bg;if(F){var R=t.cornerRadius;R&&(n=function(e){var t;if(Ur(e)){var n=e.length;if(!n)return e;t=1===n?[e[0],e[0],0,0]:2===n?[e[0],e[0],e[1],e[1]]:3===n?e.concat(e[2]):e}else t=[e,e,e,e];return t}(R),h=n[0],g=n[1],f=n[2],y=n[3]);var B=fg(i-a)/2;if(v=xg(B,f),x=xg(B,y),b=xg(B,h),w=xg(B,g),_=S=vg(v,x),T=C=vg(b,w),(S>bg||C>bg)&&(I=i*mg(l),E=i*ug(l),M=a*mg(s),k=a*ug(s),ubg){var j=xg(f,_),H=xg(y,_),W=wg(M,k,P,D,i,j,d),$=wg(I,E,O,A,i,H,d);e.moveTo(p+W.cx+W.x0,c+W.cy+W.y0),_0&&e.arc(p+W.cx,c+W.cy,j,gg(W.y0,W.x0),gg(W.y1,W.x1),!d),e.arc(p,c,i,gg(W.cy+W.y1,W.cx+W.x1),gg($.cy+$.y1,$.cx+$.x1),!d),H>0&&e.arc(p+$.cx,c+$.cy,H,gg($.y1,$.x1),gg($.y0,$.x0),!d))}else e.moveTo(p+P,c+D),e.arc(p,c,i,s,l,!d);else e.moveTo(p+P,c+D);if(a>bg&&F)if(T>bg){j=xg(h,T),W=wg(O,A,I,E,a,-(H=xg(g,T)),d),$=wg(P,D,M,k,a,-j,d);e.lineTo(p+W.cx+W.x0,c+W.cy+W.y0),T0&&e.arc(p+W.cx,c+W.cy,H,gg(W.y0,W.x0),gg(W.y1,W.x1),!d),e.arc(p,c,a,gg(W.cy+W.y1,W.cx+W.x1),gg($.cy+$.y1,$.cx+$.x1),d),j>0&&e.arc(p+$.cx,c+$.cy,j,gg($.y1,$.x1),gg($.y0,$.x0),!d))}else e.lineTo(p+O,c+A),e.arc(p,c,a,l,s,d);else e.lineTo(p+O,c+A)}else e.moveTo(p,c);e.closePath()}}}var Cg=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0,this.cornerRadius=0},_g=function(e){function t(t){return e.call(this,t)||this}return ze(t,e),t.prototype.getDefaultShape=function(){return new Cg},t.prototype.buildPath=function(e,t){Sg(e,t)},t.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},t}(gm);_g.prototype.type="sector";var Tg=function(){this.cx=0,this.cy=0,this.r=0,this.r0=0},Ig=function(e){function t(t){return e.call(this,t)||this}return ze(t,e),t.prototype.getDefaultShape=function(){return new Tg},t.prototype.buildPath=function(e,t){var n=t.cx,i=t.cy,a=2*Math.PI;e.moveTo(n+t.r,i),e.arc(n,i,t.r,0,a,!1),e.moveTo(n+t.r0,i),e.arc(n,i,t.r0,0,a,!0)},t}(gm);function Eg(e,t,n){var i=t.smooth,a=t.points;if(a&&a.length>=2){if(i){var r=function(e,t,n,i){var a,r,o,s,l=[],p=[],c=[],d=[];if(i){o=[1/0,1/0],s=[-1/0,-1/0];for(var u=0,m=e.length;uWg[1]){if(o=!1,a)return o;var p=Math.abs(Wg[0]-Hg[1]),c=Math.abs(Hg[0]-Wg[1]);Math.min(p,c)>i.len()&&(p0){var d={duration:c.duration,delay:c.delay||0,easing:c.easing,done:r,force:!!r||!!o,setToFinal:!p,scope:e,during:o};l?t.animateFrom(n,d):t.animateTo(n,d)}else t.stopAnimation(),!l&&t.attr(n),o&&o(1),r&&r()}function tf(e,t,n,i,a,r){ef("update",e,t,n,i,a,r)}function nf(e,t,n,i,a,r){ef("enter",e,t,n,i,a,r)}function af(e){if(!e.__zr)return!0;for(var t=0;tMath.abs(r[1])?r[0]>0?"right":"left":r[1]>0?"bottom":"top"}function Ef(e){return!e.isGroup}function Mf(e,t,n){if(e&&t){var i,a=(i={},e.traverse((function(e){Ef(e)&&e.anid&&(i[e.anid]=e)})),i);t.traverse((function(e){if(Ef(e)&&e.anid){var t=a[e.anid];if(t){var i=r(e);e.attr(r(t)),tf(e,i,n,Um(e).dataIndex)}}}))}function r(e){var t={x:e.x,y:e.y,rotation:e.rotation};return function(e){return null!=e.shape}(e)&&(t.shape=kr({},e.shape)),t}}function kf(e,t){return Br(e,(function(e){var n=e[0];n=pf(n,t.x),n=cf(n,t.x+t.width);var i=e[1];return i=pf(i,t.y),[n,i=cf(i,t.y+t.height)]}))}function Pf(e,t){var n=pf(e.x,t.x),i=cf(e.x+e.width,t.x+t.width),a=pf(e.y,t.y),r=cf(e.y+e.height,t.y+t.height);if(i>=n&&r>=a)return{x:n,y:a,width:i-n,height:r-a}}function Df(e,t,n){var i=kr({rectHover:!0},t),a=i.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},e)return 0===e.indexOf("image://")?(a.image=e.slice(8),Pr(a,n),new bm(i)):yf(e.replace("path://",""),i,n,"center")}function Of(e,t,n,i,a){for(var r=0,o=a[a.length-1];r=-1e-6)return!1;var h=e-a,g=t-r,f=Ff(h,g,p,c)/m;if(f<0||f>1)return!1;var y=Ff(h,g,d,u)/m;return!(y<0||y>1)}function Ff(e,t,n,i){return e*i-n*t}function Rf(e){var t=e.itemTooltipOption,n=e.componentModel,i=e.itemName,a=Hr(t)?{formatter:t}:t,r=n.mainType,o=n.componentIndex,s={componentType:r,name:i,$vars:["name"]};s[r+"Index"]=o;var l=e.formatterParamsExtra;l&&Rr(qr(l),(function(e){bo(s,e)||(s[e]=l[e],s.$vars.push(e))}));var p=Um(e.el);p.componentMainType=r,p.componentIndex=o,p.tooltipConfig={name:i,option:Pr({content:i,formatterParams:s},a)}}function Bf(e,t){var n;e.isGroup&&(n=t(e)),n||e.traverse(t)}function Nf(e,t){if(e)if(Ur(e))for(var n=0;n-1?fy:vy;function Sy(e,t){e=e.toUpperCase(),by[e]=new uy(t),xy[e]=t}function Cy(e){return by[e]}Sy(yy,{time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst",custom:"Custom chart",chart:"Chart"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}}),Sy(fy,{time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图",custom:"自定义图表",chart:"图表"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}});var _y=1e3,Ty=6e4,Iy=36e5,Ey=864e5,My=31536e6,ky={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}",none:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},Py="{yyyy}-{MM}-{dd}",Dy={year:"{yyyy}",month:"{yyyy}-{MM}",day:Py,hour:Py+" "+ky.hour,minute:Py+" "+ky.minute,second:Py+" "+ky.second,millisecond:ky.none},Oy=["year","month","day","hour","minute","second","millisecond"],Ay=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function Fy(e,t){return"0000".substr(0,t-(e+="").length)+e}function Ry(e){switch(e){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return e}}function By(e){return e===Ry(e)}function Ny(e,t,n,i){var a=Xc(e),r=a[qy(n)](),o=a[Gy(n)]()+1,s=Math.floor((o-1)/3)+1,l=a[zy(n)](),p=a["get"+(n?"UTC":"")+"Day"](),c=a[Uy(n)](),d=(c-1)%12+1,u=a[jy(n)](),m=a[Hy(n)](),h=a[Wy(n)](),g=(i instanceof uy?i:Cy(i||wy)||by[vy]).getModel("time"),f=g.get("month"),y=g.get("monthAbbr"),v=g.get("dayOfWeek"),x=g.get("dayOfWeekAbbr");return(t||"").replace(/{yyyy}/g,r+"").replace(/{yy}/g,Fy(r%100+"",2)).replace(/{Q}/g,s+"").replace(/{MMMM}/g,f[o-1]).replace(/{MMM}/g,y[o-1]).replace(/{MM}/g,Fy(o,2)).replace(/{M}/g,o+"").replace(/{dd}/g,Fy(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,v[p]).replace(/{ee}/g,x[p]).replace(/{e}/g,p+"").replace(/{HH}/g,Fy(c,2)).replace(/{H}/g,c+"").replace(/{hh}/g,Fy(d+"",2)).replace(/{h}/g,d+"").replace(/{mm}/g,Fy(u,2)).replace(/{m}/g,u+"").replace(/{ss}/g,Fy(m,2)).replace(/{s}/g,m+"").replace(/{SSS}/g,Fy(h,3)).replace(/{S}/g,h+"")}function Ly(e,t){var n=Xc(e),i=n[Gy(t)]()+1,a=n[zy(t)](),r=n[Uy(t)](),o=n[jy(t)](),s=n[Hy(t)](),l=0===n[Wy(t)](),p=l&&0===s,c=p&&0===o,d=c&&0===r,u=d&&1===a;return u&&1===i?"year":u?"month":d?"day":c?"hour":p?"minute":l?"second":"millisecond"}function Vy(e,t,n){var i=$r(e)?Xc(e):e;switch(t=t||Ly(e,n)){case"year":return i[qy(n)]();case"half-year":return i[Gy(n)]()>=6?1:0;case"quarter":return Math.floor((i[Gy(n)]()+1)/4);case"month":return i[Gy(n)]();case"day":return i[zy(n)]();case"half-day":return i[Uy(n)]()/24;case"hour":return i[Uy(n)]();case"minute":return i[jy(n)]();case"second":return i[Hy(n)]();case"millisecond":return i[Wy(n)]()}}function qy(e){return e?"getUTCFullYear":"getFullYear"}function Gy(e){return e?"getUTCMonth":"getMonth"}function zy(e){return e?"getUTCDate":"getDate"}function Uy(e){return e?"getUTCHours":"getHours"}function jy(e){return e?"getUTCMinutes":"getMinutes"}function Hy(e){return e?"getUTCSeconds":"getSeconds"}function Wy(e){return e?"getUTCMilliseconds":"getMilliseconds"}function $y(e){return e?"setUTCFullYear":"setFullYear"}function Ky(e){return e?"setUTCMonth":"setMonth"}function Yy(e){return e?"setUTCDate":"setDate"}function Xy(e){return e?"setUTCHours":"setHours"}function Zy(e){return e?"setUTCMinutes":"setMinutes"}function Qy(e){return e?"setUTCSeconds":"setSeconds"}function Jy(e){return e?"setUTCMilliseconds":"setMilliseconds"}function ev(e){if(!nd(e))return Hr(e)?e:"-";var t=(e+"").split(".");return t[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(t.length>1?"."+t[1]:"")}function tv(e,t){return e=(e||"").toLowerCase().replace(/-(.)/g,(function(e,t){return t.toUpperCase()})),t&&e&&(e=e.charAt(0).toUpperCase()+e.slice(1)),e}var nv=oo;function iv(e,t,n){function i(e){return e&&lo(e)?e:"-"}function a(e){return!(null==e||isNaN(e)||!isFinite(e))}var r="time"===t,o=e instanceof Date;if(r||o){var s=r?Xc(e):e;if(!isNaN(+s))return Ny(s,"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}",n);if(o)return"-"}if("ordinal"===t)return Wr(e)?i(e):$r(e)&&a(e)?e+"":"-";var l=td(e);return a(l)?ev(l):Wr(e)?i(e):"boolean"==typeof e?e+"":"-"}var av=["a","b","c","d","e","f","g"],rv=function(e,t){return"{"+e+(null==t?"":t)+"}"};function ov(e,t,n){Ur(t)||(t=[t]);var i=t.length;if(!i)return"";for(var a=t[0].$vars||[],r=0;r':'':{renderMode:r,content:"{"+(n.markerId||"markerX")+"|} ",style:"subItem"===a?{width:4,height:4,borderRadius:2,backgroundColor:i}:{width:10,height:10,borderRadius:5,backgroundColor:i}}:""}function lv(e,t){return t=t||"transparent",Hr(e)?e:Kr(e)&&e.colorStops&&(e.colorStops[0]||{}).color||t}function pv(e,t){if("_blank"===t||"blank"===t){var n=window.open();n.opener=null,n.location.href=e}else window.open(e,t)}var cv=Rr,dv=["left","right","top","bottom","width","height"],uv=[["width","left","right"],["height","top","bottom"]];function mv(e,t,n,i,a){var r=0,o=0;null==i&&(i=1/0),null==a&&(a=1/0);var s=0;t.eachChild((function(l,p){var c,d,u=l.getBoundingRect(),m=t.childAt(p+1),h=m&&m.getBoundingRect();if("horizontal"===e){var g=u.width+(h?-h.x+u.x:0);(c=r+g)>i||l.newline?(r=0,c=g,o+=s+n,s=u.height):s=Math.max(s,u.height)}else{var f=u.height+(h?-h.y+u.y:0);(d=o+f)>a||l.newline?(r+=s+n,o=0,d=f,s=u.width):s=Math.max(s,u.width)}l.newline||(l.x=r,l.y=o,l.markRedraw(),"horizontal"===e?r=c+n:o=d+n)}))}var hv=mv;zr(mv,"vertical"),zr(mv,"horizontal");function gv(e,t,n){n=nv(n||0);var i=t.width,a=t.height,r=Vc(e.left,i),o=Vc(e.top,a),s=Vc(e.right,i),l=Vc(e.bottom,a),p=Vc(e.width,i),c=Vc(e.height,a),d=n[2]+n[0],u=n[1]+n[3],m=e.aspect;switch(isNaN(p)&&(p=i-s-u-r),isNaN(c)&&(c=a-l-d-o),null!=m&&(isNaN(p)&&isNaN(c)&&(m>i/a?p=.8*i:c=.8*a),isNaN(p)&&(p=m*c),isNaN(c)&&(c=p/m)),isNaN(r)&&(r=i-s-p-u),isNaN(o)&&(o=a-l-c-d),e.left||e.right){case"center":r=i/2-p/2-n[3];break;case"right":r=i-p-u}switch(e.top||e.bottom){case"middle":case"center":o=a/2-c/2-n[0];break;case"bottom":o=a-c-d}r=r||0,o=o||0,isNaN(p)&&(p=i-u-r-(s||0)),isNaN(c)&&(c=a-d-o-(l||0));var h=new Ps(r+n[3],o+n[0],p,c);return h.margin=n,h}function fv(e,t,n,i,a,r){var o,s=!a||!a.hv||a.hv[0],l=!a||!a.hv||a.hv[1],p=a&&a.boundingMode||"all";if((r=r||e).x=e.x,r.y=e.y,!s&&!l)return!1;if("raw"===p)o="group"===e.type?new Ps(0,0,+t.width||0,+t.height||0):e.getBoundingRect();else if(o=e.getBoundingRect(),e.needLocalTransform()){var c=e.getLocalTransform();(o=o.clone()).applyTransform(c)}var d=gv(Pr({width:o.width,height:o.height},t),n,i),u=s?d.x-o.x:0,m=l?d.y-o.y:0;return"raw"===p?(r.x=u,r.y=m):(r.x+=u,r.y+=m),r===e&&e.markRedraw(),!0}function yv(e){var t=e.layoutMode||e.constructor.layoutMode;return Kr(t)?t:t?{type:t}:null}function vv(e,t,n){var i=n&&n.ignoreSize;!Ur(i)&&(i=[i,i]);var a=o(uv[0],0),r=o(uv[1],1);function o(n,a){var r={},o=0,p={},c=0;if(cv(n,(function(t){p[t]=e[t]})),cv(n,(function(e){s(t,e)&&(r[e]=p[e]=t[e]),l(r,e)&&o++,l(p,e)&&c++})),i[a])return l(t,n[1])?p[n[2]]=null:l(t,n[2])&&(p[n[1]]=null),p;if(2!==c&&o){if(o>=2)return r;for(var d=0;d=0;o--)r=Er(r,n[o],!0);t.defaultOption=r}return t.defaultOption},t.prototype.getReferringComponents=function(e,t){var n=e+"Index",i=e+"Id";return kd(this.ecModel,e,{index:this.get(n,!0),id:this.get(i,!0)},t)},t.prototype.getBoxLayoutParams=function(){var e=this;return{left:e.get("left"),top:e.get("top"),right:e.get("right"),bottom:e.get("bottom"),width:e.get("width"),height:e.get("height")}},t.prototype.getZLevelKey=function(){return""},t.prototype.setZLevel=function(e){this.option.zlevel=e},t.protoInitialize=function(){var e=t.prototype;e.type="component",e.id="",e.name="",e.mainType="",e.subType="",e.componentIndex=0}(),t}(uy);Nd(Sv,uy),Gd(Sv),function(e){var t={};e.registerSubTypeDefaulter=function(e,n){var i=Rd(e);t[i.main]=n},e.determineSubType=function(n,i){var a=i.type;if(!a){var r=Rd(n).main;e.hasSubTypes(n)&&t[r]&&(a=t[r](i))}return a}}(Sv),function(e,t){function n(e,t){return e[t]||(e[t]={predecessor:[],successor:[]}),e[t]}e.topologicalTravel=function(e,i,a,r){if(e.length){var o=function(e){var i={},a=[];return Rr(e,(function(r){var o=n(i,r),s=function(e,t){var n=[];return Rr(e,(function(e){Dr(t,e)>=0&&n.push(e)})),n}(o.originalDeps=t(r),e);o.entryCount=s.length,0===o.entryCount&&a.push(r),Rr(s,(function(e){Dr(o.predecessor,e)<0&&o.predecessor.push(e);var t=n(i,e);Dr(t.successor,e)<0&&t.successor.push(r)}))})),{graph:i,noEntryList:a}}(i),s=o.graph,l=o.noEntryList,p={};for(Rr(e,(function(e){p[e]=!0}));l.length;){var c=l.pop(),d=s[c],u=!!p[c];u&&(a.call(r,c,d.originalDeps.slice()),delete p[c]),Rr(d.successor,u?h:m)}Rr(p,(function(){var e="";throw new Error(e)}))}function m(e){s[e].entryCount--,0===s[e].entryCount&&l.push(e)}function h(e){p[e]=!0,m(e)}}}(Sv,(function(e){var t=[];Rr(Sv.getClassesByMainType(e),(function(e){t=t.concat(e.dependencies||e.prototype.dependencies||[])})),t=Br(t,(function(e){return Rd(e).main})),"dataset"!==e&&Dr(t,"dataset")<=0&&t.unshift("dataset");return t}));var Cv="";"undefined"!=typeof navigator&&(Cv=navigator.platform||"");var _v="rgba(0, 0, 0, 0.2)",Tv={darkMode:"auto",colorBy:"series",color:["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],gradientColor:["#f6efa6","#d88273","#bf444c"],aria:{decal:{decals:[{color:_v,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:_v,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:_v,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:_v,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:_v,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:_v,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:Cv.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},Iv=fo(["tooltip","label","itemName","itemId","itemGroupId","itemChildGroupId","seriesName"]),Ev="original",Mv="arrayRows",kv="objectRows",Pv="keyedColumns",Dv="typedArray",Ov="unknown",Av="column",Fv="row",Rv=1,Bv=2,Nv=3,Lv=Cd();function Vv(e,t,n){var i={},a=Gv(t);if(!a||!e)return i;var r,o,s=[],l=[],p=t.ecModel,c=Lv(p).datasetMap,d=a.uid+"_"+n.seriesLayoutBy;Rr(e=e.slice(),(function(t,n){var a=Kr(t)?t:e[n]={name:t};"ordinal"===a.type&&null==r&&(r=n,o=h(a)),i[a.name]=[]}));var u=c.get(d)||c.set(d,{categoryWayDim:o,valueWayDim:0});function m(e,t,n){for(var i=0;it)return e[i];return e[n-1]}(i,o):n;if((c=c||n)&&c.length){var d=c[l];return a&&(p[a]=d),s.paletteIdx=(l+1)%c.length,d}}var Jv="\0_ec_inner";var ex=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.init=function(e,t,n,i,a,r){i=i||{},this.option=null,this._theme=new uy(i),this._locale=new uy(a),this._optionManager=r},t.prototype.setOption=function(e,t,n){var i=ix(t);this._optionManager.setOption(e,n,i),this._resetOption(null,i)},t.prototype.resetOption=function(e,t){return this._resetOption(e,ix(t))},t.prototype._resetOption=function(e,t){var n=!1,i=this._optionManager;if(!e||"recreate"===e){var a=i.mountOption("recreate"===e);0,this.option&&"recreate"!==e?(this.restoreData(),this._mergeOption(a,t)):$v(this,a),n=!0}if("timeline"!==e&&"media"!==e||this.restoreData(),!e||"recreate"===e||"timeline"===e){var r=i.getTimelineOption(this);r&&(n=!0,this._mergeOption(r,t))}if(!e||"recreate"===e||"media"===e){var o=i.getMediaOption(this);o.length&&Rr(o,(function(e){n=!0,this._mergeOption(e,t)}),this)}return n},t.prototype.mergeOption=function(e){this._mergeOption(e,null)},t.prototype._mergeOption=function(e,t){var n=this.option,i=this._componentsMap,a=this._componentsCount,r=[],o=fo(),s=t&&t.replaceMergeMainTypeMap;Lv(this).datasetMap=fo(),Rr(e,(function(e,t){null!=e&&(Sv.hasClass(t)?t&&(r.push(t),o.set(t,!0)):n[t]=null==n[t]?Ir(e):Er(n[t],e,!0))})),s&&s.each((function(e,t){Sv.hasClass(t)&&!o.get(t)&&(r.push(t),o.set(t,!0))})),Sv.topologicalTravel(r,Sv.getAllClassMainTypes(),(function(t){var r=function(e,t,n){var i=jv.get(t);if(!i)return n;var a=i(e);return a?n.concat(a):n}(this,t,dd(e[t])),o=i.get(t),l=o?s&&s.get(t)?"replaceMerge":"normalMerge":"replaceAll",p=fd(o,r,l);(function(e,t,n){Rr(e,(function(e){var i=e.newOption;Kr(i)&&(e.keyInfo.mainType=t,e.keyInfo.subType=function(e,t,n,i){return t.type?t.type:n?n.subType:i.determineSubType(e,t)}(t,i,e.existing,n))}))})(p,t,Sv),n[t]=null,i.set(t,null),a.set(t,0);var c,d=[],u=[],m=0;Rr(p,(function(e,n){var i=e.existing,a=e.newOption;if(a){var r="series"===t,o=Sv.getClass(t,e.keyInfo.subType,!r);if(!o)return;if("tooltip"===t){if(c)return void 0;c=!0}if(i&&i.constructor===o)i.name=e.keyInfo.name,i.mergeOption(a,this),i.optionUpdated(a,!1);else{var s=kr({componentIndex:n},e.keyInfo);kr(i=new o(a,this,this,s),s),e.brandNew&&(i.__requireNewView=!0),i.init(a,this,this),i.optionUpdated(null,!0)}}else i&&(i.mergeOption({},this),i.optionUpdated({},!1));i?(d.push(i.option),u.push(i),m++):(d.push(void 0),u.push(void 0))}),this),n[t]=d,i.set(t,u),a.set(t,m),"series"===t&&Hv(this)}),this),this._seriesIndices||Hv(this)},t.prototype.getOption=function(){var e=Ir(this.option);return Rr(e,(function(t,n){if(Sv.hasClass(n)){for(var i=dd(t),a=i.length,r=!1,o=a-1;o>=0;o--)i[o]&&!wd(i[o])?r=!0:(i[o]=null,!r&&a--);i.length=a,e[n]=i}})),delete e[Jv],e},t.prototype.getTheme=function(){return this._theme},t.prototype.getLocaleModel=function(){return this._locale},t.prototype.setUpdatePayload=function(e){this._payload=e},t.prototype.getUpdatePayload=function(){return this._payload},t.prototype.getComponent=function(e,t){var n=this._componentsMap.get(e);if(n){var i=n[t||0];if(i)return i;if(null==t)for(var a=0;a=t:"max"===n?e<=t:e===t})(i[o],e,r)||(a=!1)}})),a}var dx=Rr,ux=Kr,mx=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function hx(e){var t=e&&e.itemStyle;if(t)for(var n=0,i=mx.length;n=0;g--){var f=e[g];if(s||(u=f.data.rawIndexOf(f.stackedByDimension,d)),u>=0){var y=f.data.getByRawIndex(f.stackResultDimension,u);if("all"===l||"positive"===l&&y>0||"negative"===l&&y<0||"samesign"===l&&m>=0&&y>0||"samesign"===l&&m<=0&&y<0){m=Wc(m,y),h=y;break}}}return i[0]=m,i[1]=h,i}))}))}var Ax,Fx,Rx,Bx,Nx,Lx=function(e){this.data=e.data||(e.sourceFormat===Pv?{}:[]),this.sourceFormat=e.sourceFormat||Ov,this.seriesLayoutBy=e.seriesLayoutBy||Av,this.startIndex=e.startIndex||0,this.dimensionsDetectedCount=e.dimensionsDetectedCount,this.metaRawOption=e.metaRawOption;var t=this.dimensionsDefine=e.dimensionsDefine;if(t)for(var n=0;np&&(p=m)}s[0]=l,s[1]=p}},i=function(){return this._data?this._data.length/this._dimSize:0};function a(e){for(var t=0;t=0&&(s=r.interpolatedValue[l])}return null!=s?s+"":""})):void 0},e.prototype.getRawValue=function(e,t){return ib(this.getData(t),e)},e.prototype.formatTooltip=function(e,t,n){},e}();function ob(e){var t,n;return Kr(e)?e.type&&(n=e):t=e,{text:t,frag:n}}function sb(e){return new lb(e)}var lb=function(){function e(e){e=e||{},this._reset=e.reset,this._plan=e.plan,this._count=e.count,this._onDirty=e.onDirty,this._dirty=!0}return e.prototype.perform=function(e){var t,n=this._upstream,i=e&&e.skip;if(this._dirty&&n){var a=this.context;a.data=a.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this),this._plan&&!i&&(t=this._plan(this.context));var r,o=c(this._modBy),s=this._modDataCount||0,l=c(e&&e.modBy),p=e&&e.modDataCount||0;function c(e){return!(e>=1)&&(e=1),e}o===l&&s===p||(t="reset"),(this._dirty||"reset"===t)&&(this._dirty=!1,r=this._doReset(i)),this._modBy=l,this._modDataCount=p;var d=e&&e.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var u=this._dueIndex,m=Math.min(null!=d?this._dueIndex+d:1/0,this._dueEnd);if(!i&&(r||u1&&i>0?s:o}};return r;function o(){return t=e?null:rt},gte:function(e,t){return e>=t}},hb=function(){function e(e,t){if(!$r(t)){var n="";0,sd(n)}this._opFn=mb[e],this._rvalFloat=td(t)}return e.prototype.evaluate=function(e){return $r(e)?this._opFn(e,this._rvalFloat):this._opFn(td(e),this._rvalFloat)},e}(),gb=function(){function e(e,t){var n="desc"===e;this._resultLT=n?1:-1,null==t&&(t=n?"min":"max"),this._incomparable="min"===t?-1/0:1/0}return e.prototype.evaluate=function(e,t){var n=$r(e)?e:td(e),i=$r(t)?t:td(t),a=isNaN(n),r=isNaN(i);if(a&&(n=this._incomparable),r&&(i=this._incomparable),a&&r){var o=Hr(e),s=Hr(t);o&&(n=s?e:0),s&&(i=o?t:0)}return ni?-this._resultLT:0},e}(),fb=function(){function e(e,t){this._rval=t,this._isEQ=e,this._rvalTypeof=typeof t,this._rvalFloat=td(t)}return e.prototype.evaluate=function(e){var t=e===this._rval;if(!t){var n=typeof e;n===this._rvalTypeof||"number"!==n&&"number"!==this._rvalTypeof||(t=td(e)===this._rvalFloat)}return this._isEQ?t:!t},e}();function yb(e,t){return"eq"===e||"ne"===e?new fb("eq"===e,t):bo(mb,e)?new hb(e,t):null}var vb=function(){function e(){}return e.prototype.getRawData=function(){throw new Error("not supported")},e.prototype.getRawDataItem=function(e){throw new Error("not supported")},e.prototype.cloneRawData=function(){},e.prototype.getDimensionInfo=function(e){},e.prototype.cloneAllDimensionInfo=function(){},e.prototype.count=function(){},e.prototype.retrieveValue=function(e,t){},e.prototype.retrieveValueFromItem=function(e,t){},e.prototype.convertValue=function(e,t){return cb(e,t)},e}();function xb(e){var t=e.sourceFormat;if(!Tb(t)){var n="";0,sd(n)}return e.data}function bb(e){var t=e.sourceFormat,n=e.data;if(!Tb(t)){var i="";0,sd(i)}if(t===Mv){for(var a=[],r=0,o=n.length;r65535?Mb:kb}function Fb(e,t,n,i,a){var r=Ob[n||"float"];if(a){var o=e[t],s=o&&o.length;if(s!==i){for(var l=new r(i),p=0;pg[1]&&(g[1]=h)}return this._rawCount=this._count=s,{start:o,end:s}},e.prototype._initDataFromProvider=function(e,t,n){for(var i=this._provider,a=this._chunks,r=this._dimensions,o=r.length,s=this._rawExtent,l=Br(r,(function(e){return e.property})),p=0;pf[1]&&(f[1]=g)}}!i.persistent&&i.clean&&i.clean(),this._rawCount=this._count=t,this._extent=[]},e.prototype.count=function(){return this._count},e.prototype.get=function(e,t){if(!(t>=0&&t=0&&t=this._rawCount||e<0)return-1;if(!this._indices)return e;var t=this._indices,n=t[e];if(null!=n&&ne))return r;a=r-1}}return-1},e.prototype.indicesOfNearest=function(e,t,n){var i=this._chunks[e],a=[];if(!i)return a;null==n&&(n=1/0);for(var r=1/0,o=-1,s=0,l=0,p=this.count();l=0&&o<0)&&(r=d,o=c,s=0),c===o&&(a[s++]=l))}return a.length=s,a},e.prototype.getIndices=function(){var e,t=this._indices;if(t){var n=t.constructor,i=this._count;if(n===Array){e=new n(i);for(var a=0;a=p&&x<=c||isNaN(x))&&(o[s++]=m),m++}u=!0}else if(2===a){h=d[i[0]];var f=d[i[1]],y=e[i[1]][0],v=e[i[1]][1];for(g=0;g=p&&x<=c||isNaN(x))&&(b>=y&&b<=v||isNaN(b))&&(o[s++]=m),m++}u=!0}}if(!u)if(1===a)for(g=0;g=p&&x<=c||isNaN(x))&&(o[s++]=w)}else for(g=0;ge[_][1])&&(S=!1)}S&&(o[s++]=t.getRawIndex(g))}return sf[1]&&(f[1]=g)}}}},e.prototype.lttbDownSample=function(e,t){var n,i,a,r=this.clone([e],!0),o=r._chunks[e],s=this.count(),l=0,p=Math.floor(1/t),c=this.getRawIndex(0),d=new(Ab(this._rawCount))(Math.min(2*(Math.ceil(s/p)+2),s));d[l++]=c;for(var u=1;un&&(n=i,a=T)}_>0&&_p-m&&(s=p-m,o.length=s);for(var h=0;hc[1]&&(c[1]=f),d[u++]=y}return a._count=u,a._indices=d,a._updateGetRawIdx(),a},e.prototype.each=function(e,t){if(this._count)for(var n=e.length,i=this._chunks,a=0,r=this.count();ao&&(o=l)}return i=[r,o],this._extent[e]=i,i},e.prototype.getRawDataItem=function(e){var t=this.getRawIndex(e);if(this._provider.persistent)return this._provider.getItem(t);for(var n=[],i=this._chunks,a=0;a=0?this._indices[e]:-1},e.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},e.internalField=function(){function e(e,t,n,i){return cb(e[i],this._dimensions[i])}Ib={arrayRows:e,objectRows:function(e,t,n,i){return cb(e[t],this._dimensions[i])},keyedColumns:e,original:function(e,t,n,i){var a=e&&(null==e.value?e:e.value);return cb(a instanceof Array?a[i]:a,this._dimensions[i])},typedArray:function(e,t,n,i){return e[i]}}}(),e}(),Bb=function(){function e(e){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=e}return e.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},e.prototype._setLocalSource=function(e,t){this._sourceList=e,this._upstreamSignList=t,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},e.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},e.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},e.prototype._createSource=function(){this._setLocalSource([],[]);var e,t,n=this._sourceHost,i=this._getUpstreamSourceManagers(),a=!!i.length;if(Lb(n)){var r=n,o=void 0,s=void 0,l=void 0;if(a){var p=i[0];p.prepareSource(),o=(l=p.getSource()).data,s=l.sourceFormat,t=[p._getVersionSign()]}else s=Xr(o=r.get("data",!0))?Dv:Ev,t=[];var c=this._getSourceMetaRawOption()||{},d=l&&l.metaRawOption||{},u=io(c.seriesLayoutBy,d.seriesLayoutBy)||null,m=io(c.sourceHeader,d.sourceHeader),h=io(c.dimensions,d.dimensions);e=u!==d.seriesLayoutBy||!!m!=!!d.sourceHeader||h?[qx(o,{seriesLayoutBy:u,sourceHeader:m,dimensions:h},s)]:[]}else{var g=n;if(a){var f=this._applyTransform(i);e=f.sourceList,t=f.upstreamSignList}else{e=[qx(g.get("source",!0),this._getSourceMetaRawOption(),null)],t=[]}}this._setLocalSource(e,t)},e.prototype._applyTransform=function(e){var t,n=this._sourceHost,i=n.get("transform",!0),a=n.get("fromTransformResult",!0);if(null!=a){var r="";1!==e.length&&Vb(r)}var o,s=[],l=[];return Rr(e,(function(e){e.prepareSource();var t=e.getSource(a||0),n="";null==a||t||Vb(n),s.push(t),l.push(e._getVersionSign())})),i?t=function(e,t){var n=dd(e),i=n.length,a="";i||sd(a);for(var r=0,o=i;r1||n>0&&!e.noHeader;return Rr(e.blocks,(function(e){var n=$b(e);n>=t&&(t=n+ +(i&&(!n||Hb(e)&&!e.noHeader)))})),t}return 0}function Kb(e,t,n,i){var a,r=t.noHeader,o=(a=$b(t),{html:zb[a],richText:Ub[a]}),s=[],l=t.blocks||[];so(!l||Ur(l)),l=l||[];var p=e.orderMode;if(t.sortBlocks&&p){l=l.slice();var c={valueAsc:"asc",valueDesc:"desc"};if(bo(c,p)){var d=new gb(c[p],null);l.sort((function(e,t){return d.evaluate(e.sortParam,t.sortParam)}))}else"seriesDesc"===p&&l.reverse()}Rr(l,(function(n,a){var r=t.valueFormatter,l=Wb(n)(r?kr(kr({},e),{valueFormatter:r}):e,n,a>0?o.html:0,i);null!=l&&s.push(l)}));var u="richText"===e.renderMode?s.join(o.richText):Zb(s.join(""),r?n:o.html);if(r)return u;var m=iv(t.header,"ordinal",e.useUTC),h=Gb(i,e.renderMode).nameStyle;return"richText"===e.renderMode?Qb(e,m,h)+o.richText+u:Zb('
'+Jo(m)+"
"+u,n)}function Yb(e,t,n,i){var a=e.renderMode,r=t.noName,o=t.noValue,s=!t.markerType,l=t.name,p=e.useUTC,c=t.valueFormatter||e.valueFormatter||function(e){return Br(e=Ur(e)?e:[e],(function(e,t){return iv(e,Ur(m)?m[t]:m,p)}))};if(!r||!o){var d=s?"":e.markupStyleCreator.makeTooltipMarker(t.markerType,t.markerColor||"#333",a),u=r?"":iv(l,"ordinal",p),m=t.valueType,h=o?[]:c(t.value,t.dataIndex),g=!s||!r,f=!s&&r,y=Gb(i,a),v=y.nameStyle,x=y.valueStyle;return"richText"===a?(s?"":d)+(r?"":Qb(e,u,v))+(o?"":function(e,t,n,i,a){var r=[a],o=i?10:20;return n&&r.push({padding:[0,0,0,o],align:"right"}),e.markupStyleCreator.wrapRichTextStyle(Ur(t)?t.join(" "):t,r)}(e,h,g,f,x)):Zb((s?"":d)+(r?"":function(e,t,n){return''+Jo(e)+""}(u,!s,v))+(o?"":function(e,t,n,i){var a=n?"10px":"20px",r=t?"float:right;margin-left:"+a:"";return e=Ur(e)?e:[e],''+Br(e,(function(e){return Jo(e)})).join("  ")+""}(h,g,f,x)),n)}}function Xb(e,t,n,i,a,r){if(e)return Wb(e)({useUTC:a,renderMode:n,orderMode:i,markupStyleCreator:t,valueFormatter:e.valueFormatter},e,0,r)}function Zb(e,t){return'
'+e+'
'}function Qb(e,t,n){return e.markupStyleCreator.wrapRichTextStyle(t,n)}function Jb(e,t){return lv(e.getData().getItemVisual(t,"style")[e.visualDrawType])}function ew(e,t){var n=e.get("padding");return null!=n?n:"richText"===t?[8,10]:10}var tw=function(){function e(){this.richTextStyles={},this._nextStyleNameId=id()}return e.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},e.prototype.makeTooltipMarker=function(e,t,n){var i="richText"===n?this._generateStyleName():null,a=sv({color:t,type:e,renderMode:n,markerId:i});return Hr(a)?a:(this.richTextStyles[i]=a.style,a.content)},e.prototype.wrapRichTextStyle=function(e,t){var n={};Ur(t)?Rr(t,(function(e){return kr(n,e)})):kr(n,t);var i=this._generateStyleName();return this.richTextStyles[i]=n,"{"+i+"|"+e+"}"},e}();function nw(e){var t,n,i,a,r=e.series,o=e.dataIndex,s=e.multipleSeries,l=r.getData(),p=l.mapDimensionsAll("defaultedTooltip"),c=p.length,d=r.getRawValue(o),u=Ur(d),m=Jb(r,o);if(c>1||u&&!c){var h=function(e,t,n,i,a){var r=t.getData(),o=Nr(e,(function(e,t,n){var i=r.getDimensionInfo(n);return e||i&&!1!==i.tooltip&&null!=i.displayName}),!1),s=[],l=[],p=[];function c(e,t){var n=r.getDimensionInfo(t);n&&!1!==n.otherDims.tooltip&&(o?p.push(jb("nameValue",{markerType:"subItem",markerColor:a,name:n.displayName,value:e,valueType:n.type})):(s.push(e),l.push(n.type)))}return i.length?Rr(i,(function(e){c(ib(r,n,e),e)})):Rr(e,c),{inlineValues:s,inlineValueTypes:l,blocks:p}}(d,r,o,p,m);t=h.inlineValues,n=h.inlineValueTypes,i=h.blocks,a=h.inlineValues[0]}else if(c){var g=l.getDimensionInfo(p[0]);a=t=ib(l,o,p[0]),n=g.type}else a=t=u?d[0]:d;var f=bd(r),y=f&&r.name||"",v=l.getName(o),x=s?y:v;return jb("section",{header:y,noHeader:s||!f,sortParam:a,blocks:[jb("nameValue",{markerType:"item",markerColor:m,name:x,noName:!lo(x),value:t,valueType:n,dataIndex:o})].concat(i||[])})}var iw=Cd();function aw(e,t){return e.getName(t)||e.getId(t)}var rw="__universalTransitionEnabled",ow=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t._selectedDataIndicesMap={},t}return ze(t,e),t.prototype.init=function(e,t,n){this.seriesIndex=this.componentIndex,this.dataTask=sb({count:lw,reset:pw}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(e,n),(iw(this).sourceManager=new Bb(this)).prepareSource();var i=this.getInitialData(e,n);dw(i,this),this.dataTask.context.data=i,iw(this).dataBeforeProcessed=i,sw(this),this._initSelectedMapFromData(i)},t.prototype.mergeDefaultAndTheme=function(e,t){var n=yv(this),i=n?xv(e):{},a=this.subType;Sv.hasClass(a)&&(a+="Series"),Er(e,t.getTheme().get(this.subType)),Er(e,this.getDefaultOption()),ud(e,"label",["show"]),this.fillDataTextStyle(e.data),n&&vv(e,i,n)},t.prototype.mergeOption=function(e,t){e=Er(this.option,e,!0),this.fillDataTextStyle(e.data);var n=yv(this);n&&vv(this.option,e,n);var i=iw(this).sourceManager;i.dirty(),i.prepareSource();var a=this.getInitialData(e,t);dw(a,this),this.dataTask.dirty(),this.dataTask.context.data=a,iw(this).dataBeforeProcessed=a,sw(this),this._initSelectedMapFromData(a)},t.prototype.fillDataTextStyle=function(e){if(e&&!Xr(e))for(var t=["show"],n=0;nthis.getShallow("animationThreshold")&&(t=!1),!!t},t.prototype.restoreData=function(){this.dataTask.dirty()},t.prototype.getColorFromPalette=function(e,t,n){var i=this.ecModel,a=Xv.prototype.getColorFromPalette.call(this,e,t,n);return a||(a=i.getColorFromPalette(e,t,n)),a},t.prototype.coordDimToDataDim=function(e){return this.getRawData().mapDimensionsAll(e)},t.prototype.getProgressive=function(){return this.get("progressive")},t.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},t.prototype.select=function(e,t){this._innerSelect(this.getData(t),e)},t.prototype.unselect=function(e,t){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,a=this.getData(t);if("series"===i||"all"===n)return this.option.selectedMap={},void(this._selectedDataIndicesMap={});for(var r=0;r=0&&n.push(a)}return n},t.prototype.isSelected=function(e,t){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(t);return("all"===n||n[aw(i,e)])&&!i.getItemModel(e).get(["select","disabled"])},t.prototype.isUniversalTransitionEnabled=function(){if(this[rw])return!0;var e=this.option.universalTransition;return!!e&&(!0===e||e&&e.enabled)},t.prototype._innerSelect=function(e,t){var n,i,a=this.option,r=a.selectedMode,o=t.length;if(r&&o)if("series"===r)a.selectedMap="all";else if("multiple"===r){Kr(a.selectedMap)||(a.selectedMap={});for(var s=a.selectedMap,l=0;l0&&this._innerSelect(e,t)}},t.registerClass=function(e){return Sv.registerClass(e)},t.protoInitialize=function(){var e=t.prototype;e.type="series.__base__",e.seriesIndex=0,e.ignoreStyleOnData=!1,e.hasSymbolVisual=!1,e.defaultSymbol="circle",e.visualStyleAccessPath="itemStyle",e.visualDrawType="fill"}(),t}(Sv);function sw(e){var t=e.name;bd(e)||(e.name=function(e){var t=e.getRawData(),n=t.mapDimensionsAll("seriesName"),i=[];return Rr(n,(function(e){var n=t.getDimensionInfo(e);n.displayName&&i.push(n.displayName)})),i.join(" ")}(e)||t)}function lw(e){return e.model.getRawData().count()}function pw(e){var t=e.model;return t.setData(t.getRawData().cloneShallow()),cw}function cw(e,t){t.outputData&&e.end>t.outputData.count()&&t.model.getRawData().cloneShallow(t.outputData)}function dw(e,t){Rr(yo(e.CHANGABLE_METHODS,e.DOWNSAMPLE_METHODS),(function(n){e.wrapMethod(n,zr(uw,t))}))}function uw(e,t){var n=mw(e);return n&&n.setOutputEnd((t||this).count()),t}function mw(e){var t=(e.ecModel||{}).scheduler,n=t&&t.getPipeline(e.uid);if(n){var i=n.currentTask;if(i){var a=i.agentStubMap;a&&(i=a.get(e.uid))}return i}}Ar(ow,rb),Ar(ow,Xv),Nd(ow,Sv);var hw=function(){function e(){this.group=new Mc,this.uid=hy("viewComponent")}return e.prototype.init=function(e,t){},e.prototype.render=function(e,t,n,i){},e.prototype.dispose=function(e,t){},e.prototype.updateView=function(e,t,n,i){},e.prototype.updateLayout=function(e,t,n,i){},e.prototype.updateVisual=function(e,t,n,i){},e.prototype.toggleBlurSeries=function(e,t,n){},e.prototype.eachRendered=function(e){var t=this.group;t&&t.traverse(e)},e}();function gw(){var e=Cd();return function(t){var n=e(t),i=t.pipelineContext,a=!!n.large,r=!!n.progressiveRender,o=n.large=!(!i||!i.large),s=n.progressiveRender=!(!i||!i.progressiveRender);return!(a===o&&r===s)&&"reset"}}Bd(hw),Gd(hw);var fw=Cd(),yw=gw(),vw=function(){function e(){this.group=new Mc,this.uid=hy("viewChart"),this.renderTask=sb({plan:ww,reset:Sw}),this.renderTask.context={view:this}}return e.prototype.init=function(e,t){},e.prototype.render=function(e,t,n,i){0},e.prototype.highlight=function(e,t,n,i){var a=e.getData(i&&i.dataType);a&&bw(a,i,"emphasis")},e.prototype.downplay=function(e,t,n,i){var a=e.getData(i&&i.dataType);a&&bw(a,i,"normal")},e.prototype.remove=function(e,t){this.group.removeAll()},e.prototype.dispose=function(e,t){},e.prototype.updateView=function(e,t,n,i){this.render(e,t,n,i)},e.prototype.updateLayout=function(e,t,n,i){this.render(e,t,n,i)},e.prototype.updateVisual=function(e,t,n,i){this.render(e,t,n,i)},e.prototype.eachRendered=function(e){Nf(this.group,e)},e.markUpdateMethod=function(e,t){fw(e).updateMethod=t},e.protoInitialize=void(e.prototype.type="chart"),e}();function xw(e,t,n){e&&Lh(e)&&("emphasis"===t?vh:xh)(e,n)}function bw(e,t,n){var i=Sd(e,t),a=t&&null!=t.highlightKey?function(e){var t=Wm[e];return null==t&&Hm<=32&&(t=Wm[e]=Hm++),t}(t.highlightKey):null;null!=i?Rr(dd(i),(function(t){xw(e.getItemGraphicEl(t),n,a)})):e.eachItemGraphicEl((function(e){xw(e,n,a)}))}function ww(e){return yw(e.model)}function Sw(e){var t=e.model,n=e.ecModel,i=e.api,a=e.payload,r=t.pipelineContext.progressiveRender,o=e.view,s=a&&fw(a).updateMethod,l=r?"incrementalPrepareRender":s&&o[s]?s:"render";return"render"!==l&&o[l](t,n,i,a),Cw[l]}Bd(vw),Gd(vw);var Cw={incrementalPrepareRender:{progress:function(e,t){t.view.incrementalRender(e,t.model,t.ecModel,t.api,t.payload)}},render:{forceFirstProgress:!0,progress:function(e,t){t.view.render(t.model,t.ecModel,t.api,t.payload)}}},_w="\0__throttleOriginMethod",Tw="\0__throttleRate",Iw="\0__throttleType";function Ew(e,t,n){var i,a,r,o,s,l=0,p=0,c=null;function d(){p=(new Date).getTime(),c=null,e.apply(r,o||[])}t=t||0;var u=function(){for(var e=[],u=0;u=0?d():c=setTimeout(d,-a),l=i};return u.clear=function(){c&&(clearTimeout(c),c=null)},u.debounceNextCall=function(e){s=e},u}function Mw(e,t,n,i){var a=e[t];if(a){var r=a[_w]||a,o=a[Iw];if(a[Tw]!==n||o!==i){if(null==n||!i)return e[t]=r;(a=e[t]=Ew(r,n,"debounce"===i))[_w]=r,a[Iw]=i,a[Tw]=n}return a}}function kw(e,t){var n=e[t];n&&n[_w]&&(n.clear&&n.clear(),e[t]=n[_w])}var Pw=Cd(),Dw={itemStyle:zd(py,!0),lineStyle:zd(oy,!0)},Ow={lineStyle:"stroke",itemStyle:"fill"};function Aw(e,t){var n=e.visualStyleMapper||Dw[t];return n||(console.warn("Unknown style type '"+t+"'."),Dw.itemStyle)}function Fw(e,t){var n=e.visualDrawType||Ow[t];return n||(console.warn("Unknown style type '"+t+"'."),"fill")}var Rw={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){var n=e.getData(),i=e.visualStyleAccessPath||"itemStyle",a=e.getModel(i),r=Aw(e,i)(a),o=a.getShallow("decal");o&&(n.setVisual("decal",o),o.dirty=!0);var s=Fw(e,i),l=r[s],p=jr(l)?l:null,c="auto"===r.fill||"auto"===r.stroke;if(!r[s]||p||c){var d=e.getColorFromPalette(e.name,null,t.getSeriesCount());r[s]||(r[s]=d,n.setVisual("colorFromPalette",!0)),r.fill="auto"===r.fill||jr(r.fill)?d:r.fill,r.stroke="auto"===r.stroke||jr(r.stroke)?d:r.stroke}if(n.setVisual("style",r),n.setVisual("drawType",s),!t.isSeriesFiltered(e)&&p)return n.setVisual("colorFromPalette",!1),{dataEach:function(t,n){var i=e.getDataParams(n),a=kr({},r);a[s]=p(i),t.setItemVisual(n,"style",a)}}}},Bw=new uy,Nw={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){if(!e.ignoreStyleOnData&&!t.isSeriesFiltered(e)){var n=e.getData(),i=e.visualStyleAccessPath||"itemStyle",a=Aw(e,i),r=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(e,t){var n=e.getRawDataItem(t);if(n&&n[i]){Bw.option=n[i];var o=a(Bw);kr(e.ensureUniqueItemVisual(t,"style"),o),Bw.option.decal&&(e.setItemVisual(t,"decal",Bw.option.decal),Bw.option.decal.dirty=!0),r in o&&e.setItemVisual(t,"colorFromPalette",!1)}}:null}}}},Lw={performRawSeries:!0,overallReset:function(e){var t=fo();e.eachSeries((function(e){var n=e.getColorBy();if(!e.isColorBySeries()){var i=e.type+"-"+n,a=t.get(i);a||(a={},t.set(i,a)),Pw(e).scope=a}})),e.eachSeries((function(t){if(!t.isColorBySeries()&&!e.isSeriesFiltered(t)){var n=t.getRawData(),i={},a=t.getData(),r=Pw(t).scope,o=t.visualStyleAccessPath||"itemStyle",s=Fw(t,o);a.each((function(e){var t=a.getRawIndex(e);i[t]=e})),n.each((function(e){var o=i[e];if(a.getItemVisual(o,"colorFromPalette")){var l=a.ensureUniqueItemVisual(o,"style"),p=n.getName(e)||e+"",c=n.count();l[s]=t.getColorFromPalette(p,r,c)}}))}}))}},Vw=Math.PI;var qw=function(){function e(e,t,n,i){this._stageTaskMap=fo(),this.ecInstance=e,this.api=t,n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice(),this._allHandlers=n.concat(i)}return e.prototype.restoreData=function(e,t){e.restoreData(t),this._stageTaskMap.each((function(e){var t=e.overallTask;t&&t.dirty()}))},e.prototype.getPerformArgs=function(e,t){if(e.__pipeline){var n=this._pipelineMap.get(e.__pipeline.id),i=n.context,a=!t&&n.progressiveEnabled&&(!i||i.progressiveRender)&&e.__idxInPipeline>n.blockIndex?n.step:null,r=i&&i.modDataCount;return{step:a,modBy:null!=r?Math.ceil(r/a):null,modDataCount:r}}},e.prototype.getPipeline=function(e){return this._pipelineMap.get(e)},e.prototype.updateStreamModes=function(e,t){var n=this._pipelineMap.get(e.uid),i=e.getData().count(),a=n.progressiveEnabled&&t.incrementalPrepareRender&&i>=n.threshold,r=e.get("large")&&i>=e.get("largeThreshold"),o="mod"===e.get("progressiveChunkMode")?i:null;e.pipelineContext=n.context={progressiveRender:a,modDataCount:o,large:r}},e.prototype.restorePipelines=function(e){var t=this,n=t._pipelineMap=fo();e.eachSeries((function(e){var i=e.getProgressive(),a=e.uid;n.set(a,{id:a,head:null,tail:null,threshold:e.getProgressiveThreshold(),progressiveEnabled:i&&!(e.preventIncremental&&e.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),t._pipe(e,e.dataTask)}))},e.prototype.prepareStageTasks=function(){var e=this._stageTaskMap,t=this.api.getModel(),n=this.api;Rr(this._allHandlers,(function(i){var a=e.get(i.uid)||e.set(i.uid,{}),r="";so(!(i.reset&&i.overallReset),r),i.reset&&this._createSeriesStageTask(i,a,t,n),i.overallReset&&this._createOverallStageTask(i,a,t,n)}),this)},e.prototype.prepareView=function(e,t,n,i){var a=e.renderTask,r=a.context;r.model=t,r.ecModel=n,r.api=i,a.__block=!e.incrementalPrepareRender,this._pipe(t,a)},e.prototype.performDataProcessorTasks=function(e,t){this._performStageTasks(this._dataProcessorHandlers,e,t,{block:!0})},e.prototype.performVisualTasks=function(e,t,n){this._performStageTasks(this._visualHandlers,e,t,n)},e.prototype._performStageTasks=function(e,t,n,i){i=i||{};var a=!1,r=this;function o(e,t){return e.setDirty&&(!e.dirtyMap||e.dirtyMap.get(t.__pipeline.id))}Rr(e,(function(e,s){if(!i.visualType||i.visualType===e.visualType){var l=r._stageTaskMap.get(e.uid),p=l.seriesTaskMap,c=l.overallTask;if(c){var d,u=c.agentStubMap;u.each((function(e){o(i,e)&&(e.dirty(),d=!0)})),d&&c.dirty(),r.updatePayload(c,n);var m=r.getPerformArgs(c,i.block);u.each((function(e){e.perform(m)})),c.perform(m)&&(a=!0)}else p&&p.each((function(s,l){o(i,s)&&s.dirty();var p=r.getPerformArgs(s,i.block);p.skip=!e.performRawSeries&&t.isSeriesFiltered(s.context.model),r.updatePayload(s,n),s.perform(p)&&(a=!0)}))}})),this.unfinished=a||this.unfinished},e.prototype.performSeriesTasks=function(e){var t;e.eachSeries((function(e){t=e.dataTask.perform()||t})),this.unfinished=t||this.unfinished},e.prototype.plan=function(){this._pipelineMap.each((function(e){var t=e.tail;do{if(t.__block){e.blockIndex=t.__idxInPipeline;break}t=t.getUpstream()}while(t)}))},e.prototype.updatePayload=function(e,t){"remain"!==t&&(e.context.payload=t)},e.prototype._createSeriesStageTask=function(e,t,n,i){var a=this,r=t.seriesTaskMap,o=t.seriesTaskMap=fo(),s=e.seriesType,l=e.getTargetSeries;function p(t){var s=t.uid,l=o.set(s,r&&r.get(s)||sb({plan:Hw,reset:Ww,count:Yw}));l.context={model:t,ecModel:n,api:i,useClearVisual:e.isVisual&&!e.isLayout,plan:e.plan,reset:e.reset,scheduler:a},a._pipe(t,l)}e.createOnAllSeries?n.eachRawSeries(p):s?n.eachRawSeriesByType(s,p):l&&l(n,i).each(p)},e.prototype._createOverallStageTask=function(e,t,n,i){var a=this,r=t.overallTask=t.overallTask||sb({reset:Gw});r.context={ecModel:n,api:i,overallReset:e.overallReset,scheduler:a};var o=r.agentStubMap,s=r.agentStubMap=fo(),l=e.seriesType,p=e.getTargetSeries,c=!0,d=!1,u="";function m(e){var t=e.uid,n=s.set(t,o&&o.get(t)||(d=!0,sb({reset:zw,onDirty:jw})));n.context={model:e,overallProgress:c},n.agent=r,n.__block=c,a._pipe(e,n)}so(!e.createOnAllSeries,u),l?n.eachRawSeriesByType(l,m):p?p(n,i).each(m):(c=!1,Rr(n.getSeries(),m)),d&&r.dirty()},e.prototype._pipe=function(e,t){var n=e.uid,i=this._pipelineMap.get(n);!i.head&&(i.head=t),i.tail&&i.tail.pipe(t),i.tail=t,t.__idxInPipeline=i.count++,t.__pipeline=i},e.wrapStageHandler=function(e,t){return jr(e)&&(e={overallReset:e,seriesType:Xw(e)}),e.uid=hy("stageHandler"),t&&(e.visualType=t),e},e}();function Gw(e){e.overallReset(e.ecModel,e.api,e.payload)}function zw(e){return e.overallProgress&&Uw}function Uw(){this.agent.dirty(),this.getDownstream().dirty()}function jw(){this.agent&&this.agent.dirty()}function Hw(e){return e.plan?e.plan(e.model,e.ecModel,e.api,e.payload):null}function Ww(e){e.useClearVisual&&e.data.clearAllVisual();var t=e.resetDefines=dd(e.reset(e.model,e.ecModel,e.api,e.payload));return t.length>1?Br(t,(function(e,t){return Kw(t)})):$w}var $w=Kw(0);function Kw(e){return function(t,n){var i=n.data,a=n.resetDefines[e];if(a&&a.dataEach)for(var r=t.start;r0&&c===a.length-p.length){var d=a.slice(0,c);"data"!==d&&(t.mainType=d,t[p.toLowerCase()]=e,s=!0)}}o.hasOwnProperty(a)&&(n[a]=e,s=!0),s||(i[a]=e)}))}return{cptQuery:t,dataQuery:n,otherQuery:i}},e.prototype.filter=function(e,t){var n=this.eventInfo;if(!n)return!0;var i=n.targetEl,a=n.packedEvent,r=n.model,o=n.view;if(!r||!o)return!0;var s=t.cptQuery,l=t.dataQuery;return p(s,r,"mainType")&&p(s,r,"subType")&&p(s,r,"index","componentIndex")&&p(s,r,"name")&&p(s,r,"id")&&p(l,a,"name")&&p(l,a,"dataIndex")&&p(l,a,"dataType")&&(!o.filterForExposedEvent||o.filterForExposedEvent(e,t.otherQuery,i,a));function p(e,t,n,i){return null==e[n]||t[i||n]===e[n]}},e.prototype.afterTrigger=function(){this.eventInfo=null},e}(),pS=["symbol","symbolSize","symbolRotate","symbolOffset"],cS=pS.concat(["symbolKeepAspect"]),dS={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){var n=e.getData();if(e.legendIcon&&n.setVisual("legendIcon",e.legendIcon),e.hasSymbolVisual){for(var i={},a={},r=!1,o=0;o=0&&AS(l)?l:.5,e.createRadialGradient(o,s,0,o,s,l)}(e,t,n):function(e,t,n){var i=null==t.x?0:t.x,a=null==t.x2?1:t.x2,r=null==t.y?0:t.y,o=null==t.y2?0:t.y2;return t.global||(i=i*n.width+n.x,a=a*n.width+n.x,r=r*n.height+n.y,o=o*n.height+n.y),i=AS(i)?i:0,a=AS(a)?a:1,r=AS(r)?r:0,o=AS(o)?o:0,e.createLinearGradient(i,r,a,o)}(e,t,n),a=t.colorStops,r=0;r0&&(t=i.lineDash,n=i.lineWidth,t&&"solid"!==t&&n>0?"dashed"===t?[4*n,2*n]:"dotted"===t?[n]:$r(t)?[t]:Ur(t)?t:null:null),r=i.lineDashOffset;if(a){var o=i.strokeNoScale&&e.getLineScale?e.getLineScale():1;o&&1!==o&&(a=Br(a,(function(e){return e/o})),r/=o)}return[a,r]}var LS=new Yu(!0);function VS(e){var t=e.stroke;return!(null==t||"none"===t||!(e.lineWidth>0))}function qS(e){return"string"==typeof e&&"none"!==e}function GS(e){var t=e.fill;return null!=t&&"none"!==t}function zS(e,t){if(null!=t.fillOpacity&&1!==t.fillOpacity){var n=e.globalAlpha;e.globalAlpha=t.fillOpacity*t.opacity,e.fill(),e.globalAlpha=n}else e.fill()}function US(e,t){if(null!=t.strokeOpacity&&1!==t.strokeOpacity){var n=e.globalAlpha;e.globalAlpha=t.strokeOpacity*t.opacity,e.stroke(),e.globalAlpha=n}else e.stroke()}function jS(e,t,n){var i=$d(t.image,t.__image,n);if(Yd(i)){var a=e.createPattern(i,t.repeat||"repeat");if("function"==typeof DOMMatrix&&a&&a.setTransform){var r=new DOMMatrix;r.translateSelf(t.x||0,t.y||0),r.rotateSelf(0,0,(t.rotation||0)*So),r.scaleSelf(t.scaleX||1,t.scaleY||1),a.setTransform(r)}return a}}var HS=["shadowBlur","shadowOffsetX","shadowOffsetY"],WS=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function $S(e,t,n,i,a){var r=!1;if(!i&&t===(n=n||{}))return!1;if(i||t.opacity!==n.opacity){XS(e,a),r=!0;var o=Math.max(Math.min(t.opacity,1),0);e.globalAlpha=isNaN(o)?pu.opacity:o}(i||t.blend!==n.blend)&&(r||(XS(e,a),r=!0),e.globalCompositeOperation=t.blend||pu.blend);for(var s=0;s0&&e.unfinished);e.unfinished||this._zr.flush()}}},t.prototype.getDom=function(){return this._dom},t.prototype.getId=function(){return this.id},t.prototype.getZr=function(){return this._zr},t.prototype.isSSR=function(){return this._ssr},t.prototype.setOption=function(e,t,n){if(!this[mC])if(this._disposed)jC(this.id);else{var i,a,r;if(Kr(t)&&(n=t.lazyUpdate,i=t.silent,a=t.replaceMerge,r=t.transition,t=t.notMerge),this[mC]=!0,!this._model||t){var o=new px(this._api),s=this._theme,l=this._model=new ex;l.scheduler=this._scheduler,l.ssr=this._ssr,l.init(null,null,null,s,this._locale,o)}this._model.setOption(e,{replaceMerge:a},KC);var p={seriesTransition:r,optionChanged:!0};if(n)this[hC]={silent:i,updateParams:p},this[mC]=!1,this.getZr().wakeUp();else{try{wC(this),_C.update.call(this,null,p)}catch(e){throw this[hC]=null,this[mC]=!1,e}this._ssr||this._zr.flush(),this[hC]=null,this[mC]=!1,MC.call(this,i),kC.call(this,i)}}},t.prototype.setTheme=function(){od()},t.prototype.getModel=function(){return this._model},t.prototype.getOption=function(){return this._model&&this._model.getOption()},t.prototype.getWidth=function(){return this._zr.getWidth()},t.prototype.getHeight=function(){return this._zr.getHeight()},t.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||rr.hasGlobalWindow&&window.devicePixelRatio||1},t.prototype.getRenderedCanvas=function(e){return this.renderToCanvas(e)},t.prototype.renderToCanvas=function(e){e=e||{};var t=this._zr.painter;return t.getRenderedCanvas({backgroundColor:e.backgroundColor||this._model.get("backgroundColor"),pixelRatio:e.pixelRatio||this.getDevicePixelRatio()})},t.prototype.renderToSVGString=function(e){e=e||{};var t=this._zr.painter;return t.renderToString({useViewBox:e.useViewBox})},t.prototype.getSvgDataURL=function(){if(rr.svgSupported){var e=this._zr;return Rr(e.storage.getDisplayList(),(function(e){e.stopAnimation(null,!0)})),e.painter.toDataURL()}},t.prototype.getDataURL=function(e){if(!this._disposed){var t=(e=e||{}).excludeComponents,n=this._model,i=[],a=this;Rr(t,(function(e){n.eachComponent({mainType:e},(function(e){var t=a._componentsMap[e.__viewId];t.group.ignore||(i.push(t),t.group.ignore=!0)}))}));var r="svg"===this._zr.painter.getType()?this.getSvgDataURL():this.renderToCanvas(e).toDataURL("image/"+(e&&e.type||"png"));return Rr(i,(function(e){e.group.ignore=!1})),r}jC(this.id)},t.prototype.getConnectedDataURL=function(e){if(!this._disposed){var t="svg"===e.type,n=this.group,i=Math.min,a=Math.max,r=1/0;if(JC[n]){var o=r,s=r,l=-1/0,p=-1/0,c=[],d=e&&e.pixelRatio||this.getDevicePixelRatio();Rr(QC,(function(r,d){if(r.group===n){var u=t?r.getZr().painter.getSvgDom().innerHTML:r.renderToCanvas(Ir(e)),m=r.getDom().getBoundingClientRect();o=i(m.left,o),s=i(m.top,s),l=a(m.right,l),p=a(m.bottom,p),c.push({dom:u,left:m.left,top:m.top})}}));var u=(l*=d)-(o*=d),m=(p*=d)-(s*=d),h=dr.createCanvas(),g=Ac(h,{renderer:t?"svg":"canvas"});if(g.resize({width:u,height:m}),t){var f="";return Rr(c,(function(e){var t=e.left-o,n=e.top-s;f+=''+e.dom+""})),g.painter.getSvgRoot().innerHTML=f,e.connectedBackgroundColor&&g.painter.setBackgroundColor(e.connectedBackgroundColor),g.refreshImmediately(),g.painter.toDataURL()}return e.connectedBackgroundColor&&g.add(new Em({shape:{x:0,y:0,width:u,height:m},style:{fill:e.connectedBackgroundColor}})),Rr(c,(function(e){var t=new bm({style:{x:e.left*d-o,y:e.top*d-s,image:e.dom}});g.add(t)})),g.refreshImmediately(),h.toDataURL("image/"+(e&&e.type||"png"))}return this.getDataURL(e)}jC(this.id)},t.prototype.convertToPixel=function(e,t){return TC(this,"convertToPixel",e,t)},t.prototype.convertFromPixel=function(e,t){return TC(this,"convertFromPixel",e,t)},t.prototype.containPixel=function(e,t){var n;if(!this._disposed)return Rr(Td(this._model,e),(function(e,i){i.indexOf("Models")>=0&&Rr(e,(function(e){var a=e.coordinateSystem;if(a&&a.containPoint)n=n||!!a.containPoint(t);else if("seriesModels"===i){var r=this._chartsMap[e.__viewId];r&&r.containPoint&&(n=n||r.containPoint(t,e))}else 0}),this)}),this),!!n;jC(this.id)},t.prototype.getVisual=function(e,t){var n=Td(this._model,e,{defaultMainType:"series"}),i=n.seriesModel;var a=i.getData(),r=n.hasOwnProperty("dataIndexInside")?n.dataIndexInside:n.hasOwnProperty("dataIndex")?a.indexOfRawIndex(n.dataIndex):null;return null!=r?mS(a,r,t):hS(a,t)},t.prototype.getViewOfComponentModel=function(e){return this._componentsMap[e.__viewId]},t.prototype.getViewOfSeriesModel=function(e){return this._chartsMap[e.__viewId]},t.prototype._initEvents=function(){var e,t,n,i=this;Rr(UC,(function(e){var t=function(t){var n,a=i.getModel(),r=t.target,o="globalout"===e;if(o?n={}:r&&vS(r,(function(e){var t=Um(e);if(t&&null!=t.dataIndex){var i=t.dataModel||a.getSeriesByIndex(t.seriesIndex);return n=i&&i.getDataParams(t.dataIndex,t.dataType,r)||{},!0}if(t.eventData)return n=kr({},t.eventData),!0}),!0),n){var s=n.componentType,l=n.componentIndex;"markLine"!==s&&"markPoint"!==s&&"markArea"!==s||(s="series",l=n.seriesIndex);var p=s&&null!=l&&a.getComponent(s,l),c=p&&i["series"===p.mainType?"_chartsMap":"_componentsMap"][p.__viewId];0,n.event=t,n.type=e,i._$eventProcessor.eventInfo={targetEl:r,packedEvent:n,model:p,view:c},i.trigger(e,n)}};t.zrEventfulCallAtLast=!0,i._zr.on(e,t,i)})),Rr(WC,(function(e,t){i._messageCenter.on(t,(function(e){this.trigger(t,e)}),i)})),Rr(["selectchanged"],(function(e){i._messageCenter.on(e,(function(t){this.trigger(e,t)}),i)})),e=this._messageCenter,t=this,n=this._api,e.on("selectchanged",(function(e){var i=n.getModel();e.isFromClick?(yS("map","selectchanged",t,i,e),yS("pie","selectchanged",t,i,e)):"select"===e.fromAction?(yS("map","selected",t,i,e),yS("pie","selected",t,i,e)):"unselect"===e.fromAction&&(yS("map","unselected",t,i,e),yS("pie","unselected",t,i,e))}))},t.prototype.isDisposed=function(){return this._disposed},t.prototype.clear=function(){this._disposed?jC(this.id):this.setOption({series:[]},!0)},t.prototype.dispose=function(){if(this._disposed)jC(this.id);else{this._disposed=!0,this.getDom()&&Pd(this.getDom(),t_,"");var e=this,t=e._api,n=e._model;Rr(e._componentsViews,(function(e){e.dispose(n,t)})),Rr(e._chartsViews,(function(e){e.dispose(n,t)})),e._zr.dispose(),e._dom=e._model=e._chartsMap=e._componentsMap=e._chartsViews=e._componentsViews=e._scheduler=e._api=e._zr=e._throttledZrFlush=e._theme=e._coordSysMgr=e._messageCenter=null,delete QC[e.id]}},t.prototype.resize=function(e){if(!this[mC])if(this._disposed)jC(this.id);else{this._zr.resize(e);var t=this._model;if(this._loadingFX&&this._loadingFX.resize(),t){var n=t.resetOption("media"),i=e&&e.silent;this[hC]&&(null==i&&(i=this[hC].silent),n=!0,this[hC]=null),this[mC]=!0;try{n&&wC(this),_C.update.call(this,{type:"resize",animation:kr({duration:0},e&&e.animation)})}catch(e){throw this[mC]=!1,e}this[mC]=!1,MC.call(this,i),kC.call(this,i)}}},t.prototype.showLoading=function(e,t){if(this._disposed)jC(this.id);else if(Kr(e)&&(t=e,e=""),e=e||"default",this.hideLoading(),ZC[e]){var n=ZC[e](this._api,t),i=this._zr;this._loadingFX=n,i.add(n)}},t.prototype.hideLoading=function(){this._disposed?jC(this.id):(this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null)},t.prototype.makeActionFromEvent=function(e){var t=kr({},e);return t.type=WC[e.type],t},t.prototype.dispatchAction=function(e,t){if(this._disposed)jC(this.id);else if(Kr(t)||(t={silent:!!t}),HC[e.type]&&this._model)if(this[mC])this._pendingActions.push(e);else{var n=t.silent;EC.call(this,e,n);var i=t.flush;i?this._zr.flush():!1!==i&&rr.browser.weChat&&this._throttledZrFlush(),MC.call(this,n),kC.call(this,n)}},t.prototype.updateLabelLayout=function(){sC.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},t.prototype.appendData=function(e){if(this._disposed)jC(this.id);else{var t=e.seriesIndex,n=this.getModel().getSeriesByIndex(t);0,n.appendData(e),this._scheduler.unfinished=!0,this.getZr().wakeUp()}},t.internalField=function(){function e(e){e.clearColorPalette(),e.eachSeries((function(e){e.clearColorPalette()}))}function t(e){for(var t=[],n=e.currentStates,i=0;i0?{duration:r,delay:i.get("delay"),easing:i.get("easing")}:null;n.eachRendered((function(e){if(e.states&&e.states.emphasis){if(af(e))return;if(e instanceof gm&&function(e){var t=$m(e);t.normalFill=e.style.fill,t.normalStroke=e.style.stroke;var n=e.states.select||{};t.selectFill=n.style&&n.style.fill||null,t.selectStroke=n.style&&n.style.stroke||null}(e),e.__dirty){var n=e.prevStates;n&&e.useStates(n)}if(a){e.stateTransition=o;var i=e.getTextContent(),r=e.getTextGuideLine();i&&(i.stateTransition=o),r&&(r.stateTransition=o)}e.__dirty&&t(e)}}))}wC=function(e){var t=e._scheduler;t.restorePipelines(e._model),t.prepareStageTasks(),SC(e,!0),SC(e,!1),t.plan()},SC=function(e,t){for(var n=e._model,i=e._scheduler,a=t?e._componentsViews:e._chartsViews,r=t?e._componentsMap:e._chartsMap,o=e._zr,s=e._api,l=0;lt.get("hoverLayerThreshold")&&!rr.node&&!rr.worker&&t.eachSeries((function(t){if(!t.preventUsingHoverLayer){var n=e._chartsMap[t.__viewId];n.__alive&&n.eachRendered((function(e){e.states.emphasis&&(e.states.emphasis.hoverLayer=!0)}))}}))}(e,t),sC.trigger("series:afterupdate",t,i,s)},NC=function(e){e[gC]=!0,e.getZr().wakeUp()},LC=function(e){e[gC]&&(e.getZr().storage.traverse((function(e){af(e)||t(e)})),e[gC]=!1)},RC=function(e){return new(function(t){function n(){return null!==t&&t.apply(this,arguments)||this}return ze(n,t),n.prototype.getCoordinateSystems=function(){return e._coordSysMgr.getCoordinateSystems()},n.prototype.getComponentByElement=function(t){for(;t;){var n=t.__ecComponentInfo;if(null!=n)return e._model.getComponent(n.mainType,n.index);t=t.parent}},n.prototype.enterEmphasis=function(t,n){vh(t,n),NC(e)},n.prototype.leaveEmphasis=function(t,n){xh(t,n),NC(e)},n.prototype.enterBlur=function(t){bh(t),NC(e)},n.prototype.leaveBlur=function(t){wh(t),NC(e)},n.prototype.enterSelect=function(t){Sh(t),NC(e)},n.prototype.leaveSelect=function(t){Ch(t),NC(e)},n.prototype.getModel=function(){return e.getModel()},n.prototype.getViewOfComponentModel=function(t){return e.getViewOfComponentModel(t)},n.prototype.getViewOfSeriesModel=function(t){return e.getViewOfSeriesModel(t)},n}(rx))(e)},BC=function(e){function t(e,t){for(var n=0;n=0)){p_.push(n);var r=qw.wrapStageHandler(n,a);r.__prio=t,r.__raw=n,e.push(r)}}function d_(e,t){ZC[e]=t}var u_=function(e){var t=(e=Ir(e)).type,n="";t||sd(n);var i=t.split(":");2!==i.length&&sd(n);var a=!1;"echarts"===i[0]&&(t=i[1],a=!0),e.__isBuiltIn=a,Cb.set(t,e)};function m_(e){return null==e?0:e.length||1}function h_(e){return e}l_(cC,Rw),l_(dC,Nw),l_(dC,Lw),l_(cC,dS),l_(dC,uS),l_(7e3,(function(e,t){e.eachRawSeries((function(n){if(!e.isSeriesFiltered(n)){var i=n.getData();i.hasItemVisual()&&i.each((function(e){var n=i.getItemVisual(e,"decal");n&&(i.ensureUniqueItemVisual(e,"style").decal=iC(n,t))}));var a=i.getVisual("decal");if(a)i.getVisual("style").decal=iC(a,t)}}))})),a_(Dx),r_(900,(function(e){var t=fo();e.eachSeries((function(e){var n=e.get("stack");if(n){var i=t.get(n)||t.set(n,[]),a=e.getData(),r={stackResultDimension:a.getCalculationInfo("stackResultDimension"),stackedOverDimension:a.getCalculationInfo("stackedOverDimension"),stackedDimension:a.getCalculationInfo("stackedDimension"),stackedByDimension:a.getCalculationInfo("stackedByDimension"),isStackedByIndex:a.getCalculationInfo("isStackedByIndex"),data:a,seriesModel:e};if(!r.stackedDimension||!r.isStackedByIndex&&!r.stackedByDimension)return;i.length&&a.setCalculationInfo("stackedOnSeries",i[i.length-1].seriesModel),i.push(r)}})),t.each(Ox)})),d_("default",(function(e,t){Pr(t=t||{},{text:"loading",textColor:"#000",fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255, 255, 255, 0.8)",showSpinner:!0,color:"#5470c6",spinnerRadius:10,lineWidth:5,zlevel:0});var n=new Mc,i=new Em({style:{fill:t.maskColor},zlevel:t.zlevel,z:1e4});n.add(i);var a,r=new Pm({style:{text:t.text,fill:t.textColor,fontSize:t.fontSize,fontWeight:t.fontWeight,fontStyle:t.fontStyle,fontFamily:t.fontFamily},zlevel:t.zlevel,z:10001}),o=new Em({style:{fill:"none"},textContent:r,textConfig:{position:"right",distance:10},zlevel:t.zlevel,z:10001});return n.add(o),t.showSpinner&&((a=new qg({shape:{startAngle:-Vw/2,endAngle:-Vw/2+.1,r:t.spinnerRadius},style:{stroke:t.color,lineCap:"round",lineWidth:t.lineWidth},zlevel:t.zlevel,z:10001})).animateShape(!0).when(1e3,{endAngle:3*Vw/2}).start("circularInOut"),a.animateShape(!0).when(1e3,{startAngle:3*Vw/2}).delay(300).start("circularInOut"),n.add(a)),n.resize=function(){var n=r.getBoundingRect().width,s=t.showSpinner?t.spinnerRadius:0,l=(e.getWidth()-2*s-(t.showSpinner&&n?10:0)-n)/2-(t.showSpinner&&n?0:5+n/2)+(t.showSpinner?0:n/2)+(n?0:s),p=e.getHeight()/2;t.showSpinner&&a.setShape({cx:l,cy:p}),o.setShape({x:l-s,y:p-s,width:2*s,height:2*s}),i.setShape({x:0,y:0,width:e.getWidth(),height:e.getHeight()})},n.resize(),n})),s_({type:Qm,event:Qm,update:Qm},wo),s_({type:Jm,event:Jm,update:Jm},wo),s_({type:eh,event:eh,update:eh},wo),s_({type:th,event:th,update:th},wo),s_({type:nh,event:nh,update:nh},wo),i_("light",nS),i_("dark",sS);var g_=function(){function e(e,t,n,i,a,r){this._old=e,this._new=t,this._oldKeyGetter=n||h_,this._newKeyGetter=i||h_,this.context=a,this._diffModeMultiple="multiple"===r}return e.prototype.add=function(e){return this._add=e,this},e.prototype.update=function(e){return this._update=e,this},e.prototype.updateManyToOne=function(e){return this._updateManyToOne=e,this},e.prototype.updateOneToMany=function(e){return this._updateOneToMany=e,this},e.prototype.updateManyToMany=function(e){return this._updateManyToMany=e,this},e.prototype.remove=function(e){return this._remove=e,this},e.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},e.prototype._executeOneToOne=function(){var e=this._old,t=this._new,n={},i=new Array(e.length),a=new Array(t.length);this._initIndexMap(e,null,i,"_oldKeyGetter"),this._initIndexMap(t,n,a,"_newKeyGetter");for(var r=0;r1){var p=s.shift();1===s.length&&(n[o]=s[0]),this._update&&this._update(p,r)}else 1===l?(n[o]=null,this._update&&this._update(s,r)):this._remove&&this._remove(r)}this._performRestAdd(a,n)},e.prototype._executeMultiple=function(){var e=this._old,t=this._new,n={},i={},a=[],r=[];this._initIndexMap(e,n,a,"_oldKeyGetter"),this._initIndexMap(t,i,r,"_newKeyGetter");for(var o=0;o1&&1===d)this._updateManyToOne&&this._updateManyToOne(p,l),i[s]=null;else if(1===c&&d>1)this._updateOneToMany&&this._updateOneToMany(p,l),i[s]=null;else if(1===c&&1===d)this._update&&this._update(p,l),i[s]=null;else if(c>1&&d>1)this._updateManyToMany&&this._updateManyToMany(p,l),i[s]=null;else if(c>1)for(var u=0;u1)for(var o=0;o30}var E_,M_,k_,P_,D_,O_,A_,F_=Kr,R_=Br,B_="undefined"==typeof Int32Array?Array:Int32Array,N_=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],L_=["_approximateExtent"],V_=function(){function e(e,t){var n;this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","lttbDownSample"];var i=!1;C_(e)?(n=e.dimensions,this._dimOmitted=e.isDimensionOmitted(),this._schema=e):(i=!0,n=e),n=n||["x","y"];for(var a={},r=[],o={},s=!1,l={},p=0;p=t)){var n=this._store.getProvider();this._updateOrdinalMeta();var i=this._nameList,a=this._idList;if(n.getSource().sourceFormat===Ev&&!n.pure)for(var r=[],o=e;o0},e.prototype.ensureUniqueItemVisual=function(e,t){var n=this._itemVisuals,i=n[e];i||(i=n[e]={});var a=i[t];return null==a&&(Ur(a=this.getVisual(t))?a=a.slice():F_(a)&&(a=kr({},a)),i[t]=a),a},e.prototype.setItemVisual=function(e,t,n){var i=this._itemVisuals[e]||{};this._itemVisuals[e]=i,F_(t)?kr(i,t):i[t]=n},e.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},e.prototype.setLayout=function(e,t){F_(e)?kr(this._layout,e):this._layout[e]=t},e.prototype.getLayout=function(e){return this._layout[e]},e.prototype.getItemLayout=function(e){return this._itemLayouts[e]},e.prototype.setItemLayout=function(e,t,n){this._itemLayouts[e]=n?kr(this._itemLayouts[e]||{},t):t},e.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},e.prototype.setItemGraphicEl=function(e,t){var n=this.hostModel&&this.hostModel.seriesIndex;jm(n,this.dataType,e,t),this._graphicEls[e]=t},e.prototype.getItemGraphicEl=function(e){return this._graphicEls[e]},e.prototype.eachItemGraphicEl=function(e,t){Rr(this._graphicEls,(function(n,i){n&&e&&e.call(t,n,i)}))},e.prototype.cloneShallow=function(t){return t||(t=new e(this._schema?this._schema:R_(this.dimensions,this._getDimInfo,this),this.hostModel)),D_(t,this),t._store=this._store,t},e.prototype.wrapMethod=function(e,t){var n=this[e];jr(n)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(e),this[e]=function(){var e=n.apply(this,arguments);return t.apply(this,[e].concat(ro(arguments)))})},e.internalField=(E_=function(e){var t=e._invertedIndicesMap;Rr(t,(function(n,i){var a=e._dimInfos[i],r=a.ordinalMeta,o=a.stack,s=e._store;if(r||o){if(n=t[i]=o?new Array(s.count()):new B_(r.categories.length),r)for(var l=0;l1&&(s+="__ec__"+p),i[t]=s}})),e}();function q_(e,t){Vx(e)||(e=Gx(e));var n=(t=t||{}).coordDimensions||[],i=t.dimensionsDefine||e.dimensionsDefine||[],a=fo(),r=[],o=function(e,t,n,i){var a=Math.max(e.dimensionsDetectedCount||1,t.length,n.length,i||0);return Rr(t,(function(e){var t;Kr(e)&&(t=e.dimsDef)&&(a=Math.max(a,t.length))})),a}(e,n,i,t.dimensionsCount),s=t.canOmitUnusedDimensions&&I_(o),l=i===e.dimensionsDefine,p=l?T_(e):__(i),c=t.encodeDefine;!c&&t.encodeDefaulter&&(c=t.encodeDefaulter(e,o));for(var d=fo(c),u=new Pb(o),m=0;m0&&(i.name=a+(r-1)),r++,t.set(a,r)}}(r),new S_({source:e,dimensions:r,fullDimensionCount:o,dimensionOmitted:s})}function G_(e,t,n){if(n||t.hasKey(e)){for(var i=0;t.hasKey(e+i);)i++;e+=i}return t.set(e,!0),e}var z_=function(e){this.coordSysDims=[],this.axisMap=fo(),this.categoryAxisMap=fo(),this.coordSysName=e};var U_={cartesian2d:function(e,t,n,i){var a=e.getReferringComponents("xAxis",Ed).models[0],r=e.getReferringComponents("yAxis",Ed).models[0];t.coordSysDims=["x","y"],n.set("x",a),n.set("y",r),j_(a)&&(i.set("x",a),t.firstCategoryDimIndex=0),j_(r)&&(i.set("y",r),null==t.firstCategoryDimIndex&&(t.firstCategoryDimIndex=1))},singleAxis:function(e,t,n,i){var a=e.getReferringComponents("singleAxis",Ed).models[0];t.coordSysDims=["single"],n.set("single",a),j_(a)&&(i.set("single",a),t.firstCategoryDimIndex=0)},polar:function(e,t,n,i){var a=e.getReferringComponents("polar",Ed).models[0],r=a.findAxisModel("radiusAxis"),o=a.findAxisModel("angleAxis");t.coordSysDims=["radius","angle"],n.set("radius",r),n.set("angle",o),j_(r)&&(i.set("radius",r),t.firstCategoryDimIndex=0),j_(o)&&(i.set("angle",o),null==t.firstCategoryDimIndex&&(t.firstCategoryDimIndex=1))},geo:function(e,t,n,i){t.coordSysDims=["lng","lat"]},parallel:function(e,t,n,i){var a=e.ecModel,r=a.getComponent("parallel",e.get("parallelIndex")),o=t.coordSysDims=r.dimensions.slice();Rr(r.parallelAxisIndex,(function(e,r){var s=a.getComponent("parallelAxis",e),l=o[r];n.set(l,s),j_(s)&&(i.set(l,s),null==t.firstCategoryDimIndex&&(t.firstCategoryDimIndex=r))}))}};function j_(e){return"category"===e.get("type")}function H_(e,t,n){var i,a,r,o=(n=n||{}).byIndex,s=n.stackedCoordDimension;!function(e){return!C_(e.schema)}(t)?(a=t.schema,i=a.dimensions,r=t.store):i=t;var l,p,c,d,u=!(!e||!e.get("stack"));if(Rr(i,(function(e,t){Hr(e)&&(i[t]=e={name:e}),u&&!e.isExtraCoord&&(o||l||!e.ordinalMeta&&!e.stack||(l=e),p||"ordinal"===e.type||"time"===e.type||s&&s!==e.coordDim||(p=e))})),!p||o||l||(o=!0),p){c="__\0ecstackresult_"+e.id,d="__\0ecstackedover_"+e.id,l&&(l.createInvertedIndices=!0);var m=p.coordDim,h=p.type,g=0;Rr(i,(function(e){e.coordDim===m&&g++}));var f={name:c,coordDim:m,coordDimIndex:g,type:h,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},y={name:d,coordDim:d,coordDimIndex:g+1,type:h,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};a?(r&&(f.storeDimIndex=r.ensureCalculationDimension(d,h),y.storeDimIndex=r.ensureCalculationDimension(c,h)),a.appendCalculationDimension(f),a.appendCalculationDimension(y)):(i.push(f),i.push(y))}return{stackedDimension:p&&p.name,stackedByDimension:l&&l.name,isStackedByIndex:o,stackedOverDimension:d,stackResultDimension:c}}function W_(e,t){return!!t&&t===e.getCalculationInfo("stackedDimension")}function $_(e,t){return W_(e,t)?e.getCalculationInfo("stackResultDimension"):t}function K_(e,t,n){n=n||{};var i,a=t.getSourceManager(),r=!1;e?(r=!0,i=Gx(e)):r=(i=a.getSource()).sourceFormat===Ev;var o=function(e){var t=e.get("coordinateSystem"),n=new z_(t),i=U_[t];if(i)return i(e,n,n.axisMap,n.categoryAxisMap),n}(t),s=function(e,t){var n,i=e.get("coordinateSystem"),a=sx.get(i);return t&&t.coordSysDims&&(n=Br(t.coordSysDims,(function(e){var n={name:e},i=t.axisMap.get(e);if(i){var a=i.get("type");n.type=v_(a)}return n}))),n||(n=a&&(a.getDimensionsInfo?a.getDimensionsInfo():a.dimensions.slice())||["x","y"]),n}(t,o),l=n.useEncodeDefaulter,p=jr(l)?l:l?zr(Vv,s,t):null,c=q_(i,{coordDimensions:s,generateCoord:n.generateCoord,encodeDefine:t.getEncode(),encodeDefaulter:p,canOmitUnusedDimensions:!r}),d=function(e,t,n){var i,a;return n&&Rr(e,(function(e,r){var o=e.coordDim,s=n.categoryAxisMap.get(o);s&&(null==i&&(i=r),e.ordinalMeta=s.getOrdinalMeta(),t&&(e.createInvertedIndices=!0)),null!=e.otherDims.itemName&&(a=!0)})),a||null==i||(e[i].otherDims.itemName=0),i}(c.dimensions,n.createInvertedIndices,o),u=r?null:a.getSharedDataStore(c),m=H_(t,{schema:c,store:u}),h=new V_(c,t);h.setCalculationInfo(m);var g=null!=d&&function(e){if(e.sourceFormat===Ev){var t=function(e){var t=0;for(;tt[1]&&(t[1]=e[1])},e.prototype.unionExtentFromData=function(e,t){this.unionExtent(e.getApproximateExtent(t))},e.prototype.getExtent=function(){return this._extent.slice()},e.prototype.setExtent=function(e,t){var n=this._extent;isNaN(e)||(n[0]=e),isNaN(t)||(n[1]=t)},e.prototype.isInExtentRange=function(e){return this._extent[0]<=e&&this._extent[1]>=e},e.prototype.isBlank=function(){return this._isBlank},e.prototype.setBlank=function(e){this._isBlank=e},e}();Gd(Y_);var X_=0,Z_=function(){function e(e){this.categories=e.categories||[],this._needCollect=e.needCollect,this._deduplication=e.deduplication,this.uid=++X_}return e.createByAxisModel=function(t){var n=t.option,i=n.data,a=i&&Br(i,Q_);return new e({categories:a,needCollect:!a,deduplication:!1!==n.dedplication})},e.prototype.getOrdinal=function(e){return this._getOrCreateMap().get(e)},e.prototype.parseAndCollect=function(e){var t,n=this._needCollect;if(!Hr(e)&&!n)return e;if(n&&!this._deduplication)return t=this.categories.length,this.categories[t]=e,t;var i=this._getOrCreateMap();return null==(t=i.get(e))&&(n?(t=this.categories.length,this.categories[t]=e,i.set(e,t)):t=NaN),t},e.prototype._getOrCreateMap=function(){return this._map||(this._map=fo(this.categories))},e}();function Q_(e){return Kr(e)&&null!=e.value?e.value:e+""}function J_(e){return"interval"===e.type||"log"===e.type}function eT(e,t,n,i){var a={},r=e[1]-e[0],o=a.interval=Jc(r/t,!0);null!=n&&oi&&(o=a.interval=i);var s=a.intervalPrecision=nT(o);return function(e,t){!isFinite(e[0])&&(e[0]=t[0]),!isFinite(e[1])&&(e[1]=t[1]),iT(e,0,t),iT(e,1,t),e[0]>e[1]&&(e[0]=e[1])}(a.niceTickExtent=[qc(Math.ceil(e[0]/o)*o,s),qc(Math.floor(e[1]/o)*o,s)],e),a}function tT(e){var t=Math.pow(10,Qc(e)),n=e/t;return n?2===n?n=3:3===n?n=5:n*=2:n=1,qc(n*t)}function nT(e){return zc(e)+2}function iT(e,t,n){e[t]=Math.max(Math.min(e[t],n[1]),n[0])}function aT(e,t){return e>=t[0]&&e<=t[1]}function rT(e,t){return t[1]===t[0]?.5:(e-t[0])/(t[1]-t[0])}function oT(e,t){return e*(t[1]-t[0])+t[0]}var sT=function(e){function t(t){var n=e.call(this,t)||this;n.type="ordinal";var i=n.getSetting("ordinalMeta");return i||(i=new Z_({})),Ur(i)&&(i=new Z_({categories:Br(i,(function(e){return Kr(e)?e.value:e}))})),n._ordinalMeta=i,n._extent=n.getSetting("extent")||[0,i.categories.length-1],n}return ze(t,e),t.prototype.parse=function(e){return null==e?NaN:Hr(e)?this._ordinalMeta.getOrdinal(e):Math.round(e)},t.prototype.contain=function(e){return aT(e=this.parse(e),this._extent)&&null!=this._ordinalMeta.categories[e]},t.prototype.normalize=function(e){return rT(e=this._getTickNumber(this.parse(e)),this._extent)},t.prototype.scale=function(e){return e=Math.round(oT(e,this._extent)),this.getRawOrdinalNumber(e)},t.prototype.getTicks=function(){for(var e=[],t=this._extent,n=t[0];n<=t[1];)e.push({value:n}),n++;return e},t.prototype.getMinorTicks=function(e){},t.prototype.setSortInfo=function(e){if(null!=e){for(var t=e.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],a=0,r=this._ordinalMeta.categories.length,o=Math.min(r,t.length);a=0&&e=0&&e=e},t.prototype.getOrdinalMeta=function(){return this._ordinalMeta},t.prototype.calcNiceTicks=function(){},t.prototype.calcNiceExtent=function(){},t.type="ordinal",t}(Y_);Y_.registerClass(sT);var lT=qc,pT=function(e){function t(t){var n=e.call(this,t)||this;n.type="interval",n._interval=0,n._intervalPrecision=2;var i=n.getSetting("ticksGenerator");return jr(i)&&(n._ticksGenerator=i),n}return ze(t,e),t.prototype.parse=function(e){return e},t.prototype.contain=function(e){return aT(e,this._extent)},t.prototype.normalize=function(e){return rT(e,this._extent)},t.prototype.scale=function(e){return oT(e,this._extent)},t.prototype.setExtent=function(e,t){var n=this._extent;isNaN(e)||(n[0]=parseFloat(e)),isNaN(t)||(n[1]=parseFloat(t))},t.prototype.unionExtent=function(e){var t=this._extent;e[0]t[1]&&(t[1]=e[1]),this.setExtent(t[0],t[1])},t.prototype.getInterval=function(){return this._interval},t.prototype.setInterval=function(e){this._interval=e,this._niceExtent=this._extent.slice(),this._intervalPrecision=nT(e)},t.prototype.getTicks=function(e){var t,n=this._interval,i=this._extent,a=this._niceExtent,r=this._intervalPrecision,o=this._ticksGenerator;if(o)try{if(t=o(i,n,a,r))return t}catch(e){}if(t=[],!n)return t;i[0]1e4)return[];var l=t.length?t[t.length-1].value:a[1];return i[1]>l&&(e?t.push({value:lT(l+n,r)}):t.push({value:i[1]})),t},t.prototype.getMinorTicks=function(e){for(var t=this.getTicks(!0),n=[],i=this.getExtent(),a=1;ai[0]&&c0&&(r=null===r?s:Math.min(r,s))}n[i]=r}}return n}(e),n=[];return Rr(e,(function(e){var i,a=e.coordinateSystem.getBaseAxis(),r=a.getExtent();if("category"===a.type)i=a.getBandWidth();else if("value"===a.type||"time"===a.type){var o=a.dim+"_"+a.index,s=t[o],l=Math.abs(r[1]-r[0]),p=a.scale.getExtent(),c=Math.abs(p[1]-p[0]);i=s?l/c*s:l}else{var d=e.getData();i=Math.abs(r[1]-r[0])/d.count()}var u=Vc(e.get("barWidth"),i),m=Vc(e.get("barMaxWidth"),i),h=Vc(e.get("barMinWidth")||(ST(e)?.5:1),i),g=e.get("barGap"),f=e.get("barCategoryGap");n.push({bandWidth:i,barWidth:u,barMaxWidth:m,barMinWidth:h,barGap:g,barCategoryGap:f,axisKey:gT(a),stackId:hT(e)})})),vT(n)}function vT(e){var t={};Rr(e,(function(e,n){var i=e.axisKey,a=e.bandWidth,r=t[i]||{bandWidth:a,remainedWidth:a,autoWidthCount:0,categoryGap:null,gap:"20%",stacks:{}},o=r.stacks;t[i]=r;var s=e.stackId;o[s]||r.autoWidthCount++,o[s]=o[s]||{width:0,maxWidth:0};var l=e.barWidth;l&&!o[s].width&&(o[s].width=l,l=Math.min(r.remainedWidth,l),r.remainedWidth-=l);var p=e.barMaxWidth;p&&(o[s].maxWidth=p);var c=e.barMinWidth;c&&(o[s].minWidth=c);var d=e.barGap;null!=d&&(r.gap=d);var u=e.barCategoryGap;null!=u&&(r.categoryGap=u)}));var n={};return Rr(t,(function(e,t){n[t]={};var i=e.stacks,a=e.bandWidth,r=e.categoryGap;if(null==r){var o=qr(i).length;r=Math.max(35-4*o,15)+"%"}var s=Vc(r,a),l=Vc(e.gap,1),p=e.remainedWidth,c=e.autoWidthCount,d=(p-s)/(c+(c-1)*l);d=Math.max(d,0),Rr(i,(function(e){var t=e.maxWidth,n=e.minWidth;if(e.width){i=e.width;t&&(i=Math.min(i,t)),n&&(i=Math.max(i,n)),e.width=i,p-=i+l*i,c--}else{var i=d;t&&ti&&(i=n),i!==d&&(e.width=i,p-=i+l*i,c--)}})),d=(p-s)/(c+(c-1)*l),d=Math.max(d,0);var u,m=0;Rr(i,(function(e,t){e.width||(e.width=d),u=e,m+=e.width*(1+l)})),u&&(m-=u.width*l);var h=-m/2;Rr(i,(function(e,i){n[t][i]=n[t][i]||{bandWidth:a,offset:h,width:e.width},h+=e.width*(1+l)}))})),n}function xT(e,t){var n=fT(e,t),i=yT(n);Rr(n,(function(e){var t=e.getData(),n=e.coordinateSystem.getBaseAxis(),a=hT(e),r=i[gT(n)][a],o=r.offset,s=r.width;t.setLayout({bandWidth:r.bandWidth,offset:o,size:s})}))}function bT(e){return{seriesType:e,plan:gw(),reset:function(e){if(wT(e)){var t=e.getData(),n=e.coordinateSystem,i=n.getBaseAxis(),a=n.getOtherAxis(i),r=t.getDimensionIndex(t.mapDimension(a.dim)),o=t.getDimensionIndex(t.mapDimension(i.dim)),s=e.get("showBackground",!0),l=t.mapDimension(a.dim),p=t.getCalculationInfo("stackResultDimension"),c=W_(t,l)&&!!t.getCalculationInfo("stackedOnSeries"),d=a.isHorizontal(),u=function(e,t){return t.toGlobalCoord(t.dataToCoord("log"===t.type?1:0))}(0,a),m=ST(e),h=e.get("barMinHeight")||0,g=p&&t.getDimensionIndex(p),f=t.getLayout("size"),y=t.getLayout("offset");return{progress:function(e,t){for(var i,a=e.count,l=m&&uT(3*a),p=m&&s&&uT(3*a),v=m&&uT(a),x=n.master.getRect(),b=d?x.width:x.height,w=t.getStore(),S=0;null!=(i=e.next());){var C=w.get(c?g:r,i),_=w.get(o,i),T=u,I=void 0;c&&(I=+C-w.get(r,i));var E=void 0,M=void 0,k=void 0,P=void 0;if(d){var D=n.dataToPoint([C,_]);if(c)T=n.dataToPoint([I,_])[0];E=T,M=D[1]+y,k=D[0]-T,P=f,Math.abs(k)0)for(var s=0;s=0;--s)if(l[p]){r=l[p];break}r=r||o.none}if(Ur(r)){var c=null==e.level?0:e.level>=0?e.level:r.length+e.level;r=r[c=Math.min(c,r.length-1)]}}return Ny(new Date(e.value),r,a,i)}(e,t,n,this.getSetting("locale"),i)},t.prototype.getTicks=function(){var e=this._interval,t=this._extent,n=[];if(!e)return n;n.push({value:t[0],level:0});var i=this.getSetting("useUTC"),a=function(e,t,n,i){var a=1e4,r=Ay,o=0;function s(e,t,n,a,r,o,s){for(var l=new Date(t),p=t,c=l[a]();p1&&0===p&&r.unshift({value:r[0].value-u})}}for(p=0;p=i[0]&&y<=i[1]&&d++)}var v=(i[1]-i[0])/t;if(d>1.5*v&&u>v/1.5)break;if(p.push(g),d>v||e===r[m])break}c=[]}}0;var x=Lr(Br(p,(function(e){return Lr(e,(function(e){return e.value>=i[0]&&e.value<=i[1]&&!e.notAdd}))})),(function(e){return e.length>0})),b=[],w=x.length-1;for(m=0;mn&&(this._approxInterval=n);var r=_T.length,o=Math.min(function(e,t,n,i){for(;n>>1;e[a][1]16?16:e>7.5?7:e>3.5?4:e>1.5?2:1}function IT(e){return(e/=2592e6)>6?6:e>3?3:e>2?2:1}function ET(e){return(e/=Iy)>12?12:e>6?6:e>3.5?4:e>2?2:1}function MT(e,t){return(e/=t?Ty:_y)>30?30:e>20?20:e>15?15:e>10?10:e>5?5:e>2?2:1}function kT(e){return Jc(e,!0)}function PT(e,t,n){var i=new Date(e);switch(Ry(t)){case"year":case"month":i[Ky(n)](0);case"day":i[Yy(n)](1);case"hour":i[Xy(n)](0);case"minute":i[Zy(n)](0);case"second":i[Qy(n)](0),i[Jy(n)](0)}return i.getTime()}Y_.registerClass(CT);var DT=Y_.prototype,OT=pT.prototype,AT=qc,FT=Math.floor,RT=Math.ceil,BT=Math.pow,NT=Math.log,LT=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type="log",t.base=10,t._originalScale=new pT,t._interval=0,t}return ze(t,e),t.prototype.getTicks=function(e){var t=this._originalScale,n=this._extent,i=t.getExtent();return Br(OT.getTicks.call(this,e),(function(e){var t=e.value,a=qc(BT(this.base,t));return a=t===n[0]&&this._fixMin?qT(a,i[0]):a,{value:a=t===n[1]&&this._fixMax?qT(a,i[1]):a}}),this)},t.prototype.setExtent=function(e,t){var n=NT(this.base);e=NT(Math.max(0,e))/n,t=NT(Math.max(0,t))/n,OT.setExtent.call(this,e,t)},t.prototype.getExtent=function(){var e=this.base,t=DT.getExtent.call(this);t[0]=BT(e,t[0]),t[1]=BT(e,t[1]);var n=this._originalScale.getExtent();return this._fixMin&&(t[0]=qT(t[0],n[0])),this._fixMax&&(t[1]=qT(t[1],n[1])),t},t.prototype.unionExtent=function(e){this._originalScale.unionExtent(e);var t=this.base;e[0]=NT(e[0])/NT(t),e[1]=NT(e[1])/NT(t),DT.unionExtent.call(this,e)},t.prototype.unionExtentFromData=function(e,t){this.unionExtent(e.getApproximateExtent(t))},t.prototype.calcNiceTicks=function(e){e=e||10;var t=this._extent,n=t[1]-t[0];if(!(n===1/0||n<=0)){var i=Zc(n);for(e/n*i<=.5&&(i*=10);!isNaN(i)&&Math.abs(i)<1&&Math.abs(i)>0;)i*=10;var a=[qc(RT(t[0]/i)*i),qc(FT(t[1]/i)*i)];this._interval=i,this._niceExtent=a}},t.prototype.calcNiceExtent=function(e){OT.calcNiceExtent.call(this,e),this._fixMin=e.fixMin,this._fixMax=e.fixMax},t.prototype.parse=function(e){return e},t.prototype.contain=function(e){return aT(e=NT(e)/NT(this.base),this._extent)},t.prototype.normalize=function(e){return rT(e=NT(e)/NT(this.base),this._extent)},t.prototype.scale=function(e){return e=oT(e,this._extent),BT(this.base,e)},t.type="log",t}(Y_),VT=LT.prototype;function qT(e,t){return AT(e,zc(t))}VT.getMinorTicks=OT.getMinorTicks,VT.getLabel=OT.getLabel,Y_.registerClass(LT);var GT=function(){function e(e,t,n){this._prepareParams(e,t,n)}return e.prototype._prepareParams=function(e,t,n){n[1]0&&s>0&&!l&&(o=0),o<0&&s<0&&!p&&(s=0));var d=this._determinedMin,u=this._determinedMax;return null!=d&&(o=d,l=!0),null!=u&&(s=u,p=!0),{min:o,max:s,minFixed:l,maxFixed:p,isBlank:c}},e.prototype.modifyDataMinMax=function(e,t){this[UT[e]]=t},e.prototype.setDeterminedMinMax=function(e,t){var n=zT[e];this[n]=t},e.prototype.freeze=function(){this.frozen=!0},e}(),zT={min:"_determinedMin",max:"_determinedMax"},UT={min:"_dataMin",max:"_dataMax"};function jT(e,t,n){var i=e.rawExtentInfo;return i||(i=new GT(e,t,n),e.rawExtentInfo=i,i)}function HT(e,t){return null==t?null:to(t)?NaN:e.parse(t)}function WT(e,t){var n=e.type,i=jT(e,t,e.getExtent()).calculate();e.setBlank(i.isBlank);var a=i.min,r=i.max,o=t.ecModel;if(o&&"time"===n){var s=fT("bar",o),l=!1;if(Rr(s,(function(e){l=l||e.getBaseAxis()===t.axis})),l){var p=yT(s),c=function(e,t,n,i){var a=n.axis.getExtent(),r=a[1]-a[0],o=function(e,t,n){if(e&&t){var i=e[gT(t)];return null!=i&&null!=n?i[hT(n)]:i}}(i,n.axis);if(void 0===o)return{min:e,max:t};var s=1/0;Rr(o,(function(e){s=Math.min(e.offset,s)}));var l=-1/0;Rr(o,(function(e){l=Math.max(e.offset+e.width,l)})),s=Math.abs(s),l=Math.abs(l);var p=s+l,c=t-e,d=c/(1-(s+l)/r)-c;return t+=d*(l/p),e-=d*(s/p),{min:e,max:t}}(a,r,t,p);a=c.min,r=c.max}}return{extent:[a,r],fixMin:i.minFixed,fixMax:i.maxFixed}}function $T(e,t){var n=t,i=WT(e,n),a=i.extent,r=n.get("splitNumber");e instanceof LT&&(e.base=n.get("logBase"));var o=e.type,s=n.get("interval"),l="interval"===o||"time"===o;e.setExtent(a[0],a[1]),e.calcNiceExtent({splitNumber:r,fixMin:i.fixMin,fixMax:i.fixMax,minInterval:l?n.get("minInterval"):null,maxInterval:l?n.get("maxInterval"):null}),null!=s&&e.setInterval&&e.setInterval(s)}function KT(e,t){if(t=t||e.get("type"))switch(t){case"category":return new sT({ordinalMeta:e.getOrdinalMeta?e.getOrdinalMeta():e.getCategories(),extent:[1/0,-1/0]});case"time":return new CT({locale:e.ecModel.getLocaleModel(),useUTC:e.ecModel.get("useUTC")});case"value":return new pT({ticksGenerator:e.getTicksGenerator()});default:return new(Y_.getClass(t)||pT)}}function YT(e){var t,n,i=e.getLabelModel().get("formatter"),a="category"===e.type?e.scale.getExtent()[0]:null;return"time"===e.scale.type?(n=i,function(t,i){return e.scale.getFormattedLabel(t,i,n)}):Hr(i)?function(t){return function(n){var i=e.scale.getLabel(n);return t.replace("{value}",null!=i?i:"")}}(i):jr(i)?(t=i,function(n,i){return null!=a&&(i=n.value-a),t(XT(e,n),i,null!=n.level?{level:n.level}:null)}):function(t){return e.scale.getLabel(t)}}function XT(e,t){return"category"===e.type?e.scale.getLabel(t):t.value}function ZT(e,t){var n=t*Math.PI/180,i=e.width,a=e.height,r=i*Math.abs(Math.cos(n))+Math.abs(a*Math.sin(n)),o=i*Math.abs(Math.sin(n))+Math.abs(a*Math.cos(n));return new Ps(e.x,e.y,r,o)}function QT(e){var t=e.get("interval");return null==t?"auto":t}function JT(e){return"category"===e.type&&0===QT(e.getLabelModel())}function eI(e,t){var n={};return Rr(e.mapDimensionsAll(t),(function(t){n[$_(e,t)]=!0})),qr(n)}var tI=function(){function e(){}return e.prototype.getNeedCrossZero=function(){return!this.option.scale},e.prototype.getCoordSysModel=function(){},e}();var nI=[],iI={registerPreprocessor:a_,registerProcessor:r_,registerPostInit:function(e){o_("afterinit",e)},registerPostUpdate:function(e){o_("afterupdate",e)},registerUpdateLifecycle:o_,registerAction:s_,registerCoordinateSystem:function(e,t){sx.register(e,t)},registerLayout:function(e,t){c_(YC,e,t,1e3,"layout")},registerVisual:l_,registerTransform:u_,registerLoading:d_,registerMap:function(e,t,n){var i=pC("registerMap");i&&i(e,t,n)},registerImpl:function(e,t){lC[e]=t},PRIORITY:uC,ComponentModel:Sv,ComponentView:hw,SeriesModel:ow,ChartView:vw,registerComponentModel:function(e){Sv.registerClass(e)},registerComponentView:function(e){hw.registerClass(e)},registerSeriesModel:function(e){ow.registerClass(e)},registerChartView:function(e){vw.registerClass(e)},registerSubTypeDefaulter:function(e,t){Sv.registerSubTypeDefaulter(e,t)},registerPainter:function(e,t){Fc(e,t)}};function aI(e){Ur(e)?Rr(e,(function(e){aI(e)})):Dr(nI,e)>=0||(nI.push(e),jr(e)&&(e={install:e}),e.install(iI))}function rI(e,t){return Math.abs(e-t)<1e-8}function oI(e,t,n){var i=0,a=e[0];if(!a)return!1;for(var r=1;rn&&(e=a,n=o)}if(e)return function(e){for(var t=0,n=0,i=0,a=e.length,r=e[a-1][0],o=e[a-1][1],s=0;s>1^-(1&s),l=l>>1^-(1&l),a=s+=a,r=l+=r,i.push([s/n,l/n])}return i}function yI(e,t){return Br(Lr((e=function(e){if(!e.UTF8Encoding)return e;var t=e,n=t.UTF8Scale;return null==n&&(n=1024),Rr(t.features,(function(e){var t=e.geometry,i=t.encodeOffsets,a=t.coordinates;if(i)switch(t.type){case"LineString":t.coordinates=fI(a,i,n);break;case"Polygon":case"MultiLineString":gI(a,i,n);break;case"MultiPolygon":Rr(a,(function(e,t){return gI(e,i[t],n)}))}})),t.UTF8Encoding=!1,t}(e)).features,(function(e){return e.geometry&&e.properties&&e.geometry.coordinates.length>0})),(function(e){var n=e.properties,i=e.geometry,a=[];switch(i.type){case"Polygon":var r=i.coordinates;a.push(new dI(r[0],r.slice(1)));break;case"MultiPolygon":Rr(i.coordinates,(function(e){e[0]&&a.push(new dI(e[0],e.slice(1)))}));break;case"LineString":a.push(new uI([i.coordinates]));break;case"MultiLineString":a.push(new uI(i.coordinates))}var o=new mI(n[t||"name"],a,n.cp);return o.properties=n,o}))}var vI=Cd();function xI(e){return"category"===e.type?function(e){var t=e.getLabelModel(),n=wI(e,t);return!t.get("show")||e.scale.isBlank()?{labels:[],labelCategoryInterval:n.labelCategoryInterval}:n}(e):function(e){var t=e.scale.getTicks(),n=YT(e);return{labels:Br(t,(function(t,i){return{level:t.level,formattedLabel:n(t,i),rawLabel:e.scale.getLabel(t),tickValue:t.value}}))}}(e)}function bI(e,t){return"category"===e.type?function(e,t){var n,i,a=SI(e,"ticks"),r=QT(t),o=CI(a,r);if(o)return o;t.get("show")&&!e.scale.isBlank()||(n=[]);if(jr(r))n=II(e,r,!0);else if("auto"===r){var s=wI(e,e.getLabelModel());i=s.labelCategoryInterval,n=Br(s.labels,(function(e){return e.tickValue}))}else n=TI(e,i=r,!0);return _I(a,r,{ticks:n,tickCategoryInterval:i})}(e,t):{ticks:Br(e.scale.getTicks(),(function(e){return e.value}))}}function wI(e,t){var n,i,a=SI(e,"labels"),r=QT(t),o=CI(a,r);return o||(jr(r)?n=II(e,r):(i="auto"===r?function(e){var t=vI(e).autoInterval;return null!=t?t:vI(e).autoInterval=e.calculateCategoryInterval()}(e):r,n=TI(e,i)),_I(a,r,{labels:n,labelCategoryInterval:i}))}function SI(e,t){return vI(e)[t]||(vI(e)[t]=[])}function CI(e,t){for(var n=0;n1&&c/l>2&&(p=Math.round(Math.ceil(p/l)*l));var d=JT(e),u=o.get("showMinLabel")||d,m=o.get("showMaxLabel")||d;u&&p!==r[0]&&g(r[0]);for(var h=p;h<=r[1];h+=l)g(h);function g(e){var t={value:e};s.push(n?e:{formattedLabel:i(t),rawLabel:a.getLabel(t),tickValue:e})}return m&&h-l!==r[1]&&g(r[1]),s}function II(e,t,n){var i=e.scale,a=YT(e),r=[];return Rr(i.getTicks(),(function(e){var o=i.getLabel(e),s=e.value;t(e.value,o)&&r.push(n?s:{formattedLabel:a(e),rawLabel:o,tickValue:s})})),r}var EI=[0,1],MI=function(){function e(e,t,n){this.onBand=!1,this.inverse=!1,this.dim=e,this.scale=t,this._extent=n||[0,0]}return e.prototype.contain=function(e){var t=this._extent,n=Math.min(t[0],t[1]),i=Math.max(t[0],t[1]);return e>=n&&e<=i},e.prototype.containData=function(e){return this.scale.contain(e)},e.prototype.getExtent=function(){return this._extent.slice()},e.prototype.getPixelPrecision=function(e){return jc(e||this.scale.getExtent(),this._extent)},e.prototype.setExtent=function(e,t){var n=this._extent;n[0]=e,n[1]=t},e.prototype.dataToCoord=function(e,t){var n=this._extent,i=this.scale;return e=i.normalize(e),this.onBand&&"ordinal"===i.type&&kI(n=n.slice(),i.count()),Lc(e,EI,n,t)},e.prototype.coordToData=function(e,t){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&kI(n=n.slice(),i.count());var a=Lc(e,n,EI,t);return this.scale.scale(a)},e.prototype.pointToData=function(e,t){},e.prototype.getTicksCoords=function(e){var t=(e=e||{}).tickModel||this.getTickModel(),n=Br(bI(this,t).ticks,(function(e){return{coord:this.dataToCoord("ordinal"===this.scale.type?this.scale.getRawOrdinalNumber(e):e),tickValue:e}}),this);return function(e,t,n,i){var a=t.length;if(!e.onBand||n||!a)return;var r,o,s=e.getExtent();if(1===a)t[0].coord=s[0],r=t[1]={coord:s[1]};else{var l=t[a-1].tickValue-t[0].tickValue,p=(t[a-1].coord-t[0].coord)/l;Rr(t,(function(e){e.coord-=p/2})),o=1+e.scale.getExtent()[1]-t[a-1].tickValue,r={coord:t[a-1].coord+p*o},t.push(r)}var c=s[0]>s[1];d(t[0].coord,s[0])&&(i?t[0].coord=s[0]:t.shift());i&&d(s[0],t[0].coord)&&t.unshift({coord:s[0]});d(s[1],r.coord)&&(i?r.coord=s[1]:t.pop());i&&d(r.coord,s[1])&&t.push({coord:s[1]});function d(e,t){return e=qc(e),t=qc(t),c?e>t:e0&&e<100||(e=5),Br(this.scale.getMinorTicks(e),(function(e){return Br(e,(function(e){return{coord:this.dataToCoord(e),tickValue:e}}),this)}),this)},e.prototype.getViewLabels=function(){return xI(this).labels},e.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},e.prototype.getTickModel=function(){return this.model.getModel("axisTick")},e.prototype.getBandWidth=function(){if("time"===this.type){var e=this.model,t=e.get("bandWidthCalculator"),n=void 0;if(jr(t))try{if(n=t(e))return n}catch(e){}}var i=this._extent,a=this.scale.getExtent(),r=a[1]-a[0]+(this.onBand?1:0);0===r&&(r=1);var o=Math.abs(i[1]-i[0]);return Math.abs(o)/r},e.prototype.calculateCategoryInterval=function(){return function(e){var t=function(e){var t=e.getLabelModel();return{axisRotate:e.getRotate?e.getRotate():e.isHorizontal&&!e.isHorizontal()?90:0,labelRotate:t.get("rotate")||0,font:t.getFont()}}(e),n=YT(e),i=(t.axisRotate-t.labelRotate)/180*Math.PI,a=e.scale,r=a.getExtent(),o=a.count();if(r[1]-r[0]<1)return 0;var s=1;o>40&&(s=Math.max(1,Math.floor(o/40)));for(var l=r[0],p=e.dataToCoord(l+1)-e.dataToCoord(l),c=Math.abs(p*Math.cos(i)),d=Math.abs(p*Math.sin(i)),u=0,m=0;l<=r[1];l+=s){var h,g,f=uc(n({value:l}),t.font,"center","top");h=1.3*f.width,g=1.3*f.height,u=Math.max(u,h,7),m=Math.max(m,g,7)}var y=u/c,v=m/d;isNaN(y)&&(y=1/0),isNaN(v)&&(v=1/0);var x=Math.max(0,Math.floor(Math.min(y,v))),b=vI(e.model),w=e.getExtent(),S=b.lastAutoInterval,C=b.lastTickCount;return null!=S&&null!=C&&Math.abs(S-x)<=1&&Math.abs(C-o)<=1&&S>x&&b.axisExtent0===w[0]&&b.axisExtent1===w[1]?x=S:(b.lastTickCount=o,b.lastAutoInterval=x,b.axisExtent0=w[0],b.axisExtent1=w[1]),x}(this)},e}();function kI(e,t){var n=(e[1]-e[0])/t/2;e[0]+=n,e[1]-=n}var PI=2*Math.PI,DI=Yu.CMD,OI=["top","right","bottom","left"];function AI(e,t,n,i,a){var r=n.width,o=n.height;switch(e){case"top":i.set(n.x+r/2,n.y-t),a.set(0,-1);break;case"bottom":i.set(n.x+r/2,n.y+o+t),a.set(0,1);break;case"left":i.set(n.x-t,n.y+o/2),a.set(-1,0);break;case"right":i.set(n.x+r+t,n.y+o/2),a.set(1,0)}}function FI(e,t,n,i,a,r,o,s,l){o-=e,s-=t;var p=Math.sqrt(o*o+s*s),c=(o/=p)*n+e,d=(s/=p)*n+t;if(Math.abs(i-a)%PI<1e-4)return l[0]=c,l[1]=d,p-n;if(r){var u=i;i=em(a),a=em(u)}else i=em(i),a=em(a);i>a&&(a+=PI);var m=Math.atan2(s,o);if(m<0&&(m+=PI),m>=i&&m<=a||m+PI>=i&&m+PI<=a)return l[0]=c,l[1]=d,p-n;var h=n*Math.cos(i)+e,g=n*Math.sin(i)+t,f=n*Math.cos(a)+e,y=n*Math.sin(a)+t,v=(h-o)*(h-o)+(g-s)*(g-s),x=(f-o)*(f-o)+(y-s)*(y-s);return v0){t=t/180*Math.PI,qI.fromArray(e[0]),GI.fromArray(e[1]),zI.fromArray(e[2]),ws.sub(UI,qI,GI),ws.sub(jI,zI,GI);var n=UI.len(),i=jI.len();if(!(n<.001||i<.001)){UI.scale(1/n),jI.scale(1/i);var a=UI.dot(jI);if(Math.cos(t)1&&ws.copy($I,zI),$I.toArray(e[1])}}}}function YI(e,t,n){if(n<=180&&n>0){n=n/180*Math.PI,qI.fromArray(e[0]),GI.fromArray(e[1]),zI.fromArray(e[2]),ws.sub(UI,GI,qI),ws.sub(jI,zI,GI);var i=UI.len(),a=jI.len();if(!(i<.001||a<.001))if(UI.scale(1/i),jI.scale(1/a),UI.dot(t)=o)ws.copy($I,zI);else{$I.scaleAndAdd(jI,r/Math.tan(Math.PI/2-s));var l=zI.x!==GI.x?($I.x-GI.x)/(zI.x-GI.x):($I.y-GI.y)/(zI.y-GI.y);if(isNaN(l))return;l<0?ws.copy($I,GI):l>1&&ws.copy($I,zI)}$I.toArray(e[1])}}}function XI(e,t,n,i){var a="normal"===n,r=a?e:e.ensureState(n);r.ignore=t;var o=i.get("smooth");o&&!0===o&&(o=.3),r.shape=r.shape||{},o>0&&(r.shape.smooth=o);var s=i.getModel("lineStyle").getLineStyle();a?e.useStyle(s):r.style=s}function ZI(e,t){var n=t.smooth,i=t.points;if(i)if(e.moveTo(i[0][0],i[0][1]),n>0&&i.length>=3){var a=Fo(i[0],i[1]),r=Fo(i[1],i[2]);if(!a||!r)return e.lineTo(i[1][0],i[1][1]),void e.lineTo(i[2][0],i[2][1]);var o=Math.min(a,r)*n,s=No([],i[1],i[0],o/a),l=No([],i[1],i[2],o/r),p=No([],s,l,.5);e.bezierCurveTo(s[0],s[1],s[0],s[1],p[0],p[1]),e.bezierCurveTo(l[0],l[1],l[0],l[1],i[2][0],i[2][1])}else for(var c=1;c0&&r&&S(-d/o,0,o);var f,y,v=e[0],x=e[o-1];return b(),f<0&&C(-f,.8),y<0&&C(y,.8),b(),w(f,y,1),w(y,f,-1),b(),f<0&&_(-f),y<0&&_(y),p}function b(){f=v.rect[t]-i,y=a-x.rect[t]-x.rect[n]}function w(e,t,n){if(e<0){var i=Math.min(t,-e);if(i>0){S(i*n,0,o);var a=i+e;a<0&&C(-a*n,1)}else C(-e*n,1)}}function S(n,i,a){0!==n&&(p=!0);for(var r=i;r0)for(l=0;l0;l--){S(-(r[l-1]*d),l,o)}}}function _(e){var t=e<0?-1:1;e=Math.abs(e);for(var n=Math.ceil(e/(o-1)),i=0;i0?S(n,0,i+1):S(-n,o-i-1,o),(e-=n)<=0)return}}function nE(e,t,n,i){return tE(e,"y","height",t,n,i)}function iE(e){var t=[];e.sort((function(e,t){return t.priority-e.priority}));var n=new Ps(0,0,0,0);function i(e){if(!e.ignore){var t=e.ensureState("emphasis");null==t.ignore&&(t.ignore=!1)}e.ignore=!0}for(var a=0;a=0&&n.attr(m.oldLayoutSelect),Dr(p,"emphasis")>=0&&n.attr(m.oldLayoutEmphasis)),tf(n,s,t,o)}else if(n.attr(s),!Zf(n).valueAnimation){var c=io(n.style.opacity,1);n.style.opacity=0,nf(n,{style:{opacity:c}},t,o)}if(m.oldLayout=s,n.states.select){var d=m.oldLayoutSelect={};cE(d,s,dE),cE(d,n.states.select,dE)}if(n.states.emphasis){var u=m.oldLayoutEmphasis={};cE(u,s,dE),cE(u,n.states.emphasis,dE)}Jf(n,o,l,t,t)}if(i&&!i.ignore&&!i.invisible){a=(m=pE(i)).oldLayout;var m,h={points:i.shape.points};a?(i.attr({shape:a}),tf(i,{shape:h},t)):(i.setShape(h),i.style.strokePercent=0,nf(i,{style:{strokePercent:1}},t)),m.oldLayout=h}},e}(),mE=Cd();function hE(e){e.registerUpdateLifecycle("series:beforeupdate",(function(e,t,n){var i=mE(t).labelManager;i||(i=mE(t).labelManager=new uE),i.clearLabels()})),e.registerUpdateLifecycle("series:layoutlabels",(function(e,t,n){var i=mE(t).labelManager;n.updatedSeries.forEach((function(e){i.addLabelsOfSeries(t.getViewOfSeriesModel(e))})),i.updateLayoutConfig(t),i.layout(t),i.processLabelsOverall()}))}aI(hE);var gE=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n}return ze(t,e),t.prototype.getInitialData=function(e){return K_(null,this,{useEncodeDefaulter:!0})},t.prototype.getLegendIcon=function(e){var t=new Mc,n=PS("line",0,e.itemHeight/2,e.itemWidth,0,e.lineStyle.stroke,!1);t.add(n),n.setStyle(e.lineStyle);var i=this.getData().getVisual("symbol"),a=this.getData().getVisual("symbolRotate"),r="none"===i?"circle":i,o=.8*e.itemHeight,s=PS(r,(e.itemWidth-o)/2,(e.itemHeight-o)/2,o,o,e.itemStyle.fill);t.add(s),s.setStyle(e.itemStyle);var l="inherit"===e.iconRotate?a:e.iconRotate||0;return s.rotation=l*Math.PI/180,s.setOrigin([e.itemWidth/2,e.itemHeight/2]),r.indexOf("empty")>-1&&(s.style.stroke=s.style.fill,s.style.fill="#fff",s.style.lineWidth=2),t},t.type="series.line",t.dependencies=["grid","polar"],t.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1},t}(ow);function fE(e,t){var n=e.mapDimensionsAll("defaultedLabel"),i=n.length;if(1===i){var a=ib(e,t,n[0]);return null!=a?a+"":null}if(i){for(var r=[],o=0;o=0&&i.push(t[r])}return i.join(" ")}var vE=function(e){function t(t,n,i,a){var r=e.call(this)||this;return r.updateData(t,n,i,a),r}return ze(t,e),t.prototype._createSymbol=function(e,t,n,i,a){this.removeAll();var r=PS(e,-1,-1,2,2,null,a);r.attr({z2:100,culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),r.drift=xE,this._symbolType=e,this.add(r)},t.prototype.stopSymbolAnimation=function(e){this.childAt(0).stopAnimation(null,e)},t.prototype.getSymbolType=function(){return this._symbolType},t.prototype.getSymbolPath=function(){return this.childAt(0)},t.prototype.highlight=function(){vh(this.childAt(0))},t.prototype.downplay=function(){xh(this.childAt(0))},t.prototype.setZ=function(e,t){var n=this.childAt(0);n.zlevel=e,n.z=t},t.prototype.setDraggable=function(e,t){var n=this.childAt(0);n.draggable=e,n.cursor=!t&&e?"move":n.cursor},t.prototype.updateData=function(e,n,i,a){this.silent=!1;var r=e.getItemVisual(n,"symbol")||"circle",o=e.hostModel,s=t.getSymbolSize(e,n),l=r!==this._symbolType,p=a&&a.disableAnimation;if(l){var c=e.getItemVisual(n,"symbolKeepAspect");this._createSymbol(r,e,n,s,c)}else{(u=this.childAt(0)).silent=!1;var d={scaleX:s[0]/2,scaleY:s[1]/2};p?u.attr(d):tf(u,d,o,n),lf(u)}if(this._updateCommon(e,n,s,i,a),l){var u=this.childAt(0);if(!p){d={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:u.style.opacity}};u.scaleX=u.scaleY=0,u.style.opacity=0,nf(u,d,o,n)}}p&&this.childAt(0).stopAnimation("leave")},t.prototype._updateCommon=function(e,t,n,i,a){var r,o,s,l,p,c,d,u,m,h=this.childAt(0),g=e.hostModel;if(i&&(r=i.emphasisItemStyle,o=i.blurItemStyle,s=i.selectItemStyle,l=i.focus,p=i.blurScope,d=i.labelStatesModels,u=i.hoverScale,m=i.cursorStyle,c=i.emphasisDisabled),!i||e.hasItemOption){var f=i&&i.itemModel?i.itemModel:e.getItemModel(t),y=f.getModel("emphasis");r=y.getModel("itemStyle").getItemStyle(),s=f.getModel(["select","itemStyle"]).getItemStyle(),o=f.getModel(["blur","itemStyle"]).getItemStyle(),l=y.get("focus"),p=y.get("blurScope"),c=y.get("disabled"),d=Uf(f),u=y.getShallow("scale"),m=f.getShallow("cursor")}var v=e.getItemVisual(t,"symbolRotate");h.attr("rotation",(v||0)*Math.PI/180||0);var x=OS(e.getItemVisual(t,"symbolOffset"),n);x&&(h.x=x[0],h.y=x[1]),m&&h.attr("cursor",m);var b=e.getItemVisual(t,"style"),w=b.fill;if(h instanceof bm){var S=h.style;h.useStyle(kr({image:S.image,x:S.x,y:S.y,width:S.width,height:S.height},b))}else h.__isEmptyBrush?h.useStyle(kr({},b)):h.useStyle(b),h.style.decal=null,h.setColor(w,a&&a.symbolInnerColor),h.style.strokeNoScale=!0;var C=e.getItemVisual(t,"liftZ"),_=this._z2;null!=C?null==_&&(this._z2=h.z2,h.z2+=C):null!=_&&(h.z2=_,this._z2=null);var T=a&&a.useNameLabel;zf(h,d,{labelFetcher:g,labelDataIndex:t,defaultText:function(t){return T?e.getName(t):fE(e,t)},inheritColor:w,defaultOpacity:b.opacity}),this._sizeX=n[0]/2,this._sizeY=n[1]/2;var I=h.ensureState("emphasis");I.style=r,h.ensureState("select").style=s,h.ensureState("blur").style=o;var E=null==u||!0===u?Math.max(1.1,3/this._sizeY):isFinite(u)&&u>0?+u:1;I.scaleX=this._sizeX*E,I.scaleY=this._sizeY*E,this.setSymbolScale(1),Oh(this,l,p,c)},t.prototype.setSymbolScale=function(e){this.scaleX=this.scaleY=e},t.prototype.fadeOut=function(e,t,n){var i=this.childAt(0),a=Um(this).dataIndex,r=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var o=i.getTextContent();o&&rf(o,{style:{opacity:0}},t,{dataIndex:a,removeOpt:r,cb:function(){i.removeTextContent()}})}else i.removeTextContent();rf(i,{style:{opacity:0},scaleX:0,scaleY:0},t,{dataIndex:a,cb:e,removeOpt:r})},t.getSymbolSize=function(e,t){return DS(e.getItemVisual(t,"symbolSize"))},t}(Mc);function xE(e,t){this.parent.drift(e,t)}function bE(e,t,n,i){return t&&!isNaN(t[0])&&!isNaN(t[1])&&!(i.isIgnore&&i.isIgnore(n))&&!(i.clipShape&&!i.clipShape.contain(t[0],t[1]))&&"none"!==e.getItemVisual(n,"symbol")}function wE(e){return null==e||Kr(e)||(e={isIgnore:e}),e||{}}function SE(e){var t=e.hostModel,n=t.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:t.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:t.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:Uf(t),cursorStyle:t.get("cursor")}}var CE=function(){function e(e){this.group=new Mc,this._SymbolCtor=e||vE}return e.prototype.updateData=function(e,t){this._progressiveEls=null,t=wE(t);var n=this.group,i=e.hostModel,a=this._data,r=this._SymbolCtor,o=t.disableAnimation,s=SE(e),l={disableAnimation:o},p=t.getSymbolPoint||function(t){return e.getItemLayout(t)};a||n.removeAll(),e.diff(a).add((function(i){var a=p(i);if(bE(e,a,i,t)){var o=new r(e,i,s,l);o.setPosition(a),e.setItemGraphicEl(i,o),n.add(o)}})).update((function(c,d){var u=a.getItemGraphicEl(d),m=p(c);if(bE(e,m,c,t)){var h=e.getItemVisual(c,"symbol")||"circle",g=u&&u.getSymbolType&&u.getSymbolType();if(!u||g&&g!==h)n.remove(u),(u=new r(e,c,s,l)).setPosition(m);else{u.updateData(e,c,s,l);var f={x:m[0],y:m[1]};o?u.attr(f):tf(u,f,i)}n.add(u),e.setItemGraphicEl(c,u)}else n.remove(u)})).remove((function(e){var t=a.getItemGraphicEl(e);t&&t.fadeOut((function(){n.remove(t)}),i)})).execute(),this._getSymbolPoint=p,this._data=e},e.prototype.updateLayout=function(){var e=this,t=this._data;t&&t.eachItemGraphicEl((function(t,n){var i=e._getSymbolPoint(n);t.setPosition(i),t.markRedraw()}))},e.prototype.incrementalPrepareUpdate=function(e){this._seriesScope=SE(e),this._data=null,this.group.removeAll()},e.prototype.incrementalUpdate=function(e,t,n){function i(e){e.isGroup||(e.incremental=!0,e.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[],n=wE(n);for(var a=e.start;a0?n=i[0]:i[1]<0&&(n=i[1]);return n}(a,n),o=i.dim,s=a.dim,l=t.mapDimension(s),p=t.mapDimension(o),c="x"===s||"radius"===s?1:0,d=Br(e.dimensions,(function(e){return t.mapDimension(e)})),u=!1,m=t.getCalculationInfo("stackResultDimension");return W_(t,d[0])&&(u=!0,d[0]=m),W_(t,d[1])&&(u=!0,d[1]=m),{dataDimsForPoint:d,valueStart:r,valueAxisDim:s,baseAxisDim:o,stacked:!!u,valueDim:l,baseDim:p,baseDataOffset:c,stackedOverDimension:t.getCalculationInfo("stackedOverDimension")}}function TE(e,t,n,i){var a=NaN;e.stacked&&(a=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(a)&&(a=e.valueStart);var r=e.baseDataOffset,o=[];return o[r]=n.get(e.baseDim,i),o[1-r]=a,t.dataToPoint(o)}var IE=Math.min,EE=Math.max;function ME(e,t){return isNaN(e)||isNaN(t)}function kE(e,t,n,i,a,r,o,s,l){for(var p,c,d,u,m,h,g=n,f=0;f=a||g<0)break;if(ME(y,v)){if(l){g+=r;continue}break}if(g===n)e[r>0?"moveTo":"lineTo"](y,v),d=y,u=v;else{var x=y-p,b=v-c;if(x*x+b*b<.5){g+=r;continue}if(o>0){for(var w=g+r,S=t[2*w],C=t[2*w+1];S===y&&C===v&&f=i||ME(S,C))m=y,h=v;else{I=S-p,E=C-c;var P=y-p,D=S-y,O=v-c,A=C-v,F=void 0,R=void 0;if("x"===s){var B=I>0?1:-1;m=y-B*(F=Math.abs(P))*o,h=v,M=y+B*(R=Math.abs(D))*o,k=v}else if("y"===s){var N=E>0?1:-1;m=y,h=v-N*(F=Math.abs(O))*o,M=y,k=v+N*(R=Math.abs(A))*o}else F=Math.sqrt(P*P+O*O),m=y-I*o*(1-(T=(R=Math.sqrt(D*D+A*A))/(R+F))),h=v-E*o*(1-T),k=v+E*o*T,M=IE(M=y+I*o*T,EE(S,y)),k=IE(k,EE(C,v)),M=EE(M,IE(S,y)),h=v-(E=(k=EE(k,IE(C,v)))-v)*F/R,m=IE(m=y-(I=M-y)*F/R,EE(p,y)),h=IE(h,EE(c,v)),M=y+(I=y-(m=EE(m,IE(p,y))))*R/F,k=v+(E=v-(h=EE(h,IE(c,v))))*R/F}e.bezierCurveTo(d,u,m,h,y,v),d=M,u=k}else e.lineTo(y,v)}p=y,c=v,g+=r}return f}var PE=function(){this.smooth=0,this.smoothConstraint=!0},DE=function(e){function t(t){var n=e.call(this,t)||this;return n.type="ec-polyline",n}return ze(t,e),t.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},t.prototype.getDefaultShape=function(){return new PE},t.prototype.buildPath=function(e,t){var n=t.points,i=0,a=n.length/2;if(t.connectNulls){for(;a>0&&ME(n[2*a-2],n[2*a-1]);a--);for(;i=0){var f=o?(c-i)*g+i:(p-n)*g+n;return o?[e,f]:[f,e]}n=p,i=c;break;case r.C:p=a[l++],c=a[l++],d=a[l++],u=a[l++],m=a[l++],h=a[l++];var y=o?ul(n,p,d,m,e,s):ul(i,c,u,h,e,s);if(y>0)for(var v=0;v=0){f=o?cl(i,c,u,h,x):cl(n,p,d,m,x);return o?[e,f]:[f,e]}}n=m,i=h}}},t}(gm),OE=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t}(PE),AE=function(e){function t(t){var n=e.call(this,t)||this;return n.type="ec-polygon",n}return ze(t,e),t.prototype.getDefaultShape=function(){return new OE},t.prototype.buildPath=function(e,t){var n=t.points,i=t.stackedOnPoints,a=0,r=n.length/2,o=t.smoothMonotone;if(t.connectNulls){for(;r>0&&ME(n[2*r-2],n[2*r-1]);r--);for(;a=0;o--){var s=e.getDimensionInfo(i[o].dimension);if("x"===(a=s&&s.coordDim)||"y"===a){r=i[o];break}}if(r){var l=t.getAxis(a),p=Br(r.stops,(function(e){return{coord:l.toGlobalCoord(l.dataToCoord(e.value)),color:e.color}})),c=p.length,d=r.outerColors.slice();c&&p[0].coord>p[c-1].coord&&(p.reverse(),d.reverse());var u=function(e,t){var n,i,a=[],r=e.length;function o(e,t,n){var i=e.coord;return{coord:n,color:Hl((n-i)/(t.coord-i),[e.color,t.color])}}for(var s=0;st){i?a.push(o(i,l,t)):n&&a.push(o(n,l,0),o(n,l,t));break}n&&(a.push(o(n,l,0)),n=null),a.push(l),i=l}}return a}(p,"x"===a?n.getWidth():n.getHeight()),m=u.length;if(!m&&c)return p[0].coord<0?d[1]?d[1]:p[c-1].color:d[0]?d[0]:p[0].color;var h=u[0].coord-10,g=u[m-1].coord+10,f=g-h;if(f<.001)return"transparent";Rr(u,(function(e){e.offset=(e.coord-h)/f})),u.push({offset:m?u[m-1].offset:.5,color:d[1]||"transparent"}),u.unshift({offset:m?u[0].offset:.5,color:d[0]||"transparent"});var y=new Ug(0,0,0,0,u,!0);return y[a]=h,y[a+"2"]=g,y}}}function jE(e,t,n){var i=e.get("showAllSymbol"),a="auto"===i;if(!i||a){var r=n.getAxesByScale("ordinal")[0];if(r&&(!a||!function(e,t){var n=e.getExtent(),i=Math.abs(n[1]-n[0])/e.scale.count();isNaN(i)&&(i=0);for(var a=t.count(),r=Math.max(1,Math.round(a/5)),o=0;oi)return!1;return!0}(r,t))){var o=t.mapDimension(r.dim),s={};return Rr(r.getViewLabels(),(function(e){var t=r.scale.getRawOrdinalNumber(e.tickValue);s[t]=1})),function(e){return!s.hasOwnProperty(t.get(o,e))}}}}function HE(e,t){return[e[2*t],e[2*t+1]]}function WE(e){if(e.get(["endLabel","show"]))return!0;for(var t=0;t0&&"bolder"===e.get(["emphasis","lineStyle","width"]))&&(m.getState("emphasis").style.lineWidth=+m.style.lineWidth+1);Um(m).seriesIndex=e.seriesIndex,Oh(m,P,D,O);var A=GE(e.get("smooth")),F=e.get("smoothMonotone");if(m.setShape({smooth:A,smoothMonotone:F,connectNulls:S}),h){var R=o.getCalculationInfo("stackedOnSeries"),B=0;h.useStyle(Pr(l.getAreaStyle(),{fill:E,opacity:.7,lineJoin:"bevel",decal:o.getVisual("style").decal})),R&&(B=GE(R.get("smooth"))),h.setShape({smooth:A,stackedOnSmooth:B,smoothMonotone:F,connectNulls:S}),Bh(h,e,"areaStyle"),Um(h).seriesIndex=e.seriesIndex,Oh(h,P,D,O)}var N=function(e){i._changePolyState(e)};o.eachItemGraphicEl((function(e){e&&(e.onHoverStateChange=N)})),this._polyline.onHoverStateChange=N,this._data=o,this._coordSys=a,this._stackedOnPoints=b,this._points=p,this._step=I,this._valueOrigin=v,e.get("triggerLineEvent")&&(this.packEventData(e,m),h&&this.packEventData(e,h))},t.prototype.packEventData=function(e,t){Um(t).eventData={componentType:"series",componentSubType:"line",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"line"}},t.prototype.highlight=function(e,t,n,i){var a=e.getData(),r=Sd(a,i);if(this._changePolyState("emphasis"),!(r instanceof Array)&&null!=r&&r>=0){var o=a.getLayout("points"),s=a.getItemGraphicEl(r);if(!s){var l=o[2*r],p=o[2*r+1];if(isNaN(l)||isNaN(p))return;if(this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(l,p))return;var c=e.get("zlevel")||0,d=e.get("z")||0;(s=new vE(a,r)).x=l,s.y=p,s.setZ(c,d);var u=s.getSymbolPath().getTextContent();u&&(u.zlevel=c,u.z=d,u.z2=this._polyline.z2+1),s.__temp=!0,a.setItemGraphicEl(r,s),s.stopSymbolAnimation(!0),this.group.add(s)}s.highlight()}else vw.prototype.highlight.call(this,e,t,n,i)},t.prototype.downplay=function(e,t,n,i){var a=e.getData(),r=Sd(a,i);if(this._changePolyState("normal"),null!=r&&r>=0){var o=a.getItemGraphicEl(r);o&&(o.__temp?(a.setItemGraphicEl(r,null),this.group.remove(o)):o.downplay())}else vw.prototype.downplay.call(this,e,t,n,i)},t.prototype._changePolyState=function(e){var t=this._polygon;mh(this._polyline,e),t&&mh(t,e)},t.prototype._newPolyline=function(e){var t=this._polyline;return t&&this._lineGroup.remove(t),t=new DE({shape:{points:e},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(t),this._polyline=t,t},t.prototype._newPolygon=function(e,t){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new AE({shape:{points:e,stackedOnPoints:t},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},t.prototype._initSymbolLabelAnimation=function(e,t,n){var i,a,r=t.getBaseAxis(),o=r.inverse;"cartesian2d"===t.type?(i=r.isHorizontal(),a=!1):"polar"===t.type&&(i="angle"===r.dim,a=!0);var s=e.hostModel,l=s.get("animationDuration");jr(l)&&(l=l(null));var p=s.get("animationDelay")||0,c=jr(p)?p(null):p;e.eachItemGraphicEl((function(e,r){var s=e;if(s){var d=[e.x,e.y],u=void 0,m=void 0,h=void 0;if(n)if(a){var g=n,f=t.pointToCoord(d);i?(u=g.startAngle,m=g.endAngle,h=-f[1]/180*Math.PI):(u=g.r0,m=g.r,h=f[0])}else{var y=n;i?(u=y.x,m=y.x+y.width,h=e.x):(u=y.y+y.height,m=y.y,h=e.y)}var v=m===u?0:(h-u)/(m-u);o&&(v=1-v);var x=jr(p)?p(r):l*v+c,b=s.getSymbolPath(),w=b.getTextContent();s.attr({scaleX:0,scaleY:0}),s.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:x}),w&&w.animateFrom({style:{opacity:0}},{duration:300,delay:x}),b.disableLabelAnimation=!0}}))},t.prototype._initOrUpdateEndLabel=function(e,t,n){var i=e.getModel("endLabel");if(WE(e)){var a=e.getData(),r=this._polyline,o=a.getLayout("points");if(!o)return r.removeTextContent(),void(this._endLabel=null);var s=this._endLabel;s||((s=this._endLabel=new Pm({z2:200})).ignoreClip=!0,r.setTextContent(this._endLabel),r.disableLabelAnimation=!0);var l=function(e){for(var t,n,i=e.length/2;i>0&&(t=e[2*i-2],n=e[2*i-1],isNaN(t)||isNaN(n));i--);return i-1}(o);l>=0&&(zf(r,Uf(e,"endLabel"),{inheritColor:n,labelFetcher:e,labelDataIndex:l,defaultText:function(e,t,n){return null!=n?yE(a,n):fE(a,e)},enableTextSetter:!0},function(e,t){var n=t.getBaseAxis(),i=n.isHorizontal(),a=n.inverse,r=i?a?"right":"left":"center",o=i?"middle":a?"top":"bottom";return{normal:{align:e.get("align")||r,verticalAlign:e.get("verticalAlign")||o}}}(i,t)),r.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},t.prototype._endLabelOnDuring=function(e,t,n,i,a,r,o){var s=this._endLabel,l=this._polyline;if(s){e<1&&null==i.originalX&&(i.originalX=s.x,i.originalY=s.y);var p=n.getLayout("points"),c=n.hostModel,d=c.get("connectNulls"),u=r.get("precision"),m=r.get("distance")||0,h=o.getBaseAxis(),g=h.isHorizontal(),f=h.inverse,y=t.shape,v=f?g?y.x:y.y+y.height:g?y.x+y.width:y.y,x=(g?m:0)*(f?-1:1),b=(g?0:-m)*(f?-1:1),w=g?"x":"y",S=function(e,t,n){for(var i,a,r=e.length/2,o="x"===n?0:1,s=0,l=-1,p=0;p=t||i>=t&&a<=t){l=p;break}s=p,i=a}else i=a;return{range:[s,l],t:(t-i)/(a-i)}}(p,v,w),C=S.range,_=C[1]-C[0],T=void 0;if(_>=1){if(_>1&&!d){var I=HE(p,C[0]);s.attr({x:I[0]+x,y:I[1]+b}),a&&(T=c.getRawValue(C[0]))}else{(I=l.getPointOn(v,w))&&s.attr({x:I[0]+x,y:I[1]+b});var E=c.getRawValue(C[0]),M=c.getRawValue(C[1]);a&&(T=Od(n,u,E,M,S.t))}i.lastFrameIndex=C[0]}else{var k=1===e||i.lastFrameIndex>0?C[0]:0;I=HE(p,k);a&&(T=c.getRawValue(k)),s.attr({x:I[0]+x,y:I[1]+b})}if(a){var P=Zf(s);"function"==typeof P.setLabelText&&P.setLabelText(T)}}},t.prototype._doUpdateAnimation=function(e,t,n,i,a,r,o){var s=this._polyline,l=this._polygon,p=e.hostModel,c=function(e,t,n,i,a,r,o){for(var s=function(e,t){var n=[];return t.diff(e).add((function(e){n.push({cmd:"+",idx:e})})).update((function(e,t){n.push({cmd:"=",idx:t,idx1:e})})).remove((function(e){n.push({cmd:"-",idx:e})})).execute(),n}(e,t),l=[],p=[],c=[],d=[],u=[],m=[],h=[],g=_E(a,t,o),f=e.getLayout("points")||[],y=t.getLayout("points")||[],v=0;v3e3||l&&qE(u,h)>3e3)return s.stopAnimation(),s.setShape({points:m}),void(l&&(l.stopAnimation(),l.setShape({points:m,stackedOnPoints:h})));s.shape.__points=c.current,s.shape.points=d;var g={shape:{points:m}};c.current!==d&&(g.shape.__points=c.next),s.stopAnimation(),tf(s,g,p),l&&(l.setShape({points:d,stackedOnPoints:u}),l.stopAnimation(),tf(l,{shape:{stackedOnPoints:h}},p),s.shape.points!==l.shape.points&&(l.shape.points=s.shape.points));for(var f=[],y=c.status,v=0;vt&&(t=e[n]);return isFinite(t)?t:NaN},min:function(e){for(var t=1/0,n=0;nt&&(t=r,n=a)}return isFinite(n)?n:NaN},nearest:function(e){return e[0]}},ZE=function(e){return Math.round(e.length/2)};function QE(e){return{seriesType:e,reset:function(e,t,n){var i=e.getData(),a=e.get("sampling"),r=e.coordinateSystem,o=i.count();if(o>10&&"cartesian2d"===r.type&&a){var s=r.getBaseAxis(),l=r.getOtherAxis(s),p=s.getExtent(),c=n.getDevicePixelRatio(),d=Math.abs(p[1]-p[0])*(c||1),u=Math.round(o/d);if(isFinite(u)&&u>1){"lttb"===a&&e.setData(i.lttbDownSample(i.mapDimension(l.dim),1/u));var m=void 0;Hr(a)?m=XE[a]:jr(a)&&(m=a),m&&e.setData(i.downSample(i.mapDimension(l.dim),1/u,m,ZE))}}}}}function JE(e){e.registerChartView(KE),e.registerSeriesModel(gE),e.registerLayout(YE("line",!0)),e.registerVisual({seriesType:"line",reset:function(e){var t=e.getData(),n=e.getModel("lineStyle").getLineStyle();n&&!n.stroke&&(n.stroke=t.getVisual("style").fill),t.setVisual("legendLineStyle",n)}}),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,QE("line"))}var eM=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.getInitialData=function(e,t){return K_(null,this,{useEncodeDefaulter:!0})},t.prototype.getMarkerPosition=function(e,t,n){var i=this.coordinateSystem;if(i&&i.clampData){var a=i.clampData(e),r=i.dataToPoint(a);if(n)Rr(i.getAxes(),(function(e,n){if("category"===e.type&&null!=t){var i=e.getTicksCoords(),o=e.getTickModel().get("alignWithLabel"),s=a[n],l="x1"===t[n]||"y1"===t[n];if(l&&!o&&(s+=1),i.length<2)return;if(2===i.length)return void(r[n]=e.toGlobalCoord(e.getExtent()[l?1:0]));for(var p=void 0,c=void 0,d=1,u=0;us){c=(m+p)/2;break}1===u&&(d=h-i[0].tickValue)}null==c&&(p?p&&(c=i[i.length-1].coord):c=i[0].coord),r[n]=e.toGlobalCoord(c)}}));else{var o=this.getData(),s=o.getLayout("offset"),l=o.getLayout("size"),p=i.getBaseAxis().isHorizontal()?0:1;r[p]+=s+l/2}return r}return[NaN,NaN]},t.type="series.__base_bar__",t.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod"},t}(ow);ow.registerClass(eM);var tM=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.getInitialData=function(){return K_(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},t.prototype.getProgressive=function(){return!!this.get("large")&&this.get("progressive")},t.prototype.getProgressiveThreshold=function(){var e=this.get("progressiveThreshold"),t=this.get("largeThreshold");return t>e&&(e=t),e},t.prototype.brushSelector=function(e,t,n){return n.rect(t.getItemLayout(e))},t.type="series.bar",t.dependencies=["grid","polar"],t.defaultOption=gy(eM.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:"#212121"}},realtimeSort:!1}),t}(eM),nM=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},iM=function(e){function t(t){var n=e.call(this,t)||this;return n.type="sausage",n}return ze(t,e),t.prototype.getDefaultShape=function(){return new nM},t.prototype.buildPath=function(e,t){var n=t.cx,i=t.cy,a=Math.max(t.r0||0,0),r=Math.max(t.r,0),o=.5*(r-a),s=a+o,l=t.startAngle,p=t.endAngle,c=t.clockwise,d=2*Math.PI,u=c?p-lr)return!0;r=p}return!1},t.prototype._isOrderDifferentInView=function(e,t){for(var n=t.scale,i=n.getExtent(),a=Math.max(0,i[0]),r=Math.min(i[1],n.getOrdinalMeta().categories.length-1);a<=r;++a)if(e.ordinalNumbers[a]!==n.getRawOrdinalNumber(a))return!0},t.prototype._updateSortWithinSameData=function(e,t,n,i){if(this._isOrderChangedWithinSameData(e,t,n)){var a=this._dataSort(e,n,t);this._isOrderDifferentInView(a,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:a}))}},t.prototype._dispatchInitSort=function(e,t,n){var i=t.baseAxis,a=this._dataSort(e,i,(function(n){return e.get(e.mapDimension(t.otherAxis.dim),n)}));n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:a})},t.prototype.remove=function(e,t){this._clear(this._model),this._removeOnRenderedListener(t)},t.prototype.dispose=function(e,t){this._removeOnRenderedListener(t)},t.prototype._removeOnRenderedListener=function(e){this._onRendered&&(e.getZr().off("rendered",this._onRendered),this._onRendered=null)},t.prototype._clear=function(e){var t=this.group,n=this._data;e&&e.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl((function(t){sf(t,e,Um(t).dataIndex)}))):t.removeAll(),this._data=null,this._isFirstFrame=!0},t.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},t.type="bar",t}(vw),cM={cartesian2d:function(e,t){var n=t.width<0?-1:1,i=t.height<0?-1:1;n<0&&(t.x+=t.width,t.width=-t.width),i<0&&(t.y+=t.height,t.height=-t.height);var a=e.x+e.width,r=e.y+e.height,o=sM(t.x,e.x),s=lM(t.x+t.width,a),l=sM(t.y,e.y),p=lM(t.y+t.height,r),c=sa?s:o,t.y=d&&l>r?p:l,t.width=c?0:s-o,t.height=d?0:p-l,n<0&&(t.x+=t.width,t.width=-t.width),i<0&&(t.y+=t.height,t.height=-t.height),c||d},polar:function(e,t){var n=t.r0<=t.r?1:-1;if(n<0){var i=t.r;t.r=t.r0,t.r0=i}var a=lM(t.r,e.r),r=sM(t.r0,e.r0);t.r=a,t.r0=r;var o=a-r<0;if(n<0){i=t.r;t.r=t.r0,t.r0=i}return o}},dM={cartesian2d:function(e,t,n,i,a,r,o,s,l){var p=new Em({shape:kr({},i),z2:1});(p.__dataIndex=n,p.name="item",r)&&(p.shape[a?"height":"width"]=0);return p},polar:function(e,t,n,i,a,r,o,s,l){var p=!a&&l?iM:_g,c=new p({shape:i,z2:1});c.name="item";var d,u,m=vM(a);if(c.calculateTextPosition=(d=m,u=({isRoundCap:p===iM}||{}).isRoundCap,function(e,t,n){var i=t.position;if(!i||i instanceof Array)return yc(e,t,n);var a=d(i),r=null!=t.distance?t.distance:5,o=this.shape,s=o.cx,l=o.cy,p=o.r,c=o.r0,m=(p+c)/2,h=o.startAngle,g=o.endAngle,f=(h+g)/2,y=u?Math.abs(p-c)/2:0,v=Math.cos,x=Math.sin,b=s+p*v(h),w=l+p*x(h),S="left",C="top";switch(a){case"startArc":b=s+(c-r)*v(f),w=l+(c-r)*x(f),S="center",C="top";break;case"insideStartArc":b=s+(c+r)*v(f),w=l+(c+r)*x(f),S="center",C="bottom";break;case"startAngle":b=s+m*v(h)+aM(h,r+y,!1),w=l+m*x(h)+rM(h,r+y,!1),S="right",C="middle";break;case"insideStartAngle":b=s+m*v(h)+aM(h,-r+y,!1),w=l+m*x(h)+rM(h,-r+y,!1),S="left",C="middle";break;case"middle":b=s+m*v(f),w=l+m*x(f),S="center",C="middle";break;case"endArc":b=s+(p+r)*v(f),w=l+(p+r)*x(f),S="center",C="bottom";break;case"insideEndArc":b=s+(p-r)*v(f),w=l+(p-r)*x(f),S="center",C="top";break;case"endAngle":b=s+m*v(g)+aM(g,r+y,!0),w=l+m*x(g)+rM(g,r+y,!0),S="left",C="middle";break;case"insideEndAngle":b=s+m*v(g)+aM(g,-r+y,!0),w=l+m*x(g)+rM(g,-r+y,!0),S="right",C="middle";break;default:return yc(e,t,n)}return(e=e||{}).x=b,e.y=w,e.align=S,e.verticalAlign=C,e}),r){var h=a?"r":"endAngle",g={};c.shape[h]=a?i.r0:i.startAngle,g[h]=i[h],(s?tf:nf)(c,{shape:g},r)}return c}};function uM(e,t,n,i,a,r,o,s){var l,p;r?(p={x:i.x,width:i.width},l={y:i.y,height:i.height}):(p={y:i.y,height:i.height},l={x:i.x,width:i.width}),s||(o?tf:nf)(n,{shape:l},t,a,null),(o?tf:nf)(n,{shape:p},t?e.baseAxis.model:null,a)}function mM(e,t){for(var n=0;n0?1:-1,o=i.height>0?1:-1;return{x:i.x+r*a/2,y:i.y+o*a/2,width:i.width-r*a,height:i.height-o*a}},polar:function(e,t,n){var i=e.getItemLayout(t);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}}};function vM(e){return function(e){var t=e?"Arc":"Angle";return function(e){switch(e){case"start":case"insideStart":case"end":case"insideEnd":return e+t;default:return e}}}(e)}function xM(e,t,n,i,a,r,o,s){var l=t.getItemVisual(n,"style");if(s){if(!r.get("roundCap")){var p=e.shape;kr(p,oM(i.getModel("itemStyle"),p,!0)),e.setShape(p)}}else{var c=i.get(["itemStyle","borderRadius"])||0;e.setShape("r",c)}e.useStyle(l);var d=i.getShallow("cursor");d&&e.attr("cursor",d);var u=s?o?a.r>=a.r0?"endArc":"startArc":a.endAngle>=a.startAngle?"endAngle":"startAngle":o?a.height>=0?"bottom":"top":a.width>=0?"right":"left",m=Uf(i);zf(e,m,{labelFetcher:r,labelDataIndex:n,defaultText:fE(r.getData(),n),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:u});var h=e.getTextContent();if(s&&h){var g=i.get(["label","position"]);e.textConfig.inside="middle"===g||null,function(e,t,n,i){if($r(i))e.setTextConfig({rotation:i});else if(Ur(t))e.setTextConfig({rotation:0});else{var a,r=e.shape,o=r.clockwise?r.startAngle:r.endAngle,s=r.clockwise?r.endAngle:r.startAngle,l=(o+s)/2,p=n(t);switch(p){case"startArc":case"insideStartArc":case"middle":case"insideEndArc":case"endArc":a=l;break;case"startAngle":case"insideStartAngle":a=o;break;case"endAngle":case"insideEndAngle":a=s;break;default:return void e.setTextConfig({rotation:0})}var c=1.5*Math.PI-a;"middle"===p&&c>Math.PI/2&&c<1.5*Math.PI&&(c-=Math.PI),e.setTextConfig({rotation:c})}}(e,"outside"===g?u:g,vM(o),i.get(["label","rotate"]))}Qf(h,m,r.getRawValue(n),(function(e){return yE(t,e)}));var f=i.getModel(["emphasis"]);Oh(e,f.get("focus"),f.get("blurScope"),f.get("disabled")),Bh(e,i),function(e){return null!=e.startAngle&&null!=e.endAngle&&e.startAngle===e.endAngle}(a)&&(e.style.fill="none",e.style.stroke="none",Rr(e.states,(function(e){e.style&&(e.style.fill=e.style.stroke="none")})))}var bM=function(){},wM=function(e){function t(t){var n=e.call(this,t)||this;return n.type="largeBar",n}return ze(t,e),t.prototype.getDefaultShape=function(){return new bM},t.prototype.buildPath=function(e,t){for(var n=t.points,i=this.baseDimIdx,a=1-this.baseDimIdx,r=[],o=[],s=this.barWidth,l=0;l=s[0]&&t<=s[0]+l[0]&&n>=s[1]&&n<=s[1]+l[1])return o[c]}return-1}(this,e.offsetX,e.offsetY);Um(this).dataIndex=t>=0?t:null}),30,!1);function _M(e,t,n){if(NE(n,"cartesian2d")){var i=t,a=n.getArea();return{x:e?i.x:a.x,y:e?a.y:i.y,width:e?i.width:a.width,height:e?a.height:i.height}}var r=t;return{cx:(a=n.getArea()).cx,cy:a.cy,r0:e?a.r0:r.r0,r:e?a.r:r.r,startAngle:e?r.startAngle:0,endAngle:e?r.endAngle:2*Math.PI}}function TM(e){e.registerChartView(pM),e.registerSeriesModel(tM),e.registerLayout(e.PRIORITY.VISUAL.LAYOUT,zr(xT,"bar")),e.registerLayout(e.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,bT("bar")),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,QE("bar")),e.registerAction({type:"changeAxisOrder",event:"changeAxisOrder",update:"update"},(function(e,t){var n=e.componentType||"series";t.eachComponent({mainType:n,query:e},(function(t){e.sortInfo&&t.axis.setCategorySortInfo(e.sortInfo)}))}))}var IM=2*Math.PI,EM=Math.PI/180;function MM(e,t){return gv(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()})}function kM(e,t){var n=MM(e,t),i=e.get("center"),a=e.get("radius");Ur(a)||(a=[0,a]);var r,o,s=Vc(n.width,t.getWidth()),l=Vc(n.height,t.getHeight()),p=Math.min(s,l),c=Vc(a[0],p/2),d=Vc(a[1],p/2),u=e.coordinateSystem;if(u){var m=u.dataToPoint(i);r=m[0]||0,o=m[1]||0}else Ur(i)||(i=[i,i]),r=Vc(i[0],s)+n.x,o=Vc(i[1],l)+n.y;return{cx:r,cy:o,r0:c,r:d}}function PM(e,t,n){t.eachSeriesByType(e,(function(e){var t=e.getData(),i=t.mapDimension("value"),a=MM(e,n),r=kM(e,n),o=r.cx,s=r.cy,l=r.r,p=r.r0,c=-e.get("startAngle")*EM,d=e.get("endAngle"),u=e.get("padAngle")*EM;d="auto"===d?c-IM:-d*EM;var m=e.get("minAngle")*EM+u,h=0;t.each(i,(function(e){!isNaN(e)&&h++}));var g=t.getSum(i),f=Math.PI/(g||h)*2,y=e.get("clockwise"),v=e.get("roseType"),x=e.get("stillShowZeroSum"),b=t.getDataExtent(i);b[0]=0;var w=y?1:-1,S=[c,d],C=w*u/2;Ku(S,!y),c=S[0],d=S[1];var _=Math.abs(d-c),T=_,I=0,E=c;if(t.setLayout({viewRect:a,r:l}),t.each(i,(function(e,n){var i;if(isNaN(e))t.setItemLayout(n,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:y,cx:o,cy:s,r0:p,r:v?NaN:l});else{(i="area"!==v?0===g&&x?f:e*f:_/h)i?c=r=E+w*i/2:(r=E+C,c=a-C),t.setItemLayout(n,{angle:i,startAngle:r,endAngle:c,clockwise:y,cx:o,cy:s,r0:p,r:v?Lc(e,b,[p,l]):l}),E=a}})),Tn?o:r,c=Math.abs(l.label.y-n);if(c>=p.maxY){var d=l.label.x-t-l.len2*a,u=i+l.len,h=Math.abs(d)e.unconstrainedWidth?null:m:null;i.setStyle("width",h)}var g=i.getBoundingRect();r.width=g.width;var f=(i.style.margin||0)+2.1;r.height=g.height+f,r.y-=(r.height-d)/2}}}function RM(e){return"center"===e.position}function BM(e){var t,n,i=e.getData(),a=[],r=!1,o=(e.get("minShowLabelAngle")||0)*OM,s=i.getLayout("viewRect"),l=i.getLayout("r"),p=s.width,c=s.x,d=s.y,u=s.height;function m(e){e.ignore=!0}i.each((function(e){var s=i.getItemGraphicEl(e),d=s.shape,u=s.getTextContent(),h=s.getTextGuideLine(),g=i.getItemModel(e),f=g.getModel("label"),y=f.get("position")||g.get(["emphasis","label","position"]),v=f.get("distanceToLabelLine"),x=f.get("alignTo"),b=Vc(f.get("edgeDistance"),p),w=f.get("bleedMargin"),S=g.getModel("labelLine"),C=S.get("length");C=Vc(C,p);var _=S.get("length2");if(_=Vc(_,p),Math.abs(d.endAngle-d.startAngle)0?"right":"left":P>0?"left":"right"}var L=Math.PI,V=0,q=f.get("rotate");if($r(q))V=q*(L/180);else if("center"===y)V=0;else if("radial"===q||!0===q){V=P<0?-k+L:-k}else if("tangential"===q&&"outside"!==y&&"outer"!==y){var G=Math.atan2(P,D);G<0&&(G=2*L+G),D>0&&(G=L+G),V=G-L}if(r=!!V,u.x=T,u.y=I,u.rotation=V,u.setStyle({verticalAlign:"middle"}),O){u.setStyle({align:M});var z=u.states.select;z&&(z.x+=u.x,z.y+=u.y)}else{var U=u.getBoundingRect().clone();U.applyTransform(u.getComputedTransform());var j=(u.style.margin||0)+2.1;U.y-=j/2,U.height+=j,a.push({label:u,labelLine:h,position:y,len:C,len2:_,minTurnAngle:S.get("minTurnAngle"),maxSurfaceAngle:S.get("maxSurfaceAngle"),surfaceNormal:new ws(P,D),linePoints:E,textAlign:M,labelDistance:v,labelAlignTo:x,edgeDistance:b,bleedMargin:w,rect:U,unconstrainedWidth:U.width,labelStyleWidth:u.style.width})}s.setTextConfig({inside:O})}})),!r&&e.get("avoidLabelOverlap")&&function(e,t,n,i,a,r,o,s){for(var l=[],p=[],c=Number.MAX_VALUE,d=-Number.MAX_VALUE,u=0;u0){for(var l=r.getItemLayout(0),p=1;isNaN(l&&l.startAngle)&&p=n.r0}},t.type="pie",t}(vw);function VM(e,t,n){t=Ur(t)&&{coordDimensions:t}||kr({encodeDefine:e.getEncode()},t);var i=e.getSource(),a=q_(i,t).dimensions,r=new V_(a,e);return r.initData(i,n),r}var qM=function(){function e(e,t){this._getDataWithEncodedVisual=e,this._getRawData=t}return e.prototype.getAllNames=function(){var e=this._getRawData();return e.mapArray(e.getName)},e.prototype.containName=function(e){return this._getRawData().indexOfName(e)>=0},e.prototype.indexOfName=function(e){return this._getDataWithEncodedVisual().indexOfName(e)},e.prototype.getItemVisual=function(e,t){return this._getDataWithEncodedVisual().getItemVisual(e,t)},e}(),GM=Cd(),zM=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.init=function(t){e.prototype.init.apply(this,arguments),this.legendVisualProvider=new qM(Gr(this.getData,this),Gr(this.getRawData,this)),this._defaultLabelLine(t)},t.prototype.mergeOption=function(){e.prototype.mergeOption.apply(this,arguments)},t.prototype.getInitialData=function(){return VM(this,{coordDimensions:["value"],encodeDefaulter:zr(qv,this)})},t.prototype.getDataParams=function(t){var n=this.getData(),i=GM(n),a=i.seats;if(!a){var r=[];n.each(n.mapDimension("value"),(function(e){r.push(e)})),a=i.seats=Hc(r,n.hostModel.get("percentPrecision"))}var o=e.prototype.getDataParams.call(this,t);return o.percent=a[t]||0,o.$vars.push("percent"),o},t.prototype._defaultLabelLine=function(e){ud(e,"labelLine",["show"]);var t=e.labelLine,n=e.emphasis.labelLine;t.show=t.show&&e.label.show,n.show=n.show&&e.emphasis.label.show},t.type="series.pie",t.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,endAngle:"auto",padAngle:0,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",bleedMargin:10,distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:15,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},t}(ow);function UM(e){e.registerChartView(LM),e.registerSeriesModel(zM),fS("pie",e.registerAction),e.registerLayout(zr(PM,"pie")),e.registerProcessor(DM("pie")),e.registerProcessor(function(e){return{seriesType:e,reset:function(e,t){var n=e.getData();n.filterSelf((function(e){var t=n.mapDimension("value"),i=n.get(t,e);return!($r(i)&&!isNaN(i)&&i<0)}))}}}("pie"))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n}ze(t,e),t.prototype.getInitialData=function(e,t){return K_(null,this,{useEncodeDefaulter:!0})},t.prototype.getProgressive=function(){var e=this.option.progressive;return null==e?this.option.large?5e3:this.get("progressive"):e},t.prototype.getProgressiveThreshold=function(){var e=this.option.progressiveThreshold;return null==e?this.option.large?1e4:this.get("progressiveThreshold"):e},t.prototype.brushSelector=function(e,t,n){return n.point(t.getItemLayout(e))},t.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},t.type="series.scatter",t.dependencies=["grid","polar","geo","singleAxis","calendar"],t.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:"#212121"}},universalTransition:{divideShape:"clone"}}}(ow);var jM=function(){},HM=function(e){function t(t){var n=e.call(this,t)||this;return n._off=0,n.hoverDataIdx=-1,n}return ze(t,e),t.prototype.getDefaultShape=function(){return new jM},t.prototype.reset=function(){this.notClear=!1,this._off=0},t.prototype.buildPath=function(e,t){var n,i=t.points,a=t.size,r=this.symbolProxy,o=r.shape,s=e.getContext?e.getContext():e,l=s&&a[0]<4,p=this.softClipShape;if(l)this._ctx=s;else{for(this._ctx=null,n=this._off;n=0;s--){var l=2*s,p=i[l]-r/2,c=i[l+1]-o/2;if(e>=p&&t>=c&&e<=p+r&&t<=c+o)return s}return-1},t.prototype.contain=function(e,t){var n=this.transformCoordToLocal(e,t),i=this.getBoundingRect();return e=n[0],t=n[1],i.contain(e,t)?(this.hoverDataIdx=this.findDataIndex(e,t))>=0:(this.hoverDataIdx=-1,!1)},t.prototype.getBoundingRect=function(){var e=this._rect;if(!e){for(var t=this.shape,n=t.points,i=t.size,a=i[0],r=i[1],o=1/0,s=1/0,l=-1/0,p=-1/0,c=0;c=0&&(l.dataIndex=n+(e.startIndex||0))}))},e.prototype.remove=function(){this._clear()},e.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},e}(),$M=(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.render=function(e,t,n){var i=e.getData();this._updateSymbolDraw(i,e).updateData(i,{clipShape:this._getClipShape(e)}),this._finished=!0},t.prototype.incrementalPrepareRender=function(e,t,n){var i=e.getData();this._updateSymbolDraw(i,e).incrementalPrepareUpdate(i),this._finished=!1},t.prototype.incrementalRender=function(e,t,n){this._symbolDraw.incrementalUpdate(e,t.getData(),{clipShape:this._getClipShape(t)}),this._finished=e.end===t.getData().count()},t.prototype.updateTransform=function(e,t,n){var i=e.getData();if(this.group.dirty(),!this._finished||i.count()>1e4)return{update:!0};var a=YE("").reset(e,t,n);a.progress&&a.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(i)},t.prototype.eachRendered=function(e){this._symbolDraw&&this._symbolDraw.eachRendered(e)},t.prototype._getClipShape=function(e){if(e.get("clip",!0)){var t=e.coordinateSystem;return t&&t.getArea&&t.getArea(.1)}},t.prototype._updateSymbolDraw=function(e,t){var n=this._symbolDraw,i=t.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new WM:new CE,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},t.prototype.remove=function(e,t){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},t.prototype.dispose=function(){},t.type="scatter"}(vw),function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.type="grid",t.dependencies=["xAxis","yAxis"],t.layoutMode="box",t.defaultOption={show:!1,z:0,left:"10%",top:60,right:"10%",bottom:70,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"},t}(Sv)),KM=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",Ed).models[0]},t.type="cartesian2dAxis",t}(Sv);Ar(KM,tI);var YM={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#6E7079",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#E0E6F1"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.2)","rgba(210,219,238,0.2)"]}}},XM=Er({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},YM),ZM=Er({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:"#F4F7FD",width:1}}},YM),QM={category:XM,value:ZM,time:Er({splitNumber:6,axisLabel:{showMinLabel:!1,showMaxLabel:!1,rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},ZM),log:Pr({logBase:10},ZM)},JM={value:1,category:1,time:1,log:1};function ek(e,t,n,i){Rr(JM,(function(a,r){var o=Er(Er({},QM[r],!0),i,!0),s=function(e){function n(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t+"Axis."+r,n}return ze(n,e),n.prototype.mergeDefaultAndTheme=function(e,t){var n=yv(this),i=n?xv(e):{};Er(e,t.getTheme().get(r+"Axis")),Er(e,this.getDefaultOption()),e.type=tk(e),n&&vv(e,i,n)},n.prototype.optionUpdated=function(){"category"===this.option.type&&(this.__ordinalMeta=Z_.createByAxisModel(this))},n.prototype.getCategories=function(e){var t=this.option;if("category"===t.type)return e?t.data:this.__ordinalMeta.categories},n.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},n.prototype.getTicksGenerator=function(){var e=this.option;if("value"===e.type)return e.ticksGenerator},n.type=t+"Axis."+r,n.defaultOption=o,n}(n);e.registerComponentModel(s)})),e.registerSubTypeDefaulter(t+"Axis",tk)}function tk(e){return e.type||(e.data?"category":"value")}var nk=function(){function e(e){this.type="cartesian",this._dimList=[],this._axes={},this.name=e||""}return e.prototype.getAxis=function(e){return this._axes[e]},e.prototype.getAxes=function(){return Br(this._dimList,(function(e){return this._axes[e]}),this)},e.prototype.getAxesByScale=function(e){return e=e.toLowerCase(),Lr(this.getAxes(),(function(t){return t.scale.type===e}))},e.prototype.addAxis=function(e){var t=e.dim;this._axes[t]=e,this._dimList.push(t)},e}(),ik=["x","y"];function ak(e){return"interval"===e.type||"time"===e.type}var rk=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type="cartesian2d",t.dimensions=ik,t}return ze(t,e),t.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var e=this.getAxis("x").scale,t=this.getAxis("y").scale;if(ak(e)&&ak(t)){var n=e.getExtent(),i=t.getExtent(),a=this.dataToPoint([n[0],i[0]]),r=this.dataToPoint([n[1],i[1]]),o=n[1]-n[0],s=i[1]-i[0];if(o&&s){var l=(r[0]-a[0])/o,p=(r[1]-a[1])/s,c=a[0]-n[0]*l,d=a[1]-i[0]*p,u=this._transform=[l,0,0,p,c,d];this._invTransform=xs([],u)}}},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},t.prototype.containPoint=function(e){var t=this.getAxis("x"),n=this.getAxis("y");return t.contain(t.toLocalCoord(e[0]))&&n.contain(n.toLocalCoord(e[1]))},t.prototype.containData=function(e){return this.getAxis("x").containData(e[0])&&this.getAxis("y").containData(e[1])},t.prototype.containZone=function(e,t){var n=this.dataToPoint(e),i=this.dataToPoint(t),a=this.getArea(),r=new Ps(n[0],n[1],i[0]-n[0],i[1]-n[1]);return a.intersect(r)},t.prototype.dataToPoint=function(e,t,n){n=n||[];var i=e[0],a=e[1];if(this._transform&&null!=i&&isFinite(i)&&null!=a&&isFinite(a))return Lo(n,e,this._transform);var r=this.getAxis("x"),o=this.getAxis("y");return n[0]=r.toGlobalCoord(r.dataToCoord(i,t)),n[1]=o.toGlobalCoord(o.dataToCoord(a,t)),n},t.prototype.clampData=function(e,t){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,a=n.getExtent(),r=i.getExtent(),o=n.parse(e[0]),s=i.parse(e[1]);return(t=t||[])[0]=Math.min(Math.max(Math.min(a[0],a[1]),o),Math.max(a[0],a[1])),t[1]=Math.min(Math.max(Math.min(r[0],r[1]),s),Math.max(r[0],r[1])),t},t.prototype.pointToData=function(e,t){var n=[];if(this._invTransform)return Lo(n,e,this._invTransform);var i=this.getAxis("x"),a=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(e[0]),t),n[1]=a.coordToData(a.toLocalCoord(e[1]),t),n},t.prototype.getOtherAxis=function(e){return this.getAxis("x"===e.dim?"y":"x")},t.prototype.getArea=function(e){e=e||0;var t=this.getAxis("x").getGlobalExtent(),n=this.getAxis("y").getGlobalExtent(),i=Math.min(t[0],t[1])-e,a=Math.min(n[0],n[1])-e,r=Math.max(t[0],t[1])-i+e,o=Math.max(n[0],n[1])-a+e;return new Ps(i,a,r,o)},t}(nk),ok=function(e){function t(t,n,i,a,r){var o=e.call(this,t,n,i)||this;return o.index=0,o.type=a||"value",o.position=r||"bottom",o}return ze(t,e),t.prototype.isHorizontal=function(){var e=this.position;return"top"===e||"bottom"===e},t.prototype.getGlobalExtent=function(e){var t=this.getExtent();return t[0]=this.toGlobalCoord(t[0]),t[1]=this.toGlobalCoord(t[1]),e&&t[0]>t[1]&&t.reverse(),t},t.prototype.pointToData=function(e,t){return this.coordToData(this.toLocalCoord(e["x"===this.dim?0:1]),t)},t.prototype.setCategorySortInfo=function(e){if("category"!==this.type)return!1;this.model.option.categorySortInfo=e,this.scale.setSortInfo(e)},t}(MI);function sk(e,t,n){n=n||{};var i=e.coordinateSystem,a=t.axis,r={},o=a.getAxesOnZeroOf()[0],s=a.position,l=o?"onZero":s,p=a.dim,c=i.getRect(),d=[c.x,c.x+c.width,c.y,c.y+c.height],u={left:0,right:1,top:0,bottom:1,onZero:2},m=t.get("offset")||0,h="x"===p?[d[2]-m,d[3]+m]:[d[0]-m,d[1]+m];if(o){var g=o.toGlobalCoord(o.dataToCoord(0));h[u.onZero]=Math.max(Math.min(g,h[1]),h[0])}r.position=["y"===p?h[u[l]]:d[0],"x"===p?h[u[l]]:d[3]],r.rotation=Math.PI/2*("x"===p?0:1);r.labelDirection=r.tickDirection=r.nameDirection={top:-1,bottom:1,left:-1,right:1}[s],r.labelOffset=o?h[u[s]]-h[u.onZero]:0,t.get(["axisTick","inside"])&&(r.tickDirection=-r.tickDirection),no(n.labelInside,t.get(["axisLabel","inside"]))&&(r.labelDirection=-r.labelDirection);var f=t.get(["axisLabel","rotate"]);return r.labelRotate="top"===l?-f:f,r.z2=1,r}function lk(e){return"cartesian2d"===e.get("coordinateSystem")}function pk(e){var t={xAxisModel:null,yAxisModel:null};return Rr(t,(function(n,i){var a=i.replace(/Model$/,""),r=e.getReferringComponents(a,Ed).models[0];t[i]=r})),t}var ck=Math.log;function dk(e,t,n){var i=pT.prototype,a=i.getTicks.call(n),r=i.getTicks.call(n,!0),o=a.length-1,s=i.getInterval.call(n),l=WT(e,t),p=l.extent,c=l.fixMin,d=l.fixMax;if("log"===e.type){var u=ck(e.base);p=[ck(p[0])/u,ck(p[1])/u]}e.setExtent(p[0],p[1]),e.calcNiceExtent({splitNumber:o,fixMin:c,fixMax:d});var m=i.getExtent.call(e);c&&(p[0]=m[0]),d&&(p[1]=m[1]);var h=i.getInterval.call(e),g=p[0],f=p[1];if(c&&d)h=(f-g)/o;else if(c)for(f=p[0]+h*o;fp[0]&&isFinite(g)&&isFinite(p[0]);)h=tT(h),g=p[1]-h*o;else{e.getTicks().length-1>o&&(h=tT(h));var y=h*o;(g=qc((f=Math.ceil(p[1]/h)*h)-y))<0&&p[0]>=0?(g=0,f=qc(y)):f>0&&p[1]<=0&&(f=0,g=-qc(y))}var v=(a[0].value-r[0].value)/s,x=(a[o].value-r[o].value)/s;i.setExtent.call(e,g+h*v,f+h*x),i.setInterval.call(e,h),(v||x)&&i.setNiceExtent.call(e,g+h,f-h)}var uk=function(){function e(e,t,n){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=ik,this._initCartesian(e,t,n),this.model=e}return e.prototype.getRect=function(){return this._rect},e.prototype.update=function(e,t){var n=this._axesMap;function i(e){var t,n=qr(e),i=n.length;if(i){for(var a=[],r=i-1;r>=0;r--){var o=(l=e[+n[r]]).model,s=l.scale;J_(s)&&o.get("alignTicks")&&null==o.get("interval")&&null==o.getTicksGenerator()?a.push(l):($T(s,o),J_(s)&&!s.isBlank()&&(t=l))}if(a.length){for(;!t&&a.length;){var l;$T((l=a.pop()).scale,l.model),l.scale.isBlank()||(t=l)}a.length&&t&&Rr(a,(function(e){dk(e.scale,e.model,t.scale)}))}}}this._updateScale(e,this.model),i(n.x),i(n.y);var a={};Rr(n.x,(function(e){hk(n,"y",e,a)})),Rr(n.y,(function(e){hk(n,"x",e,a)})),this.resize(this.model,t)},e.prototype.resize=function(e,t,n){var i=e.getBoxLayoutParams(),a=!n&&e.get("containLabel"),r=gv(i,{width:t.getWidth(),height:t.getHeight()});this._rect=r;var o=this._axesList;function s(){Rr(o,(function(e){var t=e.isHorizontal(),n=t?[0,r.width]:[0,r.height],i=e.inverse?1:0;e.setExtent(n[i],n[1-i]),function(e,t){var n=e.getExtent(),i=n[0]+n[1];e.toGlobalCoord="x"===e.dim?function(e){return e+t}:function(e){return i-e+t},e.toLocalCoord="x"===e.dim?function(e){return e-t}:function(e){return i-e+t}}(e,t?r.x:r.y)}))}s(),a&&(Rr(o,(function(e){if(!e.model.get(["axisLabel","inside"])){var t=function(e){var t=e.model,n=e.scale;if(t.get(["axisLabel","show"])&&!n.isBlank()){var i,a,r=n.getExtent();a=n instanceof sT?n.count():(i=n.getTicks()).length;var o,s=e.getLabelModel(),l=YT(e),p=1;a>40&&(p=Math.ceil(a/40));for(var c=0;c0&&i>0||n<0&&i<0)}(e)}var fk=Math.PI,yk=function(){function e(e,t){this.group=new Mc,this.opt=t,this.axisModel=e,Pr(t,{labelOffset:0,nameDirection:1,tickDirection:1,labelDirection:1,silent:!0,handleAutoShown:function(){return!0}});var n=new Mc({x:t.position[0],y:t.position[1],rotation:t.rotation});n.updateTransform(),this._transformGroup=n}return e.prototype.hasBuilder=function(e){return!!vk[e]},e.prototype.add=function(e){vk[e](this.opt,this.axisModel,this.group,this._transformGroup)},e.prototype.getGroup=function(){return this.group},e.innerTextLayout=function(e,t,n){var i,a,r=$c(t-e);return Kc(r)?(a=n>0?"top":"bottom",i="center"):Kc(r-fk)?(a=n>0?"bottom":"top",i="center"):(a="middle",i=r>0&&r0?"right":"left":n>0?"left":"right"),{rotation:r,textAlign:i,textVerticalAlign:a}},e.makeAxisEventDataBase=function(e){var t={componentType:e.mainType,componentIndex:e.componentIndex};return t[e.mainType+"Index"]=e.componentIndex,t},e.isLabelSilent=function(e){var t=e.get("tooltip");return e.get("silent")||!(e.get("triggerEvent")||t&&t.show)},e}(),vk={axisLine:function(e,t,n,i){var a=t.get(["axisLine","show"]);if("auto"===a&&e.handleAutoShown&&(a=e.handleAutoShown("axisLine")),a){var r=t.axis.getExtent(),o=i.transform,s=[r[0],0],l=[r[1],0],p=s[0]>l[0];o&&(Lo(s,s,o),Lo(l,l,o));var c=kr({lineCap:"round"},t.getModel(["axisLine","lineStyle"]).getLineStyle()),d=new Fg({shape:{x1:s[0],y1:s[1],x2:l[0],y2:l[1]},style:c,strokeContainThreshold:e.strokeContainThreshold||5,silent:!0,z2:1});Sf(d.shape,d.style.lineWidth),d.anid="line",n.add(d);var u=t.get(["axisLine","symbol"]);if(null!=u){var m=t.get(["axisLine","symbolSize"]);Hr(u)&&(u=[u,u]),(Hr(m)||$r(m))&&(m=[m,m]);var h=OS(t.get(["axisLine","symbolOffset"])||0,m),g=m[0],f=m[1];Rr([{rotate:e.rotation+Math.PI/2,offset:h[0],r:0},{rotate:e.rotation-Math.PI/2,offset:h[1],r:Math.sqrt((s[0]-l[0])*(s[0]-l[0])+(s[1]-l[1])*(s[1]-l[1]))}],(function(t,i){if("none"!==u[i]&&null!=u[i]){var a=PS(u[i],-g/2,-f/2,g,f,c.stroke,!0),r=t.r+t.offset,o=p?l:s;a.attr({rotation:t.rotate,x:o[0]+r*Math.cos(e.rotation),y:o[1]-r*Math.sin(e.rotation),silent:!0,z2:11}),n.add(a)}}))}}},axisTickLabel:function(e,t,n,i){var a=function(e,t,n,i){var a=n.axis,r=n.getModel("axisTick"),o=r.get("show");"auto"===o&&i.handleAutoShown&&(o=i.handleAutoShown("axisTick"));if(!o||a.scale.isBlank())return;for(var s=r.getModel("lineStyle"),l=i.tickDirection*r.get("length"),p=Sk(a.getTicksCoords(),t.transform,l,Pr(s.getLineStyle(),{stroke:n.get(["axisLine","lineStyle","color"])}),"ticks"),c=0;cd[1]?-1:1,m=["start"===s?d[0]-u*c:"end"===s?d[1]+u*c:(d[0]+d[1])/2,wk(s)?e.labelOffset+l*c:0],h=t.get("nameRotate");null!=h&&(h=h*fk/180),wk(s)?r=yk.innerTextLayout(e.rotation,null!=h?h:e.rotation,l):(r=function(e,t,n,i){var a,r,o=$c(n-e),s=i[0]>i[1],l="start"===t&&!s||"start"!==t&&s;Kc(o-fk/2)?(r=l?"bottom":"top",a="center"):Kc(o-1.5*fk)?(r=l?"top":"bottom",a="center"):(r="middle",a=o<1.5*fk&&o>fk/2?l?"left":"right":l?"right":"left");return{rotation:o,textAlign:a,textVerticalAlign:r}}(e.rotation,s,h||0,d),null!=(o=e.axisNameAvailableWidth)&&(o=Math.abs(o/Math.sin(r.rotation)),!isFinite(o)&&(o=null)));var g=p.getFont(),f=t.get("nameTruncate",!0)||{},y=f.ellipsis,v=no(e.nameTruncateMaxWidth,f.maxWidth,o),x=new Pm({x:m[0],y:m[1],rotation:r.rotation,silent:yk.isLabelSilent(t),style:jf(p,{text:a,font:g,overflow:"truncate",width:v,ellipsis:y,fill:p.getTextColor()||t.get(["axisLine","lineStyle","color"]),align:p.get("align")||r.textAlign,verticalAlign:p.get("verticalAlign")||r.textVerticalAlign}),z2:1});if(Rf({el:x,componentModel:t,itemName:a}),x.__fullText=a,x.anid="name",t.get("triggerEvent")){var b=yk.makeAxisEventDataBase(t);b.targetType="axisName",b.name=a,Um(x).eventData=b}i.add(x),x.updateTransform(),n.add(x),x.decomposeTransform()}}};function xk(e){e&&(e.ignore=!0)}function bk(e,t){var n=e&&e.getBoundingRect().clone(),i=t&&t.getBoundingRect().clone();if(n&&i){var a=ms([]);return ys(a,a,-e.rotation),n.applyTransform(gs([],a,e.getLocalTransform())),i.applyTransform(gs([],a,t.getLocalTransform())),n.intersect(i)}}function wk(e){return"middle"===e||"center"===e}function Sk(e,t,n,i,a){for(var r=[],o=[],s=[],l=0;l=0||e===t}function Tk(e){var t=(e.ecModel.getComponent("axisPointer")||{}).coordSysAxesInfo;return t&&t.axesInfo[Ek(e)]}function Ik(e){return!!e.get(["handle","show"])}function Ek(e){return e.type+"||"+e.id}var Mk={},kk=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.render=function(t,n,i,a){this.axisPointerClass&&function(e){var t=Tk(e);if(t){var n=t.axisPointerModel,i=t.axis.scale,a=n.option,r=n.get("status"),o=n.get("value");null!=o&&(o=i.parse(o));var s=Ik(n);null==r&&(a.status=s?"show":"hide");var l=i.getExtent().slice();l[0]>l[1]&&l.reverse(),(null==o||o>l[1])&&(o=l[1]),o0&&!d.min?d.min=0:null!=d.min&&d.min<0&&!d.max&&(d.max=0);var u=o;null!=d.color&&(u=Pr({color:d.color},o));var m=Er(Ir(d),{boundaryGap:e,splitNumber:t,scale:n,axisLine:i,axisTick:a,axisLabel:r,name:d.text,showName:s,nameLocation:"end",nameGap:p,nameTextStyle:u,triggerEvent:c},!1);if(Hr(l)){var h=m.name;m.name=l.replace("{value}",null!=h?h:"")}else jr(l)&&(m.name=l(m.name,m));var g=new uy(m,null,this.ecModel);return Ar(g,tI.prototype),g.mainType="radar",g.componentIndex=this.componentIndex,g}),this);this._indicatorModels=d},t.prototype.getIndicatorModels=function(){return this._indicatorModels},t.type="radar",t.defaultOption={z:0,center:["50%","50%"],radius:"75%",startAngle:90,axisName:{show:!0},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:Er({lineStyle:{color:"#bbb"}},Kk.axisLine),axisLabel:Yk(Kk.axisLabel,!1),axisTick:Yk(Kk.axisTick,!1),splitLine:Yk(Kk.splitLine,!0),splitArea:Yk(Kk.splitArea,!0),indicator:[]},t}(Sv),Zk=["axisLine","axisTickLabel","axisName"],Qk=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.render=function(e,t,n){this.group.removeAll(),this._buildAxes(e),this._buildSplitLineAndArea(e)},t.prototype._buildAxes=function(e){var t=e.coordinateSystem;Rr(Br(t.getIndicatorAxes(),(function(e){var n=e.model.get("showName")?e.name:"";return new yk(e.model,{axisName:n,position:[t.cx,t.cy],rotation:e.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})})),(function(e){Rr(Zk,e.add,e),this.group.add(e.getGroup())}),this)},t.prototype._buildSplitLineAndArea=function(e){var t=e.coordinateSystem,n=t.getIndicatorAxes();if(n.length){var i=e.get("shape"),a=e.getModel("splitLine"),r=e.getModel("splitArea"),o=a.getModel("lineStyle"),s=r.getModel("areaStyle"),l=a.get("show"),p=r.get("show"),c=o.get("color"),d=s.get("color"),u=Ur(c)?c:[c],m=Ur(d)?d:[d],h=[],g=[];if("circle"===i)for(var f=n[0].getTicksCoords(),y=t.cx,v=t.cy,x=0;x3?1.4:a>1?1.2:1.1;sP(this,"zoom","zoomOnMouseWheel",e,{scale:i>0?s:1/s,originX:r,originY:o,isAvailableBehavior:null})}if(n){var l=Math.abs(i);sP(this,"scrollMove","moveOnMouseWheel",e,{scrollDelta:(i>0?1:-1)*(l>3?.4:l>1?.15:.05),originX:r,originY:o,isAvailableBehavior:null})}}},t.prototype._pinchHandler=function(e){aP(this._zr,"globalPan")||sP(this,"zoom",null,e,{scale:e.pinchScale>1?1.1:1/1.1,originX:e.pinchX,originY:e.pinchY,isAvailableBehavior:null})},t}(Uo);function sP(e,t,n,i,a){e.pointerChecker&&e.pointerChecker(i,a.originX,a.originY)&&(ls(i.event),lP(e,t,n,i,a))}function lP(e,t,n,i,a){a.isAvailableBehavior=Gr(pP,null,n,i),e.trigger(t,a)}function pP(e,t,n){var i=n[e];return!e||i&&(!Hr(i)||t.event[i+"Key"])}function cP(e,t,n){var i=e.target;i.x+=t,i.y+=n,i.dirty()}function dP(e,t,n,i){var a=e.target,r=e.zoomLimit,o=e.zoom=e.zoom||1;if(o*=t,r){var s=r.min||0,l=r.max||1/0;o=Math.max(Math.min(l,o),s)}var p=o/e.zoom;e.zoom=o,a.x-=(n-a.x)*(p-1),a.y-=(i-a.y)*(p-1),a.scaleX*=p,a.scaleY*=p,a.dirty()}var uP,mP={axisPointer:1,tooltip:1,brush:1};function hP(e,t,n){var i=t.getComponentByElement(e.topTarget),a=i&&i.coordinateSystem;return i&&i!==n&&!mP.hasOwnProperty(i.mainType)&&a&&a.model!==n}function gP(e){Hr(e)&&(e=(new DOMParser).parseFromString(e,"text/xml"));var t=e;for(9===t.nodeType&&(t=t.firstChild);"svg"!==t.nodeName.toLowerCase()||1!==t.nodeType;)t=t.nextSibling;return t}var fP={fill:"fill",stroke:"stroke","stroke-width":"lineWidth",opacity:"opacity","fill-opacity":"fillOpacity","stroke-opacity":"strokeOpacity","stroke-dasharray":"lineDash","stroke-dashoffset":"lineDashOffset","stroke-linecap":"lineCap","stroke-linejoin":"lineJoin","stroke-miterlimit":"miterLimit","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","text-anchor":"textAlign",visibility:"visibility",display:"display"},yP=qr(fP),vP={"alignment-baseline":"textBaseline","stop-color":"stopColor"},xP=qr(vP),bP=function(){function e(){this._defs={},this._root=null}return e.prototype.parse=function(e,t){t=t||{};var n=gP(e);this._defsUsePending=[];var i=new Mc;this._root=i;var a=[],r=n.getAttribute("viewBox")||"",o=parseFloat(n.getAttribute("width")||t.width),s=parseFloat(n.getAttribute("height")||t.height);isNaN(o)&&(o=null),isNaN(s)&&(s=null),IP(n,i,null,!0,!1);for(var l,p,c=n.firstChild;c;)this._parseNode(c,i,a,null,!1,!1),c=c.nextSibling;if(function(e,t){for(var n=0;n=4&&(l={x:parseFloat(d[0]||0),y:parseFloat(d[1]||0),width:parseFloat(d[2]),height:parseFloat(d[3])})}if(l&&null!=o&&null!=s&&(p=RP(l,{x:0,y:0,width:o,height:s}),!t.ignoreViewBox)){var u=i;(i=new Mc).add(u),u.scaleX=u.scaleY=p.scale,u.x=p.x,u.y=p.y}return t.ignoreRootClip||null==o||null==s||i.setClipPath(new Em({shape:{x:0,y:0,width:o,height:s}})),{root:i,width:o,height:s,viewBoxRect:l,viewBoxTransform:p,named:a}},e.prototype._parseNode=function(e,t,n,i,a,r){var o,s=e.nodeName.toLowerCase(),l=i;if("defs"===s&&(a=!0),"text"===s&&(r=!0),"defs"===s||"switch"===s)o=t;else{if(!a){var p=uP[s];if(p&&bo(uP,s)){o=p.call(this,e,t);var c=e.getAttribute("name");if(c){var d={name:c,namedFrom:null,svgNodeTagLower:s,el:o};n.push(d),"g"===s&&(l=d)}else i&&n.push({name:i.name,namedFrom:i,svgNodeTagLower:s,el:o});t.add(o)}}var u=wP[s];if(u&&bo(wP,s)){var m=u.call(this,e),h=e.getAttribute("id");h&&(this._defs[h]=m)}}if(o&&o.isGroup)for(var g=e.firstChild;g;)1===g.nodeType?this._parseNode(g,o,n,l,a,r):3===g.nodeType&&r&&this._parseText(g,o),g=g.nextSibling},e.prototype._parseText=function(e,t){var n=new ym({style:{text:e.textContent},silent:!0,x:this._textX||0,y:this._textY||0});_P(t,n),IP(e,n,this._defsUsePending,!1,!1),function(e,t){var n=t.__selfStyle;if(n){var i=n.textBaseline,a=i;i&&"auto"!==i?"baseline"===i?a="alphabetic":"before-edge"===i||"text-before-edge"===i?a="top":"after-edge"===i||"text-after-edge"===i?a="bottom":"central"!==i&&"mathematical"!==i||(a="middle"):a="alphabetic",e.style.textBaseline=a}var r=t.__inheritedStyle;if(r){var o=r.textAlign,s=o;o&&("middle"===o&&(s="center"),e.style.textAlign=s)}}(n,t);var i=n.style,a=i.fontSize;a&&a<9&&(i.fontSize=9,n.scaleX*=a/9,n.scaleY*=a/9);var r=(i.fontSize||i.fontFamily)&&[i.fontStyle,i.fontWeight,(i.fontSize||12)+"px",i.fontFamily||"sans-serif"].join(" ");i.font=r;var o=n.getBoundingRect();return this._textX+=o.width,t.add(n),n},e.internalField=void(uP={g:function(e,t){var n=new Mc;return _P(t,n),IP(e,n,this._defsUsePending,!1,!1),n},rect:function(e,t){var n=new Em;return _P(t,n),IP(e,n,this._defsUsePending,!1,!1),n.setShape({x:parseFloat(e.getAttribute("x")||"0"),y:parseFloat(e.getAttribute("y")||"0"),width:parseFloat(e.getAttribute("width")||"0"),height:parseFloat(e.getAttribute("height")||"0")}),n.silent=!0,n},circle:function(e,t){var n=new sg;return _P(t,n),IP(e,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(e.getAttribute("cx")||"0"),cy:parseFloat(e.getAttribute("cy")||"0"),r:parseFloat(e.getAttribute("r")||"0")}),n.silent=!0,n},line:function(e,t){var n=new Fg;return _P(t,n),IP(e,n,this._defsUsePending,!1,!1),n.setShape({x1:parseFloat(e.getAttribute("x1")||"0"),y1:parseFloat(e.getAttribute("y1")||"0"),x2:parseFloat(e.getAttribute("x2")||"0"),y2:parseFloat(e.getAttribute("y2")||"0")}),n.silent=!0,n},ellipse:function(e,t){var n=new pg;return _P(t,n),IP(e,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(e.getAttribute("cx")||"0"),cy:parseFloat(e.getAttribute("cy")||"0"),rx:parseFloat(e.getAttribute("rx")||"0"),ry:parseFloat(e.getAttribute("ry")||"0")}),n.silent=!0,n},polygon:function(e,t){var n,i=e.getAttribute("points");i&&(n=TP(i));var a=new kg({shape:{points:n||[]},silent:!0});return _P(t,a),IP(e,a,this._defsUsePending,!1,!1),a},polyline:function(e,t){var n,i=e.getAttribute("points");i&&(n=TP(i));var a=new Dg({shape:{points:n||[]},silent:!0});return _P(t,a),IP(e,a,this._defsUsePending,!1,!1),a},image:function(e,t){var n=new bm;return _P(t,n),IP(e,n,this._defsUsePending,!1,!1),n.setStyle({image:e.getAttribute("xlink:href")||e.getAttribute("href"),x:+e.getAttribute("x"),y:+e.getAttribute("y"),width:+e.getAttribute("width"),height:+e.getAttribute("height")}),n.silent=!0,n},text:function(e,t){var n=e.getAttribute("x")||"0",i=e.getAttribute("y")||"0",a=e.getAttribute("dx")||"0",r=e.getAttribute("dy")||"0";this._textX=parseFloat(n)+parseFloat(a),this._textY=parseFloat(i)+parseFloat(r);var o=new Mc;return _P(t,o),IP(e,o,this._defsUsePending,!1,!0),o},tspan:function(e,t){var n=e.getAttribute("x"),i=e.getAttribute("y");null!=n&&(this._textX=parseFloat(n)),null!=i&&(this._textY=parseFloat(i));var a=e.getAttribute("dx")||"0",r=e.getAttribute("dy")||"0",o=new Mc;return _P(t,o),IP(e,o,this._defsUsePending,!1,!0),this._textX+=parseFloat(a),this._textY+=parseFloat(r),o},path:function(e,t){var n=rg(e.getAttribute("d")||"");return _P(t,n),IP(e,n,this._defsUsePending,!1,!1),n.silent=!0,n}}),e}(),wP={lineargradient:function(e){var t=parseInt(e.getAttribute("x1")||"0",10),n=parseInt(e.getAttribute("y1")||"0",10),i=parseInt(e.getAttribute("x2")||"10",10),a=parseInt(e.getAttribute("y2")||"0",10),r=new Ug(t,n,i,a);return SP(e,r),CP(e,r),r},radialgradient:function(e){var t=parseInt(e.getAttribute("cx")||"0",10),n=parseInt(e.getAttribute("cy")||"0",10),i=parseInt(e.getAttribute("r")||"0",10),a=new jg(t,n,i);return SP(e,a),CP(e,a),a}};function SP(e,t){"userSpaceOnUse"===e.getAttribute("gradientUnits")&&(t.global=!0)}function CP(e,t){for(var n=e.firstChild;n;){if(1===n.nodeType&&"stop"===n.nodeName.toLocaleLowerCase()){var i=n.getAttribute("offset"),a=void 0;a=i&&i.indexOf("%")>0?parseInt(i,10)/100:i?parseFloat(i):0;var r={};FP(n,r,r);var o=r.stopColor||n.getAttribute("stop-color")||"#000000";t.colorStops.push({offset:a,color:o})}n=n.nextSibling}}function _P(e,t){e&&e.__inheritedStyle&&(t.__inheritedStyle||(t.__inheritedStyle={}),Pr(t.__inheritedStyle,e.__inheritedStyle))}function TP(e){for(var t=PP(e),n=[],i=0;i0;r-=2){var o=i[r],s=i[r-1],l=PP(o);switch(a=a||[1,0,0,1,0,0],s){case"translate":fs(a,a,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":vs(a,a,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":ys(a,a,-parseFloat(l[0])*OP,[parseFloat(l[1]||"0"),parseFloat(l[2]||"0")]);break;case"skewX":gs(a,[1,0,Math.tan(parseFloat(l[0])*OP),1,0,0],a);break;case"skewY":gs(a,[1,Math.tan(parseFloat(l[0])*OP),0,1,0,0],a);break;case"matrix":a[0]=parseFloat(l[0]),a[1]=parseFloat(l[1]),a[2]=parseFloat(l[2]),a[3]=parseFloat(l[3]),a[4]=parseFloat(l[4]),a[5]=parseFloat(l[5])}}t.setLocalTransform(a)}}(e,t),FP(e,o,s),i||function(e,t,n){for(var i=0;i0,h={api:n,geo:s,mapOrGeoModel:e,data:o,isVisualEncodedByVisualMap:m,isGeo:r,transformInfoRaw:d};"geoJSON"===s.resourceType?this._buildGeoJSON(h):"geoSVG"===s.resourceType&&this._buildSVG(h),this._updateController(e,t,n),this._updateMapSelectHandler(e,l,n,i)},e.prototype._buildGeoJSON=function(e){var t=this._regionsGroupByName=fo(),n=fo(),i=this._regionsGroup,a=e.transformInfoRaw,r=e.mapOrGeoModel,o=e.data,s=e.geo.projection,l=s&&s.stream;function p(e,t){return t&&(e=t(e)),e&&[e[0]*a.scaleX+a.x,e[1]*a.scaleY+a.y]}function c(e){for(var t=[],n=!l&&s&&s.project,i=0;i=0)&&(u=a);var m=o?{normal:{align:"center",verticalAlign:"middle"}}:null;zf(t,Uf(i),{labelFetcher:u,labelDataIndex:d,defaultText:n},m);var h=t.getTextContent();if(h&&(QP(h).ignore=h.ignore,t.textConfig&&o)){var g=t.getBoundingRect().clone();t.textConfig.layoutRect=g,t.textConfig.position=[(o[0]-g.x)/g.width*100+"%",(o[1]-g.y)/g.height*100+"%"]}t.disableLabelAnimation=!0}else t.removeTextContent(),t.removeTextConfig(),t.disableLabelAnimation=null}function aD(e,t,n,i,a,r){e.data?e.data.setItemGraphicEl(r,t):Um(t).eventData={componentType:"geo",componentIndex:a.componentIndex,geoIndex:a.componentIndex,name:n,region:i&&i.option||{}}}function rD(e,t,n,i,a){e.data||Rf({el:t,componentModel:a,itemName:n,itemTooltipOption:i.get("tooltip")})}function oD(e,t,n,i,a){t.highDownSilentOnTouch=!!a.get("selectedMode");var r=i.getModel("emphasis"),o=r.get("focus");return Oh(t,o,r.get("blurScope"),r.get("disabled")),e.isGeo&&function(e,t,n){var i=Um(e);i.componentMainType=t.mainType,i.componentIndex=t.componentIndex,i.componentHighDownName=n}(t,a,n),o}function sD(e,t,n){var i,a=[];function r(){i=[]}function o(){i.length&&(a.push(i),i=[])}var s=t({polygonStart:r,polygonEnd:o,lineStart:r,lineEnd:o,point:function(e,t){isFinite(e)&&isFinite(t)&&i.push([e,t])},sphere:function(){}});return!n&&s.polygonStart(),Rr(e,(function(e){s.lineStart();for(var t=0;t-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2),n},t.type="series.map",t.dependencies=["geo"],t.layoutMode="box",t.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:"#000"},itemStyle:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{areaColor:"rgba(255,215,0,0.8)"}},select:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{color:"rgba(255,215,0,0.8)"}},nameProperty:"name"}}(ow);var lD=Lo,pD=function(e){function t(t){var n=e.call(this)||this;return n.type="view",n.dimensions=["x","y"],n._roamTransformable=new oc,n._rawTransformable=new oc,n.name=t,n}return ze(t,e),t.prototype.setBoundingRect=function(e,t,n,i){return this._rect=new Ps(e,t,n,i),this._rect},t.prototype.getBoundingRect=function(){return this._rect},t.prototype.setViewRect=function(e,t,n,i){this._transformTo(e,t,n,i),this._viewRect=new Ps(e,t,n,i)},t.prototype._transformTo=function(e,t,n,i){var a=this.getBoundingRect(),r=this._rawTransformable;r.transform=a.calculateTransform(new Ps(e,t,n,i));var o=r.parent;r.parent=null,r.decomposeTransform(),r.parent=o,this._updateTransform()},t.prototype.setCenter=function(e,t){e&&(this._center=[Vc(e[0],t.getWidth()),Vc(e[1],t.getHeight())],this._updateCenterAndZoom())},t.prototype.setZoom=function(e){e=e||1;var t=this.zoomLimit;t&&(null!=t.max&&(e=Math.min(t.max,e)),null!=t.min&&(e=Math.max(t.min,e))),this._zoom=e,this._updateCenterAndZoom()},t.prototype.getDefaultCenter=function(){var e=this.getBoundingRect();return[e.x+e.width/2,e.y+e.height/2]},t.prototype.getCenter=function(){return this._center||this.getDefaultCenter()},t.prototype.getZoom=function(){return this._zoom||1},t.prototype.getRoamTransform=function(){return this._roamTransformable.getLocalTransform()},t.prototype._updateCenterAndZoom=function(){var e=this._rawTransformable.getLocalTransform(),t=this._roamTransformable,n=this.getDefaultCenter(),i=this.getCenter(),a=this.getZoom();i=Lo([],i,e),n=Lo([],n,e),t.originX=i[0],t.originY=i[1],t.x=n[0]-i[0],t.y=n[1]-i[1],t.scaleX=t.scaleY=a,this._updateTransform()},t.prototype._updateTransform=function(){var e=this._roamTransformable,t=this._rawTransformable;t.parent=e,e.updateTransform(),t.updateTransform(),hs(this.transform||(this.transform=[]),t.transform||[1,0,0,1,0,0]),this._rawTransform=t.getLocalTransform(),this.invTransform=this.invTransform||[],xs(this.invTransform,this.transform),this.decomposeTransform()},t.prototype.getTransformInfo=function(){var e=this._rawTransformable,t=this._roamTransformable,n=new oc;return n.transform=t.transform,n.decomposeTransform(),{roam:{x:n.x,y:n.y,scaleX:n.scaleX,scaleY:n.scaleY},raw:{x:e.x,y:e.y,scaleX:e.scaleX,scaleY:e.scaleY}}},t.prototype.getViewRect=function(){return this._viewRect},t.prototype.getViewRectAfterRoam=function(){var e=this.getBoundingRect().clone();return e.applyTransform(this.transform),e},t.prototype.dataToPoint=function(e,t,n){var i=t?this._rawTransform:this.transform;return n=n||[],i?lD(n,e,i):_o(n,e)},t.prototype.pointToData=function(e){var t=this.invTransform;return t?lD([],e,t):[e[0],e[1]]},t.prototype.convertToPixel=function(e,t,n){var i=cD(t);return i===this?i.dataToPoint(n):null},t.prototype.convertFromPixel=function(e,t,n){var i=cD(t);return i===this?i.pointToData(n):null},t.prototype.containPoint=function(e){return this.getViewRectAfterRoam().contain(e[0],e[1])},t.dimensions=["x","y"],t}(oc);function cD(e){var t=e.seriesModel;return t?t.coordinateSystem:null}var dD={geoJSON:{aspectScale:.75,invertLongitute:!0},geoSVG:{aspectScale:1,invertLongitute:!1}},uD=["lng","lat"],mD=function(e){function t(t,n,i){var a=e.call(this,t)||this;a.dimensions=uD,a.type="geo",a._nameCoordMap=fo(),a.map=n;var r,o=i.projection,s=$P(n,i.nameMap,i.nameProperty),l=WP(n),p=(a.resourceType=l?l.type:null,a.regions=s.regions),c=dD[l.type];if(a._regionsMap=s.regionsMap,a.regions=s.regions,a.projection=o,o)for(var d=0;d1?(m.width=u,m.height=u/x):(m.height=u,m.width=u*x),m.y=d[1]-m.height/2,m.x=d[0]-m.width/2;else{var w=e.getBoxLayoutParams();w.aspect=x,m=gv(w,{width:y,height:v})}this.setViewRect(m.x,m.y,m.width,m.height),this.setCenter(e.get("center"),t),this.setZoom(e.get("zoom"))}Ar(mD,pD);var fD=function(){function e(){this.dimensions=uD}return e.prototype.create=function(e,t){var n=[];function i(e){return{nameProperty:e.get("nameProperty"),aspectScale:e.get("aspectScale"),projection:e.get("projection")}}e.eachComponent("geo",(function(e,a){var r=e.get("map"),o=new mD(r+a,r,kr({nameMap:e.get("nameMap")},i(e)));o.zoomLimit=e.get("scaleLimit"),n.push(o),e.coordinateSystem=o,o.model=e,o.resize=gD,o.resize(e,t)})),e.eachSeries((function(e){if("geo"===e.get("coordinateSystem")){var t=e.get("geoIndex")||0;e.coordinateSystem=n[t]}}));var a={};return e.eachSeriesByType("map",(function(e){if(!e.getHostGeoModel()){var t=e.getMapType();a[t]=a[t]||[],a[t].push(e)}})),Rr(a,(function(e,a){var r=Br(e,(function(e){return e.get("nameMap")})),o=new mD(a,a,kr({nameMap:Mr(r)},i(e[0])));o.zoomLimit=no.apply(null,Br(e,(function(e){return e.get("scaleLimit")}))),n.push(o),o.resize=gD,o.resize(e[0],t),Rr(e,(function(e){e.coordinateSystem=o,function(e,t){Rr(t.get("geoCoord"),(function(t,n){e.addGeoCoord(n,t)}))}(o,e)}))})),n},e.prototype.getFilledRegions=function(e,t,n,i){for(var a=(e||[]).slice(),r=fo(),o=0;ov.x)||(b-=Math.PI);var C=w?"left":"right",_=s.getModel("label"),T=_.get("rotate"),I=T*(Math.PI/180),E=f.getTextContent();E&&(f.setTextConfig({position:_.get("position")||C,rotation:null==T?-b:I,origin:"center"}),E.setStyle("verticalAlign","middle"))}var M=s.get(["emphasis","focus"]),k="relative"===M?yo(o.getAncestorsIndices(),o.getDescendantIndices()):"ancestor"===M?o.getAncestorsIndices():"descendant"===M?o.getDescendantIndices():null;k&&(Um(n).focus=k),function(e,t,n,i,a,r,o,s){var l=t.getModel(),p=e.get("edgeShape"),c=e.get("layout"),d=e.getOrient(),u=e.get(["lineStyle","curveness"]),m=e.get("edgeForkPosition"),h=l.getModel("lineStyle").getLineStyle(),g=i.__edge;if("curve"===p)t.parentNode&&t.parentNode!==n&&(g||(g=i.__edge=new Lg({shape:ID(c,d,u,a,a)})),tf(g,{shape:ID(c,d,u,r,o)},e));else if("polyline"===p)if("orthogonal"===c){if(t!==n&&t.children&&0!==t.children.length&&!0===t.isExpand){for(var f=t.children,y=[],v=0;vt&&(t=i.height)}this.height=t+1},e.prototype.getNodeById=function(e){if(this.getId()===e)return this;for(var t=0,n=this.children,i=n.length;t=0&&this.hostTree.data.setItemLayout(this.dataIndex,e,t)},e.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},e.prototype.getModel=function(e){if(!(this.dataIndex<0))return this.hostTree.data.getItemModel(this.dataIndex).getModel(e)},e.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},e.prototype.setVisual=function(e,t){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,e,t)},e.prototype.getVisual=function(e){return this.hostTree.data.getItemVisual(this.dataIndex,e)},e.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},e.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},e.prototype.getChildIndex=function(){if(this.parentNode){for(var e=this.parentNode.children,t=0;t=0){var i=n.getData().tree.root,a=e.targetNode;if(Hr(a)&&(a=i.getNodeById(a)),a&&i.contains(a))return{node:a};var r=e.targetNodeId;if(null!=r&&(a=i.getNodeById(r)))return{node:a}}}function VD(e){for(var t=[];e;)(e=e.parentNode)&&t.push(e);return t.reverse()}function qD(e,t){return Dr(VD(e),t)>=0}function GD(e,t){for(var n=[];e;){var i=e.dataIndex;n.push({name:e.name,dataIndex:i,value:t.getRawValue(i)}),e=e.parentNode}return n.reverse(),n}!function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.hasSymbolVisual=!0,t.ignoreStyleOnData=!0,t}ze(t,e),t.prototype.getInitialData=function(e){var t={name:e.name,children:e.data},n=e.leaves||{},i=new uy(n,this,this.ecModel),a=ND.createTree(t,this,(function(e){e.wrapMethod("getItemModel",(function(e,t){var n=a.getNodeByDataIndex(t);return n&&n.children.length&&n.isExpand||(e.parentModel=i),e}))}));var r=0;a.eachNode("preorder",(function(e){e.depth>r&&(r=e.depth)}));var o=e.expandAndCollapse&&e.initialTreeDepth>=0?e.initialTreeDepth:r;return a.root.eachNode("preorder",(function(e){var t=e.hostTree.data.getRawDataItem(e.dataIndex);e.isExpand=t&&null!=t.collapsed?!t.collapsed:e.depth<=o})),a.data},t.prototype.getOrient=function(){var e=this.get("orient");return"horizontal"===e?e="LR":"vertical"===e&&(e="TB"),e},t.prototype.setZoom=function(e){this.option.zoom=e},t.prototype.setCenter=function(e){this.option.center=e},t.prototype.formatTooltip=function(e,t,n){for(var i=this.getData().tree,a=i.root.children[0],r=i.getNodeByDataIndex(e),o=r.getValue(),s=r.name;r&&r!==a;)s=r.parentNode.name+"."+s,r=r.parentNode;return jb("nameValue",{name:s,value:o,noValue:isNaN(o)||null==o})},t.prototype.getDataParams=function(t){var n=e.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(t);return n.treeAncestors=GD(i,this),n.collapsed=!i.isExpand,n},t.type="series.tree",t.layoutMode="box",t.defaultOption={z:2,coordinateSystem:"view",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:"#ccc",width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500}}(ow);function zD(e){var t=e.getData().tree,n={};t.eachNode((function(t){for(var i=t;i&&i.depth>1;)i=i.parentNode;var a=Zv(e.ecModel,i.name||i.dataIndex+"",n);t.setVisual("decal",a)}))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.preventUsingHoverLayer=!0,n}ze(t,e),t.prototype.getInitialData=function(e,t){var n={name:e.name,children:e.data};UD(n);var i=e.levels||[],a=this.designatedVisualItemStyle={},r=new uy({itemStyle:a},this,t);i=e.levels=function(e,t){var n,i,a=dd(t.get("color")),r=dd(t.get(["aria","decal","decals"]));if(!a)return;e=e||[],Rr(e,(function(e){var t=new uy(e),a=t.get("color"),r=t.get("decal");(t.get(["itemStyle","color"])||a&&"none"!==a)&&(n=!0),(t.get(["itemStyle","decal"])||r&&"none"!==r)&&(i=!0)}));var o=e[0]||(e[0]={});n||(o.color=a.slice());!i&&r&&(o.decal=r.slice());return e}(i,t);var o=Br(i||[],(function(e){return new uy(e,r,t)}),this),s=ND.createTree(n,this,(function(e){e.wrapMethod("getItemModel",(function(e,t){var n=s.getNodeByDataIndex(t),i=n?o[n.depth]:null;return e.parentModel=i||r,e}))}));return s.data},t.prototype.optionUpdated=function(){this.resetViewRoot()},t.prototype.formatTooltip=function(e,t,n){var i=this.getData(),a=this.getRawValue(e);return jb("nameValue",{name:i.getName(e),value:a})},t.prototype.getDataParams=function(t){var n=e.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(t);return n.treeAncestors=GD(i,this),n.treePathInfo=n.treeAncestors,n},t.prototype.setLayoutInfo=function(e){this.layoutInfo=this.layoutInfo||{},kr(this.layoutInfo,e)},t.prototype.mapIdToIndex=function(e){var t=this._idIndexMap;t||(t=this._idIndexMap=fo(),this._idIndexMapCount=0);var n=t.get(e);return null==n&&t.set(e,n=this._idIndexMapCount++),n},t.prototype.getViewRoot=function(){return this._viewRoot},t.prototype.resetViewRoot=function(e){e?this._viewRoot=e:e=this._viewRoot;var t=this.getRawData().tree.root;e&&(e===t||t.contains(e))||(this._viewRoot=t)},t.prototype.enableAriaDecal=function(){zD(this)},t.type="series.treemap",t.layoutMode="box",t.defaultOption={progressive:0,left:"center",top:"middle",width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{color:"rgba(0,0,0,0.7)",textStyle:{color:"#fff"}},emphasis:{itemStyle:{color:"rgba(0,0,0,0.9)"}}},label:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]}}(ow);function UD(e){var t=0;Rr(e.children,(function(e){UD(e);var n=e.value;Ur(n)&&(n=n[0]),t+=n}));var n=e.value;Ur(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=t),n<0&&(n=0),Ur(e.value)?e.value[0]=n:e.value=n}var jD=function(){function e(e){this.group=new Mc,e.add(this.group)}return e.prototype.render=function(e,t,n,i){var a=e.getModel("breadcrumb"),r=this.group;if(r.removeAll(),a.get("show")&&n){var o=a.getModel("itemStyle"),s=a.getModel("emphasis"),l=o.getModel("textStyle"),p=s.getModel(["itemStyle","textStyle"]),c={pos:{left:a.get("left"),right:a.get("right"),top:a.get("top"),bottom:a.get("bottom")},box:{width:t.getWidth(),height:t.getHeight()},emptyItemWidth:a.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(n,c,l),this._renderContent(e,c,o,s,l,p,i),fv(r,c.pos,c.box)}},e.prototype._prepare=function(e,t,n){for(var i=e;i;i=i.parentNode){var a=xd(i.getModel().get("name"),""),r=n.getTextRect(a),o=Math.max(r.width+16,t.emptyItemWidth);t.totalWidth+=o+8,t.renderList.push({node:i,text:a,width:o})}},e.prototype._renderContent=function(e,t,n,i,a,r,o){for(var s,l,p,c,d,u,m,h,g,f=0,y=t.emptyItemWidth,v=e.get(["breadcrumb","height"]),x=(s=t.pos,l=t.box,c=l.width,d=l.height,u=Vc(s.left,c),m=Vc(s.top,d),h=Vc(s.right,c),g=Vc(s.bottom,d),(isNaN(u)||isNaN(parseFloat(s.left)))&&(u=0),(isNaN(h)||isNaN(parseFloat(s.right)))&&(h=c),(isNaN(m)||isNaN(parseFloat(s.top)))&&(m=0),(isNaN(g)||isNaN(parseFloat(s.bottom)))&&(g=d),p=nv(p||0),{width:Math.max(h-u-p[1]-p[3],0),height:Math.max(g-m-p[0]-p[2],0)}),b=t.totalWidth,w=t.renderList,S=i.getModel("itemStyle").getItemStyle(),C=w.length-1;C>=0;C--){var _=w[C],T=_.node,I=_.width,E=_.text;b>x.width&&(b-=I-y,I=y,E=null);var M=new kg({shape:{points:HD(f,0,I,v,C===w.length-1,0===C)},style:Pr(n.getItemStyle(),{lineJoin:"bevel"}),textContent:new Pm({style:jf(a,{text:E})}),textConfig:{position:"inside"},z2:1e5,onclick:zr(o,T)});M.disableLabelAnimation=!0,M.getTextContent().ensureState("emphasis").style=jf(r,{text:E}),M.ensureState("emphasis").style=S,Oh(M,i.get("focus"),i.get("blurScope"),i.get("disabled")),this.group.add(M),WD(M,e,T),f+=I+8}},e.prototype.remove=function(){this.group.removeAll()},e}();function HD(e,t,n,i,a,r){var o=[[a?e:e-5,t],[e+n,t],[e+n,t+i],[a?e:e-5,t+i]];return!r&&o.splice(2,0,[e+n+5,t+i/2]),!a&&o.push([e,t+i/2]),o}function WD(e,t,n){Um(e).eventData={componentType:"series",componentSubType:"treemap",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&GD(n,t)}}var $D=function(){function e(){this._storage=[],this._elExistsMap={}}return e.prototype.add=function(e,t,n,i,a){return!this._elExistsMap[e.id]&&(this._elExistsMap[e.id]=!0,this._storage.push({el:e,target:t,duration:n,delay:i,easing:a}),!0)},e.prototype.finished=function(e){return this._finishedCallback=e,this},e.prototype.start=function(){for(var e=this,t=this._storage.length,n=function(){--t<=0&&(e._storage.length=0,e._elExistsMap={},e._finishedCallback&&e._finishedCallback())},i=0,a=this._storage.length;i3||Math.abs(e.dy)>3)){var t=this.seriesModel.getData().tree.root;if(!t)return;var n=t.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+e.dx,y:n.y+e.dy,width:n.width,height:n.height}})}},t.prototype._onZoom=function(e){var t=e.originX,n=e.originY;if("animating"!==this._state){var i=this.seriesModel.getData().tree.root;if(!i)return;var a=i.getLayout();if(!a)return;var r=new Ps(a.x,a.y,a.width,a.height),o=this.seriesModel.layoutInfo,s=[1,0,0,1,0,0];fs(s,s,[-(t-=o.x),-(n-=o.y)]),vs(s,s,[e.scale,e.scale]),fs(s,s,[t,n]),r.applyTransform(s),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:r.x,y:r.y,width:r.width,height:r.height}})}},t.prototype._initEvents=function(e){var t=this;e.on("click",(function(e){if("ready"===t._state){var n=t.seriesModel.get("nodeClick",!0);if(n){var i=t.findTarget(e.offsetX,e.offsetY);if(i){var a=i.node;if(a.getLayout().isLeafRoot)t._rootToNode(i);else if("zoomToNode"===n)t._zoomToNode(i);else if("link"===n){var r=a.hostTree.data.getItemModel(a.dataIndex),o=r.get("link",!0),s=r.get("target",!0)||"blank";o&&pv(o,s)}}}}}),this)},t.prototype._renderBreadcrumb=function(e,t,n){var i=this;n||(n=null!=e.get("leafDepth",!0)?{node:e.getViewRoot()}:this.findTarget(t.getWidth()/2,t.getHeight()/2))||(n={node:e.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new jD(this.group))).render(e,t,n.node,(function(t){"animating"!==i._state&&(qD(e.getViewRoot(),t)?i._rootToNode({node:t}):i._zoomToNode({node:t}))}))},t.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage={nodeGroup:[],background:[],content:[]},this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},t.prototype.dispose=function(){this._clearController()},t.prototype._zoomToNode=function(e){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:e.node})},t.prototype._rootToNode=function(e){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:e.node})},t.prototype.findTarget=function(e,t){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},(function(i){var a=this._storage.background[i.getRawIndex()];if(a){var r=a.transformCoordToLocal(e,t),o=a.shape;if(!(o.x<=r[0]&&r[0]<=o.x+o.width&&o.y<=r[1]&&r[1]<=o.y+o.height))return!1;n={node:i,offsetX:r[0],offsetY:r[1]}}}),this),n},t.type="treemap"}(vw);var tO=Rr,nO=Kr,iO=-1,aO=function(){function e(t){var n=t.mappingMethod,i=t.type,a=this.option=Ir(t);this.type=i,this.mappingMethod=n,this._normalizeData=hO[n];var r=e.visualHandlers[i];this.applyVisual=r.applyVisual,this.getColorMapper=r.getColorMapper,this._normalizedToVisual=r._normalizedToVisual[n],"piecewise"===n?(rO(a),function(e){var t=e.pieceList;e.hasSpecialVisual=!1,Rr(t,(function(t,n){t.originIndex=n,null!=t.visual&&(e.hasSpecialVisual=!0)}))}(a)):"category"===n?a.categories?function(e){var t=e.categories,n=e.categoryMap={},i=e.visual;if(tO(t,(function(e,t){n[e]=t})),!Ur(i)){var a=[];Kr(i)?tO(i,(function(e,t){var i=n[t];a[null!=i?i:iO]=e})):a[-1]=i,i=mO(e,a)}for(var r=t.length-1;r>=0;r--)null==i[r]&&(delete n[t[r]],t.pop())}(a):rO(a,!0):(so("linear"!==n||a.dataExtent),rO(a))}return e.prototype.mapValueToVisual=function(e){var t=this._normalizeData(e);return this._normalizedToVisual(t,e)},e.prototype.getNormalizer=function(){return Gr(this._normalizeData,this)},e.listVisualTypes=function(){return qr(e.visualHandlers)},e.isValidType=function(t){return e.visualHandlers.hasOwnProperty(t)},e.eachVisual=function(e,t,n){Kr(e)?Rr(e,t,n):t.call(n,e)},e.mapVisual=function(t,n,i){var a,r=Ur(t)?[]:Kr(t)?{}:(a=!0,null);return e.eachVisual(t,(function(e,t){var o=n.call(i,e,t);a?r=o:r[t]=o})),r},e.retrieveVisuals=function(t){var n,i={};return t&&tO(e.visualHandlers,(function(e,a){t.hasOwnProperty(a)&&(i[a]=t[a],n=!0)})),n?i:null},e.prepareVisualTypes=function(e){if(Ur(e))e=e.slice();else{if(!nO(e))return[];var t=[];tO(e,(function(e,n){t.push(n)})),e=t}return e.sort((function(e,t){return"color"===t&&"color"!==e&&0===e.indexOf("color")?1:-1})),e},e.dependsOn=function(e,t){return"color"===t?!(!e||0!==e.indexOf(t)):e===t},e.findPieceIndex=function(e,t,n){for(var i,a=1/0,r=0,o=t.length;ri&&(i=t);var r=i%2?i+2:i+3;a=[];for(var o=0;o0&&(v[0]=-v[0],v[1]=-v[1]);var b=y[0]<0?-1:1;if("start"!==i.__position&&"end"!==i.__position){var w=-Math.atan2(y[1],y[0]);p[0].8?"left":c[0]<-.8?"right":"center",u=c[1]>.8?"top":c[1]<-.8?"bottom":"middle";break;case"start":i.x=-c[0]*h+l[0],i.y=-c[1]*g+l[1],d=c[0]>.8?"right":c[0]<-.8?"left":"center",u=c[1]>.8?"bottom":c[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=h*b+l[0],i.y=l[1]+S,d=y[0]<0?"right":"left",i.originX=-h*b,i.originY=-S;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+S,d="center",i.originY=-S;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-h*b+p[0],i.y=p[1]+S,d=y[0]>=0?"right":"left",i.originX=h*b,i.originY=-S}i.scaleX=i.scaleY=a,i.setStyle({verticalAlign:i.__verticalAlign||u,align:i.__align||d})}}}function C(e,t){var n=e.__specifiedRotation;if(null==n){var i=o.tangentAt(t);e.attr("rotation",(1===t?-1:1)*Math.PI/2-Math.atan2(i[1],i[0]))}else e.attr("rotation",n)}},t}(Mc),zO=function(){function e(e){this.group=new Mc,this._LineCtor=e||GO}return e.prototype.updateData=function(e){var t=this;this._progressiveEls=null;var n=this,i=n.group,a=n._lineData;n._lineData=e,a||i.removeAll();var r=UO(e);e.diff(a).add((function(n){t._doAdd(e,n,r)})).update((function(n,i){t._doUpdate(a,e,i,n,r)})).remove((function(e){i.remove(a.getItemGraphicEl(e))})).execute()},e.prototype.updateLayout=function(){var e=this._lineData;e&&e.eachItemGraphicEl((function(t,n){t.updateLayout(e,n)}),this)},e.prototype.incrementalPrepareUpdate=function(e){this._seriesScope=UO(e),this._lineData=null,this.group.removeAll()},e.prototype.incrementalUpdate=function(e,t){function n(e){e.isGroup||function(e){return e.animators&&e.animators.length>0}(e)||(e.incremental=!0,e.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[];for(var i=e.start;i=0?i+=p:i-=p:h>=0?i-=p:i+=p}return i}function JO(e,t){var n=[],i=bl,a=[[],[],[]],r=[[],[]],o=[];t/=2,e.eachEdge((function(e,s){var l=e.getLayout(),p=e.getVisual("fromSymbol"),c=e.getVisual("toSymbol");l.__original||(l.__original=[To(l[0]),To(l[1])],l[2]&&l.__original.push(To(l[2])));var d=l.__original;if(null!=l[2]){if(_o(a[0],d[0]),_o(a[1],d[2]),_o(a[2],d[1]),p&&"none"!==p){var u=TO(e.node1),m=QO(a,d[0],u*t);i(a[0][0],a[1][0],a[2][0],m,n),a[0][0]=n[3],a[1][0]=n[4],i(a[0][1],a[1][1],a[2][1],m,n),a[0][1]=n[3],a[1][1]=n[4]}if(c&&"none"!==c){u=TO(e.node2),m=QO(a,d[1],u*t);i(a[0][0],a[1][0],a[2][0],m,n),a[1][0]=n[1],a[2][0]=n[2],i(a[0][1],a[1][1],a[2][1],m,n),a[1][1]=n[1],a[2][1]=n[2]}_o(l[0],a[0]),_o(l[1],a[2]),_o(l[2],a[1])}else{if(_o(r[0],d[0]),_o(r[1],d[1]),Mo(o,r[1],r[0]),Oo(o,o),p&&"none"!==p){u=TO(e.node1);Eo(r[0],r[0],o,u*t)}if(c&&"none"!==c){u=TO(e.node2);Eo(r[1],r[1],o,-u*t)}_o(l[0],r[0]),_o(l[1],r[1])}}))}function eA(e){return"view"===e.type}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.init=function(e,t){var n=new CE,i=new zO,a=this.group;this._controller=new oP(t.getZr()),this._controllerHost={target:a},a.add(n.group),a.add(i.group),this._symbolDraw=n,this._lineDraw=i,this._firstRender=!0},t.prototype.render=function(e,t,n){var i=this,a=e.coordinateSystem;this._model=e;var r=this._symbolDraw,o=this._lineDraw,s=this.group;if(eA(a)){var l={x:a.x,y:a.y,scaleX:a.scaleX,scaleY:a.scaleY};this._firstRender?s.attr(l):tf(s,l,e)}JO(e.getGraph(),_O(e));var p=e.getData();r.updateData(p);var c=e.getEdgeData();o.updateData(c),this._updateNodeAndLinkScale(),this._updateController(e,t,n),clearTimeout(this._layoutTimeout);var d=e.forceLayout,u=e.get(["force","layoutAnimation"]);d&&this._startForceLayoutIteration(d,u);var m=e.get("layout");p.graph.eachNode((function(t){var n=t.dataIndex,a=t.getGraphicEl(),r=t.getModel();if(a){a.off("drag").off("dragend");var o=r.get("draggable");o&&a.on("drag",(function(r){switch(m){case"force":d.warmUp(),!i._layouting&&i._startForceLayoutIteration(d,u),d.setFixed(n),p.setItemLayout(n,[a.x,a.y]);break;case"circular":p.setItemLayout(n,[a.x,a.y]),t.setLayout({fixed:!0},!0),MO(e,"symbolSize",t,[r.offsetX,r.offsetY]),i.updateLayout(e);break;default:p.setItemLayout(n,[a.x,a.y]),CO(e.getGraph(),e),i.updateLayout(e)}})).on("dragend",(function(){d&&d.setUnfixed(n)})),a.setDraggable(o,!!r.get("cursor")),"adjacency"===r.get(["emphasis","focus"])&&(Um(a).focus=t.getAdjacentDataIndices())}})),p.graph.eachEdge((function(e){var t=e.getGraphicEl(),n=e.getModel().get(["emphasis","focus"]);t&&"adjacency"===n&&(Um(t).focus={edge:[e.dataIndex],node:[e.node1.dataIndex,e.node2.dataIndex]})}));var h="circular"===e.get("layout")&&e.get(["circular","rotateLabel"]),g=p.getLayout("cx"),f=p.getLayout("cy");p.graph.eachNode((function(e){PO(e,h,g,f)})),this._firstRender=!1},t.prototype.dispose=function(){this.remove(),this._controller&&this._controller.dispose(),this._controllerHost=null},t.prototype._startForceLayoutIteration=function(e,t){var n=this;!function i(){e.step((function(e){n.updateLayout(n._model),(n._layouting=!e)&&(t?n._layoutTimeout=setTimeout(i,16):i())}))}()},t.prototype._updateController=function(e,t,n){var i=this,a=this._controller,r=this._controllerHost,o=this.group;a.setPointerChecker((function(t,i,a){var r=o.getBoundingRect();return r.applyTransform(o.transform),r.contain(i,a)&&!hP(t,n,e)})),eA(e.coordinateSystem)?(a.enable(e.get("roam")),r.zoomLimit=e.get("scaleLimit"),r.zoom=e.coordinateSystem.getZoom(),a.off("pan").off("zoom").on("pan",(function(t){cP(r,t.dx,t.dy),n.dispatchAction({seriesId:e.id,type:"graphRoam",dx:t.dx,dy:t.dy})})).on("zoom",(function(t){dP(r,t.scale,t.originX,t.originY),n.dispatchAction({seriesId:e.id,type:"graphRoam",zoom:t.scale,originX:t.originX,originY:t.originY}),i._updateNodeAndLinkScale(),JO(e.getGraph(),_O(e)),i._lineDraw.updateLayout(),n.updateLabelLayout()}))):a.disable()},t.prototype._updateNodeAndLinkScale=function(){var e=this._model,t=e.getData(),n=_O(e);t.eachItemGraphicEl((function(e,t){e&&e.setSymbolScale(n)}))},t.prototype.updateLayout=function(e){JO(e.getGraph(),_O(e)),this._symbolDraw.updateLayout(),this._lineDraw.updateLayout()},t.prototype.remove=function(){clearTimeout(this._layoutTimeout),this._layouting=!1,this._layoutTimeout=null,this._symbolDraw&&this._symbolDraw.remove(),this._lineDraw&&this._lineDraw.remove()},t.type="graph"}(vw);function tA(e){return"_EC_"+e}var nA=function(){function e(e){this.type="graph",this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this._directed=e||!1}return e.prototype.isDirected=function(){return this._directed},e.prototype.addNode=function(e,t){e=null==e?""+t:""+e;var n=this._nodesMap;if(!n[tA(e)]){var i=new iA(e,t);return i.hostGraph=this,this.nodes.push(i),n[tA(e)]=i,i}},e.prototype.getNodeByIndex=function(e){var t=this.data.getRawIndex(e);return this.nodes[t]},e.prototype.getNodeById=function(e){return this._nodesMap[tA(e)]},e.prototype.addEdge=function(e,t,n){var i=this._nodesMap,a=this._edgesMap;if($r(e)&&(e=this.nodes[e]),$r(t)&&(t=this.nodes[t]),e instanceof iA||(e=i[tA(e)]),t instanceof iA||(t=i[tA(t)]),e&&t){var r=e.id+"-"+t.id,o=new aA(e,t,n);return o.hostGraph=this,this._directed&&(e.outEdges.push(o),t.inEdges.push(o)),e.edges.push(o),e!==t&&t.edges.push(o),this.edges.push(o),a[r]=o,o}},e.prototype.getEdgeByIndex=function(e){var t=this.edgeData.getRawIndex(e);return this.edges[t]},e.prototype.getEdge=function(e,t){e instanceof iA&&(e=e.id),t instanceof iA&&(t=t.id);var n=this._edgesMap;return this._directed?n[e+"-"+t]:n[e+"-"+t]||n[t+"-"+e]},e.prototype.eachNode=function(e,t){for(var n=this.nodes,i=n.length,a=0;a=0&&e.call(t,n[a],a)},e.prototype.eachEdge=function(e,t){for(var n=this.edges,i=n.length,a=0;a=0&&n[a].node1.dataIndex>=0&&n[a].node2.dataIndex>=0&&e.call(t,n[a],a)},e.prototype.breadthFirstTraverse=function(e,t,n,i){if(t instanceof iA||(t=this._nodesMap[tA(t)]),t){for(var a="out"===n?"outEdges":"in"===n?"inEdges":"edges",r=0;r=0&&n.node2.dataIndex>=0}));for(a=0,r=i.length;a=0&&this[e][t].setItemVisual(this.dataIndex,n,i)},getVisual:function(n){return this[e][t].getItemVisual(this.dataIndex,n)},setLayout:function(n,i){this.dataIndex>=0&&this[e][t].setItemLayout(this.dataIndex,n,i)},getLayout:function(){return this[e][t].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[e][t].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[e][t].getRawIndex(this.dataIndex)}}}function oA(e,t,n,i,a){for(var r=new nA(i),o=0;o "+u)),p++)}var m,h=n.get("coordinateSystem");if("cartesian2d"===h||"polar"===h)m=K_(e,n);else{var g=sx.get(h),f=g&&g.dimensions||[];Dr(f,"value")<0&&f.concat(["value"]);var y=q_(e,{coordDimensions:f,encodeDefine:n.getEncode()}).dimensions;(m=new V_(y,n)).initData(e)}var v=new V_(["value"],n);return v.initData(l,s),a&&a(m,v),MD({mainData:m,struct:r,structAttr:"graph",datas:{node:m,edge:v},datasAttr:{node:"data",edge:"edgeData"}}),r.update(),r}Ar(iA,rA("hostGraph","data")),Ar(aA,rA("hostGraph","edgeData"));!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n}ze(t,e),t.prototype.init=function(t){e.prototype.init.apply(this,arguments);var n=this;function i(){return n._categoriesData}this.legendVisualProvider=new qM(i,i),this.fillDataTextStyle(t.edges||t.links),this._updateCategoriesData()},t.prototype.mergeOption=function(t){e.prototype.mergeOption.apply(this,arguments),this.fillDataTextStyle(t.edges||t.links),this._updateCategoriesData()},t.prototype.mergeDefaultAndTheme=function(t){e.prototype.mergeDefaultAndTheme.apply(this,arguments),ud(t,"edgeLabel",["show"])},t.prototype.getInitialData=function(e,t){var n,i=e.edges||e.links||[],a=e.data||e.nodes||[],r=this;if(a&&i){yO(n=this)&&(n.__curvenessList=[],n.__edgeMap={},vO(n));var o=oA(a,i,this,!0,(function(e,t){e.wrapMethod("getItemModel",(function(e){var t=r._categoriesModels[e.getShallow("category")];return t&&(t.parentModel=e.parentModel,e.parentModel=t),e}));var n=uy.prototype.getModel;function i(e,t){var i=n.call(this,e,t);return i.resolveParentPath=a,i}function a(e){if(e&&("label"===e[0]||"label"===e[1])){var t=e.slice();return"label"===e[0]?t[0]="edgeLabel":"label"===e[1]&&(t[1]="edgeLabel"),t}return e}t.wrapMethod("getItemModel",(function(e){return e.resolveParentPath=a,e.getModel=i,e}))}));return Rr(o.edges,(function(e){!function(e,t,n,i){if(yO(n)){var a=xO(e,t,n),r=n.__edgeMap,o=r[bO(a)];r[a]&&!o?r[a].isForward=!0:o&&r[a]&&(o.isForward=!0,r[a].isForward=!1),r[a]=r[a]||[],r[a].push(i)}}(e.node1,e.node2,this,e.dataIndex)}),this),o.data}},t.prototype.getGraph=function(){return this.getData().graph},t.prototype.getEdgeData=function(){return this.getGraph().edgeData},t.prototype.getCategoriesData=function(){return this._categoriesData},t.prototype.formatTooltip=function(e,t,n){if("edge"===n){var i=this.getData(),a=this.getDataParams(e,n),r=i.graph.getEdgeByIndex(e),o=i.getName(r.node1.dataIndex),s=i.getName(r.node2.dataIndex),l=[];return null!=o&&l.push(o),null!=s&&l.push(s),jb("nameValue",{name:l.join(" > "),value:a.value,noValue:null==a.value})}return nw({series:this,dataIndex:e,multipleSeries:t})},t.prototype._updateCategoriesData=function(){var e=Br(this.option.categories||[],(function(e){return null!=e.value?e:kr({value:0},e)})),t=new V_(["value"],this);t.initData(e),this._categoriesData=t,this._categoriesModels=t.mapArray((function(e){return t.getItemModel(e)}))},t.prototype.setZoom=function(e){this.option.zoom=e},t.prototype.setCenter=function(e){this.option.center=e},t.prototype.isAnimationEnabled=function(){return e.prototype.isAnimationEnabled.call(this)&&!("force"===this.get("layout")&&this.get(["force","layoutAnimation"]))},t.type="series.graph",t.dependencies=["grid","polar","geo","singleAxis","calendar"],t.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:"#aaa",width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}}}(ow);var sA=function(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0},lA=function(e){function t(t){var n=e.call(this,t)||this;return n.type="pointer",n}return ze(t,e),t.prototype.getDefaultShape=function(){return new sA},t.prototype.buildPath=function(e,t){var n=Math.cos,i=Math.sin,a=t.r,r=t.width,o=t.angle,s=t.x-n(o)*r*(r>=a/3?1:2),l=t.y-i(o)*r*(r>=a/3?1:2);o=t.angle-Math.PI/2,e.moveTo(s,l),e.lineTo(t.x+n(o)*r,t.y+i(o)*r),e.lineTo(t.x+n(t.angle)*a,t.y+i(t.angle)*a),e.lineTo(t.x-n(o)*r,t.y-i(o)*r),e.lineTo(s,l)},t}(gm);function pA(e,t){var n=null==e?"":e+"";return t&&(Hr(t)?n=t.replace("{value}",n):jr(t)&&(n=t(e))),n}(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.render=function(e,t,n){this.group.removeAll();var i=e.get(["axisLine","lineStyle","color"]),a=function(e,t){var n=e.get("center"),i=t.getWidth(),a=t.getHeight(),r=Math.min(i,a);return{cx:Vc(n[0],t.getWidth()),cy:Vc(n[1],t.getHeight()),r:Vc(e.get("radius"),r/2)}}(e,n);this._renderMain(e,t,n,i,a),this._data=e.getData()},t.prototype.dispose=function(){},t.prototype._renderMain=function(e,t,n,i,a){var r=this.group,o=e.get("clockwise"),s=-e.get("startAngle")/180*Math.PI,l=-e.get("endAngle")/180*Math.PI,p=e.getModel("axisLine"),c=p.get("roundCap")?iM:_g,d=p.get("show"),u=p.getModel("lineStyle"),m=u.get("width"),h=[s,l];Ku(h,!o);for(var g=(l=h[1])-(s=h[0]),f=s,y=[],v=0;d&&v=e&&(0===t?0:i[t-1][0])Math.PI/2&&(L+=Math.PI):"tangential"===N?L=-_-Math.PI/2:$r(N)&&(L=N*Math.PI/180),0===L?d.add(new Pm({style:jf(x,{text:A,x:R,y:B,verticalAlign:c<-.8?"top":c>.8?"bottom":"middle",align:p<-.4?"left":p>.4?"right":"center"},{inheritColor:F}),silent:!0})):d.add(new Pm({style:jf(x,{text:A,x:R,y:B,verticalAlign:"middle",align:"center"},{inheritColor:F}),silent:!0,originX:R,originY:B,rotation:L}))}if(v.get("show")&&P!==b){O=(O=v.get("distance"))?O+l:l;for(var V=0;V<=w;V++){p=Math.cos(_),c=Math.sin(_);var q=new Fg({shape:{x1:p*(h-O)+u,y1:c*(h-O)+m,x2:p*(h-C-O)+u,y2:c*(h-C-O)+m},silent:!0,style:M});"auto"===M.stroke&&q.setStyle({stroke:i((P+V/w)/b)}),d.add(q),_+=I}_-=I}else _+=T}},t.prototype._renderPointer=function(e,t,n,i,a,r,o,s,l){var p=this.group,c=this._data,d=this._progressEls,u=[],m=e.get(["pointer","show"]),h=e.getModel("progress"),g=h.get("show"),f=e.getData(),y=f.mapDimension("value"),v=+e.get("min"),x=+e.get("max"),b=[v,x],w=[r,o];function S(t,n){var i,r=f.getItemModel(t).getModel("pointer"),o=Vc(r.get("width"),a.r),s=Vc(r.get("length"),a.r),l=e.get(["pointer","icon"]),p=r.get("offsetCenter"),c=Vc(p[0],a.r),d=Vc(p[1],a.r),u=r.get("keepAspect");return(i=l?PS(l,c-o/2,d-s,o,s,null,u):new lA({shape:{angle:-Math.PI/2,width:o,r:s,x:c,y:d}})).rotation=-(n+Math.PI/2),i.x=a.cx,i.y=a.cy,i}function C(e,t){var n=h.get("roundCap")?iM:_g,i=h.get("overlap"),o=i?h.get("width"):l/f.count(),p=i?a.r-o:a.r-(e+1)*o,c=i?a.r:a.r-e*o,d=new n({shape:{startAngle:r,endAngle:t,cx:a.cx,cy:a.cy,clockwise:s,r0:p,r:c}});return i&&(d.z2=x-f.get(y,e)%x),d}(g||m)&&(f.diff(c).add((function(t){var n=f.get(y,t);if(m){var i=S(t,r);nf(i,{rotation:-((isNaN(+n)?w[0]:Lc(n,b,w,!0))+Math.PI/2)},e),p.add(i),f.setItemGraphicEl(t,i)}if(g){var a=C(t,r),o=h.get("clip");nf(a,{shape:{endAngle:Lc(n,b,w,o)}},e),p.add(a),jm(e.seriesIndex,f.dataType,t,a),u[t]=a}})).update((function(t,n){var i=f.get(y,t);if(m){var a=c.getItemGraphicEl(n),o=a?a.rotation:r,s=S(t,o);s.rotation=o,tf(s,{rotation:-((isNaN(+i)?w[0]:Lc(i,b,w,!0))+Math.PI/2)},e),p.add(s),f.setItemGraphicEl(t,s)}if(g){var l=d[n],v=C(t,l?l.shape.endAngle:r),x=h.get("clip");tf(v,{shape:{endAngle:Lc(i,b,w,x)}},e),p.add(v),jm(e.seriesIndex,f.dataType,t,v),u[t]=v}})).execute(),f.each((function(e){var t=f.getItemModel(e),n=t.getModel("emphasis"),a=n.get("focus"),r=n.get("blurScope"),o=n.get("disabled");if(m){var s=f.getItemGraphicEl(e),l=f.getItemVisual(e,"style"),p=l.fill;if(s instanceof bm){var c=s.style;s.useStyle(kr({image:c.image,x:c.x,y:c.y,width:c.width,height:c.height},l))}else s.useStyle(l),"pointer"!==s.type&&s.setColor(p);s.setStyle(t.getModel(["pointer","itemStyle"]).getItemStyle()),"auto"===s.style.fill&&s.setStyle("fill",i(Lc(f.get(y,e),b,[0,1],!0))),s.z2EmphasisLift=0,Bh(s,t),Oh(s,a,r,o)}if(g){var d=u[e];d.useStyle(f.getItemVisual(e,"style")),d.setStyle(t.getModel(["progress","itemStyle"]).getItemStyle()),d.z2EmphasisLift=0,Bh(d,t),Oh(d,a,r,o)}})),this._progressEls=u)},t.prototype._renderAnchor=function(e,t){var n=e.getModel("anchor");if(n.get("show")){var i=n.get("size"),a=n.get("icon"),r=n.get("offsetCenter"),o=n.get("keepAspect"),s=PS(a,t.cx-i/2+Vc(r[0],t.r),t.cy-i/2+Vc(r[1],t.r),i,i,null,o);s.z2=n.get("showAbove")?1:0,s.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(s)}},t.prototype._renderTitleAndDetail=function(e,t,n,i,a){var r=this,o=e.getData(),s=o.mapDimension("value"),l=+e.get("min"),p=+e.get("max"),c=new Mc,d=[],u=[],m=e.isAnimationEnabled(),h=e.get(["pointer","showAbove"]);o.diff(this._data).add((function(e){d[e]=new Pm({silent:!0}),u[e]=new Pm({silent:!0})})).update((function(e,t){d[e]=r._titleEls[t],u[e]=r._detailEls[t]})).execute(),o.each((function(t){var n=o.getItemModel(t),r=o.get(s,t),g=new Mc,f=i(Lc(r,[l,p],[0,1],!0)),y=n.getModel("title");if(y.get("show")){var v=y.get("offsetCenter"),x=a.cx+Vc(v[0],a.r),b=a.cy+Vc(v[1],a.r);(M=d[t]).attr({z2:h?0:2,style:jf(y,{x:x,y:b,text:o.getName(t),align:"center",verticalAlign:"middle"},{inheritColor:f})}),g.add(M)}var w=n.getModel("detail");if(w.get("show")){var S=w.get("offsetCenter"),C=a.cx+Vc(S[0],a.r),_=a.cy+Vc(S[1],a.r),T=Vc(w.get("width"),a.r),I=Vc(w.get("height"),a.r),E=e.get(["progress","show"])?o.getItemVisual(t,"style").fill:f,M=u[t],k=w.get("formatter");M.attr({z2:h?0:2,style:jf(w,{x:C,y:_,text:pA(r,k),width:isNaN(T)?null:T,height:isNaN(I)?null:I,align:"center",verticalAlign:"middle"},{inheritColor:E})}),Qf(M,{normal:w},r,(function(e){return pA(e,k)})),m&&Jf(M,t,o,e,{getFormattedLabel:function(e,t,n,i,a,o){return pA(o?o.interpolatedValue:r,k)}}),g.add(M)}c.add(g)})),this.group.add(c),this._titleEls=d,this._detailEls=u},t.type="gauge"})(vw),function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.visualStyleAccessPath="itemStyle",n}ze(t,e),t.prototype.getInitialData=function(e,t){return VM(this,["value"])},t.type="series.gauge",t.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,"#E6EBF8"]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:"#63677A",width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:"#63677A",width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:"#464646",fontSize:12,rotate:0},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:"#fff",borderWidth:0,borderColor:"#5470c6"}},title:{show:!0,offsetCenter:[0,"20%"],color:"#464646",fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:"rgba(0,0,0,0)",borderWidth:0,borderColor:"#ccc",width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:"#464646",fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}}}(ow);var cA=["itemStyle","opacity"],dA=function(e){function t(t,n){var i=e.call(this)||this,a=i,r=new Dg,o=new Pm;return a.setTextContent(o),i.setTextGuideLine(r),i.updateData(t,n,!0),i}return ze(t,e),t.prototype.updateData=function(e,t,n){var i=this,a=e.hostModel,r=e.getItemModel(t),o=e.getItemLayout(t),s=r.getModel("emphasis"),l=r.get(cA);l=null==l?1:l,n||lf(i),i.useStyle(e.getItemVisual(t,"style")),i.style.lineJoin="round",n?(i.setShape({points:o.points}),i.style.opacity=0,nf(i,{style:{opacity:l}},a,t)):tf(i,{style:{opacity:l},shape:{points:o.points}},a,t),Bh(i,r),this._updateLabel(e,t),Oh(this,s.get("focus"),s.get("blurScope"),s.get("disabled"))},t.prototype._updateLabel=function(e,t){var n=this,i=this.getTextGuideLine(),a=n.getTextContent(),r=e.hostModel,o=e.getItemModel(t),s=e.getItemLayout(t).label,l=e.getItemVisual(t,"style"),p=l.fill;zf(a,Uf(o),{labelFetcher:e.hostModel,labelDataIndex:t,defaultOpacity:l.opacity,defaultText:e.getName(t)},{normal:{align:s.textAlign,verticalAlign:s.verticalAlign}}),n.setTextConfig({local:!0,inside:!!s.inside,insideStroke:p,outsideFill:p});var c=s.linePoints;i.setShape({points:c}),n.textGuideLineConfig={anchor:c?new ws(c[0][0],c[0][1]):null},tf(a,{style:{x:s.x,y:s.y}},r,t),a.attr({rotation:s.rotation,originX:s.x,originY:s.y,z2:10}),QI(n,JI(o),{stroke:p})},t}(kg);(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.ignoreLabelLineUpdate=!0,n}ze(t,e),t.prototype.render=function(e,t,n){var i=e.getData(),a=this._data,r=this.group;i.diff(a).add((function(e){var t=new dA(i,e);i.setItemGraphicEl(e,t),r.add(t)})).update((function(e,t){var n=a.getItemGraphicEl(t);n.updateData(i,e),r.add(n),i.setItemGraphicEl(e,n)})).remove((function(t){sf(a.getItemGraphicEl(t),e,t)})).execute(),this._data=i},t.prototype.remove=function(){this.group.removeAll(),this._data=null},t.prototype.dispose=function(){},t.type="funnel"})(vw),function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.init=function(t){e.prototype.init.apply(this,arguments),this.legendVisualProvider=new qM(Gr(this.getData,this),Gr(this.getRawData,this)),this._defaultLabelLine(t)},t.prototype.getInitialData=function(e,t){return VM(this,{coordDimensions:["value"],encodeDefaulter:zr(qv,this)})},t.prototype._defaultLabelLine=function(e){ud(e,"labelLine",["show"]);var t=e.labelLine,n=e.emphasis.labelLine;t.show=t.show&&e.label.show,n.show=n.show&&e.emphasis.label.show},t.prototype.getDataParams=function(t){var n=this.getData(),i=e.prototype.getDataParams.call(this,t),a=n.mapDimension("value"),r=n.getSum(a);return i.percent=r?+(n.get(a,t)/r*100).toFixed(2):0,i.$vars.push("percent"),i},t.type="series.funnel",t.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:60,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}}}(ow);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._dataGroup=new Mc,n._initialized=!1,n}ze(t,e),t.prototype.init=function(){this.group.add(this._dataGroup)},t.prototype.render=function(e,t,n,i){this._progressiveEls=null;var a=this._dataGroup,r=e.getData(),o=this._data,s=e.coordinateSystem,l=s.dimensions,p=hA(e);if(r.diff(o).add((function(e){gA(mA(r,a,e,l,s),r,e,p)})).update((function(t,n){var i=o.getItemGraphicEl(n),a=uA(r,t,l,s);r.setItemGraphicEl(t,i),tf(i,{shape:{points:a}},e,t),lf(i),gA(i,r,t,p)})).remove((function(e){var t=o.getItemGraphicEl(e);a.remove(t)})).execute(),!this._initialized){this._initialized=!0;var c=function(e,t,n){var i=e.model,a=e.getRect(),r=new Em({shape:{x:a.x,y:a.y,width:a.width,height:a.height}}),o="horizontal"===i.get("layout")?"width":"height";return r.setShape(o,0),nf(r,{shape:{width:a.width,height:a.height}},t,n),r}(s,e,(function(){setTimeout((function(){a.removeClipPath()}))}));a.setClipPath(c)}this._data=r},t.prototype.incrementalPrepareRender=function(e,t,n){this._initialized=!0,this._data=null,this._dataGroup.removeAll()},t.prototype.incrementalRender=function(e,t,n){for(var i=t.getData(),a=t.coordinateSystem,r=a.dimensions,o=hA(t),s=this._progressiveEls=[],l=e.start;l5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([e.offsetX,e.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(e){if(!this._mouseDownPoint&&vA(this,"mousemove")){var t=this._model,n=t.coordinateSystem.getSlidedAxisExpandWindow([e.offsetX,e.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(t.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function vA(e,t){var n=e._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===t}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.init=function(){e.prototype.init.apply(this,arguments),this.mergeOption({})},t.prototype.mergeOption=function(e){var t=this.option;e&&Er(t,e,!0),this._initDimensions()},t.prototype.contains=function(e,t){var n=e.get("parallelIndex");return null!=n&&t.getComponent("parallel",n)===this},t.prototype.setAxisExpand=function(e){Rr(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(t){e.hasOwnProperty(t)&&(this.option[t]=e[t])}),this)},t.prototype._initDimensions=function(){var e=this.dimensions=[],t=this.parallelAxisIndex=[];Rr(Lr(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(e){return(e.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){e.push("dim"+n.get("dim")),t.push(n.componentIndex)}))},t.type="parallel",t.dependencies=["parallelAxis"],t.layoutMode="box",t.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null}}(Sv);var xA=function(e){function t(t,n,i,a,r){var o=e.call(this,t,n,i)||this;return o.type=a||"value",o.axisIndex=r,o}return ze(t,e),t.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},t}(MI);function bA(e,t,n,i,a,r){e=e||0;var o=n[1]-n[0];if(null!=a&&(a=SA(a,[0,o])),null!=r&&(r=Math.max(r,null!=a?a:0)),"all"===i){var s=Math.abs(t[1]-t[0]);s=SA(s,[0,o]),a=r=SA(s,[a,r]),i=0}t[0]=SA(t[0],n),t[1]=SA(t[1],n);var l=wA(t,i);t[i]+=e;var p,c=a||0,d=n.slice();return l.sign<0?d[0]+=c:d[1]-=c,t[i]=SA(t[i],d),p=wA(t,i),null!=a&&(p.sign!==l.sign||p.spanr&&(t[1-i]=t[i]+p.sign*r),t}function wA(e,t){var n=e[t]-e[1-t];return{span:Math.abs(n),sign:n>0?-1:n<0?1:t?-1:1}}function SA(e,t){return Math.min(null!=t[1]?t[1]:1/0,Math.max(null!=t[0]?t[0]:-1/0,e))}var CA=Rr,_A=Math.min,TA=Math.max,IA=Math.floor,EA=Math.ceil,MA=qc,kA=Math.PI;!function(){function e(e,t,n){this.type="parallel",this._axesMap=fo(),this._axesLayout={},this.dimensions=e.dimensions,this._model=e,this._init(e,t,n)}e.prototype._init=function(e,t,n){var i=e.dimensions,a=e.parallelAxisIndex;CA(i,(function(e,n){var i=a[n],r=t.getComponent("parallelAxis",i),o=this._axesMap.set(e,new xA(e,KT(r),[0,0],r.get("type"),i)),s="category"===o.type;o.onBand=s&&r.get("boundaryGap"),o.inverse=r.get("inverse"),r.axis=o,o.model=r,o.coordinateSystem=r.coordinateSystem=this}),this)},e.prototype.update=function(e,t){this._updateAxesFromSeries(this._model,e)},e.prototype.containPoint=function(e){var t=this._makeLayoutInfo(),n=t.axisBase,i=t.layoutBase,a=t.pixelDimIndex,r=e[1-a],o=e[a];return r>=n&&r<=n+t.axisLength&&o>=i&&o<=i+t.layoutLength},e.prototype.getModel=function(){return this._model},e.prototype._updateAxesFromSeries=function(e,t){t.eachSeries((function(n){if(e.contains(n,t)){var i=n.getData();CA(this.dimensions,(function(e){var t=this._axesMap.get(e);t.scale.unionExtentFromData(i,i.mapDimension(e)),$T(t.scale,t.model)}),this)}}),this)},e.prototype.resize=function(e,t){this._rect=gv(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()}),this._layoutAxes()},e.prototype.getRect=function(){return this._rect},e.prototype._makeLayoutInfo=function(){var e,t=this._model,n=this._rect,i=["x","y"],a=["width","height"],r=t.get("layout"),o="horizontal"===r?0:1,s=n[a[o]],l=[0,s],p=this.dimensions.length,c=PA(t.get("axisExpandWidth"),l),d=PA(t.get("axisExpandCount")||0,[0,p]),u=t.get("axisExpandable")&&p>3&&p>d&&d>1&&c>0&&s>0,m=t.get("axisExpandWindow");m?(e=PA(m[1]-m[0],l),m[1]=m[0]+e):(e=PA(c*(d-1),l),(m=[c*(t.get("axisExpandCenter")||IA(p/2))-e/2])[1]=m[0]+e);var h=(s-e)/(p-d);h<3&&(h=0);var g=[IA(MA(m[0]/c,1))+1,EA(MA(m[1]/c,1))-1],f=h/c*m[0];return{layout:r,pixelDimIndex:o,layoutBase:n[i[o]],layoutLength:s,axisBase:n[i[1-o]],axisLength:n[a[1-o]],axisExpandable:u,axisExpandWidth:c,axisCollapseWidth:h,axisExpandWindow:m,axisCount:p,winInnerIndices:g,axisExpandWindow0Pos:f}},e.prototype._layoutAxes=function(){var e=this._rect,t=this._axesMap,n=this.dimensions,i=this._makeLayoutInfo(),a=i.layout;t.each((function(e){var t=[0,i.axisLength],n=e.inverse?1:0;e.setExtent(t[n],t[1-n])})),CA(n,(function(t,n){var r=(i.axisExpandable?OA:DA)(n,i),o={horizontal:{x:r.position,y:i.axisLength},vertical:{x:0,y:r.position}},s={horizontal:kA/2,vertical:0},l=[o[a].x+e.x,o[a].y+e.y],p=s[a],c=[1,0,0,1,0,0];ys(c,c,p),fs(c,c,l),this._axesLayout[t]={position:l,rotation:p,transform:c,axisNameAvailableWidth:r.axisNameAvailableWidth,axisLabelShow:r.axisLabelShow,nameTruncateMaxWidth:r.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}}),this)},e.prototype.getAxis=function(e){return this._axesMap.get(e)},e.prototype.dataToPoint=function(e,t){return this.axisCoordToPoint(this._axesMap.get(t).dataToCoord(e),t)},e.prototype.eachActiveState=function(e,t,n,i){null==n&&(n=0),null==i&&(i=e.count());var a=this._axesMap,r=this.dimensions,o=[],s=[];Rr(r,(function(t){o.push(e.mapDimension(t)),s.push(a.get(t).model)}));for(var l=this.hasAxisBrushed(),p=n;pa*(1-c[0])?(l="jump",o=s-a*(1-c[2])):(o=s-a*c[1])>=0&&(o=s-a*(1-c[1]))<=0&&(o=0),(o*=t.axisExpandWidth/p)?bA(o,i,r,"all"):l="none";else{var u=i[1]-i[0];(i=[TA(0,r[1]*s/u-u/2)])[1]=_A(r[1],i[0]+u),i[0]=i[1]-u}return{axisExpandWindow:i,behavior:l}}}();function PA(e,t){return _A(TA(e,t[0]),t[1])}function DA(e,t){var n=t.layoutLength/(t.axisCount-1);return{position:n*e,axisNameAvailableWidth:n,axisLabelShow:!0}}function OA(e,t){var n,i,a=t.layoutLength,r=t.axisExpandWidth,o=t.axisCount,s=t.axisCollapseWidth,l=t.winInnerIndices,p=s,c=!1;return e=0;n--)Gc(t[n])},t.prototype.getActiveState=function(e){var t=this.activeIntervals;if(!t.length)return"normal";if(null==e||isNaN(+e))return"inactive";if(1===t.length){var n=t[0];if(n[0]<=e&&e<=n[1])return"active"}else for(var i=0,a=t.length;i6}(e)||r){if(o&&!r){"single"===s.brushMode&&QA(e);var l=Ir(s);l.brushType=gF(l.brushType,o),l.panelId=o===FA?null:o.panelId,r=e._creatingCover=jA(e,l),e._covers.push(r)}if(r){var p=vF[gF(e._brushType,o)];r.__brushOption.range=p.getCreatingRange(dF(e,r,e._track)),i&&(HA(e,r),p.updateCommon(e,r)),WA(e,r),a={isEnd:i}}}else i&&"single"===s.brushMode&&s.removeOnClick&&XA(e,t,n)&&QA(e)&&(a={isEnd:i,removeOnClick:!0});return a}function gF(e,t){return"auto"===e?t.defaultBrushType:e}var fF={mousedown:function(e){if(this._dragging)yF(this,e);else if(!e.target||!e.target.draggable){uF(e);var t=this.group.transformCoordToLocal(e.offsetX,e.offsetY);this._creatingCover=null,(this._creatingPanel=XA(this,e,t))&&(this._dragging=!0,this._track=[t.slice()])}},mousemove:function(e){var t=e.offsetX,n=e.offsetY,i=this.group.transformCoordToLocal(t,n);if(function(e,t,n){if(e._brushType&&!function(e,t,n){var i=e._zr;return t<0||t>i.getWidth()||n<0||n>i.getHeight()}(e,t.offsetX,t.offsetY)){var i=e._zr,a=e._covers,r=XA(e,t,n);if(!e._dragging)for(var o=0;o=0&&(r[a[o].depth]=new uy(a[o],this,t));if(i&&n){var s=oA(i,n,this,!0,(function(e,t){e.wrapMethod("getItemModel",(function(e,t){var n=e.parentModel,i=n.getData().getItemLayout(t);if(i){var a=i.depth,r=n.levelModels[a];r&&(e.parentModel=r)}return e})),t.wrapMethod("getItemModel",(function(e,t){var n=e.parentModel,i=n.getGraph().getEdgeByIndex(t).node1.getLayout();if(i){var a=i.depth,r=n.levelModels[a];r&&(e.parentModel=r)}return e}))}));return s.data}},t.prototype.setNodePosition=function(e,t){var n=(this.option.data||this.option.nodes)[e];n.localX=t[0],n.localY=t[1]},t.prototype.getGraph=function(){return this.getData().graph},t.prototype.getEdgeData=function(){return this.getGraph().edgeData},t.prototype.formatTooltip=function(e,t,n){function i(e){return isNaN(e)||null==e}if("edge"===n){var a=this.getDataParams(e,n),r=a.data,o=a.value;return jb("nameValue",{name:r.source+" -- "+r.target,value:o,noValue:i(o)})}var s=this.getGraph().getNodeByIndex(e).getLayout().value,l=this.getDataParams(e,n).data.name;return jb("nameValue",{name:null!=l?l+"":null,value:s,noValue:i(s)})},t.prototype.optionUpdated=function(){},t.prototype.getDataParams=function(t,n){var i=e.prototype.getDataParams.call(this,t,n);if(null==i.value&&"node"===n){var a=this.getGraph().getNodeByIndex(t).getLayout().value;i.value=a}return i},t.type="series.sankey",t.defaultOption={z:2,coordinateSystem:"view",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,label:{show:!0,position:"right",fontSize:12},edgeLabel:{show:!1,fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:"#314656",opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:"#212121"}},animationEasing:"linear",animationDuration:1e3}}(ow);var MF=function(){function e(){}return e.prototype.getInitialData=function(e,t){var n,i,a=t.getComponent("xAxis",this.get("xAxisIndex")),r=t.getComponent("yAxis",this.get("yAxisIndex")),o=a.get("type"),s=r.get("type");"category"===o?(e.layout="horizontal",n=a.getOrdinalMeta(),i=!0):"category"===s?(e.layout="vertical",n=r.getOrdinalMeta(),i=!0):e.layout=e.layout||"horizontal";var l=["x","y"],p="horizontal"===e.layout?0:1,c=this._baseAxisDim=l[p],d=l[1-p],u=[a,r],m=u[p].get("type"),h=u[1-p].get("type"),g=e.data;if(g&&i){var f=[];Rr(g,(function(e,t){var n;Ur(e)?(n=e.slice(),e.unshift(t)):Ur(e.value)?((n=kr({},e)).value=n.value.slice(),e.value.unshift(t)):n=e,f.push(n)})),e.data=f}var y=this.defaultValueDimensions,v=[{name:c,type:v_(m),ordinalMeta:n,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:d,type:v_(h),dimsDef:y.slice()}];return VM(this,{coordDimensions:v,dimensionsCount:y.length+1,encodeDefaulter:zr(Vv,v,this)})},e.prototype.getBaseAxis=function(){var e=this._baseAxisDim;return this.ecModel.getComponent(e+"Axis",this.get(e+"AxisIndex")).axis},e}(),kF=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.defaultValueDimensions=[{name:"min",defaultTooltip:!0},{name:"Q1",defaultTooltip:!0},{name:"median",defaultTooltip:!0},{name:"Q3",defaultTooltip:!0},{name:"max",defaultTooltip:!0}],n.visualDrawType="stroke",n}return ze(t,e),t.type="series.boxplot",t.dependencies=["xAxis","yAxis","grid"],t.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,boxWidth:[7,50],itemStyle:{color:"#fff",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2,shadowBlur:5,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0,0,0,0.2)"}},animationDuration:800},t}(ow);Ar(kF,MF,!0);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.render=function(e,t,n){var i=e.getData(),a=this.group,r=this._data;this._data||a.removeAll();var o="horizontal"===e.get("layout")?1:0;i.diff(r).add((function(e){if(i.hasValue(e)){var t=OF(i.getItemLayout(e),i,e,o,!0);i.setItemGraphicEl(e,t),a.add(t)}})).update((function(e,t){var n=r.getItemGraphicEl(t);if(i.hasValue(e)){var s=i.getItemLayout(e);n?(lf(n),AF(s,n,i,e)):n=OF(s,i,e,o),a.add(n),i.setItemGraphicEl(e,n)}else a.remove(n)})).remove((function(e){var t=r.getItemGraphicEl(e);t&&a.remove(t)})).execute(),this._data=i},t.prototype.remove=function(e){var t=this.group,n=this._data;this._data=null,n&&n.eachItemGraphicEl((function(e){e&&t.remove(e)}))},t.type="boxplot"}(vw);var PF=function(){},DF=function(e){function t(t){var n=e.call(this,t)||this;return n.type="boxplotBoxPath",n}return ze(t,e),t.prototype.getDefaultShape=function(){return new PF},t.prototype.buildPath=function(e,t){var n=t.points,i=0;for(e.moveTo(n[i][0],n[i][1]),i++;i<4;i++)e.lineTo(n[i][0],n[i][1]);for(e.closePath();i0?"borderColor":"borderColor0"])||n.get(["itemStyle",e>0?"color":"color0"]);0===e&&(a=n.get(["itemStyle","borderColorDoji"]));var r=n.getModel("itemStyle").getItemStyle(RF);t.useStyle(r),t.style.fill=null,t.style.stroke=a}var WF=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],n}return ze(t,e),t.prototype.getShadowDim=function(){return"open"},t.prototype.brushSelector=function(e,t,n){var i=t.getItemLayout(e);return i&&n.rect(i.brushRect)},t.type="series.candlestick",t.dependencies=["xAxis","yAxis","grid"],t.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderColorDoji:null,borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},t}(ow);Ar(WF,MF,!0);var $F=["itemStyle","borderColor"],KF=["itemStyle","borderColor0"],YF=["itemStyle","borderColorDoji"],XF=["itemStyle","color"],ZF=["itemStyle","color0"];gw(),gw();function QF(e,t,n,i,a,r){return n>i?-1:n0?e.get(a,t-1)<=i?1:-1:1}function JF(e,t){var n=t.rippleEffectColor||t.color;e.eachChild((function(e){e.attr({z:t.z,zlevel:t.zlevel,style:{stroke:"stroke"===t.brushType?n:null,fill:"fill"===t.brushType?n:null}})}))}var eR=function(e){function t(t,n){var i=e.call(this)||this,a=new vE(t,n),r=new Mc;return i.add(a),i.add(r),i.updateData(t,n),i}return ze(t,e),t.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},t.prototype.startEffectAnimation=function(e){for(var t=e.symbolType,n=e.color,i=e.rippleNumber,a=this.childAt(1),r=0;r0&&(r=this._getLineLength(i)/l*1e3),r!==this._period||o!==this._loop||s!==this._roundTrip){i.stopAnimation();var c=void 0;c=jr(p)?p(n):p,i.__t>0&&(c=-r*i.__t),this._animateSymbol(i,r,c,o,s)}this._period=r,this._loop=o,this._roundTrip=s}},t.prototype._animateSymbol=function(e,t,n,i,a){if(t>0){e.__t=0;var r=this,o=e.animate("",i).when(a?2*t:t,{__t:a?2:1}).delay(n).during((function(){r._updateSymbolPosition(e)}));i||o.done((function(){r.remove(e)})),o.start()}},t.prototype._getLineLength=function(e){return Fo(e.__p1,e.__cp1)+Fo(e.__cp1,e.__p2)},t.prototype._updateAnimationPoints=function(e,t){e.__p1=t[0],e.__p2=t[1],e.__cp1=t[2]||[(t[0][0]+t[1][0])/2,(t[0][1]+t[1][1])/2]},t.prototype.updateData=function(e,t,n){this.childAt(0).updateData(e,t,n),this._updateEffectSymbol(e,t)},t.prototype._updateSymbolPosition=function(e){var t=e.__p1,n=e.__p2,i=e.__cp1,a=e.__t<1?e.__t:2-e.__t,r=[e.x,e.y],o=r.slice(),s=yl,l=vl;r[0]=s(t[0],i[0],n[0],a),r[1]=s(t[1],i[1],n[1],a);var p=e.__t<1?l(t[0],i[0],n[0],a):l(n[0],i[0],t[0],1-a),c=e.__t<1?l(t[1],i[1],n[1],a):l(n[1],i[1],t[1],1-a);e.rotation=-Math.atan2(c,p)-Math.PI/2,"line"!==this._symbolType&&"rect"!==this._symbolType&&"roundRect"!==this._symbolType||(void 0!==e.__lastT&&e.__lastT=0&&!(i[r]<=t);r--);r=Math.min(r,a-2)}else{for(r=o;rt);r++);r=Math.min(r-1,a-2)}var s=(t-i[r])/(i[r+1]-i[r]),l=n[r],p=n[r+1];e.x=l[0]*(1-s)+s*p[0],e.y=l[1]*(1-s)+s*p[1];var c=e.__t<1?p[0]-l[0]:l[0]-p[0],d=e.__t<1?p[1]-l[1]:l[1]-p[1];e.rotation=-Math.atan2(d,c)-Math.PI/2,this._lastFrame=r,this._lastFramePercent=t,e.ignore=!1}},t}(tR),aR=function(){this.polyline=!1,this.curveness=0,this.segs=[]},rR=function(e){function t(t){var n=e.call(this,t)||this;return n._off=0,n.hoverDataIdx=-1,n}return ze(t,e),t.prototype.reset=function(){this.notClear=!1,this._off=0},t.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},t.prototype.getDefaultShape=function(){return new aR},t.prototype.buildPath=function(e,t){var n,i=t.segs,a=t.curveness;if(t.polyline)for(n=this._off;n0){e.moveTo(i[n++],i[n++]);for(var o=1;o0){var d=(s+p)/2-(l-c)*a,u=(l+c)/2-(p-s)*a;e.quadraticCurveTo(d,u,p,c)}else e.lineTo(p,c)}this.incremental&&(this._off=n,this.notClear=!0)},t.prototype.findDataIndex=function(e,t){var n=this.shape,i=n.segs,a=n.curveness,r=this.style.lineWidth;if(n.polyline)for(var o=0,s=0;s0)for(var p=i[s++],c=i[s++],d=1;d0){if(Qu(p,c,(p+u)/2-(c-m)*a,(c+m)/2-(u-p)*a,u,m,r,e,t))return o}else if(Xu(p,c,u,m,r,e,t))return o;o++}return-1},t.prototype.contain=function(e,t){var n=this.transformCoordToLocal(e,t),i=this.getBoundingRect();return e=n[0],t=n[1],i.contain(e,t)?(this.hoverDataIdx=this.findDataIndex(e,t))>=0:(this.hoverDataIdx=-1,!1)},t.prototype.getBoundingRect=function(){var e=this._rect;if(!e){for(var t=this.shape.segs,n=1/0,i=1/0,a=-1/0,r=-1/0,o=0;o0&&(r.dataIndex=n+e.__startIndex)}))},e.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},e}(),sR={seriesType:"lines",plan:gw(),reset:function(e){var t=e.coordinateSystem;if(t){var n=e.get("polyline"),i=e.pipelineContext.large;return{progress:function(a,r){var o=[];if(i){var s=void 0,l=a.end-a.start;if(n){for(var p=0,c=a.start;c0&&(l||s.configLayer(r,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(o/10+.9,1),0)})),a.updateData(i);var p=e.get("clip",!0)&&BE(e.coordinateSystem,!1,e);p?this.group.setClipPath(p):this.group.removeClipPath(),this._lastZlevel=r,this._finished=!0},t.prototype.incrementalPrepareRender=function(e,t,n){var i=e.getData();this._updateLineDraw(i,e).incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},t.prototype.incrementalRender=function(e,t,n){this._lineDraw.incrementalUpdate(e,t.getData()),this._finished=e.end===t.getData().count()},t.prototype.eachRendered=function(e){this._lineDraw&&this._lineDraw.eachRendered(e)},t.prototype.updateTransform=function(e,t,n){var i=e.getData(),a=e.pipelineContext;if(!this._finished||a.large||a.progressiveRender)return{update:!0};var r=sR.reset(e,t,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._lineDraw.updateLayout(),this._clearLayer(n)},t.prototype._updateLineDraw=function(e,t){var n=this._lineDraw,i=this._showEffect(t),a=!!t.get("polyline"),r=t.pipelineContext.large;return n&&i===this._hasEffet&&a===this._isPolyline&&r===this._isLargeDraw||(n&&n.remove(),n=this._lineDraw=r?new oR:new zO(a?i?iR:nR:i?tR:GO),this._hasEffet=i,this._isPolyline=a,this._isLargeDraw=r),this.group.add(n.group),n},t.prototype._showEffect=function(e){return!!e.get(["effect","show"])},t.prototype._clearLayer=function(e){var t=e.getZr();"svg"===t.painter.getType()||null==this._lastZlevel||t.painter.getLayer(this._lastZlevel).clear(!0)},t.prototype.remove=function(e,t){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(t)},t.prototype.dispose=function(e,t){this.remove(e,t)},t.type="lines"}(vw),"undefined"==typeof Uint32Array?Array:Uint32Array),pR="undefined"==typeof Float64Array?Array:Float64Array;function cR(e){var t=e.data;t&&t[0]&&t[0][0]&&t[0][0].coord&&(e.data=Br(t,(function(e){var t={coords:[e[0].coord,e[1].coord]};return e[0].name&&(t.fromName=e[0].name),e[1].name&&(t.toName=e[1].name),Mr([t,e[0],e[1]])})))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.visualStyleAccessPath="lineStyle",n.visualDrawType="stroke",n}ze(t,e),t.prototype.init=function(t){t.data=t.data||[],cR(t);var n=this._processFlatCoordsArray(t.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(t.data=new Float32Array(n.count)),e.prototype.init.apply(this,arguments)},t.prototype.mergeOption=function(t){if(cR(t),t.data){var n=this._processFlatCoordsArray(t.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(t.data=new Float32Array(n.count))}e.prototype.mergeOption.apply(this,arguments)},t.prototype.appendData=function(e){var t=this._processFlatCoordsArray(e.data);t.flatCoords&&(this._flatCoords?(this._flatCoords=yo(this._flatCoords,t.flatCoords),this._flatCoordsOffset=yo(this._flatCoordsOffset,t.flatCoordsOffset)):(this._flatCoords=t.flatCoords,this._flatCoordsOffset=t.flatCoordsOffset),e.data=new Float32Array(t.count)),this.getRawData().appendData(e.data)},t.prototype._getCoordsFromItemModel=function(e){var t=this.getData().getItemModel(e),n=t.option instanceof Array?t.option:t.getShallow("coords");return n},t.prototype.getLineCoordsCount=function(e){return this._flatCoordsOffset?this._flatCoordsOffset[2*e+1]:this._getCoordsFromItemModel(e).length},t.prototype.getLineCoords=function(e,t){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[2*e],i=this._flatCoordsOffset[2*e+1],a=0;a ")})},t.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},t.prototype.getProgressive=function(){var e=this.option.progressive;return null==e?this.option.large?1e4:this.get("progressive"):e},t.prototype.getProgressiveThreshold=function(){var e=this.option.progressiveThreshold;return null==e?this.option.large?2e4:this.get("progressiveThreshold"):e},t.prototype.getZLevelKey=function(){var e=this.getModel("effect"),t=e.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:e.get("show")&&t>0?t+"":""},t.type="series.lines",t.dependencies=["grid","polar","geo","calendar"],t.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}}}(ow);var dR=function(){function e(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var e=dr.createCanvas();this.canvas=e}return e.prototype.update=function(e,t,n,i,a,r){var o=this._getBrush(),s=this._getGradient(a,"inRange"),l=this._getGradient(a,"outOfRange"),p=this.pointSize+this.blurSize,c=this.canvas,d=c.getContext("2d"),u=e.length;c.width=t,c.height=n;for(var m=0;m0){var T=r(y)?s:l;y>0&&(y=y*C+S),x[b++]=T[_],x[b++]=T[_+1],x[b++]=T[_+2],x[b++]=T[_+3]*y*256}else b+=4}return d.putImageData(v,0,0),c},e.prototype._getBrush=function(){var e=this._brushCanvas||(this._brushCanvas=dr.createCanvas()),t=this.pointSize+this.blurSize,n=2*t;e.width=n,e.height=n;var i=e.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor="#000",i.beginPath(),i.arc(-t,t,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),e},e.prototype._getGradient=function(e,t){for(var n=this._gradientPixels,i=n[t]||(n[t]=new Uint8ClampedArray(1024)),a=[0,0,0,0],r=0,o=0;o<256;o++)e[t](o/255,!0,a),i[r++]=a[0],i[r++]=a[1],i[r++]=a[2],i[r++]=a[3];return i},e}();function uR(e){var t=e.dimensions;return"lng"===t[0]&&"lat"===t[1]}(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.render=function(e,t,n){var i;t.eachComponent("visualMap",(function(t){t.eachTargetSeries((function(n){n===e&&(i=t)}))})),this._progressiveEls=null,this.group.removeAll();var a=e.coordinateSystem;"cartesian2d"===a.type||"calendar"===a.type?this._renderOnCartesianAndCalendar(e,n,0,e.getData().count()):uR(a)&&this._renderOnGeo(a,e,i,n)},t.prototype.incrementalPrepareRender=function(e,t,n){this.group.removeAll()},t.prototype.incrementalRender=function(e,t,n,i){var a=t.coordinateSystem;a&&(uR(a)?this.render(t,n,i):(this._progressiveEls=[],this._renderOnCartesianAndCalendar(t,i,e.start,e.end,!0)))},t.prototype.eachRendered=function(e){Nf(this._progressiveEls||this.group,e)},t.prototype._renderOnCartesianAndCalendar=function(e,t,n,i,a){var r,o,s,l,p=e.coordinateSystem,c=NE(p,"cartesian2d");if(c){var d=p.getAxis("x"),u=p.getAxis("y");0,r=d.getBandWidth()+.5,o=u.getBandWidth()+.5,s=d.scale.getExtent(),l=u.scale.getExtent()}for(var m=this.group,h=e.getData(),g=e.getModel(["emphasis","itemStyle"]).getItemStyle(),f=e.getModel(["blur","itemStyle"]).getItemStyle(),y=e.getModel(["select","itemStyle"]).getItemStyle(),v=e.get(["itemStyle","borderRadius"]),x=Uf(e),b=e.getModel("emphasis"),w=b.get("focus"),S=b.get("blurScope"),C=b.get("disabled"),_=c?[h.mapDimension("x"),h.mapDimension("y"),h.mapDimension("value")]:[h.mapDimension("time"),h.mapDimension("value")],T=n;Ts[1]||kl[1])continue;var P=p.dataToPoint([M,k]);I=new Em({shape:{x:P[0]-r/2,y:P[1]-o/2,width:r,height:o},style:E})}else{if(isNaN(h.get(_[1],T)))continue;I=new Em({z2:1,shape:p.dataToRect([h.get(_[0],T)]).contentShape,style:E})}if(h.hasItemOption){var D=h.getItemModel(T),O=D.getModel("emphasis");g=O.getModel("itemStyle").getItemStyle(),f=D.getModel(["blur","itemStyle"]).getItemStyle(),y=D.getModel(["select","itemStyle"]).getItemStyle(),v=D.get(["itemStyle","borderRadius"]),w=O.get("focus"),S=O.get("blurScope"),C=O.get("disabled"),x=Uf(D)}I.shape.r=v;var A=e.getRawValue(T),F="-";A&&null!=A[2]&&(F=A[2]+""),zf(I,x,{labelFetcher:e,labelDataIndex:T,defaultOpacity:E.opacity,defaultText:F}),I.ensureState("emphasis").style=g,I.ensureState("blur").style=f,I.ensureState("select").style=y,Oh(I,w,S,C),I.incremental=a,a&&(I.states.emphasis.hoverLayer=!0),m.add(I),h.setItemGraphicEl(T,I),this._progressiveEls&&this._progressiveEls.push(I)}},t.prototype._renderOnGeo=function(e,t,n,i){var a=n.targetVisuals.inRange,r=n.targetVisuals.outOfRange,o=t.getData(),s=this._hmLayer||this._hmLayer||new dR;s.blurSize=t.get("blurSize"),s.pointSize=t.get("pointSize"),s.minOpacity=t.get("minOpacity"),s.maxOpacity=t.get("maxOpacity");var l=e.getViewRect().clone(),p=e.getRoamTransform();l.applyTransform(p);var c=Math.max(l.x,0),d=Math.max(l.y,0),u=Math.min(l.width+l.x,i.getWidth()),m=Math.min(l.height+l.y,i.getHeight()),h=u-c,g=m-d,f=[o.mapDimension("lng"),o.mapDimension("lat"),o.mapDimension("value")],y=o.mapArray(f,(function(t,n,i){var a=e.dataToPoint([t,n]);return a[0]-=c,a[1]-=d,a.push(i),a})),v=n.getExtent(),x="visualMap.continuous"===n.type?function(e,t){var n=e[1]-e[0];return t=[(t[0]-e[0])/n,(t[1]-e[0])/n],function(e){return e>=t[0]&&e<=t[1]}}(v,n.option.range):function(e,t,n){var i=e[1]-e[0],a=(t=Br(t,(function(t){return{interval:[(t.interval[0]-e[0])/i,(t.interval[1]-e[0])/i]}}))).length,r=0;return function(e){var i;for(i=r;i=0;i--){var o;if((o=t[i].interval)[0]<=e&&e<=o[1]){r=i;break}}return i>=0&&i0?1:-1}(n,r,a,i,d),function(e,t,n,i,a,r,o,s,l,p){var c,d=l.valueDim,u=l.categoryDim,m=Math.abs(n[u.wh]),h=e.getItemVisual(t,"symbolSize");c=Ur(h)?h.slice():null==h?["100%","100%"]:[h,h];c[u.index]=Vc(c[u.index],m),c[d.index]=Vc(c[d.index],i?m:Math.abs(r)),p.symbolSize=c;var g=p.symbolScale=[c[0]/s,c[1]/s];g[d.index]*=(l.isHorizontal?-1:1)*o}(e,t,a,r,0,d.boundingLength,d.pxSign,p,i,d),function(e,t,n,i,a){var r=e.get(mR)||0;r&&(gR.attr({scaleX:t[0],scaleY:t[1],rotation:n}),gR.updateTransform(),r/=gR.getLineScale(),r*=t[i.valueDim.index]);a.valueLineWidth=r||0}(n,d.symbolScale,l,i,d);var u=d.symbolSize,m=OS(n.get("symbolOffset"),u);return function(e,t,n,i,a,r,o,s,l,p,c,d){var u=c.categoryDim,m=c.valueDim,h=d.pxSign,g=Math.max(t[m.index]+s,0),f=g;if(i){var y=Math.abs(l),v=no(e.get("symbolMargin"),"15%")+"",x=!1;v.lastIndexOf("!")===v.length-1&&(x=!0,v=v.slice(0,v.length-1));var b=Vc(v,t[m.index]),w=Math.max(g+2*b,0),S=x?0:2*b,C=nd(i),_=C?i:OR((y+S)/w);w=g+2*(b=(y-_*g)/2/(x?_:Math.max(_-1,1))),S=x?0:2*b,C||"fixed"===i||(_=p?OR((Math.abs(p)+S)/w):0),f=_*w-S,d.repeatTimes=_,d.symbolMargin=b}var T=h*(f/2),I=d.pathPosition=[];I[u.index]=n[u.wh]/2,I[m.index]="start"===o?T:"end"===o?l-T:l/2,r&&(I[0]+=r[0],I[1]+=r[1]);var E=d.bundlePosition=[];E[u.index]=n[u.xy],E[m.index]=n[m.xy];var M=d.barRectShape=kr({},n);M[m.wh]=h*Math.max(Math.abs(n[m.wh]),Math.abs(I[m.index]+T)),M[u.wh]=n[u.wh];var k=d.clipShape={};k[u.xy]=-n[u.xy],k[u.wh]=c.ecSize[u.wh],k[m.xy]=0,k[m.wh]=n[m.wh]}(n,u,a,r,0,m,s,d.valueLineWidth,d.boundingLength,d.repeatCutLength,i,d),d}function yR(e,t){return e.toGlobalCoord(e.dataToCoord(e.scale.parse(t)))}function vR(e){var t=e.symbolPatternSize,n=PS(e.symbolType,-t/2,-t/2,t,t);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function xR(e,t,n,i){var a=e.__pictorialBundle,r=n.symbolSize,o=n.valueLineWidth,s=n.pathPosition,l=t.valueDim,p=n.repeatTimes||0,c=0,d=r[t.valueDim.index]+o+2*n.symbolMargin;for(kR(e,(function(e){e.__pictorialAnimationIndex=c,e.__pictorialRepeatTimes=p,c0:i<0)&&(a=p-1-e),t[l.index]=d*(a-p/2+.5)+s[l.index],{x:t[0],y:t[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation}}}function bR(e,t,n,i){var a=e.__pictorialBundle,r=e.__pictorialMainPath;r?PR(r,null,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation},n,i):(r=e.__pictorialMainPath=vR(n),a.add(r),PR(r,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:0,scaleY:0,rotation:n.rotation},{scaleX:n.symbolScale[0],scaleY:n.symbolScale[1]},n,i))}function wR(e,t,n){var i=kr({},t.barRectShape),a=e.__pictorialBarRect;a?PR(a,null,{shape:i},t,n):((a=e.__pictorialBarRect=new Em({z2:2,shape:i,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}})).disableMorphing=!0,e.add(a))}function SR(e,t,n,i){if(n.symbolClip){var a=e.__pictorialClipPath,r=kr({},n.clipShape),o=t.valueDim,s=n.animationModel,l=n.dataIndex;if(a)tf(a,{shape:r},s,l);else{r[o.wh]=0,a=new Em({shape:r}),e.__pictorialBundle.setClipPath(a),e.__pictorialClipPath=a;var p={};p[o.wh]=n.clipShape[o.wh],Lf[i?"updateProps":"initProps"](a,{shape:p},s,l)}}}function CR(e,t){var n=e.getItemModel(t);return n.getAnimationDelayParams=_R,n.isAnimationEnabled=TR,n}function _R(e){return{index:e.__pictorialAnimationIndex,count:e.__pictorialRepeatTimes}}function TR(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function IR(e,t,n,i){var a=new Mc,r=new Mc;return a.add(r),a.__pictorialBundle=r,r.x=n.bundlePosition[0],r.y=n.bundlePosition[1],n.symbolRepeat?xR(a,t,n):bR(a,0,n),wR(a,n,i),SR(a,t,n,i),a.__pictorialShapeStr=MR(e,n),a.__pictorialSymbolMeta=n,a}function ER(e,t,n,i){var a=i.__pictorialBarRect;a&&a.removeTextContent();var r=[];kR(i,(function(e){r.push(e)})),i.__pictorialMainPath&&r.push(i.__pictorialMainPath),i.__pictorialClipPath&&(n=null),Rr(r,(function(e){rf(e,{scaleX:0,scaleY:0},n,t,(function(){i.parent&&i.parent.remove(i)}))})),e.setItemGraphicEl(t,null)}function MR(e,t){return[e.getItemVisual(t.dataIndex,"symbol")||"none",!!t.symbolRepeat,!!t.symbolClip].join(":")}function kR(e,t,n){Rr(e.__pictorialBundle.children(),(function(i){i!==e.__pictorialBarRect&&t.call(n,i)}))}function PR(e,t,n,i,a,r){t&&e.attr(t),i.symbolClip&&!a?n&&e.attr(n):n&&Lf[a?"updateProps":"initProps"](e,n,i.animationModel,i.dataIndex,r)}function DR(e,t,n){var i=n.dataIndex,a=n.itemModel,r=a.getModel("emphasis"),o=r.getModel("itemStyle").getItemStyle(),s=a.getModel(["blur","itemStyle"]).getItemStyle(),l=a.getModel(["select","itemStyle"]).getItemStyle(),p=a.getShallow("cursor"),c=r.get("focus"),d=r.get("blurScope"),u=r.get("scale");kR(e,(function(e){if(e instanceof bm){var t=e.style;e.useStyle(kr({image:t.image,x:t.x,y:t.y,width:t.width,height:t.height},n.style))}else e.useStyle(n.style);var i=e.ensureState("emphasis");i.style=o,u&&(i.scaleX=1.1*e.scaleX,i.scaleY=1.1*e.scaleY),e.ensureState("blur").style=s,e.ensureState("select").style=l,p&&(e.cursor=p),e.z2=n.z2}));var m=t.valueDim.posDesc[+(n.boundingLength>0)],h=e.__pictorialBarRect;h.ignoreClip=!0,zf(h,Uf(a),{labelFetcher:t.seriesModel,labelDataIndex:i,defaultText:fE(t.seriesModel.getData(),i),inheritColor:n.style.fill,defaultOpacity:n.style.opacity,defaultOutsidePosition:m}),Oh(e,c,d,r.get("disabled"))}function OR(e){var t=Math.round(e);return Math.abs(e-t)<1e-4?t:Math.ceil(e)}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.hasSymbolVisual=!0,n.defaultSymbol="roundRect",n}ze(t,e),t.prototype.getInitialData=function(t){return t.stack=null,e.prototype.getInitialData.apply(this,arguments)},t.type="series.pictorialBar",t.dependencies=["grid"],t.defaultOption=gy(eM.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",clip:!1,progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:"#212121"}}})}(eM);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._layers=[],n}ze(t,e),t.prototype.render=function(e,t,n){var i=e.getData(),a=this,r=this.group,o=e.getLayerSeries(),s=i.getLayout("layoutInfo"),l=s.rect,p=s.boundaryGap;function c(e){return e.name}r.x=0,r.y=l.y+p[0];var d=new g_(this._layersSeries||[],o,c,c),u=[];function m(t,n,s){var l=a._layers;if("remove"!==t){for(var p,c,d=[],m=[],h=o[n].indices,g=0;gT&&!Kc(E-T)&&E0?(a.virtualPiece?a.virtualPiece.updateData(!1,i,e,t,n):(a.virtualPiece=new AR(i,e,t,n),l.add(a.virtualPiece)),r.piece.off("click"),a.virtualPiece.on("click",(function(e){a._rootToNode(r.parentNode)}))):a.virtualPiece&&(l.remove(a.virtualPiece),a.virtualPiece=null)}(o,s),this._initEvents(),this._oldChildren=c},t.prototype._initEvents=function(){var e=this;this.group.off("click"),this.group.on("click",(function(t){var n=!1;e.seriesModel.getViewRoot().eachNode((function(i){if(!n&&i.piece&&i.piece===t.target){var a=i.getModel().get("nodeClick");if("rootToNode"===a)e._rootToNode(i);else if("link"===a){var r=i.getModel(),o=r.get("link");if(o)pv(o,r.get("target",!0)||"_blank")}n=!0}}))}))},t.prototype._rootToNode=function(e){e!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:FR,from:this.uid,seriesId:this.seriesModel.id,targetNode:e})},t.prototype.containPoint=function(e,t){var n=t.getData().getItemLayout(0);if(n){var i=e[0]-n.cx,a=e[1]-n.cy,r=Math.sqrt(i*i+a*a);return r<=n.r&&r>=n.r0}},t.type="sunburst"})(vw),function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.ignoreStyleOnData=!0,n}ze(t,e),t.prototype.getInitialData=function(e,t){var n={name:e.name,children:e.data};RR(n);var i=this._levelModels=Br(e.levels||[],(function(e){return new uy(e,this,t)}),this),a=ND.createTree(n,this,(function(e){e.wrapMethod("getItemModel",(function(e,t){var n=a.getNodeByDataIndex(t),r=i[n.depth];return r&&(e.parentModel=r),e}))}));return a.data},t.prototype.optionUpdated=function(){this.resetViewRoot()},t.prototype.getDataParams=function(t){var n=e.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(t);return n.treePathInfo=GD(i,this),n},t.prototype.getLevelModel=function(e){return this._levelModels&&this._levelModels[e.depth]},t.prototype.getViewRoot=function(){return this._viewRoot},t.prototype.resetViewRoot=function(e){e?this._viewRoot=e:e=this._viewRoot;var t=this.getRawData().tree.root;e&&(e===t||t.contains(e))||(this._viewRoot=t)},t.prototype.enableAriaDecal=function(){zD(this)},t.type="series.sunburst",t.defaultOption={z:2,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,stillShowZeroSum:!0,nodeClick:"rootToNode",renderLabelForZeroData:!1,label:{rotate:"radial",show:!0,opacity:1,align:"center",position:"inside",distance:5,silent:!0},itemStyle:{borderWidth:1,borderColor:"white",borderType:"solid",shadowBlur:0,shadowColor:"rgba(0, 0, 0, 0.2)",shadowOffsetX:0,shadowOffsetY:0,opacity:1},emphasis:{focus:"descendant"},blur:{itemStyle:{opacity:.2},label:{opacity:.1}},animationType:"expansion",animationDuration:1e3,animationDurationUpdate:500,data:[],sort:"desc"}}(ow);function RR(e){var t=0;Rr(e.children,(function(e){RR(e);var n=e.value;Ur(n)&&(n=n[0]),t+=n}));var n=e.value;Ur(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=t),n<0&&(n=0),Ur(e.value)?e.value[0]=n:e.value=n}Math.PI;var BR={color:"fill",borderColor:"stroke"},NR={symbol:1,symbolSize:1,symbolKeepAspect:1,legendIcon:1,visualMeta:1,liftZ:1,decal:1},LR=Cd(),VR=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.optionUpdated=function(){this.currentZLevel=this.get("zlevel",!0),this.currentZ=this.get("z",!0)},t.prototype.getInitialData=function(e,t){return K_(null,this)},t.prototype.getDataParams=function(t,n,i){var a=e.prototype.getDataParams.call(this,t,n);return i&&(a.info=LR(i).info),a},t.type="series.custom",t.dependencies=["grid","polar","geo","singleAxis","calendar"],t.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,clip:!1},t}(ow);function qR(e,t){return t=t||[0,0],Br(["x","y"],(function(n,i){var a=this.getAxis(n),r=t[i],o=e[i]/2;return"category"===a.type?a.getBandWidth():Math.abs(a.dataToCoord(r-o)-a.dataToCoord(r+o))}),this)}function GR(e,t){return t=t||[0,0],Br([0,1],(function(n){var i=t[n],a=e[n]/2,r=[],o=[];return r[n]=i-a,o[n]=i+a,r[1-n]=o[1-n]=t[1-n],Math.abs(this.dataToPoint(r)[n]-this.dataToPoint(o)[n])}),this)}function zR(e,t){var n=this.getAxis(),i=t instanceof Array?t[0]:t,a=(e instanceof Array?e[0]:e)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(i-a)-n.dataToCoord(i+a))}function UR(e,t){return t=t||[0,0],Br(["Radius","Angle"],(function(n,i){var a=this["get"+n+"Axis"](),r=t[i],o=e[i]/2,s="category"===a.type?a.getBandWidth():Math.abs(a.dataToCoord(r-o)-a.dataToCoord(r+o));return"Angle"===n&&(s=s*Math.PI/180),s}),this)}function jR(e,t,n,i){return e&&(e.legacy||!1!==e.legacy&&!n&&!i&&"tspan"!==t&&("text"===t||bo(e,"text")))}function HR(e,t,n){var i,a,r,o=e;if("text"===t)r=o;else{r={},bo(o,"text")&&(r.text=o.text),bo(o,"rich")&&(r.rich=o.rich),bo(o,"textFill")&&(r.fill=o.textFill),bo(o,"textStroke")&&(r.stroke=o.textStroke),bo(o,"fontFamily")&&(r.fontFamily=o.fontFamily),bo(o,"fontSize")&&(r.fontSize=o.fontSize),bo(o,"fontStyle")&&(r.fontStyle=o.fontStyle),bo(o,"fontWeight")&&(r.fontWeight=o.fontWeight),a={type:"text",style:r,silent:!0},i={};var s=bo(o,"textPosition");n?i.position=s?o.textPosition:"inside":s&&(i.position=o.textPosition),bo(o,"textPosition")&&(i.position=o.textPosition),bo(o,"textOffset")&&(i.offset=o.textOffset),bo(o,"textRotation")&&(i.rotation=o.textRotation),bo(o,"textDistance")&&(i.distance=o.textDistance)}return WR(r,e),Rr(r.rich,(function(e){WR(e,e)})),{textConfig:i,textContent:a}}function WR(e,t){t&&(t.font=t.textFont||t.font,bo(t,"textStrokeWidth")&&(e.lineWidth=t.textStrokeWidth),bo(t,"textAlign")&&(e.align=t.textAlign),bo(t,"textVerticalAlign")&&(e.verticalAlign=t.textVerticalAlign),bo(t,"textLineHeight")&&(e.lineHeight=t.textLineHeight),bo(t,"textWidth")&&(e.width=t.textWidth),bo(t,"textHeight")&&(e.height=t.textHeight),bo(t,"textBackgroundColor")&&(e.backgroundColor=t.textBackgroundColor),bo(t,"textPadding")&&(e.padding=t.textPadding),bo(t,"textBorderColor")&&(e.borderColor=t.textBorderColor),bo(t,"textBorderWidth")&&(e.borderWidth=t.textBorderWidth),bo(t,"textBorderRadius")&&(e.borderRadius=t.textBorderRadius),bo(t,"textBoxShadowColor")&&(e.shadowColor=t.textBoxShadowColor),bo(t,"textBoxShadowBlur")&&(e.shadowBlur=t.textBoxShadowBlur),bo(t,"textBoxShadowOffsetX")&&(e.shadowOffsetX=t.textBoxShadowOffsetX),bo(t,"textBoxShadowOffsetY")&&(e.shadowOffsetY=t.textBoxShadowOffsetY))}function $R(e,t,n){var i=e;i.textPosition=i.textPosition||n.position||"inside",null!=n.offset&&(i.textOffset=n.offset),null!=n.rotation&&(i.textRotation=n.rotation),null!=n.distance&&(i.textDistance=n.distance);var a=i.textPosition.indexOf("inside")>=0,r=e.fill||"#000";KR(i,t);var o=null==i.textFill;return a?o&&(i.textFill=n.insideFill||"#fff",!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=r),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(o&&(i.textFill=e.fill||n.outsideFill||"#000"),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=t.text,i.rich=t.rich,Rr(t.rich,(function(e){KR(e,e)})),i}function KR(e,t){t&&(bo(t,"fill")&&(e.textFill=t.fill),bo(t,"stroke")&&(e.textStroke=t.fill),bo(t,"lineWidth")&&(e.textStrokeWidth=t.lineWidth),bo(t,"font")&&(e.font=t.font),bo(t,"fontStyle")&&(e.fontStyle=t.fontStyle),bo(t,"fontWeight")&&(e.fontWeight=t.fontWeight),bo(t,"fontSize")&&(e.fontSize=t.fontSize),bo(t,"fontFamily")&&(e.fontFamily=t.fontFamily),bo(t,"align")&&(e.textAlign=t.align),bo(t,"verticalAlign")&&(e.textVerticalAlign=t.verticalAlign),bo(t,"lineHeight")&&(e.textLineHeight=t.lineHeight),bo(t,"width")&&(e.textWidth=t.width),bo(t,"height")&&(e.textHeight=t.height),bo(t,"backgroundColor")&&(e.textBackgroundColor=t.backgroundColor),bo(t,"padding")&&(e.textPadding=t.padding),bo(t,"borderColor")&&(e.textBorderColor=t.borderColor),bo(t,"borderWidth")&&(e.textBorderWidth=t.borderWidth),bo(t,"borderRadius")&&(e.textBorderRadius=t.borderRadius),bo(t,"shadowColor")&&(e.textBoxShadowColor=t.shadowColor),bo(t,"shadowBlur")&&(e.textBoxShadowBlur=t.shadowBlur),bo(t,"shadowOffsetX")&&(e.textBoxShadowOffsetX=t.shadowOffsetX),bo(t,"shadowOffsetY")&&(e.textBoxShadowOffsetY=t.shadowOffsetY),bo(t,"textShadowColor")&&(e.textShadowColor=t.textShadowColor),bo(t,"textShadowBlur")&&(e.textShadowBlur=t.textShadowBlur),bo(t,"textShadowOffsetX")&&(e.textShadowOffsetX=t.textShadowOffsetX),bo(t,"textShadowOffsetY")&&(e.textShadowOffsetY=t.textShadowOffsetY))}var YR={position:["x","y"],scale:["scaleX","scaleY"],origin:["originX","originY"]},XR=qr(YR),ZR=(Nr(sc,(function(e,t){return e[t]=1,e}),{}),sc.join(", "),["","style","shape","extra"]),QR=Cd();function JR(e,t,n,i,a){var r=e+"Animation",o=Jg(e,i,a)||{},s=QR(t).userDuring;return o.duration>0&&(o.during=s?Gr(oB,{el:t,userDuring:s}):null,o.setToFinal=!0,o.scope=e),kr(o,n[r]),o}function eB(e,t,n,i){var a=(i=i||{}).dataIndex,r=i.isInit,o=i.clearStyle,s=n.isAnimationEnabled(),l=QR(e),p=t.style;l.userDuring=t.during;var c={},d={};if(function(e,t,n){for(var i=0;i=0)){var d=e.getAnimationStyleProps(),u=d?d.style:null;if(u){!a&&(a=i.style={});var m=qr(n);for(p=0;p0&&e.animateFrom(u,m)}else!function(e,t,n,i,a){if(a){var r=JR("update",e,t,i,n);r.duration>0&&e.animateFrom(a,r)}}(e,t,a||0,n,c);tB(e,t),p?e.dirty():e.markRedraw()}function tB(e,t){for(var n=QR(e).leaveToProps,i=0;i=0){!r&&(r=i[e]={});var u=qr(o);for(c=0;ci[1]&&i.reverse(),{coordSys:{type:"polar",cx:e.cx,cy:e.cy,r:i[1],r0:i[0]},api:{coord:function(i){var a=t.dataToRadius(i[0]),r=n.dataToAngle(i[1]),o=e.coordToPoint([a,r]);return o.push(a,r*Math.PI/180),o},size:Gr(UR,e)}}},calendar:function(e){var t=e.getRect(),n=e.getRangeInfo();return{coordSys:{type:"calendar",x:t.x,y:t.y,width:t.width,height:t.height,cellWidth:e.getCellWidth(),cellHeight:e.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:function(t,n){return e.dataToPoint(t,n)}}}}};function _B(e){return e instanceof gm}function TB(e){return e instanceof mu}var IB=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.render=function(e,t,n,i){this._progressiveEls=null;var a=this._data,r=e.getData(),o=this.group,s=DB(e,r,t,n);a||o.removeAll(),r.diff(a).add((function(t){AB(n,null,t,s(t,i),e,o,r)})).remove((function(t){var n=a.getItemGraphicEl(t);n&&nB(n,LR(n).option,e)})).update((function(t,l){var p=a.getItemGraphicEl(l);AB(n,p,t,s(t,i),e,o,r)})).execute();var l=e.get("clip",!0)?BE(e.coordinateSystem,!1,e):null;l?o.setClipPath(l):o.removeClipPath(),this._data=r},t.prototype.incrementalPrepareRender=function(e,t,n){this.group.removeAll(),this._data=null},t.prototype.incrementalRender=function(e,t,n,i,a){var r=t.getData(),o=DB(t,r,n,i),s=this._progressiveEls=[];function l(e){e.isGroup||(e.incremental=!0,e.ensureState("emphasis").hoverLayer=!0)}for(var p=e.start;p=0?t.getStore().get(a,n):void 0}var r=t.get(i.name,n),o=i&&i.ordinalMeta;return o?o.categories[r]:r},styleEmphasis:function(n,i){0;null==i&&(i=s);var a=v(i,hB).getItemStyle(),r=x(i,hB),o=jf(r,null,null,!0,!0);o.text=r.getShallow("show")?ao(e.getFormattedLabel(i,hB),e.getFormattedLabel(i,gB),fE(t,i)):null;var l=Hf(r,null,!0);return w(n,a),a=$R(a,o,l),n&&b(a,n),a.legacy=!0,a},visual:function(e,n){if(null==n&&(n=s),bo(BR,e)){var i=t.getItemVisual(n,"style");return i?i[BR[e]]:null}if(bo(NR,e))return t.getItemVisual(n,e)},barLayout:function(e){if("cartesian2d"===r.type){return function(e){var t=[],n=e.axis,i="axis0";if("category"===n.type){for(var a=n.getBandWidth(),r=0;r=d;h--){var g=t.childAt(h);VB(t,g,a)}}(e,d,n,i,a),o>=0?r.replaceAt(d,o):r.add(d),d}function RB(e,t,n){var i,a=LR(e),r=t.type,o=t.shape,s=t.style;return n.isUniversalTransitionEnabled()||null!=r&&r!==a.customGraphicType||"path"===r&&((i=o)&&(bo(i,"pathData")||bo(i,"d")))&&UB(o)!==a.customPathData||"image"===r&&bo(s,"image")&&s.image!==a.customImagePath}function BB(e,t,n){var i=t?NB(e,t):e,a=t?LB(e,i,hB):e.style,r=e.type,o=i?i.textConfig:null,s=e.textContent,l=s?t?NB(s,t):s:null;if(a&&(n.isLegacy||jR(a,r,!!o,!!l))){n.isLegacy=!0;var p=HR(a,r,!t);!o&&p.textConfig&&(o=p.textConfig),!l&&p.textContent&&(l=p.textContent)}if(!t&&l){var c=l;!c.type&&(c.type="text")}var d=t?n[t]:n.normal;d.cfg=o,d.conOpt=l}function NB(e,t){return t?e?e[t]:null:e}function LB(e,t,n){var i=t&&t.style;return null==i&&n===hB&&e&&(i=e.styleEmphasis),i}function VB(e,t,n){t&&nB(t,LR(e).option,n)}function qB(e,t){var n=e&&e.name;return null!=n?n:"e\0\0"+t}function GB(e,t){var n=this.context,i=null!=e?n.newChildren[e]:null,a=null!=t?n.oldChildren[t]:null;FB(n.api,a,n.dataIndex,i,n.seriesModel,n.group)}function zB(e){var t=this.context,n=t.oldChildren[e];n&&nB(n,LR(n).option,t.seriesModel)}function UB(e){return e&&(e.pathData||e.d)}function jB(e){e.registerChartView(IB),e.registerSeriesModel(VR)}var HB=Cd(),WB=Ir,$B=Gr,KB=function(){function e(){this._dragging=!1,this.animationThreshold=15}return e.prototype.render=function(e,t,n,i){var a=t.get("value"),r=t.get("status");if(this._axisModel=e,this._axisPointerModel=t,this._api=n,i||this._lastValue!==a||this._lastStatus!==r){this._lastValue=a,this._lastStatus=r;var o=this._group,s=this._handle;if(!r||"hide"===r)return o&&o.hide(),void(s&&s.hide());o&&o.show(),s&&s.show();var l={};this.makeElOption(l,a,e,t,n);var p=l.graphicKey;p!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=p;var c=this._moveAnimation=this.determineAnimation(e,t);if(o){var d=zr(YB,t,c);this.updatePointerEl(o,l,d),this.updateLabelEl(o,l,d,t)}else o=this._group=new Mc,this.createPointerEl(o,l,e,t),this.createLabelEl(o,l,e,t),n.getZr().add(o);JB(o,t,!0),this._renderHandle(a)}},e.prototype.remove=function(e){this.clear(e)},e.prototype.dispose=function(e){this.clear(e)},e.prototype.determineAnimation=function(e,t){var n=t.get("animation"),i=e.axis,a="category"===i.type,r=t.get("snap");if(!r&&!a)return!1;if("auto"===n||null==n){var o=this.animationThreshold;if(a&&i.getBandWidth()>o)return!0;if(r){var s=Tk(e).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>o}return!1}return!0===n},e.prototype.makeElOption=function(e,t,n,i,a){},e.prototype.createPointerEl=function(e,t,n,i){var a=t.pointer;if(a){var r=HB(e).pointerEl=new Lf[a.type](WB(t.pointer));e.add(r)}},e.prototype.createLabelEl=function(e,t,n,i){if(t.label){var a=HB(e).labelEl=new Pm(WB(t.label));e.add(a),ZB(a,i)}},e.prototype.updatePointerEl=function(e,t,n){var i=HB(e).pointerEl;i&&t.pointer&&(i.setStyle(t.pointer.style),n(i,{shape:t.pointer.shape}))},e.prototype.updateLabelEl=function(e,t,n,i){var a=HB(e).labelEl;a&&(a.setStyle(t.label.style),n(a,{x:t.label.x,y:t.label.y}),ZB(a,i))},e.prototype._renderHandle=function(e){if(!this._dragging&&this.updateHandleTransform){var t,n=this._axisPointerModel,i=this._api.getZr(),a=this._handle,r=n.getModel("handle"),o=n.get("status");if(!r.get("show")||!o||"hide"===o)return a&&i.remove(a),void(this._handle=null);this._handle||(t=!0,a=this._handle=Df(r.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(e){ls(e.event)},onmousedown:$B(this._onHandleDragMove,this,0,0),drift:$B(this._onHandleDragMove,this),ondragend:$B(this._onHandleDragEnd,this)}),i.add(a)),JB(a,n,!1),a.setStyle(r.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=r.get("size");Ur(s)||(s=[s,s]),a.scaleX=s[0]/2,a.scaleY=s[1]/2,Mw(this,"_doDispatchAxisPointer",r.get("throttle")||0,"fixRate"),this._moveHandleToValue(e,t)}},e.prototype._moveHandleToValue=function(e,t){YB(this._axisPointerModel,!t&&this._moveAnimation,this._handle,QB(this.getHandleTransform(e,this._axisModel,this._axisPointerModel)))},e.prototype._onHandleDragMove=function(e,t){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(QB(n),[e,t],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(QB(i)),HB(n).lastProp=null,this._doDispatchAxisPointer()}},e.prototype._doDispatchAxisPointer=function(){if(this._handle){var e=this._payloadInfo,t=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:e.cursorPoint[0],y:e.cursorPoint[1],tooltipOption:e.tooltipOption,axesInfo:[{axisDim:t.axis.dim,axisIndex:t.componentIndex}]})}},e.prototype._onHandleDragEnd=function(){if(this._dragging=!1,this._handle){var e=this._axisPointerModel.get("value");this._moveHandleToValue(e),this._api.dispatchAction({type:"hideTip"})}},e.prototype.clear=function(e){this._lastValue=null,this._lastStatus=null;var t=e.getZr(),n=this._group,i=this._handle;t&&n&&(this._lastGraphicKey=null,n&&t.remove(n),i&&t.remove(i),this._group=null,this._handle=null,this._payloadInfo=null),kw(this,"_doDispatchAxisPointer")},e.prototype.doClear=function(){},e.prototype.buildLabel=function(e,t,n){return{x:e[n=n||0],y:e[1-n],width:t[n],height:t[1-n]}},e}();function YB(e,t,n,i){XB(HB(n).lastProp,i)||(HB(n).lastProp=i,t?tf(n,i,e):(n.stopAnimation(),n.attr(i)))}function XB(e,t){if(Kr(e)&&Kr(t)){var n=!0;return Rr(t,(function(t,i){n=n&&XB(e[i],t)})),!!n}return e===t}function ZB(e,t){e[t.get(["label","show"])?"show":"hide"]()}function QB(e){return{x:e.x||0,y:e.y||0,rotation:e.rotation||0}}function JB(e,t,n){var i=t.get("z"),a=t.get("zlevel");e&&e.traverse((function(e){"group"!==e.type&&(null!=i&&(e.z=i),null!=a&&(e.zlevel=a),e.silent=n)}))}function eN(e){var t,n=e.get("type"),i=e.getModel(n+"Style");return"line"===n?(t=i.getLineStyle()).fill=null:"shadow"===n&&((t=i.getAreaStyle()).stroke=null),t}function tN(e,t,n,i,a){var r=nN(n.get("value"),t.axis,t.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),o=n.getModel("label"),s=nv(o.get("padding")||0),l=o.getFont(),p=uc(r,l),c=a.position,d=p.width+s[1]+s[3],u=p.height+s[0]+s[2],m=a.align;"right"===m&&(c[0]-=d),"center"===m&&(c[0]-=d/2);var h=a.verticalAlign;"bottom"===h&&(c[1]-=u),"middle"===h&&(c[1]-=u/2),function(e,t,n,i){var a=i.getWidth(),r=i.getHeight();e[0]=Math.min(e[0]+t,a)-t,e[1]=Math.min(e[1]+n,r)-n,e[0]=Math.max(e[0],0),e[1]=Math.max(e[1],0)}(c,d,u,i);var g=o.get("backgroundColor");g&&"auto"!==g||(g=t.get(["axisLine","lineStyle","color"])),e.label={x:c[0],y:c[1],style:jf(o,{text:r,font:l,fill:o.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function nN(e,t,n,i,a){e=t.scale.parse(e);var r=t.scale.getLabel({value:e},{precision:a.precision}),o=a.formatter;if(o){var s={value:XT(t,{value:e}),axisDimension:t.dim,axisIndex:t.index,seriesData:[]};Rr(i,(function(e){var t=n.getSeriesByIndex(e.seriesIndex),i=e.dataIndexInside,a=t&&t.getDataParams(i);a&&s.seriesData.push(a)})),Hr(o)?r=o.replace("{value}",r):jr(o)&&(r=o(s))}return r}function iN(e,t,n){var i=[1,0,0,1,0,0];return ys(i,i,n.rotation),fs(i,i,n.position),Tf([e.dataToCoord(t),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function aN(e,t,n,i,a,r){var o=yk.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=a.get(["label","margin"]),tN(t,i,a,r,{position:iN(i.axis,e,n),align:o.textAlign,verticalAlign:o.textVerticalAlign})}function rN(e,t,n){return{x1:e[n=n||0],y1:e[1-n],x2:t[n],y2:t[1-n]}}function oN(e,t,n){return{x:e[n=n||0],y:e[1-n],width:t[n],height:t[1-n]}}function sN(e,t,n,i,a,r){return{cx:e,cy:t,r0:n,r:i,startAngle:a,endAngle:r,clockwise:!0}}var lN=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.makeElOption=function(e,t,n,i,a){var r=n.axis,o=r.grid,s=i.get("type"),l=pN(o,r).getOtherAxis(r).getGlobalExtent(),p=r.toGlobalCoord(r.dataToCoord(t,!0));if(s&&"none"!==s){var c=eN(i),d=cN[s](r,p,l);d.style=c,e.graphicKey=d.type,e.pointer=d}aN(t,e,sk(o.model,n),n,i,a)},t.prototype.getHandleTransform=function(e,t,n){var i=sk(t.axis.grid.model,t,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var a=iN(t.axis,e,i);return{x:a[0],y:a[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},t.prototype.updateHandleTransform=function(e,t,n,i){var a=n.axis,r=a.grid,o=a.getGlobalExtent(!0),s=pN(r,a).getOtherAxis(a).getGlobalExtent(),l="x"===a.dim?0:1,p=[e.x,e.y];p[l]+=t[l],p[l]=Math.min(o[1],p[l]),p[l]=Math.max(o[0],p[l]);var c=(s[1]+s[0])/2,d=[c,c];d[l]=p[l];return{x:p[0],y:p[1],rotation:e.rotation,cursorPoint:d,tooltipOption:[{verticalAlign:"middle"},{align:"center"}][l]}},t}(KB);function pN(e,t){var n={};return n[t.dim+"AxisIndex"]=t.index,e.getCartesian(n)}var cN={line:function(e,t,n){return{type:"Line",subPixelOptimize:!0,shape:rN([t,n[0]],[t,n[1]],dN(e))}},shadow:function(e,t,n){var i=Math.max(1,e.getBandWidth()),a=n[1]-n[0];return{type:"Rect",shape:oN([t-i/2,n[0]],[i,a],dN(e))}}};function dN(e){return"x"===e.dim?0:1}var uN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="axisPointer",t.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#B9BEC9",width:1,type:"dashed"},shadowStyle:{color:"rgba(210,219,238,0.2)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}},t}(Sv),mN=Cd(),hN=Rr;function gN(e,t,n){if(!rr.node){var i=t.getZr();mN(i).records||(mN(i).records={}),function(e,t){if(mN(e).initialized)return;function n(n,i){e.on(n,(function(n){var a=function(e){var t={showTip:[],hideTip:[]},n=function(i){var a=t[i.type];a?a.push(i):(i.dispatchAction=n,e.dispatchAction(i))};return{dispatchAction:n,pendings:t}}(t);hN(mN(e).records,(function(e){e&&i(e,n,a.dispatchAction)})),function(e,t){var n,i=e.showTip.length,a=e.hideTip.length;i?n=e.showTip[i-1]:a&&(n=e.hideTip[a-1]);n&&(n.dispatchAction=null,t.dispatchAction(n))}(a.pendings,t)}))}mN(e).initialized=!0,n("click",zr(yN,"click")),n("mousemove",zr(yN,"mousemove")),n("globalout",fN)}(i,t),(mN(i).records[e]||(mN(i).records[e]={})).handler=n}}function fN(e,t,n){e.handler("leave",null,n)}function yN(e,t,n,i){t.handler(e,n,i)}function vN(e,t){if(!rr.node){var n=t.getZr();(mN(n).records||{})[e]&&(mN(n).records[e]=null)}}var xN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.render=function(e,t,n){var i=t.getComponent("tooltip"),a=e.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";gN("axisPointer",n,(function(e,t,n){"none"!==a&&("leave"===e||a.indexOf(e)>=0)&&n({type:"updateAxisPointer",currTrigger:e,x:t&&t.offsetX,y:t&&t.offsetY})}))},t.prototype.remove=function(e,t){vN("axisPointer",t)},t.prototype.dispose=function(e,t){vN("axisPointer",t)},t.type="axisPointer",t}(hw);function bN(e,t){var n,i=[],a=e.seriesIndex;if(null==a||!(n=t.getSeriesByIndex(a)))return{point:[]};var r=n.getData(),o=Sd(r,e);if(null==o||o<0||Ur(o))return{point:[]};var s=r.getItemGraphicEl(o),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(o)||[];else if(l&&l.dataToPoint)if(e.isStacked){var p=l.getBaseAxis(),c=l.getOtherAxis(p).dim,d=p.dim,u="x"===c||"radius"===c?1:0,m=r.mapDimension(d),h=[];h[u]=r.get(m,o),h[1-u]=r.get(r.getCalculationInfo("stackResultDimension"),o),i=l.dataToPoint(h)||[]}else i=l.dataToPoint(r.getValues(Br(l.dimensions,(function(e){return r.mapDimension(e)})),o))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),i=[g.x+g.width/2,g.y+g.height/2]}return{point:i,el:s}}var wN=Cd();function SN(e,t,n){var i=e.currTrigger,a=[e.x,e.y],r=e,o=e.dispatchAction||Gr(n.dispatchAction,n),s=t.getComponent("axisPointer").coordSysAxesInfo;if(s){EN(a)&&(a=bN({seriesIndex:r.seriesIndex,dataIndex:r.dataIndex},t).point);var l=EN(a),p=r.axesInfo,c=s.axesInfo,d="leave"===i||EN(a),u={},m={},h={list:[],map:{}},g={showPointer:zr(_N,m),showTooltip:zr(TN,h)};Rr(s.coordSysMap,(function(e,t){var n=l||e.containPoint(a);Rr(s.coordSysAxesInfo[t],(function(e,t){var i=e.axis,r=function(e,t){for(var n=0;n<(e||[]).length;n++){var i=e[n];if(t.axis.dim===i.axisDim&&t.axis.model.componentIndex===i.axisIndex)return i}}(p,e);if(!d&&n&&(!p||r)){var o=r&&r.value;null!=o||l||(o=i.pointToData(a)),null!=o&&CN(e,o,g,!1,u)}}))}));var f={};return Rr(c,(function(e,t){var n=e.linkGroup;n&&!m[t]&&Rr(n.axesInfo,(function(t,i){var a=m[i];if(t!==e&&a){var r=a.value;n.mapper&&(r=e.axis.scale.parse(n.mapper(r,IN(t),IN(e)))),f[e.key]=r}}))})),Rr(f,(function(e,t){CN(c[t],e,g,!0,u)})),function(e,t,n){var i=n.axesInfo=[];Rr(t,(function(t,n){var a=t.axisPointerModel.option,r=e[n];r?(!t.useHandle&&(a.status="show"),a.value=r.value,a.seriesDataIndices=(r.payloadBatch||[]).slice()):!t.useHandle&&(a.status="hide"),"show"===a.status&&i.push({axisDim:t.axis.dim,axisIndex:t.axis.model.componentIndex,value:a.value})}))}(m,c,u),function(e,t,n,i){if(EN(t)||!e.list.length)return void i({type:"hideTip"});var a=((e.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:t[0],y:t[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:a.dataIndexInside,dataIndex:a.dataIndex,seriesIndex:a.seriesIndex,dataByCoordSys:e.list})}(h,a,e,o),function(e,t,n){var i=n.getZr(),a="axisPointerLastHighlights",r=wN(i)[a]||{},o=wN(i)[a]={};Rr(e,(function(e,t){var n=e.axisPointerModel.option;"show"===n.status&&e.triggerEmphasis&&Rr(n.seriesDataIndices,(function(e){var t=e.seriesIndex+" | "+e.dataIndex;o[t]=e}))}));var s=[],l=[];Rr(r,(function(e,t){!o[t]&&l.push(e)})),Rr(o,(function(e,t){!r[t]&&s.push(e)})),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}(c,0,n),u}}function CN(e,t,n,i,a){var r=e.axis;if(!r.scale.isBlank()&&r.containData(t))if(e.involveSeries){var o=function(e,t){var n=t.axis,i=n.dim,a=e,r=[],o=Number.MAX_VALUE,s=-1;return Rr(t.seriesModels,(function(t,l){var p,c,d=t.getData().mapDimensionsAll(i);if(t.getAxisTooltipData){var u=t.getAxisTooltipData(d,e,n);c=u.dataIndices,p=u.nestestValue}else{if(!(c=t.getData().indicesOfNearest(d[0],e,"category"===n.type?.5:null)).length)return;p=t.getData().get(d[0],c[0])}if(null!=p&&isFinite(p)){var m=e-p,h=Math.abs(m);h<=o&&((h=0&&s<0)&&(o=h,s=m,a=p,r.length=0),Rr(c,(function(e){r.push({seriesIndex:t.seriesIndex,dataIndexInside:e,dataIndex:t.getData().getRawIndex(e)})})))}})),{payloadBatch:r,snapToValue:a}}(t,e),s=o.payloadBatch,l=o.snapToValue;s[0]&&null==a.seriesIndex&&kr(a,s[0]),!i&&e.snap&&r.containData(l)&&null!=l&&(t=l),n.showPointer(e,t,s),n.showTooltip(e,o,l)}else n.showPointer(e,t)}function _N(e,t,n,i){e[t.key]={value:n,payloadBatch:i}}function TN(e,t,n,i){var a=n.payloadBatch,r=t.axis,o=r.model,s=t.axisPointerModel;if(t.triggerTooltip&&a.length){var l=t.coordSys.model,p=Ek(l),c=e.map[p];c||(c=e.map[p]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},e.list.push(c)),c.dataByAxis.push({axisDim:r.dim,axisIndex:o.componentIndex,axisType:o.type,axisId:o.id,value:i,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:a.slice()})}}function IN(e){var t=e.axis.model,n={},i=n.axisDim=e.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=t.componentIndex,n.axisName=n[i+"AxisName"]=t.name,n.axisId=n[i+"AxisId"]=t.id,n}function EN(e){return!e||null==e[0]||isNaN(e[0])||null==e[1]||isNaN(e[1])}function MN(e){kk.registerAxisPointerClass("CartesianAxisPointer",lN),e.registerComponentModel(uN),e.registerComponentView(xN),e.registerPreprocessor((function(e){if(e){(!e.axisPointer||0===e.axisPointer.length)&&(e.axisPointer={});var t=e.axisPointer.link;t&&!Ur(t)&&(e.axisPointer.link=[t])}})),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,(function(e,t){e.getComponent("axisPointer").coordSysAxesInfo=Ck(e,t)})),e.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},SN)}function kN(e){aI(Gk),aI(MN)}var PN=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.makeElOption=function(e,t,n,i,a){var r=n.axis;"angle"===r.dim&&(this.animationThreshold=Math.PI/18);var o=r.polar,s=o.getOtherAxis(r).getExtent(),l=r.dataToCoord(t),p=i.get("type");if(p&&"none"!==p){var c=eN(i),d=DN[p](r,o,l,s);d.style=c,e.graphicKey=d.type,e.pointer=d}var u=function(e,t,n,i,a){var r=t.axis,o=r.dataToCoord(e),s=i.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l,p,c,d=i.getRadiusAxis().getExtent();if("radius"===r.dim){var u=[1,0,0,1,0,0];ys(u,u,s),fs(u,u,[i.cx,i.cy]),l=Tf([o,-a],u);var m=t.getModel("axisLabel").get("rotate")||0,h=yk.innerTextLayout(s,m*Math.PI/180,-1);p=h.textAlign,c=h.textVerticalAlign}else{var g=d[1];l=i.coordToPoint([g+a,o]);var f=i.cx,y=i.cy;p=Math.abs(l[0]-f)/g<.3?"center":l[0]>f?"left":"right",c=Math.abs(l[1]-y)/g<.3?"middle":l[1]>y?"top":"bottom"}return{position:l,align:p,verticalAlign:c}}(t,n,0,o,i.get(["label","margin"]));tN(e,n,i,a,u)},t}(KB);var DN={line:function(e,t,n,i){return"angle"===e.dim?{type:"Line",shape:rN(t.coordToPoint([i[0],n]),t.coordToPoint([i[1],n]))}:{type:"Circle",shape:{cx:t.cx,cy:t.cy,r:n}}},shadow:function(e,t,n,i){var a=Math.max(1,e.getBandWidth()),r=Math.PI/180;return"angle"===e.dim?{type:"Sector",shape:sN(t.cx,t.cy,i[0],i[1],(-n-a/2)*r,(a/2-n)*r)}:{type:"Sector",shape:sN(t.cx,t.cy,n-a/2,n+a/2,0,2*Math.PI)}}},ON=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.findAxisModel=function(e){var t;return this.ecModel.eachComponent(e,(function(e){e.getCoordSysModel()===this&&(t=e)}),this),t},t.type="polar",t.dependencies=["radiusAxis","angleAxis"],t.defaultOption={z:0,center:["50%","50%"],radius:"80%"},t}(Sv),AN=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return ze(t,e),t.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",Ed).models[0]},t.type="polarAxis",t}(Sv);Ar(AN,tI);var FN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="angleAxis",t}(AN),RN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="radiusAxis",t}(AN),BN=function(e){function t(t,n){return e.call(this,"radius",t,n)||this}return ze(t,e),t.prototype.pointToData=function(e,t){return this.polar.pointToData(e,t)["radius"===this.dim?0:1]},t}(MI);BN.prototype.dataToRadius=MI.prototype.dataToCoord,BN.prototype.radiusToData=MI.prototype.coordToData;var NN=Cd(),LN=function(e){function t(t,n){return e.call(this,"angle",t,n||[0,360])||this}return ze(t,e),t.prototype.pointToData=function(e,t){return this.polar.pointToData(e,t)["radius"===this.dim?0:1]},t.prototype.calculateCategoryInterval=function(){var e=this,t=e.getLabelModel(),n=e.scale,i=n.getExtent(),a=n.count();if(i[1]-i[0]<1)return 0;var r=i[0],o=e.dataToCoord(r+1)-e.dataToCoord(r),s=Math.abs(o),l=uc(null==r?"":r+"",t.getFont(),"center","top"),p=Math.max(l.height,7)/s;isNaN(p)&&(p=1/0);var c=Math.max(0,Math.floor(p)),d=NN(e.model),u=d.lastAutoInterval,m=d.lastTickCount;return null!=u&&null!=m&&Math.abs(u-c)<=1&&Math.abs(m-a)<=1&&u>c?c=u:(d.lastTickCount=a,d.lastAutoInterval=c),c},t}(MI);LN.prototype.dataToAngle=MI.prototype.dataToCoord,LN.prototype.angleToData=MI.prototype.coordToData;var VN=["radius","angle"],qN=function(){function e(e){this.dimensions=VN,this.type="polar",this.cx=0,this.cy=0,this._radiusAxis=new BN,this._angleAxis=new LN,this.axisPointerEnabled=!0,this.name=e||"",this._radiusAxis.polar=this._angleAxis.polar=this}return e.prototype.containPoint=function(e){var t=this.pointToCoord(e);return this._radiusAxis.contain(t[0])&&this._angleAxis.contain(t[1])},e.prototype.containData=function(e){return this._radiusAxis.containData(e[0])&&this._angleAxis.containData(e[1])},e.prototype.getAxis=function(e){return this["_"+e+"Axis"]},e.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},e.prototype.getAxesByScale=function(e){var t=[],n=this._angleAxis,i=this._radiusAxis;return n.scale.type===e&&t.push(n),i.scale.type===e&&t.push(i),t},e.prototype.getAngleAxis=function(){return this._angleAxis},e.prototype.getRadiusAxis=function(){return this._radiusAxis},e.prototype.getOtherAxis=function(e){var t=this._angleAxis;return e===t?this._radiusAxis:t},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},e.prototype.getTooltipAxes=function(e){var t=null!=e&&"auto"!==e?this.getAxis(e):this.getBaseAxis();return{baseAxes:[t],otherAxes:[this.getOtherAxis(t)]}},e.prototype.dataToPoint=function(e,t){return this.coordToPoint([this._radiusAxis.dataToRadius(e[0],t),this._angleAxis.dataToAngle(e[1],t)])},e.prototype.pointToData=function(e,t){var n=this.pointToCoord(e);return[this._radiusAxis.radiusToData(n[0],t),this._angleAxis.angleToData(n[1],t)]},e.prototype.pointToCoord=function(e){var t=e[0]-this.cx,n=e[1]-this.cy,i=this.getAngleAxis(),a=i.getExtent(),r=Math.min(a[0],a[1]),o=Math.max(a[0],a[1]);i.inverse?r=o-360:o=r+360;var s=Math.sqrt(t*t+n*n);t/=s,n/=s;for(var l=Math.atan2(-n,t)/Math.PI*180,p=lo;)l+=360*p;return[s,l]},e.prototype.coordToPoint=function(e){var t=e[0],n=e[1]/180*Math.PI;return[Math.cos(n)*t+this.cx,-Math.sin(n)*t+this.cy]},e.prototype.getArea=function(){var e=this.getAngleAxis(),t=this.getRadiusAxis().getExtent().slice();t[0]>t[1]&&t.reverse();var n=e.getExtent(),i=Math.PI/180;return{cx:this.cx,cy:this.cy,r0:t[0],r:t[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:e.inverse,contain:function(e,t){var n=e-this.cx,i=t-this.cy,a=n*n+i*i-1e-4,r=this.r,o=this.r0;return a<=r*r&&a>=o*o}}},e.prototype.convertToPixel=function(e,t,n){return GN(t)===this?this.dataToPoint(n):null},e.prototype.convertFromPixel=function(e,t,n){return GN(t)===this?this.pointToData(n):null},e}();function GN(e){var t=e.seriesModel,n=e.polarModel;return n&&n.coordinateSystem||t&&t.coordinateSystem}function zN(e,t){var n=this,i=n.getAngleAxis(),a=n.getRadiusAxis();if(i.scale.setExtent(1/0,-1/0),a.scale.setExtent(1/0,-1/0),e.eachSeries((function(e){if(e.coordinateSystem===n){var t=e.getData();Rr(eI(t,"radius"),(function(e){a.scale.unionExtentFromData(t,e)})),Rr(eI(t,"angle"),(function(e){i.scale.unionExtentFromData(t,e)}))}})),$T(i.scale,i.model),$T(a.scale,a.model),"category"===i.type&&!i.onBand){var r=i.getExtent(),o=360/i.scale.count();i.inverse?r[1]+=o:r[1]-=o,i.setExtent(r[0],r[1])}}function UN(e,t){var n;if(e.type=t.get("type"),e.scale=KT(t),e.onBand=t.get("boundaryGap")&&"category"===e.type,e.inverse=t.get("inverse"),function(e){return"angleAxis"===e.mainType}(t)){e.inverse=e.inverse!==t.get("clockwise");var i=t.get("startAngle"),a=null!==(n=t.get("endAngle"))&&void 0!==n?n:i+(e.inverse?-360:360);e.setExtent(i,a)}t.axis=e,e.model=t}var jN={dimensions:VN,create:function(e,t){var n=[];return e.eachComponent("polar",(function(e,i){var a=new qN(i+"");a.update=zN;var r=a.getRadiusAxis(),o=a.getAngleAxis(),s=e.findAxisModel("radiusAxis"),l=e.findAxisModel("angleAxis");UN(r,s),UN(o,l),function(e,t,n){var i=t.get("center"),a=n.getWidth(),r=n.getHeight();e.cx=Vc(i[0],a),e.cy=Vc(i[1],r);var o=e.getRadiusAxis(),s=Math.min(a,r)/2,l=t.get("radius");null==l?l=[0,"100%"]:Ur(l)||(l=[0,l]);var p=[Vc(l[0],s),Vc(l[1],s)];o.inverse?o.setExtent(p[1],p[0]):o.setExtent(p[0],p[1])}(a,e,t),n.push(a),e.coordinateSystem=a,a.model=e})),e.eachSeries((function(e){if("polar"===e.get("coordinateSystem")){var t=e.getReferringComponents("polar",Ed).models[0];0,e.coordinateSystem=t.coordinateSystem}})),n}},HN=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function WN(e,t,n){t[1]>t[0]&&(t=t.slice().reverse());var i=e.coordToPoint([t[0],n]),a=e.coordToPoint([t[1],n]);return{x1:i[0],y1:i[1],x2:a[0],y2:a[1]}}function $N(e){return e.getRadiusAxis().inverse?0:1}function KN(e){var t=e[0],n=e[e.length-1];t&&n&&Math.abs(Math.abs(t.coord-n.coord)-360)<1e-4&&e.pop()}var YN=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.axisPointerClass="PolarAxisPointer",n}return ze(t,e),t.prototype.render=function(e,t){if(this.group.removeAll(),e.get("show")){var n=e.axis,i=n.polar,a=i.getRadiusAxis().getExtent(),r=n.getTicksCoords(),o=n.getMinorTicksCoords(),s=Br(n.getViewLabels(),(function(e){e=Ir(e);var t=n.scale,i="ordinal"===t.type?t.getRawOrdinalNumber(e.tickValue):e.tickValue;return e.coord=n.dataToCoord(i),e}));KN(s),KN(r),Rr(HN,(function(t){!e.get([t,"show"])||n.scale.isBlank()&&"axisLine"!==t||XN[t](this.group,e,i,r,o,a,s)}),this)}},t.type="angleAxis",t}(kk),XN={axisLine:function(e,t,n,i,a,r){var o,s=t.getModel(["axisLine","lineStyle"]),l=n.getAngleAxis(),p=Math.PI/180,c=l.getExtent(),d=$N(n),u=d?0:1,m=360===Math.abs(c[1]-c[0])?"Circle":"Arc";(o=0===r[u]?new Lf[m]({shape:{cx:n.cx,cy:n.cy,r:r[d],startAngle:-c[0]*p,endAngle:-c[1]*p,clockwise:l.inverse},style:s.getLineStyle(),z2:1,silent:!0}):new Ig({shape:{cx:n.cx,cy:n.cy,r:r[d],r0:r[u]},style:s.getLineStyle(),z2:1,silent:!0})).style.fill=null,e.add(o)},axisTick:function(e,t,n,i,a,r){var o=t.getModel("axisTick"),s=(o.get("inside")?-1:1)*o.get("length"),l=r[$N(n)],p=Br(i,(function(e){return new Fg({shape:WN(n,[l,l+s],e.coord)})}));e.add(bf(p,{style:Pr(o.getModel("lineStyle").getLineStyle(),{stroke:t.get(["axisLine","lineStyle","color"])})}))},minorTick:function(e,t,n,i,a,r){if(a.length){for(var o=t.getModel("axisTick"),s=t.getModel("minorTick"),l=(o.get("inside")?-1:1)*s.get("length"),p=r[$N(n)],c=[],d=0;dh?"left":"right",y=Math.abs(m[1]-g)/u<.3?"middle":m[1]>g?"top":"bottom";if(s&&s[d]){var v=s[d];Kr(v)&&v.textStyle&&(o=new uy(v.textStyle,l,l.ecModel))}var x=new Pm({silent:yk.isLabelSilent(t),style:jf(o,{x:m[0],y:m[1],fill:o.getTextColor()||t.get(["axisLine","lineStyle","color"]),text:i.formattedLabel,align:f,verticalAlign:y})});if(e.add(x),c){var b=yk.makeAxisEventDataBase(t);b.targetType="axisLabel",b.value=i.rawLabel,Um(x).eventData=b}}),this)},splitLine:function(e,t,n,i,a,r){var o=t.getModel("splitLine").getModel("lineStyle"),s=o.get("color"),l=0;s=s instanceof Array?s:[s];for(var p=[],c=0;c=0?"p":"n",I=b;v&&(i[s][_]||(i[s][_]={p:b,n:b}),I=i[s][_][T]);var E=void 0,M=void 0,k=void 0,P=void 0;if("radius"===d.dim){var D=d.dataToCoord(C)-b,O=r.dataToCoord(_);Math.abs(D)=P})}}}))}var aL={startAngle:90,clockwise:!0,splitNumber:12,axisLabel:{rotate:0}},rL={splitNumber:5},oL=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="polar",t}(hw);function sL(e){aI(MN),kk.registerAxisPointerClass("PolarAxisPointer",PN),e.registerCoordinateSystem("polar",jN),e.registerComponentModel(ON),e.registerComponentView(oL),ek(e,"angle",FN,aL),ek(e,"radius",RN,rL),e.registerComponentView(YN),e.registerComponentView(JN),e.registerLayout(zr(iL,"bar"))}function lL(e,t){t=t||{};var n=e.coordinateSystem,i=e.axis,a={},r=i.position,o=i.orient,s=n.getRect(),l=[s.x,s.x+s.width,s.y,s.y+s.height],p={horizontal:{top:l[2],bottom:l[3]},vertical:{left:l[0],right:l[1]}};a.position=["vertical"===o?p.vertical[r]:l[0],"horizontal"===o?p.horizontal[r]:l[3]];a.rotation=Math.PI/2*{horizontal:0,vertical:1}[o];a.labelDirection=a.tickDirection=a.nameDirection={top:-1,bottom:1,right:1,left:-1}[r],e.get(["axisTick","inside"])&&(a.tickDirection=-a.tickDirection),no(t.labelInside,e.get(["axisLabel","inside"]))&&(a.labelDirection=-a.labelDirection);var c=t.rotate;return null==c&&(c=e.get(["axisLabel","rotate"])),a.labelRotation="top"===r?-c:c,a.z2=1,a}var pL=["axisLine","axisTickLabel","axisName"],cL=["splitArea","splitLine"],dL=(function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.axisPointerClass="SingleAxisPointer",n}ze(t,e),t.prototype.render=function(t,n,i,a){var r=this.group;r.removeAll();var o=this._axisGroup;this._axisGroup=new Mc;var s=lL(t),l=new yk(t,s);Rr(pL,l.add,l),r.add(this._axisGroup),r.add(l.getGroup()),Rr(cL,(function(e){t.get([e,"show"])&&dL[e](this,this.group,this._axisGroup,t)}),this),Mf(o,this._axisGroup,t),e.prototype.render.call(this,t,n,i,a)},t.prototype.remove=function(){Ok(this)},t.type="singleAxis"}(kk),{splitLine:function(e,t,n,i){var a=i.axis;if(!a.scale.isBlank()){var r=i.getModel("splitLine"),o=r.getModel("lineStyle"),s=o.get("color");s=s instanceof Array?s:[s];for(var l=o.get("width"),p=i.coordinateSystem.getRect(),c=a.isHorizontal(),d=[],u=0,m=a.getTicksCoords({tickModel:r}),h=[],g=[],f=0;f=t.y&&e[1]<=t.y+t.height:n.contain(n.toLocalCoord(e[1]))&&e[0]>=t.y&&e[0]<=t.y+t.height},e.prototype.pointToData=function(e){var t=this.getAxis();return[t.coordToData(t.toLocalCoord(e["horizontal"===t.orient?0:1]))]},e.prototype.dataToPoint=function(e){var t=this.getAxis(),n=this.getRect(),i=[],a="horizontal"===t.orient?0:1;return e instanceof Array&&(e=e[0]),i[a]=t.toGlobalCoord(t.dataToCoord(+e)),i[1-a]=0===a?n.y+n.height/2:n.x+n.width/2,i},e.prototype.convertToPixel=function(e,t,n){return gL(t)===this?this.dataToPoint(n):null},e.prototype.convertFromPixel=function(e,t,n){return gL(t)===this?this.pointToData(n):null}}();function gL(e){var t=e.seriesModel,n=e.singleAxisModel;return n&&n.coordinateSystem||t&&t.coordinateSystem}var fL=["x","y"],yL=["width","height"],vL=(function(e){function t(){return null!==e&&e.apply(this,arguments)||this}ze(t,e),t.prototype.makeElOption=function(e,t,n,i,a){var r=n.axis,o=r.coordinateSystem,s=bL(o,1-xL(r)),l=o.dataToPoint(t)[0],p=i.get("type");if(p&&"none"!==p){var c=eN(i),d=vL[p](r,l,s);d.style=c,e.graphicKey=d.type,e.pointer=d}aN(t,e,lL(n),n,i,a)},t.prototype.getHandleTransform=function(e,t,n){var i=lL(t,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var a=iN(t.axis,e,i);return{x:a[0],y:a[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},t.prototype.updateHandleTransform=function(e,t,n,i){var a=n.axis,r=a.coordinateSystem,o=xL(a),s=bL(r,o),l=[e.x,e.y];l[o]+=t[o],l[o]=Math.min(s[1],l[o]),l[o]=Math.max(s[0],l[o]);var p=bL(r,1-o),c=(p[1]+p[0])/2,d=[c,c];return d[o]=l[o],{x:l[0],y:l[1],rotation:e.rotation,cursorPoint:d,tooltipOption:{verticalAlign:"middle"}}}}(KB),{line:function(e,t,n){return{type:"Line",subPixelOptimize:!0,shape:rN([t,n[0]],[t,n[1]],xL(e))}},shadow:function(e,t,n){var i=e.getBandWidth(),a=n[1]-n[0];return{type:"Rect",shape:oN([t-i/2,n[0]],[i,a],xL(e))}}});function xL(e){return e.isHorizontal()?0:1}function bL(e,t){var n=e.getRect();return[n[fL[t]],n[fL[t]]+n[yL[t]]]}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.type="single"}(hw);!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.init=function(t,n,i){var a=xv(t);e.prototype.init.apply(this,arguments),wL(t,a)},t.prototype.mergeOption=function(t){e.prototype.mergeOption.apply(this,arguments),wL(this.option,t)},t.prototype.getCellSize=function(){return this.option.cellSize},t.type="calendar",t.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{color:"#fff",borderWidth:1,borderColor:"#ccc"},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}}}(Sv);function wL(e,t){var n,i=e.cellSize;1===(n=Ur(i)?i:e.cellSize=[i,i]).length&&(n[1]=n[0]);var a=Br([0,1],(function(e){return function(e,t){return null!=e[uv[t][0]]||null!=e[uv[t][1]]&&null!=e[uv[t][2]]}(t,e)&&(n[e]="auto"),null!=n[e]&&"auto"!==n[e]}));vv(e,t,{type:"box",ignoreSize:a})}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.render=function(e,t,n){var i=this.group;i.removeAll();var a=e.coordinateSystem,r=a.getRangeInfo(),o=a.getOrient(),s=t.getLocaleModel();this._renderDayRect(e,r,i),this._renderLines(e,r,o,i),this._renderYearText(e,r,o,i),this._renderMonthText(e,s,o,i),this._renderWeekText(e,s,r,o,i)},t.prototype._renderDayRect=function(e,t,n){for(var i=e.coordinateSystem,a=e.getModel("itemStyle").getItemStyle(),r=i.getCellWidth(),o=i.getCellHeight(),s=t.start.time;s<=t.end.time;s=i.getNextNDay(s,1).time){var l=i.dataToRect([s],!1).tl,p=new Em({shape:{x:l[0],y:l[1],width:r,height:o},cursor:"default",style:a});n.add(p)}},t.prototype._renderLines=function(e,t,n,i){var a=this,r=e.coordinateSystem,o=e.getModel(["splitLine","lineStyle"]).getLineStyle(),s=e.get(["splitLine","show"]),l=o.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var p=t.start,c=0;p.time<=t.end.time;c++){u(p.formatedDate),0===c&&(p=r.getDateInfo(t.start.y+"-"+t.start.m));var d=p.date;d.setMonth(d.getMonth()+1),p=r.getDateInfo(d)}function u(t){a._firstDayOfMonth.push(r.getDateInfo(t)),a._firstDayPoints.push(r.dataToRect([t],!1).tl);var l=a._getLinePointsOfOneWeek(e,t,n);a._tlpoints.push(l[0]),a._blpoints.push(l[l.length-1]),s&&a._drawSplitline(l,o,i)}u(r.getNextNDay(t.end.time,1).formatedDate),s&&this._drawSplitline(a._getEdgesPoints(a._tlpoints,l,n),o,i),s&&this._drawSplitline(a._getEdgesPoints(a._blpoints,l,n),o,i)},t.prototype._getEdgesPoints=function(e,t,n){var i=[e[0].slice(),e[e.length-1].slice()],a="horizontal"===n?0:1;return i[0][a]=i[0][a]-t/2,i[1][a]=i[1][a]+t/2,i},t.prototype._drawSplitline=function(e,t,n){var i=new Dg({z2:20,shape:{points:e},style:t});n.add(i)},t.prototype._getLinePointsOfOneWeek=function(e,t,n){for(var i=e.coordinateSystem,a=i.getDateInfo(t),r=[],o=0;o<7;o++){var s=i.getNextNDay(a.time,o),l=i.dataToRect([s.time],!1);r[2*s.day]=l.tl,r[2*s.day+1]=l["horizontal"===n?"bl":"tr"]}return r},t.prototype._formatterLabel=function(e,t){return Hr(e)&&e?(n=e,Rr(t,(function(e,t){n=n.replace("{"+t+"}",i?Jo(e):e)})),n):jr(e)?e(t):t.nameMap;var n,i},t.prototype._yearTextPositionControl=function(e,t,n,i,a){var r=t[0],o=t[1],s=["center","bottom"];"bottom"===i?(o+=a,s=["center","top"]):"left"===i?r-=a:"right"===i?(r+=a,s=["center","top"]):o-=a;var l=0;return"left"!==i&&"right"!==i||(l=Math.PI/2),{rotation:l,x:r,y:o,style:{align:s[0],verticalAlign:s[1]}}},t.prototype._renderYearText=function(e,t,n,i){var a=e.getModel("yearLabel");if(a.get("show")){var r=a.get("margin"),o=a.get("position");o||(o="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],l=(s[0][0]+s[1][0])/2,p=(s[0][1]+s[1][1])/2,c="horizontal"===n?0:1,d={top:[l,s[c][1]],bottom:[l,s[1-c][1]],left:[s[1-c][0],p],right:[s[c][0],p]},u=t.start.y;+t.end.y>+t.start.y&&(u=u+"-"+t.end.y);var m=a.get("formatter"),h={start:t.start.y,end:t.end.y,nameMap:u},g=this._formatterLabel(m,h),f=new Pm({z2:30,style:jf(a,{text:g})});f.attr(this._yearTextPositionControl(f,d[o],n,o,r)),i.add(f)}},t.prototype._monthTextPositionControl=function(e,t,n,i,a){var r="left",o="top",s=e[0],l=e[1];return"horizontal"===n?(l+=a,t&&(r="center"),"start"===i&&(o="bottom")):(s+=a,t&&(o="middle"),"start"===i&&(r="right")),{x:s,y:l,align:r,verticalAlign:o}},t.prototype._renderMonthText=function(e,t,n,i){var a=e.getModel("monthLabel");if(a.get("show")){var r=a.get("nameMap"),o=a.get("margin"),s=a.get("position"),l=a.get("align"),p=[this._tlpoints,this._blpoints];r&&!Hr(r)||(r&&(t=Cy(r)||t),r=t.get(["time","monthAbbr"])||[]);var c="start"===s?0:1,d="horizontal"===n?0:1;o="start"===s?-o:o;for(var u="center"===l,m=0;m=i.start.time&&n.timeo.end.time&&e.reverse(),e},e.prototype._getRangeInfo=function(e){var t,n=[this.getDateInfo(e[0]),this.getDateInfo(e[1])];n[0].time>n[1].time&&(t=!0,n.reverse());var i=Math.floor(n[1].time/SL)-Math.floor(n[0].time/SL)+1,a=new Date(n[0].time),r=a.getDate(),o=n[1].date.getDate();a.setDate(r+i-1);var s=a.getDate();if(s!==o)for(var l=a.getTime()-n[1].time>0?1:-1;(s=a.getDate())!==o&&(a.getTime()-n[1].time)*l>0;)i-=l,a.setDate(s-l);var p=Math.floor((i+n[0].day+6)/7),c=t?1-p:p-1;return t&&n.reverse(),{range:[n[0].formatedDate,n[1].formatedDate],start:n[0],end:n[1],allDay:i,weeks:p,nthWeek:c,fweek:n[0].day,lweek:n[1].day}},e.prototype._getDateByWeeksAndDay=function(e,t,n){var i=this._getRangeInfo(n);if(e>i.weeks||0===e&&ti.lweek)return null;var a=7*(e-1)-i.fweek+t,r=new Date(i.start.time);return r.setDate(+i.start.d+a),this.getDateInfo(r)},e.create=function(t,n){var i=[];return t.eachComponent("calendar",(function(a){var r=new e(a,t,n);i.push(r),a.coordinateSystem=r})),t.eachSeries((function(e){"calendar"===e.get("coordinateSystem")&&(e.coordinateSystem=i[e.get("calendarIndex")||0])})),i},e.dimensions=["time","value"]}();function CL(e){var t=e.calendarModel,n=e.seriesModel;return t?t.coordinateSystem:n?n.coordinateSystem:null}function _L(e,t){var n;return Rr(t,(function(t){null!=e[t]&&"auto"!==e[t]&&(n=!0)})),n}var TL=["transition","enterFrom","leaveTo"],IL=TL.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function EL(e,t,n){if(n&&(!e[n]&&t[n]&&(e[n]={}),e=e[n],t=t[n]),e&&t)for(var i=n?TL:IL,a=0;a=0;l--){var u,m,h;if(h=null!=(m=xd((u=n[l]).id,null))?a.get(m):null){var g=h.parent,f=(d=kL(g),{}),y=fv(h,u,g===i?{width:r,height:o}:{width:d.width,height:d.height},null,{hv:u.hv,boundingMode:u.bounding},f);if(!kL(h).isNew&&y){for(var v=u.transition,x={},b=0;b=0)?x[w]=S:h[w]=S}tf(h,x,e,0)}else h.attr(f)}}},t.prototype._clear=function(){var e=this,t=this._elMap;t.each((function(n){OL(n,kL(n).option,t,e._lastGraphicModel)})),this._elMap=fo()},t.prototype.dispose=function(){this._clear()},t.type="graphic"}(hw);function PL(e){var t=bo(ML,e)?ML[e]:ff(e);var n=new t({});return kL(n).type=e,n}function DL(e,t,n,i){var a=PL(n);return t.add(a),i.set(e,a),kL(a).id=e,kL(a).isNew=!0,a}function OL(e,t,n,i){e&&e.parent&&("group"===e.type&&e.traverse((function(e){OL(e,t,n,i)})),nB(e,t,i),n.removeKey(kL(e).id))}function AL(e,t,n,i){e.isGroup||Rr([["cursor",mu.prototype.cursor],["zlevel",i||0],["z",n||0],["z2",0]],(function(n){var i=n[0];bo(t,i)?e[i]=io(t[i],n[1]):null==e[i]&&(e[i]=n[1])})),Rr(qr(t),(function(n){if(0===n.indexOf("on")){var i=t[n];e[n]=jr(i)?i:null}})),bo(t,"draggable")&&(e.draggable=t.draggable),null!=t.name&&(e.name=t.name),null!=t.id&&(e.id=t.id)}var FL=["x","y","radius","angle","single"],RL=["cartesian2d","polar","singleAxis"];function BL(e){return e+"Axis"}function NL(e,t){var n,i=fo(),a=[],r=fo();e.eachComponent({mainType:"dataZoom",query:t},(function(e){r.get(e.uid)||s(e)}));do{n=!1,e.eachComponent("dataZoom",o)}while(n);function o(e){!r.get(e.uid)&&function(e){var t=!1;return e.eachTargetAxis((function(e,n){var a=i.get(e);a&&a[n]&&(t=!0)})),t}(e)&&(s(e),n=!0)}function s(e){r.set(e.uid,!0),a.push(e),e.eachTargetAxis((function(e,t){(i.get(e)||i.set(e,[]))[t]=!0}))}return a}function LL(e){var t=e.ecModel,n={infoList:[],infoMap:fo()};return e.eachTargetAxis((function(e,i){var a=t.getComponent(BL(e),i);if(a){var r=a.getCoordSysModel();if(r){var o=r.uid,s=n.infoMap.get(o);s||(s={model:r,axisModels:[]},n.infoList.push(s),n.infoMap.set(o,s)),s.axisModels.push(a)}}})),n}var VL=function(){function e(){this.indexList=[],this.indexMap=[]}return e.prototype.add=function(e){this.indexMap[e]||(this.indexList.push(e),this.indexMap[e]=!0)},e}(),qL=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._autoThrottle=!0,n._noTarget=!0,n._rangePropMode=["percent","percent"],n}return ze(t,e),t.prototype.init=function(e,t,n){var i=GL(e);this.settledOption=i,this.mergeDefaultAndTheme(e,n),this._doInit(i)},t.prototype.mergeOption=function(e){var t=GL(e);Er(this.option,e,!0),Er(this.settledOption,t,!0),this._doInit(t)},t.prototype._doInit=function(e){var t=this.option;this._setDefaultThrottle(e),this._updateRangeUse(e);var n=this.settledOption;Rr([["start","startValue"],["end","endValue"]],(function(e,i){"value"===this._rangePropMode[i]&&(t[e[0]]=n[e[0]]=null)}),this),this._resetTarget()},t.prototype._resetTarget=function(){var e=this.get("orient",!0),t=this._targetAxisInfoMap=fo();this._fillSpecifiedTargetAxis(t)?this._orient=e||this._makeAutoOrientByTargetAxis():(this._orient=e||"horizontal",this._fillAutoTargetAxisByOrient(t,this._orient)),this._noTarget=!0,t.each((function(e){e.indexList.length&&(this._noTarget=!1)}),this)},t.prototype._fillSpecifiedTargetAxis=function(e){var t=!1;return Rr(FL,(function(n){var i=this.getReferringComponents(BL(n),Md);if(i.specified){t=!0;var a=new VL;Rr(i.models,(function(e){a.add(e.componentIndex)})),e.set(n,a)}}),this),t},t.prototype._fillAutoTargetAxisByOrient=function(e,t){var n=this.ecModel,i=!0;if(i){var a="vertical"===t?"y":"x";r(n.findComponents({mainType:a+"Axis"}),a)}i&&r(n.findComponents({mainType:"singleAxis",filter:function(e){return e.get("orient",!0)===t}}),"single");function r(t,n){var a=t[0];if(a){var r=new VL;if(r.add(a.componentIndex),e.set(n,r),i=!1,"x"===n||"y"===n){var o=a.getReferringComponents("grid",Ed).models[0];o&&Rr(t,(function(e){a.componentIndex!==e.componentIndex&&o===e.getReferringComponents("grid",Ed).models[0]&&r.add(e.componentIndex)}))}}}i&&Rr(FL,(function(t){if(i){var a=n.findComponents({mainType:BL(t),filter:function(e){return"category"===e.get("type",!0)}});if(a[0]){var r=new VL;r.add(a[0].componentIndex),e.set(t,r),i=!1}}}),this)},t.prototype._makeAutoOrientByTargetAxis=function(){var e;return this.eachTargetAxis((function(t){!e&&(e=t)}),this),"y"===e?"vertical":"horizontal"},t.prototype._setDefaultThrottle=function(e){if(e.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var t=this.ecModel.option;this.option.throttle=t.animation&&t.animationDurationUpdate>0?100:20}},t.prototype._updateRangeUse=function(e){var t=this._rangePropMode,n=this.get("rangeMode");Rr([["start","startValue"],["end","endValue"]],(function(i,a){var r=null!=e[i[0]],o=null!=e[i[1]];r&&!o?t[a]="percent":!r&&o?t[a]="value":n?t[a]=n[a]:r&&(t[a]="percent")}))},t.prototype.noTarget=function(){return this._noTarget},t.prototype.getFirstTargetAxisModel=function(){var e;return this.eachTargetAxis((function(t,n){null==e&&(e=this.ecModel.getComponent(BL(t),n))}),this),e},t.prototype.eachTargetAxis=function(e,t){this._targetAxisInfoMap.each((function(n,i){Rr(n.indexList,(function(n){e.call(t,i,n)}))}))},t.prototype.getAxisProxy=function(e,t){var n=this.getAxisModel(e,t);if(n)return n.__dzAxisProxy},t.prototype.getAxisModel=function(e,t){var n=this._targetAxisInfoMap.get(e);if(n&&n.indexMap[t])return this.ecModel.getComponent(BL(e),t)},t.prototype.setRawRange=function(e){var t=this.option,n=this.settledOption;Rr([["start","startValue"],["end","endValue"]],(function(i){null==e[i[0]]&&null==e[i[1]]||(t[i[0]]=n[i[0]]=e[i[0]],t[i[1]]=n[i[1]]=e[i[1]])}),this),this._updateRangeUse(e)},t.prototype.setCalculatedRange=function(e){var t=this.option;Rr(["start","startValue","end","endValue"],(function(n){t[n]=e[n]}))},t.prototype.getPercentRange=function(){var e=this.findRepresentativeAxisProxy();if(e)return e.getDataPercentWindow()},t.prototype.getValueRange=function(e,t){var n;if(null==e&&null==t){if(n=this.findRepresentativeAxisProxy())return n.getDataValueWindow()}else if(n=this.getAxisProxy(e,t))return n.getDataValueWindow()},t.prototype.findRepresentativeAxisProxy=function(e){if(e)return e.__dzAxisProxy;for(var t,n=this._targetAxisInfoMap.keys(),i=0;i=0}(t)){var n=BL(this._dimName),i=t.getReferringComponents(n,Ed).models[0];i&&this._axisIndex===i.componentIndex&&e.push(t)}}),this),e},e.prototype.getAxisModel=function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},e.prototype.getMinMaxSpan=function(){return Ir(this._minMaxSpan)},e.prototype.calculateDataWindow=function(e){var t,n=this._dataExtent,i=this.getAxisModel().axis.scale,a=this._dataZoomModel.getRangePropMode(),r=[0,100],o=[],s=[];UL(["start","end"],(function(l,p){var c=e[l],d=e[l+"Value"];"percent"===a[p]?(null==c&&(c=r[p]),d=i.parse(Lc(c,r,n))):(t=!0,c=Lc(d=null==d?n[p]:i.parse(d),n,r)),s[p]=null==d||isNaN(d)?n[p]:d,o[p]=null==c||isNaN(c)?r[p]:c})),jL(s),jL(o);var l=this._minMaxSpan;function p(e,t,n,a,r){var o=r?"Span":"ValueSpan";bA(0,e,n,"all",l["min"+o],l["max"+o]);for(var s=0;s<2;s++)t[s]=Lc(e[s],n,a,!0),r&&(t[s]=i.parse(t[s]))}return t?p(s,o,n,r,!1):p(o,s,r,n,!0),{valueWindow:s,percentWindow:o}},e.prototype.reset=function(e){if(e===this._dataZoomModel){var t=this.getTargetSeriesModels();this._dataExtent=function(e,t,n){var i=[1/0,-1/0];UL(n,(function(e){!function(e,t,n){t&&Rr(eI(t,n),(function(n){var i=t.getApproximateExtent(n);i[0]e[1]&&(e[1]=i[1])}))}(i,e.getData(),t)}));var a=e.getAxisModel(),r=jT(a.axis.scale,a,i).calculate();return[r.min,r.max]}(this,this._dimName,t),this._updateMinMaxSpan();var n=this.calculateDataWindow(e.settledOption);this._valueWindow=n.valueWindow,this._percentWindow=n.percentWindow,this._setAxisModel()}},e.prototype.filterData=function(e,t){if(e===this._dataZoomModel){var n=this._dimName,i=this.getTargetSeriesModels(),a=e.get("filterMode"),r=this._valueWindow;"none"!==a&&UL(i,(function(e){var t=e.getData(),i=t.mapDimensionsAll(n);if(i.length){if("weakFilter"===a){var o=t.getStore(),s=Br(i,(function(e){return t.getDimensionIndex(e)}),t);t.filterSelf((function(e){for(var t,n,a,l=0;lr[1];if(c&&!d&&!u)return!0;c&&(a=!0),d&&(t=!0),u&&(n=!0)}return a&&t&&n}))}else UL(i,(function(n){if("empty"===a)e.setData(t=t.map(n,(function(e){return function(e){return e>=r[0]&&e<=r[1]}(e)?e:NaN})));else{var i={};i[n]=r,t.selectRange(i)}}));UL(i,(function(e){t.setApproximateExtent(r,e)}))}}))}},e.prototype._updateMinMaxSpan=function(){var e=this._minMaxSpan={},t=this._dataZoomModel,n=this._dataExtent;UL(["min","max"],(function(i){var a=t.get(i+"Span"),r=t.get(i+"ValueSpan");null!=r&&(r=this.getAxisModel().axis.scale.parse(r)),null!=r?a=Lc(n[0]+r,n,[0,100],!0):null!=a&&(r=Lc(a,[0,100],n,!0)-n[0]),e[i+"Span"]=a,e[i+"ValueSpan"]=r}),this)},e.prototype._setAxisModel=function(){var e=this.getAxisModel(),t=this._percentWindow,n=this._valueWindow;if(t){var i=jc(n,[0,500]);i=Math.min(i,20);var a=e.axis.scale.rawExtentInfo;0!==t[0]&&a.setDeterminedMinMax("min",+n[0].toFixed(i)),100!==t[1]&&a.setDeterminedMinMax("max",+n[1].toFixed(i)),a.freeze()}},e}();var WL={getTargetSeries:function(e){function t(t){e.eachComponent("dataZoom",(function(n){n.eachTargetAxis((function(i,a){var r=e.getComponent(BL(i),a);t(i,a,r,n)}))}))}t((function(e,t,n,i){n.__dzAxisProxy=null}));var n=[];t((function(t,i,a,r){a.__dzAxisProxy||(a.__dzAxisProxy=new HL(t,i,r,e),n.push(a.__dzAxisProxy))}));var i=fo();return Rr(n,(function(e){Rr(e.getTargetSeriesModels(),(function(e){i.set(e.uid,e)}))})),i},overallReset:function(e,t){e.eachComponent("dataZoom",(function(e){e.eachTargetAxis((function(t,n){var i=e.getAxisProxy(t,n);i&&i.reset(e)})),e.eachTargetAxis((function(n,i){var a=e.getAxisProxy(n,i);a&&a.filterData(e,t)}))})),e.eachComponent("dataZoom",(function(e){var t=e.findRepresentativeAxisProxy();if(t){var n=t.getDataPercentWindow(),i=t.getDataValueWindow();e.setCalculatedRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]})}}))}};var $L=!1;function KL(e){$L||($L=!0,e.registerProcessor(e.PRIORITY.PROCESSOR.FILTER,WL),function(e){e.registerAction("dataZoom",(function(e,t){Rr(NL(t,e),(function(t){t.setRawRange({start:e.start,end:e.end,startValue:e.startValue,endValue:e.endValue})}))}))}(e),e.registerSubTypeDefaulter("dataZoom",(function(){return"slider"})))}var YL=function(){},XL={};function ZL(e){return XL[e]}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.optionUpdated=function(){e.prototype.optionUpdated.apply(this,arguments);var t=this.ecModel;Rr(this.option.feature,(function(e,n){var i=ZL(n);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(t)),Er(e,i.defaultOption))}))},t.type="toolbox",t.layoutMode={type:"box",ignoreSize:!0},t.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}},tooltip:{show:!1,position:"bottom"}}}(Sv);function QL(e,t){var n=nv(t.get("padding")),i=t.getItemStyle(["color","opacity"]);return i.fill=t.get("backgroundColor"),e=new Em({shape:{x:e.x-n[3],y:e.y-n[0],width:e.width+n[1]+n[3],height:e.height+n[0]+n[2],r:t.get("borderRadius")},style:i,silent:!0,z2:-1})}!function(e){function t(){return null!==e&&e.apply(this,arguments)||this}ze(t,e),t.prototype.render=function(e,t,n,i){var a=this.group;if(a.removeAll(),e.get("show")){var r=+e.get("itemSize"),o="vertical"===e.get("orient"),s=e.get("feature")||{},l=this._features||(this._features={}),p=[];Rr(s,(function(e,t){p.push(t)})),new g_(this._featureNames||[],p).add(c).update(c).remove(zr(c,null)).execute(),this._featureNames=p,function(e,t,n){var i=t.getBoxLayoutParams(),a=t.get("padding"),r={width:n.getWidth(),height:n.getHeight()},o=gv(i,r,a);hv(t.get("orient"),e,t.get("itemGap"),o.width,o.height),fv(e,i,r,a)}(a,e,n),a.add(QL(a.getBoundingRect(),e)),o||a.eachChild((function(e){var t=e.__title,i=e.ensureState("emphasis"),o=i.textConfig||(i.textConfig={}),s=e.getTextContent(),l=s&&s.ensureState("emphasis");if(l&&!jr(l)&&t){var p=l.style||(l.style={}),c=uc(t,Pm.makeFont(p)),d=e.x+a.x,u=!1;e.y+a.y+r+c.height>n.getHeight()&&(o.position="top",u=!0);var m=u?-5-c.height:r+10;d+c.width/2>n.getWidth()?(o.position=["100%",m],p.align="right"):d-c.width/2<0&&(o.position=[0,m],p.align="left")}}))}function c(c,d){var u,m=p[c],h=p[d],g=s[m],f=new uy(g,e,e.ecModel);if(i&&null!=i.newTitle&&i.featureName===m&&(g.title=i.newTitle),m&&!h){if(function(e){return 0===e.indexOf("my")}(m))u={onclick:f.option.onclick,featureName:m};else{var y=ZL(m);if(!y)return;u=new y}l[m]=u}else if(!(u=l[h]))return;u.uid=hy("toolbox-feature"),u.model=f,u.ecModel=t,u.api=n;var v=u instanceof YL;m||!h?!f.get("show")||v&&u.unusable?v&&u.remove&&u.remove(t,n):(!function(i,s,l){var p,c,d=i.getModel("iconStyle"),u=i.getModel(["emphasis","iconStyle"]),m=s instanceof YL&&s.getIcons?s.getIcons():i.get("icon"),h=i.get("title")||{};Hr(m)?(p={})[l]=m:p=m;Hr(h)?(c={})[l]=h:c=h;var g=i.iconPaths={};Rr(p,(function(l,p){var m=Df(l,{},{x:-r/2,y:-r/2,width:r,height:r});m.setStyle(d.getItemStyle()),m.ensureState("emphasis").style=u.getItemStyle();var h=new Pm({style:{text:c[p],align:u.get("textAlign"),borderRadius:u.get("textBorderRadius"),padding:u.get("textPadding"),fill:null,font:Xf({fontStyle:u.get("textFontStyle"),fontFamily:u.get("textFontFamily"),fontSize:u.get("textFontSize"),fontWeight:u.get("textFontWeight")},t)},ignore:!0});m.setTextContent(h),Rf({el:m,componentModel:e,itemName:p,formatterParamsExtra:{title:c[p]}}),m.__title=c[p],m.on("mouseover",(function(){var t=u.getItemStyle(),i=o?null==e.get("right")&&"right"!==e.get("left")?"right":"left":null==e.get("bottom")&&"bottom"!==e.get("top")?"bottom":"top";h.setStyle({fill:u.get("textFill")||t.fill||t.stroke||"#000",backgroundColor:u.get("textBackgroundColor")}),m.setTextConfig({position:u.get("textPosition")||i}),h.ignore=!e.get("showTitle"),n.enterEmphasis(this)})).on("mouseout",(function(){"emphasis"!==i.get(["iconStatus",p])&&n.leaveEmphasis(this),h.hide()})),("emphasis"===i.get(["iconStatus",p])?vh:xh)(m),a.add(m),m.on("click",Gr(s.onclick,s,t,n,p)),g[p]=m}))}(f,u,m),f.setIconStatus=function(e,t){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[e]=t,i[e]&&("emphasis"===t?vh:xh)(i[e])},u instanceof YL&&u.render&&u.render(f,t,n,i)):v&&u.dispose&&u.dispose(t,n)}},t.prototype.updateView=function(e,t,n,i){Rr(this._features,(function(e){e instanceof YL&&e.updateView&&e.updateView(e.model,t,n,i)}))},t.prototype.remove=function(e,t){Rr(this._features,(function(n){n instanceof YL&&n.remove&&n.remove(e,t)})),this.group.removeAll()},t.prototype.dispose=function(e,t){Rr(this._features,(function(n){n instanceof YL&&n.dispose&&n.dispose(e,t)}))},t.type="toolbox"}(hw);!function(e){function t(){return null!==e&&e.apply(this,arguments)||this}ze(t,e),t.prototype.onclick=function(e,t){var n=this.model,i=n.get("name")||e.get("title.0.text")||"echarts",a="svg"===t.getZr().painter.getType(),r=a?"svg":n.get("type",!0)||"png",o=t.getConnectedDataURL({type:r,backgroundColor:n.get("backgroundColor",!0)||e.get("backgroundColor")||"#fff",connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),s=rr.browser;if(jr(MouseEvent)&&(s.newEdge||!s.ie&&!s.edge)){var l=document.createElement("a");l.download=i+"."+r,l.target="_blank",l.href=o;var p=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});l.dispatchEvent(p)}else if(window.navigator.msSaveOrOpenBlob||a){var c=o.split(","),d=c[0].indexOf("base64")>-1,u=a?decodeURIComponent(c[1]):c[1];d&&(u=window.atob(u));var m=i+"."+r;if(window.navigator.msSaveOrOpenBlob){for(var h=u.length,g=new Uint8Array(h);h--;)g[h]=u.charCodeAt(h);var f=new Blob([g]);window.navigator.msSaveOrOpenBlob(f,m)}else{var y=document.createElement("iframe");document.body.appendChild(y);var v=y.contentWindow,x=v.document;x.open("image/svg+xml","replace"),x.write(u),x.close(),v.focus(),x.execCommand("SaveAs",!0,m),document.body.removeChild(y)}}else{var b=n.get("lang"),w='',S=window.open();S.document.write(w),S.document.title=i}},t.getDefaultOption=function(e){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:e.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:e.getLocaleModel().get(["toolbox","saveAsImage","lang"])}}}(YL);var JL="__ec_magicType_stack__",eV=[["line","bar"],["stack"]],tV=(function(e){function t(){return null!==e&&e.apply(this,arguments)||this}ze(t,e),t.prototype.getIcons=function(){var e=this.model,t=e.get("icon"),n={};return Rr(e.get("type"),(function(e){t[e]&&(n[e]=t[e])})),n},t.getDefaultOption=function(e){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:e.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},t.prototype.onclick=function(e,t,n){var i=this.model,a=i.get(["seriesIndex",n]);if(tV[n]){var r,o={series:[]};Rr(eV,(function(e){Dr(e,n)>=0&&Rr(e,(function(e){i.setIconStatus(e,"normal")}))})),i.setIconStatus(n,"emphasis"),e.eachComponent({mainType:"series",query:null==a?null:{seriesIndex:a}},(function(e){var t=e.subType,a=e.id,r=tV[n](t,a,e,i);r&&(Pr(r,e.option),o.series.push(r));var s=e.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var p=l.dim+"Axis",c=e.getReferringComponents(p,Ed).models[0].componentIndex;o[p]=o[p]||[];for(var d=0;d<=c;d++)o[p][c]=o[p][c]||{};o[p][c].boundaryGap="bar"===n}}}));var s=n;"stack"===n&&(r=Er({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),t.dispatchAction({type:"changeMagicType",currentType:s,newOption:o,newTitle:r,featureName:"magicType"})}}}(YL),{line:function(e,t,n,i){if("bar"===e)return Er({id:t,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(e,t,n,i){if("line"===e)return Er({id:t,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(e,t,n,i){var a=n.get("stack")===JL;if("line"===e||"bar"===e)return i.setIconStatus("stack",a?"normal":"emphasis"),Er({id:t,stack:a?"":JL},i.get(["option","stack"])||{},!0)}});s_({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},(function(e,t){t.mergeOption(e.newOption)}));var nV=new Array(60).join("-"),iV="\t";function aV(e){return e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var rV=new RegExp("[\t]+","g");function oV(e,t){var n=e.split(new RegExp("\n*"+nV+"\n*","g")),i={series:[]};return Rr(n,(function(e,n){if(function(e){if(e.slice(0,e.indexOf("\n")).indexOf(iV)>=0)return!0}(e)){var a=function(e){for(var t=e.split(/\n+/g),n=[],i=Br(aV(t.shift()).split(rV),(function(e){return{name:e,data:[]}})),a=0;a=0)&&e(a,i._targetInfoList)}))}return e.prototype.setOutputRanges=function(e,t){return this.matchOutputRanges(e,t,(function(e,t,n){if((e.coordRanges||(e.coordRanges=[])).push(t),!e.coordRange){e.coordRange=t;var i=vV[e.brushType](0,n,t);e.__rangeOffset={offset:bV[e.brushType](i.values,e.range,[1,1]),xyMinMax:i.xyMinMax}}})),e},e.prototype.matchOutputRanges=function(e,t,n){Rr(e,(function(e){var i=this.findTargetInfo(e,t);i&&!0!==i&&Rr(i.coordSyses,(function(i){var a=vV[e.brushType](1,i,e.range,!0);n(e,a.values,i,t)}))}),this)},e.prototype.setInputRanges=function(e,t){Rr(e,(function(e){var n,i,a,r,o,s=this.findTargetInfo(e,t);if(e.range=e.range||[],s&&!0!==s){e.panelId=s.panelId;var l=vV[e.brushType](0,s.coordSys,e.coordRange),p=e.__rangeOffset;e.range=p?bV[e.brushType](l.values,p.offset,(n=l.xyMinMax,i=p.xyMinMax,a=SV(n),r=SV(i),o=[a[0]/r[0],a[1]/r[1]],isNaN(o[0])&&(o[0]=1),isNaN(o[1])&&(o[1]=1),o)):l.values}}),this)},e.prototype.makePanelOpts=function(e,t){return Br(this._targetInfoList,(function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:t?t(n):null,clipPath:bF(i),isTargetByCursor:SF(i,e,n.coordSysModel),getLinearBrushOtherExtent:wF(i)}}))},e.prototype.controlSeries=function(e,t,n){var i=this.findTargetInfo(e,n);return!0===i||i&&Dr(i.coordSyses,t.coordinateSystem)>=0},e.prototype.findTargetInfo=function(e,t){for(var n=this._targetInfoList,i=hV(t,e),a=0;ae[1]&&e.reverse(),e}function hV(e,t){return Td(e,t,{includeMainTypes:dV})}var gV={grid:function(e,t){var n=e.xAxisModels,i=e.yAxisModels,a=e.gridModels,r=fo(),o={},s={};(n||i||a)&&(Rr(n,(function(e){var t=e.axis.grid.model;r.set(t.id,t),o[t.id]=!0})),Rr(i,(function(e){var t=e.axis.grid.model;r.set(t.id,t),s[t.id]=!0})),Rr(a,(function(e){r.set(e.id,e),o[e.id]=!0,s[e.id]=!0})),r.each((function(e){var a=e.coordinateSystem,r=[];Rr(a.getCartesians(),(function(e,t){(Dr(n,e.getAxis("x").model)>=0||Dr(i,e.getAxis("y").model)>=0)&&r.push(e)})),t.push({panelId:"grid--"+e.id,gridModel:e,coordSysModel:e,coordSys:r[0],coordSyses:r,getPanelRect:yV.grid,xAxisDeclared:o[e.id],yAxisDeclared:s[e.id]})})))},geo:function(e,t){Rr(e.geoModels,(function(e){var n=e.coordinateSystem;t.push({panelId:"geo--"+e.id,geoModel:e,coordSysModel:e,coordSys:n,coordSyses:[n],getPanelRect:yV.geo})}))}},fV=[function(e,t){var n=e.xAxisModel,i=e.yAxisModel,a=e.gridModel;return!a&&n&&(a=n.axis.grid.model),!a&&i&&(a=i.axis.grid.model),a&&a===t.gridModel},function(e,t){var n=e.geoModel;return n&&n===t.geoModel}],yV={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var e=this.coordSys,t=e.getBoundingRect().clone();return t.applyTransform(_f(e)),t}},vV={lineX:zr(xV,0),lineY:zr(xV,1),rect:function(e,t,n,i){var a=e?t.pointToData([n[0][0],n[1][0]],i):t.dataToPoint([n[0][0],n[1][0]],i),r=e?t.pointToData([n[0][1],n[1][1]],i):t.dataToPoint([n[0][1],n[1][1]],i),o=[mV([a[0],r[0]]),mV([a[1],r[1]])];return{values:o,xyMinMax:o}},polygon:function(e,t,n,i){var a=[[1/0,-1/0],[1/0,-1/0]];return{values:Br(n,(function(n){var r=e?t.pointToData(n,i):t.dataToPoint(n,i);return a[0][0]=Math.min(a[0][0],r[0]),a[1][0]=Math.min(a[1][0],r[1]),a[0][1]=Math.max(a[0][1],r[0]),a[1][1]=Math.max(a[1][1],r[1]),r})),xyMinMax:a}}};function xV(e,t,n,i){var a=n.getAxis(["x","y"][e]),r=mV(Br([0,1],(function(e){return t?a.coordToData(a.toLocalCoord(i[e]),!0):a.toGlobalCoord(a.dataToCoord(i[e]))}))),o=[];return o[e]=r,o[1-e]=[NaN,NaN],{values:r,xyMinMax:o}}var bV={lineX:zr(wV,0),lineY:zr(wV,1),rect:function(e,t,n){return[[e[0][0]-n[0]*t[0][0],e[0][1]-n[0]*t[0][1]],[e[1][0]-n[1]*t[1][0],e[1][1]-n[1]*t[1][1]]]},polygon:function(e,t,n){return Br(e,(function(e,i){return[e[0]-n[0]*t[i][0],e[1]-n[1]*t[i][1]]}))}};function wV(e,t,n,i){return[t[0]-i[e]*n[0],t[1]-i[e]*n[1]]}function SV(e){return e?[e[0][1]-e[0][0],e[1][1]-e[1][0]]:[NaN,NaN]}var CV,_V,TV=Rr,IV=cd+"toolbox-dataZoom_",EV=(function(e){function t(){return null!==e&&e.apply(this,arguments)||this}ze(t,e),t.prototype.render=function(e,t,n,i){this._brushController||(this._brushController=new UA(n.getZr()),this._brushController.on("brush",Gr(this._onBrush,this)).mount()),function(e,t,n,i,a){var r=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(r="dataZoomSelect"===i.key&&i.dataZoomSelectActive);n._isZoomActive=r,e.setIconStatus("zoom",r?"emphasis":"normal");var o=new uV(MV(e),t,{include:["grid"]}),s=o.makePanelOpts(a,(function(e){return e.xAxisDeclared&&!e.yAxisDeclared?"lineX":!e.xAxisDeclared&&e.yAxisDeclared?"lineY":"rect"}));n._brushController.setPanels(s).enableBrush(!(!r||!s.length)&&{brushType:"auto",brushStyle:e.getModel("brushStyle").getItemStyle()})}(e,t,this,i,n),function(e,t){e.setIconStatus("back",function(e){return cV(e).length}(t)>1?"emphasis":"normal")}(e,t)},t.prototype.onclick=function(e,t,n){EV[n].call(this)},t.prototype.remove=function(e,t){this._brushController&&this._brushController.unmount()},t.prototype.dispose=function(e,t){this._brushController&&this._brushController.dispose()},t.prototype._onBrush=function(e){var t=e.areas;if(e.isEnd&&t.length){var n={},i=this.ecModel;this._brushController.updateCovers([]),new uV(MV(this.model),i,{include:["grid"]}).matchOutputRanges(t,i,(function(e,t,n){if("cartesian2d"===n.type){var i=e.brushType;"rect"===i?(a("x",n,t[0]),a("y",n,t[1])):a({lineX:"x",lineY:"y"}[i],n,t)}})),function(e,t){var n=cV(e);lV(t,(function(t,i){for(var a=n.length-1;a>=0&&!n[a][i];a--);if(a<0){var r=e.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(r){var o=r.getPercentRange();n[0][i]={dataZoomId:i,start:o[0],end:o[1]}}}})),n.push(t)}(i,n),this._dispatchZoomAction(n)}function a(e,t,a){var r=t.getAxis(e),o=r.model,s=function(e,t,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},(function(n){n.getAxisModel(e,t.componentIndex)&&(i=n)})),i}(e,o,i),l=s.findRepresentativeAxisProxy(o);if(l){var p=l.getMinMaxSpan();null==p.minValueSpan&&null==p.maxValueSpan||(a=bA(0,a.slice(),r.scale.getExtent(),0,p.minValueSpan,p.maxValueSpan))}s&&(n[s.id]={dataZoomId:s.id,startValue:a[0],endValue:a[1]})}},t.prototype._dispatchZoomAction=function(e){var t=[];TV(e,(function(e,n){t.push(Ir(e))})),t.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:t})},t.getDefaultOption=function(e){return{show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:e.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:"rgba(210,219,238,0.2)"}}}}(YL),{zoom:function(){var e=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:e})},back:function(){this._dispatchZoomAction(function(e){var t=cV(e),n=t[t.length-1];t.length>1&&t.pop();var i={};return lV(n,(function(e,n){for(var a=t.length-1;a>=0;a--)if(e=t[a][n]){i[n]=e;break}})),i}(this.ecModel))}});function MV(e){var t={xAxisIndex:e.get("xAxisIndex",!0),yAxisIndex:e.get("yAxisIndex",!0),xAxisId:e.get("xAxisId",!0),yAxisId:e.get("yAxisId",!0)};return null==t.xAxisIndex&&null==t.xAxisId&&(t.xAxisIndex="all"),null==t.yAxisIndex&&null==t.yAxisId&&(t.yAxisIndex="all"),t}CV="dataZoom",_V=function(e){var t=e.getComponent("toolbox",0),n=["feature","dataZoom"];if(t&&null!=t.get(n)){var i=t.getModel(n),a=[],r=Td(e,MV(i));return TV(r.xAxisModels,(function(e){return o(e,"xAxis","xAxisIndex")})),TV(r.yAxisModels,(function(e){return o(e,"yAxis","yAxisIndex")})),a}function o(e,t,n){var r=e.componentIndex,o={type:"select",$fromToolbox:!0,filterMode:i.get("filterMode",!0)||"filter",id:IV+t+r};o[n]=r,a.push(o)}},so(null==jv.get(CV)&&_V),jv.set(CV,_V);var kV=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="tooltip",t.dependencies=["axisPointer"],t.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"#fff",shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#666",fontSize:14}},t}(Sv);function PV(e){var t=e.get("confine");return null!=t?!!t:"richText"===e.get("renderMode")}function DV(e){if(rr.domSupported)for(var t=document.documentElement.style,n=0,i=e.length;n-1?(p+="top:50%",c+="translateY(-50%) rotate("+(o="left"===s?-225:-45)+"deg)"):(p+="left:50%",c+="translateX(-50%) rotate("+(o="top"===s?225:45)+"deg)");var d=o*Math.PI/180,u=l+a,m=u*Math.abs(Math.cos(d))+u*Math.abs(Math.sin(d)),h=t+" solid "+a+"px;";return'
'}(n,i,a)),Hr(e))r.innerHTML=e+o;else if(e){r.innerHTML="",Ur(e)||(e=[e]);for(var s=0;s=0?this._tryShow(n,i):"leave"===t&&this._hide(i))}),this))},t.prototype._keepShow=function(){var e=this._tooltipModel,t=this._ecModel,n=this._api,i=e.get("triggerOn");if(null!=this._lastX&&null!=this._lastY&&"none"!==i&&"click"!==i){var a=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout((function(){!n.isDisposed()&&a.manuallyShowTip(e,t,n,{x:a._lastX,y:a._lastY,dataByCoordSys:a._lastDataByCoordSys})}))}},t.prototype.manuallyShowTip=function(e,t,n,i){if(i.from!==this.uid&&!rr.node&&n.getDom()){var a=KV(i,n);this._ticket="";var r=i.dataByCoordSys,o=function(e,t,n){var i=Id(e).queryOptionMap,a=i.keys()[0];if(!a||"series"===a)return;var r=kd(t,a,i.get(a),{useDefault:!1,enableAll:!1,enableNone:!1}),o=r.models[0];if(!o)return;var s,l=n.getViewOfComponentModel(o);if(l.group.traverse((function(t){var n=Um(t).tooltipConfig;if(n&&n.name===e.name)return s=t,!0})),s)return{componentMainType:a,componentIndex:o.componentIndex,el:s}}(i,t,n);if(o){var s=o.el.getBoundingRect().clone();s.applyTransform(o.el.transform),this._tryShow({offsetX:s.x+s.width/2,offsetY:s.y+s.height/2,target:o.el,position:i.position,positionDefault:"bottom"},a)}else if(i.tooltip&&null!=i.x&&null!=i.y){var l=HV;l.x=i.x,l.y=i.y,l.update(),Um(l).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:l},a)}else if(r)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:r,tooltipOption:i.tooltipOption},a);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(e,t,n,i))return;var p=bN(i,t),c=p.point[0],d=p.point[1];null!=c&&null!=d&&this._tryShow({offsetX:c,offsetY:d,target:p.el,position:i.position,positionDefault:"bottom"},a)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},a))}},t.prototype.manuallyHideTip=function(e,t,n,i){var a=this._tooltipContent;this._tooltipModel&&a.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,i.from!==this.uid&&this._hide(KV(i,n))},t.prototype._manuallyAxisShowTip=function(e,t,n,i){var a=i.seriesIndex,r=i.dataIndex,o=t.getComponent("axisPointer").coordSysAxesInfo;if(null!=a&&null!=r&&null!=o){var s=t.getSeriesByIndex(a);if(s)if("axis"===$V([s.getData().getItemModel(r),s,(s.coordinateSystem||{}).model],this._tooltipModel).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:a,dataIndex:r,position:i.position}),!0}},t.prototype._tryShow=function(e,t){var n=e.target;if(this._tooltipModel){this._lastX=e.offsetX,this._lastY=e.offsetY;var i=e.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,e);else if(n){var a,r;if("legend"===Um(n).ssrType)return;this._lastDataByCoordSys=null,vS(n,(function(e){return null!=Um(e).dataIndex?(a=e,!0):null!=Um(e).tooltipConfig?(r=e,!0):void 0}),!0),a?this._showSeriesItemTooltip(e,a,t):r?this._showComponentItemTooltip(e,r,t):this._hide(t)}else this._lastDataByCoordSys=null,this._hide(t)}},t.prototype._showOrMove=function(e,t){var n=e.get("showDelay");t=Gr(t,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(t,n):t()},t.prototype._showAxisTooltip=function(e,t){var n=this._ecModel,i=this._tooltipModel,a=[t.offsetX,t.offsetY],r=$V([t.tooltipOption],i),o=this._renderMode,s=[],l=jb("section",{blocks:[],noHeader:!0}),p=[],c=new tw;Rr(e,(function(e){Rr(e.dataByAxis,(function(e){var t=n.getComponent(e.axisDim+"Axis",e.axisIndex),a=e.value;if(t&&t.axis&&null!=a){var r=nN(a,t.axis,n,e.seriesDataIndices,e.valueLabelOpt),d=jb("section",{header:r,noHeader:!lo(r),sortBlocks:!0,blocks:[]});l.blocks.push(d),Rr(e.seriesDataIndices,(function(l){var u=n.getSeriesByIndex(l.seriesIndex),m=l.dataIndexInside,h=u.getDataParams(m);if(!(h.dataIndex<0)){h.axisDim=e.axisDim,h.axisIndex=e.axisIndex,h.axisType=e.axisType,h.axisId=e.axisId,h.axisValue=XT(t.axis,{value:a}),h.axisValueLabel=r,h.marker=c.makeTooltipMarker("item",lv(h.color),o);var g=ob(u.formatTooltip(m,!0,null)),f=g.frag;if(f){var y=$V([u],i).get("valueFormatter");d.blocks.push(y?kr({valueFormatter:y},f):f)}g.text&&p.push(g.text),s.push(h)}}))}}))})),l.blocks.reverse(),p.reverse();var d=t.position,u=r.get("order"),m=Xb(l,c,o,u,n.get("useUTC"),r.get("textStyle"));m&&p.unshift(m);var h="richText"===o?"\n\n":"
",g=p.join(h);this._showOrMove(r,(function(){this._updateContentNotChangedOnAxis(e,s)?this._updatePosition(r,d,a[0],a[1],this._tooltipContent,s):this._showTooltipContent(r,g,s,Math.random()+"",a[0],a[1],d,null,c)}))},t.prototype._showSeriesItemTooltip=function(e,t,n){var i=this._ecModel,a=Um(t),r=a.seriesIndex,o=i.getSeriesByIndex(r),s=a.dataModel||o,l=a.dataIndex,p=a.dataType,c=s.getData(p),d=this._renderMode,u=e.positionDefault,m=$V([c.getItemModel(l),s,o&&(o.coordinateSystem||{}).model],this._tooltipModel,u?{position:u}:null),h=m.get("trigger");if(null==h||"item"===h){var g=s.getDataParams(l,p),f=new tw;g.marker=f.makeTooltipMarker("item",lv(g.color),d);var y=ob(s.formatTooltip(l,!1,p)),v=m.get("order"),x=m.get("valueFormatter"),b=y.frag,w=b?Xb(x?kr({valueFormatter:x},b):b,f,d,v,i.get("useUTC"),m.get("textStyle")):y.text,S="item_"+s.name+"_"+l;this._showOrMove(m,(function(){this._showTooltipContent(m,w,g,S,e.offsetX,e.offsetY,e.position,e.target,f)})),n({type:"showTip",dataIndexInside:l,dataIndex:c.getRawIndex(l),seriesIndex:r,from:this.uid})}},t.prototype._showComponentItemTooltip=function(e,t,n){var i=Um(t),a=i.tooltipConfig.option||{};if(Hr(a)){a={content:a,formatter:a}}var r=[a],o=this._ecModel.getComponent(i.componentMainType,i.componentIndex);o&&r.push(o),r.push({formatter:a.content});var s=e.positionDefault,l=$V(r,this._tooltipModel,s?{position:s}:null),p=l.get("content"),c=Math.random()+"",d=new tw;this._showOrMove(l,(function(){var n=Ir(l.get("formatterParams")||{});this._showTooltipContent(l,p,n,c,e.offsetX,e.offsetY,e.position,t,d)})),n({type:"showTip",from:this.uid})},t.prototype._showTooltipContent=function(e,t,n,i,a,r,o,s,l){if(this._ticket="",e.get("showContent")&&e.get("show")){var p=this._tooltipContent;p.setEnterable(e.get("enterable"));var c=e.get("formatter");o=o||e.get("position");var d=t,u=this._getNearestPoint([a,r],n,e.get("trigger"),e.get("borderColor")).color;if(c)if(Hr(c)){var m=e.ecModel.get("useUTC"),h=Ur(n)?n[0]:n;d=c,h&&h.axisType&&h.axisType.indexOf("time")>=0&&(d=Ny(h.axisValue,d,m)),d=ov(d,n,!0)}else if(jr(c)){var g=Gr((function(t,i){t===this._ticket&&(p.setContent(i,l,e,u,o),this._updatePosition(e,o,a,r,p,n,s))}),this);this._ticket=i,d=c(n,i,g)}else d=c;p.setContent(d,l,e,u,o),p.show(e,u),this._updatePosition(e,o,a,r,p,n,s)}},t.prototype._getNearestPoint=function(e,t,n,i){return"axis"===n||Ur(t)?{color:i||("html"===this._renderMode?"#fff":"none")}:Ur(t)?void 0:{color:i||t.color||t.borderColor}},t.prototype._updatePosition=function(e,t,n,i,a,r,o){var s=this._api.getWidth(),l=this._api.getHeight();t=t||e.get("position");var p=a.getSize(),c=e.get("align"),d=e.get("verticalAlign"),u=o&&o.getBoundingRect().clone();if(o&&u.applyTransform(o.transform),jr(t)&&(t=t([n,i],r,a.el,u,{viewSize:[s,l],contentSize:p.slice()})),Ur(t))n=Vc(t[0],s),i=Vc(t[1],l);else if(Kr(t)){var m=t;m.width=p[0],m.height=p[1];var h=gv(m,{width:s,height:l});n=h.x,i=h.y,c=null,d=null}else if(Hr(t)&&o){var g=function(e,t,n,i){var a=n[0],r=n[1],o=Math.ceil(Math.SQRT2*i)+8,s=0,l=0,p=t.width,c=t.height;switch(e){case"inside":s=t.x+p/2-a/2,l=t.y+c/2-r/2;break;case"top":s=t.x+p/2-a/2,l=t.y-r-o;break;case"bottom":s=t.x+p/2-a/2,l=t.y+c+o;break;case"left":s=t.x-a-o,l=t.y+c/2-r/2;break;case"right":s=t.x+p+o,l=t.y+c/2-r/2}return[s,l]}(t,u,p,e.get("borderWidth"));n=g[0],i=g[1]}else{g=function(e,t,n,i,a,r,o){var s=n.getSize(),l=s[0],p=s[1];null!=r&&(e+l+r+2>i?e-=l+r:e+=r);null!=o&&(t+p+o>a?t-=p+o:t+=o);return[e,t]}(n,i,a,s,l,c?null:20,d?null:20);n=g[0],i=g[1]}if(c&&(n-=YV(c)?p[0]/2:"right"===c?p[0]:0),d&&(i-=YV(d)?p[1]/2:"bottom"===d?p[1]:0),PV(e)){g=function(e,t,n,i,a){var r=n.getSize(),o=r[0],s=r[1];return e=Math.min(e+o,i)-o,t=Math.min(t+s,a)-s,e=Math.max(e,0),t=Math.max(t,0),[e,t]}(n,i,a,s,l);n=g[0],i=g[1]}a.moveTo(n,i)},t.prototype._updateContentNotChangedOnAxis=function(e,t){var n=this._lastDataByCoordSys,i=this._cbParamsList,a=!!n&&n.length===e.length;return a&&Rr(n,(function(n,r){var o=n.dataByAxis||[],s=(e[r]||{}).dataByAxis||[];(a=a&&o.length===s.length)&&Rr(o,(function(e,n){var r=s[n]||{},o=e.seriesDataIndices||[],l=r.seriesDataIndices||[];(a=a&&e.value===r.value&&e.axisType===r.axisType&&e.axisId===r.axisId&&o.length===l.length)&&Rr(o,(function(e,t){var n=l[t];a=a&&e.seriesIndex===n.seriesIndex&&e.dataIndex===n.dataIndex})),i&&Rr(e.seriesDataIndices,(function(e){var n=e.seriesIndex,r=t[n],o=i[n];r&&o&&o.data!==r.data&&(a=!1)}))}))})),this._lastDataByCoordSys=e,this._cbParamsList=t,!!a},t.prototype._hide=function(e){this._lastDataByCoordSys=null,e({type:"hideTip",from:this.uid})},t.prototype.dispose=function(e,t){!rr.node&&t.getDom()&&(kw(this,"_updatePosition"),this._tooltipContent.dispose(),vN("itemTooltip",t))},t.type="tooltip",t}(hw);function $V(e,t,n){var i,a=t.ecModel;n?(i=new uy(n,a,a),i=new uy(t.option,i,a)):i=t;for(var r=e.length-1;r>=0;r--){var o=e[r];o&&(o instanceof uy&&(o=o.get("tooltip",!0)),Hr(o)&&(o={formatter:o}),o&&(i=new uy(o,i,a)))}return i}function KV(e,t){return e.dispatchAction||Gr(t.dispatchAction,t)}function YV(e){return"center"===e||"middle"===e}function XV(e){aI(MN),e.registerComponentModel(kV),e.registerComponentView(WV),e.registerAction({type:"showTip",event:"showTip",update:"tooltip:manuallyShowTip"},wo),e.registerAction({type:"hideTip",event:"hideTip",update:"tooltip:manuallyHideTip"},wo)}var ZV=Rr;function QV(e){if(e)for(var t in e)if(e.hasOwnProperty(t))return!0}function JV(e,t,n){var i={};return ZV(t,(function(t){var a,r=i[t]=((a=function(){}).prototype.__hidden=a.prototype,new a);ZV(e[t],(function(e,i){if(aO.isValidType(i)){var a={type:i,visual:e};n&&n(a,t),r[i]=new aO(a),"opacity"===i&&((a=Ir(a)).type="colorAlpha",r.__hidden.__alphaForOpacity=new aO(a))}}))})),i}function eq(e,t,n){var i;Rr(n,(function(e){t.hasOwnProperty(e)&&QV(t[e])&&(i=!0)})),i&&Rr(n,(function(n){t.hasOwnProperty(n)&&QV(t[n])?e[n]=Ir(t[n]):delete e[n]}))}tq(0),tq(1);function tq(e){var t=["x","y"],n=["width","height"];return{point:function(t,n,i){if(t){var a=i.range;return nq(t[e],a)}},rect:function(i,a,r){if(i){var o=r.range,s=[i[t[e]],i[t[e]]+i[n[e]]];return s[1]=t&&(e=t-1),e<0&&(e=0)),this.option.currentIndex=e},t.prototype.getCurrentIndex=function(){return this.option.currentIndex},t.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},t.prototype.setPlayState=function(e){this.option.autoPlay=!!e},t.prototype.getPlayState=function(){return!!this.option.autoPlay},t.prototype._initData=function(){var e,t=this.option,n=t.data||[],i=t.axisType,a=this._names=[];"category"===i?(e=[],Rr(n,(function(t,n){var i,r=xd(hd(t),"");Kr(t)?(i=Ir(t)).value=n:i=n,e.push(i),a.push(r)}))):e=n;var r={category:"ordinal",time:"time",value:"number"}[i]||"number";(this._data=new V_([{name:"value",type:r}],this)).initData(e,a)},t.prototype.getData=function(){return this._data},t.prototype.getCategories=function(){if("category"===this.get("axisType"))return this._names.slice()},t.type="timeline",t.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:5,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:"#000"},data:[]},t}(Sv),sq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="timeline.slider",t.defaultOption=gy(oq.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:"#DAE1F5"},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:"#A4B1D7"},itemStyle:{color:"#A4B1D7",borderWidth:1},checkpointStyle:{symbol:"circle",symbolSize:15,color:"#316bf3",borderColor:"#fff",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0, 0, 0, 0.3)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z",stopIcon:"path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z",nextIcon:"M2,18.5A1.52,1.52,0,0,1,.92,18a1.49,1.49,0,0,1,0-2.12L7.81,9.36,1,3.11A1.5,1.5,0,1,1,3,.89l8,7.34a1.48,1.48,0,0,1,.49,1.09,1.51,1.51,0,0,1-.46,1.1L3,18.08A1.5,1.5,0,0,1,2,18.5Z",prevIcon:"M10,.5A1.52,1.52,0,0,1,11.08,1a1.49,1.49,0,0,1,0,2.12L4.19,9.64,11,15.89a1.5,1.5,0,1,1-2,2.22L1,10.77A1.48,1.48,0,0,1,.5,9.68,1.51,1.51,0,0,1,1,8.58L9,.92A1.5,1.5,0,0,1,10,.5Z",prevBtnSize:18,nextBtnSize:18,color:"#A4B1D7",borderColor:"#A4B1D7",borderWidth:1},emphasis:{label:{show:!0,color:"#6f778d"},itemStyle:{color:"#316BF3"},controlStyle:{color:"#316BF3",borderColor:"#316BF3",borderWidth:2}},progress:{lineStyle:{color:"#316BF3"},itemStyle:{color:"#316BF3"},label:{color:"#6f778d"}},data:[]}),t}(oq);Ar(sq,rb.prototype);var lq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="timeline",t}(hw),pq=function(e){function t(t,n,i,a){var r=e.call(this,t,n,i)||this;return r.type=a||"value",r}return ze(t,e),t.prototype.getLabelModel=function(){return this.model.getModel("label")},t.prototype.isHorizontal=function(){return"horizontal"===this.model.get("orient")},t}(MI),cq=Math.PI,dq=Cd();!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.init=function(e,t){this.api=t},t.prototype.render=function(e,t,n){if(this.model=e,this.api=n,this.ecModel=t,this.group.removeAll(),e.get("show",!0)){var i=this._layout(e,n),a=this._createGroup("_mainGroup"),r=this._createGroup("_labelGroup"),o=this._axis=this._createAxis(i,e);e.formatTooltip=function(e){return jb("nameValue",{noName:!0,value:o.scale.getLabel({value:e})})},Rr(["AxisLine","AxisTick","Control","CurrentPointer"],(function(t){this["_render"+t](i,a,o,e)}),this),this._renderAxisLabel(i,r,o,e),this._position(i,e)}this._doPlayStop(),this._updateTicksStatus()},t.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},t.prototype.dispose=function(){this._clearTimer()},t.prototype._layout=function(e,t){var n,i,a,r,o=e.get(["label","position"]),s=e.get("orient"),l=function(e,t){return gv(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()},e.get("padding"))}(e,t),p={horizontal:"center",vertical:(n=null==o||"auto"===o?"horizontal"===s?l.y+l.height/2=0||"+"===n?"left":"right"},c={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},d={horizontal:0,vertical:cq/2},u="vertical"===s?l.height:l.width,m=e.getModel("controlStyle"),h=m.get("show",!0),g=h?m.get("itemSize"):0,f=h?m.get("itemGap"):0,y=g+f,v=e.get(["label","rotate"])||0;v=v*cq/180;var x=m.get("position",!0),b=h&&m.get("showPlayBtn",!0),w=h&&m.get("showPrevBtn",!0),S=h&&m.get("showNextBtn",!0),C=0,_=u;"left"===x||"bottom"===x?(b&&(i=[0,0],C+=y),w&&(a=[C,0],C+=y),S&&(r=[_-g,0],_-=y)):(b&&(i=[_-g,0],_-=y),w&&(a=[0,0],C+=y),S&&(r=[_-g,0],_-=y));var T=[C,_];return e.get("inverse")&&T.reverse(),{viewRect:l,mainLength:u,orient:s,rotation:d[s],labelRotation:v,labelPosOpt:n,labelAlign:e.get(["label","align"])||p[s],labelBaseline:e.get(["label","verticalAlign"])||e.get(["label","baseline"])||c[s],playPosition:i,prevBtnPosition:a,nextBtnPosition:r,axisExtent:T,controlSize:g,controlGap:f}},t.prototype._position=function(e,t){var n=this._mainGroup,i=this._labelGroup,a=e.viewRect;if("vertical"===e.orient){var r=[1,0,0,1,0,0],o=a.x,s=a.y+a.height;fs(r,r,[-o,-s]),ys(r,r,-cq/2),fs(r,r,[o,s]),(a=a.clone()).applyTransform(r)}var l=f(a),p=f(n.getBoundingRect()),c=f(i.getBoundingRect()),d=[n.x,n.y],u=[i.x,i.y];u[0]=d[0]=l[0][0];var m,h=e.labelPosOpt;null==h||Hr(h)?(y(d,p,l,1,m="+"===h?0:1),y(u,c,l,1,1-m)):(y(d,p,l,1,m=h>=0?0:1),u[1]=d[1]+h);function g(e){e.originX=l[0][0]-e.x,e.originY=l[1][0]-e.y}function f(e){return[[e.x,e.x+e.width],[e.y,e.y+e.height]]}function y(e,t,n,i,a){e[i]+=n[i][a]-t[i][a]}n.setPosition(d),i.setPosition(u),n.rotation=i.rotation=e.rotation,g(n),g(i)},t.prototype._createAxis=function(e,t){var n=t.getData(),i=t.get("axisType"),a=function(e,t){if(t=t||e.get("type"),t)switch(t){case"category":return new sT({ordinalMeta:e.getCategories(),extent:[1/0,-1/0]});case"time":return new CT({locale:e.ecModel.getLocaleModel(),useUTC:e.ecModel.get("useUTC")});default:return new pT}}(t,i);a.getTicks=function(){return n.mapArray(["value"],(function(e){return{value:e}}))};var r=n.getDataExtent("value");a.setExtent(r[0],r[1]),a.calcNiceTicks();var o=new pq("value",a,e.axisExtent,i);return o.model=t,o},t.prototype._createGroup=function(e){var t=this[e]=new Mc;return this.group.add(t),t},t.prototype._renderAxisLine=function(e,t,n,i){var a=n.getExtent();if(i.get(["lineStyle","show"])){var r=new Fg({shape:{x1:a[0],y1:0,x2:a[1],y2:0},style:kr({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});t.add(r);var o=this._progressLine=new Fg({shape:{x1:a[0],x2:this._currentPointer?this._currentPointer.x:a[0],y1:0,y2:0},style:Pr({lineCap:"round",lineWidth:r.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});t.add(o)}},t.prototype._renderAxisTick=function(e,t,n,i){var a=this,r=i.getData(),o=n.scale.getTicks();this._tickSymbols=[],Rr(o,(function(e){var o=n.dataToCoord(e.value),s=r.getItemModel(e.value),l=s.getModel("itemStyle"),p=s.getModel(["emphasis","itemStyle"]),c=s.getModel(["progress","itemStyle"]),d={x:o,y:0,onclick:Gr(a._changeTimeline,a,e.value)},u=uq(s,l,t,d);u.ensureState("emphasis").style=p.getItemStyle(),u.ensureState("progress").style=c.getItemStyle(),Dh(u);var m=Um(u);s.get("tooltip")?(m.dataIndex=e.value,m.dataModel=i):m.dataIndex=m.dataModel=null,a._tickSymbols.push(u)}))},t.prototype._renderAxisLabel=function(e,t,n,i){var a=this;if(n.getLabelModel().get("show")){var r=i.getData(),o=n.getViewLabels();this._tickLabels=[],Rr(o,(function(i){var o=i.tickValue,s=r.getItemModel(o),l=s.getModel("label"),p=s.getModel(["emphasis","label"]),c=s.getModel(["progress","label"]),d=n.dataToCoord(i.tickValue),u=new Pm({x:d,y:0,rotation:e.labelRotation-e.rotation,onclick:Gr(a._changeTimeline,a,o),silent:!1,style:jf(l,{text:i.formattedLabel,align:e.labelAlign,verticalAlign:e.labelBaseline})});u.ensureState("emphasis").style=jf(p),u.ensureState("progress").style=jf(c),t.add(u),Dh(u),dq(u).dataIndex=o,a._tickLabels.push(u)}))}},t.prototype._renderControl=function(e,t,n,i){var a=e.controlSize,r=e.rotation,o=i.getModel("controlStyle").getItemStyle(),s=i.getModel(["emphasis","controlStyle"]).getItemStyle(),l=i.getPlayState(),p=i.get("inverse",!0);function c(e,n,l,p){if(e){var c=fc(io(i.get(["controlStyle",n+"BtnSize"]),a),a),d=function(e,t,n,i){var a=i.style,r=Df(e.get(["controlStyle",t]),i||{},new Ps(n[0],n[1],n[2],n[3]));a&&r.setStyle(a);return r}(i,n+"Icon",[0,-c/2,c,c],{x:e[0],y:e[1],originX:a/2,originY:0,rotation:p?-r:0,rectHover:!0,style:o,onclick:l});d.ensureState("emphasis").style=s,t.add(d),Dh(d)}}c(e.nextBtnPosition,"next",Gr(this._changeTimeline,this,p?"-":"+")),c(e.prevBtnPosition,"prev",Gr(this._changeTimeline,this,p?"+":"-")),c(e.playPosition,l?"stop":"play",Gr(this._handlePlayClick,this,!l),!0)},t.prototype._renderCurrentPointer=function(e,t,n,i){var a=i.getData(),r=i.getCurrentIndex(),o=a.getItemModel(r).getModel("checkpointStyle"),s=this,l={onCreate:function(e){e.draggable=!0,e.drift=Gr(s._handlePointerDrag,s),e.ondragend=Gr(s._handlePointerDragend,s),mq(e,s._progressLine,r,n,i,!0)},onUpdate:function(e){mq(e,s._progressLine,r,n,i)}};this._currentPointer=uq(o,o,this._mainGroup,{},this._currentPointer,l)},t.prototype._handlePlayClick=function(e){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:e,from:this.uid})},t.prototype._handlePointerDrag=function(e,t,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},t.prototype._handlePointerDragend=function(e){this._pointerChangeTimeline([e.offsetX,e.offsetY],!0)},t.prototype._pointerChangeTimeline=function(e,t){var n=this._toAxisCoord(e)[0],i=Gc(this._axis.getExtent().slice());n>i[1]&&(n=i[1]),n=0&&(o[r]=+o[r].toFixed(d)),[o,c]}var bq={min:zr(xq,"min"),max:zr(xq,"max"),average:zr(xq,"average"),median:zr(xq,"median")};function wq(e,t){if(t){var n=e.getData(),i=e.coordinateSystem,a=i&&i.dimensions;if(!function(e){return!isNaN(parseFloat(e.x))&&!isNaN(parseFloat(e.y))}(t)&&!Ur(t.coord)&&Ur(a)){var r=Sq(t,n,i,e);if((t=Ir(t)).type&&bq[t.type]&&r.baseAxis&&r.valueAxis){var o=Dr(a,r.baseAxis.dim),s=Dr(a,r.valueAxis.dim),l=bq[t.type](n,r.baseDataDim,r.valueDataDim,o,s);t.coord=l[0],t.value=l[1]}else t.coord=[null!=t.xAxis?t.xAxis:t.radiusAxis,null!=t.yAxis?t.yAxis:t.angleAxis]}if(null!=t.coord&&Ur(a))for(var p=t.coord,c=0;c<2;c++)bq[p[c]]&&(p[c]=Tq(n,n.mapDimension(a[c]),p[c]));else t.coord=[];return t}}function Sq(e,t,n,i){var a={};return null!=e.valueIndex||null!=e.valueDim?(a.valueDataDim=null!=e.valueIndex?t.getDimension(e.valueIndex):e.valueDim,a.valueAxis=n.getAxis(function(e,t){var n=e.getData().getDimensionInfo(t);return n&&n.coordDim}(i,a.valueDataDim)),a.baseAxis=n.getOtherAxis(a.valueAxis),a.baseDataDim=t.mapDimension(a.baseAxis.dim)):(a.baseAxis=i.getBaseAxis(),a.valueAxis=n.getOtherAxis(a.baseAxis),a.baseDataDim=t.mapDimension(a.baseAxis.dim),a.valueDataDim=t.mapDimension(a.valueAxis.dim)),a}function Cq(e,t){return!(e&&e.containData&&t.coord&&!vq(t))||e.containData(t.coord)}function _q(e,t){return e?function(e,n,i,a){return cb(a<2?e.coord&&e.coord[a]:e.value,t[a])}:function(e,n,i,a){return cb(e.value,t[a])}}function Tq(e,t,n){if("average"===n){var i=0,a=0;return e.each(t,(function(e,t){isNaN(e)||(i+=e,a++)})),i/a}return"median"===n?e.getMedian(t):e.getDataExtent(t)["max"===n?1:0]}var Iq=Cd(),Eq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.init=function(){this.markerGroupMap=fo()},t.prototype.render=function(e,t,n){var i=this,a=this.markerGroupMap;a.each((function(e){Iq(e).keep=!1})),t.eachSeries((function(e){var a=yq.getMarkerModelFromSeries(e,i.type);a&&i.renderSeries(e,a,t,n)})),a.each((function(e){!Iq(e).keep&&i.group.remove(e.group)}))},t.prototype.markKeep=function(e){Iq(e).keep=!0},t.prototype.toggleBlurSeries=function(e,t){var n=this;Rr(e,(function(e){var i=yq.getMarkerModelFromSeries(e,n.type);i&&i.getData().eachItemGraphicEl((function(e){e&&(t?bh(e):wh(e))}))}))},t.type="marker",t}(hw);function Mq(e,t,n){var i=t.coordinateSystem;e.each((function(a){var r,o=e.getItemModel(a),s=Vc(o.get("x"),n.getWidth()),l=Vc(o.get("y"),n.getHeight());if(isNaN(s)||isNaN(l)){if(t.getMarkerPosition)r=t.getMarkerPosition(e.getValues(e.dimensions,a));else if(i){var p=e.get(i.dimensions[0],a),c=e.get(i.dimensions[1],a);r=i.dataToPoint([p,c])}}else r=[s,l];isNaN(s)||(r[0]=s),isNaN(l)||(r[1]=l),e.setItemLayout(a,r)}))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.updateTransform=function(e,t,n){t.eachSeries((function(e){var t=yq.getMarkerModelFromSeries(e,"markPoint");t&&(Mq(t.getData(),e,n),this.markerGroupMap.get(e.id).updateLayout())}),this)},t.prototype.renderSeries=function(e,t,n,i){var a=e.coordinateSystem,r=e.id,o=e.getData(),s=this.markerGroupMap,l=s.get(r)||s.set(r,new CE),p=function(e,t,n){var i;i=e?Br(e&&e.dimensions,(function(e){return kr(kr({},t.getData().getDimensionInfo(t.getData().mapDimension(e))||{}),{name:e,ordinalMeta:null})})):[{name:"value",type:"float"}];var a=new V_(i,n),r=Br(n.get("data"),zr(wq,t));e&&(r=Lr(r,zr(Cq,e)));var o=_q(!!e,i);return a.initData(r,null,o),a}(a,e,t);t.setData(p),Mq(t.getData(),e,i),p.each((function(e){var n=p.getItemModel(e),i=n.getShallow("symbol"),a=n.getShallow("symbolSize"),r=n.getShallow("symbolRotate"),s=n.getShallow("symbolOffset"),l=n.getShallow("symbolKeepAspect");if(jr(i)||jr(a)||jr(r)||jr(s)){var c=t.getRawValue(e),d=t.getDataParams(e);jr(i)&&(i=i(c,d)),jr(a)&&(a=a(c,d)),jr(r)&&(r=r(c,d)),jr(s)&&(s=s(c,d))}var u=n.getModel("itemStyle").getItemStyle(),m=hS(o,"color");u.fill||(u.fill=m),p.setItemVisual(e,{symbol:i,symbolSize:a,symbolRotate:r,symbolOffset:s,symbolKeepAspect:l,style:u})})),l.updateData(p),this.group.add(l.group),p.eachItemGraphicEl((function(e){e.traverse((function(e){Um(e).dataModel=t}))})),this.markKeep(l),l.group.silent=t.get("silent")||e.get("silent")},t.type="markPoint"}(Eq);var kq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.createMarkerModelFromSeries=function(e,n,i){return new t(e,n,i)},t.type="markLine",t.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},t}(yq),Pq=Cd(),Dq=function(e,t,n,i){var a,r=e.getData();if(Ur(i))a=i;else{var o=i.type;if("min"===o||"max"===o||"average"===o||"median"===o||null!=i.xAxis||null!=i.yAxis){var s=void 0,l=void 0;if(null!=i.yAxis||null!=i.xAxis)s=t.getAxis(null!=i.yAxis?"y":"x"),l=no(i.yAxis,i.xAxis);else{var p=Sq(i,r,t,e);s=p.valueAxis,l=Tq(r,$_(r,p.valueDataDim),o)}var c="x"===s.dim?0:1,d=1-c,u=Ir(i),m={coord:[]};u.type=null,u.coord=[],u.coord[d]=-1/0,m.coord[d]=1/0;var h=n.get("precision");h>=0&&$r(l)&&(l=+l.toFixed(Math.min(h,20))),u.coord[c]=m.coord[c]=l,a=[u,m,{type:o,valueIndex:i.valueIndex,value:l}]}else a=[]}var g=[wq(e,a[0]),wq(e,a[1]),kr({},a[2])];return g[2].type=g[2].type||null,Er(g[2],g[0]),Er(g[2],g[1]),g};function Oq(e){return!isNaN(e)&&!isFinite(e)}function Aq(e,t,n,i){var a=1-e,r=i.dimensions[e];return Oq(t[a])&&Oq(n[a])&&t[e]===n[e]&&i.getAxis(r).containData(t[e])}function Fq(e,t){if("cartesian2d"===e.type){var n=t[0].coord,i=t[1].coord;if(n&&i&&(Aq(1,n,i,e)||Aq(0,n,i,e)))return!0}return Cq(e,t[0])&&Cq(e,t[1])}function Rq(e,t,n,i,a){var r,o=i.coordinateSystem,s=e.getItemModel(t),l=Vc(s.get("x"),a.getWidth()),p=Vc(s.get("y"),a.getHeight());if(isNaN(l)||isNaN(p)){if(i.getMarkerPosition)r=i.getMarkerPosition(e.getValues(e.dimensions,t));else{var c=o.dimensions,d=e.get(c[0],t),u=e.get(c[1],t);r=o.dataToPoint([d,u])}if(NE(o,"cartesian2d")){var m=o.getAxis("x"),h=o.getAxis("y");c=o.dimensions;Oq(e.get(c[0],t))?r[0]=m.toGlobalCoord(m.getExtent()[n?0:1]):Oq(e.get(c[1],t))&&(r[1]=h.toGlobalCoord(h.getExtent()[n?0:1]))}isNaN(l)||(r[0]=l),isNaN(p)||(r[1]=p)}else r=[l,p];e.setItemLayout(t,r)}var Bq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.updateTransform=function(e,t,n){t.eachSeries((function(e){var t=yq.getMarkerModelFromSeries(e,"markLine");if(t){var i=t.getData(),a=Pq(t).from,r=Pq(t).to;a.each((function(t){Rq(a,t,!0,e,n),Rq(r,t,!1,e,n)})),i.each((function(e){i.setItemLayout(e,[a.getItemLayout(e),r.getItemLayout(e)])})),this.markerGroupMap.get(e.id).updateLayout()}}),this)},t.prototype.renderSeries=function(e,t,n,i){var a=e.coordinateSystem,r=e.id,o=e.getData(),s=this.markerGroupMap,l=s.get(r)||s.set(r,new zO);this.group.add(l.group);var p=function(e,t,n){var i;i=e?Br(e&&e.dimensions,(function(e){return kr(kr({},t.getData().getDimensionInfo(t.getData().mapDimension(e))||{}),{name:e,ordinalMeta:null})})):[{name:"value",type:"float"}];var a=new V_(i,n),r=new V_(i,n),o=new V_([],n),s=Br(n.get("data"),zr(Dq,t,e,n));e&&(s=Lr(s,zr(Fq,e)));var l=_q(!!e,i);return a.initData(Br(s,(function(e){return e[0]})),null,l),r.initData(Br(s,(function(e){return e[1]})),null,l),o.initData(Br(s,(function(e){return e[2]}))),o.hasItemOption=!0,{from:a,to:r,line:o}}(a,e,t),c=p.from,d=p.to,u=p.line;Pq(t).from=c,Pq(t).to=d,t.setData(u);var m=t.get("symbol"),h=t.get("symbolSize"),g=t.get("symbolRotate"),f=t.get("symbolOffset");function y(t,n,a){var r=t.getItemModel(n);Rq(t,n,a,e,i);var s=r.getModel("itemStyle").getItemStyle();null==s.fill&&(s.fill=hS(o,"color")),t.setItemVisual(n,{symbolKeepAspect:r.get("symbolKeepAspect"),symbolOffset:io(r.get("symbolOffset",!0),f[a?0:1]),symbolRotate:io(r.get("symbolRotate",!0),g[a?0:1]),symbolSize:io(r.get("symbolSize"),h[a?0:1]),symbol:io(r.get("symbol",!0),m[a?0:1]),style:s})}Ur(m)||(m=[m,m]),Ur(h)||(h=[h,h]),Ur(g)||(g=[g,g]),Ur(f)||(f=[f,f]),p.from.each((function(e){y(c,e,!0),y(d,e,!1)})),u.each((function(e){var t=u.getItemModel(e).getModel("lineStyle").getLineStyle();u.setItemLayout(e,[c.getItemLayout(e),d.getItemLayout(e)]),null==t.stroke&&(t.stroke=c.getItemVisual(e,"style").fill),u.setItemVisual(e,{fromSymbolKeepAspect:c.getItemVisual(e,"symbolKeepAspect"),fromSymbolOffset:c.getItemVisual(e,"symbolOffset"),fromSymbolRotate:c.getItemVisual(e,"symbolRotate"),fromSymbolSize:c.getItemVisual(e,"symbolSize"),fromSymbol:c.getItemVisual(e,"symbol"),toSymbolKeepAspect:d.getItemVisual(e,"symbolKeepAspect"),toSymbolOffset:d.getItemVisual(e,"symbolOffset"),toSymbolRotate:d.getItemVisual(e,"symbolRotate"),toSymbolSize:d.getItemVisual(e,"symbolSize"),toSymbol:d.getItemVisual(e,"symbol"),style:t})})),l.updateData(u),p.line.eachItemGraphicEl((function(e){Um(e).dataModel=t,e.traverse((function(e){Um(e).dataModel=t}))})),this.markKeep(l),l.group.silent=t.get("silent")||e.get("silent")},t.type="markLine",t}(Eq);function Nq(e){e.registerComponentModel(kq),e.registerComponentView(Bq),e.registerPreprocessor((function(e){hq(e.series,"markLine")&&(e.markLine=e.markLine||{})}))}!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.createMarkerModelFromSeries=function(e,n,i){return new t(e,n,i)},t.type="markArea",t.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}}}(yq);var Lq=Cd(),Vq=function(e,t,n,i){var a=i[0],r=i[1];if(a&&r){var o=wq(e,a),s=wq(e,r),l=o.coord,p=s.coord;l[0]=no(l[0],-1/0),l[1]=no(l[1],-1/0),p[0]=no(p[0],1/0),p[1]=no(p[1],1/0);var c=Mr([{},o,s]);return c.coord=[o.coord,s.coord],c.x0=o.x,c.y0=o.y,c.x1=s.x,c.y1=s.y,c}};function qq(e){return!isNaN(e)&&!isFinite(e)}function Gq(e,t,n,i){var a=1-e;return qq(t[a])&&qq(n[a])}function zq(e,t){var n=t.coord[0],i=t.coord[1],a={coord:n,x:t.x0,y:t.y0},r={coord:i,x:t.x1,y:t.y1};return NE(e,"cartesian2d")?!(!n||!i||!Gq(1,n,i)&&!Gq(0,n,i))||function(e,t,n){return!(e&&e.containZone&&t.coord&&n.coord&&!vq(t)&&!vq(n))||e.containZone(t.coord,n.coord)}(e,a,r):Cq(e,a)||Cq(e,r)}function Uq(e,t,n,i,a){var r,o=i.coordinateSystem,s=e.getItemModel(t),l=Vc(s.get(n[0]),a.getWidth()),p=Vc(s.get(n[1]),a.getHeight());if(isNaN(l)||isNaN(p)){if(i.getMarkerPosition){var c=e.getValues(["x0","y0"],t),d=e.getValues(["x1","y1"],t),u=o.clampData(c),m=o.clampData(d),h=[];"x0"===n[0]?h[0]=u[0]>m[0]?d[0]:c[0]:h[0]=u[0]>m[0]?c[0]:d[0],"y0"===n[1]?h[1]=u[1]>m[1]?d[1]:c[1]:h[1]=u[1]>m[1]?c[1]:d[1],r=i.getMarkerPosition(h,n,!0)}else{var g=[v=e.get(n[0],t),x=e.get(n[1],t)];o.clampData&&o.clampData(g,g),r=o.dataToPoint(g,!0)}if(NE(o,"cartesian2d")){var f=o.getAxis("x"),y=o.getAxis("y"),v=e.get(n[0],t),x=e.get(n[1],t);qq(v)?r[0]=f.toGlobalCoord(f.getExtent()["x0"===n[0]?0:1]):qq(x)&&(r[1]=y.toGlobalCoord(y.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(r[0]=l),isNaN(p)||(r[1]=p)}else r=[l,p];return r}var jq=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]];!function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}ze(t,e),t.prototype.updateTransform=function(e,t,n){t.eachSeries((function(e){var t=yq.getMarkerModelFromSeries(e,"markArea");if(t){var i=t.getData();i.each((function(t){var a=Br(jq,(function(a){return Uq(i,t,a,e,n)}));i.setItemLayout(t,a),i.getItemGraphicEl(t).setShape("points",a)}))}}),this)},t.prototype.renderSeries=function(e,t,n,i){var a=e.coordinateSystem,r=e.id,o=e.getData(),s=this.markerGroupMap,l=s.get(r)||s.set(r,{group:new Mc});this.group.add(l.group),this.markKeep(l);var p=function(e,t,n){var i,a,r=["x0","y0","x1","y1"];if(e){var o=Br(e&&e.dimensions,(function(e){var n=t.getData();return kr(kr({},n.getDimensionInfo(n.mapDimension(e))||{}),{name:e,ordinalMeta:null})}));a=Br(r,(function(e,t){return{name:e,type:o[t%2].type}})),i=new V_(a,n)}else i=new V_(a=[{name:"value",type:"float"}],n);var s=Br(n.get("data"),zr(Vq,t,e,n));e&&(s=Lr(s,zr(zq,e)));var l=e?function(e,t,n,i){return cb(e.coord[Math.floor(i/2)][i%2],a[i])}:function(e,t,n,i){return cb(e.value,a[i])};return i.initData(s,null,l),i.hasItemOption=!0,i}(a,e,t);t.setData(p),p.each((function(t){var n=Br(jq,(function(n){return Uq(p,t,n,e,i)})),r=a.getAxis("x").scale,s=a.getAxis("y").scale,l=r.getExtent(),c=s.getExtent(),d=[r.parse(p.get("x0",t)),r.parse(p.get("x1",t))],u=[s.parse(p.get("y0",t)),s.parse(p.get("y1",t))];Gc(d),Gc(u);var m=!!(l[0]>d[1]||l[1]u[1]||c[1]=0},t.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},t.type="legend.plain",t.dependencies=["series"],t.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},t}(Sv),Wq=zr,$q=Rr,Kq=Mc,Yq=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.newlineDisabled=!1,n}return ze(t,e),t.prototype.init=function(){this.group.add(this._contentGroup=new Kq),this.group.add(this._selectorGroup=new Kq),this._isFirstRender=!0},t.prototype.getContentGroup=function(){return this._contentGroup},t.prototype.getSelectorGroup=function(){return this._selectorGroup},t.prototype.render=function(e,t,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),e.get("show",!0)){var a=e.get("align"),r=e.get("orient");a&&"auto"!==a||(a="right"===e.get("left")&&"vertical"===r?"right":"left");var o=e.get("selector",!0),s=e.get("selectorPosition",!0);!o||s&&"auto"!==s||(s="horizontal"===r?"end":"start"),this.renderInner(a,e,t,n,o,r,s);var l=e.getBoxLayoutParams(),p={width:n.getWidth(),height:n.getHeight()},c=e.get("padding"),d=gv(l,p,c),u=this.layoutInner(e,a,d,i,o,s),m=gv(Pr({width:u.width,height:u.height},l),p,c);this.group.x=m.x-u.x,this.group.y=m.y-u.y,this.group.markRedraw(),this.group.add(this._backgroundEl=QL(u,e))}},t.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},t.prototype.renderInner=function(e,t,n,i,a,r,o){var s=this.getContentGroup(),l=fo(),p=t.get("selectedMode"),c=[];n.eachRawSeries((function(e){!e.get("legendHoverLink")&&c.push(e.id)})),$q(t.getData(),(function(a,r){var o=a.get("name");if(!this.newlineDisabled&&(""===o||"\n"===o)){var d=new Kq;return d.newline=!0,void s.add(d)}var u=n.getSeriesByName(o)[0];if(!l.get(o)){if(u){var m=u.getData(),h=m.getVisual("legendLineStyle")||{},g=m.getVisual("legendIcon"),f=m.getVisual("style"),y=this._createItem(u,o,r,a,t,e,h,f,g,p,i);y.on("click",Wq(Xq,o,null,i,c)).on("mouseover",Wq(Qq,u.name,null,i,c)).on("mouseout",Wq(Jq,u.name,null,i,c)),n.ssr&&y.eachChild((function(e){var t=Um(e);t.seriesIndex=u.seriesIndex,t.dataIndex=r,t.ssrType="legend"})),l.set(o,!0)}else n.eachRawSeries((function(s){if(!l.get(o)&&s.legendVisualProvider){var d=s.legendVisualProvider;if(!d.containName(o))return;var u=d.indexOfName(o),m=d.getItemVisual(u,"style"),h=d.getItemVisual(u,"legendIcon"),g=Gl(m.fill);g&&0===g[3]&&(g[3]=.2,m=kr(kr({},m),{fill:Kl(g,"rgba")}));var f=this._createItem(s,o,r,a,t,e,{},m,h,p,i);f.on("click",Wq(Xq,null,o,i,c)).on("mouseover",Wq(Qq,null,o,i,c)).on("mouseout",Wq(Jq,null,o,i,c)),n.ssr&&f.eachChild((function(e){var t=Um(e);t.seriesIndex=s.seriesIndex,t.dataIndex=r,t.ssrType="legend"})),l.set(o,!0)}}),this);0}}),this),a&&this._createSelector(a,t,i,r,o)},t.prototype._createSelector=function(e,t,n,i,a){var r=this.getSelectorGroup();$q(e,(function(e){var i=e.type,a=new Pm({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect"})}});r.add(a),zf(a,{normal:t.getModel("selectorLabel"),emphasis:t.getModel(["emphasis","selectorLabel"])},{defaultText:e.title}),Dh(a)}))},t.prototype._createItem=function(e,t,n,i,a,r,o,s,l,p,c){var d=e.visualDrawType,u=a.get("itemWidth"),m=a.get("itemHeight"),h=a.isSelected(t),g=i.get("symbolRotate"),f=i.get("symbolKeepAspect"),y=i.get("icon"),v=function(e,t,n,i,a,r,o){function s(e,t){"auto"===e.lineWidth&&(e.lineWidth=t.lineWidth>0?2:0),$q(e,(function(n,i){"inherit"===e[i]&&(e[i]=t[i])}))}var l=t.getModel("itemStyle"),p=l.getItemStyle(),c=0===e.lastIndexOf("empty",0)?"fill":"stroke",d=l.getShallow("decal");p.decal=d&&"inherit"!==d?iC(d,o):i.decal,"inherit"===p.fill&&(p.fill=i[a]);"inherit"===p.stroke&&(p.stroke=i[c]);"inherit"===p.opacity&&(p.opacity=("fill"===a?i:n).opacity);s(p,i);var u=t.getModel("lineStyle"),m=u.getLineStyle();if(s(m,n),"auto"===p.fill&&(p.fill=i.fill),"auto"===p.stroke&&(p.stroke=i.fill),"auto"===m.stroke&&(m.stroke=i.fill),!r){var h=t.get("inactiveBorderWidth"),g=p[c];p.lineWidth="auto"===h?i.lineWidth>0&&g?2:0:p.lineWidth,p.fill=t.get("inactiveColor"),p.stroke=t.get("inactiveBorderColor"),m.stroke=u.get("inactiveColor"),m.lineWidth=u.get("inactiveWidth")}return{itemStyle:p,lineStyle:m}}(l=y||l||"roundRect",i,o,s,d,h,c),x=new Kq,b=i.getModel("textStyle");if(!jr(e.getLegendIcon)||y&&"inherit"!==y){var w="inherit"===y&&e.getData().getVisual("symbol")?"inherit"===g?e.getData().getVisual("symbolRotate"):g:0;x.add(function(e){var t=e.icon||"roundRect",n=PS(t,0,0,e.itemWidth,e.itemHeight,e.itemStyle.fill,e.symbolKeepAspect);n.setStyle(e.itemStyle),n.rotation=(e.iconRotate||0)*Math.PI/180,n.setOrigin([e.itemWidth/2,e.itemHeight/2]),t.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2);return n}({itemWidth:u,itemHeight:m,icon:l,iconRotate:w,itemStyle:v.itemStyle,lineStyle:v.lineStyle,symbolKeepAspect:f}))}else x.add(e.getLegendIcon({itemWidth:u,itemHeight:m,icon:l,iconRotate:g,itemStyle:v.itemStyle,lineStyle:v.lineStyle,symbolKeepAspect:f}));var S="left"===r?u+5:-5,C=r,_=a.get("formatter"),T=t;Hr(_)&&_?T=_.replace("{name}",null!=t?t:""):jr(_)&&(T=_(t));var I=h?b.getTextColor():i.get("inactiveColor");x.add(new Pm({style:jf(b,{text:T,x:S,y:m/2,fill:I,align:C,verticalAlign:"middle"},{inheritColor:I})}));var E=new Em({shape:x.getBoundingRect(),style:{fill:"transparent"}}),M=i.getModel("tooltip");return M.get("show")&&Rf({el:E,componentModel:a,itemName:t,itemTooltipOption:M.option}),x.add(E),x.eachChild((function(e){e.silent=!0})),E.silent=!p,this.getContentGroup().add(x),Dh(x),x.__legendDataIndex=n,x},t.prototype.layoutInner=function(e,t,n,i,a,r){var o=this.getContentGroup(),s=this.getSelectorGroup();hv(e.get("orient"),o,e.get("itemGap"),n.width,n.height);var l=o.getBoundingRect(),p=[-l.x,-l.y];if(s.markRedraw(),o.markRedraw(),a){hv("horizontal",s,e.get("selectorItemGap",!0));var c=s.getBoundingRect(),d=[-c.x,-c.y],u=e.get("selectorButtonGap",!0),m=e.getOrient().index,h=0===m?"width":"height",g=0===m?"height":"width",f=0===m?"y":"x";"end"===r?d[m]+=l[h]+u:p[m]+=c[h]+u,d[1-m]+=l[g]/2-c[g]/2,s.x=d[0],s.y=d[1],o.x=p[0],o.y=p[1];var y={x:0,y:0};return y[h]=l[h]+u+c[h],y[g]=Math.max(l[g],c[g]),y[f]=Math.min(0,c[f]+d[1-m]),y}return o.x=p[0],o.y=p[1],this.group.getBoundingRect()},t.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},t.type="legend.plain",t}(hw);function Xq(e,t,n,i){Jq(e,t,n,i),n.dispatchAction({type:"legendToggleSelect",name:null!=e?e:t}),Qq(e,t,n,i)}function Zq(e){for(var t,n=e.getZr().storage.getDisplayList(),i=0,a=n.length;in[a],h=[-d.x,-d.y];t||(h[i]=l[s]);var g=[0,0],f=[-u.x,-u.y],y=io(e.get("pageButtonGap",!0),e.get("itemGap",!0));m&&("end"===e.get("pageButtonPosition",!0)?f[i]+=n[a]-u[a]:g[i]+=u[a]+y);f[1-i]+=d[r]/2-u[r]/2,l.setPosition(h),p.setPosition(g),c.setPosition(f);var v={x:0,y:0};if(v[a]=m?n[a]:d[a],v[r]=Math.max(d[r],u[r]),v[o]=Math.min(0,u[o]+f[1-i]),p.__rectSize=n[a],m){var x={x:0,y:0};x[a]=Math.max(n[a]-u[a]-y,0),x[r]=v[r],p.setClipPath(new Em({shape:x})),p.__rectSize=x[a]}else c.eachChild((function(e){e.attr({invisible:!0,silent:!0})}));var b=this._getPageInfo(e);return null!=b.pageIndex&&tf(l,{x:b.contentPosition[0],y:b.contentPosition[1]},m?e:null),this._updatePageInfoView(e,b),v},t.prototype._pageGo=function(e,t,n){var i=this._getPageInfo(t)[e];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:t.id})},t.prototype._updatePageInfoView=function(e,t){var n=this._controllerGroup;Rr(["pagePrev","pageNext"],(function(i){var a=null!=t[i+"DataIndex"],r=n.childOfName(i);r&&(r.setStyle("fill",a?e.get("pageIconColor",!0):e.get("pageIconInactiveColor",!0)),r.cursor=a?"pointer":"default")}));var i=n.childOfName("pageText"),a=e.get("pageFormatter"),r=t.pageIndex,o=null!=r?r+1:0,s=t.pageCount;i&&a&&i.setStyle("text",Hr(a)?a.replace("{current}",null==o?"":o+"").replace("{total}",null==s?"":s+""):a({current:o,total:s}))},t.prototype._getPageInfo=function(e){var t=e.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,a=e.getOrient().index,r=nG[a],o=iG[a],s=this._findTargetItemIndex(t),l=n.children(),p=l[s],c=l.length,d=c?1:0,u={contentPosition:[n.x,n.y],pageCount:d,pageIndex:d-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!p)return u;var m=v(p);u.contentPosition[a]=-m.s;for(var h=s+1,g=m,f=m,y=null;h<=c;++h)(!(y=v(l[h]))&&f.e>g.s+i||y&&!x(y,g.s))&&(g=f.i>g.i?f:y)&&(null==u.pageNextDataIndex&&(u.pageNextDataIndex=g.i),++u.pageCount),f=y;for(h=s-1,g=m,f=m,y=null;h>=-1;--h)(y=v(l[h]))&&x(f,y.s)||!(g.i=t&&e.s<=t+i}},t.prototype._findTargetItemIndex=function(e){return this._showController?(this.getContentGroup().eachChild((function(i,a){var r=i.__legendDataIndex;null==n&&null!=r&&(n=a),r===e&&(t=a)})),null!=t?t:n):0;var t,n},t.type="legend.scroll"}(Yq);var aG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="dataZoom.inside",t.defaultOption=gy(qL.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),t}(qL),rG=Cd();function oG(e,t){if(t){e.removeKey(t.model.uid);var n=t.controller;n&&n.dispose()}}function sG(e,t){e.isDisposed()||e.dispatchAction({type:"dataZoom",animation:{easing:"cubicOut",duration:100},batch:t})}function lG(e,t,n,i){return e.coordinateSystem.containPoint([n,i])}function pG(e){e.registerProcessor(e.PRIORITY.PROCESSOR.FILTER,(function(e,t){var n=rG(t),i=n.coordSysRecordMap||(n.coordSysRecordMap=fo());i.each((function(e){e.dataZoomInfoMap=null})),e.eachComponent({mainType:"dataZoom",subType:"inside"},(function(e){Rr(LL(e).infoList,(function(n){var a=n.model.uid,r=i.get(a)||i.set(a,function(e,t){var n={model:t,containsPoint:zr(lG,t),dispatchAction:zr(sG,e),dataZoomInfoMap:null,controller:null},i=n.controller=new oP(e.getZr());return Rr(["pan","zoom","scrollMove"],(function(e){i.on(e,(function(t){var i=[];n.dataZoomInfoMap.each((function(a){if(t.isAvailableBehavior(a.model.option)){var r=(a.getRange||{})[e],o=r&&r(a.dzReferCoordSysInfo,n.model.mainType,n.controller,t);!a.model.get("disabled",!0)&&o&&i.push({dataZoomId:a.model.id,start:o[0],end:o[1]})}})),i.length&&n.dispatchAction(i)}))})),n}(t,n.model));(r.dataZoomInfoMap||(r.dataZoomInfoMap=fo())).set(e.uid,{dzReferCoordSysInfo:n,model:e,getRange:null})}))})),i.each((function(e){var t,n=e.controller,a=e.dataZoomInfoMap;if(a){var r=a.keys()[0];null!=r&&(t=a.get(r))}if(t){var o=function(e){var t,n="type_",i={type_true:2,type_move:1,type_false:0,type_undefined:-1},a=!0;return e.each((function(e){var r=e.model,o=!r.get("disabled",!0)&&(!r.get("zoomLock",!0)||"move");i[n+o]>i[n+t]&&(t=o),a=a&&r.get("preventDefaultMouseMove",!0)})),{controlType:t,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!a}}}(a);n.enable(o.controlType,o.opt),n.setPointerChecker(e.containsPoint),Mw(e,"dispatchAction",t.model.get("throttle",!0),"fixRate")}else oG(i,e)}))}))}var cG=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type="dataZoom.inside",t}return ze(t,e),t.prototype.render=function(t,n,i){e.prototype.render.apply(this,arguments),t.noTarget()?this._clear():(this.range=t.getPercentRange(),function(e,t,n){rG(e).coordSysRecordMap.each((function(e){var i=e.dataZoomInfoMap.get(t.uid);i&&(i.getRange=n)}))}(i,t,{pan:Gr(dG.pan,this),zoom:Gr(dG.zoom,this),scrollMove:Gr(dG.scrollMove,this)}))},t.prototype.dispose=function(){this._clear(),e.prototype.dispose.apply(this,arguments)},t.prototype._clear=function(){!function(e,t){for(var n=rG(e).coordSysRecordMap,i=n.keys(),a=0;a0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(r[1]-r[0])+r[0],p=Math.max(1/i.scale,0);r[0]=(r[0]-l)*p+l,r[1]=(r[1]-l)*p+l;var c=this.dataZoomModel.findRepresentativeAxisProxy();if(c){var d=c.getMinMaxSpan();if(bA(0,r,[0,100],0,d.minSpan,d.maxSpan),this.range=r,a[0]!==r[0]||a[1]!==r[1])return r}}},pan:uG((function(e,t,n,i,a,r){var o=mG[i]([r.oldX,r.oldY],[r.newX,r.newY],t,a,n);return o.signal*(e[1]-e[0])*o.pixel/o.pixelLength})),scrollMove:uG((function(e,t,n,i,a,r){return mG[i]([0,0],[r.scrollDelta,r.scrollDelta],t,a,n).signal*(e[1]-e[0])*r.scrollDelta}))};function uG(e){return function(t,n,i,a){var r=this.range,o=r.slice(),s=t.axisModels[0];if(s)return bA(e(o,s,t,n,i,a),o,[0,100],"all"),this.range=o,r[0]!==o[0]||r[1]!==o[1]?o:void 0}}var mG={grid:function(e,t,n,i,a){var r=n.axis,o={},s=a.model.coordinateSystem.getRect();return e=e||[0,0],"x"===r.dim?(o.pixel=t[0]-e[0],o.pixelLength=s.width,o.pixelStart=s.x,o.signal=r.inverse?1:-1):(o.pixel=t[1]-e[1],o.pixelLength=s.height,o.pixelStart=s.y,o.signal=r.inverse?-1:1),o},polar:function(e,t,n,i,a){var r=n.axis,o={},s=a.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),p=s.getAngleAxis().getExtent();return e=e?s.pointToCoord(e):[0,0],t=s.pointToCoord(t),"radiusAxis"===n.mainType?(o.pixel=t[0]-e[0],o.pixelLength=l[1]-l[0],o.pixelStart=l[0],o.signal=r.inverse?1:-1):(o.pixel=t[1]-e[1],o.pixelLength=p[1]-p[0],o.pixelStart=p[0],o.signal=r.inverse?-1:1),o},singleAxis:function(e,t,n,i,a){var r=n.axis,o=a.model.coordinateSystem.getRect(),s={};return e=e||[0,0],"horizontal"===r.orient?(s.pixel=t[0]-e[0],s.pixelLength=o.width,s.pixelStart=o.x,s.signal=r.inverse?1:-1):(s.pixel=t[1]-e[1],s.pixelLength=o.height,s.pixelStart=o.y,s.signal=r.inverse?-1:1),s}};function hG(e){KL(e),e.registerComponentModel(aG),e.registerComponentView(cG),pG(e)}var gG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.type="dataZoom.slider",t.layoutMode="box",t.defaultOption=gy(qL.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:"#d2dbee",borderRadius:3,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#d2dbee",width:.5},areaStyle:{color:"#d2dbee",opacity:.2}},selectedDataBackground:{lineStyle:{color:"#8fb0f7",width:.5},areaStyle:{color:"#8fb0f7",opacity:.2}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:"#fff",borderColor:"#ACB8D1"},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:"#D2DBEE",opacity:.7},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#6E7079"},brushSelect:!0,brushStyle:{color:"rgba(135,175,274,0.15)"},emphasis:{handleStyle:{borderColor:"#8FB0F7"},moveHandleStyle:{color:"#8FB0F7"}}}),t}(qL),fG=Em,yG="horizontal",vG="vertical",xG=["line","bar","candlestick","scatter","custom"],bG={easing:"cubicOut",duration:100,delay:0},wG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._displayables={},n}return ze(t,e),t.prototype.init=function(e,t){this.api=t,this._onBrush=Gr(this._onBrush,this),this._onBrushEnd=Gr(this._onBrushEnd,this)},t.prototype.render=function(t,n,i,a){if(e.prototype.render.apply(this,arguments),Mw(this,"_dispatchZoomAction",t.get("throttle"),"fixRate"),this._orient=t.getOrient(),!1!==t.get("show")){if(t.noTarget())return this._clear(),void this.group.removeAll();a&&"dataZoom"===a.type&&a.from===this.uid||this._buildView(),this._updateView()}else this.group.removeAll()},t.prototype.dispose=function(){this._clear(),e.prototype.dispose.apply(this,arguments)},t.prototype._clear=function(){kw(this,"_dispatchZoomAction");var e=this.api.getZr();e.off("mousemove",this._onBrush),e.off("mouseup",this._onBrushEnd)},t.prototype._buildView=function(){var e=this.group;e.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var t=this._displayables.sliderGroup=new Mc;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),e.add(t),this._positionGroup()},t.prototype._resetLocation=function(){var e=this.dataZoomModel,t=this.api,n=e.get("brushSelect")?7:0,i=this._findCoordRect(),a={width:t.getWidth(),height:t.getHeight()},r=this._orient===yG?{right:a.width-i.x-i.width,top:a.height-30-7-n,width:i.width,height:30}:{right:7,top:i.y,width:30,height:i.height},o=xv(e.option);Rr(["right","top","width","height"],(function(e){"ph"===o[e]&&(o[e]=r[e])}));var s=gv(o,a);this._location={x:s.x,y:s.y},this._size=[s.width,s.height],this._orient===vG&&this._size.reverse()},t.prototype._positionGroup=function(){var e=this.group,t=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),a=i&&i.get("inverse"),r=this._displayables.sliderGroup,o=(this._dataShadowInfo||{}).otherAxisInverse;r.attr(n!==yG||a?n===yG&&a?{scaleY:o?1:-1,scaleX:-1}:n!==vG||a?{scaleY:o?-1:1,scaleX:-1,rotation:Math.PI/2}:{scaleY:o?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:o?1:-1,scaleX:1});var s=e.getBoundingRect([r]);e.x=t.x-s.x,e.y=t.y-s.y,e.markRedraw()},t.prototype._getViewExtent=function(){return[0,this._size[0]]},t.prototype._renderBackground=function(){var e=this.dataZoomModel,t=this._size,n=this._displayables.sliderGroup,i=e.get("brushSelect");n.add(new fG({silent:!0,shape:{x:0,y:0,width:t[0],height:t[1]},style:{fill:e.get("backgroundColor")},z2:-40}));var a=new fG({shape:{x:0,y:0,width:t[0],height:t[1]},style:{fill:"transparent"},z2:0,onclick:Gr(this._onClickPanel,this)}),r=this.api.getZr();i?(a.on("mousedown",this._onBrushStart,this),a.cursor="crosshair",r.on("mousemove",this._onBrush),r.on("mouseup",this._onBrushEnd)):(r.off("mousemove",this._onBrush),r.off("mouseup",this._onBrushEnd)),n.add(a)},t.prototype._renderDataShadow=function(){var e=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],e){var t=this._size,n=this._shadowSize||[],i=e.series,a=i.getRawData(),r=i.getShadowDim&&i.getShadowDim(),o=r&&a.getDimensionInfo(r)?i.getShadowDim():e.otherDim;if(null!=o){var s=this._shadowPolygonPts,l=this._shadowPolylinePts;if(a!==this._shadowData||o!==this._shadowDim||t[0]!==n[0]||t[1]!==n[1]){var p=a.getDataExtent(o),c=.3*(p[1]-p[0]);p=[p[0]-c,p[1]+c];var d,u=[0,t[1]],m=[0,t[0]],h=[[t[0],0],[0,0]],g=[],f=m[1]/(a.count()-1),y=0,v=Math.round(a.count()/t[0]);a.each([o],(function(e,t){if(v>0&&t%v)y+=f;else{var n=null==e||isNaN(e)||""===e,i=n?0:Lc(e,p,u,!0);n&&!d&&t?(h.push([h[h.length-1][0],0]),g.push([g[g.length-1][0],0])):!n&&d&&(h.push([y,0]),g.push([y,0])),h.push([y,i]),g.push([y,i]),y+=f,d=n}})),s=this._shadowPolygonPts=h,l=this._shadowPolylinePts=g}this._shadowData=a,this._shadowDim=o,this._shadowSize=[t[0],t[1]];for(var x=this.dataZoomModel,b=0;b<3;b++){var w=S(1===b);this._displayables.sliderGroup.add(w),this._displayables.dataShadowSegs.push(w)}}}function S(e){var t=x.getModel(e?"selectedDataBackground":"dataBackground"),n=new Mc,i=new kg({shape:{points:s},segmentIgnoreThreshold:1,style:t.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),a=new Dg({shape:{points:l},segmentIgnoreThreshold:1,style:t.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return n.add(i),n.add(a),n}},t.prototype._prepareDataShadowInfo=function(){var e=this.dataZoomModel,t=e.get("showDataShadow");if(!1!==t){var n,i=this.ecModel;return e.eachTargetAxis((function(a,r){var o=e.getAxisProxy(a,r);o&&Rr(o.getTargetSeriesModels(),(function(e){if(!(n||!0!==t&&Dr(xG,e.get("type"))<0)){var o,s=i.getComponent(BL(a),r).axis,l=function(e){var t={x:"y",y:"x",radius:"angle",angle:"radius"};return t[e]}(a),p=e.coordinateSystem;null!=l&&p.getOtherAxis&&(o=p.getOtherAxis(s).inverse),l=e.getData().mapDimension(l),n={thisAxis:s,series:e,thisDim:a,otherDim:l,otherAxisInverse:o}}}),this)}),this),n}},t.prototype._renderHandle=function(){var e=this.group,t=this._displayables,n=t.handles=[null,null],i=t.handleLabels=[null,null],a=this._displayables.sliderGroup,r=this._size,o=this.dataZoomModel,s=this.api,l=o.get("borderRadius")||0,p=o.get("brushSelect"),c=t.filler=new fG({silent:p,style:{fill:o.get("fillerColor")},textConfig:{position:"inside"}});a.add(c),a.add(new fG({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:r[0],height:r[1],r:l},style:{stroke:o.get("dataBackgroundColor")||o.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}})),Rr([0,1],(function(t){var r=o.get("handleIcon");!ES[r]&&r.indexOf("path://")<0&&r.indexOf("image://")<0&&(r="path://"+r);var s=PS(r,-1,0,2,2,null,!0);s.attr({cursor:SG(this._orient),draggable:!0,drift:Gr(this._onDragMove,this,t),ondragend:Gr(this._onDragEnd,this),onmouseover:Gr(this._showDataInfo,this,!0),onmouseout:Gr(this._showDataInfo,this,!1),z2:5});var l=s.getBoundingRect(),p=o.get("handleSize");this._handleHeight=Vc(p,this._size[1]),this._handleWidth=l.width/l.height*this._handleHeight,s.setStyle(o.getModel("handleStyle").getItemStyle()),s.style.strokeNoScale=!0,s.rectHover=!0,s.ensureState("emphasis").style=o.getModel(["emphasis","handleStyle"]).getItemStyle(),Dh(s);var c=o.get("handleColor");null!=c&&(s.style.fill=c),a.add(n[t]=s);var d=o.getModel("textStyle");e.add(i[t]=new Pm({silent:!0,invisible:!0,style:jf(d,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:d.getTextColor(),font:d.getFont()}),z2:10}))}),this);var d=c;if(p){var u=Vc(o.get("moveHandleSize"),r[1]),m=t.moveHandle=new Em({style:o.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:r[1]-.5,height:u}}),h=.8*u,g=t.moveHandleIcon=PS(o.get("moveHandleIcon"),-h/2,-h/2,h,h,"#fff",!0);g.silent=!0,g.y=r[1]+u/2-.5,m.ensureState("emphasis").style=o.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var f=Math.min(r[1]/2,Math.max(u,10));(d=t.moveZone=new Em({invisible:!0,shape:{y:r[1]-f,height:u+f}})).on("mouseover",(function(){s.enterEmphasis(m)})).on("mouseout",(function(){s.leaveEmphasis(m)})),a.add(m),a.add(g),a.add(d)}d.attr({draggable:!0,cursor:SG(this._orient),drift:Gr(this._onDragMove,this,"all"),ondragstart:Gr(this._showDataInfo,this,!0),ondragend:Gr(this._onDragEnd,this),onmouseover:Gr(this._showDataInfo,this,!0),onmouseout:Gr(this._showDataInfo,this,!1)})},t.prototype._resetInterval=function(){var e=this._range=this.dataZoomModel.getPercentRange(),t=this._getViewExtent();this._handleEnds=[Lc(e[0],[0,100],t,!0),Lc(e[1],[0,100],t,!0)]},t.prototype._updateInterval=function(e,t){var n=this.dataZoomModel,i=this._handleEnds,a=this._getViewExtent(),r=n.findRepresentativeAxisProxy();if(r){var o=r.getMinMaxSpan(),s=[0,100];bA(t,i,a,n.get("zoomLock")?"all":e,null!=o.minSpan?Lc(o.minSpan,s,a,!0):null,null!=o.maxSpan?Lc(o.maxSpan,s,a,!0):null);var l=this._range,p=this._range=Gc([Lc(i[0],a,s,!0),Lc(i[1],a,s,!0)]);return!l||l[0]!==p[0]||l[1]!==p[1]}return!1},t.prototype._updateView=function(e){var t=this._displayables,n=this._handleEnds,i=Gc(n.slice()),a=this._size;Rr([0,1],(function(e){var i=t.handles[e],r=this._handleHeight;i.attr({scaleX:r/2,scaleY:r/2,x:n[e]+(e?-1:1),y:a[1]/2-r/2})}),this),t.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:a[1]});var r={x:i[0],width:i[1]-i[0]};t.moveHandle&&(t.moveHandle.setShape(r),t.moveZone.setShape(r),t.moveZone.getBoundingRect(),t.moveHandleIcon&&t.moveHandleIcon.attr("x",r.x+r.width/2));for(var o=t.dataShadowSegs,s=[0,i[0],i[1],a[0]],l=0;lt[0]||n[1]<0||n[1]>t[1])){var i=this._handleEnds,a=(i[0]+i[1])/2,r=this._updateInterval("all",n[0]-a);this._updateView(),r&&this._dispatchZoomAction(!1)}},t.prototype._onBrushStart=function(e){var t=e.offsetX,n=e.offsetY;this._brushStart=new ws(t,n),this._brushing=!0,this._brushStartTime=+new Date},t.prototype._onBrushEnd=function(e){if(this._brushing){var t=this._displayables.brushRect;if(this._brushing=!1,t){t.attr("ignore",!0);var n=t.shape;if(!(+new Date-this._brushStartTime<200&&Math.abs(n.width)<5)){var i=this._getViewExtent(),a=[0,100];this._range=Gc([Lc(n.x,i,a,!0),Lc(n.x+n.width,i,a,!0)]),this._handleEnds=[n.x,n.x+n.width],this._updateView(),this._dispatchZoomAction(!1)}}}},t.prototype._onBrush=function(e){this._brushing&&(ls(e.event),this._updateBrushRect(e.offsetX,e.offsetY))},t.prototype._updateBrushRect=function(e,t){var n=this._displayables,i=this.dataZoomModel,a=n.brushRect;a||(a=n.brushRect=new fG({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(a)),a.attr("ignore",!1);var r=this._brushStart,o=this._displayables.sliderGroup,s=o.transformCoordToLocal(e,t),l=o.transformCoordToLocal(r.x,r.y),p=this._size;s[0]=Math.max(Math.min(p[0],s[0]),0),a.setShape({x:l[0],y:0,width:s[0]-l[0],height:p[1]})},t.prototype._dispatchZoomAction=function(e){var t=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:e?bG:null,start:t[0],end:t[1]})},t.prototype._findCoordRect=function(){var e,t=LL(this.dataZoomModel).infoList;if(!e&&t.length){var n=t[0].model.coordinateSystem;e=n.getRect&&n.getRect()}if(!e){var i=this.api.getWidth(),a=this.api.getHeight();e={x:.2*i,y:.2*a,width:.6*i,height:.6*a}}return e},t.type="dataZoom.slider",t}(zL);function SG(e){return"vertical"===e?"ns-resize":"ew-resize"}function CG(e){e.registerComponentModel(gG),e.registerComponentView(wG),KL(e)}function _G(e){aI(hG),aI(CG)}var TG=function(e,t,n){var i=Ir((IG[e]||{})[t]);return n&&Ur(i)?i[i.length-1]:i},IG={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},EG=aO.mapVisual,MG=aO.eachVisual,kG=Ur,PG=Rr,DG=Gc,OG=Lc,AG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n.stateList=["inRange","outOfRange"],n.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],n.layoutMode={type:"box",ignoreSize:!0},n.dataBound=[-1/0,1/0],n.targetVisuals={},n.controllerVisuals={},n}return ze(t,e),t.prototype.init=function(e,t,n){this.mergeDefaultAndTheme(e,n)},t.prototype.optionUpdated=function(e,t){var n=this.option;!t&&eq(n,e,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},t.prototype.resetVisual=function(e){var t=this.stateList;e=Gr(e,this),this.controllerVisuals=JV(this.option.controller,t,e),this.targetVisuals=JV(this.option.target,t,e)},t.prototype.getItemSymbol=function(){return null},t.prototype.getTargetSeriesIndices=function(){var e=this.option.seriesIndex,t=[];return null==e||"all"===e?this.ecModel.eachSeries((function(e,n){t.push(n)})):t=dd(e),t},t.prototype.eachTargetSeries=function(e,t){Rr(this.getTargetSeriesIndices(),(function(n){var i=this.ecModel.getSeriesByIndex(n);i&&e.call(t,i)}),this)},t.prototype.isTargetSeries=function(e){var t=!1;return this.eachTargetSeries((function(n){n===e&&(t=!0)})),t},t.prototype.formatValueText=function(e,t,n){var i,a=this.option,r=a.precision,o=this.dataBound,s=a.formatter;n=n||["<",">"],Ur(e)&&(e=e.slice(),i=!0);var l=t?e:i?[p(e[0]),p(e[1])]:p(e);return Hr(s)?s.replace("{value}",i?l[0]:l).replace("{value2}",i?l[1]:l):jr(s)?i?s(e[0],e[1]):s(e):i?e[0]===o[0]?n[0]+" "+l[1]:e[1]===o[1]?n[1]+" "+l[0]:l[0]+" - "+l[1]:l;function p(e){return e===o[0]?"min":e===o[1]?"max":(+e).toFixed(Math.min(r,20))}},t.prototype.resetExtent=function(){var e=this.option,t=DG([e.min,e.max]);this._dataExtent=t},t.prototype.getDataDimensionIndex=function(e){var t=this.option.dimension;if(null!=t)return e.getDimensionIndex(t);for(var n=e.dimensions,i=n.length-1;i>=0;i--){var a=n[i],r=e.getDimensionInfo(a);if(!r.isCalculationCoord)return r.storeDimIndex}},t.prototype.getExtent=function(){return this._dataExtent.slice()},t.prototype.completeVisualOption=function(){var e=this.ecModel,t=this.option,n={inRange:t.inRange,outOfRange:t.outOfRange},i=t.target||(t.target={}),a=t.controller||(t.controller={});Er(i,n),Er(a,n);var r=this.isCategory();function o(n){kG(t.color)&&!n.inRange&&(n.inRange={color:t.color.slice().reverse()}),n.inRange=n.inRange||{color:e.get("gradientColor")}}o.call(this,i),o.call(this,a),function(e,t,n){var i=e[t],a=e[n];i&&!a&&(a=e[n]={},PG(i,(function(e,t){if(aO.isValidType(t)){var n=TG(t,"inactive",r);null!=n&&(a[t]=n,"color"!==t||a.hasOwnProperty("opacity")||a.hasOwnProperty("colorAlpha")||(a.opacity=[0,0]))}})))}.call(this,i,"inRange","outOfRange"),function(e){var t=(e.inRange||{}).symbol||(e.outOfRange||{}).symbol,n=(e.inRange||{}).symbolSize||(e.outOfRange||{}).symbolSize,i=this.get("inactiveColor"),a=this.getItemSymbol()||"roundRect";PG(this.stateList,(function(o){var s=this.itemSize,l=e[o];l||(l=e[o]={color:r?i:[i]}),null==l.symbol&&(l.symbol=t&&Ir(t)||(r?a:[a])),null==l.symbolSize&&(l.symbolSize=n&&Ir(n)||(r?s[0]:[s[0],s[0]])),l.symbol=EG(l.symbol,(function(e){return"none"===e?a:e}));var p=l.symbolSize;if(null!=p){var c=-1/0;MG(p,(function(e){e>c&&(c=e)})),l.symbolSize=EG(p,(function(e){return OG(e,[0,c],[0,s[0]],!0)}))}}),this)}.call(this,a)},t.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},t.prototype.isCategory=function(){return!!this.option.categories},t.prototype.setSelected=function(e){},t.prototype.getSelected=function(){return null},t.prototype.getValueState=function(e){return null},t.prototype.getVisualMeta=function(e){return null},t.type="visualMap",t.dependencies=["series"],t.defaultOption={show:!0,z:4,seriesIndex:"all",min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,textStyle:{color:"#333"}},t}(Sv),FG=[20,140],RG=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.optionUpdated=function(t,n){e.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual((function(e){e.mappingMethod="linear",e.dataExtent=this.getExtent()})),this._resetRange()},t.prototype.resetItemSize=function(){e.prototype.resetItemSize.apply(this,arguments);var t=this.itemSize;(null==t[0]||isNaN(t[0]))&&(t[0]=FG[0]),(null==t[1]||isNaN(t[1]))&&(t[1]=FG[1])},t.prototype._resetRange=function(){var e=this.getExtent(),t=this.option.range;!t||t.auto?(e.auto=1,this.option.range=e):Ur(t)&&(t[0]>t[1]&&t.reverse(),t[0]=Math.max(t[0],e[0]),t[1]=Math.min(t[1],e[1]))},t.prototype.completeVisualOption=function(){e.prototype.completeVisualOption.apply(this,arguments),Rr(this.stateList,(function(e){var t=this.option.controller[e].symbolSize;t&&t[0]!==t[1]&&(t[0]=t[1]/3)}),this)},t.prototype.setSelected=function(e){this.option.range=e.slice(),this._resetRange()},t.prototype.getSelected=function(){var e=this.getExtent(),t=Gc((this.get("range")||[]).slice());return t[0]>e[1]&&(t[0]=e[1]),t[1]>e[1]&&(t[1]=e[1]),t[0]=n[1]||e<=t[1])?"inRange":"outOfRange"},t.prototype.findTargetDataIndices=function(e){var t=[];return this.eachTargetSeries((function(n){var i=[],a=n.getData();a.each(this.getDataDimensionIndex(a),(function(t,n){e[0]<=t&&t<=e[1]&&i.push(n)}),this),t.push({seriesId:n.id,dataIndex:i})}),this),t},t.prototype.getVisualMeta=function(e){var t=BG(this,"outOfRange",this.getExtent()),n=BG(this,"inRange",this.option.range.slice()),i=[];function a(t,n){i.push({value:t,color:e(t,n)})}for(var r=0,o=0,s=n.length,l=t.length;oe[1])break;n.push({color:this.getControllerVisual(r,"color",t),offset:a/100})}return n.push({color:this.getControllerVisual(e[1],"color",t),offset:1}),n},t.prototype._createBarPoints=function(e,t){var n=this.visualMapModel.itemSize;return[[n[0]-t[0],e[0]],[n[0],e[0]],[n[0],e[1]],[n[0]-t[1],e[1]]]},t.prototype._createBarGroup=function(e){var t=this._orient,n=this.visualMapModel.get("inverse");return new Mc("horizontal"!==t||n?"horizontal"===t&&n?{scaleX:"bottom"===e?-1:1,rotation:-Math.PI/2}:"vertical"!==t||n?{scaleX:"left"===e?1:-1}:{scaleX:"left"===e?1:-1,scaleY:-1}:{scaleX:"bottom"===e?1:-1,rotation:Math.PI/2})},t.prototype._updateHandle=function(e,t){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,a=n.handleThumbs,r=n.handleLabels,o=i.itemSize,s=i.getExtent();zG([0,1],(function(l){var p=a[l];p.setStyle("fill",t.handlesColor[l]),p.y=e[l];var c=GG(e[l],[0,o[1]],s,!0),d=this.getControllerVisual(c,"symbolSize");p.scaleX=p.scaleY=d/o[0],p.x=o[0]-d/2;var u=Tf(n.handleLabelPoints[l],_f(p,this.group));r[l].setStyle({x:u[0],y:u[1],text:i.formatValueText(this._dataInterval[l]),verticalAlign:"middle",align:"vertical"===this._orient?this._applyTransform("left",n.mainGroup):"center"})}),this)}},t.prototype._showIndicator=function(e,t,n,i){var a=this.visualMapModel,r=a.getExtent(),o=a.itemSize,s=[0,o[1]],l=this._shapes,p=l.indicator;if(p){p.attr("invisible",!1);var c=this.getControllerVisual(e,"color",{convertOpacityToAlpha:!0}),d=this.getControllerVisual(e,"symbolSize"),u=GG(e,r,s,!0),m=o[0]-d/2,h={x:p.x,y:p.y};p.y=u,p.x=m;var g=Tf(l.indicatorLabelPoint,_f(p,this.group)),f=l.indicatorLabel;f.attr("invisible",!1);var y=this._applyTransform("left",l.mainGroup),v="horizontal"===this._orient;f.setStyle({text:(n||"")+a.formatValueText(t),verticalAlign:v?y:"middle",align:v?"center":y});var x={x:m,y:u,style:{fill:c}},b={style:{x:g[0],y:g[1]}};if(a.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var w={duration:100,easing:"cubicInOut",additive:!0};p.x=h.x,p.y=h.y,p.animateTo(x,w),f.animateTo(b,w)}else p.attr(x),f.attr(b);this._firstShowIndicator=!1;var S=this._shapes.handleLabels;if(S)for(var C=0;Ca[1]&&(p[1]=1/0),t&&(p[0]===-1/0?this._showIndicator(l,p[1],"< ",o):p[1]===1/0?this._showIndicator(l,p[0],"> ",o):this._showIndicator(l,l,"≈ ",o));var c=this._hoverLinkDataIndices,d=[];(t||$G(n))&&(d=this._hoverLinkDataIndices=n.findTargetDataIndices(p));var u=function(e,t){var n={},i={};return a(e||[],n),a(t||[],i,n),[r(n),r(i)];function a(e,t,n){for(var i=0,a=e.length;i=0&&(a.dimension=r,i.push(a))}})),e.getData().setVisual("visualMeta",i)}}];function QG(e,t,n,i){for(var a=t.targetVisuals[i],r=aO.prepareVisualTypes(a),o={color:hS(e.getData(),"color")},s=0,l=r.length;s0:e.splitNumber>0)&&!e.calculable?"piecewise":"continuous"})),e.registerAction(YG,XG),Rr(ZG,(function(t){e.registerVisual(e.PRIORITY.VISUAL.COMPONENT,t)})),e.registerPreprocessor(ez))}function az(e){e.registerComponentModel(RG),e.registerComponentView(HG),iz(e)}var rz=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n._pieceList=[],n}return ze(t,e),t.prototype.optionUpdated=function(t,n){e.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var i=this._mode=this._determineMode();this._pieceList=[],oz[this._mode].call(this,this._pieceList),this._resetSelected(t,n);var a=this.option.categories;this.resetVisual((function(e,t){"categories"===i?(e.mappingMethod="category",e.categories=Ir(a)):(e.dataExtent=this.getExtent(),e.mappingMethod="piecewise",e.pieceList=Br(this._pieceList,(function(e){return e=Ir(e),"inRange"!==t&&(e.visual=null),e})))}))},t.prototype.completeVisualOption=function(){var t=this.option,n={},i=aO.listVisualTypes(),a=this.isCategory();function r(e,t,n){return e&&e[t]&&e[t].hasOwnProperty(n)}Rr(t.pieces,(function(e){Rr(i,(function(t){e.hasOwnProperty(t)&&(n[t]=1)}))})),Rr(n,(function(e,n){var i=!1;Rr(this.stateList,(function(e){i=i||r(t,e,n)||r(t.target,e,n)}),this),!i&&Rr(this.stateList,(function(e){(t[e]||(t[e]={}))[n]=TG(n,"inRange"===e?"active":"inactive",a)}))}),this),e.prototype.completeVisualOption.apply(this,arguments)},t.prototype._resetSelected=function(e,t){var n=this.option,i=this._pieceList,a=(t?n:e).selected||{};if(n.selected=a,Rr(i,(function(e,t){var n=this.getSelectedMapKey(e);a.hasOwnProperty(n)||(a[n]=!0)}),this),"single"===n.selectedMode){var r=!1;Rr(i,(function(e,t){var n=this.getSelectedMapKey(e);a[n]&&(r?a[n]=!1:r=!0)}),this)}},t.prototype.getItemSymbol=function(){return this.get("itemSymbol")},t.prototype.getSelectedMapKey=function(e){return"categories"===this._mode?e.value+"":e.index+""},t.prototype.getPieceList=function(){return this._pieceList},t.prototype._determineMode=function(){var e=this.option;return e.pieces&&e.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},t.prototype.setSelected=function(e){this.option.selected=Ir(e)},t.prototype.getValueState=function(e){var t=aO.findPieceIndex(e,this._pieceList);return null!=t&&this.option.selected[this.getSelectedMapKey(this._pieceList[t])]?"inRange":"outOfRange"},t.prototype.findTargetDataIndices=function(e){var t=[],n=this._pieceList;return this.eachTargetSeries((function(i){var a=[],r=i.getData();r.each(this.getDataDimensionIndex(r),(function(t,i){aO.findPieceIndex(t,n)===e&&a.push(i)}),this),t.push({seriesId:i.id,dataIndex:a})}),this),t},t.prototype.getRepresentValue=function(e){var t;if(this.isCategory())t=e.value;else if(null!=e.value)t=e.value;else{var n=e.interval||[];t=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return t},t.prototype.getVisualMeta=function(e){if(!this.isCategory()){var t=[],n=["",""],i=this,a=this._pieceList.slice();if(a.length){var r=a[0].interval[0];r!==-1/0&&a.unshift({interval:[-1/0,r]}),(r=a[a.length-1].interval[1])!==1/0&&a.push({interval:[r,1/0]})}else a.push({interval:[-1/0,1/0]});var o=-1/0;return Rr(a,(function(e){var t=e.interval;t&&(t[0]>o&&s([o,t[0]],"outOfRange"),s(t.slice()),o=t[1])}),this),{stops:t,outerColors:n}}function s(a,r){var o=i.getRepresentValue({interval:a});r||(r=i.getValueState(o));var s=e(o,r);a[0]===-1/0?n[0]=s:a[1]===1/0?n[1]=s:t.push({value:a[0],color:s},{value:a[1],color:s})}},t.type="visualMap.piecewise",t.defaultOption=gy(AG.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),t}(AG),oz={splitNumber:function(e){var t=this.option,n=Math.min(t.precision,20),i=this.getExtent(),a=t.splitNumber;a=Math.max(parseInt(a,10),1),t.splitNumber=a;for(var r=(i[1]-i[0])/a;+r.toFixed(n)!==r&&n<5;)n++;t.precision=n,r=+r.toFixed(n),t.minOpen&&e.push({interval:[-1/0,i[0]],close:[0,0]});for(var o=0,s=i[0];o","≥"][t[0]]];e.text=e.text||this.formatValueText(null!=e.value?e.value:e.interval,!1,n)}),this)}};function sz(e,t){var n=e.inverse;("vertical"===e.orient?!n:n)&&t.reverse()}var lz=function(e){function t(){var n=null!==e&&e.apply(this,arguments)||this;return n.type=t.type,n}return ze(t,e),t.prototype.doRender=function(){var e=this.group;e.removeAll();var t=this.visualMapModel,n=t.get("textGap"),i=t.textStyleModel,a=i.getFont(),r=i.getTextColor(),o=this._getItemAlign(),s=t.itemSize,l=this._getViewData(),p=l.endsText,c=no(t.get("showLabel",!0),!p);p&&this._renderEndsText(e,p[0],s,c,o),Rr(l.viewPieceList,(function(i){var l=i.piece,p=new Mc;p.onclick=Gr(this._onItemClick,this,l),this._enableHoverLink(p,i.indexInModelPieceList);var d=t.getRepresentValue(l);if(this._createItemSymbol(p,d,[0,0,s[0],s[1]]),c){var u=this.visualMapModel.getValueState(d);p.add(new Pm({style:{x:"right"===o?-n:s[0]+n,y:s[1]/2,text:l.text,verticalAlign:"middle",align:o,font:a,fill:r,opacity:"outOfRange"===u?.5:1}}))}e.add(p)}),this),p&&this._renderEndsText(e,p[1],s,c,o),hv(t.get("orient"),e,t.get("itemGap")),this.renderBackground(e),this.positionGroup(e)},t.prototype._enableHoverLink=function(e,t){var n=this;e.on("mouseover",(function(){return i("highlight")})).on("mouseout",(function(){return i("downplay")}));var i=function(e){var i=n.visualMapModel;i.option.hoverLink&&n.api.dispatchAction({type:e,batch:qG(i.findTargetDataIndices(t),i)})}},t.prototype._getItemAlign=function(){var e=this.visualMapModel,t=e.option;if("vertical"===t.orient)return VG(e,this.api,e.itemSize);var n=t.align;return n&&"auto"!==n||(n="left"),n},t.prototype._renderEndsText=function(e,t,n,i,a){if(t){var r=new Mc,o=this.visualMapModel.textStyleModel;r.add(new Pm({style:jf(o,{x:i?"right"===a?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?a:"center",text:t})})),e.add(r)}},t.prototype._getViewData=function(){var e=this.visualMapModel,t=Br(e.getPieceList(),(function(e,t){return{piece:e,indexInModelPieceList:t}})),n=e.get("text"),i=e.get("orient"),a=e.get("inverse");return("horizontal"===i?a:!a)?t.reverse():n&&(n=n.slice().reverse()),{viewPieceList:t,endsText:n}},t.prototype._createItemSymbol=function(e,t,n){e.add(PS(this.getControllerVisual(t,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(t,"color")))},t.prototype._onItemClick=function(e){var t=this.visualMapModel,n=t.option,i=n.selectedMode;if(i){var a=Ir(n.selected),r=t.getSelectedMapKey(e);"single"===i||!0===i?(a[r]=!0,Rr(a,(function(e,t){a[t]=t===r}))):a[r]=!a[r],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:a})}},t.type="visualMap.piecewise",t}(NG);function pz(e){e.registerComponentModel(rz),e.registerComponentView(lz),iz(e)}function cz(e){aI(az),aI(pz)}Cd();var dz={value:"eq","<":"lt","<=":"lte",">":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},uz=function(){function e(e){if(null==(this._condVal=Hr(e)?new RegExp(e):eo(e)?e:null)){var t="";0,sd(t)}}return e.prototype.evaluate=function(e){var t=typeof e;return Hr(t)?this._condVal.test(e):!!$r(t)&&this._condVal.test(e+"")},e}(),mz=function(){function e(){}return e.prototype.evaluate=function(){return this.value},e}(),hz=function(){function e(){}return e.prototype.evaluate=function(){for(var e=this.children,t=0;t=_z:-l>=_z),u=l>0?l%_z:l%_z+_z,m=!1;m=!!d||!tp(c)&&u>=Cz==!!p;var h=e+n*Sz(r),g=t+i*wz(r);this._start&&this._add("M",h,g);var f=Math.round(a*Tz);if(d){var y=1/this._p,v=(p?1:-1)*(_z-y);this._add("A",n,i,f,1,+p,e+n*Sz(r+v),t+i*wz(r+v)),y>.01&&this._add("A",n,i,f,0,+p,h,g)}else{var x=e+n*Sz(o),b=t+i*wz(o);this._add("A",n,i,f,+m,+p,x,b)}},e.prototype.rect=function(e,t,n,i){this._add("M",e,t),this._add("l",n,0),this._add("l",0,i),this._add("l",-n,0),this._add("Z")},e.prototype.closePath=function(){this._d.length>0&&this._add("Z")},e.prototype._add=function(e,t,n,i,a,r,o,s,l){for(var p=[],c=this._p,d=1;d"}(a,r)+("style"!==a?Jo(o):o||"")+(i?""+n+Br(i,(function(t){return e(t)})).join(n)+n:"")+("")}(e)}function Lz(e){return{zrId:e,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssStyleCache:{},cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function Vz(e,t,n,i){return Bz("svg","root",{width:e,height:t,xmlns:Oz,"xmlns:xlink":Az,version:"1.1",baseProfile:"full",viewBox:!!i&&"0 0 "+e+" "+t},n)}var qz=0;function Gz(){return qz++}var zz={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},Uz="transform-origin";function jz(e,t,n){var i=kr({},e.shape);kr(i,t),e.buildPath(n,i);var a=new Iz;return a.reset(dp(e)),n.rebuildPath(a,1),a.generateStr(),a.getStr()}function Hz(e,t){var n=t.originX,i=t.originY;(n||i)&&(e[Uz]=n+"px "+i+"px")}var Wz={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function $z(e,t){var n=t.zrId+"-ani-"+t.cssAnimIdx++;return t.cssAnims[n]=e,n}function Kz(e){return Hr(e)?zz[e]?"cubic-bezier("+zz[e]+")":_l(e)?e:"":""}function Yz(e,t,n,i){var a=e.animators,r=a.length,o=[];if(e instanceof Gg){var s=function(e,t,n){var i,a,r=e.shape.paths,o={};if(Rr(r,(function(e){var t=Lz(n.zrId);t.animation=!0,Yz(e,{},t,!0);var r=t.cssAnims,s=t.cssNodes,l=qr(r),p=l.length;if(p){var c=r[a=l[p-1]];for(var d in c){var u=c[d];o[d]=o[d]||{d:""},o[d].d+=u.d||""}for(var m in s){var h=s[m].animation;h.indexOf(a)>=0&&(i=h)}}})),i){t.d=!1;var s=$z(o,n);return i.replace(a,s)}}(e,t,n);if(s)o.push(s);else if(!r)return}else if(!r)return;for(var l={},p=0;p0})).length)return $z(c,n)+" "+a[0]+" both"}for(var f in l){(s=g(l[f]))&&o.push(s)}if(o.length){var y=n.zrId+"-cls-"+Gz();n.cssNodes["."+y]={animation:o.join(",")},t.class=y}}function Xz(e,t,n,i){var a=JSON.stringify(e),r=n.cssStyleCache[a];r||(r=n.zrId+"-cls-"+Gz(),n.cssStyleCache[a]=r,n.cssNodes["."+r+(i?":hover":"")]=e),t.class=t.class?t.class+" "+r:r}var Zz=Math.round;function Qz(e){return e&&Hr(e.src)}function Jz(e){return e&&jr(e.toDataURL)}function eU(e,t,n,i){Dz((function(a,r){var o="fill"===a||"stroke"===a;o&&pp(r)?dU(t,e,a,i):o&&op(r)?uU(n,e,a,i):e[a]=o&&"none"===r?"transparent":r}),t,n,!1),function(e,t,n){var i=e.style;if(function(e){return e&&(e.shadowBlur||e.shadowOffsetX||e.shadowOffsetY)}(i)){var a=function(e){var t=e.style,n=e.getGlobalScale();return[t.shadowColor,(t.shadowBlur||0).toFixed(2),(t.shadowOffsetX||0).toFixed(2),(t.shadowOffsetY||0).toFixed(2),n[0],n[1]].join(",")}(e),r=n.shadowCache,o=r[a];if(!o){var s=e.getGlobalScale(),l=s[0],p=s[1];if(!l||!p)return;var c=i.shadowOffsetX||0,d=i.shadowOffsetY||0,u=i.shadowBlur,m=Jl(i.shadowColor),h=m.opacity,g=m.color,f=u/2/l+" "+u/2/p;o=n.zrId+"-s"+n.shadowIdx++,n.defs[o]=Bz("filter",o,{id:o,x:"-100%",y:"-100%",width:"300%",height:"300%"},[Bz("feDropShadow","",{dx:c/l,dy:d/p,stdDeviation:f,"flood-color":g,"flood-opacity":h})]),r[a]=o}t.filter=cp(o)}}(n,e,i)}function tU(e,t){var n=Rc(t);n&&(n.each((function(t,n){null!=t&&(e[(Fz+n).toLowerCase()]=t+"")})),t.isSilent()&&(e[Fz+"silent"]="true"))}function nU(e){return tp(e[0]-1)&&tp(e[1])&&tp(e[2])&&tp(e[3]-1)}function iU(e,t,n){if(t&&(!function(e){return tp(e[4])&&tp(e[5])}(t)||!nU(t))){var i=n?10:1e4;e.transform=nU(t)?"translate("+Zz(t[4]*i)/i+" "+Zz(t[5]*i)/i+")":function(e){return"matrix("+np(e[0])+","+np(e[1])+","+np(e[2])+","+np(e[3])+","+ip(e[4])+","+ip(e[5])+")"}(t)}}function aU(e,t,n){for(var i=e.points,a=[],r=0;r=0&&o||r;s&&(a=Zl(s))}var l=i.lineWidth;l&&(l/=!i.strokeNoScale&&e.transform?e.transform[0]:1);var p={cursor:"pointer"};a&&(p.fill=a),i.stroke&&(p.stroke=i.stroke),l&&(p["stroke-width"]=l),Xz(p,t,n,!0)}}(e,r,t),Bz(s,e.id+"",r)}function cU(e,t){return e instanceof gm?pU(e,t):e instanceof bm?function(e,t){var n=e.style,i=n.image;if(i&&!Hr(i)&&(Qz(i)?i=i.src:Jz(i)&&(i=i.toDataURL())),i){var a=n.x||0,r=n.y||0,o={href:i,width:n.width,height:n.height};return a&&(o.x=a),r&&(o.y=r),iU(o,e.transform),eU(o,n,e,t),tU(o,e),t.animation&&Yz(e,o,t),Bz("image",e.id+"",o)}}(e,t):e instanceof ym?function(e,t){var n=e.style,i=n.text;if(null!=i&&(i+=""),i&&!isNaN(n.x)&&!isNaN(n.y)){var a=n.font||sr,r=n.x||0,o=function(e,t,n){return"top"===n?e+=t/2:"bottom"===n&&(e-=t/2),e}(n.y||0,gc(a),n.textBaseline),s={"dominant-baseline":"central","text-anchor":ap[n.textAlign]||n.textAlign};if(Bm(n)){var l="",p=n.fontStyle,c=Fm(n.fontSize);if(!parseFloat(c))return;var d=n.fontFamily||or,u=n.fontWeight;l+="font-size:"+c+";font-family:"+d+";",p&&"normal"!==p&&(l+="font-style:"+p+";"),u&&"normal"!==u&&(l+="font-weight:"+u+";"),s.style=l}else s.style="font: "+a;return i.match(/\s/)&&(s["xml:space"]="preserve"),r&&(s.x=r),o&&(s.y=o),iU(s,e.transform),eU(s,n,e,t),tU(s,e),t.animation&&Yz(e,s,t),Bz("text",e.id+"",s,void 0,i)}}(e,t):void 0}function dU(e,t,n,i){var a,r=e[n],o={gradientUnits:r.global?"userSpaceOnUse":"objectBoundingBox"};if(sp(r))a="linearGradient",o.x1=r.x,o.y1=r.y,o.x2=r.x2,o.y2=r.y2;else{if(!lp(r))return void 0;a="radialGradient",o.cx=io(r.x,.5),o.cy=io(r.y,.5),o.r=io(r.r,.5)}for(var s=r.colorStops,l=[],p=0,c=s.length;pl?EU(e,null==n[d+1]?null:n[d+1].elm,n,s,d):MU(e,t,o,l))}(n,i,a):CU(a)?(CU(e.text)&&bU(n,""),EU(n,null,a,0,a.length-1)):CU(i)?MU(n,i,0,i.length-1):CU(e.text)&&bU(n,""):e.text!==t.text&&(CU(i)&&MU(n,i,0,i.length-1),bU(n,t.text)))}var DU=0,OU=function(){function e(e,t,n){if(this.type="svg",this.refreshHover=AU("refreshHover"),this.configLayer=AU("configLayer"),this.storage=t,this._opts=n=kr({},n),this.root=e,this._id="zr"+DU++,this._oldVNode=Vz(n.width,n.height),e&&!n.ssr){var i=this._viewport=document.createElement("div");i.style.cssText="position:relative;overflow:hidden";var a=this._svgDom=this._oldVNode.elm=Rz("svg");kU(null,this._oldVNode),i.appendChild(a),e.appendChild(i)}this.resize(n.width,n.height)}return e.prototype.getType=function(){return this.type},e.prototype.getViewportRoot=function(){return this._viewport},e.prototype.getViewportRootOffset=function(){var e=this.getViewportRoot();if(e)return{offsetLeft:e.offsetLeft||0,offsetTop:e.offsetTop||0}},e.prototype.getSvgDom=function(){return this._svgDom},e.prototype.refresh=function(){if(this.root){var e=this.renderToVNode({willUpdate:!0});e.attrs.style="position:absolute;left:0;top:0;user-select:none",function(e,t){if(TU(e,t))PU(e,t);else{var n=e.elm,i=vU(n);IU(t),null!==i&&(gU(i,t.elm,xU(n)),MU(i,[e],0,0))}}(this._oldVNode,e),this._oldVNode=e}},e.prototype.renderOneToVNode=function(e){return cU(e,Lz(this._id))},e.prototype.renderToVNode=function(e){e=e||{};var t=this.storage.getDisplayList(!0),n=this._width,i=this._height,a=Lz(this._id);a.animation=e.animation,a.willUpdate=e.willUpdate,a.compress=e.compress,a.emphasis=e.emphasis;var r=[],o=this._bgVNode=function(e,t,n,i){var a;if(n&&"none"!==n)if(a=Bz("rect","bg",{width:e,height:t,x:"0",y:"0"}),pp(n))dU({fill:n},a.attrs,"fill",i);else if(op(n))uU({style:{fill:n},dirty:wo,getBoundingRect:function(){return{width:e,height:t}}},a.attrs,"fill",i);else{var r=Jl(n),o=r.color,s=r.opacity;a.attrs.fill=o,s<1&&(a.attrs["fill-opacity"]=s)}return a}(n,i,this._backgroundColor,a);o&&r.push(o);var s=e.compress?null:this._mainVNode=Bz("g","main",{},[]);this._paintList(t,a,s?s.children:r),s&&r.push(s);var l=Br(qr(a.defs),(function(e){return a.defs[e]}));if(l.length&&r.push(Bz("defs","defs",{},l)),e.animation){var p=function(e,t,n){var i=(n=n||{}).newline?"\n":"",a=" {"+i,r=i+"}",o=Br(qr(e),(function(t){return t+a+Br(qr(e[t]),(function(n){return n+":"+e[t][n]+";"})).join(i)+r})).join(i),s=Br(qr(t),(function(e){return"@keyframes "+e+a+Br(qr(t[e]),(function(n){return n+a+Br(qr(t[e][n]),(function(i){var a=t[e][n][i];return"d"===i&&(a='path("'+a+'")'),i+":"+a+";"})).join(i)+r})).join(i)+r})).join(i);return o||s?[""].join(i):""}(a.cssNodes,a.cssAnims,{newline:!0});if(p){var c=Bz("style","stl",{},[],p);r.push(c)}}return Vz(n,i,r,e.useViewBox)},e.prototype.renderToString=function(e){return e=e||{},Nz(this.renderToVNode({animation:io(e.cssAnimation,!0),emphasis:io(e.cssEmphasis,!0),willUpdate:!1,compress:!0,useViewBox:io(e.useViewBox,!0)}),{newline:!0})},e.prototype.setBackgroundColor=function(e){this._backgroundColor=e},e.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},e.prototype._paintList=function(e,t,n){for(var i,a,r=e.length,o=[],s=0,l=0,p=0;p=0&&(!d||!a||d[h]!==a[h]);h--);for(var g=m-1;g>h;g--)i=o[--s-1];for(var f=h+1;f=o)}}for(var c=this.__startIndex;c15)break}n.prevElClipPaths&&d.restore()};if(m)if(0===m.length)s=l.__endIndex;else for(var b=u.dpr,w=0;w0&&e>i[0]){for(s=0;se);s++);o=n[i[s]]}if(i.splice(s+1,0,e),n[e]=t,!t.virtual)if(o){var l=o.dom;l.nextSibling?r.insertBefore(t.dom,l.nextSibling):r.appendChild(t.dom)}else r.firstChild?r.insertBefore(t.dom,r.firstChild):r.appendChild(t.dom);t.painter||(t.painter=this)}},e.prototype.eachLayer=function(e,t){for(var n=this._zlevelList,i=0;i0?VU:0),this._needsManuallyCompositing),p.__builtin__||Tr("ZLevel "+l+" has been used by unkown layer "+p.id),p!==r&&(p.__used=!0,p.__startIndex!==a&&(p.__dirty=!0),p.__startIndex=a,p.incremental?p.__drawIndex=-1:p.__drawIndex=a,t(a),r=p),1&s.__dirty&&!s.__inHover&&(p.__dirty=!0,p.incremental&&p.__drawIndex<0&&(p.__drawIndex=a))}t(a),this.eachBuiltinLayer((function(e,t){!e.__used&&e.getElementCount()>0&&(e.__dirty=!0,e.__startIndex=e.__endIndex=e.__drawIndex=0),e.__dirty&&e.__drawIndex<0&&(e.__drawIndex=e.__startIndex)}))},e.prototype.clear=function(){return this.eachBuiltinLayer(this._clearLayer),this},e.prototype._clearLayer=function(e){e.clear()},e.prototype.setBackgroundColor=function(e){this._backgroundColor=e,Rr(this._layers,(function(e){e.setUnpainted()}))},e.prototype.configLayer=function(e,t){if(t){var n=this._layerConfig;n[e]?Er(n[e],t,!0):n[e]=t;for(var i=0;i({legendKey:e,left:!0}),jU=e=>({background:e});function HU(e,n){1&e&&t.ɵɵelementContainer(0)}function WU(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,HU,1,0,"ng-container",8),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e.widgetTitlePanel)}}function $U(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Min")))}function KU(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Max")))}function YU(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Avg")))}function XU(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Total")))}function ZU(e,n){1&e&&(t.ɵɵelementStart(0,"th",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"legend.Latest")))}function QU(e,n){1&e&&t.ɵɵelementContainer(0)}function JU(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].min,"html"),t.ɵɵsanitizeHtml)}}function ej(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].max,"html"),t.ɵɵsanitizeHtml)}}function tj(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].avg,"html"),t.ɵɵsanitizeHtml)}}function nj(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].total,"html"),t.ɵɵsanitizeHtml)}}function ij(e,n){if(1&e&&(t.ɵɵelement(0,"td",16),t.ɵɵpipe(1,"safe")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(1,1,n.legendData.data[e.dataIndex].latest,"html"),t.ɵɵsanitizeHtml)}}function aj(e,n){if(1&e&&(t.ɵɵelementStart(0,"tr")(1,"th"),t.ɵɵtemplate(2,QU,1,0,"ng-container",14),t.ɵɵelementEnd(),t.ɵɵtemplate(3,JU,2,4,"td",15)(4,ej,2,4,"td",15)(5,tj,2,4,"td",15)(6,nj,2,4,"td",15)(7,ij,2,4,"td",15),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2),a=t.ɵɵreference(8);t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",a)("ngTemplateOutletContext",t.ɵɵpureFunction1(7,UU,e)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showMin),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showMax),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showAvg),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showTotal),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===i.legendConfig.showLatest)}}function rj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"table",10)(2,"thead")(3,"tr"),t.ɵɵelement(4,"th"),t.ɵɵtemplate(5,$U,3,3,"th",11)(6,KU,3,3,"th",11)(7,YU,3,3,"th",11)(8,XU,3,3,"th",11)(9,ZU,3,3,"th",11),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"tbody"),t.ɵɵtemplate(11,aj,8,9,"tr",12),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵproperty("ngIf",!0===e.legendConfig.showMin),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showMax),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showAvg),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showTotal),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!0===e.legendConfig.showLatest),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",e.legendKeys)}}function oj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",18),t.ɵɵelement(2,"div",19)(3,"div",20),t.ɵɵpipe(4,"safe"),t.ɵɵelementEnd()()),2&e){const e=n.legendKey,i=n.left;t.ɵɵclassProp("left",i),t.ɵɵadvance(2),t.ɵɵstyleMap(t.ɵɵpureFunction1(8,jU,e.dataKey.color)),t.ɵɵadvance(),t.ɵɵproperty("innerHTML",t.ɵɵpipeBind2(4,5,e.dataKey.label,"html"),t.ɵɵsanitizeHtml)}}class sj{constructor(e,t,n){this.renderer=e,this.sanitizer=t,this.widgetComponent=n,this.updateXAxisTimeWindow=(e,t)=>{e.min=t.minTime,e.max=t.maxTime}}ngOnInit(){this.initEchart(),this.initLegend()}ngAfterViewInit(){this.myChart=function(e,t,n){var i=!(n&&n.ssr);if(i){var a=n_(e);if(a)return a}var r=new GC(e,t,n);return r.id="ec_"+e_++,QC[r.id]=r,i&&Pd(e,t_,r.id),BC(r),sC.trigger("afterinit",r),r}(this.echartContainer.nativeElement,null,{renderer:"svg"}),this.initResize(),this.xAxis=this.setupXAxis(),this.yAxis=this.setupYAxis(),this.option={...this.setupAnimationSettings(),formatter:e=>this.setupTooltipElement(e),backgroundColor:"transparent",darkMode:!1,tooltip:{show:!0,trigger:"axis",confine:!0,padding:[8,12],appendTo:"body",textStyle:{fontFamily:"Roboto",fontSize:12,fontWeight:"normal",lineHeight:16}},grid:[{backgroundColor:null,borderColor:"#ccc",borderWidth:1,bottom:45,left:5,right:5,show:!1,top:10}],xAxis:[this.xAxis],yAxis:[this.yAxis],series:this.setupChartLines(),dataZoom:[{type:"inside",disabled:!1,realtime:!0,filterMode:"none"},{type:"slider",show:!0,showDetail:!1,realtime:!0,filterMode:"none",bottom:5}]},this.myChart.setOption(this.option),this.updateAxisOffset(!1)}onDataUpdated(){const e=[];this.onResize(),this.updateXAxisTimeWindow(this.xAxis,this.ctx.defaultSubscription.timeWindow);for(const t in this.ctx.data){e[t]=[];for(const[n,i]of this.ctx.data[t].data)e[t].push({name:n,value:[n,i]})}const t=[];for(const n of e)t.push({data:n});this.option.series=t,this.myChart.setOption(this.option),this.updateAxisOffset()}updateAxisOffset(e=!0){const t=Ze(this.myChart,this.yAxis.mainType,this.yAxis.id),n=Qe(this.myChart,this.yAxis.mainType,this.yAxis.id,this.yAxis.name),i=Ze(this.myChart,this.xAxis.mainType,this.xAxis.id),a=t+n,r=i+Qe(this.myChart,this.yAxis.mainType,this.yAxis.id,this.yAxis.name)+35;this.option.grid[0].left===a&&this.option.grid[0].bottom===r||(this.option.grid[0].left=a,this.yAxis.nameGap=t,this.option.grid[0].bottom=r,this.xAxis.nameGap=i,this.myChart.setOption(this.option,{replaceMerge:["yAxis","xAxis","grid"],lazyUpdate:e}))}initEchart(){aI([XV,kN,cz,_G,Nq,sL,tP,JE,TM,UM,nP,jB,hE,GU,FU])}initLegend(){this.showLegend=this.ctx.settings.showLegend,this.showLegend&&(this.legendConfig=this.ctx.settings.legendConfig,this.legendData=this.ctx.defaultSubscription.legendData,this.legendKeys=this.legendData.keys,this.legendClass=`legend-${this.legendConfig.position}`,this.legendConfig.sortDataKeys?this.legendKeys=this.legendData.keys.sort(((e,t)=>e.dataKey.label.localeCompare(t.dataKey.label))):this.legendKeys=this.legendData.keys)}initResize(){this.shapeResize$=new ResizeObserver((()=>{this.onResize()})),this.shapeResize$.observe(this.echartContainer.nativeElement)}onResize(){this.myChart.resize()}setupTooltipElement(e){const t=this.renderer.createElement("div");if(this.renderer.setStyle(t,"display","flex"),this.renderer.setStyle(t,"flex-direction","column"),this.renderer.setStyle(t,"align-items","flex-start"),this.renderer.setStyle(t,"gap","16px"),e.length){const n=this.renderer.createElement("div");this.renderer.setStyle(n,"display","flex"),this.renderer.setStyle(n,"flex-direction","column"),this.renderer.setStyle(n,"align-items","flex-start"),this.renderer.setStyle(n,"gap","4px"),this.renderer.appendChild(n,this.setTooltipDate(e));for(const[t,i]of e.entries())this.renderer.appendChild(n,this.constructTooltipSeriesElement(i,t));this.renderer.appendChild(t,n)}return t}constructTooltipSeriesElement(e,t){const n=this.renderer.createElement("div");this.renderer.setStyle(n,"display","flex"),this.renderer.setStyle(n,"flex-direction","row"),this.renderer.setStyle(n,"align-items","center"),this.renderer.setStyle(n,"align-self","stretch"),this.renderer.setStyle(n,"gap","12px");const i=this.renderer.createElement("div");this.renderer.setStyle(i,"display","flex"),this.renderer.setStyle(i,"align-items","center"),this.renderer.setStyle(i,"gap","8px"),this.renderer.appendChild(n,i);const a=this.renderer.createElement("div");this.renderer.setStyle(a,"width","8px"),this.renderer.setStyle(a,"height","8px"),this.renderer.setStyle(a,"border-radius","50%"),this.renderer.setStyle(a,"background",e.color),this.renderer.appendChild(i,a);const r=this.renderer.createElement("div");this.renderer.setProperty(r,"innerHTML",this.sanitizer.sanitize(g.HTML,e.seriesName)),this.renderer.setStyle(r,"font-family","Roboto"),this.renderer.setStyle(r,"font-size","12px"),this.renderer.setStyle(r,"font-style","normal"),this.renderer.setStyle(r,"font-weight",400),this.renderer.setStyle(r,"line-height","16px"),this.renderer.setStyle(r,"color","rgba(0, 0, 0, 0.76)"),this.renderer.appendChild(i,r);const o=ke(this.ctx.data[t].dataKey.decimals)?this.ctx.data[t].dataKey.decimals:this.ctx.decimals,s=ke(this.ctx.data[t].dataKey.units)?this.ctx.data[t].dataKey.units:this.ctx.units,l=Pe(e.value[1],o,s,!1),p=this.renderer.createElement("div");return this.renderer.setProperty(p,"innerHTML",this.sanitizer.sanitize(g.HTML,l)),this.renderer.setStyle(p,"flex","1"),this.renderer.setStyle(p,"text-align","end"),this.renderer.setStyle(p,"font-family","Roboto"),this.renderer.setStyle(p,"font-size","12px"),this.renderer.setStyle(p,"font-style","normal"),this.renderer.setStyle(p,"font-weight",500),this.renderer.setStyle(p,"line-height","16px"),this.renderer.setStyle(p,"color","rgba(0, 0, 0, 0.76)"),this.renderer.appendChild(n,p),n}setTooltipDate(e){const t=this.renderer.createElement("div");return this.renderer.appendChild(t,this.renderer.createText(new Date(e[0].value[0]).toLocaleString("en-GB"))),this.renderer.setStyle(t,"font-family","Roboto"),this.renderer.setStyle(t,"font-size","11px"),this.renderer.setStyle(t,"font-style","normal"),this.renderer.setStyle(t,"font-weight","400"),this.renderer.setStyle(t,"line-height","16px"),this.renderer.setStyle(t,"color","rgba(0, 0, 0, 0.76)"),t}setupAnimationSettings(){return{animation:!0,animationDelay:0,animationDelayUpdate:0,animationDuration:500,animationDurationUpdate:300,animationEasing:"cubicOut",animationEasingUpdate:"cubicOut",animationThreshold:2e3}}setupChartLines(){const e=[];for(const[t,n]of this.ctx.datasources[0].dataKeys.entries())e.push({id:t,name:n.label,type:"line",showSymbol:!1,smooth:!1,step:!1,stackStrategy:"all",data:[],lineStyle:{color:n.color},itemStyle:{color:n.color}});return e}setupYAxis(){return{type:"value",position:"left",mainType:"yAxis",id:"yAxis",offset:0,nameLocation:"middle",nameRotate:90,alignTicks:!0,scale:!0,show:!0,axisLabel:{color:"rgba(0, 0, 0, 0.54)",fontFamily:"Roboto",fontSize:12,fontStyle:"normal",fontWeight:400,show:!0,formatter:e=>Pe(e,this.ctx.decimals,this.ctx.units,!1)},splitLine:{show:!0},axisLine:{show:!0,lineStyle:{color:"rgba(0, 0, 0, 0.54)"}},axisTick:{lineStyle:{color:"rgba(0, 0, 0, 0.54)"},show:!0},nameTextStyle:{color:"rgba(0, 0, 0, 0.54)",fontFamily:"Roboto",fontSize:12,fontStyle:"normal",fontWeight:600}}}setupXAxis(){return{id:"xAxis",mainType:"xAxis",show:!0,type:"time",position:"bottom",offset:0,nameLocation:"middle",max:this.ctx.defaultSubscription.timeWindow.maxTime,min:this.ctx.defaultSubscription.timeWindow.minTime,nameTextStyle:{color:"rgba(0, 0, 0, 0.54)",fontStyle:"normal",fontWeight:600,fontFamily:"Roboto",fontSize:12},axisPointer:{shadowStyle:{color:"rgba(210,219,238,0.2)"}},splitLine:{show:!0},axisTick:{show:!0,lineStyle:{color:"rgba(0, 0, 0, 0.54)"}},axisLine:{onZero:!1,show:!0,lineStyle:{color:"rgba(0, 0, 0, 0.54)"}},axisLabel:{color:"rgba(0, 0, 0, 0.54)",fontFamily:"Roboto",fontSize:10,fontStyle:"normal",fontWeight:400,show:!0,hideOverlap:!0}}}static{this.ɵfac=function(e){return new(e||sj)(t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(Je.DomSanitizer),t.ɵɵdirectiveInject(et.WidgetComponent))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:sj,selectors:[["tb-gateway-statistics-chart"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(zU,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.echartContainer=e.first)}},inputs:{ctx:"ctx",widgetTitlePanel:"widgetTitlePanel"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:9,vars:4,consts:[["echartContainer",""],["legendItem",""],[1,"tb-time-series-chart-panel"],[1,"tb-time-series-chart-overlay"],[4,"ngIf"],[1,"tb-time-series-chart-content"],[1,"tb-time-series-chart-shape"],["class","tb-time-series-chart-legend",4,"ngIf"],[4,"ngTemplateOutlet"],[1,"tb-time-series-chart-legend"],[1,"tb-time-series-chart-legend-table","vertical"],["class","tb-time-series-chart-legend-type-label right legend legend-row-color",4,"ngIf"],[4,"ngFor","ngForOf"],[1,"tb-time-series-chart-legend-type-label","right","legend","legend-row-color"],[4,"ngTemplateOutlet","ngTemplateOutletContext"],["class","tb-time-series-chart-legend-value legend",3,"innerHTML",4,"ngIf"],[1,"tb-time-series-chart-legend-value","legend",3,"innerHTML"],[1,"tb-time-series-chart-legend-item"],[1,"tb-time-series-chart-legend-item-label"],[1,"tb-time-series-chart-legend-item-label-circle"],[1,"legend","legend-label-color",3,"innerHTML"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",2),t.ɵɵelement(1,"div",3),t.ɵɵtemplate(2,WU,2,1,"ng-container",4),t.ɵɵelementStart(3,"div",5),t.ɵɵelement(4,"div",6,0),t.ɵɵtemplate(6,rj,12,6,"div",7)(7,oj,5,10,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.widgetComponent.dashboardWidget.showWidgetTitlePanel),t.ɵɵadvance(),t.ɵɵclassMap(n.legendClass),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.showLegend))},dependencies:t.ɵɵgetComponentDepsFactory(sj,[j,_]),styles:['@charset "UTF-8";.tb-time-series-chart-panel[_ngcontent-%COMP%]{width:100%;height:100%;position:relative;display:flex;flex-direction:column;gap:8px;padding:12px}.tb-time-series-chart-panel[_ngcontent-%COMP%] > div[_ngcontent-%COMP%]:not(.tb-time-series-chart-overlay){z-index:1}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-overlay[_ngcontent-%COMP%]{position:absolute;inset:12px}.tb-time-series-chart-panel[_ngcontent-%COMP%] div.tb-widget-title[_ngcontent-%COMP%]{padding:0}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%]{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;gap:8px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%]{flex-direction:column-reverse}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-right[_ngcontent-%COMP%]{flex-direction:row}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-left[_ngcontent-%COMP%]{flex-direction:row-reverse}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-shape[_ngcontent-%COMP%]{flex:1;min-width:0;min-height:0;display:flex;align-items:center;justify-content:center}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-right[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-left[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]{display:inline-grid;grid-auto-flow:column;grid-template-rows:repeat(auto-fit,minmax(16px,min-content));max-width:calc(25% - 8px);height:fit-content;max-height:100%}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-bottom[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]{align-self:center}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%] .tb-time-series-chart-legend.tb-simple-legend[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-bottom[_ngcontent-%COMP%] .tb-time-series-chart-legend.tb-simple-legend[_ngcontent-%COMP%]{justify-content:center}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-top[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]:not(.tb-simple-legend), .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content.legend-bottom[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]:not(.tb-simple-legend){width:100%}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%]{display:flex;align-items:flex-start;align-self:stretch;column-gap:16px;row-gap:8px;flex-wrap:wrap;overflow:auto;width:fit-content;max-width:100%;max-height:calc(35% - 8px)}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%]{border-spacing:0;table-layout:fixed}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table.vertical[_ngcontent-%COMP%]{width:100%;table-layout:auto}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table.vertical[_ngcontent-%COMP%] tbody[_ngcontent-%COMP%] th[_ngcontent-%COMP%]{width:95%}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] th[_ngcontent-%COMP%]:not(:last-child), .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] td[_ngcontent-%COMP%]:not(:last-child){padding-right:16px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] thead[_ngcontent-%COMP%] tr[_ngcontent-%COMP%] th[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] thead[_ngcontent-%COMP%] tr[_ngcontent-%COMP%] td[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] tbody[_ngcontent-%COMP%] tr[_ngcontent-%COMP%]:not(:last-child) th[_ngcontent-%COMP%], .tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] tbody[_ngcontent-%COMP%] tr[_ngcontent-%COMP%]:not(:last-child) td[_ngcontent-%COMP%]{padding-bottom:8px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%]{align-items:flex-end}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-table[_ngcontent-%COMP%] .tb-time-series-chart-legend-item.left[_ngcontent-%COMP%]{align-items:flex-start}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%]{display:flex;flex-direction:column;justify-content:center;align-items:flex-start;-webkit-user-select:none;user-select:none}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%] .tb-time-series-chart-legend-item-label[_ngcontent-%COMP%]{display:flex;align-items:center;gap:4px;color:#ccc;white-space:nowrap;cursor:pointer}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-item[_ngcontent-%COMP%] .tb-time-series-chart-legend-item-label[_ngcontent-%COMP%] .tb-time-series-chart-legend-item-label-circle[_ngcontent-%COMP%]{width:8px;height:8px;border-radius:50%;background-color:#ccc}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-type-label[_ngcontent-%COMP%]{white-space:nowrap;text-align:left}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-type-label.right[_ngcontent-%COMP%]{text-align:right}.tb-time-series-chart-panel[_ngcontent-%COMP%] .tb-time-series-chart-content[_ngcontent-%COMP%] .tb-time-series-chart-legend[_ngcontent-%COMP%] .tb-time-series-chart-legend-value[_ngcontent-%COMP%]{white-space:nowrap;text-align:right}.tb-time-series-chart-panel[_ngcontent-%COMP%] .legend[_ngcontent-%COMP%]{font-size:12px;font-style:normal;font-weight:500;letter-spacing:normal;line-height:16px}.tb-time-series-chart-panel[_ngcontent-%COMP%] .legend.legend-row-color[_ngcontent-%COMP%]{color:#00000061}.tb-time-series-chart-panel[_ngcontent-%COMP%] .legend.legend-label-color[_ngcontent-%COMP%]{color:#000}']})}}const lj=["statisticChart"];function pj(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",12),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.openEditCommandDialog())})),t.ɵɵelementStart(2,"mat-icon",13),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",12),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.onDeleteClick())})),t.ɵɵelementStart(6,"mat-icon",13),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function cj(e,n){if(1&e&&t.ɵɵelement(0,"tb-gateway-statistics-chart",14,0),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("ctx",e.ctx)}}function dj(e,n){if(1&e&&t.ɵɵelement(0,"tb-custom-statistics-table",15),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("data",e.subscriptionData)}}function uj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10),t.ɵɵtemplate(1,cj,2,1,"tb-gateway-statistics-chart",14)(2,dj,1,1,"tb-custom-statistics-table",15),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵconditional(e.isNumericData?1:2)}}function mj(e,n){1&e&&(t.ɵɵelementStart(0,"div",11),t.ɵɵelement(1,"div",16),t.ɵɵelementStart(2,"div",17),t.ɵɵtext(3,"attribute.no-telemetry-text"),t.ɵɵelementEnd()())}class hj{constructor(e,t,n,i,a,r){this.fb=e,this.attributeService=t,this.destroyRef=n,this.dialog=i,this.dialogService=a,this.utils=r,this.subscriptionData=[],this.statisticForm=this.fb.group({command:[]}),this.isNumericData=!1,this.commands=[],this.subscribed=!1,this.dataTypeDefined=!1,this.subscriptionOptions={callbacks:{onDataUpdated:()=>this.ctx.ngZone.run((()=>{this.isDataOnlyNumbers(),this.isNumericData&&this.statisticChart?.onDataUpdated()})),onDataUpdateError:(e,t)=>this.ctx.ngZone.run((()=>{this.onDataUpdateError(t)}))},useDashboardTimewindow:!1,legendConfig:F(R.timeseries)},this.statisticForm.get("command").valueChanges.pipe(bn()).subscribe((e=>{this.subscribed=!1,this.subscriptionInfo&&e?.attributeOnGateway&&this.createSubscription(this.ctx.defaultSubscription.datasources[0].entity,e.attributeOnGateway)}))}ngAfterViewInit(){if(this.ctx.defaultSubscription.datasources.length){const e=this.ctx.defaultSubscription.datasources[0].entity;if(e.id.id===B)return;this.getGatewayGeneralConfig().pipe(bn(this.destroyRef)).subscribe((t=>{this.commands=t?.statistics.commands.reverse()??[],this.commands.length&&(this.statisticForm.get("command").setValue(this.commands[0]),this.createSubscription(e,this.commands[0].attributeOnGateway))}))}}openEditCommandDialog(){const e=this.statisticForm.get("command").value,t="string"==typeof e||!e,n="string"==typeof e?{attributeOnGateway:e}:e;let i;this.dialog.open(ir,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{titleText:t?"gateway.statistics.create-command":"gateway.statistics.edit-command",buttonText:t?"action.add":"action.apply",command:n,existingCommands:this.commands.map((e=>e.attributeOnGateway))}}).afterClosed().pipe(xe((e=>re(this.getGatewayGeneralConfig(),ae(e)))),xe((([e,t])=>(this.commands=[...e?.statistics.commands.filter((e=>e.attributeOnGateway!==t?.prev?.attributeOnGateway))??[],...t?.current?[{...t.current}]:[]],i=t?.current,this.updateStatisticsCommands(e,this.commands)))),bn(this.destroyRef)).subscribe((()=>{i&&this.statisticForm.get("command").patchValue(i)}))}onDeleteClick(){const e=this.statisticForm.get("command").value.attributeOnGateway;this.dialogService.confirm(this.ctx.translate.instant("gateway.statistics.delete-command",{command:e}),this.ctx.translate.instant("gateway.statistics.delete-command-data"),this.ctx.translate.instant("action.cancel"),this.ctx.translate.instant("action.confirm")).pipe(ve(Boolean),xe((()=>this.getGatewayGeneralConfig())),xe((t=>(this.commands=[...t.statistics.commands.filter((t=>t.attributeOnGateway!==e))],this.updateStatisticsCommands(t,this.commands)))),bn(this.destroyRef)).subscribe()}getGatewayGeneralConfig(){const e=this.ctx.defaultSubscription.datasources[0].entity;return e.id.id===B?ae(null):this.attributeService.getEntityAttributes(e.id,D.SHARED_SCOPE,["general_configuration"]).pipe(pe((e=>e[0]?.value)))}updateStatisticsCommands(e,t){const n=this.ctx.defaultSubscription.datasources[0].entity;return n.id.id!==B&&e?this.attributeService.saveEntityAttributes(n.id,D.SHARED_SCOPE,[{key:"general_configuration",value:{...e,statistics:{...e.statistics,commands:t}}}]):ae(null)}createSubscription(e,t){const n=[{type:N.entity,entityType:L.DEVICE,entityId:e.id.id,entityName:e.name,timeseries:[]}];n[0].timeseries=[{name:t,label:t,settings:{}}],this.subscriptionInfo=n,this.changeSubscription(n)}onDataUpdateError(e){const t=this.utils.parseException(e);let n=t.name;t.message&&(n+=": "+t.message),console.error(n)}isDataOnlyNumbers(){this.subscriptionData=this.ctx.defaultSubscription.data[0]?.data??[],this.subscriptionData.length&&!this.dataTypeDefined&&(this.isNumericData=this.subscriptionData.every((e=>!isNaN(+e[1]))),this.dataTypeDefined=!0),this.ctx.detectChanges()}changeSubscription(e){this.ctx.defaultSubscription?.unsubscribe(),this.ctx.datasources[0].entity&&this.ctx.subscriptionApi.createSubscriptionFromInfo(R.timeseries,e,this.subscriptionOptions,!1,!0).pipe(bn(this.destroyRef)).subscribe((e=>{this.dataTypeDefined=!1,this.ctx.defaultSubscription=e,this.ctx.settings.showLegend=!1,this.ctx.data=e.data,this.ctx.datasources=e.datasources,this.isDataOnlyNumbers(),this.subscribed=!0}))}static{this.ɵfac=function(e){return new(e||hj)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(_e.AttributeService),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(_e.DialogService),t.ɵɵdirectiveInject(_e.UtilsService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:hj,selectors:[["tb-gateway-statistics"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(lj,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.statisticChart=e.first)}},inputs:{ctx:"ctx"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:18,vars:13,consts:[["statisticChart",""],[1,"flex","flex-1","flex-col","max-h-full"],[1,"entry-container","flex","items-center"],[1,"tb-form-panel","stroked","w-full","flex-1",3,"formGroup"],["translate","",1,"tb-form-panel-title"],[1,"entry-container","flex","w-full","gap-2"],["formControlName","command",1,"flex-1",3,"onCreateNewClicked","commands"],["appearance","outline",1,"flex-1"],["matInput","","disabled","",3,"tbTruncateWithTooltip","value"],[1,"actions-container","flex","flex-col","p-2","min-w-16"],[1,"chart-box","flex","flex-1","flex-col","overflow-auto"],[1,"tb-no-data-available","h-full"],["type","button","matSuffix","","mat-icon-button","","aria-label","Edit","matTooltipPosition","above",1,"action-button",3,"click","matTooltip"],[1,"material-icons"],[1,"flex-1",3,"ctx"],[1,"h-full","flex-1",3,"data"],[1,"tb-no-data-bg"],["translate","",1,"tb-no-data-text"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"div",2)(2,"div",3)(3,"div",4),t.ɵɵtext(4,"gateway.statistics.entry"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",5)(6,"tb-statistics-commands-autocomplete",6),t.ɵɵlistener("onCreateNewClicked",(function(){return n.openEditCommandDialog()})),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-form-field",7)(8,"mat-label"),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(11,"input",8),t.ɵɵpipe(12,"translate"),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(14,"div",9),t.ɵɵtemplate(15,pj,8,6),t.ɵɵelementEnd()(),t.ɵɵtemplate(16,uj,3,1,"div",10)(17,mj,4,0,"div",11),t.ɵɵelementEnd()),2&e){let e,i,a,r;t.ɵɵadvance(2),t.ɵɵproperty("formGroup",n.statisticForm),t.ɵɵadvance(4),t.ɵɵproperty("commands",n.commands),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(10,7,"gateway.statistics.command")),t.ɵɵadvance(2),t.ɵɵproperty("tbTruncateWithTooltip",null!==(e=null==(e=n.statisticForm.get("command").value)?null:e.command)&&void 0!==e?e:t.ɵɵpipeBind1(12,9,"gateway.statistics.no-config-commands-found"))("value",null!==(i=null==(i=n.statisticForm.get("command").value)?null:i.command)&&void 0!==i?i:t.ɵɵpipeBind1(13,11,"gateway.statistics.no-config-commands-found")),t.ɵɵadvance(4),t.ɵɵconditional(null!=(a=n.statisticForm.get("command").value)&&a.attributeOnGateway?15:-1),t.ɵɵadvance(),t.ɵɵconditional(null!=(r=n.statisticForm.get("command").value)&&r.attributeOnGateway&&n.subscriptionData.length&&n.subscribed?16:17)}},dependencies:t.ɵɵgetComponentDepsFactory(hj,[j,_,Dn,Vn,sj]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;padding:4px;display:flex;flex-direction:column}[_nghost-%COMP%] .action-button[_ngcontent-%COMP%]{opacity:.7}@media screen and (max-width: 599px){[_nghost-%COMP%] .entry-container[_ngcontent-%COMP%]{flex-direction:column}[_nghost-%COMP%] .actions-container[_ngcontent-%COMP%]{flex-direction:row}}']})}}var gj;e("GatewayStatisticsComponent",hj),e("BACnetRequestTypes",gj),function(e){e.WriteProperty="writeProperty",e.ReadProperty="readProperty"}(gj||e("BACnetRequestTypes",gj={}));const fj=e("BACnetRequestTypesTranslates",new Map([[gj.WriteProperty,"gateway.rpc.write-property"],[gj.ReadProperty,"gateway.rpc.read-property"]]));var yj;e("BACnetObjectTypes",yj),function(e){e.BinaryInput="binaryInput",e.BinaryOutput="binaryOutput",e.AnalogInput="analogInput",e.AnalogOutput="analogOutput",e.BinaryValue="binaryValue",e.AnalogValue="analogValue"}(yj||e("BACnetObjectTypes",yj={}));const vj=e("BACnetObjectTypesTranslates",new Map([[yj.AnalogOutput,"gateway.rpc.analog-output"],[yj.AnalogInput,"gateway.rpc.analog-input"],[yj.BinaryOutput,"gateway.rpc.binary-output"],[yj.BinaryInput,"gateway.rpc.binary-input"],[yj.BinaryValue,"gateway.rpc.binary-value"],[yj.AnalogValue,"gateway.rpc.analog-value"]]));var xj;e("BLEMethods",xj),function(e){e.WRITE="write",e.READ="read",e.SCAN="scan"}(xj||e("BLEMethods",xj={}));const bj=e("BLEMethodsTranslates",new Map([[xj.WRITE,"gateway.rpc.write"],[xj.READ,"gateway.rpc.read"],[xj.SCAN,"gateway.rpc.scan"]]));var wj,Sj;e("CANByteOrders",wj),function(e){e.LITTLE="LITTLE",e.BIG="BIG"}(wj||e("CANByteOrders",wj={})),e("SocketMethodProcessings",Sj),function(e){e.WRITE="write",e.READ="read"}(Sj||e("SocketMethodProcessings",Sj={}));const Cj=e("SocketMethodProcessingsTranslates",new Map([[Sj.WRITE,"gateway.rpc.write"],[Sj.READ,"gateway.rpc.read"]]));var _j;e("SNMPMethods",_j),function(e){e.SET="set",e.MULTISET="multiset",e.GET="get",e.BULKWALK="bulkwalk",e.TABLE="table",e.MULTIGET="multiget",e.GETNEXT="getnext",e.BULKGET="bulkget",e.WALKS="walk"}(_j||e("SNMPMethods",_j={}));const Tj=e("SNMPMethodsTranslations",new Map([[_j.SET,"gateway.rpc.set"],[_j.MULTISET,"gateway.rpc.multiset"],[_j.GET,"gateway.rpc.get"],[_j.BULKWALK,"gateway.rpc.bulk-walk"],[_j.TABLE,"gateway.rpc.table"],[_j.MULTIGET,"gateway.rpc.multi-get"],[_j.GETNEXT,"gateway.rpc.get-next"],[_j.BULKGET,"gateway.rpc.bulk-get"],[_j.WALKS,"gateway.rpc.walk"]]));var Ij,Ej;e("SocketEncodings",Ij),function(e){e.UTF_8="utf-8"}(Ij||e("SocketEncodings",Ij={})),e("RestSecurityType",Ej),function(e){e.ANONYMOUS="anonymous",e.BASIC="basic"}(Ej||e("RestSecurityType",Ej={}));const Mj=e("RestSecurityTypeTranslationsMap",new Map([[Ej.ANONYMOUS,"gateway.broker.security-types.anonymous"],[Ej.BASIC,"gateway.broker.security-types.basic"]]));class kj{transform(e){return e.map((e=>(e?.value??e).toString())).join(", ")}static{this.ɵfac=function(e){return new(e||kj)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"getRpcTemplateArrayView",type:kj,pure:!0,standalone:!0})}}e("RpcTemplateArrayViewPipe",kj);class Pj{constructor(){this.differs=i(f),this.keyValues=[]}transform(e){if(!e||!(e instanceof Map)&&"object"!=typeof e)return null;this.differ??=this.differs.find(e).create();const t=this.differ.diff(e);return t&&(this.keyValues=[],t.forEachItem((e=>{ke(e.currentValue)&&this.keyValues.push(this.makeKeyValuePair(e.key,e.currentValue))}))),this.keyValues}makeKeyValuePair(e,t){return{key:e,value:t}}static{this.ɵfac=function(e){return new(e||Pj)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"keyValueIsNotEmpty",type:Pj,pure:!1,standalone:!0})}}e("KeyValueIsNotEmptyPipe",Pj);const Dj=e=>({$implicit:e,innerValue:!1}),Oj=e=>({"padding-left":e}),Aj=(e,t)=>({"boolean-true":e,"boolean-false":t}),Fj=e=>({$implicit:e,innerValue:!0});function Rj(e,n){if(1&e&&t.ɵɵelementContainer(0,13),2&e){const e=n.$implicit;t.ɵɵnextContext();const i=t.ɵɵreference(15);t.ɵɵproperty("ngTemplateOutlet",i)("ngTemplateOutletContext",t.ɵɵpureFunction1(2,Dj,e))}}function Bj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",19),t.ɵɵtext(1),t.ɵɵpipe(2,"getRpcTemplateArrayView"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,e.value)," ")}}function Nj(e,n){if(1&e&&t.ɵɵelementContainer(0,20),2&e){t.ɵɵnextContext();const e=t.ɵɵreference(12);t.ɵɵproperty("ngTemplateOutlet",e)}}function Lj(e,n){if(1&e&&t.ɵɵelementContainer(0,20),2&e){t.ɵɵnextContext(2);const e=t.ɵɵreference(10);t.ɵɵproperty("ngTemplateOutlet",e)}}function Vj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div"),t.ɵɵtemplate(1,Lj,1,1,"ng-container",21),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵreference(8),i=t.ɵɵnextContext().$implicit,a=t.ɵɵnextContext();t.ɵɵclassMap(t.ɵɵpureFunction2(4,Aj,!0===e.value,!1===e.value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",i.type===a.ConnectorType.SNMP&&"method"===e.key)("ngIfElse",n)}}function qj(e,n){if(1&e&&t.ɵɵtext(0),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵtextInterpolate(e.value)}}function Gj(e,n){if(1&e&&(t.ɵɵtext(0),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵtextInterpolate(t.ɵɵpipeBind1(1,1,n.SNMPMethodsTranslations.get(e.value)))}}function zj(e,n){if(1&e&&t.ɵɵelementContainer(0,13),2&e){const e=n.$implicit;t.ɵɵnextContext(3);const i=t.ɵɵreference(15);t.ɵɵproperty("ngTemplateOutlet",i)("ngTemplateOutletContext",t.ɵɵpureFunction1(2,Fj,e))}}function Uj(e,n){if(1&e&&(t.ɵɵtemplate(0,zj,1,4,"ng-container",12),t.ɵɵpipe(1,"keyvalue")),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("ngForOf",t.ɵɵpipeBind2(1,1,e.value,n.originalOrder))}}function jj(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",14)(1,"div",15),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(4,Bj,3,3,"div",16)(5,Nj,1,1,"ng-container",17)(6,Vj,2,7,"div",18)(7,qj,1,1,"ng-template",null,1,t.ɵɵtemplateRefExtractor)(9,Gj,2,3,"ng-template",null,2,t.ɵɵtemplateRefExtractor)(11,Uj,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=n.innerValue,a=t.ɵɵnextContext(2);t.ɵɵstyleMap(t.ɵɵpureFunction1(10,Oj,i?"16px":"0")),t.ɵɵclassMap(a.getRpcParamsRowClasses(e.value)),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",i?e.key:t.ɵɵpipeBind1(3,8,"gateway.rpc."+e.key)," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",a.isArray(e.value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",a.isObject(e.value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!a.isObject(e.value)&&!a.isArray(e.value))}}function Hj(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-expansion-panel",6)(1,"mat-expansion-panel-header")(2,"mat-panel-title",7)(3,"span",8),t.ɵɵtext(4),t.ɵɵelementEnd()(),t.ɵɵelementStart(5,"mat-panel-description")(6,"button",9),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteTemplate(n,i))})),t.ɵɵelementStart(7,"mat-icon",10),t.ɵɵtext(8,"delete"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",11),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.applyTemplate(n,i))})),t.ɵɵelementStart(10,"mat-icon",10),t.ɵɵtext(11,"play_arrow"),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(12,Rj,1,4,"ng-container",12),t.ɵɵpipe(13,"keyValueIsNotEmpty"),t.ɵɵtemplate(14,jj,13,12,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()}if(2&e){const e=n.$implicit;t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",e.name),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.name),t.ɵɵadvance(8),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(13,3,e.config))}}class Wj{constructor(e){this.attributeService=e,this.saveTemplate=new u,this.useTemplate=new u,this.ConnectorType=ct,this.originalOrder=()=>0,this.isObject=e=>De(e),this.isArray=e=>Array.isArray(e),this.SNMPMethodsTranslations=Tj}applyTemplate(e,t){e.stopPropagation(),this.useTemplate.emit(t)}deleteTemplate(e,t){e.stopPropagation();const n=this.rpcTemplates.findIndex((e=>e.name==t.name));this.rpcTemplates.splice(n,1);const i=`${this.connectorType}_template`;this.attributeService.saveEntityAttributes({id:this.ctx.defaultSubscription.targetDeviceId,entityType:L.DEVICE},D.SERVER_SCOPE,[{key:i,value:this.rpcTemplates}]).subscribe((()=>{}))}getRpcParamsRowClasses(e){return this.isObject(e)?"flex-col":"flex-row justify-between items-center"}static{this.ɵfac=function(e){return new(e||Wj)(t.ɵɵdirectiveInject(_e.AttributeService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Wj,selectors:[["tb-gateway-service-rpc-connector-templates"]],inputs:{connectorType:"connectorType",ctx:"ctx",rpcTemplates:"rpcTemplates"},outputs:{saveTemplate:"saveTemplate",useTemplate:"useTemplate"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:4,vars:4,consts:[["RPCTemplateRef",""],["value",""],["SNMPMethod",""],["RPCObjectRow",""],[1,"mat-subtitle-1","title"],["hideToggle","",4,"ngFor","ngForOf"],["hideToggle",""],[1,"template-name"],["matTooltipPosition","above",3,"matTooltip"],["mat-icon-button","","matTooltip","Delete",3,"click"],[1,"material-icons"],["mat-icon-button","","matTooltip","Use",3,"click"],[3,"ngTemplateOutlet","ngTemplateOutletContext",4,"ngFor","ngForOf"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"rpc-params-row","flex"],[1,"template-key"],["tbTruncateWithTooltip","","class","array-value",4,"ngIf"],[3,"ngTemplateOutlet",4,"ngIf"],[3,"class",4,"ngIf"],["tbTruncateWithTooltip","",1,"array-value"],[3,"ngTemplateOutlet"],[3,"ngTemplateOutlet",4,"ngIf","ngIfElse"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",4),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(3,Hj,16,5,"mat-expansion-panel",5)),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,"gateway.rpc.templates-title")),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",n.rpcTemplates))},dependencies:t.ɵɵgetComponentDepsFactory(Wj,[j,_,kj,Pj]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex;flex-direction:column;padding:0}[_nghost-%COMP%] .title[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .template-key[_ngcontent-%COMP%]{color:#00000061;height:32px;line-height:32px}[_nghost-%COMP%] .boolean-true[_ngcontent-%COMP%], [_nghost-%COMP%] .boolean-false[_ngcontent-%COMP%]{border-radius:3px;height:32px;line-height:32px;padding:0 12px;width:fit-content;font-size:14px;text-transform:capitalize}[_nghost-%COMP%] .boolean-false[_ngcontent-%COMP%]{color:#d12730;background-color:#d1273014}[_nghost-%COMP%] .boolean-true[_ngcontent-%COMP%]{color:#198038;background-color:#19803814}[_nghost-%COMP%] mat-expansion-panel[_ngcontent-%COMP%]{margin-top:10px;overflow:visible}[_nghost-%COMP%] .mat-expansion-panel-header-description[_ngcontent-%COMP%]{flex-direction:row-reverse;align-items:center;margin-right:0;flex:0}[_nghost-%COMP%] .mat-expansion-panel-header-description[_ngcontent-%COMP%] > mat-icon[_ngcontent-%COMP%]{margin-left:15px;color:#00000061}[_nghost-%COMP%] .mat-expansion-panel-header[_ngcontent-%COMP%]{padding:0 0 0 12px}[_nghost-%COMP%] .mat-expansion-panel-header.mat-expansion-panel-header.mat-expanded[_ngcontent-%COMP%]{height:48px}[_nghost-%COMP%] .mat-expansion-panel-header[_ngcontent-%COMP%] .mat-content.mat-content-hide-toggle[_ngcontent-%COMP%]{margin-right:0}[_nghost-%COMP%] .rpc-params-row[_ngcontent-%COMP%]{overflow:hidden;white-space:nowrap}[_nghost-%COMP%] .rpc-params-row[_ngcontent-%COMP%] [_ngcontent-%COMP%]:not(:first-child){white-space:pre;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .template-name[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis;display:block}[_nghost-%COMP%] .mat-content{align-items:center}[_nghost-%COMP%] .mat-expansion-panel-header-title[_ngcontent-%COMP%]{flex:1;margin:0}[_nghost-%COMP%] .array-value[_ngcontent-%COMP%]{margin-left:10px}']})}}function $j(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rpc.template-name-required")," "))}function Kj(e,n){1&e&&(t.ɵɵelementStart(0,"div",12),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rpc.template-name-duplicate")," "))}e("GatewayServiceRPCConnectorTemplatesComponent",Wj);class Yj extends A{constructor(e,t,n,i,a){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.config=this.data.config,this.templates=this.data.templates,this.templateNameCtrl=this.fb.control("",[$.required])}validateDuplicateName(e){const t=e.value.trim();return!!this.templates.find((e=>e.name===t))}close(){this.dialogRef.close()}save(){this.templateNameCtrl.setValue(this.templateNameCtrl.value.trim()),this.templateNameCtrl.valid&&this.dialogRef.close(this.templateNameCtrl.value)}static{this.ɵfac=function(e){return new(e||Yj)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:Yj,selectors:[["tb-gateway-service-rpc-connector-template-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:20,vars:10,consts:[["color","primary",1,"justify-between"],["translate",""],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"mat-content","flex","flex-col",2,"width","600px"],[1,"mat-block","tb-value-type",2,"flex-grow","0"],["matInput","","required","",3,"formControl"],[4,"ngIf"],["class","mat-mdc-form-field-error","style","margin-top: -15px; padding-left: 10px; font-size: 14px;",4,"ngIf"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"mat-mdc-form-field-error",2,"margin-top","-15px","padding-left","10px","font-size","14px"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-toolbar",0)(1,"h2",1),t.ɵɵtext(2,"gateway.rpc.save-template"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"button",2),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵelementStart(4,"mat-icon",3),t.ɵɵtext(5,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(6,"div",4)(7,"mat-form-field",5)(8,"mat-label",1),t.ɵɵtext(9,"gateway.rpc.template-name"),t.ɵɵelementEnd(),t.ɵɵelement(10,"input",6),t.ɵɵtemplate(11,$j,3,3,"mat-error",7),t.ɵɵelementEnd(),t.ɵɵtemplate(12,Kj,3,3,"div",8),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",9)(14,"button",10),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵlistener("click",(function(){return n.save()})),t.ɵɵtext(18),t.ɵɵpipe(19,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(10),t.ɵɵproperty("formControl",n.templateNameCtrl),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.templateNameCtrl.hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.validateDuplicateName(n.templateNameCtrl)),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(16,6,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",!n.templateNameCtrl.valid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(19,8,"action.save")," "))},dependencies:t.ɵɵgetComponentDepsFactory(Yj,[j,_]),encapsulation:2})}}function Xj(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",6),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SecurityTypeTranslationsMap.get(e))," ")}}function Zj(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.username-required"))}function Qj(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.password-required"))}function Jj(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",7)(2,"div",8),t.ɵɵtext(3,"gateway.username"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",9)(5,"mat-form-field",10),t.ɵɵelement(6,"input",11),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,Zj,3,3,"mat-icon",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",7)(10,"div",8),t.ɵɵtext(11,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"div",9)(13,"mat-form-field",10),t.ɵɵelement(14,"input",13),t.ɵɵpipe(15,"translate"),t.ɵɵtemplate(16,Qj,3,3,"mat-icon",12),t.ɵɵelementStart(17,"div",14),t.ɵɵelement(18,"tb-toggle-password",15),t.ɵɵelementEnd()()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,6,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("username").hasError("required")&&e.securityFormGroup.get("username").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("password").hasError("required")&&e.securityFormGroup.get("password").touched),t.ɵɵadvance(),t.ɵɵclassProp("hide-toggle",e.securityFormGroup.get("password").hasError("required"))}}e("GatewayServiceRPCConnectorTemplateDialogComponent",Yj);class eH{constructor(e){this.fb=e,this.BrokerSecurityType=Ej,this.securityTypes=Object.values(Ej),this.SecurityTypeTranslationsMap=Mj,this.destroy$=new te,this.propagateChange=e=>{},this.securityFormGroup=this.fb.group({type:[Ej.ANONYMOUS,[]],username:["",[$.required,$.pattern(an)]],password:["",[$.required,$.pattern(an)]]}),this.observeSecurityForm()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}writeValue(e){e.type||(e.type=Ej.ANONYMOUS),this.securityFormGroup.reset(e),this.updateView(e)}validate(){return this.securityFormGroup.valid?null:{securityForm:{valid:!1}}}updateView(e){this.propagateChange(e)}updateValidators(e){e===Ej.BASIC?(this.securityFormGroup.get("username").enable({emitEvent:!1}),this.securityFormGroup.get("password").enable({emitEvent:!1})):(this.securityFormGroup.get("username").disable({emitEvent:!1}),this.securityFormGroup.get("password").disable({emitEvent:!1}))}observeSecurityForm(){this.securityFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.updateView(e))),this.securityFormGroup.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.updateValidators(e)))}static{this.ɵfac=function(e){return new(e||eH)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:eH,selectors:[["tb-rest-connector-security"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>eH)),multi:!0},{provide:K,useExisting:c((()=>eH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:7,vars:3,consts:[[1,"tb-form-row","space-between","same-padding","tb-flex","column",3,"formGroup"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fields-label"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],[4,"ngIf"],[3,"value"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["translate","",1,"fixed-title-width"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","username",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","password","name","value","formControlName","password",3,"placeholder"],["matSuffix","",1,"tb-flex","no-gap","align-center","fill-height"],[1,"tb-flex","align-center","fill-height"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.security"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"tb-toggle-select",3),t.ɵɵtemplate(5,Xj,3,4,"tb-toggle-option",4),t.ɵɵelementEnd()(),t.ɵɵtemplate(6,Jj,19,10,"ng-container",5),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.securityFormGroup),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.securityTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.securityFormGroup.get("type").value===n.BrokerSecurityType.BASIC))},dependencies:t.ɵɵgetComponentDepsFactory(eH,[_,j]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block;margin-bottom:10px}[_nghost-%COMP%] .fields-label[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .hide-toggle[_ngcontent-%COMP%]{display:none}'],changeDetection:d.OnPush})}}e("RestConnectorSecurityComponent",eH);const tH=e=>({type:e});function nH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.bACnetRequestTypesTranslates.get(e))," ")}}function iH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.bACnetObjectTypesTranslates.get(e))," ")}}function aH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",9),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",10)(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-select",11),t.ɵɵtemplate(10,nH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field")(12,"mat-label"),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",13),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",14)(17,"mat-form-field",15)(18,"mat-label"),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-select",16),t.ɵɵtemplate(22,iH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(23,"mat-form-field",15)(24,"mat-label"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",17),t.ɵɵelementEnd()(),t.ɵɵelementStart(28,"mat-form-field",10)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",18),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,8,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,10,"gateway.rpc.requestType")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.bACnetRequestTypes),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,12,"gateway.rpc.requestTimeout")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,14,"gateway.rpc.objectType")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.bACnetObjectTypes),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(26,16,"gateway.rpc.identifier")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,18,"gateway.rpc.propertyId"))}}function rH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.bLEMethodsTranslates.get(e))," ")}}function oH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",20),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",21),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-form-field",10)(11,"mat-label"),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"mat-select",22),t.ɵɵtemplate(15,rH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(16,"mat-slide-toggle",23),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,5,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,7,"gateway.rpc.characteristicUUID")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,9,"gateway.rpc.methodProcessing")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.bLEMethods),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,11,"gateway.rpc.withResponse")," ")}}function sH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e)," ")}}function lH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",24),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",25),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-slide-toggle",26),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-slide-toggle",27),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"mat-slide-toggle",28),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",14)(20,"mat-form-field",15)(21,"mat-label"),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(24,"input",29),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",15)(26,"mat-label"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"mat-select",30),t.ɵɵtemplate(30,sH,3,4,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(31,"div",14)(32,"mat-form-field",15)(33,"mat-label"),t.ɵɵtext(34),t.ɵɵpipe(35,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(36,"input",31),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"mat-form-field",15)(38,"mat-label"),t.ɵɵtext(39),t.ɵɵpipe(40,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(41,"input",32),t.ɵɵelementEnd()(),t.ɵɵelementStart(42,"mat-form-field")(43,"mat-label"),t.ɵɵtext(44),t.ɵɵpipe(45,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(46,"input",33),t.ɵɵelementEnd(),t.ɵɵelementStart(47,"mat-form-field")(48,"mat-label"),t.ɵɵtext(49),t.ɵɵpipe(50,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(51,"input",34),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,12,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,14,"gateway.rpc.nodeID")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,16,"gateway.rpc.isExtendedID")," "),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,18,"gateway.rpc.isFD")," "),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,20,"gateway.rpc.bitrateSwitch")," "),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(23,22,"gateway.rpc.dataLength")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(28,24,"gateway.rpc.dataByteorder")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.cANByteOrders),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(35,26,"gateway.rpc.dataBefore")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(40,28,"gateway.rpc.dataAfter")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(45,30,"gateway.rpc.dataInHEX")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(50,32,"gateway.rpc.dataExpression"))}}function pH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",35),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",36),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,2,"gateway.rpc.methodFilter")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,4,"gateway.rpc.valueExpression")))}function cH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",37),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",36),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-slide-toggle",38),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,3,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,5,"gateway.rpc.valueExpression")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,7,"gateway.rpc.withResponse")," "))}function dH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",37),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",36),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-slide-toggle",38),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,3,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,5,"gateway.rpc.valueExpression")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,7,"gateway.rpc.withResponse")," "))}function uH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SNMPMethodsTranslations.get(e))," ")}}function mH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",45)(1,"mat-form-field",46),t.ɵɵelement(2,"input",47),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-icon",48),t.ɵɵpipe(4,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext(3);return t.ɵɵresetView(i.removeSNMPoid(n))})),t.ɵɵtext(5,"delete "),t.ɵɵelementEnd()()}if(2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵproperty("formControl",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(4,2,"gateway.rpc.remove"))}}function hH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",39),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",10)(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-select",40),t.ɵɵtemplate(10,uH,3,4,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-slide-toggle",38),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"fieldset",41)(15,"span",42),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(18,mH,6,4,"div",43),t.ɵɵelementStart(19,"button",44),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.addSNMPoid())})),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,7,"gateway.rpc.requestFilter")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,9,"gateway.rpc.method")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.sNMPMethods),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,11,"gateway.rpc.withResponse")," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(17,13,"gateway.rpc.oids"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",e.getFormArrayControls("oid")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(21,15,"gateway.rpc.add-oid")," ")}}function gH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function fH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",59),t.ɵɵelementContainerStart(1,63),t.ɵɵelementStart(2,"mat-form-field",64),t.ɵɵelement(3,"input",65),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",64),t.ɵɵelement(5,"input",66),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-icon",67),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext(4);return t.ɵɵresetView(i.removeHTTPHeader(n))})),t.ɵɵtext(8,"delete "),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()}if(2&e){const e=n.index;t.ɵɵadvance(),t.ɵɵproperty("formGroupName",e),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,2,"gateway.rpc.remove"))}}function yH(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",58)(1,"div",59)(2,"span",60),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"span",60),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(8,"span",61),t.ɵɵelementEnd(),t.ɵɵelement(9,"mat-divider"),t.ɵɵtemplate(10,fH,9,4,"div",62),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rpc.header-name")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(7,5,"gateway.rpc.value")),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.getFormArrayControls("httpHeaders"))}}function vH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",49),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",14)(6,"mat-form-field",50)(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-select",51),t.ɵɵtemplate(11,gH,2,2,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",15)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",52),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",14)(18,"mat-form-field",15)(19,"mat-label"),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(22,"input",53),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",15)(24,"mat-label"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",54),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-form-field",15)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",55),t.ɵɵelementEnd()(),t.ɵɵelementStart(33,"mat-form-field")(34,"mat-label"),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(37,"input",36),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"fieldset",56)(39,"span",42),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(42,yH,11,7,"div",57),t.ɵɵelementStart(43,"button",44),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.addHTTPHeader())})),t.ɵɵtext(44),t.ɵɵpipe(45,"translate"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,11,"gateway.rpc.methodFilter")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,13,"gateway.rpc.httpMethod")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.hTTPMethods),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,15,"gateway.rpc.requestUrlExpression")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(21,17,"gateway.rpc.responseTimeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(26,19,"gateway.rpc.timeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,21,"gateway.rpc.tries")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,23,"gateway.rpc.valueExpression")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,25,"gateway.rpc.httpHeaders")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.getFormArrayControls("httpHeaders").length),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(45,27,"gateway.rpc.add-header")," ")}}function xH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",19),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function bH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",59),t.ɵɵelementContainerStart(1,63),t.ɵɵelementStart(2,"mat-form-field",64),t.ɵɵelement(3,"input",73),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",64),t.ɵɵelement(6,"input",74),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-icon",67),t.ɵɵpipe(8,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext(4);return t.ɵɵresetView(i.removeHTTPHeader(n))})),t.ɵɵtext(9,"delete "),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()}if(2&e){const e=n.index;t.ɵɵadvance(),t.ɵɵproperty("formGroupName",e),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(4,3,"gateway.rpc.set")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,5,"gateway.rpc.remove"))}}function wH(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",58)(1,"div",59)(2,"span",60),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"span",60),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(8,"span",61),t.ɵɵelementEnd(),t.ɵɵelement(9,"mat-divider"),t.ɵɵtemplate(10,bH,10,7,"div",62),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rpc.header-name")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(7,5,"gateway.rpc.value")),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.getFormArrayControls("httpHeaders"))}}function SH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",68),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",59)(6,"mat-form-field",50)(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-select",51),t.ɵɵtemplate(11,xH,2,2,"mat-option",12),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",15)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",52),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",59)(18,"mat-form-field",15)(19,"mat-label"),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(22,"input",53),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",15)(24,"mat-label"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",69),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-form-field",15)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",70),t.ɵɵelementEnd()(),t.ɵɵelementStart(33,"mat-form-field")(34,"mat-label"),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(37,"input",71),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"mat-form-field")(39,"mat-label"),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(42,"input",72),t.ɵɵelementEnd(),t.ɵɵelementStart(43,"fieldset",56)(44,"span",42),t.ɵɵtext(45),t.ɵɵpipe(46,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(47,wH,11,7,"div",57),t.ɵɵelementStart(48,"button",44),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.addHTTPHeader())})),t.ɵɵtext(49),t.ɵɵpipe(50,"translate"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,12,"gateway.rpc.methodFilter")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,14,"gateway.rpc.httpMethod")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.hTTPMethods),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,16,"gateway.rpc.requestUrlExpression")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(21,18,"gateway.rpc.responseTimeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(26,20,"gateway.rpc.timeout")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,22,"gateway.rpc.tries")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,24,"gateway.rpc.requestValueExpression")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,26,"gateway.rpc.responseValueExpression")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(46,28,"gateway.rpc.httpHeaders")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.getFormArrayControls("httpHeaders").length),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(50,30,"gateway.rpc.add-header")," ")}}function CH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rpc.json-value-invalid")," "))}function _H(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",75),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field")(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(9,"input",76),t.ɵɵelementStart(10,"mat-icon",77),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext(2);return t.ɵɵresetView(i.openEditJSONDialog(n))})),t.ɵɵtext(12,"edit "),t.ɵɵelementEnd(),t.ɵɵtemplate(13,CH,3,3,"mat-error",78),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,4,"gateway.statistics.command")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,6,"widget-config.datasource-parameters")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,8,"gateway.rpc-command-edit-params")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.commandForm.get("params").hasError("invalidJSON"))}}function TH(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,6),t.ɵɵtemplate(1,aH,33,20,"ng-template",7)(2,oH,19,13,"ng-template",7)(3,lH,52,34,"ng-template",7)(4,pH,10,6,"ng-template",7)(5,cH,13,9,"ng-template",7)(6,dH,13,9,"ng-template",7)(7,hH,22,17,"ng-template",7)(8,vH,46,29,"ng-template",7)(9,SH,51,32,"ng-template",7)(10,_H,14,10,"ng-template",8),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("ngSwitch",e.connectorType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.BACNET),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.BLE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.CAN),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.FTP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.OCPP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.XMPP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.SNMP),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.REST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.REQUEST)}}class IH{constructor(e,t){this.fb=e,this.dialog=t,this.sendCommand=new u,this.saveTemplate=new u,this.ConnectorType=ct,this.bACnetRequestTypes=Object.values(gj),this.bACnetObjectTypes=Object.values(yj),this.bLEMethods=Object.values(xj),this.cANByteOrders=Object.values(wj),this.sNMPMethods=Object.values(_j),this.hTTPMethods=Object.values(ht),this.bACnetRequestTypesTranslates=fj,this.bACnetObjectTypesTranslates=vj,this.bLEMethodsTranslates=bj,this.SNMPMethodsTranslations=Tj,this.gatewayConnectorDefaultTypesTranslates=dt,this.urlPattern=/^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/,this.numbersOnlyPattern=/^[0-9]*$/,this.hexOnlyPattern=/^[0-9A-Fa-f ]+$/,this.propagateChange=e=>{},this.destroy$=new te}ngOnInit(){this.commandForm=this.connectorParamsFormGroupByType(this.connectorType),this.observeFormChanges()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}connectorParamsFormGroupByType(e){let t;switch(e){case ct.BACNET:t=this.fb.group({method:[null,[$.required,$.pattern(an)]],requestType:[null,[$.required,$.pattern(an)]],requestTimeout:[null,[$.required,$.min(10),$.pattern(this.numbersOnlyPattern)]],objectType:[null,[]],identifier:[null,[$.required,$.min(1),$.pattern(this.numbersOnlyPattern)]],propertyId:[null,[$.required,$.pattern(an)]]});break;case ct.BLE:t=this.fb.group({methodRPC:[null,[$.required,$.pattern(an)]],characteristicUUID:["00002A00-0000-1000-8000-00805F9B34FB",[$.required,$.pattern(an)]],methodProcessing:[null,[$.required]],withResponse:[!1,[]]});break;case ct.CAN:t=this.fb.group({method:[null,[$.required,$.pattern(an)]],nodeID:[null,[$.required,$.min(0),$.pattern(this.numbersOnlyPattern)]],isExtendedID:[!1,[]],isFD:[!1,[]],bitrateSwitch:[!1,[]],dataLength:[null,[$.min(1),$.pattern(this.numbersOnlyPattern)]],dataByteorder:[null,[]],dataBefore:[null,[$.pattern(an),$.pattern(this.hexOnlyPattern)]],dataAfter:[null,[$.pattern(an),$.pattern(this.hexOnlyPattern)]],dataInHEX:[null,[$.pattern(an),$.pattern(this.hexOnlyPattern)]],dataExpression:[null,[$.pattern(an)]]});break;case ct.FTP:t=this.fb.group({methodFilter:[null,[$.required,$.pattern(an)]],valueExpression:[null,[$.required,$.pattern(an)]]});break;case ct.OCPP:case ct.XMPP:t=this.fb.group({methodRPC:[null,[$.required,$.pattern(an)]],valueExpression:[null,[$.required,$.pattern(an)]],withResponse:[!1,[]]});break;case ct.SNMP:t=this.fb.group({requestFilter:[null,[$.required,$.pattern(an)]],method:[null,[$.required]],withResponse:[!1,[]],oid:this.fb.array([],[$.required])});break;case ct.REST:t=this.fb.group({methodFilter:[null,[$.required,$.pattern(an)]],httpMethod:[null,[$.required]],requestUrlExpression:[null,[$.required,$.pattern(this.urlPattern)]],responseTimeout:[null,[$.required,$.min(10),$.pattern(this.numbersOnlyPattern)]],timeout:[null,[$.required,$.min(10),$.pattern(this.numbersOnlyPattern)]],tries:[null,[$.required,$.min(1),$.pattern(this.numbersOnlyPattern)]],valueExpression:[null,[$.required,$.pattern(an)]],httpHeaders:this.fb.array([]),security:[{},[$.required]]});break;case ct.REQUEST:t=this.fb.group({methodFilter:[null,[$.required,$.pattern(an)]],httpMethod:[null,[$.required]],requestUrlExpression:[null,[$.required,$.pattern(this.urlPattern)]],responseTimeout:[null,[$.required,$.min(10),$.pattern(this.numbersOnlyPattern)]],timeout:[null,[$.required,$.min(10),$.pattern(this.numbersOnlyPattern)]],tries:[null,[$.required,$.min(1),$.pattern(this.numbersOnlyPattern)]],requestValueExpression:[null,[$.required,$.pattern(an)]],responseValueExpression:[null,[$.pattern(an)]],httpHeaders:this.fb.array([])});break;default:t=this.fb.group({command:[null,[$.required,$.pattern(an)]],params:[{},[st]]})}return t}addSNMPoid(e=null){const t=this.commandForm.get("oid");t&&t.push(this.fb.control(e,[$.required,$.pattern(an)]),{emitEvent:!1})}removeSNMPoid(e){this.commandForm.get("oid").removeAt(e)}addHTTPHeader(e={headerName:null,value:null}){const t=this.commandForm.get("httpHeaders"),n=this.fb.group({headerName:[e.headerName,[$.required,$.pattern(an)]],value:[e.value,[$.required,$.pattern(an)]]});t&&t.push(n,{emitEvent:!1})}removeHTTPHeader(e){this.commandForm.get("httpHeaders").removeAt(e)}getFormArrayControls(e){return this.commandForm.get(e).controls}openEditJSONDialog(e){e&&e.stopPropagation(),this.dialog.open(tt,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{jsonValue:this.commandForm.get("params").value,required:!0}}).afterClosed().subscribe((e=>{e&&this.commandForm.get("params").setValue(e)}))}save(){this.saveTemplate.emit()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}clearFromArrayByName(e){const t=this.commandForm.get(e);for(;0!==t.length;)t.removeAt(0)}writeValue(e){if("object"==typeof e){switch(e=Oe(e),this.connectorType){case ct.SNMP:this.clearFromArrayByName("oid"),e.oid.forEach((e=>{this.addSNMPoid(e)})),delete e.oid;break;case ct.REQUEST:case ct.REST:this.clearFromArrayByName("httpHeaders"),e.httpHeaders&&Object.entries(e.httpHeaders).forEach((e=>{this.addHTTPHeader({headerName:e[0],value:e[1]})})),delete e.httpHeaders}this.commandForm.patchValue(e,{onlySelf:!1})}}observeFormChanges(){this.commandForm.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.connectorType!==ct.REST&&this.connectorType!==ct.REQUEST||(e.httpHeaders=e.httpHeaders.reduce(((e,t)=>(e[t.headerName]=t.value,e)),{})),this.commandForm.valid&&this.propagateChange({...this.commandForm.value,...e})}))}static{this.ɵfac=function(e){return new(e||IH)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(qe.MatDialog))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:IH,selectors:[["tb-gateway-service-rpc-connector"]],inputs:{connectorType:"connectorType"},outputs:{sendCommand:"sendCommand",saveTemplate:"saveTemplate"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>IH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:12,vars:16,consts:[[1,"command-form","flex","flex-col",3,"formGroup"],[1,"mat-subtitle-1","title"],[3,"ngIf"],[1,"template-actions","flex","flex-row","justify-end","gap-2.5"],["mat-raised-button","",3,"click","disabled"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"ngSwitch"],[3,"ngSwitchCase"],["ngSwitchDefault",""],["matInput","","formControlName","method","placeholder","set_state"],[1,"mat-block"],["formControlName","requestType"],[3,"value",4,"ngFor","ngForOf"],["matInput","","formControlName","requestTimeout","type","number","min","10","step","1","placeholder","1000"],[1,"flex","flex-1","flex-row","gap-2.5"],[1,"flex-1"],["formControlName","objectType"],["matInput","","formControlName","identifier","type","number","min","1","step","1","placeholder","1"],["matInput","","formControlName","propertyId","placeholder","presentValue"],[3,"value"],["matInput","","formControlName","methodRPC","placeholder","rpcMethod1"],["matInput","","formControlName","characteristicUUID","placeholder","00002A00-0000-1000-8000-00805F9B34FB"],["formControlName","methodProcessing"],["formControlName","withResponse",1,"mat-slide"],["matInput","","formControlName","method","placeholder","sendSameData"],["matInput","","formControlName","nodeID","type","number","placeholder","4","min","0","step","1"],["formControlName","isExtendedID",1,"mat-slide","margin"],["formControlName","isFD",1,"mat-slide","margin"],["formControlName","bitrateSwitch",1,"mat-slide","margin"],["matInput","","formControlName","dataLength","type","number","placeholder","2","min","1","step","1"],["formControlName","dataByteorder"],["matInput","","formControlName","dataBefore","placeholder","00AA"],["matInput","","formControlName","dataAfter","placeholder","0102"],["matInput","","formControlName","dataInHEX","placeholder","aa bb cc dd ee ff aa bb aa bb cc d ee ff"],["matInput","","formControlName","dataExpression","placeholder","userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed"],["matInput","","formControlName","methodFilter","placeholder","read"],["matInput","","formControlName","valueExpression","placeholder","${params}"],["matInput","","formControlName","methodRPC","placeholder","rpc1"],["formControlName","withResponse",1,"mat-slide","margin"],["matInput","","formControlName","requestFilter","placeholder","setData"],["formControlName","method"],["formArrayName","oid",1,"fields","flex","flex-col","gap-2.5","border"],[1,"fields-label"],["class","flex flex-1 flex-row items-center justify-center gap-2.5",4,"ngFor","ngForOf"],["mat-raised-button","",1,"self-start",3,"click"],[1,"flex","flex-1","flex-row","items-center","justify-center","gap-2.5"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","flex-1"],["matInput","","required","",3,"formControl"],[1,"flex-[1_1_30px]",2,"cursor","pointer","max-width","30px","min-width","30px",3,"click","matTooltip"],["matInput","","formControlName","methodFilter","placeholder","post_attributes"],[1,"max-w-4/12","flex-[1_1_33%]"],["formControlName","httpMethod"],["matInput","","formControlName","requestUrlExpression","placeholder","http://127.0.0.1:5000/my_devices"],["matInput","","formControlName","responseTimeout","type","number","step","1","min","10","placeholder","10"],["matInput","","formControlName","timeout","type","number","step","1","min","10","placeholder","1000"],["matInput","","formControlName","tries","type","number","step","1","min","1","placeholder","3"],["formArrayName","httpHeaders",1,"fields","flex","flex-col","gap-2.5","border"],["class","flex flex-col gap-2.5 border",4,"ngIf"],[1,"flex","flex-col","gap-2.5","border"],[1,"flex","flex-row","items-center","justify-center","gap-2.5"],[1,"title","flex-1"],[2,"width","30px"],["class","flex flex-row items-center justify-center gap-2.5",4,"ngFor","ngForOf"],[3,"formGroupName"],["appearance","outline",1,"flex-1"],["matInput","","formControlName","headerName"],["matInput","","formControlName","value","placeholder","application/json"],[2,"cursor","pointer","width","30px",3,"click","matTooltip"],["matInput","","formControlName","methodFilter","placeholder","echo"],["matInput","","formControlName","timeout","type","number","step","1","min","10","placeholder","10"],["matInput","","formControlName","tries","type","number","step","1","min","1","placeholder","1"],["matInput","","formControlName","requestValueExpression","placeholder","${params}"],["matInput","","formControlName","responseValueExpression","placeholder","${temp}"],["matInput","","formControlName","headerName",3,"placeholder"],["matInput","","formControlName","value"],["matInput","","formControlName","command"],["matInput","","formControlName","params","type","JSON","tb-json-to-string",""],["aria-hidden","false","aria-label","help-icon","matIconSuffix","",1,"material-icons-outlined",2,"cursor","pointer",3,"click","matTooltip"],[4,"ngIf"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(4,TH,11,10,"ng-template",2),t.ɵɵelementStart(5,"div",3)(6,"button",4),t.ɵɵlistener("click",(function(){return n.save()})),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"button",5),t.ɵɵlistener("click",(function(){return n.sendCommand.emit()})),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.commandForm),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(3,7,"gateway.rpc.title",t.ɵɵpureFunction1(14,tH,n.gatewayConnectorDefaultTypesTranslates.get(n.connectorType)))),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.connectorType),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.commandForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(8,10,"gateway.rpc-command-save-template")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.commandForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,12,"gateway.rpc-command-send")," "))},dependencies:t.ɵɵgetComponentDepsFactory(IH,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:flex;flex-direction:column;padding:0}[_nghost-%COMP%] .title[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%]{flex-wrap:nowrap}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%] > button[_ngcontent-%COMP%]{margin-top:10px}[_nghost-%COMP%] .mat-mdc-slide-toggle.margin[_ngcontent-%COMP%]{margin-bottom:10px;margin-left:10px}[_nghost-%COMP%] .fields[_ngcontent-%COMP%] .fields-label[_ngcontent-%COMP%]{font-weight:500}[_nghost-%COMP%] .border[_ngcontent-%COMP%]{padding:16px;margin-bottom:10px;box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;border:solid 1px #e0e0e0;border-radius:4px}[_nghost-%COMP%] .border[_ngcontent-%COMP%] .title[_ngcontent-%COMP%]{color:#0000008a}[_nghost-%COMP%] .border[_ngcontent-%COMP%] .mat-icon[_ngcontent-%COMP%]{color:#00000061}[_nghost-%COMP%] .border[_ngcontent-%COMP%] .mat-divider[_ngcontent-%COMP%]{margin-left:-16px;margin-right:-16px;margin-bottom:16px}']})}}function EH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",11),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function MH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",11),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.ModbusFunctionCodeTranslationsMap.get(e)))}}function kH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",12),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-required"))}function PH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",12),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function DH(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",13)(1,"mat-form-field",3)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",14),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,PH,3,3,"mat-icon",8),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rpc.value")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.rpcParametersFormGroup.get("value").hasError("required")&&e.rpcParametersFormGroup.get("value").touched)}}class OH{constructor(e){this.fb=e,this.ModbusEditableDataTypes=$t,this.ModbusFunctionCodeTranslationsMap=Zt,this.modbusDataTypes=Object.values(Wt),this.writeFunctionCodes=[5,6,15,16],this.defaultFunctionCodes=[3,4,6,16],this.readFunctionCodes=[1,2,3,4],this.bitsFunctionCodes=[...this.readFunctionCodes,...this.writeFunctionCodes],this.destroy$=new te,this.rpcParametersFormGroup=this.fb.group({type:[Wt.BYTES,[$.required]],functionCode:[this.defaultFunctionCodes[0],[$.required]],value:[{value:"",disabled:!0},[$.required,$.pattern(an)]],address:[null,[$.required]],objectsCount:[1,[$.required]]}),this.updateFunctionCodes(this.rpcParametersFormGroup.get("type").value),this.observeValueChanges(),this.observeKeyDataType(),this.observeFunctionCode()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.rpcParametersFormGroup.valid?null:{rpcParametersFormGroup:{valid:!1}}}writeValue(e){this.rpcParametersFormGroup.patchValue(e,{emitEvent:!1})}observeValueChanges(){this.rpcParametersFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}observeKeyDataType(){this.rpcParametersFormGroup.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.ModbusEditableDataTypes.includes(e)||this.rpcParametersFormGroup.get("objectsCount").patchValue(Kt[e],{emitEvent:!1}),this.updateFunctionCodes(e)}))}observeFunctionCode(){this.rpcParametersFormGroup.get("functionCode").valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.updateValueEnabling(e)))}updateValueEnabling(e){this.writeFunctionCodes.includes(e)?this.rpcParametersFormGroup.get("value").enable({emitEvent:!1}):(this.rpcParametersFormGroup.get("value").setValue(null),this.rpcParametersFormGroup.get("value").disable({emitEvent:!1}))}updateFunctionCodes(e){this.functionCodes=e===Wt.BITS?this.bitsFunctionCodes:this.defaultFunctionCodes,this.functionCodes.includes(this.rpcParametersFormGroup.get("functionCode").value)||this.rpcParametersFormGroup.get("functionCode").patchValue(this.functionCodes[0],{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||OH)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:OH,selectors:[["tb-gateway-modbus-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>OH)),multi:!0},{provide:K,useExisting:c((()=>OH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:35,vars:30,consts:[[3,"formGroup"],[1,"tb-form-hint","tb-primary-fill","no-padding-top","hint-container"],[1,"flex","flex-1","flex-row","gap-2.5"],[1,"flex-1"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["formControlName","functionCode"],["matInput","","type","number","min","0","max","50000","name","value","formControlName","address",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","number","min","1","max","50000","name","value","formControlName","objectsCount",3,"placeholder","readonly"],["class","flex",4,"ngIf"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[1,"flex"],["matInput","","name","value","formControlName","value",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelement(4,"br"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",2)(8,"mat-form-field",3)(9,"mat-label"),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-select",4),t.ɵɵtemplate(13,EH,2,2,"mat-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(14,"mat-form-field",3)(15,"mat-label"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"mat-select",6),t.ɵɵtemplate(19,MH,3,4,"mat-option",5),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",2)(21,"mat-form-field",3)(22,"mat-label"),t.ɵɵtext(23),t.ɵɵpipe(24,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(25,"input",7),t.ɵɵpipe(26,"translate"),t.ɵɵtemplate(27,kH,3,3,"mat-icon",8),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-form-field",3)(29,"mat-label"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(32,"input",9),t.ɵɵpipe(33,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(34,DH,8,7,"div",10),t.ɵɵelementContainerEnd()),2&e&&(t.ɵɵproperty("formGroup",n.rpcParametersFormGroup),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,14,"gateway.rpc.hint.modbus-response-reading"),""),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,16,"gateway.rpc.hint.modbus-writing-functions")," "),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(11,18,"gateway.rpc.type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.modbusDataTypes),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(17,20,"gateway.rpc.functionCode")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.functionCodes),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(24,22,"gateway.rpc.address")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(26,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.rpcParametersFormGroup.get("address").hasError("required")&&n.rpcParametersFormGroup.get("address").touched),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(31,26,"gateway.rpc.objectsCount")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(33,28,"gateway.set")),t.ɵɵproperty("readonly",!n.ModbusEditableDataTypes.includes(n.rpcParametersFormGroup.get("type").value)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.writeFunctionCodes.includes(n.rpcParametersFormGroup.get("functionCode").value)))},dependencies:t.ɵɵgetComponentDepsFactory(OH,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .hint-container[_ngcontent-%COMP%]{margin-bottom:12px}'],changeDetection:d.OnPush})}}function AH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",6),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rpc.responseTopicExpression")))}function FH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-form-field")(1,"mat-label"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",7),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rpc.responseTimeout")))}class RH{constructor(e){this.fb=e,this.onChange=e=>{},this.onTouched=()=>{},this.destroy$=new te,this.rpcParametersFormGroup=this.fb.group({methodFilter:[null,[$.required,$.pattern(an)]],requestTopicExpression:[null,[$.required,$.pattern(an)]],responseTopicExpression:[{value:null,disabled:!0},[$.required,$.pattern(an)]],responseTimeout:[{value:null,disabled:!0},[$.min(10),$.pattern(rn)]],valueExpression:[null,[$.required,$.pattern(an)]],withResponse:[!1,[]]}),this.observeValueChanges(),this.observeWithResponse()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.rpcParametersFormGroup.valid?null:{rpcParametersFormGroup:{valid:!1}}}writeValue(e){this.rpcParametersFormGroup.patchValue(e,{emitEvent:!1}),this.toggleResponseFields(e.withResponse)}observeValueChanges(){this.rpcParametersFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}observeWithResponse(){this.rpcParametersFormGroup.get("withResponse").valueChanges.pipe(me((e=>this.toggleResponseFields(e))),le(this.destroy$)).subscribe()}toggleResponseFields(e){const t=this.rpcParametersFormGroup.get("responseTopicExpression"),n=this.rpcParametersFormGroup.get("responseTimeout");e?(t.enable(),n.enable()):(t.disable(),n.disable())}static{this.ɵfac=function(e){return new(e||RH)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:RH,selectors:[["tb-gateway-mqtt-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>RH)),multi:!0},{provide:K,useExisting:c((()=>RH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:21,vars:15,consts:[[3,"formGroup"],["matInput","","formControlName","methodFilter","placeholder","echo"],["matInput","","formControlName","requestTopicExpression","placeholder","sensor/${deviceName}/request/${methodName}/${requestId}"],["formControlName","withResponse",1,"margin",3,"click"],[4,"ngIf"],["matInput","","formControlName","valueExpression","placeholder","${params}"],["matInput","","formControlName","responseTopicExpression","placeholder","sensor/${deviceName}/response/${methodName}/${requestId}"],["matInput","","formControlName","responseTimeout","type","number","placeholder","10000","min","10","step","1"]],template:function(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"mat-form-field")(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",1),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field")(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(10,"input",2),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-slide-toggle",3),t.ɵɵlistener("click",(function(e){return e.stopPropagation()})),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(14,AH,5,3,"mat-form-field",4)(15,FH,5,3,"mat-form-field",4),t.ɵɵelementStart(16,"mat-form-field")(17,"mat-label"),t.ɵɵtext(18),t.ɵɵpipe(19,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(20,"input",5),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e){let e,i;t.ɵɵproperty("formGroup",n.rpcParametersFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,7,"gateway.rpc.method-name")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,9,"gateway.rpc.requestTopicExpression")),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,11,"gateway.rpc.withResponse")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",null==(e=n.rpcParametersFormGroup.get("withResponse"))?null:e.value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",null==(i=n.rpcParametersFormGroup.get("withResponse"))?null:i.value),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(19,13,"gateway.rpc.valueExpression"))}},dependencies:t.ɵɵgetComponentDepsFactory(RH,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:flex;flex-direction:column}[_nghost-%COMP%] .mat-mdc-slide-toggle.margin[_ngcontent-%COMP%]{margin-bottom:10px;margin-left:10px}'],changeDetection:d.OnPush})}}function BH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",26),t.ɵɵelement(1,"mat-icon",27),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("svgIcon",i.valueTypes.get(e).icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,i.valueTypes.get(e).name))}}function NH(e,n){1&e&&(t.ɵɵelement(0,"input",28),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function LH(e,n){1&e&&(t.ɵɵelement(0,"input",29),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function VH(e,n){1&e&&(t.ɵɵelement(0,"input",30),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function qH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-select",31)(1,"mat-option",26),t.ɵɵtext(2,"true"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-option",26),t.ɵɵtext(4,"false"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵproperty("value",!0),t.ɵɵadvance(2),t.ɵɵproperty("value",!1))}function GH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",32),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function zH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",8)(1,"div",9)(2,"div",10),t.ɵɵtext(3,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",11)(5,"mat-form-field",12)(6,"mat-select",13)(7,"mat-select-trigger")(8,"div",14),t.ɵɵelement(9,"mat-icon",15),t.ɵɵelementStart(10,"span"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(13,BH,5,5,"mat-option",16),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(14,"div",17)(15,"div",10),t.ɵɵtext(16,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"mat-form-field",18),t.ɵɵelementContainerStart(18,19),t.ɵɵtemplate(19,NH,2,3,"input",20)(20,LH,2,3,"input",21)(21,VH,2,3,"input",22)(22,qH,5,2,"mat-select",23),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(23,GH,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(24,"button",25),t.ɵɵpipe(25,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext();return t.ɵɵresetView(i.removeArgument(n))})),t.ɵɵelementStart(26,"mat-icon"),t.ɵɵtext(27,"delete"),t.ɵɵelementEnd()()()}if(2&e){let e,i;const a=n.$implicit,r=t.ɵɵnextContext();t.ɵɵproperty("formGroup",a),t.ɵɵadvance(9),t.ɵɵproperty("svgIcon",null==(e=r.valueTypes.get(a.get("type").value))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,11,null==(i=r.valueTypes.get(a.get("type").value))?null:i.name)),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",r.valueTypeKeys),t.ɵɵadvance(5),t.ɵɵproperty("ngSwitch",a.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.STRING),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.INTEGER),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.DOUBLE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",r.MappingValueType.BOOLEAN),t.ɵɵadvance(),t.ɵɵproperty("ngIf",a.get(a.get("type").value+"Value").hasError("required")&&a.get(a.get("type").value+"Value").touched),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(25,13,"gateway.rpc.remove"))}}class UH{constructor(e,t){this.fb=e,this.cdr=t,this.valueTypeKeys=Object.values(Yt),this.MappingValueType=Yt,this.valueTypes=Xt,this.onChange=e=>{},this.onTouched=()=>{},this.destroy$=new te,this.rpcParametersFormGroup=this.fb.group({method:[null,[$.required,$.pattern(an)]],arguments:this.fb.array([])}),this.observeValueChanges()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.rpcParametersFormGroup.valid?null:{rpcParametersFormGroup:{valid:!1}}}writeValue(e){this.clearArguments(),e.arguments?.map((({type:e,value:t})=>({type:e,[e+"Value"]:t}))).forEach((e=>this.addArgument(e))),this.cdr.markForCheck(),this.rpcParametersFormGroup.get("method").patchValue(e.method)}observeValueChanges(){this.rpcParametersFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{const t=e.arguments.map((({type:e,...t})=>({type:e,value:t[e+"Value"]})));this.onChange({method:e.method,arguments:t}),this.onTouched()}))}removeArgument(e){this.rpcParametersFormGroup.get("arguments").removeAt(e)}addArgument(e={}){const t=this.fb.group({type:[e.type??Yt.STRING],stringValue:[e.stringValue??{value:"",disabled:!(Se(e,{})||e.stringValue)},[$.required,$.pattern(an)]],integerValue:[{value:e.integerValue??0,disabled:!ke(e.integerValue)},[$.required,$.pattern(rn)]],doubleValue:[{value:e.doubleValue??0,disabled:!ke(e.doubleValue)},[$.required]],booleanValue:[{value:e.booleanValue??!1,disabled:!ke(e.booleanValue)},[$.required]]});this.observeTypeChange(t),this.rpcParametersFormGroup.get("arguments").push(t,{emitEvent:!1})}clearArguments(){const e=this.rpcParametersFormGroup.get("arguments");for(;0!==e.length;)e.removeAt(0)}observeTypeChange(e){e.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((t=>{e.disable({emitEvent:!1}),e.get("type").enable({emitEvent:!1}),e.get(t+"Value").enable({emitEvent:!1})}))}static{this.ɵfac=function(e){return new(e||UH)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:UH,selectors:[["tb-gateway-opc-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>UH)),multi:!0},{provide:K,useExisting:c((()=>UH)),multi:!0}]),t.ɵɵStandaloneFeature],decls:18,vars:14,consts:[[3,"formGroup"],[1,"tb-form-hint","tb-primary-fill","tb-flex","no-padding-top","hint-container"],[1,"tb-flex"],["matInput","","formControlName","method","placeholder","multiply"],["formArrayName","arguments",1,"tb-form-panel","stroked","arguments-container"],[1,"fields-label"],["class","flex flex-1 items-center justify-center gap-2.5",3,"formGroup",4,"ngFor","ngForOf"],["mat-raised-button","",1,"self-start",3,"click"],[1,"flex","flex-1","items-center","justify-center","gap-2.5",3,"formGroup"],[1,"tb-form-row","column-xs","type-container","items-center","justify-between"],["translate","",1,"tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","type"],[1,"tb-flex","align-center"],[1,"tb-mat-18",3,"svgIcon"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-row","column-xs","value-container","item-center","justify-between"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","tb-suffix-absolute","flex"],[3,"ngSwitch"],["matInput","","required","","formControlName","stringValue",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder",4,"ngSwitchCase"],["formControlName","booleanValue",4,"ngSwitchCase"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["mat-icon-button","","matTooltipPosition","above",1,"tb-box-button",3,"click","matTooltip"],[3,"value"],[1,"tb-mat-20",3,"svgIcon"],["matInput","","required","","formControlName","stringValue",3,"placeholder"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder"],["formControlName","booleanValue"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",2)(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(8,"input",3),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"fieldset",4)(10,"strong")(11,"span",5),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(14,zH,28,15,"div",6),t.ɵɵelementStart(15,"button",7),t.ɵɵlistener("click",(function(){return n.addArgument()})),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e&&(t.ɵɵproperty("formGroup",n.rpcParametersFormGroup),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,6,"gateway.rpc.hint.opc-method")," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(7,8,"gateway.rpc.method")),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,10,"gateway.rpc.arguments")),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",n.rpcParametersFormGroup.get("arguments").controls),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,12,"gateway.rpc.add-argument")," "))},dependencies:t.ɵɵgetComponentDepsFactory(UH,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .arguments-container[_ngcontent-%COMP%]{margin-bottom:10px}[_nghost-%COMP%] .type-container[_ngcontent-%COMP%]{width:40%}[_nghost-%COMP%] .value-container[_ngcontent-%COMP%]{width:50%}[_nghost-%COMP%] .hint-container[_ngcontent-%COMP%]{margin-bottom:12px}'],changeDetection:d.OnPush})}}function jH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SocketMethodProcessingsTranslates.get(e))," ")}}function HH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}class WH extends Ra{constructor(){super(...arguments),this.SocketMethodProcessingsTranslates=Cj,this.socketMethodProcessings=Object.values(Sj),this.socketEncoding=Object.values(mt)}initFormGroup(){return this.fb.group({methodRPC:[null,[$.required,$.pattern(an)]],methodProcessing:[Sj.WRITE,[$.required]],encoding:[Ij.UTF_8,[$.required,$.pattern(an)]],withResponse:[!1,[]]})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(WH)))(n||WH)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:WH,selectors:[["tb-gateway-socket-rpc-parameters"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>WH)),multi:!0},{provide:K,useExisting:c((()=>WH)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:21,vars:15,consts:[[3,"formGroup"],[1,"w-full"],["matInput","","formControlName","methodRPC","placeholder","rpcMethod1"],[1,"mat-block"],["formControlName","methodProcessing"],[3,"value",4,"ngFor","ngForOf"],["formControlName","encoding"],["formControlName","withResponse",1,"mat-slide","margin"],[3,"value"]],template:function(e,n){1&e&&(t.ɵɵelementContainerStart(0,0),t.ɵɵelementStart(1,"mat-form-field",1)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",2),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",3)(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-select",4),t.ɵɵtemplate(11,jH,3,4,"mat-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",3)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"mat-select",6),t.ɵɵtemplate(17,HH,2,2,"mat-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"mat-slide-toggle",7),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e&&(t.ɵɵproperty("formGroup",n.formGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,7,"gateway.rpc.methodRPC")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,9,"gateway.rpc.methodProcessing")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.socketMethodProcessings),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,11,"gateway.encoding")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(20,13,"gateway.rpc.withResponse")," "))},dependencies:t.ɵɵgetComponentDepsFactory(WH,[j,_]),encapsulation:2,changeDetection:d.OnPush})}}const $H=e=>({border:e}),KH=e=>({type:e});function YH(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",15),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function XH(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-min")," "))}function ZH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"mat-form-field")(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-select",9),t.ɵɵtemplate(6,YH,2,2,"mat-option",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"mat-form-field",11)(8,"mat-label"),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(11,"input",12),t.ɵɵtemplate(12,XH,3,3,"mat-error",13),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"button",14),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.sendCommand())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,6,"gateway.statistics.command")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.RPCCommands),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(10,8,"gateway.statistics.timeout")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.commandForm.get("time").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("disabled",e.commandForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,10,"gateway.rpc-command-send")," ")}}function QH(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-service-rpc-connector",17),t.ɵɵlistener("sendCommand",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.sendCommand())}))("saveTemplate",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.saveTemplate())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("connectorType",e.connectorType)}}function JH(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-modbus-rpc-parameters",24)}function eW(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-mqtt-rpc-parameters",24)}function tW(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-opc-rpc-parameters",24)}function nW(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-socket-rpc-parameters",24)}function iW(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",18)(1,"div",19),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(4,20),t.ɵɵtemplate(5,JH,1,0,"tb-gateway-modbus-rpc-parameters",21)(6,eW,1,0,"tb-gateway-mqtt-rpc-parameters",21)(7,tW,1,0,"tb-gateway-opc-rpc-parameters",21)(8,nW,1,0,"tb-gateway-socket-rpc-parameters",21),t.ɵɵelementContainerEnd(),t.ɵɵelementStart(9,"div",22)(10,"button",23),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.saveTemplate())})),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"button",14),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.sendCommand())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(3,10,"gateway.rpc.title",t.ɵɵpureFunction1(17,KH,e.gatewayConnectorDefaultTypesTranslates.get(e.connectorType)))),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",e.connectorType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MODBUS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MQTT),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.OPCUA),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.SOCKET),t.ɵɵadvance(2),t.ɵɵproperty("disabled",e.commandForm.get("params").invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,13,"gateway.rpc-command-save-template")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",e.commandForm.get("params").invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,15,"gateway.rpc-command-send")," ")}}function aW(e,n){if(1&e&&t.ɵɵtemplate(0,QH,1,1,"tb-gateway-service-rpc-connector",16)(1,iW,16,19,"ng-template",null,1,t.ɵɵtemplateRefExtractor),2&e){const e=t.ɵɵreference(2),n=t.ɵɵnextContext();t.ɵɵproperty("ngIf",!n.typesWithUpdatedParams.has(n.connectorType))("ngIfElse",e)}}function rW(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",25)(1,"mat-icon",26),t.ɵɵtext(2,"schedule"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"span"),t.ɵɵtext(4),t.ɵɵpipe(5,"date"),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind2(5,1,e.resultTime,"yyyy/MM/dd HH:mm:ss"))}}function oW(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-service-rpc-connector-templates",27),t.ɵɵlistener("useTemplate",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.useTemplate(n))})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("rpcTemplates",e.templates)("ctx",e.ctx)("connectorType",e.connectorType)}}class sW{constructor(e,t,n,i,a){this.fb=e,this.dialog=t,this.utils=n,this.cd=i,this.attributeService=a,this.contentTypes=V,this.RPCCommands=["Ping","Stats","Devices","Update","Version","Restart","Reboot"],this.templates=[],this.ConnectorType=ct,this.gatewayConnectorDefaultTypesTranslates=dt,this.typesWithUpdatedParams=new Set([ct.MQTT,ct.OPCUA,ct.MODBUS,ct.SOCKET]),this.modbusReadFunctionCodes=[1,2,3,4],this.subscriptionOptions={callbacks:{onDataUpdated:()=>this.ctx.ngZone.run((()=>{this.updateTemplates()})),onDataUpdateError:(e,t)=>this.ctx.ngZone.run((()=>{this.onDataUpdateError(t)})),dataLoading:()=>{}}},this.commandForm=this.fb.group({command:[null,[$.required]],time:[60,[$.required,$.min(1)]],params:["{}",[st]],result:[null]})}ngOnInit(){if(this.isConnector=this.ctx.settings.isConnector,this.isConnector){this.connectorType=this.ctx.stateController.getStateParams().connector_rpc.value.type;const e=[{type:N.entity,entityType:L.DEVICE,entityId:this.ctx.defaultSubscription.targetDeviceId,entityName:"Connector",attributes:[{name:`${this.connectorType}_template`}]}];this.ctx.subscriptionApi.createSubscriptionFromInfo(R.latest,e,this.subscriptionOptions,!1,!0).subscribe((e=>{this.subscription=e}))}else this.commandForm.get("command").setValue(this.RPCCommands[0])}sendCommand(e){this.resultTime=null;const t=e||this.commandForm.value,n=this.isConnector?`${this.connectorType}_`:"gateway_",i=this.isConnector?this.getCommandFromParamsByType(t.params):t.command.toLowerCase(),a=this.ctx.stateController.getStateParams().connector_rpc?.value.configurationJson.id,r=a?{...t.params,connectorId:a}:t.params;this.ctx.controlApi.sendTwoWayCommand(n+i,r,t.time).subscribe({next:e=>{this.resultTime=(new Date).getTime(),this.commandForm.get("result").setValue(JSON.stringify(e))},error:e=>{this.resultTime=(new Date).getTime(),console.error(e),this.commandForm.get("result").setValue(JSON.stringify(e.error))}})}getCommandFromParamsByType(e){switch(this.connectorType){case ct.MQTT:case ct.FTP:case ct.SNMP:case ct.REST:case ct.REQUEST:return e.methodFilter;case ct.MODBUS:return this.modbusReadFunctionCodes.includes(e.functionCode)?"get":"set";case ct.BACNET:case ct.CAN:case ct.OPCUA:return e.method;case ct.BLE:case ct.OCPP:case ct.SOCKET:case ct.XMPP:return e.methodRPC;default:return e.command}}saveTemplate(){this.dialog.open(Yj,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{config:this.commandForm.value.params,templates:this.templates}}).afterClosed().subscribe((e=>{if(e){const t={name:e,config:this.commandForm.value.params,type:this.connectorType},n=this.templates,i=n.findIndex((e=>e.name==t.name));i>-1&&n.splice(i,1),n.push(t);const a=`${this.connectorType}_template`;this.attributeService.saveEntityAttributes({id:this.ctx.defaultSubscription.targetDeviceId,entityType:L.DEVICE},D.SERVER_SCOPE,[{key:a,value:n}]).subscribe((()=>{this.cd.detectChanges()}))}}))}useTemplate(e){this.commandForm.get("params").patchValue(e.config)}updateTemplates(){this.templates=this.subscription.data[0].data[0][1].length?JSON.parse(this.subscription.data[0].data[0][1]):[],this.cd.detectChanges()}onDataUpdateError(e){const t=this.utils.parseException(e);let n=t.name;t.message&&(n+=": "+t.message),console.error(n)}static{this.ɵfac=function(e){return new(e||sW)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(_e.UtilsService),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(_e.AttributeService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:sW,selectors:[["tb-gateway-service-rpc"]],inputs:{ctx:"ctx",dialogRef:"dialogRef"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:12,vars:14,consts:[["connectorForm",""],["updatedParameters",""],[1,"flex","flex-1","flex-col"],[1,"command-form","flex","flex-row","gap-2.5","lt-sm:flex-col",3,"formGroup"],[4,"ngIf","ngIfElse"],[1,"result-block",3,"formGroup"],["class","result-time flex flex-1 flex-row items-center justify-center",4,"ngIf"],["readonly","true","formControlName","result",3,"contentType"],["class","border",3,"rpcTemplates","ctx","connectorType","useTemplate",4,"ngIf"],["formControlName","command"],[3,"value",4,"ngFor","ngForOf"],[1,"flex-1"],["matInput","","formControlName","time","type","number","min","1"],[4,"ngIf"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["formControlName","params",3,"connectorType","sendCommand","saveTemplate",4,"ngIf","ngIfElse"],["formControlName","params",3,"sendCommand","saveTemplate","connectorType"],[1,"rpc-parameters","flex","flex-col"],[1,"mat-subtitle-1","tb-form-panel-title"],[3,"ngSwitch"],["formControlName","params",4,"ngSwitchCase"],[1,"fex-row","template-actions","flex","flex-1","items-center","justify-end","gap-2.5"],["mat-raised-button","",3,"click","disabled"],["formControlName","params"],[1,"result-time","flex","flex-1","flex-row","items-center","justify-center"],[1,"material-icons"],[1,"border",3,"useTemplate","rpcTemplates","ctx","connectorType"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",2)(1,"div",3),t.ɵɵtemplate(2,ZH,16,12,"ng-container",4)(3,aW,3,2,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"section",5)(6,"span"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,rW,6,4,"div",6),t.ɵɵelementEnd(),t.ɵɵelement(10,"tb-json-content",7),t.ɵɵelementEnd()(),t.ɵɵtemplate(11,oW,1,3,"tb-gateway-service-rpc-connector-templates",8)),2&e){const e=t.ɵɵreference(4);t.ɵɵclassMap(t.ɵɵpureFunction1(12,$H,n.isConnector)),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.commandForm),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.isConnector)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵproperty("formGroup",n.commandForm),t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(8,10,"gateway.rpc-command-result")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.resultTime),t.ɵɵadvance(),t.ɵɵproperty("contentType",n.contentTypes.JSON),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isConnector)}},dependencies:t.ɵɵgetComponentDepsFactory(sW,[j,_,IH,OH,RH,UH,Wj,WH]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;overflow:auto;display:flex;flex-direction:row;padding:0 5px}[_nghost-%COMP%] > *[_ngcontent-%COMP%]{height:100%;overflow:auto}[_nghost-%COMP%] > tb-gateway-service-rpc-connector-templates[_ngcontent-%COMP%]:last-child{margin-left:10px}[_nghost-%COMP%] tb-gateway-service-rpc-connector-templates[_ngcontent-%COMP%]{flex:1 1 30%;max-width:30%}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%]{flex-wrap:nowrap;padding:0 5px 5px}[_nghost-%COMP%] .command-form[_ngcontent-%COMP%] > button[_ngcontent-%COMP%]{margin-top:10px}[_nghost-%COMP%] .rpc-parameters[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%]{padding:0 5px;display:flex;flex-direction:column;flex:1}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] > span[_ngcontent-%COMP%]{font-weight:600;position:relative;font-size:14px;margin-bottom:10px}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] > span[_ngcontent-%COMP%] .result-time[_ngcontent-%COMP%]{font-weight:400;font-size:14px;line-height:32px;position:absolute;left:0;top:25px;z-index:5;color:#0000008a}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] > span[_ngcontent-%COMP%] .result-time[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{padding-left:10px}[_nghost-%COMP%] .result-block[_ngcontent-%COMP%] tb-json-content[_ngcontent-%COMP%]{flex:1}[_nghost-%COMP%] .border[_ngcontent-%COMP%]{padding:16px;box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;border:solid 1px #e0e0e0;border-radius:4px}']})}}function lW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.configuration-delete-dialog-input-required")," "))}e("GatewayServiceRPCComponent",sW);class pW extends A{constructor(e,t,n,i,a){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.gatewayName=this.data.gatewayName,this.gatewayControl=this.fb.control("")}close(){this.dialogRef.close()}turnOff(){this.dialogRef.close(!0)}static{this.ɵfac=function(e){return new(e||pW)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:pW,selectors:[["tb-gateway-remote-configuration-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:24,vars:14,consts:[["color","warn"],["translate",""],[1,"flex-1"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"mat-content","flex-col",2,"max-width","600px"],[3,"innerHTML"],[1,"mat-block","tb-value-type",2,"flex-grow","0"],["matInput","","required","",3,"formControl"],[4,"ngIf"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","warn","type","button","cdkFocusInitial","",3,"click"],["mat-button","","color","warn","type","button",3,"click","disabled"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-toolbar",0)(1,"mat-icon"),t.ɵɵtext(2,"warning"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"h2",1),t.ɵɵtext(4,"gateway.configuration-delete-dialog-header"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",2),t.ɵɵelementStart(6,"button",3),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵelementStart(7,"mat-icon",4),t.ɵɵtext(8,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",5),t.ɵɵelement(10,"span",6),t.ɵɵpipe(11,"translate"),t.ɵɵelementStart(12,"mat-form-field",7)(13,"mat-label",1),t.ɵɵtext(14,"gateway.configuration-delete-dialog-input"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",8),t.ɵɵtemplate(16,lW,3,3,"mat-error",9),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",10)(18,"button",11),t.ɵɵlistener("click",(function(){return n.close()})),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"button",12),t.ɵɵlistener("click",(function(){return n.turnOff()})),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(10),t.ɵɵpropertyInterpolate2("innerHTML","",t.ɵɵpipeBind1(11,8,"gateway.configuration-delete-dialog-body")," ",n.gatewayName,"",t.ɵɵsanitizeHtml),t.ɵɵadvance(5),t.ɵɵproperty("formControl",n.gatewayControl),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayControl.hasError("required")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(20,10,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.gatewayControl.value!==n.gatewayName),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(23,12,"gateway.configuration-delete-dialog-confirm")," "))},dependencies:t.ɵɵgetComponentDepsFactory(pW,[j,_]),encapsulation:2})}}var cW;e("GatewayRemoteConfigurationDialogComponent",pW),function(e){e.tls="tls",e.accessToken="accessToken"}(cW||(cW={}));const dW="configuration_drafts",uW="RemoteLoggingLevel",mW=new Map([[cW.tls,"gateway.security-types.tls"],[cW.accessToken,"gateway.security-types.access-token"]]);var hW,gW;!function(e){e.none="NONE",e.critical="CRITICAL",e.error="ERROR",e.warning="WARNING",e.info="INFO",e.debug="DEBUG"}(hW||(hW={})),function(e){e.memory="memory",e.file="file"}(gW||(gW={}));const fW=new Map([[gW.memory,"gateway.storage-types.memory-storage"],[gW.file,"gateway.storage-types.file-storage"]]);var yW;!function(e){e.mqtt="MQTT",e.modbus="Modbus",e.opcua="OPC-UA",e.ble="BLE",e.request="Request",e.can="CAN",e.bacnet="BACnet",e.custom="Custom"}(yW||(yW={}));const vW={config:{},name:"",configType:null,enabled:!1};function xW(e){return JSON.stringify(e.value)===JSON.stringify({})?{validJSON:!0}:null}function bW(e){return e.replace("_","").replace("-","").replace(/^\s+|\s+/g,"").toLowerCase()+".json"}function wW(e,t){return'[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"'.replace(/{ERROR}/g,e).replace(/{.\/logs\/}/g,t)}function SW(e){return{id:e,entityType:L.DEVICE}}function CW(e){const t={};return Object.prototype.hasOwnProperty.call(e,"thingsboard")&&(t.host=e.thingsboard.host,t.port=e.thingsboard.port,t.remoteConfiguration=e.thingsboard.remoteConfiguration,Object.prototype.hasOwnProperty.call(e.thingsboard.security,cW.accessToken)?(t.securityType=cW.accessToken,t.accessToken=e.thingsboard.security.accessToken):(t.securityType=cW.tls,t.caCertPath=e.thingsboard.security.caCert,t.privateKeyPath=e.thingsboard.security.privateKey,t.certPath=e.thingsboard.security.cert)),Object.prototype.hasOwnProperty.call(e,"storage")&&Object.prototype.hasOwnProperty.call(e.storage,"type")&&(e.storage.type===gW.memory?(t.storageType=gW.memory,t.readRecordsCount=e.storage.read_records_count,t.maxRecordsCount=e.storage.max_records_count):e.storage.type===gW.file&&(t.storageType=gW.file,t.dataFolderPath=e.storage.data_folder_path,t.maxFilesCount=e.storage.max_file_count,t.readRecordsCount=e.storage.read_records_count,t.maxRecordsCount=e.storage.max_records_count)),t}function _W(e){const t={};for(const n of e)n.enabled||(t[n.name]={connector:n.configType,config:n.config});return t}function TW(e){const t={thingsboard:IW(e)};return function(e,t){for(const n of t)if(n.enabled){const t=n.configType;Array.isArray(e[t])||(e[t]=[]);const i={name:n.name,config:n.config};e[t].push(i)}}(t,e.connectors),t}function IW(e){let t;t=e.securityType===cW.accessToken?{accessToken:e.accessToken}:{caCert:e.caCertPath,privateKey:e.privateKeyPath,cert:e.certPath};const n={host:e.host,remoteConfiguration:e.remoteConfiguration,port:e.port,security:t};let i;i=e.storageType===gW.memory?{type:gW.memory,read_records_count:e.readRecordsCount,max_records_count:e.maxRecordsCount}:{type:gW.file,data_folder_path:e.dataFolderPath,max_file_count:e.maxFilesCount,max_read_records_count:e.readRecordsCount,max_records_per_file:e.maxRecordsCount};const a=[];for(const t of e.connectors)if(t.enabled){const e={configuration:bW(t.name),name:t.name,type:t.configType};a.push(e)}return{thingsboard:n,connectors:a,storage:i,logs:window.btoa(wW(e.remoteLoggingLevel,e.remoteLoggingPathToLogs))}}const EW=["formContainer"],MW=(e,t,n)=>({"gap-1.25":e,"flex-row":t,"flex-col":n}),kW=(e,t,n)=>({"gap-1.25":e,"flex-row justify-end item-center":t,"flex-col justify-evenly item-center":n});function PW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e.value.toString())," ")}}function DW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-host-required "),t.ɵɵelementEnd())}function OW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-required "),t.ɵɵelementEnd())}function AW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-min "),t.ɵɵelementEnd())}function FW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-max "),t.ɵɵelementEnd())}function RW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.thingsboard-port-pattern "),t.ɵɵelementEnd())}function BW(e,n){1&e&&(t.ɵɵelementStart(0,"div",16)(1,"mat-form-field")(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",30),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field")(7,"mat-label"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(10,"input",31),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field")(12,"mat-label"),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",32),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.tls-path-ca-certificate")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,5,"gateway.tls-path-private-key")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,7,"gateway.tls-path-client-certificate")))}function NW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function LW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.path-logs-required "),t.ɵɵelementEnd())}function VW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e.value.toString())," ")}}function qW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-pack-size-required "),t.ɵɵelementEnd())}function GW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-pack-size-min "),t.ɵɵelementEnd())}function zW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-pack-size-pattern "),t.ɵɵelementEnd())}function UW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-records-required "),t.ɵɵelementEnd())}function jW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-records-min "),t.ɵɵelementEnd())}function HW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-records-pattern "),t.ɵɵelementEnd())}function WW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-files-required "),t.ɵɵelementEnd())}function $W(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-files-min "),t.ɵɵelementEnd())}function KW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-max-files-pattern "),t.ɵɵelementEnd())}function YW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.storage-path-required "),t.ɵɵelementEnd())}function XW(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",5)(1,"mat-form-field",8)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",33),t.ɵɵtemplate(6,WW,2,0,"mat-error",10)(7,$W,2,0,"mat-error",10)(8,KW,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-form-field",8)(10,"mat-label"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",34),t.ɵɵtemplate(14,YW,2,0,"mat-error",10),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵclassMap(t.ɵɵpureFunction3(12,MW,e.layoutGap,e.alignment,!e.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,8,"gateway.storage-max-files")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("maxFilesCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("maxFilesCount").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("maxFilesCount").hasError("pattern")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,10,"gateway.storage-path")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.gatewayConfigurationGroup.get("dataFolderPath").hasError("required"))}}function ZW(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",28),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function QW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.connector-type-required "),t.ɵɵelementEnd())}function JW(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error",29),t.ɵɵtext(1," gateway.connector-name-required "),t.ɵɵelementEnd())}function e$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"section",35)(1,"div",36)(2,"div",37),t.ɵɵelement(3,"mat-slide-toggle",38),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",39)(5,"mat-form-field",8)(6,"mat-label"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-select",40),t.ɵɵlistener("selectionChange",(function(){const n=t.ɵɵrestoreView(e).$implicit,i=t.ɵɵnextContext();return t.ɵɵresetView(i.changeConnectorType(n))})),t.ɵɵtemplate(10,ZW,2,2,"mat-option",7),t.ɵɵelementEnd(),t.ɵɵtemplate(11,QW,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",8)(13,"mat-label"),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"input",41),t.ɵɵlistener("blur",(function(){const n=t.ɵɵrestoreView(e),i=n.$implicit,a=n.index,r=t.ɵɵnextContext();return t.ɵɵresetView(r.changeConnectorName(i,a))})),t.ɵɵelementEnd(),t.ɵɵtemplate(17,JW,2,0,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",42)(19,"button",43),t.ɵɵpipe(20,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e),a=i.$implicit,r=i.index,o=t.ɵɵnextContext();return t.ɵɵresetView(o.openConfigDialog(n,r,a.get("config").value,a.get("name").value))})),t.ɵɵelementStart(21,"mat-icon"),t.ɵɵtext(22,"more_horiz"),t.ɵɵelementEnd()(),t.ɵɵelementStart(23,"button",43),t.ɵɵpipe(24,"translate"),t.ɵɵlistener("click",(function(){const n=t.ɵɵrestoreView(e).index,i=t.ɵɵnextContext();return t.ɵɵresetView(i.removeConnector(n))})),t.ɵɵelementStart(25,"mat-icon"),t.ɵɵtext(26,"close"),t.ɵɵelementEnd()()()()()}if(2&e){const e=n.$implicit,i=n.index,a=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("formGroupName",i),t.ɵɵadvance(3),t.ɵɵclassMap(t.ɵɵpureFunction3(24,MW,a.layoutGap,a.alignment,!a.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(8,16,"gateway.connector-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",a.connectorTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("configType").hasError("required")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,18,"gateway.connector-name")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.get("name").hasError("required")),t.ɵɵadvance(),t.ɵɵclassMap(t.ɵɵpureFunction3(28,kW,a.layoutGap,a.alignment,!a.alignment)),t.ɵɵadvance(),t.ɵɵclassProp("mat-warn",e.get("config").invalid),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(20,20,"gateway.update-config")),t.ɵɵproperty("disabled",a.isReadOnlyForm),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(24,22,"gateway.delete")),t.ɵɵproperty("disabled",a.isReadOnlyForm)}}function t$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",44),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.exportConfig())})),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,3,"gateway.download-tip")),t.ɵɵproperty("disabled",!e.gatewayConfigurationGroup.dirty||e.gatewayConfigurationGroup.invalid),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,5,"action.download")," ")}}function n$(e,n){if(1&e&&(t.ɵɵelementStart(0,"button",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,3,"gateway.save-tip")),t.ɵɵproperty("disabled",!e.gatewayConfigurationGroup.dirty||e.gatewayConfigurationGroup.invalid),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,5,"action.save")," ")}}class i$ extends q{constructor(e,t,n,i,a,r,o,s,l,p,c){super(e),this.store=e,this.elementRef=t,this.utils=n,this.ngZone=i,this.fb=a,this.window=r,this.dialog=o,this.translate=s,this.deviceService=l,this.attributeService=p,this.importExport=c,this.alignment=!0,this.layoutGap=!0,this.securityTypes=mW,this.gatewayLogLevels=Object.keys(hW).map((e=>hW[e])),this.connectorTypes=Object.keys(yW),this.storageTypes=fW,this.toastTargetId="gateway-configuration-widget"+this.utils.guid(),this.isReadOnlyForm=!1}get connectors(){return this.gatewayConfigurationGroup.get("connectors")}ngOnInit(){this.initWidgetSettings(this.ctx.settings),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(){this.formResize$&&this.formResize$.disconnect(),this.subscribeGateway$.unsubscribe(),this.subscribeStorageType$.unsubscribe()}initWidgetSettings(e){let t;t=e.gatewayTitle&&e.gatewayTitle.length?this.utils.customTranslation(e.gatewayTitle,e.gatewayTitle):this.translate.instant("gateway.gateway"),this.ctx.widgetTitle=t,this.isReadOnlyForm=!!e.readOnly&&e.readOnly,this.archiveFileName=e.archiveFileName?.length?e.archiveFileName:"gatewayConfiguration",this.gatewayType=e.gatewayType?.length?e.gatewayType:"Gateway",this.gatewayNameExists=this.utils.customTranslation(e.gatewayNameExists,e.gatewayNameExists)||this.translate.instant("gateway.gateway-exists"),this.successfulSaved=this.utils.customTranslation(e.successfulSave,e.successfulSave)||this.translate.instant("gateway.gateway-saved"),this.updateWidgetDisplaying()}resize(){this.ngZone.run((()=>{this.updateWidgetDisplaying(),this.ctx.detectChanges()}))}updateWidgetDisplaying(){this.ctx.$container&&this.ctx.$container[0].offsetWidth<=425?(this.layoutGap=!1,this.alignment=!1):(this.layoutGap=!0,this.alignment=!0)}saveAttribute(e,t,n){const i=this.gatewayConfigurationGroup.get("gateway").value,a={key:e,value:t};return this.attributeService.saveEntityAttributes(SW(i),n,[a])}createConnector(e=vW){this.connectors.push(this.fb.group({enabled:[e.enabled],configType:[e.configType,[$.required]],name:[e.name,[$.required]],config:[e.config,[$.nullValidator,xW]]}))}getFormField(e){return this.gatewayConfigurationGroup.get(e)}buildForm(){this.gatewayConfigurationGroup=this.fb.group({gateway:[null,[]],accessToken:[null,[$.required]],securityType:[cW.accessToken],host:[this.window.location.hostname,[$.required]],port:[1883,[$.required,$.min(1),$.max(65535),$.pattern(/^-?[0-9]+$/)]],remoteConfiguration:[!0],caCertPath:["/etc/thingsboard-gateway/ca.pem"],privateKeyPath:["/etc/thingsboard-gateway/privateKey.pem"],certPath:["/etc/thingsboard-gateway/certificate.pem"],remoteLoggingLevel:[hW.debug],remoteLoggingPathToLogs:["./logs/",[$.required]],storageType:[gW.memory],readRecordsCount:[100,[$.required,$.min(1),$.pattern(/^-?[0-9]+$/)]],maxRecordsCount:[1e4,[$.required,$.min(1),$.pattern(/^-?[0-9]+$/)]],maxFilesCount:[5,[$.required,$.min(1),$.pattern(/^-?[0-9]+$/)]],dataFolderPath:["./data/",[$.required]],connectors:this.fb.array([])}),this.isReadOnlyForm&&this.gatewayConfigurationGroup.disable({emitEvent:!1}),this.subscribeStorageType$=this.getFormField("storageType").valueChanges.subscribe((e=>{e===gW.memory?(this.getFormField("maxFilesCount").disable(),this.getFormField("dataFolderPath").disable()):(this.getFormField("maxFilesCount").enable(),this.getFormField("dataFolderPath").enable())})),this.subscribeGateway$=this.getFormField("gateway").valueChanges.subscribe((e=>{null!==e?oe([this.deviceService.getDeviceCredentials(e).pipe(me((e=>{this.getFormField("accessToken").patchValue(e.credentialsId)}))),...this.getAttributes(e)]).subscribe((()=>{this.gatewayConfigurationGroup.markAsPristine(),this.ctx.detectChanges()})):this.getFormField("accessToken").patchValue("")}))}gatewayExist(){this.ctx.showErrorToast(this.gatewayNameExists,"top","left",this.toastTargetId)}exportConfig(){const e=this.gatewayConfigurationGroup.value,t={};var n,i,a;t["tb_gateway.yaml"]=function(e){let t;t="thingsboard:\n",t+=" host: "+e.host+"\n",t+=" remoteConfiguration: "+e.remoteConfiguration+"\n",t+=" port: "+e.port+"\n",t+=" security:\n",e.securityType===cW.accessToken?t+=" access-token: "+e.accessToken+"\n":(t+=" ca_cert: "+e.caCertPath+"\n",t+=" privateKey: "+e.privateKeyPath+"\n",t+=" cert: "+e.certPath+"\n"),t+="storage:\n",e.storageType===gW.memory?(t+=" type: memory\n",t+=" read_records_count: "+e.readRecordsCount+"\n",t+=" max_records_count: "+e.maxRecordsCount+"\n"):(t+=" type: file\n",t+=" data_folder_path: "+e.dataFolderPath+"\n",t+=" max_file_count: "+e.maxFilesCount+"\n",t+=" max_read_records_count: "+e.readRecordsCount+"\n",t+=" max_records_per_file: "+e.maxRecordsCount+"\n"),t+="connectors:\n";for(const n of e.connectors)n.enabled&&(t+=" -\n",t+=" name: "+n.name+"\n",t+=" type: "+n.configType+"\n",t+=" configuration: "+bW(n.name)+"\n");return t}(e),function(e,t){for(const n of t)n.enabled&&(e[bW(n.name)]=JSON.stringify(n.config))}(t,e.connectors),n=t,i=e.remoteLoggingLevel,a=e.remoteLoggingPathToLogs,n["logs.conf"]=wW(i,a),this.importExport.exportJSZip(t,this.archiveFileName),this.saveAttribute(uW,this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),D.SHARED_SCOPE)}addNewConnector(){this.createConnector()}removeConnector(e){e>-1&&(this.connectors.removeAt(e),this.connectors.markAsDirty())}openConfigDialog(e,t,n,i){e&&(e.stopPropagation(),e.preventDefault()),this.dialog.open(tt,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{jsonValue:n,required:!0,title:this.translate.instant("gateway.title-connectors-json",{typeName:i})}}).afterClosed().subscribe((e=>{e&&(this.connectors.at(t).get("config").patchValue(e),this.ctx.detectChanges())}))}createConnectorName(e,t,n=0){const i=n?t+n:t;return-1===e.findIndex((e=>e.name===i))?i:this.createConnectorName(e,t,++n)}validateConnectorName(e,t,n,i=0){for(let a=0;a{this.ctx.showSuccessToast(this.successfulSaved,2e3,"top","left",this.toastTargetId),this.gatewayConfigurationGroup.markAsPristine()}))}getAttributes(e){const t=[];return t.push(oe([this.getAttribute("current_configuration",D.CLIENT_SCOPE,e),this.getAttribute(dW,D.SERVER_SCOPE,e)]).pipe(me((([e,t])=>{this.setFormGatewaySettings(e),this.setFormConnectorsDraft(t),this.isReadOnlyForm&&this.gatewayConfigurationGroup.disable({emitEvent:!1})})))),t.push(this.getAttribute(uW,D.SHARED_SCOPE,e).pipe(me((e=>this.processLoggingLevel(e))))),t}getAttribute(e,t,n){return this.attributeService.getEntityAttributes(SW(n),t,[e])}setFormGatewaySettings(e){if(this.connectors.clear(),e.length>0){const t=JSON.parse(window.atob(e[0].value));for(const e of Object.keys(t)){const n=t[e];if("thingsboard"===e)null!==n&&Object.keys(n).length>0&&this.gatewayConfigurationGroup.patchValue(CW(n));else for(const t of Object.keys(n)){let i="No name";Object.prototype.hasOwnProperty.call(n[t],"name")&&(i=n[t].name);const a={enabled:!0,configType:e,config:n[t].config,name:i};this.createConnector(a)}}}}setFormConnectorsDraft(e){if(e.length>0){const t=JSON.parse(window.atob(e[0].value));for(const e of Object.keys(t)){const n={enabled:!1,configType:t[e].connector,config:t[e].config,name:e};this.createConnector(n)}}}processLoggingLevel(e){let t=hW.debug;e.length>0&&hW[e[0].value.toLowerCase()]&&(t=hW[e[0].value.toLowerCase()]),this.getFormField("remoteLoggingLevel").patchValue(t)}static{this.ɵfac=function(e){return new(e||i$)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(t.ElementRef),t.ɵɵdirectiveInject(_e.UtilsService),t.ɵɵdirectiveInject(t.NgZone),t.ɵɵdirectiveInject(H.UntypedFormBuilder),t.ɵɵdirectiveInject(Ce),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(He.TranslateService),t.ɵɵdirectiveInject(_e.DeviceService),t.ɵɵdirectiveInject(_e.AttributeService),t.ɵɵdirectiveInject(nt.ImportExportService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:i$,selectors:[["tb-gateway-form"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(EW,7),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.formContainerRef=e.first)}},inputs:{ctx:"ctx",isStateForm:"isStateForm"},standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:104,vars:104,consts:[["formContainer",""],["tb-toast","",1,"gateway-form",3,"ngSubmit","formGroup","toastTarget"],["multi","true",1,"mat-body-2"],[1,"tb-panel-title"],["formControlName","gateway","required","",3,"gatewayNameExist","deviceName","isStateForm","newGatewayType"],[1,"flex"],["formControlName","securityType"],[3,"value",4,"ngFor","ngForOf"],[1,"flex-1"],["matInput","","type","text","formControlName","host"],["translate","",4,"ngIf"],["matInput","","type","number","formControlName","port"],["class","flex flex-col",4,"ngIf"],["formControlName","remoteConfiguration"],["formControlName","remoteLoggingLevel"],["matInput","","type","text","formControlName","remoteLoggingPathToLogs"],[1,"flex","flex-col"],["formControlName","storageType"],["matInput","","type","number","formControlName","readRecordsCount"],["matInput","","type","number","formControlName","maxRecordsCount"],["class","flex",3,"class",4,"ngIf"],[1,"gateway-config","flex","flex-col"],["formArrayName","connectors",4,"ngFor","ngForOf"],[1,"no-data-found","items-center","justify-center"],["mat-raised-button","","type","button","matTooltipPosition","above",3,"click","matTooltip"],[1,"form-action-buttons","flex","flex-row","items-center","justify-end"],["mat-raised-button","","color","primary","type","button",3,"disabled","matTooltip","click",4,"ngIf"],["mat-raised-button","","color","primary","type","submit",3,"disabled","matTooltip",4,"ngIf"],[3,"value"],["translate",""],["matInput","","type","text","formControlName","caCertPath"],["matInput","","type","text","formControlName","privateKeyPath"],["matInput","","type","text","formControlName","certPath"],["matInput","","type","number","formControlName","maxFilesCount"],["matInput","","type","text","formControlName","dataFolderPath"],["formArrayName","connectors"],[1,"flex","flex-row","items-stretch","justify-between","gap-2",3,"formGroupName"],[1,"flex","flex-col","justify-center"],["formControlName","enabled"],[1,"flex-full","flex"],["formControlName","configType",3,"selectionChange"],["matInput","","type","text","formControlName","name",3,"blur"],[1,"action-buttons","flex"],["mat-icon-button","","matTooltipPosition","above",3,"click","disabled","matTooltip"],["mat-raised-button","","color","primary","type","button",3,"click","disabled","matTooltip"],["mat-raised-button","","color","primary","type","submit",3,"disabled","matTooltip"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"form",1,0),t.ɵɵlistener("ngSubmit",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.save())})),t.ɵɵelementStart(2,"mat-accordion",2)(3,"mat-expansion-panel")(4,"mat-expansion-panel-header")(5,"mat-panel-title")(6,"div",3),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵpipe(9,"uppercase"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"tb-entity-gateway-select",4),t.ɵɵlistener("gatewayNameExist",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.gatewayExist())})),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",5)(12,"mat-label"),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-select",6),t.ɵɵtemplate(16,PW,3,4,"mat-option",7),t.ɵɵpipe(17,"keyvalue"),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",5)(19,"mat-form-field",8)(20,"mat-label"),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(23,"input",9),t.ɵɵtemplate(24,DW,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",8)(26,"mat-label"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(29,"input",11),t.ɵɵtemplate(30,OW,2,0,"mat-error",10)(31,AW,2,0,"mat-error",10)(32,FW,2,0,"mat-error",10)(33,RW,2,0,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵtemplate(34,BW,16,9,"div",12),t.ɵɵelementStart(35,"mat-checkbox",13),t.ɵɵtext(36),t.ɵɵpipe(37,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"div",5)(39,"mat-form-field",8)(40,"mat-label"),t.ɵɵtext(41),t.ɵɵpipe(42,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(43,"mat-select",14),t.ɵɵtemplate(44,NW,2,2,"mat-option",7),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"mat-form-field",8)(46,"mat-label"),t.ɵɵtext(47),t.ɵɵpipe(48,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(49,"input",15),t.ɵɵtemplate(50,LW,2,0,"mat-error",10),t.ɵɵelementEnd()()(),t.ɵɵelementStart(51,"mat-expansion-panel")(52,"mat-expansion-panel-header")(53,"mat-panel-title")(54,"div",3),t.ɵɵtext(55),t.ɵɵpipe(56,"translate"),t.ɵɵpipe(57,"uppercase"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(58,"div",16)(59,"mat-form-field")(60,"mat-label"),t.ɵɵtext(61),t.ɵɵpipe(62,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(63,"mat-select",17),t.ɵɵtemplate(64,VW,3,4,"mat-option",7),t.ɵɵpipe(65,"keyvalue"),t.ɵɵelementEnd()(),t.ɵɵelementStart(66,"div",5)(67,"mat-form-field",8)(68,"mat-label"),t.ɵɵtext(69),t.ɵɵpipe(70,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(71,"input",18),t.ɵɵtemplate(72,qW,2,0,"mat-error",10)(73,GW,2,0,"mat-error",10)(74,zW,2,0,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(75,"mat-form-field",8)(76,"mat-label"),t.ɵɵtext(77),t.ɵɵpipe(78,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(79,"input",19),t.ɵɵtemplate(80,UW,2,0,"mat-error",10)(81,jW,2,0,"mat-error",10)(82,HW,2,0,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵtemplate(83,XW,15,16,"div",20),t.ɵɵelementEnd()(),t.ɵɵelementStart(84,"mat-expansion-panel")(85,"mat-expansion-panel-header")(86,"mat-panel-title")(87,"div",3),t.ɵɵtext(88),t.ɵɵpipe(89,"translate"),t.ɵɵpipe(90,"uppercase"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(91,"div",21),t.ɵɵtemplate(92,e$,27,32,"section",22),t.ɵɵelementStart(93,"span",23),t.ɵɵtext(94),t.ɵɵpipe(95,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(96,"div")(97,"button",24),t.ɵɵpipe(98,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addNewConnector())})),t.ɵɵtext(99),t.ɵɵpipe(100,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(101,"section",25),t.ɵɵtemplate(102,t$,4,7,"button",26)(103,n$,4,7,"button",27),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("toastTarget",n.toastTargetId),t.ɵɵproperty("formGroup",n.gatewayConfigurationGroup),t.ɵɵadvance(7),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(9,54,t.ɵɵpipeBind1(8,52,"gateway.thingsboard"))),t.ɵɵadvance(3),t.ɵɵproperty("deviceName",n.deviceNameForm)("isStateForm",n.isStateForm)("newGatewayType",n.gatewayType),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,56,"gateway.security-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(17,58,n.securityTypes)),t.ɵɵadvance(2),t.ɵɵclassMap(t.ɵɵpureFunction3(92,MW,n.layoutGap,n.alignment,!n.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(22,60,"gateway.thingsboard-host")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("host").hasError("required")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(28,62,"gateway.thingsboard-port")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("max")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("port").hasError("pattern")),t.ɵɵadvance(),t.ɵɵproperty("ngIf","tls"===n.gatewayConfigurationGroup.get("securityType").value),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(37,64,"gateway.remote")),t.ɵɵadvance(2),t.ɵɵclassMap(t.ɵɵpureFunction3(96,MW,n.layoutGap,n.alignment,!n.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(42,66,"gateway.remote-logging-level")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.gatewayLogLevels),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(48,68,"gateway.path-logs")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("remoteLoggingPathToLogs").hasError("required")),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(57,72,t.ɵɵpipeBind1(56,70,"gateway.storage"))),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(62,74,"gateway.storage-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(65,76,n.storageTypes)),t.ɵɵadvance(2),t.ɵɵclassMap(t.ɵɵpureFunction3(100,MW,n.layoutGap,n.alignment,!n.alignment)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(70,78,"gateway.storage-pack-size")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("readRecordsCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("readRecordsCount").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("readRecordsCount").hasError("pattern")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(78,80,"file"!==n.gatewayConfigurationGroup.get("storageType").value?"gateway.storage-max-records":"gateway.storage-max-file-records")," "),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("maxRecordsCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("maxRecordsCount").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("maxRecordsCount").hasError("pattern")),t.ɵɵadvance(),t.ɵɵproperty("ngIf","file"===n.gatewayConfigurationGroup.get("storageType").value),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(90,84,t.ɵɵpipeBind1(89,82,"gateway.connectors-config"))),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",n.connectors.controls),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.connectors.length),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(95,86,"gateway.no-connectors")),t.ɵɵadvance(3),t.ɵɵclassProp("!hidden",n.isReadOnlyForm),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(98,88,"gateway.connector-add")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(100,90,"action.add")," "),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.isReadOnlyForm),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.gatewayConfigurationGroup.get("remoteConfiguration").value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.gatewayConfigurationGroup.get("remoteConfiguration").value))},dependencies:t.ɵɵgetComponentDepsFactory(i$,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .gateway-form[_ngcontent-%COMP%]{height:100%;padding:5px;background-color:transparent;overflow-y:auto;overflow-x:hidden}[_nghost-%COMP%] .gateway-form[_ngcontent-%COMP%] .form-action-buttons[_ngcontent-%COMP%]{padding-top:8px}[_nghost-%COMP%] .gateway-form[_ngcontent-%COMP%] .gateway-config[_ngcontent-%COMP%] .no-data-found[_ngcontent-%COMP%]{position:relative;display:flex;height:40px}']})}}e("GatewayFormComponent",i$);class a${transform(e,t){return za.parseVersion(e)>=za.parseVersion(Ei.get(t))}static{this.ɵfac=function(e){return new(e||a$)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"isLatestVersionConfig",type:a$,pure:!0,standalone:!0})}}class r${constructor(e){this.translate=e}transform(e){return e.hasError("required")?this.translate.instant("gateway.port-required"):e.hasError("min")||e.hasError("max")?this.translate.instant("gateway.port-limits-error",{min:Ii.MIN,max:Ii.MAX}):""}static{this.ɵfac=function(e){return new(e||r$)(t.ɵɵdirectiveInject(He.TranslateService,16))}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"getGatewayPortTooltip",type:r$,pure:!0,standalone:!0})}}class o${transform(e,t,n,i){switch(e){case ct.OPCUA:return this.getOpcConnectorHelpLink(t,n);case ct.MQTT:return this.getMqttConnectorHelpLink(t,n,i);case ct.BACNET:return this.getBacnetConnectorHelpLink(t,n);case ct.REST:return this.getRestConnectorHelpLink(n)}}getOpcConnectorHelpLink(e,t){if(t!==Mi.CONST)return`widget/lib/gateway/${e}-${t}_fn`}getMqttConnectorHelpLink(e,t,n){if(t!==ei.CONST)return n?e!==Ni.ATTRIBUTES&&e!==Ni.TIMESERIES||n!==Jn.JSON?`widget/lib/gateway/mqtt-${n}-expression_fn`:"widget/lib/gateway/mqtt-json-key-expression_fn":"widget/lib/gateway/mqtt-expression_fn"}getBacnetConnectorHelpLink(e,t){if(t!==Mi.CONST)return`widget/lib/gateway/bacnet-device-${e}-${t}_fn`}getRestConnectorHelpLink(e){if(e!==fi.CONST)return"widget/lib/gateway/rest-json_fn"}static{this.ɵfac=function(e){return new(e||o$)}}static{this.ɵpipe=t.ɵɵdefinePipe({name:"getConnectorMappingHelpLink",type:o$,pure:!0,standalone:!0})}}function s$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.gatewayConnectorDefaultTypesTranslatesMap.get(e)," ")}}function l$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",22),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.connectorForm.get("name").hasError("duplicateName")?"gateway.connector-duplicate-name":"gateway.name-required"))}}function p$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"div",9),t.ɵɵtext(2,"gateway.connectors-table-class"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",10),t.ɵɵelement(4,"input",23),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,1,"gateway.set")))}function c$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"div",9),t.ɵɵtext(2,"gateway.connectors-table-key"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",10),t.ɵɵelement(4,"input",24),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,1,"gateway.set")))}function d$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function u$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"mat-slide-toggle",25)(2,"mat-label",26),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.fill-connector-defaults-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.fill-connector-defaults")," "))}function m$(e,n){1&e&&(t.ɵɵelementStart(0,"div",8)(1,"mat-slide-toggle",27)(2,"mat-label",26),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.send-change-data-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.send-change-data")," "))}class h$ extends A{constructor(e,t,n,i,a,r){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.isLatestVersionConfig=r,this.connectorType=ct,this.gatewayConnectorDefaultTypesTranslatesMap=dt,this.gatewayLogLevel=Object.values(lt),this.submitted=!1,this.destroy$=new te,this.updateConnectorTypesByVersion(),this.connectorForm=this.fb.group({type:[ct.MQTT,[]],name:["",[$.required,this.uniqNameRequired(),$.pattern(an)]],logLevel:[lt.INFO,[]],useDefaults:[!0,[]],sendDataOnlyOnChange:[!1,[]],class:["",[]],key:["auto",[]]})}ngOnInit(){this.observeTypeChange()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}helpLinkId(){return O+"/docs/iot-gateway/configuration/"}cancel(){this.dialogRef.close(null)}add(){this.submitted=!0;const e=this.connectorForm.getRawValue();if(e.useDefaults){const t=Ht(e.type),n=this.data.gatewayVersion;n&&(e.configVersion=n),e.configurationJson=(this.isLatestVersionConfig.transform(n,e.type)?t[Ei.get(e.type)]:t[pt.Legacy])??t,this.connectorForm.valid&&this.dialogRef.close(e)}else this.connectorForm.valid&&this.dialogRef.close(e)}uniqNameRequired(){return e=>{const t=e.value.trim().toLowerCase();return this.data.dataSourceData.some((({value:{name:e}})=>e.toLowerCase()===t))?{duplicateName:{valid:!1}}:null}}updateConnectorTypesByVersion(){const e=ut.keys();for(let t of e)if(t===pt.Legacy||za.parseVersion(this.data.gatewayVersion)>=za.parseVersion(t)){this.connectorTypes=ut.get(t);break}}observeTypeChange(){this.connectorForm.get("type").valueChanges.pipe(me((e=>{const t=this.connectorForm.get("useDefaults");e===ct.GRPC||e===ct.CUSTOM?t.setValue(!1):t.value||t.setValue(!0)})),le(this.destroy$)).subscribe()}static{this.ɵfac=function(e){return new(e||h$)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(a$))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:h$,selectors:[["tb-add-connector-dialog"]],standalone:!0,features:[t.ɵɵProvidersFeature([a$]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:43,vars:25,consts:[[1,"add-connector",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","autocomplete","off","name","value","formControlName","name",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf"],["formControlName","logLevel"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","name","value","formControlName","class",3,"placeholder"],["matInput","","name","value","formControlName","key",3,"placeholder"],["formControlName","useDefaults",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"mat-toolbar",1)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",2)(6,"div",3),t.ɵɵelementStart(7,"button",4),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",5),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",6)(11,"div",7)(12,"div",8)(13,"div",9),t.ɵɵtext(14,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-form-field",10)(16,"mat-select",11),t.ɵɵtemplate(17,s$,2,2,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(18,"div",8)(19,"div",13),t.ɵɵtext(20,"gateway.name"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",10),t.ɵɵelement(22,"input",14),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,l$,3,3,"mat-icon",15),t.ɵɵelementEnd()(),t.ɵɵtemplate(25,p$,6,3,"div",16)(26,c$,6,3,"div",16),t.ɵɵelementStart(27,"div",8)(28,"div",9),t.ɵɵtext(29,"gateway.remote-logging-level"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",10)(31,"mat-select",17),t.ɵɵtemplate(32,d$,2,2,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵtemplate(33,u$,6,6,"div",16)(34,m$,6,6,"div",16),t.ɵɵpipe(35,"withReportStrategy"),t.ɵɵelementEnd()(),t.ɵɵelementStart(36,"div",18)(37,"button",19),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(38),t.ɵɵpipe(39,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(40,"button",20),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(41),t.ɵɵpipe(42,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.connectorForm),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,14,"gateway.add-connector")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.helpLinkId()),t.ɵɵadvance(11),t.ɵɵproperty("ngForOf",n.connectorTypes),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,16,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.connectorForm.get("name").hasError("required")&&n.connectorForm.get("name").touched||n.connectorForm.get("name").hasError("duplicateName")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value===n.connectorType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value===n.connectorType.GRPC),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.gatewayLogLevel),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value!==n.connectorType.GRPC&&n.connectorForm.get("type").value!==n.connectorType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.connectorForm.get("type").value===n.connectorType.MQTT&&!t.ɵɵpipeBind2(35,18,n.data.gatewayVersion,n.connectorType.MQTT)),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(39,21,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.connectorForm.invalid||!n.connectorForm.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(42,23,"action.add")," "))},dependencies:t.ɵɵgetComponentDepsFactory(h$,[j,_,Ka]),styles:['@charset "UTF-8";[_nghost-%COMP%] .add-connector[_ngcontent-%COMP%]{min-width:400px;width:500px}']})}}e("AddConnectorDialogComponent",h$);const g$=()=>({maxWidth:"970px"});function f$(e,n){1&e&&(t.ɵɵelementStart(0,"div",4),t.ɵɵtext(1,"gateway.device-info.source"),t.ɵɵelementEnd())}function y$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function v$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"mat-form-field",18)(2,"mat-select",19),t.ɵɵtemplate(3,y$,3,4,"mat-option",20),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.sourceTypes)}}function x$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",22),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-info.device-name-expression-required"))}function b$(e,n){if(1&e&&(t.ɵɵelement(0,"div",23),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,e.connectorType,"name-field",e.mappingFormGroup.get("deviceNameExpressionSource").value,e.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,g$))}}function w$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function S$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"mat-form-field",18)(2,"mat-select",26),t.ɵɵtemplate(3,w$,3,4,"mat-option",20),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",e.sourceTypes)}}function C$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",22),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-info.device-profile-expression-required"))}function _$(e,n){if(1&e&&(t.ɵɵelement(0,"div",23),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,e.connectorType,"profile-name",e.mappingFormGroup.get("deviceProfileExpressionSource").value,e.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,g$))}}function T$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",24)(1,"div",9),t.ɵɵtext(2,"gateway.device-info.profile-name"),t.ɵɵelementEnd(),t.ɵɵtemplate(3,S$,4,1,"div",10),t.ɵɵelementStart(4,"div",11)(5,"mat-form-field",12),t.ɵɵelement(6,"input",25),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,C$,3,3,"mat-icon",14)(9,_$,2,8,"div",15),t.ɵɵpipe(10,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵproperty("ngIf",e.useSource),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,4,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingFormGroup.get("deviceProfileExpression").hasError("required")&&e.mappingFormGroup.get("deviceProfileExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(10,6,e.connectorType,"profile-name",e.mappingFormGroup.get("deviceProfileExpressionSource").value,e.convertorType))}}class I$ extends q{get deviceInfoType(){return this.deviceInfoTypeValue}set deviceInfoType(e){this.deviceInfoTypeValue!==e&&(this.deviceInfoTypeValue=e)}constructor(e,t,n,i){super(e),this.store=e,this.translate=t,this.dialog=n,this.fb=i,this.SourceTypeTranslationsMap=Bi,this.DeviceInfoType=ga,this.useSource=!0,this.required=!1,this.connectorType=ct.MQTT,this.sourceTypes=Object.values(ei),this.destroy$=new te,this.propagateChange=e=>{}}ngOnInit(){this.mappingFormGroup=this.fb.group({deviceNameExpression:["",this.required?[$.required,$.pattern(an)]:[$.pattern(an)]]}),this.useSource&&this.mappingFormGroup.addControl("deviceNameExpressionSource",this.fb.control(this.sourceTypes[0],[])),this.deviceInfoType===ga.FULL&&(this.useSource&&this.mappingFormGroup.addControl("deviceProfileExpressionSource",this.fb.control(this.sourceTypes[0],[])),this.mappingFormGroup.addControl("deviceProfileExpression",this.fb.control("",this.required?[$.required,$.pattern(an)]:[$.pattern(an)]))),this.mappingFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.updateView(e)}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}writeValue(e){this.mappingFormGroup.patchValue(e,{emitEvent:!1})}validate(){return this.mappingFormGroup.valid?null:{mappingForm:{valid:!1}}}updateView(e){this.propagateChange(e)}static{this.ɵfac=function(e){return new(e||I$)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(He.TranslateService),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:I$,selectors:[["tb-device-info-table"]],inputs:{useSource:"useSource",required:"required",connectorType:"connectorType",convertorType:"convertorType",sourceTypes:"sourceTypes",deviceInfoType:"deviceInfoType"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>I$)),multi:!0},{provide:K,useExisting:c((()=>I$)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:23,vars:18,consts:[[1,"tb-form-panel","stroked",3,"formGroup"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-table","no-padding","no-gap"],[1,"tb-form-table-header"],["translate","",1,"tb-form-table-header-cell","w-1/5"],["class","tb-form-table-header-cell w-1/5","translate","",4,"ngIf"],["translate","",1,"tb-form-table-header-cell","w-1/2"],[1,"tb-form-table-body","no-gap"],[1,"tb-form-table-row","tb-form-row","no-border","same-padding","pt-4"],["translate","",1,"tb-required","w-1/5"],["class","no-gap w-1/5",4,"ngIf"],[1,"tb-form-table-row-cell","tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","deviceNameExpression",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style",4,"ngIf"],["class","tb-form-table-row tb-form-row no-border same-padding pb-4",4,"ngIf"],[1,"no-gap","w-1/5"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","!w-full"],["formControlName","deviceNameExpressionSource"],[3,"value",4,"ngFor","ngForOf"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"tb-form-table-row","tb-form-row","no-border","same-padding","pb-4"],["matInput","","name","value","formControlName","deviceProfileExpression",3,"placeholder"],["formControlName","deviceProfileExpressionSource"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2,"device.device"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",2)(4,"div",3)(5,"div",4),t.ɵɵtext(6,"gateway.device-info.entity-field"),t.ɵɵelementEnd(),t.ɵɵtemplate(7,f$,2,0,"div",5),t.ɵɵelementStart(8,"div",6),t.ɵɵtext(9," gateway.device-info.expression "),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",7)(11,"div",8)(12,"div",9),t.ɵɵtext(13,"gateway.device-info.name"),t.ɵɵelementEnd(),t.ɵɵtemplate(14,v$,4,1,"div",10),t.ɵɵelementStart(15,"div",11)(16,"mat-form-field",12),t.ɵɵelement(17,"input",13),t.ɵɵpipe(18,"translate"),t.ɵɵtemplate(19,x$,3,3,"mat-icon",14)(20,b$,2,8,"div",15),t.ɵɵpipe(21,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(22,T$,11,11,"div",16),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingFormGroup),t.ɵɵadvance(),t.ɵɵclassProp("tb-required",n.required),t.ɵɵadvance(6),t.ɵɵproperty("ngIf",n.useSource),t.ɵɵadvance(4),t.ɵɵclassProp("pb-4",n.deviceInfoType!==n.DeviceInfoType.FULL),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.useSource),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(18,11,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.mappingFormGroup.get("deviceNameExpression").hasError("required")&&n.mappingFormGroup.get("deviceNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(21,13,n.connectorType,"name-field",n.mappingFormGroup.get("deviceNameExpressionSource").value,n.convertorType)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceInfoType===n.DeviceInfoType.FULL))},dependencies:t.ɵɵgetComponentDepsFactory(I$,[j,_,o$]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}'],changeDetection:d.OnPush})}}Ge([I()],I$.prototype,"useSource",void 0),Ge([I()],I$.prototype,"required",void 0);const E$=()=>({maxWidth:"970px"});function M$(e,n){if(1&e&&t.ɵɵelement(0,"mat-icon",19),2&e){let e;const n=t.ɵɵnextContext();t.ɵɵproperty("svgIcon",null==(e=n.valueTypes.get(n.valueTypeFormGroup.get("type").value))?null:e.icon)}}function k$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵelement(1,"mat-icon",22),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){let e,i;const a=n.$implicit,r=t.ɵɵnextContext(2);t.ɵɵproperty("value",a),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("svgIcon",null==(e=r.valueTypes.get(a))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,null==(i=r.valueTypes.get(a))?null:i.name))}}function P$(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,k$,5,5,"mat-option",20),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.valueTypeKeys)}}function D$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-option",23)(1,"span"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.raw")))}function O$(e,n){1&e&&(t.ɵɵelement(0,"input",24),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function A$(e,n){1&e&&(t.ɵɵelement(0,"input",25),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function F$(e,n){1&e&&(t.ɵɵelement(0,"input",26),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function R$(e,n){1&e&&(t.ɵɵelement(0,"input",27),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(1,1,"gateway.set"))}function B$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-select",28)(1,"mat-option",21),t.ɵɵtext(2,"true"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-option",21),t.ɵɵtext(4,"false"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵproperty("value",!0),t.ɵɵadvance(2),t.ɵɵproperty("value",!1))}function N$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",29),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function L$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",30),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("tb-help-popup",e.helpLink)("tb-help-popup-style",t.ɵɵpureFunction0(2,E$))}}class V${constructor(e){this.fb=e,this.valueTypeKeys=Object.values(Yt),this.valueTypes=Xt,this.MappingValueType=Yt,this.destroy$=new te,this.onChange=e=>{},this.valueTypeFormGroup=this.fb.group({type:[Yt.STRING],stringValue:[{value:"",disabled:this.rawData},[$.required,$.pattern(an)]],integerValue:[{value:0,disabled:!0},[$.required,$.pattern(rn)]],doubleValue:[{value:0,disabled:!0},[$.required]],booleanValue:[{value:!1,disabled:!0},[$.required]],rawValue:[{value:"",disabled:!this.rawData},[$.required,$.pattern(an)]]}),this.valueTypeFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((({type:e,...t})=>{this.onChange({type:e,value:t[e+"Value"]})})),this.observeTypeChange()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}observeTypeChange(){this.valueTypeFormGroup.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.toggleTypeInputs(e)))}toggleTypeInputs(e){this.valueTypeFormGroup.disable({emitEvent:!1}),this.valueTypeFormGroup.get("type").enable({emitEvent:!1}),this.valueTypeFormGroup.get(e+"Value").enable({emitEvent:!1})}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){const t=this.getValueType(e?.value),n={stringValue:"",rawValue:"",integerValue:0,doubleValue:0,booleanValue:!1,type:t};n[t+"Value"]=e?.value,this.toggleTypeInputs(t),this.valueTypeFormGroup.patchValue(n,{emitEvent:!1})}validate(){return this.valueTypeFormGroup.valid?null:{valueTypeFormGroup:{valid:!1}}}getValueType(e){if(this.rawData)return"raw";switch(typeof e){case"boolean":return Yt.BOOLEAN;case"number":return Number.isInteger(e)?Yt.INTEGER:Yt.DOUBLE;default:return Yt.STRING}}static{this.ɵfac=function(e){return new(e||V$)(t.ɵɵdirectiveInject(H.UntypedFormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:V$,selectors:[["tb-type-value-field"]],inputs:{rawData:"rawData",helpLink:"helpLink"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>V$)),multi:!0},{provide:K,useExisting:c((()=>V$)),multi:!0}]),t.ɵɵStandaloneFeature],decls:29,vars:17,consts:[["raw",""],[3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","type"],[1,"tb-flex","align-center"],["class","tb-mat-18",3,"svgIcon",4,"ngIf"],[4,"ngIf","ngIfElse"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","flex","tb-suffix-absolute"],[3,"ngSwitch"],["matInput","","required","","formControlName","stringValue",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder",4,"ngSwitchCase"],["matInput","","required","","formControlName","rawValue",3,"placeholder",4,"ngSwitchCase"],["formControlName","booleanValue",4,"ngSwitchCase"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style","click",4,"ngIf"],[1,"tb-mat-18",3,"svgIcon"],[3,"value",4,"ngFor","ngForOf"],[3,"value"],[1,"tb-mat-20",3,"svgIcon"],["value","raw"],["matInput","","required","","formControlName","stringValue",3,"placeholder"],["matInput","","required","","formControlName","integerValue","type","number",3,"placeholder"],["matInput","","required","","formControlName","doubleValue","type","number",3,"placeholder"],["matInput","","required","","formControlName","rawValue",3,"placeholder"],["formControlName","booleanValue"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"click","tb-help-popup","tb-help-popup-style"]],template:function(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,1),t.ɵɵelementStart(1,"div",2)(2,"div",3),t.ɵɵtext(3,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",4)(5,"mat-form-field",5)(6,"mat-select",6)(7,"mat-select-trigger")(8,"div",7),t.ɵɵtemplate(9,M$,1,1,"mat-icon",8),t.ɵɵelementStart(10,"span"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(14,P$,2,1,"ng-container",9)(15,D$,4,3,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(17,"div",2)(18,"div",3),t.ɵɵtext(19,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(20,"mat-form-field",10),t.ɵɵelementContainerStart(21,11),t.ɵɵtemplate(22,O$,2,3,"input",12)(23,A$,2,3,"input",13)(24,F$,2,3,"input",14)(25,R$,2,3,"input",15)(26,B$,5,2,"mat-select",16),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(27,N$,3,3,"mat-icon",17)(28,L$,1,3,"div",18),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e){let e,i;const a=t.ɵɵreference(16);t.ɵɵproperty("formGroup",n.valueTypeFormGroup),t.ɵɵadvance(9),t.ɵɵproperty("ngIf",!n.rawData),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,13,null==(e=n.valueTypes.get(n.valueTypeFormGroup.get("type").value))?null:e.name)||t.ɵɵpipeBind1(13,15,"gateway.raw")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",!n.rawData)("ngIfElse",a),t.ɵɵadvance(7),t.ɵɵproperty("ngSwitch",n.valueTypeFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.STRING),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.INTEGER),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.DOUBLE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase","raw"),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingValueType.BOOLEAN),t.ɵɵadvance(),t.ɵɵproperty("ngIf",(null==(i=n.valueTypeFormGroup.get(n.valueTypeFormGroup.get("type").value))?null:i.hasError("required"))&&n.valueTypeFormGroup.get(n.valueTypeFormGroup.get("type").value).touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.helpLink)}},dependencies:t.ɵɵgetComponentDepsFactory(V$,[_,j]),styles:['@charset "UTF-8";[_nghost-%COMP%]{gap:16px;display:grid;width:100%}']})}}function q$(e,n){if(1&e&&t.ɵɵelement(0,"tb-type-value-field",14),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("helpLink",e.helpLink)}}function G$(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",6)(1,"div",7),t.ɵɵelementContainerStart(2,8),t.ɵɵelementStart(3,"mat-expansion-panel",9)(4,"mat-expansion-panel-header",10)(5,"mat-panel-title")(6,"div",11),t.ɵɵtext(7),t.ɵɵelementEnd()()(),t.ɵɵtemplate(8,q$,1,1,"ng-template",12),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"button",13),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"delete"),t.ɵɵelementEnd()()()}if(2&e){let e;const i=n.$implicit,a=n.last;t.ɵɵadvance(2),t.ɵɵproperty("formGroup",i),t.ɵɵadvance(),t.ɵɵproperty("expanded",a),t.ɵɵadvance(4),t.ɵɵtextInterpolate(null!==(e=null==(e=i.get("typeValue").value)?null:e.value)&&void 0!==e?e:""),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(10,4,"gateway.delete-argument"))}}function z$(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4),t.ɵɵtemplate(1,G$,13,6,"div",5),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.valueListFormArray.controls)("ngForTrackBy",e.trackByKey)}}function U$(e,n){1&e&&(t.ɵɵelementStart(0,"div",15)(1,"span",16),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate("gateway.no-value"))}Ge([I()],V$.prototype,"rawData",void 0);class j${constructor(e){this.fb=e,this.destroy$=new te,this.onChange=e=>{}}ngOnInit(){this.valueListFormArray=this.fb.array([]),this.valueListFormArray.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e.map((({typeValue:e})=>({...e}))))}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}trackByKey(e,t){return t}addKey(){const e=this.fb.group({typeValue:[]});this.valueListFormArray.push(e)}deleteKey(e,t){e&&e.stopPropagation(),this.valueListFormArray.removeAt(t),this.valueListFormArray.markAsDirty()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){for(const t of e){const e={typeValue:[t]},n=this.fb.group(e);this.valueListFormArray.push(n)}}validate(){return this.valueListFormArray.valid?null:{valueListForm:{valid:!1}}}static{this.ɵfac=function(e){return new(e||j$)(t.ɵɵdirectiveInject(H.UntypedFormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:j$,selectors:[["tb-type-value-panel"]],inputs:{helpLink:"helpLink"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>j$)),multi:!0},{provide:K,useExisting:c((()=>j$)),multi:!0}]),t.ɵɵStandaloneFeature],decls:8,vars:5,consts:[["noKeys",""],[1,"tb-form-panel","no-border","no-padding"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],["tbTruncateWithTooltip","",1,"title-container"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],["formControlName","typeValue",3,"helpLink"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",1),t.ɵɵtemplate(1,z$,2,2,"div",2),t.ɵɵelementStart(2,"div")(3,"button",3),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(6,U$,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor)}if(2&e){const e=t.ɵɵreference(7);t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.valueListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,3,"gateway.add-value")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(j$,[j,_,V$]),styles:['@charset "UTF-8";[_nghost-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw}[_nghost-%COMP%] .key-panel[_ngcontent-%COMP%]{height:250px;overflow:auto}[_nghost-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}']})}}const H$=()=>({maxWidth:"970px"});function W$(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",19),t.ɵɵtext(2),t.ɵɵelementEnd(),t.ɵɵtext(3),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",e.get("key").value," "),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",":","  ")}}function $$(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function K$(e,n){if(1&e&&(t.ɵɵelement(0,"div",41),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext(3).$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,n.connectorType,n.keysType,e.get("type").value,n.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,H$))}}function Y$(e,n){if(1&e&&t.ɵɵelement(0,"mat-icon",44),2&e){let e;const n=t.ɵɵnextContext(4).$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("svgIcon",null==(e=i.valueTypes.get(n.get("type").value))?null:e.icon)}}function X$(e,n){if(1&e&&(t.ɵɵelementStart(0,"span"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){let e;const n=t.ɵɵnextContext(4).$implicit,i=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,null!==(e=null==(e=i.valueTypes.get(n.get("type").value))?null:e.name)&&void 0!==e?e:i.valueTypes.get(n.get("type").value))," ")}}function Z$(e,n){1&e&&(t.ɵɵelementStart(0,"span"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.raw")))}function Q$(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-select-trigger")(1,"div",42),t.ɵɵtemplate(2,Y$,1,1,"mat-icon",43)(3,X$,3,3,"span",36)(4,Z$,3,3,"ng-template",null,2,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()),2&e){let e;const n=t.ɵɵreference(5),i=t.ɵɵnextContext(3).$implicit,a=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("ngIf",null==(e=a.valueTypes.get(i.get("type").value))?null:e.icon),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!a.rawData)("ngIfElse",n)}}function J$(e,n){if(1&e&&t.ɵɵelement(0,"mat-icon",48),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(6);t.ɵɵpropertyInterpolate("svgIcon",n.valueTypes.get(e).icon)}}function eK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",46),t.ɵɵtemplate(1,J$,1,1,"mat-icon",47),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){let e,i;const a=n.$implicit,r=t.ɵɵnextContext(6);t.ɵɵproperty("value",a),t.ɵɵadvance(),t.ɵɵproperty("ngIf",null==(e=r.valueTypes.get(a))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,3,null!==(i=null==(i=r.valueTypes.get(a))?null:i.name)&&void 0!==i?i:r.valueTypes.get(a))," ")}}function tK(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,eK,5,5,"mat-option",45),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(5);t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.valueTypeKeys)}}function nK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-option",46)(1,"span"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("value","raw"),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,2,"gateway.raw")))}function iK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function aK(e,n){if(1&e&&(t.ɵɵelement(0,"div",41),t.ɵɵpipe(1,"getConnectorMappingHelpLink")),2&e){const e=t.ɵɵnextContext(3).$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind4(1,2,n.connectorType,n.keysType,e.get("type").value,n.convertorType))("tb-help-popup-style",t.ɵɵpureFunction0(7,H$))}}function rK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"div",24)(2,"div",25),t.ɵɵtext(3,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",26)(5,"div",27),t.ɵɵpipe(6,"translate"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-form-field",28),t.ɵɵelement(10,"input",29),t.ɵɵpipe(11,"translate"),t.ɵɵtemplate(12,$$,3,3,"mat-icon",30)(13,K$,2,8,"div",31),t.ɵɵpipe(14,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",24)(16,"div",25),t.ɵɵtext(17,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"div",32)(19,"div",33),t.ɵɵtext(20,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",34)(22,"mat-select",35),t.ɵɵtemplate(23,Q$,6,3,"mat-select-trigger",18)(24,tK,2,1,"ng-container",36)(25,nK,4,4,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()()(),t.ɵɵelementStart(27,"div",37)(28,"div",27),t.ɵɵpipe(29,"translate"),t.ɵɵtext(30),t.ɵɵpipe(31,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(32,"mat-form-field",38),t.ɵɵelement(33,"input",39),t.ɵɵpipe(34,"translate"),t.ɵɵtemplate(35,iK,3,3,"mat-icon",30)(36,aK,2,8,"div",31),t.ɵɵpipe(37,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵreference(26),n=t.ɵɵnextContext(2).$implicit,i=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,13,"gateway.JSONPath-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(8,15,"gateway.key")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(11,17,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.get("key").hasError("required")&&n.get("key").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(14,19,i.connectorType,i.keysType,n.get("type").value,i.convertorType)),t.ɵɵadvance(10),t.ɵɵproperty("ngIf",!i.rawData),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!i.rawData)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(29,24,"gateway.JSONPath-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(31,26,"gateway.value")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(34,28,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.get("value").hasError("required")&&n.get("value").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind4(37,30,i.connectorType,i.keysType,n.get("type").value,i.convertorType))}}function oK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function sK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function lK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"div",26)(2,"div",33),t.ɵɵtext(3,"gateway.key"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",28),t.ɵɵelement(5,"input",29),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,oK,3,3,"mat-icon",30),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",26)(9,"div",33),t.ɵɵtext(10,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",49),t.ɵɵelement(12,"input",39),t.ɵɵpipe(13,"translate"),t.ɵɵtemplate(14,sK,3,3,"mat-icon",30),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,4,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("key").hasError("required")&&e.get("key").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(13,6,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("value").hasError("required")&&e.get("value").touched)}}function pK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-required"))}function cK(e,n){1&e&&t.ɵɵelement(0,"tb-type-value-panel",54)}function dK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"div",26)(2,"div",27),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",28),t.ɵɵelement(7,"input",50),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,pK,3,3,"mat-icon",30),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",14)(11,"mat-expansion-panel",51)(12,"mat-expansion-panel-header",52)(13,"mat-panel-title")(14,"div",53),t.ɵɵpipe(15,"translate"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(18,cK,1,0,"ng-template",20),t.ɵɵelementEnd()()()),2&e){let e;const n=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,7,"gateway.hints.method-name")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,9,"gateway.method-name")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,11,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.get("method").hasError("required")&&n.get("method").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(15,13,"gateway.hints.arguments")),t.ɵɵadvance(2),t.ɵɵtextInterpolate2(" ",t.ɵɵpipeBind1(17,15,"gateway.arguments"),""," ("+(null==(e=n.get("arguments").value)?null:e.length)+")"," ")}}function uK(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",55),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Key)}}function mK(e,n){if(1&e&&t.ɵɵtemplate(0,rK,38,35,"div",22)(1,lK,15,8,"div",22)(2,dK,19,17,"div",22)(3,uK,1,2,"tb-report-strategy",23),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("ngIf",e.keysType!==e.MappingKeysType.CUSTOM&&e.keysType!==e.MappingKeysType.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.MappingKeysType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.MappingKeysType.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.withReportStrategy&&(e.keysType===e.MappingKeysType.ATTRIBUTES||e.keysType===e.MappingKeysType.TIMESERIES))}}function hK(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",13)(1,"div",14),t.ɵɵelementContainerStart(2,15),t.ɵɵelementStart(3,"mat-expansion-panel",16)(4,"mat-expansion-panel-header",17)(5,"mat-panel-title"),t.ɵɵtemplate(6,W$,4,2,"ng-container",18),t.ɵɵelementStart(7,"div",19),t.ɵɵtext(8),t.ɵɵelementEnd()()(),t.ɵɵtemplate(9,mK,4,4,"ng-template",20),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",21),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.last,a=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",a.keysType!==a.MappingKeysType.RPC_METHODS),t.ɵɵadvance(2),t.ɵɵtextInterpolate(a.valueTitle(e)),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,a.deleteKeyTitle))}}function gK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",11),t.ɵɵtemplate(1,hK,14,7,"div",12),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.keysListFormArray.controls)("ngForTrackBy",e.trackByKey)}}function fK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",56)(1,"span",57),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class yK extends q{constructor(e,t,n){super(n),this.fb=e,this.popover=t,this.store=n,this.valueTypeEnum=Yt,this.valueTypes=Xt,this.valueTypeKeys=Object.values(Yt),this.rawData=!1,this.withReportStrategy=!0,this.keysDataApplied=new u,this.MappingKeysType=Ni,this.ReportStrategyDefaultValue=en,this.ConnectorType=ct,this.errorText=""}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}trackByKey(e,t){return t}addKey(){let e;e=this.keysType===Ni.RPC_METHODS?this.fb.group({method:["",[$.required]],arguments:[[],[]]}):this.keysType===Ni.CUSTOM?this.fb.group({key:["",[$.required,$.pattern(an)]],value:["",[$.required,$.pattern(an)]]}):this.fb.group({key:["",[$.required,$.pattern(an)]],type:[this.rawData?"raw":this.valueTypeKeys[0]],value:["",[$.required,$.pattern(an)]],reportStrategy:[{value:null,disabled:this.isReportStrategyDisabled()}]}),this.keysListFormArray.push(e)}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){let e=this.keysListFormArray.value.map((({reportStrategy:e,...t})=>({...t,...e&&{reportStrategy:e}})));if(this.keysType===Ni.CUSTOM){e={};for(let t of this.keysListFormArray.value)e[t.key]=t.value}this.keysDataApplied.emit(e)}prepareKeysFormArray(e){const t=[];return e&&(this.keysType===Ni.CUSTOM&&(e=Object.keys(e).map((t=>({key:t,value:e[t],type:""})))),e.forEach((e=>{let n;if(this.keysType===Ni.RPC_METHODS)n=this.fb.group({method:[e.method,[$.required]],arguments:[[...e.arguments],[]]});else if(this.keysType===Ni.CUSTOM){const{key:t,value:i}=e;n=this.fb.group({key:[t,[$.required,$.pattern(an)]],value:[i,[$.required,$.pattern(an)]]})}else{const{key:t,value:i,type:a,reportStrategy:r}=e;n=this.fb.group({key:[t,[$.required,$.pattern(an)]],type:[a],value:[i,[$.required,$.pattern(an)]],reportStrategy:[{value:r,disabled:this.isReportStrategyDisabled()}]})}t.push(n)}))),this.fb.array(t)}valueTitle(e){const t=this.keysType===Ni.RPC_METHODS?e.get("method").value:e.get("value").value;return ke(t)?"object"==typeof t?JSON.stringify(t):t:""}isReportStrategyDisabled(){return!(this.withReportStrategy&&(this.keysType===Ni.ATTRIBUTES||this.keysType===Ni.TIMESERIES))}static{this.ɵfac=function(e){return new(e||yK)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(it.TbPopoverComponent),t.ɵɵdirectiveInject(Ye.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:yK,selectors:[["tb-mapping-data-keys-panel"]],inputs:{panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keys:"keys",keysType:"keysType",connectorType:"connectorType",convertorType:"convertorType",sourceType:"sourceType",valueTypeEnum:"valueTypeEnum",valueTypes:"valueTypes",valueTypeKeys:"valueTypeKeys",rawData:"rawData",withReportStrategy:"withReportStrategy"},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:16,consts:[["noKeys",""],["rawOption",""],["rawText",""],[1,"tb-mapping-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex","flex-row","flex-wrap"],[4,"ngIf"],["tbTruncateWithTooltip","",1,"title-container"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],["class","tb-form-panel no-border no-padding",4,"ngIf"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue",4,"ngIf"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style",4,"ngIf"],[1,"tb-form-row"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","type"],[4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs","flex","items-center","justify-between"],["appearance","outline","subscriptSizing","dynamic",1,"no-gap","flex","flex-1"],["matInput","","required","","formControlName","value",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"tb-flex","align-center"],["class","tb-mat-18",3,"svgIcon",4,"ngIf"],[1,"tb-mat-18",3,"svgIcon"],[3,"value",4,"ngFor","ngForOf"],[3,"value"],["class","tb-mat-20",3,"svgIcon",4,"ngIf"],[1,"tb-mat-20",3,"svgIcon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","tb-suffix-absolute","flex"],["matInput","","name","value","formControlName","method",3,"placeholder"],[1,"tb-settings"],[1,"flex","flex-wrap"],[1,"title-container",3,"tb-hint-tooltip-icon"],["formControlName","arguments"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"div",5),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,gK,2,2,"div",6),t.ɵɵelementStart(6,"div")(7,"button",7),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(10,fK,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(12,"div",8)(13,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",10),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.applyKeysData())})),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(11);t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,8,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")",""),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.keysListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,10,n.addKeyTitle)," "),t.ɵɵadvance(6),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,12,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,14,"action.apply")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(yK,[j,_,Xn,j$,o$]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:500px;overflow:auto}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] tb-value-input[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] .tb-mapping-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}']})}}Ge([I()],yK.prototype,"rawData",void 0),Ge([I()],yK.prototype,"withReportStrategy",void 0);const vK=()=>({maxWidth:"970px"}),xK=(e,t)=>[e,t];function bK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.topic-required"))}function wK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.QualityTranslationsMap.get(e))," ")}}function SK(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.ConvertorTypeTranslationsMap.get(e))," ")}}function CK(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",41),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("convertorType",e.ConvertorTypeEnum.JSON)("deviceInfoType",e.DeviceInfoType.FULL)}}function _K(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",42),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("deviceInfoType",e.DeviceInfoType.FULL)("convertorType",e.ConvertorTypeEnum.BYTES)("sourceTypes",t.ɵɵpureFunction2(3,xK,e.sourceTypesEnum.MSG,e.sourceTypesEnum.CONST))}}function TK(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",37),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function IK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function EK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function MK(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",14)(1,"div",31)(2,"div",32),t.ɵɵtext(3,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",43)(5,"mat-chip-listbox",44),t.ɵɵtemplate(6,IK,2,1,"mat-chip",45),t.ɵɵelementStart(7,"mat-chip",46),t.ɵɵelement(8,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",48,0),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(10),a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES))})),t.ɵɵelementStart(12,"tb-icon",49),t.ɵɵtext(13,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(14,"div",31)(15,"div",32),t.ɵɵtext(16,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"div",43)(18,"mat-chip-listbox",44),t.ɵɵtemplate(19,EK,2,1,"mat-chip",45),t.ɵɵelementStart(20,"mat-chip",46),t.ɵɵelement(21,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"button",48,1),t.ɵɵpipe(24,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(23),a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.TIMESERIES))})),t.ɵɵelementStart(25,"tb-icon",49),t.ɵɵtext(26,"edit"),t.ɵɵelementEnd()()()()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",e.converterAttributes),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.converterAttributes),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,6,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.converterTelemetry),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.converterTelemetry),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(24,8,"action.edit"))}}function kK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.extension-required"))}function PK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function DK(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",14)(1,"div",21)(2,"div",50),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",23),t.ɵɵelement(7,"input",51),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,kK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",52)(11,"div",35),t.ɵɵtext(12,"gateway.extension-configuration"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",15),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",31)(17,"div",32),t.ɵɵtext(18,"gateway.keys"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",43)(20,"mat-chip-listbox",44),t.ɵɵtemplate(21,PK,2,1,"mat-chip",45),t.ɵɵelementStart(22,"mat-chip",46),t.ɵɵelement(23,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(24,"button",48,2),t.ɵɵpipe(26,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(25),a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.CUSTOM))})),t.ɵɵelementStart(27,"tb-icon",49),t.ɵɵtext(28,"edit"),t.ɵɵelementEnd()()()()()()}if(2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,8,"gateway.extension-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,10,"gateway.extension")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,12,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("converter.custom.extension").hasError("required")&&e.mappingForm.get("converter.custom.extension").touched),t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(15,14,"gateway.extension-configuration-hint")),t.ɵɵadvance(6),t.ɵɵproperty("tbEllipsisChipList",e.customKeys),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.customKeys),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(26,16,"action.edit"))}}function OK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",22),t.ɵɵtext(2,"gateway.topic-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",23),t.ɵɵelement(4,"input",24),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,bK,3,3,"mat-icon",25),t.ɵɵelement(7,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",21)(9,"div",27),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",23)(14,"mat-select",28),t.ɵɵtemplate(15,wK,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementContainerStart(16,30),t.ɵɵelementStart(17,"div",31)(18,"div",32),t.ɵɵtext(19,"gateway.payload-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(20,"tb-toggle-select",33),t.ɵɵtemplate(21,SK,3,4,"tb-toggle-option",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"div",34)(23,"div",35),t.ɵɵtext(24,"gateway.data-conversion"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"div",15),t.ɵɵtext(26),t.ɵɵpipe(27,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(28,36),t.ɵɵtemplate(29,CK,1,2,"ng-template",17)(30,_K,1,6,"ng-template",17)(31,TK,1,2,"tb-report-strategy",37)(32,MK,27,10,"div",38)(33,DK,29,18,"div",38),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,16,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("topicFilter").hasError("required")&&e.mappingForm.get("topicFilter").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/topic-filter_fn")("tb-help-popup-style",t.ɵɵpureFunction0(24,vK)),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,18,"gateway.response-topic-Qos-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,20,"gateway.mqtt-qos")," "),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.qualityTypes),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",e.convertorTypes),t.ɵɵadvance(5),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(27,22,e.DataConversionTranslationsMap.get(e.converterType))," "),t.ɵɵadvance(2),t.ɵɵproperty("formGroupName",e.converterType)("ngSwitch",e.converterType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConvertorTypeEnum.JSON),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConvertorTypeEnum.BYTES),t.ɵɵadvance(),t.ɵɵconditional(e.data.withReportStrategy?31:-1),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.converterType===e.ConvertorTypeEnum.BYTES||e.converterType===e.ConvertorTypeEnum.JSON),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.converterType===e.ConvertorTypeEnum.CUSTOM)}}function AK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.RequestTypesTranslationsMap.get(e))," ")}}function FK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.topic-required"))}function RK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",22),t.ɵɵtext(2,"gateway.topic-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",23),t.ɵɵelement(4,"input",57),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,FK,3,3,"mat-icon",25),t.ɵɵelement(7,"div",26),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,5,"gateway.set")),t.ɵɵproperty("formControl",e.mappingForm.get("requestValue").get(e.requestMappingType).get("topicFilter")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue").get(e.requestMappingType).get("topicFilter").hasError("required")&&e.mappingForm.get("requestValue").get(e.requestMappingType).get("topicFilter").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/topic-filter_fn")("tb-help-popup-style",t.ɵɵpureFunction0(7,vK))}}function BK(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",58),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("deviceInfoType",e.DeviceInfoType.FULL)}}function NK(e,n){if(1&e&&t.ɵɵelement(0,"tb-device-info-table",58),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("deviceInfoType",e.DeviceInfoType.PARTIAL)}}function LK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function VK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-info.device-name-expression-required"))}function qK(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(3);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function GK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.attribute-name-expression-required"))}function zK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-value-expression-required"))}function UK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-topic-expression-required"))}function jK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",34)(1,"div",59),t.ɵɵtext(2,"gateway.from-device-request-settings"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",60),t.ɵɵtext(4," gateway.from-device-request-settings-hint "),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",61)(6,"div",62)(7,"div",63),t.ɵɵtext(8,"gateway.device-info.device-name-expression"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",64)(10,"div",65)(11,"mat-form-field",23)(12,"mat-select",66),t.ɵɵtemplate(13,LK,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(14,"mat-form-field",23),t.ɵɵelement(15,"input",67),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,VK,3,3,"mat-icon",25),t.ɵɵelement(18,"div",26),t.ɵɵelementEnd()()(),t.ɵɵelementStart(19,"div",21)(20,"div",22),t.ɵɵtext(21,"gateway.attribute-name-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"div",64)(23,"div",65)(24,"mat-form-field",23)(25,"mat-select",68),t.ɵɵtemplate(26,qK,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(27,"mat-form-field",23),t.ɵɵelement(28,"input",69),t.ɵɵpipe(29,"translate"),t.ɵɵtemplate(30,GK,3,3,"mat-icon",25),t.ɵɵelement(31,"div",26),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(32,"div",34)(33,"div",59),t.ɵɵtext(34,"gateway.to-device-response-settings"),t.ɵɵelementEnd(),t.ɵɵelementStart(35,"div",60),t.ɵɵtext(36," gateway.to-device-response-settings-hint "),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"div",21)(38,"div",22),t.ɵɵtext(39,"gateway.response-value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(40,"mat-form-field",23),t.ɵɵelement(41,"input",70),t.ɵɵpipe(42,"translate"),t.ɵɵtemplate(43,zK,3,3,"mat-icon",25),t.ɵɵelement(44,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"div",21)(46,"div",22),t.ɵɵtext(47,"gateway.response-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(48,"mat-form-field",23),t.ɵɵelement(49,"input",71),t.ɵɵpipe(50,"translate"),t.ɵɵtemplate(51,UK,3,3,"mat-icon",25),t.ɵɵelement(52,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(53,"div",72)(54,"mat-slide-toggle",73)(55,"mat-label",74),t.ɵɵpipe(56,"translate"),t.ɵɵtext(57),t.ɵɵpipe(58,"translate"),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(13),t.ɵɵproperty("ngForOf",e.sourceTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.deviceInfo.deviceNameExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.deviceInfo.deviceNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(32,vK)),t.ɵɵadvance(8),t.ɵɵproperty("ngForOf",e.sourceTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(29,22,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.attributeNameExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.attributeNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(33,vK)),t.ɵɵadvance(10),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(42,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.valueExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.valueExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(34,vK)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(50,26,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeRequests.topicExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeRequests.topicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(35,vK)),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(56,28,"gateway.retain-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(58,30,"gateway.retain")," ")}}function HK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-filter-required"))}function WK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.attribute-filter-required"))}function $K(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-value-expression-required"))}function KK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-topic-expression-required"))}function YK(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",50),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",23),t.ɵɵelement(6,"input",75),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,HK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",21)(10,"div",50),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"mat-form-field",23),t.ɵɵelement(15,"input",76),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,WK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",21)(19,"div",22),t.ɵɵtext(20,"gateway.response-value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",23),t.ɵɵelement(22,"input",70),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,$K,3,3,"mat-icon",25),t.ɵɵelement(25,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"div",21)(27,"div",22),t.ɵɵtext(28,"gateway.response-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"mat-form-field",23),t.ɵɵelement(30,"input",71),t.ɵɵpipe(31,"translate"),t.ɵɵtemplate(32,KK,3,3,"mat-icon",25),t.ɵɵelement(33,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"div",72)(35,"mat-slide-toggle",73)(36,"mat-label",74),t.ɵɵpipe(37,"translate"),t.ɵɵtext(38),t.ɵɵpipe(39,"translate"),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,18,"gateway.device-name-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,20,"gateway.device-name-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,22,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.deviceNameFilter").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.deviceNameFilter").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,24,"gateway.attribute-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,26,"gateway.attribute-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,28,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.attributeFilter").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.attributeFilter").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,30,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.valueExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.valueExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(38,vK)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(31,32,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.attributeUpdates.topicExpression").hasError("required")&&e.mappingForm.get("requestValue.attributeUpdates.topicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(39,vK)),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(37,34,"gateway.retain-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(39,36,"gateway.retain")," ")}}function XK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-filter-required"))}function ZK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-filter-required"))}function QK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.request-topic-expression-required"))}function JK(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-expression-required"))}function eY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.response-topic-expression-required"))}function tY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(4);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.QualityTranslationsMap.get(e))," ")}}function nY(e,n){if(1&e&&t.ɵɵelement(0,"tb-error-icon",84),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("tooltipText",e.responseTimeoutErrorTooltip)}}function iY(e,n){1&e&&(t.ɵɵelementStart(0,"span",85),t.ɵɵtext(1,"gateway.suffix.ms"),t.ɵɵelementEnd())}function aY(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",21)(2,"div",22),t.ɵɵtext(3,"gateway.response-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",23),t.ɵɵelement(5,"input",81),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,eY,3,3,"mat-icon",25),t.ɵɵelement(8,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",21)(10,"div",27),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"mat-form-field",23)(15,"mat-select",82),t.ɵɵtemplate(16,tY,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(17,"div",21)(18,"div",22),t.ɵɵtext(19,"gateway.response-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(20,"mat-form-field",23),t.ɵɵelement(21,"input",83),t.ɵɵpipe(22,"translate"),t.ɵɵtemplate(23,nY,1,1,"tb-error-icon",84)(24,iY,2,0,"span",85),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.responseTopicExpression").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.responseTopicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(17,vK)),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,11,"gateway.response-topic-Qos-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(13,13,"gateway.response-topic-Qos")," "),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",e.qualityTypes),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(22,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional((e.mappingForm.get("requestValue.serverSideRpc.responseTimeout").hasError("required")||e.mappingForm.get("requestValue.serverSideRpc.responseTimeout").hasError("min"))&&e.mappingForm.get("requestValue.serverSideRpc.responseTimeout").touched?23:24)}}function rY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",77)(1,"tb-toggle-select",33)(2,"tb-toggle-option",40),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-option",40),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",21)(9,"div",50),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",23),t.ɵɵelement(14,"input",75),t.ɵɵpipe(15,"translate"),t.ɵɵtemplate(16,XK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"div",21)(18,"div",50),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",23),t.ɵɵelement(23,"input",78),t.ɵɵpipe(24,"translate"),t.ɵɵtemplate(25,ZK,3,3,"mat-icon",25),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"div",21)(27,"div",22),t.ɵɵtext(28,"gateway.request-topic-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"mat-form-field",23),t.ɵɵelement(30,"input",79),t.ɵɵpipe(31,"translate"),t.ɵɵtemplate(32,QK,3,3,"mat-icon",25),t.ɵɵelement(33,"div",26),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"div",21)(35,"div",22),t.ɵɵtext(36,"gateway.value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"mat-form-field",23),t.ɵɵelement(38,"input",70),t.ɵɵpipe(39,"translate"),t.ɵɵtemplate(40,JK,3,3,"mat-icon",25),t.ɵɵelement(41,"div",26),t.ɵɵelementEnd()(),t.ɵɵtemplate(42,aY,25,18,"ng-container",80)),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("value",e.ServerSideRPCType.TWO_WAY),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,21,"gateway.with-response")," "),t.ɵɵadvance(2),t.ɵɵproperty("value",e.ServerSideRPCType.ONE_WAY),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,23,"gateway.without-response")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,25,"gateway.device-name-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,27,"gateway.device-name-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,29,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.deviceNameFilter").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.deviceNameFilter").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(19,31,"gateway.method-filter-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(21,33,"gateway.method-filter")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(24,35,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.methodFilter").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.methodFilter").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(31,37,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.requestTopicExpression").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.requestTopicExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(41,vK)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(39,39,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.valueExpression").hasError("required")&&e.mappingForm.get("requestValue.serverSideRpc.valueExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/mqtt-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(42,vK)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.mappingForm.get("requestValue.serverSideRpc.type").value===e.ServerSideRPCType.TWO_WAY)}}function oY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",32),t.ɵɵtext(2,"gateway.request-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",23)(4,"mat-select",53),t.ɵɵtemplate(5,AK,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementContainerStart(6,54)(7,55),t.ɵɵtemplate(8,RK,8,8,"div",56)(9,BK,1,1,"ng-template",17)(10,NK,1,1,"ng-template",17)(11,jK,59,36,"ng-template",17)(12,YK,40,40,"ng-template",17)(13,rY,43,43,"ng-template",17),t.ɵɵelementContainerEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.requestTypes),t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e.mappingForm.get("requestValue").get(e.requestMappingType))("ngSwitch",e.requestMappingType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.requestMappingType===e.RequestTypeEnum.ATTRIBUTE_REQUEST||e.requestMappingType===e.RequestTypeEnum.CONNECT_REQUEST||e.requestMappingType===e.RequestTypeEnum.DISCONNECT_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.CONNECT_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.DISCONNECT_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.ATTRIBUTE_REQUEST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.ATTRIBUTE_UPDATE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.RequestTypeEnum.SERVER_SIDE_RPC)}}function sY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",40),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SourceTypeTranslationsMap.get(e))," ")}}function lY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",39),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-node-required"))}function pY(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",37),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function cY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function dY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function uY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function mY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function hY(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",86)(1,"div",87)(2,"div",88),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"div",89)(7,"mat-form-field",90)(8,"mat-select",91),t.ɵɵtemplate(9,sY,3,4,"mat-option",29),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"mat-form-field",90),t.ɵɵelement(11,"input",92),t.ɵɵpipe(12,"translate"),t.ɵɵtemplate(13,lY,3,3,"mat-icon",25),t.ɵɵelement(14,"div",26),t.ɵɵpipe(15,"getConnectorMappingHelpLink"),t.ɵɵelementEnd()(),t.ɵɵelement(16,"tb-device-info-table",93),t.ɵɵtemplate(17,pY,1,2,"tb-report-strategy",37),t.ɵɵelementStart(18,"div",31)(19,"div",32),t.ɵɵtext(20,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",43)(22,"mat-chip-listbox",44),t.ɵɵtemplate(23,cY,2,1,"mat-chip",45),t.ɵɵelementStart(24,"mat-chip",46),t.ɵɵelement(25,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"button",48,3),t.ɵɵpipe(28,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(27),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES))})),t.ɵɵelementStart(29,"tb-icon",49),t.ɵɵtext(30,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(31,"div",31)(32,"div",32),t.ɵɵtext(33,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"div",43)(35,"mat-chip-listbox",44),t.ɵɵtemplate(36,dY,2,1,"mat-chip",45),t.ɵɵelementStart(37,"mat-chip",46),t.ɵɵelement(38,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(39,"button",48,4),t.ɵɵpipe(41,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(40),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.TIMESERIES))})),t.ɵɵelementStart(42,"tb-icon",49),t.ɵɵtext(43,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(44,"div",31)(45,"div",32),t.ɵɵtext(46,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(47,"div",43)(48,"mat-chip-listbox",44),t.ɵɵtemplate(49,uY,2,1,"mat-chip",45),t.ɵɵelementStart(50,"mat-chip",46),t.ɵɵelement(51,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(52,"button",48,5),t.ɵɵpipe(54,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(53),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES_UPDATES))})),t.ɵɵelementStart(55,"tb-icon",49),t.ɵɵtext(56,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(57,"div",31)(58,"div",32),t.ɵɵtext(59,"gateway.rpc-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(60,"div",43)(61,"mat-chip-listbox",44),t.ɵɵtemplate(62,mY,2,1,"mat-chip",45),t.ɵɵelementStart(63,"mat-chip",46),t.ɵɵelement(64,"label",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(65,"button",48,6),t.ɵɵpipe(67,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(66),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.RPC_METHODS))})),t.ɵɵelementStart(68,"tb-icon",49),t.ɵɵtext(69,"edit"),t.ɵɵelementEnd()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,23,"gateway.device-node-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,25,"gateway.device-node")," "),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",t.ɵɵpureFunction2(41,xK,e.OPCUaSourceTypesEnum.PATH,e.OPCUaSourceTypesEnum.IDENTIFIER)),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(12,27,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mappingForm.get("deviceNodePattern").hasError("required")&&e.mappingForm.get("deviceNodePattern").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup",t.ɵɵpipeBind3(15,29,e.ConnectorType.OPCUA,"device-node",e.mappingForm.get("deviceNodeSource").value))("tb-help-popup-style",t.ɵɵpureFunction0(44,vK)),t.ɵɵadvance(2),t.ɵɵproperty("connectorType",e.ConnectorType.OPCUA)("sourceTypes",e.OPCUaSourceTypes)("deviceInfoType",e.DeviceInfoType.FULL),t.ɵɵadvance(),t.ɵɵconditional(e.data.withReportStrategy?17:-1),t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",e.opcAttributes),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcAttributes),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(28,33,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.opcTelemetry),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcTelemetry),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(41,35,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.opcAttributesUpdates),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcAttributesUpdates),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(54,37,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.opcRpcMethods),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.opcRpcMethods),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(67,39,"action.edit"))}}class gY extends A{constructor(e,t,n,i,a,r,o,s,l){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.translate=l,this.MappingType=Di,this.qualityTypes=zi,this.QualityTranslationsMap=ni,this.convertorTypes=Object.values(Jn),this.ConvertorTypeEnum=Jn,this.ConvertorTypeTranslationsMap=ii,this.sourceTypes=Object.values(ei),this.OPCUaSourceTypes=Object.values(Mi),this.OPCUaSourceTypesEnum=Mi,this.sourceTypesEnum=ei,this.SourceTypeTranslationsMap=Bi,this.requestTypes=Object.values(ai),this.RequestTypeEnum=ai,this.RequestTypesTranslationsMap=ri,this.DeviceInfoType=ga,this.ServerSideRPCType=Wi,this.MappingKeysType=Ni,this.MappingHintTranslationsMap=Hi,this.MappingTypeTranslationsMap=Oi,this.DataConversionTranslationsMap=oi,this.HelpLinkByMappingTypeMap=ji,this.ConnectorType=ct,this.ReportStrategyDefaultValue=en,this.keysPopupClosed=!0,this.destroy$=new te,this.createMappingForm()}get converterAttributes(){if(this.converterType)return this.mappingForm.get("converter").get(this.converterType).value.attributes.map((e=>e.key))}get converterTelemetry(){if(this.converterType)return this.mappingForm.get("converter").get(this.converterType).value.timeseries.map((e=>e.key))}get opcAttributes(){return this.mappingForm.get("attributes").value?.map((e=>e.key))||[]}get opcTelemetry(){return this.mappingForm.get("timeseries").value?.map((e=>e.key))||[]}get opcRpcMethods(){return this.mappingForm.get("rpc_methods").value?.map((e=>e.method))||[]}get opcAttributesUpdates(){return this.mappingForm.get("attributes_updates")?.value?.map((e=>e.key))||[]}get converterType(){return this.mappingForm.get("converter")?.get("type").value}get customKeys(){return Object.keys(this.mappingForm.get("converter").get("custom").value.extensionConfig)}get requestMappingType(){return this.mappingForm.get("requestType").value}get responseTimeoutErrorTooltip(){const e=this.mappingForm.get("requestValue.serverSideRpc.responseTimeout");return e.hasError("required")?this.translate.instant("gateway.response-timeout-required"):e.hasError("min")?this.translate.instant("gateway.response-timeout-limits-error",{min:1}):""}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}createMappingForm(){switch(this.data.mappingType){case Di.DATA:this.mappingForm=this.fb.group({}),this.createDataMappingForm();break;case Di.REQUESTS:this.mappingForm=this.fb.group({}),this.createRequestMappingForm();break;case Di.OPCUA:this.createOPCUAMappingForm()}}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){this.mappingForm.valid&&this.dialogRef.close(this.prepareMappingData())}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))this.popoverService.hidePopover(i);else{const e=(this.data.mappingType!==Di.OPCUA?this.mappingForm.get("converter").get(this.converterType):this.mappingForm).get(n),t={keys:e.value,keysType:n,rawData:this.mappingForm.get("converter.type")?.value===Jn.BYTES,panelTitle:Li.get(n),addKeyTitle:Vi.get(n),deleteKeyTitle:qi.get(n),noKeysText:Gi.get(n),withReportStrategy:this.data.withReportStrategy,connectorType:this.data.mappingType===Di.OPCUA?ct.OPCUA:ct.MQTT,convertorType:this.converterType};this.data.mappingType===Di.OPCUA&&(t.valueTypeKeys=Object.values(Mi),t.valueTypeEnum=Mi,t.valueTypes=Bi,t.sourceType=this.mappingForm.get("deviceNodeSource").value),this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,yK,"leftBottom",!1,null,t,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(le(this.destroy$)).subscribe((t=>{this.popoverComponent.hide(),e.patchValue(t),e.markAsDirty()})),this.popoverComponent.tbHideStart.pipe(le(this.destroy$)).subscribe((()=>{this.keysPopupClosed=!0}))}}prepareMappingData(){const e=this.mappingForm.value;switch(Te(e),this.data.mappingType){case Di.DATA:const{converter:t,topicFilter:n,subscriptionQos:i}=e,{reportStrategy:a,...r}=t[t.type];return{topicFilter:n,subscriptionQos:i,...a?{reportStrategy:a}:{},converter:{type:t.type,...r}};case Di.REQUESTS:return{requestType:e.requestType,requestValue:e.requestValue[e.requestType]};default:return e}}getFormValueData(){if(this.data.value&&Object.keys(this.data.value).length)switch(this.data.mappingType){case Di.DATA:const{converter:e,topicFilter:t,subscriptionQos:n,reportStrategy:i}=this.data.value;return{topicFilter:t,subscriptionQos:n,converter:{type:e.type,[e.type]:{...e,...i?{reportStrategy:i}:{}}}};case Di.REQUESTS:return{requestType:this.data.value.requestType,requestValue:{[this.data.value.requestType]:this.data.value.requestValue}};default:return this.data.value}}createDataMappingForm(){this.mappingForm.addControl("topicFilter",this.fb.control("",[$.required,$.pattern(an)])),this.mappingForm.addControl("subscriptionQos",this.fb.control(0)),this.mappingForm.addControl("converter",this.fb.group({type:[Jn.JSON,[]],json:this.fb.group({deviceInfo:[{},[]],attributes:[[],[]],timeseries:[[],[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),bytes:this.fb.group({deviceInfo:[{},[]],attributes:[[],[]],timeseries:[[],[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),custom:this.fb.group({extension:["",[$.required,$.pattern(an)]],extensionConfig:[{},[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]})})),this.mappingForm.patchValue(this.getFormValueData()),this.mappingForm.get("converter.type").valueChanges.pipe(be(this.mappingForm.get("converter.type").value),le(this.destroy$)).subscribe((e=>{const t=this.mappingForm.get("converter");t.get("json").disable({emitEvent:!1}),t.get("bytes").disable({emitEvent:!1}),t.get("custom").disable({emitEvent:!1}),t.get(e).enable({emitEvent:!1})}))}createRequestMappingForm(){this.mappingForm.addControl("requestType",this.fb.control(ai.CONNECT_REQUEST,[])),this.mappingForm.addControl("requestValue",this.fb.group({connectRequests:this.fb.group({topicFilter:["",[$.required,$.pattern(an)]],deviceInfo:[{},[]]}),disconnectRequests:this.fb.group({topicFilter:["",[$.required,$.pattern(an)]],deviceInfo:[{},[]]}),attributeRequests:this.fb.group({topicFilter:["",[$.required,$.pattern(an)]],deviceInfo:this.fb.group({deviceNameExpressionSource:[ei.MSG,[]],deviceNameExpression:["",[$.required]]}),attributeNameExpressionSource:[ei.MSG,[]],attributeNameExpression:["",[$.required,$.pattern(an)]],topicExpression:["",[$.required,$.pattern(an)]],valueExpression:["",[$.required,$.pattern(an)]],retain:[!1,[]]}),attributeUpdates:this.fb.group({deviceNameFilter:["",[$.required,$.pattern(an)]],attributeFilter:["",[$.required,$.pattern(an)]],topicExpression:["",[$.required,$.pattern(an)]],valueExpression:["",[$.required,$.pattern(an)]],retain:[!0,[]]}),serverSideRpc:this.fb.group({type:[Wi.TWO_WAY,[]],deviceNameFilter:["",[$.required,$.pattern(an)]],methodFilter:["",[$.required,$.pattern(an)]],requestTopicExpression:["",[$.required,$.pattern(an)]],responseTopicExpression:["",[$.required,$.pattern(an)]],valueExpression:["",[$.required,$.pattern(an)]],responseTopicQoS:[0,[]],responseTimeout:[1e4,[$.required,$.min(1)]]}),reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]})),this.mappingForm.get("requestType").valueChanges.pipe(be(this.mappingForm.get("requestType").value),le(this.destroy$)).subscribe((e=>{const t=this.mappingForm.get("requestValue");t.get("connectRequests").disable({emitEvent:!1}),t.get("disconnectRequests").disable({emitEvent:!1}),t.get("attributeRequests").disable({emitEvent:!1}),t.get("attributeUpdates").disable({emitEvent:!1}),t.get("serverSideRpc").disable({emitEvent:!1}),t.get(e).enable()})),this.mappingForm.get("requestValue.serverSideRpc.type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>{const t=this.mappingForm.get("requestValue.serverSideRpc");e===Wi.ONE_WAY?(t.get("responseTopicExpression").disable({emitEvent:!1}),t.get("responseTopicQoS").disable({emitEvent:!1}),t.get("responseTimeout").disable({emitEvent:!1})):(t.get("responseTopicExpression").enable({emitEvent:!1}),t.get("responseTopicQoS").enable({emitEvent:!1}),t.get("responseTimeout").enable({emitEvent:!1}))})),this.mappingForm.patchValue(this.getFormValueData())}createOPCUAMappingForm(){this.mappingForm=this.fb.group({deviceNodeSource:[Mi.PATH,[]],deviceNodePattern:["",[$.required]],deviceInfo:[{},[]],attributes:[[],[]],timeseries:[[],[]],rpc_methods:[[],[]],attributes_updates:[[],[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),this.mappingForm.patchValue(this.getFormValueData())}static{this.ɵfac=function(e){return new(e||gY)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(He.TranslateService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:gY,selectors:[["tb-mapping-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:26,vars:19,consts:[["attributesButton",""],["telemetryButton",""],["keysButton",""],["opcAttributesButton",""],["opcTelemetryButton",""],["attributesUpdatesButton",""],["rpcMethodsButton",""],[1,"key-mapping",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-hint","tb-primary-fill"],[3,"ngSwitch"],[3,"ngSwitchCase"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex"],["matInput","","name","value","formControlName","topicFilter",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","subscriptionQos"],[3,"value",4,"ngFor","ngForOf"],["formGroupName","converter"],[1,"tb-form-row","space-between","tb-flex"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[3,"formGroupName","ngSwitch"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],["class","tb-form-panel no-border no-padding",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["formControlName","deviceInfo","required","true",3,"convertorType","deviceInfoType"],["formControlName","deviceInfo","required","true",3,"deviceInfoType","convertorType","sourceTypes"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[4,"ngFor","ngForOf"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","name","value","formControlName","extension",3,"placeholder"],[1,"tb-form-row","space-between","same-padding","tb-flex","column"],["formControlName","requestType"],["formGroupName","requestValue"],[3,"formGroup","ngSwitch"],["class","tb-form-row column-xs",4,"ngIf"],["matInput","","name","value",3,"formControl","placeholder"],["formControlName","deviceInfo","required","true",3,"deviceInfoType"],["translate","",1,"tb-form-panel-title","tb-required"],["translate","",1,"tb-form-hint","tb-primary-fill"],["formGroupName","deviceInfo",1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-flex","no-flex","align-center"],["translate","",1,"tb-required"],[1,"flex","flex-1","gap-2"],[1,"flex","w-1/4"],["formControlName","deviceNameExpressionSource"],["matInput","","name","value","formControlName","deviceNameExpression",3,"placeholder"],["formControlName","attributeNameExpressionSource"],["matInput","","name","value","formControlName","attributeNameExpression",3,"placeholder"],["matInput","","name","value","formControlName","valueExpression",3,"placeholder"],["matInput","","name","value","formControlName","topicExpression",3,"placeholder"],[1,"tb-form-row"],["formControlName","retain",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["matInput","","name","value","formControlName","deviceNameFilter",3,"placeholder"],["matInput","","name","value","formControlName","attributeFilter",3,"placeholder"],[1,"tb-flex","row","center","align-center","no-gap","fill-width"],["matInput","","name","value","formControlName","methodFilter",3,"placeholder"],["matInput","","name","value","formControlName","requestTopicExpression",3,"placeholder"],[4,"ngIf"],["matInput","","name","value","formControlName","responseTopicExpression",3,"placeholder"],["formControlName","responseTopicQoS"],["matInput","","name","value","type","number","min","1","formControlName","responseTimeout",3,"placeholder"],["matSuffix","",3,"tooltipText"],["translate","","matSuffix","",1,"block","pr-2"],[1,"device-node-row","tb-form-row","column-xs","pr-4"],["translate","",1,"tb-flex","no-flex","align-center","w-1/5"],[1,"tb-required",3,"tb-hint-tooltip-icon"],[1,"flex","w-1/5"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","deviceNodeSource"],["matInput","","name","value","formControlName","deviceNodePattern",3,"placeholder"],["formControlName","deviceInfo","required","true",3,"connectorType","sourceTypes","deviceInfoType"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",7)(1,"mat-toolbar",8)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",9)(6,"div",10),t.ɵɵelementStart(7,"button",11),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",12),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",13)(11,"div",14)(12,"div",15),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(15,16),t.ɵɵtemplate(16,OK,34,25,"ng-template",17)(17,oY,14,9,"ng-template",17)(18,hY,70,45,"ng-template",17),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()(),t.ɵɵelementStart(19,"div",18)(20,"button",19),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"button",20),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingForm),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,11,n.MappingTypeTranslationsMap.get(null==n.data?null:n.data.mappingType))),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.HelpLinkByMappingTypeMap.get(n.data.mappingType)),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,13,n.MappingHintTranslationsMap.get(null==n.data?null:n.data.mappingType))," "),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",n.data.mappingType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingType.DATA),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingType.REQUESTS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.MappingType.OPCUA),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(22,15,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.mappingForm.invalid||!n.mappingForm.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(25,17,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(gY,[j,_,o$,Gn,I$,Qn,Xn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:grid;height:100%}[_nghost-%COMP%] .key-mapping[_ngcontent-%COMP%]{max-width:900px;display:flex;flex-direction:column}[_nghost-%COMP%] .key-mapping[_ngcontent-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{min-height:64px}[_nghost-%COMP%] .key-mapping[_ngcontent-%COMP%] tb-toggle-select[_ngcontent-%COMP%]{padding:4px 0}[_nghost-%COMP%] .mat-mdc-dialog-content[_ngcontent-%COMP%]{height:670px}[_nghost-%COMP%] .ellipsis-chips-container[_ngcontent-%COMP%]{max-width:70%}[_nghost-%COMP%] .key-mapping .mat-mdc-chip-listbox .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center;flex-wrap:nowrap}[_nghost-%COMP%] .tb-form-row .fixed-title-width{min-width:40px;width:35%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-form-row .mat-mdc-form-field{width:0}[_nghost-%COMP%] .device-node-row.tb-form-row{gap:12px}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}']})}}e("MappingDialogComponent",gY);const fY=["searchInput"],yY=()=>({minWidth:"96px",textAlign:"center"});function vY(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",24)(2,"span",25),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",9),t.ɵɵelementStart(6,"button",11),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageMapping(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",11),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,e.mappingTypeTranslationsMap.get(e.mappingType))),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search"))}}function xY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-header-cell",29),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext();t.ɵɵclassProp("request-column",n.mappingType===n.mappingTypeEnum.REQUESTS),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,3,e.title)," ")}}function bY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext().$implicit,a=t.ɵɵnextContext();t.ɵɵclassProp("request-column",a.mappingType===a.mappingTypeEnum.REQUESTS),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e[i.def]," ")}}function wY(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,26),t.ɵɵtemplate(1,xY,3,5,"mat-header-cell",27)(2,bY,2,3,"mat-cell",28),t.ɵɵelementContainerEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("matColumnDef",e.def)}}function SY(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",31)}function CY(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",11),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageMapping(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",11),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteMapping(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function _Y(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,CY,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",32),t.ɵɵelementContainer(4,33),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",34)(6,"button",35),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",36),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",37,2),t.ɵɵelementContainer(11,33),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,yY)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function TY(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",38)}function IY(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class EY{set mappingType(e){this.mappingTypeValue!==e&&(this.mappingTypeValue=e)}get mappingType(){return this.mappingTypeValue}constructor(e,t,n,i){this.translate=e,this.dialog=t,this.dialogService=n,this.fb=i,this.required=!1,this.withReportStrategy=!0,this.mappingTypeTranslationsMap=Oi,this.mappingTypeEnum=Di,this.displayedColumns=[],this.mappingColumns=[],this.textSearchMode=!1,this.hidePageSize=!1,this.activeValue=!1,this.dirtyValue=!1,this.textSearch=this.fb.control("",{nonNullable:!0}),this.onChange=()=>{},this.onTouched=()=>{},this.destroy$=new te,this.mappingFormGroup=this.fb.array([]),this.dirtyValue=!this.activeValue,this.dataSource=new MY}ngOnInit(){this.setMappingColumns(),this.displayedColumns.push(...this.mappingColumns.map((e=>e.def)),"actions"),this.mappingFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.updateTableData(e),this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(de(150),ce(((e,t)=>(e??"")===t.trim())),le(this.destroy$)).subscribe((e=>{const t=e.trim();this.updateTableData(this.mappingFormGroup.value,t.trim())}))}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){this.mappingFormGroup.clear(),this.pushDataAsFormArrays(e)}validate(){return!this.required||this.mappingFormGroup.controls.length?null:{mappingFormGroup:{valid:!1}}}enterFilterMode(){this.textSearchMode=!0,setTimeout((()=>{this.searchInputField.nativeElement.focus(),this.searchInputField.nativeElement.setSelectionRange(0,0)}),10)}exitFilterMode(){this.updateTableData(this.mappingFormGroup.value),this.textSearchMode=!1,this.textSearch.reset()}manageMapping(e,t){e&&e.stopPropagation();const n=ke(t)?this.mappingFormGroup.at(t).value:{};this.dialog.open(gY,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{mappingType:this.mappingType,value:n,buttonTitle:Ae(t)?"action.add":"action.apply",withReportStrategy:this.withReportStrategy}}).afterClosed().pipe(ye(1),le(this.destroy$)).subscribe((e=>{e&&(ke(t)?this.mappingFormGroup.at(t).patchValue(e):this.pushDataAsFormArrays([e]),this.mappingFormGroup.markAsDirty())}))}updateTableData(e,t){let n=e.map((e=>this.getMappingValue(e)));t&&(n=n.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(n)}deleteMapping(e,t){e&&e.stopPropagation();const n=this.mappingFormGroup.controls[t].value,i=n.deviceInfo?.deviceNameExpression??n.topicFilter??this.getRequestDetails(n);this.dialogService.confirm(this.translate.instant("gateway.delete-mapping-title",{name:i}),this.translate.instant("gateway.delete-mapping-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).subscribe((e=>{e&&(this.mappingFormGroup.removeAt(t),this.mappingFormGroup.markAsDirty())}))}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.mappingFormGroup.push(this.fb.control(e))))}getMappingValue(e){switch(this.mappingType){case Di.DATA:const t=ii.get(e.converter?.type);return{topicFilter:e.topicFilter,QoS:e.subscriptionQos,converter:t?this.translate.instant(t):""};case Di.REQUESTS:return{requestType:e.requestType,type:this.translate.instant(ri.get(e.requestType)),details:this.getRequestDetails(e)};case Di.OPCUA:const n=e.deviceInfo?.deviceNameExpression,i=e.deviceInfo?.deviceProfileExpression,{deviceNodePattern:a}=e;return{deviceNodePattern:a,deviceNamePattern:n,deviceProfileExpression:i};default:return{}}}getRequestDetails(e){let t;return t=e.requestType===ai.ATTRIBUTE_UPDATE?e.requestValue.attributeFilter:e.requestType===ai.SERVER_SIDE_RPC?e.requestValue.methodFilter:e.requestValue.topicFilter,t}setMappingColumns(){switch(this.mappingType){case Di.DATA:this.mappingColumns.push({def:"topicFilter",title:"gateway.topic-filter"},{def:"QoS",title:"gateway.mqtt-qos"},{def:"converter",title:"gateway.payload-type"});break;case Di.REQUESTS:this.mappingColumns.push({def:"type",title:"gateway.type"},{def:"details",title:"gateway.details"});break;case Di.OPCUA:this.mappingColumns.push({def:"deviceNodePattern",title:"gateway.device-node"},{def:"deviceNamePattern",title:"gateway.device-name"},{def:"deviceProfileExpression",title:"gateway.device-profile"})}}static{this.ɵfac=function(e){return new(e||EY)(t.ɵɵdirectiveInject(He.TranslateService),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(_e.DialogService),t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:EY,selectors:[["tb-mapping-table"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(fY,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{required:"required",withReportStrategy:"withReportStrategy",mappingType:"mappingType"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>EY)),multi:!0},{provide:K,useExisting:c((()=>EY)),multi:!0}]),t.ɵɵStandaloneFeature],decls:40,vars:33,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-mapping-table","tb-absolute-fill"],[1,"tb-mapping-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef",4,"ngFor","ngForOf"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-mapping-table-title"],[3,"matColumnDef"],["class","table-value-column",3,"request-column",4,"matHeaderCellDef"],["tbTruncateWithTooltip","","class","table-value-column",3,"request-column",4,"matCellDef"],[1,"table-value-column"],["tbTruncateWithTooltip","",1,"table-value-column"],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,vY,14,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"div",7)(7,"button",8),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-icon"),t.ɵɵtext(10,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",9)(12,"mat-label"),t.ɵɵtext(13," "),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",10,0),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵpipe(18,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(19,"mat-icon"),t.ɵɵtext(20,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(21,"div",12)(22,"table",13),t.ɵɵtemplate(23,wY,3,1,"ng-container",14),t.ɵɵelementContainerStart(24,15),t.ɵɵtemplate(25,SY,1,0,"mat-header-cell",16)(26,_Y,12,6,"mat-cell",17),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(27,TY,1,0,"mat-header-row",18)(28,IY,1,0,"mat-row",19),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"section",20),t.ɵɵpipe(30,"async"),t.ɵɵelementStart(31,"button",21),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageMapping(i))})),t.ɵɵelementStart(32,"mat-icon",22),t.ɵɵtext(33,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"span"),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(37,"span",23),t.ɵɵpipe(38,"async"),t.ɵɵtext(39," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(4,19,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,21,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,23,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,25,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.mappingColumns),t.ɵɵadvance(4),t.ɵɵproperty("matHeaderRowDef",n.displayedColumns)("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",n.displayedColumns),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(30,27,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,29,"gateway.add-mapping")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(38,31,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(EY,[j,_,qn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%]{width:100%;height:100%;background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content.tb-outlined-border[_ngcontent-%COMP%]{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;border:solid 1px #e0e0e0;border-radius:4px}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .tb-mapping-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:21%}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column.request-column[_ngcontent-%COMP%]{width:35%}[_nghost-%COMP%] .tb-mapping-table[_ngcontent-%COMP%] .tb-mapping-table-content[_ngcontent-%COMP%] .ellipsis[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-mapping-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}e("MappingTableComponent",EY),Ge([I()],EY.prototype,"required",void 0),Ge([I()],EY.prototype,"withReportStrategy",void 0);let MY=class extends G{constructor(){super()}};function kY(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",7),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SecurityTypeTranslationsMap.get(e))," ")}}function PY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",18),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.username-required"))}function DY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",8)(1,"div",9),t.ɵɵtext(2,"gateway.username"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",10)(4,"mat-form-field",11),t.ɵɵelement(5,"input",12),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,PY,3,3,"mat-icon",13),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",8)(9,"div",14),t.ɵɵtext(10,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"div",10)(12,"mat-form-field",11),t.ɵɵelement(13,"input",15),t.ɵɵpipe(14,"translate"),t.ɵɵelementStart(15,"div",16),t.ɵɵelement(16,"tb-toggle-password",17),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,3,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("username").hasError("required")&&e.securityFormGroup.get("username").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,5,"gateway.set"))}}function OY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",7),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e," ")}}function AY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",18),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.username-required"))}function FY(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",8)(2,"div",14),t.ɵɵtext(3,"gateway.mode"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",10)(5,"mat-form-field",11)(6,"mat-select",25),t.ɵɵtemplate(7,OY,2,2,"mat-option",4),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(8,"div",8)(9,"div",14),t.ɵɵtext(10,"gateway.username"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"div",10)(12,"mat-form-field",11),t.ɵɵelement(13,"input",12),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,AY,3,3,"mat-icon",13),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",8)(17,"div",14),t.ɵɵtext(18,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",10)(20,"mat-form-field",11),t.ɵɵelement(21,"input",15),t.ɵɵpipe(22,"translate"),t.ɵɵelementStart(23,"div",16),t.ɵɵelement(24,"tb-toggle-password",17),t.ɵɵelementEnd()()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",e.modeTypes),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,4,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.securityFormGroup.get("username").hasError("required")&&e.securityFormGroup.get("username").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(22,6,"gateway.set"))}}function RY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",19),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",8)(4,"div",20),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",10)(8,"mat-form-field",11),t.ɵɵelement(9,"input",21),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(11,"div",8)(12,"div",20),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"div",10)(16,"mat-form-field",11),t.ɵɵelement(17,"input",22),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(19,"div",8)(20,"div",20),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"div",10)(24,"mat-form-field",11),t.ɵɵelement(25,"input",23),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(27,FY,25,8,"ng-container",24)),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,8,"gateway.path-hint")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,10,"gateway.CA-certificate-path")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(10,12,"gateway.set")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(14,14,"gateway.private-key-path")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(18,16,"gateway.set")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(22,18,"gateway.client-cert-path")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(26,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.mode()===e.SecurityMode.extendedCertificates)}}e("MappingDatasource",MY);class BY{constructor(e,t){this.fb=e,this.cdr=t,this.title="gateway.security",this.mode=l($i.certificates),this.BrokerSecurityType=ki,this.SecurityMode=$i,this.securityTypes=Object.values(ki),this.modeTypes=Object.values(Pi),this.SecurityTypeTranslationsMap=Ri,this.destroy$=new te,o((()=>{this.mode()===$i.basic&&(this.securityTypes=this.securityTypes.filter((e=>e!==ki.CERTIFICATES)))}))}ngOnInit(){this.securityFormGroup=this.fb.group({type:[ki.ANONYMOUS,[]],username:["",[$.required,$.pattern(an)]],password:["",[$.pattern(an)]],pathToCACert:["",[$.pattern(an)]],pathToPrivateKey:["",[$.pattern(an)]],pathToClientCert:["",[$.pattern(an)]]}),this.mode()===$i.extendedCertificates&&this.securityFormGroup.addControl("mode",this.fb.control(Pi.NONE,[])),this.securityFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{Te(e),this.onChange(e),this.onTouched()})),this.securityFormGroup.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.updateValidators(e)))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}writeValue(e){if(e)e.type||(e.type=ki.ANONYMOUS),this.updateValidators(e.type),this.securityFormGroup.reset(e,{emitEvent:!1});else{const e={type:ki.ANONYMOUS};this.securityFormGroup.reset(e,{emitEvent:!1})}this.cdr.markForCheck()}validate(){return this.securityFormGroup.get("type").value!==ki.BASIC||this.securityFormGroup.valid?null:{securityForm:{valid:!1}}}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}updateValidators(e){if(e)if(this.securityFormGroup.get("username").disable({emitEvent:!1}),this.securityFormGroup.get("password").disable({emitEvent:!1}),this.securityFormGroup.get("pathToCACert").disable({emitEvent:!1}),this.securityFormGroup.get("pathToPrivateKey").disable({emitEvent:!1}),this.securityFormGroup.get("pathToClientCert").disable({emitEvent:!1}),this.securityFormGroup.get("mode")?.disable({emitEvent:!1}),e===ki.BASIC)this.securityFormGroup.get("username").enable({emitEvent:!1}),this.securityFormGroup.get("password").enable({emitEvent:!1});else if(e===ki.CERTIFICATES&&(this.securityFormGroup.get("pathToCACert").enable({emitEvent:!1}),this.securityFormGroup.get("pathToPrivateKey").enable({emitEvent:!1}),this.securityFormGroup.get("pathToClientCert").enable({emitEvent:!1}),this.mode()===$i.extendedCertificates)){const e=this.securityFormGroup.get("mode");e&&!e.value&&e.setValue(Pi.NONE,{emitEvent:!1}),e?.enable({emitEvent:!1}),this.securityFormGroup.get("username").enable({emitEvent:!1}),this.securityFormGroup.get("password").enable({emitEvent:!1})}}static{this.ɵfac=function(e){return new(e||BY)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:BY,selectors:[["tb-security-config"]],inputs:{title:"title",mode:[1,"mode"]},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>BY)),multi:!0},{provide:K,useExisting:c((()=>BY)),multi:!0}]),t.ɵɵStandaloneFeature],decls:10,vars:8,consts:[[1,"tb-form-row","space-between","same-padding","tb-flex","column",3,"formGroup"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],[1,"fixed-title-width","tb-required"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],[3,"ngSwitch"],[3,"ngSwitchCase"],[3,"value"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","username",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width"],["matInput","","type","password","name","value","formControlName","password",3,"placeholder"],["matSuffix","",1,"tb-flex","no-gap","align-center","fill-height"],[1,"tb-flex","align-center","fill-height"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[1,"tb-form-hint","tb-primary-fill"],["tbTruncateWithTooltip","",1,"fixed-title-width"],["matInput","","name","value","formControlName","pathToCACert",3,"placeholder"],["matInput","","name","value","formControlName","pathToPrivateKey",3,"placeholder"],["matInput","","name","value","formControlName","pathToClientCert",3,"placeholder"],[4,"ngIf"],["formControlName","mode"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-select",3),t.ɵɵtemplate(6,kY,3,4,"tb-toggle-option",4),t.ɵɵelementEnd()(),t.ɵɵelementContainerStart(7,5),t.ɵɵtemplate(8,DY,17,7,"ng-template",6)(9,RY,28,22,"ng-template",6),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.securityFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,6,n.title)),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.securityTypes),t.ɵɵadvance(),t.ɵɵproperty("ngSwitch",n.securityFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.BrokerSecurityType.BASIC),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.BrokerSecurityType.CERTIFICATES))},dependencies:t.ɵɵgetComponentDepsFactory(BY,[j,_,qn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}'],changeDetection:d.OnPush})}}e("SecurityConfigComponent",BY);const NY=()=>({min:1e3}),LY=()=>({min:50}),VY=()=>({min:100});function qY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.server-url-required"))}function GY(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",9),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.timeout-error",t.ɵɵpureFunction0(4,NY)))}function zY(e,n){1&e&&(t.ɵɵelementStart(0,"span",10),t.ɵɵtext(1,"gateway.suffix.ms"),t.ɵɵelementEnd())}function UY(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",22),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.value),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.name)}}function jY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind2(1,1,"gateway.scan-period-error",t.ɵɵpureFunction0(4,NY)))}function HY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind2(1,1,"gateway.poll-period-error",t.ɵɵpureFunction0(4,LY)))}function WY(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"div",6),t.ɵɵpipe(2,"translate"),t.ɵɵelementStart(3,"div",7),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"mat-form-field",3),t.ɵɵelement(7,"input",23),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,HY,3,5,"mat-icon",5),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,4,"gateway.hints.poll-period")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,6,"gateway.poll-period")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.serverConfigFormGroup.get("pollPeriodInMillis").hasError("required")||e.serverConfigFormGroup.get("pollPeriodInMillis").hasError("min"))&&e.serverConfigFormGroup.get("pollPeriodInMillis").touched)}}function $Y(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind2(1,1,"gateway.sub-check-period-error",t.ɵɵpureFunction0(4,VY)))}class KY{constructor(e){this.fb=e,this.hideNewFields=!1,this.securityPolicyTypes=Fi,this.SecurityMode=$i,this.destroy$=new te,this.serverConfigFormGroup=this.fb.group({url:["",[$.required,$.pattern(an)]],timeoutInMillis:[1e3,[$.required,$.min(1e3)]],scanPeriodInMillis:[z,[$.required,$.min(1e3)]],pollPeriodInMillis:[5e3,[$.required,$.min(50)]],enableSubscriptions:[!0,[]],subCheckPeriodInMillis:[100,[$.required,$.min(100)]],showMap:[!1,[]],security:[Ai.BASIC128,[]],identity:[]}),this.serverConfigFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}ngAfterViewInit(){this.hideNewFields&&this.serverConfigFormGroup.get("pollPeriodInMillis").disable({emitEvent:!1})}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.serverConfigFormGroup.valid?null:{serverConfigFormGroup:{valid:!1}}}writeValue(e){const{timeoutInMillis:t=1e3,scanPeriodInMillis:n=z,pollPeriodInMillis:i=5e3,enableSubscriptions:a=!0,subCheckPeriodInMillis:r=100,showMap:o=!1,security:s=Ai.BASIC128,identity:l={}}=e;this.serverConfigFormGroup.reset({...e,timeoutInMillis:t,scanPeriodInMillis:n,pollPeriodInMillis:i,enableSubscriptions:a,subCheckPeriodInMillis:r,showMap:o,security:s,identity:l},{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||KY)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:KY,selectors:[["tb-opc-server-config"]],inputs:{hideNewFields:"hideNewFields"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>KY)),multi:!0},{provide:K,useExisting:c((()=>KY)),multi:!0}]),t.ɵɵStandaloneFeature],decls:63,vars:56,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],["tbTruncateWithTooltip","","translate","",1,"fixed-title-width"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","url",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],[1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip",""],["matInput","","type","number","min","1000","name","value","formControlName","timeoutInMillis",3,"placeholder"],["matSuffix","",3,"tooltipText"],["translate","","matSuffix","",1,"block","pr-2"],["formControlName","security"],[3,"value",4,"ngFor","ngForOf"],["matInput","","type","number","min","1000","name","value","formControlName","scanPeriodInMillis",3,"placeholder"],["class","tb-form-row column-xs",4,"ngIf"],["matInput","","type","number","min","100","name","value","formControlName","subCheckPeriodInMillis",3,"placeholder"],[1,"tb-form-row"],["formControlName","enableSubscriptions",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","showMap",1,"mat-slide"],["formControlName","identity",3,"mode"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["matInput","","type","number","min","50","name","value","formControlName","pollPeriodInMillis",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.server-url"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",3),t.ɵɵelement(5,"input",4),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,qY,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",1)(9,"div",6),t.ɵɵpipe(10,"translate"),t.ɵɵelementStart(11,"div",7),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(14,"mat-form-field",3),t.ɵɵelement(15,"input",8),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,GY,2,5,"tb-error-icon",9)(18,zY,2,0,"span",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(19,"div",1)(20,"div",6),t.ɵɵpipe(21,"translate"),t.ɵɵelementStart(22,"div",7),t.ɵɵtext(23),t.ɵɵpipe(24,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(25,"mat-form-field",3)(26,"mat-select",11),t.ɵɵtemplate(27,UY,2,2,"mat-option",12),t.ɵɵelementEnd()()(),t.ɵɵelementStart(28,"div",1)(29,"div",6),t.ɵɵpipe(30,"translate"),t.ɵɵelementStart(31,"div",7),t.ɵɵtext(32),t.ɵɵpipe(33,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"mat-form-field",3),t.ɵɵelement(35,"input",13),t.ɵɵpipe(36,"translate"),t.ɵɵtemplate(37,jY,3,5,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵtemplate(38,WY,10,10,"div",14),t.ɵɵelementStart(39,"div",1)(40,"div",6),t.ɵɵpipe(41,"translate"),t.ɵɵelementStart(42,"div",7),t.ɵɵtext(43),t.ɵɵpipe(44,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"mat-form-field",3),t.ɵɵelement(46,"input",15),t.ɵɵpipe(47,"translate"),t.ɵɵtemplate(48,$Y,3,5,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(49,"div",16)(50,"mat-slide-toggle",17)(51,"mat-label",18),t.ɵɵpipe(52,"translate"),t.ɵɵelementStart(53,"div",7),t.ɵɵtext(54),t.ɵɵpipe(55,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(56,"div",16)(57,"mat-slide-toggle",19)(58,"mat-label",18),t.ɵɵpipe(59,"translate"),t.ɵɵtext(60),t.ɵɵpipe(61,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelement(62,"tb-security-config",20),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.serverConfigFormGroup),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.serverConfigFormGroup.get("url").hasError("required")&&n.serverConfigFormGroup.get("url").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,26,"gateway.hints.opc-timeout")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,28,"gateway.timeout")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,30,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional((n.serverConfigFormGroup.get("timeoutInMillis").hasError("required")||n.serverConfigFormGroup.get("timeoutInMillis").hasError("min"))&&n.serverConfigFormGroup.get("timeoutInMillis").touched?17:18),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(21,32,"gateway.hints.security-policy")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(24,34,"gateway.security-policy")),t.ɵɵadvance(4),t.ɵɵproperty("ngForOf",n.securityPolicyTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(30,36,"gateway.hints.scan-period")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(33,38,"gateway.scan-period")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,40,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.serverConfigFormGroup.get("scanPeriodInMillis").hasError("required")||n.serverConfigFormGroup.get("scanPeriodInMillis").hasError("min"))&&n.serverConfigFormGroup.get("scanPeriodInMillis").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.hideNewFields),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(41,42,"gateway.hints.sub-check-period")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(44,44,"gateway.sub-check-period")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(47,46,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.serverConfigFormGroup.get("subCheckPeriodInMillis").hasError("required")||n.serverConfigFormGroup.get("subCheckPeriodInMillis").hasError("min"))&&n.serverConfigFormGroup.get("subCheckPeriodInMillis").touched),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(52,48,"gateway.hints.enable-subscription")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(55,50,"gateway.enable-subscription")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(59,52,"gateway.hints.show-map")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(61,54,"gateway.show-map")," "),t.ɵɵadvance(2),t.ɵɵproperty("mode",n.SecurityMode.extendedCertificates))},dependencies:t.ɵɵgetComponentDepsFactory(KY,[j,_,BY,qn,Qn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}'],changeDetection:d.OnPush})}}e("OpcServerConfigComponent",KY),Ge([I()],KY.prototype,"hideNewFields",void 0);class YY extends Ba{constructor(){super(...arguments),this.withReportStrategy=!0,this.mappingTypes=Di,this.isLegacy=!0}initBasicFormGroup(){return this.fb.group({mapping:[],server:[]})}mapConfigToFormValue(e){return{server:e.server?ja.mapServerToUpgradedVersion(e.server):{},mapping:e.server?.mapping?ja.mapMappingToUpgradedVersion(e.server.mapping):[]}}getMappedValue(e){return{server:ja.mapServerToDowngradedVersion(e)}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(YY)))(n||YY)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:YY,selectors:[["tb-opc-ua-legacy-basic-config"]],inputs:{withReportStrategy:"withReportStrategy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>YY)),multi:!0},{provide:K,useExisting:c((()=>YY)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server",3,"hideNewFields"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"required","withReportStrategy","mappingType"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-opc-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,11,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,13,"gateway.server"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("hideNewFields",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,15,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("required",!0)("withReportStrategy",n.withReportStrategy)("mappingType",n.mappingTypes.OPCUA))},dependencies:t.ɵɵgetComponentDepsFactory(YY,[j,_,BY,EY,KY]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("OpcUaLegacyBasicConfigComponent",YY),Ge([I()],YY.prototype,"withReportStrategy",void 0);class XY extends Ba{constructor(){super(...arguments),this.withReportStrategy=!0,this.mappingTypes=Di,this.isLegacy=!1}initBasicFormGroup(){return this.fb.group({mapping:[],server:[]})}mapConfigToFormValue(e){return{server:e.server??{},mapping:e.mapping??[]}}getMappedValue(e){return{server:e.server,mapping:e.mapping}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(XY)))(n||XY)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:XY,selectors:[["tb-opc-ua-basic-config"]],inputs:{withReportStrategy:"withReportStrategy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>XY)),multi:!0},{provide:K,useExisting:c((()=>XY)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server",3,"hideNewFields"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"required","withReportStrategy","mappingType"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-opc-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,11,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,13,"gateway.server"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("hideNewFields",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,15,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("required",!0)("withReportStrategy",n.withReportStrategy)("mappingType",n.mappingTypes.OPCUA))},dependencies:t.ɵɵgetComponentDepsFactory(XY,[j,_,BY,EY,KY]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("OpcUaBasicConfigComponent",XY),Ge([I()],XY.prototype,"withReportStrategy",void 0);class ZY extends Ba{constructor(){super(...arguments),this.withReportStrategy=!0,this.MappingType=Di}initBasicFormGroup(){return this.fb.group({mapping:[],requestsMapping:[],broker:[],workers:[]})}getRequestDataArray(e){const t=[];return Fe(e)&&Object.keys(e).forEach((n=>{for(const i of e[n])t.push({requestType:n,requestValue:i})})),t}getRequestDataObject(e){return e.reduce(((e,{requestType:t,requestValue:n})=>(e[t].push(n),e)),{connectRequests:[],disconnectRequests:[],attributeRequests:[],attributeUpdates:[],serverSideRpc:[]})}getBrokerMappedValue(e,t){return{...e,maxNumberOfWorkers:t.maxNumberOfWorkers??100,maxMessageNumberPerWorker:t.maxMessageNumberPerWorker??10}}writeValue(e){this.basicFormGroup.setValue(this.mapConfigToFormValue(e),{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(ZY)))(n||ZY)}})()}static{this.ɵdir=t.ɵɵdefineDirective({type:ZY,inputs:{withReportStrategy:"withReportStrategy"},features:[t.ɵɵInheritDefinitionFeature]})}}function QY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",8),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.max-number-of-workers-required"))}function JY(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",8),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.max-messages-queue-for-worker-required"))}e("MqttBasicConfigDirective",ZY),Ge([I()],ZY.prototype,"withReportStrategy",void 0);class eX{constructor(e){this.fb=e,this.destroy$=new te,this.workersConfigFormGroup=this.fb.group({maxNumberOfWorkers:[100,[$.required,$.min(1)]],maxMessageNumberPerWorker:[10,[$.required,$.min(1)]]}),this.workersConfigFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){const{maxNumberOfWorkers:t,maxMessageNumberPerWorker:n}=e;this.workersConfigFormGroup.reset({maxNumberOfWorkers:t||100,maxMessageNumberPerWorker:n||10},{emitEvent:!1})}validate(){return this.workersConfigFormGroup.valid?null:{workersConfigFormGroup:{valid:!1}}}static{this.ɵfac=function(e){return new(e||eX)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:eX,selectors:[["tb-workers-config-control"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>eX)),multi:!0},{provide:K,useExisting:c((()=>eX)),multi:!0}]),t.ɵɵStandaloneFeature],decls:21,vars:21,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",2,"width","50%",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip",""],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","type","number","min","1","formControlName","maxNumberOfWorkers",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","name","value","type","number","min","1","formControlName","maxMessageNumberPerWorker",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"div",3),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"mat-form-field",4),t.ɵɵelement(8,"input",5),t.ɵɵpipe(9,"translate"),t.ɵɵtemplate(10,QY,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"div",1)(12,"div",2),t.ɵɵpipe(13,"translate"),t.ɵɵelementStart(14,"div",3),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"mat-form-field",4),t.ɵɵelement(18,"input",7),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,JY,3,3,"mat-icon",6),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.workersConfigFormGroup),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,9,"gateway.max-number-of-workers-hint")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,11,"gateway.max-number-of-workers")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(9,13,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.workersConfigFormGroup.get("maxNumberOfWorkers").hasError("min")||n.workersConfigFormGroup.get("maxNumberOfWorkers").hasError("required")&&n.workersConfigFormGroup.get("maxNumberOfWorkers").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(13,15,"gateway.max-messages-queue-for-worker-hint")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(16,17,"gateway.max-messages-queue-for-worker")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,19,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.workersConfigFormGroup.get("maxMessageNumberPerWorker").hasError("min")||n.workersConfigFormGroup.get("maxMessageNumberPerWorker").hasError("required")&&n.workersConfigFormGroup.get("maxMessageNumberPerWorker").touched))},dependencies:t.ɵɵgetComponentDepsFactory(eX,[j,_,qn]),encapsulation:2,changeDetection:d.OnPush})}}function tX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",13),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function nX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",13),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.brokerConfigFormGroup.get("port")))}}function iX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",14),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.value),t.ɵɵadvance(),t.ɵɵtextInterpolate(e.name)}}function aX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",15),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("clientId"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.generate-client-id"))}e("WorkersConfigControlComponent",eX);class rX{constructor(e,t){this.fb=e,this.cdr=t,this.mqttVersions=ti,this.portLimits=Ii,this.destroy$=new te,this.brokerConfigFormGroup=this.fb.group({host:["",[$.required,$.pattern(an)]],port:[null,[$.required,$.min(Ii.MIN),$.max(Ii.MAX)]],version:[5,[]],clientId:["tb_gw_"+Re(5),[$.pattern(an)]],security:[]}),this.brokerConfigFormGroup.valueChanges.subscribe((e=>{this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}generate(e){this.brokerConfigFormGroup.get(e)?.patchValue("tb_gw_"+Re(5))}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){const{version:t=5,clientId:n=`tb_gw_${Re(5)}`,security:i={}}=e;this.brokerConfigFormGroup.reset({...e,version:t,clientId:n,security:i},{emitEvent:!1}),this.cdr.markForCheck()}validate(){return this.brokerConfigFormGroup.valid?null:{brokerConfigFormGroup:{valid:!1}}}static{this.ɵfac=function(e){return new(e||rX)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:rX,selectors:[["tb-broker-config-control"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>rX)),multi:!0},{provide:K,useExisting:c((()=>rX)),multi:!0}]),t.ɵɵStandaloneFeature],decls:29,vars:16,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["translate","",1,"fixed-title-width"],["formControlName","version"],[3,"value",4,"ngFor","ngForOf"],["matInput","","name","value","formControlName","clientId",3,"placeholder"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"matTooltip","click",4,"ngIf"],["formControlName","security"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"click","matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",3),t.ɵɵelement(5,"input",4),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,tX,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",1)(9,"div",2),t.ɵɵtext(10,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",3),t.ɵɵelement(12,"input",6),t.ɵɵpipe(13,"translate"),t.ɵɵtemplate(14,nX,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"div",1)(16,"div",7),t.ɵɵtext(17,"gateway.mqtt-version"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"mat-form-field",3)(19,"mat-select",8),t.ɵɵtemplate(20,iX,2,2,"mat-option",9),t.ɵɵelementEnd()()(),t.ɵɵelementStart(21,"div",1)(22,"div",7),t.ɵɵtext(23,"gateway.client-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(24,"mat-form-field",3),t.ɵɵelement(25,"input",10),t.ɵɵpipe(26,"translate"),t.ɵɵtemplate(27,aX,4,3,"button",11),t.ɵɵelementEnd()(),t.ɵɵelement(28,"tb-security-config",12),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.brokerConfigFormGroup),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,10,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.brokerConfigFormGroup.get("host").hasError("required")&&n.brokerConfigFormGroup.get("host").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(13,12,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.brokerConfigFormGroup.get("port").hasError("required")||n.brokerConfigFormGroup.get("port").hasError("min")||n.brokerConfigFormGroup.get("port").hasError("max"))&&n.brokerConfigFormGroup.get("port").touched),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.mqttVersions),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(26,14,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",!n.brokerConfigFormGroup.get("clientId").value))},dependencies:t.ɵɵgetComponentDepsFactory(rX,[j,_,BY,r$]),encapsulation:2,changeDetection:d.OnPush})}}e("BrokerConfigControlComponent",rX);class oX extends ZY{mapConfigToFormValue(e){const{broker:t,mapping:n=[],requestsMapping:i}=e;return{workers:t&&(t.maxNumberOfWorkers||t.maxMessageNumberPerWorker)?{maxNumberOfWorkers:t.maxNumberOfWorkers,maxMessageNumberPerWorker:t.maxMessageNumberPerWorker}:{},mapping:n??[],broker:t??{},requestsMapping:this.getRequestDataArray(i)}}getMappedValue(e){const{broker:t,workers:n,mapping:i,requestsMapping:a}=e||{};return{broker:this.getBrokerMappedValue(t,n),mapping:i,requestsMapping:a?.length?this.getRequestDataObject(a):{}}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(oX)))(n||oX)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:oX,selectors:[["tb-mqtt-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>oX)),multi:!0},{provide:K,useExisting:c((()=>oX)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:24,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","broker"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"withReportStrategy","required","mappingType"],["formControlName","requestsMapping",3,"withReportStrategy","mappingType"],[1,"tb-form-panel","no-border","no-padding"],["formControlName","workers"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-broker-config-control",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-tab",1),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"div",4),t.ɵɵelement(14,"tb-mapping-table",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"mat-tab",1),t.ɵɵpipe(16,"translate"),t.ɵɵelementStart(17,"div",7),t.ɵɵelement(18,"tb-workers-config-control",8),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,14,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,16,"gateway.broker.connection"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,18,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("required",!0)("mappingType",n.MappingType.DATA),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(12,20,"gateway.requests-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("mappingType",n.MappingType.REQUESTS),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(16,22,"gateway.workers-settings")))},dependencies:t.ɵɵgetComponentDepsFactory(oX,[j,_,eX,rX,EY]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("MqttBasicConfigComponent",oX);class sX extends ZY{mapConfigToFormValue(e){const{broker:t,mapping:n=[],connectRequests:i=[],disconnectRequests:a=[],attributeRequests:r=[],attributeUpdates:o=[],serverSideRpc:s=[]}=e,l=Pa.mapRequestsToUpgradedVersion({connectRequests:i,disconnectRequests:a,attributeRequests:r,attributeUpdates:o,serverSideRpc:s});return{workers:t&&(t.maxNumberOfWorkers||t.maxMessageNumberPerWorker)?{maxNumberOfWorkers:t.maxNumberOfWorkers,maxMessageNumberPerWorker:t.maxMessageNumberPerWorker}:{},mapping:Pa.mapMappingToUpgradedVersion(n)||[],broker:t||{},requestsMapping:this.getRequestDataArray(l)}}getMappedValue(e){const{broker:t,workers:n,mapping:i,requestsMapping:a}=e||{},r=a?.length?this.getRequestDataObject(a):{};return{broker:this.getBrokerMappedValue(t,n),mapping:Pa.mapMappingToDowngradedVersion(i),...Pa.mapRequestsToDowngradedVersion(r)}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(sX)))(n||sX)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:sX,selectors:[["tb-mqtt-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>sX)),multi:!0},{provide:K,useExisting:c((()=>sX)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:24,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","broker"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","mapping",3,"withReportStrategy","required","mappingType"],["formControlName","requestsMapping",3,"withReportStrategy","mappingType"],[1,"tb-form-panel","no-border","no-padding"],["formControlName","workers"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-broker-config-control",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-mapping-table",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-tab",1),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"div",4),t.ɵɵelement(14,"tb-mapping-table",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"mat-tab",1),t.ɵɵpipe(16,"translate"),t.ɵɵelementStart(17,"div",7),t.ɵɵelement(18,"tb-workers-config-control",8),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,14,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,16,"gateway.broker.connection"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,18,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("required",!0)("mappingType",n.MappingType.DATA),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(12,20,"gateway.requests-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("withReportStrategy",n.withReportStrategy)("mappingType",n.MappingType.REQUESTS),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(16,22,"gateway.workers-settings")))},dependencies:t.ɵɵgetComponentDepsFactory(sX,[j,_,eX,rX,EY]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("MqttLegacyBasicConfigComponent",sX);class lX extends A{constructor(e,t,n,i,a){super(t,n,a),this.fb=e,this.store=t,this.router=n,this.data=i,this.dialogRef=a,this.portLimits=Ii,this.modbusProtocolTypes=Object.values(Ki),this.modbusMethodTypes=Object.values(Yi),this.modbusSerialMethodTypes=Object.values(Xi),this.modbusParities=Object.values(Zi),this.modbusByteSizes=na,this.modbusBaudrates=ta,this.modbusOrderType=Object.values(Qi),this.ModbusProtocolType=Ki,this.ModbusParityLabelsMap=la,this.ModbusProtocolLabelsMap=sa,this.ModbusMethodLabelsMap=oa,this.ReportStrategyDefaultValue=en,this.modbusHelpLink=O+"/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters",this.serialSpecificControlKeys=["serialPort","baudrate","stopbits","bytesize","parity","strict"],this.tcpUdpSpecificControlKeys=["port","security","host"],this.destroy$=new te,this.showSecurityControl=this.fb.control(!1),this.initializeSlaveFormGroup(),this.updateSlaveFormGroup(),this.updateControlsEnabling(this.data.value.type),this.observeTypeChange(),this.observeShowSecurity(),this.showSecurityControl.patchValue(!!this.data.value.security&&!Se(this.data.value.security,{}))}get protocolType(){return this.slaveConfigFormGroup.get("type").value}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}cancel(){this.dialogRef.close(null)}add(){this.slaveConfigFormGroup.valid&&this.dialogRef.close(this.getSlaveResultData())}initializeSlaveFormGroup(){this.slaveConfigFormGroup=this.fb.group({type:[Ki.TCP],host:["",[$.required,$.pattern(an)]],port:[null,[$.required,$.min(Ii.MIN),$.max(Ii.MAX)]],serialPort:["",[$.required,$.pattern(an)]],method:[Yi.SOCKET,[$.required]],baudrate:[this.modbusBaudrates[0]],stopbits:[1],bytesize:[na[0]],parity:[Zi.None],strict:[!0],unitId:[null,[$.required]],deviceName:["",[$.required,$.pattern(an)]],deviceType:["",[$.required,$.pattern(an)]],timeout:[35],byteOrder:[Qi.BIG],wordOrder:[Qi.BIG],retries:[!0],retryOnEmpty:[!0],retryOnInvalid:[!0],pollPeriod:[1e3,[$.required]],connectAttemptTimeMs:[500,[$.required]],connectAttemptCount:[5,[$.required]],waitAfterFailedAttemptsMs:[3e4,[$.required]],values:[{}],security:[{}]}),this.addFieldsToFormGroup()}updateSlaveFormGroup(){this.slaveConfigFormGroup.patchValue({...this.data.value,port:this.data.value.type===Ki.Serial?null:this.data.value.port,serialPort:this.data.value.type===Ki.Serial?this.data.value.port:"",values:{attributes:this.data.value.attributes??[],timeseries:this.data.value.timeseries??[],attributeUpdates:this.data.value.attributeUpdates??[],rpc:this.data.value.rpc??[]}})}observeTypeChange(){this.slaveConfigFormGroup.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.updateControlsEnabling(e),this.updateMethodType(e)}))}updateMethodType(e){this.slaveConfigFormGroup.get("method").value!==Yi.RTU&&this.slaveConfigFormGroup.get("method").patchValue(e===Ki.Serial?Xi.ASCII:Yi.SOCKET,{emitEvent:!1})}updateControlsEnabling(e){const[t,n]=e===Ki.Serial?[this.serialSpecificControlKeys,this.tcpUdpSpecificControlKeys]:[this.tcpUdpSpecificControlKeys,this.serialSpecificControlKeys];t.forEach((e=>this.slaveConfigFormGroup.get(e)?.enable({emitEvent:!1}))),n.forEach((e=>this.slaveConfigFormGroup.get(e)?.disable({emitEvent:!1}))),this.updateSecurityEnabling(this.showSecurityControl.value)}observeShowSecurity(){this.showSecurityControl.valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.updateSecurityEnabling(e)))}updateSecurityEnabling(e){e&&this.protocolType!==Ki.Serial?this.slaveConfigFormGroup.get("security").enable({emitEvent:!1}):this.slaveConfigFormGroup.get("security").disable({emitEvent:!1})}static{this.ɵfac=function(e){return new(e||lX)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef))}}static{this.ɵdir=t.ɵɵdefineDirective({type:lX,features:[t.ɵɵInheritDefinitionFeature]})}}e("ModbusSlaveDialogAbstract",lX);const pX=()=>({maxWidth:"970px"});function cX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",20),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate3(" ",e.get("tag").value,"",": ","",e.get("value").value," ")}}function dX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21)(1,"div",22),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"span",23),t.ɵɵtext(5),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"div",24),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"span",25),t.ɵɵtext(10),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"div",24),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementStart(14,"span",25),t.ɵɵtext(15),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(3,6,n.keysType===n.ModbusValueKey.RPC_REQUESTS?"gateway.method":"gateway.key"),": "),t.ɵɵadvance(3),t.ɵɵtextInterpolate(e.get("tag").value),t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(8,8,"gateway.address"),": "),t.ɵɵadvance(3),t.ɵɵtextInterpolate(e.get("address").value),t.ɵɵadvance(2),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(13,10,"gateway.type"),": "),t.ɵɵadvance(3),t.ɵɵtextInterpolate(e.get("type").value)}}function uX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.keysType===e.ModbusValueKey.RPC_REQUESTS?"gateway.method-required":"gateway.key-required"))}}function mX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",46),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function hX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",46),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(5);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.ModbusFunctionCodeTranslationsMap.get(e))," ")}}function gX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",30)(1,"div",35),t.ɵɵtext(2,"gateway.function-code"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",32)(4,"mat-select",47),t.ɵɵtemplate(5,hX,3,4,"mat-option",37),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.functionCodesMap.get(e.get("id").value)||n.defaultFunctionCodes)}}function fX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.objects-count-required"))}function yX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.modbus.max-bit"))}function vX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",30)(1,"div",48),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.bit"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",49),t.ɵɵelement(5,"input",50),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,yX,3,3,"mat-icon",34),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(3).$implicit;t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.bit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("bit").hasError("max")&&e.get("bit").touched)}}function xX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",46),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(6);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.BitTargetTypeTranslationMap.get(e)))}}function bX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",30)(1,"div",48),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.bit-target-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",51)(5,"mat-form-field",52)(6,"mat-select",53),t.ɵɵtemplate(7,xX,3,4,"mat-option",37),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(5);t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,2,"gateway.hints.modbus.bit-target-type")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",e.bitTargetTypes)}}function wX(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,vX,8,7,"div",38)(2,bX,8,4,"div",38),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("objectsCount").value>1),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!n.hideNewFields)}}function SX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-required"))}function CX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",46),t.ɵɵelement(1,"mat-icon",62),t.ɵɵelementStart(2,"span"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(5);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("svgIcon",i.ModifierTypesMap.get(e).icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,i.ModifierTypesMap.get(e).name))}}function _X(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.modifier-invalid"))}function TX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",54)(1,"mat-expansion-panel",15)(2,"mat-expansion-panel-header",16)(3,"mat-panel-title")(4,"mat-slide-toggle",55),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label",56),t.ɵɵpipe(6,"translate"),t.ɵɵtext(7),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(9,"div",51)(10,"div",57)(11,"div",35),t.ɵɵtext(12,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",52)(14,"mat-select",58)(15,"mat-select-trigger")(16,"div",59),t.ɵɵelement(17,"mat-icon",60),t.ɵɵelementStart(18,"span"),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(21,CX,5,5,"mat-option",37),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(22,"div",30)(23,"div",35),t.ɵɵtext(24,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",49),t.ɵɵelement(26,"input",61),t.ɵɵpipe(27,"translate"),t.ɵɵtemplate(28,_X,3,3,"mat-icon",34),t.ɵɵelementEnd()()()()}if(2&e){let e,n;const i=t.ɵɵnextContext(2).$implicit,a=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("expanded",a.enableModifiersControlMap.get(i.get("id").value).value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",a.enableModifiersControlMap.get(i.get("id").value)),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,9,"gateway.hints.modbus.modifier")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(8,11,"gateway.modifier")," "),t.ɵɵadvance(10),t.ɵɵproperty("svgIcon",null==(e=a.ModifierTypesMap.get(i.get("modifierType").value))?null:e.icon),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,13,null==(n=a.ModifierTypesMap.get(i.get("modifierType").value))?null:n.name)),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",a.modifierTypes),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(27,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",i.get("modifierValue").hasError("pattern")&&i.get("modifierValue").touched)}}function IX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",45),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function EX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",30)(1,"div",63),t.ɵɵtext(2,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",32),t.ɵɵelement(4,"input",64),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,IX,3,3,"mat-icon",34),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,2,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("value").hasError("required")&&e.get("value").touched)}}function MX(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",65),2&e){const e=t.ɵɵnextContext(4);t.ɵɵproperty("defaultValue",e.ReportStrategyDefaultValue.Key)("isExpansionMode",!0)}}function kX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",26),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelement(3,"div",27),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",28)(5,"div",29),t.ɵɵtext(6,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",30)(8,"div",31),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",32),t.ɵɵelement(13,"input",33),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,uX,3,3,"mat-icon",34),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",28)(17,"div",29),t.ɵɵtext(18,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",30)(20,"div",35),t.ɵɵtext(21," gateway.type "),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",32)(23,"mat-select",36),t.ɵɵtemplate(24,mX,2,2,"mat-option",37),t.ɵɵelementEnd()()(),t.ɵɵtemplate(25,gX,6,1,"div",38),t.ɵɵelementStart(26,"div",30)(27,"div",39),t.ɵɵpipe(28,"translate"),t.ɵɵtext(29,"gateway.objects-count"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",32),t.ɵɵelement(31,"input",40),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,fX,3,3,"mat-icon",34),t.ɵɵelementEnd()(),t.ɵɵtemplate(34,wX,3,2,"ng-container",41),t.ɵɵelementStart(35,"div",30)(36,"div",39),t.ɵɵpipe(37,"translate"),t.ɵɵtext(38,"gateway.address"),t.ɵɵelementEnd(),t.ɵɵelementStart(39,"mat-form-field",32),t.ɵɵelement(40,"input",42),t.ɵɵpipe(41,"translate"),t.ɵɵtemplate(42,SX,3,3,"mat-icon",34),t.ɵɵelementEnd()(),t.ɵɵtemplate(43,TX,29,17,"div",43)(44,EX,7,4,"div",38)(45,MX,1,2,"tb-report-strategy",44),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,20,"gateway.hints.modbus.data-keys")," "),t.ɵɵadvance(2),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/modbus-functions-data-types_fn")("tb-help-popup-style",t.ɵɵpureFunction0(36,pX)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(9,22,n.keysType===n.ModbusValueKey.RPC_REQUESTS?"gateway.hints.modbus.method":"gateway.hints.modbus.key")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,24,n.keysType===n.ModbusValueKey.RPC_REQUESTS?"gateway.method-name":"gateway.key")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,26,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("tag").hasError("required")&&e.get("tag").touched),t.ɵɵadvance(9),t.ɵɵproperty("ngForOf",n.modbusDataTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.withFunctionCode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(28,28,"gateway.hints.modbus.objects-count")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,30,"gateway.set")),t.ɵɵproperty("readonly",!n.ModbusEditableDataTypes.includes(e.get("type").value)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("objectsCount").hasError("required")&&e.get("objectsCount").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("type").value===n.ModbusDataType.BITS),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(37,32,"gateway.hints.modbus.address")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(41,34,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("address").hasError("required")&&e.get("address").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.showModifiersMap.get(e.get("id").value)),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isMaster),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.withReportStrategy)}}function PX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",12)(1,"div",13),t.ɵɵelementContainerStart(2,14),t.ɵɵelementStart(3,"mat-expansion-panel",15)(4,"mat-expansion-panel-header",16)(5,"mat-panel-title"),t.ɵɵtemplate(6,cX,2,3,"div",17)(7,dX,16,12,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()(),t.ɵɵtemplate(9,kX,46,37,"ng-template",18),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",19),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.last,a=t.ɵɵreference(8),r=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",r.isMaster)("ngIfElse",a),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,r.deleteKeyTitle))}}function DX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10),t.ɵɵtemplate(1,PX,14,7,"div",11),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.keysListFormArray.controls)("ngForTrackBy",e.trackByControlId)}}function OX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",66)(1,"span",67),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class AX{constructor(e,t){this.fb=e,this.popover=t,this.isMaster=!1,this.hideNewFields=!1,this.keysDataApplied=new u,this.withFunctionCode=!0,this.withReportStrategy=!0,this.enableModifiersControlMap=new Map,this.showModifiersMap=new Map,this.functionCodesMap=new Map,this.defaultFunctionCodes=[],this.modbusDataTypes=Object.values(Wt),this.modifierTypes=Object.values(ma),this.bitTargetTypes=Object.values(aa),this.BitTargetTypeTranslationMap=ra,this.ModbusEditableDataTypes=$t,this.ModbusFunctionCodeTranslationsMap=Zt,this.ModifierTypesMap=ha,this.ReportStrategyDefaultValue=en,this.ModbusDataType=Wt,this.ModbusValueKey=ea,this.destroy$=new te,this.defaultReadFunctionCodes=[3,4],this.bitsReadFunctionCodes=[1,2],this.defaultWriteFunctionCodes=[6,16],this.bitsWriteFunctionCodes=[5,15]}ngOnInit(){this.withFunctionCode=!this.isMaster||this.keysType!==ea.ATTRIBUTES&&this.keysType!==ea.TIMESERIES,this.withReportStrategy=!(this.isMaster||this.keysType!==ea.ATTRIBUTES&&this.keysType!==ea.TIMESERIES||this.hideNewFields),this.keysListFormArray=this.prepareKeysFormArray(this.values),this.defaultFunctionCodes=this.getDefaultFunctionCodes()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}trackByControlId(e,t){return t.value.id}addKey(){const e=Re(5),t=this.fb.group({tag:["",[$.required,$.pattern(an)]],value:[{value:"",disabled:!this.isMaster},[$.required,$.pattern(an)]],type:[Wt.BYTES,[$.required]],address:[null,[$.required]],objectsCount:[1,[$.required]],functionCode:[{value:this.getDefaultFunctionCodes()[0],disabled:!this.withFunctionCode},[$.required]],reportStrategy:[{value:null,disabled:!this.withReportStrategy}],modifierType:[{value:ma.MULTIPLIER,disabled:!0}],modifierValue:[{value:1,disabled:!0},[$.pattern(on)]],bit:[{value:null,disabled:!0}],bitTargetType:[{value:aa.IntegerType,disabled:!0}],id:[{value:e,disabled:!0}]});this.showModifiersMap.set(e,!1),this.enableModifiersControlMap.set(e,this.fb.control(!1)),this.observeKeyDataType(t),this.observeObjectsCount(t),this.observeEnableModifier(t),this.keysListFormArray.push(t)}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover.hide()}applyKeysData(){this.keysDataApplied.emit(this.getFormValue())}getFormValue(){return this.mapKeysWithModifier(this.withReportStrategy?this.cleanUpEmptyStrategies(this.keysListFormArray.value):this.keysListFormArray.value)}cleanUpEmptyStrategies(e){return e.map((e=>{const{reportStrategy:t,...n}=e;return t?e:n}))}mapKeysWithModifier(e){return e.map(((e,t)=>{if(this.showModifiersMap.get(this.keysListFormArray.controls[t].get("id").value)){const{modifierType:t,modifierValue:n,...i}=e;return t?{...i,[t]:n}:i}return e}))}prepareKeysFormArray(e){const t=[];return e&&e.forEach((e=>{const n=this.createDataKeyFormGroup(e);this.observeKeyDataType(n),this.observeObjectsCount(n),this.observeEnableModifier(n),this.functionCodesMap.set(n.get("id").value,this.getFunctionCodes(e.type)),t.push(n)})),this.fb.array(t)}createDataKeyFormGroup(e){const{tag:t,value:n,type:i,address:a,objectsCount:r,functionCode:o,multiplier:s,divider:l,reportStrategy:p,bit:c,bitTargetType:d}=e,u=Re(5),m=this.shouldShowModifier(i);return this.showModifiersMap.set(u,m),this.enableModifiersControlMap.set(u,this.fb.control((s||l)&&m)),this.fb.group({tag:[t,[$.required,$.pattern(an)]],value:[{value:n,disabled:!this.isMaster},[$.required,$.pattern(an)]],type:[i,[$.required]],address:[a,[$.required]],objectsCount:[r,[$.required]],functionCode:[{value:o,disabled:!this.withFunctionCode},[$.required]],modifierType:[{value:l?ma.DIVIDER:ma.MULTIPLIER,disabled:!this.enableModifiersControlMap.get(u).value}],bit:[{value:c,disabled:i!==Wt.BITS||r<2},[$.max(r-1)]],bitTargetType:[{value:d??aa.IntegerType,disabled:i!==Wt.BITS||this.hideNewFields}],modifierValue:[{value:s??l??1,disabled:!this.enableModifiersControlMap.get(u).value},[$.pattern(on)]],id:[{value:u,disabled:!0}],reportStrategy:[{value:p,disabled:!this.withReportStrategy}]})}shouldShowModifier(e){return!(this.isMaster||this.keysType!==ea.ATTRIBUTES&&this.keysType!==ea.TIMESERIES||this.ModbusEditableDataTypes.includes(e))}observeKeyDataType(e){e.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((t=>{this.ModbusEditableDataTypes.includes(t)||e.get("objectsCount").patchValue(Kt[t],{emitEvent:!1}),this.toggleBitsFields(e);const n=this.shouldShowModifier(t);this.showModifiersMap.set(e.get("id").value,n),this.updateFunctionCodes(e,t)}))}observeObjectsCount(e){e.get("objectsCount").valueChanges.pipe(ue((()=>e.get("type").value===Wt.BITS)),le(this.destroy$)).subscribe((()=>this.toggleBitsFields(e)))}toggleBitsFields(e){const{objectsCount:t,type:n,bit:i,bitTargetType:a}=e.controls,r=n.value===Wt.BITS,o=t.value>1;r&&o?(i.enable({emitEvent:!1}),i.setValidators($.max(t.value-1))):i.disable({emitEvent:!1}),i.updateValueAndValidity({emitEvent:!1}),a[r?"enable":"disable"]({emitEvent:!1})}observeEnableModifier(e){this.enableModifiersControlMap.get(e.get("id").value).valueChanges.pipe(le(this.destroy$)).subscribe((t=>this.toggleModifierControls(e,t)))}toggleModifierControls(e,t){const n=e.get("modifierType"),i=e.get("modifierValue");n[t?"enable":"disable"]({emitEvent:!1}),i[t?"enable":"disable"]({emitEvent:!1}),n.markAsDirty(),i.markAsDirty()}updateFunctionCodes(e,t){const n=this.getFunctionCodes(t);this.functionCodesMap.set(e.get("id").value,n),n.includes(e.get("functionCode").value)||e.get("functionCode").patchValue(n[0],{emitEvent:!1})}getFunctionCodes(e){const t=[...e===Wt.BITS?this.bitsWriteFunctionCodes:[],...this.defaultWriteFunctionCodes];if(this.keysType===ea.ATTRIBUTES_UPDATES)return t.sort(((e,t)=>e-t));const n=[...this.defaultReadFunctionCodes];return e===Wt.BITS&&n.push(...this.bitsReadFunctionCodes),this.keysType===ea.RPC_REQUESTS&&n.push(...t),n.sort(((e,t)=>e-t))}getDefaultFunctionCodes(){return this.keysType===ea.ATTRIBUTES_UPDATES?this.defaultWriteFunctionCodes:this.keysType===ea.RPC_REQUESTS?[...this.defaultReadFunctionCodes,...this.defaultWriteFunctionCodes]:this.defaultReadFunctionCodes}static{this.ɵfac=function(e){return new(e||AX)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(it.TbPopoverComponent))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:AX,selectors:[["tb-modbus-data-keys-panel"]],inputs:{isMaster:"isMaster",hideNewFields:"hideNewFields",panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keysType:"keysType",values:"values"},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:19,vars:16,consts:[["noKeys",""],["tagName",""],[1,"tb-modbus-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],["class","title-container","tbTruncateWithTooltip","",4,"ngIf","ngIfElse"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],["tbTruncateWithTooltip","",1,"title-container"],[1,"tb-flex"],[1,"title-container","tb-flex"],["tbTruncateWithTooltip","",1,"key-label"],[1,"title-container"],[1,"key-label"],[1,"tb-form-hint","tb-primary-fill","tb-flex","align-center"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","tag",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width"],["formControlName","type"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","1","max","50000","name","value","formControlName","objectsCount",3,"placeholder","readonly"],[4,"ngIf"],["matInput","","type","number","min","0","max","50000","name","value","formControlName","address",3,"placeholder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],["class","stroked tb-form-panel","formControlName","reportStrategy",3,"defaultValue","isExpansionMode",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"],["formControlName","functionCode"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-inline-field","tb-suffix-absolute","flex"],["matInput","","formControlName","bit","step","1","type","number","min","0",3,"placeholder"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","fill-width"],["formControlName","bitTargetType"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"mat-slide",3,"click","formControl"],[3,"tb-hint-tooltip-icon"],[1,"tb-form-row","column-xs","w-full"],["formControlName","modifierType"],[1,"tb-flex","align-center"],[1,"tb-mat-18",3,"svgIcon"],["matInput","","required","","formControlName","modifierValue","step","0.1","type","number",3,"placeholder"],[1,"tb-mat-20",3,"svgIcon"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","value",3,"placeholder"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"defaultValue","isExpansionMode"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"div",3)(2,"div",4),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,DX,2,2,"div",5),t.ɵɵelementStart(6,"div")(7,"button",6),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(10,OX,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(12,"div",7)(13,"button",8),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.applyKeysData())})),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(11);t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,8,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")",""),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.keysListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,10,n.addKeyTitle)," "),t.ɵɵadvance(6),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,12,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,14,"action.apply")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(AX,[j,_,Xn,qn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{width:180px}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .key-label[_ngcontent-%COMP%]{font-weight:400}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:500px;overflow:auto}[_nghost-%COMP%] .tb-modbus-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}']})}}e("ModbusDataKeysPanelComponent",AX),Ge([I()],AX.prototype,"isMaster",void 0),Ge([I()],AX.prototype,"hideNewFields",void 0);const FX=()=>({$implicit:null}),RX=e=>({$implicit:e});function BX(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",7),t.ɵɵelementContainer(2,8),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(),n=t.ɵɵreference(4);t.ɵɵadvance(),t.ɵɵproperty("formGroup",e.valuesFormGroup),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",n)("ngTemplateOutletContext",t.ɵɵpureFunction0(3,FX))}}function NX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-tab",11),t.ɵɵpipe(1,"translate"),t.ɵɵelementStart(2,"div",7),t.ɵɵelementContainer(3,8),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2),a=t.ɵɵreference(4);t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(1,4,i.ModbusValuesTranslationsMap.get(e))),t.ɵɵadvance(2),t.ɵɵproperty("formGroup",i.valuesFormGroup.get(e)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",a)("ngTemplateOutletContext",t.ɵɵpureFunction1(6,RX,e))}}function LX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-tab-group",9),t.ɵɵtemplate(1,NX,4,8,"mat-tab",10),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("formGroup",e.valuesFormGroup),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.modbusRegisterTypes)}}function VX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function qX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function GX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function zX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.tag," ")}}function UX(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",12)(1,"div",13),t.ɵɵtext(2,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",14)(4,"mat-chip-listbox",15),t.ɵɵtemplate(5,VX,2,1,"mat-chip",16),t.ɵɵelementStart(6,"mat-chip",17),t.ɵɵelement(7,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"button",19,2),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(9),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.ATTRIBUTES,i))})),t.ɵɵelementStart(11,"tb-icon",20),t.ɵɵtext(12,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(13,"div",12)(14,"div",13),t.ɵɵtext(15,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",14)(17,"mat-chip-listbox",15),t.ɵɵtemplate(18,qX,2,1,"mat-chip",16),t.ɵɵelementStart(19,"mat-chip",17),t.ɵɵelement(20,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(21,"button",19,3),t.ɵɵpipe(23,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(22),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.TIMESERIES,i))})),t.ɵɵelementStart(24,"tb-icon",20),t.ɵɵtext(25,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(26,"div",12)(27,"div",13),t.ɵɵtext(28,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"div",14)(30,"mat-chip-listbox",15),t.ɵɵtemplate(31,GX,2,1,"mat-chip",16),t.ɵɵelementStart(32,"mat-chip",17),t.ɵɵelement(33,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"button",19,4),t.ɵɵpipe(36,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(35),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.ATTRIBUTES_UPDATES,i))})),t.ɵɵelementStart(37,"tb-icon",20),t.ɵɵtext(38,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(39,"div",12)(40,"div",13),t.ɵɵtext(41,"gateway.rpc-requests"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"div",14)(43,"mat-chip-listbox",15),t.ɵɵtemplate(44,zX,2,1,"mat-chip",16),t.ɵɵelementStart(45,"mat-chip",17),t.ɵɵelement(46,"label",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(47,"button",19,5),t.ɵɵpipe(49,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵreference(48),r=t.ɵɵnextContext();return t.ɵɵresetView(r.manageKeys(n,a,r.ModbusValueKey.RPC_REQUESTS,i))})),t.ɵɵelementStart(50,"tb-icon",20),t.ɵɵtext(51,"edit"),t.ɵɵelementEnd()()()()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,16,"action.edit")),t.ɵɵproperty("disabled",i.disabled),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.TIMESERIES,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.TIMESERIES,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(23,18,"action.edit")),t.ɵɵproperty("disabled",i.disabled),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES_UPDATES,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.ATTRIBUTES_UPDATES,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(36,20,"action.edit")),t.ɵɵproperty("disabled",i.disabled),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",i.getValueGroup(i.ModbusValueKey.RPC_REQUESTS,e).value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",i.getValueGroup(i.ModbusValueKey.RPC_REQUESTS,e).value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(49,22,"action.edit")),t.ɵɵproperty("disabled",i.disabled)}}class jX{constructor(e,t,n,i,a,r){this.fb=e,this.popoverService=t,this.renderer=n,this.viewContainerRef=i,this.cdr=a,this.destroyRef=r,this.singleMode=!1,this.hideNewFields=!1,this.disabled=!1,this.modbusRegisterTypes=Object.values(Ji),this.modbusValueKeys=Object.values(ea),this.ModbusValuesTranslationsMap=ia,this.ModbusValueKey=ea}ngOnInit(){this.initializeValuesFormGroup(),this.observeValuesChanges()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){if(this.singleMode)this.valuesFormGroup.setValue(this.getSingleRegisterState(e),{emitEvent:!1});else{const{holding_registers:t,coils_initializer:n,input_registers:i,discrete_inputs:a}=e;this.valuesFormGroup.setValue({holding_registers:this.getSingleRegisterState(t),coils_initializer:this.getSingleRegisterState(n),input_registers:this.getSingleRegisterState(i),discrete_inputs:this.getSingleRegisterState(a)},{emitEvent:!1})}this.cdr.markForCheck()}validate(){return this.valuesFormGroup.valid?null:{valuesFormGroup:{valid:!1}}}setDisabledState(e){this.disabled=e,this.cdr.markForCheck()}getValueGroup(e,t){return t?this.valuesFormGroup.get(t).get(e):this.valuesFormGroup.get(e)}manageKeys(e,t,n,i){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const a=t._elementRef.nativeElement;if(this.popoverService.hasPopover(a))return void this.popoverService.hidePopover(a);const r=this.getValueGroup(n,i),o={values:r.value,isMaster:!this.singleMode,keysType:n,panelTitle:pa.get(n),addKeyTitle:ca.get(n),deleteKeyTitle:da.get(n),noKeysText:ua.get(n),hideNewFields:this.hideNewFields};this.popoverComponent=this.popoverService.displayPopover(a,this.renderer,this.viewContainerRef,AX,"leftBottom",!1,null,o,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(bn(this.destroyRef)).subscribe((e=>{this.popoverComponent.hide(),r.patchValue(e),r.markAsDirty(),this.cdr.markForCheck()}))}initializeValuesFormGroup(){const e=()=>this.fb.group(this.modbusValueKeys.reduce(((e,t)=>(e[t]=this.fb.control([[],[]]),e)),{}));this.singleMode?this.valuesFormGroup=e():this.valuesFormGroup=this.fb.group(this.modbusRegisterTypes.reduce(((t,n)=>(t[n]=e(),t)),{}))}observeValuesChanges(){this.valuesFormGroup.valueChanges.pipe(bn(this.destroyRef)).subscribe((e=>{this.onChange(e),this.onTouched()}))}getSingleRegisterState(e){return{attributes:e?.attributes??[],timeseries:e?.timeseries??[],attributeUpdates:e?.attributeUpdates??[],rpc:e?.rpc??[]}}static{this.ɵfac=function(e){return new(e||jX)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:jX,selectors:[["tb-modbus-values"]],inputs:{singleMode:"singleMode",hideNewFields:"hideNewFields"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>jX)),multi:!0},{provide:K,useExisting:c((()=>jX)),multi:!0}]),t.ɵɵStandaloneFeature],decls:5,vars:2,consts:[["multipleView",""],["singleView",""],["attributesButton",""],["telemetryButton",""],["attributesUpdatesButton",""],["rpcRequestsButton",""],[4,"ngIf","ngIfElse"],[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"formGroup"],[3,"label",4,"ngFor","ngForOf"],[3,"label"],[1,"tb-form-row","space-between","tb-flex"],["translate","",1,"fixed-title-width"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[4,"ngFor","ngForOf"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","disabled","matTooltip"],["matButtonIcon",""]],template:function(e,n){if(1&e&&t.ɵɵtemplate(0,BX,3,4,"ng-container",6)(1,LX,2,2,"ng-template",null,0,t.ɵɵtemplateRefExtractor)(3,UX,52,24,"ng-template",null,1,t.ɵɵtemplateRefExtractor),2&e){const e=t.ɵɵreference(2);t.ɵɵproperty("ngIf",n.singleMode)("ngIfElse",e)}},dependencies:t.ɵɵgetComponentDepsFactory(jX,[j,_,Gn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .mat-mdc-tab-body-wrapper{min-height:320px} .mdc-evolution-chip-set__chips{align-items:center}'],changeDetection:d.OnPush})}}function HX(e,n){1&e&&(t.ɵɵelementStart(0,"div",2)(1,"div",10),t.ɵɵtext(2,"gateway.server-hostname"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",5)(4,"mat-form-field",6),t.ɵɵelement(5,"input",16),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,1,"gateway.set")))}function WX(e,n){1&e&&(t.ɵɵelementStart(0,"div",17)(1,"mat-slide-toggle",18)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.request-client-certificate")," "))}e("ModbusValuesComponent",jX),Ge([I()],jX.prototype,"singleMode",void 0),Ge([I()],jX.prototype,"hideNewFields",void 0);class $X{constructor(e,t){this.fb=e,this.cdr=t,this.isMaster=!1,this.disabled=!1,this.destroy$=new te,this.securityConfigFormGroup=this.fb.group({certfile:["",[$.pattern(an)]],keyfile:["",[$.pattern(an)]],password:["",[$.pattern(an)]],server_hostname:["",[$.pattern(an)]],reqclicert:[{value:!1,disabled:!0}]}),this.observeValueChanges()}ngOnChanges(){this.updateMasterEnabling()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}setDisabledState(e){this.disabled=e,this.disabled?this.securityConfigFormGroup.disable({emitEvent:!1}):this.securityConfigFormGroup.enable({emitEvent:!1}),this.updateMasterEnabling(),this.cdr.markForCheck()}validate(){return this.securityConfigFormGroup.valid?null:{securityConfigFormGroup:{valid:!1}}}writeValue(e){const{certfile:t,password:n,keyfile:i,server_hostname:a}=e,r={certfile:t??"",password:n??"",keyfile:i??"",server_hostname:a??"",reqclicert:!!e.reqclicert};this.securityConfigFormGroup.reset(r,{emitEvent:!1})}updateMasterEnabling(){this.isMaster?(this.disabled||this.securityConfigFormGroup.get("reqclicert").enable({emitEvent:!1}),this.securityConfigFormGroup.get("server_hostname").disable({emitEvent:!1})):(this.disabled||this.securityConfigFormGroup.get("server_hostname").enable({emitEvent:!1}),this.securityConfigFormGroup.get("reqclicert").disable({emitEvent:!1}))}observeValueChanges(){this.securityConfigFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}static{this.ɵfac=function(e){return new(e||$X)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:$X,selectors:[["tb-modbus-security-config"]],inputs:{isMaster:"isMaster"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>$X)),multi:!0},{provide:K,useExisting:c((()=>$X)),multi:!0}]),t.ɵɵNgOnChangesFeature,t.ɵɵStandaloneFeature],decls:33,vars:21,consts:[[1,"tb-form-panel","no-border","no-padding",3,"formGroup"],[1,"tb-form-hint","tb-primary-fill"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["tbTruncateWithTooltip","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","certfile",3,"placeholder"],[1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["matInput","","name","value","formControlName","keyfile",3,"placeholder"],["translate","",1,"fixed-title-width"],["matInput","","type","password","name","value","formControlName","password",3,"placeholder"],["matSuffix","",1,"tb-flex","no-gap","align-center","fill-height"],[1,"tb-flex","align-center","fill-height"],["class","tb-form-row space-between tb-flex fill-width",4,"ngIf"],["class","tb-form-row",4,"ngIf"],["matInput","","name","value","formControlName","server_hostname",3,"placeholder"],[1,"tb-form-row"],["formControlName","reqclicert",1,"mat-slide"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",2)(5,"div",3),t.ɵɵpipe(6,"translate"),t.ɵɵelementStart(7,"span",4),t.ɵɵtext(8,"gateway.client-cert-path"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",5)(10,"mat-form-field",6),t.ɵɵelement(11,"input",7),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(13,"div",2)(14,"div",8),t.ɵɵpipe(15,"translate"),t.ɵɵelementStart(16,"span",4),t.ɵɵtext(17,"gateway.private-key-path"),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",5)(19,"mat-form-field",6),t.ɵɵelement(20,"input",9),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"div",2)(23,"div",10),t.ɵɵtext(24,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"div",5)(26,"mat-form-field",6),t.ɵɵelement(27,"input",11),t.ɵɵpipe(28,"translate"),t.ɵɵelementStart(29,"div",12),t.ɵɵelement(30,"tb-toggle-password",13),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(31,HX,7,3,"div",14)(32,WX,5,3,"div",15),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.securityConfigFormGroup),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,9,"gateway.hints.path-in-os")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,11,"gateway.hints.ca-cert")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(12,13,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(15,15,"gateway.private-key-path")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,17,"gateway.set")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,19,"gateway.set")),t.ɵɵadvance(4),t.ɵɵproperty("ngIf",!n.isMaster),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.isMaster))},dependencies:t.ɵɵgetComponentDepsFactory($X,[j,_,qn]),encapsulation:2,changeDetection:d.OnPush})}}function KX(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusProtocolLabelsMap.get(e))}}function YX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function XX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",53),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,YX,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("host").hasError("required")&&e.slaveConfigFormGroup.get("host").touched)}}function ZX(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.slaveConfigFormGroup.get("port")))}}function QX(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",55),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,ZX,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.hints.modbus.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",e.portLimits.MIN),t.ɵɵpropertyInterpolate("max",e.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.slaveConfigFormGroup.get("port").hasError("required")||e.slaveConfigFormGroup.get("port").hasError("min")||e.slaveConfigFormGroup.get("port").hasError("max"))&&e.slaveConfigFormGroup.get("port").touched)}}function JX(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.port-required"))}function eZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",56),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,JX,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.serial-port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("serialPort").hasError("required")&&e.slaveConfigFormGroup.get("serialPort").touched)}}function tZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusMethodLabelsMap.get(e))}}function nZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function iZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function aZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusParityLabelsMap.get(e))}}function rZ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",17)(2,"div",18),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4,"gateway.baudrate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",19)(6,"mat-select",57),t.ɵɵtemplate(7,nZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",17)(9,"div",18),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11,"gateway.bytesize"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",19)(13,"mat-select",58),t.ɵɵtemplate(14,iZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",17)(16,"div",18),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"gateway.stopbits"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"mat-form-field",19),t.ɵɵelement(20,"input",59),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"div",17)(23,"div",18),t.ɵɵpipe(24,"translate"),t.ɵɵtext(25,"gateway.parity"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",19)(27,"mat-select",60),t.ɵɵtemplate(28,aZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(29,"div",36)(30,"mat-slide-toggle",61)(31,"mat-label",38),t.ɵɵpipe(32,"translate"),t.ɵɵtext(33),t.ɵɵpipe(34,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.modbus.baudrate")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusBaudrates),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,12,"gateway.hints.modbus.bytesize")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusByteSizes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(17,14,"gateway.hints.modbus.stopbits")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,16,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(24,18,"gateway.hints.modbus.parity")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusParities),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(32,20,"gateway.hints.modbus.strict")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(34,22,"gateway.strict")," ")}}function oZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.unit-id-required"))}function sZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function lZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function pZ(e,n){1&e&&(t.ɵɵelementStart(0,"div",36)(1,"mat-slide-toggle",62)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.send-data-on-change")," "))}function cZ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",63),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("defaultValue",e.ReportStrategyDefaultValue.Device)("isExpansionMode",!0)}}function dZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function uZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function mZ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",64)(1,"mat-expansion-panel",65)(2,"mat-expansion-panel-header",66)(3,"mat-panel-title")(4,"mat-slide-toggle",67),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(8,"tb-modbus-security-config",68),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("expanded",e.showSecurityControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showSecurityControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,3,"gateway.tls-connection")," ")}}e("ModbusSecurityConfigComponent",$X),Ge([I()],$X.prototype,"isMaster",void 0);class hZ extends lX{constructor(e,t,n,i,a){super(e,t,n,i,a),this.fb=e,this.store=t,this.router=n,this.data=i,this.dialogRef=a}getSlaveResultData(){const{values:e,type:t,serialPort:n,...i}=this.slaveConfigFormGroup.value,a={...i,type:t,...e};return t===Ki.Serial&&(a.port=n),a.reportStrategy||delete a.reportStrategy,Te(a),a}addFieldsToFormGroup(){this.slaveConfigFormGroup.addControl("reportStrategy",this.fb.control(null))}static{this.ɵfac=function(e){return new(e||hZ)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:hZ,selectors:[["tb-modbus-slave-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:141,vars:97,consts:[["serialPort",""],["reportStrategy",""],[1,"slaves-config-container"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"tb-form-panel",3,"formGroup"],[1,"stroked","tb-form-panel"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","method"],[4,"ngIf"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","unitId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],["class","tb-form-row",4,"ngIf","ngIfElse"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["formControlName","byteOrder"],["formControlName","wordOrder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],[1,"tb-form-row"],["formControlName","retries",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","retryOnEmpty",1,"mat-slide"],["formControlName","retryOnInvalid",1,"mat-slide"],[1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","100","name","value","formControlName","pollPeriod",3,"placeholder"],["translate","",1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","500","name","value","formControlName","connectAttemptTimeMs",3,"placeholder"],["matInput","","type","number","min","1","name","value","formControlName","connectAttemptCount",3,"placeholder"],["matInput","","type","number","min","30000","name","value","formControlName","waitAfterFailedAttemptsMs",3,"placeholder"],["formControlName","values",3,"singleMode","hideNewFields"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matInput","","name","value","formControlName","serialPort",3,"placeholder"],["formControlName","baudrate"],["formControlName","bytesize"],["matInput","","type","number","min","0","name","value","formControlName","stopbits",3,"placeholder"],["formControlName","parity"],["formControlName","strict",1,"mat-slide"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"defaultValue","isExpansionMode"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide","justify-start",3,"click","formControl"],["formControlName","security",1,"security-config"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"mat-toolbar",3)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",4)(6,"div",5),t.ɵɵelementStart(7,"button",6),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",7),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",8)(11,"div",9)(12,"div",10)(13,"div",11)(14,"div",12),t.ɵɵtext(15,"gateway.server-connection"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"tb-toggle-select",13),t.ɵɵtemplate(17,KX,2,2,"tb-toggle-option",14),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",10),t.ɵɵtemplate(19,XX,8,7,"div",15)(20,QX,8,9,"div",16)(21,eZ,8,7,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(23,"div",17)(24,"div",18),t.ɵɵpipe(25,"translate"),t.ɵɵtext(26," gateway.method "),t.ɵɵelementEnd(),t.ɵɵelementStart(27,"mat-form-field",19)(28,"mat-select",20),t.ɵɵtemplate(29,tZ,2,2,"mat-option",14),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(30,rZ,35,24,"ng-container",21),t.ɵɵelementStart(31,"div",17)(32,"div",22),t.ɵɵpipe(33,"translate"),t.ɵɵtext(34,"gateway.unit-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(35,"mat-form-field",19),t.ɵɵelement(36,"input",23),t.ɵɵpipe(37,"translate"),t.ɵɵtemplate(38,oZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(39,"div",17)(40,"div",25),t.ɵɵtext(41,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"mat-form-field",19),t.ɵɵelement(43,"input",26),t.ɵɵpipe(44,"translate"),t.ɵɵtemplate(45,sZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"div",17)(47,"div",25),t.ɵɵtext(48,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(49,"mat-form-field",19),t.ɵɵelement(50,"input",27),t.ɵɵpipe(51,"translate"),t.ɵɵtemplate(52,lZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵtemplate(53,pZ,5,3,"div",28)(54,cZ,1,2,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(56,"div",29)(57,"mat-expansion-panel",30)(58,"mat-expansion-panel-header")(59,"mat-panel-title")(60,"div",31),t.ɵɵtext(61,"gateway.advanced-connection-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(62,"div",10)(63,"div",17)(64,"div",18),t.ɵɵpipe(65,"translate"),t.ɵɵtext(66,"gateway.connection-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(67,"mat-form-field",19),t.ɵɵelement(68,"input",32),t.ɵɵpipe(69,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(70,"div",17)(71,"div",18),t.ɵɵpipe(72,"translate"),t.ɵɵtext(73,"gateway.byte-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(74,"mat-form-field",19)(75,"mat-select",33),t.ɵɵtemplate(76,dZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(77,"div",17)(78,"div",18),t.ɵɵpipe(79,"translate"),t.ɵɵtext(80,"gateway.word-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(81,"mat-form-field",19)(82,"mat-select",34),t.ɵɵtemplate(83,uZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵtemplate(84,mZ,9,5,"div",35),t.ɵɵelementStart(85,"div",36)(86,"mat-slide-toggle",37)(87,"mat-label",38),t.ɵɵpipe(88,"translate"),t.ɵɵtext(89),t.ɵɵpipe(90,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(91,"div",36)(92,"mat-slide-toggle",39)(93,"mat-label",38),t.ɵɵpipe(94,"translate"),t.ɵɵtext(95),t.ɵɵpipe(96,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(97,"div",36)(98,"mat-slide-toggle",40)(99,"mat-label",38),t.ɵɵpipe(100,"translate"),t.ɵɵtext(101),t.ɵɵpipe(102,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(103,"div",17)(104,"div",41),t.ɵɵpipe(105,"translate"),t.ɵɵelementStart(106,"span",42),t.ɵɵtext(107," gateway.poll-period "),t.ɵɵelementEnd()(),t.ɵɵelementStart(108,"mat-form-field",19),t.ɵɵelement(109,"input",43),t.ɵɵpipe(110,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(111,"div",17)(112,"div",44),t.ɵɵpipe(113,"translate"),t.ɵɵtext(114,"gateway.connect-attempt-time"),t.ɵɵelementEnd(),t.ɵɵelementStart(115,"mat-form-field",19),t.ɵɵelement(116,"input",45),t.ɵɵpipe(117,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(118,"div",17)(119,"div",44),t.ɵɵpipe(120,"translate"),t.ɵɵtext(121,"gateway.connect-attempt-count"),t.ɵɵelementEnd(),t.ɵɵelementStart(122,"mat-form-field",19),t.ɵɵelement(123,"input",46),t.ɵɵpipe(124,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(125,"div",17)(126,"div",44),t.ɵɵpipe(127,"translate"),t.ɵɵtext(128,"gateway.wait-after-failed-attempts"),t.ɵɵelementEnd(),t.ɵɵelementStart(129,"mat-form-field",19),t.ɵɵelement(130,"input",47),t.ɵɵpipe(131,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(132,"div",29),t.ɵɵelement(133,"tb-modbus-values",48),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(134,"div",49)(135,"button",50),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(136),t.ɵɵpipe(137,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(138,"button",51),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(139),t.ɵɵpipe(140,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(22),i=t.ɵɵreference(55);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,45,"gateway.server-slave")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.modbusHelpLink),t.ɵɵadvance(4),t.ɵɵproperty("formGroup",n.slaveConfigFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",n.modbusProtocolTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(25,47,"gateway.hints.modbus.framer-type")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.protocolType===n.ModbusProtocolType.Serial?n.modbusSerialMethodTypes:n.modbusMethodTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType===n.ModbusProtocolType.Serial),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(33,49,"gateway.hints.modbus.unit-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(37,51,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("unitId").hasError("required")&&n.slaveConfigFormGroup.get("unitId").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(44,53,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceName").hasError("required")&&n.slaveConfigFormGroup.get("deviceName").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(51,55,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceType").hasError("required")&&n.slaveConfigFormGroup.get("deviceType").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.data.hideNewFields)("ngIfElse",i),t.ɵɵadvance(11),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(65,57,"gateway.hints.modbus.connection-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(69,59,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(72,61,"gateway.hints.modbus.byte-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(79,63,"gateway.hints.modbus.word-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(88,65,"gateway.hints.modbus.retries")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(90,67,"gateway.retries")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(94,69,"gateway.hints.modbus.retries-on-empty")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(96,71,"gateway.retries-on-empty")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(100,73,"gateway.hints.modbus.retries-on-invalid")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(102,75,"gateway.retries-on-invalid")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(105,77,"gateway.hints.modbus.poll-period")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(110,79,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(113,81,"gateway.hints.modbus.connect-attempt-time")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(117,83,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(120,85,"gateway.hints.modbus.connect-attempt-count")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(124,87,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(127,89,"gateway.hints.modbus.wait-after-failed-attempts")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(131,91,"gateway.set")),t.ɵɵadvance(3),t.ɵɵproperty("singleMode",!0)("hideNewFields",n.data.hideNewFields),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(137,93,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.slaveConfigFormGroup.invalid||!n.slaveConfigFormGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(140,95,n.data.buttonTitle)," ")}},dependencies:t.ɵɵgetComponentDepsFactory(hZ,[j,_,jX,$X,r$,Xn,qn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .slaves-config-container[_ngcontent-%COMP%]{width:80vw;max-width:900px}[_nghost-%COMP%] .slave-name-label[_ngcontent-%COMP%]{margin-right:16px;color:#000000de}[_nghost-%COMP%] .fixed-title-width-260[_ngcontent-%COMP%]{min-width:260px}[_nghost-%COMP%] .security-config .fixed-title-width{min-width:230px}'],changeDetection:d.OnPush})}}function gZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusProtocolLabelsMap.get(e))}}function fZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function yZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",53),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,fZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("host").hasError("required")&&e.slaveConfigFormGroup.get("host").touched)}}function vZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.slaveConfigFormGroup.get("port")))}}function xZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",55),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,vZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.hints.modbus.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",e.portLimits.MIN),t.ɵɵpropertyInterpolate("max",e.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.slaveConfigFormGroup.get("port").hasError("required")||e.slaveConfigFormGroup.get("port").hasError("min")||e.slaveConfigFormGroup.get("port").hasError("max"))&&e.slaveConfigFormGroup.get("port").touched)}}function bZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.port-required"))}function wZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",17)(1,"div",22),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",19),t.ɵɵelement(5,"input",56),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,bZ,3,3,"mat-icon",24),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.serial-port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("serialPort").hasError("required")&&e.slaveConfigFormGroup.get("serialPort").touched)}}function SZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusMethodLabelsMap.get(e))}}function CZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function _Z(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function TZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusParityLabelsMap.get(e))}}function IZ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵelementStart(1,"div",17)(2,"div",18),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4,"gateway.baudrate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",19)(6,"mat-select",57),t.ɵɵtemplate(7,CZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(8,"div",17)(9,"div",18),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11,"gateway.bytesize"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",19)(13,"mat-select",58),t.ɵɵtemplate(14,_Z,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",17)(16,"div",18),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"gateway.stopbits"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"mat-form-field",19),t.ɵɵelement(20,"input",59),t.ɵɵpipe(21,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"div",17)(23,"div",18),t.ɵɵpipe(24,"translate"),t.ɵɵtext(25,"gateway.parity"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",19)(27,"mat-select",60),t.ɵɵtemplate(28,TZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(29,"div",36)(30,"mat-slide-toggle",61)(31,"mat-label",38),t.ɵɵpipe(32,"translate"),t.ɵɵtext(33),t.ɵɵpipe(34,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.modbus.baudrate")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusBaudrates),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(10,12,"gateway.hints.modbus.bytesize")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusByteSizes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(17,14,"gateway.hints.modbus.stopbits")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,16,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(24,18,"gateway.hints.modbus.parity")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusParities),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(32,20,"gateway.hints.modbus.strict")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(34,22,"gateway.strict")," ")}}function EZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.unit-id-required"))}function MZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function kZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",54),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function PZ(e,n){1&e&&(t.ɵɵelementStart(0,"div",36)(1,"mat-slide-toggle",62)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.send-data-on-change")," "))}function DZ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",63),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("defaultValue",e.ReportStrategyDefaultValue.Device)("isExpansionMode",!0)}}function OZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function AZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",52),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function FZ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",64)(1,"mat-expansion-panel",65)(2,"mat-expansion-panel-header",66)(3,"mat-panel-title")(4,"mat-slide-toggle",67),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(8,"tb-modbus-security-config",68),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("expanded",e.showSecurityControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showSecurityControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,3,"gateway.tls-connection")," ")}}e("ModbusSlaveDialogComponent",hZ);class RZ extends lX{constructor(e,t,n,i,a){super(e,t,n,i,a),this.fb=e,this.store=t,this.router=n,this.data=i,this.dialogRef=a}getSlaveResultData(){const{values:e,type:t,serialPort:n,...i}=this.slaveConfigFormGroup.value,a={...i,type:t,...e};return t===Ki.Serial&&(a.port=n),Te(a),a}addFieldsToFormGroup(){this.slaveConfigFormGroup.addControl("sendDataOnlyOnChange",this.fb.control(!1))}static{this.ɵfac=function(e){return new(e||RZ)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:RZ,selectors:[["tb-modbus-legacy-slave-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:141,vars:97,consts:[["serialPort",""],["reportStrategy",""],[1,"slaves-config-container"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content","",1,"tb-form-panel",3,"formGroup"],[1,"stroked","tb-form-panel"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],["class","tb-form-row column-xs",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","method"],[4,"ngIf"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","unitId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],["class","tb-form-row",4,"ngIf","ngIfElse"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["formControlName","byteOrder"],["formControlName","wordOrder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],[1,"tb-form-row"],["formControlName","retries",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","retryOnEmpty",1,"mat-slide"],["formControlName","retryOnInvalid",1,"mat-slide"],[1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","100","name","value","formControlName","pollPeriod",3,"placeholder"],["translate","",1,"fixed-title-width-260","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","500","name","value","formControlName","connectAttemptTimeMs",3,"placeholder"],["matInput","","type","number","min","1","name","value","formControlName","connectAttemptCount",3,"placeholder"],["matInput","","type","number","min","30000","name","value","formControlName","waitAfterFailedAttemptsMs",3,"placeholder"],["formControlName","values",3,"singleMode","hideNewFields"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[3,"value"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matInput","","name","value","formControlName","serialPort",3,"placeholder"],["formControlName","baudrate"],["formControlName","bytesize"],["matInput","","type","number","min","0","name","value","formControlName","stopbits",3,"placeholder"],["formControlName","parity"],["formControlName","strict",1,"mat-slide"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"defaultValue","isExpansionMode"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide","justify-start",3,"click","formControl"],["formControlName","security",1,"security-config"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"mat-toolbar",3)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",4)(6,"div",5),t.ɵɵelementStart(7,"button",6),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",7),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",8)(11,"div",9)(12,"div",10)(13,"div",11)(14,"div",12),t.ɵɵtext(15,"gateway.server-connection"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"tb-toggle-select",13),t.ɵɵtemplate(17,gZ,2,2,"tb-toggle-option",14),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",10),t.ɵɵtemplate(19,yZ,8,7,"div",15)(20,xZ,8,9,"div",16)(21,wZ,8,7,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(23,"div",17)(24,"div",18),t.ɵɵpipe(25,"translate"),t.ɵɵtext(26," gateway.method "),t.ɵɵelementEnd(),t.ɵɵelementStart(27,"mat-form-field",19)(28,"mat-select",20),t.ɵɵtemplate(29,SZ,2,2,"mat-option",14),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(30,IZ,35,24,"ng-container",21),t.ɵɵelementStart(31,"div",17)(32,"div",22),t.ɵɵpipe(33,"translate"),t.ɵɵtext(34,"gateway.unit-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(35,"mat-form-field",19),t.ɵɵelement(36,"input",23),t.ɵɵpipe(37,"translate"),t.ɵɵtemplate(38,EZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(39,"div",17)(40,"div",25),t.ɵɵtext(41,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"mat-form-field",19),t.ɵɵelement(43,"input",26),t.ɵɵpipe(44,"translate"),t.ɵɵtemplate(45,MZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"div",17)(47,"div",25),t.ɵɵtext(48,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(49,"mat-form-field",19),t.ɵɵelement(50,"input",27),t.ɵɵpipe(51,"translate"),t.ɵɵtemplate(52,kZ,3,3,"mat-icon",24),t.ɵɵelementEnd()(),t.ɵɵtemplate(53,PZ,5,3,"div",28)(54,DZ,1,2,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(56,"div",29)(57,"mat-expansion-panel",30)(58,"mat-expansion-panel-header")(59,"mat-panel-title")(60,"div",31),t.ɵɵtext(61,"gateway.advanced-connection-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(62,"div",10)(63,"div",17)(64,"div",18),t.ɵɵpipe(65,"translate"),t.ɵɵtext(66,"gateway.connection-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(67,"mat-form-field",19),t.ɵɵelement(68,"input",32),t.ɵɵpipe(69,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(70,"div",17)(71,"div",18),t.ɵɵpipe(72,"translate"),t.ɵɵtext(73,"gateway.byte-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(74,"mat-form-field",19)(75,"mat-select",33),t.ɵɵtemplate(76,OZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵelementStart(77,"div",17)(78,"div",18),t.ɵɵpipe(79,"translate"),t.ɵɵtext(80,"gateway.word-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(81,"mat-form-field",19)(82,"mat-select",34),t.ɵɵtemplate(83,AZ,2,2,"mat-option",14),t.ɵɵelementEnd()()(),t.ɵɵtemplate(84,FZ,9,5,"div",35),t.ɵɵelementStart(85,"div",36)(86,"mat-slide-toggle",37)(87,"mat-label",38),t.ɵɵpipe(88,"translate"),t.ɵɵtext(89),t.ɵɵpipe(90,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(91,"div",36)(92,"mat-slide-toggle",39)(93,"mat-label",38),t.ɵɵpipe(94,"translate"),t.ɵɵtext(95),t.ɵɵpipe(96,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(97,"div",36)(98,"mat-slide-toggle",40)(99,"mat-label",38),t.ɵɵpipe(100,"translate"),t.ɵɵtext(101),t.ɵɵpipe(102,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(103,"div",17)(104,"div",41),t.ɵɵpipe(105,"translate"),t.ɵɵelementStart(106,"span",42),t.ɵɵtext(107," gateway.poll-period "),t.ɵɵelementEnd()(),t.ɵɵelementStart(108,"mat-form-field",19),t.ɵɵelement(109,"input",43),t.ɵɵpipe(110,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(111,"div",17)(112,"div",44),t.ɵɵpipe(113,"translate"),t.ɵɵtext(114,"gateway.connect-attempt-time"),t.ɵɵelementEnd(),t.ɵɵelementStart(115,"mat-form-field",19),t.ɵɵelement(116,"input",45),t.ɵɵpipe(117,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(118,"div",17)(119,"div",44),t.ɵɵpipe(120,"translate"),t.ɵɵtext(121,"gateway.connect-attempt-count"),t.ɵɵelementEnd(),t.ɵɵelementStart(122,"mat-form-field",19),t.ɵɵelement(123,"input",46),t.ɵɵpipe(124,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(125,"div",17)(126,"div",44),t.ɵɵpipe(127,"translate"),t.ɵɵtext(128,"gateway.wait-after-failed-attempts"),t.ɵɵelementEnd(),t.ɵɵelementStart(129,"mat-form-field",19),t.ɵɵelement(130,"input",47),t.ɵɵpipe(131,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(132,"div",29),t.ɵɵelement(133,"tb-modbus-values",48),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(134,"div",49)(135,"button",50),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(136),t.ɵɵpipe(137,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(138,"button",51),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(139),t.ɵɵpipe(140,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(22),i=t.ɵɵreference(55);t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,45,"gateway.server-slave")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.modbusHelpLink),t.ɵɵadvance(4),t.ɵɵproperty("formGroup",n.slaveConfigFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",n.modbusProtocolTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(25,47,"gateway.hints.modbus.framer-type")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.protocolType===n.ModbusProtocolType.Serial?n.modbusSerialMethodTypes:n.modbusMethodTypes),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType===n.ModbusProtocolType.Serial),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(33,49,"gateway.hints.modbus.unit-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(37,51,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("unitId").hasError("required")&&n.slaveConfigFormGroup.get("unitId").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(44,53,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceName").hasError("required")&&n.slaveConfigFormGroup.get("deviceName").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(51,55,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceType").hasError("required")&&n.slaveConfigFormGroup.get("deviceType").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.data.hideNewFields)("ngIfElse",i),t.ɵɵadvance(11),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(65,57,"gateway.hints.modbus.connection-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(69,59,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(72,61,"gateway.hints.modbus.byte-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(79,63,"gateway.hints.modbus.word-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(88,65,"gateway.hints.modbus.retries")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(90,67,"gateway.retries")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(94,69,"gateway.hints.modbus.retries-on-empty")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(96,71,"gateway.retries-on-empty")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(100,73,"gateway.hints.modbus.retries-on-invalid")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(102,75,"gateway.retries-on-invalid")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(105,77,"gateway.hints.modbus.poll-period")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(110,79,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(113,81,"gateway.hints.modbus.connect-attempt-time")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(117,83,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(120,85,"gateway.hints.modbus.connect-attempt-count")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(124,87,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(127,89,"gateway.hints.modbus.wait-after-failed-attempts")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(131,91,"gateway.set")),t.ɵɵadvance(3),t.ɵɵproperty("singleMode",!0)("hideNewFields",n.data.hideNewFields),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(137,93,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.slaveConfigFormGroup.invalid||!n.slaveConfigFormGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(140,95,n.data.buttonTitle)," ")}},dependencies:t.ɵɵgetComponentDepsFactory(RZ,[j,_,jX,$X,r$,Xn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .slaves-config-container[_ngcontent-%COMP%]{width:80vw;max-width:900px}[_nghost-%COMP%] .slave-name-label[_ngcontent-%COMP%]{margin-right:16px;color:#000000de}[_nghost-%COMP%] .fixed-title-width-260[_ngcontent-%COMP%]{min-width:260px}[_nghost-%COMP%] .security-config .fixed-title-width{min-width:230px}'],changeDetection:d.OnPush})}}function BZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusProtocolLabelsMap.get(e))}}function NZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function LZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",14),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",39),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,NZ,3,3,"mat-icon",16),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("host").hasError("required")&&e.slaveConfigFormGroup.get("host").touched)}}function VZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.slaveConfigFormGroup.get("port")))}}function qZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",14),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",41),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,VZ,3,3,"mat-icon",16),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.hints.modbus.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",e.portLimits.MIN),t.ɵɵpropertyInterpolate("max",e.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(e.slaveConfigFormGroup.get("port").hasError("required")||e.slaveConfigFormGroup.get("port").hasError("min")||e.slaveConfigFormGroup.get("port").hasError("max"))&&e.slaveConfigFormGroup.get("port").touched)}}function GZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.port-required"))}function zZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",14),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12),t.ɵɵelement(5,"input",42),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,GZ,3,3,"mat-icon",16),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.modbus.serial-port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.slaveConfigFormGroup.get("port").hasError("required")&&e.slaveConfigFormGroup.get("port").touched)}}function UZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.ModbusMethodLabelsMap.get(e))}}function jZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.unit-id-required"))}function HZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function WZ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function $Z(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function KZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10)(1,"div",11),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.baudrate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",12)(5,"mat-select",43),t.ɵɵtemplate(6,$Z,2,2,"mat-option",6),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,2,"gateway.hints.modbus.baudrate")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",e.modbusBaudrates)}}function YZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function XZ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",38),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function ZZ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",44)(1,"mat-expansion-panel",45)(2,"mat-expansion-panel-header",46)(3,"mat-panel-title")(4,"mat-slide-toggle",47),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(5,"mat-label"),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(8,"tb-modbus-security-config",48),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("expanded",e.showSecurityControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",e.showSecurityControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,3,"gateway.tls-connection")," ")}}e("ModbusLegacySlaveDialogComponent",RZ);class QZ{constructor(e){this.fb=e,this.ModbusProtocolLabelsMap=sa,this.ModbusMethodLabelsMap=oa,this.portLimits=Ii,this.modbusProtocolTypes=Object.values(Ki),this.modbusMethodTypes=Object.values(Yi),this.modbusSerialMethodTypes=Object.values(Xi),this.modbusOrderType=Object.values(Qi),this.ModbusProtocolType=Ki,this.modbusBaudrates=ta,this.isSlaveEnabled=!1,this.serialSpecificControlKeys=["serialPort","baudrate"],this.tcpUdpSpecificControlKeys=["port","security","host"],this.destroy$=new te,this.showSecurityControl=this.fb.control(!1),this.slaveConfigFormGroup=this.fb.group({type:[Ki.TCP],host:["",[$.required,$.pattern(an)]],port:[null,[$.required,$.min(Ii.MIN),$.max(Ii.MAX)]],serialPort:["",[$.required,$.pattern(an)]],method:[Yi.SOCKET],unitId:[null,[$.required]],baudrate:[this.modbusBaudrates[0]],deviceName:["",[$.required,$.pattern(an)]],deviceType:["",[$.required,$.pattern(an)]],pollPeriod:[1e3,[$.required]],sendDataToThingsBoard:[!1],byteOrder:[Qi.BIG],wordOrder:[Qi.BIG],security:[],identity:this.fb.group({vendorName:["",[$.pattern(an)]],productCode:["",[$.pattern(an)]],vendorUrl:["",[$.pattern(an)]],productName:["",[$.pattern(an)]],modelName:["",[$.pattern(an)]]}),values:[]}),this.observeValueChanges(),this.observeTypeChange(),this.observeShowSecurity()}get protocolType(){return this.slaveConfigFormGroup.get("type").value}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}validate(){return this.slaveConfigFormGroup.valid?null:{slaveConfigFormGroup:{valid:!1}}}writeValue(e){this.showSecurityControl.patchValue(!!e.security&&!Se(e.security,{})),this.updateSlaveConfig(e)}setDisabledState(e){this.isSlaveEnabled=!e,this.updateFormEnableState()}observeValueChanges(){this.slaveConfigFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{e.type===Ki.Serial&&(e.port=e.serialPort,delete e.serialPort),this.onChange(e),this.onTouched()}))}observeTypeChange(){this.slaveConfigFormGroup.get("type").valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.updateFormEnableState(),this.updateMethodType(e)}))}updateMethodType(e){this.slaveConfigFormGroup.get("method").value!==Yi.RTU&&this.slaveConfigFormGroup.get("method").patchValue(e===Ki.Serial?Xi.ASCII:Yi.SOCKET,{emitEvent:!1})}updateFormEnableState(){this.isSlaveEnabled?(this.slaveConfigFormGroup.enable({emitEvent:!1}),this.showSecurityControl.enable({emitEvent:!1})):(this.slaveConfigFormGroup.disable({emitEvent:!1}),this.showSecurityControl.disable({emitEvent:!1})),this.updateEnablingByProtocol(),this.updateSecurityEnable(this.showSecurityControl.value)}observeShowSecurity(){this.showSecurityControl.valueChanges.pipe(le(this.destroy$)).subscribe((e=>this.updateSecurityEnable(e)))}updateSecurityEnable(e){e&&this.isSlaveEnabled&&this.protocolType!==Ki.Serial?this.slaveConfigFormGroup.get("security").enable({emitEvent:!1}):this.slaveConfigFormGroup.get("security").disable({emitEvent:!1})}updateEnablingByProtocol(){const e=this.protocolType===Ki.Serial,t=e?this.serialSpecificControlKeys:this.tcpUdpSpecificControlKeys,n=e?this.tcpUdpSpecificControlKeys:this.serialSpecificControlKeys;this.isSlaveEnabled&&t.forEach((e=>this.slaveConfigFormGroup.get(e)?.enable({emitEvent:!1}))),n.forEach((e=>this.slaveConfigFormGroup.get(e)?.disable({emitEvent:!1})))}updateSlaveConfig(e){const{vendorName:t="",productCode:n="",vendorUrl:i="",productName:a="",modelName:r=""}=e.identity??{},o={vendorName:t,productCode:n,vendorUrl:i,productName:a,modelName:r},{type:s=Ki.TCP,method:l=Yi.RTU,unitId:p=0,deviceName:c="",deviceType:d="",pollPeriod:u=1e3,sendDataToThingsBoard:m=!1,byteOrder:h=Qi.BIG,wordOrder:g=Qi.BIG,security:f={},values:y={},baudrate:v=this.modbusBaudrates[0],host:x="",port:b=null}=e,w={type:s,method:l,unitId:p,deviceName:c,deviceType:d,pollPeriod:u,sendDataToThingsBoard:!!m,byteOrder:h,wordOrder:g,security:f,identity:o,values:y,baudrate:v,host:s===Ki.Serial?"":x,port:s===Ki.Serial?null:b,serialPort:s===Ki.Serial?b:""};this.slaveConfigFormGroup.setValue(w,{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||QZ)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:QZ,selectors:[["tb-modbus-slave-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>QZ)),multi:!0},{provide:K,useExisting:c((()=>QZ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:112,vars:59,consts:[["serialPort",""],[1,"slave-container",3,"formGroup"],[1,"slave-content","tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["translate","",1,"fixed-title-width"],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-panel","no-border","no-padding","padding-top"],["class","tb-form-row column-xs",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf","ngIfElse"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","method"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","unitId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","100","name","value","formControlName","pollPeriod",3,"placeholder"],[1,"tb-form-row"],["formControlName","sendDataToThingsBoard",1,"mat-slide"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],["formControlName","byteOrder"],["formControlName","wordOrder"],["class","tb-form-panel stroked tb-slide-toggle",4,"ngIf"],[3,"formGroup"],["matInput","","name","value","formControlName","vendorName",3,"placeholder"],["matInput","","name","value","formControlName","productCode",3,"placeholder"],["matInput","","name","value","formControlName","vendorUrl",3,"placeholder"],["matInput","","name","value","formControlName","productName",3,"placeholder"],["matInput","","name","value","formControlName","modelName",3,"placeholder"],["formControlName","values"],[3,"value"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matInput","","name","value","formControlName","serialPort",3,"placeholder"],["formControlName","baudrate"],[1,"tb-form-panel","stroked","tb-slide-toggle"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide",3,"click","formControl"],["formControlName","security"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",1)(1,"div",2)(2,"div",3)(3,"div",4),t.ɵɵtext(4,"gateway.server-slave-config"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-select",5),t.ɵɵtemplate(6,BZ,2,2,"tb-toggle-option",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",7),t.ɵɵtemplate(8,LZ,8,7,"div",8)(9,qZ,8,9,"div",9)(10,zZ,8,7,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(12,"div",10)(13,"div",11),t.ɵɵpipe(14,"translate"),t.ɵɵtext(15," gateway.method "),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"mat-form-field",12)(17,"mat-select",13),t.ɵɵtemplate(18,UZ,2,2,"mat-option",6),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(19,"div",10)(20,"div",14),t.ɵɵpipe(21,"translate"),t.ɵɵtext(22,"gateway.unit-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",12),t.ɵɵelement(24,"input",15),t.ɵɵpipe(25,"translate"),t.ɵɵtemplate(26,jZ,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(27,"div",10)(28,"div",17),t.ɵɵtext(29,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",12),t.ɵɵelement(31,"input",18),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,HZ,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(34,"div",10)(35,"div",17),t.ɵɵtext(36,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"mat-form-field",12),t.ɵɵelement(38,"input",19),t.ɵɵpipe(39,"translate"),t.ɵɵtemplate(40,WZ,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(41,"div",10)(42,"div",20),t.ɵɵpipe(43,"translate"),t.ɵɵelementStart(44,"span",21),t.ɵɵtext(45," gateway.poll-period "),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"mat-form-field",12),t.ɵɵelement(47,"input",22),t.ɵɵpipe(48,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(49,KZ,7,4,"div",8),t.ɵɵelementStart(50,"div",23)(51,"mat-slide-toggle",24)(52,"mat-label"),t.ɵɵtext(53),t.ɵɵpipe(54,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(55,"div",25)(56,"mat-expansion-panel",26)(57,"mat-expansion-panel-header")(58,"mat-panel-title")(59,"div",27),t.ɵɵtext(60,"gateway.advanced-connection-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(61,"div",7)(62,"div",10)(63,"div",11),t.ɵɵpipe(64,"translate"),t.ɵɵtext(65,"gateway.byte-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(66,"mat-form-field",12)(67,"mat-select",28),t.ɵɵtemplate(68,YZ,2,2,"mat-option",6),t.ɵɵelementEnd()()(),t.ɵɵelementStart(69,"div",10)(70,"div",11),t.ɵɵpipe(71,"translate"),t.ɵɵtext(72,"gateway.word-order"),t.ɵɵelementEnd(),t.ɵɵelementStart(73,"mat-form-field",12)(74,"mat-select",29),t.ɵɵtemplate(75,XZ,2,2,"mat-option",6),t.ɵɵelementEnd()()(),t.ɵɵtemplate(76,ZZ,9,5,"div",30),t.ɵɵelementContainerStart(77,31),t.ɵɵelementStart(78,"div",10)(79,"div",4),t.ɵɵtext(80,"gateway.vendor-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(81,"mat-form-field",12),t.ɵɵelement(82,"input",32),t.ɵɵpipe(83,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(84,"div",10)(85,"div",4),t.ɵɵtext(86,"gateway.product-code"),t.ɵɵelementEnd(),t.ɵɵelementStart(87,"mat-form-field",12),t.ɵɵelement(88,"input",33),t.ɵɵpipe(89,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(90,"div",10)(91,"div",4),t.ɵɵtext(92,"gateway.vendor-url"),t.ɵɵelementEnd(),t.ɵɵelementStart(93,"mat-form-field",12),t.ɵɵelement(94,"input",34),t.ɵɵpipe(95,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(96,"div",10)(97,"div",4),t.ɵɵtext(98,"gateway.product-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(99,"mat-form-field",12),t.ɵɵelement(100,"input",35),t.ɵɵpipe(101,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(102,"div",10)(103,"div",4),t.ɵɵtext(104,"gateway.model-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(105,"mat-form-field",12),t.ɵɵelement(106,"input",36),t.ɵɵpipe(107,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()(),t.ɵɵelementStart(108,"div",25)(109,"div",27),t.ɵɵtext(110,"gateway.values"),t.ɵɵelementEnd(),t.ɵɵelement(111,"tb-modbus-values",37),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵreference(11);t.ɵɵproperty("formGroup",n.slaveConfigFormGroup),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.modbusProtocolTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial)("ngIfElse",e),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(14,29,"gateway.hints.modbus.framer-type")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.protocolType===n.ModbusProtocolType.Serial?n.modbusSerialMethodTypes:n.modbusMethodTypes),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(21,31,"gateway.hints.modbus.unit-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(25,33,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("unitId").hasError("required")&&n.slaveConfigFormGroup.get("unitId").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,35,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceName").hasError("required")&&n.slaveConfigFormGroup.get("deviceName").touched),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(39,37,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.slaveConfigFormGroup.get("deviceType").hasError("required")&&n.slaveConfigFormGroup.get("deviceType").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(43,39,"gateway.hints.modbus.poll-period")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(48,41,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.protocolType===n.ModbusProtocolType.Serial),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(54,43,"gateway.send-data-to-platform")," "),t.ɵɵadvance(10),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(64,45,"gateway.hints.modbus.byte-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(71,47,"gateway.hints.modbus.word-order")),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.modbusOrderType),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.protocolType!==n.ModbusProtocolType.Serial),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.slaveConfigFormGroup.get("identity")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(83,49,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(89,51,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(95,53,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(101,55,"gateway.set")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(107,57,"gateway.set"))}},dependencies:t.ɵɵgetComponentDepsFactory(QZ,[j,_,jX,$X,r$,qn]),encapsulation:2,changeDetection:d.OnPush})}}e("ModbusSlaveConfigComponent",QZ);const JZ=["searchInput"],eQ=()=>["deviceName","info","unitId","type","actions"],tQ=()=>({minWidth:"96px",textAlign:"center"});function nQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",9)(1,"div",28)(2,"span",29),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",11),t.ɵɵelementStart(6,"button",13),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageSlave(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",13),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.servers-slaves")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search")))}function iQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.device-name")))}function aQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.deviceName)}}function rQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.info")," "))}function oQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){let e;const i=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(null!==(e=i.host)&&void 0!==e?e:i.port)}}function sQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.unit-id")," "))}function lQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.unitId)}}function pQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30)(1,"div"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.type")))}function cQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",30),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.ModbusProtocolLabelsMap.get(e.type)," ")}}function dQ(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",32)}function uQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageSlave(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",13),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteSlave(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function mQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,uQ,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",33),t.ɵɵelementContainer(4,34),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",35)(6,"button",36),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",37),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",38,2),t.ɵɵelementContainer(11,34),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,tQ)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function hQ(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",39)}function gQ(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class fQ{constructor(e,t,n,i,a){this.translate=e,this.dialog=t,this.dialogService=n,this.fb=i,this.cdr=a,this.isLegacy=!1,this.textSearchMode=!1,this.textSearch=this.fb.control("",{nonNullable:!0}),this.ModbusProtocolLabelsMap=sa,this.onChange=()=>{},this.onTouched=()=>{},this.destroy$=new te,this.masterFormGroup=this.fb.group({slaves:this.fb.array([])}),this.dataSource=new yQ}get slaves(){return this.masterFormGroup.get("slaves")}ngOnInit(){this.masterFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.updateTableData(e.slaves),this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(de(150),ce(((e,t)=>(e??"")===t.trim())),le(this.destroy$)).subscribe((e=>this.updateTableData(this.slaves.value,e.trim())))}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){this.slaves.clear(),this.pushDataAsFormArrays(e.slaves)}enterFilterMode(){this.textSearchMode=!0,this.cdr.detectChanges();const e=this.searchInputField.nativeElement;e.focus(),e.setSelectionRange(0,0)}exitFilterMode(){this.updateTableData(this.slaves.value),this.textSearchMode=!1,this.textSearch.reset()}manageSlave(e,t){e&&e.stopPropagation();const n=ke(t),i=n?this.slaves.at(t).value:{};this.getSlaveDialog(i,n?"action.apply":"action.add").afterClosed().pipe(ye(1),le(this.destroy$)).subscribe((e=>{e&&(n?this.slaves.at(t).patchValue(e):this.slaves.push(this.fb.control(e)),this.masterFormGroup.markAsDirty())}))}getSlaveDialog(e,t){return this.isLegacy?this.dialog.open(RZ,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,hideNewFields:!0,buttonTitle:t}}):this.dialog.open(hZ,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,hideNewFields:!1}})}deleteSlave(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-slave-title",{name:this.slaves.controls[t].value.deviceName}),this.translate.instant("gateway.delete-slave-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(ye(1),le(this.destroy$)).subscribe((e=>{e&&(this.slaves.removeAt(t),this.masterFormGroup.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.slaves.push(this.fb.control(e))))}static{this.ɵfac=function(e){return new(e||fQ)(t.ɵɵdirectiveInject(He.TranslateService),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(_e.DialogService),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:fQ,selectors:[["tb-modbus-master-table"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(JZ,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{isLegacy:"isLegacy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>fQ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:55,vars:41,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-master-table","tb-absolute-fill"],[1,"tb-form-panel","no-border","no-padding","padding-top","hint-container"],["tbTruncateWithTooltip","",1,"tb-form-hint","tb-primary-fill","tb-flex"],[1,"tb-master-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","table-value-column",4,"matHeaderCellDef"],["class","table-value-column",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-master-table-title"],[1,"table-value-column"],["tbTruncateWithTooltip",""],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"div",5),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(5,"div",6)(6,"mat-toolbar",7),t.ɵɵtemplate(7,nQ,14,9,"div",8),t.ɵɵpipe(8,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"mat-toolbar",7)(10,"div",9)(11,"button",10),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"mat-icon"),t.ɵɵtext(14,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"mat-form-field",11)(16,"mat-label"),t.ɵɵtext(17," "),t.ɵɵelementEnd(),t.ɵɵelement(18,"input",12,0),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"button",13),t.ɵɵpipe(22,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(23,"mat-icon"),t.ɵɵtext(24,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(25,"div",14)(26,"table",15),t.ɵɵelementContainerStart(27,16),t.ɵɵtemplate(28,iQ,4,3,"mat-header-cell",17)(29,aQ,3,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(30,16),t.ɵɵtemplate(31,rQ,3,3,"mat-header-cell",17)(32,oQ,3,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(33,16),t.ɵɵtemplate(34,sQ,3,3,"mat-header-cell",17)(35,lQ,3,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(36,16),t.ɵɵtemplate(37,pQ,4,3,"mat-header-cell",17)(38,cQ,2,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(39,19),t.ɵɵtemplate(40,dQ,1,0,"mat-header-cell",20)(41,mQ,12,6,"mat-cell",21),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(42,hQ,1,0,"mat-header-row",22)(43,gQ,1,0,"mat-row",23),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"section",24),t.ɵɵpipe(45,"async"),t.ɵɵelementStart(46,"button",25),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageSlave(i))})),t.ɵɵelementStart(47,"mat-icon",26),t.ɵɵtext(48,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(49,"span"),t.ɵɵtext(50),t.ɵɵpipe(51,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(52,"span",27),t.ɵɵpipe(53,"async"),t.ɵɵtext(54," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,23,"gateway.hints.modbus-master")),t.ɵɵadvance(3),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(8,25,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(12,27,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(20,29,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(22,31,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","deviceName"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","info"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","unitId"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","type"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(39,eQ))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(40,eQ)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(45,33,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(51,35,"gateway.add-slave")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(53,37,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(fQ,[j,_,qn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%]{width:100%;height:calc(100% - 60px);background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .tb-master-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-master-table[_ngcontent-%COMP%] .tb-master-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:15%}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-master-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] .hint-container[_ngcontent-%COMP%]{z-index:1000}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}e("ModbusMasterTableComponent",fQ),Ge([rt()],fQ.prototype,"isLegacy",void 0);class yQ extends G{constructor(){super()}}e("SlavesDatasource",yQ);class vQ extends Ba{constructor(){super(),this.enableSlaveControl=new X(!1),this.enableSlaveControl.valueChanges.pipe(bn()).subscribe((e=>{this.updateSlaveEnabling(e),this.basicFormGroup.get("slave").updateValueAndValidity({emitEvent:!!this.onChange})}))}writeValue(e){super.writeValue(e),this.onEnableSlaveControl(e)}validate(){const{master:e,slave:t}=this.basicFormGroup.value,n=!e?.slaves?.length&&(Se(t,{})||!t);return!this.basicFormGroup.valid||n?{basicFormGroup:{valid:!1}}:null}initBasicFormGroup(){return this.fb.group({master:[],slave:[]})}updateSlaveEnabling(e){e?this.basicFormGroup.get("slave").enable({emitEvent:!1}):this.basicFormGroup.get("slave").disable({emitEvent:!1})}onEnableSlaveControl(e){this.enableSlaveControl.setValue(!!e.slave&&!Se(e.slave,{}))}static{this.ɵfac=function(e){return new(e||vQ)}}static{this.ɵdir=t.ɵɵdefineDirective({type:vQ,features:[t.ɵɵInheritDefinitionFeature]})}}e("ModbusBasicConfigDirective",vQ);class xQ extends vQ{constructor(){super(...arguments),this.isLegacy=!1}mapConfigToFormValue({master:e,slave:t}){return{master:e?.slaves?e:{slaves:[]},slave:t??{}}}getMappedValue(e){return{master:e.master,slave:e.slave}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(xQ)))(n||xQ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:xQ,selectors:[["tb-modbus-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>xQ)),multi:!0},{provide:K,useExisting:c((()=>xQ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:19,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","master",3,"isLegacy"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-form-hint","tb-primary-fill","tb-flex","center"],[1,"tb-form-row"],[1,"mat-slide",3,"formControl"],["formControlName","slave"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-modbus-master-table",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4)(10,"div",5),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",6)(14,"mat-slide-toggle",7)(15,"mat-label"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(18,"tb-modbus-slave-config",8),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(5,11,"gateway.master-connections")),t.ɵɵadvance(2),t.ɵɵproperty("isLegacy",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(8,13,"gateway.server-config")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,15,"gateway.hints.modbus-server")),t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.enableSlaveControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,17,"gateway.enable")," "))},dependencies:t.ɵɵgetComponentDepsFactory(xQ,[j,_,QZ,fQ,Gn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}'],changeDetection:d.OnPush})}}e("ModbusBasicConfigComponent",xQ);class bQ extends vQ{constructor(){super(...arguments),this.isLegacy=!0}mapConfigToFormValue(e){return{master:e.master?.slaves?e.master:{slaves:[]},slave:e.slave?Ua.mapSlaveToUpgradedVersion(e.slave):{}}}getMappedValue(e){return{master:e.master,slave:e.slave?Ua.mapSlaveToDowngradedVersion(e.slave):{}}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(bQ)))(n||bQ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:bQ,selectors:[["tb-modbus-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>bQ)),multi:!0},{provide:K,useExisting:c((()=>bQ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:19,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","master",3,"isLegacy"],[1,"tb-form-panel","no-border","no-padding","padding-top"],[1,"tb-form-hint","tb-primary-fill","tb-flex","center"],[1,"tb-form-row"],[1,"mat-slide",3,"formControl"],["formControlName","slave"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-modbus-master-table",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4)(10,"div",5),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",6)(14,"mat-slide-toggle",7)(15,"mat-label"),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelement(18,"tb-modbus-slave-config",8),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(5,11,"gateway.master-connections")),t.ɵɵadvance(2),t.ɵɵproperty("isLegacy",n.isLegacy),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(8,13,"gateway.server-config")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(12,15,"gateway.hints.modbus-server")),t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.enableSlaveControl),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,17,"gateway.enable")," "))},dependencies:t.ɵɵgetComponentDepsFactory(bQ,[j,_,QZ,fQ,Gn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}'],changeDetection:d.OnPush})}}e("ModbusLegacyBasicConfigComponent",bQ);const wQ=()=>({maxWidth:"970px"});function SQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",20),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.get("key").value," ")}}function CQ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtext(1),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate(e.get("methodRPC").value)}}function _Q(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtext(1),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate(e.get("attributeOnThingsBoard").value)}}function TQ(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtext(1),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(2).$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate2(" ",e.get("requestExpression").value+" - ","",e.get("attributeNameExpression").value," ")}}function IQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",21),t.ɵɵtemplate(1,CQ,2,1,"ng-container",22)(2,_Q,2,1,"ng-container",22)(3,TQ,2,2,"ng-container",22),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("ngSwitch",e.keysType),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.SocketValueKey.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.SocketValueKey.ATTRIBUTES_UPDATES),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.SocketValueKey.ATTRIBUTES_REQUESTS)}}function EQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function MQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function kQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function PQ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",42),2&e){const e=t.ɵɵnextContext(5);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Key)}}function DQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",24)(2,"div",25),t.ɵɵtext(3,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",26)(5,"div",27),t.ɵɵpipe(6,"translate"),t.ɵɵpipe(7,"translate"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"div",28)(11,"mat-form-field",29),t.ɵɵelement(12,"input",30),t.ɵɵpipe(13,"translate"),t.ɵɵtemplate(14,EQ,3,3,"mat-icon",31),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(15,"div",24)(16,"div",25),t.ɵɵtext(17,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"div",26)(19,"div",32)(20,"span",33),t.ɵɵtext(21),t.ɵɵpipe(22,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(23,"div",34),t.ɵɵelementEnd(),t.ɵɵelementStart(24,"label",35),t.ɵɵtext(25,"from"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",36),t.ɵɵelement(27,"input",37),t.ɵɵpipe(28,"translate"),t.ɵɵtemplate(29,MQ,3,3,"mat-icon",31),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"label",38),t.ɵɵtext(31,"to"),t.ɵɵelementEnd(),t.ɵɵelementStart(32,"mat-form-field",36),t.ɵɵelement(33,"input",39),t.ɵɵpipe(34,"translate"),t.ɵɵtemplate(35,kQ,3,3,"mat-icon",31),t.ɵɵelementEnd()(),t.ɵɵtemplate(36,PQ,1,2,"tb-report-strategy",40),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",n.keysType===n.SocketValueKey.ATTRIBUTES?t.ɵɵpipeBind1(6,12,"gateway.hints.socket.key-attribute"):t.ɵɵpipeBind1(7,14,"gateway.hints.socket.key-telemetry")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,16,"gateway.key")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(13,18,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("key").hasError("required")&&e.get("key").touched),t.ɵɵadvance(7),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(22,20,"gateway.byte")),t.ɵɵadvance(2),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/byte_fn")("tb-help-popup-style",t.ɵɵpureFunction0(26,wQ)),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,22,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("byteFrom").hasError("required")&&e.get("byteFrom").touched),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(34,24,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("byteTo").hasError("required")&&e.get("byteTo").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.withReportStrategy&&(n.keysType===n.SocketValueKey.ATTRIBUTES||n.keysType===n.SocketValueKey.TIMESERIES))}}function OQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-required"))}function AQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}function FQ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",26)(2,"div",43),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",28)(6,"mat-form-field",29),t.ɵɵelement(7,"input",44),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,OQ,3,3,"mat-icon",31),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",26)(11,"div",45),t.ɵɵpipe(12,"translate"),t.ɵɵtext(13," gateway.encoding "),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",28)(15,"mat-form-field",29)(16,"mat-select",46),t.ɵɵtemplate(17,AQ,2,2,"mat-option",47),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(18,"div",48)(19,"mat-slide-toggle",49),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(20,"mat-label",50),t.ɵɵpipe(21,"translate"),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd()()()()}if(2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,7,"gateway.method-name")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("methodRPC").hasError("required")&&e.get("methodRPC").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(12,11,"gateway.hints.encoding")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(21,13,"gateway.hints.socket.with-response")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(23,15,"gateway.rpc.withResponse")," ")}}function RQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e)," ")}}function BQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e)," ")}}function NQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.request-expression-required"))}function LQ(e,n){1&e&&t.ɵɵelement(0,"div",34),2&e&&t.ɵɵproperty("tb-help-popup","widget/lib/gateway/request-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(2,wQ))}function VQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e)," ")}}function qQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.attribute-name-expression-required"))}function GQ(e,n){1&e&&t.ɵɵelement(0,"div",34),2&e&&t.ɵɵproperty("tb-help-popup","widget/lib/gateway/attribute-name-expression_fn")("tb-help-popup-style",t.ɵɵpureFunction0(2,wQ))}function zQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",26)(2,"div",45),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4," gateway.type "),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",28)(6,"mat-form-field",29)(7,"mat-select",52),t.ɵɵtemplate(8,RQ,3,4,"mat-option",47),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(9,"div",26)(10,"div",43),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",53)(14,"mat-form-field",29)(15,"mat-select",54),t.ɵɵtemplate(16,BQ,3,4,"mat-option",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"mat-form-field",29),t.ɵɵelement(18,"input",55),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,NQ,3,3,"mat-icon",31)(21,LQ,1,3,"div",56),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"div",26)(23,"div",43),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"div",53)(27,"mat-form-field",29)(28,"mat-select",57),t.ɵɵtemplate(29,VQ,3,4,"mat-option",47),t.ɵɵelementEnd()(),t.ɵɵelementStart(30,"mat-form-field",29),t.ɵɵelement(31,"input",58),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,qQ,3,3,"mat-icon",31)(34,GQ,1,3,"div",56),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,12,"gateway.hints.socket.attribute-requests-type")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.requestsType),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,14,"gateway.request-expression")," "),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.expressionType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,16,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("requestExpression").hasError("required")&&e.get("requestExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("requestExpressionSource").value===n.ExpressionType.Expression),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(25,18,"gateway.attribute-name-expression")," "),t.ɵɵadvance(5),t.ɵɵproperty("ngForOf",n.expressionType),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("attributeNameExpression").hasError("required")&&e.get("attributeNameExpression").touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.get("attributeNameExpressionSource").value===n.ExpressionType.Expression)}}function UQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",51),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}function jQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",41),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.socket.attribute-on-platform-required"))}function HQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",26)(2,"div",45),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4," gateway.encoding "),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",28)(6,"mat-form-field",29)(7,"mat-select",46),t.ɵɵtemplate(8,UQ,2,2,"mat-option",47),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(9,"div",26)(10,"div",43),t.ɵɵtext(11),t.ɵɵpipe(12,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"div",28)(14,"mat-form-field",29),t.ɵɵelement(15,"input",59),t.ɵɵpipe(16,"translate"),t.ɵɵtemplate(17,jQ,3,3,"mat-icon",31),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext(2).$implicit,n=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,5,"gateway.hints.encoding")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(12,7,"gateway.attribute-on-platform")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.get("attributeOnThingsBoard").hasError("required")&&e.get("attributeOnThingsBoard").touched)}}function WQ(e,n){if(1&e&&t.ɵɵtemplate(0,DQ,37,27,"div",23)(1,FQ,24,17,"div",23)(2,zQ,35,22,"div",23)(3,HQ,18,11,"div",23),2&e){const e=t.ɵɵnextContext(3);t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.TIMESERIES||e.keysType===e.SocketValueKey.ATTRIBUTES),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.RPC_METHODS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.ATTRIBUTES_REQUESTS),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.keysType===e.SocketValueKey.ATTRIBUTES_UPDATES)}}function $Q(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",12)(1,"div",13),t.ɵɵelementContainerStart(2,14),t.ɵɵelementStart(3,"mat-expansion-panel",15)(4,"mat-expansion-panel-header",16)(5,"mat-panel-title"),t.ɵɵtemplate(6,SQ,2,1,"div",17)(7,IQ,4,4,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementEnd()(),t.ɵɵtemplate(9,WQ,4,4,"ng-template",18),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",19),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.last,a=t.ɵɵreference(8),r=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",r.keysType===r.SocketValueKey.TIMESERIES||r.keysType===r.SocketValueKey.ATTRIBUTES)("ngIfElse",a),t.ɵɵadvance(4),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,r.deleteKeyTitle))}}function KQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",10),t.ɵɵtemplate(1,$Q,14,7,"div",11),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.keysListFormArray.controls)("ngForTrackBy",e.trackByKey)}}function YQ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",60)(1,"span",61),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class XQ extends q{constructor(e,t,n){super(n),this.fb=e,this.popover=t,this.store=n,this.withReportStrategy=!0,this.keysDataApplied=new u,this.SocketValueKey=li,this.socketEncoding=Object.values(mt),this.requestsType=Object.values(ci),this.expressionType=Object.values(di),this.ExpressionType=di,this.ReportStrategyDefaultValue=en}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}trackByKey(e,t){return t}addKey(){let e;e=this.keysType===li.RPC_METHODS?this.fb.group({methodRPC:["",[$.required]],encoding:[mt.UTF16,[$.required]],withResponse:[!0]}):this.keysType===li.ATTRIBUTES_UPDATES?this.fb.group({encoding:[mt.UTF16,[$.required]],attributeOnThingsBoard:["",[$.required]]}):this.keysType===li.ATTRIBUTES_REQUESTS?this.fb.group({type:[ci.Shared],requestExpressionSource:[di.Constant],attributeNameExpressionSource:[di.Constant],requestExpression:["",[$.required]],attributeNameExpression:["",[$.required]]}):this.fb.group({key:["",[$.required,$.pattern(an)]],byteFrom:[0,[$.required]],byteTo:[0,[$.required]],reportStrategy:[{value:null,disabled:this.isReportStrategyDisabled()}]}),this.keysListFormArray.push(e)}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){let e=this.keysListFormArray.value.map((({reportStrategy:e,...t})=>({...t,...e&&{reportStrategy:e}})));this.keysDataApplied.emit(e)}prepareKeysFormArray(e){const t=[];return e?.forEach((e=>{let n;if(this.keysType===li.RPC_METHODS){const t=e;n=this.fb.group({methodRPC:[t.methodRPC,[$.required]],encoding:[t.encoding,[$.required]],withResponse:[t.withResponse]})}else if(this.keysType===li.ATTRIBUTES_REQUESTS){const t=e;n=this.fb.group({type:[t.type??ci.Shared],requestExpressionSource:[t.requestExpressionSource??di.Constant],attributeNameExpressionSource:[t.attributeNameExpressionSource??di.Constant],requestExpression:[t.requestExpression,[$.required]],attributeNameExpression:[t.attributeNameExpression,[$.required]]})}else if(this.keysType===li.ATTRIBUTES_UPDATES)n=this.fb.group({encoding:[e.encoding??mt.UTF16],attributeOnThingsBoard:[e.attributeOnThingsBoard,[$.required]]});else{const{key:t,byteFrom:i,byteTo:a,reportStrategy:r}=e;n=this.fb.group({key:[t,[$.required,$.pattern(an)]],byteFrom:[i??0,[$.required]],byteTo:[a??0,[$.required]],reportStrategy:[{value:r,disabled:this.isReportStrategyDisabled()}]})}t.push(n)})),this.fb.array(t)}isReportStrategyDisabled(){return!(this.withReportStrategy&&(this.keysType===li.ATTRIBUTES||this.keysType===li.TIMESERIES))}static{this.ɵfac=function(e){return new(e||XQ)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(it.TbPopoverComponent),t.ɵɵdirectiveInject(Ye.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:XQ,selectors:[["tb-device-data-keys-panel"]],inputs:{panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keys:"keys",keysType:"keysType",withReportStrategy:"withReportStrategy"},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:19,vars:16,consts:[["noKeys",""],["valueTitle",""],[1,"tb-device-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],["class","tb-form-panel no-border no-padding key-panel",4,"ngIf","ngIfElse"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","key-panel"],["class","tb-form-panel no-border no-padding tb-flex no-flex row center fill-width",4,"ngFor","ngForOf","ngForTrackBy"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],["class","title-container",4,"ngIf","ngIfElse"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"title-container"],[1,"title-container",3,"ngSwitch"],[4,"ngSwitchCase"],["class","tb-form-panel no-border no-padding",4,"ngIf"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],[1,"fixed-title-width","tb-flex","align-center"],[1,"tb-required"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["for","byteFrom",1,"tb-small-label"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap","flex-1"],["matInput","","required","","formControlName","byteFrom","type","number","id","byteFrom",3,"placeholder"],["for","byteTo",1,"tb-small-label"],["matInput","","required","","formControlName","byteTo","type","number","id","byteTo",3,"placeholder"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue"],[1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","methodRPC",3,"placeholder"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","encoding"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-row"],["formControlName","withResponse",1,"mat-slide",3,"click"],[3,"tb-hint-tooltip-icon"],[3,"value"],["formControlName","type"],[1,"tb-flex"],["formControlName","requestExpressionSource"],["matInput","","name","value","formControlName","requestExpression",3,"placeholder"],["matSuffix","","class","see-example p-1","tb-help-popup-placement","left",3,"tb-help-popup","tb-help-popup-style",4,"ngIf"],["formControlName","attributeNameExpressionSource"],["matInput","","name","value","formControlName","attributeNameExpression",3,"placeholder"],["matInput","","name","value","formControlName","attributeOnThingsBoard",3,"placeholder"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["translate","",1,"tb-prompt"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",2)(1,"div",3)(2,"div",4),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,KQ,2,2,"div",5),t.ɵɵelementStart(6,"div")(7,"button",6),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.addKey())})),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()()(),t.ɵɵtemplate(10,YQ,3,1,"ng-template",null,0,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(12,"div",7)(13,"button",8),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(14),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.applyKeysData())})),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(11);t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,8,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")"," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.keysListFormArray.controls.length)("ngIfElse",e),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,10,n.addKeyTitle)," "),t.ɵɵadvance(6),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(15,12,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,14,"action.apply")," ")}},dependencies:t.ɵɵgetComponentDepsFactory(XQ,[j,_,Xn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:500px;overflow:auto}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-small-label[_ngcontent-%COMP%]{font-size:16px;padding-right:0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}'],changeDetection:d.OnPush})}}e("DeviceDataKeysPanelComponent",XQ),Ge([I()],XQ.prototype,"withReportStrategy",void 0);const ZQ=()=>({maxWidth:"970px"});function QQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-filter-required"))}function JQ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-name-required"))}function eJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",40),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.device-profile-required"))}function tJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",41),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",e.toUpperCase()," ")}}function nJ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",27),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function iJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function aJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function rJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.type," ")}}function oJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.encoding," ")}}function sJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.methodRPC," ")}}class lJ extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.destroyRef=l,this.cdr=p,this.deviceFormGroup=this.fb.group({address:["",[$.required,$.pattern(an)]],deviceName:["",[$.required,$.pattern(an)]],deviceType:["",[$.required,$.pattern(an)]],encoding:[mt.UTF8],telemetry:[[]],attributes:[[]],attributeRequests:[[]],attributeUpdates:[[]],serverSideRpc:[[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),this.keysPopupClosed=!0,this.SocketValueKey=li,this.socketDeviceHelpLink=O+"/docs/iot-gateway/config/socket/#device-subsection",this.socketEncoding=Object.values(mt),this.ReportStrategyDefaultValue=en,this.deviceFormGroup.patchValue(this.data.value,{emitEvent:!1})}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.deviceFormGroup.valid){const e=this.deviceFormGroup.value;Te(e),this.dialogRef.close(e)}}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))return void this.popoverService.hidePopover(i);const a=this.deviceFormGroup.get(n),r={keys:a.value,keysType:n,panelTitle:pi.get(n),addKeyTitle:ui.get(n),deleteKeyTitle:mi.get(n),noKeysText:hi.get(n),withReportStrategy:this.data.withReportStrategy};this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,XQ,"leftBottom",!1,null,r,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(bn(this.destroyRef)).subscribe((e=>{this.popoverComponent.hide(),a.patchValue(e),a.markAsDirty(),this.cdr.markForCheck()})),this.popoverComponent.tbHideStart.pipe(bn(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}static{this.ɵfac=function(e){return new(e||lJ)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:lJ,selectors:[["tb-device-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:119,vars:57,consts:[["socketTelemetryButton",""],["attributesButton",""],["attributeRequestsButton",""],["attributesUpdatesButton",""],["rpcMethodsButton",""],[1,"dialog-mapping",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width"],["translate","",1,"tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","address",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","name","value","formControlName","deviceName",3,"placeholder"],["matInput","","name","value","formControlName","deviceType",3,"placeholder"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","encoding"],[3,"value",4,"ngFor","ngForOf"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],[1,"tb-form-row","space-between","tb-flex"],["translate","",1,"fixed-title-width"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[4,"ngFor","ngForOf"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[3,"value"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",5)(1,"mat-toolbar",6)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",7)(6,"div",8),t.ɵɵelementStart(7,"button",9),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",10),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",11)(11,"div",12)(12,"div",13)(13,"div",14)(14,"div",15),t.ɵɵtext(15," gateway.address-filter "),t.ɵɵelementEnd()(),t.ɵɵelementStart(16,"div",16)(17,"mat-form-field",17),t.ɵɵelement(18,"input",18),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,QQ,3,3,"mat-icon",19),t.ɵɵelement(21,"div",20),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"div",13)(23,"div",21),t.ɵɵtext(24,"gateway.device-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"div",16)(26,"mat-form-field",17),t.ɵɵelement(27,"input",22),t.ɵɵpipe(28,"translate"),t.ɵɵtemplate(29,JQ,3,3,"mat-icon",19),t.ɵɵelementEnd()()(),t.ɵɵelementStart(30,"div",13)(31,"div",21),t.ɵɵtext(32,"gateway.device-profile"),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"div",16)(34,"mat-form-field",17),t.ɵɵelement(35,"input",23),t.ɵɵpipe(36,"translate"),t.ɵɵtemplate(37,eJ,3,3,"mat-icon",19),t.ɵɵelementEnd()()(),t.ɵɵelementStart(38,"div",13)(39,"div",24),t.ɵɵpipe(40,"translate"),t.ɵɵtext(41," gateway.encoding "),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"div",16)(43,"mat-form-field",17)(44,"mat-select",25),t.ɵɵtemplate(45,tJ,2,2,"mat-option",26),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(46,nJ,1,2,"tb-report-strategy",27),t.ɵɵelementStart(47,"div",28)(48,"div",29),t.ɵɵtext(49,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(50,"div",30)(51,"mat-chip-listbox",31),t.ɵɵtemplate(52,iJ,2,1,"mat-chip",32),t.ɵɵelementStart(53,"mat-chip",33),t.ɵɵelement(54,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(55,"button",35,0),t.ɵɵpipe(57,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(56);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.TIMESERIES))})),t.ɵɵelementStart(58,"tb-icon",36),t.ɵɵtext(59,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(60,"div",28)(61,"div",29),t.ɵɵtext(62,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(63,"div",30)(64,"mat-chip-listbox",31),t.ɵɵtemplate(65,aJ,2,1,"mat-chip",32),t.ɵɵelementStart(66,"mat-chip",33),t.ɵɵelement(67,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(68,"button",35,1),t.ɵɵpipe(70,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(69);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.ATTRIBUTES))})),t.ɵɵelementStart(71,"tb-icon",36),t.ɵɵtext(72,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(73,"div",28)(74,"div",29),t.ɵɵtext(75,"gateway.attribute-requests"),t.ɵɵelementEnd(),t.ɵɵelementStart(76,"div",30)(77,"mat-chip-listbox",31),t.ɵɵtemplate(78,rJ,2,1,"mat-chip",32),t.ɵɵelementStart(79,"mat-chip",33),t.ɵɵelement(80,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(81,"button",35,2),t.ɵɵpipe(83,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(82);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.ATTRIBUTES_REQUESTS))})),t.ɵɵelementStart(84,"tb-icon",36),t.ɵɵtext(85,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(86,"div",28)(87,"div",29),t.ɵɵtext(88,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(89,"div",30)(90,"mat-chip-listbox",31),t.ɵɵtemplate(91,oJ,2,1,"mat-chip",32),t.ɵɵelementStart(92,"mat-chip",33),t.ɵɵelement(93,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(94,"button",35,3),t.ɵɵpipe(96,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(95);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.ATTRIBUTES_UPDATES))})),t.ɵɵelementStart(97,"tb-icon",36),t.ɵɵtext(98,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(99,"div",28)(100,"div",29),t.ɵɵtext(101,"gateway.rpc-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(102,"div",30)(103,"mat-chip-listbox",31),t.ɵɵtemplate(104,sJ,2,1,"mat-chip",32),t.ɵɵelementStart(105,"mat-chip",33),t.ɵɵelement(106,"label",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(107,"button",35,4),t.ɵɵpipe(109,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(108);return t.ɵɵresetView(n.manageKeys(i,a,n.SocketValueKey.RPC_METHODS))})),t.ɵɵelementStart(110,"tb-icon",36),t.ɵɵtext(111,"edit"),t.ɵɵelementEnd()()()()()(),t.ɵɵelementStart(112,"div",37)(113,"button",38),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(114),t.ɵɵpipe(115,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(116,"button",39),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(117),t.ɵɵpipe(118,"translate"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵproperty("formGroup",n.deviceFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,32,"gateway.device")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.socketDeviceHelpLink),t.ɵɵadvance(12),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,34,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceFormGroup.get("address").hasError("required")&&n.deviceFormGroup.get("address").touched),t.ɵɵadvance(),t.ɵɵproperty("tb-help-popup","widget/lib/gateway/address-filter_fn")("tb-help-popup-style",t.ɵɵpureFunction0(56,ZQ)),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,36,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceFormGroup.get("deviceName").hasError("required")&&n.deviceFormGroup.get("deviceName").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,38,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.deviceFormGroup.get("deviceType").hasError("required")&&n.deviceFormGroup.get("deviceType").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(40,40,"gateway.hints.encoding")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.socketEncoding),t.ɵɵadvance(),t.ɵɵconditional(n.data.withReportStrategy?46:-1),t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("telemetry").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("telemetry").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(57,42,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(70,44,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributeRequests").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("attributeRequests").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(83,46,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(96,48,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(109,50,"action.edit")),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(115,52,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.deviceFormGroup.invalid||!n.deviceFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(118,54,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(lJ,[j,_,Gn,Xn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:grid;height:100%}[_nghost-%COMP%] .tb-form-panel[_ngcontent-%COMP%]{width:77vw;max-width:800px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%]{max-width:900px;display:flex;flex-direction:column}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{min-height:64px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] tb-toggle-select[_ngcontent-%COMP%]{padding:4px 0}[_nghost-%COMP%] .mat-mdc-dialog-content[_ngcontent-%COMP%]{height:670px}[_nghost-%COMP%] .ellipsis-chips-container[_ngcontent-%COMP%]{max-width:70%}[_nghost-%COMP%] .dialog-mapping .mat-mdc-chip-listbox .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center}[_nghost-%COMP%] .tb-form-row .fixed-title-width{min-width:40px;width:35%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center}[_nghost-%COMP%] .tb-form-row .mat-mdc-form-field{width:0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}[_nghost-%COMP%] .device-config{gap:12px;padding-left:10px;padding-right:10px}[_nghost-%COMP%] .device-node-pattern-field{flex-basis:3%}'],changeDetection:d.OnPush})}}e("DeviceDialogComponent",lJ);const pJ=["searchInput"],cJ=()=>["address","deviceName","actions"],dJ=()=>({minWidth:"96px",textAlign:"center"});function uJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",26)(2,"span",27),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",9),t.ɵɵelementStart(6,"button",11),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageDevices(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",11),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.devices")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search")))}function mJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.address-filter")," "))}function hJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.address)}}function gJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.device-name")))}function fJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.deviceName)}}function yJ(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",30)}function vJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",11),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageDevices(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",11),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteDevice(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function xJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,vJ,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",31),t.ɵɵelementContainer(4,32),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",33)(6,"button",34),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",35),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",36,2),t.ɵɵelementContainer(11,32),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,dJ)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function bJ(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",37)}function wJ(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class SJ{constructor(e,t,n,i,a){this.translate=e,this.dialog=t,this.dialogService=n,this.fb=i,this.cdr=a,this.withReportStrategy=!0,this.textSearchMode=!1,this.textSearch=this.fb.control("",{nonNullable:!0}),this.onChange=()=>{},this.destroy$=new te,this.devicesFormGroup=this.fb.array([]),this.dataSource=new CJ}ngOnInit(){this.devicesFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.updateTableData(e),this.onChange(e)}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}ngAfterViewInit(){this.textSearch.valueChanges.pipe(de(150),ce(((e,t)=>(e??"")===t.trim())),le(this.destroy$)).subscribe((e=>this.updateTableData(this.devicesFormGroup.value,e.trim())))}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){this.devicesFormGroup.clear(),this.pushDataAsFormArrays(e)}enterFilterMode(){this.textSearchMode=!0,this.cdr.detectChanges();const e=this.searchInputField.nativeElement;e.focus(),e.setSelectionRange(0,0)}exitFilterMode(){this.updateTableData(this.devicesFormGroup.value),this.textSearchMode=!1,this.textSearch.reset()}manageDevices(e,t){e&&e.stopPropagation();const n=ke(t),i=n?this.devicesFormGroup.at(t).value:{};this.getDeviceDialog(i,n?"action.apply":"action.add").afterClosed().pipe(ye(1),le(this.destroy$)).subscribe((e=>{e&&(n?this.devicesFormGroup.at(t).patchValue(e):this.devicesFormGroup.push(this.fb.control(e)),this.devicesFormGroup.markAsDirty())}))}validate(){return this.devicesFormGroup.controls.length?null:{devicesFormGroup:{valid:!1}}}getDeviceDialog(e,t){return this.dialog.open(lJ,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,withReportStrategy:this.withReportStrategy}})}deleteDevice(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-device-title",{name:this.devicesFormGroup.controls[t].value.deviceName}),this.translate.instant("gateway.delete-device-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(ye(1),le(this.destroy$)).subscribe((e=>{e&&(this.devicesFormGroup.removeAt(t),this.devicesFormGroup.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}pushDataAsFormArrays(e){e?.length&&e.forEach((e=>this.devicesFormGroup.push(this.fb.control(e))))}static{this.ɵfac=function(e){return new(e||SJ)(t.ɵɵdirectiveInject(He.TranslateService),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(_e.DialogService),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:SJ,selectors:[["tb-devices-config-table"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(pJ,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.searchInputField=e.first)}},inputs:{withReportStrategy:"withReportStrategy"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>SJ)),multi:!0},{provide:K,useExisting:c((()=>SJ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:45,vars:36,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-device-table","tb-absolute-fill"],[1,"tb-device-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","table-value-column",4,"matHeaderCellDef"],["class","table-value-column",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-device-table-title"],[1,"table-value-column"],["tbTruncateWithTooltip",""],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,uJ,14,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"div",7)(7,"button",8),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-icon"),t.ɵɵtext(10,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",9)(12,"mat-label"),t.ɵɵtext(13," "),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",10,0),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵpipe(18,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(19,"mat-icon"),t.ɵɵtext(20,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(21,"div",12)(22,"table",13),t.ɵɵelementContainerStart(23,14),t.ɵɵtemplate(24,mJ,3,3,"mat-header-cell",15)(25,hJ,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(26,14),t.ɵɵtemplate(27,gJ,4,3,"mat-header-cell",15)(28,fJ,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(29,17),t.ɵɵtemplate(30,yJ,1,0,"mat-header-cell",18)(31,xJ,12,6,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(32,bJ,1,0,"mat-header-row",20)(33,wJ,1,0,"mat-row",21),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"section",22),t.ɵɵpipe(35,"async"),t.ɵɵelementStart(36,"button",23),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageDevices(i))})),t.ɵɵelementStart(37,"mat-icon",24),t.ɵɵtext(38,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(39,"span"),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(42,"span",25),t.ɵɵpipe(43,"async"),t.ɵɵtext(44," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(4,20,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,22,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,24,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,26,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","address"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","deviceName"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(34,cJ))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(35,cJ)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(35,28,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,30,"gateway.add-device")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(43,32,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(SJ,[j,_,qn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%]{width:100%;height:100%;background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:35%}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}e("DevicesConfigTableComponent",SJ),Ge([I()],SJ.prototype,"withReportStrategy",void 0);let CJ=class extends G{constructor(){super()}};function _J(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",14),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function TJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",15),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.address-required"))}function IJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",15),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.socketConfigFormGroup.get("port")))}}function EJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",15),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.socketConfigFormGroup.get("bufferSize").hasError("min")?"gateway.buffer-size-range":"gateway.buffer-size-required"))}}e("DevicesDatasource",CJ);class MJ{constructor(e){this.fb=e,this.portLimits=Ii,this.socketTypes=Object.values(si),this.onChange=e=>{},this.destroy$=new te,this.socketConfigFormGroup=this.fb.group({address:["",[$.required,$.pattern(an)]],type:[si.TCP],port:[5e4,[$.required,$.min(Ii.MIN),$.max(Ii.MAX)]],bufferSize:[1024,[$.required,$.min(1),$.pattern(an)]]}),this.socketConfigFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e)}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){const{address:t="",type:n=si.TCP,port:i=5e4,bufferSize:a=1024}=e??{};this.socketConfigFormGroup.reset({address:t,type:n,port:i,bufferSize:a})}validate(){return this.socketConfigFormGroup.valid?null:{socketConfigFormGroup:{valid:!1}}}static{this.ɵfac=function(e){return new(e||MJ)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:MJ,selectors:[["tb-socket-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>MJ)),multi:!0},{provide:K,useExisting:c((()=>MJ)),multi:!0}]),t.ɵɵStandaloneFeature],decls:34,vars:25,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],[1,"fixed-title-width"],["tbTruncateWithTooltip",""],["formControlName","type","appearance","fill"],[3,"value",4,"ngFor","ngForOf"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","address",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","name","value","min","1","formControlName","bufferSize",3,"placeholder"],[3,"value"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2)(3,"div",3),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(6,"tb-toggle-select",4),t.ɵɵtemplate(7,_J,2,2,"tb-toggle-option",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"div",1)(9,"div",6),t.ɵɵtext(10,"gateway.address"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"div",7)(12,"mat-form-field",8),t.ɵɵelement(13,"input",9),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,TJ,3,3,"mat-icon",10),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",1)(17,"div",6),t.ɵɵtext(18,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",7)(20,"mat-form-field",8),t.ɵɵelement(21,"input",11),t.ɵɵpipe(22,"translate"),t.ɵɵtemplate(23,IJ,3,3,"mat-icon",10),t.ɵɵelementEnd()()(),t.ɵɵelementStart(24,"div",1)(25,"div",12),t.ɵɵpipe(26,"translate"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"div",7)(30,"mat-form-field",8),t.ɵɵelement(31,"input",13),t.ɵɵpipe(32,"translate"),t.ɵɵtemplate(33,EJ,3,3,"mat-icon",10),t.ɵɵelementEnd()()()()),2&e&&(t.ɵɵproperty("formGroup",n.socketConfigFormGroup),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,13,"gateway.connection-type")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",n.socketTypes),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.socketConfigFormGroup.get("address").hasError("required")&&n.socketConfigFormGroup.get("address").touched),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(22,17,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",(n.socketConfigFormGroup.get("port").hasError("required")||n.socketConfigFormGroup.get("port").hasError("min")||n.socketConfigFormGroup.get("port").hasError("max"))&&n.socketConfigFormGroup.get("port").touched),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(26,19,"gateway.hints.buffer-size")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(28,21,"gateway.buffer-size")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(32,23,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.socketConfigFormGroup.get("bufferSize").hasError("required")||n.socketConfigFormGroup.get("bufferSize").hasError("min")&&n.socketConfigFormGroup.get("bufferSize").touched))},dependencies:t.ɵɵgetComponentDepsFactory(MJ,[j,_,r$,qn]),encapsulation:2,changeDetection:d.OnPush})}}e("SocketConfigComponent",MJ);class kJ extends Ba{constructor(){super(...arguments),this.isLegacy=!1}getMappedValue(e){return e}initBasicFormGroup(){return this.fb.group({socket:[],devices:[]})}mapConfigToFormValue(e){return{socket:e.socket??{},devices:e.devices??[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(kJ)))(n||kJ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:kJ,selectors:[["tb-socket-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>kJ)),multi:!0},{provide:K,useExisting:c((()=>kJ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:10,vars:14,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","socket"],["formControlName","devices",3,"withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-socket-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-devices-config-table",4),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,8,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,10,"gateway.socket"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,12,"gateway.devices"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("withReportStrategy",!n.isLegacy))},dependencies:t.ɵɵgetComponentDepsFactory(kJ,[j,_,MJ,SJ]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}e("SocketBasicConfigComponent",kJ);class PJ extends Ba{constructor(){super(...arguments),this.isLegacy=!0}getMappedValue(e){return Ha.mapSocketToDowngradedVersion(e)}initBasicFormGroup(){return this.fb.group({socket:[],devices:[]})}mapConfigToFormValue(e){return{socket:Ha.mapSocketToUpgradedVersion(e),devices:e?.devices?Ha.mapDevicesToUpgradedVersion(e.devices):[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(PJ)))(n||PJ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:PJ,selectors:[["tb-socket-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>PJ)),multi:!0},{provide:K,useExisting:c((()=>PJ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:10,vars:14,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","socket"],["formControlName","devices",3,"withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-socket-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-devices-config-table",4),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,8,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,10,"gateway.socket"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,12,"gateway.devices"),"*"),t.ɵɵadvance(2),t.ɵɵproperty("withReportStrategy",!n.isLegacy))},dependencies:t.ɵɵgetComponentDepsFactory(PJ,[j,_,MJ,SJ]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}function DJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.name-required"))}function OJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function AJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.applicationConfigFormGroup.get("port")))}}function FJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.object-id-required"))}function RJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",6),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.vendor-id-required"))}function BJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",21),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.SegmentationTypeTranslationsMap.get(e))," ")}}e("SocketLegacyBasicConfigComponent",PJ);class NJ extends Ra{constructor(){super(...arguments),this.segmentationTypes=Object.values(fa),this.SegmentationTypeTranslationsMap=ya,this.portLimits=Ii}get applicationConfigFormGroup(){return this.formGroup}initFormGroup(){return this.fb.group({objectName:["",[$.required,$.pattern(an)]],host:["",[$.required,$.pattern(an)]],port:[null,[$.required,$.min(Ii.MIN),$.max(Ii.MAX)]],mask:[""],objectIdentifier:[null,[$.required]],vendorIdentifier:[null,[$.required]],maxApduLengthAccepted:[],segmentationSupported:[fa.BOTH],networkNumber:[],deviceDiscoveryTimeoutInSec:[]})}mapOnChangeValue(e){return Te(e),e}onWriteValue(e){const{maxApduLengthAccepted:t=1476,segmentationSupported:n=fa.BOTH,networkNumber:i=3,deviceDiscoveryTimeoutInSec:a=5,...r}=e;this.formGroup.reset({...r,maxApduLengthAccepted:t,segmentationSupported:n,networkNumber:i,deviceDiscoveryTimeoutInSec:a},{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(NJ)))(n||NJ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:NJ,selectors:[["tb-bacnet-application-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>NJ)),multi:!0},{provide:K,useExisting:c((()=>NJ)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:83,vars:53,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","autocomplete","off","name","value","formControlName","objectName",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["translate","",1,"fixed-title-width"],["matInput","","name","value","formControlName","mask",3,"placeholder"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["matInput","","type","number","min","0","name","value","formControlName","objectIdentifier",3,"placeholder"],["matInput","","type","number","min","0","name","value","formControlName","vendorIdentifier",3,"placeholder"],[1,"tb-form-panel","stroked"],[1,"tb-settings"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-panel","no-border","no-padding","padding-top"],["matInput","","type","number","min","0","name","value","formControlName","maxApduLengthAccepted",3,"placeholder"],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","segmentationSupported"],[3,"value"],["matInput","","type","number","min","0","name","value","formControlName","networkNumber",3,"placeholder"],["matInput","","type","number","min","0","name","value","formControlName","deviceDiscoveryTimeoutInSec",3,"placeholder"],["translate","","matSuffix","",1,"block","pr-2"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.bacnet.object-name"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",3)(5,"mat-form-field",4),t.ɵɵelement(6,"input",5),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,DJ,3,3,"mat-icon",6),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",1)(10,"div",2),t.ɵɵtext(11,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",4),t.ɵɵelement(13,"input",7),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,OJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(16,"div",1)(17,"div",2),t.ɵɵtext(18,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"mat-form-field",4),t.ɵɵelement(20,"input",8),t.ɵɵpipe(21,"translate"),t.ɵɵtemplate(22,AJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(23,"div",1)(24,"div",9),t.ɵɵtext(25,"gateway.network-mask"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",4),t.ɵɵelement(27,"input",10),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(29,"div",1)(30,"div",11),t.ɵɵpipe(31,"translate"),t.ɵɵtext(32,"gateway.object-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"mat-form-field",4),t.ɵɵelement(34,"input",12),t.ɵɵpipe(35,"translate"),t.ɵɵtemplate(36,FJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(37,"div",1)(38,"div",11),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40,"gateway.vendor-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(41,"mat-form-field",4),t.ɵɵelement(42,"input",13),t.ɵɵpipe(43,"translate"),t.ɵɵtemplate(44,RJ,3,3,"mat-icon",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(45,"div",14)(46,"mat-expansion-panel",15)(47,"mat-expansion-panel-header")(48,"mat-panel-title")(49,"div",16),t.ɵɵtext(50,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(51,"div",17)(52,"div",1)(53,"div",11),t.ɵɵpipe(54,"translate"),t.ɵɵtext(55,"gateway.bacnet.apdu-length"),t.ɵɵelementEnd(),t.ɵɵelementStart(56,"mat-form-field",4),t.ɵɵelement(57,"input",18),t.ɵɵpipe(58,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(59,"div",1)(60,"div",19),t.ɵɵpipe(61,"translate"),t.ɵɵtext(62,"gateway.bacnet.segmentation.label"),t.ɵɵelementEnd(),t.ɵɵelementStart(63,"mat-form-field",4)(64,"mat-select",20),t.ɵɵrepeaterCreate(65,BJ,3,4,"mat-option",21,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()(),t.ɵɵelementStart(67,"div",1)(68,"div",19),t.ɵɵpipe(69,"translate"),t.ɵɵtext(70,"gateway.bacnet.network-number"),t.ɵɵelementEnd(),t.ɵɵelementStart(71,"mat-form-field",4),t.ɵɵelement(72,"input",22),t.ɵɵpipe(73,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(74,"div",1)(75,"div",19),t.ɵɵpipe(76,"translate"),t.ɵɵtext(77,"gateway.bacnet.device-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(78,"mat-form-field",4),t.ɵɵelement(79,"input",23),t.ɵɵpipe(80,"translate"),t.ɵɵelementStart(81,"span",24),t.ɵɵtext(82,"gateway.suffix.s"),t.ɵɵelementEnd()()()()()()()),2&e&&(t.ɵɵproperty("formGroup",n.applicationConfigFormGroup),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,23,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("objectName").hasError("required")&&n.applicationConfigFormGroup.get("objectName").touched?8:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,25,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("host").hasError("required")&&n.applicationConfigFormGroup.get("host").touched?15:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(21,27,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional((n.applicationConfigFormGroup.get("port").hasError("required")||n.applicationConfigFormGroup.get("port").hasError("min")||n.applicationConfigFormGroup.get("port").hasError("max"))&&n.applicationConfigFormGroup.get("port").touched?22:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(28,29,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(31,31,"gateway.hints.bacnet.object-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(35,33,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("objectIdentifier").hasError("required")&&n.applicationConfigFormGroup.get("objectIdentifier").touched?36:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(39,35,"gateway.hints.bacnet.vendor-id")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(43,37,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.applicationConfigFormGroup.get("vendorIdentifier").hasError("required")&&n.applicationConfigFormGroup.get("vendorIdentifier").touched?44:-1),t.ɵɵadvance(9),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(54,39,"gateway.hints.bacnet.apdu-length")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(58,41,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(61,43,"gateway.hints.bacnet.segmentation")),t.ɵɵadvance(5),t.ɵɵrepeater(n.segmentationTypes),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(69,45,"gateway.hints.bacnet.network-number")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(73,47,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(76,49,"gateway.hints.bacnet.device-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(80,51,"gateway.set")))},dependencies:t.ɵɵgetComponentDepsFactory(NJ,[j,_,r$]),encapsulation:2,changeDetection:d.OnPush})}}function LJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",11),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function VJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",14),t.ɵɵpipe(1,"translate"),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",15)(6,"mat-form-field",6),t.ɵɵelement(7,"input",16),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,LJ,3,3,"mat-icon",11),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",e.keyType===e.BacnetDeviceKeysType.TIMESERIES?t.ɵɵpipeBind1(1,4,"gateway.hints.socket.key-telemetry"):t.ɵɵpipeBind1(2,6,"gateway.hints.socket.key-attribute")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,8,"gateway.key")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,10,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.formGroup.get("key").hasError("required")&&e.formGroup.get("key").touched?9:-1)}}function qJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",11),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.method-required"))}function GJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",14),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",15)(5,"mat-form-field",6),t.ɵɵelement(6,"input",17),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,qJ,3,3,"mat-icon",11),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(1,4,"gateway.hints.method")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,6,"gateway.method")," "),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.formGroup.get("method").hasError("required")&&e.formGroup.get("method").touched?8:-1)}}function zJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext(2);t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.BacnetRequestTypeTranslationsMap.get(e))," ")}}function UJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",18)(2,"div",14),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"tb-toggle-select",19),t.ɵɵrepeaterCreate(7,zJ,3,4,"tb-toggle-option",8,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.hints.bacnet.request-type")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,4,"gateway.bacnet.request-type.label")),t.ɵɵadvance(3),t.ɵɵrepeater(e.requestTypes)}}function jJ(e,n){1&e&&(t.ɵɵelementStart(0,"div",3)(1,"div",4),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.request-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",6),t.ɵɵelement(5,"input",20),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,2,"gateway.hints.bacnet.request-timeout")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,4,"gateway.set")))}function HJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.BacnetKeyObjectTypeTranslationsMap.get(e))," ")}}function WJ(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",11),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.object-id-required"))}function $J(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.BacnetPropertyIdTranslationsMap.get(e))," ")}}function KJ(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",13),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Key)}}class YJ extends Ra{constructor(){super(...arguments),this.withReportStrategy=!0,this.propertyIds=Ia.get(Ca.analogOutput),this.objectTypes=Object.values(Ca),this.requestTypes=Object.values(Ma),this.ReportStrategyDefaultValue=en,this.BacnetDeviceKeysType=va,this.BacnetKeyObjectTypeTranslationsMap=_a,this.BacnetPropertyIdTranslationsMap=Ea,this.BacnetRequestTypeTranslationsMap=ka}ngOnInit(){this.formGroup=this.initKeyFormGroup(),this.observeValueChanges(),this.observeObjectType()}isReportStrategyDisabled(){return!(this.withReportStrategy&&(this.keyType===va.ATTRIBUTES||this.keyType===va.TIMESERIES))}initKeyFormGroup(){return this.fb.group({key:[{value:"",disabled:this.keyType===va.RPC_METHODS},[$.required,$.pattern(an)]],method:[{value:"",disabled:this.keyType!==va.RPC_METHODS},[$.required,$.pattern(an)]],objectType:[Ca.analogOutput],objectId:[0,[$.required]],propertyId:[Ta.presentValue],requestTimeout:[{value:0,disabled:this.keyType!==va.RPC_METHODS}],requestType:[{value:Ma.Write,disabled:this.keyType!==va.RPC_METHODS}],reportStrategy:[{value:null,disabled:this.isReportStrategyDisabled()}]})}observeObjectType(){this.formGroup.get("objectType").valueChanges.pipe(bn(this.destroyRef)).subscribe((e=>{this.propertyIds=Ia.get(e),this.propertyIds.includes(this.formGroup.get("propertyId").value)||this.formGroup.get("propertyId").patchValue(this.propertyIds[0],{emitEvent:!1})}))}initFormGroup(){return this.fb.group({})}mapOnChangeValue({reportStrategy:e,...t}){return e?{...t,reportStrategy:e}:t}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(YJ)))(n||YJ)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:YJ,selectors:[["tb-bacnet-device-data-key"]],inputs:{keyType:"keyType",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>YJ)),multi:!0},{provide:K,useExisting:c((()=>YJ)),multi:!0}]),t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:35,vars:15,consts:[[1,"tb-form-panel","no-border","no-padding",3,"formGroup"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap","raw-value-option"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","objectType"],[3,"value"],[1,"tb-form-table-row-cell","tb-flex","no-gap"],["matInput","","type","number","min","0","name","value","formControlName","objectId",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["formControlName","propertyId"],["formControlName","reportStrategy",3,"isExpansionMode","defaultValue"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matInput","","name","value","formControlName","method",3,"placeholder"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],["formControlName","requestType","appearance","fill"],["matInput","","type","number","min","0","name","value","formControlName","requestTimeout",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.platform-side"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",3),t.ɵɵtemplate(5,VJ,10,12)(6,GJ,9,10),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",1)(8,"div",2),t.ɵɵtext(9,"gateway.connector-side"),t.ɵɵelementEnd(),t.ɵɵtemplate(10,UJ,9,6,"div",3)(11,jJ,7,6,"div",3),t.ɵɵelementStart(12,"div",3)(13,"div",4),t.ɵɵpipe(14,"translate"),t.ɵɵtext(15,"gateway.object-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"div",5)(17,"mat-form-field",6)(18,"mat-select",7),t.ɵɵrepeaterCreate(19,HJ,3,4,"mat-option",8,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()()(),t.ɵɵelementStart(21,"div",9)(22,"mat-form-field",6),t.ɵɵelement(23,"input",10),t.ɵɵpipe(24,"translate"),t.ɵɵtemplate(25,WJ,3,3,"mat-icon",11),t.ɵɵelementEnd()()(),t.ɵɵelementStart(26,"div",3)(27,"div",4),t.ɵɵpipe(28,"translate"),t.ɵɵtext(29,"gateway.property-id"),t.ɵɵelementEnd(),t.ɵɵelementStart(30,"mat-form-field",6)(31,"mat-select",12),t.ɵɵrepeaterCreate(32,$J,3,4,"mat-option",8,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()()(),t.ɵɵtemplate(34,KJ,1,2,"tb-report-strategy",13),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.formGroup),t.ɵɵadvance(5),t.ɵɵconditional(n.keyType!==n.BacnetDeviceKeysType.RPC_METHODS?5:6),t.ɵɵadvance(5),t.ɵɵconditional(n.keyType===n.BacnetDeviceKeysType.RPC_METHODS?10:-1),t.ɵɵadvance(),t.ɵɵconditional(n.keyType===n.BacnetDeviceKeysType.RPC_METHODS?11:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(14,9,"gateway.hints.bacnet.key-object-id")),t.ɵɵadvance(6),t.ɵɵrepeater(n.objectTypes),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(24,11,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.formGroup.get("objectId").hasError("required")&&n.formGroup.get("objectId").touched?25:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(28,13,"gateway.hints.bacnet.property-id")),t.ɵɵadvance(5),t.ɵɵrepeater(n.propertyIds),t.ɵɵadvance(2),t.ɵɵconditional(n.isReportStrategyDisabled()?-1:34))},dependencies:t.ɵɵgetComponentDepsFactory(YJ,[j,_,Xn]),encapsulation:2,changeDetection:d.OnPush})}}function XJ(e,n){if(1&e&&t.ɵɵelement(0,"tb-bacnet-device-data-key",17),2&e){const e=t.ɵɵnextContext().$implicit,n=t.ɵɵnextContext(2);t.ɵɵproperty("formControl",e)("keyType",n.keysType)("withReportStrategy",n.withReportStrategy)}}function ZJ(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵelementContainerStart(2,11),t.ɵɵelementStart(3,"mat-expansion-panel",12)(4,"mat-expansion-panel-header",13)(5,"mat-panel-title")(6,"div",14),t.ɵɵtext(7),t.ɵɵelementEnd()()(),t.ɵɵtemplate(8,XJ,1,3,"ng-template",15),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"button",16),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"delete"),t.ɵɵelementEnd()()()}if(2&e){let e;const i=n.$implicit,a=n.$index,r=n.$count,o=t.ɵɵnextContext(2);t.ɵɵadvance(2),t.ɵɵproperty("formGroup",i),t.ɵɵadvance(),t.ɵɵproperty("expanded",a===r-1),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",null!==(e=null==i.value?null:i.value.key)&&void 0!==e?e:null==i.value?null:i.value.method," "),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(10,4,o.deleteKeyTitle))}}function QJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3),t.ɵɵrepeaterCreate(1,ZJ,13,6,"div",9,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵrepeater(e.keysListFormArray.controls)}}function JJ(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",4)(1,"span",18),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.noKeysText)}}class e1 extends q{constructor(e,t,n){super(n),this.fb=e,this.popover=t,this.store=n,this.withReportStrategy=!0,this.keysDataApplied=p(),this.ReportStrategyDefaultValue=en}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}addKey(){this.keysListFormArray.push(this.fb.control({}))}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){this.keysDataApplied.emit(this.keysListFormArray.value)}prepareKeysFormArray(e){const t=[];return e?.forEach((e=>{t.push(this.fb.control(e))})),this.fb.array(t)}static{this.ɵfac=function(e){return new(e||e1)(t.ɵɵdirectiveInject(H.UntypedFormBuilder),t.ɵɵdirectiveInject(it.TbPopoverComponent),t.ɵɵdirectiveInject(Ye.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:e1,selectors:[["tb-bacnet-device-data-keys-panel"]],inputs:{panelTitle:"panelTitle",addKeyTitle:"addKeyTitle",deleteKeyTitle:"deleteKeyTitle",noKeysText:"noKeysText",keys:"keys",keysType:"keysType",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:18,vars:15,consts:[[1,"tb-device-keys-panel"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],[1,"tb-form-panel","no-border","no-padding","key-panel"],[1,"tb-flex","no-flex","center","align-center","key-panel"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"title-container"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[3,"formControl","keyType","withReportStrategy"],["translate","",1,"tb-prompt"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,QJ,3,0,"div",3)(6,JJ,3,1,"div",4),t.ɵɵelementStart(7,"div")(8,"button",5),t.ɵɵlistener("click",(function(){return n.addKey()})),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(11,"div",6)(12,"button",7),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"button",8),t.ɵɵlistener("click",(function(){return n.applyKeysData()})),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,7,n.panelTitle),""," ("+n.keysListFormArray.controls.length+")"," "),t.ɵɵadvance(2),t.ɵɵconditional(n.keysListFormArray.controls.length?5:6),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(10,9,n.addKeyTitle)," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,11,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,13,"action.apply")," "))},dependencies:t.ɵɵgetComponentDepsFactory(e1,[j,_,YJ]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%]{width:77vw;max-width:700px}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{max-width:11vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .key-panel[_ngcontent-%COMP%]{height:500px;overflow:auto}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-form-panel[_ngcontent-%COMP%] .mat-mdc-icon-button[_ngcontent-%COMP%]{width:56px;height:56px;padding:16px;color:#0000008a}[_nghost-%COMP%] .tb-device-keys-panel[_ngcontent-%COMP%] .tb-small-label[_ngcontent-%COMP%]{font-size:16px;padding-right:0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}'],changeDetection:d.OnPush})}}function t1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function n1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.deviceFormGroup.get("port")))}}function i1(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",12)(1,"mat-expansion-panel",34)(2,"mat-expansion-panel-header")(3,"mat-panel-title")(4,"div",35),t.ɵɵtext(5,"gateway.advanced-configuration-settings"),t.ɵɵelementEnd()()(),t.ɵɵelement(6,"tb-string-items-list",36),t.ɵɵpipe(7,"translate"),t.ɵɵpipe(8,"translate"),t.ɵɵelementEnd()()),2&e){let e;const n=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(7,2,"gateway.bacnet.alt-responses-address")),t.ɵɵpropertyInterpolate("placeholder",null!=(e=n.deviceFormGroup.get("altResponsesAddresses").value)&&e.length?"":t.ɵɵpipeBind1(8,4,"gateway.address"))}}function a1(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",19),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function r1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",16),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.hints.poll-period-required"))}function o1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function s1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function l1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function p1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.method," ")}}class c1 extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.cdr=l,this.destroyRef=p,this.deviceFormGroup=this.fb.group({host:["",[$.required,$.pattern(an)]],port:["",[$.required,$.min(Ii.MIN),$.max(Ii.MAX)]],deviceInfo:[],altResponsesAddresses:[{value:[],disabled:this.data.hideNewFields}],pollPeriod:[1e4,[$.required,$.min(0)]],timeseries:[[]],attributes:[[]],attributeUpdates:[[]],serverSideRpc:[[]],reportStrategy:[{value:null,disabled:!this.data.withReportStrategy}]}),this.keysPopupClosed=!0,this.BacnetDeviceKeysType=va,this.DeviceInfoType=ga,this.portLimits=Ii,this.deviceHelpLink=O+"/docs/iot-gateway/config/bacnet/#device-object-settings",this.sourceTypes=Object.values(di),this.ConnectorType=ct,this.ReportStrategyDefaultValue=en,this.deviceFormGroup.patchValue(this.data.value,{emitEvent:!1})}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.deviceFormGroup.valid){const{altResponsesAddresses:e,reportStrategy:t,...n}=this.deviceFormGroup.value;this.dialogRef.close({altResponsesAddresses:e??[],...t?{reportStrategy:t}:{},...n})}}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))return void this.popoverService.hidePopover(i);const a=this.deviceFormGroup.get(n),r={keys:a.value,keysType:n,panelTitle:xa.get(n),addKeyTitle:ba.get(n),deleteKeyTitle:wa.get(n),noKeysText:Sa.get(n),withReportStrategy:this.data.withReportStrategy};this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,e1,"leftBottom",!1,null,r,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.subscribe((e=>{this.popoverComponent.hide(),a.patchValue(e),a.markAsDirty(),this.cdr.markForCheck()})),this.popoverComponent.tbHideStart.pipe(bn(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}static{this.ɵfac=function(e){return new(e||c1)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:c1,selectors:[["tb-bacnet-device-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:100,vars:47,consts:[["attributesButton",""],["socketTelemetryButton",""],["attributesUpdatesButton",""],["rpcMethodsButton",""],[1,"dialog-mapping",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["formControlName","deviceInfo","required","true",3,"deviceInfoType","sourceTypes","connectorType"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],[1,"fixed-title-width","tb-required"],["tbTruncateWithTooltip","","translate",""],["matInput","","type","number","min","0","name","value","formControlName","pollPeriod",3,"placeholder"],[1,"tb-form-row","space-between","tb-flex"],["translate","",1,"fixed-title-width"],[1,"tb-flex","ellipsis-chips-container"],[1,"tb-flex",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[1,"tb-settings","chips-panel","w-full"],["translate","",1,"tb-form-panel-title"],["editable","","floatLabel","always","formControlName","altResponsesAddresses",1,"chips-list",3,"label","placeholder"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",4)(1,"mat-toolbar",5)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",6)(6,"div",7),t.ɵɵelementStart(7,"button",8),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(8,"mat-icon",9),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",10)(11,"div",11)(12,"div",12)(13,"div",13),t.ɵɵtext(14,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-form-field",14),t.ɵɵelement(16,"input",15),t.ɵɵpipe(17,"translate"),t.ɵɵtemplate(18,t1,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(19,"div",12)(20,"div",13),t.ɵɵtext(21,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",14),t.ɵɵelement(23,"input",17),t.ɵɵpipe(24,"translate"),t.ɵɵtemplate(25,n1,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵtemplate(26,i1,9,6,"div",12),t.ɵɵelement(27,"tb-device-info-table",18),t.ɵɵtemplate(28,a1,1,2,"tb-report-strategy",19),t.ɵɵelementStart(29,"div",12)(30,"div",20)(31,"span",21),t.ɵɵtext(32,"gateway.poll-period"),t.ɵɵelementEnd()(),t.ɵɵelementStart(33,"mat-form-field",14),t.ɵɵelement(34,"input",22),t.ɵɵpipe(35,"translate"),t.ɵɵtemplate(36,r1,3,3,"mat-icon",16),t.ɵɵelementEnd()(),t.ɵɵelementStart(37,"div",23)(38,"div",24),t.ɵɵtext(39,"gateway.attributes"),t.ɵɵelementEnd(),t.ɵɵelementStart(40,"div",25)(41,"mat-chip-listbox",26),t.ɵɵrepeaterCreate(42,o1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(44,"mat-chip",27),t.ɵɵelement(45,"label",28),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"button",29,0),t.ɵɵpipe(48,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(47);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.ATTRIBUTES))})),t.ɵɵelementStart(49,"tb-icon",30),t.ɵɵtext(50,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(51,"div",23)(52,"div",24),t.ɵɵtext(53,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(54,"div",25)(55,"mat-chip-listbox",26),t.ɵɵrepeaterCreate(56,s1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(58,"mat-chip",27),t.ɵɵelement(59,"label",28),t.ɵɵelementEnd()(),t.ɵɵelementStart(60,"button",29,1),t.ɵɵpipe(62,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(61);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.TIMESERIES))})),t.ɵɵelementStart(63,"tb-icon",30),t.ɵɵtext(64,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(65,"div",23)(66,"div",24),t.ɵɵtext(67,"gateway.attribute-updates"),t.ɵɵelementEnd(),t.ɵɵelementStart(68,"div",25)(69,"mat-chip-listbox",26),t.ɵɵrepeaterCreate(70,l1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(72,"mat-chip",27),t.ɵɵelement(73,"label",28),t.ɵɵelementEnd()(),t.ɵɵelementStart(74,"button",29,2),t.ɵɵpipe(76,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(75);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.ATTRIBUTES_UPDATES))})),t.ɵɵelementStart(77,"tb-icon",30),t.ɵɵtext(78,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(79,"div",23)(80,"div",24),t.ɵɵtext(81,"gateway.rpc-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(82,"div",25)(83,"mat-chip-listbox",26),t.ɵɵrepeaterCreate(84,p1,2,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(86,"mat-chip",27),t.ɵɵelement(87,"label",28),t.ɵɵelementEnd()(),t.ɵɵelementStart(88,"button",29,3),t.ɵɵpipe(90,"translate"),t.ɵɵlistener("click",(function(i){t.ɵɵrestoreView(e);const a=t.ɵɵreference(89);return t.ɵɵresetView(n.manageKeys(i,a,n.BacnetDeviceKeysType.RPC_METHODS))})),t.ɵɵelementStart(91,"tb-icon",30),t.ɵɵtext(92,"edit"),t.ɵɵelementEnd()()()()()(),t.ɵɵelementStart(93,"div",31)(94,"button",32),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.cancel())})),t.ɵɵtext(95),t.ɵɵpipe(96,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(97,"button",33),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.add())})),t.ɵɵtext(98),t.ɵɵpipe(99,"translate"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵproperty("formGroup",n.deviceFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,27,"gateway.device")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.deviceHelpLink),t.ɵɵadvance(10),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(17,29,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.deviceFormGroup.get("host").hasError("required")&&n.deviceFormGroup.get("host").touched?18:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(24,31,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional((n.deviceFormGroup.get("port").hasError("required")||n.deviceFormGroup.get("port").hasError("min")||n.deviceFormGroup.get("port").hasError("max"))&&n.deviceFormGroup.get("port").touched?25:-1),t.ɵɵadvance(),t.ɵɵconditional(n.data.hideNewFields?-1:26),t.ɵɵadvance(),t.ɵɵproperty("deviceInfoType",n.DeviceInfoType.FULL)("sourceTypes",n.sourceTypes)("connectorType",n.ConnectorType.BACNET),t.ɵɵadvance(),t.ɵɵconditional(n.data.withReportStrategy?28:-1),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(35,33,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.deviceFormGroup.get("pollPeriod").hasError("required")&&n.deviceFormGroup.get("pollPeriod").touched?36:-1),t.ɵɵadvance(5),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("attributes").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(48,35,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("timeseries").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("timeseries").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(62,37,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("attributeUpdates").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(76,39,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(),t.ɵɵrepeater(n.deviceFormGroup.get("serverSideRpc").value),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(90,41,"action.edit")),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(96,43,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.deviceFormGroup.invalid||!n.deviceFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(99,45,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(c1,[j,_,Gn,qn,r$,I$,Xn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{display:grid;height:100%}[_nghost-%COMP%] .tb-form-panel[_ngcontent-%COMP%]{width:77vw;max-width:800px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%]{max-width:900px;display:flex;flex-direction:column}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{min-height:64px}[_nghost-%COMP%] .dialog-mapping[_ngcontent-%COMP%] tb-toggle-select[_ngcontent-%COMP%]{padding:4px 0}[_nghost-%COMP%] .mat-mdc-dialog-content[_ngcontent-%COMP%]{height:670px}[_nghost-%COMP%] .ellipsis-chips-container[_ngcontent-%COMP%]{max-width:70%}[_nghost-%COMP%] .chips-panel[_ngcontent-%COMP%]{padding:6px 6px 6px 0}[_nghost-%COMP%] .dialog-mapping .mat-mdc-chip-listbox .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center}[_nghost-%COMP%] .tb-form-row .fixed-title-width{min-width:40px;width:35%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center}[_nghost-%COMP%] .tb-form-row .mat-mdc-form-field{width:0}[_nghost-%COMP%] .tb-form-row .chips-list .mat-mdc-form-field{width:100%}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex}[_nghost-%COMP%] .device-config{gap:12px;padding-left:10px;padding-right:10px}[_nghost-%COMP%] .device-node-pattern-field{flex-basis:3%}'],changeDetection:d.OnPush})}}const d1=()=>["deviceName","host","port","actions"],u1=()=>({minWidth:"96px",textAlign:"center"});function m1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",26)(2,"span",27),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelement(5,"span",9),t.ɵɵelementStart(6,"button",11),t.ɵɵpipe(7,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageDevices(n))})),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"button",11),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.devices")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,7,"action.search")))}function h1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.device-name")))}function g1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(null==e.deviceInfo?null:e.deviceInfo.deviceNameExpression)}}function f1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.host")," "))}function y1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.host)}}function v1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",28),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.port")," "))}function x1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",28)(1,"div",29),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.port)}}function b1(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",30)}function w1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",11),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageDevices(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",11),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteDevice(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function S1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,w1,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",31),t.ɵɵelementContainer(4,32),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",33)(6,"button",34),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",35),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",36,2),t.ɵɵelementContainer(11,32),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(3),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,u1)),t.ɵɵadvance(),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function C1(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",37)}function _1(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class T1 extends qa{constructor(){super(...arguments),this.hideNewFields=!1}getDatasource(){return new I1}manageDevices(e,t){e&&e.stopPropagation();const n=ke(t),i=n?this.entityFormArray.at(t).value:{};this.getDeviceDialog(i,n?"action.apply":"action.add").afterClosed().pipe(ye(1),bn(this.destroyRef)).subscribe((e=>{e&&(n?this.entityFormArray.at(t).patchValue(e):this.entityFormArray.push(this.fb.control(e)),this.entityFormArray.markAsDirty())}))}deleteDevice(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-device-title",{name:this.entityFormArray.controls[t].value.deviceInfo?.deviceNameExpression}),this.translate.instant("gateway.delete-device-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(ye(1),bn(this.destroyRef)).subscribe((e=>{e&&(this.entityFormArray.removeAt(t),this.entityFormArray.markAsDirty())}))}getDeviceDialog(e,t){return this.dialog.open(c1,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,withReportStrategy:this.withReportStrategy,hideNewFields:this.hideNewFields}})}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())||e.deviceNameExpression?.toString().toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(T1)))(n||T1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:T1,selectors:[["tb-bacnet-devices-config-table"]],inputs:{hideNewFields:[2,"hideNewFields","hideNewFields",m]},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>T1)),multi:!0},{provide:K,useExisting:c((()=>T1)),multi:!0}]),t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:48,vars:37,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-device-table","tb-absolute-fill"],[1,"tb-device-table-content","flex","flex-col"],[1,"mat-mdc-table-toolbar"],["class","mat-toolbar-tools",4,"ngIf"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"table-container"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","table-value-column",4,"matHeaderCellDef"],["class","table-value-column",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"tb-device-table-title"],[1,"table-value-column"],["tbTruncateWithTooltip",""],[1,"w-12"],[1,"lt-lg:!hidden","flex","flex-1","flex-row","items-stretch","justify-end"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,m1,14,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"div",7)(7,"button",8),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-icon"),t.ɵɵtext(10,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",9)(12,"mat-label"),t.ɵɵtext(13," "),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",10,0),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"button",11),t.ɵɵpipe(18,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(19,"mat-icon"),t.ɵɵtext(20,"close"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(21,"div",12)(22,"table",13),t.ɵɵelementContainerStart(23,14),t.ɵɵtemplate(24,h1,4,3,"mat-header-cell",15)(25,g1,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(26,14),t.ɵɵtemplate(27,f1,3,3,"mat-header-cell",15)(28,y1,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(29,14),t.ɵɵtemplate(30,v1,3,3,"mat-header-cell",15)(31,x1,3,1,"mat-cell",16),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(32,17),t.ɵɵtemplate(33,b1,1,0,"mat-header-cell",18)(34,S1,12,6,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(35,C1,1,0,"mat-header-row",20)(36,_1,1,0,"mat-row",21),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"section",22),t.ɵɵpipe(38,"async"),t.ɵɵelementStart(39,"button",23),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageDevices(i))})),t.ɵɵelementStart(40,"mat-icon",24),t.ɵɵtext(41,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"span"),t.ɵɵtext(43),t.ɵɵpipe(44,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(45,"span",25),t.ɵɵpipe(46,"async"),t.ɵɵtext(47," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵproperty("ngIf",!1===t.ɵɵpipeBind1(4,21,n.dataSource.isEmpty())),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,23,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(16,25,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,27,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","deviceName"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","host"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","port"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(35,d1))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(36,d1)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(38,29,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(44,31,"gateway.add-device")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(46,33,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(T1,[j,_,qn]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%]{width:100%;height:100%;background:#fff;overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .mat-toolbar-tools[_ngcontent-%COMP%]{min-height:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .title-container[_ngcontent-%COMP%]{overflow:hidden}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%]{table-layout:fixed;min-width:450px}[_nghost-%COMP%] .tb-device-table[_ngcontent-%COMP%] .tb-device-table-content[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%] .mat-mdc-table[_ngcontent-%COMP%] .table-value-column[_ngcontent-%COMP%]{padding:0 12px;width:21%}[_nghost-%COMP%] .no-data-found[_ngcontent-%COMP%]{height:calc(100% - 120px)}@media screen and (max-width: 599px){[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{height:auto;min-height:100px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%] .tb-device-table-title[_ngcontent-%COMP%]{padding-bottom:5px;width:100%}}[_nghost-%COMP%] mat-cell.tb-value-cell{cursor:pointer}[_nghost-%COMP%] mat-cell.tb-value-cell .mat-icon{height:24px;width:24px;font-size:24px;color:#757575}'],changeDetection:d.OnPush})}}class I1 extends G{constructor(){super()}}class E1 extends Ba{initBasicFormGroup(){return this.fb.group({application:[],devices:[]})}mapConfigToFormValue(e){return{application:e.application??{},devices:e.devices??[]}}getMappedValue(e){return{application:e.application,devices:e.devices??[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(E1)))(n||E1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:E1,selectors:[["tb-bacnet-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>E1)),multi:!0},{provide:K,useExisting:c((()=>E1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:15,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","application"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","devices",3,"hideNewFields","withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-bacnet-application-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-bacnet-devices-config-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.application"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.devices"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("hideNewFields",n.isLegacy)("withReportStrategy",n.withReportStrategy))},dependencies:t.ɵɵgetComponentDepsFactory(E1,[j,_,NJ,T1]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}class M1 extends Ba{constructor(){super(...arguments),this.isLegacy=!0}initBasicFormGroup(){return this.fb.group({application:[],devices:[]})}mapConfigToFormValue(e){return{application:e.general?Wa.mapApplicationToUpgradedVersion(e.general):{},devices:e.devices?.length?Wa.mapDevicesToUpgradedVersion(e.devices):[]}}getMappedValue(e){return{general:e.application?Wa.mapApplicationToDowngradedVersion(e.application):{},devices:e.devices?.length?Wa.mapDevicesToDowngradedVersion(e.devices):[]}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(M1)))(n||M1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:M1,selectors:[["tb-bacnet-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>M1)),multi:!0},{provide:K,useExisting:c((()=>M1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:15,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","application"],[1,"tb-form-panel","no-border","no-padding","padding-top","tb-flex","fill-height"],["formControlName","devices",3,"hideNewFields","withReportStrategy"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-bacnet-application-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"div",4),t.ɵɵelement(10,"tb-bacnet-devices-config-table",5),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.application"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.devices"),"*"),t.ɵɵadvance(3),t.ɵɵproperty("hideNewFields",n.isLegacy)("withReportStrategy",n.withReportStrategy))},dependencies:t.ɵɵgetComponentDepsFactory(M1,[j,_,NJ,T1]),styles:['@charset "UTF-8";[_nghost-%COMP%]{height:100%}[_nghost-%COMP%] .mat-mdc-tab-group, [_nghost-%COMP%] .mat-mdc-tab-body-wrapper{height:100%}'],changeDetection:d.OnPush})}}function k1(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",5),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.host-required"))}function P1(e,n){if(1&e&&(t.ɵɵelement(0,"tb-error-icon",8),t.ɵɵpipe(1,"getGatewayPortTooltip"),t.ɵɵpipe(2,"translate")),2&e){t.ɵɵnextContext();const e=t.ɵɵreadContextLet(16);t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(2,3,t.ɵɵpipeBind1(1,1,e)))}}class D1 extends Ra{constructor(){super(...arguments),this.portLimits=Ii}get serverConfigFormGroup(){return this.formGroup}initFormGroup(){return this.fb.group({host:["",[$.required,$.pattern(an)]],port:[null,[$.required,$.min(Ii.MIN),$.max(Ii.MAX)]],SSL:[!1],security:this.fb.group({cert:[""],key:[""]})})}mapOnChangeValue(e){return Te(e),e}onWriteValue(e){this.formGroup.reset(e,{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(D1)))(n||D1)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:D1,selectors:[["tb-rest-server-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>D1)),multi:!0},{provide:K,useExisting:c((()=>D1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:46,vars:43,consts:[[1,"tb-form-panel","no-border","no-padding","padding-top",3,"formGroup"],[1,"tb-form-row"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","host",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],[1,"tb-form-row","column-xs"],["matInput","","type","number","name","value","formControlName","port",3,"min","max","placeholder"],["matSuffix","",3,"tooltipText"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","space-between","tb-flex","fill-width"],["formControlName","SSL",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],[3,"formGroup"],["tbTruncateWithTooltip","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],[1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","cert",3,"placeholder"],["matInput","","name","value","formControlName","key",3,"placeholder"]],template:function(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4,"gateway.host"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-form-field",3),t.ɵɵelement(6,"input",4),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,k1,3,3,"mat-icon",5),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",6)(10,"div",2),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"gateway.port"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",3),t.ɵɵelement(14,"input",7),t.ɵɵpipe(15,"translate"),t.ɵɵdeclareLet(16),t.ɵɵtemplate(17,P1,3,5,"tb-error-icon",8),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",9)(19,"div",10),t.ɵɵtext(20,"gateway.security"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",11)(22,"mat-slide-toggle",12)(23,"mat-label",13),t.ɵɵpipe(24,"translate"),t.ɵɵtext(25),t.ɵɵpipe(26,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerStart(27,14),t.ɵɵelementStart(28,"div",11)(29,"div",15),t.ɵɵpipe(30,"translate"),t.ɵɵtext(31),t.ɵɵpipe(32,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"div",16)(34,"mat-form-field",3),t.ɵɵelement(35,"input",17),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(37,"div",11)(38,"div",15),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40),t.ɵɵpipe(41,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(42,"div",16)(43,"mat-form-field",3),t.ɵɵelement(44,"input",18),t.ɵɵpipe(45,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()),2&e){t.ɵɵproperty("formGroup",n.serverConfigFormGroup),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,18,"gateway.hints.rest.host")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,20,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.serverConfigFormGroup.get("host").hasError("required")&&n.serverConfigFormGroup.get("host").touched?8:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,22,"gateway.hints.rest.port")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("min",n.portLimits.MIN),t.ɵɵpropertyInterpolate("max",n.portLimits.MAX),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,24,"gateway.set")),t.ɵɵadvance(2);const e=t.ɵɵstoreLet(n.serverConfigFormGroup.get("port"));t.ɵɵadvance(),t.ɵɵconditional((e.hasError("required")||e.hasError("min")||e.hasError("max"))&&e.touched?17:-1),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(24,27,"gateway.hints.rest.ssl-verify")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(26,29,"gateway.rest.ssl-verify")," "),t.ɵɵadvance(2),t.ɵɵproperty("formGroup",n.serverConfigFormGroup.get("security")),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(30,31,"gateway.hints.rest.cert")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(32,33,"gateway.certificate")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,35,"gateway.set")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(39,37,"gateway.hints.rest.key")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(41,39,"gateway.key")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(45,41,"gateway.set"))}},dependencies:t.ɵɵgetComponentDepsFactory(D1,[j,_,r$,qn,Qn]),encapsulation:2,changeDetection:d.OnPush})}}const O1=()=>({min:1});function A1(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",4),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.ResponseTypeTranslationsMap.get(e)))}}function F1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",4),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function R1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",4),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function B1(e,n){if(1&e&&(t.ɵɵelementContainerStart(0,7),t.ɵɵelementStart(1,"div",8)(2,"div",9),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",10)(6,"mat-form-field",11)(7,"mat-select",12),t.ɵɵrepeaterCreate(8,F1,2,2,"mat-option",4,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(10,"div",8)(11,"div",9),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",10)(15,"mat-form-field",11)(16,"mat-select",13),t.ɵɵrepeaterCreate(17,R1,2,2,"mat-option",4,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("formGroup",e.responseConfigFormGroup.get(e.ResponseType.CONST)),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.rest.on-success")),t.ɵɵadvance(5),t.ɵɵrepeater(e.responseStatuses),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,5,"gateway.rest.on-error")),t.ɵɵadvance(5),t.ɵɵrepeater(e.responseStatuses)}}function N1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.response-timeout-required"))}function L1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,O1)))}function V1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function q1(e,n){1&e&&(t.ɵɵelementStart(0,"span",18),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function G1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.response-attribute-required"))}function z1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",17),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.response-attribute-pattern"))}function U1(e,n){if(1&e&&(t.ɵɵdeclareLet(0),t.ɵɵelementContainerStart(1,7),t.ɵɵelementStart(2,"div",8)(3,"mat-slide-toggle",14)(4,"mat-label"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(7,"div",8)(8,"div",15),t.ɵɵtext(9,"gateway.timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-form-field",11),t.ɵɵelement(11,"input",16),t.ɵɵpipe(12,"translate"),t.ɵɵdeclareLet(13),t.ɵɵtemplate(14,N1,2,3,"tb-error-icon",17)(15,L1,2,5,"tb-error-icon",17)(16,V1,2,3,"tb-error-icon",17)(17,q1,2,0,"span",18),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",8)(19,"div",15),t.ɵɵtext(20,"gateway.rest.response-attribute"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",11),t.ɵɵelement(22,"input",19),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,G1,2,3,"tb-error-icon",17)(25,z1,2,3,"tb-error-icon",17),t.ɵɵelementEnd()(),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵnextContext(),n=e.responseConfigFormGroup.get(e.ResponseType.ADVANCED);t.ɵɵadvance(),t.ɵɵproperty("formGroup",n),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,6,"gateway.rest.response-expected")," "),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(12,8,"gateway.set"));const i=n.get("timeout").touched;t.ɵɵadvance(3),t.ɵɵconditional(n.get("timeout").hasError("required")&&i?14:n.get("timeout").hasError("min")&&i?15:n.get("timeout").hasError("pattern")&&i?16:17),t.ɵɵadvance(8),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,10,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.get("responseAttribute").hasError("required")&&n.get("responseAttribute").touched?24:n.get("responseAttribute").hasError("pattern")&&n.get("responseAttribute").touched?25:-1)}}class j1 extends Ra{get responseConfigFormGroup(){return this.formGroup}constructor(){super(),this.ResponseTypeTranslationsMap=xi,this.ResponseType=yi,this.responseTypes=Object.values(yi),this.responseStatuses=Object.values(vi),this.observeIsExpected()}initFormGroup(){return this.fb.group({type:[yi.DEFAULT],[yi.CONST]:this.fb.group({successResponse:[vi.OK],unsuccessfulResponse:[vi.ERROR]}),[yi.ADVANCED]:this.fb.group({responseExpected:[!0],timeout:[null,[$.required,$.min(.001),$.pattern(nn)]],responseAttribute:["",[$.required,$.pattern(an)]]})})}mapOnChangeValue({type:e,...t}){return{type:e,...t[e]??{}}}onWriteValue(e){const{type:t=yi.DEFAULT,...n}=e??{};this.toggleIsExpected(n.responseExpected,t),this.responseConfigFormGroup.patchValue({type:t,[t]:n},{emitEvent:!1})}toggleIsExpected(e,t){const n=e&&t===yi.ADVANCED;this.responseConfigFormGroup.get(yi.ADVANCED).get("timeout")[n?"enable":"disable"]({emitEvent:!1}),this.responseConfigFormGroup.get(yi.ADVANCED).get("responseAttribute")[n?"enable":"disable"]({emitEvent:!1})}observeIsExpected(){se(this.responseConfigFormGroup.get("type").valueChanges,this.responseConfigFormGroup.get(yi.ADVANCED).get("responseExpected").valueChanges).pipe(bn()).subscribe((()=>this.toggleIsExpected(this.responseConfigFormGroup.get(yi.ADVANCED).get("responseExpected").value,this.responseConfigFormGroup.get("type").value)))}static{this.ɵfac=function(e){return new(e||j1)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:j1,selectors:[["tb-rest-response-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>j1)),multi:!0},{provide:K,useExisting:c((()=>j1)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:11,vars:7,consts:[[1,"tb-form-row","space-between","same-padding","tb-flex","column","size-full",3,"formGroup"],[1,"tb-flex","row","space-between","align-center","no-gap","fill-width"],[1,"fixed-title-width","tb-required"],["formControlName","type","appearance","fill"],[3,"value"],[3,"ngSwitch"],[3,"ngSwitchCase"],[3,"formGroup"],[1,"tb-form-row"],[1,"fixed-title-width"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["formControlName","successResponse"],["formControlName","unsuccessfulResponse"],["formControlName","responseExpected",1,"mat-slide"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["matSuffix","",3,"tooltipText"],["translate","","matSuffix","",1,"block","pr-2"],["matInput","","type","text","name","value","formControlName","responseAttribute",3,"placeholder"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"tb-toggle-select",3),t.ɵɵrepeaterCreate(6,A1,3,4,"tb-toggle-option",4,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()(),t.ɵɵelementContainerStart(8,5),t.ɵɵtemplate(9,B1,19,7,"ng-template",6)(10,U1,26,12,"ng-template",6),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()),2&e&&(t.ɵɵproperty("formGroup",n.responseConfigFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,5,"gateway.response")),t.ɵɵadvance(3),t.ɵɵrepeater(n.responseTypes),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",n.responseConfigFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.ResponseType.CONST),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.ResponseType.ADVANCED))},dependencies:t.ɵɵgetComponentDepsFactory(j1,[j,_,Qn]),encapsulation:2,changeDetection:d.OnPush})}}function H1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",15),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.endpoint-required"))}function W1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",17),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function $1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",15),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.http-methods-required"))}function K1(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",17),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.RestConvertorTypeTranslationsMap.get(e)))}}function Y1(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",27),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Device)}}function X1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",40),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function Z1(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",40),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function Q1(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",28)(1,"div",20)(2,"div",21),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",33)(6,"mat-chip-listbox",34),t.ɵɵrepeaterCreate(7,X1,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(9,"mat-chip",35),t.ɵɵelement(10,"label",36),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"button",37,0),t.ɵɵpipe(13,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(12),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.ATTRIBUTES))})),t.ɵɵelementStart(14,"tb-icon",38),t.ɵɵtext(15,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(16,"div",20)(17,"div",39),t.ɵɵtext(18,"gateway.timeseries"),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",33)(20,"mat-chip-listbox",34),t.ɵɵrepeaterCreate(21,Z1,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(23,"mat-chip",35),t.ɵɵelement(24,"label",36),t.ɵɵelementEnd()(),t.ɵɵelementStart(25,"button",37,1),t.ɵɵpipe(27,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(26),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.TIMESERIES))})),t.ɵɵelementStart(28,"tb-icon",38),t.ɵɵtext(29,"edit"),t.ɵɵelementEnd()()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,5,"gateway.attributes")),t.ɵɵadvance(3),t.ɵɵproperty("tbEllipsisChipList",e.converterAttributes),t.ɵɵadvance(),t.ɵɵrepeater(e.converterAttributes),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(13,7,"action.edit")),t.ɵɵadvance(9),t.ɵɵproperty("tbEllipsisChipList",e.converterTelemetry),t.ɵɵadvance(),t.ɵɵrepeater(e.converterTelemetry),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(27,9,"action.edit"))}}function J1(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",15),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.extension-required"))}function e0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",40),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function t0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",28)(1,"div",11)(2,"div",12),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"mat-form-field",41),t.ɵɵelement(7,"input",42),t.ɵɵpipe(8,"translate"),t.ɵɵtemplate(9,J1,2,3,"tb-error-icon",15),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"div",43)(11,"div",24),t.ɵɵtext(12),t.ɵɵpipe(13,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",25),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"div",20)(18,"div",21),t.ɵɵtext(19),t.ɵɵpipe(20,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",44)(22,"mat-chip-listbox",34),t.ɵɵtemplate(23,e0,3,1,"mat-chip",45),t.ɵɵelementStart(24,"mat-chip",35),t.ɵɵelement(25,"label",36),t.ɵɵelementEnd()(),t.ɵɵelementStart(26,"button",37,2),t.ɵɵpipe(28,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(27),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i,a.MappingKeysType.CUSTOM))})),t.ɵɵelementStart(29,"tb-icon",38),t.ɵɵtext(30,"edit"),t.ɵɵelementEnd()()()()()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.rest.extension")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,12,"gateway.extension")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(8,14,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("converter.extension").hasError("required")&&e.mappingFormGroup.get("converter.extension").touched?9:-1),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(13,16,"gateway.extension-configuration")),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(16,18,"gateway.extension-configuration-hint")),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(20,20,"gateway.keys")),t.ɵɵadvance(3),t.ɵɵproperty("tbEllipsisChipList",e.customKeys),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",e.customKeys),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(28,22,"action.edit"))}}class n0 extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.destroyRef=l,this.cd=p,this.mappingFormGroup=this.fb.group({endpoint:["",[$.required,$.pattern(an)]],HTTPMethods:[[ht.POST],[$.required]],security:[{type:ki.ANONYMOUS}],converter:this.fb.group({type:[gi.JSON,[]],deviceInfo:[],reportStrategy:[],attributes:[[]],timeseries:[[]],extension:["",[$.required,$.pattern(an)]],extensionConfig:[]}),response:[]}),this.keysPopupClosed=!0,this.helpLink=`${O}/docs/iot-gateway/config/rest/#mapping-section`,this.httpMethods=Object.values(ht),this.converterTypes=Object.values(gi),this.restSourceTypes=Object.values(fi),this.SecurityMode=$i,this.MappingKeysType=Ni,this.DeviceInfoType=ga,this.RestConvertorTypeTranslationsMap=_i,this.RestDataConversionTranslationsMap=Ti,this.RestConverterType=gi,this.ConnectorType=ct,this.ReportStrategyDefaultValue=en,this.setInitialFormValue(),this.observeConverterTypeChange(),this.toggleConverterFieldsByType(this.converterType)}get converterType(){return this.mappingFormGroup.get("converter")?.get("type").value}get converterAttributes(){if(this.converterType)return this.mappingFormGroup.get("converter").value.attributes.map((e=>e.key))}get converterTelemetry(){if(this.converterType)return this.mappingFormGroup.get("converter").value.timeseries.map((e=>e.key))}get customKeys(){return Object.keys(this.mappingFormGroup.get("converter").value.extensionConfig??{})}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.mappingFormGroup.valid){const{converter:e,...t}=this.mappingFormGroup.value,{reportStrategy:n,...i}=e,a={...t,reportStrategy:n,converter:i};Te(a),this.dialogRef.close(a)}}manageKeys(e,t,n){e?.stopPropagation(),this.popoverComponent&&!this.popoverComponent.tbHidden&&this.popoverComponent.hide();const i=t._elementRef.nativeElement;if(this.popoverService.hasPopover(i))this.popoverService.hidePopover(i);else{const e=this.mappingFormGroup.get("converter").get(n),t={keys:e.value,keysType:n,panelTitle:Li.get(n),addKeyTitle:Vi.get(n),deleteKeyTitle:qi.get(n),noKeysText:Gi.get(n),withReportStrategy:this.data.withReportStrategy,convertorType:this.converterType,connectorType:ct.REST};this.keysPopupClosed=!1,this.popoverComponent=this.popoverService.displayPopover(i,this.renderer,this.viewContainerRef,yK,"leftBottom",!1,null,t,{},{},{},!0),this.popoverComponent.tbComponentRef.instance.keysDataApplied.pipe(bn(this.destroyRef)).subscribe((t=>{this.popoverComponent.hide(),e.patchValue(t),e.markAsDirty(),this.cd.markForCheck()})),this.popoverComponent.tbHideStart.pipe(bn(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}}setInitialFormValue(){const{converter:e,reportStrategy:t,...n}=this.data.value;this.mappingFormGroup.patchValue({...n,converter:{...e,...t?{reportStrategy:t}:{}}},{emitEvent:!1})}observeConverterTypeChange(){this.mappingFormGroup.get("converter").get("type").valueChanges.pipe(bn(this.destroyRef)).subscribe((e=>this.toggleConverterFieldsByType(e)))}toggleConverterFieldsByType(e){const t=e===gi.JSON;this.mappingFormGroup.get("converter").get("attributes")[t?"enable":"disable"]({emitEvent:!1}),this.mappingFormGroup.get("converter").get("timeseries")[t?"enable":"disable"]({emitEvent:!1}),this.mappingFormGroup.get("converter").get("extension")[t?"disable":"enable"]({emitEvent:!1}),this.mappingFormGroup.get("converter").get("extensionConfig")[t?"disable":"enable"]({emitEvent:!1})}static{this.ɵfac=function(e){return new(e||n0)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:n0,selectors:[["tb-rest-mapping-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:59,vars:45,consts:[["attributesButton",""],["telemetryButton",""],["keysButton",""],[1,"h-full","w-[77vw]","max-w-3xl",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row"],[1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","endpoint",3,"placeholder"],["matSuffix","",3,"tooltipText"],["formControlName","HTTPMethods","multiple",""],[3,"value"],["formControlName","security",3,"mode"],["formGroupName","converter"],[1,"tb-form-row","space-between","tb-flex"],[1,"fixed-title-width"],["formControlName","type","appearance","fill"],[1,"tb-form-panel","stroked"],[1,"tb-form-panel-title"],[1,"tb-form-hint","tb-primary-fill"],["formControlName","deviceInfo","required","true",1,"device-info",3,"sourceTypes","convertorType","deviceInfoType","connectorType"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"],[1,"tb-form-panel","no-border","no-padding","w-full"],["formControlName","response"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],[1,"tb-flex","max-w-70%"],[1,"tb-flex","gw-chip-list",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["translate","",1,"fixed-title-width"],["tbTruncateWithTooltip",""],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex"],["matInput","","name","value","formControlName","extension",3,"placeholder"],[1,"tb-form-row","space-between","same-padding","tb-flex","column"],[1,"tb-flex","ellipsis-chips-container"],[4,"ngFor","ngForOf"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",3)(1,"mat-toolbar",4)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",5)(6,"div",6),t.ɵɵelementStart(7,"button",7),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",8),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",9)(11,"div",10)(12,"div",11)(13,"div",12),t.ɵɵpipe(14,"translate"),t.ɵɵtext(15),t.ɵɵpipe(16,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"mat-form-field",13),t.ɵɵelement(18,"input",14),t.ɵɵpipe(19,"translate"),t.ɵɵtemplate(20,H1,2,3,"tb-error-icon",15),t.ɵɵelementEnd()(),t.ɵɵelementStart(21,"div",11)(22,"div",12),t.ɵɵpipe(23,"translate"),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(26,"mat-form-field",13)(27,"mat-select",16),t.ɵɵrepeaterCreate(28,W1,2,2,"mat-option",17,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd(),t.ɵɵtemplate(30,$1,2,3,"tb-error-icon",15),t.ɵɵelementEnd()(),t.ɵɵelement(31,"tb-security-config",18),t.ɵɵelementContainerStart(32,19),t.ɵɵelementStart(33,"div",20)(34,"div",21),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"tb-toggle-select",22),t.ɵɵrepeaterCreate(38,K1,3,4,"tb-toggle-option",17,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()(),t.ɵɵelementStart(40,"div",23)(41,"div",24),t.ɵɵtext(42),t.ɵɵpipe(43,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"div",25),t.ɵɵtext(45),t.ɵɵpipe(46,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(47,"tb-device-info-table",26),t.ɵɵtemplate(48,Y1,1,2,"tb-report-strategy",27)(49,Q1,30,11,"div",28)(50,t0,31,24,"div",28),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelement(51,"tb-rest-response-config",29),t.ɵɵelementEnd()(),t.ɵɵelementStart(52,"div",30)(53,"button",31),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(54),t.ɵɵpipe(55,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(56,"button",32),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(57),t.ɵɵpipe(58,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,23,"gateway.data-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.helpLink),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(14,25,"gateway.hints.rest.endpoint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(16,27,"gateway.rest.endpoint")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(19,29,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(n.mappingFormGroup.get("endpoint").hasError("required")&&n.mappingFormGroup.get("endpoint").touched?20:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(23,31,"gateway.hints.rest.http-methods")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(25,33,"gateway.rest.http-methods")),t.ɵɵadvance(4),t.ɵɵrepeater(n.httpMethods),t.ɵɵadvance(2),t.ɵɵconditional(n.mappingFormGroup.get("HTTPMethods").hasError("required")&&n.mappingFormGroup.get("HTTPMethods").touched?30:-1),t.ɵɵadvance(),t.ɵɵproperty("mode",n.SecurityMode.basic),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(36,35,"gateway.payload-type")),t.ɵɵadvance(3),t.ɵɵrepeater(n.converterTypes),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(43,37,"gateway.data-conversion")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(46,39,n.RestDataConversionTranslationsMap.get(n.converterType))," "),t.ɵɵadvance(2),t.ɵɵproperty("sourceTypes",n.restSourceTypes)("convertorType",n.RestConverterType.JSON)("deviceInfoType",n.DeviceInfoType.FULL)("connectorType",n.ConnectorType.REST),t.ɵɵadvance(),t.ɵɵconditional(n.data.withReportStrategy?48:-1),t.ɵɵadvance(),t.ɵɵconditional(n.converterType===n.RestConverterType.JSON?49:50),t.ɵɵadvance(5),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(55,41,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.mappingFormGroup.invalid||!n.mappingFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(58,43,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(n0,[j,_,Gn,I$,BY,j1,Qn,Xn]),styles:['@charset "UTF-8";[_nghost-%COMP%] .device-info .fixed-title-width{min-width:0}'],changeDetection:d.OnPush})}}const i0=()=>["endpoint","httpMethods","securityType","actions"];function a0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",6)(1,"div",29),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"span",8),t.ɵɵelementStart(5,"button",10),t.ɵɵpipe(6,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageMapping(n))})),t.ɵɵelementStart(7,"mat-icon"),t.ɵɵtext(8,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",10),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,3,"gateway.data-mapping")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(6,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,7,"action.search")))}function r0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30)(1,"div",31),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rest.endpoint")))}function o0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",32)(1,"div",31),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.endpoint)}}function s0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",33),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.rest.http-methods")," "))}function l0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",38),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e)}}function p0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",34)(1,"mat-chip-listbox",35),t.ɵɵrepeaterCreate(2,l0,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementStart(4,"mat-chip",36),t.ɵɵelement(5,"label",37),t.ɵɵelementEnd()()()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵproperty("tbEllipsisChipList",e.HTTPMethods),t.ɵɵadvance(),t.ɵɵrepeater(e.HTTPMethods)}}function c0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.security-type")," "))}function d0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",39)(1,"div",31),t.ɵɵtext(2),t.ɵɵpipe(3,"titlecase"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,null==e.security?null:e.security.type))}}function u0(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",40)}function m0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",10),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageMapping(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",10),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteMapping(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function h0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,m0,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",41),t.ɵɵelementContainer(4,42),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",43)(6,"button",44),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",45),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",46,2),t.ɵɵelementContainer(11,42),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(4),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function g0(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",47)}function f0(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class y0 extends qa{getDatasource(){return new v0}manageMapping(e,t){e&&e.stopPropagation();const n=ke(t),i=n?this.entityFormArray.at(t).value:{};this.getMappingDialog(i,n?"action.apply":"action.add").afterClosed().pipe(bn(this.destroyRef)).subscribe((e=>{e&&(n?this.entityFormArray.at(t).patchValue(e):this.entityFormArray.push(this.fb.control(e)),this.entityFormArray.markAsDirty())}))}deleteMapping(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-mapping-title",{name:this.entityFormArray.at(t).value.endpoint}),this.translate.instant("gateway.delete-mapping-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(bn(this.destroyRef)).subscribe((e=>{e&&(this.entityFormArray.removeAt(t),this.entityFormArray.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>Object.values(e).some((e=>e.toString().toLowerCase().includes(t.toLowerCase())||e.type?.toLowerCase().includes(t.toLowerCase())))))),this.dataSource.loadData(e)}getMappingDialog(e,t){return this.dialog.open(n0,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,withReportStrategy:this.withReportStrategy}})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(y0)))(n||y0)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:y0,selectors:[["tb-rest-mapping-table"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>y0)),multi:!0},{provide:K,useExisting:c((()=>y0)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:47,vars:37,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-absolute-fill","size-full"],[1,"flex","size-full","flex-col"],[1,"gw-table-toolbar","mat-mdc-table-toolbar"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"gw-connector-table"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","!w-1/5",4,"matHeaderCellDef"],["class","w-1/5",4,"matCellDef"],["class","w-1/3 !pl-0",4,"matHeaderCellDef"],["class","w-1/3",4,"matCellDef"],["class","w-1/6",4,"matHeaderCellDef"],["class","w-1/6",4,"matCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"!w-1/5"],["tbTruncateWithTooltip",""],[1,"w-1/5"],[1,"w-1/3","!pl-0"],[1,"w-1/3"],[1,"tb-flex",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text","http-method-label","font-medium"],["tbTruncateWithTooltip","",1,"http-method-label","font-medium"],[1,"w-1/6"],[1,"w-12"],[1,"lt-lg:!hidden","flex","min-w-24","flex-1","flex-row","items-stretch","justify-end","text-center"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,a0,13,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"button",7),t.ɵɵpipe(7,"translate"),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"mat-form-field",8)(11,"mat-label"),t.ɵɵtext(12," "),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",9,0),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",10),t.ɵɵpipe(17,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(18,"mat-icon"),t.ɵɵtext(19,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",11)(21,"table",12),t.ɵɵelementContainerStart(22,13),t.ɵɵtemplate(23,r0,4,3,"mat-header-cell",14)(24,o0,3,1,"mat-cell",15),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(25,13),t.ɵɵtemplate(26,s0,3,3,"mat-header-cell",16)(27,p0,6,1,"mat-cell",17),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(28,13),t.ɵɵtemplate(29,c0,3,3,"mat-header-cell",18)(30,d0,4,3,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(31,20),t.ɵɵtemplate(32,u0,1,0,"mat-header-cell",21)(33,h0,12,3,"mat-cell",22),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(34,g0,1,0,"mat-header-row",23)(35,f0,1,0,"mat-row",24),t.ɵɵelementEnd(),t.ɵɵelementStart(36,"section",25),t.ɵɵpipe(37,"async"),t.ɵɵelementStart(38,"button",26),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageMapping(i))})),t.ɵɵelementStart(39,"mat-icon",27),t.ɵɵtext(40,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(41,"span"),t.ɵɵtext(42),t.ɵɵpipe(43,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(44,"span",28),t.ɵɵpipe(45,"async"),t.ɵɵtext(46," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵconditional(!1===t.ɵɵpipeBind1(4,21,n.dataSource.isEmpty())?3:-1),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,23,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,25,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,27,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","endpoint"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","httpMethods"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","securityType"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(35,i0))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(36,i0)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(37,29,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(43,31,"gateway.add-mapping")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(45,33,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(y0,[j,_,qn,Gn]),styles:[".http-method-label[_ngcontent-%COMP%]{font-size:12px}"],changeDetection:d.OnPush})}}let v0=class extends G{constructor(){super()}};function x0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",13),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" : ",e.value.value," ")}}function b0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.key-required"))}function w0(e,n){1&e&&(t.ɵɵelementStart(0,"mat-icon",21),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e&&t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,"gateway.value-required"))}function S0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",16)(1,"div",17),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",18)(5,"mat-form-field",19),t.ɵɵelement(6,"input",20),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,b0,3,3,"mat-icon",21),t.ɵɵelementEnd()()(),t.ɵɵelementStart(9,"div",16)(10,"div",22),t.ɵɵtext(11,"gateway.value"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",19),t.ɵɵelement(13,"input",23),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,w0,3,3,"mat-icon",21),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,5,"gateway.key")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.get("key").hasError("required")&&e.get("key").touched?8:-1),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.get("value").hasError("required")&&e.get("value").touched?15:-1)}}function C0(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵelementContainerStart(2,11),t.ɵɵelementStart(3,"mat-expansion-panel",12)(4,"mat-expansion-panel-header")(5,"mat-panel-title")(6,"div",13),t.ɵɵtext(7),t.ɵɵelementEnd(),t.ɵɵtemplate(8,x0,2,1,"div",13),t.ɵɵelementEnd()(),t.ɵɵtemplate(9,S0,16,11,"ng-template",14),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"button",15),t.ɵɵpipe(11,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.deleteKey(n,i))})),t.ɵɵelementStart(12,"mat-icon"),t.ɵɵtext(13,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.$index,a=n.$count;t.ɵɵadvance(2),t.ɵɵproperty("formGroup",e),t.ɵɵadvance(),t.ɵɵproperty("expanded",i===a-1),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",null==e.value?null:e.value.key," "),t.ɵɵadvance(),t.ɵɵconditional(null!=e.value&&e.value.value?8:-1),t.ɵɵadvance(2),t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(11,5,"gateway.rest.delete-http-header"))}}function _0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",3),t.ɵɵrepeaterCreate(1,C0,14,7,"div",9,t.ɵɵrepeaterTrackByIndex),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵrepeater(e.keysListFormArray.controls)}}function T0(e,n){1&e&&(t.ɵɵelementStart(0,"div",4)(1,"span",24),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.rest.no-http-headers")))}class I0 extends q{constructor(e,t){super(t),this.fb=e,this.store=t,this.withReportStrategy=!0,this.keysDataApplied=p(),this.ReportStrategyDefaultValue=en}ngOnInit(){this.keysListFormArray=this.prepareKeysFormArray(this.keys)}addKey(){this.keysListFormArray.push(this.fb.group({key:["",[$.required,$.pattern(an)]],value:["",[$.required,$.pattern(an)]]}))}deleteKey(e,t){e&&e.stopPropagation(),this.keysListFormArray.removeAt(t),this.keysListFormArray.markAsDirty()}cancel(){this.popover?.hide()}applyKeysData(){this.keysDataApplied.emit(this.keysListFormArray.value.reduce(((e,{key:t,value:n})=>({...e,[t]:n})),{}))}prepareKeysFormArray(e){const t=[];return Object.keys(e)?.forEach((n=>{t.push(this.fb.group({key:[n,[$.required,$.pattern(an)]],value:[e[n],[$.required,$.pattern(an)]]}))})),this.fb.array(t)}static{this.ɵfac=function(e){return new(e||I0)(t.ɵɵdirectiveInject(H.UntypedFormBuilder),t.ɵɵdirectiveInject(Ye.Store))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:I0,selectors:[["tb-rest-http-headers-panel"]],inputs:{keys:"keys",popover:"popover",withReportStrategy:[2,"withReportStrategy","withReportStrategy",m]},outputs:{keysDataApplied:"keysDataApplied"},standalone:!0,features:[t.ɵɵInputTransformsFeature,t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:18,vars:15,consts:[[1,"w-[77vw]","max-w-2xl"],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-panel-title"],[1,"tb-form-panel","no-border","no-padding","h-[500px]"],[1,"tb-flex","no-flex","center","align-center","h-[500px]"],["type","button","mat-stroked-button","","color","primary",3,"click"],[1,"tb-flex","flex-end"],["mat-button","","color","primary","type","button",3,"click"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[1,"tb-form-panel","no-border","no-padding","tb-flex","no-flex","row","center","fill-width"],[1,"tb-form-panel","stroked","tb-flex"],[3,"formGroup"],[1,"tb-settings",3,"expanded"],["tbTruncateWithTooltip","",1,"max-w-[11vw]"],["matExpansionPanelContent",""],["type","button","mat-icon-button","","matTooltipPosition","above",1,"gw-delete-icon-button",3,"click","matTooltip"],[1,"tb-form-row"],[1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","name","value","formControlName","key",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","type","text","name","value","formControlName","value",3,"placeholder"],[1,"tb-prompt"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(5,_0,3,0,"div",3)(6,T0,4,3,"div",4),t.ɵɵelementStart(7,"div")(8,"button",5),t.ɵɵlistener("click",(function(){return n.addKey()})),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(11,"div",6)(12,"button",7),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"button",8),t.ɵɵlistener("click",(function(){return n.applyKeysData()})),t.ɵɵtext(16),t.ɵɵpipe(17,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate2("",t.ɵɵpipeBind1(4,7,"gateway.rest.http-headers"),""," ("+n.keysListFormArray.controls.length+")"," "),t.ɵɵadvance(2),t.ɵɵconditional(n.keysListFormArray.controls.length?5:6),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(10,9,"gateway.rest.add-http-header")," "),t.ɵɵadvance(4),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,11,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.keysListFormArray.invalid||!n.keysListFormArray.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(17,13,"action.apply")," "))},dependencies:t.ɵɵgetComponentDepsFactory(I0,[j,_]),encapsulation:2,changeDetection:d.OnPush})}}const E0=()=>({min:1}),M0=()=>({maxWidth:"970px"});function k0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,i.RestRequestTypesTranslationsMap.get(e)))}}function P0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.endpoint-required"))}function D0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.endpoint-pattern"))}function O0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function A0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.http-methods-required"))}function F0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵpipe(2,"titlecase"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,2,e))}}function R0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.rest.endpoint"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",21),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,P0,2,3,"tb-error-icon",22)(8,D0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",9)(10,"div",19),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"gateway.rest.http-methods"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",20)(14,"mat-select",23),t.ɵɵrepeaterCreate(15,O0,2,2,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd(),t.ɵɵtemplate(17,A0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",9)(19,"div",24),t.ɵɵpipe(20,"translate"),t.ɵɵtext(21,"gateway.type"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"mat-form-field",20)(23,"mat-select",25),t.ɵɵrepeaterCreate(24,F0,3,4,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,6,"gateway.hints.rest.endpoint")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,8,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("endpoint").hasError("required")&&e.mappingFormGroup.get("endpoint").touched?7:e.mappingFormGroup.get("endpoint").hasError("pattern")&&e.mappingFormGroup.get("endpoint").touched?8:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,10,"gateway.hints.rest.http-methods")),t.ɵɵadvance(5),t.ɵɵrepeater(e.httpMethods),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("HTTPMethods").hasError("required")&&e.mappingFormGroup.get("HTTPMethods").touched?17:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(20,12,"gateway.hints.rest.scope-type")),t.ɵɵadvance(5),t.ɵɵrepeater(e.requestsScopeType)}}function B0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-required"))}function N0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-pattern"))}function L0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.device-name-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",26),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,B0,2,3,"tb-error-icon",22)(8,N0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.device-name-filter-hint")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("deviceNameFilter").hasError("required")&&e.mappingFormGroup.get("deviceNameFilter").touched?7:e.mappingFormGroup.get("deviceNameFilter").hasError("pattern")&&e.mappingFormGroup.get("deviceNameFilter").touched?8:-1)}}function V0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.method-filter-required"))}function q0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.method-filter-pattern"))}function G0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.method-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",27),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,V0,2,3,"tb-error-icon",22)(8,q0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.rest.method-filter")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("methodFilter").hasError("required")&&e.mappingFormGroup.get("methodFilter").touched?7:e.mappingFormGroup.get("methodFilter").hasError("pattern")&&e.mappingFormGroup.get("methodFilter").touched?8:-1)}}function z0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-filter-required"))}function U0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-filter-pattern"))}function j0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.attribute-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",28),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,z0,2,3,"tb-error-icon",22)(8,U0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,3,"gateway.hints.rest.attribute-filter")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,5,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("attributeFilter").hasError("required")&&e.mappingFormGroup.get("attributeFilter").touched?7:e.mappingFormGroup.get("attributeFilter").hasError("pattern")&&e.mappingFormGroup.get("attributeFilter").touched?8:-1)}}function H0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.request-url-expression-required"))}function W0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.request-url-expression-pattern"))}function $0(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",13),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function K0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.value-expression-required"))}function Y0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.value-expression-pattern"))}function X0(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",29)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.rest.request-url-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",30),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,H0,2,3,"tb-error-icon",22)(8,W0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",9)(10,"div",19),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"gateway.rest.http-method"),t.ɵɵelementEnd(),t.ɵɵelementStart(13,"mat-form-field",20)(14,"mat-select",31),t.ɵɵrepeaterCreate(15,$0,2,2,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()(),t.ɵɵelementStart(17,"div",9)(18,"div",19),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"gateway.value-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",20),t.ɵɵelement(22,"input",32),t.ɵɵpipe(23,"translate"),t.ɵɵtemplate(24,K0,2,3,"tb-error-icon",22)(25,Y0,2,3,"tb-error-icon",22),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,7,"gateway.hints.rest.attribute-url-expression")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("requestUrlExpression").hasError("required")&&e.mappingFormGroup.get("requestUrlExpression").touched?7:e.mappingFormGroup.get("requestUrlExpression").hasError("pattern")&&e.mappingFormGroup.get("requestUrlExpression").touched?8:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(11,11,"gateway.hints.rest.http-method")),t.ɵɵadvance(5),t.ɵɵrepeater(e.httpMethods),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(19,13,"gateway.hints.rest.value-expression")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(23,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("valueExpression").hasError("required")&&e.mappingFormGroup.get("valueExpression").touched?24:e.mappingFormGroup.get("valueExpression").hasError("pattern")&&e.mappingFormGroup.get("valueExpression").touched?25:-1)}}function Z0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,E0)))}function Q0(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function J0(e,n){1&e&&(t.ɵɵelementStart(0,"span",34),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function e2(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵtext(2,"gateway.response-timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",20),t.ɵɵelement(4,"input",33),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,Z0,2,5,"tb-error-icon",22)(7,Q0,2,3,"tb-error-icon",22)(8,J0,2,0,"span",34),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,2,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("responseTimeout").hasError("min")&&e.mappingFormGroup.get("responseTimeout").touched?6:e.mappingFormGroup.get("responseTimeout").hasError("pattern")&&e.mappingFormGroup.get("responseTimeout").touched?7:8)}}function t2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-required"))}function n2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.device-name-filter-pattern"))}function i2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-name-expression-required"))}function a2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.attribute-name-expression-pattern"))}function r2(e,n){1&e&&t.ɵɵelement(0,"div",38),2&e&&t.ɵɵproperty("tb-help-popup","widget/lib/gateway/rest-json_fn")("tb-help-popup-style",t.ɵɵpureFunction0(2,M0))}function o2(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",19),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3,"gateway.device-name-filter"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-form-field",20),t.ɵɵelement(5,"input",35),t.ɵɵpipe(6,"translate"),t.ɵɵtemplate(7,t2,2,3,"tb-error-icon",22)(8,n2,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"div",9)(10,"div",36),t.ɵɵtext(11,"gateway.attribute-name-expression"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"mat-form-field",20),t.ɵɵelement(13,"input",37),t.ɵɵpipe(14,"translate"),t.ɵɵtemplate(15,i2,2,3,"tb-error-icon",22)(16,a2,2,3,"tb-error-icon",22)(17,r2,1,3,"div",38),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,5,"gateway.device-name-filter-hint")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,7,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("deviceNameExpression").hasError("required")&&e.mappingFormGroup.get("deviceNameExpression").touched?7:e.mappingFormGroup.get("deviceNameExpression").hasError("pattern")&&e.mappingFormGroup.get("deviceNameExpression").touched?8:-1),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(14,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("attributeNameExpression").hasError("required")&&e.mappingFormGroup.get("attributeNameExpression").touched?15:e.mappingFormGroup.get("attributeNameExpression").hasError("pattern")&&e.mappingFormGroup.get("attributeNameExpression").touched?16:17)}}function s2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,E0)))}function l2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function p2(e,n){1&e&&(t.ɵɵelementStart(0,"span",34),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function c2(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",9)(1,"div",10),t.ɵɵtext(2,"gateway.timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"mat-form-field",20),t.ɵɵelement(4,"input",39),t.ɵɵpipe(5,"translate"),t.ɵɵtemplate(6,s2,2,5,"tb-error-icon",22)(7,l2,2,3,"tb-error-icon",22)(8,p2,2,0,"span",34),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(5,2,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("timeout").hasError("min")&&e.mappingFormGroup.get("timeout").touched?6:e.mappingFormGroup.get("timeout").hasError("pattern")&&e.mappingFormGroup.get("timeout").touched?7:8)}}function d2(e,n){1&e&&(t.ɵɵelementStart(0,"div",14)(1,"mat-slide-toggle",40)(2,"mat-label",41),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.hints.rest.update-ssl")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.rest.ssl-verify")," "))}function u2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-chip")(1,"div",53),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit;t.ɵɵadvance(2),t.ɵɵtextInterpolate(e.value)}}function m2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind2(1,1,"gateway.response-timeout-limits-error",t.ɵɵpureFunction0(4,E0)))}function h2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.timeout-pattern"))}function g2(e,n){1&e&&(t.ɵɵelementStart(0,"span",34),t.ɵɵtext(1,"gateway.suffix.s"),t.ɵɵelementEnd())}function f2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.tries-min"))}function y2(e,n){1&e&&(t.ɵɵelement(0,"tb-error-icon",22),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("tooltipText",t.ɵɵpipeBind1(1,1,"gateway.hints.rest.tries-pattern"))}function v2(e,n){1&e&&(t.ɵɵelementStart(0,"div",14)(1,"mat-slide-toggle",54)(2,"mat-label"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,1,"gateway.rest.allow-redirects")," "))}function x2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",8)(1,"mat-expansion-panel",42)(2,"mat-expansion-panel-header")(3,"mat-panel-title",43)(4,"div",44),t.ɵɵtext(5,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(6,"div",45)(7,"div",10),t.ɵɵtext(8,"gateway.rest.http-headers"),t.ɵɵelementEnd(),t.ɵɵelementStart(9,"div",46)(10,"mat-chip-listbox",47),t.ɵɵpipe(11,"keyvalue"),t.ɵɵrepeaterCreate(12,u2,3,1,"mat-chip",null,t.ɵɵrepeaterTrackByIdentity),t.ɵɵpipe(14,"keyvalue"),t.ɵɵelementStart(15,"mat-chip",48),t.ɵɵelement(16,"label",49),t.ɵɵelementEnd()(),t.ɵɵelementStart(17,"button",50,0),t.ɵɵpipe(19,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵreference(18),a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageKeys(n,i))})),t.ɵɵelementStart(20,"tb-icon",51),t.ɵɵtext(21,"edit"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(22,"div",9)(23,"div",10),t.ɵɵtext(24,"gateway.timeout"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-form-field",20),t.ɵɵelement(26,"input",39),t.ɵɵpipe(27,"translate"),t.ɵɵtemplate(28,m2,2,5,"tb-error-icon",22)(29,h2,2,3,"tb-error-icon",22)(30,g2,2,0,"span",34),t.ɵɵelementEnd()(),t.ɵɵelementStart(31,"div",9)(32,"div",10),t.ɵɵtext(33,"gateway.rest.tries"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"mat-form-field",20),t.ɵɵelement(35,"input",52),t.ɵɵpipe(36,"translate"),t.ɵɵtemplate(37,f2,2,3,"tb-error-icon",22)(38,y2,2,3,"tb-error-icon",22),t.ɵɵelementEnd()(),t.ɵɵtemplate(39,v2,5,3,"div",14),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(10),t.ɵɵproperty("tbEllipsisChipList",t.ɵɵpipeBind1(11,7,e.mappingFormGroup.get("httpHeaders").value)),t.ɵɵadvance(2),t.ɵɵrepeater(t.ɵɵpipeBind1(14,9,e.mappingFormGroup.get("httpHeaders").value)),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,11,"action.edit")),t.ɵɵadvance(9),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(27,13,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("timeout").hasError("min")&&e.mappingFormGroup.get("timeout").touched?28:e.mappingFormGroup.get("timeout").hasError("pattern")&&e.mappingFormGroup.get("timeout").touched?29:30),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(36,15,"gateway.set")),t.ɵɵadvance(2),t.ɵɵconditional(e.mappingFormGroup.get("tries").hasError("min")&&e.mappingFormGroup.get("tries").touched?37:e.mappingFormGroup.get("tries").hasError("pattern")&&e.mappingFormGroup.get("tries").touched?38:-1),t.ɵɵadvance(2),t.ɵɵconditional(e.requestType===e.RestRequestType.ATTRIBUTE_UPDATE?39:-1)}}class b2 extends A{constructor(e,t,n,i,a,r,o,s,l,p){super(e,t,i),this.store=e,this.router=t,this.data=n,this.dialogRef=i,this.fb=a,this.popoverService=r,this.renderer=o,this.viewContainerRef=s,this.cdr=l,this.destroyRef=p,this.requestTypes=Object.values(bi),this.requestsScopeType=Object.values(Si),this.httpMethods=Object.values(ht),this.helpLink=`${O}/docs/iot-gateway/config/rest/#attribute-request-section`,this.RestRequestTypesTranslationsMap=wi,this.SecurityMode=$i,this.RestRequestType=bi,this.mappingFormGroup=this.fb.group({requestType:[bi.ATTRIBUTE_REQUEST],endpoint:["",[$.required,$.pattern(an)]],HTTPMethods:[[ht.POST],[$.required]],HTTPMethod:[ht.POST,[$.required]],type:[Si.Shared],security:[{type:ki.ANONYMOUS}],timeout:[null,[$.min(.001),$.pattern(nn)]],deviceNameExpression:["",[$.required,$.pattern(an)]],attributeNameExpression:["",[$.required,$.pattern(an)]],SSLVerify:[!1],deviceNameFilter:["",[$.required,$.pattern(an)]],methodFilter:["",[$.required,$.pattern(an)]],attributeFilter:["",[$.required,$.pattern(an)]],requestUrlExpression:[this.data.defaultRequestUrl,[$.required,$.pattern(an)]],valueExpression:["",[$.required,$.pattern(an)]],responseTimeout:[null,[$.min(.001),$.pattern(nn)]],tries:[null,[$.min(1),$.pattern(nn)]],allowRedirects:[!1],httpHeaders:[{}]}),this.keysPopupClosed=!0,this.mappingFormGroup.patchValue(this.data.value,{emitEvent:!1}),this.observeRequestTypeChange(),this.toggleFieldsByRequestType(this.mappingFormGroup.get("requestType").value)}get requestType(){return this.mappingFormGroup.get("requestType").value}cancel(){this.keysPopupClosed&&this.dialogRef.close(null)}add(){if(this.mappingFormGroup.valid){const{requestType:e,...t}=this.mappingFormGroup.value;Te(t),this.dialogRef.close({requestType:e,requestValue:t})}}manageKeys(e,t){e.stopPropagation();const n=t._elementRef.nativeElement;if(this.popoverService.hasPopover(n))return void this.popoverService.hidePopover(n);this.popoverService.hasPopover(n)&&this.popoverService.hidePopover(n),this.keysPopupClosed=!1;const i=this.popoverService.displayPopover(n,this.renderer,this.viewContainerRef,I0,"leftBottom",!1,null,{keys:this.mappingFormGroup.get("httpHeaders").value,withReportStrategy:this.data.withReportStrategy},{},{},{},!0);i.tbComponentRef.instance.popover=i,i.tbComponentRef.instance.keysDataApplied.subscribe((e=>{i.hide(),this.mappingFormGroup.get("httpHeaders").patchValue(e),this.mappingFormGroup.get("httpHeaders").markAsDirty(),this.cdr.markForCheck()})),i.tbHideStart.pipe(bn(this.destroyRef)).subscribe((()=>{this.keysPopupClosed=!0}))}observeRequestTypeChange(){this.mappingFormGroup.get("requestType").valueChanges.pipe(ce(),bn(this.destroyRef)).subscribe((e=>this.toggleFieldsByRequestType(e)))}toggleFieldsByRequestType(e){this.mappingFormGroup.disable({emitEvent:!1}),Ci.get("all").forEach((e=>this.mappingFormGroup.get(e).enable({emitEvent:!1}))),Ci.get(e).forEach((e=>this.mappingFormGroup.get(e).enable({emitEvent:!1})))}static{this.ɵfac=function(e){return new(e||b2)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(Xe.Router),t.ɵɵdirectiveInject(Ve),t.ɵɵdirectiveInject(qe.MatDialogRef),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(at.TbPopoverService),t.ɵɵdirectiveInject(t.Renderer2),t.ɵɵdirectiveInject(t.ViewContainerRef),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:b2,selectors:[["tb-rest-requests-mapping-dialog"]],standalone:!0,features:[t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:37,vars:23,consts:[["button",""],[1,"h-full","w-[77vw]","max-w-3xl",3,"formGroup"],["color","primary"],[1,"flex-1"],[3,"tb-help"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["mat-dialog-content",""],[1,"tb-form-panel","no-border","no-padding"],[1,"tb-form-row"],["translate","",1,"fixed-title-width"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex"],["formControlName","requestType"],[3,"value"],[1,"tb-form-row","tb-flex","fill-width"],["formControlName","security",3,"mode"],["mat-dialog-actions","",1,"justify-end"],["mat-button","","color","primary","type","button","cdkFocusInitial","",3,"click"],["mat-raised-button","","color","primary",3,"click","disabled"],["translate","",1,"fixed-title-width","tb-required",3,"tb-hint-tooltip-icon"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","type","text","name","value","formControlName","endpoint",3,"placeholder"],["matSuffix","",3,"tooltipText"],["formControlName","HTTPMethods","multiple",""],["translate","",1,"fixed-title-width",3,"tb-hint-tooltip-icon"],["formControlName","type"],["matInput","","type","text","name","value","formControlName","deviceNameFilter",3,"placeholder"],["matInput","","type","text","name","value","formControlName","methodFilter",3,"placeholder"],["matInput","","type","text","name","value","formControlName","attributeFilter",3,"placeholder"],[1,"tb-form-row","request-url-row"],["matInput","","type","text","name","value","formControlName","requestUrlExpression",3,"placeholder"],["formControlName","HTTPMethod"],["matInput","","type","text","name","value","formControlName","valueExpression",3,"placeholder"],["matInput","","type","number","min","0","name","value","formControlName","responseTimeout",3,"placeholder"],["translate","","matSuffix","",1,"block","pr-2"],["matInput","","type","text","name","value","formControlName","deviceNameExpression",3,"placeholder"],["translate","",1,"fixed-title-width","tb-required"],["matInput","","type","text","name","value","formControlName","attributeNameExpression",3,"placeholder"],["matSuffix","","tb-help-popup-placement","left",1,"see-example","p-1",3,"tb-help-popup","tb-help-popup-style"],["matInput","","type","number","min","0","name","value","formControlName","timeout",3,"placeholder"],["formControlName","SSLVerify",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],[1,"tb-settings"],[1,"justify-end"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","space-between","tb-flex"],[1,"tb-flex","gw-chip-list"],[1,"tb-flex",3,"tbEllipsisChipList"],[1,"mat-mdc-chip","ellipsis-chip"],[1,"ellipsis-text"],["type","button","mat-icon-button","","color","primary","matTooltipPosition","above",3,"click","matTooltip"],["matButtonIcon",""],["matInput","","type","number","min","0","name","value","formControlName","tries",3,"placeholder"],["tbTruncateWithTooltip",""],["formControlName","allowRedirects",1,"mat-slide"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",1)(1,"mat-toolbar",2)(2,"h2"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(5,"span",3)(6,"div",4),t.ɵɵelementStart(7,"button",5),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵelementStart(8,"mat-icon",6),t.ɵɵtext(9,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(10,"div",7)(11,"div",8)(12,"div",9)(13,"div",10),t.ɵɵtext(14,"gateway.request-type"),t.ɵɵelementEnd(),t.ɵɵelementStart(15,"mat-form-field",11)(16,"mat-select",12),t.ɵɵrepeaterCreate(17,k0,3,4,"mat-option",13,t.ɵɵrepeaterTrackByIdentity),t.ɵɵelementEnd()()(),t.ɵɵtemplate(19,R0,26,14)(20,L0,9,7,"div",9)(21,G0,9,7,"div",9)(22,j0,9,7,"div",9)(23,X0,26,17)(24,e2,9,4,"div",9)(25,o2,18,11)(26,c2,9,4,"div",9)(27,d2,6,6,"div",14),t.ɵɵelement(28,"tb-security-config",15),t.ɵɵtemplate(29,x2,40,17,"div",8),t.ɵɵelementEnd()(),t.ɵɵelementStart(30,"div",16)(31,"button",17),t.ɵɵlistener("click",(function(){return n.cancel()})),t.ɵɵtext(32),t.ɵɵpipe(33,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(34,"button",18),t.ɵɵlistener("click",(function(){return n.add()})),t.ɵɵtext(35),t.ɵɵpipe(36,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.mappingFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,17,"gateway.requests-mapping")),t.ɵɵadvance(3),t.ɵɵproperty("tb-help",n.helpLink),t.ɵɵadvance(11),t.ɵɵrepeater(n.requestTypes),t.ɵɵadvance(2),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_REQUEST?19:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType!==n.RestRequestType.ATTRIBUTE_REQUEST?20:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.SERVER_SIDE_RPC?21:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_UPDATE?22:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType!==n.RestRequestType.ATTRIBUTE_REQUEST?23:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.SERVER_SIDE_RPC?24:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_REQUEST?25:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_REQUEST?26:-1),t.ɵɵadvance(),t.ɵɵconditional(n.requestType===n.RestRequestType.ATTRIBUTE_UPDATE?27:-1),t.ɵɵadvance(),t.ɵɵproperty("mode",n.SecurityMode.basic),t.ɵɵadvance(),t.ɵɵconditional(n.requestType!==n.RestRequestType.ATTRIBUTE_REQUEST?29:-1),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(33,19,"action.cancel")," "),t.ɵɵadvance(2),t.ɵɵproperty("disabled",n.mappingFormGroup.invalid||!n.mappingFormGroup.dirty||!n.keysPopupClosed),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(36,21,n.data.buttonTitle)," "))},dependencies:t.ɵɵgetComponentDepsFactory(b2,[j,_,Gn,BY,Qn]),styles:["[_nghost-%COMP%] .tb-form-row.request-url-row[_ngcontent-%COMP%]{gap:6px}"],changeDetection:d.OnPush})}}const w2=()=>["type","details","actions"],S2=()=>({});function C2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",6)(1,"div",26),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelement(4,"span",8),t.ɵɵelementStart(5,"button",10),t.ɵɵpipe(6,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.manageMapping(n))})),t.ɵɵelementStart(7,"mat-icon"),t.ɵɵtext(8,"add"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"button",10),t.ɵɵpipe(10,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.enterFilterMode())})),t.ɵɵelementStart(11,"mat-icon"),t.ɵɵtext(12,"search"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,3,"gateway.requests-mapping")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(6,5,"action.add")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,7,"action.search")))}function _2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",27)(1,"div",28),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,"gateway.type")))}function T2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",29)(1,"div",28),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,1,i.RestRequestTypesTranslationsMap.get(e.requestType)))}}function I2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",30),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.details")," "))}function E2(e,n){if(1&e&&t.ɵɵtext(0),2&e){let e;const n=t.ɵɵnextContext().$implicit,i=t.ɵɵnextContext();t.ɵɵtextInterpolate1(" ",i.Object.values(null!==(e=n.requestValue.httpHeaders)&&void 0!==e?e:t.ɵɵpureFunction0(1,S2)).join(", ")," ")}}function M2(e,n){if(1&e&&t.ɵɵtext(0),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵtextInterpolate1(" ",e.requestValue.methodFilter," ")}}function k2(e,n){if(1&e&&t.ɵɵtext(0),2&e){const e=t.ɵɵnextContext().$implicit;t.ɵɵtextInterpolate1(" ",e.requestValue.endpoint," ")}}function P2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",29)(1,"div",28),t.ɵɵtemplate(2,E2,1,2)(3,M2,1,1)(4,k2,1,1),t.ɵɵelementEnd()()),2&e){let e;const i=n.$implicit;t.ɵɵadvance(2),t.ɵɵconditional("attributeUpdates"===(e=i.requestType)?2:"serverSideRpc"===e?3:4)}}function D2(e,n){1&e&&t.ɵɵelement(0,"mat-header-cell",31)}function O2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",10),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.manageMapping(n,i))})),t.ɵɵelementStart(2,"tb-icon"),t.ɵɵtext(3,"edit"),t.ɵɵelementEnd()(),t.ɵɵelementStart(4,"button",10),t.ɵɵpipe(5,"translate"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext().index,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteMapping(n,i))})),t.ɵɵelementStart(6,"tb-icon"),t.ɵɵtext(7,"delete"),t.ɵɵelementEnd()()}2&e&&(t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.edit")),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(5,4,"action.delete")))}function A2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtemplate(1,O2,8,6,"ng-template",null,1,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(3,"div",32),t.ɵɵelementContainer(4,33),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",34)(6,"button",35),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(7,"mat-icon",36),t.ɵɵtext(8,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(9,"mat-menu",37,2),t.ɵɵelementContainer(11,33),t.ɵɵelementEnd()()()}if(2&e){const e=t.ɵɵreference(2),n=t.ɵɵreference(10);t.ɵɵadvance(4),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(2),t.ɵɵproperty("matMenuTriggerFor",n),t.ɵɵadvance(5),t.ɵɵproperty("ngTemplateOutlet",e)}}function F2(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",38)}function R2(e,n){1&e&&t.ɵɵelement(0,"mat-row")}class B2 extends qa{constructor(){super(...arguments),this.Object=Object,this.RestRequestTypesTranslationsMap=wi}getDatasource(){return new N2}manageMapping(e,t){e&&e.stopPropagation();const n=ke(t),{requestType:i=bi.ATTRIBUTE_REQUEST,requestValue:a={}}=n?this.entityFormArray.at(t).value:{};this.getMappingDialog({requestType:i,...a},n?"action.apply":"action.add").afterClosed().pipe(bn(this.destroyRef)).subscribe((e=>{e&&(n?this.entityFormArray.at(t).patchValue(e):this.entityFormArray.push(this.fb.control(e)),this.entityFormArray.markAsDirty())}))}deleteMapping(e,t){e&&e.stopPropagation(),this.dialogService.confirm(this.translate.instant("gateway.delete-mapping-title",{name:this.getRequestDetails(this.entityFormArray.at(t).value)}),this.translate.instant("gateway.delete-mapping-description"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0).pipe(bn(this.destroyRef)).subscribe((e=>{e&&(this.entityFormArray.removeAt(t),this.entityFormArray.markAsDirty())}))}updateTableData(e,t){t&&(e=e.filter((e=>{const n=this.getRequestDetails(e);return Object.values(e).some((e=>Me(e)&&this.translate.instant(wi.get(e)).toLowerCase().includes(t.toLowerCase())||n?.toLowerCase().includes(t.toLowerCase())))}))),this.dataSource.loadData(e)}getRequestDetails(e){let t;switch(e.requestType){case"attributeUpdates":t=Object.values(e.requestValue.httpHeaders).join(", ");break;case"serverSideRpc":t=e.requestValue.methodFilter;break;default:t=e.requestValue.endpoint}return t}validate(){return null}getMappingDialog(e,t){return this.dialog.open(b2,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{value:e,buttonTitle:t,defaultRequestUrl:this.defaultRequestUrl,withReportStrategy:this.withReportStrategy}})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(B2)))(n||B2)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:B2,selectors:[["tb-rest-request-mapping-table"]],inputs:{defaultRequestUrl:"defaultRequestUrl"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>B2)),multi:!0},{provide:K,useExisting:c((()=>B2)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:44,vars:36,consts:[["searchInput",""],["rowActions",""],["cellActionsMenu","matMenu"],[1,"tb-absolute-fill","size-full"],[1,"flex","size-full","flex-col"],[1,"gw-table-toolbar","mat-mdc-table-toolbar"],[1,"mat-toolbar-tools"],["mat-icon-button","","matTooltipPosition","above",3,"matTooltip"],[1,"flex-1"],["matInput","",3,"formControl","placeholder"],["mat-icon-button","","matTooltipPosition","above",3,"click","matTooltip"],[1,"gw-connector-table"],["mat-table","",3,"dataSource"],[3,"matColumnDef"],["class","!w-1/3",4,"matHeaderCellDef"],["class","w-1/3",4,"matCellDef"],["class","w-1/3 !pl-0",4,"matHeaderCellDef"],["matColumnDef","actions","stickyEnd",""],["class","w-12",4,"matHeaderCellDef"],[4,"matCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],[4,"matRowDef","matRowDefColumns"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],["translate","",1,"no-data-found","items-center","justify-center"],[1,"title-container"],[1,"!w-1/3"],["tbTruncateWithTooltip",""],[1,"w-1/3"],[1,"w-1/3","!pl-0"],[1,"w-12"],[1,"lt-lg:!hidden","flex","min-w-24","flex-1","flex-row","items-stretch","justify-end","text-center"],[3,"ngTemplateOutlet"],[1,"gt-md:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",3)(1,"div",4)(2,"mat-toolbar",5),t.ɵɵtemplate(3,C2,13,9,"div",6),t.ɵɵpipe(4,"async"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-toolbar",5)(6,"button",7),t.ɵɵpipe(7,"translate"),t.ɵɵelementStart(8,"mat-icon"),t.ɵɵtext(9,"search"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"mat-form-field",8)(11,"mat-label"),t.ɵɵtext(12," "),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",9,0),t.ɵɵpipe(15,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(16,"button",10),t.ɵɵpipe(17,"translate"),t.ɵɵlistener("click",(function(){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.exitFilterMode())})),t.ɵɵelementStart(18,"mat-icon"),t.ɵɵtext(19,"close"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",11)(21,"table",12),t.ɵɵelementContainerStart(22,13),t.ɵɵtemplate(23,_2,4,3,"mat-header-cell",14)(24,T2,4,3,"mat-cell",15),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(25,13),t.ɵɵtemplate(26,I2,3,3,"mat-header-cell",16)(27,P2,5,1,"mat-cell",15),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(28,17),t.ɵɵtemplate(29,D2,1,0,"mat-header-cell",18)(30,A2,12,3,"mat-cell",19),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(31,F2,1,0,"mat-header-row",20)(32,R2,1,0,"mat-row",21),t.ɵɵelementEnd(),t.ɵɵelementStart(33,"section",22),t.ɵɵpipe(34,"async"),t.ɵɵelementStart(35,"button",23),t.ɵɵlistener("click",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.manageMapping(i))})),t.ɵɵelementStart(36,"mat-icon",24),t.ɵɵtext(37,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(38,"span"),t.ɵɵtext(39),t.ɵɵpipe(40,"translate"),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(41,"span",25),t.ɵɵpipe(42,"async"),t.ɵɵtext(43," widget.no-data-found "),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",n.textSearchMode),t.ɵɵadvance(),t.ɵɵconditional(!1===t.ɵɵpipeBind1(4,20,n.dataSource.isEmpty())?3:-1),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(7,22,"action.search")),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(15,24,"common.enter-search")),t.ɵɵproperty("formControl",n.textSearch),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,26,"action.close")),t.ɵɵadvance(5),t.ɵɵproperty("dataSource",n.dataSource),t.ɵɵadvance(),t.ɵɵproperty("matColumnDef","type"),t.ɵɵadvance(3),t.ɵɵproperty("matColumnDef","details"),t.ɵɵadvance(6),t.ɵɵproperty("matHeaderRowDef",t.ɵɵpureFunction0(34,w2))("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",t.ɵɵpureFunction0(35,w2)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.textSearchMode||!1===t.ɵɵpipeBind1(34,28,n.dataSource.isEmpty())),t.ɵɵadvance(6),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(40,30,"gateway.add-mapping")),t.ɵɵadvance(2),t.ɵɵclassProp("!hidden",!n.textSearchMode||!1===t.ɵɵpipeBind1(42,32,n.dataSource.isEmpty())))},dependencies:t.ɵɵgetComponentDepsFactory(B2,[j,_,qn]),encapsulation:2,changeDetection:d.OnPush})}}class N2 extends G{constructor(){super()}}class L2 extends Ba{initBasicFormGroup(){return this.fb.group({server:[],mapping:[],requestsMapping:[]})}getRequestDataArray(e){const t=[];return Fe(e)&&Object.keys(e).forEach((n=>{for(const i of e[n])t.push({requestType:n,requestValue:i})})),t}getRequestDataObject(e){return e.reduce(((e,{requestType:t,requestValue:n})=>(e[t]?.push(n),e)),{attributeRequests:[],attributeUpdates:[],serverSideRpc:[]})}updateDefaultUrl({host:e,port:t,SSL:n}){this.defaultRequestUrl=e&&t?`${n?"https":"http"}//${e}:${t}/`:document.location.origin}writeValue(e){this.basicFormGroup.setValue(this.mapConfigToFormValue(e),{emitEvent:!1})}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(L2)))(n||L2)}})()}static{this.ɵdir=t.ɵɵdefineDirective({type:L2,features:[t.ɵɵInheritDefinitionFeature]})}}class V2 extends L2{mapConfigToFormValue(e){return{server:e.server??{},mapping:e.mapping??[],requestsMapping:this.getRequestDataArray(e.requestsMapping)}}getMappedValue(e){return this.updateDefaultUrl(e?.server??{}),{server:e.server??{},mapping:e.mapping??[],requestsMapping:this.getRequestDataObject(e.requestsMapping??[])}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(V2)))(n||V2)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:V2,selectors:[["tb-rest-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>V2)),multi:!0},{provide:K,useExisting:c((()=>V2)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:13,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server"],["formControlName","mapping"],["formControlName","requestsMapping",3,"defaultRequestUrl"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-rest-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-rest-mapping-table",4),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-tab",1),t.ɵɵpipe(11,"translate"),t.ɵɵelement(12,"tb-rest-request-mapping-table",5),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.server"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(11,15,"gateway.requests-mapping")),t.ɵɵadvance(2),t.ɵɵproperty("defaultRequestUrl",n.defaultRequestUrl))},dependencies:t.ɵɵgetComponentDepsFactory(V2,[j,_,D1,y0,B2]),encapsulation:2,changeDetection:d.OnPush})}}class q2 extends L2{mapConfigToFormValue({attributeRequests:e=[],attributeUpdates:t=[],serverSideRpc:n=[],mapping:i=[],...a}){return{server:{...a??{}},mapping:$a.mapMappingToUpgradedVersion(i),requestsMapping:this.getRequestDataArray({attributeRequests:e,attributeUpdates:t,serverSideRpc:n})}}getMappedValue({requestsMapping:e,mapping:t=[],server:n={}}){this.updateDefaultUrl(n);const{attributeRequests:i=[],attributeUpdates:a=[],serverSideRpc:r=[]}=this.getRequestDataObject(e);return{...n,mapping:$a.mapMappingToDowngradedVersion(t),attributeRequests:i,attributeUpdates:a,serverSideRpc:r}}static{this.ɵfac=(()=>{let e;return function(n){return(e||(e=t.ɵɵgetInheritedFactory(q2)))(n||q2)}})()}static{this.ɵcmp=t.ɵɵdefineComponent({type:q2,selectors:[["tb-rest-legacy-basic-config"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>q2)),multi:!0},{provide:K,useExisting:c((()=>q2)),multi:!0}]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:13,vars:17,consts:[[3,"formGroup"],[3,"label"],[3,"ngTemplateOutlet"],["formControlName","server"],["formControlName","mapping"],["formControlName","requestsMapping",3,"defaultRequestUrl"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"mat-tab-group",0)(1,"mat-tab",1),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,2),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",1),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-rest-server-config",3),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"mat-tab",1),t.ɵɵpipe(8,"translate"),t.ɵɵelement(9,"tb-rest-mapping-table",4),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"mat-tab",1),t.ɵɵpipe(11,"translate"),t.ɵɵelement(12,"tb-rest-request-mapping-table",5),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,9,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",n.generalTabContent),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,11,"gateway.server"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(8,13,"gateway.data-mapping"),"*"),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(11,15,"gateway.requests-mapping")),t.ɵɵadvance(2),t.ɵɵproperty("defaultRequestUrl",n.defaultRequestUrl))},dependencies:t.ɵɵgetComponentDepsFactory(q2,[j,_,D1,y0,B2]),encapsulation:2,changeDetection:d.OnPush})}}const G2=(e,t)=>({hasErrors:e,noErrors:t}),z2=()=>({minWidth:"144px",maxWidth:"144px",textAlign:"center"}),U2=()=>({minWidth:"144px",maxWidth:"144px",width:"144px",textAlign:"center"}),j2=e=>({"tb-current-entity":e});function H2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",32),t.ɵɵpipe(1,"translate"),t.ɵɵpipe(2,"async"),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onAddConnector(n))})),t.ɵɵelementStart(3,"mat-icon"),t.ɵɵtext(4,"add"),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,2,"action.add")),t.ɵɵproperty("disabled",t.ɵɵpipeBind1(2,4,e.isLoading$))}}function W2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"section",33)(1,"button",34),t.ɵɵlistener("click",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onAddConnector(n))})),t.ɵɵelementStart(2,"mat-icon",35),t.ɵɵtext(3,"add"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"span"),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()}2&e&&(t.ɵɵadvance(5),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,1,"gateway.add-connector")))}function $2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",36),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-enabled")," "))}function K2(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell")(1,"mat-slide-toggle",37),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return n.stopPropagation(),t.ɵɵresetView(a.onEnableConnector(i))})),t.ɵɵelementEnd()()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("checked",i.activeConnectors.includes(e.key))}}function Y2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",38),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-name"),""))}function X2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell"),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",e.key," ")}}function Z2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-type")," "))}function Q2(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",40),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.returnType(e)," ")}}function J2(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.configuration")," "))}function e3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-cell",40)(1,"div",41),t.ɵɵtext(2),t.ɵɵelementEnd()()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassMap(i.isConnectorSynced(e)?"status-sync":"status-unsync"),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",i.isConnectorSynced(e)?"sync":"out of sync"," ")}}function t3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell",39),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.connectors-table-status")," "))}function n3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell",40)(1,"span",42),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorLogs(i,n))})),t.ɵɵelementEnd()()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassMap(t.ɵɵpureFunction2(3,G2,+i.getErrorsCount(e)>0,0==+i.getErrorsCount(e)||""===i.getErrorsCount(e))),t.ɵɵpropertyInterpolate("matTooltip","Errors: "+i.getErrorsCount(e))}}function i3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-header-cell"),t.ɵɵelement(1,"div",43),t.ɵɵelementStart(2,"div",44),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,z2)),t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(4,3,"gateway.connectors-table-actions")))}function a3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-cell")(1,"div",45)(2,"button",46),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorRpc(i,n))})),t.ɵɵelementStart(3,"mat-icon"),t.ɵɵtext(4,"private_connectivity"),t.ɵɵelementEnd()(),t.ɵɵelementStart(5,"button",47),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorLogs(i,n))})),t.ɵɵelementStart(6,"mat-icon"),t.ɵɵtext(7,"list"),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"button",48),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteConnector(i,n))})),t.ɵɵelementStart(9,"mat-icon"),t.ɵɵtext(10,"delete"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(11,"div",49)(12,"button",50),t.ɵɵlistener("click",(function(n){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.stopPropagation())})),t.ɵɵelementStart(13,"mat-icon",51),t.ɵɵtext(14,"more_vert"),t.ɵɵelementEnd()(),t.ɵɵelementStart(15,"mat-menu",52,1)(17,"button",46),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorRpc(i,n))})),t.ɵɵelementStart(18,"mat-icon"),t.ɵɵtext(19,"private_connectivity"),t.ɵɵelementEnd()(),t.ɵɵelementStart(20,"button",47),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.connectorLogs(i,n))})),t.ɵɵelementStart(21,"mat-icon"),t.ɵɵtext(22,"list"),t.ɵɵelementEnd()(),t.ɵɵelementStart(23,"button",48),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.deleteConnector(i,n))})),t.ɵɵelementStart(24,"mat-icon"),t.ɵɵtext(25,"delete"),t.ɵɵelementEnd()()()()()}if(2&e){const e=n.$implicit,i=t.ɵɵreference(16);t.ɵɵadvance(),t.ɵɵstyleMap(t.ɵɵpureFunction0(5,U2)),t.ɵɵadvance(),t.ɵɵproperty("disabled",!e.value.configurationJson.id),t.ɵɵadvance(10),t.ɵɵproperty("matMenuTriggerFor",i),t.ɵɵadvance(5),t.ɵɵproperty("disabled",!e.value.configurationJson.id)}}function r3(e,n){1&e&&t.ɵɵelement(0,"mat-header-row",53)}function o3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-row",54),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).$implicit,a=t.ɵɵnextContext();return t.ɵɵresetView(a.selectConnector(n,i))})),t.ɵɵelementEnd()}if(2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵclassMap(t.ɵɵpureFunction1(2,j2,i.isSameConnector(e)))}}function s3(e,n){if(1&e&&(t.ɵɵelementStart(0,"span",55),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵtextInterpolate1("v",e.connectorForm.get("configVersion").value,"")}}function l3(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-select",56)(1,"tb-toggle-option",57),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"tb-toggle-option",57),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("value",e.ConnectorConfigurationModes.BASIC),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(3,4,"gateway.basic")," "),t.ɵɵadvance(2),t.ɵɵproperty("value",e.ConnectorConfigurationModes.ADVANCED),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,6,"gateway.advanced")," ")}}function p3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-mqtt-basic-config",65),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function c3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-mqtt-legacy-basic-config",65),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function d3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,p3,2,4,"tb-mqtt-basic-config",64),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,c3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.MQTT))("ngIfElse",e)}}function u3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-opc-ua-basic-config",65),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function m3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-opc-ua-legacy-basic-config",65),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function h3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,u3,2,4,"tb-opc-ua-basic-config",64),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,m3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.OPCUA))("ngIfElse",e)}}function g3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-modbus-basic-config",67),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function f3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-modbus-legacy-basic-config",67),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function y3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,g3,1,1,"tb-modbus-basic-config",66),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,f3,1,1,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.MODBUS))("ngIfElse",e)}}function v3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-socket-basic-config",67),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function x3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-socket-legacy-basic-config",67),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){t.ɵɵnextContext(4);const e=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",e)}}function b3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,v3,1,1,"tb-socket-basic-config",66),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,x3,1,1,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.SOCKET))("ngIfElse",e)}}function w3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-bacnet-basic-config",65),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function S3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-bacnet-legacy-basic-config",65),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function C3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,w3,2,4,"tb-bacnet-basic-config",64),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,S3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.BACNET))("ngIfElse",e)}}function _3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-rest-basic-config",69),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function T3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-rest-legacy-basic-config",69),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(4);return t.ɵɵresetView(n.basicConfigInitSubject.next())})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext(4),n=t.ɵɵreference(41);t.ɵɵproperty("generalTabContent",n)("withReportStrategy",t.ɵɵpipeBind1(1,2,e.connectorForm.get("configVersion").value))}}function I3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0),t.ɵɵtemplate(1,_3,2,4,"tb-rest-basic-config",68),t.ɵɵpipe(2,"isLatestVersionConfig"),t.ɵɵtemplate(3,T3,2,4,"ng-template",null,3,t.ɵɵtemplateRefExtractor),t.ɵɵelementContainerEnd()),2&e){const e=t.ɵɵreference(4),n=t.ɵɵnextContext(3);t.ɵɵadvance(),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(2,2,n.connectorForm.get("configVersion").value,n.ConnectorType.REST))("ngIfElse",e)}}function E3(e,n){if(1&e&&(t.ɵɵelementContainerStart(0)(1,62),t.ɵɵtemplate(2,d3,5,5,"ng-container",63)(3,h3,5,5,"ng-container",63)(4,y3,5,5,"ng-container",63)(5,b3,5,5,"ng-container",63)(6,C3,5,5,"ng-container",63)(7,I3,5,5,"ng-container",63),t.ɵɵelementContainerEnd()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("ngSwitch",e.initialConnector.type),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MQTT),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.OPCUA),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.MODBUS),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.SOCKET),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.BACNET),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",e.ConnectorType.REST)}}function M3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-tab-group")(1,"mat-tab",70),t.ɵɵpipe(2,"translate"),t.ɵɵelementContainer(3,71),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"mat-tab",70),t.ɵɵpipe(5,"translate"),t.ɵɵelement(6,"tb-json-object-edit",72),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()),2&e){t.ɵɵnextContext(2);const e=t.ɵɵreference(41);t.ɵɵadvance(),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,6,"gateway.general")),t.ɵɵadvance(2),t.ɵɵproperty("ngTemplateOutlet",e),t.ɵɵadvance(),t.ɵɵpropertyInterpolate1("label","",t.ɵɵpipeBind1(5,8,"gateway.configuration"),"*"),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(7,10,"gateway.configuration")),t.ɵɵproperty("fillHeight",!0)}}function k3(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"section",58),t.ɵɵtemplate(1,E3,8,7,"ng-container",59)(2,M3,8,12,"ng-template",null,2,t.ɵɵtemplateRefExtractor),t.ɵɵelementStart(4,"div",60)(5,"button",61),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.onSaveConnector())})),t.ɵɵtext(6),t.ɵɵpipe(7,"translate"),t.ɵɵelementEnd()()()}if(2&e){let e;const n=t.ɵɵreference(3),i=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵproperty("ngIf",(null==(e=i.connectorForm.get("mode"))?null:e.value)===i.ConnectorConfigurationModes.BASIC)("ngIfElse",n),t.ɵɵadvance(4),t.ɵɵproperty("disabled",!i.connectorForm.dirty||i.connectorForm.invalid),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(7,4,"action.save")," ")}}function P3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-icon",89),t.ɵɵpipe(1,"translate"),t.ɵɵtext(2," warning "),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("matTooltip",t.ɵɵpipeBind1(1,1,e.connectorForm.get("name").hasError("duplicateName")?"gateway.connector-duplicate-name":"gateway.name-required"))}}function D3(e,n){1&e&&(t.ɵɵelementStart(0,"div",74)(1,"div",85),t.ɵɵtext(2,"gateway.connectors-table-class"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",76)(4,"mat-form-field",77),t.ɵɵelement(5,"input",90),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,1,"gateway.set")))}function O3(e,n){1&e&&(t.ɵɵelementStart(0,"div",74)(1,"div",85),t.ɵɵtext(2,"gateway.connectors-table-key"),t.ɵɵelementEnd(),t.ɵɵelementStart(3,"div",76)(4,"mat-form-field",77),t.ɵɵelement(5,"input",91),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(6,1,"gateway.set")))}function A3(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",57),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function F3(e,n){1&e&&(t.ɵɵelementStart(0,"div",74)(1,"mat-slide-toggle",92)(2,"mat-label",93),t.ɵɵpipe(3,"translate"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,2,"gateway.send-change-data-hint")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(5,4,"gateway.send-change-data")," "))}function R3(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",94),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("isExpansionMode",!0)("defaultValue",e.ReportStrategyDefaultValue.Connector)}}function B3(e,n){if(1&e&&(t.ɵɵelementStart(0,"section",73)(1,"div",74)(2,"div",75),t.ɵɵtext(3,"gateway.name"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",76)(5,"mat-form-field",77),t.ɵɵelement(6,"input",78),t.ɵɵpipe(7,"translate"),t.ɵɵtemplate(8,P3,3,3,"mat-icon",79),t.ɵɵelementEnd()()(),t.ɵɵtemplate(9,D3,7,3,"div",80)(10,O3,7,3,"div",80),t.ɵɵelementStart(11,"div",81)(12,"div",82),t.ɵɵtext(13,"gateway.logs-configuration"),t.ɵɵelementEnd(),t.ɵɵelementStart(14,"div",83)(15,"mat-slide-toggle",84)(16,"mat-label"),t.ɵɵtext(17),t.ɵɵpipe(18,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(19,"div",74)(20,"div",85),t.ɵɵtext(21,"gateway.remote-logging-level"),t.ɵɵelementEnd(),t.ɵɵelementStart(22,"div",76)(23,"mat-form-field",77)(24,"mat-select",86),t.ɵɵtemplate(25,A3,2,2,"mat-option",87),t.ɵɵelementEnd()()()()(),t.ɵɵtemplate(26,F3,6,6,"div",80),t.ɵɵpipe(27,"withReportStrategy"),t.ɵɵtemplate(28,R3,1,2,"tb-report-strategy",88),t.ɵɵpipe(29,"withReportStrategy"),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("formGroup",e.connectorForm),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("placeholder",t.ɵɵpipeBind1(7,9,"gateway.set")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.connectorForm.get("name").hasError("required")&&e.connectorForm.get("name").touched||e.connectorForm.get("name").hasError("duplicateName")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.connectorForm.get("type").value===e.ConnectorType.CUSTOM),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.connectorForm.get("type").value===e.ConnectorType.GRPC),t.ɵɵadvance(7),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(18,11,"gateway.enable-remote-logging")," "),t.ɵɵadvance(8),t.ɵɵproperty("ngForOf",e.gatewayLogLevel),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.connectorForm.get("type").value===e.ConnectorType.MQTT&&!t.ɵɵpipeBind2(27,13,e.connectorForm.get("configVersion").value,e.ConnectorType.MQTT)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",t.ɵɵpipeBind2(29,16,e.connectorForm.get("configVersion").value,e.connectorForm.get("type").value))}}class N3{isErrorState(e){return e&&e.invalid}}e("ForceErrorStateMatcher",N3);class L3 extends q{constructor(e,t,n,i,a,r,o,s,l,p,c){super(e),this.store=e,this.fb=t,this.translate=n,this.attributeService=i,this.dialogService=a,this.dialog=r,this.telemetryWsService=o,this.zone=s,this.utils=l,this.withReportStrategy=p,this.cd=c,this.ConnectorType=ct,this.allowBasicConfig=new Set([ct.MQTT,ct.OPCUA,ct.MODBUS,ct.SOCKET,ct.BACNET,ct.REST]),this.gatewayLogLevel=Object.values(lt),this.displayedColumns=["enabled","key","type","syncStatus","errors","actions"],this.GatewayConnectorTypesTranslatesMap=dt,this.ConnectorConfigurationModes=Qt,this.ReportStrategyDefaultValue=en,this.basicConfigInitSubject=new te,this.activeData=[],this.inactiveData=[],this.sharedAttributeData=[],this.subscriptionOptions={callbacks:{onDataUpdated:()=>this.ctx.ngZone.run((()=>{this.onErrorsUpdated()})),onDataUpdateError:(e,t)=>this.ctx.ngZone.run((()=>{this.onDataUpdateError(t)}))}},this.destroy$=new te,this.attributeUpdateSubject=new te,this.initDataSources(),this.initConnectorForm(),this.observeAttributeChange()}ngAfterViewInit(){this.dataSource.sort=this.sort,this.dataSource.sortingDataAccessor=this.getSortingDataAccessor(),this.ctx.$scope.gatewayConnectors=this,this.loadConnectors(),this.loadGatewayState(),this.observeModeChange()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),super.ngOnDestroy()}onSaveConnector(){this.saveConnector(this.getUpdatedConnectorData(this.connectorForm.value),!1)}saveConnector(e,t=!0){const n=t||this.activeConnectors.includes(this.initialConnector.name)?D.SHARED_SCOPE:D.SERVER_SCOPE;oe(this.getEntityAttributeTasks(e,n)).pipe(ye(1)).subscribe((n=>{this.showToast(t?this.translate.instant("gateway.connector-created"):this.translate.instant("gateway.connector-updated")),this.initialConnector=e,this.updateData(!0),this.connectorForm.markAsPristine()}))}getEntityAttributeTasks(e,t){const n=[],i=[{key:e.name,value:e}],a=[],r=!this.activeConnectors.includes(e.name)&&t===D.SHARED_SCOPE||!this.inactiveConnectors.includes(e.name)&&t===D.SERVER_SCOPE,o=this.initialConnector&&this.initialConnector.name!==e.name;return o&&(a.push({key:this.initialConnector.name}),this.removeConnectorFromList(this.initialConnector.name,!0),this.removeConnectorFromList(this.initialConnector.name,!1)),r&&(t===D.SHARED_SCOPE?this.activeConnectors.push(e.name):this.inactiveConnectors.push(e.name)),(o||r)&&n.push(this.getSaveEntityAttributesTask(t)),n.push(this.attributeService.saveEntityAttributes(this.device,t,i)),a.length&&n.push(this.attributeService.deleteEntityAttributes(this.device,t,a)),n}getSaveEntityAttributesTask(e){const t=e===D.SHARED_SCOPE?"active_connectors":"inactive_connectors",n=e===D.SHARED_SCOPE?this.activeConnectors:this.inactiveConnectors;return this.attributeService.saveEntityAttributes(this.device,e,[{key:t,value:n}])}removeConnectorFromList(e,t){const n=t?this.activeConnectors:this.inactiveConnectors,i=n.indexOf(e);-1!==i&&n.splice(i,1)}getUpdatedConnectorData(e){const t={...e};return t.configuration=`${Be(t.name)}.json`,delete t.basicConfig,t.type!==ct.GRPC&&delete t.key,t.type!==ct.CUSTOM&&delete t.class,this.allowBasicConfig.has(t.type)||delete t.mode,this.withReportStrategy.transform(t.configVersion,t.type)&&(t.configurationJson.reportStrategy=t.reportStrategy,Ae(t.reportStrategy)&&delete t.reportStrategy,Ae(t.configurationJson.reportStrategy)&&delete t.configurationJson.reportStrategy),this.gatewayVersion&&!t.configVersion&&(t.configVersion=this.gatewayVersion),t.ts=Date.now(),t}updateData(e=!1){this.pageLink.sortOrder.property=this.sort.active,this.pageLink.sortOrder.direction=w[this.sort.direction.toUpperCase()],this.attributeDataSource.loadAttributes(this.device,D.CLIENT_SCOPE,this.pageLink,e).subscribe((e=>{this.activeData=e.data.filter((e=>this.activeConnectors.includes(e.key))),this.combineData(),this.generateSubscription(),this.setClientData(e)})),this.inactiveConnectorsDataSource.loadAttributes(this.device,D.SHARED_SCOPE,this.pageLink,e).subscribe((e=>{this.sharedAttributeData=e.data.filter((e=>this.activeConnectors.includes(e.key))),this.combineData()})),this.serverDataSource.loadAttributes(this.device,D.SERVER_SCOPE,this.pageLink,e).subscribe((e=>{this.inactiveData=e.data.filter((e=>this.inactiveConnectors.includes(e.key))),this.combineData()}))}isConnectorSynced(e){const t=e.value;if(!t.ts||e.skipSync||!this.isGatewayActive)return!1;if(-1===this.activeData.findIndex((e=>("string"==typeof e.value?JSON.parse(e.value):e.value).name===t.name)))return!1;return-1!==this.sharedAttributeData.findIndex((e=>{const n=e.value,i=n.name===t.name,a=Se(n.configurationJson,{})&&i,r=this.hasSameConfig(n.configurationJson,t.configurationJson),o=n.ts&&n.ts<=t.ts;return i&&o&&(r||a)}))}hasSameConfig(e,t){const{name:n,id:i,enableRemoteLogging:a,logLevel:r,reportStrategy:o,configVersion:s,...l}=e,{name:p,id:c,enableRemoteLogging:d,logLevel:u,reportStrategy:m,configVersion:h,...g}=t;return Se(l,g)}combineData(){const e=[...this.activeData,...this.inactiveData,...this.sharedAttributeData].reduce(((e,t)=>{const n=e.findIndex((e=>e.key===t.key));return-1===n?e.push(t):t.lastUpdateTs>e[n].lastUpdateTs&&!this.isConnectorSynced(e[n])&&(e[n]={...t,skipSync:!0}),e}),[]);this.dataSource.data=e.map((e=>({...e,value:"string"==typeof e.value?JSON.parse(e.value):e.value})))}clearOutConnectorForm(){this.initialConnector=null,this.connectorForm.setValue({mode:Qt.BASIC,name:"",type:ct.MQTT,sendDataOnlyOnChange:!1,enableRemoteLogging:!1,logLevel:lt.INFO,key:"auto",class:"",configuration:"",configurationJson:{},basicConfig:{},configVersion:"",reportStrategy:[{value:{},disabled:!0}]},{emitEvent:!1}),this.connectorForm.markAsPristine()}selectConnector(e,t){e&&e.stopPropagation();const n=t.value;n?.name!==this.initialConnector?.name&&this.confirmConnectorChange().subscribe((e=>{e&&this.setFormValue(n)}))}isSameConnector(e){if(!this.initialConnector)return!1;const t=e.value;return this.initialConnector.name===t.name}showToast(e){this.store.dispatch({type:"[Notification] Show",notification:{message:e,type:"success",duration:1e3,verticalPosition:"top",horizontalPosition:"left",target:"dashboardRoot",forceDismiss:!0}})}returnType(e){const t=e.value;return this.GatewayConnectorTypesTranslatesMap.get(t.type)}deleteConnector(e,t){t?.stopPropagation();const n=`Delete connector "${e.key}"?`;this.dialogService.confirm(n,"All connector data will be deleted.","Cancel","Delete").pipe(ye(1),xe((t=>{if(!t)return;const n=[],i=this.activeConnectors.includes(e.value?.name)?D.SHARED_SCOPE:D.SERVER_SCOPE;return n.push(this.attributeService.deleteEntityAttributes(this.device,i,[e])),this.removeConnectorFromList(e.key,!0),this.removeConnectorFromList(e.key,!1),n.push(this.getSaveEntityAttributesTask(i)),oe(n)}))).subscribe((()=>{this.initialConnector&&this.initialConnector.name!==e.key||(this.clearOutConnectorForm(),this.cd.detectChanges()),this.updateData(!0)}))}connectorLogs(e,t){t&&t.stopPropagation();const n=Oe(this.ctx.stateController.getStateParams());n.connector_logs=e,n.targetEntityParamName="connector_logs",this.ctx.stateController.openState("connector_logs",n)}connectorRpc(e,t){t&&t.stopPropagation();const n=Oe(this.ctx.stateController.getStateParams());n.connector_rpc=e,n.targetEntityParamName="connector_rpc",this.ctx.stateController.openState("connector_rpc",n)}onEnableConnector(e){e.value.ts=(new Date).getTime(),this.updateActiveConnectorKeys(e.key),this.attributeUpdateSubject.next(e)}getErrorsCount(e){const t=e.key,n=this.subscription&&this.subscription.data.find((e=>e&&e.dataKey.name===`${t}_ERRORS_COUNT`));return n&&this.activeConnectors.includes(t)?n.data[0][1]||0:"Inactive"}onAddConnector(e){e?.stopPropagation(),this.confirmConnectorChange().pipe(ye(1),ue(Boolean),xe((()=>this.openAddConnectorDialog())),ue(Boolean)).subscribe((e=>this.addConnector(e)))}addConnector(e){e.configurationJson||(e.configurationJson={}),this.gatewayVersion&&!e.configVersion&&(e.configVersion=this.gatewayVersion),e.basicConfig=e.configurationJson,this.initialConnector=e;const t=this.connectorForm.get("type").value;this.setInitialConnectorValues(e),this.saveConnector(this.getUpdatedConnectorData(e)),t!==e.type&&this.allowBasicConfig.has(e.type)?this.basicConfigInitSubject.pipe(ye(1)).subscribe((()=>{this.patchConnectorBasicConfig(e.basicConfig)})):this.patchConnectorBasicConfig(e.basicConfig)}setInitialConnectorValues(e){const{basicConfig:t,mode:n,enableRemoteLogging:i,...a}=e;this.toggleReportStrategy(e),this.connectorForm.get("mode").setValue(this.allowBasicConfig.has(e.type)?e.mode??Qt.BASIC:null,{emitEvent:!1}),this.connectorForm.get("enableRemoteLogging").setValue(i,{emitEvent:!1}),this.connectorForm.patchValue(a,{emitEvent:!1})}openAddConnectorDialog(){return this.ctx.ngZone.run((()=>this.dialog.open(h$,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{dataSourceData:this.dataSource.data,gatewayVersion:this.gatewayVersion}}).afterClosed()))}uniqNameRequired(){return e=>{const t=e.value?.trim().toLowerCase(),n=this.dataSource.data.some((e=>e.value.name.toLowerCase()===t)),i=this.initialConnector?.name.toLowerCase()===t;return n&&!i?{duplicateName:{valid:!1}}:null}}initDataSources(){const e={property:"key",direction:w.ASC};this.pageLink=new S(1e3,0,null,e),this.attributeDataSource=new zn(this.attributeService,this.telemetryWsService,this.zone,this.translate),this.inactiveConnectorsDataSource=new zn(this.attributeService,this.telemetryWsService,this.zone,this.translate),this.serverDataSource=new zn(this.attributeService,this.telemetryWsService,this.zone,this.translate),this.dataSource=new x([])}initConnectorForm(){this.connectorForm=this.fb.group({mode:[Qt.BASIC],name:["",[$.required,this.uniqNameRequired(),$.pattern(an)]],type:["",[$.required]],enableRemoteLogging:[!1],logLevel:["",[$.required]],sendDataOnlyOnChange:[!1],key:["auto"],class:[""],configuration:[""],configurationJson:[{},[$.required]],basicConfig:[{}],configVersion:[""],reportStrategy:[{value:{},disabled:!0}]})}getSortingDataAccessor(){return(e,t)=>{switch(t){case"syncStatus":return this.isConnectorSynced(e)?1:0;case"enabled":return this.activeConnectors.includes(e.key)?1:0;case"errors":const n=this.getErrorsCount(e);return"string"==typeof n?this.sort.direction.toUpperCase()===w.DESC?-1:1/0:n;default:return e[t]||e.value[t]}}}loadConnectors(){this.device&&this.device.id!==B&&oe([this.attributeService.getEntityAttributes(this.device,D.SHARED_SCOPE,["active_connectors"]),this.attributeService.getEntityAttributes(this.device,D.SERVER_SCOPE,["inactive_connectors"]),this.attributeService.getEntityAttributes(this.device,D.CLIENT_SCOPE,["Version"])]).pipe(le(this.destroy$)).subscribe((e=>{this.activeConnectors=this.parseConnectors(e[0]),this.inactiveConnectors=this.parseConnectors(e[1]),this.gatewayVersion=e[2][0]?.value,this.updateData(!0)}))}loadGatewayState(){this.attributeService.getEntityAttributes(this.device,D.SERVER_SCOPE).pipe(le(this.destroy$)).subscribe((e=>{const t=e.find((e=>"active"===e.key)).value,n=e.find((e=>"lastDisconnectTime"===e.key))?.value,i=e.find((e=>"lastConnectTime"===e.key))?.value;this.isGatewayActive=this.getGatewayStatus(t,i,n)}))}parseConnectors(e){const t=e?.[0]?.value||[];return Me(t)?JSON.parse(t):t}observeModeChange(){this.connectorForm.get("mode").valueChanges.pipe(le(this.destroy$)).subscribe((e=>{e===Qt.BASIC&&this.patchConnectorBasicConfig(this.connectorForm.get("configurationJson").value)}))}observeAttributeChange(){this.attributeUpdateSubject.pipe(de(300),me((e=>this.executeAttributeUpdates(e))),le(this.destroy$)).subscribe()}updateActiveConnectorKeys(e){if(this.activeConnectors.includes(e)){const t=this.activeConnectors.indexOf(e);-1!==t&&this.activeConnectors.splice(t,1),this.inactiveConnectors.push(e)}else{const t=this.inactiveConnectors.indexOf(e);-1!==t&&this.inactiveConnectors.splice(t,1),this.activeConnectors.push(e)}}executeAttributeUpdates(e){oe(this.getAttributeExecutionTasks(e)).pipe(ye(1),me((()=>this.updateData(!0))),le(this.destroy$)).subscribe()}getAttributeExecutionTasks(e){const t=this.activeConnectors.includes(e.key),n=t?D.SERVER_SCOPE:D.SHARED_SCOPE,i=t?D.SHARED_SCOPE:D.SERVER_SCOPE;return[this.attributeService.saveEntityAttributes(this.device,D.SHARED_SCOPE,[{key:"active_connectors",value:this.activeConnectors}]),this.attributeService.saveEntityAttributes(this.device,D.SERVER_SCOPE,[{key:"inactive_connectors",value:this.inactiveConnectors}]),this.attributeService.deleteEntityAttributes(this.device,n,[e]),this.attributeService.saveEntityAttributes(this.device,i,[e])]}onDataUpdateError(e){const t=this.utils.parseException(e);let n=t.name;t.message&&(n+=": "+t.message),console.error(n)}onErrorsUpdated(){this.cd.detectChanges()}onDataUpdated(){const e=this.ctx.defaultSubscription.data,t=e.find((e=>"active"===e.dataKey.name)).data[0][1],n=e.find((e=>"lastDisconnectTime"===e.dataKey.name)).data[0][1],i=e.find((e=>"lastConnectTime"===e.dataKey.name)).data[0][1];this.isGatewayActive=this.getGatewayStatus(t,i,n),this.cd.detectChanges()}getGatewayStatus(e,t,n){return!!e&&(!n||t>n)}generateSubscription(){if(this.subscription&&this.subscription.unsubscribe(),this.device){const e=[{type:N.entity,entityType:L.DEVICE,entityId:this.device.id,entityName:"Gateway",timeseries:[]}];this.dataSource.data.forEach((t=>{e[0].timeseries.push({name:`${t.key}_ERRORS_COUNT`,label:`${t.key}_ERRORS_COUNT`})})),this.ctx.subscriptionApi.createSubscriptionFromInfo(R.latest,e,this.subscriptionOptions,!1,!0).subscribe((e=>{this.subscription=e}))}}createBasicConfigWatcher(){this.basicConfigSub&&this.basicConfigSub.unsubscribe(),this.basicConfigSub=this.connectorForm.get("basicConfig").valueChanges.pipe(ue((()=>!!this.initialConnector)),le(this.destroy$)).subscribe((e=>{const t=this.connectorForm.get("configurationJson"),n=this.connectorForm.get("type").value,i=this.connectorForm.get("mode").value;if(!Se(e,t?.value)&&this.allowBasicConfig.has(n)&&i===Qt.BASIC){const n={...t.value,...e};this.connectorForm.get("configurationJson").patchValue(n,{emitEvent:!1})}}))}createJsonConfigWatcher(){this.jsonConfigSub&&this.jsonConfigSub.unsubscribe(),this.jsonConfigSub=this.connectorForm.get("configurationJson").valueChanges.pipe(le(this.destroy$)).subscribe((e=>{const t=this.connectorForm.get("basicConfig"),n=this.connectorForm.get("type").value,i=this.connectorForm.get("mode").value;!Se(e,t?.value)&&this.allowBasicConfig.has(n)&&i===Qt.ADVANCED&&this.connectorForm.get("basicConfig").patchValue(e,{emitEvent:!1})}))}confirmConnectorChange(){return this.initialConnector&&this.connectorForm.dirty?this.dialogService.confirm(this.translate.instant("gateway.change-connector-title"),this.translate.instant("gateway.change-connector-text"),this.translate.instant("action.no"),this.translate.instant("action.yes"),!0):ae(!0)}setFormValue(e){this.connectorForm.disabled&&this.connectorForm.enable();const t=za.getConfig({configuration:"",key:"auto",configurationJson:{},...e},this.gatewayVersion);this.gatewayVersion&&!t.configVersion&&(t.configVersion=this.gatewayVersion),t.basicConfig=t.configurationJson,this.initialConnector=t,this.updateConnector(t)}updateConnector(e){this.jsonConfigSub?.unsubscribe(),this.allowBasicConfig.has(e.type)?this.updateBasicConfigConnector(e):(this.setInitialConnectorValues(e),this.connectorForm.markAsPristine(),this.createJsonConfigWatcher())}updateBasicConfigConnector(e){this.basicConfigSub?.unsubscribe();const t=this.connectorForm.get("type").value;this.setInitialConnectorValues(e),t!==e.type&&this.allowBasicConfig.has(e.type)&&e.mode!==Qt.ADVANCED?this.basicConfigInitSubject.asObservable().pipe(ye(1)).subscribe((()=>{this.patchConnectorBasicConfig(e.basicConfig)})):this.patchConnectorBasicConfig(e.basicConfig)}patchConnectorBasicConfig(e){this.connectorForm.get("basicConfig").patchValue(e,{emitEvent:!1}),this.connectorForm.markAsPristine(),this.createBasicConfigWatcher(),this.createJsonConfigWatcher()}toggleReportStrategy(e){const t=this.connectorForm.get("reportStrategy"),n=this.connectorForm.get("sendDataOnlyOnChange");this.connectorForm.get("reportStrategy").reset(e.reportStrategy,{emitEvent:!1}),this.withReportStrategy.transform(e.configVersion,e.type)?(t.enable({emitEvent:!1}),n.disable({emitEvent:!1})):(t.disable({emitEvent:!1}),e.type===ct.MQTT&&n.enable({emitEvent:!1}))}setClientData(e){if(this.initialConnector){const t=e.data.find((e=>e.key===this.initialConnector.name));t&&(t.value="string"==typeof t.value?JSON.parse(t.value):t.value,this.isConnectorSynced(t)&&t.value.configurationJson&&this.setFormValue({...t.value,mode:this.connectorForm.get("mode").value??t.value.mode}))}}static{this.ɵfac=function(e){return new(e||L3)(t.ɵɵdirectiveInject(Ye.Store),t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(He.TranslateService),t.ɵɵdirectiveInject(_e.AttributeService),t.ɵɵdirectiveInject(_e.DialogService),t.ɵɵdirectiveInject(qe.MatDialog),t.ɵɵdirectiveInject(_e.TelemetryWebsocketService),t.ɵɵdirectiveInject(t.NgZone),t.ɵɵdirectiveInject(_e.UtilsService),t.ɵɵdirectiveInject(Ka),t.ɵɵdirectiveInject(t.ChangeDetectorRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:L3,selectors:[["tb-gateway-connector"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(v,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.sort=e.first)}},inputs:{ctx:"ctx",device:"device"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:ot,useClass:N3},Ka]),t.ɵɵInheritDefinitionFeature,t.ɵɵStandaloneFeature],decls:42,vars:21,consts:[["generalTabContent",""],["cellActionsMenu","matMenu"],["defaultConfig",""],["legacy",""],[1,"connector-container","tb-form-panel","no-border"],[1,"table-section","tb-form-panel","no-padding","section-container","flex"],[1,"mat-mdc-table-toolbar","justify-between"],["mat-icon-button","","matTooltipPosition","above",3,"disabled","matTooltip","click",4,"ngIf"],[1,"table-container"],["class","mat-headline-5 tb-absolute-fill tb-add-new items-center justify-center",4,"ngIf"],["mat-table","","matSort","","matSortDisableClear","",3,"dataSource","matSortActive","matSortDirection"],["matColumnDef","enabled","sticky",""],["style","width: 60px;min-width: 60px;",4,"matHeaderCellDef"],[4,"matCellDef"],["matColumnDef","key"],["mat-sort-header","","style","width: 40%",4,"matHeaderCellDef"],["matColumnDef","type"],["mat-sort-header","","style","width: 30%",4,"matHeaderCellDef"],["style","text-transform: uppercase",4,"matCellDef"],["matColumnDef","syncStatus"],["matColumnDef","errors"],["matColumnDef","actions","stickyEnd",""],[4,"matHeaderCellDef"],["class","mat-row-select",4,"matHeaderRowDef","matHeaderRowDefSticky"],["class","mat-row-select",3,"class","click",4,"matRowDef","matRowDefColumns"],[1,"tb-form-panel","section-container","flex",3,"formGroup"],[1,"tb-form-panel-title","tb-flex","no-flex","space-between","align-center"],[1,"tb-form-panel-title"],["class","version-placeholder",4,"ngIf"],["formControlName","mode","appearance","fill",4,"ngIf"],["translate","",1,"no-data-found","items-center","justify-center"],["class","tb-form-panel section-container no-border no-padding tb-flex space-between",4,"ngIf"],["mat-icon-button","","matTooltipPosition","above",3,"click","disabled","matTooltip"],[1,"mat-headline-5","tb-absolute-fill","tb-add-new","items-center","justify-center"],["mat-button","",1,"connector",3,"click"],[1,"tb-mat-96"],[2,"width","60px","min-width","60px"],[3,"click","checked"],["mat-sort-header","",2,"width","40%"],["mat-sort-header","",2,"width","30%"],[2,"text-transform","uppercase"],[1,"status"],["matTooltipPosition","above",1,"dot",3,"click","matTooltip"],[1,"gt-md:!hidden",2,"width","48px","min-width","48px","max-width","48px"],[1,"lt-lg:!hidden"],[1,"lt-md:!hidden","flex-row","justify-end"],["mat-icon-button","","matTooltip","RPC","matTooltipPosition","above",3,"click","disabled"],["mat-icon-button","","matTooltip","Logs","matTooltipPosition","above",3,"click"],["mat-icon-button","","matTooltip","Delete connector","matTooltipPosition","above",3,"click"],[1,"gt-sm:!hidden"],["mat-icon-button","",3,"click","matMenuTriggerFor"],[1,"material-icons"],["xPosition","before"],[1,"mat-row-select"],[1,"mat-row-select",3,"click"],[1,"version-placeholder"],["formControlName","mode","appearance","fill"],[3,"value"],[1,"tb-form-panel","section-container","no-border","no-padding","tb-flex","space-between"],[4,"ngIf","ngIfElse"],[1,"flex","justify-end"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],[3,"ngSwitch"],[4,"ngSwitchCase"],["formControlName","basicConfig",3,"generalTabContent","withReportStrategy","initialized",4,"ngIf","ngIfElse"],["formControlName","basicConfig",3,"initialized","generalTabContent","withReportStrategy"],["formControlName","basicConfig",3,"generalTabContent","initialized",4,"ngIf","ngIfElse"],["formControlName","basicConfig",3,"initialized","generalTabContent"],["class","h-full","formControlName","basicConfig",3,"generalTabContent","withReportStrategy","initialized",4,"ngIf","ngIfElse"],["formControlName","basicConfig",1,"h-full",3,"initialized","generalTabContent","withReportStrategy"],[3,"label"],[3,"ngTemplateOutlet"],["jsonRequired","","formControlName","configurationJson",1,"configuration-json",3,"fillHeight","label"],[1,"tb-form-panel","no-border","no-padding","padding-top","section-container","flex",3,"formGroup"],[1,"tb-form-row","column-xs"],["translate","",1,"fixed-title-width","tb-required"],[1,"tb-flex","no-gap"],["appearance","outline","subscriptSizing","dynamic",1,"tb-flex","no-gap"],["matInput","","autocomplete","off","name","value","formControlName","name",3,"placeholder"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip","class","tb-error",3,"matTooltip",4,"ngIf"],["class","tb-form-row column-xs",4,"ngIf"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row"],["formControlName","enableRemoteLogging",1,"mat-slide"],["translate","",1,"fixed-title-width"],["formControlName","logLevel"],[3,"value",4,"ngFor","ngForOf"],["class","stroked tb-form-panel","formControlName","reportStrategy",3,"isExpansionMode","defaultValue",4,"ngIf"],["matSuffix","","matTooltipPosition","above","matTooltipClass","tb-error-tooltip",1,"tb-error",3,"matTooltip"],["matInput","","name","value","formControlName","class",3,"placeholder"],["matInput","","name","value","formControlName","key",3,"placeholder"],["formControlName","sendDataOnlyOnChange",1,"mat-slide"],[3,"tb-hint-tooltip-icon"],["formControlName","reportStrategy",1,"stroked","tb-form-panel",3,"isExpansionMode","defaultValue"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",4)(1,"section",5)(2,"mat-toolbar",6)(3,"h2"),t.ɵɵtext(4),t.ɵɵpipe(5,"translate"),t.ɵɵelementEnd(),t.ɵɵtemplate(6,H2,5,6,"button",7),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",8),t.ɵɵtemplate(8,W2,7,3,"section",9),t.ɵɵelementStart(9,"table",10),t.ɵɵelementContainerStart(10,11),t.ɵɵtemplate(11,$2,3,3,"mat-header-cell",12)(12,K2,2,1,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(13,14),t.ɵɵtemplate(14,Y2,3,3,"mat-header-cell",15)(15,X2,2,1,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(16,16),t.ɵɵtemplate(17,Z2,3,3,"mat-header-cell",17)(18,Q2,2,1,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(19,19),t.ɵɵtemplate(20,J2,3,3,"mat-header-cell",17)(21,e3,3,3,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(22,20),t.ɵɵtemplate(23,t3,3,3,"mat-header-cell",17)(24,n3,2,6,"mat-cell",18),t.ɵɵelementContainerEnd(),t.ɵɵelementContainerStart(25,21),t.ɵɵtemplate(26,i3,5,6,"mat-header-cell",22)(27,a3,26,6,"mat-cell",13),t.ɵɵelementContainerEnd(),t.ɵɵtemplate(28,r3,1,0,"mat-header-row",23)(29,o3,1,4,"mat-row",24),t.ɵɵelementEnd()()(),t.ɵɵelementStart(30,"section",25)(31,"div",26)(32,"div",27),t.ɵɵtext(33),t.ɵɵpipe(34,"translate"),t.ɵɵtemplate(35,s3,2,1,"span",28),t.ɵɵelementEnd(),t.ɵɵtemplate(36,l3,7,8,"tb-toggle-select",29),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"span",30),t.ɵɵtext(38," gateway.select-connector "),t.ɵɵelementEnd(),t.ɵɵtemplate(39,k3,8,6,"section",31),t.ɵɵelementEnd()(),t.ɵɵtemplate(40,B3,30,19,"ng-template",null,0,t.ɵɵtemplateRefExtractor)),2&e&&(t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(5,17,"gateway.connectors")),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",null==n.dataSource||null==n.dataSource.data?null:n.dataSource.data.length),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",!(null!=n.dataSource&&(null!=n.dataSource.data&&n.dataSource.data.length))),t.ɵɵadvance(),t.ɵɵproperty("dataSource",n.dataSource)("matSortActive",n.pageLink.sortOrder.property)("matSortDirection",n.pageLink.sortDirection()),t.ɵɵadvance(19),t.ɵɵproperty("matHeaderRowDef",n.displayedColumns)("matHeaderRowDefSticky",!0),t.ɵɵadvance(),t.ɵɵproperty("matRowDefColumns",n.displayedColumns),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.connectorForm),t.ɵɵadvance(3),t.ɵɵtextInterpolate2(" ",null!=n.initialConnector&&n.initialConnector.type?n.GatewayConnectorTypesTranslatesMap.get(n.initialConnector.type):""," ",t.ɵɵpipeBind1(34,19,"gateway.configuration")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.connectorForm.get("configVersion").value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.initialConnector&&n.allowBasicConfig.has(n.initialConnector.type)),t.ɵɵadvance(),t.ɵɵclassProp("!hidden",n.initialConnector),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.initialConnector))},dependencies:t.ɵɵgetComponentDepsFactory(L3,[j,_,Ka,a$,oX,sX,XY,YY,xQ,bQ,kJ,PJ,Xn,E1,M1,q2,V2]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:block;overflow-x:auto;padding:0}[_nghost-%COMP%] .version-placeholder[_ngcontent-%COMP%]{color:gray;font-size:12px}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%]{height:100%;width:100%;flex-direction:row}@media screen and (max-width: 1279px){[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%]{flex-direction:column}}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] > section[_ngcontent-%COMP%]:not(.table-section){max-width:unset}@media screen and (min-width: 1280px){[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] > section[_ngcontent-%COMP%]:not(.table-section){max-width:50%}}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .table-section[_ngcontent-%COMP%]{min-height:35vh;overflow:hidden}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .table-section[_ngcontent-%COMP%] .table-container[_ngcontent-%COMP%]{overflow:auto}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .flex[_ngcontent-%COMP%]{flex:1}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .input-container[_ngcontent-%COMP%]{height:auto}[_nghost-%COMP%] .connector-container[_ngcontent-%COMP%] .section-container[_ngcontent-%COMP%]{background-color:#fff}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{background:transparent;color:#000000de!important}[_nghost-%COMP%] .mat-mdc-slide-toggle[_ngcontent-%COMP%]{margin:0 8px}[_nghost-%COMP%] .status[_ngcontent-%COMP%]{text-align:center;border-radius:16px;font-weight:500;width:fit-content;padding:5px 15px}[_nghost-%COMP%] .status-sync[_ngcontent-%COMP%]{background:#1980380f;color:#198038}[_nghost-%COMP%] .status-unsync[_ngcontent-%COMP%]{background:#cb25300f;color:#cb2530}[_nghost-%COMP%] mat-row[_ngcontent-%COMP%]{cursor:pointer}[_nghost-%COMP%] .dot[_ngcontent-%COMP%]{height:12px;width:12px;background-color:#bbb;border-radius:50%;display:inline-block}[_nghost-%COMP%] .hasErrors[_ngcontent-%COMP%]{background-color:#cb2530}[_nghost-%COMP%] .noErrors[_ngcontent-%COMP%]{background-color:#198038}[_nghost-%COMP%] .connector-container .mat-mdc-tab-group, [_nghost-%COMP%] .connector-container .mat-mdc-tab-body-wrapper{height:100%}[_nghost-%COMP%] .connector-container .mat-mdc-tab-body.mat-mdc-tab-body-active{position:absolute}[_nghost-%COMP%] .connector-container .tb-form-row .fixed-title-width{min-width:120px;width:30%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[_nghost-%COMP%] .connector-container .tb-add-new{display:flex;z-index:999;pointer-events:none;background-color:#fff}[_nghost-%COMP%] .connector-container .tb-add-new button.connector{height:auto;padding-right:12px;font-size:20px;border-style:dashed;border-width:2px;border-radius:8px;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;color:#00000061}@media screen and (min-width: 960px){[_nghost-%COMP%] .configuration-json .ace_tooltip{transform:translate(-250px,-120px)}}']})}}e("GatewayConnectorComponent",L3);class V3{constructor(e){this.deviceService=e}download(e){e&&e.stopPropagation(),this.deviceId&&this.deviceService.downloadGatewayDockerComposeFile(this.deviceId).subscribe((()=>{}))}static{this.ɵfac=function(e){return new(e||V3)(t.ɵɵdirectiveInject(_e.DeviceService))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:V3,selectors:[["tb-gateway-command"]],inputs:{deviceId:"deviceId"},standalone:!0,features:[t.ɵɵStandaloneFeature],decls:32,vars:9,consts:[["mat-dialog-content","",1,"tb-form-panel","no-border",2,"padding","16px 16px 8px"],[1,"tb-no-data-text"],[1,"tb-form-panel","stroked"],["translate","",1,"tb-form-panel-title"],[1,"tb-form-row","no-border","no-padding","space-between"],["translate","",1,"tb-no-data-text","tb-commands-hint"],["mat-stroked-button","","color","primary","href","https://docs.docker.com/compose/install/","target","_blank"],["mat-stroked-button","","color","primary",3,"click"],["usePlainMarkdown","","containerClass","start-code","data","\n ```bash\n docker compose up\n {:copy-code}\n ```\n "]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2),t.ɵɵpipe(3,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",2)(5,"div",3),t.ɵɵtext(6,"device.connectivity.install-necessary-client-tools"),t.ɵɵelementEnd(),t.ɵɵelementStart(7,"div",4)(8,"div",5),t.ɵɵtext(9,"gateway.install-docker-compose"),t.ɵɵelementEnd(),t.ɵɵelementStart(10,"a",6)(11,"mat-icon"),t.ɵɵtext(12,"description"),t.ɵɵelementEnd(),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(15,"div",2)(16,"div",3),t.ɵɵtext(17,"gateway.download-configuration-file"),t.ɵɵelementEnd(),t.ɵɵelementStart(18,"div",4)(19,"div",5),t.ɵɵtext(20,"gateway.download-docker-compose"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"button",7),t.ɵɵlistener("click",(function(e){return n.download(e)})),t.ɵɵelementStart(22,"mat-icon"),t.ɵɵtext(23,"download"),t.ɵɵelementEnd(),t.ɵɵtext(24),t.ɵɵpipe(25,"translate"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(26,"div",2)(27,"div",3),t.ɵɵtext(28,"gateway.launch-gateway"),t.ɵɵelementEnd(),t.ɵɵelementStart(29,"div",5),t.ɵɵtext(30,"gateway.launch-docker-compose"),t.ɵɵelementEnd(),t.ɵɵelement(31,"tb-markdown",8),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(3,3,"gateway.docker-label")),t.ɵɵadvance(11),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,5,"common.documentation")," "),t.ɵɵadvance(11),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(25,7,"action.download")," "))},dependencies:t.ɵɵgetComponentDepsFactory(V3,[j,_]),styles:['@charset "UTF-8";[_nghost-%COMP%] .tb-commands-hint[_ngcontent-%COMP%]{color:inherit;font-weight:400;flex:1}[_nghost-%COMP%] .tb-markdown-view .start-code .code-wrapper{padding:0}[_nghost-%COMP%] .tb-markdown-view .start-code .code-wrapper pre[class*=language-]{margin:0;background:#f3f6fa;border-color:#305680;padding-right:38px;overflow:scroll;padding-bottom:4px;min-height:42px;scrollbar-width:thin}[_nghost-%COMP%] .tb-markdown-view .start-code .code-wrapper pre[class*=language-]::-webkit-scrollbar{width:4px;height:4px}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn{right:-2px}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn p{color:#305680}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn p, [_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn div{background-color:#f3f6fa}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn div img{display:none}[_nghost-%COMP%] .tb-markdown-view .start-code button.clipboard-btn div:after{content:"";position:initial;display:block;width:18px;height:18px;background:#305680;mask-image:url(/assets/copy-code-icon.svg);-webkit-mask-image:url(/assets/copy-code-icon.svg);mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}']})}}var q3,G3,z3;e("DeviceGatewayCommandComponent",V3),e("GatewayBasicConfigTab",q3),function(e){e[e.general=0]="general",e[e.logs=1]="logs",e[e.storage=2]="storage",e[e.grpc=3]="grpc",e[e.statistics=4]="statistics",e[e.other=5]="other"}(q3||e("GatewayBasicConfigTab",q3={})),e("StorageTypes",G3),function(e){e.MEMORY="memory",e.FILE="file",e.SQLITE="sqlite"}(G3||e("StorageTypes",G3={})),e("LocalLogsConfigs",z3),function(e){e.service="service",e.connector="connector",e.converter="converter",e.tb_connection="tb_connection",e.storage="storage",e.extension="extension"}(z3||e("LocalLogsConfigs",z3={}));const U3=e("LocalLogsConfigTranslateMap",new Map([[z3.service,"Service"],[z3.connector,"Connector"],[z3.converter,"Converter"],[z3.tb_connection,"TB Connection"],[z3.storage,"Storage"],[z3.extension,"Extension"]])),j3=e("StorageTypesTranslationMap",new Map([[G3.MEMORY,"gateway.storage-types.memory-storage"],[G3.FILE,"gateway.storage-types.file-storage"],[G3.SQLITE,"gateway.storage-types.sqlite"]]));var H3;e("LogSavingPeriod",H3),function(e){e.days="D",e.hours="H",e.minutes="M",e.seconds="S"}(H3||e("LogSavingPeriod",H3={}));const W3=e("LogSavingPeriodTranslations",new Map([[H3.days,"gateway.logs.days"],[H3.hours,"gateway.logs.hours"],[H3.minutes,"gateway.logs.minutes"],[H3.seconds,"gateway.logs.seconds"]]));var $3;e("SecurityTypes",$3),function(e){e.ACCESS_TOKEN="accessToken",e.USERNAME_PASSWORD="usernamePassword",e.TLS_ACCESS_TOKEN="tlsAccessToken",e.TLS_PRIVATE_KEY="tlsPrivateKey"}($3||e("SecurityTypes",$3={}));const K3=e("SecurityTypesTranslationsMap",new Map([[$3.ACCESS_TOKEN,"gateway.security-types.access-token"],[$3.USERNAME_PASSWORD,"gateway.security-types.username-password"],[$3.TLS_ACCESS_TOKEN,"gateway.security-types.tls-access-token"]])),Y3=e("logsHandlerClass","thingsboard_gateway.tb_utility.tb_rotating_file_handler.TimedRotatingFileHandler"),X3=e("logsLegacyHandlerClass","thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler");function Z3(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",10),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,i.storageTypesTranslationMap.get(e))," ")}}function Q3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-read-record-count-required")," "))}function J3(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-read-record-count-min")," "))}function e4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-read-record-count-pattern")," "))}function t4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-required")," "))}function n4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-min")," "))}function i4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-pattern")," "))}function a4(e,n){if(1&e&&(t.ɵɵelementStart(0,"section",11)(1,"mat-form-field",12)(2,"mat-label",13),t.ɵɵtext(3,"gateway.storage-read-record-count"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",14),t.ɵɵtemplate(5,Q3,3,3,"mat-error",15)(6,J3,3,3,"mat-error",15)(7,e4,3,3,"mat-error",15),t.ɵɵelementStart(8,"mat-icon",16),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",12)(12,"mat-label",13),t.ɵɵtext(13,"gateway.storage-max-records"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",17),t.ɵɵtemplate(15,t4,3,3,"mat-error",15)(16,n4,3,3,"mat-error",15)(17,i4,3,3,"mat-error",15),t.ɵɵelementStart(18,"mat-icon",16),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"info_outlined "),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(5),t.ɵɵproperty("ngIf",e.storageFormGroup.get("read_records_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("read_records_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("read_records_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,8,"gateway.hints.read-record-count")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,10,"gateway.hints.max-records-count"))}}function r4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-data-folder-path-required")," "))}function o4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-files-required")," "))}function s4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-files-min")," "))}function l4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-files-pattern")," "))}function p4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-required")," "))}function c4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-min")," "))}function d4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-read-record-count-pattern")," "))}function u4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-required")," "))}function m4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-min")," "))}function h4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-pattern")," "))}function g4(e,n){if(1&e&&(t.ɵɵelementStart(0,"section")(1,"div",11)(2,"mat-form-field",12)(3,"mat-label",13),t.ɵɵtext(4,"gateway.storage-data-folder-path"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",18),t.ɵɵtemplate(6,r4,3,3,"mat-error",15),t.ɵɵelementStart(7,"mat-icon",19),t.ɵɵpipe(8,"translate"),t.ɵɵtext(9,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"mat-form-field",12)(11,"mat-label",13),t.ɵɵtext(12,"gateway.storage-max-files"),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",20),t.ɵɵtemplate(14,o4,3,3,"mat-error",15)(15,s4,3,3,"mat-error",15)(16,l4,3,3,"mat-error",15),t.ɵɵelementStart(17,"mat-icon",16),t.ɵɵpipe(18,"translate"),t.ɵɵtext(19,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",11)(21,"mat-form-field",12)(22,"mat-label",13),t.ɵɵtext(23,"gateway.storage-max-read-record-count"),t.ɵɵelementEnd(),t.ɵɵelement(24,"input",21),t.ɵɵtemplate(25,p4,3,3,"mat-error",15)(26,c4,3,3,"mat-error",15)(27,d4,3,3,"mat-error",15),t.ɵɵelementStart(28,"mat-icon",16),t.ɵɵpipe(29,"translate"),t.ɵɵtext(30,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(31,"mat-form-field",12)(32,"mat-label",13),t.ɵɵtext(33,"gateway.storage-max-records"),t.ɵɵelementEnd(),t.ɵɵelement(34,"input",22),t.ɵɵtemplate(35,u4,3,3,"mat-error",15)(36,m4,3,3,"mat-error",15)(37,h4,3,3,"mat-error",15),t.ɵɵelementStart(38,"mat-icon",16),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40,"info_outlined "),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵproperty("ngIf",e.storageFormGroup.get("data_folder_path").hasError("required")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,14,"gateway.hints.data-folder")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_file_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_file_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_file_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,16,"gateway.hints.max-file-count")),t.ɵɵadvance(8),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_read_records_count").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(29,18,"gateway.hints.max-read-count")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(39,20,"gateway.hints.max-records"))}}function f4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-path-required")," "))}function y4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-check-in-hours-required")," "))}function v4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-check-in-hours-min")," "))}function x4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-check-in-hours-pattern")," "))}function b4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-in-days-required")," "))}function w4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-in-days-min")," "))}function S4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.messages-ttl-in-days-pattern")," "))}function C4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-required")," "))}function _4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-min")," "))}function T4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.storage-max-records-pattern")," "))}function I4(e,n){if(1&e&&(t.ɵɵelementStart(0,"section")(1,"div",11)(2,"mat-form-field",12)(3,"mat-label",13),t.ɵɵtext(4,"gateway.storage-path"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",23),t.ɵɵtemplate(6,f4,3,3,"mat-error",15),t.ɵɵelementStart(7,"mat-icon",16),t.ɵɵpipe(8,"translate"),t.ɵɵtext(9,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"mat-form-field",12)(11,"mat-label",13),t.ɵɵtext(12,"gateway.messages-ttl-check-in-hours"),t.ɵɵelementEnd(),t.ɵɵelement(13,"input",24),t.ɵɵtemplate(14,y4,3,3,"mat-error",15)(15,v4,3,3,"mat-error",15)(16,x4,3,3,"mat-error",15),t.ɵɵelementStart(17,"mat-icon",16),t.ɵɵpipe(18,"translate"),t.ɵɵtext(19,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(20,"div",11)(21,"mat-form-field",12)(22,"mat-label",13),t.ɵɵtext(23,"gateway.messages-ttl-in-days"),t.ɵɵelementEnd(),t.ɵɵelement(24,"input",25),t.ɵɵtemplate(25,b4,3,3,"mat-error",15)(26,w4,3,3,"mat-error",15)(27,S4,3,3,"mat-error",15),t.ɵɵelementStart(28,"mat-icon",26),t.ɵɵpipe(29,"translate"),t.ɵɵtext(30,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(31,"mat-form-field",12)(32,"mat-label",13),t.ɵɵtext(33,"gateway.storage-max-records"),t.ɵɵelementEnd(),t.ɵɵelement(34,"input",22),t.ɵɵtemplate(35,C4,3,3,"mat-error",15)(36,_4,3,3,"mat-error",15)(37,T4,3,3,"mat-error",15),t.ɵɵelementStart(38,"mat-icon",26),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40,"info_outlined "),t.ɵɵelementEnd()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(6),t.ɵɵproperty("ngIf",e.storageFormGroup.get("data_file_path").hasError("required")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,14,"gateway.hints.data-folder")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_check_in_hours").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_check_in_hours").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_check_in_hours").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,16,"gateway.hints.ttl-check-hour")),t.ɵɵadvance(8),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_in_days").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_in_days").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("messages_ttl_in_days").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(29,18,"gateway.hints.ttl-messages-day")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",e.storageFormGroup.get("max_records_per_file").hasError("pattern")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(39,20,"gateway.hints.max-records"))}}class E4{constructor(e){this.fb=e,this.initialized=new u,this.StorageTypes=G3,this.storageTypes=Object.values(G3),this.storageTypesTranslationMap=j3,this.onChange=()=>{},this.storageFormGroup=this.initStorageFormGroup(),this.observeStorageTypeChanges(),this.storageFormGroup.valueChanges.pipe(bn()).subscribe((e=>{this.onChange(e)}))}ngAfterViewInit(){this.initialized.emit({storage:this.storageFormGroup.value})}writeValue(e){this.storageFormGroup.patchValue(e,{emitEvent:!1})}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.storageFormGroup.valid?null:{storageFormGroup:{valid:!1}}}removeAllStorageValidators(){for(const e in this.storageFormGroup.controls)"type"!==e&&(this.storageFormGroup.controls[e].clearValidators(),this.storageFormGroup.controls[e].setErrors(null),this.storageFormGroup.controls[e].updateValueAndValidity())}initStorageFormGroup(){return this.fb.group({type:[G3.MEMORY,[$.required]],read_records_count:[100,[$.required,$.min(1),$.pattern(nn)]],max_records_count:[1e5,[$.required,$.min(1),$.pattern(nn)]],data_folder_path:["./data/",[$.required]],max_file_count:[10,[$.min(1),$.pattern(nn)]],max_read_records_count:[10,[$.min(1),$.pattern(nn)]],max_records_per_file:[1e4,[$.min(1),$.pattern(nn)]],data_file_path:["./data/data.db",[$.required]],messages_ttl_check_in_hours:[1,[$.min(1),$.pattern(nn)]],messages_ttl_in_days:[7,[$.min(1),$.pattern(nn)]]})}observeStorageTypeChanges(){this.storageFormGroup.get("type").valueChanges.pipe(bn()).subscribe((e=>{switch(this.removeAllStorageValidators(),e){case G3.MEMORY:this.addMemoryStorageValidators(this.storageFormGroup);break;case G3.FILE:this.addFileStorageValidators(this.storageFormGroup);break;case G3.SQLITE:this.addSqliteStorageValidators(this.storageFormGroup)}}))}addMemoryStorageValidators(e){e.get("read_records_count").addValidators([$.required,$.min(1),$.pattern(nn)]),e.get("max_records_count").addValidators([$.required,$.min(1),$.pattern(nn)]),e.get("read_records_count").updateValueAndValidity({emitEvent:!1}),e.get("max_records_count").updateValueAndValidity({emitEvent:!1})}addFileStorageValidators(e){["max_file_count","max_read_records_count","max_records_per_file"].forEach((t=>{e.get(t).addValidators([$.required,$.min(1),$.pattern(nn)]),e.get(t).updateValueAndValidity({emitEvent:!1})}))}addSqliteStorageValidators(e){["messages_ttl_check_in_hours","messages_ttl_in_days","max_records_per_file"].forEach((t=>{e.get(t).addValidators([$.required,$.min(1),$.pattern(nn)]),e.get(t).updateValueAndValidity({emitEvent:!1})}))}static{this.ɵfac=function(e){return new(e||E4)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:E4,selectors:[["tb-gateway-storage-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>E4)),multi:!0},{provide:K,useExisting:c((()=>E4)),multi:!0}]),t.ɵɵStandaloneFeature],decls:15,vars:9,consts:[[1,"mat-content","mat-padding","flex","w-full","flex-col","gap-2",3,"formGroup"],[1,"tb-form-panel","no-padding-bottom","w-full"],["translate","",1,"tb-form-panel-title"],["translate","",1,"tb-form-panel-hint"],["formControlName","type",1,"flex"],[3,"value",4,"ngFor","ngForOf"],[1,"tb-form-panel-hint"],[3,"ngSwitch"],["class","tb-form-row no-border no-padding tb-standard-fields column-xs",4,"ngSwitchCase"],[4,"ngSwitchCase"],[3,"value"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["translate",""],["type","number","matInput","","formControlName","read_records_count"],[4,"ngIf"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["type","number","matInput","","formControlName","max_records_count"],["matInput","","formControlName","data_folder_path"],["aria-hidden","false","aria-label","help-icon","matSuffix","",1,"mat-form-field-infix","pointer-event","suffix-icon",2,"cursor","pointer",3,"matTooltip"],["matInput","","type","number","formControlName","max_file_count"],["matInput","","type","number","formControlName","max_read_records_count"],["matInput","","type","number","formControlName","max_records_per_file"],["matInput","","formControlName","data_file_path"],["matInput","","type","number","formControlName","messages_ttl_check_in_hours"],["matInput","","type","number","formControlName","messages_ttl_in_days"],["matIconSuffix","",1,"cursor-pointer",3,"matTooltip"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2),t.ɵɵtext(3,"gateway.storage"),t.ɵɵelementEnd(),t.ɵɵelementStart(4,"div",3),t.ɵɵtext(5,"gateway.hints.storage"),t.ɵɵelementEnd(),t.ɵɵelementStart(6,"tb-toggle-select",4),t.ɵɵtemplate(7,Z3,3,4,"tb-toggle-option",5),t.ɵɵelementEnd(),t.ɵɵelementStart(8,"div",6),t.ɵɵtext(9),t.ɵɵpipe(10,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(11,7),t.ɵɵtemplate(12,a4,21,12,"section",8)(13,g4,41,22,"section",9)(14,I4,41,22,"section",9),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.storageFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngForOf",n.storageTypes),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(10,7,"gateway.hints."+n.storageFormGroup.get("type").value)),t.ɵɵadvance(2),t.ɵɵproperty("ngSwitch",n.storageFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.StorageTypes.MEMORY),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.StorageTypes.FILE),t.ɵɵadvance(),t.ɵɵproperty("ngSwitchCase",n.StorageTypes.SQLITE))},dependencies:t.ɵɵgetComponentDepsFactory(E4,[j,_]),encapsulation:2})}}function M4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-required")," "))}function k4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-min")," "))}function P4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-max")," "))}function D4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-pattern")," "))}function O4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-timeout-required")," "))}function A4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-timeout-min")," "))}function F4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-timeout-pattern")," "))}function R4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-required")," "))}function B4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-min")," "))}function N4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-keep-alive-pattern")," "))}function L4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-time-between-pings-required")," "))}function V4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-time-between-pings-min")," "))}function q4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-time-between-pings-pattern")," "))}function G4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-max-pings-without-data-required")," "))}function z4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-max-pings-without-data-min")," "))}function U4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-max-pings-without-data-pattern")," "))}function j4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-ping-interval-without-data-required")," "))}function H4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-ping-interval-without-data-min")," "))}function W4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.grpc-min-ping-interval-without-data-pattern")," "))}class $4{constructor(e){this.fb=e,this.initialized=new u,this.onChange=()=>{},this.grpcFormGroup=this.initGrpcFormGroup(),this.grpcFormGroup.valueChanges.pipe(bn()).subscribe((e=>{this.onChange(e)})),this.grpcFormGroup.get("enabled").valueChanges.pipe(bn()).subscribe((e=>{this.toggleRpcFields(e)}))}ngAfterViewInit(){this.initialized.emit({grpc:this.grpcFormGroup.value})}writeValue(e){e&&this.toggleRpcFields(e.enabled),this.grpcFormGroup.patchValue(e,{emitEvent:!1})}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.grpcFormGroup.valid?null:{grpcFormGroup:{valid:!1}}}toggleRpcFields(e){const t=this.grpcFormGroup;e?(t.get("serverPort").enable({emitEvent:!1}),t.get("keepAliveTimeMs").enable({emitEvent:!1}),t.get("keepAliveTimeoutMs").enable({emitEvent:!1}),t.get("keepalivePermitWithoutCalls").enable({emitEvent:!1}),t.get("maxPingsWithoutData").enable({emitEvent:!1}),t.get("minTimeBetweenPingsMs").enable({emitEvent:!1}),t.get("minPingIntervalWithoutDataMs").enable({emitEvent:!1})):(t.get("serverPort").disable({emitEvent:!1}),t.get("keepAliveTimeMs").disable({emitEvent:!1}),t.get("keepAliveTimeoutMs").disable({emitEvent:!1}),t.get("keepalivePermitWithoutCalls").disable({emitEvent:!1}),t.get("maxPingsWithoutData").disable({emitEvent:!1}),t.get("minTimeBetweenPingsMs").disable({emitEvent:!1}),t.get("minPingIntervalWithoutDataMs").disable({emitEvent:!1}))}initGrpcFormGroup(){return this.fb.group({enabled:[!1],serverPort:[9595,[$.required,$.min(1),$.max(65535),$.pattern(nn)]],keepAliveTimeMs:[1e4,[$.required,$.min(1),$.pattern(nn)]],keepAliveTimeoutMs:[5e3,[$.required,$.min(1),$.pattern(nn)]],keepalivePermitWithoutCalls:[!0],maxPingsWithoutData:[0,[$.required,$.min(0),$.pattern(nn)]],minTimeBetweenPingsMs:[1e4,[$.required,$.min(1),$.pattern(nn)]],minPingIntervalWithoutDataMs:[5e3,[$.required,$.min(1),$.pattern(nn)]]})}static{this.ɵfac=function(e){return new(e||$4)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:$4,selectors:[["tb-gateway-grpc-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>$4)),multi:!0},{provide:K,useExisting:c((()=>$4)),multi:!0}]),t.ɵɵStandaloneFeature],decls:75,vars:47,consts:[[1,"mat-content","mat-padding","flex","flex-col","gap-2",3,"formGroup"],[1,"tb-form-panel","no-padding-bottom","w-full"],["color","primary","formControlName","enabled",1,"mat-slide"],[1,"tb-form-row","no-border","no-padding",3,"tb-hint-tooltip-icon"],["color","primary","formControlName","keepalivePermitWithoutCalls",1,"mat-slide"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["translate",""],["matInput","","formControlName","serverPort","type","number","min","0"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],[4,"ngIf"],["matInput","","formControlName","keepAliveTimeoutMs","type","number","min","0"],["matInput","","formControlName","keepAliveTimeMs","type","number","min","0"],["matInput","","formControlName","minTimeBetweenPingsMs","type","number","min","0"],["matInput","","formControlName","maxPingsWithoutData","type","number","min","0"],["matInput","","formControlName","minPingIntervalWithoutDataMs","type","number","min","0"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"mat-slide-toggle",2),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"div",3),t.ɵɵpipe(6,"translate"),t.ɵɵelementStart(7,"mat-slide-toggle",4),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(10,"section")(11,"section",5)(12,"mat-form-field",6)(13,"mat-label",7),t.ɵɵtext(14,"gateway.server-port"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",8),t.ɵɵelementStart(16,"mat-icon",9),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(19,M4,3,3,"mat-error",10)(20,k4,3,3,"mat-error",10)(21,P4,3,3,"mat-error",10)(22,D4,3,3,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(23,"mat-form-field",6)(24,"mat-label",7),t.ɵɵtext(25,"gateway.grpc-keep-alive-timeout"),t.ɵɵelementEnd(),t.ɵɵelement(26,"input",11),t.ɵɵelementStart(27,"mat-icon",9),t.ɵɵpipe(28,"translate"),t.ɵɵtext(29,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(30,O4,3,3,"mat-error",10)(31,A4,3,3,"mat-error",10)(32,F4,3,3,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(33,"section",5)(34,"mat-form-field",6)(35,"mat-label",7),t.ɵɵtext(36,"gateway.grpc-keep-alive"),t.ɵɵelementEnd(),t.ɵɵelement(37,"input",12),t.ɵɵelementStart(38,"mat-icon",9),t.ɵɵpipe(39,"translate"),t.ɵɵtext(40,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(41,R4,3,3,"mat-error",10)(42,B4,3,3,"mat-error",10)(43,N4,3,3,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"mat-form-field",6)(45,"mat-label",7),t.ɵɵtext(46,"gateway.grpc-min-time-between-pings"),t.ɵɵelementEnd(),t.ɵɵelement(47,"input",13),t.ɵɵelementStart(48,"mat-icon",9),t.ɵɵpipe(49,"translate"),t.ɵɵtext(50,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(51,L4,3,3,"mat-error",10)(52,V4,3,3,"mat-error",10)(53,q4,3,3,"mat-error",10),t.ɵɵelementEnd()(),t.ɵɵelementStart(54,"section",5)(55,"mat-form-field",6)(56,"mat-label",7),t.ɵɵtext(57,"gateway.grpc-max-pings-without-data"),t.ɵɵelementEnd(),t.ɵɵelement(58,"input",14),t.ɵɵelementStart(59,"mat-icon",9),t.ɵɵpipe(60,"translate"),t.ɵɵtext(61,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(62,G4,3,3,"mat-error",10)(63,z4,3,3,"mat-error",10)(64,U4,3,3,"mat-error",10),t.ɵɵelementEnd(),t.ɵɵelementStart(65,"mat-form-field",6)(66,"mat-label",7),t.ɵɵtext(67,"gateway.grpc-min-ping-interval-without-data"),t.ɵɵelementEnd(),t.ɵɵelement(68,"input",15),t.ɵɵelementStart(69,"mat-icon",9),t.ɵɵpipe(70,"translate"),t.ɵɵtext(71,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(72,j4,3,3,"mat-error",10)(73,H4,3,3,"mat-error",10)(74,W4,3,3,"mat-error",10),t.ɵɵelementEnd()()()()()),2&e&&(t.ɵɵproperty("formGroup",n.grpcFormGroup),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,29,"gateway.grpc")," "),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(6,31,"gateway.hints.permit-without-calls")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,33,"gateway.permit-without-calls")," "),t.ɵɵadvance(8),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,35,"gateway.hints.server-port")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("max")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("serverPort").hasError("pattern")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(28,37,"gateway.hints.grpc-keep-alive-timeout")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeoutMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeoutMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeoutMs").hasError("pattern")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(39,39,"gateway.hints.grpc-keep-alive")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("keepAliveTimeMs").hasError("pattern")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(49,41,"gateway.hints.grpc-min-time-between-pings")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minTimeBetweenPingsMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minTimeBetweenPingsMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minTimeBetweenPingsMs").hasError("pattern")),t.ɵɵadvance(6),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(60,43,"gateway.hints.grpc-max-pings-without-data")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("maxPingsWithoutData").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("maxPingsWithoutData").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("maxPingsWithoutData").hasError("pattern")),t.ɵɵadvance(5),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(70,45,"gateway.hints.grpc-min-ping-interval-without-data")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minPingIntervalWithoutDataMs").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minPingIntervalWithoutDataMs").hasError("min")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.grpcFormGroup.get("minPingIntervalWithoutDataMs").hasError("pattern")))},dependencies:t.ɵɵgetComponentDepsFactory($4,[j,_]),encapsulation:2})}}function K4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.date-format-required")," "))}function Y4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.log-format-required")," "))}function X4(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",29),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function Z4(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",30),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit,i=t.ɵɵnextContext();t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(i.localLogsConfigTranslateMap.get(e))}}function Q4(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",29),t.ɵɵtext(1),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e),t.ɵɵadvance(),t.ɵɵtextInterpolate(e)}}function J4(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.file-path-required")," "))}function e5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.saving-period-required")," "))}function t5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.saving-period-min")," "))}function n5(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-option",29),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,2,e.value)," ")}}function i5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.backup-count-required")," "))}function a5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.logs.backup-count-min")," "))}class r5{constructor(e){this.fb=e,this.initialized=new u,this.logSavingPeriods=W3,this.localLogsConfigs=Object.keys(z3),this.localLogsConfigTranslateMap=U3,this.gatewayLogLevel=Object.values(lt),this.remoteLogLevel=Object.values(lt).filter((e=>e!==lt.NONE)),this.onChange=()=>{},this.logsFormGroup=this.initLogsFormGroup(),this.showRemoteLogsControl=this.fb.control(!1),this.logsFormGroup.valueChanges.pipe(bn()).subscribe((e=>{this.onChange(e)})),this.logSelector=this.fb.control(z3.service);for(const e of Object.keys(z3))this.addLocalLogConfig(e,{});this.showRemoteLogsControl.valueChanges.pipe(bn()).subscribe((e=>this.logsFormGroup.get("logLevel")[e?"enable":"disable"]()))}ngAfterViewInit(){this.initialized.emit({logs:this.logsFormGroup.value})}writeValue(e){this.logsFormGroup.patchValue(e,{emitEvent:!1}),this.updateRemoteLogs(e?.logLevel??lt.NONE)}registerOnChange(e){this.onChange=e}registerOnTouched(e){}getLogFormGroup(e){return this.logsFormGroup.get(`local.${e}`)}validate(){return this.logsFormGroup.valid?null:{logsFormGroup:{valid:!1}}}initLogsFormGroup(){return this.fb.group({dateFormat:["%Y-%m-%d %H:%M:%S",[$.required,$.pattern(/^[^\s].*[^\s]$/)]],logFormat:["%(asctime)s.%(msecs)03d - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s",[$.required,$.pattern(/^[^\s].*[^\s]$/)]],type:["remote",[$.required]],logLevel:[{value:lt.INFO,disabled:!0}],local:this.fb.group({})})}addLocalLogConfig(e,t){const n=this.logsFormGroup.get("local"),i=this.fb.group({logLevel:[t.logLevel||lt.INFO,[$.required]],filePath:[t.filePath||"./logs",[$.required]],backupCount:[t.backupCount||7,[$.required,$.min(0)]],savingTime:[t.savingTime||3,[$.required,$.min(0)]],savingPeriod:[t.savingPeriod||H3.days,[$.required]]});n.addControl(e,i,{emitEvent:!1})}updateRemoteLogs(e){const t=e&&e!==lt.NONE;this.showRemoteLogsControl.patchValue(t,{emitEvent:!1}),this.logsFormGroup.get("logLevel")[t?"enable":"disable"]({emitEvent:!1}),this.logsFormGroup.get("logLevel").patchValue(e===lt.NONE?lt.INFO:e,{emitEvent:!1})}static{this.ɵfac=function(e){return new(e||r5)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:r5,selectors:[["tb-gateway-logs-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>r5)),multi:!0},{provide:K,useExisting:c((()=>r5)),multi:!0}]),t.ɵɵStandaloneFeature],decls:72,vars:33,consts:[[1,"mat-content","mat-padding","flex","flex-col","gap-2",3,"formGroup"],[1,"tb-form-panel","no-padding-bottom"],[1,"flex","flex-col"],["appearance","outline"],["translate",""],["matInput","","formControlName","dateFormat"],[4,"ngIf"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["matInput","","formControlName","logFormat","rows","2"],[1,"tb-form-panel"],[1,"tb-settings",3,"expanded"],[1,"flex-wrap"],[1,"mat-slide",3,"click","formControl"],[3,"tb-hint-tooltip-icon"],["formControlName","logLevel"],[3,"value",4,"ngFor","ngForOf"],["formGroupName","local",1,"tb-form-panel","no-padding-bottom"],["translate","",1,"tb-form-panel-title"],[1,"toggle-group",3,"formControl"],["class","first-capital",3,"value",4,"ngFor","ngForOf"],[3,"formGroup"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["matInput","","formControlName","filePath"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","saving-period"],["matInput","","formControlName","savingTime","type","number","min","0"],["appearance","outline","hideRequiredMarker","",2,"min-width","110px","width","30%"],["formControlName","savingPeriod"],["matInput","","formControlName","backupCount","type","number","min","0"],[3,"value"],[1,"first-capital",3,"value"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1)(2,"div",2)(3,"mat-form-field",3)(4,"mat-label",4),t.ɵɵtext(5,"gateway.logs.date-format"),t.ɵɵelementEnd(),t.ɵɵelement(6,"input",5),t.ɵɵtemplate(7,K4,3,3,"mat-error",6),t.ɵɵelementStart(8,"mat-icon",7),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",3)(12,"mat-label",4),t.ɵɵtext(13,"gateway.logs.log-format"),t.ɵɵelementEnd(),t.ɵɵelement(14,"textarea",8),t.ɵɵtemplate(15,Y4,3,3,"mat-error",6),t.ɵɵelementStart(16,"mat-icon",7),t.ɵɵpipe(17,"translate"),t.ɵɵtext(18,"info_outlined "),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(19,"div",9)(20,"mat-expansion-panel",10)(21,"mat-expansion-panel-header",11)(22,"mat-panel-title")(23,"mat-slide-toggle",12),t.ɵɵlistener("click",(function(e){return e.stopPropagation()})),t.ɵɵelementStart(24,"mat-label")(25,"div",13),t.ɵɵpipe(26,"translate"),t.ɵɵtext(27),t.ɵɵpipe(28,"translate"),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(29,"mat-form-field",3)(30,"mat-label",4),t.ɵɵtext(31,"gateway.logs.level"),t.ɵɵelementEnd(),t.ɵɵelementStart(32,"mat-select",14),t.ɵɵtemplate(33,X4,2,2,"mat-option",15),t.ɵɵelementEnd()()()(),t.ɵɵelementStart(34,"div",16)(35,"div",17),t.ɵɵtext(36,"gateway.logs.local"),t.ɵɵelementEnd(),t.ɵɵelementStart(37,"tb-toggle-select",18),t.ɵɵtemplate(38,Z4,2,2,"tb-toggle-option",19),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(39,20),t.ɵɵelementStart(40,"div",21)(41,"mat-form-field",22)(42,"mat-label",4),t.ɵɵtext(43,"gateway.logs.level"),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"mat-select",14),t.ɵɵtemplate(45,Q4,2,2,"mat-option",15),t.ɵɵelementEnd()(),t.ɵɵelementStart(46,"mat-form-field",22)(47,"mat-label",4),t.ɵɵtext(48,"gateway.logs.file-path"),t.ɵɵelementEnd(),t.ɵɵelement(49,"input",23),t.ɵɵtemplate(50,J4,3,3,"mat-error",6),t.ɵɵelementEnd()(),t.ɵɵelementStart(51,"div",21)(52,"div",24)(53,"mat-form-field",22)(54,"mat-label",4),t.ɵɵtext(55,"gateway.logs.saving-period"),t.ɵɵelementEnd(),t.ɵɵelement(56,"input",25),t.ɵɵtemplate(57,e5,3,3,"mat-error",6)(58,t5,3,3,"mat-error",6),t.ɵɵelementEnd(),t.ɵɵelementStart(59,"mat-form-field",26)(60,"mat-select",27),t.ɵɵtemplate(61,n5,3,4,"mat-option",15),t.ɵɵpipe(62,"keyvalue"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(63,"mat-form-field",22)(64,"mat-label",4),t.ɵɵtext(65,"gateway.logs.backup-count"),t.ɵɵelementEnd(),t.ɵɵelement(66,"input",28),t.ɵɵtemplate(67,i5,3,3,"mat-error",6)(68,a5,3,3,"mat-error",6),t.ɵɵelementStart(69,"mat-icon",7),t.ɵɵpipe(70,"translate"),t.ɵɵtext(71,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()),2&e&&(t.ɵɵproperty("formGroup",n.logsFormGroup),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.logsFormGroup.get("dateFormat").hasError("required")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,21,"gateway.hints.date-form")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.logsFormGroup.get("logFormat").hasError("required")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(17,23,"gateway.hints.log-format")),t.ɵɵadvance(4),t.ɵɵproperty("expanded",n.showRemoteLogsControl.value),t.ɵɵadvance(3),t.ɵɵproperty("formControl",n.showRemoteLogsControl),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(26,25,"gateway.hints.remote-log")),t.ɵɵadvance(2),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(28,27,"gateway.logs.remote")),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.remoteLogLevel),t.ɵɵadvance(4),t.ɵɵproperty("formControl",n.logSelector),t.ɵɵadvance(),t.ɵɵproperty("ngForOf",n.localLogsConfigs),t.ɵɵadvance(),t.ɵɵproperty("formGroup",n.getLogFormGroup(n.logSelector.value)),t.ɵɵadvance(6),t.ɵɵproperty("ngForOf",n.gatewayLogLevel),t.ɵɵadvance(5),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".filePath").hasError("required")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".savingTime").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".savingTime").hasError("min")),t.ɵɵadvance(3),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(62,29,n.logSavingPeriods)),t.ɵɵadvance(6),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".backupCount").hasError("required")),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.logsFormGroup.get("local."+n.logSelector.value+".backupCount").hasError("min")),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(70,31,"gateway.hints.backup-count")))},dependencies:t.ɵɵgetComponentDepsFactory(r5,[j,_]),encapsulation:2})}}function o5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"security.clientId-required")," "))}function s5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",6),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"gateway.copy-client-id")),t.ɵɵproperty("copyText",e.usernameFormGroup.get("clientId").value)}}function l5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("clientId"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-client-id"))}function p5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"security.username-required")," "))}function c5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",6),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"gateway.copy-username")),t.ɵɵproperty("copyText",e.usernameFormGroup.get("username").value)}}function d5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("username"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-user-name"))}function u5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",6),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext();t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"gateway.copy-password")),t.ɵɵproperty("copyText",e.usernameFormGroup.get("password").value)}}function m5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",13),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.generate("password"))})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-password"))}function h5(e,n){1&e&&(t.ɵɵelement(0,"tb-error",14),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("error",t.ɵɵpipeBind1(1,1,"device.client-id-or-user-name-necessary"))}function g5(e,n){1&e&&(t.ɵɵelement(0,"tb-error",14),t.ɵɵpipe(1,"translate")),2&e&&t.ɵɵproperty("error",t.ɵɵpipeBind1(1,1,"gateway.hints.username-required-with-password"))}class f5{constructor(e){this.fb=e,this.onChange=()=>{},this.initForm(),this.usernameFormGroup.valueChanges.pipe(bn()).subscribe((e=>this.onChange(e)))}writeValue(e){this.usernameFormGroup.patchValue(e,{emitEvent:!1})}registerOnChange(e){this.onChange=e}registerOnTouched(e){}setDisabledState(e){e?this.usernameFormGroup.disable({emitEvent:!1}):this.usernameFormGroup.enable({emitEvent:!1})}validate(){return this.usernameFormGroup.valid?null:{usernameFormGroup:{valid:!1}}}initForm(){this.usernameFormGroup=this.createSecurityFormGroup()}createSecurityFormGroup(){return this.fb.group({clientId:[null,[$.pattern(/^[^.\s]+$/)]],username:[null,[$.pattern(/^[^.\s]+$/)]],password:[null,[$.pattern(/^[^.\s]+$/)]]},{validators:[this.atLeastOneRequired,this.usernameRequired]})}atLeastOneRequired(e){const t=e.get("clientId").value,n=e.get("username").value;return t||n?null:{atLeastOneRequired:!0}}usernameRequired(e){const t=e.get("username").value,n=e.get("password").value;return!t&&n?{usernameRequired:!0}:null}generate(e){this.usernameFormGroup.get(e).patchValue(Re(20))}static{this.ɵfac=function(e){return new(e||f5)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:f5,selectors:[["tb-gateway-username-configuration"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>f5)),multi:!0},{provide:K,useExisting:c((()=>f5)),multi:!0}]),t.ɵɵStandaloneFeature],decls:33,vars:17,consts:[[3,"formGroup"],[1,"xs:flex-col","no-border","no-padding","tb-standard-fields","flex","gap-2"],["appearance","outline",1,"flex","flex-1"],["translate",""],["matInput","","formControlName","clientId"],[4,"ngIf"],["matSuffix","","miniButton","false","tooltipPosition","above","icon","content_copy",3,"copyText","tooltipText"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"matTooltip"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["matInput","","formControlName","username"],["appearance","outline","subscriptSizing","dynamic",2,"width","100%"],["matInput","","formControlName","password"],["class","block",3,"error",4,"ngIf"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"click","matTooltip"],[1,"block",3,"error"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"section",0)(1,"div",1)(2,"mat-form-field",2)(3,"mat-label",3),t.ɵɵtext(4,"security.clientId"),t.ɵɵelementEnd(),t.ɵɵelement(5,"input",4),t.ɵɵtemplate(6,o5,3,3,"mat-error",5)(7,s5,2,4,"tb-copy-button",6)(8,l5,4,3,"button",7),t.ɵɵelementStart(9,"mat-icon",8),t.ɵɵpipe(10,"translate"),t.ɵɵtext(11,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"mat-form-field",2)(13,"mat-label",3),t.ɵɵtext(14,"security.username"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",9),t.ɵɵtemplate(16,p5,3,3,"mat-error",5)(17,c5,2,4,"tb-copy-button",6)(18,d5,4,3,"button",7),t.ɵɵelementStart(19,"mat-icon",8),t.ɵɵpipe(20,"translate"),t.ɵɵtext(21,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(22,"mat-form-field",10)(23,"mat-label",3),t.ɵɵtext(24,"gateway.password"),t.ɵɵelementEnd(),t.ɵɵelement(25,"input",11),t.ɵɵtemplate(26,u5,2,4,"tb-copy-button",6)(27,m5,4,3,"button",7),t.ɵɵelementStart(28,"mat-icon",8),t.ɵɵpipe(29,"translate"),t.ɵɵtext(30,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵtemplate(31,h5,2,3,"tb-error",12)(32,g5,2,3,"tb-error",12)),2&e&&(t.ɵɵproperty("formGroup",n.usernameFormGroup),t.ɵɵadvance(6),t.ɵɵproperty("ngIf",n.usernameFormGroup.get("clientId").hasError("required")),t.ɵɵadvance(),t.ɵɵconditional(n.usernameFormGroup.get("clientId").value?7:8),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(10,11,"gateway.hints.client-id")),t.ɵɵadvance(7),t.ɵɵproperty("ngIf",n.usernameFormGroup.get("username").hasError("required")),t.ɵɵadvance(),t.ɵɵconditional(n.usernameFormGroup.get("username").value?17:18),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(20,13,"gateway.hints.username")),t.ɵɵadvance(7),t.ɵɵconditional(n.usernameFormGroup.get("password").value?26:27),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(29,15,"gateway.hints.password")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",n.usernameFormGroup.hasError("atLeastOneRequired")&&n.usernameFormGroup.touched),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.usernameFormGroup.hasError("usernameRequired")&&n.usernameFormGroup.touched))},dependencies:t.ɵɵgetComponentDepsFactory(f5,[j,_]),encapsulation:2})}}class y5{constructor(e,t){this.deviceService=e,this.destroyRef=t,this.initialCredentialsSubject=new ie(null)}get initialCredentials(){return this.initialCredentialsSubject.value}get initialCredentials$(){return this.initialCredentialsSubject.asObservable()}updateCredentials(e){let t={};switch(e.type){case $3.USERNAME_PASSWORD:this.shouldUpdateCredentials(e)&&(t=this.generateMqttCredentials(e));break;case $3.ACCESS_TOKEN:case $3.TLS_ACCESS_TOKEN:this.shouldUpdateAccessToken(e)&&(t={credentialsType:U.ACCESS_TOKEN,credentialsId:e.accessToken,credentialsValue:null})}return this.initialCredentialsSubject.next({...this.initialCredentials,...t}),Object.keys(t).length?this.deviceService.saveDeviceCredentials(this.initialCredentials):ae(null)}setInitialCredentials(e){this.deviceService.getDeviceCredentials(e.id).pipe(bn(this.destroyRef)).subscribe((e=>{this.initialCredentialsSubject.next({...e,version:null})}))}shouldUpdateSecurityConfig(e){switch(e.type){case $3.USERNAME_PASSWORD:return this.shouldUpdateCredentials(e);case $3.ACCESS_TOKEN:case $3.TLS_ACCESS_TOKEN:return this.shouldUpdateAccessToken(e)}}credentialsToSecurityConfig(e){const t=e.credentialsType===U.MQTT_BASIC?$3.USERNAME_PASSWORD:$3.ACCESS_TOKEN;if(e.credentialsType!==U.MQTT_BASIC)return{type:t,accessToken:e.credentialsId};if(e.credentialsValue){const{clientId:n,userName:i,password:a}=JSON.parse(e.credentialsValue);return{type:t,clientId:n,username:i,password:a}}}shouldUpdateCredentials(e){if(this.initialCredentials.credentialsType!==U.MQTT_BASIC)return!0;const t=JSON.parse(this.initialCredentials.credentialsValue);return!(t.clientId===e.clientId&&t.userName===e.username&&t.password===e.password)}shouldUpdateAccessToken(e){return this.initialCredentials.credentialsType!==U.ACCESS_TOKEN||this.initialCredentials.credentialsId!==e.accessToken}generateMqttCredentials(e){const{clientId:t,username:n,password:i}=e,a={...t&&{clientId:t},...n&&{userName:n},...i&&{password:i}};return{credentialsType:U.MQTT_BASIC,credentialsValue:JSON.stringify(a)}}static{this.ɵfac=function(e){return new(e||y5)(t.ɵɵinject(_e.DeviceService),t.ɵɵinject(t.DestroyRef))}}static{this.ɵprov=t.ɵɵdefineInjectable({token:y5,factory:y5.ɵfac})}}function v5(e,n){if(1&e&&(t.ɵɵelementStart(0,"tb-toggle-option",8),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e){const e=n.$implicit;t.ɵɵproperty("value",e.key),t.ɵɵadvance(),t.ɵɵtextInterpolate1("",t.ɵɵpipeBind1(2,2,e.value)," ")}}function x5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"security.access-token-required")," "))}function b5(e,n){if(1&e&&(t.ɵɵelement(0,"tb-copy-button",13),t.ɵɵpipe(1,"translate")),2&e){const e=t.ɵɵnextContext(2);t.ɵɵpropertyInterpolate("tooltipText",t.ɵɵpipeBind1(1,2,"device.copy-access-token")),t.ɵɵproperty("copyText",e.securityFormGroup.get("accessToken").value)}}function w5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",16),t.ɵɵpipe(1,"translate"),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext(2);return t.ɵɵresetView(n.generateAccessToken())})),t.ɵɵelementStart(2,"mat-icon"),t.ɵɵtext(3,"autorenew"),t.ɵɵelementEnd()()}2&e&&t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(1,1,"device.generate-access-token"))}function S5(e,n){if(1&e&&(t.ɵɵelementStart(0,"mat-form-field",9)(1,"mat-label",10),t.ɵɵtext(2,"security.access-token"),t.ɵɵelementEnd(),t.ɵɵelement(3,"input",11),t.ɵɵtemplate(4,x5,3,3,"mat-error",12)(5,b5,2,4,"tb-copy-button",13)(6,w5,4,3,"button",14),t.ɵɵelementStart(7,"mat-icon",15),t.ɵɵpipe(8,"translate"),t.ɵɵtext(9,"info_outlined "),t.ɵɵelementEnd()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(4),t.ɵɵproperty("ngIf",e.securityFormGroup.get("accessToken").hasError("required")),t.ɵɵadvance(),t.ɵɵconditional(e.securityFormGroup.get("accessToken").value?5:6),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(8,3,"gateway.hints.token"))}}function C5(e,n){1&e&&(t.ɵɵelement(0,"tb-file-input",17),t.ɵɵpipe(1,"translate"),t.ɵɵpipe(2,"translate"),t.ɵɵpipe(3,"translate")),2&e&&(t.ɵɵpropertyInterpolate("hint",t.ɵɵpipeBind1(1,5,"gateway.hints.ca-cert")),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(2,7,"security.ca-cert")),t.ɵɵpropertyInterpolate("dropLabel",t.ɵɵpipeBind1(3,9,"gateway.drop-file")),t.ɵɵproperty("allowedExtensions","pem,cert,key")("accept",".pem, application/pem,.cert, application/cert, .key,application/key"))}class _5{constructor(e,t,n){this.fb=e,this.cd=t,this.gatewayCredentialsService=n,this.initialized=new u,this.securityTypes=K3,this.onChange=()=>{},this.securityFormGroup=this.createSecurityFormGroup(),this.setupFormListeners()}ngAfterViewInit(){const{usernamePassword:e,...t}=this.securityFormGroup.value;this.initialized.emit({thingsboard:{security:e?{...t,...e}:t}})}writeValue(e){e?this.updateFormBySecurityConfig(e):this.updateFormBySecurityConfig(this.gatewayCredentialsService.credentialsToSecurityConfig(this.gatewayCredentialsService.initialCredentials))}registerOnChange(e){this.onChange=e}registerOnTouched(e){}validate(){return this.securityFormGroup.valid?null:{securityFormGroup:{valid:!1}}}updateFormBySecurityConfig(e){const{clientId:t,username:n,password:i,...a}=e??{};a?.type===$3.USERNAME_PASSWORD?this.securityFormGroup.patchValue({...a,usernamePassword:{clientId:t,username:n,password:i}},{emitEvent:!1}):this.securityFormGroup.patchValue(a,{emitEvent:!1}),this.toggleBySecurityType(this.securityFormGroup.get("type").value)}createSecurityFormGroup(){return this.fb.group({type:[$3.ACCESS_TOKEN,[$.required]],accessToken:[null,[$.required,$.pattern(/^[^.\s]+$/)]],caCert:[null,[$.required]],usernamePassword:[]})}setupFormListeners(){this.securityFormGroup.valueChanges.pipe(bn()).subscribe((({usernamePassword:e,...t})=>{this.onChange(e?{...t,...e}:t)})),this.securityFormGroup.get("type").valueChanges.pipe(bn()).subscribe((e=>{this.toggleBySecurityType(e)})),this.securityFormGroup.get("caCert").valueChanges.pipe(bn()).subscribe((()=>this.cd.detectChanges()))}toggleBySecurityType(e){switch(this.securityFormGroup.disable({emitEvent:!1}),this.securityFormGroup.get("type").enable({emitEvent:!1}),e){case $3.ACCESS_TOKEN:this.securityFormGroup.get("accessToken").enable({emitEvent:!1});break;case $3.TLS_PRIVATE_KEY:this.securityFormGroup.get("caCert").enable({emitEvent:!1});break;case $3.TLS_ACCESS_TOKEN:this.securityFormGroup.get("accessToken").enable({emitEvent:!1}),this.securityFormGroup.get("caCert").enable({emitEvent:!1});break;case $3.USERNAME_PASSWORD:this.securityFormGroup.get("usernamePassword").enable({emitEvent:!1})}}generateAccessToken(){this.securityFormGroup.get("accessToken").patchValue(Re(20))}static{this.ɵfac=function(e){return new(e||_5)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(y5))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:_5,selectors:[["tb-gateway-security-configuration"]],outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>_5)),multi:!0},{provide:K,useExisting:c((()=>_5)),multi:!0}]),t.ɵɵStandaloneFeature],decls:10,vars:8,consts:[[1,"tb-form-panel"],["translate","",1,"tb-form-panel-title"],[3,"formGroup"],["formControlName","type",1,"toggle-group","flex"],[3,"value",4,"ngFor","ngForOf"],["appearance","outline",4,"ngIf"],["formControlName","usernamePassword"],["formControlName","caCert",3,"hint","label","allowedExtensions","accept","dropLabel",4,"ngIf"],[3,"value"],["appearance","outline"],["translate",""],["matInput","","formControlName","accessToken"],[4,"ngIf"],["matSuffix","","miniButton","false","tooltipPosition","above","icon","content_copy",3,"copyText","tooltipText"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"matTooltip"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["type","button","matSuffix","","mat-icon-button","","aria-label","Generate","matTooltipPosition","above",3,"click","matTooltip"],["formControlName","caCert",3,"hint","label","allowedExtensions","accept","dropLabel"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"div",1),t.ɵɵtext(2,"security.security"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(3,2),t.ɵɵelementStart(4,"tb-toggle-select",3),t.ɵɵtemplate(5,v5,3,4,"tb-toggle-option",4),t.ɵɵpipe(6,"keyvalue"),t.ɵɵelementEnd(),t.ɵɵtemplate(7,S5,10,5,"mat-form-field",5),t.ɵɵelement(8,"tb-gateway-username-configuration",6),t.ɵɵtemplate(9,C5,4,11,"tb-file-input",7),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(3),t.ɵɵproperty("formGroup",n.securityFormGroup),t.ɵɵadvance(2),t.ɵɵproperty("ngForOf",t.ɵɵpipeBind1(6,6,n.securityTypes)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.securityFormGroup.get("type").value.toLowerCase().includes("accesstoken")),t.ɵɵadvance(),t.ɵɵclassProp("hidden","usernamePassword"!==n.securityFormGroup.get("type").value),t.ɵɵadvance(),t.ɵɵproperty("ngIf",n.securityFormGroup.get("type").value.toLowerCase().includes("tls")))},dependencies:t.ɵɵgetComponentDepsFactory(_5,[j,_,f5]),encapsulation:2})}}const T5=["configGroup"];function I5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-host-required")))}function E5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-required")))}function M5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-min")))}function k5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-max")))}function P5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.thingsboard-port-pattern")))}function D5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-security-configuration",20),t.ɵɵlistener("initialized",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext(2);return t.ɵɵresetView(i.onInitialized(n))})),t.ɵɵelementEnd()}}function O5(e,n){if(1&e&&t.ɵɵelement(0,"tb-report-strategy",21),2&e){const e=t.ɵɵnextContext(2);t.ɵɵproperty("defaultValue",e.ReportStrategyDefaultValue.Gateway)}}function A5(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",7)(1,"div",8)(2,"div",9),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"mat-slide-toggle",10),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",9),t.ɵɵpipe(8,"translate"),t.ɵɵelementStart(9,"mat-slide-toggle",11),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(12,"div",12)(13,"mat-form-field",13)(14,"mat-label",14),t.ɵɵtext(15,"gateway.thingsboard-host"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",15),t.ɵɵelementStart(17,"mat-icon",16),t.ɵɵpipe(18,"translate"),t.ɵɵtext(19,"info_outlined "),t.ɵɵelementEnd(),t.ɵɵtemplate(20,I5,3,3,"mat-error"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"mat-form-field",13)(22,"mat-label",14),t.ɵɵtext(23,"gateway.thingsboard-port"),t.ɵɵelementEnd(),t.ɵɵelement(24,"input",17),t.ɵɵtemplate(25,E5,3,3,"mat-error")(26,M5,3,3,"mat-error")(27,k5,3,3,"mat-error")(28,P5,3,3,"mat-error"),t.ɵɵelementStart(29,"mat-icon",16),t.ɵɵpipe(30,"translate"),t.ɵɵtext(31,"info_outlined "),t.ɵɵelementEnd()()()(),t.ɵɵtemplate(32,D5,1,0,"tb-gateway-security-configuration",18),t.ɵɵpipe(33,"async"),t.ɵɵtemplate(34,O5,1,1,"tb-report-strategy",19),t.ɵɵelementEnd()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,10,"gateway.hints.remote-configuration")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,12,"gateway.remote-configuration")," "),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(8,14,"gateway.hints.remote-shell")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,16,"gateway.remote-shell")," "),t.ɵɵadvance(7),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(18,18,"gateway.hints.host")),t.ɵɵadvance(3),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.host").hasError("required")?20:-1),t.ɵɵadvance(5),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.port").hasError("required")?25:e.basicFormGroup.get("thingsboard.port").hasError("min")?26:e.basicFormGroup.get("thingsboard.port").hasError("max")?27:e.basicFormGroup.get("thingsboard.port").hasError("pattern")?28:-1),t.ɵɵadvance(4),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(30,20,"gateway.hints.port")),t.ɵɵadvance(3),t.ɵɵproperty("ngIf",t.ɵɵpipeBind1(33,22,e.initialCredentials$)),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.withReportStrategy)}}function F5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-slide-toggle",23)(1,"mat-label",33),t.ɵɵpipe(2,"translate"),t.ɵɵtext(3),t.ɵɵpipe(4,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(5,"mat-slide-toggle",34)(6,"mat-label",33),t.ɵɵpipe(7,"translate"),t.ɵɵtext(8),t.ɵɵpipe(9,"translate"),t.ɵɵelementEnd()()),2&e&&(t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(2,4,"gateway.hints.enable-general-statistics")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(4,6,"gateway.statistics.general-statistics")," "),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(7,8,"gateway.hints.enable-custom-statistics")),t.ɵɵadvance(2),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(9,10,"gateway.statistics.custom-statistics")," "))}function R5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-slide-toggle",23),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.statistics")," "))}function B5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.send-period-required")))}function N5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.send-period-min")))}function L5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.send-period-pattern")))}function V5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.custom-send-period-required")))}function q5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.custom-send-period-min")))}function G5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.custom-send-period-pattern")))}function z5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-required")))}function U5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-pattern")," "))}function j5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.time-series-name-already-exists")))}function H5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-required")))}function W5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-min")))}function $5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.timeout-pattern")))}function K5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.command-required")))}function Y5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.command-pattern")))}function X5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",35)(1,"section",36)(2,"section",37)(3,"mat-form-field",38)(4,"mat-label",14),t.ɵɵtext(5,"gateway.statistics.name"),t.ɵɵelementEnd(),t.ɵɵelement(6,"input",39),t.ɵɵtemplate(7,z5,3,3,"mat-error")(8,U5,3,3,"mat-error")(9,j5,3,3,"mat-error"),t.ɵɵelementStart(10,"mat-icon",16),t.ɵɵpipe(11,"translate"),t.ɵɵtext(12,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(13,"mat-form-field",38)(14,"mat-label",14),t.ɵɵtext(15,"gateway.statistics.timeout"),t.ɵɵelementEnd(),t.ɵɵelement(16,"input",40),t.ɵɵtemplate(17,H5,3,3,"mat-error")(18,W5,3,3,"mat-error")(19,$5,3,3,"mat-error"),t.ɵɵelementStart(20,"mat-icon",16),t.ɵɵpipe(21,"translate"),t.ɵɵtext(22,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(23,"section")(24,"mat-form-field",41)(25,"mat-label",14),t.ɵɵtext(26,"gateway.statistics.command"),t.ɵɵelementEnd(),t.ɵɵelement(27,"input",42),t.ɵɵtemplate(28,K5,3,3,"mat-error")(29,Y5,3,3,"mat-error"),t.ɵɵelementStart(30,"mat-icon",16),t.ɵɵpipe(31,"translate"),t.ɵɵtext(32,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(33,"section")(34,"mat-expansion-panel",43)(35,"mat-expansion-panel-header")(36,"mat-panel-title")(37,"div",28),t.ɵɵtext(38,"gateway.advanced-settings"),t.ɵɵelementEnd()()(),t.ɵɵelementStart(39,"mat-form-field",38)(40,"mat-label",14),t.ɵɵtext(41,"gateway.statistics.install-cmd"),t.ɵɵelementEnd(),t.ɵɵelement(42,"input",44),t.ɵɵelementStart(43,"mat-icon",45),t.ɵɵpipe(44,"translate"),t.ɵɵtext(45,"info_outlined "),t.ɵɵelementEnd()()()()(),t.ɵɵelementStart(46,"button",46),t.ɵɵpipe(47,"translate"),t.ɵɵlistener("click",(function(n){const i=t.ɵɵrestoreView(e).index,a=t.ɵɵnextContext(2);return t.ɵɵresetView(a.removeCommandControl(i,n))})),t.ɵɵelementStart(48,"mat-icon"),t.ɵɵtext(49,"delete"),t.ɵɵelementEnd()()()}if(2&e){const e=n.$implicit,i=n.index,a=t.ɵɵnextContext(2);t.ɵɵadvance(),t.ɵɵproperty("formGroupName",i),t.ɵɵadvance(6),t.ɵɵconditional(e.get("attributeOnGateway").hasError("required")?7:e.get("attributeOnGateway").hasError("pattern")?8:e.get("attributeOnGateway").hasError("duplicateName")?9:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(11,10,"gateway.hints.attribute")),t.ɵɵadvance(7),t.ɵɵconditional(e.get("timeout").hasError("required")?17:e.get("timeout").hasError("min")?18:e.get("timeout").hasError("pattern")?19:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(21,12,"gateway.hints.timeout")),t.ɵɵadvance(8),t.ɵɵconditional(e.get("command").hasError("required")?28:e.get("command").hasError("pattern")?29:-1),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(31,14,"gateway.hints.command")),t.ɵɵadvance(13),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(44,16,"gateway.hints.install-cmd")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(47,18,"gateway.statistics.remove")),t.ɵɵproperty("disabled",!a.basicFormGroup.get("thingsboard.remoteConfiguration").value)}}function Z5(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"div",7)(1,"div",22),t.ɵɵtemplate(2,F5,10,12)(3,R5,3,3,"mat-slide-toggle",23),t.ɵɵelementStart(4,"mat-form-field",24)(5,"mat-label",14),t.ɵɵtext(6,"gateway.statistics.send-period"),t.ɵɵelementEnd(),t.ɵɵelement(7,"input",25),t.ɵɵtemplate(8,B5,3,3,"mat-error")(9,N5,3,3,"mat-error")(10,L5,3,3,"mat-error"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"mat-form-field",24)(12,"mat-label",14),t.ɵɵtext(13,"gateway.statistics.custom-send-period"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",26),t.ɵɵtemplate(15,V5,3,3,"mat-error")(16,q5,3,3,"mat-error")(17,G5,3,3,"mat-error"),t.ɵɵelementEnd()(),t.ɵɵelementStart(18,"div",27)(19,"div",28),t.ɵɵtext(20,"gateway.statistics.commands"),t.ɵɵelementEnd(),t.ɵɵelementStart(21,"div",29),t.ɵɵtext(22,"gateway.hints.commands"),t.ɵɵelementEnd(),t.ɵɵelementContainerStart(23,30),t.ɵɵtemplate(24,X5,50,20,"div",31),t.ɵɵelementStart(25,"button",32),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.addCommand())})),t.ɵɵtext(26),t.ɵɵpipe(27,"translate"),t.ɵɵelementEnd(),t.ɵɵelementContainerEnd(),t.ɵɵelementEnd()()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(2),t.ɵɵconditional(e.hasUpdatedStatistics?2:3),t.ɵɵadvance(6),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.statistics.statsSendPeriodInSeconds").hasError("required")?8:e.basicFormGroup.get("thingsboard.statistics.statsSendPeriodInSeconds").hasError("min")?9:e.basicFormGroup.get("thingsboard.statistics.statsSendPeriodInSeconds").hasError("pattern")?10:-1),t.ɵɵadvance(7),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.statistics.customStatsSendPeriodInSeconds").hasError("required")?15:e.basicFormGroup.get("thingsboard.statistics.customStatsSendPeriodInSeconds").hasError("min")?16:e.basicFormGroup.get("thingsboard.statistics.customStatsSendPeriodInSeconds").hasError("pattern")?17:-1),t.ɵɵadvance(9),t.ɵɵproperty("ngForOf",e.commandFormArray().controls),t.ɵɵadvance(),t.ɵɵproperty("disabled",!e.basicFormGroup.get("thingsboard.remoteConfiguration").value),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(27,6,"gateway.statistics.add")," ")}}function Q5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-timeout-seconds-required")))}function J5(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-timeout-seconds-min")))}function e6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-timeout-seconds-pattern")))}function t6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-check-period-seconds-required")))}function n6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-check-period-seconds-min")))}function i6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.inactivity-check-period-seconds-pattern")))}function a6(e,n){if(1&e&&(t.ɵɵelementStart(0,"section",37)(1,"mat-form-field",38)(2,"mat-label",14),t.ɵɵtext(3,"gateway.inactivity-timeout-seconds"),t.ɵɵelementEnd(),t.ɵɵelement(4,"input",56),t.ɵɵtemplate(5,Q5,3,3,"mat-error")(6,J5,3,3,"mat-error")(7,e6,3,3,"mat-error"),t.ɵɵelementStart(8,"mat-icon",16),t.ɵɵpipe(9,"translate"),t.ɵɵtext(10,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-form-field",38)(12,"mat-label",14),t.ɵɵtext(13,"gateway.inactivity-check-period-seconds"),t.ɵɵelementEnd(),t.ɵɵelement(14,"input",57),t.ɵɵtemplate(15,t6,3,3,"mat-error")(16,n6,3,3,"mat-error")(17,i6,3,3,"mat-error"),t.ɵɵelementStart(18,"mat-icon",16),t.ɵɵpipe(19,"translate"),t.ɵɵtext(20,"info_outlined "),t.ɵɵelementEnd()()()),2&e){const e=t.ɵɵnextContext(2);t.ɵɵadvance(5),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds").hasError("required")?5:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds").hasError("min")?6:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds").hasError("pattern")?7:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(9,4,"gateway.hints.inactivity-timeout")),t.ɵɵadvance(7),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds").hasError("required")?15:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds").hasError("min")?16:e.basicFormGroup.get("thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds").hasError("pattern")?17:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(19,6,"gateway.hints.inactivity-period"))}}function r6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.min-pack-send-delay-required")))}function o6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.min-pack-send-delay-min")))}function s6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.min-pack-send-delay-pattern")))}function l6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.mqtt-qos-required")))}function p6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.mqtt-qos-range")))}function c6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.mqtt-qos-range")))}function d6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.check-connectors-configuration-required")))}function u6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.check-connectors-configuration-min")))}function m6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.check-connectors-configuration-pattern")))}function h6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.max-payload-size-bytes-required")))}function g6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.max-payload-size-bytes-min")))}function f6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.max-payload-size-bytes-pattern")))}function y6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.min-pack-size-to-send-required")))}function v6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.min-pack-size-to-send-min")))}function x6(e,n){1&e&&(t.ɵɵelementStart(0,"mat-error"),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()),2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(2,1,"gateway.statistics.min-pack-size-to-send-pattern")))}function b6(e,n){if(1&e&&(t.ɵɵelementStart(0,"div",7)(1,"div",47)(2,"div",9),t.ɵɵpipe(3,"translate"),t.ɵɵelementStart(4,"mat-slide-toggle",48),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(7,a6,21,8,"section",49),t.ɵɵelementEnd(),t.ɵɵelementStart(8,"div",8)(9,"div",28),t.ɵɵtext(10,"gateway.advanced"),t.ɵɵelementEnd(),t.ɵɵelementStart(11,"section",37)(12,"mat-form-field",38)(13,"mat-label",14),t.ɵɵtext(14,"gateway.min-pack-send-delay"),t.ɵɵelementEnd(),t.ɵɵelement(15,"input",50),t.ɵɵtemplate(16,r6,3,3,"mat-error")(17,o6,3,3,"mat-error")(18,s6,3,3,"mat-error"),t.ɵɵelementStart(19,"mat-icon",16),t.ɵɵpipe(20,"translate"),t.ɵɵtext(21,"info_outlined "),t.ɵɵelementEnd()(),t.ɵɵelementStart(22,"mat-form-field",38)(23,"mat-label",14),t.ɵɵtext(24,"gateway.mqtt-qos"),t.ɵɵelementEnd(),t.ɵɵelementStart(25,"mat-select",51)(26,"mat-option",52),t.ɵɵtext(27,"0"),t.ɵɵelementEnd(),t.ɵɵelementStart(28,"mat-option",52),t.ɵɵtext(29,"1"),t.ɵɵelementEnd()(),t.ɵɵtemplate(30,l6,3,3,"mat-error")(31,p6,3,3,"mat-error")(32,c6,3,3,"mat-error"),t.ɵɵelementStart(33,"mat-icon",16),t.ɵɵpipe(34,"translate"),t.ɵɵtext(35,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(36,"section",37)(37,"mat-form-field",38)(38,"mat-label",14),t.ɵɵtext(39,"gateway.statistics.check-connectors-configuration"),t.ɵɵelementEnd(),t.ɵɵelement(40,"input",53),t.ɵɵtemplate(41,d6,3,3,"mat-error")(42,u6,3,3,"mat-error")(43,m6,3,3,"mat-error"),t.ɵɵelementEnd(),t.ɵɵelementStart(44,"mat-form-field",38)(45,"mat-label",14),t.ɵɵtext(46,"gateway.statistics.max-payload-size-bytes"),t.ɵɵelementEnd(),t.ɵɵelement(47,"input",54),t.ɵɵtemplate(48,h6,3,3,"mat-error")(49,g6,3,3,"mat-error")(50,f6,3,3,"mat-error"),t.ɵɵelementStart(51,"mat-icon",16),t.ɵɵpipe(52,"translate"),t.ɵɵtext(53,"info_outlined "),t.ɵɵelementEnd()()(),t.ɵɵelementStart(54,"section",37)(55,"mat-form-field",38)(56,"mat-label",14),t.ɵɵtext(57,"gateway.statistics.min-pack-size-to-send"),t.ɵɵelementEnd(),t.ɵɵelement(58,"input",55),t.ɵɵtemplate(59,y6,3,3,"mat-error")(60,v6,3,3,"mat-error")(61,x6,3,3,"mat-error"),t.ɵɵelementStart(62,"mat-icon",16),t.ɵɵpipe(63,"translate"),t.ɵɵtext(64,"info_outlined "),t.ɵɵelementEnd()()()()()),2&e){const e=t.ɵɵnextContext();t.ɵɵadvance(),t.ɵɵclassProp("no-padding-bottom",e.basicFormGroup.get("thingsboard.checkingDeviceActivity.checkDeviceInactivity").value),t.ɵɵadvance(),t.ɵɵpropertyInterpolate("tb-hint-tooltip-icon",t.ɵɵpipeBind1(3,16,"gateway.hints.check-device-activity")),t.ɵɵadvance(3),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(6,18,"gateway.checking-device-activity")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",e.basicFormGroup.get("thingsboard.checkingDeviceActivity.checkDeviceInactivity").value),t.ɵɵadvance(9),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.minPackSendDelayMS").hasError("required")?16:e.basicFormGroup.get("thingsboard.minPackSendDelayMS").hasError("min")?17:e.basicFormGroup.get("thingsboard.minPackSendDelayMS").hasError("pattern")?18:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(20,20,"gateway.hints.minimal-pack-delay")),t.ɵɵadvance(7),t.ɵɵproperty("value",0),t.ɵɵadvance(2),t.ɵɵproperty("value",1),t.ɵɵadvance(2),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.qos").hasError("required")?30:e.basicFormGroup.get("thingsboard.qos").hasError("min")?31:e.basicFormGroup.get("thingsboard.qos").hasError("max")?32:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(34,22,"gateway.hints.qos")),t.ɵɵadvance(8),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.checkConnectorsConfigurationInSeconds").hasError("required")?41:e.basicFormGroup.get("thingsboard.checkConnectorsConfigurationInSeconds").hasError("min")?42:e.basicFormGroup.get("thingsboard.checkConnectorsConfigurationInSeconds").hasError("pattern")?43:-1),t.ɵɵadvance(7),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.maxPayloadSizeBytes").hasError("required")?48:e.basicFormGroup.get("thingsboard.maxPayloadSizeBytes").hasError("min")?49:e.basicFormGroup.get("thingsboard.maxPayloadSizeBytes").hasError("pattern")?50:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(52,24,"gateway.hints.max-payload-size-bytes")),t.ɵɵadvance(8),t.ɵɵconditional(e.basicFormGroup.get("thingsboard.minPackSizeToSend").hasError("required")?59:e.basicFormGroup.get("thingsboard.minPackSizeToSend").hasError("min")?60:e.basicFormGroup.get("thingsboard.minPackSizeToSend").hasError("pattern")?61:-1),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("matTooltip",t.ɵɵpipeBind1(63,26,"gateway.hints.min-pack-size-to-send"))}}class w6{constructor(e,t,n,i,a){this.fb=e,this.deviceService=t,this.gatewayCredentialsService=n,this.destroyRef=i,this.dialog=a,this.gatewayVersion=pt.Legacy,this.dialogMode=!1,this.withReportStrategy=!1,this.initialized=new u,this.ReportStrategyDefaultValue=en,this.initialCredentials$=this.gatewayCredentialsService.initialCredentials$,this.onChange=()=>{},this.destroy$=new te,this.initBasicFormGroup(),this.observeFormChanges(),this.basicFormGroup.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e)}))}ngOnChanges(e){e.withReportStrategy&&!e.withReportStrategy.firstChange&&this.withReportStrategy&&this.basicFormGroup.get("thingsboard.reportStrategy").enable({emitEvent:!1}),e.gatewayVersion?.previousValue!==e.gatewayVersion.currentValue&&this.onVersionChange()}ngAfterViewInit(){this.defaultTab&&(this.configGroup.selectedIndex=q3[this.defaultTab])}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){}writeValue(e){this.basicFormGroup.patchValue(e,{emitEvent:!1});const t=e?.thingsboard?.statistics?.commands??[];this.commandFormArray().clear({emitEvent:!1}),t.forEach((e=>this.addCommand(e,!1)))}validate(){return this.basicFormGroup.valid?null:{basicFormGroup:{valid:!1}}}commandFormArray(){return this.basicFormGroup.get("thingsboard.statistics.commands")}removeCommandControl(e,t){""!==t.pointerType&&(this.commandFormArray().removeAt(e),this.basicFormGroup.markAsDirty())}openConfigurationConfirmDialog(){this.deviceService.getDevice(this.device.id).pipe(le(this.destroy$)).subscribe((e=>{this.dialog.open(pW,{disableClose:!0,panelClass:["tb-dialog","tb-fullscreen-dialog"],data:{gatewayName:e.name}}).afterClosed().pipe(ye(1)).subscribe((e=>{e||this.basicFormGroup.get("thingsboard.remoteConfiguration").setValue(!0,{emitEvent:!1})}))}))}addCommand(e,t=!0){const{attributeOnGateway:n=null,command:i=null,timeout:a=10,installCmd:r=""}=e||{},o=this.fb.group({attributeOnGateway:[n,[$.required,$.pattern(an),this.uniqNameRequired()]],command:[i,[$.required,$.pattern(/^(?=\S).*\S$/)]],timeout:[a,[$.required,$.min(1),$.pattern(nn),$.pattern(/^[^.\s]+$/)]],installCmd:[r,$.pattern(an)]});this.commandFormArray().push(o,{emitEvent:t})}uniqNameRequired(){return e=>{const t=e.value?.trim().toLowerCase(),n=e.dirty&&t&&this.commandFormArray().value.some((e=>e.attributeOnGateway?.toLowerCase()===t));return n?{duplicateName:{valid:!1}}:null}}onInitialized(e){this.basicFormGroup.patchValue(e,{emitEvent:!1}),this.initialized.emit(this.basicFormGroup.value)}initBasicFormGroup(){this.basicFormGroup=this.fb.group({thingsboard:this.initThingsboardFormGroup(),storage:[],grpc:[],connectors:this.fb.array([]),logs:[]})}initThingsboardFormGroup(){return this.fb.group({host:[window.location.hostname,[$.required,$.pattern(/^[^\s]+$/)]],port:[1883,[$.required,$.min(1),$.max(65535),$.pattern(nn)]],remoteShell:[!1],remoteConfiguration:[!0],checkConnectorsConfigurationInSeconds:[60,[$.required,$.min(1),$.pattern(nn)]],statistics:this.fb.group({enable:[!0],enableCustom:[{value:!1,disabled:!0}],statsSendPeriodInSeconds:[3600,[$.required,$.min(60),$.pattern(nn)]],customStatsSendPeriodInSeconds:[3600,[$.required,$.min(60),$.pattern(nn)]],commands:this.fb.array([])}),maxPayloadSizeBytes:[8196,[$.required,$.min(100),$.pattern(nn)]],minPackSendDelayMS:[50,[$.required,$.min(10),$.pattern(nn)]],minPackSizeToSend:[500,[$.required,$.min(100),$.pattern(nn)]],handleDeviceRenaming:[!0],checkingDeviceActivity:this.initCheckingDeviceActivityFormGroup(),security:[],qos:[1],reportStrategy:[{value:{type:Jt.OnReceived},disabled:!0}]})}initCheckingDeviceActivityFormGroup(){return this.fb.group({checkDeviceInactivity:[!1],inactivityTimeoutSeconds:[300,[$.min(1),$.pattern(nn)]],inactivityCheckPeriodSeconds:[10,[$.min(1),$.pattern(nn)]]})}onVersionChange(){if(this.hasUpdatedStatistics=za.parseVersion(this.gatewayVersion)>=za.parseVersion(pt.v3_7_3),this.hasUpdatedStatistics){const e=this.basicFormGroup.get("thingsboard.statistics.enableCustom");e.enable({emitEvent:!1}),this.basicFormGroup.get("thingsboard.statistics.enable").valueChanges.pipe(bn(this.destroyRef)).subscribe((t=>{t||e.patchValue(!1,{emitEvent:!1}),e[t?"enable":"disable"]({emitEvent:!1})}))}}observeFormChanges(){this.observeRemoteConfigurationChanges(),this.observeDeviceActivityChanges()}observeRemoteConfigurationChanges(){this.basicFormGroup.get("thingsboard.remoteConfiguration").valueChanges.pipe(le(this.destroy$)).subscribe((e=>{e||this.openConfigurationConfirmDialog()}))}observeDeviceActivityChanges(){const e=this.basicFormGroup.get("thingsboard.checkingDeviceActivity");e.get("checkDeviceInactivity").valueChanges.pipe(le(this.destroy$)).subscribe((t=>{e.updateValueAndValidity();const n=[$.min(1),$.required,$.pattern(nn)];t?(e.get("inactivityTimeoutSeconds").setValidators(n),e.get("inactivityCheckPeriodSeconds").setValidators(n)):(e.get("inactivityTimeoutSeconds").clearValidators(),e.get("inactivityCheckPeriodSeconds").clearValidators()),e.get("inactivityTimeoutSeconds").updateValueAndValidity({emitEvent:!1}),e.get("inactivityCheckPeriodSeconds").updateValueAndValidity({emitEvent:!1})}))}static{this.ɵfac=function(e){return new(e||w6)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(_e.DeviceService),t.ɵɵdirectiveInject(y5),t.ɵɵdirectiveInject(t.DestroyRef),t.ɵɵdirectiveInject(qe.MatDialog))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:w6,selectors:[["tb-gateway-basic-configuration"]],viewQuery:function(e,n){if(1&e&&t.ɵɵviewQuery(T5,5),2&e){let e;t.ɵɵqueryRefresh(e=t.ɵɵloadQuery())&&(n.configGroup=e.first)}},inputs:{device:"device",defaultTab:"defaultTab",gatewayVersion:"gatewayVersion",dialogMode:"dialogMode",withReportStrategy:"withReportStrategy"},outputs:{initialized:"initialized"},standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>w6)),multi:!0},{provide:K,useExisting:c((()=>w6)),multi:!0}]),t.ɵɵNgOnChangesFeature,t.ɵɵStandaloneFeature],decls:20,vars:21,consts:[["configGroup",""],[1,"tab-group-block",3,"formGroup"],[3,"label"],["matTabContent",""],["formControlName","logs",1,"configuration-block",3,"initialized"],["formControlName","storage",3,"initialized"],["formControlName","grpc",3,"initialized"],["formGroupName","thingsboard",1,"mat-content","mat-padding","configuration-block"],[1,"tb-form-panel","no-padding-bottom"],[1,"tb-form-row","no-border","no-padding",3,"tb-hint-tooltip-icon"],["color","primary","formControlName","remoteConfiguration",1,"mat-slide"],["color","primary","formControlName","remoteShell",1,"mat-slide"],[1,"no-border","no-padding","tb-standard-fields","xs:flex-col","flex","gap-2"],["appearance","outline",1,"flex","flex-1"],["translate",""],["matInput","","formControlName","host"],["matIconSuffix","",2,"cursor","pointer",3,"matTooltip"],["matInput","","formControlName","port","type","number","min","0"],["formControlName","security",3,"initialized",4,"ngIf"],["class","tb-form-panel","formControlName","reportStrategy",3,"defaultValue",4,"ngIf"],["formControlName","security",3,"initialized"],["formControlName","reportStrategy",1,"tb-form-panel",3,"defaultValue"],["formGroupName","statistics",1,"tb-form-panel","no-padding-bottom"],["color","primary","formControlName","enable",1,"mat-slide"],["appearance","outline"],["matInput","","formControlName","statsSendPeriodInSeconds","type","number","min","60"],["matInput","","formControlName","customStatsSendPeriodInSeconds","type","number","min","60"],[1,"tb-form-panel"],["translate","",1,"tb-form-panel-title"],["translate","",1,"tb-form-panel-hint"],["formGroupName","statistics"],["formArrayName","commands","class","statistics-container flex flex-row",4,"ngFor","ngForOf"],["mat-stroked-button","","color","primary","type","button",2,"width","fit-content",3,"click","disabled"],[3,"tb-hint-tooltip-icon"],["color","primary","formControlName","enableCustom",1,"mat-slide"],["formArrayName","commands",1,"statistics-container","flex","flex-row"],[1,"tb-form-panel","stroked","no-padding-bottom","no-gap","command-container",3,"formGroupName"],[1,"tb-form-row","no-border","no-padding","tb-standard-fields","column-xs"],["appearance","outline",1,"flex"],["matInput","","formControlName","attributeOnGateway"],["matInput","","formControlName","timeout","type","number","min","0"],["appearance","outline",1,"mat-block"],["matInput","","formControlName","command"],[1,"tb-settings","pb-4"],["matInput","","formControlName","installCmd"],["matIconSuffix","",1,"cursor-pointer",3,"matTooltip"],["mat-icon-button","","matTooltipPosition","above",1,"tb-box-button",3,"click","disabled","matTooltip"],["formGroupName","checkingDeviceActivity",1,"tb-form-panel"],["color","primary","formControlName","checkDeviceInactivity",1,"mat-slide"],["class","tb-form-row no-border no-padding tb-standard-fields column-xs",4,"ngIf"],["matInput","","formControlName","minPackSendDelayMS","type","number","min","0"],["formControlName","qos"],[3,"value"],["matInput","","formControlName","checkConnectorsConfigurationInSeconds","type","number","min","0"],["matInput","","formControlName","maxPayloadSizeBytes","type","number","min","0"],["matInput","","formControlName","minPackSizeToSend","type","number","min","0"],["matInput","","formControlName","inactivityTimeoutSeconds","type","number","min","0"],["matInput","","type","number","min","0","formControlName","inactivityCheckPeriodSeconds"]],template:function(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"mat-tab-group",1,0)(2,"mat-tab",2),t.ɵɵpipe(3,"translate"),t.ɵɵtemplate(4,A5,35,24,"ng-template",3),t.ɵɵelementEnd(),t.ɵɵelementStart(5,"mat-tab",2),t.ɵɵpipe(6,"translate"),t.ɵɵelementStart(7,"tb-gateway-logs-configuration",4),t.ɵɵlistener("initialized",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.onInitialized(i))})),t.ɵɵelementEnd()(),t.ɵɵelementStart(8,"mat-tab",2),t.ɵɵpipe(9,"translate"),t.ɵɵelementStart(10,"tb-gateway-storage-configuration",5),t.ɵɵlistener("initialized",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.onInitialized(i))})),t.ɵɵelementEnd()(),t.ɵɵelementStart(11,"mat-tab",2),t.ɵɵpipe(12,"translate"),t.ɵɵelementStart(13,"tb-gateway-grpc-configuration",6),t.ɵɵlistener("initialized",(function(i){return t.ɵɵrestoreView(e),t.ɵɵresetView(n.onInitialized(i))})),t.ɵɵelementEnd()(),t.ɵɵelementStart(14,"mat-tab",2),t.ɵɵpipe(15,"translate"),t.ɵɵtemplate(16,Z5,28,8,"ng-template",3),t.ɵɵelementEnd(),t.ɵɵelementStart(17,"mat-tab",2),t.ɵɵpipe(18,"translate"),t.ɵɵtemplate(19,b6,65,28,"ng-template",3),t.ɵɵelementEnd()()}2&e&&(t.ɵɵclassProp("dialog-mode",n.dialogMode),t.ɵɵproperty("formGroup",n.basicFormGroup),t.ɵɵadvance(2),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(3,9,"gateway.general")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(6,11,"gateway.logs.logs")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(9,13,"gateway.storage")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(12,15,"gateway.grpc")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(15,17,"gateway.statistics.statistics")),t.ɵɵadvance(3),t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(18,19,"gateway.other")))},dependencies:t.ɵɵgetComponentDepsFactory(w6,[j,_,Xn,E4,$4,r5,_5]),styles:['@charset "UTF-8";[_nghost-%COMP%]{width:100%;height:100%;display:grid;grid-template-rows:min-content minmax(auto,1fr) min-content}[_nghost-%COMP%] .configuration-block[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:16px}[_nghost-%COMP%] .mat-toolbar[_ngcontent-%COMP%]{grid-row:1;background:transparent;color:#000000de!important}[_nghost-%COMP%] .tab-group-block[_ngcontent-%COMP%]{min-width:0;height:100%;min-height:0;grid-row:2}[_nghost-%COMP%] .toggle-group[_ngcontent-%COMP%]{margin-right:auto}[_nghost-%COMP%] .first-capital[_ngcontent-%COMP%]{text-transform:capitalize}[_nghost-%COMP%] textarea[_ngcontent-%COMP%]{resize:none}[_nghost-%COMP%] .saving-period[_ngcontent-%COMP%]{flex:1}[_nghost-%COMP%] .statistics-container[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] .statistics-container[_ngcontent-%COMP%] .command-container[_ngcontent-%COMP%]{width:100%}[_nghost-%COMP%] mat-form-field[_ngcontent-%COMP%] mat-error[_ngcontent-%COMP%]{display:none!important}[_nghost-%COMP%] mat-form-field[_ngcontent-%COMP%] mat-error[_ngcontent-%COMP%]:first-child{display:block!important}[_nghost-%COMP%] .pointer-event{pointer-events:all}[_nghost-%COMP%] .toggle-group span{padding:0 25px}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{color:#e0e0e0}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix:hover{color:#9e9e9e}[_nghost-%COMP%] .mat-mdc-form-field-icon-suffix{display:flex;align-items:center}']})}}e("GatewayBasicConfigurationComponent",w6),Ge([I()],w6.prototype,"dialogMode",void 0),Ge([I()],w6.prototype,"withReportStrategy",void 0);class S6{constructor(e){this.fb=e,this.destroy$=new te,this.advancedFormControl=this.fb.control(""),this.advancedFormControl.valueChanges.pipe(le(this.destroy$)).subscribe((e=>{this.onChange(e),this.onTouched()}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}writeValue(e){this.advancedFormControl.reset(e,{emitEvent:!1})}validate(){return this.advancedFormControl.valid?null:{advancedFormControl:{valid:!1}}}static{this.ɵfac=function(e){return new(e||S6)(t.ɵɵdirectiveInject(H.FormBuilder))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:S6,selectors:[["tb-gateway-advanced-configuration"]],standalone:!0,features:[t.ɵɵProvidersFeature([{provide:W,useExisting:c((()=>S6)),multi:!0},{provide:K,useExisting:c((()=>S6)),multi:!0}]),t.ɵɵStandaloneFeature],decls:2,vars:4,consts:[["fillHeight","true","jsonRequired","",1,"flex","h-full","flex-col","p-2",3,"label","formControl"]],template:function(e,n){1&e&&(t.ɵɵelement(0,"tb-json-object-edit",0),t.ɵɵpipe(1,"translate")),2&e&&(t.ɵɵpropertyInterpolate("label",t.ɵɵpipeBind1(1,2,"gateway.configuration")),t.ɵɵproperty("formControl",n.advancedFormControl))},dependencies:t.ɵɵgetComponentDepsFactory(S6,[j,_]),encapsulation:2})}}function C6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",14),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.cancel())})),t.ɵɵelementStart(1,"mat-icon",15),t.ɵɵtext(2,"close"),t.ɵɵelementEnd()()}}function _6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"tb-gateway-basic-configuration",16),t.ɵɵpipe(1,"withReportStrategy"),t.ɵɵlistener("initialized",(function(n){t.ɵɵrestoreView(e);const i=t.ɵɵnextContext();return t.ɵɵresetView(i.onInitialized(n))})),t.ɵɵelementEnd()}if(2&e){const e=t.ɵɵnextContext();t.ɵɵproperty("device",e.device)("defaultTab",e.defaultTab)("gatewayVersion",e.gatewayVersion)("dialogMode",!!e.dialogRef)("withReportStrategy",t.ɵɵpipeBind1(1,5,e.gatewayVersion))}}function T6(e,n){1&e&&t.ɵɵelement(0,"tb-gateway-advanced-configuration",10)}function I6(e,n){if(1&e){const e=t.ɵɵgetCurrentView();t.ɵɵelementStart(0,"button",17),t.ɵɵlistener("click",(function(){t.ɵɵrestoreView(e);const n=t.ɵɵnextContext();return t.ɵɵresetView(n.cancel())})),t.ɵɵtext(1),t.ɵɵpipe(2,"translate"),t.ɵɵelementEnd()}2&e&&(t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(2,1,"action.cancel")," "))}e("GatewayAdvancedConfigurationComponent",S6);class E6{constructor(e,t,n,i,a){this.fb=e,this.attributeService=t,this.cd=n,this.gatewayCredentialsService=i,this.destroyRef=a,this.ConfigurationModes=Qt,this.gatewayConfigAttributeKeys=["general_configuration","grpc_configuration","logs_configuration","storage_configuration","RemoteLoggingLevel","mode"],this.useUpdatedLogs=!1,this.gatewayConfigGroup=this.fb.group({basicConfig:[],advancedConfig:[],mode:[Qt.BASIC]}),this.observeAlignConfigs()}ngAfterViewInit(){this.fetchConfigAttribute(this.device)}saveConfig(){const{mode:e,advancedConfig:t}=Ne(this.removeEmpty(this.gatewayConfigGroup.value)),n={mode:e,...t};n.thingsboard.statistics.commands=Object.values(n.thingsboard.statistics.commands??[]);const i=this.generateAttributes(n);this.attributeService.saveEntityAttributes(this.device,D.SHARED_SCOPE,i).pipe(xe((e=>this.gatewayCredentialsService.updateCredentials(n.thingsboard.security))),bn(this.destroyRef)).subscribe((()=>{this.dialogRef?this.dialogRef.close():(this.gatewayConfigGroup.markAsPristine(),this.cd.detectChanges())}))}onInitialized(e){this.gatewayConfigGroup.get("basicConfig").patchValue(e,{emitEvent:!1}),this.gatewayConfigGroup.get("advancedConfig").patchValue(e,{emitEvent:!1})}observeAlignConfigs(){this.gatewayConfigGroup.get("basicConfig").valueChanges.pipe(bn(this.destroyRef)).subscribe((e=>{const t=this.gatewayConfigGroup.get("advancedConfig");Se(t.value,e)||this.gatewayConfigGroup.get("mode").value!==Qt.BASIC||t.patchValue(e,{emitEvent:!1})})),this.gatewayConfigGroup.get("advancedConfig").valueChanges.pipe(bn(this.destroyRef)).subscribe((e=>{const t=this.gatewayConfigGroup.get("basicConfig");Se(t.value,e)||this.gatewayConfigGroup.get("mode").value!==Qt.ADVANCED||t.patchValue(e,{emitEvent:!1})}))}generateAttributes(e){const t=[],n=(e,n)=>{t.push({key:e,value:n})},i=(e,t)=>{t={...t,ts:(new Date).getTime()},n(e,t)};return n("RemoteLoggingLevel",e.logs?.logLevel??lt.NONE),delete e.connectors,n("logs_configuration",this.generateLogsFile(e.logs)),i("grpc_configuration",e.grpc),i("storage_configuration",e.storage),i("general_configuration",e.thingsboard),n("mode",e.mode),t}cancel(){this.dialogRef&&this.dialogRef.close()}removeEmpty(e){return Object.fromEntries(Object.entries(e).filter((([e,t])=>null!=t)).map((([e,t])=>[e,t===Object(t)?this.removeEmpty(t):t])))}generateLogsFile(e){const t={version:1,disable_existing_loggers:!1,formatters:{LogFormatter:{class:"logging.Formatter",format:e.logFormat,datefmt:e.dateFormat}},handlers:{consoleHandler:{class:"logging.StreamHandler",formatter:"LogFormatter",level:0,stream:"ext://sys.stdout"},...this.useUpdatedLogs?{}:{databaseHandler:{class:this.useUpdatedLogs?Y3:X3,formatter:"LogFormatter",filename:"./logs/database.log",backupCount:1,encoding:"utf-8"}}},loggers:{...this.useUpdatedLogs?{}:{database:{handlers:["databaseHandler","consoleHandler"],level:"DEBUG",propagate:!1}}},root:{level:"ERROR",handlers:["consoleHandler"]},ts:(new Date).getTime()};return this.addLocalLoggers(t,e?.local),t}addLocalLoggers(e,t){if(t)for(const n of Object.keys(t))e.handlers[n+"Handler"]=this.createHandlerObj(t[n],n),e.loggers[n]=this.createLoggerObj(t[n],n)}createHandlerObj(e,t){return{class:this.useUpdatedLogs?Y3:X3,formatter:"LogFormatter",filename:`${e.filePath}/${t}.log`,backupCount:e.backupCount,interval:e.savingTime,when:e.savingPeriod,encoding:"utf-8"}}createLoggerObj(e,t){return{handlers:[`${t}Handler`,"consoleHandler"],level:e.logLevel,propagate:!1}}fetchConfigAttribute(e){e.id!==B&&this.attributeService.getEntityAttributes(e,D.CLIENT_SCOPE).pipe(we((t=>t.length?ae(t):this.attributeService.getEntityAttributes(e,D.SHARED_SCOPE,this.gatewayConfigAttributeKeys))),bn(this.destroyRef)).subscribe((e=>{this.gatewayVersion=e.find((e=>"Version"===e.key))?.value,this.useUpdatedLogs=za.parseVersion(this.gatewayVersion??pt.Legacy)>=za.parseVersion(pt.v3_6_3),this.updateConfigs(e),this.cd.detectChanges()}))}updateConfigs(e){let t={},n=lt.NONE;this.gatewayCredentialsService.setInitialCredentials(this.device),e.forEach((e=>{switch(e.key){case"general_configuration":e.value&&(t={...t,thingsboard:e.value});break;case"grpc_configuration":e.value&&(t={...t,grpc:e.value});break;case"logs_configuration":e.value&&(t={...t,logs:this.logsToObj(e.value)});break;case"storage_configuration":e.value&&(t={...t,storage:e.value});break;case"mode":t={...t,mode:e.value??Qt.BASIC};break;case"RemoteLoggingLevel":e.value&&(n=e.value)}})),t.logs&&(t={...t,logs:{...t.logs,logLevel:n}}),t.thingsboard?.security?this.gatewayCredentialsService.initialCredentials$.pipe(ue(Boolean),ye(1),bn(this.destroyRef)).subscribe((e=>{this.gatewayCredentialsService.shouldUpdateSecurityConfig(t.thingsboard.security)&&(t.thingsboard.security=this.gatewayCredentialsService.credentialsToSecurityConfig(e)),this.gatewayConfigGroup.get("basicConfig").patchValue(t,{emitEvent:!1}),this.gatewayConfigGroup.get("advancedConfig").patchValue(t,{emitEvent:!1})})):(this.gatewayConfigGroup.get("basicConfig").patchValue(t,{emitEvent:!1}),this.gatewayConfigGroup.get("advancedConfig").patchValue(t,{emitEvent:!1}))}logsToObj(e){const{format:t,datefmt:n}=e.formatters.LogFormatter;return{local:Object.keys(z3).reduce(((t,n)=>{const i=e.handlers[`${n}Handler`]||{},a=e.loggers[n]||{};return t[n]={logLevel:a.level||lt.INFO,filePath:i.filename?.split(`/${n}`)[0]||"./logs",backupCount:i.backupCount||7,savingTime:i.interval||3,savingPeriod:i.when||H3.days},t}),{}),logFormat:t,dateFormat:n}}static{this.ɵfac=function(e){return new(e||E6)(t.ɵɵdirectiveInject(H.FormBuilder),t.ɵɵdirectiveInject(_e.AttributeService),t.ɵɵdirectiveInject(t.ChangeDetectorRef),t.ɵɵdirectiveInject(y5),t.ɵɵdirectiveInject(t.DestroyRef))}}static{this.ɵcmp=t.ɵɵdefineComponent({type:E6,selectors:[["tb-gateway-configuration"]],inputs:{device:"device",defaultTab:"defaultTab",dialogRef:"dialogRef"},standalone:!0,features:[t.ɵɵProvidersFeature([y5]),t.ɵɵStandaloneFeature],decls:24,vars:24,consts:[[1,"flex","size-full","max-w-full","flex-col",3,"formGroup"],["color","primary"],[1,"tb-flex","space-between","align-center","h-16"],["tbTruncateWithTooltip",""],[1,"flex","items-center"],["formControlName","mode",3,"appearance"],[3,"value"],["mat-icon-button","","type","button",3,"click",4,"ngIf"],[1,"content-wrapper","flex-1"],["formControlName","basicConfig",3,"device","defaultTab","gatewayVersion","dialogMode","withReportStrategy"],["formControlName","advancedConfig"],[1,"flex","h-full","items-center","justify-end","gap-2","pr-4"],["mat-button","","color","primary","type","button"],["mat-raised-button","","color","primary","type","button",3,"click","disabled"],["mat-icon-button","","type","button",3,"click"],[1,"material-icons"],["formControlName","basicConfig",3,"initialized","device","defaultTab","gatewayVersion","dialogMode","withReportStrategy"],["mat-button","","color","primary","type","button",3,"click"]],template:function(e,n){1&e&&(t.ɵɵelementStart(0,"div",0)(1,"mat-toolbar",1)(2,"div",2)(3,"h2")(4,"div",3),t.ɵɵtext(5),t.ɵɵpipe(6,"translate"),t.ɵɵelementEnd()(),t.ɵɵelementStart(7,"div",4)(8,"tb-toggle-select",5)(9,"tb-toggle-option",6),t.ɵɵtext(10),t.ɵɵpipe(11,"translate"),t.ɵɵelementEnd(),t.ɵɵelementStart(12,"tb-toggle-option",6),t.ɵɵtext(13),t.ɵɵpipe(14,"translate"),t.ɵɵelementEnd()(),t.ɵɵtemplate(15,C6,3,0,"button",7),t.ɵɵelementEnd()()(),t.ɵɵelementStart(16,"div",8),t.ɵɵtemplate(17,_6,2,7,"tb-gateway-basic-configuration",9)(18,T6,1,0,"tb-gateway-advanced-configuration",10),t.ɵɵelementEnd(),t.ɵɵelementStart(19,"div",11),t.ɵɵtemplate(20,I6,3,3,"button",12),t.ɵɵelementStart(21,"button",13),t.ɵɵlistener("click",(function(){return n.saveConfig()})),t.ɵɵtext(22),t.ɵɵpipe(23,"translate"),t.ɵɵelementEnd()()()),2&e&&(t.ɵɵproperty("formGroup",n.gatewayConfigGroup),t.ɵɵadvance(),t.ɵɵclassProp("page-header",!n.dialogRef),t.ɵɵadvance(4),t.ɵɵtextInterpolate(t.ɵɵpipeBind1(6,16,"gateway.gateway-configuration")),t.ɵɵadvance(3),t.ɵɵclassProp("dialog-toggle",!!n.dialogRef),t.ɵɵpropertyInterpolate("appearance",n.dialogRef?"stroked":"fill"),t.ɵɵadvance(),t.ɵɵproperty("value",n.ConfigurationModes.BASIC),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(11,18,"gateway.basic")," "),t.ɵɵadvance(2),t.ɵɵproperty("value",n.ConfigurationModes.ADVANCED),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(14,20,"gateway.advanced")," "),t.ɵɵadvance(2),t.ɵɵproperty("ngIf",n.dialogRef),t.ɵɵadvance(2),t.ɵɵconditional(n.gatewayConfigGroup.get("mode").value===n.ConfigurationModes.BASIC?17:18),t.ɵɵadvance(3),t.ɵɵconditional(n.dialogRef?20:-1),t.ɵɵadvance(),t.ɵɵproperty("disabled",n.gatewayConfigGroup.invalid||!n.gatewayConfigGroup.dirty),t.ɵɵadvance(),t.ɵɵtextInterpolate1(" ",t.ɵɵpipeBind1(23,22,"action.save")," "))},dependencies:t.ɵɵgetComponentDepsFactory(E6,[j,_,Ka,w6,S6]),styles:['@charset "UTF-8";[_nghost-%COMP%]{overflow:hidden;padding:0!important;max-height:75vh;height:75vh;width:800px;max-width:80vw}@media screen and (max-width: 599px){[_nghost-%COMP%]{max-height:100%;height:100%;width:100%;max-width:100%}}[_nghost-%COMP%] .page-header.mat-toolbar[_ngcontent-%COMP%]{background:transparent;color:#000000de!important}[_nghost-%COMP%] .content-wrapper[_ngcontent-%COMP%]{min-height:calc(100% - 116px)}.dialog-toggle[_ngcontent-%COMP%] .mat-button-toggle-button{color:#ffffffbf}']})}}e("GatewayConfigurationComponent",E6);class M6{constructor(){}static{this.ɵfac=function(e){return new(e||M6)}}static{this.ɵcmp=t.ɵɵdefineComponent({type:M6,selectors:[["tb-lib-styles-entry"]],standalone:!0,features:[t.ɵɵStandaloneFeature],decls:0,vars:0,template:function(e,t){},styles:['@charset "UTF-8";.gw-delete-icon-button{color:#0000008a}.gw-connector-table .mat-mdc-table{table-layout:fixed}.gw-chip-list .mdc-evolution-chip-set__chips{justify-content:flex-end;align-items:center}.gw-table-toolbar.mat-mdc-table-toolbar.mat-mdc-table-toolbar{padding:0 8px}.tb-default .h-16{height:4rem}.tb-default .h-\\[500px\\]{height:500px}.tb-default .max-h-full{max-height:100%}.tb-default .min-h-10{min-height:2.5rem}.tb-default .\\!w-1\\/5{width:20%!important}.tb-default .w-1\\/2{width:50%}.tb-default .w-12{width:3rem}.tb-default .w-\\[77vw\\]{width:77vw}.tb-default .min-w-16{min-width:4rem}.tb-default .min-w-24{min-width:6rem}.tb-default .max-w-2xl{max-width:42rem}.tb-default .max-w-3xl{max-width:48rem}.tb-default .max-w-4\\/12{max-width:33.333333%}.tb-default .max-w-\\[11vw\\]{max-width:11vw}.tb-default .max-w-full{max-width:100%}.tb-default .flex-\\[1_1_30px\\]{flex:1 1 30px}.tb-default .flex-\\[1_1_33\\%\\]{flex:1 1 33%}.tb-default .flex-grow{flex-grow:1}.tb-default .gap-1\\.25{gap:.3125rem}.tb-default .p-1{padding:.25rem}.tb-default .\\!pl-0{padding-left:0!important}.tb-default .pr-4{padding-right:1rem}.tb-default .pt-4{padding-top:1rem}.tb-default .text-center{text-align:center}@media (max-width: 599px){.tb-default .lt-sm\\:flex-col{flex-direction:column}}\n'],encapsulation:2})}}const k6=(e,t)=>{const n=e[y];if(n.styles?.length){const e=n.styles[0];let i=document.getElementById(t);if(!i){i=document.createElement("style"),i.id=t;(document.head||document.getElementsByTagName("head")[0]).appendChild(i)}i.innerHTML=e}};class P6{constructor(e){this.translate=e,function(e){e.setTranslation("en_US",gt,!0),e.setTranslation("ar_AE",ft,!0),e.setTranslation("ca_ES",yt,!0),e.setTranslation("cs_CZ",vt,!0),e.setTranslation("da_DK",xt,!0),e.setTranslation("es_ES",bt,!0),e.setTranslation("ko_KR",wt,!0),e.setTranslation("lt_LT",St,!0),e.setTranslation("nl_BE",Ct,!0),e.setTranslation("pl_PL",_t,!0),e.setTranslation("pt_BR",Tt,!0),e.setTranslation("sl_SI",It,!0),e.setTranslation("tr_TR",Et,!0),e.setTranslation("zh_CN",Mt,!0),e.setTranslation("zh_TW",kt,!0)}(e),(e=>{k6(M6,e)})("tb-gateway-css")}static{this.ɵfac=function(e){return new(e||P6)(t.ɵɵinject(He.TranslateService))}}static{this.ɵmod=t.ɵɵdefineNgModule({type:P6})}static{this.ɵinj=t.ɵɵdefineInjector({imports:[j,_,$e,hj,sW,i$,E6,xn,V3,L3]})}}e("GatewayExtensionModule",P6),("undefined"==typeof ngJitMode||ngJitMode)&&t.ɵɵsetNgModuleScope(P6,{imports:[j,_,$e,hj,sW,i$,E6,xn,V3,L3]})}}}));//# sourceMappingURL=gateway-management-extension.js.map diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 98c7a93c52..2f9c0cd302 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,33 +14,14 @@ -- limitations under the License. -- --- UPDATE OTA PACKAGE EXTERNAL ID START +-- CALCULATED FIELD ADDITIONAL INFO ADDITION START -ALTER TABLE ota_package - ADD COLUMN IF NOT EXISTS external_id uuid; +ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar; -DO -$$ - BEGIN - IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN - ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); - END IF; - END; -$$; +-- CALCULATED FIELD ADDITIONAL INFO ADDITION END --- UPDATE OTA PACKAGE EXTERNAL ID END +-- RULE CHAIN NOTES MIGRATION START --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START +ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS notes varchar(1000000); -DROP INDEX IF EXISTS idx_device_external_id; -DROP INDEX IF EXISTS idx_device_profile_external_id; -DROP INDEX IF EXISTS idx_asset_external_id; -DROP INDEX IF EXISTS idx_entity_view_external_id; -DROP INDEX IF EXISTS idx_rule_chain_external_id; -DROP INDEX IF EXISTS idx_dashboard_external_id; -DROP INDEX IF EXISTS idx_customer_external_id; -DROP INDEX IF EXISTS idx_widgets_bundle_external_id; - --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END - -ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); \ No newline at end of file +-- RULE CHAIN NOTES MIGRATION END diff --git a/application/src/main/data/upgrade/lts/schema_update.sql b/application/src/main/data/upgrade/lts/schema_update.sql index 3140b4f253..790dc50b52 100644 --- a/application/src/main/data/upgrade/lts/schema_update.sql +++ b/application/src/main/data/upgrade/lts/schema_update.sql @@ -17,3 +17,9 @@ -- LTS cumulative schema update file. -- All statements must be idempotent (use IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, DO $$ ... END $$ guards, etc.). -- This file is executed by SystemPatchApplier on every version increase within the LTS family. + +-- CALCULATED FIELD ADDITIONAL INFO ADDITION START + +ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar; + +-- CALCULATED FIELD ADDITIONAL INFO ADDITION 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 925b60de76..4266c97303 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.actors; +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; @@ -57,7 +58,6 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; @@ -97,6 +97,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; @@ -120,7 +121,7 @@ import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.cf.CalculatedFieldStateService; -import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; @@ -146,14 +147,12 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig; import java.io.PrintWriter; import java.io.StringWriter; import java.util.List; -import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; @Slf4j @Component @@ -575,6 +574,14 @@ public class ActorSystemContext { @Getter private JobManager jobManager; + @Autowired + @Getter + private ApiKeyService apiKeyService; + + @Autowired + @Getter + private OwnerService ownerService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private int maxConcurrentSessionsPerDevice; @@ -862,7 +869,7 @@ public class ActorSystemContext { persistCalculatedFieldDebugEvent(cfe.getCtx().getTenantId(), cfe.getCtx().getCfId(), cfe.getEventEntity(), cfe.getArguments(), cfe.getMsgId(), cfe.getMsgType(), null, message); } - public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map arguments, UUID tbMsgId, TbMsgType tbMsgType, String result, String errorMessage) { + public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, JsonNode arguments, UUID tbMsgId, String tbMsgType, String result, String errorMessage) { if (checkLimits(tenantId)) { try { CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder() @@ -875,13 +882,10 @@ public class ActorSystemContext { eventBuilder.msgId(tbMsgId); } if (tbMsgType != null) { - eventBuilder.msgType(tbMsgType.name()); + eventBuilder.msgType(tbMsgType); } - if (arguments != null) { - eventBuilder.arguments(JacksonUtil.toString( - arguments.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toTbelCfArg())) - )); + if (arguments != null && !arguments.isEmpty()) { + eventBuilder.arguments(JacksonUtil.toString(arguments)); } if (result != null) { eventBuilder.result(result); @@ -889,8 +893,9 @@ public class ActorSystemContext { if (errorMessage != null) { eventBuilder.error(errorMessage); } - - ListenableFuture future = eventService.saveAsync(eventBuilder.build()); + CalculatedFieldDebugEvent event = eventBuilder.build(); + log.debug("Persisting calculated field debug event: {}", event); + ListenableFuture future = eventService.saveAsync(event); Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); } catch (IllegalArgumentException ex) { log.warn("Failed to persist calculated field debug message", ex); @@ -900,7 +905,7 @@ public class ActorSystemContext { private boolean checkLimits(TenantId tenantId) { if (debugModeRateLimitsConfig.isCalculatedFieldDebugPerTenantLimitsEnabled() && - !rateLimitService.checkRateLimit(LimitedApi.CALCULATED_FIELD_DEBUG_EVENTS, (Object) tenantId, debugModeRateLimitsConfig.getCalculatedFieldDebugPerTenantLimitsConfiguration())) { + !rateLimitService.checkRateLimit(LimitedApi.CALCULATED_FIELD_DEBUG_EVENTS, (Object) tenantId, debugModeRateLimitsConfig.getCalculatedFieldDebugPerTenantLimitsConfiguration())) { log.trace("[{}] Calculated field debug event limits exceeded!", tenantId); return false; } @@ -924,12 +929,13 @@ public class ActorSystemContext { return getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS); } - public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { + public ScheduledFuture scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs); if (delayInMs > 0) { - getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); + return getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); } else { ctx.tell(msg); + return null; } } 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 6b780a2945..7e6cfc1b15 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 @@ -32,6 +32,7 @@ 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.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.MsgType; @@ -43,7 +44,6 @@ 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.tenant.TenantService; -import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; import java.util.Optional; @@ -89,16 +89,20 @@ public class AppActor extends ContextAwareActor { break; case PARTITION_CHANGE_MSG: case CF_PARTITIONS_CHANGE_MSG: + case CF_STATE_PARTITION_RESTORE_MSG: ctx.broadcastToChildren(msg, true); break; case COMPONENT_LIFE_CYCLE_MSG: onComponentLifecycleMsg((ComponentLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + forwardToTenantActor((TenantAwareMsg) msg, true); + break; case QUEUE_TO_RULE_ENGINE_MSG: onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg); break; case TRANSPORT_TO_DEVICE_ACTOR_MSG: - onToDeviceActorMsg((TenantAwareMsg) msg, false); + forwardToTenantActor((TenantAwareMsg) msg, false); break; case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: @@ -108,7 +112,7 @@ public class AppActor extends ContextAwareActor { case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case REMOVE_RPC_TO_DEVICE_ACTOR_MSG: - onToDeviceActorMsg((TenantAwareMsg) msg, true); + forwardToTenantActor((TenantAwareMsg) msg, true); break; case SESSION_TIMEOUT_MSG: ctx.broadcastToChildrenByType(msg, EntityType.TENANT); @@ -117,11 +121,11 @@ public class AppActor extends ContextAwareActor { case CF_STATE_RESTORE_MSG: //TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. // same for the Linked telemetry. - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); + forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false); + forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, false); break; default: return false; @@ -162,7 +166,19 @@ public class AppActor extends ContextAwareActor { private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) { TbActorRef target = null; if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) { - if (!EntityType.TENANT_PROFILE.equals(msg.getEntityId().getEntityType())) { + if (systemContext.isTenantComponentsInitEnabled()) { + if (msg.getEntityId() instanceof TenantProfileId tenantProfileId) { + tenantService.findTenantIdsByTenantProfileId(tenantProfileId).forEach(tenantId -> { + getOrCreateTenantActor(tenantId).ifPresentOrElse(tenantActor -> { + log.debug("[{}] Sending component lifecycle msg for tenant.", tenantId); + tenantActor.tellWithHighPriority(msg); + }, () -> { + log.debug("Ignoring component lifecycle msg for tenant {} because it is not managed by this service", tenantId); + }); + }); + } + } + if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE, EntityType.USER)) { log.warn("Message has system tenant id: {}", msg); } } else { @@ -187,7 +203,7 @@ public class AppActor extends ContextAwareActor { } } - private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + private void forwardToTenantActor(TenantAwareMsg msg, boolean priority) { getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> { if (priority) { tenantActor.tellWithHighPriority(msg); @@ -199,21 +215,6 @@ public class AppActor extends ContextAwareActor { }); } - - private void onToDeviceActorMsg(TenantAwareMsg msg, boolean priority) { - getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> { - if (priority) { - tenantActor.tellWithHighPriority(msg); - } else { - tenantActor.tell(msg); - } - }, () -> { - if (msg instanceof TransportToDeviceActorMsgWrapper) { - ((TransportToDeviceActorMsgWrapper) msg).getCallback().onSuccess(); - } - }); - } - private Optional getOrCreateTenantActor(TenantId tenantId) { if (deletedTenants.contains(tenantId)) { return Optional.empty(); @@ -245,6 +246,7 @@ public class AppActor extends ContextAwareActor { public TbActor createActor() { return new AppActor(context); } + } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java new file mode 100644 index 0000000000..26b00bdfe2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; + +@Data +@Builder +public class CalculatedFieldAlarmActionMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final Alarm alarm; + private final ActionType action; + private final TbCallback callback; + + @Override + public MsgType getMsgType() { + return MsgType.CF_ALARM_ACTION_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java new file mode 100644 index 0000000000..01a61a0158 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldArgumentResetMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final CalculatedFieldCtx ctx; + private final CalculatedFieldEventType eventType; + private final TbCallback callback; + + @Override + public MsgType getMsgType() { + return MsgType.CF_ARGUMENT_RESET_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java new file mode 100644 index 0000000000..582a150f9c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto; + +@Data +@Builder +public class CalculatedFieldEntityActionEventMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final EntityId entityId; + private final JsonNode entity; + private final ActionType action; + private final TbCallback callback; + + public static CalculatedFieldEntityActionEventMsg fromProto(EntityActionEventProto proto, + TbCallback callback) { + return CalculatedFieldEntityActionEventMsg.builder() + .tenantId((TenantId) ProtoUtils.fromProto(proto.getTenantId())) + .entityId(ProtoUtils.fromProto(proto.getEntityId())) + .entity(JacksonUtil.toJsonNode(proto.getEntity())) + .action(ActionType.valueOf(proto.getAction())) + .callback(callback) + .build(); + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_ENTITY_ACTION_EVENT_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index c2f0208b56..dbc97bb240 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -21,6 +21,7 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; @@ -51,7 +52,7 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { @Override public void destroy(TbActorStopReason stopReason, Throwable cause) throws TbActorException { log.debug("[{}] Stopping CF entity actor.", processor.tenantId); - processor.stop(); + processor.stop(false); } @Override @@ -63,18 +64,33 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_STATE_RESTORE_MSG: processor.process((CalculatedFieldStateRestoreMsg) msg); break; + case CF_STATE_PARTITION_RESTORE_MSG: + processor.process((CalculatedFieldStatePartitionRestoreMsg) msg); + break; case CF_ENTITY_INIT_CF_MSG: processor.process((EntityInitCalculatedFieldMsg) msg); break; case CF_ENTITY_DELETE_MSG: processor.process((CalculatedFieldEntityDeleteMsg) msg); break; + case CF_RELATION_ACTION_MSG: + processor.process((CalculatedFieldRelationActionMsg) msg); + break; case CF_ENTITY_TELEMETRY_MSG: processor.process((EntityCalculatedFieldTelemetryMsg) msg); break; case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; + case CF_REEVALUATE_MSG: + processor.process((CalculatedFieldReevaluateMsg) msg); + break; + case CF_ALARM_ACTION_MSG: + processor.process((CalculatedFieldAlarmActionMsg) msg); + break; + case CF_ARGUMENT_RESET_MSG: + processor.process((CalculatedFieldArgumentResetMsg) msg); + break; default: return false; } @@ -85,4 +101,5 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { void logProcessingException(Exception e) { log.warn("[{}][{}] Processing failure", tenantId, processor.entityId, e); } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 4f59628656..a2dc9b50f4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -21,10 +21,14 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; +import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.cf.CalculatedFieldEventType; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -33,6 +37,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -50,6 +56,12 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.util.ArrayList; import java.util.Collection; @@ -64,7 +76,9 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry.getValueForTsRecord; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; /** * @author Andrew Shvayka @@ -79,7 +93,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM final CalculatedFieldProcessingService cfService; final CalculatedFieldStateService cfStateService; - TbActorCtx ctx; + TbActorCtx actorCtx; Map states = new HashMap<>(); CalculatedFieldEntityMessageProcessor(ActorSystemContext systemContext, TenantId tenantId, EntityId entityId) { @@ -91,48 +105,79 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } void init(TbActorCtx ctx) { - this.ctx = ctx; + this.actorCtx = ctx; } - public void stop() { - log.info("[{}][{}] Stopping entity actor.", tenantId, entityId); + public void stop(boolean partitionChanged) { + log.info(partitionChanged ? + "[{}][{}] Stopping entity actor due to change partition event." : + "[{}][{}] Stopping entity actor.", + tenantId, entityId); + states.values().forEach(this::closeState); states.clear(); - ctx.stop(ctx.getSelf()); + actorCtx.stop(actorCtx.getSelf()); } public void process(CalculatedFieldPartitionChangeMsg msg) { if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { - log.info("[{}] Stopping entity actor due to change partition event.", entityId); - ctx.stop(ctx.getSelf()); + stop(true); } } public void process(CalculatedFieldStateRestoreMsg msg) { CalculatedFieldId cfId = msg.getId().cfId(); log.debug("[{}] [{}] Processing CF state restore msg.", msg.getId().entityId(), cfId); - if (msg.getState() != null) { - states.put(cfId, msg.getState()); + CalculatedFieldState state = msg.getState(); + if (state != null) { + state.setCtx(msg.getCtx(), actorCtx); + state.setPartition(msg.getPartition()); + states.put(cfId, state); } else { - states.remove(cfId); + removeState(cfId); } msg.getCallback().onSuccess(); } + public void process(CalculatedFieldStatePartitionRestoreMsg msg) { + log.debug("Processing CF state partition restore msg: {}", msg); + for (CalculatedFieldState state : states.values()) { + if (msg.getPartition().equals(state.getPartition())) { + state.init(true); + } + } + } + public void process(EntityInitCalculatedFieldMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing entity init CF msg.", msg.getCtx().getCfId()); + log.debug("[{}] Processing entity init CF msg: {}", msg.getCtx().getCfId(), msg); var ctx = msg.getCtx(); - if (msg.isForceReinit()) { - log.debug("Force reinitialization of CF: [{}].", ctx.getCfId()); - states.remove(ctx.getCfId()); + CalculatedFieldState state; + if (msg.getStateAction() == StateAction.RECREATE) { + removeState(ctx.getCfId()); + state = null; + } else { + state = states.get(ctx.getCfId()); } try { - var state = getOrInitState(ctx); - if (state.isSizeOk()) { - processStateIfReady(ctx, Collections.singletonList(ctx.getCfId()), state, null, null, msg.getCallback()); + if (state == null) { + state = createState(ctx); + } else if (msg.getStateAction() == StateAction.REINIT) { + log.debug("Force reinitialization of CF: [{}].", ctx.getCfId()); + state.reset(); + initState(state, ctx); } else { - throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); + state.setCtx(ctx, actorCtx); + } + if (msg.getStateAction() != StateAction.REFRESH_CTX) { + if (state.isSizeOk()) { + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, msg.getEventType().name(), msg.getCallback()); + } else { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); + } + } else { + msg.getCallback().onSuccess(); } } catch (Exception e) { + log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -140,31 +185,133 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - public void process(CalculatedFieldEntityDeleteMsg msg) { + public void process(CalculatedFieldArgumentResetMsg msg) throws CalculatedFieldException { + log.debug("[{}] Processing CF argument reset msg.", entityId); + var ctx = msg.getCtx(); + try { + Map dynamicSourceArgs = ctx.getArguments().entrySet().stream() + .filter(entry -> entry.getValue().hasOwnerSource()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs); + fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); + + processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), msg.getCallback(), fetchedArgs, null, msg.getEventType().name()); + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } + + public void process(CalculatedFieldEntityDeleteMsg msg) throws CalculatedFieldException { log.debug("[{}] Processing CF entity delete msg.", msg.getEntityId()); if (this.entityId.equals(msg.getEntityId())) { if (states.isEmpty()) { msg.getCallback().onSuccess(); } else { MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); - states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); - ctx.stop(ctx.getSelf()); + states.forEach((cfId, state) -> cfStateService.deleteState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); + actorCtx.stop(actorCtx.getSelf()); } } else { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); - var state = states.remove(cfId); + var state = removeState(cfId); if (state != null) { - cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); + cfStateService.deleteState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); } else { msg.getCallback().onSuccess(); } } } + public void process(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + log.debug("[{}] Processing CF {} related entity msg.", msg.getRelatedEntityId(), msg.getAction()); + switch (msg.getAction()) { + case UPDATED -> handleRelationUpdate(msg); + case DELETED -> handleRelationDelete(msg); + default -> msg.getCallback().onSuccess(); + } + } + + private void handleRelationUpdate(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx ctx = msg.getCalculatedField(); + var state = states.get(ctx.getCfId()); + try { + Map updatedArgs = null; + if (state == null) { + state = createState(ctx); + } else { + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments()); + updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs)); + } + if (state instanceof PropagationCalculatedFieldState propagationState) { + PropagationArgumentEntry entry = new PropagationArgumentEntry(); + entry.setAdded(List.of(msg.getRelatedEntityId())); + updatedArgs = propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx); + } + if (CollectionsUtil.isEmpty(updatedArgs)) { + msg.getCallback().onSuccess(); + return; + } + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + } + if (state.isSizeOk()) { + processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, TbMsgType.RELATION_ADD_OR_UPDATE.name(), msg.getCallback()); + } else { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); + } + } catch (Exception e) { + log.debug("[{}][{}] Failed to handle relation update", entityId, ctx.getCfId(), e); + if (e instanceof CalculatedFieldException cfe) { + throw cfe; + } + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } + + private void handleRelationDelete(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx ctx = msg.getCalculatedField(); + CalculatedFieldId cfId = ctx.getCfId(); + CalculatedFieldState state = states.get(cfId); + try { + if (state == null) { + msg.getCallback().onSuccess(); + return; + } + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) { + aggState.cleanupEntityData(msg.getRelatedEntityId()); + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + + if (state.isSizeOk()) { + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, TbMsgType.RELATION_DELETED.name(), msg.getCallback()); + } else { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); + } + return; + } + if (state instanceof PropagationCalculatedFieldState propagationState) { + PropagationArgumentEntry entry = new PropagationArgumentEntry(); + entry.setRemoved(msg.getRelatedEntityId()); + propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx); + if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), null, TbMsgType.RELATION_DELETED.name(), null, null); + } + } + msg.getCallback().onSuccess(); + } catch (Exception e) { + log.debug("[{}][{}] Failed to handle relation delete", entityId, ctx.getCfId(), e); + if (e instanceof CalculatedFieldException cfe) { + throw cfe; + } + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } + public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); - var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); + var numberOfCallbacks = msg.getEntityIdFields().size() + msg.getProfileIdFields().size(); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); List cfIdList = getCalculatedFieldIds(proto); Set cfIdSet = new HashSet<>(cfIdList); @@ -177,28 +324,29 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF link telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); var ctx = msg.getCtx(); - var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); + var callback = msg.getCallback(); try { List cfIds = getCalculatedFieldIds(proto); if (cfIds.contains(ctx.getCfId())) { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } else { if (proto.getTsDataCount() > 0) { - processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); + processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toMsgType(proto)); } else if (proto.getAttrDataCount() > 0) { - processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); + processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toMsgType(proto)); } else if (proto.getRemovedTsKeysCount() > 0) { - processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, msg.getEntityId(), proto.getRemovedTsKeysList()), toTbMsgId(proto), toMsgType(proto)); } else if (proto.getRemovedAttrKeysCount() > 0) { - processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toMsgType(proto)); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -206,10 +354,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection cfIds, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection cfIds, List cfIdList, TbCallback callback) throws CalculatedFieldException { try { if (cfIds.contains(ctx.getCfId())) { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } else { if (proto.getTsDataCount() > 0) { processTelemetry(ctx, proto, cfIdList, callback); @@ -220,10 +368,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else if (proto.getRemovedAttrKeysCount() > 0) { processRemovedAttributes(ctx, proto, cfIdList, callback); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process CF telemetry msg: {}", entityId, ctx.getCfId(), proto, e); if (e instanceof CalculatedFieldException cfe) { if (DebugModeUtil.isDebugFailuresAvailable(cfe.getCtx().getCalculatedField())) { systemContext.persistCalculatedFieldDebugError(cfe); @@ -235,88 +384,163 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { - processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); + public void process(CalculatedFieldReevaluateMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx ctx = msg.getCtx(); + CalculatedFieldId cfId = ctx.getCfId(); + CalculatedFieldState state = states.get(cfId); + if (state == null) { + log.warn("[{}][{}] Failed to find CF state (probably wasn't restored properly) for entity to handle {}", entityId, cfId, msg); + state = createState(ctx); + } + if (state.isSizeOk()) { + log.debug("[{}][{}] Reevaluating CF state", entityId, cfId); + processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, CalculatedFieldEventType.REEVALUATION_MSG.name(), msg.getCallback()); + } else { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); + } + } + + public void process(CalculatedFieldAlarmActionMsg msg) { + log.debug("[{}] Processing alarm action event msg: {}", entityId, msg); + for (CalculatedFieldState state : states.values()) { + if (state instanceof AlarmCalculatedFieldState alarmCfState) { + Alarm stateAlarm = alarmCfState.getCurrentAlarm(); + if (stateAlarm != null && stateAlarm.getId().equals(msg.getAlarm().getId())) { + alarmCfState.processAlarmAction(msg.getAlarm(), msg.getAction()); + } + } + } + msg.getCallback().onSuccess(); + } + + private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { + processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toMsgType(proto)); } - private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { - processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); + private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { + processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toMsgType(proto)); } - private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { - processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { + processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, entityId, proto.getRemovedTsKeysList()), toTbMsgId(proto), toMsgType(proto)); } - private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { - processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { + processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toMsgType(proto)); } - private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List cfIdList, MultipleTbCallback callback, - Map newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException { + private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List cfIdList, TbCallback callback, + Map newArgValues, UUID tbMsgId, String msgType) throws CalculatedFieldException { if (newArgValues.isEmpty()) { log.debug("[{}] No new argument values to process for CF.", ctx.getCfId()); - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } CalculatedFieldState state = states.get(ctx.getCfId()); boolean justRestored = false; if (state == null) { - state = getOrInitState(ctx); + state = createState(ctx); justRestored = true; + } else if (ctx.shouldFetchRelatedEntities(state)) { + log.debug("[{}][{}] Going to update related entities for CF.", entityId, ctx.getCfId()); + try { + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesState) { + List relatedEntities = cfService.fetchRelatedEntities(ctx, entityId); + List missingEntities = relatedEntitiesState.checkRelatedEntities(relatedEntities); + if (!missingEntities.isEmpty()) { + missingEntities.forEach(missingEntityId -> { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, missingEntityId, ctx.getArguments()); + relatedEntitiesState.updateEntityData(setEntityIdToSingleEntityArguments(missingEntityId, fetchedArgs)); + }); + justRestored = true; + } + } + if (state instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) { + Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); + dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); + geofencingCalculatedFieldState.updateScheduledRefreshTs(); + } + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } } if (state.isSizeOk()) { - if (state.updateState(ctx, newArgValues) || justRestored) { + Map updatedArgs = state.update(newArgValues, ctx); + if (!updatedArgs.isEmpty() || justRestored) { cfIdList = new ArrayList<>(cfIdList); cfIdList.add(ctx.getCfId()); - processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback); + processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, msgType, callback); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } else { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); } } - @SneakyThrows - private CalculatedFieldState getOrInitState(CalculatedFieldCtx ctx) { - CalculatedFieldState state = states.get(ctx.getCfId()); - if (state != null) { - return state; - } else { - ListenableFuture stateFuture = systemContext.getCalculatedFieldProcessingService().fetchStateFromDb(ctx, entityId); - // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. - // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. - // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, - // but this will significantly complicate the code. - state = stateFuture.get(1, TimeUnit.MINUTES); - state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); - states.put(ctx.getCfId(), state); - } + private CalculatedFieldState createState(CalculatedFieldCtx ctx) { + CalculatedFieldState state = createStateByType(ctx, entityId); + initState(state, ctx); return state; } - private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) { + state.setCtx(ctx, actorCtx); + state.init(false); + + if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isCfHasRelationPathQuerySource()) { + GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state; + geofencingState.updateScheduledRefreshTs(); + } + + Map arguments = fetchArguments(ctx); + state.update(arguments, ctx); + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + states.put(ctx.getCfId(), state); + } + + @SneakyThrows + private Map fetchArguments(CalculatedFieldCtx ctx) { + ListenableFuture> argumentsFuture = cfService.fetchArguments(ctx, entityId); + // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. + // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. + // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, + // but this will significantly complicate the code. + return argumentsFuture.get(1, TimeUnit.MINUTES); + } + + private void processStateIfReady(CalculatedFieldState state, Map updatedArgs, CalculatedFieldCtx ctx, + List cfIdList, UUID tbMsgId, String tbMsgType, TbCallback callback) throws CalculatedFieldException { + callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback); + log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + log.trace("[{}][{}] Performing calculation. Updated args: {}", entityId, ctx.getCfId(), updatedArgs); + CalculatedFieldResult calculationResult = state.performCalculation(updatedArgs, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { if (!calculationResult.isEmpty()) { - cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); + cfService.processResult(tenantId, entityId, ctx.getCfName(), calculationResult, cfIdList, callback); } else { callback.onSuccess(); } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { - systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResult().toString(), null); + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), tbMsgId, tbMsgType, calculationResult.stringValue(), null); } } } else { + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!"; + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), tbMsgId, tbMsgType, null, errorMsg); + } callback.onSuccess(); } } catch (Exception e) { - throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); + log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e); + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArgumentsJson()).cause(e).build(); } finally { if (!stateSizeChecked) { state.checkStateSize(ctxId, ctx.getMaxStateSize()); @@ -324,14 +548,30 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state.isSizeOk()) { cfStateService.persistState(ctxId, state, callback); } else { - removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); + deleteStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); } } } - private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException { + private CalculatedFieldState removeState(CalculatedFieldId cfId) { + CalculatedFieldState state = states.remove(cfId); + closeState(state); + return state; + } + + private void closeState(CalculatedFieldState state) { + if (state != null) { + try { + state.close(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to close CF state", tenantId, state.getEntityId(), e); + } + } + } + + private void deleteStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException { // We remove the state, but remember that it is over-sized in a local map. - cfStateService.removeState(ctxId, new TbCallback() { + cfStateService.deleteState(ctxId, new TbCallback() { @Override public void onSuccess() { callback.onFailure(ex); @@ -346,129 +586,196 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArguments(CalculatedFieldCtx ctx, List data) { - return mapToArguments(ctx.getMainEntityArguments(), data); + return mapToArguments(entityId, ctx, ctx.getMainEntityArguments(), Collections.emptyMap(), data); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, List data) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - return mapToArguments(argNames, data); + return mapToArguments(entityId, ctx, ctx.getLinkedAndDynamicArgs(entityId), ctx.getRelatedEntityArguments(), data); } - private Map mapToArguments(Map> args, List data) { - if (args.isEmpty()) { - return Collections.emptyMap(); - } + private Map mapToArguments(EntityId originator, CalculatedFieldCtx ctx, Map> args, Map> relatedEntityArgs, List data) { Map arguments = new HashMap<>(); - for (TsKvProto item : data) { - ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); - Set argNames = args.get(key); - if (argNames != null) { + if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) { + for (TsKvProto item : data) { + ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); + + SingleValueArgumentEntry relatedArgIncoming = new SingleValueArgumentEntry(originator, item); + mapLatest(ctx, relatedArgIncoming, relatedEntityArgs.get(key), arguments); + SingleValueArgumentEntry incoming = new SingleValueArgumentEntry(item); - argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { - if (existing == null) { - return incoming; - } - existing.updateEntry(incoming); - return existing; - })); + mapLatest(ctx, incoming, args.get(key), arguments); + + key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); + mapRolling(item, args.get(key), arguments); } + } + return arguments; + } - key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); - argNames = args.get(key); - if (argNames != null) { - Double recordValue = getValueForTsRecord(ProtoUtils.fromProto(item.getKv())); - argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { - if (existing instanceof TsRollingArgumentEntry rolling) { - if (recordValue != null) { - rolling.getTsRecords().put(item.getTs(), recordValue); - } - return rolling; - } - TsRollingArgumentEntry rolling = new TsRollingArgumentEntry(); + private void mapLatest(CalculatedFieldCtx ctx, + SingleValueArgumentEntry incoming, + Set argNames, + Map arguments) { + if (argNames != null) { + argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { + if (existing == null) { + return incoming; + } + existing.updateEntry(incoming, ctx); + return existing; + })); + } + } + + private void mapRolling(TsKvProto item, + Set argNames, + Map arguments) { + if (argNames != null) { + Double recordValue = getValueForTsRecord(ProtoUtils.fromProto(item.getKv())); + argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { + if (existing instanceof TsRollingArgumentEntry rolling) { if (recordValue != null) { rolling.getTsRecords().put(item.getTs(), recordValue); } - if (existing instanceof SingleValueArgumentEntry single) { - Double existingValue = getValueForTsRecord(single.getKvEntryValue()); - if (existingValue != null) { - rolling.getTsRecords().put(single.getTs(), existingValue); - } - } return rolling; - })); - } + } + TsRollingArgumentEntry rolling = new TsRollingArgumentEntry(); + if (recordValue != null) { + rolling.getTsRecords().put(item.getTs(), recordValue); + } + if (existing instanceof SingleValueArgumentEntry single) { + Double existingValue = getValueForTsRecord(single.getKvEntryValue()); + if (existingValue != null) { + rolling.getTsRecords().put(single.getTs(), existingValue); + } + } + return rolling; + })); } - return arguments; } private Map mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List attrDataList) { - return mapToArguments(ctx.getMainEntityArguments(), scope, attrDataList); + return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, attrDataList); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List attrDataList) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - return mapToArguments(argNames, scope, attrDataList); + var args = ctx.getLinkedAndDynamicArgs(entityId); + var relatedEntityArgs = ctx.getRelatedEntityArguments(); + List geofencingArgumentNames = ctx.getLinkedEntityAndCurrentOwnerGeofencingArgumentNames(); + return mapToArguments(entityId, args, geofencingArgumentNames, relatedEntityArgs, scope, attrDataList); } - private Map mapToArguments(Map> args, AttributeScopeProto scope, List attrDataList) { + private Map mapToArguments(EntityId entityId, Map> args, List geofencingArgNames, Map> relatedEntityArgs, AttributeScopeProto scope, List attrDataList) { + if (args.isEmpty() && relatedEntityArgs.isEmpty()) { + return Collections.emptyMap(); + } Map arguments = new HashMap<>(); for (AttributeValueProto item : attrDataList) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); - Set argNames = args.get(key); + Set argNames = relatedEntityArgs.get(key); if (argNames != null) { - argNames.forEach(argName -> arguments.put(argName, new SingleValueArgumentEntry(item))); + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(entityId, item)); + }); } + argNames = args.get(key); + if (argNames == null) { + continue; + } + argNames.forEach(argName -> { + if (geofencingArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); + } else { + arguments.put(argName, new SingleValueArgumentEntry(item)); + } + }); } return arguments; } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List removedAttrKeys) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys); + var args = ctx.getLinkedAndDynamicArgs(entityId); + var relatedEntityArgs = ctx.getRelatedEntityArguments(); + List geofencingArgumentNames = ctx.getLinkedEntityAndCurrentOwnerGeofencingArgumentNames(); + return mapToArgumentsWithDefaultValue(entityId, args, ctx.getArguments(), geofencingArgumentNames, relatedEntityArgs, scope, removedAttrKeys); } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List removedAttrKeys) { - return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys); + return mapToArgumentsWithDefaultValue(null, ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, removedAttrKeys); } - private Map mapToArgumentsWithDefaultValue(Map> args, Map configArguments, AttributeScopeProto scope, List removedAttrKeys) { + private Map mapToArgumentsWithDefaultValue(EntityId msgEntityId, + Map> args, + Map configArguments, + List geofencingArgNames, + Map> relatedEntityArgs, + AttributeScopeProto scope, + List removedAttrKeys) { + if (args.isEmpty() && relatedEntityArgs.isEmpty()) { + return Collections.emptyMap(); + } Map arguments = new HashMap<>(); for (String removedKey : removedAttrKeys) { ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); - Set argNames = args.get(key); + Set argNames = relatedEntityArgs.get(key); if (argNames != null) { argNames.forEach(argName -> { - Argument argument = configArguments.get(argName); - String defaultValue = (argument != null) ? argument.getDefaultValue() : null; - arguments.put(argName, StringUtils.isNotEmpty(defaultValue) - ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) - : new SingleValueArgumentEntry()); + String defaultValue = getDefaultValue(configArguments, argName); + SingleValueArgumentEntry argumentEntry = buildSingleValue(removedKey, defaultValue, System.currentTimeMillis()); + arguments.put(argName, new SingleValueArgumentEntry(msgEntityId, argumentEntry)); }); } + argNames = args.get(key); + if (argNames == null) { + continue; + } + argNames.forEach(argName -> { + if (geofencingArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry()); + } else { + String defaultValue = getDefaultValue(configArguments, argName); + SingleValueArgumentEntry argumentEntry = buildSingleValue(removedKey, defaultValue, System.currentTimeMillis()); + arguments.put(argName, new SingleValueArgumentEntry(argumentEntry)); + } + }); } return arguments; } - private Map mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, List removedTelemetryKeys) { + private String getDefaultValue(Map configArguments, String argNames) { + Argument argument = configArguments.get(argNames); + return argument != null ? argument.getDefaultValue() : null; + } + + private SingleValueArgumentEntry buildSingleValue(String attrKey, String defaultValue, long ts) { + return StringUtils.isNotEmpty(defaultValue) + ? new SingleValueArgumentEntry(ts, new StringDataEntry(attrKey, defaultValue), null) + : new SingleValueArgumentEntry(); + } + + private Map mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, EntityId entityId, List removedTelemetryKeys) { Map deletedArguments = ctx.getArguments().entrySet().stream() .filter(entry -> removedTelemetryKeys.contains(entry.getValue().getRefEntityKey().getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, deletedArguments); + if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(ctx.getCfType())) { + fetchedArgs = setEntityIdToSingleEntityArguments(entityId, fetchedArgs); + } fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); + return fetchedArgs; } + private Map setEntityIdToSingleEntityArguments(EntityId relatedEntityId, Map fetchedArgs) { + return fetchedArgs.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + argEntry -> new SingleValueArgumentEntry(relatedEntityId, argEntry.getValue()) + )); + } + private static List getCalculatedFieldIds(CalculatedFieldTelemetryMsgProto proto) { List cfIds = new LinkedList<>(); for (var cfId : proto.getPreviousCalculatedFieldsList()) { @@ -484,9 +791,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM return null; } - private TbMsgType toTbMsgType(CalculatedFieldTelemetryMsgProto proto) { + private String toMsgType(CalculatedFieldTelemetryMsgProto proto) { if (!proto.getTbMsgType().isEmpty()) { - return TbMsgType.valueOf(proto.getTbMsgType()); + return proto.getTbMsgType(); } return null; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java index de11061e02..97cb4c9640 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java @@ -15,14 +15,12 @@ */ package org.thingsboard.server.actors.calculatedField; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Builder; import lombok.Getter; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import java.util.Map; import java.util.UUID; @Getter @@ -32,8 +30,8 @@ public class CalculatedFieldException extends Exception { private final CalculatedFieldCtx ctx; private final EntityId eventEntity; private final UUID msgId; - private final TbMsgType msgType; - private Map arguments; + private final String msgType; + private JsonNode arguments; private String errorMessage; private Exception cause; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java index 5870e8ddb5..1d52c230d3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @Data public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSystemMsg { @@ -32,9 +31,9 @@ public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSyste private final CalculatedFieldLinkedTelemetryMsgProto proto; private final TbCallback callback; - @Override public MsgType getMsgType() { return MsgType.CF_LINKED_TELEMETRY_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index 16d5db362a..503de0e652 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -20,6 +20,7 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; @@ -70,9 +71,15 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_STATE_RESTORE_MSG: processor.onStateRestoreMsg((CalculatedFieldStateRestoreMsg) msg); break; + case CF_STATE_PARTITION_RESTORE_MSG: + processor.onStatePartitionRestoreMsg((CalculatedFieldStatePartitionRestoreMsg) msg); + break; case CF_ENTITY_LIFECYCLE_MSG: processor.onEntityLifecycleMsg((CalculatedFieldEntityLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + processor.onEntityActionEventMsg((CalculatedFieldEntityActionEventMsg) msg); + break; case CF_TELEMETRY_MSG: processor.onTelemetryMsg((CalculatedFieldTelemetryMsg) msg); break; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 65ec0b332b..a1d7477a36 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -16,23 +16,44 @@ package org.thingsboard.server.actors.calculatedField; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.function.TriConsumer; +import org.thingsboard.common.util.DebugModeUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; +import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldEventType; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; @@ -41,10 +62,13 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldStateService; +import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.cf.cache.TenantEntityProfileCache; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @@ -52,11 +76,21 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.ArrayList; +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.Optional; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -69,18 +103,25 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); + private final Map> ownerEntities = new HashMap<>(); + private ScheduledFuture cfsReevaluationTask; private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; private final CalculatedFieldService cfDaoService; private final DeviceService deviceService; private final AssetService assetService; + private final CustomerService customerService; + private final RelationService relationService; private final TbAssetProfileCache assetProfileCache; private final TbDeviceProfileCache deviceProfileCache; private final TenantEntityProfileCache entityProfileCache; + private final OwnerService ownerService; private final TbQueueCalculatedFieldSettings cfSettings; protected final TenantId tenantId; + private long cfCheckInterval; + protected TbActorCtx ctx; CalculatedFieldManagerMessageProcessor(ActorSystemContext systemContext, TenantId tenantId) { @@ -90,9 +131,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware this.cfDaoService = systemContext.getCalculatedFieldService(); this.deviceService = systemContext.getDeviceService(); this.assetService = systemContext.getAssetService(); + this.customerService = systemContext.getCustomerService(); + this.relationService = systemContext.getRelationService(); this.assetProfileCache = systemContext.getAssetProfileCache(); this.deviceProfileCache = systemContext.getDeviceProfileCache(); this.entityProfileCache = new TenantEntityProfileCache(); + this.ownerService = systemContext.getOwnerService(); this.cfSettings = systemContext.getCalculatedFieldSettings(); this.tenantId = tenantId; } @@ -103,93 +147,115 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void stop() { log.info("[{}] Stopping CF manager actor.", tenantId); - calculatedFields.values().forEach(CalculatedFieldCtx::stop); + calculatedFields.values().forEach(CalculatedFieldCtx::close); calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); + cancelReevaluationTask(); ctx.stop(ctx.getSelf()); } public void onCacheInitMsg(CalculatedFieldCacheInitMsg msg) { log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId()); - initEntityProfileCache(); + initEntitiesCache(); initCalculatedFields(); + cfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); + scheduleCfsReevaluation(); msg.getCallback().onSuccess(); } public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) { var cfId = msg.getId().cfId(); - var calculatedField = calculatedFields.get(cfId); + var ctx = calculatedFields.get(cfId); - if (calculatedField != null) { - if (msg.getState() != null) { - msg.getState().setRequiredArguments(calculatedField.getArgNames()); - } + if (ctx != null) { + msg.setCtx(ctx); log.debug("[{}] Pushing CF state restore msg to specific actor [{}]", tenantId, msg.getId().entityId()); - getOrCreateActor(msg.getId().entityId()).tell(msg); + getOrCreateActor(msg.getId().entityId()).tellWithHighPriority(msg); } else if (msg.getState() != null) { log.debug("[{}] Received CF state restore msg for non-existing CF [{}]. Removing state", tenantId, cfId); - cfStateService.removeState(msg.getId(), msg.getCallback()); + cfStateService.deleteState(msg.getId(), msg.getCallback()); } else { msg.getCallback().onSuccess(); } } + public void onStatePartitionRestoreMsg(CalculatedFieldStatePartitionRestoreMsg msg) { + ctx.broadcastToChildren(msg, true); + } + + private void scheduleCfsReevaluation() { + cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { + try { + calculatedFields.values().forEach(cf -> { + if (cf.requiresScheduledReevaluation()) { + applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> { + log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId()); + getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf)); + }); + } + }); + } catch (Exception e) { + log.warn("[{}] Failed to trigger CFs reevaluation", tenantId, e); + } + }, cfCheckInterval, cfCheckInterval, TimeUnit.SECONDS); + } + public void onEntityLifecycleMsg(CalculatedFieldEntityLifecycleMsg msg) throws CalculatedFieldException { - log.debug("Processing entity lifecycle event: [{}] for entity: [{}]", msg.getData().getEvent(), msg.getData().getEntityId()); - var entityType = msg.getData().getEntityId().getEntityType(); var event = msg.getData().getEvent(); + if (ComponentLifecycleEvent.RELATION_UPDATED.equals(event) || ComponentLifecycleEvent.RELATION_DELETED.equals(event)) { + log.debug("Processing relation [{}] event from entity: [{}]", event, msg.getData().getEntityId()); + onRelationChangedEvent(msg.getData(), msg.getCallback()); + return; + } + log.debug("Processing entity lifecycle event: [{}] for entity: [{}]", event, msg.getData().getEntityId()); + var entityType = msg.getData().getEntityId().getEntityType(); switch (entityType) { - case CALCULATED_FIELD: { + case CALCULATED_FIELD -> { switch (event) { - case CREATED: - onCfCreated(msg.getData(), msg.getCallback()); - break; - case UPDATED: - onCfUpdated(msg.getData(), msg.getCallback()); - break; - case DELETED: - onCfDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case CREATED -> onCfCreated(msg.getData(), msg.getCallback()); + case UPDATED -> onCfUpdated(msg.getData(), msg.getCallback()); + case DELETED -> onCfDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - case DEVICE: - case ASSET: { + case DEVICE, ASSET, CUSTOMER -> { switch (event) { - case CREATED: - onEntityCreated(msg.getData(), msg.getCallback()); - break; - case UPDATED: - onEntityUpdated(msg.getData(), msg.getCallback()); - break; - case DELETED: - onEntityDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case CREATED -> onEntityCreated(msg.getData(), msg.getCallback()); + case UPDATED -> onEntityUpdated(msg.getData(), msg.getCallback()); + case DELETED -> onEntityDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - case DEVICE_PROFILE: - case ASSET_PROFILE: { + case DEVICE_PROFILE, ASSET_PROFILE -> { switch (event) { - case DELETED: - onProfileDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case DELETED -> onProfileDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - default: { - msg.getCallback().onSuccess(); + case TENANT_PROFILE -> { + switch (event) { + case UPDATED -> onTenantProfileUpdated(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); + } + } + default -> msg.getCallback().onSuccess(); + } + } + + public void onEntityActionEventMsg(CalculatedFieldEntityActionEventMsg msg) { + switch (msg.getAction()) { + case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> { + Alarm alarm = JacksonUtil.treeToValue(msg.getEntity(), Alarm.class); + CalculatedFieldAlarmActionMsg alarmActionMsg = CalculatedFieldAlarmActionMsg.builder() + .tenantId(tenantId) + .alarm(alarm) + .action(msg.getAction()) + .callback(msg.getCallback()) + .build(); + getOrCreateActor(alarm.getOriginator()).tellWithHighPriority(alarmActionMsg); } + default -> msg.getCallback().onSuccess(); } } @@ -198,12 +264,46 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware callback.onSuccess(); } + private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) { + checkCfIntervalForUpdate(); + + long maxRelatedEntitiesPerCfArgument = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument); + List cfsToReinit = new ArrayList<>(); + Stream.concat( + calculatedFields.values().stream(), + entityIdCalculatedFields.values().stream().flatMap(Collection::stream) + ).forEach(ctx -> { + if (ctx.hasRelatedEntities() && ctx.getMaxRelatedEntitiesPerCfArgument() != maxRelatedEntitiesPerCfArgument) { + cfsToReinit.add(ctx); + } + ctx.setTenantProfileProperties(); + }); + + if (!cfsToReinit.isEmpty()) { + MultipleTbCallback cfsReinitCallback = new MultipleTbCallback(cfsToReinit.size(), callback); + cfsToReinit.forEach(ctx -> applyToTargetCfEntityActors(ctx, cfsReinitCallback, (id, cb) -> initCfForEntity(id, ctx, StateAction.REINIT, CalculatedFieldEventType.TENANT_PROFILE_UPDATED, cb))); + } else { + callback.onSuccess(); + } + } + + private void checkCfIntervalForUpdate() { + long updatedCfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); + if (cfCheckInterval != updatedCfCheckInterval) { + cfCheckInterval = updatedCfCheckInterval; + cancelReevaluationTask(); + scheduleCfsReevaluation(); + } + } + private void onEntityCreated(ComponentLifecycleMsg msg, TbCallback callback) { EntityId entityId = msg.getEntityId(); EntityId profileId = getProfileId(tenantId, entityId); if (profileId != null) { entityProfileCache.add(profileId, entityId); } + updateEntityOwner(entityId); + if (!isMyPartition(entityId, callback)) { return; } @@ -212,8 +312,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var fieldsCount = entityIdFields.size() + profileIdFields.size(); if (fieldsCount > 0) { MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); - entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); - profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); + entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, multiCallback)); + profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, multiCallback)); } else { callback.onSuccess(); } @@ -232,21 +332,84 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); var entityId = msg.getEntityId(); oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback)); - newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); + newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, multiCallback)); } else { callback.onSuccess(); } + } else if (msg.isOwnerChanged()) { + onEntityOwnerChanged(msg, callback); + } else { + callback.onSuccess(); } } private void onEntityDeleted(ComponentLifecycleMsg msg, TbCallback callback) { - entityProfileCache.removeEntityId(msg.getEntityId()); + switch (msg.getEntityId().getEntityType()) { + case DEVICE, ASSET -> entityProfileCache.removeEntityId(msg.getEntityId()); + case CUSTOMER -> ownerEntities.remove(msg.getEntityId()); + } + ownerEntities.values().forEach(entities -> entities.remove(msg.getEntityId())); if (isMyPartition(msg.getEntityId(), callback)) { log.debug("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); } } + private void onRelationChangedEvent(ComponentLifecycleMsg msg, TbCallback callback) { + Function> relationAction = switch (msg.getEvent()) { + case RELATION_UPDATED -> relatedId -> (entityId, ctx, cb) -> initRelatedEntity(entityId, relatedId, ctx, cb); + case RELATION_DELETED -> relatedId -> (entityId, ctx, cb) -> deleteRelatedEntity(entityId, relatedId, ctx, cb); + default -> null; + }; + + if (relationAction == null) { + callback.onSuccess(); + return; + } + + EntityRelation entityRelation = JacksonUtil.treeToValue(msg.getInfo(), EntityRelation.class); + EntityId toId = entityRelation.getTo(); + EntityId fromId = entityRelation.getFrom(); + String relationType = entityRelation.getType(); + + if (!(CalculatedField.isSupportedRefEntity(toId) || CalculatedField.isSupportedRefEntity(fromId))) { + callback.onSuccess(); + return; + } + + MultipleTbCallback callbackForToAndFrom = new MultipleTbCallback(2, callback); + processRelationByDirection(EntitySearchDirection.TO, relationType, toId, callbackForToAndFrom, relationAction.apply(fromId)); + processRelationByDirection(EntitySearchDirection.FROM, relationType, fromId, callbackForToAndFrom, relationAction.apply(toId)); + } + + private void processRelationByDirection(EntitySearchDirection direction, + String relationType, + EntityId mainId, + MultipleTbCallback parentCallback, + TriConsumer relationAction) { + if (!isMyPartition(mainId, parentCallback)) { + return; + } + List cfsByEntityIdAndProfile = getCalculatedFieldsByEntityIdAndProfile(mainId); + if (cfsByEntityIdAndProfile.isEmpty()) { + parentCallback.onSuccess(); + return; + } + + List matchingCfs = cfsByEntityIdAndProfile.stream() + .filter(cf -> { + if (cf.getCalculatedField().getConfiguration() instanceof HasRelationPathLevel config) { + RelationPathLevel relation = config.getRelation(); + return direction.equals(relation.direction()) && relationType.equals(relation.relationType()); + } + return false; + }) + .toList(); + + MultipleTbCallback directionCallback = new MultipleTbCallback(matchingCfs.size(), parentCallback); + matchingCfs.forEach(ctx -> relationAction.accept(mainId, ctx, directionCallback)); + } + private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); if (calculatedFields.containsKey(cfId)) { @@ -258,7 +421,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { - var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var cfCtx = getCfCtx(cf); try { cfCtx.init(); } catch (Exception e) { @@ -269,11 +432,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); addLinks(cf); - initCf(cfCtx, callback, false); + applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, cb)); } } } + private CalculatedFieldCtx getCfCtx(CalculatedField cf) { + return new CalculatedFieldCtx(cf, systemContext); + } + private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var oldCfCtx = calculatedFields.get(cfId); @@ -285,7 +452,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { - var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var newCfCtx = getCfCtx(newCf); try { newCfCtx.init(); } catch (Exception e) { @@ -313,12 +480,33 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware addLinks(newCf); } - var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); - if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { - initCf(newCfCtx, callback, stateChanges); + StateAction stateAction; + if (newCfCtx.getCfType() != oldCfCtx.getCfType()) { + stateAction = StateAction.RECREATE; // completely recreate state, then calculate + } else if (newCfCtx.hasStateChanges(oldCfCtx)) { + stateAction = StateAction.REINIT; // refetch arguments, call state.init, then calculate + } else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) { + stateAction = StateAction.REPROCESS; // call state.setCtx, then calculate + } else if (newCfCtx.hasRefreshContextOnlyChanges(oldCfCtx)) { + stateAction = StateAction.REFRESH_CTX; } else { callback.onSuccess(); + return; } + + applyToTargetCfEntityActors(newCfCtx, new TbCallback() { + @Override + public void onSuccess() { + oldCfCtx.close(); + callback.onSuccess(); + } + + @Override + public void onFailure(Throwable t) { + oldCfCtx.close(); + callback.onFailure(t); + } + }, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, CalculatedFieldEventType.UPDATED, cb)); } } } @@ -329,38 +517,30 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (cfCtx == null) { log.debug("[{}] CF was already deleted [{}]", tenantId, cfId); callback.onSuccess(); - } else { - entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); - deleteLinks(cfCtx); - - EntityId entityId = cfCtx.getEntityId(); - EntityType entityType = cfCtx.getEntityId().getEntityType(); - if (isProfileEntity(entityType)) { - var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); - if (!entityIds.isEmpty()) { - //TODO: no need to do this if we cache all created actors and know which one belong to us; - var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - deleteCfForEntity(id, cfId, multiCallback); - } - }); - } else { - callback.onSuccess(); - } - } else { - if (isMyPartition(entityId, callback)) { - deleteCfForEntity(entityId, cfId, callback); - } - } + return; } + entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); + deleteLinks(cfCtx); + applyToTargetCfEntityActors(cfCtx, new TbCallback() { + @Override + public void onSuccess() { + cfCtx.close(); + callback.onSuccess(); + } + + @Override + public void onFailure(Throwable t) { + cfCtx.close(); + callback.onFailure(t); + } + }, (id, cb) -> deleteCfForEntity(id, cfId, cb)); } public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); log.debug("Received telemetry msg from entity [{}]", entityId); - // 2 = 1 for CF processing + 1 for links processing - MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback()); + // 4 = 1 for CF processing + 1 for links processing + 1 for owner entity processing + 1 for aggregation processing + MultipleTbCallback callback = new MultipleTbCallback(4, msg.getCallback()); // process all cfs related to entity, or it's profile; var entityIdFields = getCalculatedFieldsByEntityId(entityId); var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); @@ -378,6 +558,59 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } else { callback.onSuccess(); } + // process all cfs related to owner entity + if (entityId.getEntityType().isOneOf(EntityType.TENANT, EntityType.CUSTOMER)) { + List ownedEntitiesCFs = filterOwnedEntitiesCFs(msg); + if (!ownedEntitiesCFs.isEmpty()) { + cfExecService.pushMsgToLinks(msg, ownedEntitiesCFs, callback); + } else { + callback.onSuccess(); + } + } else { + callback.onSuccess(); + } + // process all aggregation cfs (if any); + List aggregationCalculatedFields = filterAggregationCfs(msg); + if (!aggregationCalculatedFields.isEmpty()) { + cfExecService.pushMsgToLinks(msg, aggregationCalculatedFields, callback); + } else { + callback.onSuccess(); + } + } + + private List filterAggregationCfs(CalculatedFieldTelemetryMsg msg) { + EntityId entityId = msg.getEntityId(); + return calculatedFields.values().stream() + .filter(cf -> CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cf.getCfType())) + .filter(cf -> cf.relatedEntityMatches(msg.getProto())) + .flatMap(cf -> findRelationsForCf(entityId, cf).stream()) + .toList(); + } + + private List findRelationsForCf(EntityId entityId, CalculatedFieldCtx cf) { + List result = new ArrayList<>(); + if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration configuration) { + RelationPathLevel relation = configuration.getRelation(); + EntitySearchDirection inverseDirection = switch (relation.direction()) { + case FROM -> EntitySearchDirection.TO; + case TO -> EntitySearchDirection.FROM; + }; + RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType()); + List byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation))); + EntityId cfEntityId = cf.getEntityId(); + Predicate matchesCfEntity = relatedEntity -> cfEntityId.equals(relatedEntity) || cfEntityId.equals(getProfileId(tenantId, relatedEntity)); + if (byRelationPathQuery != null && !byRelationPathQuery.isEmpty()) { + switch (relation.direction()) { + case FROM -> byRelationPathQuery.stream() + .filter(entityRelation -> matchesCfEntity.test(entityRelation.getFrom())) + .forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getFrom()))); + case TO -> byRelationPathQuery.stream() + .filter(entityRelation -> matchesCfEntity.test(entityRelation.getTo())) + .forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getTo()))); + } + } + } + return result; } public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) { @@ -392,32 +625,35 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } for (var linkProto : linksList) { var link = fromProto(linkProto); - var targetEntityId = link.entityId(); - var targetEntityType = targetEntityId.getEntityType(); var cf = calculatedFields.get(link.cfId()); - if (EntityType.DEVICE_PROFILE.equals(targetEntityType) || EntityType.ASSET_PROFILE.equals(targetEntityType)) { - // iterate over all entities that belong to profile and push the message for corresponding CF - var entityIds = entityProfileCache.getEntityIdsByProfileId(targetEntityId); - if (!entityIds.isEmpty()) { - MultipleTbCallback multipleCallback = new MultipleTbCallback(entityIds.size(), callback); - var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, multipleCallback); - entityIds.forEach(entityId -> { - if (isMyPartition(entityId, multipleCallback)) { - log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(newMsg); - } - }); + withTargetEntities(link.entityId(), callback, (ids, cb) -> { + var linkedTelemetryMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, cb); + ids.forEach(id -> linkedTelemetryMsgForEntity(id, linkedTelemetryMsg)); + }); + } + } + + private void onEntityOwnerChanged(ComponentLifecycleMsg msg, TbCallback msgCallback) { + EntityId entityId = msg.getEntityId(); + log.debug("Received changed owner msg from entity [{}]", entityId); + updateEntityOwner(entityId); + List cfs = getCalculatedFieldsByEntityIdAndProfile(entityId); + if (cfs.isEmpty()) { + msgCallback.onSuccess(); + return; + } + MultipleTbCallback callback = new MultipleTbCallback(cfs.size(), msgCallback); + cfs.forEach(cf -> { + if (isMyPartition(entityId, callback)) { + if (cf.hasCurrentOwnerSourceArguments()) { + CalculatedFieldArgumentResetMsg argResetMsg = new CalculatedFieldArgumentResetMsg(tenantId, cf, CalculatedFieldEventType.OWNER_CHANGED, callback); + log.debug("Pushing CF argument reset msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(argResetMsg); } else { callback.onSuccess(); } - } else { - if (isMyPartition(targetEntityId, callback)) { - log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); - var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback); - getOrCreateActor(targetEntityId).tell(newMsg); - } } - } + }); } private List filterCalculatedFieldLinks(CalculatedFieldTelemetryMsg msg) { @@ -425,7 +661,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var proto = msg.getProto(); List result = new ArrayList<>(); for (var link : getCalculatedFieldLinksByEntityId(entityId)) { - CalculatedFieldCtx ctx = calculatedFields.get(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFields.get(link.calculatedFieldId()); if (ctx.linkMatches(entityId, proto)) { result.add(ctx.toCalculatedFieldEntityCtxId()); } @@ -433,6 +669,27 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } + private List filterOwnedEntitiesCFs(CalculatedFieldTelemetryMsg msg) { + Set entities = getOwnedEntities(msg.getEntityId()); + var proto = msg.getProto(); + List result = new ArrayList<>(); + for (var entityId : entities) { + var ownerEntityCFs = getCalculatedFieldsByEntityId(entityId); + for (var ctx : ownerEntityCFs) { + if (ctx.dynamicSourceMatches(proto)) { + result.add(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId)); + } + } + var ownerEntityProfileCFs = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); + for (var ctx : ownerEntityProfileCFs) { + if (ctx.dynamicSourceMatches(proto)) { + result.add(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId)); + } + } + } + return result; + } + private List getCalculatedFieldsByEntityId(EntityId entityId) { if (entityId == null) { return Collections.emptyList(); @@ -444,6 +701,16 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } + private List getCalculatedFieldsByEntityIdAndProfile(EntityId entityId) { + List cfsByEntityIdAndProfile = new ArrayList<>(); + cfsByEntityIdAndProfile.addAll(getCalculatedFieldsByEntityId(entityId)); + EntityId profileId = getProfileId(tenantId, entityId); + if (profileId != null) { + cfsByEntityIdAndProfile.addAll(getCalculatedFieldsByEntityId(profileId)); + } + return cfsByEntityIdAndProfile; + } + private List getCalculatedFieldLinksByEntityId(EntityId entityId) { if (entityId == null) { return Collections.emptyList(); @@ -455,26 +722,30 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } - private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { - EntityId entityId = cfCtx.getEntityId(); - EntityType entityType = cfCtx.getEntityId().getEntityType(); - if (isProfileEntity(entityType)) { - var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); - if (!entityIds.isEmpty()) { - var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - initCfForEntity(id, cfCtx, forceStateReinit, multiCallback); - } - }); - } else { - callback.onSuccess(); - } - } else { - if (isMyPartition(entityId, callback)) { - initCfForEntity(entityId, cfCtx, forceStateReinit, callback); - } + private Set getOwnedEntities(EntityId entityId) { + if (entityId == null) { + return Collections.emptySet(); + } + var result = ownerEntities.get(entityId); + if (result == null) { + result = Collections.emptySet(); } + return result; + } + + private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { + log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(msg); + } + + private void deleteRelatedEntity(EntityId entityId, EntityId relatedEntityId, CalculatedFieldCtx cf, TbCallback callback) { + log.debug("Pushing delete related entity msg to specific actor [{}]", relatedEntityId); + getOrCreateActor(entityId).tell(new CalculatedFieldRelationActionMsg(tenantId, relatedEntityId, ActionType.DELETED, cf, callback)); + } + + private void initRelatedEntity(EntityId entityId, EntityId relatedEntityId, CalculatedFieldCtx cf, TbCallback callback) { + log.debug("Pushing init related entity msg to specific actor [{}]", relatedEntityId); + getOrCreateActor(entityId).tell(new CalculatedFieldRelationActionMsg(tenantId, relatedEntityId, ActionType.UPDATED, cf, callback)); } private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { @@ -482,9 +753,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback)); } - private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, boolean forceStateReinit, TbCallback callback) { + private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, StateAction stateAction, CalculatedFieldEventType eventType, TbCallback callback) { log.debug("Pushing entity init CF msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, callback, forceStateReinit)); + getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, stateAction, eventType, callback)); } private boolean isMyPartition(EntityId entityId, TbCallback callback) { @@ -502,8 +773,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private EntityId getProfileId(TenantId tenantId, EntityId entityId) { return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); + case ASSET -> Optional.ofNullable(assetProfileCache.get(tenantId, (AssetId) entityId)).map(AssetProfile::getId).orElse(null); + case DEVICE -> Optional.ofNullable(deviceProfileCache.get(tenantId, (DeviceId) entityId)).map(DeviceProfile::getId).orElse(null); default -> null; }; } @@ -517,13 +788,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void addLinks(CalculatedField newCf) { var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId()); - newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)); + newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link)); } private void deleteLinks(CalculatedFieldCtx cfCtx) { var oldCf = cfCtx.getCalculatedField(); var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId()); - oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); + oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); } public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) { @@ -536,19 +807,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.trace("Processing calculated field record: {}", cf); try { initCalculatedField(cf); + initCalculatedFieldLinks(cf); } catch (CalculatedFieldException e) { log.error("Failed to process calculated field record: {}", cf, e); } }); - PageDataIterable cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); - cfls.forEach(link -> { - log.trace("Processing calculated field link record: {}", link); - initCalculatedFieldLink(link); - }); } private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException { - var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var cfCtx = new CalculatedFieldCtx(cf, systemContext); try { cfCtx.init(); } catch (Exception e) { @@ -561,31 +828,86 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void initCalculatedFieldLink(CalculatedFieldLink link) { - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link); + private void initCalculatedFieldLinks(CalculatedField cf) { + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + for (CalculatedFieldLink link : links) { + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link); + } } - private void initEntityProfileCache() { + private void initEntitiesCache() { PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); for (ProfileEntityIdInfo idInfo : deviceIdInfos) { log.trace("Processing device record: {}", idInfo); try { entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId()); + ownerEntities.computeIfAbsent(idInfo.getOwnerId(), __ -> new HashSet<>()).add(idInfo.getEntityId()); } catch (Exception e) { log.error("Failed to process device record: {}", idInfo, e); } } + PageDataIterable assetIdInfos = new PageDataIterable<>(pageLink -> assetService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); for (ProfileEntityIdInfo idInfo : assetIdInfos) { log.trace("Processing asset record: {}", idInfo); try { entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId()); + ownerEntities.computeIfAbsent(idInfo.getOwnerId(), __ -> new HashSet<>()).add(idInfo.getEntityId()); } catch (Exception e) { log.error("Failed to process asset record: {}", idInfo, e); } } + + PageDataIterable customers = new PageDataIterable<>(pageLink -> customerService.findCustomersByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); + for (Customer customer : customers) { + log.trace("Processing customer record: {}", customer); + try { + ownerEntities.computeIfAbsent(customer.getTenantId(), __ -> new HashSet<>()).add(customer.getId()); + } catch (Exception e) { + log.error("Failed to process customer record: {}", customer, e); + } + } + } + + private void updateEntityOwner(EntityId entityId) { + ownerEntities.values().forEach(entities -> entities.remove(entityId)); + EntityId owner = ownerService.getOwner(tenantId, entityId); + ownerEntities.computeIfAbsent(owner, ownerId -> new HashSet<>()).add(entityId); + } + + private void applyToTargetCfEntityActors(CalculatedFieldCtx ctx, + TbCallback callback, + BiConsumer action) { + withTargetEntities(ctx.getEntityId(), callback, (ids, cb) -> ids.forEach(id -> action.accept(id, cb))); + } + + private void withTargetEntities(EntityId entityId, TbCallback parentCallback, BiConsumer, TbCallback> consumer) { + if (isProfileEntity(entityId.getEntityType())) { + var ids = entityProfileCache.getEntityIdsByProfileId(entityId); + if (ids.isEmpty()) { + parentCallback.onSuccess(); + return; + } + var multiCallback = new MultipleTbCallback(ids.size(), parentCallback); + var profileEntityIds = ids.stream().filter(id -> isMyPartition(id, multiCallback)).toList(); + if (profileEntityIds.isEmpty()) { + return; + } + consumer.accept(profileEntityIds, multiCallback); + return; + } + if (isMyPartition(entityId, parentCallback)) { + consumer.accept(List.of(entityId), parentCallback); + } + } + + private void cancelReevaluationTask() { + if (cfsReevaluationTask != null) { + cfsReevaluationTask.cancel(true); + cfsReevaluationTask = null; + } } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java new file mode 100644 index 0000000000..74308fc35d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldReevaluateMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final CalculatedFieldCtx ctx; + + @Override + public MsgType getMsgType() { + return MsgType.CF_REEVALUATE_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java new file mode 100644 index 0000000000..70e1136afe --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldRelationActionMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final EntityId relatedEntityId; + private final ActionType action; + private final CalculatedFieldCtx calculatedField; + private final TbCallback callback; + + public CalculatedFieldRelationActionMsg(TenantId tenantId, + EntityId relatedEntityId, ActionType action, + CalculatedFieldCtx calculatedField, + TbCallback callback) { + this.tenantId = tenantId; + this.relatedEntityId = relatedEntityId; + this.action = action; + this.calculatedField = calculatedField; + this.callback = callback; + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_RELATION_ACTION_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java index 9fc5af4f26..781325066a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java @@ -20,7 +20,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; @Data @@ -28,7 +30,9 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs private final CalculatedFieldEntityCtxId id; private final CalculatedFieldState state; + private final TopicPartitionInfo partition; private final TbCallback callback; + private CalculatedFieldCtx ctx; @Override public MsgType getMsgType() { @@ -39,4 +43,5 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs public TenantId getTenantId() { return id.tenantId(); } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java index f01c41f4b6..f1b6e62a41 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java @@ -31,9 +31,9 @@ public class CalculatedFieldTelemetryMsg implements ToCalculatedFieldSystemMsg { private final CalculatedFieldTelemetryMsgProto proto; private final TbCallback callback; - @Override public MsgType getMsgType() { return MsgType.CF_TELEMETRY_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java index 6ac10e4fa1..8432748276 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java @@ -16,26 +16,32 @@ package org.thingsboard.server.actors.calculatedField; import lombok.Data; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.cf.CalculatedFieldEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import java.util.List; - @Data public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final CalculatedFieldCtx ctx; + private final StateAction stateAction; + private final CalculatedFieldEventType eventType; private final TbCallback callback; - private final boolean forceReinit; @Override public MsgType getMsgType() { return MsgType.CF_ENTITY_INIT_CF_MSG; } + + public enum StateAction { + INIT, + REINIT, + RECREATE, + REPROCESS, + REFRESH_CTX + } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java index 289fe52d04..0be2eb4690 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java @@ -50,7 +50,7 @@ public class MultipleTbCallback implements TbCallback { @Override public void onFailure(Throwable t) { - log.warn("[{}][{}] onFailure.", id, callback.getId()); + log.warn("[{}][{}] onFailure.", id, callback.getId(), t); callback.onFailure(t); } } 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 3a572503d6..76f79bf1f9 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 @@ -110,6 +110,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; @@ -925,6 +926,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getJobManager(); } + @Override + public ApiKeyService getApiKeyService() { + return mainCtx.getApiKeyService(); + } + @Override public boolean isExternalNodeForceAck() { return mainCtx.isExternalNodeForceAck(); 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 59663563e3..c679dc8724 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 @@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.aware.TenantAwareMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -163,6 +164,9 @@ public class TenantActor extends RuleChainManagerActor { case COMPONENT_LIFE_CYCLE_MSG: onComponentLifecycleMsg((ComponentLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + forwardToCfActor((TenantAwareMsg) msg, true); + break; case QUEUE_TO_RULE_ENGINE_MSG: onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg); break; @@ -190,11 +194,12 @@ public class TenantActor extends RuleChainManagerActor { case CF_CACHE_INIT_MSG: case CF_STATE_RESTORE_MSG: case CF_PARTITIONS_CHANGE_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); + case CF_STATE_PARTITION_RESTORE_MSG: + forwardToCfActor((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false); + forwardToCfActor((ToCalculatedFieldSystemMsg) msg, false); break; default: return false; @@ -202,7 +207,7 @@ public class TenantActor extends RuleChainManagerActor { return true; } - private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + private void forwardToCfActor(TenantAwareMsg msg, boolean priority) { if (cfActor == null) { if (msg instanceof CalculatedFieldStateRestoreMsg) { log.warn("[{}] CF Actor is not initialized. ToCalculatedFieldSystemMsg: [{}]", tenantId, msg); @@ -353,7 +358,7 @@ public class TenantActor extends RuleChainManagerActor { } } if (cfActor != null) { - if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET)) { + if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT_PROFILE)) { cfActor.tellWithHighPriority(new CalculatedFieldEntityLifecycleMsg(tenantId, msg)); } } diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 70259a15ef..6bdb1ae758 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.config; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.jackson.ModelResolver; import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -31,6 +35,7 @@ import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.RequestBody; @@ -39,11 +44,12 @@ import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.tags.Tag; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.customizers.OperationCustomizer; -import org.springdoc.core.customizers.RouterOperationCustomizer; import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer; +import org.springdoc.core.utils.SpringDocUtils; import org.springdoc.core.models.GroupedOpenApi; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SwaggerUiConfigProperties; @@ -54,26 +60,35 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; -import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.RequestParam; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse; import org.thingsboard.server.exception.ThingsboardErrorResponse; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.Deque; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -83,13 +98,30 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @Profile("!test") public class SwaggerConfiguration { + @PostConstruct + public void configureModelResolver() { + ModelResolver.enumsAsRef = true; + SpringDocUtils.getConfig().replaceWithSchema(ByteBuffer.class, + new Schema().type("string").format("byte")); + } + public static final String LOGIN_ENDPOINT = "/api/auth/login"; public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token"; + private static final String LOGIN_PASSWORD_SCHEME = "HttpLoginForm"; + private static final String API_KEY_SCHEME = "ApiKeyForm"; + private static final ApiResponses loginResponses = loginResponses(); private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); + // Populated by mapAwareConverter, consumed by customOpenApiCustomizer. + // Keyed by the schema name that swagger-core generates (see resolveSchemaName). + private final Map> schemaPropertyOrders = new ConcurrentHashMap<>(); + private final Map> schemaOwnProps = new ConcurrentHashMap<>(); + // Tracks schema name → fully-qualified class names to detect collisions. + private final Map> schemaNameToClasses = new ConcurrentHashMap<>(); + @Value("${swagger.api_path:/api/**}") private String apiPath; @Value("${swagger.security_path_regex}") @@ -116,6 +148,8 @@ public class SwaggerConfiguration { private String appVersion; @Value("${swagger.group_name:thingsboard}") private String groupName; + @Value("${swagger.doc_expansion:list}") + private String docExpansion; @Bean public OpenAPI thingsboardApi() { @@ -140,14 +174,28 @@ public class SwaggerConfiguration { .license(license) .version(apiVersion); - SecurityScheme securityScheme = new SecurityScheme() + SecurityScheme loginPasswordScheme = new SecurityScheme() .type(SecurityScheme.Type.HTTP) .description("Enter Username / Password") .scheme("loginPassword") .bearerFormat("/api/auth/login|X-Authorization"); + SecurityScheme apiKeyScheme = new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .name("X-Authorization") + .in(SecurityScheme.In.HEADER) + .description(""" + Enter the API key value with 'ApiKey' prefix in format: **ApiKey ** + + Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW** + +
**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.
+ """); + var openApi = new OpenAPI() - .components(new Components().addSecuritySchemes("HTTP login form", securityScheme)) + .components(new Components() + .addSecuritySchemes(LOGIN_PASSWORD_SCHEME, loginPasswordScheme) + .addSecuritySchemes(API_KEY_SCHEME, apiKeyScheme)) .info(info); addDefaultSchemas(openApi); addLoginOperation(openApi); @@ -172,7 +220,7 @@ public class SwaggerConfiguration { uiProperties.setDefaultModelExpandDepth(1); uiProperties.setDefaultModelRendering("example"); uiProperties.setDisplayRequestDuration(false); - uiProperties.setDocExpansion("list"); + uiProperties.setDocExpansion(docExpansion); uiProperties.setFilter("false"); uiProperties.setMaxDisplayedTags(null); uiProperties.setOperationsSorter("alpha"); @@ -194,15 +242,17 @@ public class SwaggerConfiguration { private void addLoginOperation(OpenAPI openAPI) { var operation = new Operation(); operation.summary("Login method to get user JWT token data"); + operation.operationId("login"); operation.description(""" Login method used to authenticate user and get JWT token data. - + Value of the response **token** field can be used as **X-Authorization** header value: - + `X-Authorization: Bearer $JWT_TOKEN_VALUE`."""); + var requestBody = new RequestBody().description("Login request") .content(new Content().addMediaType(APPLICATION_JSON_VALUE, - new MediaType().schema(new Schema().$ref("#/components/schemas/LoginRequest")))); + new MediaType().schema(new Schema().$ref("#/components/schemas/LoginRequest")))); operation.requestBody(requestBody); operation.responses(loginResponses); @@ -215,12 +265,13 @@ public class SwaggerConfiguration { private void addRefreshTokenOperation(OpenAPI openAPI) { var operation = new Operation(); operation.summary("Refresh user JWT token data"); + operation.operationId("refreshToken"); operation.description(""" - Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. - - The response contains a new token that can be used for authorization. - - `X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); + Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. + + The response contains a new token that can be used for authorization. + + `X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); var requestBody = new RequestBody().description("Refresh token request") .content(new Content().addMediaType(APPLICATION_JSON_VALUE, @@ -240,7 +291,6 @@ public class SwaggerConfiguration { return GroupedOpenApi.builder() .group(groupName) .pathsToMatch(apiPath) - .addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer)) .addOperationCustomizer(operationCustomizer()) .addOpenApiCustomizer(customOpenApiCustomizer()) .build(); @@ -250,9 +300,34 @@ public class SwaggerConfiguration { @Lazy(false) ModelConverter mapAwareConverter() { return (type, context, chain) -> { + // Strip field-level @JsonIgnoreProperties from context annotations so it + // doesn't pollute the global schema. The OpenAPI schema should show all + // properties; field-level ignore is a serialization concern only. + Annotation[] ctxAnnotations = type.getCtxAnnotations(); + if (ctxAnnotations != null) { + Annotation[] filtered = Arrays.stream(ctxAnnotations) + .filter(a -> !(a instanceof JsonIgnoreProperties)) + .toArray(Annotation[]::new); + if (filtered.length != ctxAnnotations.length) { + type.ctxAnnotations(filtered); + } + } + + JavaType javaType = Json.mapper().constructType(type.getType()); + if (javaType != null) { + Class cls = javaType.getRawClass(); + Schema atomicSchema = switch (cls.getName()) { + case "java.util.concurrent.atomic.AtomicInteger" -> new IntegerSchema().format("int32"); + case "java.util.concurrent.atomic.AtomicLong" -> new IntegerSchema().format("int64"); + case "com.google.common.util.concurrent.AtomicDouble" -> new IntegerSchema().format("double"); + default -> null; + }; + if (atomicSchema != null) { + return atomicSchema; + } + } if (chain.hasNext()) { Schema schema = chain.next().resolve(type, context, chain); - JavaType javaType = Json.mapper().constructType(type.getType()); if (javaType != null) { Class cls = javaType.getRawClass(); if (Map.class.isAssignableFrom(cls)) { @@ -262,6 +337,26 @@ public class SwaggerConfiguration { schema.setProperties(null); } } + } else { + // Precompute property order and own-prop names for this class. + // The actual reordering happens later in the OpenApiCustomizer, + // which has access to the final state of all component schemas + // (including ones where the ModelConverter only sees a $ref). + try { + var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); + String schemaName = resolveSchemaName(javaType); + Set classes = schemaNameToClasses.computeIfAbsent(schemaName, k -> ConcurrentHashMap.newKeySet()); + if (classes.add(cls.getName()) && classes.size() > 1) { + log.error("Duplicate OpenAPI schema name '{}' mapped by: {}. Use @Schema(name = ...) to disambiguate.", schemaName, classes); + } + schemaPropertyOrders.put(schemaName, resolvePropertyOrder(cls, beanDesc)); + Set ownProps = computeOwnPropNames(cls, beanDesc); + if (!ownProps.isEmpty()) { + schemaOwnProps.put(schemaName, ownProps); + } + } catch (Exception e) { + log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage()); + } } } return schema; @@ -272,44 +367,19 @@ public class SwaggerConfiguration { } private void addDefaultSchemas(OpenAPI openAPI) { - var jsonNodeSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(JsonNode.class)).schema; - jsonNodeSchema.setType("any"); - //noinspection unchecked - jsonNodeSchema.setExamples(List.of(JacksonUtil.newObjectNode())); - jsonNodeSchema.setDescription("A value representing the any type (object or primitive)"); + Schema errorCodeSchema = new Schema<>() + .type("integer") + .description("Platform error code") + ._enum(Arrays.stream(ThingsboardErrorCode.values()) + .map(ThingsboardErrorCode::getErrorCode) + .collect(Collectors.toList())); openAPI.getComponents() - .addSchemas("JsonNode", jsonNodeSchema) .addSchemas("LoginRequest", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginRequest.class)).schema) .addSchemas("LoginResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginResponse.class)).schema) .addSchemas("ThingsboardErrorResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardErrorResponse.class)).schema) - .addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema); - } - - private RouterOperationCustomizer routerOperationCustomizer(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) { - return (routerOperation, handlerMethod) -> { - String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod()); - String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new); - if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) - pNames = reflectionParametersNames; - MethodParameter[] parameters = handlerMethod.getMethodParameters(); - List requestParams = new ArrayList<>(); - for (var i = 0; i < parameters.length; i++) { - var methodParameter = parameters[i]; - RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class); - if (requestParam != null) { - String pName = StringUtils.isNotBlank(requestParam.value()) ? requestParam.value() : - pNames[i]; - if (StringUtils.isNotBlank(pName)) { - requestParams.add(pName); - } - } - } - if (!requestParams.isEmpty()) { - var path = routerOperation.getPath() + "{?" + String.join(",", requestParams) + "}"; - routerOperation.setPath(path); - } - return routerOperation; - }; + .addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema) + .addSchemas("ThingsboardErrorCode", errorCodeSchema) + .addSchemas("AiChatModelConfig", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(AiChatModelConfig.class)).schema); } private OperationCustomizer operationCustomizer() { @@ -322,26 +392,38 @@ public class SwaggerConfiguration { } private OpenApiCustomizer customOpenApiCustomizer() { - var loginForm = new SecurityRequirement().addList("HTTP login form", Arrays.asList( - Authority.SYS_ADMIN.name(), - Authority.TENANT_ADMIN.name(), - Authority.CUSTOMER_USER.name() - )); + var loginRequirement = createSecurityRequirement(LOGIN_PASSWORD_SCHEME); + var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME); + return openAPI -> { + // Fail fast on duplicate schema names — two different classes resolving to the same + // OpenAPI schema name causes one to silently overwrite the other. + List duplicates = schemaNameToClasses.entrySet().stream() + .filter(e -> e.getValue().size() > 1) + .map(e -> "'" + e.getKey() + "' mapped by: " + e.getValue()) + .sorted() + .toList(); + if (!duplicates.isEmpty()) { + throw new IllegalStateException( + "Duplicate OpenAPI schema names detected. Use @Schema(name = ...) to disambiguate:\n " + + String.join("\n ", duplicates)); + } + var paths = openAPI.getPaths(); - paths.entrySet().stream().peek(entry -> { - securityCustomization(loginForm, entry); - if (!entry.getKey().equals(LOGIN_ENDPOINT)) { - defaultErrorResponsesCustomization(entry.getValue()); - } - }).map(this::tagsCustomization).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem); + paths.entrySet().stream() + .peek(entry -> { + securityCustomization(entry, loginRequirement, apiKeyRequirement); + if (!entry.getKey().equals(LOGIN_ENDPOINT)) { + defaultErrorResponsesCustomization(entry.getValue()); + } + }) + .map(this::extractTagFromPath).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem); var pathItemsByTags = new TreeMap>(); paths.forEach((k, v) -> { var tagItem = tagItemFromPathItem(v); if (tagItem != null) { - var pathItemMap = pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()); - pathItemMap.put(k, v); + pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()).put(k, v); } }); var sortedPaths = new Paths(); @@ -350,23 +432,266 @@ public class SwaggerConfiguration { }); sortedPaths.setExtensions(paths.getExtensions()); openAPI.setPaths(sortedPaths); + + if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) { + Map schemas = openAPI.getComponents().getSchemas(); + + // Fix all schemas: if they have additionalProperties but no type, set type to object + schemas.forEach((schemaName, schema) -> { + if (schema.getAdditionalProperties() != null && schema.getType() == null) { + schema.setType("object"); + log.debug("Added type 'object' to schema: {}", schemaName); + } + }); + + // Springdoc creates duplicate schemas with an "Object" suffix when a type is + // resolved through multiple inheritance paths or via generic type resolution. + // Remove the "*Object" duplicate when the base schema exists (either + // pre-registered in addDefaultSchemas or generated by springdoc). + for (String name : new ArrayList<>(schemas.keySet())) { + if (!name.endsWith("Object")) continue; + String baseName = name.substring(0, name.length() - "Object".length()); + if (!schemas.containsKey(baseName)) continue; + + schemas.remove(name); + String refToRemove = "#/components/schemas/" + name; + schemas.values().forEach(s -> { + if (s.getAllOf() != null) { + s.getAllOf().removeIf(allOfEntry -> refToRemove.equals(((Schema) allOfEntry).get$ref())); + } + }); + log.debug("Removed duplicate schema '{}' (base '{}' exists)", name, baseName); + } + + // Remove duplicate or redundant inline entries in allOf. Springdoc can + // generate multiple inline property blocks when resolving a type through + // multiple parent paths (e.g. record + sealed interface). One block may be + // a strict subset of another (same properties, but the superset has extras + // like "modelType"). Keep only the superset in that case. + schemas.values().forEach(schema -> { + if (schema.getAllOf() != null && schema.getAllOf().size() > 1) { + List allOf = schema.getAllOf(); + Set redundant = new HashSet<>(); + for (int i = 0; i < allOf.size(); i++) { + if (redundant.contains(i)) continue; + Schema a = allOf.get(i); + if (a.get$ref() != null || a.getProperties() == null) continue; + for (int j = i + 1; j < allOf.size(); j++) { + if (redundant.contains(j)) continue; + Schema b = allOf.get(j); + if (b.get$ref() != null || b.getProperties() == null) continue; + if (a.getProperties().entrySet().containsAll(b.getProperties().entrySet())) { + redundant.add(j); // b is a subset of a + } else if (b.getProperties().entrySet().containsAll(a.getProperties().entrySet())) { + redundant.add(i); // a is a subset of b + break; + } + } + } + if (!redundant.isEmpty()) { + List filtered = new ArrayList<>(); + for (int i = 0; i < allOf.size(); i++) { + if (!redundant.contains(i)) { + filtered.add(allOf.get(i)); + } + } + allOf.clear(); + allOf.addAll(filtered); + } + } + }); + + + // Fix polymorphic properties: replace inline oneOf with base type $ref + schemas.values().forEach(schema -> { + replaceInlineOneOfProperties(schema, schemas); + if (schema.getAllOf() != null) { + List allOf = schema.getAllOf(); + for (Schema allOfElement : allOf) { + replaceInlineOneOfProperties(allOfElement, schemas); + } + } + }); + + // Deduplicate allOf child schemas: remove properties that are already defined + // in the referenced parent schema to avoid duplication (e.g. EntityId children). + schemas.forEach((schemaName, schema) -> { + Set ownProps = schemaOwnProps.getOrDefault(schemaName, Set.of()); + deduplicateAllOfProperties(schema, schemas, ownProps); + }); + + // Reorder properties for all component schemas. This runs after all + // schemas are finalized so it covers schemas the ModelConverter only + // saw as a $ref (e.g. interface-based discriminator types like EntityId). + schemas.forEach((schemaName, schema) -> { + List propOrder = schemaPropertyOrders.getOrDefault(schemaName, List.of()); + reorderSchemaProperties(schema, propOrder); + }); + + // Fix polymorphic request/response bodies: replace inline oneOf with base type $ref + paths.values().stream() + .flatMap(pathItem -> pathItem.readOperationsMap().values().stream()) + .forEach(operation -> { + // Request bodies + if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) { + replaceInlineOneOfInContent(operation.getRequestBody().getContent(), schemas); + } + // Response bodies + if (operation.getResponses() != null) { + operation.getResponses().values().stream() + .filter(response -> response.getContent() != null) + .forEach(response -> replaceInlineOneOfInContent(response.getContent(), schemas)); + } + }); + } + + // Set JsonNode schema last so model scanning cannot overwrite it + openAPI.getComponents().addSchemas("JsonNode", new Schema<>() + .description("A value representing the any type (object or primitive)") + .example(JacksonUtil.newObjectNode())); + var sortedSchemas = new TreeMap<>(openAPI.getComponents().getSchemas()); openAPI.getComponents().setSchemas(new LinkedHashMap<>(sortedSchemas)); }; } + private SecurityRequirement createSecurityRequirement(String schemeName) { + return new SecurityRequirement().addList(schemeName, List.of()); + } + + private Tag extractTagFromPath(Map.Entry entry) { + var tagName = tagItemFromPathItem(entry.getValue()); + return tagName != null ? tagFromTagItem(tagName) : null; + } - private Tag tagsCustomization(Map.Entry entry) { - var tagItem = tagItemFromPathItem(entry.getValue()); - if (tagItem != null) { - return tagFromTagItem(tagItem); + private String findBaseTypeForOneOf(Map schemas, List oneOfSchemas) { + if (oneOfSchemas.isEmpty()) { + return null; + } + + for (Schema oneOfSchema : oneOfSchemas) { + String ref = oneOfSchema.get$ref(); + if (ref == null) { + continue; + } + String refName = ref.substring(ref.lastIndexOf('/') + 1); + + // Check if this entry is itself a base type with discriminator + Schema candidate = schemas.get(refName); + if (candidate != null && candidate.getDiscriminator() != null + && candidate.getDiscriminator().getMapping() != null) { + return refName; + } + + // Check if this subtype is in another schema's discriminator mapping + String baseType = schemas.entrySet().stream() + .filter(entry -> { + Schema schema = entry.getValue(); + if (schema.getDiscriminator() != null && schema.getDiscriminator().getMapping() != null) { + return schema.getDiscriminator().getMapping().values().stream() + .anyMatch(r -> r.endsWith("/" + refName)); + } + return false; + }) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + + if (baseType != null) { + return baseType; + } + + // Check if other oneOf items extend this candidate via allOf (parent-child without discriminator) + if (candidate != null) { + boolean isParent = oneOfSchemas.stream() + .filter(s -> s.get$ref() != null && !s.get$ref().equals(ref)) + .anyMatch(s -> { + String otherName = s.get$ref().substring(s.get$ref().lastIndexOf('/') + 1); + Schema otherSchema = schemas.get(otherName); + return otherSchema != null && otherSchema.getAllOf() != null && + otherSchema.getAllOf().stream().anyMatch( + a -> a.get$ref() != null && a.get$ref().endsWith("/" + refName)); + }); + if (isParent) { + return refName; + } + } } return null; } + + private void replaceInlineOneOfInContent(Content content, Map schemas) { + content.values().forEach(mediaType -> { + Schema schema = mediaType.getSchema(); + if (schema != null && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(schemas, schema.getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + mediaType.setSchema(refSchema); + log.debug("Replaced oneOf in content with $ref to {}", baseType); + } + } + }); + } + + @SuppressWarnings("unchecked") + private void replaceInlineOneOfProperties(Schema schema, Map allSchemas) { + if (schema == null || schema.getProperties() == null) { + return; + } + schema.getProperties().forEach((propName, propSchema) -> { + if (propSchema instanceof Schema) { + Schema prop = (Schema) propSchema; + + // Check if additionalProperties has oneOf (polymorphic map values) + if (prop.getAdditionalProperties() instanceof Schema) { + Schema additionalProps = (Schema) prop.getAdditionalProperties(); + if (additionalProps.getOneOf() != null && !additionalProps.getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + prop.setAdditionalProperties(refSchema); + log.debug("Replaced oneOf in additionalProperties with $ref to {} in property {}", baseType, propName); + } + } + // Check if additionalProperties is an array whose items has oneOf (e.g. Map>) + if (additionalProps.getItems() != null && additionalProps.getItems().getOneOf() != null && !additionalProps.getItems().getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getItems().getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + additionalProps.setItems(refSchema); + log.debug("Replaced oneOf in additionalProperties.items with $ref to {} in property {}", baseType, propName); + } + } + } + + // If property has oneOf, try to find the base discriminated type + if (prop.getOneOf() != null && !prop.getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(allSchemas, prop.getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + if (prop.getDescription() != null) { + refSchema.setDescription(prop.getDescription()); + } + if (prop.getReadOnly() != null) { + refSchema.setReadOnly(prop.getReadOnly()); + } + schema.getProperties().put(propName, refSchema); + log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName); + } + } + } + }); + } + private String tagItemFromPathItem(PathItem item) { var operations = item.readOperationsMap().values(); - var operation = operations.stream().findAny(); + var operation = operations.stream().findFirst(); if (operation.isPresent()) { var tags = operation.get().getTags(); if (tags != null && !tags.isEmpty()) { @@ -381,17 +706,20 @@ public class SwaggerConfiguration { StringBuilder sb = new StringBuilder(); for (String word : words) { - sb.append(word.substring(0, 1).toUpperCase()); - sb.append(word.substring(1).toLowerCase()); - sb.append(" "); + if (!word.isEmpty()) { + sb.append(word.substring(0, 1).toUpperCase()); + sb.append(word.substring(1).toLowerCase()); + sb.append(" "); + } } return new Tag().name(tagItem).description(sb.toString().trim()); } private void defaultErrorResponsesCustomization(PathItem pathItem) { - pathItem.readOperationsMap().forEach(((httpMethod, operation) -> { + pathItem.readOperationsMap().forEach((httpMethod, operation) -> { var errorResponses = httpMethod.equals(PathItem.HttpMethod.POST) ? defaultPostErrorResponses : defaultErrorResponses; + var responses = operation.getResponses(); if (responses == null) { responses = errorResponses; @@ -404,16 +732,19 @@ public class SwaggerConfiguration { }); } operation.setResponses(responses); - })); + }); } - private void securityCustomization(SecurityRequirement loginForm, Map.Entry entry) { + private void securityCustomization(Map.Entry entry, SecurityRequirement jwtBearerRequirement, SecurityRequirement apiKeyRequirement) { var path = entry.getKey(); - if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT)) { + if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT) && !path.equals(REFRESH_TOKEN_ENDPOINT)) { entry.getValue() .readOperationsMap() .values() - .forEach(operation -> operation.addSecurityItem(loginForm)); + .forEach(operation -> { + operation.addSecurityItem(jwtBearerRequirement); + operation.addSecurityItem(apiKeyRequirement); + }); } } @@ -428,6 +759,7 @@ public class SwaggerConfiguration { private static ApiResponses defaultErrorResponses(boolean isPost) { ApiResponses apiResponses = new ApiResponses(); + apiResponses.addApiResponse("400", errorResponse("400", "Bad Request", ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST))); @@ -449,29 +781,34 @@ public class SwaggerConfiguration { private static ApiResponses loginErrorResponses() { ApiResponses apiResponses = new ApiResponses(); - apiResponses.addApiResponse("401", errorResponse("Unauthorized", - Map.of( - "bad-credentials", errorExample("Bad credentials", - ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), - "token-expired", errorExample("JWT token expired", - ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)), - "account-disabled", errorExample("Disabled account", - ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), - "account-locked", errorExample("Locked account", - ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), - "authentication-failed", errorExample("General authentication error", - ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)) - ) - )); - var credentialsExpiredSchema = new Schema(); - credentialsExpiredSchema.$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse"); - apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)", - Map.of( - "credentials-expired", errorExample("Expired credentials", - ThingsboardCredentialsExpiredResponse.of("User password expired!", StringUtils.randomAlphanumeric(30))) - ), - credentialsExpiredSchema + Map unauthorizedExamples = new LinkedHashMap<>(); + + unauthorizedExamples.put("bad-credentials", errorExample("Bad credentials", + ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("token-expired", errorExample("JWT token expired", + ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("account-disabled", errorExample("Disabled account", + ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("account-locked", errorExample("Locked account", + ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("authentication-failed", errorExample("General authentication error", + ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("credentials-expired", errorExample("Expired credentials", + ThingsboardCredentialsExpiredResponse.of("User password expired!", "udgDQOpS1Q4ZFEL8qHF9s8cSKQ7d1h"))); + + Schema unauthorizedSchema = new Schema<>(); + unauthorizedSchema.oneOf(List.of( + new Schema().$ref("#/components/schemas/ThingsboardErrorResponse"), + new Schema().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse") )); + + apiResponses.addApiResponse("401", errorResponse("Unauthorized", unauthorizedExamples, unauthorizedSchema)); + return apiResponses; } @@ -480,22 +817,260 @@ public class SwaggerConfiguration { } private static ApiResponse errorResponse(String description, Map examples) { - var schema = new Schema(); - schema.$ref("#/components/schemas/ThingsboardErrorResponse"); + var schema = new Schema().$ref("#/components/schemas/ThingsboardErrorResponse"); return errorResponse(description, examples, schema); } private static ApiResponse errorResponse(String description, Map examples, Schema errorResponseSchema) { - MediaType mediaType = new MediaType().schema(errorResponseSchema); - mediaType.setExamples(examples); - Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType); + MediaType mediaType = new MediaType().schema(errorResponseSchema).examples(examples); + Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType); return new ApiResponse().description(description).content(content); } + /** + * Recursively collects all property names reachable from {@code schemaName}, walking the + * ancestor chain through allOf $ref entries (to handle multi-level inheritance). + * {@code visited} prevents infinite loops in case of circular references. + */ + @SuppressWarnings("unchecked") + private void collectAllProperties(String schemaName, Map allSchemas, + Set result, Set visited) { + if (!visited.add(schemaName)) { + return; + } + Schema schema = allSchemas.get(schemaName); + if (schema == null) { + return; + } + if (schema.getProperties() != null) { + result.addAll(schema.getProperties().keySet()); + } + if (schema.getAllOf() != null) { + for (Schema allOfElement : schema.getAllOf()) { + String ref = allOfElement.get$ref(); + if (ref != null) { + String refName = ref.substring(ref.lastIndexOf('/') + 1); + collectAllProperties(refName, allSchemas, result, visited); + } else if (allOfElement.getProperties() != null) { + result.addAll(allOfElement.getProperties().keySet()); + } + } + } + } + + @SuppressWarnings("unchecked") + private void deduplicateAllOfProperties(Schema schema, Map allSchemas, Set ownProps) { + if (schema.getAllOf() == null) { + return; + } + + // Collect properties defined in any $ref'd parent within the allOf, recursively + // walking the ancestor chain (each parent may itself use allOf to extend a grandparent). + Set parentProperties = new LinkedHashSet<>(); + for (Schema allOfElement : schema.getAllOf()) { + String ref = allOfElement.get$ref(); + if (ref != null) { + String refName = ref.substring(ref.lastIndexOf('/') + 1); + collectAllProperties(refName, allSchemas, parentProperties, new LinkedHashSet<>()); + } + } + + if (parentProperties.isEmpty()) { + return; + } + + // Properties to strip: in parent schema AND not declared as own-class fields. + // This removes inherited properties (from superclasses or pure interface getters) + // while keeping properties the class declares as its own fields. + Set toStrip = new LinkedHashSet<>(parentProperties); + toStrip.removeAll(ownProps); + + if (toStrip.isEmpty()) { + return; + } + + // Strip from inline (non-$ref) allOf elements + schema.getAllOf().removeIf(allOfElement -> { + if (allOfElement.get$ref() != null) { + return false; + } + if (allOfElement.getProperties() != null) { + Map filtered = new LinkedHashMap<>(allOfElement.getProperties()); + filtered.keySet().removeAll(toStrip); + allOfElement.setProperties(filtered.isEmpty() ? null : filtered); + } + return allOfElement.getProperties() == null + && allOfElement.getRequired() == null + && allOfElement.getType() == null; + }); + + // Remove stripped properties from the schema's required list + if (schema.getRequired() != null) { + List required = new ArrayList<>(schema.getRequired()); + required.removeAll(toStrip); + schema.setRequired(required.isEmpty() ? null : required); + } + } + + /** + * Computes the schema name that swagger-core will use for the given JavaType. + * For simple types, this is just the class simple name (e.g. {@code Device}). + * For parameterized types, type parameter names are appended + * (e.g. {@code PageData} becomes {@code PageDataDevice}). + * This matches the naming convention used by swagger-core's {@code TypeNameResolver}. + */ + private static String resolveSchemaName(JavaType javaType) { + Class cls = javaType.getRawClass(); + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + cls.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + if (schemaAnnotation != null && !schemaAnnotation.name().isEmpty()) { + return schemaAnnotation.name(); + } + StringBuilder sb = new StringBuilder(cls.getSimpleName()); + if (javaType.hasGenericTypes()) { + for (int i = 0; i < javaType.containedTypeCount(); i++) { + JavaType param = javaType.containedType(i); + if (param != null) { + sb.append(param.getRawClass().getSimpleName()); + } + } + } + return sb.toString(); + } + + /** + * Returns the JSON property names that are backed by fields declared directly in {@code cls} + * (not inherited from a superclass). Used to distinguish "own" from "inherited" properties + * when deduplicating allOf inline elements. + */ + private static Set computeOwnPropNames(Class cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) { + Map allFieldToJson = new LinkedHashMap<>(); + for (var prop : beanDesc.findProperties()) { + if (prop.getField() != null && prop.couldSerialize()) { + allFieldToJson.put(prop.getField().getName(), prop.getName()); + } + } + Set own = new LinkedHashSet<>(); + for (Field f : cls.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) continue; + String jsonName = allFieldToJson.get(f.getName()); + if (jsonName != null) own.add(jsonName); + } + return own; + } + + @SuppressWarnings("unchecked") + private static void reorderSchemaProperties(Schema schema, List propOrder) { + if (schema.getProperties() != null && schema.getProperties().size() > 1) { + schema.setProperties(reorderProperties(schema.getProperties(), propOrder)); + } + if (schema.getAllOf() != null) { + for (Schema allOfElement : schema.getAllOf()) { + if (allOfElement.get$ref() != null) continue; + if (allOfElement.getProperties() != null && allOfElement.getProperties().size() > 1) { + allOfElement.setProperties(reorderProperties(allOfElement.getProperties(), propOrder)); + } + } + } + } + + private static LinkedHashMap reorderProperties(Map current, List propOrder) { + var reordered = new LinkedHashMap(); + for (String name : propOrder) { + Schema prop = current.get(name); + if (prop != null) reordered.put(name, prop); + } + // Any properties not covered by propOrder are appended + // alphabetically to guarantee a deterministic stable order. + new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v)); + return reordered; + } + + /** + * Resolves the property ordering for a schema class. + * + *

Returns a list of JSON property names in the order they should appear in the + * OpenAPI schema. The caller uses this list to reorder the schema's property map; + * any properties not present in the returned list are appended alphabetically + * by the caller's {@code TreeMap} fallback, guaranteeing a stable, deterministic order. + * + *

Resolution strategy (first match wins): + *

    + *
  1. If {@code @JsonPropertyOrder} with an explicit {@code value()} is found on the + * class or any interface in its ancestry, that list is returned as-is. Note: if the + * annotation lists only a subset of fields, those fields are ordered first and the + * remaining properties fall through to the caller's alphabetical fallback — consistent + * with Jackson's own behaviour for partial {@code @JsonPropertyOrder}.
  2. + *
  3. Otherwise, field-backed properties are returned in declaration order (superclass + * fields first). Getter-only properties are intentionally excluded to avoid + * non-deterministic ordering across restarts.
  4. + *
+ */ + private static List resolvePropertyOrder(Class cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) { + // If an explicit @JsonPropertyOrder is present on the class or any interface in its + // ancestry, honour it directly. Walk up the class hierarchy; for each class also walk + // the full interface hierarchy (including super-interfaces) via BFS. + for (Class c = cls; c != null && c != Object.class; c = c.getSuperclass()) { + JsonPropertyOrder propOrder = c.getAnnotation(JsonPropertyOrder.class); + if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) { + return Arrays.asList(propOrder.value()); + } + Deque> ifaceQueue = new ArrayDeque<>(Arrays.asList(c.getInterfaces())); + Set> visitedIfaces = new LinkedHashSet<>(); + while (!ifaceQueue.isEmpty()) { + Class iface = ifaceQueue.poll(); + if (!visitedIfaces.add(iface)) continue; + propOrder = iface.getAnnotation(JsonPropertyOrder.class); + if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) { + return Arrays.asList(propOrder.value()); + } + ifaceQueue.addAll(Arrays.asList(iface.getInterfaces())); + } + } + + // Map backing field names to their JSON property names (respects @JsonProperty) + Map fieldToJsonName = new LinkedHashMap<>(); + for (var prop : beanDesc.findProperties()) { + if (prop.couldSerialize()) { + if (prop.getField() != null) { + fieldToJsonName.put(prop.getField().getName(), prop.getName()); + } else { + // For transient fields, Jackson may not associate the field with the property. + // Fall back to using the property name as the field name key. + fieldToJsonName.putIfAbsent(prop.getName(), prop.getName()); + } + } + } + + // Walk class hierarchy (superclass first) to get field declaration order + List> hierarchy = new ArrayList<>(); + for (Class c = cls; c != null && c != Object.class; c = c.getSuperclass()) { + hierarchy.add(0, c); + } + List ordered = new ArrayList<>(); + for (Class c : hierarchy) { + for (Field f : c.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) continue; + String jsonName = fieldToJsonName.get(f.getName()); + if (jsonName != null) ordered.add(jsonName); + } + } + + // Return only field-backed properties in declaration order. + // Getter-only properties (no backing field) are intentionally excluded: their set can vary + // between restarts (e.g. Optional-typed getters depend on Jackson module registration order), + // so including them here would make their position non-deterministic when some are in orderedNames + // and others are only in the schema map. The converter's TreeMap fallback handles ALL + // non-field-backed properties together in one alphabetical pass, guaranteeing stable order. + return ordered; + } + private static Example errorExample(String summary, ThingsboardErrorResponse example) { + var node = (ObjectNode) JacksonUtil.valueToTree(example); + node.put("timestamp", 1609459200000L); return new Example() .summary(summary) - .value(example); + .value(node); } -} +} \ No newline at end of file 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 712224b5d2..65bbe459c1 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -38,6 +38,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy; import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -46,21 +47,23 @@ import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.AuthExceptionHandler; +import org.thingsboard.server.service.security.auth.extractor.TokenExtractor; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.SkipPathRequestMatcher; -import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository; +import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider; +import org.thingsboard.server.service.security.auth.pat.ApiKeyTokenAuthenticationProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; import org.thingsboard.server.transport.http.config.PayloadSizeFilter; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; @Configuration @EnableWebSecurity @@ -69,10 +72,13 @@ import java.util.List; @TbCoreComponent public class ThingsboardSecurityConfiguration { - public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; - public static final String JWT_TOKEN_HEADER_PARAM_V2 = "Authorization"; + public static final String AUTHORIZATION_HEADER = "X-Authorization"; + public static final String AUTHORIZATION_HEADER_V2 = "Authorization"; public static final String JWT_TOKEN_QUERY_PARAM = "token"; + public static final String API_KEY_HEADER_PREFIX = "ApiKey "; + public static final String BEARER_HEADER_PREFIX = "Bearer "; + public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; @@ -114,6 +120,8 @@ public class ThingsboardSecurityConfiguration { private JwtAuthenticationProvider jwtAuthenticationProvider; @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; + @Autowired + private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider; @Autowired(required = false) OAuth2Configuration oauth2Configuration; @@ -122,6 +130,10 @@ public class ThingsboardSecurityConfiguration { @Qualifier("jwtHeaderTokenExtractor") private TokenExtractor jwtHeaderTokenExtractor; + @Autowired + @Qualifier("apiKeyHeaderTokenExtractor") + private TokenExtractor apiKeyHeaderTokenExtractor; + @Autowired private AuthenticationManager authenticationManager; @@ -140,7 +152,7 @@ public class ThingsboardSecurityConfiguration { } @Bean - protected FilterRegistrationBean buildEtagFilter() throws Exception { + protected FilterRegistrationBean buildEtagFilter() { ShallowEtagHeaderFilter etagFilter = new ShallowEtagHeaderFilter(); etagFilter.setWriteWeakETag(true); FilterRegistrationBean filterRegistrationBean @@ -151,25 +163,22 @@ public class ThingsboardSecurityConfiguration { } @Bean - protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception { + protected RestLoginProcessingFilter buildRestLoginProcessingFilter() { RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler); filter.setAuthenticationManager(this.authenticationManager); return filter; } @Bean - protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception { + protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() { RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler); filter.setAuthenticationManager(this.authenticationManager); return filter; } - protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { - List pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); - pathsToSkip.addAll(Arrays.asList(WS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, - PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT, - DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT)); - SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); + @Bean + protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() { + SkipPathRequestMatcher matcher = buildSkipPathRequestMatcher(); JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); filter.setAuthenticationManager(this.authenticationManager); @@ -177,7 +186,30 @@ public class ThingsboardSecurityConfiguration { } @Bean - protected RefreshTokenProcessingFilter buildRefreshTokenProcessingFilter() throws Exception { + protected ApiKeyTokenAuthenticationProcessingFilter buildApiKeyTokenAuthenticationProcessingFilter() { + SkipPathRequestMatcher matcher = buildSkipPathRequestMatcher(); + ApiKeyTokenAuthenticationProcessingFilter filter = + new ApiKeyTokenAuthenticationProcessingFilter(failureHandler, apiKeyHeaderTokenExtractor, matcher); + filter.setAuthenticationManager(this.authenticationManager); + return filter; + } + + private SkipPathRequestMatcher buildSkipPathRequestMatcher() { + List pathsToSkip = Stream.concat( + Arrays.stream(NON_TOKEN_BASED_AUTH_ENTRY_POINTS), + Stream.of( + WS_ENTRY_POINT, + TOKEN_REFRESH_ENTRY_POINT, + FORM_BASED_LOGIN_ENTRY_POINT, + PUBLIC_LOGIN_ENTRY_POINT, + DEVICE_API_ENTRY_POINT, + MAIL_OAUTH2_PROCESSING_ENTRY_POINT, + DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT)).toList(); + return new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); + } + + @Bean + protected RefreshTokenProcessingFilter buildRefreshTokenProcessingFilter() { RefreshTokenProcessingFilter filter = new RefreshTokenProcessingFilter(TOKEN_REFRESH_ENTRY_POINT, successHandler, failureHandler); filter.setAuthenticationManager(this.authenticationManager); return filter; @@ -188,6 +220,7 @@ public class ThingsboardSecurityConfiguration { return new ProviderManager(List.of( restAuthenticationProvider, jwtAuthenticationProvider, + apiKeyAuthenticationProvider, refreshTokenAuthenticationProvider )); } @@ -218,6 +251,7 @@ public class ThingsboardSecurityConfiguration { http.headers(headers -> { headers.defaultsDisabled(); headers.cacheControl(config -> {}); + headers.crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN)); httpSecurityHeadersCustomizer.customize(headers); }) .cors(cors -> {}) @@ -239,6 +273,7 @@ public class ThingsboardSecurityConfiguration { .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(buildApiKeyTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java index 38f37cf35d..4ce7268232 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java @@ -60,7 +60,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer { log.error("TbWebSocketHandler expected but [{}] provided", wsHandler); throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided"); } - registry.addHandler(wsHandler, WS_API_MAPPING).setAllowedOriginPatterns("*"); + registry.addHandler(wsHandler, WS_API_MAPPING) + .setAllowedOriginPatterns("*"); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 0b04a6939f..e4394bace3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -417,7 +417,7 @@ public class AdminController extends BaseController { "provider sends authorization code to specified redirect uri.)") @PreAuthorize("hasAuthority('SYS_ADMIN')") @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text") - public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { + public String getMailOAuth2AuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { String state = StringUtils.generateSafeToken(); if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180); @@ -442,7 +442,7 @@ public class AdminController extends BaseController { } @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"}) - public void codeProcessingUrl( + public void handleMailOAuth2Callback( @RequestParam(value = "code") String code, @RequestParam(value = "state") String state, HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { Optional prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME); 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 8f7c8bbbd1..0814e94700 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -227,12 +227,12 @@ public class AlarmController extends BaseController { return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser()); } - @ApiOperation(value = "Get Alarms (getAlarms)", + @ApiOperation(value = "Get Alarms (getAlarmsByEntity)", notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/alarm/{entityType}/{entityId}") - public PageData getAlarms( + public PageData getAlarmsByEntity( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java new file mode 100644 index 0000000000..957d05d1f4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java @@ -0,0 +1,259 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.databind.JsonNode; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +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.page.PageLink; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +public class AlarmRuleController extends BaseController { + + private final TbCalculatedFieldService tbCalculatedFieldService; + private final EventService eventService; + + public static final String ALARM_RULE_ID = "alarmRuleId"; + + private static final String TEST_SCRIPT_EXPRESSION = + "Execute the alarm rule TBEL condition expression and return the result. " + + "Alarm rule expressions must return a boolean value. The format of request: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"return temperature > 50;\",\n" + + " \"arguments\": {\n" + + " \"temperature\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 55 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + @ApiOperation(value = "Create Or Update Alarm Rule (saveAlarmRule)", + notes = "Creates or Updates the Alarm Rule. When creating alarm rule, platform generates Alarm Rule Id as " + UUID_WIKI_LINK + + "The newly created Alarm Rule Id will be present in the response. " + + "Specify existing Alarm Rule Id to update the alarm rule. " + + "Referencing non-existing Alarm Rule Id will cause 'Not Found' error. " + + "Remove 'id', 'tenantId' from the request body example (below) to create new Alarm Rule entity. " + + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule") + public AlarmRuleDefinition saveAlarmRule(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm rule.") + @RequestBody AlarmRuleDefinition alarmRuleDefinition) throws Exception { + alarmRuleDefinition.setTenantId(getTenantId()); + checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + if (alarmRuleDefinition.getId() != null) { + checkAlarmRule(alarmRuleDefinition.getId()); + } + CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField(); + checkReferencedEntities(calculatedField.getConfiguration()); + CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser()); + return AlarmRuleDefinition.fromCalculatedField(saved); + } + + @ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)", + notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}") + public AlarmRuleDefinition getAlarmRuleById(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + return AlarmRuleDefinition.fromCalculatedField(calculatedField); + } + + @ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)", + notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules/{entityType}/{entityId}") + public PageData getAlarmRulesByEntityId( + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + checkParameter("entityId", entityIdStr); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); + checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); + PageData result = checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, CalculatedFieldType.ALARM, pageLink)); + return result.mapData(AlarmRuleDefinition::fromCalculatedField); + } + + @ApiOperation(value = "Get alarm rules (getAlarmRules)", + notes = "Fetch tenant alarm rules based on the filter." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules") + public PageData getAlarmRules(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Entity type filter. If not specified, alarm rules for all supported entity types will be returned.") + @RequestParam(required = false) EntityType entityType, + @Parameter(description = "Entities filter. If not specified, alarm rules for entity type filter will be returned.") + @RequestParam(required = false) Set entities, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser user = getCurrentUser(); + + Set entityTypes; + if (entityType == null) { + entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream() + .filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } else { + entityTypes = EnumSet.of(entityType); + } + + CalculatedFieldFilter filter = CalculatedFieldFilter.builder() + .types(EnumSet.of(CalculatedFieldType.ALARM)) + .entityTypes(entityTypes) + .entityIds(entities) + .build(); + PageData result = calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); + return result.mapData(AlarmRuleDefinitionInfo::fromCalculatedFieldInfo); + } + + @ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)", + notes = "Fetch the list of alarm rule names." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules/names") + public PageData getAlarmRuleNames(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder); + return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), CalculatedFieldType.ALARM, pageLink); + } + + @ApiOperation(value = "Delete Alarm Rule (deleteAlarmRule)", + notes = "Deletes the alarm rule. Referencing non-existing Alarm Rule Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/alarm/rule/{alarmRuleId}") + @ResponseStatus(HttpStatus.OK) + public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); + checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); + } + + @ApiOperation(value = "Get latest alarm rule debug event (getLatestAlarmRuleDebugEvent)", + notes = "Gets latest alarm rule debug event for specified alarm rule id. " + + "Referencing non-existing alarm rule id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}/debug") + public JsonNode getLatestAlarmRuleDebugEvent(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + TenantId tenantId = getCurrentUser().getTenantId(); + return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) + .flatMap(events -> events.stream().map(EventInfo::getBody).findFirst()) + .orElse(null); + } + + @ApiOperation(value = "Test alarm rule TBEL expression (testAlarmRuleScript)", + notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule/testScript") + public JsonNode testAlarmRuleScript( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.") + @RequestBody JsonNode inputParams) throws ThingsboardException { + checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null); + return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams); + } + + private CalculatedField checkAlarmRule(CalculatedFieldId calculatedFieldId) throws ThingsboardException { + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkNotNull(calculatedField); + if (calculatedField.getType() != CalculatedFieldType.ALARM) { + throw new ThingsboardException("Alarm rule not found", ThingsboardErrorCode.ITEM_NOT_FOUND); + } + return calculatedField; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java new file mode 100644 index 0000000000..ae3ea1c60d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java @@ -0,0 +1,158 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.ApiKeyId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.pat.ApiKeyService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.Optional; +import java.util.UUID; + +import static org.thingsboard.server.controller.ControllerConstants.API_KEY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.API_KEY_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; + +@RestController +@TbCoreComponent +@Slf4j +@RequestMapping("/api") +@RequiredArgsConstructor +public class ApiKeyController extends BaseController { + + private final ApiKeyService apiKeyService; + + @ApiOperation(value = "Save API key for user (saveApiKey)", + notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey {value}'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @PostMapping(value = "/apiKey") + public ApiKey saveApiKey( + @Parameter(description = "A JSON value representing the Api Key token.") + @RequestBody @Valid ApiKeyInfo apiKeyInfo) throws ThingsboardException { + User user = checkUserId(apiKeyInfo.getUserId(), Operation.WRITE); + apiKeyInfo.setTenantId(user.getTenantId()); + checkEntity(apiKeyInfo.getId(), apiKeyInfo, Resource.API_KEY); + ApiKey savedApiKey = checkNotNull(apiKeyService.saveApiKey(apiKeyInfo.getTenantId(), apiKeyInfo)); + if (apiKeyInfo.getId() != null) { + savedApiKey.setValue(null); + } + return savedApiKey; + } + + @ApiOperation(value = "Get User Api Keys (getUserApiKeys)", + notes = "Returns a page of api keys owned by user. " + + PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/apiKeys/{userId}") + public PageData getUserApiKeys( + @Parameter(description = USER_ID_PARAM_DESCRIPTION) + @PathVariable("userId") String userIdStr, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = API_KEY_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "expirationTime", "description", "enabled"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + SecurityUser securityUser = getCurrentUser(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + UserId userId = new UserId(toUUID(userIdStr)); + accessControlService.checkPermission(securityUser, Resource.API_KEY, Operation.READ); + User user = checkUserId(userId, Operation.READ); + return apiKeyService.findApiKeysByUserId(user.getTenantId(), userId, pageLink); + } + + @ApiOperation(value = "Update API key Description", + notes = "Updates the description of the existing API key by apiKeyId. " + + "Only the description can be updated. " + + "Referencing a non-existing ApiKey Id will cause a 'Not Found' error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping("/apiKey/{id}/description") + public ApiKeyInfo updateApiKeyDescription( + @Parameter(description = API_KEY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable UUID id, + @Parameter(description = "New description for the API key", example = "Description") + @RequestBody Optional description) throws Exception { + ApiKeyId apiKeyId = new ApiKeyId(id); + ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE); + checkUserId(apiKey.getUserId(), Operation.WRITE); + apiKey.setDescription(description.orElse(null)); + return new ApiKeyInfo(apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey)); + } + + @ApiOperation(value = "Enable or disable API key (enableApiKey)", + notes = "Updates api key with enabled = true/false. " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/apiKey/{id}/enabled/{enabledValue}") + public ApiKeyInfo enableApiKey( + @Parameter(description = "Unique identifier of the API key to enable/disable", required = true) + @PathVariable UUID id, + @Parameter(description = "Enabled or disabled api key", required = true) + @PathVariable(value = "enabledValue") Boolean enabledValue) throws ThingsboardException { + ApiKeyId apiKeyId = new ApiKeyId(id); + ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE); + checkUserId(apiKey.getUserId(), Operation.WRITE); + apiKey.setEnabled(enabledValue); + return new ApiKeyInfo(apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey)); + } + + @ApiOperation(value = "Delete API key by ID (deleteApiKey)", + notes = "Deletes the API key. Referencing non-existing ApiKey Id will cause an error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @DeleteMapping(value = "/apiKey/{id}") + public void deleteApiKey(@PathVariable UUID id) throws ThingsboardException { + ApiKeyId apiKeyId = new ApiKeyId(id); + ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.DELETE); + checkUserId(apiKey.getUserId(), Operation.WRITE); + apiKeyService.deleteApiKey(apiKey.getTenantId(), apiKey, false); + } + +} 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 0030896582..4a20e2fcd2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,6 +24,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -34,6 +36,9 @@ 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.EntitySubtype; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; @@ -76,6 +81,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -83,6 +90,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @@ -137,10 +145,16 @@ public class AssetController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset", method = RequestMethod.POST) @ResponseBody - public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception { + public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { asset.setTenantId(getTenantId()); checkEntity(asset.getId(), asset, Resource.ASSET); - return tbAssetService.save(asset, getCurrentUser()); + return tbAssetService.save(asset, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete asset (deleteAsset)", @@ -205,8 +219,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/assets") public PageData getTenantAssets( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, @@ -233,8 +246,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets info objects owned by tenant. " + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/assetInfos") public PageData getTenantAssetInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, @@ -262,25 +274,30 @@ public class AssetController extends BaseController { } } - @ApiOperation(value = "Get Tenant Asset (getTenantAsset)", + @Hidden + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/assets", params = {"assetName"}) + public Asset getTenantAsset(@RequestParam String assetName) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); + } + + @ApiOperation(value = "Get Tenant Asset (getTenantAssetByName)", notes = "Requested asset must be owned by tenant that the user belongs to. " + "Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET) - @ResponseBody - public Asset getTenantAsset( + @GetMapping(value = "/tenant/asset") + public Asset getTenantAssetByName( @Parameter(description = ASSET_NAME_DESCRIPTION) @RequestParam String assetName) throws ThingsboardException { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); + return getTenantAsset(assetName); } @ApiOperation(value = "Get Customer Assets (getCustomerAssets)", notes = "Returns a page of assets objects assigned to customer. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/assets") public PageData getCustomerAssets( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -312,8 +329,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets info objects assigned to customer. " + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/assetInfos") public PageData getCustomerAssetInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -349,8 +365,7 @@ public class AssetController extends BaseController { @ApiOperation(value = "Get Assets By Ids (getAssetsByIds)", notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assets") public List getAssetsByIds( @Parameter(description = "A list of assets ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException { @@ -371,14 +386,14 @@ public class AssetController extends BaseController { return checkNotNull(assets.get()); } - @ApiOperation(value = "Find related assets (findByQuery)", + @ApiOperation(value = "Find related assets (findAssetsByQuery)", notes = "Returns all assets that are related to the specific entity. " + "The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " + "See 'Model' tab of the Parameters for more info.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/assets", method = RequestMethod.POST) @ResponseBody - public List findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { + public List findAssetsByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getAssetTypes()); @@ -457,8 +472,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets assigned to edge. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/assets") public PageData getEdgeAssets( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId, @@ -504,11 +518,11 @@ public class AssetController extends BaseController { return checkNotNull(filteredResult); } - @ApiOperation(value = "Import the bulk of assets (processAssetsBulkImport)", + @ApiOperation(value = "Import the bulk of assets (processAssetBulkImport)", notes = "There's an ability to import the bulk of assets using the only .csv file.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PostMapping("/asset/bulk_import") - public BulkImportResult processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception { + public BulkImportResult processAssetBulkImport(@RequestBody BulkImportRequest request) throws Exception { SecurityUser user = getCurrentUser(); return assetBulkImportService.processBulkImport(request, user); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 0233a488d6..86483413df 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -45,7 +48,10 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION; @@ -170,8 +176,7 @@ public class AssetProfileController extends BaseController { notes = "Returns a page of asset profile objects owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assetProfiles") public PageData getAssetProfiles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -191,8 +196,7 @@ public class AssetProfileController extends BaseController { notes = "Returns a page of asset profile info objects owned by tenant. " + PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assetProfileInfos") public PageData getAssetProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -222,4 +226,29 @@ public class AssetProfileController extends BaseController { return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly)); } + @Hidden + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"}) + public List getAssetProfilesByIdsV1( + @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("assetProfileIds") Set assetProfileUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + List assetProfileIds = new ArrayList<>(); + for (UUID assetProfileUUID : assetProfileUUIDs) { + assetProfileIds.add(new AssetProfileId(assetProfileUUID)); + } + return assetProfileService.findAssetProfilesByIds(tenantId, assetProfileIds); + } + + @ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)", + notes = "Requested asset profiles must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/assetProfileInfos/list") + public List getAssetProfilesByIds( + @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("assetProfileIds") Set assetProfileUUIDs) throws ThingsboardException { + return getAssetProfilesByIdsV1(assetProfileUUIDs); + } + } 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 e22c0aa156..6a42f034ba 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -18,11 +18,10 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.RestController; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; @@ -50,6 +49,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PA import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; @@ -73,8 +73,7 @@ public class AuditLogController extends BaseController { "and users actions (login, logout, etc.) that belong to this customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/customer/{customerId}") public PageData getAuditLogsByCustomerId( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -106,8 +105,7 @@ public class AuditLogController extends BaseController { "For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/user/{userId}") public PageData getAuditLogsByUserId( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable("userId") String strUserId, @@ -138,10 +136,9 @@ public class AuditLogController extends BaseController { notes = "Returns a page of audit logs related to the actions on the targeted entity. " + "Basically, this API call is used to get the full lifecycle of some specific entity. " + "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " + - PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/audit/logs/entity/{entityType}/{entityId}") public PageData getAuditLogsByEntityId( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String strEntityType, @@ -173,10 +170,9 @@ public class AuditLogController extends BaseController { @ApiOperation(value = "Get all audit logs (getAuditLogs)", notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " + - PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/audit/logs") public PageData getAuditLogs( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, 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 166fe312f5..af3a619906 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -39,6 +39,7 @@ 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.limit.LimitedApi; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; @@ -48,7 +49,9 @@ import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; +import org.thingsboard.server.service.security.auth.rest.RestAwareAuthenticationSuccessHandler; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; @@ -74,7 +77,8 @@ public class AuthController extends BaseController { private final SecuritySettingsService securitySettingsService; private final RateLimitService rateLimitService; private final ApplicationEventPublisher eventPublisher; - + private final TwoFactorAuthService twoFactorAuthService; + private final RestAwareAuthenticationSuccessHandler authenticationSuccessHandler; @ApiOperation(value = "Get current User (getUser)", notes = "Get the information about the User which credentials are used to perform this REST API call.") @@ -132,7 +136,7 @@ public class AuthController extends BaseController { "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " + "If token is not valid, returns '409 Conflict'. " + "If token is expired, redirects to error page.") - @GetMapping(value = "/noauth/activate", params = {"activateToken"}) + @GetMapping(value = "/noauth/activate") public ResponseEntity checkActivateToken( @Parameter(description = "The activate token string.") @RequestParam(value = "activateToken") String activateToken) { @@ -172,7 +176,7 @@ public class AuthController extends BaseController { "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " + "If token is not valid, returns '409 Conflict'. " + "If token is expired, redirects to error page.") - @GetMapping(value = "/noauth/resetPassword", params = {"resetToken"}) + @GetMapping(value = "/noauth/resetPassword") public ResponseEntity checkResetToken( @Parameter(description = "The reset token string.") @RequestParam(value = "resetToken") String resetToken) { @@ -221,7 +225,13 @@ public class AuthController extends BaseController { } } - var tokenPair = tokenFactory.createTokenPair(securityUser); + JwtPair tokenPair; + if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId(), user)) { + tokenPair = authenticationSuccessHandler.createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN); + } else { + tokenPair = tokenFactory.createTokenPair(securityUser); + } + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null); return tokenPair; } 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 de69c239e9..56f1cf6a4b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -71,6 +71,7 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; @@ -80,6 +81,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -118,6 +120,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Client; 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.pat.ApiKey; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -148,7 +151,6 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; 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.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; @@ -158,6 +160,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; @@ -171,6 +174,8 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.exception.EntitiesLimitExceededException; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; @@ -221,8 +226,6 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @TbCoreComponent public abstract class BaseController { - protected static final String DASHBOARD_ID = "dashboardId"; - protected static final String HOME_DASHBOARD_ID = "homeDashboardId"; protected static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar"; @@ -389,6 +392,9 @@ public abstract class BaseController { @Autowired protected TbAiModelService tbAiModelService; + @Autowired + protected ApiKeyService apiKeyService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -448,6 +454,8 @@ public abstract class BaseController { } if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; + } else if (exception instanceof EntitiesLimitExceededException) { + return new ThingsboardException(exception, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED); } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); @@ -509,8 +517,8 @@ public abstract class BaseController { } } - void checkParameter(String name, String param) throws ThingsboardException { - if (StringUtils.isEmpty(param)) { + static void checkParameter(String name, String param) throws ThingsboardException { + if (StringUtils.isBlank(param)) { throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } } @@ -648,6 +656,7 @@ public abstract class BaseController { case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); case AI_MODEL -> checkAiModelId(new AiModelId(entityId.getId()), operation); + case API_KEY -> checkApiKeyId(new ApiKeyId(entityId.getId()), operation); default -> (HasId) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); }; } catch (Exception e) { @@ -657,7 +666,7 @@ public abstract class BaseController { protected & HasTenantId, I extends EntityId> E checkEntityId(I entityId, ThrowingBiFunction findingFunction, Operation operation) throws ThingsboardException { try { - validateId((UUIDBased) entityId, "Invalid entity id"); + validateId((UUIDBased) entityId, id -> "Invalid entity id"); SecurityUser user = getCurrentUser(); E entity = findingFunction.apply(user.getTenantId(), entityId); checkNotNull(entity, entityId.getEntityType().getNormalName() + " with id [" + entityId + "] is not found"); @@ -673,6 +682,17 @@ public abstract class BaseController { return entity; } + protected void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { + for (EntityId referencedEntityId : calculatedFieldConfig.getReferencedEntities()) { + EntityType refEntityType = referencedEntityId.getEntityType(); + switch (refEntityType) { + case TENANT -> {} + case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); + default -> throw new IllegalArgumentException("Unsupported referenced entity type: '" + refEntityType + "'."); + } + } + } + Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException { return checkEntityId(deviceId, deviceService::findDeviceById, operation); } @@ -855,12 +875,16 @@ public abstract class BaseController { return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation); } + ApiKey checkApiKeyId(ApiKeyId apiKeyId, Operation operation) throws ThingsboardException { + return checkEntityId(apiKeyId, apiKeyService::findApiKeyById, operation); + } + protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } public static Exception toException(Throwable error) { - return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null; + return error != null ? (error instanceof Exception ? (Exception) error : new Exception(error)) : null; } protected > void logEntityAction(SecurityUser user, EntityType entityType, E savedEntity, ActionType actionType) { @@ -869,10 +893,8 @@ public abstract class BaseController { protected > void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) { EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType); - if (!user.isSystemAdmin()) { - entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, - user.getCustomerId(), actionType, e); - } + entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, + user.getCustomerId(), actionType, e); } protected > E doSaveAndLog(EntityType entityType, E entity, BiFunction savingFunction) throws Exception { @@ -939,7 +961,7 @@ public abstract class BaseController { } private CalculatedField checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException { - validateId(calculatedFieldId, "Invalid entity id"); + validateId(calculatedFieldId, id -> "Invalid entity id"); SecurityUser user = getCurrentUser(); CalculatedField cf = calculatedFieldService.findById(user.getTenantId(), calculatedFieldId); checkNotNull(cf, calculatedFieldId.getEntityType().getNormalName() + " with id [" + calculatedFieldId + "] is not found"); diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index bf42a5c044..84ff4b49a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -15,16 +15,18 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -34,16 +36,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; -import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; -import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; -import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -56,17 +54,15 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.EnumSet; +import java.util.HashSet; import java.util.Optional; -import java.util.concurrent.TimeUnit; +import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; @@ -85,43 +81,39 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @TbCoreComponent @RequestMapping("/api") @RequiredArgsConstructor -@Slf4j public class CalculatedFieldController extends BaseController { private final TbCalculatedFieldService tbCalculatedFieldService; private final EventService eventService; - private final TbelInvokeService tbelInvokeService; public static final String CALCULATED_FIELD_ID = "calculatedFieldId"; - public static final int TIMEOUT = 20; - private static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" - + MARKDOWN_CODE_BLOCK_START - + "{\n" + - " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + - " \"arguments\": {\n" + - " \"temperature\": {\n" + - " \"type\": \"TS_ROLLING\",\n" + - " \"timeWindow\": {\n" + - " \"startTs\": 1739775630002,\n" + - " \"endTs\": 65432211,\n" + - " \"limit\": 5\n" + - " },\n" + - " \"values\": [\n" + - " { \"ts\": 1739775639851, \"value\": 23 },\n" + - " { \"ts\": 1739775664561, \"value\": 43 },\n" + - " { \"ts\": 1739775713079, \"value\": 15 },\n" + - " { \"ts\": 1739775999522, \"value\": 34 },\n" + - " { \"ts\": 1739776228452, \"value\": 22 }\n" + - " ]\n" + - " },\n" + - " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + - " }\n" + - "}" - + MARKDOWN_CODE_BLOCK_END - + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + + " \"arguments\": {\n" + + " \"temperature\": {\n" + + " \"type\": \"TS_ROLLING\",\n" + + " \"timeWindow\": {\n" + + " \"startTs\": 1739775630002,\n" + + " \"endTs\": 65432211,\n" + + " \"limit\": 5\n" + + " },\n" + + " \"values\": [\n" + + " { \"ts\": 1739775639851, \"value\": 23 },\n" + + " { \"ts\": 1739775664561, \"value\": 43 },\n" + + " { \"ts\": 1739775713079, \"value\": 15 },\n" + + " { \"ts\": 1739775999522, \"value\": 34 },\n" + + " { \"ts\": 1739776228452, \"value\": 22 }\n" + + " ]\n" + + " },\n" + + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; @ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)", notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK + @@ -154,24 +146,107 @@ public class CalculatedFieldController extends BaseController { return calculatedField; } + @Hidden + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}) + public PageData getCalculatedFieldsByEntityIdV1(@PathVariable("entityType") String entityType, + @PathVariable("entityId") String entityIdStr, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) CalculatedFieldType type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + checkParameter("entityId", entityIdStr); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); + checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); + return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink)); + } + @ApiOperation(value = "Get Calculated Fields by Entity Id (getCalculatedFieldsByEntityId)", notes = "Fetch the Calculated Fields based on the provided Entity Id." ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}) + @GetMapping(value = "/calculatedField/{entityType}/{entityId}") public PageData getCalculatedFieldsByEntityId( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = "Calculated field type. If not specified, all types will be returned.") + @RequestParam(required = false) CalculatedFieldType type, @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + return getCalculatedFieldsByEntityIdV1(entityType, entityIdStr, pageSize, page, type, textSearch, sortProperty, sortOrder); + } + + @ApiOperation(value = "Get calculated fields (getCalculatedFields)", + notes = "Fetch tenant calculated fields based on the filter.") + @Parameters({ + @Parameter(name = "name", description = "Repeatable name query parameter", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/calculatedFields") + public PageData getCalculatedFields(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Calculated field types filter.") + @RequestParam(required = false) Set types, + @Parameter(description = "Entity type filter. If not specified, calculated fields for all supported entity types will be returned.") + @RequestParam(required = false) EntityType entityType, + @Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.") + @RequestParam(required = false) Set entities, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - checkParameter("entityId", entityIdStr); - EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); - checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); - return checkNotNull(tbCalculatedFieldService.findAllByTenantIdAndEntityId(entityId, getCurrentUser(), pageLink)); + SecurityUser user = getCurrentUser(); + + if (CollectionUtils.isEmpty(types)) { + types = EnumSet.allOf(CalculatedFieldType.class); + types.remove(CalculatedFieldType.ALARM); + } + + Set entityTypes; + if (entityType == null) { + entityTypes = CalculatedField.SUPPORTED_ENTITIES.keySet(); + } else { + entityTypes = EnumSet.of(entityType); + } + + CalculatedFieldFilter filter = CalculatedFieldFilter.builder() + .types(types) + .entityTypes(entityTypes) + .entityIds(entities) + .names(Optional.ofNullable(params.get("name")).map(HashSet::new).orElse(null)) + .build(); + return calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); + } + + @ApiOperation(value = "Get calculated field names (getCalculatedFieldNames)", + notes = "Fetch the list of calculated field names for specified type.") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/calculatedFields/names") + public PageData getCalculatedFieldNames(@Parameter(description = "Calculated field type filter.") + @RequestParam CalculatedFieldType type, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder); + return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), type, pageLink); } @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", @@ -207,89 +282,11 @@ public class CalculatedFieldController extends BaseController { notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/calculatedField/testScript") - public JsonNode testScript( + public JsonNode testCalculatedFieldScript( @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") - @RequestBody JsonNode inputParams) { - String expression = inputParams.get("expression").asText(); - Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), - Collections.emptyMap() - ); - - ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); - ctxAndArgNames.add("ctx"); - ctxAndArgNames.addAll(arguments.keySet()); - - String output = ""; - String errorText = ""; - - CalculatedFieldTbelScriptEngine engine = null; - try { - if (tbelInvokeService == null) { - throw new IllegalArgumentException("TBEL script engine is disabled!"); - } - - engine = new CalculatedFieldTbelScriptEngine( - getTenantId(), - tbelInvokeService, - expression, - ctxAndArgNames.toArray(String[]::new) - ); - - Object[] args = new Object[ctxAndArgNames.size()]; - args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); - for (int i = 1; i < ctxAndArgNames.size(); i++) { - var arg = arguments.get(ctxAndArgNames.get(i)); - if (arg instanceof TbelCfSingleValueArg svArg) { - args[i] = svArg.getValue(); - } else { - args[i] = arg; - } - } - - JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(json); - } catch (Exception e) { - log.error("Error evaluating expression", e); - Throwable rootCause = ExceptionUtils.getRootCause(e); - errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); - } finally { - if (engine != null) { - engine.destroy(); - } - } - return JacksonUtil.newObjectNode() - .put("output", output) - .put("error", errorText); - } - - private long getLatestTimestamp(Map arguments) { - long lastUpdateTimestamp = -1; - for (TbelCfArg entry : arguments.values()) { - if (entry instanceof TbelCfSingleValueArg singleValueArg) { - long ts = singleValueArg.getTs(); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); - } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { - long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); - } - } - return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; - } - - private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { - List referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); - for (EntityId referencedEntityId : referencedEntityIds) { - EntityType entityType = referencedEntityId.getEntityType(); - switch (entityType) { - case TENANT -> { - return; - } - case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); - } - } - + @RequestBody JsonNode inputParams) throws ThingsboardException { + checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null); + return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 55dbb8fe04..91569261b7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -81,8 +82,7 @@ public class ComponentDescriptorController extends BaseController { notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") - @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/components") public List getComponentDescriptorsByTypes( @Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("componentTypes") String[] strComponentTypes, diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 3d2cf2e50c..04afb1330e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -64,6 +64,7 @@ public class ControllerConstants { protected static final String WIDGET_TYPE_ID_PARAM_DESCRIPTION = "A string value representing the widget type id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String VC_REQUEST_ID_PARAM_DESCRIPTION = "A string value representing the version control request id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RESOURCE_ID_PARAM_DESCRIPTION = "A string value representing the resource id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String API_KEY_ID_PARAM_DESCRIPTION = "A string value representing the api key id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority."; protected static final String SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority."; protected static final String TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' authority."; @@ -91,6 +92,7 @@ public class ControllerConstants { protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; protected static final String AI_MODEL_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model name, provider and model ID."; + protected static final String API_KEY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the description."; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; @@ -442,106 +444,6 @@ public class ControllerConstants { " * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" + " * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n"; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"schedule\":{\n" + - " \"type\":\"SPECIFIC_TIME\",\n" + - " \"endsOn\":64800000,\n" + - " \"startsOn\":43200000,\n" + - " \"timezone\":\"Europe/Kiev\",\n" + - " \"daysOfWeek\":[\n" + - " 1,\n" + - " 3,\n" + - " 5\n" + - " ]\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"schedule\":{\n" + - " \"type\":\"CUSTOM\",\n" + - " \"items\":[\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":1\n" + - " },\n" + - " {\n" + - " \"endsOn\":64800000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":43200000,\n" + - " \"dayOfWeek\":2\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":3\n" + - " },\n" + - " {\n" + - " \"endsOn\":57600000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":36000000,\n" + - " \"dayOfWeek\":4\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":5\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":6\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":7\n" + - " }\n" + - " ],\n" + - " \"timezone\":\"Europe/Kiev\"\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END; - - protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"spec\":{\n" + - " \"type\":\"REPEATING\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":5,\n" + - " \"dynamicValue\":{\n" + - " \"inherit\":true,\n" + - " \"sourceType\":\"CURRENT_DEVICE\",\n" + - " \"sourceAttribute\":\"tempAttr\"\n" + - " }\n" + - " }\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - - protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"spec\":{\n" + - " \"type\":\"DURATION\",\n" + - " \"unit\":\"MINUTES\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30,\n" + - " \"dynamicValue\":null\n" + - " }\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - protected static final String RELATION_TYPE_PARAM_DESCRIPTION = "A string value representing relation type between entities. For example, 'Contains', 'Manages'. It can be any string value."; protected static final String RELATION_TYPE_GROUP_PARAM_DESCRIPTION = "A string value representing relation type group. For example, 'COMMON'"; @@ -1326,8 +1228,6 @@ public class ControllerConstants { ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE; protected static final String DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + - " \"alarms\":[\n" + - " ],\n" + " \"configuration\":{\n" + " \"type\":\"DEFAULT\"\n" + " },\n" + @@ -1341,219 +1241,6 @@ public class ControllerConstants { "}" + MARKDOWN_CODE_BLOCK_END; protected static final String CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + - " \"alarms\":[\n" + - " {\n" + - " \"id\":\"2492b935-1226-59e9-8615-17d8978a4f93\",\n" + - " \"alarmType\":\"Temperature Alarm\",\n" + - " \"clearRule\":{\n" + - " \"schedule\":null,\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"SIMPLE\"\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"temperature\",\n" + - " \"type\":\"TIME_SERIES\"\n" + - " },\n" + - " \"value\":null,\n" + - " \"predicate\":{\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"LESS\"\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " },\n" + - " \"propagate\":false,\n" + - " \"createRules\":{\n" + - " \"MAJOR\":{\n" + - " \"schedule\":{\n" + - " \"type\":\"SPECIFIC_TIME\",\n" + - " \"endsOn\":64800000,\n" + - " \"startsOn\":43200000,\n" + - " \"timezone\":\"Europe/Kiev\",\n" + - " \"daysOfWeek\":[\n" + - " 1,\n" + - " 3,\n" + - " 5\n" + - " ]\n" + - " },\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"DURATION\",\n" + - " \"unit\":\"MINUTES\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30,\n" + - " \"dynamicValue\":null\n" + - " }\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"temperature\",\n" + - " \"type\":\"TIME_SERIES\"\n" + - " },\n" + - " \"value\":null,\n" + - " \"predicate\":{\n" + - " \"type\":\"COMPLEX\",\n" + - " \"operation\":\"OR\",\n" + - " \"predicates\":[\n" + - " {\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":50.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"LESS_OR_EQUAL\"\n" + - " },\n" + - " {\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"GREATER\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " },\n" + - " \"WARNING\":{\n" + - " \"schedule\":{\n" + - " \"type\":\"CUSTOM\",\n" + - " \"items\":[\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":1\n" + - " },\n" + - " {\n" + - " \"endsOn\":64800000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":43200000,\n" + - " \"dayOfWeek\":2\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":3\n" + - " },\n" + - " {\n" + - " \"endsOn\":57600000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":36000000,\n" + - " \"dayOfWeek\":4\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":5\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":6\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":7\n" + - " }\n" + - " ],\n" + - " \"timezone\":\"Europe/Kiev\"\n" + - " },\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"REPEATING\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":5,\n" + - " \"dynamicValue\":null\n" + - " }\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"tempConstant\",\n" + - " \"type\":\"CONSTANT\"\n" + - " },\n" + - " \"value\":30,\n" + - " \"predicate\":{\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":0.0,\n" + - " \"dynamicValue\":{\n" + - " \"inherit\":false,\n" + - " \"sourceType\":\"CURRENT_DEVICE\",\n" + - " \"sourceAttribute\":\"tempThreshold\"\n" + - " }\n" + - " },\n" + - " \"operation\":\"EQUAL\"\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " },\n" + - " \"CRITICAL\":{\n" + - " \"schedule\":null,\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"SIMPLE\"\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"temperature\",\n" + - " \"type\":\"TIME_SERIES\"\n" + - " },\n" + - " \"value\":null,\n" + - " \"predicate\":{\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":50.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"GREATER\"\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " }\n" + - " },\n" + - " \"propagateRelationTypes\":null\n" + - " }\n" + - " ],\n" + " \"configuration\":{\n" + " \"type\":\"DEFAULT\"\n" + " },\n" + @@ -1575,40 +1262,11 @@ public class ControllerConstants { " }\n" + "}" + MARKDOWN_CODE_BLOCK_END; protected static final String DEVICE_PROFILE_DATA_DEFINITION = NEW_LINE + "# Device profile data definition" + NEW_LINE + - "Device profile data object contains alarm rules configuration, device provision strategy and transport type configuration for device connectivity. Let's review some examples. " + + "Device profile data object contains device provision strategy and transport type configuration for device connectivity. Let's review some examples. " + "First one is the default device profile data configuration and second one - the custom one. " + NEW_LINE + DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + "Let's review some specific objects examples related to the device profile configuration:"; - protected static final String ALARM_SCHEDULE = NEW_LINE + "# Alarm Schedule" + NEW_LINE + - "Alarm Schedule JSON object represents the time interval during which the alarm rule is active. Note, " + - NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE + NEW_LINE + "means alarm rule is active all the time. " + - "**'daysOfWeek'** field represents Monday as 1, Tuesday as 2 and so on. **'startsOn'** and **'endsOn'** fields represent hours in millis (e.g. 64800000 = 18:00 or 6pm). " + - "**'enabled'** flag specifies if item in a custom rule is active for specific day of the week:" + NEW_LINE + - "## Specific Time Schedule" + NEW_LINE + - DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE + NEW_LINE + - "## Custom Schedule" + - NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE + NEW_LINE; - - protected static final String ALARM_CONDITION_TYPE = "# Alarm condition type (**'spec'**)" + NEW_LINE + - "Alarm condition type can be either simple, duration, or repeating. For example, 5 times in a row or during 5 minutes." + NEW_LINE + - "Note, **'userValue'** field is not used and reserved for future usage, **'dynamicValue'** is used for condition appliance by using the value of the **'sourceAttribute'** " + - "or else **'defaultValue'** is used (if **'sourceAttribute'** is absent).\n" + - "\n**'sourceType'** of the **'sourceAttribute'** can be: \n" + - " * 'CURRENT_DEVICE';\n" + - " * 'CURRENT_CUSTOMER';\n" + - " * 'CURRENT_TENANT'." + NEW_LINE + - "**'sourceAttribute'** can be inherited from the owner if **'inherit'** is set to true (for CURRENT_DEVICE and CURRENT_CUSTOMER)." + NEW_LINE + - "## Repeating alarm condition" + NEW_LINE + - DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE + NEW_LINE + - "## Duration alarm condition" + NEW_LINE + - DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE + NEW_LINE + - "**'unit'** can be: \n" + - " * 'SECONDS';\n" + - " * 'MINUTES';\n" + - " * 'HOURS';\n" + - " * 'DAYS'." + NEW_LINE; - protected static final String PROVISION_CONFIGURATION = "# Provision Configuration" + NEW_LINE + "There are 3 types of device provision configuration for the device profile: \n" + " * 'DISABLED';\n" + @@ -1616,8 +1274,8 @@ public class ControllerConstants { " * 'CHECK_PRE_PROVISIONED_DEVICES'." + NEW_LINE + "Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-provisioning/) for more details." + NEW_LINE; - protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + ALARM_SCHEDULE + ALARM_CONDITION_TYPE + - KEY_FILTERS_DESCRIPTION + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION; + protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION; protected static final String DEVICE_PROFILE_ID = "deviceProfileId"; @@ -1630,11 +1288,13 @@ public class ControllerConstants { protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION; protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'."; - protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'."; + protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'. " + + "If attribute keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details."; protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys."; - protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'."; + protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'. " + + "If telemetry keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility"; protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details."; @@ -1744,4 +1404,18 @@ public class ControllerConstants { MARKDOWN_CODE_BLOCK_END ; protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities)."; + + public static final String NAME_CONFLICT_POLICY_DESC = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " + + " If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " + + " UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs."; + + public static final String UNIQUIFY_SEPARATOR_DESC = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " + + "For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for entity name 'test-name', " + + "created entity will have name like 'test-name-7fsh4f'."; + + public static final String UNIQUIFY_STRATEGY_DESC = "Optional value of uniquify strategy used by UNIQUIFY policy. Possible values: RANDOM or INCREMENTAL. " + + "By default, RANDOM strategy is used, which means random alphanumeric string will be added as a suffix to entity name. " + + "INCREMENTAL implies the first possible number starting from 1 will be added as a name suffix. " + + "For example, strategy is UNIQUIFY, uniquify strategy is INCREMENTAL; if a name conflict occurs for entity name 'test-name', " + + "created entity will have name like 'test-name-1."; } 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 a29d8ab4c8..5acf09a779 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -17,11 +17,14 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -32,6 +35,9 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -43,10 +49,17 @@ import org.thingsboard.server.service.entitiy.customer.TbCustomerService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -54,6 +67,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; @RestController @@ -128,10 +142,16 @@ public class CustomerController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer", method = RequestMethod.POST) @ResponseBody - public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer) throws Exception { + public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { customer.setTenantId(getTenantId()); checkEntity(customer.getId(), customer, Resource.CUSTOMER); - return tbCustomerService.save(customer, getCurrentUser()); + return tbCustomerService.save(customer, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete Customer (deleteCustomer)", @@ -153,8 +173,7 @@ public class CustomerController extends BaseController { notes = "Returns a page of customers owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customers") public PageData getCustomers( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -174,8 +193,7 @@ public class CustomerController extends BaseController { @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)", notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/customers") public Customer getTenantCustomer( @Parameter(description = "A string value representing the Customer title.") @RequestParam String customerTitle) throws ThingsboardException { @@ -183,4 +201,29 @@ public class CustomerController extends BaseController { return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found"); } + @Hidden + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/customers", params = {"customerIds"}) + public List getCustomersByIdsV1( + @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("customerIds") Set customerUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + List customerIds = new ArrayList<>(); + for (UUID customerUUID : customerUUIDs) { + customerIds.add(new CustomerId(customerUUID)); + } + return customerService.findCustomersByTenantIdAndIds(tenantId, customerIds); + } + + @ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)", + notes = "Returns a list of Customer objects based on the provided ids." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/customers/list") + public List getCustomersByIds( + @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("customerIds") Set customerUUIDs) throws ThingsboardException { + return getCustomersByIdsV1(customerUUIDs); + } + } 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 16f9e8f299..9f59044356 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -17,9 +17,10 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletResponse; @@ -35,9 +36,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; 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.common.util.JacksonUtil; @@ -64,6 +63,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -119,8 +119,8 @@ public class DashboardController extends BaseController { "Used to adjust view of the dashboards according to the difference between browser and server time.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/serverTime") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137"))) - public long getServerTime() throws ThingsboardException { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "1636023857137"))) + public long getServerTime() { return System.currentTimeMillis(); } @@ -131,8 +131,8 @@ public class DashboardController extends BaseController { "The actual value of the limit is configurable in the system configuration file.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/maxDatapointsLimit") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000"))) - public long getMaxDatapointsLimit() throws ThingsboardException { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "5000"))) + public long getMaxDatapointsLimit() { return maxDatapointsLimit; } @@ -151,14 +151,16 @@ public class DashboardController extends BaseController { @ApiOperation(value = "Get Dashboard (getDashboardById)", notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH ) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = Dashboard.class))) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/{dashboardId}") public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) - @PathVariable(DASHBOARD_ID) String strDashboardId, - @Parameter(description = INCLUDE_RESOURCES_DESCRIPTION) - @RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources, - @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, - HttpServletResponse response) throws Exception { + @PathVariable(DASHBOARD_ID) String strDashboardId, + @Parameter(description = INCLUDE_RESOURCES_DESCRIPTION) + @RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources, + @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, + HttpServletResponse response) throws Exception { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); @@ -176,12 +178,14 @@ public class DashboardController extends BaseController { "Referencing non-existing dashboard Id will cause 'Not Found' error. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " + TENANT_AUTHORITY_PARAGRAPH) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = Dashboard.class))) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping(value = "/dashboard") public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.") - @RequestBody Dashboard dashboard, - @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, - HttpServletResponse response) throws Exception { + @RequestBody Dashboard dashboard, + @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, + HttpServletResponse response) throws Exception { dashboard.setTenantId(getTenantId()); checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD); var savedDashboard = tbDashboardService.save(dashboard, getCurrentUser()); @@ -301,8 +305,7 @@ public class DashboardController extends BaseController { "[assign Device to Public Customer](#!/device-controller/assignDeviceToPublicCustomerUsingPOST) for this purpose. " + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/customer/public/dashboard/{dashboardId}") public Dashboard assignDashboardToPublicCustomer( @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @@ -316,8 +319,7 @@ public class DashboardController extends BaseController { notes = "Unassigns the dashboard from a special, auto-generated 'Public' Customer. Once unassigned, unauthenticated users may no longer browse the dashboard. " + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/customer/public/dashboard/{dashboardId}") public Dashboard unassignDashboardFromPublicCustomer( @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @@ -327,13 +329,12 @@ public class DashboardController extends BaseController { return tbDashboardService.unassignDashboardFromPublicCustomer(dashboard, getCurrentUser()); } - @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboards)", + @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboardsByTenantId)", notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody - public PageData getTenantDashboards( + @GetMapping(value = "/tenant/{tenantId}/dashboards") + public PageData getTenantDashboardsByTenantId( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TENANT_ID) String strTenantId, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -356,8 +357,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects owned by the tenant of a current user. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/dashboards") public PageData getTenantDashboards( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -384,8 +384,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects owned by the specified customer. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/dashboards") public PageData getCustomerDashboards( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -418,6 +417,8 @@ public class DashboardController extends BaseController { "If 'homeDashboardId' parameter is not set on the User level and the User has authority 'CUSTOMER_USER', check the same parameter for the corresponding Customer. " + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = HomeDashboard.class))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/home") public void getHomeDashboard(@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, @@ -454,8 +455,7 @@ public class DashboardController extends BaseController { "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/dashboard/home/info") public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException { SecurityUser securityUser = getCurrentUser(); if (securityUser.isSystemAdmin()) { @@ -470,8 +470,7 @@ public class DashboardController extends BaseController { notes = "Returns the home dashboard info object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the corresponding tenant. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/dashboard/home/info") public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException { Tenant tenant = tenantService.findTenantById(getTenantId()); JsonNode additionalInfo = tenant.getAdditionalInfo(); @@ -491,7 +490,7 @@ public class DashboardController extends BaseController { notes = "Update the home dashboard assignment for the current tenant. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST) + @PostMapping(value = "/tenant/dashboard/home/info") @ResponseStatus(value = HttpStatus.OK) public void setTenantHomeDashboardInfo( @Parameter(description = "A JSON object that represents home dashboard id and other parameters", required = true) @@ -540,8 +539,7 @@ public class DashboardController extends BaseController { "Third, once dashboard will be delivered to edge service, it's going to be available for usage on remote edge instance." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}") public Dashboard assignDashboardToEdge(@PathVariable("edgeId") String strEdgeId, @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); @@ -563,8 +561,7 @@ public class DashboardController extends BaseController { "Third, once 'unassign' command will be delivered to edge service, it's going to remove dashboard locally." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}") public Dashboard unassignDashboardFromEdge(@PathVariable("edgeId") String strEdgeId, @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); @@ -583,8 +580,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects assigned to the specified edge. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/dashboards") public PageData getEdgeDashboards( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, @@ -604,14 +600,7 @@ public class DashboardController extends BaseController { checkEdgeId(edgeId, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageData nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - List filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboardInfo.getId(), dashboardInfo); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); + List filteredDashboards = filterDashboardsByReadPermission(nonFilteredResult.getData()); PageData filteredResult = new PageData<>(filteredDashboards, nonFilteredResult.getTotalPages(), nonFilteredResult.getTotalElements(), @@ -619,6 +608,29 @@ public class DashboardController extends BaseController { return checkNotNull(filteredResult); } + @Hidden + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/dashboards", params = {"dashboardIds"}) + public List getDashboardsByIdsV1(@RequestParam("dashboardIds") Set dashboardUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + List dashboardIds = new ArrayList<>(); + for (UUID dashboardUUID : dashboardUUIDs) { + dashboardIds.add(new DashboardId(dashboardUUID)); + } + List dashboards = dashboardService.findDashboardInfoByIds(tenantId, dashboardIds); + return filterDashboardsByReadPermission(dashboards); + } + + @ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)", + notes = "Returns a list of DashboardInfo objects based on the provided ids. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/dashboards/list") + public List getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("dashboardIds") Set dashboardUUIDs) throws ThingsboardException { + return getDashboardsByIdsV1(dashboardUUIDs); + } + private Set customerIdFromStr(String[] strCustomerIds) { Set customerIds = new HashSet<>(); if (strCustomerIds != null) { @@ -629,4 +641,14 @@ public class DashboardController extends BaseController { return customerIds; } + private List filterDashboardsByReadPermission(List dashboards) { + return dashboards.stream().filter(dashboard -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboard.getId(), dashboard); + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java index 8b899ded41..b7e94d2212 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -75,6 +76,7 @@ public class DeviceConnectivityController extends BaseController { description = "OK", content = @Content( mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = JsonNode.class), examples = { @ExampleObject( name = "http", @@ -105,7 +107,18 @@ public class DeviceConnectivityController extends BaseController { return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device); } - @ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.") + @ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", + notes = "Download server certificate.", + responses = { + @ApiResponse( + responseCode = "200", + description = "OK", + content = @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, + schema = @Schema(type = "string", format = "binary") + ) + ) + }) @RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET) @ResponseBody public ResponseEntity downloadServerCertificate(@Parameter(description = PROTOCOL_PARAM_DESCRIPTION) @@ -122,7 +135,18 @@ public class DeviceConnectivityController extends BaseController { .body(pemCert); } - @ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)", notes = "Download generated docker-compose.yml for gateway.") + @ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)", + notes = "Download generated docker-compose.yml for gateway.", + responses = { + @ApiResponse( + responseCode = "200", + description = "OK", + content = @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, + schema = @Schema(type = "string", format = "binary") + ) + ) + }) @RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}/docker-compose/download", method = RequestMethod.GET) @ResponseBody public ResponseEntity downloadGatewayDockerCompose(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) 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 03fc5552c9..2f43c39f76 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -19,6 +19,7 @@ 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 io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -29,6 +30,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -46,8 +48,11 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -108,6 +113,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -117,6 +124,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @@ -177,30 +185,37 @@ public class DeviceController extends BaseController { @ResponseBody public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device, @Parameter(description = "Optional value of the device credentials to be used during device creation. " + - "If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws Exception { + "If omitted, access token will be auto-generated.") + @RequestParam(name = "accessToken", required = false) String accessToken, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { device.setTenantId(getCurrentUser().getTenantId()); if (device.getId() != null) { checkDeviceId(device.getId(), Operation.WRITE); } else { checkEntity(null, device, Resource.DEVICE); } - return tbDeviceService.save(device, accessToken, getCurrentUser()); + return tbDeviceService.save(device, accessToken, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Create Device (saveDevice) with credentials ", notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" + "You may find the example of device with different type of credentials below: \n\n" + - "- Credentials type: \"Access token\" with device profile ID below: \n\n" + + "- Credentials type: **\"Access token\"** with **device profile ID** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"Access token\" with device profile default below: \n\n" + + "- Credentials type: **\"Access token\"** with **device profile default** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"X509\" with device profile ID below: \n\n" + - "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" + + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + - "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " + @@ -209,12 +224,18 @@ public class DeviceController extends BaseController { @RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST) @ResponseBody public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.") - @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { + @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws ThingsboardException { Device device = deviceAndCredentials.getDevice(); DeviceCredentials credentials = deviceAndCredentials.getCredentials(); device.setTenantId(getCurrentUser().getTenantId()); checkEntity(device.getId(), device, Resource.DEVICE); - return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser()); + return tbDeviceService.saveDeviceWithCredentials(device, credentials, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete device (deleteDevice)", @@ -301,14 +322,14 @@ public class DeviceController extends BaseController { "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" + "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" + "You may find the example of device with different type of credentials below: \n\n" + - "- Credentials type: \"Access token\" with device ID and with device ID below: \n\n" + + "- Credentials type: **\"Access token\"** with **device ID** and with **device ID** below: \n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"X509\" with device profile ID below: \n\n" + - "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" + + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + - "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + "Update to real value:\n" + @@ -331,8 +352,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/devices") public PageData getTenantDevices( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -359,8 +379,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices info objects owned by tenant. " + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/deviceInfos") public PageData getTenantDeviceInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -392,25 +411,31 @@ public class DeviceController extends BaseController { return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink)); } - @ApiOperation(value = "Get Tenant Device (getTenantDevice)", - notes = "Requested device must be owned by tenant that the user belongs to. " + - "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/devices", params = {"deviceName"}) public Device getTenantDevice( - @Parameter(description = DEVICE_NAME_DESCRIPTION) @RequestParam String deviceName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); } + @ApiOperation(value = "Get Tenant Device (getTenantDeviceByName)", + notes = "Requested device must be owned by tenant that the user belongs to. " + + "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/device") + public Device getTenantDeviceByName( + @Parameter(description = DEVICE_NAME_DESCRIPTION) + @RequestParam String deviceName) throws ThingsboardException { + return getTenantDevice(deviceName); + } + @ApiOperation(value = "Get Customer Devices (getCustomerDevices)", notes = "Returns a page of devices objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/devices") public PageData getCustomerDevices( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -442,8 +467,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices info objects assigned to customer. " + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/deviceInfos") public PageData getCustomerDeviceInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable("customerId") String strCustomerId, @@ -483,8 +507,7 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)", notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/devices") public List getDevicesByIds( @Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException { @@ -505,14 +528,14 @@ public class DeviceController extends BaseController { return checkNotNull(devices.get()); } - @ApiOperation(value = "Find related devices (findByQuery)", + @ApiOperation(value = "Find related devices (findDevicesByQuery)", notes = "Returns all devices that are related to the specific entity. " + "The entity id, relation type, device types, depth of the search, and other query parameters defined using complex 'DeviceSearchQuery' object. " + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/devices", method = RequestMethod.POST) @ResponseBody - public List findByQuery( + public List findDevicesByQuery( @Parameter(description = "The device search query JSON") @RequestBody DeviceSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); @@ -711,8 +734,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices assigned to edge. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/devices") public PageData getEdgeDevices( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 7cfb9dd274..f70583f721 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -15,13 +15,16 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; 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.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -48,7 +51,9 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_DATA; @@ -125,7 +130,7 @@ public class DeviceProfileController extends BaseController { return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId())); } - @ApiOperation(value = "Get time series keys (getTimeseriesKeys)", + @ApiOperation(value = "Get time series keys (getDeviceProfileTimeseriesKeys)", notes = "Get a set of unique time series keys used by devices that belong to specified profile. " + "If profile is not set returns a list of unique keys among all profiles. " + "The call is used for auto-complete in the UI forms. " + @@ -134,7 +139,7 @@ public class DeviceProfileController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET) @ResponseBody - public List getTimeseriesKeys( + public List getDeviceProfileTimeseriesKeys( @Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException { DeviceProfileId deviceProfileId; @@ -224,8 +229,7 @@ public class DeviceProfileController extends BaseController { notes = "Returns a page of devices profile objects owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/deviceProfiles") public PageData getDeviceProfiles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -245,8 +249,7 @@ public class DeviceProfileController extends BaseController { notes = "Returns a page of devices profile info objects owned by tenant. " + PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/deviceProfileInfos") public PageData getDeviceProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -278,4 +281,27 @@ public class DeviceProfileController extends BaseController { return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly)); } + @Hidden + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"}) + public List getDeviceProfileInfosByIdsV1(@RequestParam("deviceProfileIds") Set deviceProfileUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + List deviceProfileIds = new ArrayList<>(); + for (UUID deviceProfileUUID : deviceProfileUUIDs) { + deviceProfileIds.add(new DeviceProfileId(deviceProfileUUID)); + } + return deviceProfileService.findDeviceProfilesByIds(tenantId, deviceProfileIds); + } + + @ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfileInfosByIds)", + notes = "Requested device profiles must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/deviceProfileInfos/list") + public List getDeviceProfileInfosByIds( + @Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("deviceProfileIds") Set deviceProfileUUIDs) throws ThingsboardException { + return getDeviceProfileInfosByIdsV1(deviceProfileUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DomainController.java b/application/src/main/java/org/thingsboard/server/controller/DomainController.java index 0afffda159..c34bbe6ac9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DomainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DomainController.java @@ -81,31 +81,31 @@ public class DomainController extends BaseController { return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser()); } - @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", + @ApiOperation(value = "Update oauth2 clients (updateDomainOauth2Clients)", notes = "Update oauth2 clients for the specified domain. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PutMapping(value = "/domain/{id}/oauth2Clients") - public void updateOauth2Clients(@PathVariable UUID id, - @RequestBody UUID[] clientIds) throws ThingsboardException { + public void updateDomainOauth2Clients(@PathVariable UUID id, + @RequestBody UUID[] clientIds) throws ThingsboardException { DomainId domainId = new DomainId(id); Domain domain = checkDomainId(domainId, Operation.WRITE); List oAuth2ClientIds = getOAuth2ClientIds(clientIds); tbDomainService.updateOauth2Clients(domain, oAuth2ClientIds, getCurrentUser()); } - @ApiOperation(value = "Get Domain infos (getTenantDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get Domain infos (getDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/domain/infos") - public PageData getTenantDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on domain's name") - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on domain's name") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.DOMAIN, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return domainService.findDomainInfosByTenantId(getTenantId(), pageLink); diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 784153b650..361104d9a2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -53,9 +54,8 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; -import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -71,7 +71,6 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -268,7 +267,7 @@ public class EdgeController extends BaseController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { + if (type != null && !type.trim().isEmpty()) { return checkNotNull(edgeService.findEdgesByTenantIdAndType(tenantId, type, pageLink)); } else { return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); @@ -279,7 +278,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges info objects owned by tenant. " + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/tenant/edgeInfos") public PageData getTenantEdgeInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -295,24 +294,31 @@ public class EdgeController extends BaseController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { + if (type != null && !type.trim().isEmpty()) { return checkNotNull(edgeService.findEdgeInfosByTenantIdAndType(tenantId, type, pageLink)); } else { return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink)); } } - @ApiOperation(value = "Get Tenant Edge (getTenantEdge)", - notes = "Requested edge must be owned by tenant or customer that the user belongs to. " + - "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/tenant/edges", params = {"edgeName"}) - public Edge getTenantEdge(@Parameter(description = "Unique name of the edge", required = true) - @RequestParam String edgeName) throws ThingsboardException { + public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); } + @ApiOperation(value = "Get Tenant Edge by name (getTenantEdgeByName)", + notes = "Requested edge must be owned by tenant or customer that the user belongs to. " + + "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/edge") + public Edge getTenantEdgeByName(@Parameter(description = "Unique name of the edge", required = true) + @RequestParam String edgeName) throws ThingsboardException { + return getTenantEdge(edgeName); + } + @ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)", notes = "Change root rule chain of the edge to the new provided rule chain. \n" + "This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH) @@ -336,7 +342,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/edges") public PageData getCustomerEdges( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -359,7 +365,7 @@ public class EdgeController extends BaseController { checkCustomerId(customerId, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageData result; - if (type != null && type.trim().length() > 0) { + if (type != null && !type.trim().isEmpty()) { result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); } else { result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); @@ -371,7 +377,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges info objects assigned to customer. " + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/edgeInfos") public PageData getCustomerEdgeInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -394,7 +400,7 @@ public class EdgeController extends BaseController { checkCustomerId(customerId, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageData result; - if (type != null && type.trim().length() > 0) { + if (type != null && !type.trim().isEmpty()) { result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); } else { result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); @@ -402,12 +408,10 @@ public class EdgeController extends BaseController { return checkNotNull(result); } - @ApiOperation(value = "Get Edges By Ids (getEdgesByIds)", - notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/edges", params = {"edgeIds"}) public List getEdgesByIds( - @Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("edgeIds", strEdgeIds); SecurityUser user = getCurrentUser(); @@ -427,13 +431,23 @@ public class EdgeController extends BaseController { return checkNotNull(edges); } - @ApiOperation(value = "Find related edges (findByQuery)", + @ApiOperation(value = "Get Edges By Ids (getEdgeList)", + notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/edges/list") + public List getEdgeList( + @Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { + return getEdgesByIds(strEdgeIds); + } + + @ApiOperation(value = "Find related edges (findEdgesByQuery)", notes = "Returns all edges that are related to the specific entity. " + "The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping(value = "/edges") - public List findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { + public List findEdgesByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getEdgeTypes()); @@ -470,7 +484,7 @@ public class EdgeController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping(value = "/edge/sync/{edgeId}") public DeferredResult syncEdge(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); final DeferredResult response = new DeferredResult<>(); if (isEdgesEnabled() && edgeRpcServiceOpt.isPresent()) { diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java index 10f4cd2f51..8dd607bb6f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -213,19 +214,19 @@ public class EntitiesVersionControlController extends BaseController { " \"timestamp\": 1655198593000,\n" + " \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" + " \"name\": \"Devices and assets - v2.0\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " },\n" + " {\n" + " \"timestamp\": 1655198528000,\n" + " \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" + " \"name\": \"Update my device\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " },\n" + " {\n" + " \"timestamp\": 1655198280000,\n" + " \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" + " \"name\": \"Devices and assets - v1.0\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " }\n" + " ],\n" + " \"totalPages\": 1,\n" + @@ -234,7 +235,7 @@ public class EntitiesVersionControlController extends BaseController { "}" + MARKDOWN_CODE_BLOCK_END + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version/{entityType}/{externalEntityUuid}", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version/{entityType}/{externalEntityUuid}") public DeferredResult> listEntityVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = "A string value representing external entity id. This is `externalId` property of an entity, or otherwise if not set - simply id of this entity.") @@ -263,7 +264,7 @@ public class EntitiesVersionControlController extends BaseController { "If specified branch does not exist - empty page data will be returned. " + "The response structure is the same as for `listEntityVersions` API method." + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version/{entityType}", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version/{entityType}") public DeferredResult> listEntityTypeVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) @@ -288,7 +289,7 @@ public class EntitiesVersionControlController extends BaseController { "If specified branch does not exist - empty page data will be returned. " + "The response format is the same as for `listEntityVersions` API method." + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version") public DeferredResult> listVersions(@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) @RequestParam String branch, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -355,7 +356,7 @@ public class EntitiesVersionControlController extends BaseController { "Returns an object with current entity data and the one at a specific version. " + "Entity data structure is the same as stored in a repository. " + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}", params = {"versionId"}) + @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}") public DeferredResult compareEntityDataToVersion(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 1eee81ddd3..cde3129d53 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -17,86 +17,80 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; +import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; 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.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; import org.thingsboard.server.common.data.exception.ThingsboardException; -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.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; 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.EntityFilter; -import org.thingsboard.server.common.msg.edqs.EdqsApiService; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.query.EntityQueryService; import org.thingsboard.server.service.security.permission.Operation; +import java.util.Set; + import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @RequestMapping("/api") +@RequiredArgsConstructor public class EntityQueryController extends BaseController { - @Autowired - private EntityQueryService entityQueryService; - @Autowired - private EdqsService edqsService; - @Autowired - private EdqsApiService edqsApiService; + private final EntityQueryService entityQueryService; + private final EdqsService edqsService; private static final int MAX_PAGE_SIZE = 100; @ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/entitiesQuery/count") public long countEntitiesByQuery( @Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.") @RequestBody EntityCountQuery query) throws ThingsboardException { checkNotNull(query); resolveQuery(query); - return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); + return entityQueryService.countEntitiesByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entitiesQuery/find", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/entitiesQuery/find") public PageData findEntityDataByQuery( @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") @RequestBody EntityDataQuery query) throws ThingsboardException { checkNotNull(query); resolveQuery(query); - return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); + return entityQueryService.findEntityDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarmsQuery/find", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/alarmsQuery/find") public PageData findAlarmDataByQuery( @Parameter(description = "A JSON value representing the alarm data query. See API call notes above for more details.") @RequestBody AlarmDataQuery query) throws ThingsboardException { @@ -107,13 +101,12 @@ public class EntityQueryController extends BaseController { checkUserId(assigneeId, Operation.READ); } resolveQuery(query); - return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); + return entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Count Alarms by Query (countAlarmsByQuery)", notes = "Returns the number of alarms that match the query definition.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarmsQuery/count", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/alarmsQuery/count") public long countAlarmsByQuery(@Parameter(description = "A JSON value representing the alarm count query.") @RequestBody AlarmCountQuery query) throws ThingsboardException { checkNotNull(query); @@ -122,31 +115,94 @@ public class EntityQueryController extends BaseController { checkUserId(assigneeId, Operation.READ); } resolveQuery(query); - return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query); + return entityQueryService.countAlarmsByQuery(getCurrentUser(), query); } - @ApiOperation(value = "Find Entity Keys by Query", - notes = "Uses entity data query (see 'Find Entity Data by Query') to find first 100 entities. Then fetch and return all unique time-series and/or attribute keys. Used mostly for UI hints.") + @Deprecated(forRemoval = true) + @ApiOperation( + value = "Find Available Entity Keys by Query (deprecated)", + notes = """ + **Deprecated.** Use the V2 endpoint (`POST /api/v2/entitiesQuery/find/keys`) instead.\n + Returns unique time series and/or attribute key names from entities matching the query.\n + Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n + Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entitiesQuery/find/keys", method = RequestMethod.POST) - @ResponseBody - public DeferredResult findEntityTimeseriesAndAttributesKeysByQuery( - @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") + @PostMapping("/entitiesQuery/find/keys") + public DeferredResult findAvailableEntityKeysByQuery( + @Parameter(description = "Entity data query to find entities. Page size is capped at 100.") @RequestBody EntityDataQuery query, - @Parameter(description = "Include all unique time-series keys to the result.") - @RequestParam("timeseries") boolean isTimeseries, - @Parameter(description = "Include all unique attribute keys to the result.") - @RequestParam("attributes") boolean isAttributes, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) - @RequestParam(value = "scope", required = false) String scope) throws ThingsboardException { - TenantId tenantId = getTenantId(); - checkNotNull(query); + + @Parameter(description = """ + When true, includes unique time series key names in the response. + When false, the 'timeseries' list will be empty.""") + @RequestParam("timeseries") boolean includeTimeseries, + + @Parameter(description = """ + When true, includes unique attribute key names in the response. + When false, the 'attribute' list will be empty. Use 'scope' parameter to filter by attribute scope.""") + @RequestParam("attributes") boolean includeAttributes, + + @Parameter(description = """ + Filters attribute keys by scope. Only applies when 'attributes' is true. + If not specified, returns attribute keys from all scopes.""", + schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) + @RequestParam(value = "scope", required = false) AttributeScope scope + ) throws ThingsboardException { + resolveQuery(query); + EntityDataPageLink pageLink = query.getPageLink(); + if (pageLink.getPageSize() > MAX_PAGE_SIZE) { + pageLink.setPageSize(MAX_PAGE_SIZE); + } + return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope)); + } + + @ApiOperation( + value = "Find Available Entity Keys By Query", + notes = """ + Discovers unique time series and/or attribute key names available on entities that match the given query. + Works in two steps: first, the request body (an Entity Data Query) is executed to find matching entities + (page size is capped at 100); then, all distinct key names are collected from those entities.\n + Optionally, each key can include a sample — the most recent value (by timestamp) for that key + across all matched entities.""" + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @PostMapping("/v2/entitiesQuery/find/keys") + public DeferredResult findAvailableEntityKeysByQueryV2( + @Parameter(description = "Entity data query to find entities. Page size is capped at 100.") + @RequestBody EntityDataQuery query, + + @Parameter(description = """ + When true, includes unique time series keys in the response. + When false, the 'timeseries' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""") + @RequestParam(defaultValue = "true") boolean includeTimeseries, + + @Parameter(description = """ + When true, includes unique attribute keys in the response. + When false, the 'attributes' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""") + @RequestParam(defaultValue = "true") boolean includeAttributes, + + @Parameter(description = """ + Filters attribute keys by scope. Only applies when 'includeAttributes' is true. + When not specified, scopes are auto-determined: all three scopes (server, client, shared) for device entities, + server scope only for other entity types.""", + schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) + @RequestParam(required = false) Set scopes, + + @Parameter(description = """ + When true, each key entry includes a 'sample' object with the most recent value and timestamp. + When false, only key names are returned (sample is omitted from JSON).""") + @RequestParam(defaultValue = "false") boolean includeSamples + ) throws ThingsboardException { resolveQuery(query); EntityDataPageLink pageLink = query.getPageLink(); if (pageLink.getPageSize() > MAX_PAGE_SIZE) { pageLink.setPageSize(MAX_PAGE_SIZE); } - return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes, scope); + return wrapFuture(entityQueryService.findAvailableEntityKeysByQuery( + getCurrentUser(), query, + includeTimeseries, includeAttributes, scopes, includeSamples)); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index a87db28e92..a5c82f11f4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -15,17 +15,19 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; 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.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -34,6 +36,7 @@ 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.config.annotations.ApiOperation; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.entity.relation.TbEntityRelationService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -41,7 +44,6 @@ import org.thingsboard.server.service.security.permission.Operation; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; @@ -71,47 +73,39 @@ public class EntityRelationController extends BaseController { "If the user has the authority of 'Tenant Administrator', the server checks that the entity is owned by the same tenant. " + "If the user has the authority of 'Customer User', the server checks that the entity is assigned to the same customer."; - @ApiOperation(value = "Create Relation (saveRelation)", - notes = "Creates or updates a relation between two entities in the platform. " + - "Relations unique key is a combination of from/to entity id and relation type group and relation type. " + - SECURITY_CHECKS_ENTITIES_DESCRIPTION) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relation", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) - public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) - @RequestBody EntityRelation relation) throws ThingsboardException { + @PostMapping(value = "/relation") + public void saveRelationV1(@Parameter(description = "A JSON value representing the relation.", required = true) + @RequestBody EntityRelation relation) throws ThingsboardException { doSave(relation); } - @ApiOperation(value = "Create Relation (saveRelationV2)", + @ApiOperation(value = "Create Relation (saveRelation)", notes = "Creates or updates a relation between two entities in the platform. " + "Relations unique key is a combination of from/to entity id and relation type group and relation type. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/v2/relation", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) - public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true) - @RequestBody EntityRelation relation) throws ThingsboardException { + @PostMapping(value = "/v2/relation") + public EntityRelation saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) + @RequestBody EntityRelation relation) throws ThingsboardException { return doSave(relation); } private EntityRelation doSave(EntityRelation relation) throws ThingsboardException { - checkNotNull(relation); - checkCanCreateRelation(relation.getFrom()); - checkCanCreateRelation(relation.getTo()); if (relation.getTypeGroup() == null) { relation.setTypeGroup(RelationTypeGroup.COMMON); } - + ConstraintValidator.validateFields(relation); + checkCanCreateRelation(relation.getFrom()); + checkCanCreateRelation(relation.getTo()); return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } - @ApiOperation(value = "Delete Relation (deleteRelation)", - notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - @ResponseStatus(value = HttpStatus.OK) - public void deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @DeleteMapping(value = "/relation") + public void deleteRelationV1(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @@ -120,12 +114,11 @@ public class EntityRelationController extends BaseController { doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType); } - @ApiOperation(value = "Delete Relation (deleteRelationV2)", + @ApiOperation(value = "Delete Relation (deleteRelation)", notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/v2/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - @ResponseStatus(value = HttpStatus.OK) - public EntityRelation deleteRelationV2(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @DeleteMapping(value = "/v2/relation") + public EntityRelation deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @@ -137,7 +130,6 @@ public class EntityRelationController extends BaseController { private EntityRelation doDelete(String strFromId, String strFromType, String strRelationType, String strRelationTypeGroup, String strToId, String strToType) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); - checkParameter(RELATION_TYPE, strRelationType); checkParameter(TO_ID, strToId); checkParameter(TO_TYPE, strToType); EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); @@ -150,12 +142,11 @@ public class EntityRelationController extends BaseController { return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } - @ApiOperation(value = "Delete common relations (deleteCommonRelations)", + @ApiOperation(value = "Delete common relations (deleteRelations)", notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"}) - @ResponseStatus(value = HttpStatus.OK) + @DeleteMapping(value = "/relations") public void deleteRelations(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam("entityId") String strId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam("entityType") String strType) throws ThingsboardException { checkParameter("entityId", strId); @@ -168,8 +159,7 @@ public class EntityRelationController extends BaseController { @ApiOperation(value = "Get Relation (getRelation)", notes = "Returns relation object between two specified entities if present. Otherwise throws exception. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - @ResponseBody + @GetMapping(value = "/relation") public EntityRelation getRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @@ -178,7 +168,6 @@ public class EntityRelationController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); - checkParameter(RELATION_TYPE, strRelationType); checkParameter(TO_ID, strToId); checkParameter(TO_TYPE, strToType); EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); @@ -189,15 +178,11 @@ public class EntityRelationController extends BaseController { return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup)); } - @ApiOperation(value = "Get List of Relations (findByFrom)", - notes = "Returns list of relation objects for the specified entity by the 'from' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE}) - @ResponseBody - public List findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, - @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE}) + public List findByFrom(@RequestParam(FROM_ID) String strFromId, + @RequestParam(FROM_TYPE) String strFromType, @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -207,12 +192,21 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup))); } - @ApiOperation(value = "Get List of Relation Infos (findInfoByFrom)", - notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @ApiOperation(value = "Get List of Relations (findEntityRelationsByFrom)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/from/{fromType}/{fromId}") + public List findEntityRelationsByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByFrom(strFromId, strFromType, strRelationTypeGroup); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/info", params = {FROM_ID, FROM_TYPE}) public List findInfoByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @@ -225,16 +219,24 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get())); } - @ApiOperation(value = "Get List of Relations (findByFrom)", - notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByFrom)", + notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/info/from/{fromType}/{fromId}") + public List findEntityRelationInfosByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { + return findInfoByFrom(strFromId, strFromType, strRelationTypeGroup); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE}) - @ResponseBody - public List findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, - @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, - @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE, RELATION_TYPE}) + public List findByFrom(@RequestParam(FROM_ID) String strFromId, + @RequestParam(FROM_TYPE) String strFromType, + @RequestParam(RELATION_TYPE) String strRelationType, @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -245,12 +247,22 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup))); } - @ApiOperation(value = "Get List of Relations (findByTo)", - notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + + @ApiOperation(value = "Get List of Relations (findEntityRelationsByFromAndRelationType)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/from/{fromType}/{fromId}/{relationType}") + public List findEntityRelationsByFromAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByFrom(strFromId, strFromType, strRelationType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE}) public List findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @@ -263,12 +275,21 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup))); } - @ApiOperation(value = "Get List of Relation Infos (findInfoByTo)", - notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @ApiOperation(value = "Get List of Relations (findEntityRelationsByTo)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {TO_ID, TO_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/to/{toType}/{toId}") + public List findEntityRelationsByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByTo(strToId, strToType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/info", params = {TO_ID, TO_TYPE}) public List findInfoByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @@ -281,12 +302,21 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get())); } - @ApiOperation(value = "Get List of Relations (findByTo)", - notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByTo)", + notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE, RELATION_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/info/to/{toType}/{toId}") + public List findEntityRelationInfosByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { + return findInfoByTo(strToId, strToType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE, RELATION_TYPE}) public List findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @@ -301,32 +331,41 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup))); } - @ApiOperation(value = "Find related entities (findByQuery)", + @ApiOperation(value = "Get List of Relations (findEntityRelationsByToAndRelationType)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/to/{toType}/{toId}/{relationType}") + public List findEntityRelationsByToAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByTo(strToId, strToType, strRelationType, strRelationTypeGroup); + } + + @ApiOperation(value = "Find related entities (findEntityRelationsByQuery)", notes = "Returns all entities that are related to the specific entity. " + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + "See 'Model' tab of the Parameters for more info.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.POST) - @ResponseBody - public List findByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) - @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { - checkNotNull(query); + @PostMapping("/relations") + public List findEntityRelationsByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) + @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get())); } - @ApiOperation(value = "Find related entity infos (findInfoByQuery)", + @ApiOperation(value = "Find related entity infos (findEntityRelationInfosByQuery)", notes = "Returns all entity infos that are related to the specific entity. " + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + "See 'Model' tab of the Parameters for more info. " + RELATION_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations/info", method = RequestMethod.POST) - @ResponseBody - public List findInfoByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) - @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { - checkNotNull(query); + @PostMapping("/relations/info") + public List findEntityRelationInfosByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) + @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); @@ -354,18 +393,18 @@ public class EntityRelationController extends BaseController { return false; } return true; - }).collect(Collectors.toList()); + }).toList(); } - private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) { - RelationTypeGroup result = defaultValue; - if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length() > 0) { - try { - result = RelationTypeGroup.valueOf(strRelationTypeGroup); - } catch (IllegalArgumentException e) { - } + private static RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) { + if (StringUtils.isBlank(strRelationTypeGroup)) { + return defaultValue; + } + try { + return RelationTypeGroup.valueOf(strRelationTypeGroup); + } catch (IllegalArgumentException e) { + return defaultValue; } - return result; } } 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 15bda4b87a..821a3c77a1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -16,24 +16,30 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; 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.Customer; 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.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -53,7 +59,10 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -69,6 +78,7 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE; import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -76,11 +86,10 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; -/** - * Created by Victor Basanets on 8/28/2017. - */ @RestController @TbCoreComponent @RequiredArgsConstructor @@ -96,8 +105,7 @@ public class EntityViewController extends BaseController { notes = "Fetch the EntityView object based on the provided entity view id. " + ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/entityView/{entityViewId}") public EntityView getEntityViewById( @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @@ -109,8 +117,7 @@ public class EntityViewController extends BaseController { notes = "Fetch the Entity View info object based on the provided Entity View Id. " + ENTITY_VIEW_INFO_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entityView/info/{entityViewId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/entityView/info/{entityViewId}") public EntityViewInfo getEntityViewInfoById( @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @@ -124,11 +131,16 @@ public class EntityViewController extends BaseController { "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Entity View entity." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entityView", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/entityView") public EntityView saveEntityView( @Parameter(description = "A JSON object representing the entity view.") - @RequestBody EntityView entityView) throws Exception { + @RequestBody EntityView entityView, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { entityView.setTenantId(getCurrentUser().getTenantId()); EntityView existingEntityView = null; if (entityView.getId() == null) { @@ -137,14 +149,14 @@ public class EntityViewController extends BaseController { } else { existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE); } - return tbEntityViewService.save(entityView, existingEntityView, getCurrentUser()); + return tbEntityViewService.save(entityView, existingEntityView, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete entity view (deleteEntityView)", notes = "Delete the EntityView object based on the provided entity view id. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE) + @DeleteMapping(value = "/entityView/{entityViewId}") @ResponseStatus(value = HttpStatus.OK) public void deleteEntityView( @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @@ -155,23 +167,29 @@ public class EntityViewController extends BaseController { tbEntityViewService.delete(entityView, getCurrentUser()); } - @ApiOperation(value = "Get Entity View by name (getTenantEntityView)", - notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/entityViews", params = {"entityViewName"}) public EntityView getTenantEntityView( - @Parameter(description = "Entity View name") @RequestParam String entityViewName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName)); } + @ApiOperation(value = "Get Entity View by name (getTenantEntityViewByName)", + notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/entityView") + public EntityView getTenantEntityViewByName( + @Parameter(description = "Entity View name") + @RequestParam String entityViewName) throws ThingsboardException { + return getTenantEntityView(entityViewName); + } + @ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)", notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/customer/{customerId}/entityView/{entityViewId}") public EntityView assignEntityViewToCustomer( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -192,8 +210,7 @@ public class EntityViewController extends BaseController { @ApiOperation(value = "Unassign Entity View from customer (unassignEntityViewFromCustomer)", notes = "Clears assignment of the Entity View to customer. Customer will not be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/entityView/{entityViewId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/customer/entityView/{entityViewId}") public EntityView unassignEntityViewFromCustomer( @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @@ -213,8 +230,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of Entity View objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/entityViews") public PageData getCustomerEntityViews( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -235,7 +251,7 @@ public class EntityViewController extends BaseController { 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) { + if (type != null && !type.trim().isEmpty()) { return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId, customerId, pageLink, type)); } else { return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); @@ -246,8 +262,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/entityViewInfos") public PageData getCustomerEntityViewInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -268,7 +283,7 @@ public class EntityViewController extends BaseController { 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) { + if (type != null && !type.trim().isEmpty()) { return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); } else { return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); @@ -279,8 +294,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/entityViews") public PageData getTenantEntityViews( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -297,7 +311,7 @@ public class EntityViewController extends BaseController { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { + if (type != null && !type.trim().isEmpty()) { return checkNotNull(entityViewService.findEntityViewByTenantIdAndType(tenantId, pageLink, type)); } else { return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink)); @@ -308,8 +322,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/entityViewInfos") public PageData getTenantEntityViewInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -325,21 +338,20 @@ public class EntityViewController extends BaseController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (type != null && type.trim().length() > 0) { + if (type != null && !type.trim().isEmpty()) { return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndType(tenantId, type, pageLink)); } else { return checkNotNull(entityViewService.findEntityViewInfosByTenantId(tenantId, pageLink)); } } - @ApiOperation(value = "Find related entity views (findByQuery)", + @ApiOperation(value = "Find related entity views (findEntityViewsByQuery)", notes = "Returns all entity views that are related to the specific entity. " + "The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entityViews", method = RequestMethod.POST) - @ResponseBody - public List findByQuery( + @PostMapping(value = "/entityViews") + public List findEntityViewsByQuery( @Parameter(description = "The entity view search query JSON") @RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); @@ -347,14 +359,7 @@ public class EntityViewController extends BaseController { checkNotNull(query.getEntityViewTypes()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); List entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(getTenantId(), query).get()); - entityViews = entityViews.stream().filter(entityView -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); + entityViews = filterEntityViewsByReadPermission(entityViews); return entityViews; } @@ -362,8 +367,7 @@ public class EntityViewController extends BaseController { notes = "Returns a set of unique entity view types based on entity views that are either owned by the tenant or assigned to the customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entityView/types", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/entityView/types") public List getEntityViewTypes() throws ThingsboardException, ExecutionException, InterruptedException { SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); @@ -376,8 +380,7 @@ public class EntityViewController extends BaseController { "This is useful to create dashboards that you plan to share/embed on a publicly available website. " + "However, users that are logged-in and belong to different tenant will not be able to access the entity view." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/public/entityView/{entityViewId}", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/customer/public/entityView/{entityViewId}") public EntityView assignEntityViewToPublicCustomer( @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @@ -394,8 +397,7 @@ public class EntityViewController extends BaseController { EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + "Third, once entity view will be delivered to edge service, it's going to be available for usage on remote edge instance.") @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/edge/{edgeId}/entityView/{entityViewId}") public EntityView assignEntityViewToEdge(@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); @@ -418,8 +420,7 @@ public class EntityViewController extends BaseController { EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + "Third, once 'unassign' command will be delivered to edge service, it's going to remove entity view locally.") @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/edge/{edgeId}/entityView/{entityViewId}") public EntityView unassignEntityViewFromEdge(@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); @@ -436,8 +437,7 @@ public class EntityViewController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/entityViews") public PageData getEdgeEntityViews( @PathVariable(EDGE_ID) String strEdgeId, @RequestParam int pageSize, @@ -454,23 +454,50 @@ public class EntityViewController extends BaseController { checkEdgeId(edgeId, Operation.READ); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); PageData nonFilteredResult; - if (type != null && type.trim().length() > 0) { + if (type != null && !type.trim().isEmpty()) { nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); } else { nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); } - List filteredEntityViews = nonFilteredResult.getData().stream().filter(entityView -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); + List filteredEntityViews = filterEntityViewsByReadPermission(nonFilteredResult.getData()); PageData filteredResult = new PageData<>(filteredEntityViews, nonFilteredResult.getTotalPages(), nonFilteredResult.getTotalElements(), nonFilteredResult.hasNext()); return checkNotNull(filteredResult); } + + @Hidden + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/entityViews", params = {"entityViewIds"}) + public List getEntityViewsByIdsV1(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("entityViewIds") Set entityViewUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + List entityViewIds = new ArrayList<>(); + for (UUID entityViewUUID : entityViewUUIDs) { + entityViewIds.add(new EntityViewId(entityViewUUID)); + } + List entityViews = entityViewService.findEntityViewsByTenantIdAndIds(tenantId, entityViewIds); + return filterEntityViewsByReadPermission(entityViews); + } + + @ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)", + notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/entityViews/list") + public List getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("entityViewIds") Set entityViewUUIDs) throws ThingsboardException { + return getEntityViewsByIdsV1(entityViewUUIDs); + } + + private List filterEntityViewsByReadPermission(List entityViews) { + return entityViews.stream().filter(entityView -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + } + } 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 7bb2d33366..6e8c17c674 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.beans.factory.annotation.Autowired; @@ -113,13 +114,13 @@ public class EventController extends BaseController { @Autowired private EventService eventService; - @ApiOperation(value = "Get Events by type (getEvents)", + @ApiOperation(value = "Get Events by type (getEventsByType)", notes = "Returns a page of events for specified entity by specifying event type. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET) @ResponseBody - public PageData getEvents( + public PageData getEventsByType( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @@ -152,16 +153,12 @@ public class EventController extends BaseController { return checkNotNull(eventService.findEvents(tenantId, entityId, resolveEventType(eventType), pageLink)); } - @ApiOperation(value = "Get Events (Deprecated)", - notes = "Returns a page of events for specified entity. Deprecated and will be removed in next minor release. " + - "The call was deprecated to improve the performance of the system. " + - "Current implementation will return 'Lifecycle' events only. " + - "Use 'Get events by type' or 'Get events by filter' instead. " + - PAGE_DATA_PARAMETERS) + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody - public PageData getEvents( + public PageData getEventsDeprecated( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @@ -194,14 +191,14 @@ public class EventController extends BaseController { return checkNotNull(eventService.findEvents(tenantId, entityId, EventType.LC_EVENT, pageLink)); } - @ApiOperation(value = "Get Events by event filter (getEvents)", + @ApiOperation(value = "Get Events by event filter (getEventsByFilter)", notes = "Returns a page of events for the chosen entity by specifying the event filter. " + PAGE_DATA_PARAMETERS + NEW_LINE + EVENT_FILTER_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST) @ResponseBody - public PageData getEvents( + public PageData getEventsByFilter( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index ab3d7239d9..5566c11a4a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -37,6 +39,7 @@ import org.thingsboard.server.service.lwm2m.LwM2MService; import java.util.Map; +import static org.thingsboard.server.common.data.NameConflictStrategy.DEFAULT; import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; @@ -66,13 +69,13 @@ public class Lwm2mController extends BaseController { return lwM2MService.getServerSecurityInfo(bootstrapServer); } - @ApiOperation(hidden = true, value = "Save device with credentials (Deprecated)") + @ApiOperation(hidden = true, value = "Save LwM2M device with credentials (saveLwm2mDeviceWithCredentials) (Deprecated)") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST) @ResponseBody - public Device saveDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { + public Device saveLwm2mDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); - return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials)); + return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy()); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java index 70ac96db67..f8a3f8ad0b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java @@ -43,12 +43,12 @@ public class MailConfigTemplateController extends BaseController { private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports"; private final TbMailConfigTemplateService mailConfigTemplateService; - @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getMailConfigTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, notes = MAIL_CONFIG_TEMPLATE_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(method = RequestMethod.GET, produces = "application/json") @ResponseBody - public JsonNode getClientRegistrationTemplates() throws ThingsboardException, IOException { + public JsonNode getMailConfigTemplates() throws ThingsboardException, IOException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return mailConfigTemplateService.findAllMailConfigTemplates(); } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java index 304805a820..feb4aed0e3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java @@ -81,12 +81,12 @@ public class MobileAppBundleController extends BaseController { return tbMobileAppBundleService.save(mobileAppBundle, getOAuth2ClientIds(ids), getCurrentUser()); } - @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", + @ApiOperation(value = "Update oauth2 clients (updateMobileAppBundleOauth2Clients)", notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PutMapping(value = "/mobile/bundle/{id}/oauth2Clients") - public void updateOauth2Clients(@PathVariable UUID id, - @RequestBody UUID[] clientIds) throws ThingsboardException { + public void updateMobileAppBundleOauth2Clients(@PathVariable UUID id, + @RequestBody UUID[] clientIds) throws ThingsboardException { MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.WRITE); List oAuth2ClientIds = getOAuth2ClientIds(clientIds); diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 7c77a26e42..3ca23b2ddc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -123,7 +123,7 @@ public class MobileAppController extends BaseController { return tbMobileAppService.save(mobileApp, getCurrentUser()); } - @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get mobile app infos (getTenantMobileApps)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/app") public PageData getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS") @@ -142,7 +142,7 @@ public class MobileAppController extends BaseController { return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink); } - @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get mobile info by id (getMobileAppById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/app/{id}") public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index d504cf24b2..126f050bec 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,6 +36,8 @@ import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationTargetId; @@ -44,6 +47,9 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.info.EntitiesLimitIncreaseRequestNotificationInfo; +import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; @@ -51,6 +57,7 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; @@ -69,6 +76,7 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import org.thingsboard.server.service.security.system.SystemSecurityService; import java.util.Comparator; import java.util.HashMap; @@ -76,6 +84,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -90,6 +99,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; @RestController @@ -105,6 +115,7 @@ public class NotificationController extends BaseController { private final NotificationTargetService notificationTargetService; private final NotificationCenter notificationCenter; private final NotificationSettingsService notificationSettingsService; + private final SystemSecurityService systemSecurityService; @ApiOperation(value = "Get notifications (getNotifications)", notes = "Returns the page of notifications for current user." + NEW_LINE + @@ -282,6 +293,34 @@ public class NotificationController extends BaseController { return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, (tenantId, request) -> notificationCenter.processNotificationRequest(tenantId, request, null)); } + @ApiOperation(value = "Send entity limit increase request notification to System administrators (sendEntitiesLimitIncreaseRequest)", + notes = "Send entity limit increase request notification by Tenant Administrator to System administrators." + + TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/notification/entitiesLimitIncreaseRequest/{entityType}") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + public void sendEntitiesLimitIncreaseRequest(@Parameter(description = "Entity type", required = true, schema = @Schema(allowableValues = {"DEVICE", "ASSET", "CUSTOMER", "USER", "DASHBOARD", "RULE_CHAIN", "EDGE"})) + @PathVariable("entityType") String strEntityType, + @AuthenticationPrincipal SecurityUser user, + HttpServletRequest request) throws Exception { + EntityType entityType = checkEnumParameter("entityType", strEntityType, EntityType::valueOf); + Optional sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(TenantId.SYS_TENANT_ID, UsersFilterType.SYSTEM_ADMINISTRATORS) + .stream().findFirst(); + if (sysAdmins.isPresent()) { + NotificationTargetId notificationTargetId = sysAdmins.get().getId(); + String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + NotificationInfo info = EntitiesLimitIncreaseRequestNotificationInfo.builder() + .entityType(entityType) + .userEmail(user.getEmail()) + .increaseLimitActionLabel("Set new limit") + .increaseLimitLink("/tenants/"+user.getTenantId().toString()) + .baseUrl(baseUrl) + .build(); + notificationCenter.sendSystemNotification(TenantId.SYS_TENANT_ID, notificationTargetId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, info); + } else { + throw new IllegalArgumentException("Notification target for 'System administrators' not found"); + } + } + @ApiOperation(value = "Get notification request preview (getNotificationRequestPreview)", notes = "Returns preview for notification request." + NEW_LINE + "`processedTemplates` shows how the notifications for each delivery method will look like " + diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java index 72c7f15a3b..17bb87233e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -153,12 +154,10 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink); } - @ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)", - notes = "Returns the list of notification targets found by provided ids." + - SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @GetMapping(value = "/targets", params = {"ids"}) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public List getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + public List getNotificationTargetsByIdsV1(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("ids") UUID[] ids, @AuthenticationPrincipal SecurityUser user) { // PE: generic permission @@ -166,6 +165,17 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds); } + @ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)", + notes = "Returns the list of notification targets found by provided ids." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets/list") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public List getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("ids") UUID[] ids, + @AuthenticationPrincipal SecurityUser user) { + return getNotificationTargetsByIdsV1(ids, user); + } + @ApiOperation(value = "Get notification targets (getNotificationTargets)", notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE + PAGE_DATA_PARAMETERS + @@ -188,13 +198,10 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink); } - @ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)", - notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE + - PAGE_DATA_PARAMETERS + - SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @GetMapping(value = "/targets", params = "notificationType") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public PageData getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize, + public PageData getNotificationTargetsBySupportedNotificationTypeV1(@RequestParam int pageSize, @RequestParam int page, @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @@ -206,6 +213,22 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink); } + @ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)", + notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets/notificationType/{notificationType}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationTargetsBySupportedNotificationType(@PathVariable NotificationType notificationType, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + return getNotificationTargetsBySupportedNotificationTypeV1(pageSize, page, textSearch, sortProperty, sortOrder, notificationType, user); + } + @ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)", notes = "Deletes notification target by its id." + NEW_LINE + "This target cannot be referenced by existing scheduled notification requests or any notification rules." + diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java index 8891fcc64e..6c773c9509 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java @@ -71,12 +71,12 @@ public class OAuth2ConfigTemplateController extends BaseController { oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); } - @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getOAuth2ClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(method = RequestMethod.GET, produces = "application/json") @ResponseBody - public List getClientRegistrationTemplates() throws ThingsboardException { + public List getOAuth2ClientRegistrationTemplates() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 71b8ac27a9..5e51e0bbb6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -115,32 +116,40 @@ public class OAuth2Controller extends BaseController { return tbOauth2ClientService.save(oAuth2Client, getCurrentUser()); } - @ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get OAuth2 Client infos (findOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/infos") - public PageData findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on client's title") - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData findOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on client's title") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) + public List findTenantOAuth2ClientInfosByIdsV1( + @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { + List oAuth2ClientIds = getOAuth2ClientIds(clientIds); + return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); + } + @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) + @GetMapping(value = "/oauth2/client/list") public List findTenantOAuth2ClientInfosByIds( @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { - List oAuth2ClientIds = getOAuth2ClientIds(clientIds); - return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); + return findTenantOAuth2ClientInfosByIdsV1(clientIds); } @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 4967023ec0..ab93a29db5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -181,25 +181,25 @@ public class OtaPackageController extends BaseController { return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink)); } - @ApiOperation(value = "Get OTA Package Infos (getOtaPackages)", + @ApiOperation(value = "Get OTA Package Infos by device profile and type (getOtaPackagesByDeviceProfileAndType)", notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}") - public PageData getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) - @PathVariable("deviceProfileId") String strDeviceProfileId, - @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) - @PathVariable("type") String strType, - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION) - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"})) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getOtaPackagesByDeviceProfileAndType(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable("deviceProfileId") String strDeviceProfileId, + @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) + @PathVariable("type") String strType, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("deviceProfileId", strDeviceProfileId); checkParameter("type", strType); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index e74763b8ad..6b90fb142f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -31,15 +31,12 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.app.MobileApp; -import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.common.data.mobile.app.StoreInfo; -import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.mobile.QrCodeSettingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.mobile.secret.MobileAppSecretService; @@ -132,7 +129,7 @@ public class QrCodeSettingsController extends BaseController { return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings); } - @ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", + @ApiOperation(value = "Get Mobile application settings (getQrCodeSettings)", notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/api/mobile/qr/settings") diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 2d584a6e27..dd2cc19fd6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -19,7 +19,9 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -65,8 +67,7 @@ public class QueueController extends BaseController { notes = "Returns a page of queues registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/queues") public PageData getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String serviceType, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -122,8 +123,7 @@ public class QueueController extends BaseController { "Remove 'id', 'tenantId' from the request body example (below) to create new Queue entity. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/queues", params = {"serviceType"}, method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/queues") public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.") @RequestBody Queue queue, @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java b/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java index 1ccc35b875..57ec9a43c9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -85,12 +86,10 @@ public class QueueStatsController extends BaseController { return checkNotNull(queueStatsService.findQueueStatsById(getTenantId(), queueStatsId)); } - @ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)", - notes = "Fetch the Queue stats objects based on the provided ids. ") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/queueStats", params = {"queueStatsIds"}) - public List getQueueStatsByIds( - @Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + public List getQueueStatsByIdsV1( @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { checkArrayParameter("queueStatsIds", strQueueStatsIds); List queueStatsIds = new ArrayList<>(); @@ -99,4 +98,14 @@ public class QueueStatsController extends BaseController { } return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds); } + + @ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)", + notes = "Fetch the Queue stats objects based on the provided ids. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/queueStats/list") + public List getQueueStatsByIds( + @Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { + return getQueueStatsByIdsV1(strQueueStatsIds); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java index dc17136d63..5e555cbcbe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -43,26 +45,28 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CU @Slf4j public class RpcV1Controller extends AbstractRpcController { - @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleOneWayDeviceRPCRequest( + public DeferredResult handleOneWayDeviceRPCRequestV1( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } - @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleTwoWayDeviceRPCRequest( + public DeferredResult handleTwoWayDeviceRPCRequestV1( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 412e775327..4366134927 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.FutureCallback; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -113,7 +114,7 @@ public class RpcV2Controller extends AbstractRpcController { private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; - @ApiOperation(value = "Send one-way RPC request", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION) + @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV2)", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC request was sent to the device."), @ApiResponse(responseCode = "400", description = "Invalid structure of the request."), @@ -124,15 +125,16 @@ public class RpcV2Controller extends AbstractRpcController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleOneWayDeviceRPCRequest( + public DeferredResult handleOneWayDeviceRPCRequestV2( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } - @ApiOperation(value = "Send two-way RPC request", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION) + @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV2)", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC response received."), @ApiResponse(responseCode = "400", description = "Invalid structure of the request."), @@ -143,10 +145,11 @@ public class RpcV2Controller extends AbstractRpcController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleTwoWayDeviceRPCRequest( + public DeferredResult handleTwoWayDeviceRPCRequestV2( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } 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 12c26dedb3..0ac6fcdac1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -19,7 +19,9 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -79,6 +81,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; @@ -90,6 +93,7 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIG import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -226,12 +230,12 @@ public class RuleChainController extends BaseController { return tbRuleChainService.save(ruleChain, getCurrentUser()); } - @ApiOperation(value = "Create Default Rule Chain", + @ApiOperation(value = "Create Default Rule Chain (setDeviceDefaultRuleChain)", notes = "Create rule chain from template, based on the specified name in the request. " + "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/ruleChain/device/default") - public RuleChain saveRuleChain( + public RuleChain setDeviceDefaultRuleChain( @Parameter(description = "A JSON value representing the request.") @RequestBody DefaultRuleChainCreateRequest request) throws Exception { checkNotNull(request); @@ -278,7 +282,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chains (getRuleChains)", notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/ruleChains", params = {"pageSize", "page"}) + @GetMapping(value = "/ruleChains") public PageData getRuleChains( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -344,7 +348,7 @@ public class RuleChainController extends BaseController { notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/ruleChain/testScript") - public JsonNode testScript( + public JsonNode testRuleChainScript( @Parameter(description = "Script language: JS or TBEL") @RequestParam(required = false) ScriptLanguage scriptLang, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.") @@ -406,7 +410,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/ruleChains/export", params = {"limit"}) + @GetMapping(value = "/ruleChains/export") public RuleChainData exportRuleChains( @Parameter(description = "A limit of rule chains to export.", required = true) @RequestParam("limit") int limit) throws ThingsboardException { @@ -502,7 +506,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}) + @GetMapping(value = "/edge/{edgeId}/ruleChains") public PageData getEdgeRuleChains( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, @@ -579,4 +583,27 @@ public class RuleChainController extends BaseController { return checkNotNull(result); } + @Hidden + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/ruleChains", params = {"ruleChainIds"}) + public List getRuleChainsByIdsV1(@RequestParam("ruleChainIds") Set ruleChainUUIDs) throws Exception { + TenantId tenantId = getCurrentUser().getTenantId(); + List ruleChainIds = new ArrayList<>(); + for (UUID ruleChainUUID : ruleChainUUIDs) { + ruleChainIds.add(new RuleChainId(ruleChainUUID)); + } + return ruleChainService.findRuleChainsByIds(tenantId, ruleChainIds); + } + + @ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)", + notes = "Requested rule chains must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/ruleChains/list") + public List getRuleChainsByIds( + @Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("ruleChainIds") Set ruleChainUUIDs) throws Exception { + return getRuleChainsByIdsV1(ruleChainUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java index 96095ddf9e..d63e0f5d19 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java @@ -17,9 +17,12 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.FutureCallback; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -60,7 +63,10 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_ @RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX) @Slf4j public class RuleEngineController extends BaseController { - public static final int DEFAULT_TIMEOUT = 10000; + + @Value("${server.rest.rule_engine.response_timeout:10000}") + public int defaultResponseTimeout; + private static final String MSG_DESCRIPTION_PREFIX = "Creates the Message with type 'REST_API_REQUEST' and payload taken from the request body. "; private static final String MSG_DESCRIPTION = "This method allows you to extend the regular platform API with the power of Rule Engine. You may use default and custom rule nodes to handle the message. " + "The generated message contains two important metadata fields:\n\n" + @@ -73,7 +79,7 @@ public class RuleEngineController extends BaseController { @Autowired private AccessValidator accessValidator; - @ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequestForUser)", notes = MSG_DESCRIPTION_PREFIX + "Uses current User Id ( the one which credentials is used to perform the request) as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -82,13 +88,14 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( - @Parameter(description = "A JSON value representing the message.", required = true) + public DeferredResult handleRuleEngineRequestForUser( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(null, null, null, DEFAULT_TIMEOUT, requestBody); + return handleRuleEngineRequestForEntityWithQueueAndTimeout(null, null, null, defaultResponseTimeout, requestBody); } - @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequestForEntity)", notes = MSG_DESCRIPTION_PREFIX + "Uses specified Entity Id as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -97,17 +104,18 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( + public DeferredResult handleRuleEngineRequestForEntity( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(entityType, entityIdStr, null, DEFAULT_TIMEOUT, requestBody); + return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, defaultResponseTimeout, requestBody); } - @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequestForEntityWithTimeout)", notes = MSG_DESCRIPTION_PREFIX + "Uses specified Entity Id as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -116,19 +124,20 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{timeout}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( + public DeferredResult handleRuleEngineRequestForEntityWithTimeout( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = "Timeout to process the request in milliseconds", required = true) @PathVariable("timeout") int timeout, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(entityType, entityIdStr, null, timeout, requestBody); + return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, timeout, requestBody); } - @ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequestForEntityWithQueueAndTimeout)", notes = MSG_DESCRIPTION_PREFIX + "Uses specified Entity Id as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -138,7 +147,7 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{queueName}/{timeout}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( + public DeferredResult handleRuleEngineRequestForEntityWithQueueAndTimeout( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @@ -147,7 +156,8 @@ public class RuleEngineController extends BaseController { @PathVariable("queueName") String queueName, @Parameter(description = "Timeout to process the request in milliseconds", required = true) @PathVariable("timeout") int timeout, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { try { SecurityUser currentUser = getCurrentUser(); @@ -240,5 +250,7 @@ public class RuleEngineController extends BaseController { response != null ? response.getData() : ""); } - private record LocalRequestMetaData(TbMsg request, SecurityUser user, DeferredResult responseWriter) {} + private record LocalRequestMetaData(TbMsg request, SecurityUser user, + DeferredResult responseWriter) { + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 61240d304e..9bcd756de0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -76,6 +76,9 @@ public class SystemInfoController extends BaseController { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + @Value("${sql.query.key-filters-or-conditions.enabled:true}") + private boolean keyFiltersOrConditionsEnabled; + @Autowired(required = false) private BuildProperties buildProperties; @@ -162,11 +165,18 @@ public class SystemInfoController extends BaseController { } systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF()); systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); + systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF()); + systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); + systemParams.setMaxRelatedEntitiesToReturnPerCfArgument(tenantProfileConfiguration.getMaxRelatedEntitiesToReturnPerCfArgument()); + systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); + systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); + systemParams.setIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getIntermediateAggregationIntervalInSecForCF()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) .map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) .orElse(false)); + systemParams.setAllowKeyFiltersOrConditions(keyFiltersOrConditionsEnabled); return systemParams; } diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 14c0a3dcb2..429f9b9cf2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -32,12 +33,16 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; 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.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; @@ -114,7 +119,7 @@ public class TbResourceController extends BaseController { .body(resource); } - @ApiOperation(value = "Download resource (downloadResource)", + @ApiOperation(value = "Download resource (downloadResourceIfChanged)", notes = "Download resource with a given type and key for the given scope" + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/resource/{resourceType}/{scope}/{key}") @@ -215,6 +220,7 @@ public class TbResourceController extends BaseController { "\n\nResource combination of the title with the key is unique in the scope of tenant. " + "Remove 'id', 'tenantId' from the request body example (below) to create new Resource entity." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Deprecated // resource should be save or update with an upload request @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping(value = "/resource") public TbResourceInfo saveResource(@Parameter(description = "A JSON value representing the Resource.") @@ -224,6 +230,71 @@ public class TbResourceController extends BaseController { return tbResourceService.save(resource, getCurrentUser()); } + @ApiOperation(value = "Upload Resource via Multipart File (uploadResource)", + notes = "Create the Resource using multipart file upload. " + + "\n\nResource combination of the title with the key is unique in the scope of tenant. " + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PostMapping(value = "/resource/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public TbResourceInfo uploadResource(@Parameter(description = "Resource title.", example = "Title") + @RequestPart(name = "title", required = false) String title, + @Parameter(description = "Resource type.") + @RequestPart(name = "resourceType") String resourceTypeStr, + @Parameter(description = "Resource descriptor (JSON).") + @RequestPart(name = "descriptor", required = false) String descriptor, + @Parameter(description = "Resource sub type.") + @RequestPart(name = "resourceSubType", required = false) String resourceSubTypeStr, + @Parameter(description = "Resource file.") + @RequestPart MultipartFile file) throws Exception { + TbResource resource = new TbResource(); + resource.setTenantId(getTenantId()); + resource.setTitle(StringUtils.isNotEmpty(title) ? title : file.getOriginalFilename()); + ResourceType resourceType = ResourceType.valueOf(resourceTypeStr); + resource.setResourceType(resourceType); + + if (StringUtils.isNotEmpty(descriptor)) { + resource.setDescriptor(JacksonUtil.toJsonNode(descriptor)); + } else { + String mediaType = resourceType.getMediaType() != null ? resourceType.getMediaType() : file.getContentType(); + resource.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType)); + } + + if (StringUtils.isNotEmpty(resourceSubTypeStr)) { + resource.setResourceSubType(ResourceSubType.valueOf(resourceSubTypeStr)); + } + resource.setFileName(file.getOriginalFilename()); + resource.setData(file.getBytes()); + + checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); + return tbResourceService.save(resource, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping(value = "/resource/{id}/data", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public TbResourceInfo updateResourceData(@Parameter(description = "Unique identifier of the Resource to update", required = true) + @PathVariable UUID id, + @Parameter(description = "Resource file.") + @RequestPart MultipartFile file) throws Exception { + TbResourceId tbResourceId = new TbResourceId(id); + TbResource resource = checkResourceId(tbResourceId, Operation.WRITE); + resource.setFileName(file.getOriginalFilename()); + resource.setData(file.getBytes()); + return tbResourceService.save(resource, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping("/resource/{id}/info") + public TbResourceInfo updateResourceInfo(@Parameter(description = "Unique identifier of the Resource to update", required = true) + @PathVariable UUID id, + @Parameter(description = "A JSON value representing the Resource Info.") + @RequestBody TbResourceInfo resourceInfo) throws Exception { + TbResourceId tbResourceId = new TbResourceId(id); + checkResourceInfoId(tbResourceId, Operation.WRITE); + resourceInfo.setId(tbResourceId); + TbResource resource = new TbResource(resourceInfo); + return tbResourceService.save(resource, getCurrentUser()); + } + @ApiOperation(value = "Get Resource Infos (getResources)", notes = "Returns a page of Resource Info objects owned by tenant or sysadmin. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @@ -266,11 +337,10 @@ public class TbResourceController extends BaseController { } } - @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/resource", params = {"resourceIds"}) - public List getSystemOrTenantResourcesByIds( - @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + public List getSystemOrTenantResourcesByIdsV1( @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { SecurityUser user = getCurrentUser(); List resourceIds = new ArrayList<>(); @@ -280,7 +350,16 @@ public class TbResourceController extends BaseController { return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds); } - @ApiOperation(value = "Get All Resource Infos (getAllResources)", + @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/resource/list") + public List getSystemOrTenantResourcesByIds( + @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { + return getSystemOrTenantResourcesByIdsV1(resourceUuids); + } + + @ApiOperation(value = "Get All Resource Infos (getTenantResources)", notes = "Returns a page of Resource Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java b/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java index 46291b7b3f..d74a1c02b3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.controller; -/** - * Created by ashvayka on 17.05.18. - */ public class TbUrlConstants { public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry"; public static final String RPC_V1_URL_PREFIX = "/api/plugins/rpc"; 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 8ff71494df..754c9253d9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -24,7 +24,12 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -36,13 +41,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; 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.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.JacksonUtil; @@ -86,6 +92,7 @@ import org.thingsboard.server.service.telemetry.TsData; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -129,10 +136,6 @@ import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_SC import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TS_STRICT_DATA_EXAMPLE; - -/** - * Created by ashvayka on 22.03.18. - */ @RestController @TbCoreComponent @RequestMapping(TbUrlConstants.TELEMETRY_URL_PREFIX) @@ -168,10 +171,10 @@ public class TelemetryController extends BaseController { "\n\n * SERVER_SCOPE - supported for all entity types;" + "\n * CLIENT_SCOPE - supported for devices;" + "\n * SHARED_SCOPE - supported for devices. " - + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/keys/attributes") public DeferredResult getAttributeKeys( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { @@ -183,10 +186,10 @@ public class TelemetryController extends BaseController { "\n\n * SERVER_SCOPE - supported for all entity types;" + "\n * CLIENT_SCOPE - supported for devices;" + "\n * SHARED_SCOPE - supported for devices. " - + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}") public DeferredResult getAttributeKeysByScope( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @@ -201,17 +204,23 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + ATTRIBUTE_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AttributeData.class))))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/values/attributes") public DeferredResult getAttributes( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keys)); } @@ -223,26 +232,32 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + ATTRIBUTE_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = AttributeData.class))))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}") public DeferredResult getAttributesByScope( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keys)); } @ApiOperation(value = "Get time series keys (getTimeseriesKeys)", notes = "Returns a set of unique time series key names for the selected entity. " + - "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/keys/timeseries") public DeferredResult getTimeseriesKeys( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { @@ -261,22 +276,53 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + LATEST_TS_STRICT_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class)))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries") public DeferredResult getLatestTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); + (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes)); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"}) + public DeferredResult getTimeseries( + @PathVariable("entityType") String entityType, + @PathVariable("entityId") String entityIdStr, + @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam(name = "startTs") Long startTs, + @RequestParam(name = "endTs") Long endTs, + @RequestParam(name = "intervalType", required = false) IntervalType intervalType, + @RequestParam(name = "interval", defaultValue = "0") Long interval, + @RequestParam(name = "timeZone", required = false) String timeZone, + @RequestParam(name = "limit", defaultValue = "100") Integer limit, + @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, + @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); + DeferredResult response = new DeferredResult<>(); + Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs, + intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), + getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); + return response; } - @ApiOperation(value = "Get time series data (getTimeseries)", + @ApiOperation(value = "Get time series data (getTimeseriesHistory)", notes = "Returns a range of time series values for specified entity. " + "Returns not aggregated data by default. " + "Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " + @@ -284,14 +330,17 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + TS_STRICT_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class)))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"}) - @ResponseBody - public DeferredResult getTimeseries( + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries/history") + public DeferredResult getTimeseriesHistory( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys, + @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A long value representing the start timestamp of the time range in milliseconds, UTC.") @RequestParam(name = "startTs") Long startTs, @Parameter(description = "A long value representing the end timestamp of the time range in milliseconds, UTC.") @@ -312,12 +361,10 @@ public class TelemetryController extends BaseController { @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { - DeferredResult response = new DeferredResult<>(); - Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs, - intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), - getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); - return response; + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { + return getTimeseries(entityType, entityIdStr, keysStr, startTs, endTs, intervalType, interval, timeZone, limit, aggStr, orderBy, useStrictDataTypes, params); } @ApiOperation(value = "Save device attributes (saveDeviceAttributes)", @@ -339,7 +386,8 @@ public class TelemetryController extends BaseController { @PathVariable("deviceId") String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -364,7 +412,8 @@ public class TelemetryController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -389,7 +438,8 @@ public class TelemetryController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -408,13 +458,13 @@ public class TelemetryController extends BaseController { @ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/{entityType}/{entityId}/timeseries/{scope}") public DeferredResult saveEntityTelemetry( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, 0L); } @@ -432,14 +482,14 @@ public class TelemetryController extends BaseController { @ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}") public DeferredResult saveEntityTelemetryWithTTL( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope, @Parameter(description = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, ttl); } @@ -452,6 +502,9 @@ public class TelemetryController extends BaseController { " Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." + " The replacement value will be fetched from the 'time series' table, and its timestamp will be the most recent one before the defined time-range. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), @@ -461,12 +514,11 @@ public class TelemetryController extends BaseController { "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/{entityType}/{entityId}/timeseries/delete") public DeferredResult deleteEntityTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr, + @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.") @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys, @Parameter(description = "A long value representing the start timestamp of removal time range in milliseconds.") @@ -476,16 +528,18 @@ public class TelemetryController extends BaseController { @Parameter(description = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.") @RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest, @Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") - @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { + @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); + return deleteTimeseries(entityId, keys, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); } - private DeferredResult deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, + private DeferredResult deleteTimeseries(EntityId entityIdStr, List keys, boolean deleteAllDataForKeys, Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException { - List keys = toKeysList(keysStr); if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -533,6 +587,9 @@ public class TelemetryController extends BaseController { @ApiOperation(value = "Delete device attributes (deleteDeviceAttributes)", notes = "Delete device attributes using provided Device Id, scope and a list of keys. " + "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), @@ -542,21 +599,27 @@ public class TelemetryController extends BaseController { "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/{deviceId}/{scope}") public DeferredResult deleteDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); } @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " + + "This operation is idempotent: keys that do not exist are silently ignored and the response is still 200 OK. " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + + @ApiResponse(responseCode = "200", description = "Entity attributes were removed for the selected keys in the request (keys that did not exist are silently ignored). " + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @@ -564,21 +627,26 @@ public class TelemetryController extends BaseController { "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/{entityType}/{entityId}/{scope}") public DeferredResult deleteEntityAttributes( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @Parameter(hidden = true) + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); } - private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, String keysStr) throws ThingsboardException { - List keys = toKeysList(keysStr); + private List getKeys(String keysStr, MultiValueMap params) { + return params.get("key") != null ? params.get("key") : toKeysList(keysStr); + } + + private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, List keys) throws ThingsboardException { if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -709,30 +777,29 @@ public class TelemetryController extends BaseController { }); } - private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, String keys, Boolean useStrictDataTypes) { + private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, List keys, Boolean useStrictDataTypes) { ListenableFuture> future; - if (StringUtils.isEmpty(keys)) { + if (keys.isEmpty()) { future = tsService.findAllLatest(user.getTenantId(), entityId); } else { - future = tsService.findLatest(user.getTenantId(), entityId, toKeysList(keys)); + future = tsService.findLatest(user.getTenantId(), entityId, keys); } Futures.addCallback(future, getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); } - private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, String keys) { - List keyList = toKeysList(keys); - FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList); + private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, List keys) { + FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keys); if (scope != null) { - if (keyList != null && !keyList.isEmpty()) { - Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keyList), callback, MoreExecutors.directExecutor()); + if (keys != null && !keys.isEmpty()) { + Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keys), callback, MoreExecutors.directExecutor()); } else { Futures.addCallback(attributesService.findAll(user.getTenantId(), entityId, scope), callback, MoreExecutors.directExecutor()); } } else { List>> futures = new ArrayList<>(); for (AttributeScope tmpScope : AttributeScope.values()) { - if (keyList != null && !keyList.isEmpty()) { - futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keyList)); + if (keys != null && !keys.isEmpty()) { + futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keys)); } else { futures.add(attributesService.findAll(user.getTenantId(), entityId, tmpScope)); } @@ -872,11 +939,11 @@ public class TelemetryController extends BaseController { } private List toKeysList(String keys) { - List keyList = null; if (!StringUtils.isEmpty(keys)) { - keyList = Arrays.asList(keys.split(",")); + return Arrays.asList(keys.split(",")); + } else { + return Collections.emptyList(); } - return keyList; } private DeferredResult getImmediateDeferredResult(String message, HttpStatus status) { 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 55bd76fdcb..dcd53f7762 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -15,18 +15,21 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; 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.Tenant; @@ -42,6 +45,11 @@ import org.thingsboard.server.service.entitiy.tenant.TbTenantService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -70,8 +78,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Get Tenant (getTenantById)", notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/{tenantId}") public Tenant getTenantById( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION) @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { @@ -86,8 +93,7 @@ public class TenantController extends BaseController { notes = "Fetch the Tenant Info object based on the provided Tenant Id. " + TENANT_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/info/{tenantId}") public TenantInfo getTenantInfoById( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION) @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { @@ -105,8 +111,7 @@ public class TenantController extends BaseController { "Remove 'id', 'tenantId' from the request body example (below) to create new Tenant entity." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/tenant") public Tenant saveTenant(@Parameter(description = "A JSON value representing the tenant.") @RequestBody Tenant tenant) throws Exception { checkEntity(tenant.getId(), tenant, Resource.TENANT); @@ -114,9 +119,9 @@ public class TenantController extends BaseController { } @ApiOperation(value = "Delete Tenant (deleteTenant)", - notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE) + notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @DeleteMapping(value = "/tenant/{tenantId}") @ResponseStatus(value = HttpStatus.OK) public void deleteTenant(@Parameter(description = TENANT_ID_PARAM_DESCRIPTION) @PathVariable(TENANT_ID) String strTenantId) throws Exception { @@ -128,8 +133,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenants", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenants") public PageData getTenants( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -148,8 +152,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. " + TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantInfos") public PageData getTenantInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -166,4 +169,38 @@ public class TenantController extends BaseController { return checkNotNull(tenantService.findTenantInfos(pageLink)); } + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/tenants", params = {"tenantIds"}) + public List getTenantsByIdsV1( + @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("tenantIds") Set tenantUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + List tenantIds = new ArrayList<>(); + for (UUID tenantIdUUID : tenantUUIDs) { + tenantIds.add(TenantId.fromUUID(tenantIdUUID)); + } + List tenants = tenantService.findTenantsByIds(tenantId, tenantIds); + return filterTenantsByReadPermission(tenants); + } + + @ApiOperation(value = "Get Tenants list (getTenantsByIds)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/tenants/list") + public List getTenantsByIds( + @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("tenantIds") Set tenantUUIDs) throws ThingsboardException { + return getTenantsByIdsV1(tenantUUIDs); + } + + private List filterTenantsByReadPermission(List tenants) { + return tenants.stream().filter(tenant -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.TENANT, Operation.READ, tenant.getId(), tenant); + } catch (ThingsboardException e) { + return false; + } + }).toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 7d2f84e827..a47768e91c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,13 +24,13 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; 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; @@ -74,8 +75,7 @@ public class TenantProfileController extends BaseController { @ApiOperation(value = "Get Tenant Profile (getTenantProfileById)", notes = "Fetch the Tenant Profile object based on the provided Tenant Profile Id. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantProfile/{tenantProfileId}") public TenantProfile getTenantProfileById( @Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { @@ -87,8 +87,7 @@ public class TenantProfileController extends BaseController { @ApiOperation(value = "Get Tenant Profile Info (getTenantProfileInfoById)", notes = "Fetch the Tenant Profile Info object based on the provided Tenant Profile Id. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantProfileInfo/{tenantProfileId}") public EntityInfo getTenantProfileInfoById( @Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { @@ -100,8 +99,7 @@ public class TenantProfileController extends BaseController { @ApiOperation(value = "Get default Tenant Profile Info (getDefaultTenantProfileInfo)", notes = "Fetch the default Tenant Profile Info object based. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantProfileInfo/default") public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException { return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId())); } @@ -164,9 +162,17 @@ public class TenantProfileController extends BaseController { " \"warnThreshold\": 0,\n" + " \"maxCalculatedFieldsPerEntity\": 5,\n" + " \"maxArgumentsPerCF\": 10,\n" + + " \"minAllowedScheduledUpdateIntervalInSecForCF\": 10,\n" + + " \"maxRelationLevelPerCfArgument\": 2,\n" + + " \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" + " \"maxDataPointsPerRollingArg\": 1000,\n" + " \"maxStateSizeInKBytes\": 32,\n" + - " \"maxSingleValueArgumentSizeInKBytes\": 2" + + " \"maxSingleValueArgumentSizeInKBytes\": 2," + + " \"minAllowedDeduplicationIntervalInSecForCF\": 10," + + " \"minAllowedAggregationIntervalInSecForCF\": 60," + + " \"intermediateAggregationIntervalInSecForCF\": 300," + + " \"cfReevaluationCheckInterval\": 60," + + " \"alarmsReevaluationInterval\": 60" + " }\n" + " },\n" + " \"default\": false\n" + @@ -175,8 +181,7 @@ public class TenantProfileController extends BaseController { "Remove 'id', from the request body example (below) to create new Tenant Profile entity." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/tenantProfile") public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.") @Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException { TenantProfile oldProfile; @@ -187,41 +192,39 @@ public class TenantProfileController extends BaseController { oldProfile = checkTenantProfileId(tenantProfile.getId(), Operation.WRITE); } - return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile); + return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile, getCurrentUser()); } @ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)", notes = "Deletes the tenant profile. Referencing non-existing tenant profile Id will cause an error. Referencing profile that is used by the tenants will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE) + @DeleteMapping(value = "/tenantProfile/{tenantProfileId}") @ResponseStatus(value = HttpStatus.OK) public void deleteTenantProfile(@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE); - tbTenantProfileService.delete(getTenantId(), profile); + tbTenantProfileService.delete(getTenantId(), profile, getCurrentUser()); } @ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)", notes = "Makes specified tenant profile to be default. Referencing non-existing tenant profile Id will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/tenantProfile/{tenantProfileId}/default") public TenantProfile setDefaultTenantProfile( @Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE); - tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId); + tenantProfile = tbTenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfile, getCurrentUser()); return tenantProfile; } @ApiOperation(value = "Get Tenant Profiles (getTenantProfiles)", notes = "Returns a page of tenant profiles registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantProfiles", params = {"pageSize", "page"}) public PageData getTenantProfiles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -240,8 +243,7 @@ public class TenantProfileController extends BaseController { @ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. " + TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantProfileInfos") public PageData getTenantProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -257,12 +259,19 @@ public class TenantProfileController extends BaseController { return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); } + @Hidden @GetMapping(value = "/tenantProfiles", params = {"ids"}) @PreAuthorize("hasAuthority('SYS_ADMIN')") - public List getTenantProfilesByIds(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string"))) - @RequestParam("ids") UUID[] ids) { + public List getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) { return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids); } + @ApiOperation(value = "Get Tenant Profile list (getTenantProfileList)") + @GetMapping(value = "/tenantProfiles/list") + @PreAuthorize("hasAuthority('SYS_ADMIN')") + public List getTenantProfileList(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("ids") UUID[] ids) { + return getTenantProfilesByIds(ids); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 5ad64a9f7b..3b15085651 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -16,7 +16,11 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -56,7 +60,6 @@ public class TwoFactorAuthConfigController extends BaseController { private final TwoFaConfigManager twoFaConfigManager; private final TwoFactorAuthService twoFactorAuthService; - @ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)", notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE + "Example:\n" + @@ -67,13 +70,12 @@ public class TwoFactorAuthConfigController extends BaseController { " }\n}\n```" + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @GetMapping("/account/settings") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException { SecurityUser user = getCurrentUser(); - return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()).orElse(null); + return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user).orElse(null); } - @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)", notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE + "For TOTP, this will return a corresponding account config template " + @@ -99,7 +101,7 @@ public class TwoFactorAuthConfigController extends BaseController { "Will throw an error (Bad Request) if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/generate") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam TwoFaProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); @@ -127,7 +129,7 @@ public class TwoFactorAuthConfigController extends BaseController { "or if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/submit") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); @@ -139,11 +141,11 @@ public class TwoFactorAuthConfigController extends BaseController { "Will throw an error (Bad Request) if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig, @RequestParam(required = false) String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); - if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) { + if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user, accountConfig.getProviderType()).isPresent()) { throw new IllegalArgumentException("2FA provider is already configured"); } @@ -154,7 +156,7 @@ public class TwoFactorAuthConfigController extends BaseController { verificationSuccess = true; } if (verificationSuccess) { - return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user, accountConfig); } else { throw new IllegalArgumentException("Verification code is incorrect"); } @@ -162,50 +164,48 @@ public class TwoFactorAuthConfigController extends BaseController { @ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes = "Update config for a given provider type. \n" + - "Update request example:\n" + - "```\n{\n \"useByDefault\": true\n}\n```\n" + - "Returns whole account's 2FA settings object.\n" + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) + "Update request example:\n" + + "```\n{\n \"useByDefault\": true\n}\n```\n" + + "Returns whole account's 2FA settings object.\n" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PutMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType, @RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException { SecurityUser user = getCurrentUser(); - TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) + TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType) .orElseThrow(() -> new IllegalArgumentException("Config for " + providerType + " 2FA provider not found")); accountConfig.setUseByDefault(updateRequest.isUseByDefault()); - return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user, accountConfig); } @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes = "Delete 2FA config for a given 2FA provider type. \n" + - "Returns whole account's 2FA settings object.\n" + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) + "Returns whole account's 2FA settings object.\n" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @DeleteMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException { SecurityUser user = getCurrentUser(); - return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType); + return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user, providerType); } - - @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = + @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderTypes)", notes = "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" + - "Example of response:\n" + - "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + "Example of response:\n" + + "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER ) @GetMapping("/providers") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public List getAvailableTwoFaProviders() throws ThingsboardException { + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") + public List getAvailableTwoFaProviderTypes() throws ThingsboardException { return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true) .map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream() .map(TwoFaProviderConfig::getProviderType) .collect(Collectors.toList()); } - @ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)", notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " + "If 2FA is not configured, then an empty response will be returned." + @@ -260,12 +260,12 @@ public class TwoFactorAuthConfigController extends BaseController { @PostMapping("/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") public PlatformTwoFaSettings savePlatformTwoFaSettings(@Parameter(description = "Settings value", required = true) - @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException { + @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException { return twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings); } - @Data + @Schema public static class TwoFaAccountConfigUpdateRequest { private boolean useByDefault; } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 4dd2da547f..8007a77cb1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,7 +29,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.StringUtils; 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.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; @@ -64,7 +64,6 @@ public class TwoFactorAuthController extends BaseController { private final SystemSecurityService systemSecurityService; private final UserService userService; - @ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)", notes = "Request 2FA verification code." + NEW_LINE + "To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " + @@ -92,30 +91,28 @@ public class TwoFactorAuthController extends BaseController { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); if (verificationSuccess) { - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); - user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); - return tokenFactory.createTokenPair(user); + logLogInAction(servletRequest, user, null); + return createTokenPair(user); } else { - ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); + IllegalArgumentException error = new IllegalArgumentException("Verification code is incorrect"); + logLogInAction(servletRequest, user, error); throw error; } } - - @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = + @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderInfos)", notes = "Get the list of 2FA provider infos available for user to use. Example:\n" + - "```\n[\n" + - " {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + - " {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" + - " {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" + - "]\n```") + "```\n[\n" + + " {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + + " {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" + + " {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" + + "]\n```") @GetMapping("/providers") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public List getAvailableTwoFaProviders() throws ThingsboardException { + public List getAvailableTwoFaProviderInfos() throws ThingsboardException { SecurityUser user = getCurrentUser(); Optional platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true); - return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()) + return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user) .map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList()) .stream().map(config -> { String contact = null; @@ -139,11 +136,38 @@ public class TwoFactorAuthController extends BaseController { .collect(Collectors.toList()); } + @ApiOperation(value = "Get regular token pair after successfully configuring 2FA", + notes = "Checks 2FA is configured, returning token pair on success.") + @PostMapping("/login") + @PreAuthorize("hasAuthority('MFA_CONFIGURATION_TOKEN')") + public JwtPair authenticateByTwoFaConfigurationToken(HttpServletRequest servletRequest) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + if (twoFactorAuthService.isTwoFaEnabled(user.getTenantId(), user)) { + logLogInAction(servletRequest, user, null); + return createTokenPair(user); + } else { + IllegalArgumentException error = new IllegalArgumentException("2FA is not configured"); + logLogInAction(servletRequest, user, error); + throw error; + } + } + + private JwtPair createTokenPair(SecurityUser user) { + log.debug("[{}][{}] Creating token pair for user", user.getTenantId(), user.getId()); + user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); + return tokenFactory.createTokenPair(user); + } + + private void logLogInAction(HttpServletRequest servletRequest, SecurityUser user, Exception error) { + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); + } + @Data @AllArgsConstructor @Builder public static class TwoFaProviderInfo { private TwoFaProviderType type; + @JsonProperty("default") private boolean isDefault; private String contact; private Integer minVerificationCodeSendPeriod; diff --git a/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java index fa7004de82..4c820478e9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java @@ -17,11 +17,9 @@ package org.thingsboard.server.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; 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.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -37,9 +35,8 @@ public class UiSettingsController extends BaseController { notes = "Get UI help base url used to fetch help assets. " + "The actual value of the base url is configurable in the system configuration file.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/uiSettings/helpBaseUrl", method = RequestMethod.GET) - @ResponseBody - public String getHelpBaseUrl() throws ThingsboardException { + @GetMapping(value = "/uiSettings/helpBaseUrl") + public String getHelpBaseUrl() { return helpBaseUrl; } diff --git a/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java b/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java index 14fff9c16c..ba09447388 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java @@ -18,9 +18,8 @@ package org.thingsboard.server.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; 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.UsageInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -37,9 +36,9 @@ public class UsageInfoController extends BaseController { private UsageInfoService usageInfoService; @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/usage", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/usage") public UsageInfo getTenantUsageInfo() throws ThingsboardException { return checkNotNull(usageInfoService.getUsageInfo(getCurrentUser().getTenantId())); } + } 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 be0528734a..b919b664e5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -16,7 +16,9 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -33,9 +35,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; 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.common.util.JacksonUtil; @@ -84,6 +84,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; @@ -133,8 +136,7 @@ public class UserController extends BaseController { "If the user has the authority of 'TENANT_ADMIN', the server checks that the requested user is owned by the same tenant. " + "If the user has the authority of 'CUSTOMER_USER', the server checks that the requested user is owned by the same customer.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/user/{userId}") public User getUserById( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId) throws ThingsboardException { @@ -150,8 +152,7 @@ public class UserController extends BaseController { "If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to login as any tenant administrator. " + "If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to login as any customer user. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/user/tokenAccessEnabled") public boolean isUserTokenAccessEnabled() { return userTokenAccessEnabled; } @@ -161,8 +162,7 @@ public class UserController extends BaseController { "If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to get the token of any tenant administrator. " + "If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to get the token of any customer user that belongs to the same tenant. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/user/{userId}/token") public JwtPair getUserToken( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId) throws ThingsboardException { @@ -189,8 +189,7 @@ public class UserController extends BaseController { "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new User entity." + "\n\nAvailable for users with 'SYS_ADMIN', 'TENANT_ADMIN' or 'CUSTOMER_USER' authority.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/user", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/user") public User saveUser( @Parameter(description = "A JSON value representing the User.", required = true) @RequestBody User user, @@ -206,7 +205,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Send or re-send the activation email", notes = "Force send the activation email to the user. Useful to resend the email if user has accidentally deleted it. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/user/sendActivationMail", method = RequestMethod.POST) + @PostMapping(value = "/user/sendActivationMail") @ResponseStatus(value = HttpStatus.OK) public void sendActivationEmail( @Parameter(description = "Email of the user", required = true) @@ -229,7 +228,6 @@ public class UserController extends BaseController { "The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/user/{userId}/activationLink", produces = "text/plain") - @ResponseBody public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId, HttpServletRequest request) throws ThingsboardException { @@ -255,7 +253,7 @@ public class UserController extends BaseController { notes = "Deletes the User, it's credentials and all the relations (from and to the User). " + "Referencing non-existing User Id will cause an error. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE) + @DeleteMapping(value = "/user/{userId}") @ResponseStatus(value = HttpStatus.OK) public void deleteUser( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @@ -276,8 +274,7 @@ public class UserController extends BaseController { notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/users") public PageData getUsers( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -302,8 +299,7 @@ public class UserController extends BaseController { notes = "Returns page of user data objects. Search is been executed by email, firstName and " + "lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/users/info", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/users/info") public PageData findUsersByQuery( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -339,8 +335,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get Tenant Users (getTenantAdmins)", notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/{tenantId}/users") public PageData getTenantAdmins( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TENANT_ID) String strTenantId, @@ -363,8 +358,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get Customer Users (getCustomerUsers)", notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/users") public PageData getCustomerUsers( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -389,8 +383,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Enable/Disable User credentials (setUserCredentialsEnabled)", notes = "Enables or Disables user credentials. Useful when you would like to block user account without deleting it. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/user/{userId}/userCredentialsEnabled", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/user/{userId}/userCredentialsEnabled") public void setUserCredentialsEnabled( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId, @@ -398,7 +391,7 @@ public class UserController extends BaseController { @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException { checkParameter(USER_ID, strUserId); UserId userId = new UserId(toUUID(strUserId)); - User user = checkUserId(userId, Operation.WRITE); + checkUserId(userId, Operation.WRITE); TenantId tenantId = getCurrentUser().getTenantId(); userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); @@ -412,8 +405,7 @@ public class UserController extends BaseController { "Search is been executed by email, firstName and lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/users/assign/{alarmId}") public PageData getUsersForAssign( @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @PathVariable("alarmId") String strAlarmId, @@ -465,10 +457,7 @@ public class UserController extends BaseController { return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings(); } - @ApiOperation(value = "Update user settings (saveUserSettings)", - notes = "Update user settings for authorized user. Only specified json elements will be updated." + - "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + - "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PutMapping(value = "/user/settings") public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { @@ -476,8 +465,17 @@ public class UserController extends BaseController { userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, settings); } - @ApiOperation(value = "Get user settings (getUserSettings)", - notes = "Fetch the User settings based on authorized user. ") + @ApiOperation(value = "Update user settings (putGeneralUserSettings)", + notes = "Update user settings for authorized user. Only specified json elements will be updated." + + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + + "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/user/settings/general") + public void putGeneralUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + putUserSettings(settings); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/user/settings") public JsonNode getUserSettings() throws ThingsboardException { @@ -487,20 +485,28 @@ public class UserController extends BaseController { return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); } - @ApiOperation(value = "Delete user settings (deleteUserSettings)", + @ApiOperation(value = "Get user settings (getGeneralUserSettings)", + notes = "Fetch the User settings based on authorized user. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/settings/general") + public JsonNode getGeneralUserSettings() throws ThingsboardException { + return getUserSettings(); + } + + @ApiOperation(value = "Delete user settings (deleteGeneralUserSettings)", notes = "Delete user settings by specifying list of json element xpaths. \n " + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/user/settings/{paths}", method = RequestMethod.DELETE) - public void deleteUserSettings(@Parameter(description = PATHS) - @PathVariable(PATHS) String paths) throws ThingsboardException { + @DeleteMapping(value = "/user/settings/{paths}") + public void deleteGeneralUserSettings(@Parameter(description = PATHS) + @PathVariable(PATHS) String paths) throws ThingsboardException { checkParameter(USER_ID, paths); SecurityUser currentUser = getCurrentUser(); userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, Arrays.asList(paths.split(","))); } - @ApiOperation(value = "Update user settings (saveUserSettings)", + @ApiOperation(value = "Update user settings (putUserSettings)", notes = "Update user settings for authorized user. Only specified json elements will be updated." + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") @@ -527,15 +533,15 @@ public class UserController extends BaseController { return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); } - @ApiOperation(value = "Delete user settings (deleteUserSettings)", + @ApiOperation(value = "Delete user settings by type (deleteUserSettingsByType)", notes = "Delete user settings by specifying list of json element xpaths. \n " + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/user/settings/{type}/{paths}", method = RequestMethod.DELETE) - public void deleteUserSettings(@Parameter(description = PATHS) - @PathVariable(PATHS) String paths, - @Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") - @PathVariable("type") String strType) throws ThingsboardException { + @DeleteMapping(value = "/user/settings/{type}/{paths}") + public void deleteUserSettingsByType(@Parameter(description = PATHS) + @PathVariable(PATHS) String paths, + @Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType) throws ThingsboardException { checkParameter(USER_ID, paths); UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); checkNotReserved(strType, type); @@ -543,8 +549,7 @@ public class UserController extends BaseController { userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), type, Arrays.asList(paths.split(","))); } - @ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)", - notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/user/dashboards") public UserDashboardsInfo getUserDashboardsInfo() throws ThingsboardException { @@ -552,11 +557,18 @@ public class UserController extends BaseController { return userSettingsService.findUserDashboardsInfo(currentUser.getTenantId(), currentUser.getId()); } + @ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)", + notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/lastVisitedDashboards") + public UserDashboardsInfo getLastVisitedDashboards() throws ThingsboardException { + return getUserDashboardsInfo(); + } + @ApiOperation(value = "Report action of User over the dashboard (reportUserDashboardAction)", notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/user/dashboards/{dashboardId}/{action}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/user/dashboards/{dashboardId}/{action}") public UserDashboardsInfo reportUserDashboardAction( @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DashboardController.DASHBOARD_ID) String strDashboardId, @@ -593,6 +605,40 @@ public class UserController extends BaseController { userService.removeMobileSession(user.getTenantId(), mobileToken); } + @Hidden + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/users", params = {"userIds"}) + public List getUsersByIdsV1( + @RequestParam("userIds") Set userUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + List userIds = new ArrayList<>(); + for (UUID userUUID : userUUIDs) { + userIds.add(new UserId(userUUID)); + } + List users = userService.findUsersByTenantIdAndIds(tenantId, userIds); + return filterUsersByReadPermission(users); + } + + @ApiOperation(value = "Get Users By Ids (getUsersByIds)", + notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/users/list") + public List getUsersByIds( + @Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("userIds") Set userUUIDs) throws ThingsboardException { + return getUsersByIdsV1(userUUIDs); + } + + private List filterUsersByReadPermission(List users) { + return users.stream().filter(user -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.USER, Operation.READ, user.getId(), user); + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + } + private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException { if (type.isReserved()) { throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); 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 bbcf370158..a89d870bf0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -15,19 +15,20 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; 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.StringUtils; @@ -111,8 +112,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)", notes = "Get the Widget Type Info based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetTypeInfo/{widgetTypeId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypeInfo/{widgetTypeId}") public WidgetTypeInfo getWidgetTypeInfoById( @Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { @@ -132,8 +132,7 @@ public class WidgetTypeController extends AutoCommitController { "Remove 'id', 'tenantId' rom the request body example (below) to create new Widget Type entity." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetType", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/widgetType") public WidgetTypeDetails saveWidgetType( @Parameter(description = "A JSON value representing the Widget Type Details.", required = true) @RequestBody WidgetTypeDetails widgetTypeDetails, @@ -153,7 +152,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Delete widget type (deleteWidgetType)", notes = "Deletes the Widget Type. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.DELETE) + @DeleteMapping(value = "/widgetType/{widgetTypeId}") @ResponseStatus(value = HttpStatus.OK) public void deleteWidgetType( @Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @@ -168,8 +167,7 @@ public class WidgetTypeController extends AutoCommitController { notes = "Returns a page of Widget Type objects available for current user. " + WIDGET_TYPE_DESCRIPTION + " " + PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypes", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypes", params = {"pageSize", "page"}) public PageData getWidgetTypes( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -212,11 +210,9 @@ public class WidgetTypeController extends AutoCommitController { } } - @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)", - notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}) @Deprecated public List getBundleWidgetTypesByBundleAlias( @Parameter(description = "System or Tenant", required = true) @@ -233,23 +229,29 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", - notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypes", params = {"widgetsBundleId"}, method = RequestMethod.GET) - @ResponseBody - public List getBundleWidgetTypes( + @GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"}) + public List getBundleWidgetTypesV1( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId)); } - @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)", - notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", + notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes") + public List getBundleWidgetTypes( + @Parameter(description = "Widget Bundle Id", required = true) + @PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + return getBundleWidgetTypesV1(strWidgetsBundleId); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}) @Deprecated public List getBundleWidgetTypesDetailsByBundleAlias( @Parameter(description = "System or Tenant", required = true) @@ -269,8 +271,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetails)", notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}) public List getBundleWidgetTypesDetails( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId, @@ -291,8 +292,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypeFqns") public List getBundleWidgetTypeFqns( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @@ -300,11 +300,9 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId)); } - @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)", - notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}) @Deprecated public List getBundleWidgetTypesInfosByBundleAlias( @Parameter(description = "System or Tenant", required = true) @@ -325,8 +323,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)", notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"}) public PageData getBundleWidgetTypesInfos( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId, @@ -354,11 +351,9 @@ public class WidgetTypeController extends AutoCommitController { widgetTypeDeprecatedFilter, widgetTypes, pageLink)); } - @ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)", - notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}) @Deprecated public WidgetType getWidgetTypeByBundleAliasAndTypeAlias( @Parameter(description = "System or Tenant", required = true) @@ -382,8 +377,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get Widget Type (getWidgetType)", notes = "Get the Widget Type by FQN. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER, hidden = true) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetType", params = {"fqn"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetType", params = {"fqn"}) public WidgetType getWidgetType( @Parameter(description = "Widget Type fqn", required = true) @RequestParam String fqn) throws ThingsboardException { 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 357c23259a..af087c5fd2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -15,17 +15,20 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; 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.exception.ThingsboardException; @@ -48,10 +51,12 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -79,8 +84,7 @@ public class WidgetsBundleController extends BaseController { @ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)", notes = "Get the Widget Bundle based on the provided Widget Bundle Id. " + WIDGET_BUNDLE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetsBundle/{widgetsBundleId}") public WidgetsBundle getWidgetsBundleById( @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("widgetsBundleId") String strWidgetsBundleId, @@ -106,8 +110,7 @@ public class WidgetsBundleController extends BaseController { "Remove 'id', 'tenantId' from the request body example (below) to create new Widgets Bundle entity." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetsBundle", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/widgetsBundle") public WidgetsBundle saveWidgetsBundle( @Parameter(description = "A JSON value representing the Widget Bundle.", required = true) @RequestBody WidgetsBundle widgetsBundle) throws Exception { @@ -126,7 +129,7 @@ public class WidgetsBundleController extends BaseController { @ApiOperation(value = "Update widgets bundle widgets types list (updateWidgetsBundleWidgetTypes)", notes = "Updates widgets bundle widgets list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes", method = RequestMethod.POST) + @PostMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes") @ResponseStatus(value = HttpStatus.OK) public void updateWidgetsBundleWidgetTypes( @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @@ -152,7 +155,7 @@ public class WidgetsBundleController extends BaseController { @ApiOperation(value = "Update widgets bundle widgets list from widget type FQNs list (updateWidgetsBundleWidgetFqns)", notes = "Updates widgets bundle widgets list from widget type FQNs list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns", method = RequestMethod.POST) + @PostMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns") @ResponseStatus(value = HttpStatus.OK) public void updateWidgetsBundleWidgetFqns( @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @@ -169,7 +172,7 @@ public class WidgetsBundleController extends BaseController { @ApiOperation(value = "Delete widgets bundle (deleteWidgetsBundle)", notes = "Deletes the widget bundle. Referencing non-existing Widget Bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.DELETE) + @DeleteMapping(value = "/widgetsBundle/{widgetsBundleId}") @ResponseStatus(value = HttpStatus.OK) public void deleteWidgetsBundle( @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @@ -184,8 +187,7 @@ public class WidgetsBundleController extends BaseController { notes = "Returns a page of Widget Bundle objects available for current user. " + WIDGET_BUNDLE_DESCRIPTION + " " + PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetsBundles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetsBundles", params = {"pageSize", "page"}) public PageData getWidgetsBundles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -220,12 +222,10 @@ public class WidgetsBundleController extends BaseController { } } - @ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)", - notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET) - @ResponseBody - public List getWidgetsBundles() throws ThingsboardException { + @GetMapping(value = "/widgetsBundles") + public List getWidgetsBundlesV1() throws ThingsboardException { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); } else { @@ -234,4 +234,35 @@ public class WidgetsBundleController extends BaseController { } } + @ApiOperation(value = "Get all Widget Bundles (getAllWidgetsBundles)", + notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles/all") + public List getAllWidgetsBundles() throws ThingsboardException { + return getWidgetsBundlesV1(); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"}) + public List getWidgetsBundlesByIds( + @RequestParam("widgetsBundleIds") Set widgetsBundleUUIDs) throws ThingsboardException { + List widgetsBundleIds = new ArrayList<>(); + for (UUID widgetsBundleUUID : widgetsBundleUUIDs) { + widgetsBundleIds.add(new WidgetsBundleId(widgetsBundleUUID)); + } + return widgetsBundleService.findSystemOrTenantWidgetsBundlesByIds(getTenantId(), widgetsBundleIds); + } + + @ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesList)", + notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles/list", params = {"widgetsBundleIds"}) + public List getWidgetsBundlesList( + @Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("widgetsBundleIds") Set widgetsBundleUUIDs) throws ThingsboardException { + return getWidgetsBundlesByIds(widgetsBundleUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 133584201c..cf56d051ca 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -52,6 +52,7 @@ import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; +import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -100,6 +101,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke private RateLimitService rateLimitService; @Autowired private JwtAuthenticationProvider authenticationProvider; + @Autowired + private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider; @Value("${server.ws.send_timeout:5000}") private long sendTimeout; @@ -194,7 +197,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke log.trace("{} Authenticating session", sessionRef); SecurityUser securityCtx; try { - securityCtx = authenticationProvider.authenticate(authCmd.getToken()); + if (StringUtils.isNotEmpty(authCmd.getApiKey())) { + securityCtx = apiKeyAuthenticationProvider.authenticate(authCmd.getApiKey()); + } else { + securityCtx = authenticationProvider.authenticate(authCmd.getToken()); + } } catch (Exception e) { close(sessionRef, CloseStatus.BAD_DATA.withReason(e.getMessage())); return; @@ -328,9 +335,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } SecurityUser securityCtx = null; - String token = StringUtils.substringAfter(session.getUri().getQuery(), "token="); - if (StringUtils.isNotEmpty(token)) { - securityCtx = authenticationProvider.authenticate(token); + String query = session.getUri().getQuery(); + if (query != null) { + String token = extractQueryParam(query, "token"); + if (StringUtils.isNotEmpty(token)) { + securityCtx = authenticationProvider.authenticate(token); + } } return WebSocketSessionRef.builder() .sessionId(UUID.randomUUID().toString()) @@ -341,6 +351,15 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke .build(); } + private String extractQueryParam(String query, String paramName) { + for (String param : query.split("&")) { + if (param.startsWith(paramName + "=")) { + return param.substring(paramName.length() + 1); + } + } + return null; + } + private SessionMetaData getSessionMd(String internalSessionId) { SessionMetaData sessionMd = internalSessionMap.get(internalSessionId); if (sessionMd == null) { @@ -482,6 +501,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } } } + } @Override diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java new file mode 100644 index 0000000000..268fae2b1f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.http.HttpStatus; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; + +@Schema +public class ThingsboardEntitiesLimitExceededResponse extends ThingsboardErrorResponse { + + private final EntityType entityType; + + private final Long limit; + + protected ThingsboardEntitiesLimitExceededResponse(String message, EntityType entityType, Long limit) { + super(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN); + this.entityType = entityType; + this.limit = limit; + } + + public static ThingsboardEntitiesLimitExceededResponse of(final String message, final EntityType entityType, final Long limit) { + return new ThingsboardEntitiesLimitExceededResponse(message, entityType, limit); + } + + @Schema(description = "Entity type", accessMode = Schema.AccessMode.READ_ONLY) + public EntityType getEntityType() { + return entityType; + } + + @Schema(description = "Limit", accessMode = Schema.AccessMode.READ_ONLY) + public Long getLimit() { + return limit; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java index 96464668b6..28f08ae7ba 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java @@ -64,7 +64,8 @@ public class ThingsboardErrorResponse { "\n\n* `32` - Item not found (HTTP: 404 - Not Found)" + "\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" + "\n\n* `34` - Too many updates (Too many updates over Websocket session)" + - "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)", + "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)" + + "\n\n* `41` - Entities limit exceeded (HTTP: 403 - Forbidden)", example = "10", type = "integer", accessMode = Schema.AccessMode.READ_ONLY) public ThingsboardErrorCode getErrorCode() { diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index bcf897f8fb..58b1e8d422 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -33,6 +33,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; @@ -47,6 +48,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.util.WebUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; @@ -96,6 +98,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS); errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_UPDATES, HttpStatus.TOO_MANY_REQUESTS); errorCodeToStatusMap.put(ThingsboardErrorCode.SUBSCRIPTION_VIOLATION, HttpStatus.FORBIDDEN); + errorCodeToStatusMap.put(ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN); errorCodeToStatusMap.put(ThingsboardErrorCode.VERSION_CONFLICT, HttpStatus.CONFLICT); } @@ -139,23 +142,28 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand try { response.setContentType(MediaType.APPLICATION_JSON_VALUE); - if (exception instanceof ThingsboardException) { - ThingsboardException thingsboardException = (ThingsboardException) exception; + if (exception instanceof ThingsboardException thingsboardException) { if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) { - handleSubscriptionException((ThingsboardException) exception, response); + handleSubscriptionException(thingsboardException, response); } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) { handleDatabaseException(thingsboardException.getCause(), response); + } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED) { + if (thingsboardException.getCause() instanceof EntitiesLimitExceededException entitiesLimitExceededException) { + handleEntitiesLimitExceededException(entitiesLimitExceededException, response); + } else { + handleEntitiesLimitExceededException(thingsboardException, response); + } } else { - handleThingsboardException((ThingsboardException) exception, response); + handleThingsboardException(thingsboardException, response); } - } else if (exception instanceof TbRateLimitsException) { - handleRateLimitException(response, (TbRateLimitsException) exception); + } else if (exception instanceof TbRateLimitsException rateLimitsException) { + handleRateLimitException(response, rateLimitsException); } else if (exception instanceof AccessDeniedException) { handleAccessDeniedException(response); - } else if (exception instanceof AuthenticationException) { - handleAuthenticationException((AuthenticationException) exception, response); - } else if (exception instanceof MaxPayloadSizeExceededException) { - handleMaxPayloadSizeExceededException(response, (MaxPayloadSizeExceededException) exception); + } else if (exception instanceof AuthenticationException authenticationException) { + handleAuthenticationException(authenticationException, response); + } else if (exception instanceof MaxPayloadSizeExceededException maxPayloadSizeExceededException) { + handleMaxPayloadSizeExceededException(response, maxPayloadSizeExceededException); } else if (exception instanceof DataAccessException || exception instanceof PersistenceException) { handleDatabaseException(exception, response); } else { @@ -229,6 +237,20 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand writeResponse(errorResponse, response); } + private void handleEntitiesLimitExceededException(ThingsboardException entitiesLimitExceededException, HttpServletResponse response) throws IOException { + response.setStatus(HttpStatus.FORBIDDEN.value()); + JacksonUtil.writeValue(response.getWriter(), + JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitExceededException.getCause()).getResponseBodyAsByteArray(), Object.class)); + } + + private void handleEntitiesLimitExceededException(EntitiesLimitExceededException entitiesLimitExceededException, HttpServletResponse response) throws IOException { + EntityType entityType = entitiesLimitExceededException.getEntityType(); + Long limit = entitiesLimitExceededException.getLimit(); + response.setStatus(HttpStatus.FORBIDDEN.value()); + JacksonUtil.writeValue(response.getWriter(), + ThingsboardEntitiesLimitExceededResponse.of(entitiesLimitExceededException.getMessage(), entityType, limit)); + } + private void handleAccessDeniedException(HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); JacksonUtil.writeValue(response.getWriter(), @@ -249,13 +271,13 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof AuthMethodNotSupportedException) { JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); - } else if (authenticationException instanceof UserPasswordExpiredException) { - UserPasswordExpiredException expiredException = (UserPasswordExpiredException) authenticationException; + } else if (authenticationException instanceof UserPasswordExpiredException expiredException) { String resetToken = expiredException.getResetToken(); JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken)); - } else if (authenticationException instanceof UserPasswordNotValidException) { - UserPasswordNotValidException expiredException = (UserPasswordNotValidException) authenticationException; + } else if (authenticationException instanceof UserPasswordNotValidException expiredException) { JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsViolationResponse.of(expiredException.getMessage())); + } else if (authenticationException instanceof CredentialsExpiredException credentialsExpiredException) { + JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsViolationResponse.of(credentialsExpiredException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else { JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 774396a704..3b72ac662f 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.action; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -235,7 +236,7 @@ public class EntityActionService { } } - public void logEntityAction(User user, I entityId, E entity, CustomerId customerId, + public void logEntityAction(User user, @NotNull I entityId, E entity, CustomerId customerId, ActionType actionType, Exception e, Object... additionalInfo) { if (customerId == null || customerId.isNullUid()) { customerId = user.getCustomerId(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java new file mode 100644 index 0000000000..8cc0043198 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -0,0 +1,565 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +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.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonElement; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.function.TriConsumer; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesImmediateOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +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.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.DataConstants.CF_NAME_METADATA_KEY; +import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.dao.util.KvUtils.filterChangedAttr; +import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultTsKvEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggMetricArgument; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggregationArgument; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformTsRollingArgument; + +@Data +@Slf4j +public abstract class AbstractCalculatedFieldProcessingService { + + protected final AttributesService attributesService; + protected final TimeseriesService timeseriesService; + protected final TelemetrySubscriptionService tsSubService; + protected final ApiLimitService apiLimitService; + protected final RelationService relationService; + protected final OwnerService ownerService; + protected final TbClusterService clusterService; + + protected ListeningExecutorService calculatedFieldCallbackExecutor; + + @PostConstruct + public void init() { + calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( + Math.max(4, Runtime.getRuntime().availableProcessors()), getExecutorNamePrefix())); + } + + @PreDestroy + public void stop() { + if (calculatedFieldCallbackExecutor != null) { + calculatedFieldCallbackExecutor.shutdownNow(); + } + } + + protected abstract String getExecutorNamePrefix(); + + protected ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + Map> argFutures = switch (ctx.getCfType()) { + case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false, ts); + case SIMPLE, SCRIPT, ALARM, PROPAGATION -> getBaseCalculatedFieldArguments(ctx, entityId, ts); + case RELATED_ENTITIES_AGGREGATION -> fetchRelatedEntitiesAggArguments(ctx, entityId, ts); + case ENTITY_AGGREGATION -> fetchEntityAggArguments(ctx, entityId, ts); + }; + if (ctx.getCfType() == PROPAGATION) { + argFutures.put(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId)); + } + return Futures.whenAllComplete(argFutures.values()) + .call(() -> resolveArgumentFutures(argFutures), + MoreExecutors.directExecutor()); + } + + private Map> getBaseCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + Map> futures = new HashMap<>(); + for (var entry : ctx.getArguments().entrySet()) { + var argEntityId = resolveEntityId(ctx.getTenantId(), entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(ctx.getTenantId(), argEntityId, entry.getValue(), ts); + futures.put(entry.getKey(), argValueFuture); + } + return futures; + } + + protected EntityId resolveEntityId(TenantId tenantId, EntityId entityId, Argument argument) { + if (argument.getRefEntityId() != null) { + return argument.getRefEntityId(); + } + if (!argument.hasOwnerSource()) { + return entityId; + } + return resolveOwnerArgument(tenantId, entityId); + } + + protected Map resolveArgumentFutures(Map> argFutures) { + return argFutures.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, // Keep the key as is + entry -> resolveArgumentValue(entry.getKey(), entry.getValue()) + )); + } + + protected ArgumentEntry resolveArgumentValue(String key, ListenableFuture future) { + try { + return future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + throw new RuntimeException("Failed to fetch '" + key + "' argument: " + cause.getMessage(), cause); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to fetch '" + key + "' argument!", e); + } + } + + protected ListenableFuture fetchPropagationCalculatedFieldArgument(CalculatedFieldCtx ctx, EntityId entityId) { + ListenableFuture> propagationEntityIds = fromDynamicSource(ctx.getTenantId(), entityId, ctx.getPropagationArgument()); + return Futures.transform(propagationEntityIds, ArgumentEntry::createPropagationArgument, MoreExecutors.directExecutor()); + } + + protected Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly, long startTs) { + Map> argFutures = new HashMap<>(); + Set> entries = ctx.getArguments().entrySet(); + if (dynamicArgumentsOnly) { + entries = entries.stream() + .filter(entry -> entry.getValue().hasRelationQuerySource()) + .collect(Collectors.toSet()); + } + for (var entry : entries) { + switch (entry.getKey()) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> + argFutures.put(entry.getKey(), fetchArgumentValue(ctx.getTenantId(), entityId, entry.getValue(), startTs)); + default -> { + var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); + argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> + fetchGeofencingArgumentValue(ctx.getTenantId(), resolvedEntityIds, entry.getValue(), startTs), MoreExecutors.directExecutor())); + } + } + } + return argFutures; + } + + private Map> fetchRelatedEntitiesAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + if (!(ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config)) { + return Collections.emptyMap(); + } + ListenableFuture> relatedEntitiesFut = resolveRelatedEntities(ctx.getTenantId(), entityId, config.getRelation()); + + return config.getArguments().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> Futures.transformAsync(relatedEntitiesFut, relatedEntities -> fetchRelatedEntitiesArgumentEntry(ctx.getTenantId(), relatedEntities, entry.getValue(), ts), MoreExecutors.directExecutor()) + )); + } + + private Map> fetchEntityAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + if (!(ctx.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration config)) { + return Collections.emptyMap(); + } + return config.getArguments().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> fetchTimeSeries(ctx.getTenantId(), entityId, entry.getValue(), config.getInterval(), ts) + )); + } + + protected ListenableFuture> resolveRelatedEntities(TenantId tenantId, EntityId entityId, RelationPathLevel relation) { + Predicate filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo()); + ListenableFuture> relationsFut = relationService.findFilteredRelationsByPathQueryAsync(tenantId, new EntityRelationPathQuery(entityId, List.of(relation)), filter); + + return Futures.transform(relationsFut, relations -> { + if (relations == null) { + return Collections.emptyList(); + } + + return switch (relation.direction()) { + case FROM -> relations.stream().map(EntityRelation::getTo).toList(); + case TO -> relations.stream().map(EntityRelation::getFrom).toList(); + }; + }, calculatedFieldCallbackExecutor); + } + + private ListenableFuture> resolveGeofencingEntityIds(TenantId tenantId, EntityId entityId, Map.Entry entry) { + Argument value = entry.getValue(); + if (value.getRefEntityId() != null) { + return Futures.immediateFuture(List.of(value.getRefEntityId())); + } + if (!value.hasDynamicSource()) { + return Futures.immediateFuture(List.of(entityId)); + } + return fromDynamicSource(tenantId, entityId, value); + } + + private ListenableFuture> fromDynamicSource(TenantId tenantId, EntityId entityId, Argument value) { + var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); + return switch (refDynamicSourceConfiguration.getType()) { + case CURRENT_OWNER -> Futures.immediateFuture(List.of(resolveOwnerArgument(tenantId, entityId))); + case RELATION_PATH_QUERY -> { + var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; + Predicate filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo()); + yield Futures.transform(relationService.findFilteredRelationsByPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId), filter), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + } + }; + } + + private EntityId resolveOwnerArgument(TenantId tenantId, EntityId entityId) { + return ownerService.getOwner(tenantId, entityId); + } + + private ListenableFuture fetchGeofencingArgumentValue(TenantId tenantId, List geofencingEntities, Argument argument, long startTs) { + if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { + throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); + } + var geofencingEntityIdToKvEntryMapFutures = Futures.allAsList(fetchGeofencingEntityIdToKvEntriesFutures(tenantId, geofencingEntities, argument, startTs)); + return Futures.transform(geofencingEntityIdToKvEntryMapFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), MoreExecutors.directExecutor()); + } + + private List>> fetchGeofencingEntityIdToKvEntriesFutures(TenantId tenantId, List geofencingEntities, Argument argument, long startTs) { + return geofencingEntities.stream() + .map(entityId -> { + AttributeScope scope = argument.getRefEntityKey().getScope(); + String key = argument.getRefEntityKey().getKey(); + var attributesFuture = attributesService.find(tenantId, entityId, scope, key); + return Futures.transform(attributesFuture, resultOpt -> + Map.entry(entityId, resultOpt.orElseGet(() -> createDefaultAttributeEntry(argument, startTs))), + calculatedFieldCallbackExecutor); + }).collect(Collectors.toList()); + } + + private ListenableFuture fetchRelatedEntitiesArgumentEntry(TenantId tenantId, List aggEntities, Argument argument, long startTs) { + List>> futures = aggEntities.stream() + .map(entityId -> { + ListenableFuture argumentEntryFut = fetchArgumentValue(tenantId, entityId, argument, startTs); + return Futures.transform(argumentEntryFut, argumentEntry -> Map.entry(entityId, ArgumentEntry.createSingleValueArgument(entityId, argumentEntry)), MoreExecutors.directExecutor()); + }) + .toList(); + + ListenableFuture>> allFutures = Futures.allAsList(futures); + + return Futures.transform(allFutures, + entries -> ArgumentEntry.createAggArgument( + entries.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ), + MoreExecutors.directExecutor()); + } + + protected ListenableFuture fetchArgumentValue(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { + return switch (argument.getRefEntityKey().getType()) { + case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument, startTs); + case ATTRIBUTE -> fetchAttribute(tenantId, entityId, argument, startTs); + case TS_LATEST -> fetchTsLatest(tenantId, entityId, argument, startTs); + }; + } + + protected ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval) { + AggFunction function = metric.getFunction(); + long intervalMs = interval.getEndTs() - interval.getStartTs(); + BaseReadTsKvQuery query = new BaseReadTsKvQuery(argKey, interval.getStartTs(), interval.getEndTs(), intervalMs, 1, Aggregation.valueOf(function.name())); + ListenableFuture argumentEntryFut = fetchTimeSeriesInternal(tenantId, entityId, query, timeSeries -> transformAggMetricArgument(timeSeries, argKey, metric)); + return resolveArgumentValue(argKey, argumentEntryFut); + } + + private ListenableFuture fetchTimeSeries(TenantId tenantId, EntityId entityId, Argument argument, AggInterval interval, long queryEndTs) { + long intervalStartTs = interval.getCurrentIntervalStartTs(); + long intervalEndTs = interval.getCurrentIntervalEndTs(); + ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), intervalStartTs, queryEndTs, 0, 1, Aggregation.NONE); + return fetchTimeSeriesInternal(tenantId, entityId, query, timeSeries -> transformAggregationArgument(timeSeries, intervalStartTs, intervalEndTs)); + } + + private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument, long queryEndTs) { + long argTimeWindow = argument.getTimeWindow() == 0 ? queryEndTs : argument.getTimeWindow(); + long startInterval = queryEndTs - argTimeWindow; + ReadTsKvQuery query = buildTsRollingQuery(tenantId, argument, startInterval, queryEndTs); + return fetchTimeSeriesInternal(tenantId, entityId, query, tsRolling -> transformTsRollingArgument(tsRolling, query.getLimit(), argTimeWindow)); + } + + private ListenableFuture fetchAttribute(TenantId tenantId, EntityId entityId, Argument argument, long defaultLastUpdateTs) { + log.trace("[{}][{}] Fetching attribute for key {}", tenantId, entityId, argument.getRefEntityKey()); + var attributeOptFuture = attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()); + + return Futures.transform(attributeOptFuture, attrOpt -> { + log.debug("[{}][{}] Fetched attribute for key {}: {}", tenantId, entityId, argument.getRefEntityKey(), attrOpt); + return transformSingleValueArgument(attrOpt.orElseGet(() -> createDefaultAttributeEntry(argument, defaultLastUpdateTs))); + }, calculatedFieldCallbackExecutor); + } + + private ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long defaultTs) { + String timeseriesKey = argument.getRefEntityKey().getKey(); + log.trace("[{}][{}] Fetching latest timeseries {}", tenantId, entityId, timeseriesKey); + return Futures.transform(timeseriesService.findLatest(tenantId, entityId, timeseriesKey), result -> { + log.debug("[{}][{}] Fetched latest timeseries {}: {}", tenantId, entityId, timeseriesKey, result); + return transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, defaultTs))); + }, calculatedFieldCallbackExecutor); + } + + private ListenableFuture fetchTimeSeriesInternal(TenantId tenantId, EntityId entityId, ReadTsKvQuery query, Function, ArgumentEntry> transformArgument) { + log.trace("[{}][{}] Fetching timeseries for query {}", tenantId, entityId, query); + ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); + return Futures.transform(tsRollingFuture, tsRolling -> { + log.debug("[{}][{}] Fetched {} timeseries for query {}", tenantId, entityId, tsRolling == null ? 0 : tsRolling.size(), query); + return transformArgument.apply(tsRolling); + }, calculatedFieldCallbackExecutor); + } + + private ReadTsKvQuery buildTsRollingQuery(TenantId tenantId, Argument argument, long startTs, long endTs) { + long maxDataPoints = apiLimitService.getLimit( + tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); + int argumentLimit = argument.getLimit(); + int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argumentLimit; + return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE); + } + + protected void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback, + TriConsumer telemetryResultHandler) { + List propagationEntityIds = propagationResult.getEntityIds(); + if (propagationEntityIds.isEmpty()) { + callback.onSuccess(); + return; + } + if (propagationEntityIds.size() == 1) { + EntityId propagationEntityId = propagationEntityIds.get(0); + telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), callback); + return; + } + MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback); + for (var propagationEntityId : propagationEntityIds) { + telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), multipleTbCallback); + } + } + + protected void sendMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbCallback callback, TbMsg msg) { + try { + clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.trace("[{}][{}] Pushed message to rule engine: {} ", tenantId, entityId, msg); + callback.onSuccess(); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + }); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push message to rule engine: {}", tenantId, entityId, msg, e); + callback.onFailure(e); + } + } + + protected void saveTelemetryResult(TenantId tenantId, EntityId entityId, String cfName, TelemetryCalculatedFieldResult cfResult, List cfIds, TbCallback callback) { + OutputType type = cfResult.getType(); + JsonElement jsonResult = cfResult.toJsonElement(); + + log.trace("[{}][{}] Saving CF result: {}", tenantId, entityId, jsonResult); + switch (type) { + case ATTRIBUTES -> saveAttributes(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfResult.getScope(), cfName, cfIds, callback); + case TIME_SERIES -> saveTimeSeries(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfIds, System.currentTimeMillis(), callback); + } + } + + private void saveAttributes(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, AttributeScope scope, String cfName, List cfIds, TbCallback callback) { + if (!(outputStrategy instanceof AttributesImmediateOutputStrategy attOutputStrategy)) { + callback.onFailure(new IllegalArgumentException("Only AttributeImmediateOutputStrategy is supported.")); + } else { + AttributesSaveRequest.Strategy strategy = new Strategy(attOutputStrategy.isSaveAttribute(), attOutputStrategy.isSendWsUpdate(), attOutputStrategy.isProcessCfs()); + List newAttributes = JsonConverter.convertToAttributes(jsonResult); + + if (!attOutputStrategy.isUpdateAttributesOnlyOnValueChange()) { + saveAttributesInternal(tenantId, entityId, scope, cfName, cfIds, newAttributes, strategy, attOutputStrategy.isSendAttributesUpdatedNotification(), callback); + return; + } + + List keys = newAttributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); + ListenableFuture> findFuture = attributesService.find(tenantId, entityId, scope, keys); + + DonAsynchron.withCallback(findFuture, + existingAttributes -> { + List changed = filterChangedAttr(existingAttributes, newAttributes); + if (changed.isEmpty()) { + callback.onSuccess(); + return; + } + saveAttributesInternal(tenantId, entityId, scope, cfName, cfIds, changed, strategy, attOutputStrategy.isSendAttributesUpdatedNotification(), callback); + }, + callback::onFailure, + MoreExecutors.directExecutor()); + } + } + + private void saveAttributesInternal(TenantId tenantId, EntityId entityId, + AttributeScope scope, + String cfName, + List cfIds, + List entries, + AttributesSaveRequest.Strategy strategy, + boolean sendAttributesUpdatedNotification, + TbCallback callback) { + tsSubService.saveAttributes(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .scope(scope) + .entries(entries) + .strategy(strategy) + .previousCalculatedFieldIds(cfIds) + .callback(new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + if (sendAttributesUpdatedNotification) { + sendAttributesUpdatedMsg(tenantId, entityId, scope, cfName, entries); + } + callback.onSuccess(); + log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, entries); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, entries, t); + } + }) + .build()); + } + + private void saveTimeSeries(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, List cfIds, long ts, TbCallback callback) { + if (!(outputStrategy instanceof TimeSeriesImmediateOutputStrategy tsOutputStrategy)) { + callback.onFailure(new IllegalArgumentException("Only TimeSeriesImmediateOutputStrategy is supported.")); + } else { + TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(tsOutputStrategy.isSaveTimeSeries(), tsOutputStrategy.isSaveLatest(), tsOutputStrategy.isSendWsUpdate(), tsOutputStrategy.isProcessCfs()); + saveTimeSeriesInternal(tenantId, entityId, jsonResult, tsOutputStrategy.getTtl(), cfIds, ts, strategy, callback); + } + } + + private void saveTimeSeriesInternal(TenantId tenantId, EntityId entityId, JsonElement jsonResult, Long ttl, List cfIds, long ts, TimeseriesSaveRequest.Strategy strategy, TbCallback callback) { + Map> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts); + if (tsKvMap.isEmpty()) { + callback.onSuccess(); + return; + } + List tsEntries = toTsKvEntryList(tsKvMap); + TimeseriesSaveRequest.Builder builder = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .entries(tsEntries) + .strategy(strategy) + .callback(new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + callback.onSuccess(); + log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, tsEntries); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, tsEntries, t); + } + }); + if (ttl != null) { + builder.ttl(ttl); + } + if (cfIds != null && !cfIds.isEmpty()) { + builder.previousCalculatedFieldIds(cfIds); + } + tsSubService.saveTimeseries(builder.build()); + } + + private void sendAttributesUpdatedMsg(TenantId tenantId, EntityId entityId, + AttributeScope scope, + String cfName, + List entries) { + ObjectNode entityNode = JacksonUtil.newObjectNode(); + if (entries != null) { + entries.forEach(attributeKvEntry -> JacksonUtil.addKvEntry(entityNode, attributeKvEntry)); + } + + TbMsg attributesUpdatedMsg = TbMsg.newMsg() + .type(ATTRIBUTES_UPDATED) + .originator(entityId) + .data(JacksonUtil.toString(entityNode)) + .metaData(new TbMsgMetaData(Map.of( + CF_NAME_METADATA_KEY, cfName, + SCOPE, scope.name() + ))) + .build(); + + sendMsgToRuleEngine(tenantId, entityId, TbCallback.EMPTY, attributesUpdatedMsg); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java index a94c3c2261..4bd162ea91 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.service.cf; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldStateRestoreMsg; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.exception.TenantNotFoundException; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; +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.exception.CalculatedFieldStateException; @@ -37,6 +42,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; +@Slf4j public abstract class AbstractCalculatedFieldStateService implements CalculatedFieldStateService { @Autowired @@ -56,25 +62,44 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF protected abstract void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback); @Override - public final void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback) { + public final void deleteState(CalculatedFieldEntityCtxId stateId, TbCallback callback) { doRemove(stateId, callback); } protected abstract void doRemove(CalculatedFieldEntityCtxId stateId, TbCallback callback); - protected void processRestoredState(CalculatedFieldStateProto stateMsg, TbCallback callback) { + protected void processRestoredState(CalculatedFieldStateProto stateMsg, TopicPartitionInfo partition, TbCallback callback) { var id = fromProto(stateMsg.getId()); - var state = fromProto(stateMsg); - processRestoredState(id, state, callback); + if (partition == null) { + try { + partition = actorSystemContext.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, id.tenantId(), id.entityId()); + } catch (TenantNotFoundException e) { + log.debug("Skipping CF state msg for non-existing tenant {}", id.tenantId()); + return; + } + } + var state = fromProto(id, stateMsg); + processRestoredState(id, state, partition, callback); } - protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TbCallback callback) { - actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state, callback)); + protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TopicPartitionInfo partition, TbCallback callback) { + partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME); + actorSystemContext.tellWithHighPriority(new CalculatedFieldStateRestoreMsg(id, state, partition, callback)); } @Override public void restore(QueueKey queueKey, Set partitions) { - stateService.update(queueKey, partitions, null); + stateService.update(queueKey, partitions, new QueueStateService.RestoreCallback() { + @Override + public void onAllPartitionsRestored() { + } + + @Override + public void onPartitionRestored(TopicPartitionInfo partition) { + partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME); + actorSystemContext.tellWithHighPriority(new CalculatedFieldStatePartitionRestoreMsg(partition)); + } + }); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java new file mode 100644 index 0000000000..5130979a51 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; + +@Data +@Builder +@RequiredArgsConstructor +public class AlarmCalculatedFieldResult implements CalculatedFieldResult { + + private final TbAlarmResult alarmResult; + + @Override + public TbMsg toTbMsg(EntityId entityId, String cfName, List cfIds) { + TbMsgType msgType; + TbMsgMetaData metaData = new TbMsgMetaData(); + if (alarmResult.isCreated()) { + msgType = TbMsgType.ALARM_CREATED; + metaData.putValue(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isUpdated()) { + msgType = TbMsgType.ALARM_UPDATED; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isSeverityUpdated()) { + msgType = TbMsgType.ALARM_SEVERITY_UPDATED; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + metaData.putValue(DataConstants.IS_SEVERITY_UPDATED_ALARM, Boolean.TRUE.toString()); + } else { + msgType = TbMsgType.ALARM_CLEAR; + metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); + } + if (alarmResult.getConditionRepeats() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_REPEATS, String.valueOf(alarmResult.getConditionRepeats())); + } + if (alarmResult.getConditionDuration() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_DURATION, String.valueOf(alarmResult.getConditionDuration())); + } + + return TbMsg.newMsg() + .type(msgType) + .originator(entityId) + .data(JacksonUtil.toString(alarmResult.getAlarm())) + .metaData(metaData) + .build(); + } + + @Override + public String stringValue() { + return alarmResult != null ? JacksonUtil.toString(alarmResult) : null; + } + + @Override + public boolean isEmpty() { + return alarmResult == null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java index 720a1b48b0..6e9da33d0b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java @@ -17,12 +17,17 @@ package org.thingsboard.server.service.cf; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; public interface CalculatedFieldCache { @@ -36,10 +41,28 @@ public interface CalculatedFieldCache { List getCalculatedFieldCtxsByEntityId(EntityId entityId); + Stream getCalculatedFieldCtxsByType(TenantId tenantId, CalculatedFieldType cfType); + + boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter); + void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); void updateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); void evict(CalculatedFieldId calculatedFieldId); + void handleTenantProfileUpdate(TenantProfileId tenantProfileId); + + EntityId getProfileId(TenantId tenantId, EntityId entityId); + + Set getDynamicEntities(TenantId tenantId, EntityId entityId); + + void updateOwnerEntity(TenantId tenantId, EntityId entityId); + + void addOwnerEntity(TenantId tenantId, EntityId entityId); + + void evictOwnerEntity(EntityId entityId); + + void evictOwner(EntityId owner); + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index b0678875d6..1f264d118e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.cf; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -25,18 +26,28 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; import java.util.List; import java.util.Map; +import java.util.Optional; public interface CalculatedFieldProcessingService { - ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId); + ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId); + + Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId); + + Optional fetchPropagationArgumentFromDb(CalculatedFieldCtx ctx, EntityId entityId); + + List fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId); Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); - void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List cfIds, TbCallback callback); + void processResult(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List cfIds, TbCallback callback); + + ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval); void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List linkedCalculatedFields, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index f48d4737be..48d6f5a6b7 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -15,23 +15,25 @@ */ package org.thingsboard.server.service.cf; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.cf.configuration.OutputType; - -@Data -public final class CalculatedFieldResult { - - private final OutputType type; - private final AttributeScope scope; - private final JsonNode result; - - public boolean isEmpty() { - return result == null || result.isMissingNode() || result.isNull() || - (result.isObject() && result.isEmpty()) || - (result.isArray() && result.isEmpty()) || - (result.isTextual() && result.asText().isEmpty()); +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.List; +import java.util.Objects; + +public interface CalculatedFieldResult { + + TbMsg toTbMsg(EntityId entityId, String cfName, List cfIds); + + String stringValue(); + + boolean isEmpty(); + + default JsonElement toJsonElement() { + return JsonParser.parseString(Objects.requireNonNull(stringValue())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java index abddca594d..d084b05bdf 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java @@ -33,7 +33,7 @@ public interface CalculatedFieldStateService { void persistState(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) throws CalculatedFieldStateException; - void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback); + void deleteState(CalculatedFieldEntityCtxId stateId, TbCallback callback); void restore(QueueKey queueKey, Set partitions); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index fc54b4b0db..3a989aaa60 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -19,34 +19,47 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.util.ConcurrentReferenceHashMap; -import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.DeviceId; 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.TenantProfileId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.profile.TbAssetProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Collections; 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.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; @Service @Slf4j @@ -56,8 +69,12 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final ConcurrentReferenceHashMap calculatedFieldFetchLocks = new ConcurrentReferenceHashMap<>(); private final CalculatedFieldService calculatedFieldService; - private final TbelInvokeService tbelInvokeService; - private final ApiLimitService apiLimitService; + private final TbAssetProfileCache assetProfileCache; + private final TbDeviceProfileCache deviceProfileCache; + private final TbTenantProfileCache tenantProfileCache; + @Lazy + private final ActorSystemContext systemContext; + private final OwnerService ownerService; private final ConcurrentMap calculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap> entityIdCalculatedFields = new ConcurrentHashMap<>(); @@ -65,6 +82,8 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final ConcurrentMap> entityIdCalculatedFieldLinks = new ConcurrentHashMap<>(); private final ConcurrentMap calculatedFieldsCtx = new ConcurrentHashMap<>(); + private final ConcurrentMap> ownerEntities = new ConcurrentHashMap<>(); + @Value("${queue.calculated_fields.init_fetch_pack_size:50000}") @Getter private int initFetchPackSize; @@ -75,19 +94,17 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { cfs.forEach(cf -> { if (cf != null) { calculatedFields.putIfAbsent(cf.getId(), cf); + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + calculatedFieldLinks.put(cf.getId(), new CopyOnWriteArrayList<>(links)); } }); calculatedFields.values().forEach(cf -> { entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf); }); - PageDataIterable cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize); - cfls.forEach(link -> { - calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link); - }); calculatedFieldLinks.values().stream() .flatMap(List::stream) .forEach(link -> - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link) ); } @@ -117,7 +134,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { if (ctx == null) { CalculatedField calculatedField = getCalculatedField(calculatedFieldId); if (calculatedField != null) { - ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService); + ctx = new CalculatedFieldCtx(calculatedField, systemContext); calculatedFieldsCtx.put(calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); } @@ -140,6 +157,38 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { .toList(); } + @Override + public Stream getCalculatedFieldCtxsByType(TenantId tenantId, CalculatedFieldType cfType) { + return calculatedFields.values().stream() + .filter(cf -> cf.getTenantId().equals(tenantId) && cfType.equals(cf.getType())) + .map(cf -> getCalculatedFieldCtx(cf.getId())); + } + + @Override + public boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter) { + List entityCfs = getCalculatedFieldCtxsByEntityId(entityId); + for (CalculatedFieldCtx ctx : entityCfs) { + if (filter.test(ctx)) { + return true; + } + } + + return hasCalculatedFieldsByProfile(tenantId, entityId, filter); + } + + public boolean hasCalculatedFieldsByProfile(TenantId tenantId, EntityId entityId, Predicate filter) { + EntityId profileId = getProfileId(tenantId, entityId); + if (profileId != null) { + List profileCfs = getCalculatedFieldCtxsByEntityId(profileId); + for (CalculatedFieldCtx ctx : profileCfs) { + if (filter.test(ctx)) { + return true; + } + } + } + return false; + } + @Override public void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId) { Lock lock = getFetchLock(calculatedFieldId); @@ -185,19 +234,112 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { log.debug("[{}] evict calculated field links from cache: {}", calculatedFieldId, oldCalculatedField); calculatedFieldsCtx.remove(calculatedFieldId); log.debug("[{}] evict calculated field ctx from cache: {}", calculatedFieldId, oldCalculatedField); - entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.getCalculatedFieldId().equals(calculatedFieldId))); + entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.calculatedFieldId().equals(calculatedFieldId))); log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField); } + @Override + public void handleTenantProfileUpdate(TenantProfileId tenantProfileId) { + calculatedFieldsCtx.values().stream() + .filter(ctx -> { + TenantProfile tenantProfile = tenantProfileCache.get(ctx.getTenantId()); + return tenantProfile != null && tenantProfileId.equals(tenantProfile.getId()); + }) + .forEach(CalculatedFieldCtx::setTenantProfileProperties); + } + + @Override + public EntityId getProfileId(TenantId tenantId, EntityId entityId) { + HasId profile = switch (entityId.getEntityType()) { + case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId); + case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId); + default -> null; + }; + return profile != null ? profile.getId() : null; + } + + @Override + public Set getDynamicEntities(TenantId tenantId, EntityId entityId) { + if (entityId != null && entityId.getEntityType().isOneOf(EntityType.CUSTOMER, EntityType.TENANT)) { + return getOwnedEntities(tenantId, entityId); + } + return Collections.emptySet(); + } + + @Override + public void addOwnerEntity(TenantId tenantId, EntityId entityId) { + EntityId owner = ownerService.getOwner(tenantId, entityId); + getOwnedEntities(tenantId, owner).add(entityId); + } + + @Override + public void updateOwnerEntity(TenantId tenantId, EntityId entityId) { + evictOwnerEntity(entityId); + addOwnerEntity(tenantId, entityId); + } + + @Override + public void evictOwnerEntity(EntityId entityId) { + ownerEntities.values().forEach(entities -> entities.remove(entityId)); + } + + @Override + public void evictOwner(EntityId owner) { + Set removedEntities = ownerEntities.remove(owner); + if (removedEntities != null) { + Set removedCustomers = removedEntities + .stream() + .filter(entityId -> entityId.getEntityType() == EntityType.CUSTOMER) + .collect(Collectors.toSet()); + removedCustomers.forEach(this::evictOwner); + } + } + + private Set getOwnedEntities(TenantId tenantId, EntityId ownerId) { + return ownerEntities.computeIfAbsent(ownerId, owner -> { + Set entities = ConcurrentHashMap.newKeySet(); + entities.addAll(ownerService.getOwnedEntities(tenantId, ownerId)); + return entities; + }); + } + @EventListener(ComponentLifecycleMsg.class) public void onComponentLifecycleEvent(ComponentLifecycleMsg event) { switch (event.getEntityId().getEntityType()) { + case TENANT_PROFILE: + if (event.getEvent() == ComponentLifecycleEvent.UPDATED) { + TenantProfileId tenantProfileId = new TenantProfileId(event.getEntityId().getId()); + handleTenantProfileUpdate(tenantProfileId); + } + break; case TENANT: if (event.getEvent() == ComponentLifecycleEvent.DELETED) { - evictTenantCfs(event.getTenantId()); + TenantId tenantId = event.getTenantId(); + evictTenantCfs(tenantId); + evictOwner(tenantId); + } + break; + case CUSTOMER: + if (event.getEvent() == ComponentLifecycleEvent.CREATED) { + addOwnerEntity(event.getTenantId(), event.getEntityId()); + } else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) { + updateOwnerEntity(event.getTenantId(), event.getEntityId()); + } else if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + evictOwner(event.getEntityId()); + evictOwnerEntity(event.getEntityId()); } break; - case DEVICE, ASSET, DEVICE_PROFILE, ASSET_PROFILE: + case DEVICE, ASSET: + if (event.getEvent() == ComponentLifecycleEvent.CREATED) { + addOwnerEntity(event.getTenantId(), event.getEntityId()); + } else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) { + updateOwnerEntity(event.getTenantId(), event.getEntityId()); + } else if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + evictOwnerEntity(event.getEntityId()); + evictEntityCfs(event.getEntityId()); + } + break; + case DEVICE_PROFILE, ASSET_PROFILE: if (event.getEvent() == ComponentLifecycleEvent.DELETED) { evictEntityCfs(event.getEntityId()); } @@ -207,7 +349,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { addCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId()); } else if (event.getEvent() == ComponentLifecycleEvent.UPDATED) { updateCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId()); - } else if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + } else { evict((CalculatedFieldId) event.getEntityId()); } break; @@ -225,7 +367,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { CalculatedField cf = calculatedFields.remove(cfId); List links = calculatedFieldLinks.remove(cfId); if (links != null) { - links.forEach(link -> removedLinkEntityIds.add(link.getEntityId())); + links.forEach(link -> removedLinkEntityIds.add(link.entityId())); } calculatedFieldsCtx.remove(cfId); if (cf != null) { @@ -244,7 +386,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { removedLinkEntityIds.forEach(entityId -> { entityIdCalculatedFieldLinks.compute(entityId, ((entityId1, links) -> { if (links != null) { - links.removeIf(link -> toRemove.contains(link.getCalculatedFieldId())); + links.removeIf(link -> toRemove.contains(link.calculatedFieldId())); return links.isEmpty() ? null : links; } return null; @@ -263,7 +405,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { cfIds.add(cf.getId()); log.debug("[{}] evict calculated field from cache on entity deletion: {}", cf.getId(), cf); }); - entityIdCalculatedFieldLinks.values().forEach(list -> list.removeIf(link -> cfIds.contains(link.getCalculatedFieldId()))); + entityIdCalculatedFieldLinks.values().forEach(list -> list.removeIf(link -> cfIds.contains(link.calculatedFieldId()))); } entityIdCalculatedFieldLinks.remove(entityId); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index c0cb4d0bab..7082c1d58a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -15,38 +15,26 @@ */ package org.thingsboard.server.service.cf; -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 jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -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.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; @@ -61,123 +49,133 @@ import org.thingsboard.server.queue.util.TbRuleEngineComponent; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.DataConstants.SCOPE; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultTsKvEntry; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @Service @Slf4j -@RequiredArgsConstructor -public class DefaultCalculatedFieldProcessingService implements CalculatedFieldProcessingService { +public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService { - private final AttributesService attributesService; - private final TimeseriesService timeseriesService; - private final TbClusterService clusterService; - private final ApiLimitService apiLimitService; private final PartitionService partitionService; - private ListeningExecutorService calculatedFieldCallbackExecutor; + public DefaultCalculatedFieldProcessingService(AttributesService attributesService, + TimeseriesService timeseriesService, + ApiLimitService apiLimitService, + RelationService relationService, + OwnerService ownerService, + TbClusterService clusterService, + TelemetrySubscriptionService tsSubService, + PartitionService partitionService) { + super(attributesService, timeseriesService, tsSubService, apiLimitService, relationService, ownerService, clusterService); + this.partitionService = partitionService; + } + + @Override + protected String getExecutorNamePrefix() { + return "calculated-field-callback"; + } - @PostConstruct - public void init() { - calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( - Math.max(4, Runtime.getRuntime().availableProcessors()), "calculated-field-callback")); + @Override + public ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId) { + return super.fetchArguments(ctx, entityId, System.currentTimeMillis()); + } + + @Override + public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + return ctx.getCfType() == CalculatedFieldType.GEOFENCING ? + resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())) : + Collections.emptyMap(); } - @PreDestroy - public void stop() { - if (calculatedFieldCallbackExecutor != null) { - calculatedFieldCallbackExecutor.shutdownNow(); + @Override + public Optional fetchPropagationArgumentFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + if (ctx.getCfType() != CalculatedFieldType.PROPAGATION) { + return Optional.empty(); } + return Optional.of((PropagationArgumentEntry) + resolveArgumentValue(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); } @Override - public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - Map> argFutures = new HashMap<>(); - for (var entry : ctx.getArguments().entrySet()) { - var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; - var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); - argFutures.put(entry.getKey(), argValueFuture); + public List fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId) { + try { + if (ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) { + return resolveRelatedEntities(ctx.getTenantId(), entityId, config.getRelation()).get(); + } + return Collections.emptyList(); + } catch (ExecutionException | InterruptedException e) { + Throwable cause = e.getCause(); + throw new RuntimeException("Failed to fetch related entities for entity [" + entityId + "]: " + cause.getMessage(), cause); } - return Futures.whenAllComplete(argFutures.values()).call(() -> { - var result = createStateByType(ctx); - result.updateState(ctx, argFutures.entrySet().stream() - .collect(Collectors.toMap( - Entry::getKey, // Keep the key as is - entry -> { - try { - // Resolve the future to get the value - return entry.getValue().get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); - } - } - ))); - return result; - }, calculatedFieldCallbackExecutor); } @Override public Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments) { Map> argFutures = new HashMap<>(); for (var entry : arguments.entrySet()) { - var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; - var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue()); + if (entry.getValue().hasRelationQuerySource()) { + continue; + } + var argEntityId = resolveEntityId(tenantId, entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(tenantId, argEntityId, entry.getValue(), System.currentTimeMillis()); argFutures.put(entry.getKey(), argValueFuture); } - return argFutures.entrySet().stream() - .collect(Collectors.toMap( - Entry::getKey, // Keep the key as is - entry -> { - try { - // Resolve the future to get the value - return entry.getValue().get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); - } - } - )); + return resolveArgumentFutures(argFutures); } @Override - public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculatedFieldResult, List cfIds, TbCallback callback) { - try { - OutputType type = calculatedFieldResult.getType(); - TbMsgType msgType = OutputType.ATTRIBUTES.equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST; - TbMsgMetaData md = OutputType.ATTRIBUTES.equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY; - TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.getResult().toString()).build(); - clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - callback.onSuccess(); - log.trace("[{}][{}] Pushed message to rule engine: {} ", tenantId, entityId, msg); - } + public ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval) { + return super.fetchMetricDuringInterval(tenantId, entityId, argKey, metric, interval); + } - @Override - public void onFailure(Throwable t) { - callback.onFailure(t); - } - }); - } catch (Exception e) { - log.warn("[{}][{}] Failed to push message to rule engine. CalculatedFieldResult: {}", tenantId, entityId, calculatedFieldResult, e); - callback.onFailure(e); + public void processResult(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List cfIds, TbCallback callback) { + if (result instanceof AlarmCalculatedFieldResult) { + sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfName, cfIds)); + return; } + TelemetryCalculatedFieldResult telemetryResult = result instanceof TelemetryCalculatedFieldResult telemetryRes + ? telemetryRes : ((PropagationCalculatedFieldResult) result).getResult(); + switch (telemetryResult.getOutputStrategy().getType()) { + case IMMEDIATE -> processImmediately(tenantId, entityId, cfName, result, cfIds, callback); + case RULE_CHAIN -> pushMsgToRuleEngine(tenantId, entityId, cfName, result, cfIds, callback); + } + } + + private void processImmediately(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List cfIds, TbCallback callback) { + if (result instanceof TelemetryCalculatedFieldResult telemetryResult) { + saveTelemetryResult(tenantId, entityId, cfName, telemetryResult, cfIds, callback); + return; + } + if (result instanceof PropagationCalculatedFieldResult propagationResult) { + handlePropagationResults(propagationResult, callback, + (entity, res, cb) -> saveTelemetryResult(tenantId, entity, cfName, res, cfIds, cb)); + return; + } + callback.onSuccess(); + } + + private void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List cfIds, TbCallback callback) { + if (result instanceof PropagationCalculatedFieldResult propagationResult) { + handlePropagationResults(propagationResult, callback, + (entity, res, cb) -> sendMsgToRuleEngine(tenantId, entity, cb, res.toTbMsg(entity, cfName, cfIds))); + return; + } + + sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfName, cfIds)); } @Override @@ -233,34 +231,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return builder.build(); } - private ListenableFuture fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { - return switch (argument.getRefEntityKey().getType()) { - case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); - case ATTRIBUTE -> Futures.transform( - attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), - result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultAttributeEntry(argument, System.currentTimeMillis()))), - calculatedFieldCallbackExecutor); - case TS_LATEST -> Futures.transform( - timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()), - result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, System.currentTimeMillis()))), - calculatedFieldCallbackExecutor); - }; - } - - private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) { - long currentTime = System.currentTimeMillis(); - long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); - long startTs = currentTime - timeWindow; - long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - int argumentLimit = argument.getLimit(); - int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit(); - - ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE); - ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); - - return Futures.transform(tsRollingFuture, tsRolling -> tsRolling == null ? new TsRollingArgumentEntry(limit, timeWindow) : ArgumentEntry.createTsRollingArgument(tsRolling, limit, timeWindow), calculatedFieldCallbackExecutor); - } - private static class TbCallbackWrapper implements TbQueueCallback { private final TbCallback callback; @@ -277,6 +247,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP public void onFailure(Throwable t) { callback.onFailure(t); } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index a816eb104c..4c6128f4ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -25,10 +25,11 @@ import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; -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; @@ -36,7 +37,12 @@ import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -45,13 +51,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.profile.TbAssetProfileCache; -import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Collections; -import java.util.EnumSet; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.function.Predicate; import java.util.function.Supplier; @@ -74,14 +76,9 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } }; - private final TbAssetProfileCache assetProfileCache; - private final TbDeviceProfileCache deviceProfileCache; private final CalculatedFieldCache calculatedFieldCache; private final TbClusterService clusterService; - - private static final Set supportedReferencedEntities = EnumSet.of( - EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT - ); + private final RelationService relationService; @Override public void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback callback) { @@ -91,6 +88,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(entries), cf -> cf.linkMatches(entityId, entries), + cf -> cf.dynamicSourceMatches(request.getEntries()), + cf -> cf.relatedEntityMatches(entries), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -108,6 +107,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(entries, scope), cf -> cf.linkMatches(entityId, entries, scope), + cf -> cf.dynamicSourceMatches(request.getEntries(), request.getScope()), + cf -> cf.relatedEntityMatches(entries, scope), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -124,6 +125,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result, scope), cf -> cf.linkMatchesAttrKeys(entityId, result, scope), + cf -> cf.matchesDynamicSourceKeys(result, request.getScope()), + cf -> cf.matchesRelatedEntityKeys(result, scope), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -134,16 +137,21 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result), cf -> cf.linkMatchesTsKeys(entityId, result), + cf -> cf.matchesDynamicSourceKeys(result), + cf -> cf.matchesRelatedEntityKeys(result), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } private void checkEntityAndPushToQueue(TenantId tenantId, EntityId entityId, - Predicate mainEntityFilter, Predicate linkedEntityFilter, + Predicate mainEntityFilter, + Predicate linkedEntityFilter, + Predicate dynamicSourceFilter, + Predicate relatedEntityFilter, Supplier msg, FutureCallback callback) { if (EntityType.TENANT.equals(entityId.getEntityType())) { tenantId = (TenantId) entityId; } - boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter); + boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter, dynamicSourceFilter, relatedEntityFilter); if (send) { ToCalculatedFieldMsg calculatedFieldMsg = msg.get(); clusterService.pushMsgToCalculatedFields(tenantId, entityId, calculatedFieldMsg, wrap(callback)); @@ -154,44 +162,63 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } } - private boolean checkEntityForCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter, Predicate linkedEntityFilter) { - if (!supportedReferencedEntities.contains(entityId.getEntityType())) { + private boolean checkEntityForCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter, Predicate linkedEntityFilter, Predicate dynamicSourceFilter, Predicate relatedEntityFilter) { + if (!CalculatedField.isSupportedRefEntity(entityId)) { return false; } - List entityCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(entityId); - for (CalculatedFieldCtx ctx : entityCfs) { - if (filter.test(ctx)) { - return true; - } - } - EntityId profileId = getProfileId(tenantId, entityId); - if (profileId != null) { - List profileCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(profileId); - for (CalculatedFieldCtx ctx : profileCfs) { - if (filter.test(ctx)) { - return true; - } - } + if (calculatedFieldCache.hasCalculatedFields(tenantId, entityId, filter)) { + return true; } List links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId); for (CalculatedFieldLink link : links) { - CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.calculatedFieldId()); if (ctx != null && linkedEntityFilter.test(ctx)) { return true; } } - return false; - } + for (EntityId dynamicEntity : calculatedFieldCache.getDynamicEntities(tenantId, entityId)) { + if (calculatedFieldCache.getCalculatedFieldCtxsByEntityId(dynamicEntity).stream().anyMatch(dynamicSourceFilter)) { + return true; + } + EntityId dynamicEntityProfileId = calculatedFieldCache.getProfileId(tenantId, dynamicEntity); + if (calculatedFieldCache.getCalculatedFieldCtxsByEntityId(dynamicEntityProfileId).stream().anyMatch(dynamicSourceFilter)) { + return true; + } + } + + boolean hasMatchesEntityAggCfs = calculatedFieldCache.getCalculatedFieldCtxsByType(tenantId, CalculatedFieldType.ENTITY_AGGREGATION).anyMatch(filter); + if (hasMatchesEntityAggCfs) { + return true; + } + + List relatedEntitiesAggregationCfs = calculatedFieldCache.getCalculatedFieldCtxsByType(tenantId, CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) + .filter(relatedEntityFilter) + .toList(); + for (CalculatedFieldCtx cfCtx : relatedEntitiesAggregationCfs) { + if (cfCtx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfig) { + RelationPathLevel relation = aggConfig.getRelation(); + EntitySearchDirection inverseDirection = switch (relation.direction()) { + case FROM -> EntitySearchDirection.TO; + case TO -> EntitySearchDirection.FROM; + }; + RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType()); + List byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation))); + if (!byRelationPathQuery.isEmpty()) { + EntityId cfEntityId = cfCtx.getEntityId(); + for (EntityRelation entityRelation : byRelationPathQuery) { + EntityId relatedId = (inverseDirection == EntitySearchDirection.FROM) ? entityRelation.getTo() : entityRelation.getFrom(); + if (cfEntityId.equals(relatedId) || cfEntityId.equals(calculatedFieldCache.getProfileId(tenantId, relatedId))) { + return true; + } + } + } + } + } - private EntityId getProfileId(TenantId tenantId, EntityId entityId) { - return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); - default -> null; - }; + return false; } private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request, TimeseriesSaveResult result) { @@ -305,6 +332,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS public void onFailure(Throwable t) { callback.onFailure(t); } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java b/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java new file mode 100644 index 0000000000..5cd913531b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceInfoFilter; +import org.thingsboard.server.common.data.EntityType; +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.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.device.DeviceService; + +import java.util.HashSet; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class OwnerService { + + private final DeviceService deviceService; + private final AssetService assetService; + private final CustomerService customerService; + + public EntityId getOwner(TenantId tenantId, EntityId entityId) { + return switch (entityId.getEntityType()) { + case DEVICE -> deviceService.findDeviceById(tenantId, (DeviceId) entityId).getOwnerId(); + case ASSET -> assetService.findAssetById(tenantId, (AssetId) entityId).getOwnerId(); + case CUSTOMER -> tenantId; + default -> throw new UnsupportedOperationException(); + }; + } + + public Set getOwnedEntities(TenantId tenantId, EntityId ownerId) { + Set ownedEntities = new HashSet<>(); + if (EntityType.CUSTOMER.equals(ownerId.getEntityType())) { + PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId((CustomerId) ownerId).build(), pageLink), 1000); + deviceIdInfos.forEach(deviceInfo -> ownedEntities.add(deviceInfo.getId())); + + PageDataIterable assets = new PageDataIterable<>(pageLink -> assetService.findAssetsByTenantIdAndCustomerId(tenantId, (CustomerId) ownerId, pageLink), 1000); + assets.forEach(asset -> ownedEntities.add(asset.getId())); + } else if (EntityType.TENANT.equals(ownerId.getEntityType())) { + PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId((TenantId) ownerId).customerId(new CustomerId(CustomerId.NULL_UUID)).build(), pageLink), 1000); + deviceIdInfos.forEach(deviceInfo -> ownedEntities.add(deviceInfo.getId())); + + PageDataIterable assets = new PageDataIterable<>(pageLink -> assetService.findAssetsByTenantIdAndCustomerId((TenantId) ownerId, new CustomerId(CustomerId.NULL_UUID), pageLink), 1000); + assets.forEach(asset -> ownedEntities.add(asset.getId())); + + PageDataIterable customers = new PageDataIterable<>(pageLink -> customerService.findCustomersByTenantId((TenantId) ownerId, pageLink), 1000); + customers.forEach(customer -> ownedEntities.add(customer.getId())); + } + return ownedEntities; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java new file mode 100644 index 0000000000..2beaf999b6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.List; + +@Data +@Builder +public final class PropagationCalculatedFieldResult implements CalculatedFieldResult { + + private final List entityIds; + private final TelemetryCalculatedFieldResult result; + + @Override + public TbMsg toTbMsg(EntityId entityId, String cfName, List cfIds) { + return result.toTbMsg(entityId, cfName, cfIds); + } + + @Override + public String stringValue() { + return result.stringValue(); + } + + @Override + public boolean isEmpty() { + return CollectionsUtil.isEmpty(entityIds) || result.isEmpty(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java new file mode 100644 index 0000000000..497921d834 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; + +import static org.thingsboard.server.common.data.DataConstants.CF_NAME_METADATA_KEY; +import static org.thingsboard.server.common.data.DataConstants.SCOPE; + +@Data +@Builder +public final class TelemetryCalculatedFieldResult implements CalculatedFieldResult { + + private final OutputType type; + private final AttributeScope scope; + private final OutputStrategy outputStrategy; + private final JsonNode result; + + public static final TelemetryCalculatedFieldResult EMPTY = TelemetryCalculatedFieldResult.builder().result(null).build(); + + @Override + public TbMsg toTbMsg(EntityId entityId, String cfName, List cfIds) { + TbMsgType msgType = switch (type) { + case ATTRIBUTES -> TbMsgType.POST_ATTRIBUTES_REQUEST; + case TIME_SERIES -> TbMsgType.POST_TELEMETRY_REQUEST; + }; + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue(CF_NAME_METADATA_KEY, cfName); + if (OutputType.ATTRIBUTES == type) { + metaData.putValue(SCOPE, scope.name()); + } + return TbMsg.newMsg() + .type(msgType) + .originator(entityId) + .previousCalculatedFieldIds(cfIds) + .data(stringValue()) + .metaData(metaData) + .build(); + } + + @Override + public String stringValue() { + return result == null ? null : result.toString(); + } + + @Override + public boolean isEmpty() { + return result == null || result.isMissingNode() || result.isNull() || + (result.isObject() && result.isEmpty()) || + (result.isArray() && result.isEmpty()) || + (result.isTextual() && result.asText().isEmpty()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index 5fe1fee31e..492f004c89 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -18,11 +18,19 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; import java.util.List; +import java.util.Map; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -31,7 +39,11 @@ import java.util.List; ) @JsonSubTypes({ @JsonSubTypes.Type(value = SingleValueArgumentEntry.class, name = "SINGLE_VALUE"), - @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING") + @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING"), + @JsonSubTypes.Type(value = GeofencingArgumentEntry.class, name = "GEOFENCING"), + @JsonSubTypes.Type(value = PropagationArgumentEntry.class, name = "PROPAGATION"), + @JsonSubTypes.Type(value = RelatedEntitiesArgumentEntry.class, name = "RELATED_ENTITIES"), + @JsonSubTypes.Type(value = EntityAggregationArgumentEntry.class, name = "ENTITY_AGGREGATION") }) public interface ArgumentEntry { @@ -40,10 +52,14 @@ public interface ArgumentEntry { Object getValue(); - boolean updateEntry(ArgumentEntry entry); + boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx); boolean isEmpty(); + default JsonNode jsonValue() { + return JacksonUtil.valueToTree(toTbelCfArg()); + } + TbelCfArg toTbelCfArg(); boolean isForceResetPrevious(); @@ -54,8 +70,24 @@ public interface ArgumentEntry { return new SingleValueArgumentEntry(kvEntry); } + static ArgumentEntry createSingleValueArgument(EntityId entityId, ArgumentEntry argumentEntry) { + return new SingleValueArgumentEntry(entityId, argumentEntry); + } + static ArgumentEntry createTsRollingArgument(List kvEntries, int limit, long timeWindow) { return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); } + static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap) { + return new GeofencingArgumentEntry(entityIdkvEntryMap); + } + + static ArgumentEntry createPropagationArgument(List entityIds) { + return new PropagationArgumentEntry(entityIds); + } + + static ArgumentEntry createAggArgument(Map entityIdkvEntryMap) { + return new RelatedEntitiesArgumentEntry(entityIdkvEntryMap, false); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java index 22cec4252e..16a7a20216 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.service.cf.ctx.state; public enum ArgumentEntryType { - SINGLE_VALUE, TS_ROLLING + SINGLE_VALUE, TS_ROLLING, GEOFENCING, PROPAGATION, RELATED_ENTITIES, ENTITY_AGGREGATION } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 5b9ed588df..8bb78c29dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -15,44 +15,64 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import lombok.AllArgsConstructor; -import lombok.Data; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; import org.thingsboard.server.utils.CalculatedFieldUtils; +import java.io.Closeable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; -import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; +@Getter +public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { -@Data -@AllArgsConstructor -public abstract class BaseCalculatedFieldState implements CalculatedFieldState { - - static final long DEFAULT_LAST_UPDATE_TS = -1L; + public static final long DEFAULT_LAST_UPDATE_TS = -1L; + protected final EntityId entityId; + protected CalculatedFieldCtx ctx; + protected TbActorRef actorCtx; protected List requiredArguments; - protected Map arguments; + + protected Map arguments = new HashMap<>(); protected boolean sizeExceedsLimit; + protected ReadinessStatus readinessStatus; - public BaseCalculatedFieldState(List requiredArguments) { - this.requiredArguments = requiredArguments; - this.arguments = new HashMap<>(); + @Setter + private TopicPartitionInfo partition; + + public BaseCalculatedFieldState(EntityId entityId) { + this.entityId = entityId; } - public BaseCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false); + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + this.ctx = ctx; + this.actorCtx = actorCtx; + this.requiredArguments = ctx.getArgNames(); + this.readinessStatus = checkReadiness(); } @Override - public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { - if (arguments == null) { - arguments = new HashMap<>(); - } + public void init(boolean restored) { + } - boolean stateUpdated = false; + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + Map updatedArguments = null; for (Map.Entry entry : argumentValues.entrySet()) { String key = entry.getKey(); @@ -64,26 +84,48 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { boolean entryUpdated; if (existingEntry == null || newEntry.isForceResetPrevious()) { - validateNewEntry(newEntry); - arguments.put(key, newEntry); + validateNewEntry(key, newEntry); + if (existingEntry instanceof RelatedEntitiesArgumentEntry || + existingEntry instanceof EntityAggregationArgumentEntry) { + updateEntry(existingEntry, newEntry, ctx); + } else { + arguments.put(key, newEntry); + } entryUpdated = true; } else { - entryUpdated = existingEntry.updateEntry(newEntry); + entryUpdated = updateEntry(existingEntry, newEntry, ctx); } if (entryUpdated) { - stateUpdated = true; + if (updatedArguments == null) { + updatedArguments = new HashMap<>(argumentValues.size()); + } + updatedArguments.put(key, newEntry); } } - return stateUpdated; + if (updatedArguments == null) { + return Collections.emptyMap(); + } + readinessStatus = checkReadiness(); + return updatedArguments; + } + + protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry, CalculatedFieldCtx ctx) { + return existingEntry.updateEntry(newEntry, ctx); + } + + @Override + public void reset() { // must reset everything dependent on arguments + requiredArguments = null; + arguments.clear(); + sizeExceedsLimit = false; } @Override public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + return readinessStatus.ready(); } @Override @@ -95,19 +137,26 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } @Override - public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { - if (entry instanceof TsRollingArgumentEntry) { - return; + public void close() { + } + + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + } + + protected ObjectNode toResultNode(ObjectNode valuesNode) { + if (ctx.getOutput().getType() == OutputType.ATTRIBUTES || !ctx.isUseLatestTs()) { + return valuesNode; } - if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { - throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); - } + long latestTs = getLatestTimestamp(); + if (latestTs == DEFAULT_LAST_UPDATE_TS) { + return valuesNode; } + ObjectNode resultNode = JacksonUtil.newObjectNode(); + resultNode.put("ts", latestTs); + resultNode.set("values", valuesNode); + return resultNode; } - protected abstract void validateNewEntry(ArgumentEntry newEntry); - public long getLatestTimestamp() { long latestTs = DEFAULT_LAST_UPDATE_TS; @@ -125,12 +174,37 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } else if (!single.isDefaultValue()) { latestTs = Math.max(latestTs, single.getTs()); } - } else if (entry instanceof TsRollingArgumentEntry rolling) { - latestTs = Math.max(latestTs, rolling.getLatestTs()); + } else if (entry instanceof HasLatestTs hasLatestTsEntry) { + latestTs = Math.max(latestTs, hasLatestTsEntry.getLatestTs()); } } return latestTs; } + protected ReadinessStatus checkReadiness() { + if (arguments == null) { + return ReadinessStatus.from(requiredArguments); + } + List emptyArguments = null; + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + if (emptyArguments == null) { + emptyArguments = new ArrayList<>(); + } + emptyArguments.add(requiredArgumentKey); + } + } + return ReadinessStatus.from(emptyArguments); + } + + @Override + public JsonNode getArgumentsJson() { + return JacksonUtil.valueToTree(arguments.entrySet().stream() + .filter(entry -> !entry.getValue().isEmpty()) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue())) + ); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 373e2f588f..1a0b609c5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -15,130 +15,397 @@ */ package org.thingsboard.server.service.cf.ctx.state; +import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import net.objecthunter.exp4j.Expression; -import net.objecthunter.exp4j.ExpressionBuilder; import org.mvel2.MVEL; +import org.thingsboard.common.util.ExpressionUtils; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldReevaluateMsg; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.BasicKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.util.ProtoUtils; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.util.TimeUtils; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.geofencing.ScheduledRefreshSupported; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; +import java.io.Closeable; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; -import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; @Data -public class CalculatedFieldCtx { +@Slf4j +public class CalculatedFieldCtx implements Closeable { + + public static final long DISABLED_INTERVAL_VALUE = -1L; private CalculatedField calculatedField; private CalculatedFieldId cfId; + private String cfName; private TenantId tenantId; private EntityId entityId; private CalculatedFieldType cfType; private final Map arguments; private final Map> mainEntityArguments; private final Map>> linkedEntityArguments; + private final Map> dynamicEntityArguments; + private final Map> relatedEntityArguments; private final List argNames; private Output output; private String expression; private boolean useLatestTs; + + private long lastReevaluationTs; + + private ActorSystemContext systemContext; private TbelInvokeService tbelInvokeService; - private CalculatedFieldScriptEngine calculatedFieldScriptEngine; - private ThreadLocal customExpression; + private RelationService relationService; + private AlarmSubscriptionService alarmService; + private CalculatedFieldProcessingService cfProcessingService; + + private Map tbelExpressions; + private Map> simpleExpressions; private boolean initialized; - private long maxDataPointsPerRollingArg; private long maxStateSize; private long maxSingleValueArgumentSize; + private long intermediateAggregationIntervalMillis; + + private boolean cfHasRelationPathQuerySource; + private List mainEntityGeofencingArgumentNames; + private List linkedEntityAndCurrentOwnerGeofencingArgumentNames; + private List relatedEntityArgumentNames; + + private long scheduledUpdateIntervalMillis; + private long cfCheckReevaluationIntervalMillis; + private long alarmReevaluationIntervalMillis; + private long maxRelatedEntitiesPerCfArgument; + private long minScheduledUpdateIntervalMillis; + private long minDeduplicationIntervalMillis; + + private Argument propagationArgument; + private boolean applyExpressionForResolvedArguments; - public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) { + public CalculatedFieldCtx(CalculatedField calculatedField, + ActorSystemContext systemContext) { this.calculatedField = calculatedField; this.cfId = calculatedField.getId(); + this.cfName = calculatedField.getName(); this.tenantId = calculatedField.getTenantId(); this.entityId = calculatedField.getEntityId(); this.cfType = calculatedField.getType(); - CalculatedFieldConfiguration configuration = calculatedField.getConfiguration(); - this.arguments = configuration.getArguments(); + this.arguments = new HashMap<>(); this.mainEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>(); - for (Map.Entry entry : arguments.entrySet()) { - var refId = entry.getValue().getRefEntityId(); - var refKey = entry.getValue().getRefEntityKey(); - if (refId == null || refId.equals(calculatedField.getEntityId())) { - mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); - } else { - linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()) - .compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + this.dynamicEntityArguments = new HashMap<>(); + this.relatedEntityArguments = new HashMap<>(); + this.argNames = new ArrayList<>(); + this.mainEntityGeofencingArgumentNames = new ArrayList<>(); + this.linkedEntityAndCurrentOwnerGeofencingArgumentNames = new ArrayList<>(); + this.relatedEntityArgumentNames = new ArrayList<>(); + this.output = calculatedField.getConfiguration().getOutput(); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + this.arguments.putAll(argBasedConfig.getArguments()); + for (Map.Entry entry : arguments.entrySet()) { + var refId = entry.getValue().getRefEntityId(); + var refKey = entry.getValue().getRefEntityKey(); + if (refId == null) { + if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cfType)) { + relatedEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + cfHasRelationPathQuerySource = true; + continue; + } + if (entry.getValue().hasRelationQuerySource()) { + cfHasRelationPathQuerySource = true; + continue; + } + if (entry.getValue().hasOwnerSource()) { + dynamicEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } else { + mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } + } else if (refId.equals(calculatedField.getEntityId())) { + mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } else { + linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()) + .compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } + } + this.argNames.addAll(arguments.keySet()); + this.relatedEntityArgumentNames = relatedEntityArguments.values().stream() + .flatMap(Set::stream) + .collect(Collectors.toList()); + if (argBasedConfig instanceof ExpressionBasedCalculatedFieldConfiguration expressionBasedConfig) { + this.expression = expressionBasedConfig.getExpression(); + } + if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration geofencingConfig) { + geofencingConfig.getZoneGroups().forEach((zoneGroupName, config) -> { + if (config.isCfEntitySource(entityId)) { + mainEntityGeofencingArgumentNames.add(zoneGroupName); + return; + } + if (config.isLinkedCfEntitySource(entityId) || config.hasCurrentOwnerSource()) { + linkedEntityAndCurrentOwnerGeofencingArgumentNames.add(zoneGroupName); + } + }); } + if (calculatedField.getConfiguration() instanceof PropagationCalculatedFieldConfiguration propagationConfig) { + propagationArgument = propagationConfig.toPropagationArgument(); + applyExpressionForResolvedArguments = propagationConfig.isApplyExpressionToResolvedArguments(); + cfHasRelationPathQuerySource = true; + } + } + if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) { + this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : DISABLED_INTERVAL_VALUE; + } + if (calculatedField.getConfiguration() instanceof HasUseLatestTsConfig hasUseLatestTsConfig) { + this.useLatestTs = hasUseLatestTsConfig.isUseLatestTs(); } - this.argNames = new ArrayList<>(arguments.keySet()); - this.output = configuration.getOutput(); - this.expression = configuration.getExpression(); - this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isUseLatestTs(); - this.tbelInvokeService = tbelInvokeService; + this.systemContext = systemContext; + this.tbelInvokeService = systemContext.getTbelInvokeService(); + this.relationService = systemContext.getRelationService(); + this.alarmService = systemContext.getAlarmService(); + this.cfProcessingService = systemContext.getCalculatedFieldProcessingService(); - this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + setTenantProfileProperties(); + } + + public boolean requiresScheduledReevaluation() { + long now = System.currentTimeMillis(); + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) { + Watermark watermark = entityAggregationConfig.getWatermark(); + if (watermark != null && watermark.getDuration() > 0) { + return true; + } + if (lastReevaluationTs == 0) { + lastReevaluationTs = now; + return true; + } + if (entityAggregationConfig.isProduceIntermediateResult()) { + if (now - lastReevaluationTs >= intermediateAggregationIntervalMillis) { + lastReevaluationTs = now; + return true; + } + } + ZonedDateTime lastReevaluationTime = TimeUtils.toZonedDateTime(lastReevaluationTs, entityAggregationConfig.getInterval().getZoneId()); + long previousIntervalEndTs = entityAggregationConfig.getInterval().getDateTimeIntervalEndTs(lastReevaluationTime); + if (now >= previousIntervalEndTs) { + lastReevaluationTs = now; + return true; + } + } + boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); + if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { + if (requiresScheduledReevaluation) { + if (now - lastReevaluationTs >= alarmReevaluationIntervalMillis) { + lastReevaluationTs = now; + return true; + } + return false; + } + } + return requiresScheduledReevaluation; } public void init() { - if (CalculatedFieldType.SCRIPT.equals(cfType)) { - try { - this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); + switch (cfType) { + case SCRIPT -> { + initTbelExpression(expression); initialized = true; - } catch (Exception e) { - initialized = false; - throw new RuntimeException("Failed to initialize CF context. The script expression is invalid. Please check for syntax errors or unsupported functions.", e); } - } else { - if (isValidExpression(expression)) { - this.customExpression = ThreadLocal.withInitial(() -> - new ExpressionBuilder(expression) - .functions(userDefinedFunctions) - .implicitMultiplication(true) - .variables(this.arguments.keySet()) - .build() - ); + case GEOFENCING -> initialized = true; + case SIMPLE -> { + initSimpleExpression(expression); + initialized = true; + } + case ALARM -> { + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + configuration.getAllRules().map(rule -> rule.getValue().getCondition().getExpression()) + .forEach(expression -> { + if (expression instanceof TbelAlarmConditionExpression tbelExpression) { + initTbelExpression(tbelExpression.getExpression()); + } + }); + initialized = true; + } + case PROPAGATION -> { + if (applyExpressionForResolvedArguments) { + initTbelExpression(expression); + } initialized = true; + } + case RELATED_ENTITIES_AGGREGATION -> { + RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) calculatedField.getConfiguration(); + configuration.getMetrics().forEach((key, metric) -> { + if (metric.getInput() instanceof AggFunctionInput functionInput) { + initTbelExpression(functionInput.getFunction()); + } + String filter = metric.getFilter(); + if (filter != null && !filter.isEmpty()) { + initTbelExpression(filter); + } + }); + initialized = true; + } + case ENTITY_AGGREGATION -> initialized = true; + } + } + + public void setTenantProfileProperties() { + TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenantId); + if (tenantProfile == null) { + log.warn("[{}][{}][{}] Tenant Profile not found for tenant: {}. CF limits and thresholds will not be updated.", tenantId, entityId, cfId, tenantId); + return; + } + tenantProfile.getProfileConfiguration().ifPresent(config -> { + this.maxStateSize = config.getMaxStateSizeInKBytes() * 1024L; + this.maxSingleValueArgumentSize = config.getMaxSingleValueArgumentSizeInKBytes() * 1024L; + this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getIntermediateAggregationIntervalInSecForCF()); + this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getCfReevaluationCheckInterval()); + this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getAlarmsReevaluationInterval()); + this.maxRelatedEntitiesPerCfArgument = config.getMaxRelatedEntitiesToReturnPerCfArgument(); + this.minScheduledUpdateIntervalMillis = TimeUnit.SECONDS.toMillis(config.getMinAllowedScheduledUpdateIntervalInSecForCF()); + this.minDeduplicationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getMinAllowedDeduplicationIntervalInSecForCF()); + }); + } + + public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { + for (Map.Entry entry : state.getArguments().entrySet()) { + try { + BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue(); + double value = switch (kvEntry.getDataType()) { + case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow(); + case DOUBLE -> kvEntry.getDoubleValue().orElseThrow(); + case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow(); + case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString()); + }; + expression.setVariable(entry.getKey(), value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number."); + } + } + return expression.evaluate(); + } + + public ListenableFuture evaluateTbelExpression(String expression, CalculatedFieldState state) { + return evaluateTbelExpression(tbelExpressions.get(expression), state.getArguments(), state.getLatestTimestamp()); + } + + public ListenableFuture evaluateTbelExpression(CalculatedFieldScriptEngine expression, CalculatedFieldState state) { + return evaluateTbelExpression(expression, state.getArguments(), state.getLatestTimestamp()); + } + + public ListenableFuture evaluateTbelExpression(String expression, Map entries, long latestTimestamp) { + return evaluateTbelExpression(tbelExpressions.get(expression), entries, latestTimestamp); + } + + public ListenableFuture evaluateTbelExpression(CalculatedFieldScriptEngine expression, Map entries, long latestTimestamp) { + Map arguments = new LinkedHashMap<>(); + List args = new ArrayList<>(argNames.size() + 1); + args.add(new Object()); // first element is a ctx, but we will set it later; + for (String argName : argNames) { + var arg = toTbelArgument(argName, entries); + arguments.put(argName, arg); + if (arg instanceof TbelCfSingleValueArg svArg) { + args.add(svArg.getValue()); } else { - initialized = false; - throw new RuntimeException("Failed to initialize CF context. The expression has invalid syntax or unknown variables. Ensure all mathematical operators are correct."); + args.add(arg); } } + args.set(0, new TbelCfCtx(arguments, latestTimestamp)); + + return expression.executeScriptAsync(args.toArray()); + } + + public ScheduledFuture scheduleReevaluation(long delayMs, TbActorRef actorCtx) { + log.debug("[{}] Scheduling CF reevaluation in {} ms", cfId, delayMs); + return systemContext.scheduleMsgWithDelay(actorCtx, new CalculatedFieldReevaluateMsg(tenantId, this), delayMs); + } + + private TbelCfArg toTbelArgument(String key, Map arguments) { + return arguments.get(key).toTbelCfArg(); } - public void stop() { - if (calculatedFieldScriptEngine != null) { - calculatedFieldScriptEngine.destroy(); + private void initTbelExpression(String expression) { + if (tbelExpressions == null) { + tbelExpressions = new HashMap<>(); + } else if (tbelExpressions.containsKey(expression)) { + return; } - if (customExpression != null) { - customExpression.remove(); + try { + CalculatedFieldScriptEngine engine = initEngine(tenantId, expression, tbelInvokeService); + tbelExpressions.put(expression, engine); + } catch (Exception e) { + initialized = false; + throw new RuntimeException("Failed to initialize CF context. The script expression is invalid. Please check for syntax errors or unsupported functions.", e); + } + } + + private void initSimpleExpression(String expression) { + if (simpleExpressions == null) { + simpleExpressions = new HashMap<>(); + } else if (simpleExpressions.containsKey(expression)) { + return; + } + if (isValidExpression(expression)) { + ThreadLocal compiledExpression = ThreadLocal.withInitial(() -> + ExpressionUtils.createExpression(expression, this.arguments.keySet()) + ); + simpleExpressions.put(expression, compiledExpression); + } else { + initialized = false; + throw new RuntimeException("Failed to initialize CF context. The expression has invalid syntax or unknown variables. Ensure all mathematical operators are correct."); } } @@ -185,6 +452,14 @@ public class CalculatedFieldCtx { return map != null && matchesTimeSeries(map, values); } + public boolean dynamicSourceMatches(List values) { + return matchesTimeSeries(dynamicEntityArguments, values); + } + + public boolean dynamicSourceMatches(List values, AttributeScope scope) { + return matchesAttributes(dynamicEntityArguments, values, scope); + } + private boolean matchesAttributes(Map> argMap, List values, AttributeScope scope) { if (argMap.isEmpty() || values.isEmpty()) { return false; @@ -228,6 +503,14 @@ public class CalculatedFieldCtx { return matchesTimeSeriesKeys(mainEntityArguments, keys); } + public boolean matchesDynamicSourceKeys(List keys, AttributeScope scope) { + return matchesAttributesKeys(dynamicEntityArguments, keys, scope); + } + + public boolean matchesDynamicSourceKeys(List keys) { + return matchesTimeSeriesKeys(dynamicEntityArguments, keys); + } + private boolean matchesAttributesKeys(Map> argMap, List keys, AttributeScope scope) { if (argMap.isEmpty() || keys.isEmpty()) { return false; @@ -274,6 +557,60 @@ public class CalculatedFieldCtx { return map != null && matchesTimeSeriesKeys(map, keys); } + public boolean relatedEntityMatches(List values) { + return matchesTimeSeries(relatedEntityArguments, values); + } + + public boolean relatedEntityMatches(List values, AttributeScope scope) { + return matchesAttributes(relatedEntityArguments, values, scope); + } + + public boolean matchesRelatedEntityKeys(List keys, AttributeScope scope) { + return matchesAttributesKeys(relatedEntityArguments, keys, scope); + } + + public boolean matchesRelatedEntityKeys(List keys) { + return matchesTimeSeriesKeys(relatedEntityArguments, keys); + } + + public boolean relatedEntityMatches(CalculatedFieldTelemetryMsgProto proto) { + if (!proto.getTsDataList().isEmpty()) { + List updatedTelemetry = proto.getTsDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return relatedEntityMatches(updatedTelemetry); + } else if (!proto.getAttrDataList().isEmpty()) { + AttributeScope scope = AttributeScope.valueOf(proto.getScope().name()); + List updatedTelemetry = proto.getAttrDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return relatedEntityMatches(updatedTelemetry, scope); + } else if (!proto.getRemovedTsKeysList().isEmpty()) { + return matchesRelatedEntityKeys(proto.getRemovedTsKeysList()); + } else { + return matchesRelatedEntityKeys(proto.getRemovedAttrKeysList(), AttributeScope.valueOf(proto.getScope().name())); + } + } + + public boolean dynamicSourceMatches(CalculatedFieldTelemetryMsgProto proto) { + if (!proto.getTsDataList().isEmpty()) { + List updatedTelemetry = proto.getTsDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return dynamicSourceMatches(updatedTelemetry); + } else if (!proto.getAttrDataList().isEmpty()) { + AttributeScope scope = AttributeScope.valueOf(proto.getScope().name()); + List updatedTelemetry = proto.getAttrDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return dynamicSourceMatches(updatedTelemetry, scope); + } else if (!proto.getRemovedTsKeysList().isEmpty()) { + return matchesDynamicSourceKeys(proto.getRemovedTsKeysList()); + } else { + return matchesDynamicSourceKeys(proto.getRemovedAttrKeysList(), AttributeScope.valueOf(proto.getScope().name())); + } + } + public boolean linkMatches(EntityId entityId, CalculatedFieldTelemetryMsgProto proto) { if (!proto.getTsDataList().isEmpty()) { List updatedTelemetry = proto.getTsDataList().stream() @@ -293,24 +630,195 @@ public class CalculatedFieldCtx { } } + public Map> getLinkedAndDynamicArgs(EntityId entityId) { + var argNames = new HashMap>(); + var linkedArgNames = linkedEntityArguments.get(entityId); + if (linkedArgNames != null && !linkedArgNames.isEmpty()) { + argNames.putAll(linkedArgNames); + } + if (dynamicEntityArguments != null && !dynamicEntityArguments.isEmpty()) { + argNames.putAll(dynamicEntityArguments); + } + return argNames; + } + public CalculatedFieldEntityCtxId toCalculatedFieldEntityCtxId() { return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId); } - public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { - boolean expressionChanged = !expression.equals(other.expression); - boolean outputChanged = !output.equals(other.output); - return expressionChanged || outputChanged; + public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation + if (output != null) { + var thisOutputStrategy = output.getStrategy(); + var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy(); + if (thisOutputStrategy.hasRefreshContextOnlyChanges(otherOutputStrategy)) { + return true; + } + } + + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + if (thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult()) { + return true; + } + } + + return false; + } + + public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly + if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { + return true; + } + if (output != null && output.hasContextOnlyChanges(other.output)) { + return true; + } + if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig + && thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) { + return true; + } + if (cfType == CalculatedFieldType.ALARM) { + if (!calculatedField.getName().equals(other.getCalculatedField().getName())) { + return true; + } + + var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); + if (!thisConfig.rulesEqual(otherConfig, AlarmRule::equals)) { + // if the rules have any changes not tracked by hasStateChanges + return true; + } + if (!thisConfig.propagationSettingsEqual(otherConfig)) { + return true; + } + } + if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) { + return true; + } + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig + && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() + || !thisConfig.getMetrics().equals(otherConfig.getMetrics()) + || thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs())) { + return true; + } + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics()); + boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark()); + if (metricsChanged || watermarkChanged) { + return true; + } + } + return false; } public boolean hasStateChanges(CalculatedFieldCtx other) { - boolean typeChanged = !cfType.equals(other.cfType); - boolean argumentsChanged = !arguments.equals(other.arguments); - return typeChanged || argumentsChanged; + if (!arguments.equals(other.arguments)) { + return true; + } + if (cfType == CalculatedFieldType.ALARM) { + var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); + if (!thisConfig.rulesEqual(otherConfig, (thisRule, otherRule) -> { + return thisRule.getCondition().getType() == otherRule.getCondition().getType(); + })) { + // reinitializing only if the rule list changed, or if a condition type changed for any rule + return true; + } + } + if (cfType == CalculatedFieldType.PROPAGATION && !propagationArgument.equals(other.propagationArgument)) { + return true; + } + if (hasGeofencingZoneGroupConfigurationChanges(other)) { + return true; + } + if (hasRelatedEntitiesAggregationConfigurationChanges(other)) { + return true; + } + if (hasEntityAggregationConfigurationChanges(other)) { + return true; + } + return false; + } + + private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups()); + } + return false; + } + + private boolean hasRelatedEntitiesAggregationConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getRelation().equals(otherConfig.getRelation()); + } + return false; + } + + private boolean hasEntityAggregationConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getInterval().equals(otherConfig.getInterval()); + } + return false; + } + + private boolean isScheduledUpdateDisabled() { + return scheduledUpdateIntervalMillis == DISABLED_INTERVAL_VALUE; + } + + public boolean hasRelatedEntities() { + return cfHasRelationPathQuerySource; + } + + public boolean shouldFetchRelatedEntities(CalculatedFieldState state) { + if (!cfHasRelationPathQuerySource) { + return false; + } + if (isScheduledUpdateDisabled()) { + return false; + } + if (!(state instanceof ScheduledRefreshSupported scheduledRefreshSupported)) { + return false; + } + if (scheduledRefreshSupported.getLastScheduledRefreshTs() == DEFAULT_LAST_UPDATE_TS) { + return true; + } + return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - Math.max(scheduledUpdateIntervalMillis, minScheduledUpdateIntervalMillis); + } + + @Override + public void close() { + try { + if (tbelExpressions != null) { + tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy); + } + if (simpleExpressions != null) { + simpleExpressions.values().forEach(ThreadLocal::remove); + } + } catch (Exception e) { + log.warn("Failed to stop {}", this, e); + } } public String getSizeExceedsLimitMessage() { return "State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } + public boolean hasCurrentOwnerSourceArguments() { + return !dynamicEntityArguments.isEmpty(); + } + + @Override + public String toString() { + return "CalculatedFieldCtx{" + + "cfId=" + cfId + + ", cfType=" + cfType + + ", entityId=" + entityId + + '}'; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 8667d1e585..7be39c0136 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -17,42 +17,69 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; +import java.io.Closeable; import java.util.List; import java.util.Map; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type" -) +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; +import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), + @Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), + @Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), + @Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"), + @Type(value = AlarmCalculatedFieldState.class, name = "ALARM"), + @Type(value = PropagationCalculatedFieldState.class, name = "PROPAGATION"), + @Type(value = RelatedEntitiesAggregationCalculatedFieldState.class, name = "RELATED_ENTITIES_AGGREGATION"), + @Type(value = EntityAggregationCalculatedFieldState.class, name = "ENTITY_AGGREGATION") }) -public interface CalculatedFieldState { +public interface CalculatedFieldState extends Closeable { @JsonIgnore CalculatedFieldType getType(); + EntityId getEntityId(); + Map getArguments(); + JsonNode getArgumentsJson(); + long getLatestTimestamp(); - void setRequiredArguments(List requiredArguments); + void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx); + + void init(boolean restored); - boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); + Map update(Map arguments, CalculatedFieldCtx ctx); - ListenableFuture performCalculation(CalculatedFieldCtx ctx); + void reset(); + + ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception; @JsonIgnore boolean isReady(); + ReadinessStatus getReadinessStatus(); + boolean isSizeExceedsLimit(); @JsonIgnore @@ -60,8 +87,50 @@ public interface CalculatedFieldState { return !isSizeExceedsLimit(); } + TopicPartitionInfo getPartition(); + + void setPartition(TopicPartitionInfo partition); + void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); - void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx); + default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { + if (entry instanceof TsRollingArgumentEntry || entry instanceof GeofencingArgumentEntry) { + return; + } + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { + throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); + } + } + } + + record ReadinessStatus(boolean ready, String errorMsg) { + + private static final String MISSING_REQUIRED_ARGUMENTS_ERROR = "Required arguments are missing: "; + private static final String MISSING_PROPAGATION_TARGETS_ERROR = "No entities found via 'Propagation path to related entities'. " + + "Verify the configured relation type and direction."; + private static final String MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR = MISSING_PROPAGATION_TARGETS_ERROR + " Missing arguments to propagate: "; + public static final String MISSING_AGGREGATION_ENTITIES_ERROR = "No entities found via 'Aggregation path to related entities'. " + + "Verify the configured relation type and direction."; + public static final ReadinessStatus READY = new ReadinessStatus(true, null); + + public static ReadinessStatus notReady(String errorMsg) { + return new ReadinessStatus(false, errorMsg); + } + + public static ReadinessStatus from(List emptyOrMissingArguments) { + if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { + return ReadinessStatus.READY; + } + boolean propagationCtxIsEmpty = emptyOrMissingArguments.remove(PROPAGATION_CONFIG_ARGUMENT); + if (!propagationCtxIsEmpty) { + return notReady(MISSING_REQUIRED_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments)); + } + if (emptyOrMissingArguments.isEmpty()) { + return notReady(MISSING_PROPAGATION_TARGETS_ERROR); + } + return notReady(MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments)); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java new file mode 100644 index 0000000000..7d5bc47cc0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +public interface HasEntityLimit { + + default void checkEntityLimit(int currentEntitiesCount, CalculatedFieldCtx ctx) { + if (currentEntitiesCount >= ctx.getMaxRelatedEntitiesPerCfArgument()) { + throw new IllegalArgumentException( + "Exceeded the maximum allowed related entities per argument '" + + ctx.getMaxRelatedEntitiesPerCfArgument() + "'. Increase the limit in the tenant profile configuration." + ); + } + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java similarity index 82% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java rename to application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java index 442abc9a7d..a0f3b39689 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasLatestTs.java @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.profile; +package org.thingsboard.server.service.cf.ctx.state; -enum AlarmStateUpdateResult { +public interface HasLatestTs { - NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; + long getLatestTs(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index a3bab92487..93ade0238e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -99,9 +99,9 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta try { if (msg.getValue() != null) { - processRestoredState(msg.getValue(), callback); + processRestoredState(msg.getValue(), consumerKey.partition(), callback); } else { - processRestoredState(getStateId(msg.getHeaders()), null, callback); + processRestoredState(getStateId(msg.getHeaders()), null, consumerKey.partition(), callback); } } catch (Throwable t) { callback.onFailure(t); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java index 1ef386430b..5f49907816 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java @@ -69,7 +69,7 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS log.error("Failed to parse CalculatedFieldStateProto for key {}", key, e); return; } - processRestoredState(stateMsg, new TbCallback() { + processRestoredState(stateMsg, null, new TbCallback() { @Override public void onSuccess() {} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 24f3acd4a7..aa97d30325 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -15,68 +15,55 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import com.fasterxml.jackson.databind.JsonNode; 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.NoArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -@Data @Slf4j -@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { - public ScriptCalculatedFieldState(List requiredArguments) { - super(requiredArguments); - } + protected CalculatedFieldScriptEngine tbelExpression; - @Override - public CalculatedFieldType getType() { - return CalculatedFieldType.SCRIPT; + public ScriptCalculatedFieldState(EntityId entityId) { + super(entityId); } @Override - protected void validateNewEntry(ArgumentEntry newEntry) { + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { - Map arguments = new LinkedHashMap<>(); - List args = new ArrayList<>(ctx.getArgNames().size() + 1); - args.add(new Object()); // first element is a ctx, but we will set it later; - for (String argName : ctx.getArgNames()) { - var arg = toTbelArgument(argName); - arguments.put(argName, arg); - if (arg instanceof TbelCfSingleValueArg svArg) { - args.add(svArg.getValue()); - } else { - args.add(arg); - } - } - args.set(0, new TbelCfCtx(arguments, getLatestTimestamp())); - ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + ListenableFuture resultFuture = ctx.evaluateTbelExpression(tbelExpression, this); Output output = ctx.getOutput(); return Futures.transform(resultFuture, - result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), + result -> TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) + .type(output.getType()) + .scope(output.getScope()) + .result(JacksonUtil.valueToTree(result)) + .build(), MoreExecutors.directExecutor() ); } - private TbelCfArg toTbelArgument(String key) { - return arguments.get(key).toTbelCfArg(); + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.SCRIPT; } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 0d018f5def..7f21f8d2e7 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -19,80 +19,51 @@ 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 lombok.Data; -import lombok.NoArgsConstructor; +import lombok.EqualsAndHashCode; +import net.objecthunter.exp4j.Expression; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.common.util.NumberUtils; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; -import org.thingsboard.server.common.data.kv.BasicKvEntry; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; -import java.util.List; import java.util.Map; -@Data -@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { - public SimpleCalculatedFieldState(List requiredArguments) { - super(requiredArguments); - } + private ThreadLocal expression; - @Override - public CalculatedFieldType getType() { - return CalculatedFieldType.SIMPLE; + public SimpleCalculatedFieldState(EntityId entityId) { + super(entityId); } @Override - protected void validateNewEntry(ArgumentEntry newEntry) { - if (newEntry instanceof TsRollingArgumentEntry) { - throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields."); - } + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.expression = ctx.getSimpleExpressions().get(ctx.getExpression()); } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { - var expr = ctx.getCustomExpression().get(); - - for (Map.Entry entry : this.arguments.entrySet()) { - try { - BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue(); - double value = switch (kvEntry.getDataType()) { - case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow(); - case DOUBLE -> kvEntry.getDoubleValue().orElseThrow(); - case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow(); - case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString()); - }; - expr.setVariable(entry.getKey(), value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number."); - } - } - - double expressionResult = expr.evaluate(); + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + double expressionResult = ctx.evaluateSimpleExpression(expression.get(), this); Output output = ctx.getOutput(); - Object result = formatResult(expressionResult, output.getDecimalsByDefault()); - JsonNode outputResult = createResultJson(ctx, result); + Object result = NumberUtils.roundResult(expressionResult, output.getDecimalsByDefault()); + JsonNode outputResult = createResultJson(output.getName(), result); - return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult)); - } - - private Object formatResult(double expressionResult, Integer decimals) { - if (decimals == null) { - return expressionResult; - } - if (decimals.equals(0)) { - return TbUtils.toInt(expressionResult); - } - return TbUtils.toFixed(expressionResult, decimals); + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) + .type(output.getType()) + .scope(output.getScope()) + .result(outputResult) + .build()); } - private JsonNode createResultJson(CalculatedFieldCtx ctx, Object result) { - Output output = ctx.getOutput(); - String outputName = output.getName(); + private JsonNode createResultJson(String outputName, Object result) { ObjectNode valuesNode = JacksonUtil.newObjectNode(); if (result instanceof Double doubleValue) { valuesNode.put(outputName, doubleValue); @@ -101,17 +72,20 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } else { valuesNode.set(outputName, JacksonUtil.valueToTree(result)); } - if (output.getType() == OutputType.ATTRIBUTES || !ctx.isUseLatestTs()) { - return valuesNode; - } - long latestTs = getLatestTimestamp(); - if (latestTs == DEFAULT_LAST_UPDATE_TS) { - return valuesNode; + return toResultNode(valuesNode); + } + + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + if (newEntry instanceof TsRollingArgumentEntry) { + throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " + + "Rolling argument entry is not supported for simple calculated fields."); } - ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", latestTs); - resultNode.set("values", valuesNode); - return resultNode; + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.SIMPLE; } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 37ed0a5e5d..14433136f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -19,9 +19,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.type.TypeReference; import lombok.AllArgsConstructor; import lombok.Data; +import org.springframework.lang.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; @@ -39,17 +41,39 @@ public class SingleValueArgumentEntry implements ArgumentEntry { public static final Long DEFAULT_VERSION = -1L; - private long ts; - private BasicKvEntry kvEntryValue; - private Long version; + @Nullable + protected EntityId entityId; - private boolean forceResetPrevious; + protected long ts; + protected BasicKvEntry kvEntryValue; + protected Long version; + + protected boolean forceResetPrevious; public SingleValueArgumentEntry() { this.ts = DEFAULT_LAST_UPDATE_TS; this.version = DEFAULT_VERSION; } + public SingleValueArgumentEntry(EntityId entityId, ArgumentEntry entry) { + this(entry); + this.entityId = entityId; + } + + public SingleValueArgumentEntry(ArgumentEntry entry) { + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + this.ts = singleValueArgumentEntry.ts; + this.kvEntryValue = singleValueArgumentEntry.kvEntryValue; + this.version = singleValueArgumentEntry.version; + this.forceResetPrevious = singleValueArgumentEntry.forceResetPrevious; + } + } + + public SingleValueArgumentEntry(EntityId entityId, TsKvProto entry) { + this(entry); + this.entityId = entityId; + } + public SingleValueArgumentEntry(TsKvProto entry) { this.ts = entry.getTs(); if (entry.hasVersion()) { @@ -58,6 +82,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.fromProto(entry.getKv()); } + public SingleValueArgumentEntry(EntityId entityId, AttributeValueProto entry) { + this(entry); + this.entityId = entityId; + } + public SingleValueArgumentEntry(AttributeValueProto entry) { this.ts = entry.getLastUpdateTs(); if (entry.hasVersion()) { @@ -66,6 +95,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry); } + public SingleValueArgumentEntry(EntityId entityId, KvEntry entry) { + this(entry); + this.entityId = entityId; + } + public SingleValueArgumentEntry(KvEntry entry) { if (entry instanceof TsKvEntry tsKvEntry) { this.ts = tsKvEntry.getTs(); @@ -77,6 +111,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.basicKvEntryFromKvEntry(entry); } + public SingleValueArgumentEntry(EntityId entityId, long ts, BasicKvEntry kvEntryValue, Long version) { + this(ts, kvEntryValue, version); + this.entityId = entityId; + } + public SingleValueArgumentEntry(long ts, BasicKvEntry kvEntryValue, Long version) { this.ts = ts; this.kvEntryValue = kvEntryValue; @@ -100,6 +139,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry { @Override public TbelCfArg toTbelCfArg() { + if (isEmpty()) { + return new TbelCfSingleValueArg(ts, null); + } Object value = kvEntryValue.getValue(); if (kvEntryValue instanceof JsonDataEntry) { try { @@ -117,7 +159,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } @Override - public boolean updateEntry(ArgumentEntry entry) { + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) { if (singleValueEntry.getTs() < this.ts) { return false; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index 34cd0de4b2..07b65fabce 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -37,7 +37,7 @@ import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldSta @NoArgsConstructor @AllArgsConstructor @Slf4j -public class TsRollingArgumentEntry implements ArgumentEntry { +public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs { private Integer limit; private Long timeWindow; @@ -100,7 +100,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry { } @Override - public boolean updateEntry(ArgumentEntry entry) { + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof TsRollingArgumentEntry tsRollingEntry) { updateTsRollingEntry(tsRollingEntry); } else if (entry instanceof SingleValueArgumentEntry singleValueEntry) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java new file mode 100644 index 0000000000..5c5858e3bc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -0,0 +1,329 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation; + +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 lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.ScheduledRefreshSupported; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ScheduledFuture; +import java.util.stream.Collectors; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx.DISABLED_INTERVAL_VALUE; +import static org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState.ReadinessStatus.MISSING_AGGREGATION_ENTITIES_ERROR; + +@Slf4j +public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState implements ScheduledRefreshSupported { + + @Setter + @Getter + private long lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS; + @Setter + @Getter + private long lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; + private long lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS; + private long deduplicationIntervalMs = DISABLED_INTERVAL_VALUE; + private Map metrics; + + private ScheduledFuture reevaluationFuture; + + private EntityService entityService; + + public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + metrics = configuration.getMetrics(); + deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec()); + entityService = ctx.getSystemContext().getEntityService(); + } + + @Override + public void init(boolean restored) { + super.init(restored); + if (restored) { + scheduleReevaluation(); + } + } + + @Override + public void close() { + super.close(); + if (reevaluationFuture != null) { + reevaluationFuture.cancel(true); + reevaluationFuture = null; + } + } + + @Override + public void reset() { // must reset everything dependent on arguments + super.reset(); + resetScheduledRefreshTs(); + lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS; + lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; + metrics = null; + } + + @Override + public void resetScheduledRefreshTs() { + lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS; + } + + @Override + public long getLastScheduledRefreshTs() { + return lastRelatedEntitiesRefreshTs; + } + + @Override + public void updateScheduledRefreshTs() { + lastRelatedEntitiesRefreshTs = System.currentTimeMillis(); + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + } + + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + lastArgsRefreshTs = System.currentTimeMillis(); + return super.update(argumentValues, ctx); + } + + public List checkRelatedEntities(List relatedEntities) { + Map> entityInputs = prepareInputs(); + findOutdatedEntities(entityInputs, relatedEntities).forEach(this::cleanupEntityData); + updateScheduledRefreshTs(); + return findMissingEntities(entityInputs, relatedEntities); + } + + private List findMissingEntities(Map> entityInputs, List relatedEntities) { + List missing = new ArrayList<>(); + relatedEntities.forEach(entityId -> { + if (!entityInputs.containsKey(entityId)) { + missing.add(entityId); + log.warn("[{}] Missing related entity inputs for {}", ctx.getCfId(), entityId); + } + }); + return missing; + } + + private List findOutdatedEntities(Map> entityInputs, List relatedEntities) { + List outdated = new ArrayList<>(); + entityInputs.keySet().forEach(entityId -> { + if (!relatedEntities.contains(entityId)) { + outdated.add(entityId); + log.warn("[{}] CF state keeps outdated related entity {}", ctx.getCfId(), entityId); + } + }); + return outdated; + } + + public Map updateEntityData(Map fetchedArgs) { + lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; + return update(fetchedArgs, ctx); + } + + public void cleanupEntityData(EntityId relatedEntityId) { + arguments.values().forEach(argEntry -> { + RelatedEntitiesArgumentEntry aggEntry = (RelatedEntitiesArgumentEntry) argEntry; + aggEntry.getEntityInputs().remove(relatedEntityId); + }); + lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; + lastArgsRefreshTs = System.currentTimeMillis(); + readinessStatus = checkReadiness(); + } + + public void scheduleReevaluation() { + ScheduledFuture future = ctx.scheduleReevaluation(getEnforcedDeduplicationIntervalMillis(), actorCtx); + if (future != null) { + reevaluationFuture = future; + } + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception { + boolean cfUpdated = updatedArgs != null && updatedArgs.isEmpty(); + if (shouldRecalculate() || cfUpdated) { + Output output = ctx.getOutput(); + ObjectNode aggResult = aggregateMetrics(output); + lastMetricsEvalTs = System.currentTimeMillis(); + scheduleReevaluation(); + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) + .type(output.getType()) + .scope(output.getScope()) + .result(toResultNode(aggResult)) + .build()); + } else { + return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY); + } + } + + private boolean shouldRecalculate() { + boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - getEnforcedDeduplicationIntervalMillis(); + boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs; + return intervalPassed && argsUpdatedDuringInterval; + } + + private long getEnforcedDeduplicationIntervalMillis() { + return Math.max(deduplicationIntervalMs, ctx.getMinDeduplicationIntervalMillis()); + } + + private Map> prepareInputs() { + Map> inputs = new HashMap<>(); + for (Map.Entry argEntry : arguments.entrySet()) { + String key = argEntry.getKey(); + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry.getValue(); + relatedEntitiesArgumentEntry.getEntityInputs().forEach((entityId, argumentEntry) -> { + inputs.computeIfAbsent(entityId, k -> new HashMap<>()).put(key, argumentEntry); + }); + } + return inputs; + } + + private ObjectNode aggregateMetrics(Output output) throws Exception { + ObjectNode aggResult = JacksonUtil.newObjectNode(); + Map> inputs = prepareInputs(); + for (Entry entry : metrics.entrySet()) { + String metricKey = entry.getKey(); + AggMetric metric = entry.getValue(); + + AggEntry aggMetricEntry = AggEntry.createAggFunction(metric.getFunction()); + aggregateMetric(metric, aggMetricEntry, inputs); + aggMetricEntry.result(output.getDecimalsByDefault()).ifPresent(result -> { + aggResult.set(metricKey, JacksonUtil.valueToTree(result)); + }); + } + return aggResult; + } + + private void aggregateMetric(AggMetric metric, AggEntry aggEntry, Map> inputs) throws Exception { + for (Map entityInputs : inputs.values()) { + if (applyAggregation(metric.getFilter(), entityInputs)) { + Object arg = resolveAggregationInput(metric.getInput(), entityInputs); + if (arg != null) { + aggEntry.update(arg); + } + } + } + } + + private boolean applyAggregation(String filter, Map entityInputs) throws Exception { + if (filter == null || filter.isEmpty()) { + return true; + } else { + Object filterResult = ctx.evaluateTbelExpression(filter, entityInputs, getLatestTimestamp()).get(); + return filterResult instanceof Boolean booleanResult && booleanResult; + } + } + + private Object resolveAggregationInput(AggInput aggInput, Map entityInputs) throws Exception { + if (aggInput instanceof AggFunctionInput functionInput) { + return ctx.evaluateTbelExpression(functionInput.getFunction(), entityInputs, getLatestTimestamp()).get(); + } else { + String inputKey = ((AggKeyInput) aggInput).getKey(); + return entityInputs.get(inputKey).getValue(); + } + } + + @Override + public JsonNode getArgumentsJson() { + Map> inputs = prepareInputs(); + Map entityIdEntityInfos = entityService.fetchEntityInfos(ctx.getTenantId(), null, inputs.keySet()); + List entitiesArguments = new ArrayList<>(); + inputs.forEach((entityId, entityArguments) -> { + EntityInfo entityInfo = entityIdEntityInfos.get(entityId); + if (entityInfo != null) { + JsonNode entityArgumentsJson = JacksonUtil.valueToTree(entityArguments.entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().jsonValue()))); + entitiesArguments.add(new EntityArgument(entityInfo, entityArgumentsJson)); + } + }); + return JacksonUtil.valueToTree(new RelatedEntitiesArgument(ArgumentEntryType.RELATED_ENTITIES, entitiesArguments)); + } + + record RelatedEntitiesArgument(ArgumentEntryType type, List entitiesArguments) {} + + record EntityArgument(EntityInfo entity, JsonNode entityArguments) {} + + @Override + protected ReadinessStatus checkReadiness() { + if (arguments == null) { + return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR); + } + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR); + } + if (argumentEntry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { + try { + checkConstraintByDirection(relatedEntitiesArgumentEntry); + } catch (Exception e) { + return ReadinessStatus.notReady(e.getMessage()); + } + } + } + return ReadinessStatus.READY; + } + + public void checkConstraintByDirection(RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { + if (ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) { + if (EntitySearchDirection.TO == config.getRelation().direction()) { + if (relatedEntitiesArgumentEntry.getEntityInputs().size() > 1) { + throw new IllegalArgumentException("More than one related entity is not supported for relation direction 'TO'. Found: " + relatedEntitiesArgumentEntry.getEntityInputs().size() + "."); + } + } + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java new file mode 100644 index 0000000000..9cd40e4250 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfRelatedEntitiesArgumentValue; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.HasEntityLimit; +import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Map; +import java.util.stream.Collectors; + +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + +@Data +@AllArgsConstructor +public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs, HasEntityLimit { + + private final Map entityInputs; + + private boolean forceResetPrevious; + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.RELATED_ENTITIES; + } + + @Override + public Object getValue() { + return entityInputs; + } + + @Override + public long getLatestTs() { + long latestTs = DEFAULT_LAST_UPDATE_TS; + for (ArgumentEntry entry : entityInputs.values()) { + if (entry instanceof SingleValueArgumentEntry single) { + if (!single.isDefaultValue()) { + latestTs = Math.max(latestTs, single.getTs()); + } + } + } + return latestTs; + } + + @Override + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { + if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { + checkEntityLimit(entityInputs.size(), ctx); + entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); + } else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + if (entry.isForceResetPrevious()) { + checkEntityLimit(entityInputs.size(), ctx); + entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); + } else { + ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); + if (argumentEntry != null) { + argumentEntry.updateEntry(singleValueArgumentEntry, ctx); + } else { + checkEntityLimit(entityInputs.size(), ctx); + entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); + } + } + } else { + throw new IllegalArgumentException("Unsupported argument entry type for aggregation argument entry: " + entry.getType()); + } + return true; + } + + @Override + public boolean isEmpty() { + return entityInputs.isEmpty(); + } + + @Override + public TbelCfArg toTbelCfArg() { + var inputs = entityInputs.entrySet().stream() + .collect(Collectors.toMap( + e -> e.getKey().getId(), + e -> (TbelCfSingleValueArg) e.getValue().toTbelCfArg() + )); + return new TbelCfRelatedEntitiesArgumentValue(inputs); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java new file mode 100644 index 0000000000..79bf1bdf25 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AvgAggEntry.class, name = "AVG"), + @JsonSubTypes.Type(value = CountAggEntry.class, name = "COUNT"), + @JsonSubTypes.Type(value = CountUniqueAggEntry.class, name = "COUNT_UNIQUE"), + @JsonSubTypes.Type(value = MaxAggEntry.class, name = "MAX"), + @JsonSubTypes.Type(value = MinAggEntry.class, name = "MIN"), + @JsonSubTypes.Type(value = SumAggEntry.class, name = "SUM") +}) +public interface AggEntry { + + @JsonIgnore + AggFunction getType(); + + void update(Object value); + + Optional result(Integer precision); + + static AggEntry createAggFunction(AggFunction function) { + return switch (function) { + case MIN -> new MinAggEntry(); + case MAX -> new MaxAggEntry(); + case SUM -> new SumAggEntry(); + case AVG -> new AvgAggEntry(); + case COUNT -> new CountAggEntry(); + case COUNT_UNIQUE -> new CountUniqueAggEntry(); + }; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java new file mode 100644 index 0000000000..5ee6548f29 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.common.util.NumberUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class AvgAggEntry extends BaseAggEntry { + + private BigDecimal sum = BigDecimal.ZERO; + private long count = 0L; + + @Override + protected void doUpdate(double value) { + if (value != 0.0) { + sum = sum.add(BigDecimal.valueOf(value)); + } + this.count++; + } + + @Override + protected Object prepareResult(Integer precision) { + double result = sum.divide(BigDecimal.valueOf(count), RoundingMode.HALF_UP).doubleValue(); + return NumberUtils.roundResult(result, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.AVG; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java new file mode 100644 index 0000000000..471eb5555d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import java.util.Optional; + +public abstract class BaseAggEntry implements AggEntry { + + private boolean hasResult = false; + + @Override + public void update(Object value) { + doUpdate(extractDoubleValue(value)); + hasResult = true; + } + + @Override + public Optional result(Integer precision) { + if (hasResult) { + hasResult = false; + return Optional.of(prepareResult(precision)); + } else { + return Optional.empty(); + } + } + + protected abstract void doUpdate(double value); + + protected abstract Object prepareResult(Integer precision); + + protected double extractDoubleValue(Object value) { + try { + if (value instanceof Number number) { + return number.doubleValue(); + } + return Double.parseDouble(value.toString()); + } catch (Exception e) { + throw new NumberFormatException("Cannot parse value " + value.toString()); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java new file mode 100644 index 0000000000..1b0514fb9b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; + +public class CountAggEntry implements AggEntry { + + private long count = 0L; + + @Override + public void update(Object value) { + count++; + } + + @Override + public Optional result(Integer precision) { + return Optional.of(count); + } + + @Override + public AggFunction getType() { + return AggFunction.COUNT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java new file mode 100644 index 0000000000..76209ae042 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +public class CountUniqueAggEntry implements AggEntry { + + private final Set items = new HashSet<>(); + + @Override + public void update(Object value) { + if (value != null) { + items.add(String.valueOf(value)); + } + } + + @Override + public Optional result(Integer precision) { + return Optional.of(items.size()); + } + + @Override + public AggFunction getType() { + return AggFunction.COUNT_UNIQUE; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java new file mode 100644 index 0000000000..f1054bb326 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.common.util.NumberUtils; + +public class MaxAggEntry extends BaseAggEntry { + + private double max = Double.MIN_VALUE; + + @Override + protected void doUpdate(double value) { + if (value > max) { + max = value; + } + } + + @Override + protected Object prepareResult(Integer precision) { + return NumberUtils.roundResult(max, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.MAX; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java new file mode 100644 index 0000000000..fc4257dfcb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.common.util.NumberUtils; + +public class MinAggEntry extends BaseAggEntry { + + private double min = Double.MAX_VALUE; + + @Override + protected void doUpdate(double value) { + if (value < min) { + min = value; + } + } + + @Override + protected Object prepareResult(Integer precision) { + return NumberUtils.roundResult(min, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.MIN; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java new file mode 100644 index 0000000000..9908757e56 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.common.util.NumberUtils; + +import java.math.BigDecimal; + +public class SumAggEntry extends BaseAggEntry { + + private BigDecimal sum = BigDecimal.ZERO; + + @Override + protected void doUpdate(double value) { + if (value != 0.0) { + sum = sum.add(BigDecimal.valueOf(value)); + } + } + + @Override + protected Object prepareResult(Integer precision) { + return NumberUtils.roundResult(sum.doubleValue(), precision); + } + + @Override + public AggFunction getType() { + return AggFunction.SUM; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntry.java new file mode 100644 index 0000000000..5b7b28fe1e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntry.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.single; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AggIntervalEntry { + + private Long startTs; + private Long endTs; + + public boolean belongsToInterval(long ts) { + return ts >= startTs && ts < endTs; + } + + public long getIntervalDuration() { + return endTs - startTs; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntryStatus.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntryStatus.java new file mode 100644 index 0000000000..a6cb8766d5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntryStatus.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.single; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AggIntervalEntryStatus { + + private long lastArgsRefreshTs = -1; + + private long lastMetricsEvalTs = -1; + + public AggIntervalEntryStatus(long lastArgsRefreshTs) { + this.lastArgsRefreshTs = lastArgsRefreshTs; + } + + public boolean intervalPassed(long checkInterval) { + return lastMetricsEvalTs <= System.currentTimeMillis() - checkInterval; + } + + @JsonIgnore + public boolean argsUpdated() { + return lastArgsRefreshTs > -1; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java new file mode 100644 index 0000000000..b59c5d4742 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.single; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Data +public class EntityAggregationArgumentEntry implements ArgumentEntry { + + private Map aggIntervals; + + private boolean forceResetPrevious; + + public EntityAggregationArgumentEntry(Map aggIntervals) { + this.aggIntervals = aggIntervals; + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.ENTITY_AGGREGATION; + } + + @Override + public Object getValue() { + return aggIntervals; + } + + @Override + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { + if (entry instanceof EntityAggregationArgumentEntry entityAggEntry) { + aggIntervals.putAll(entityAggEntry.getAggIntervals()); + return true; + } else if (entry instanceof SingleValueArgumentEntry singleValueArgEntry) { + long entryTs = singleValueArgEntry.getTs(); + long now = System.currentTimeMillis(); + if (updateExistingIntervals(singleValueArgEntry, entryTs, now)) { + return true; + } + return createNewInterval(entryTs, now, ctx); + } + return false; + } + + private boolean updateExistingIntervals(SingleValueArgumentEntry entry, long entryTs, long now) { + boolean updated = false; + + for (Map.Entry aggIntervalEntry : aggIntervals.entrySet()) { + AggIntervalEntry interval = aggIntervalEntry.getKey(); + AggIntervalEntryStatus status = aggIntervalEntry.getValue(); + if (entry.isForceResetPrevious()) { + status.setLastArgsRefreshTs(now); + updated = true; + continue; + } + if (interval.belongsToInterval(entryTs)) { + status.setLastArgsRefreshTs(now); + return true; + } + } + + return updated; + } + + private boolean createNewInterval(long entryTs, long now, CalculatedFieldCtx ctx) { + if (!(ctx.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration config)) { + return false; + } + AggInterval interval = config.getInterval(); + Watermark watermark = config.getWatermark(); + long watermarkDuration = watermark == null ? 0 : TimeUnit.SECONDS.toMillis(watermark.getDuration()); + + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(entryTs), interval.getZoneId()); + + long startTs = interval.getDateTimeIntervalStartTs(zdt); + long endTs = interval.getDateTimeIntervalEndTs(zdt); + + if (now - endTs > watermarkDuration) { + return false; + } + + AggIntervalEntry newInterval = new AggIntervalEntry(startTs, endTs); + aggIntervals.computeIfAbsent(newInterval, i -> new AggIntervalEntryStatus(now)); + return true; + } + + @Override + public boolean isEmpty() { + return aggIntervals.isEmpty(); + } + + @Override + public JsonNode jsonValue() { + return JacksonUtil.valueToTree(aggIntervals); + } + + @Override + public TbelCfArg toTbelCfArg() { + return null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java new file mode 100644 index 0000000000..c945868576 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -0,0 +1,389 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.aggregation.single; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.common.util.DebugModeUtil; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.common.util.NumberUtils; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultMetricArgumentEntry; + +public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldState { + + private AggInterval interval; + private long watermarkDuration; + private Map metrics; + + private boolean produceIntermediateResult; + + private EntityAggregationDebugArgumentsTracker debugTracker; + + private CalculatedFieldProcessingService cfProcessingService; + + private long now; + + public EntityAggregationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.cfProcessingService = ctx.getCfProcessingService(); + var configuration = (EntityAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + Watermark watermark = configuration.getWatermark(); + watermarkDuration = watermark == null ? 0 : TimeUnit.SECONDS.toMillis(watermark.getDuration()); + interval = configuration.getInterval(); + metrics = configuration.getMetrics(); + produceIntermediateResult = configuration.isProduceIntermediateResult(); + } + + @Override + public void init(boolean restored) { + super.init(restored); + if (restored) { + fillMissingIntervals(); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ENTITY_AGGREGATION; + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception { + createIntervalIfNotExist(); + now = System.currentTimeMillis(); + + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + if (debugTracker == null) { + debugTracker = new EntityAggregationDebugArgumentsTracker(new HashMap<>()); + } else { + debugTracker.reset(); + } + debugTracker.recordUpdatedArgs(updatedArgs, arguments); + } + + Map> results = new HashMap<>(); + List expiredIntervals = new ArrayList<>(); + getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { + processInterval(intervalEntry, argIntervalStatuses, expiredIntervals, results); + }); + removeExpiredIntervals(expiredIntervals); + + Output output = ctx.getOutput(); + ArrayNode result = toResult(results, output.getDecimalsByDefault()); + if (result.isEmpty()) { + return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY); + } + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) + .type(output.getType()) + .scope(output.getScope()) + .result(result) + .build()); + } + + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + createIntervalIfNotExist(); + return super.update(argumentValues, ctx); + } + + private void removeExpiredIntervals(List expiredIntervals) { + expiredIntervals.forEach(expiredInterval -> { + arguments.values().stream() + .map(EntityAggregationArgumentEntry.class::cast) + .forEach(arg -> arg.getAggIntervals().remove(expiredInterval)); + }); + } + + private void createIntervalIfNotExist() { + AggIntervalEntry currentInterval = new AggIntervalEntry(interval.getCurrentIntervalStartTs(), interval.getCurrentIntervalEndTs()); + arguments.forEach((argName, argumentEntry) -> { + var entityAggEntry = (EntityAggregationArgumentEntry) argumentEntry; + entityAggEntry.getAggIntervals().computeIfAbsent(currentInterval, current -> new AggIntervalEntryStatus()); + }); + } + + private void fillMissingIntervals() { + long now = System.currentTimeMillis(); + ZoneId zoneId = interval.getZoneId(); + long currentIntervalEndTs = interval.getCurrentIntervalEndTs(); + long watermarkThresholdTs = now - watermarkDuration; + + Map> intervals = getIntervals(); + AggIntervalEntry lastIntervalEntry = intervals.keySet().stream().max(Comparator.comparing(AggIntervalEntry::getEndTs)).orElse(null); + if (lastIntervalEntry == null) { + return; + } + + ZonedDateTime nextStart = Instant.ofEpochMilli(lastIntervalEntry.getEndTs()).atZone(zoneId); + ZonedDateTime nextEnd = interval.getNextIntervalStart(nextStart); + + while (nextEnd.toInstant().toEpochMilli() <= currentIntervalEndTs) { + long nextStartTs = nextStart.toInstant().toEpochMilli(); + long nextEndTs = nextEnd.toInstant().toEpochMilli(); + + if (nextEndTs < watermarkThresholdTs) { + nextStart = nextEnd; + nextEnd = interval.getNextIntervalStart(nextStart); + continue; + } + + AggIntervalEntry missing = new AggIntervalEntry(nextStartTs, nextEndTs); + + arguments.forEach((argName, argumentEntry) -> { + var entityAggEntry = (EntityAggregationArgumentEntry) argumentEntry; + AggIntervalEntryStatus intervalEntryStatus = new AggIntervalEntryStatus(System.currentTimeMillis()); + entityAggEntry.getAggIntervals().computeIfAbsent(missing, missingInterval -> intervalEntryStatus); + }); + + nextStart = nextEnd; + nextEnd = interval.getNextIntervalStart(nextStart); + } + } + + private Map> getIntervals() { + Map> intervals = new HashMap<>(); + arguments.forEach((argName, entry) -> { + var argEntry = (EntityAggregationArgumentEntry) entry; + argEntry.getAggIntervals().forEach((intervalEntry, status) -> + intervals.computeIfAbsent(intervalEntry, i -> new HashMap<>()).put(argName, status) + ); + }); + return intervals; + } + + private void processInterval(AggIntervalEntry intervalEntry, + Map args, + List expiredIntervals, + Map> results) { + long startTs = intervalEntry.getStartTs(); + long endTs = intervalEntry.getEndTs(); + + if (now - endTs > watermarkDuration) { + handleExpiredInterval(intervalEntry, args, results); + expiredIntervals.add(intervalEntry); + } else if (now - startTs >= intervalEntry.getIntervalDuration()) { + handleActiveInterval(ctx.getCfCheckReevaluationIntervalMillis(), intervalEntry, args, results); + if (watermarkDuration == 0) { + expiredIntervals.add(intervalEntry); + } + } else if (produceIntermediateResult) { + handleActiveInterval(ctx.getIntermediateAggregationIntervalMillis(), intervalEntry, args, results); + } + } + + private void handleExpiredInterval(AggIntervalEntry intervalEntry, + Map args, + Map> results) { + args.forEach((argName, argEntryIntervalStatus) -> { + if (argEntryIntervalStatus.getLastArgsRefreshTs() > argEntryIntervalStatus.getLastMetricsEvalTs()) { + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + processArgument(intervalEntry, argName, false, results); + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == DEFAULT_LAST_UPDATE_TS) { + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + processArgument(intervalEntry, argName, true, results); + } + }); + } + + private void handleActiveInterval(long cfCheckInterval, + AggIntervalEntry intervalEntry, + Map args, + Map> results) { + args.forEach((argName, argEntryIntervalStatus) -> { + if (argEntryIntervalStatus.intervalPassed(cfCheckInterval)) { + if (argEntryIntervalStatus.argsUpdated()) { + argEntryIntervalStatus.setLastMetricsEvalTs(now); + argEntryIntervalStatus.setLastArgsRefreshTs(DEFAULT_LAST_UPDATE_TS); + processArgument(intervalEntry, argName, false, results); + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == DEFAULT_LAST_UPDATE_TS) { + argEntryIntervalStatus.setLastMetricsEvalTs(now); + processArgument(intervalEntry, argName, true, results); + } + } + }); + } + + private void processArgument(AggIntervalEntry intervalEntry, + String argName, + boolean useDefault, + Map> results) { + Set metrics = findMetrics(argName); + if (!metrics.isEmpty()) { + metrics.forEach(metricName -> { + AggMetric metric = this.metrics.get(metricName); + String argKey = ctx.getArguments().get(argName).getRefEntityKey().getKey(); + ArgumentEntry metricEntry = useDefault + ? createDefaultMetricArgumentEntry(argKey, metric) + : cfProcessingService.fetchMetricDuringInterval(ctx.getTenantId(), entityId, argKey, metric, intervalEntry); + if (!metricEntry.isEmpty()) { + results.computeIfAbsent(intervalEntry, i -> new HashMap<>()).put(metricName, metricEntry); + } + }); + } + } + + private Set findMetrics(String argName) { + return metrics.entrySet().stream() + .filter(e -> ((AggKeyInput) e.getValue().getInput()).getKey().equals(argName)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + protected ArrayNode toResult(Map> results, Integer precision) { + ArrayNode result = JacksonUtil.newArrayNode(); + results.forEach((interval, args) -> { + ObjectNode metricsNode = JacksonUtil.newObjectNode(); + for (Map.Entry entry : args.entrySet()) { + String metricName = entry.getKey(); + ArgumentEntry argumentEntry = entry.getValue(); + if (!argumentEntry.isEmpty()) { + Object resultValue = argumentEntry.getValue() instanceof Number number + ? NumberUtils.roundResult(number.doubleValue(), precision) + : argumentEntry.getValue(); + metricsNode.put(metricName, JacksonUtil.toString(resultValue)); + } + } + if (!metricsNode.isEmpty()) { + ObjectNode resultNode = JacksonUtil.newObjectNode(); + resultNode.put("ts", interval.getStartTs()); + resultNode.set("values", metricsNode); + result.add(resultNode); + + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + if (debugTracker != null) { + debugTracker.addInterval(interval); + } + } + } + }); + return result; + } + + @Override + public JsonNode getArgumentsJson() { + if (debugTracker == null) { + return null; + } + EntityAggregationDebugArguments debugArguments = debugTracker.toDebugArguments(); + return debugArguments == null ? null : JacksonUtil.valueToTree(debugArguments); + } + + @Override + public boolean isReady() { + return true; + } + + record EntityAggregationDebugArgumentsTracker(Map> processedIntervals) { + + public void reset() { + processedIntervals.clear(); + } + + public void addInterval(AggIntervalEntry interval) { + processedIntervals.computeIfAbsent(interval, k -> new HashMap<>()); + } + + public void recordUpdatedArgs(Map updatedArgs, Map arguments) { + if (updatedArgs != null && !updatedArgs.isEmpty()) { + updatedArgs.forEach((argName, argEntry) -> { + ArgumentEntry argumentEntry = arguments.get(argName); + if (argumentEntry instanceof EntityAggregationArgumentEntry entityAggEntry && argEntry instanceof SingleValueArgumentEntry singleEntry) { + entityAggEntry.getAggIntervals().forEach((aggIntervalEntry, aggIntervalEntryStatus) -> { + boolean match = singleEntry.isForceResetPrevious() || aggIntervalEntry.belongsToInterval(singleEntry.getTs()); + if (match) { + recordArg(aggIntervalEntry, argName, singleEntry.toTbelCfArg()); + } + }); + } + }); + } + } + + public void recordArg(AggIntervalEntry interval, String argName, TbelCfArg value) { + processedIntervals.computeIfAbsent(interval, k -> new HashMap<>()).put(argName, value); + } + + public EntityAggregationDebugArguments toDebugArguments() { + if (processedIntervals.isEmpty()) { + return null; + } + return EntityAggregationDebugArguments.toDebugArguments(processedIntervals); + } + + } + + record EntityAggregationDebugArguments(List processedIntervals) { + + public static EntityAggregationDebugArguments toDebugArguments(Map> processedIntervals) { + List result = new ArrayList<>(); + processedIntervals.forEach((interval, args) -> { + result.add(new IntervalDebugArgument(interval.getStartTs(), interval.getEndTs(), args)); + }); + return new EntityAggregationDebugArguments(result); + } + + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + record IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, JsonNode updatedArguments) { + + public IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, Map updatedArguments) { + this(intervalStartTs, intervalEndTs, updatedArguments == null || updatedArguments.isEmpty() ? null : JacksonUtil.valueToTree(updatedArguments)); + } + + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java new file mode 100644 index 0000000000..930d030d71 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -0,0 +1,586 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.alarm; + +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 lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.KvUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.query.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.service.cf.AlarmCalculatedFieldResult; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +import static org.thingsboard.server.common.data.StringUtils.equalsAny; +import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.NEW_EVENT; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.SCHEDULED_REEVALUATION; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { + + private AlarmCalculatedFieldConfiguration configuration; + private String alarmType; + + @Getter + private final Map createRuleStates = new TreeMap<>(Comparator.comparing(Enum::ordinal)); + @Getter + @Setter + private AlarmRuleState clearRuleState; + + @Getter + private Alarm currentAlarm; + private boolean initialFetchDone; + + public AlarmCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.configuration = getConfiguration(ctx); + this.alarmType = ctx.getCalculatedField().getName(); + + Map createRules = configuration.getCreateRules(); + createRules.forEach((severity, rule) -> { + AlarmRuleState ruleState = createRuleStates.get(severity); + if (ruleState != null) { + ruleState.setAlarmRule(rule); + } + }); + AlarmRule clearRule = configuration.getClearRule(); + if (clearRule != null && clearRuleState != null) { + clearRuleState.setAlarmRule(clearRule); + } + + if (currentAlarm != null && !currentAlarm.getType().equals(alarmType)) { + currentAlarm = null; + initialFetchDone = false; + } + } + + @Override + public void init(boolean restored) { + super.init(restored); + AtomicBoolean reevalNeeded = new AtomicBoolean(false); + Map createRules = configuration.getCreateRules(); + for (AlarmSeverity severity : AlarmSeverity.values()) { + AlarmRule rule = createRules.get(severity); + if (rule != null) { + createRuleStates.compute(severity, (__, ruleState) -> { + return initRuleState(severity, rule, ruleState, reevalNeeded); + }); + } else { + AlarmRuleState state = createRuleStates.remove(severity); + if (state != null) { + clearState(state); + } + } + } + + AlarmRule clearRule = configuration.getClearRule(); + if (clearRule != null) { + clearRuleState = initRuleState(null, clearRule, clearRuleState, reevalNeeded); + } else { + if (clearRuleState != null) { + clearState(clearRuleState); + clearRuleState = null; + } + } + log.debug("Initialized create rule states {} and clear rule state {} for {}. Restored: {}, reeval needed: {}", + createRuleStates, clearRuleState, configuration, restored, reevalNeeded); + + if (reevalNeeded.get()) { + initCurrentAlarm(ctx); + createOrClearAlarms(state -> { + if (state.getCondition().getType() == AlarmConditionType.DURATION) { + AlarmEvalResult evalResult = state.reeval(System.currentTimeMillis(), ctx); + if (evalResult.getStatus() == TRUE || evalResult.getStatus() == NOT_YET_TRUE) { + ScheduledFuture future = ctx.scheduleReevaluation(evalResult.getLeftDuration(), actorCtx); + if (future != null) { + state.setDurationCheckFuture(future); + } + } + } + return AlarmEvalResult.NOT_YET_TRUE; + }, ctx); + } + } + + private AlarmRuleState initRuleState(AlarmSeverity severity, AlarmRule rule, AlarmRuleState ruleState, AtomicBoolean reevalNeeded) { + if (ruleState == null) { + ruleState = new AlarmRuleState(severity, rule, this); + } else { + // when restored + ruleState.setAlarmRule(rule); + ruleState.setActive(null); + AlarmCondition condition = rule.getCondition(); + if (condition.hasSchedule() || (condition.getType() == AlarmConditionType.DURATION && !ruleState.isEmpty())) { + reevalNeeded.set(true); + } + } + return ruleState; + } + + @Override + public void reset() { + super.reset(); + configuration = null; + } + + @Override + public void close() { + super.close(); + for (AlarmRuleState state : createRuleStates.values()) { + clearState(state); + } + clearState(clearRuleState); + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + initCurrentAlarm(ctx); + TbAlarmResult result = createOrClearAlarms(state -> { + if (updatedArgs != null) { + boolean newEvent = !updatedArgs.isEmpty(); + AlarmEvalResult evalResult = state.eval(newEvent, ctx); + if (evalResult.getStatus() == NOT_YET_TRUE && evalResult.getLeftDuration() > 0) { + long leftDuration = evalResult.getLeftDuration(); + ScheduledFuture future = ctx.scheduleReevaluation(leftDuration, actorCtx); + if (future != null) { + state.setDurationCheckFuture(future); + } + } + return evalResult.withCause(NEW_EVENT); + } else { + return state.reeval(System.currentTimeMillis(), ctx).withCause(SCHEDULED_REEVALUATION); + } + }, ctx); + return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() + .alarmResult(result) + .build()); + } + + @Override + protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry, CalculatedFieldCtx ctx) { + if (!(existingArgumentEntry instanceof SingleValueArgumentEntry existingEntry) || + !(newArgumentEntry instanceof SingleValueArgumentEntry newEntry)) { + return super.updateEntry(existingArgumentEntry, newArgumentEntry, ctx); + } + if (newEntry.getTs() < existingEntry.getTs()) { + if (existingEntry.isDefaultValue()) { + existingEntry.setTs(newEntry.getTs()); + } + } + return super.updateEntry(existingEntry, newEntry, ctx); + } + + public void processAlarmAction(Alarm alarm, ActionType action) { + switch (action) { + case ALARM_ACK -> processAlarmAck(alarm); + case ALARM_CLEAR -> processAlarmClear(alarm); + case ALARM_DELETE -> processAlarmDelete(alarm); + } + } + + private void processAlarmClear(Alarm alarm) { + currentAlarm = null; + createRuleStates.values().forEach(this::clearState); + clearState(clearRuleState); + } + + private void processAlarmAck(Alarm alarm) { + currentAlarm.setAcknowledged(alarm.isAcknowledged()); + currentAlarm.setAckTs(alarm.getAckTs()); + } + + private void processAlarmDelete(Alarm alarm) { + processAlarmClear(alarm); + } + + private TbAlarmResult createOrClearAlarms(Function evalFunction, + CalculatedFieldCtx ctx) { + AlarmEvalResult evalResult = null; + AlarmRuleState resultState = null; + AlarmRuleState.StateInfo resultStateInfo = null; + + for (AlarmRuleState state : createRuleStates.values()) { + evalResult = evalFunction.apply(state); + log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult); + if (evalResult.getStatus() == TRUE) { + resultState = state; + break; + } else if (evalResult.getStatus() == FALSE) { + clearState(state); + } + } + + TbAlarmResult result = null; + if (resultState != null) { + result = calculateAlarmResult(resultState, evalResult, ctx); + resultStateInfo = resultState.getStateInfo(); + log.debug("Alarm result for state {}: {}", resultState, result); + clearState(clearRuleState); + } else if (currentAlarm != null && clearRuleState != null) { + evalResult = evalFunction.apply(clearRuleState); + log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult); + if (evalResult.getStatus() == TRUE) { + resultStateInfo = clearRuleState.getStateInfo(); + clearState(clearRuleState); + for (AlarmRuleState state : createRuleStates.values()) { + clearState(state); + } + AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm( + ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), false + ); + if (clearResult.isCleared()) { + result = TbAlarmResult.builder() + .isCleared(true) + .alarm(clearResult.getAlarm()) + .build(); + resultState = clearRuleState; + } + currentAlarm = null; + } else if (evalResult.getStatus() == FALSE) { + clearState(clearRuleState); + } + } + if (result != null && resultState != null) { + result.setConditionRepeats(resultStateInfo.eventCount()); + result.setConditionDuration(resultStateInfo.duration()); + } + return result; + } + + private void clearState(AlarmRuleState state) { + if (state != null) { + log.debug("Clearing rule state {}", state); + state.clear(); + } + } + + private void initCurrentAlarm(CalculatedFieldCtx ctx) { + if (!initialFetchDone) { + Alarm alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), entityId, alarmType); + if (alarm != null && !alarm.getStatus().isCleared()) { + currentAlarm = alarm; + } + initialFetchDone = true; + } + } + + private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, AlarmEvalResult evalResult, CalculatedFieldCtx ctx) { + AlarmSeverity severity = ruleState.getSeverity(); + if (currentAlarm != null) { + AlarmSeverity oldSeverity = currentAlarm.getSeverity(); + if (severity.ordinal() > oldSeverity.ordinal()) { + log.trace("Skipping alarm update for result state {} for eval result {} because severity is decreased", ruleState, evalResult); + return null; + } + if (severity.ordinal() == oldSeverity.ordinal() && evalResult.getCause() == SCHEDULED_REEVALUATION) { + log.trace("Skipping alarm update for result state {} for eval result {}", ruleState, evalResult); + return null; + } + + currentAlarm.setEndTs(System.currentTimeMillis()); + currentAlarm.setDetails(createDetails(ruleState)); + currentAlarm.setSeverity(severity); + AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); + } else { + var newAlarm = new Alarm(); + newAlarm.setType(alarmType); + newAlarm.setAcknowledged(false); + newAlarm.setCleared(false); + newAlarm.setSeverity(severity); + long startTs = getLatestTimestamp(); + long currentTime = System.currentTimeMillis(); + if (startTs <= 0L || startTs > currentTime) { + startTs = currentTime; + } + newAlarm.setStartTs(startTs); + newAlarm.setEndTs(startTs); + newAlarm.setDetails(createDetails(ruleState)); + newAlarm.setOriginator(entityId); + newAlarm.setTenantId(ctx.getTenantId()); + newAlarm.setPropagate(configuration.isPropagate()); + newAlarm.setPropagateToOwner(configuration.isPropagateToOwner()); + newAlarm.setPropagateToTenant(configuration.isPropagateToTenant()); + if (configuration.getPropagateRelationTypes() != null) { + newAlarm.setPropagateRelationTypes(configuration.getPropagateRelationTypes()); + } + AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(newAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); + } + } + + private JsonNode createDetails(AlarmRuleState ruleState) { + JsonNode alarmDetails; + String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); + DashboardId dashboardId = ruleState.getAlarmRule().getDashboardId(); + + if (StringUtils.isNotEmpty(alarmDetailsStr) || dashboardId != null) { + ObjectNode newDetails = JacksonUtil.newObjectNode(); + if (StringUtils.isNotEmpty(alarmDetailsStr)) { + for (Map.Entry entry : arguments.entrySet()) { + String key = entry.getKey(); + ArgumentEntry value = entry.getValue(); + alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", key), String.valueOf(value.getValue())); + } + newDetails.put("data", alarmDetailsStr); + } + if (dashboardId != null) { + newDetails.put("dashboardId", dashboardId.getId().toString()); + } + alarmDetails = newDetails; + } else if (currentAlarm != null) { + alarmDetails = currentAlarm.getDetails(); + } else { + alarmDetails = JacksonUtil.newObjectNode(); + } + + return alarmDetails; + } + + @SneakyThrows + public boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) { + if (expression instanceof TbelAlarmConditionExpression tbelExpression) { + Object result = ctx.evaluateTbelExpression(tbelExpression.getExpression(), this).get(); + if (result instanceof Boolean booleanResult) { + return booleanResult; + } else { + throw new IllegalStateException("Condition expression returned non-boolean value: '" + result + "'"); + } + } else { + SimpleAlarmConditionExpression simpleExpression = (SimpleAlarmConditionExpression) expression; + ComplexOperation operation = simpleExpression.getOperation(); + if (operation == null) { + operation = ComplexOperation.AND; + } + return switch (operation) { + case AND -> simpleExpression.getFilters().stream() + .allMatch(filter -> eval(getArgument(filter.getArgument()), filter)); + case OR -> simpleExpression.getFilters().stream() + .anyMatch(filter -> eval(getArgument(filter.getArgument()), filter)); + }; + } + } + + private boolean eval(SingleValueArgumentEntry argument, AlarmConditionFilter filter) { + ComplexOperation operation = filter.getOperation(); + if (operation == null) { + operation = ComplexOperation.AND; + } + return switch (operation) { + case AND -> filter.getPredicates().stream() + .allMatch(predicate -> eval(argument, predicate)); + case OR -> filter.getPredicates().stream() + .anyMatch(predicate -> eval(argument, predicate)); + }; + } + + private boolean eval(SingleValueArgumentEntry argument, KeyFilterPredicate predicate) { + return switch (predicate.getType()) { + case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate); + case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate); + case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate); + case NO_DATA -> evalNoDataPredicate(argument, (NoDataFilterPredicate) predicate); + case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate); + }; + } + + private boolean evalComplexPredicate(SingleValueArgumentEntry argument, ComplexFilterPredicate complexPredicate) { + return switch (complexPredicate.getOperation()) { + case OR -> { + for (KeyFilterPredicate predicate : complexPredicate.getPredicates()) { + if (eval(argument, predicate)) { + yield true; + } + } + yield false; + } + case AND -> { + for (KeyFilterPredicate predicate : complexPredicate.getPredicates()) { + if (!eval(argument, predicate)) { + yield false; + } + } + yield true; + } + }; + } + + private boolean evalBooleanPredicate(SingleValueArgumentEntry argument, BooleanFilterPredicate predicate) { + Boolean value = KvUtil.getBoolValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + Boolean predicateValue = resolveValue(predicate.getValue(), KvUtil::getBoolValue); + if (predicateValue == null) { + return false; + } + return switch (predicate.getOperation()) { + case EQUAL -> value.equals(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + }; + } + + private boolean evalNumPredicate(SingleValueArgumentEntry argument, NumericFilterPredicate predicate) { + Double value = KvUtil.getDoubleValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + Double predicateValue = resolveValue(predicate.getValue(), KvUtil::getDoubleValue); + if (predicateValue == null) { + return false; + } + return switch (predicate.getOperation()) { + case NOT_EQUAL -> !value.equals(predicateValue); + case EQUAL -> value.equals(predicateValue); + case GREATER -> value > predicateValue; + case GREATER_OR_EQUAL -> value >= predicateValue; + case LESS -> value < predicateValue; + case LESS_OR_EQUAL -> value <= predicateValue; + }; + } + + private boolean evalStrPredicate(SingleValueArgumentEntry argument, StringFilterPredicate predicate) { + String value = KvUtil.getStringValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + String predicateValue = resolveValue(predicate.getValue(), KvUtil::getStringValue); + if (predicateValue == null) { + return false; + } + if (predicate.isIgnoreCase()) { + value = value.toLowerCase(); + predicateValue = predicateValue.toLowerCase(); + } + return switch (predicate.getOperation()) { + case CONTAINS -> value.contains(predicateValue); + case EQUAL -> value.equals(predicateValue); + case STARTS_WITH -> value.startsWith(predicateValue); + case ENDS_WITH -> value.endsWith(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + case NOT_CONTAINS -> !value.contains(predicateValue); + case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + }; + } + + private boolean evalNoDataPredicate(SingleValueArgumentEntry argument, NoDataFilterPredicate predicate) { + long passedMs = System.currentTimeMillis() - argument.getTs(); + long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue); + if (duration > 0) { + long requiredDuration = predicate.getUnit().toMillis(duration); + log.trace("[{}] No data for argument {} during {} ms, required duration: {} ms", ctx, argument, passedMs, requiredDuration); + return passedMs >= requiredDuration; + } else { + return false; + } + } + + protected T resolveValue(AlarmConditionValue conditionValue, Function mapper) { + T value = conditionValue.getStaticValue(); + if (value == null) { + String argument = conditionValue.getDynamicValueArgument(); + SingleValueArgumentEntry entry = getArgument(argument); + value = mapper.apply(entry.getKvEntryValue()); + if (value == null) { + throw new IllegalArgumentException("No proper value found for argument " + argument); + } + } + return value; + } + + protected SingleValueArgumentEntry getArgument(String key) { + SingleValueArgumentEntry entry = (SingleValueArgumentEntry) arguments.get(key); + if (entry == null) { + throw new IllegalArgumentException("Argument '" + key + "' is missing"); + } + return entry; + } + + private AlarmCalculatedFieldConfiguration getConfiguration(CalculatedFieldCtx ctx) { + return (AlarmCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + } + + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + if (!(newEntry instanceof SingleValueArgumentEntry)) { + throw new IllegalArgumentException("Only single value arguments supported"); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ALARM; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java new file mode 100644 index 0000000000..805af09907 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.alarm; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class AlarmEvalResult { + + public static final AlarmEvalResult TRUE = new AlarmEvalResult(Status.TRUE); + public static final AlarmEvalResult FALSE = new AlarmEvalResult(Status.FALSE); + public static final AlarmEvalResult NOT_YET_TRUE = new AlarmEvalResult(Status.NOT_YET_TRUE); + public static final AlarmEvalResult EMPTY = new AlarmEvalResult(null); + + private final Status status; + private final long leftDuration; + private final long leftEvents; + private Cause cause; + + public AlarmEvalResult(Status status) { + this(status, 0, 0); + } + + public static AlarmEvalResult notYetTrue(long leftEvents, long leftDuration) { + return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents); + } + + public AlarmEvalResult withCause(Cause cause) { + this.cause = cause; + return this; + } + + public enum Status { + FALSE, NOT_YET_TRUE, TRUE; + } + + public enum Cause { + NEW_EVENT, SCHEDULED_REEVALUATION; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java new file mode 100644 index 0000000000..19c48272cc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java @@ -0,0 +1,360 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.alarm; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.KvUtil; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmScheduleType; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeScheduleItem; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; + +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; + +@Data +@Slf4j +public class AlarmRuleState { + + private final AlarmSeverity severity; + private AlarmRule alarmRule; + private AlarmCalculatedFieldState state; + + private AlarmCondition condition; + + private long eventCount; + private long firstEventTs; // when duration condition started + private long lastCheckTs; + private transient long duration; + private ScheduledFuture durationCheckFuture; + private Boolean active; + + public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, AlarmCalculatedFieldState state) { + this.severity = severity; + if (alarmRule != null) { + setAlarmRule(alarmRule); + } + this.state = state; + } + + public AlarmEvalResult eval(boolean newEvent, CalculatedFieldCtx ctx) { // on event or config change + long ts = newEvent ? state.getLatestTimestamp() : System.currentTimeMillis(); + active = isActive(ts); + if (!active) { + return AlarmEvalResult.FALSE; + } + return doEval(newEvent, ctx); + } + + public AlarmEvalResult reeval(long ts, CalculatedFieldCtx ctx) { // on scheduled duration check or periodic re-eval for rules with schedule + boolean active = isActive(ts); + switch (condition.getType()) { + case SIMPLE, REPEATING -> { + boolean activeChanged = this.active == null || active != this.active; + this.active = active; + if (!active) { + return AlarmEvalResult.EMPTY; + } + + if ((condition.hasSchedule() && activeChanged) || + condition.getExpression().requiresScheduledReevaluation()) { + AlarmEvalResult result = doEval(false, ctx); + if (result.getStatus() == TRUE) { + return result; + } else { + return AlarmEvalResult.EMPTY; + } + } + } + case DURATION -> { + if (!active) { + return AlarmEvalResult.FALSE; + } + long requiredDuration = getRequiredDurationInMs(); + if (requiredDuration > 0 && firstEventTs > 0 && ts > firstEventTs) { + duration = ts - firstEventTs; + long prevDuration = lastCheckTs - firstEventTs; + lastCheckTs = ts; + + long leftDuration = requiredDuration - duration; + if (leftDuration <= 0) { + if (prevDuration >= requiredDuration) { + // already evaluated as true on previous check, no need to update alarm + return AlarmEvalResult.EMPTY; + } + return AlarmEvalResult.TRUE; + } else { + return AlarmEvalResult.notYetTrue(0, leftDuration); + } + } + } + } + return AlarmEvalResult.EMPTY; + } + + public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) { + return switch (condition.getType()) { + case SIMPLE -> evalSimple(ctx); + case DURATION -> evalDuration(ctx); + case REPEATING -> evalRepeating(newEvent, ctx); + }; + } + + private AlarmEvalResult evalSimple(CalculatedFieldCtx ctx) { + return eval(condition.getExpression(), ctx) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; + } + + private AlarmEvalResult evalRepeating(boolean newEvent, CalculatedFieldCtx ctx) { + if (eval(condition.getExpression(), ctx)) { + if (newEvent) { + eventCount++; + } + long requiredRepeats = getIntValue(((RepeatingAlarmCondition) condition).getCount()); + if (requiredRepeats > 0) { + long leftRepeats = requiredRepeats - eventCount; + return leftRepeats <= 0 ? AlarmEvalResult.TRUE : AlarmEvalResult.notYetTrue(leftRepeats, 0); + } else { + return AlarmEvalResult.NOT_YET_TRUE; + } + } else { + return AlarmEvalResult.FALSE; + } + } + + private AlarmEvalResult evalDuration(CalculatedFieldCtx ctx) { + if (eval(condition.getExpression(), ctx)) { + long ts = System.currentTimeMillis(); + if (firstEventTs <= 0) { + firstEventTs = state.getLatestTimestamp(); + } + lastCheckTs = ts; + + long requiredDuration = getRequiredDurationInMs(); + if (requiredDuration > 0 && firstEventTs > 0 && ts > firstEventTs) { + duration = ts - firstEventTs; + long leftDuration = requiredDuration - duration; + if (leftDuration <= 0) { + return AlarmEvalResult.TRUE; + } else { + return AlarmEvalResult.notYetTrue(0, leftDuration); + } + } else { + return AlarmEvalResult.NOT_YET_TRUE; + } + } else { + return AlarmEvalResult.FALSE; + } + } + + private boolean isActive(long eventTs) { + if (condition.getSchedule() == null) { + return true; + } + AlarmSchedule schedule = state.resolveValue(condition.getSchedule(), entry -> Optional.ofNullable(KvUtil.getStringValue(entry)) + .map(this::parseSchedule).orElse(null)); + boolean active = switch (schedule.getType()) { + case ANY_TIME -> true; + case SPECIFIC_TIME -> isActiveSpecific((SpecificTimeSchedule) schedule, eventTs); + case CUSTOM -> isActiveCustom((CustomTimeSchedule) schedule, eventTs); + }; + log.trace("Alarm rule active = {} for schedule {}", active, schedule); + return active; + } + + 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; + } + } + long endsOn = schedule.getEndsOn(); + if (endsOn == 0) { + // 24 hours in milliseconds + endsOn = 86400000; + } + + return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), endsOn); + } + + 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()) { + long endsOn = item.getEndsOn(); + if (endsOn == 0) { + // 24 hours in milliseconds + endsOn = 86400000; + } + return isActive(eventTs, zoneId, zdt, item.getStartsOn(), endsOn); + } 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() { + clearRepeatingConditionState(); + clearDurationConditionState(); + } + + private void clearRepeatingConditionState() { + eventCount = 0L; + } + + private void clearDurationConditionState() { + firstEventTs = 0L; + lastCheckTs = 0L; + duration = 0L; + if (durationCheckFuture != null) { + durationCheckFuture.cancel(true); + durationCheckFuture = null; + } + } + + public void setDurationCheckFuture(ScheduledFuture durationCheckFuture) { + if (this.durationCheckFuture != null) { + log.warn("Setting new duration check future while previous is not null for state {}", this, new RuntimeException("stacktrace")); + this.durationCheckFuture.cancel(true); + } + this.durationCheckFuture = durationCheckFuture; + } + + public boolean isEmpty() { + return eventCount == 0L && firstEventTs == 0L && lastCheckTs == 0L && durationCheckFuture == null; + } + + private AlarmSchedule parseSchedule(String str) { + ObjectNode json = (ObjectNode) JacksonUtil.toJsonNode(str); + if (json.isEmpty()) { + return new AnyTimeSchedule(); // only if valid json, fail otherwise + } + + if (!json.hasNonNull("type")) { + // deducting the schedule type + AlarmScheduleType type; + if (json.hasNonNull("daysOfWeek")) { + type = AlarmScheduleType.SPECIFIC_TIME; + } else if (json.hasNonNull("items")) { + type = AlarmScheduleType.CUSTOM; + } else { + throw new IllegalArgumentException("Failed to parse alarm schedule from '" + str + "'"); + } + json.put("type", type.name()); + } + + return JacksonUtil.treeToValue(json, AlarmSchedule.class); + } + + private Integer getIntValue(AlarmConditionValue value) { + return state.resolveValue(value, entry -> Optional.ofNullable(KvUtil.getLongValue(entry)).map(Long::intValue).orElse(null)); + } + + private long getRequiredDurationInMs() { + DurationAlarmCondition durationCondition = (DurationAlarmCondition) condition; + return durationCondition.getUnit().toMillis(state.resolveValue(durationCondition.getValue(), KvUtil::getLongValue)); + } + + private boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) { + return state.eval(expression, ctx); + } + + public void setAlarmRule(AlarmRule alarmRule) { + this.alarmRule = alarmRule; + this.condition = alarmRule.getCondition(); + + // clearing state for other condition types (possibly left from a previous condition type) + switch (condition.getType()) { + case SIMPLE -> { + clearRepeatingConditionState(); + clearDurationConditionState(); + } + case REPEATING -> { + clearDurationConditionState(); + } + case DURATION -> { + clearRepeatingConditionState(); + } + } + } + + public StateInfo getStateInfo() { + if (condition.getType() == AlarmConditionType.REPEATING) { + return new StateInfo(eventCount, null); + } else if (condition.getType() == AlarmConditionType.DURATION) { + return new StateInfo(null, duration); + } else { + return StateInfo.EMPTY; + } + } + + @Override + public String toString() { + return "AlarmRuleState{" + + "severity=" + severity + + ", condition=" + condition + + ", eventCount=" + eventCount + + ", firstEventTs=" + firstEventTs + + ", lastCheckTs=" + lastCheckTs + + ", duration=" + duration + + ", durationCheckFuture=" + durationCheckFuture + + '}'; + } + + public record StateInfo(Long eventCount, Long duration) { + + static final StateInfo EMPTY = new StateInfo(null, null); + + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java new file mode 100644 index 0000000000..f73d8825f0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.geofencing; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfGeofencingArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; + +import java.util.Map; +import java.util.stream.Collectors; + +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + +@Data +@Slf4j +public class GeofencingArgumentEntry implements ArgumentEntry, HasLatestTs { + + private Map zoneStates; + + private boolean forceResetPrevious; + + public GeofencingArgumentEntry() { + } + + public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) { + this(Map.of(entityId, ProtoUtils.fromProto(entry))); + } + + public GeofencingArgumentEntry(Map entityIdkvEntryMap) { + this.zoneStates = toZones(entityIdkvEntryMap); + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.GEOFENCING; + } + + @Override + public Object getValue() { + return zoneStates; + } + + @Override + public long getLatestTs() { + return zoneStates.values().stream() + .mapToLong(GeofencingZoneState::getTs).max().orElse(DEFAULT_LAST_UPDATE_TS); + } + + @Override + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { + if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); + } + if (geofencingArgumentEntry.isEmpty()) { + zoneStates.clear(); + return true; + } + boolean updated = false; + for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) { + if (updateZone(zoneEntry)) { + updated = true; + } + } + return updated; + } + + @Override + public boolean isEmpty() { + return zoneStates == null || zoneStates.isEmpty(); + } + + @Override + public TbelCfArg toTbelCfArg() { + return new TbelCfGeofencingArg(zoneStates); + } + + private Map toZones(Map entityIdKvEntryMap) { + return entityIdKvEntryMap.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> new GeofencingZoneState(entry.getKey(), entry.getValue()))); + } + + private boolean updateZone(Map.Entry zoneEntry) { + EntityId zoneId = zoneEntry.getKey(); + GeofencingZoneState newZoneState = zoneEntry.getValue(); + + GeofencingZoneState existingZoneState = zoneStates.get(zoneId); + if (existingZoneState == null) { + zoneStates.put(zoneId, newZoneState); + return true; + } + if (newZoneState.getPerimeterDefinition() == null) { + zoneStates.remove(zoneId); + return true; + } + return existingZoneState.update(newZoneState); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java new file mode 100644 index 0000000000..ea669998bd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -0,0 +1,207 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.geofencing; + +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.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; + +@Slf4j +@EqualsAndHashCode(callSuper = true) +public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState implements ScheduledRefreshSupported { + + private long lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS; + + public GeofencingCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.GEOFENCING; + } + + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + if (!(newEntry instanceof SingleValueArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only SINGLE_VALUE type is allowed."); + } + } + default -> { + if (!(newEntry instanceof GeofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only GEOFENCING type is allowed."); + } + } + } + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); + double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); + Coordinates entityCoordinates = new Coordinates(latitude, longitude); + + var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + Map zoneGroups = geofencingCfg.getZoneGroups(); + + ObjectNode valuesNode = JacksonUtil.newObjectNode(); + List> relationFutures = new ArrayList<>(); + + getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { + ZoneGroupConfiguration zoneGroupCfg = zoneGroups.get(argumentKey); + if (zoneGroupCfg == null) { + throw new RuntimeException("Zone group configuration is missing for the: " + entityId); + } + boolean createRelationsWithMatchedZones = zoneGroupCfg.isCreateRelationsWithMatchedZones(); + List zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size()); + argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { + boolean firstEval = zoneState.getLastPresence() == null; + GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates); + zoneResults.add(eval); + if (!createRelationsWithMatchedZones) { + return; + } + GeofencingTransitionEvent transitionEvent = eval.transition(); + if (transitionEvent == null) { + if (!firstEval) { + return; + } + transitionEvent = eval.status() == INSIDE ? + GeofencingTransitionEvent.ENTERED : + GeofencingTransitionEvent.LEFT; + } + EntityRelation relation = switch (zoneGroupCfg.getDirection()) { + case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType()); + case FROM -> new EntityRelation(entityId, zoneId, zoneGroupCfg.getRelationType()); + }; + ListenableFuture f = switch (transitionEvent) { + case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); + case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); + }; + relationFutures.add(f); + }); + updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode); + }); + + OutputType outputType = ctx.getOutput().getType(); + var result = TelemetryCalculatedFieldResult.builder() + .outputStrategy(ctx.getOutput().getStrategy()) + .type(outputType) + .scope(ctx.getOutput().getScope()) + .result(toResultNode(valuesNode)) + .build(); + if (relationFutures.isEmpty()) { + return Futures.immediateFuture(result); + } + return Futures.whenAllComplete(relationFutures).call(() -> result, MoreExecutors.directExecutor()); + } + + @Override + public void reset() { + super.reset(); + resetScheduledRefreshTs(); + } + + @Override + public void resetScheduledRefreshTs() { + lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS; + } + + @Override + public long getLastScheduledRefreshTs() { + return lastDynamicArgumentsRefreshTs; + } + + @Override + public void updateScheduledRefreshTs() { + lastDynamicArgumentsRefreshTs = System.currentTimeMillis(); + } + + private Map getGeofencingArguments() { + return arguments.entrySet() + .stream() + .filter(entry -> entry.getValue().getType().equals(ArgumentEntryType.GEOFENCING)) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); + } + + private void updateValuesNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { + GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); + final String eventKey = argumentKey + "Event"; + final String statusKey = argumentKey + "Status"; + switch (geofencingReportStrategy) { + case REPORT_TRANSITION_EVENTS_ONLY -> addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + case REPORT_PRESENCE_STATUS_ONLY -> resultNode.put(statusKey, aggregationResult.status().name()); + case REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS -> { + addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + resultNode.put(statusKey, aggregationResult.status().name()); + } + } + } + + private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { + boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); + boolean prevInside = zoneResults.stream() + .anyMatch(r -> GeofencingTransitionEvent.LEFT.equals(r.transition()) || r.transition() == null && r.status() == INSIDE); + GeofencingTransitionEvent transition = null; + if (!prevInside && nowInside) { + transition = GeofencingTransitionEvent.ENTERED; + } else if (prevInside && !nowInside) { + transition = GeofencingTransitionEvent.LEFT; + } + return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE); + } + + private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) { + if (aggregationResult.transition() != null) { + resultNode.put(eventKey, aggregationResult.transition().name()); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java new file mode 100644 index 0000000000..e0530f621d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.geofencing; + +import jakarta.annotation.Nullable; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; + +public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, + GeofencingPresenceStatus status) { +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java new file mode 100644 index 0000000000..cd61509c18 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.geofencing; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; +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.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; + +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; + +@Data +public class GeofencingZoneState { + + private final EntityId zoneId; + + private long ts; + private Long version; + private PerimeterDefinition perimeterDefinition; + + @EqualsAndHashCode.Exclude + private GeofencingPresenceStatus lastPresence; + + public GeofencingZoneState(EntityId zoneId, KvEntry entry) { + this.zoneId = zoneId; + if (!(entry instanceof AttributeKvEntry attributeKvEntry)) { + throw new IllegalArgumentException("Invalid perimeter data source for zone with id: " + zoneId + ". Perimeter definition must be stored as attribute!"); + } + this.ts = attributeKvEntry.getLastUpdateTs(); + this.version = attributeKvEntry.getVersion(); + if (entry.getValueAsString() == null) { + throw new IllegalArgumentException("Perimeter attribute '" + entry.getKey() + "' not found for Zone with id: " + zoneId); + } + this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class, + "Invalid perimeter definition format for Zone with id: " + zoneId + ". Failed to parse attribute '" + entry.getKey() + "'"); + } + + public GeofencingZoneState(GeofencingZoneProto proto) { + this.zoneId = ProtoUtils.fromProto(proto.getZoneId()); + this.ts = proto.getTs(); + this.version = proto.getVersion(); + this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class); + if (proto.hasInside()) { + this.lastPresence = proto.getInside() ? INSIDE : OUTSIDE; + } + } + + public boolean update(GeofencingZoneState newZoneState) { + if (newZoneState.getTs() <= this.ts) { + return false; + } + Long newVersion = newZoneState.getVersion(); + if (newVersion == null || this.version == null || newVersion > this.version) { + this.ts = newZoneState.getTs(); + this.version = newVersion; + this.perimeterDefinition = newZoneState.getPerimeterDefinition(); + this.lastPresence = null; + return true; + } + return false; + } + + public GeofencingEvalResult evaluate(Coordinates entityCoordinates) { + boolean nowInside = perimeterDefinition.checkMatches(entityCoordinates); + + GeofencingPresenceStatus status = nowInside ? INSIDE : OUTSIDE; + + // first evaluation + if (this.lastPresence == null) { + this.lastPresence = status; + return new GeofencingEvalResult(null, status); + } + // State changed + if (this.lastPresence != status) { + this.lastPresence = status; + GeofencingTransitionEvent transition = (status == GeofencingPresenceStatus.INSIDE) ? + GeofencingTransitionEvent.ENTERED : GeofencingTransitionEvent.LEFT; + return new GeofencingEvalResult(transition, status); + } + // State unchanged + return new GeofencingEvalResult(null, status); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java new file mode 100644 index 0000000000..4a532ea03d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.geofencing; + +public interface ScheduledRefreshSupported { + + void resetScheduledRefreshTs(); + + long getLastScheduledRefreshTs(); + + void updateScheduledRefreshTs(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java new file mode 100644 index 0000000000..8fdc105168 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.propagation; + +import lombok.Data; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfPropagationArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.HasEntityLimit; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Data +public class PropagationArgumentEntry implements ArgumentEntry, HasEntityLimit { + + private Set entityIds; + private transient List added; + private transient EntityId removed; + + private transient boolean forceResetPrevious; + private transient boolean syncWithDb; + + public PropagationArgumentEntry() { + this.entityIds = new HashSet<>(); + this.added = null; + this.removed = null; + } + + public PropagationArgumentEntry(List entityIds) { + this.entityIds = new HashSet<>(entityIds); + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.PROPAGATION; + } + + @Override + public Object getValue() { + return entityIds; + } + + @Override + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { + if (!(entry instanceof PropagationArgumentEntry updated)) { + throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); + } + if (updated.getAdded() != null) { + return checkAdded(updated.getAdded(), ctx); + } + if (updated.getRemoved() != null) { + return entityIds.remove(updated.getRemoved()); + } + if (updated.isSyncWithDb()) { + Set dbEntityIds = updated.getEntityIds(); + if (dbEntityIds.isEmpty()) { + if (entityIds.isEmpty()) { + return false; + } + entityIds.clear(); + return true; + } + boolean retained = entityIds.retainAll(dbEntityIds); + boolean added = checkAdded(dbEntityIds, ctx); + return retained || added; + } + if (updated.isEmpty()) { + entityIds.clear(); + return true; + } + entityIds = updated.getEntityIds(); + return true; + } + + private boolean checkAdded(Collection updatedIds, CalculatedFieldCtx ctx) { + for (EntityId id : updatedIds) { + checkEntityLimit(entityIds.size(), ctx); + if (entityIds.add(id)) { + if (added == null) { + added = new ArrayList<>(); + } + added.add(id); + } + } + return added != null && !added.isEmpty(); + } + + @Override + public boolean isEmpty() { + return entityIds.isEmpty(); + } + + @Override + public TbelCfArg toTbelCfArg() { + return new TbelCfPropagationArg(entityIds); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java new file mode 100644 index 0000000000..7ab0a51af3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -0,0 +1,177 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state.propagation; + +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.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +@Slf4j +public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState { + + private CalculatedFieldProcessingService cfProcessingService; + + public PropagationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + this.ctx = ctx; + this.actorCtx = actorCtx; + this.cfProcessingService = ctx.getCfProcessingService(); + this.requiredArguments = new ArrayList<>(ctx.getArgNames()); + requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); + this.readinessStatus = checkReadiness(); + if (ctx.isApplyExpressionForResolvedArguments()) { + this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); + } + } + + @Override + public void init(boolean restored) { + super.init(restored); + if (restored) { + cfProcessingService.fetchPropagationArgumentFromDb(ctx, entityId).ifPresent(fromDb -> { + fromDb.setSyncWithDb(true); + var updatedArgs = update(Map.of(PROPAGATION_CONFIG_ARGUMENT, fromDb), ctx); + if (updatedArgs.isEmpty()) { + return; + } + log.warn("[{}][{}] Propagation argument was out of sync during state restore and was reconciled with DB.", ctx.getCfId(), entityId); + var updatedPropagationArgument = (PropagationArgumentEntry) arguments.get(PROPAGATION_CONFIG_ARGUMENT); + if (updatedPropagationArgument.getAdded() != null) { + log.warn("[{}][{}] New propagation entities were detected during restore. Scheduling reevaluation for new entities...", ctx.getCfId(), entityId); + ctx.scheduleReevaluation(0L, actorCtx); + } + }); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.PROPAGATION; + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + ArgumentEntry argumentEntry = arguments.get(PROPAGATION_CONFIG_ARGUMENT); + if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry)) { + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); + } + boolean newEntityAdded = propagationArgumentEntry.getAdded() != null; + List entityIds; + if (newEntityAdded) { + entityIds = propagationArgumentEntry.getAdded(); + propagationArgumentEntry.setAdded(null); + } else { + if (propagationArgumentEntry.getEntityIds().isEmpty()) { + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); + } + entityIds = List.copyOf(propagationArgumentEntry.getEntityIds()); + } + if (ctx.isApplyExpressionForResolvedArguments()) { + return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult -> + PropagationCalculatedFieldResult.builder() + .entityIds(entityIds) + .result((TelemetryCalculatedFieldResult) telemetryCfResult) + .build(), + MoreExecutors.directExecutor()); + } + if (newEntityAdded || CollectionsUtil.isEmpty(updatedArgs)) { + updatedArgs = arguments; + } + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() + .entityIds(entityIds) + .result(toTelemetryResult(ctx, updatedArgs)) + .build()); + } + + @Override + protected ReadinessStatus checkReadiness() { + if (ctx.isApplyExpressionForResolvedArguments() || arguments == null) { + return super.checkReadiness(); + } + boolean propagationNotEmpty = false; + boolean hasOtherNonEmpty = false; + List emptyArguments = null; + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + if (emptyArguments == null) { + emptyArguments = new ArrayList<>(); + } + emptyArguments.add(requiredArgumentKey); + } else if (PROPAGATION_CONFIG_ARGUMENT.equals(requiredArgumentKey)) { + propagationNotEmpty = true; + } else { + hasOtherNonEmpty = true; + } + } + if (propagationNotEmpty && hasOtherNonEmpty) { + return ReadinessStatus.READY; + } + return ReadinessStatus.from(emptyArguments); + } + + private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx, Map updatedArgs) { + Output output = ctx.getOutput(); + TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = + TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) + .type(output.getType()) + .scope(output.getScope()); + ObjectNode valuesNode = JacksonUtil.newObjectNode(); + updatedArgs.forEach((outputKey, argumentEntry) -> { + if (argumentEntry instanceof PropagationArgumentEntry) { + return; + } + if (argumentEntry instanceof SingleValueArgumentEntry singleArgumentEntry) { + if (!singleArgumentEntry.isEmpty()) { + JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); + } + return; + } + throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " + + "Only Latest telemetry or Attribute arguments supported for 'Arguments Only' propagation mode!"); + }); + telemetryCfBuilder.result(toResultNode(valuesNode)); + return telemetryCfBuilder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index fbb3591bb5..065c3f2cec 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -15,15 +15,20 @@ */ package org.thingsboard.server.service.edge; +import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.dao.ai.AiModelService; +import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; @@ -59,6 +64,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor; +import org.thingsboard.server.service.edge.rpc.processor.apikey.ApiKeyProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; @@ -73,13 +80,17 @@ import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgePr import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainMetadataEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.user.UserProcessor; import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService; import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; @Lazy @Data @@ -90,17 +101,31 @@ public class EdgeContextComponent { private final Map processorMap = new EnumMap<>(EdgeEventType.class); + private ScheduledExecutorService edgeEventProcessingExecutorService; + @Autowired - public EdgeContextComponent(List processors) { + public EdgeContextComponent(List processors, + @Value("${edges.scheduler_pool_size}") int schedulerPoolSize) { processors.forEach(processor -> { EdgeEventType eventType = processor.getEdgeEventType(); if (eventType != null) { processorMap.put(eventType, processor); } }); + this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); + } + + @PreDestroy + private void destroy() { + if (edgeEventProcessingExecutorService != null && !edgeEventProcessingExecutorService.isShutdown()) { + edgeEventProcessingExecutorService.shutdownNow(); + } } // services + @Autowired + private TelemetrySubscriptionService tsSubService; + @Autowired private AdminSettingsService adminSettingsService; @@ -203,7 +228,6 @@ public class EdgeContextComponent { @Autowired private Optional statsCounterService; - // processors @Autowired private AlarmProcessor alarmProcessor; @@ -261,6 +285,21 @@ public class EdgeContextComponent { @Autowired private CalculatedFieldProcessor calculatedFieldProcessor; + @Autowired + private AiModelService aiModelService; + + @Autowired + private AiModelProcessor aiModelProcessor; + + @Autowired + private ApiKeyService apiKeyService; + + @Autowired + private ApiKeyProcessor apiKeyProcessor; + + @Autowired + private UserProcessor userProcessor; + public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { EdgeProcessor processor = processorMap.get(edgeEventType); if (processor == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index a01bfdc009..1e737df4e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -113,7 +113,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) { + if (EntityType.TENANT == entityType || EntityType.EDGE == entityType) { return; } log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); @@ -227,7 +227,7 @@ public class EdgeEventSourcingListener { break; case TENANT: return !event.getCreated(); - case API_USAGE_STATE, EDGE, AI_MODEL: + case API_USAGE_STATE, EDGE: return false; case DOMAIN: if (entity instanceof Domain domain) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 2417931e0b..01d2178cd1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -21,11 +21,14 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.edge.rpc.EdgeVersionComparator; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.ai.TbAiNode; import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode; @@ -38,20 +41,28 @@ 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.EntityView; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -74,6 +85,7 @@ import org.thingsboard.server.common.data.id.TenantId; 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.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -86,6 +98,9 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; @@ -124,11 +139,16 @@ import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j public class EdgeMsgConstructorUtils { @@ -153,6 +173,14 @@ public class EdgeMsgConstructorUtils { ); public static final Map> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of( + EdgeVersion.V_4_1_0, + Set.of( + TbAiNode.class.getName() + ), + EdgeVersion.V_4_0_0, + Set.of( + TbAiNode.class.getName() + ), EdgeVersion.V_3_9_0, Set.of( TbCalculatedFieldsNode.class.getName() @@ -169,6 +197,10 @@ public class EdgeMsgConstructorUtils { ) ); + private static void resetVersion(HasVersion entity) { + entity.setVersion(null); + } + public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) { return AlarmUpdateMsg.newBuilder().setMsgType(msgType) .setEntity(JacksonUtil.toString(alarm)) @@ -181,6 +213,7 @@ public class EdgeMsgConstructorUtils { } public static AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset) { + resetVersion(asset); return AssetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(asset)) .setIdMSB(asset.getUuidId().getMostSignificantBits()) .setIdLSB(asset.getUuidId().getLeastSignificantBits()).build(); @@ -194,6 +227,7 @@ public class EdgeMsgConstructorUtils { } public static AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) { + resetVersion(assetProfile); return AssetProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(assetProfile)) .setIdMSB(assetProfile.getId().getId().getMostSignificantBits()) .setIdLSB(assetProfile.getId().getId().getLeastSignificantBits()).build(); @@ -207,6 +241,7 @@ public class EdgeMsgConstructorUtils { } public static CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) { + resetVersion(customer); return CustomerUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(customer)) .setIdMSB(customer.getId().getId().getMostSignificantBits()) .setIdLSB(customer.getId().getId().getLeastSignificantBits()).build(); @@ -220,6 +255,7 @@ public class EdgeMsgConstructorUtils { } public static DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) { + resetVersion(dashboard); return DashboardUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(dashboard)) .setIdMSB(dashboard.getId().getId().getMostSignificantBits()) .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()).build(); @@ -233,6 +269,7 @@ public class EdgeMsgConstructorUtils { } public static DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device) { + resetVersion(device); return DeviceUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(device)) .setIdMSB(device.getId().getId().getMostSignificantBits()) .setIdLSB(device.getId().getId().getLeastSignificantBits()).build(); @@ -246,15 +283,45 @@ public class EdgeMsgConstructorUtils { } public static DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) { + resetVersion(deviceCredentials); return DeviceCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(deviceCredentials)).build(); } - public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { - return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(deviceProfile)) + public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile, EdgeVersion edgeVersion) { + resetVersion(deviceProfile); + String entity = getEntityAndFixLwm2mBootstrapShortServerId(deviceProfile, edgeVersion); + return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(entity) .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()).build(); } + public static String getEntityAndFixLwm2mBootstrapShortServerId(DeviceProfile deviceProfile, EdgeVersion edgeVersion) { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + if (!(transportConfiguration instanceof Lwm2mDeviceProfileTransportConfiguration) || EdgeVersionComparator.INSTANCE.compare(edgeVersion, EdgeVersion.V_4_3_0) >= 0) { + return JacksonUtil.toString(deviceProfile); + } + JsonNode jsonNode = JacksonUtil.valueToTree(deviceProfile); + JsonNode profileDataNode = jsonNode.get("profileData"); + if (profileDataNode != null && profileDataNode.has("transportConfiguration")) { + JsonNode transportConfigNode = profileDataNode.get("transportConfiguration"); + JsonNode bootstrapNode = transportConfigNode.get("bootstrap"); + if (bootstrapNode != null && bootstrapNode.isArray()) { + for (JsonNode bootstrapServerNode : bootstrapNode) { + if (bootstrapServerNode.isObject()) { + ObjectNode serverObjectNode = (ObjectNode) bootstrapServerNode; + JsonNode isBootstrapNode = serverObjectNode.get("bootstrapServerIs"); + boolean isBootstrapServer = isBootstrapNode != null && isBootstrapNode.asBoolean(false); + JsonNode shortServerIdNode = serverObjectNode.get("shortServerId"); + if (isBootstrapServer && (shortServerIdNode == null || shortServerIdNode.isNull())) { + serverObjectNode.put("shortServerId", 0); + } + } + } + } + } + return JacksonUtil.toString(jsonNode); + } + public static DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId) { return DeviceProfileUpdateMsg.newBuilder() .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) @@ -335,6 +402,7 @@ public class EdgeMsgConstructorUtils { } public static EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView) { + resetVersion(entityView); return EntityViewUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityView)) .setIdMSB(entityView.getId().getId().getMostSignificantBits()) .setIdLSB(entityView.getId().getId().getLeastSignificantBits()).build(); @@ -434,6 +502,7 @@ public class EdgeMsgConstructorUtils { } public static RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) { + resetVersion(entityRelation); return RelationUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityRelation)).build(); } @@ -451,6 +520,7 @@ public class EdgeMsgConstructorUtils { } public static RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) { + resetVersion(ruleChain); boolean isTemplateRoot = ruleChain.isRoot(); ruleChain.setRoot(isRoot); RuleChainUpdateMsg result = RuleChainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChain)) @@ -468,6 +538,7 @@ public class EdgeMsgConstructorUtils { } public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + resetVersion(ruleChainMetaData); String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion); return RuleChainMetadataUpdateMsg.newBuilder() @@ -518,7 +589,7 @@ public class EdgeMsgConstructorUtils { .setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) .setEntityType(entityId.getEntityType().name()); - long ts = getTs(entityData.getAsJsonObject()); + long ts = extractTs(entityData.getAsJsonObject()); switch (actionType) { case TIMESERIES_UPDATED: try { @@ -571,8 +642,8 @@ public class EdgeMsgConstructorUtils { return builder.build(); } - private static long getTs(JsonObject data) { - if (data.get("ts") != null && !data.get("ts").isJsonNull()) { + private static long extractTs(JsonObject data) { + if (data.has("ts") && data.get("ts").isJsonPrimitive()) { return data.getAsJsonPrimitive("ts").getAsLong(); } return System.currentTimeMillis(); @@ -588,6 +659,7 @@ public class EdgeMsgConstructorUtils { } public static TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) { + resetVersion(tenant); return TenantUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tenant)).build(); } @@ -596,6 +668,7 @@ public class EdgeMsgConstructorUtils { } public static UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user) { + resetVersion(user); return UserUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(user)) .setIdMSB(user.getId().getId().getMostSignificantBits()) .setIdLSB(user.getId().getId().getLeastSignificantBits()).build(); @@ -613,6 +686,7 @@ public class EdgeMsgConstructorUtils { } public static WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List widgets) { + resetVersion(widgetsBundle); return WidgetsBundleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetsBundle)) .setWidgets(JacksonUtil.toString(widgets)) .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) @@ -628,6 +702,7 @@ public class EdgeMsgConstructorUtils { } public static WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails) { + resetVersion(widgetTypeDetails); return WidgetTypeUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetTypeDetails)) .setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits()) .setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()).build(); @@ -642,6 +717,7 @@ public class EdgeMsgConstructorUtils { } public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) { + resetVersion(calculatedField); return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField)) .setIdMSB(calculatedField.getId().getId().getMostSignificantBits()) .setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build(); @@ -654,4 +730,160 @@ public class EdgeMsgConstructorUtils { .setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build(); } + public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) { + resetVersion(aiModel); + return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel)) + .setIdMSB(aiModel.getId().getId().getMostSignificantBits()) + .setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build(); + } + + public static AiModelUpdateMsg constructAiModelDeleteMsg(AiModelId aiModelId) { + return AiModelUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(aiModelId.getId().getMostSignificantBits()) + .setIdLSB(aiModelId.getId().getLeastSignificantBits()).build(); + } + + public static ApiKeyUpdateMsg constructApiKeyUpdatedMsg(UpdateMsgType msgType, ApiKey apiKey) { + return ApiKeyUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(apiKey)) + .setIdMSB(apiKey.getId().getId().getMostSignificantBits()) + .setIdLSB(apiKey.getId().getId().getLeastSignificantBits()).build(); + } + + public static ApiKeyUpdateMsg constructApiKeyDeleteMsg(ApiKeyId apiKeyId) { + return ApiKeyUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(apiKeyId.getId().getMostSignificantBits()) + .setIdLSB(apiKeyId.getId().getLeastSignificantBits()).build(); + } + + public static List mergeAndFilterDownlinkDuplicates(List edgeEvents) { + try { + edgeEvents = removeDownlinkDuplicates(edgeEvents); + + List attrUpdateMsgs = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + if (EdgeEventActionType.ATTRIBUTES_UPDATED.equals(edgeEvent.getAction())) { + attrUpdateMsgs.add(new AttrUpdateMsg(edgeEvent.getEntityId(), edgeEvent.getBody())); + } + } + Map> latestTsByEntityAndKey = computeLatestTsByEntityAndKey(attrUpdateMsgs); + + List result = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + if (!EdgeEventActionType.ATTRIBUTES_UPDATED.equals(edgeEvent.getAction())) { + result.add(edgeEvent); + continue; + } + + Map latestByKey = latestTsByEntityAndKey.get(edgeEvent.getEntityId()); + JsonNode filteredBody = filterAttributesBody(edgeEvent.getBody(), latestByKey); + if (filteredBody == null) { + continue; + } + + result.add(createFilteredEdgeEvent(edgeEvent, filteredBody)); + } + + result.sort(Comparator.comparingLong(EdgeEvent::getSeqId)); + return result; + } catch (Exception e) { + log.info("Can't merge downlink duplicates. Sending downlinks without merge. Original edgeEvents [{}]", edgeEvents, e); + return edgeEvents; + } + } + + private static AttrsTs extractAttributes(JsonNode body) { + if (body == null) { + return new AttrsTs(0L, List.of()); + } + String bodyStr = JacksonUtil.toString(body); + var jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); + if (!jsonObject.has("ts")) { + return new AttrsTs(0L, List.of()); + } + long ts = jsonObject.get("ts").getAsLong(); + var kv = jsonObject.getAsJsonObject("kv"); + List attrs = JsonConverter.convertToAttributes( + JsonUtils.getJsonObject( + JsonConverter.convertToAttributesProto(kv).getKvList() + ), ts); + return new AttrsTs(ts, attrs); + } + + private static JsonNode filterAttributesBody(JsonNode body, Map latestByKey) { + if (body == null) { + return null; + } + String bodyStr = JacksonUtil.toString(body); + JsonObject jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); + if (jsonObject.has("ts") && latestByKey != null && !latestByKey.isEmpty()) { + long ts = jsonObject.get("ts").getAsLong(); + JsonObject kv = jsonObject.getAsJsonObject("kv"); + for (Iterator> it = kv.entrySet().iterator(); it.hasNext(); ) { + Map.Entry e = it.next(); + Long latestTs = latestByKey.get(e.getKey()); + if (latestTs == null || !latestTs.equals(ts)) { + it.remove(); + } + } + if (kv.isEmpty()) { + return null; + } + } + return JacksonUtil.toJsonNode(jsonObject.toString()); + } + + private static Map> computeLatestTsByEntityAndKey(List attrUpdateMsgs) { + Map> latestTsByEntityAndKey = new HashMap<>(); + for (AttrUpdateMsg attrUpdateMsg : attrUpdateMsgs) { + UUID entityId = attrUpdateMsg.entityId(); + AttrsTs attrsTs = extractAttributes(attrUpdateMsg.body()); + Map map = latestTsByEntityAndKey.computeIfAbsent(entityId, id -> new HashMap<>()); + long ts = attrsTs.ts(); + for (AttributeKvEntry attr : attrsTs.attrs()) { + map.merge(attr.getKey(), ts, Math::max); + } + } + return latestTsByEntityAndKey; + } + + private static EdgeEvent createFilteredEdgeEvent(EdgeEvent edgeEvent, JsonNode filteredBody) { + EdgeEvent filtered = new EdgeEvent(); + filtered.setSeqId(edgeEvent.getSeqId()); + filtered.setTenantId(edgeEvent.getTenantId()); + filtered.setEdgeId(edgeEvent.getEdgeId()); + filtered.setAction(edgeEvent.getAction()); + filtered.setEntityId(edgeEvent.getEntityId()); + filtered.setUid(edgeEvent.getUid()); + filtered.setType(edgeEvent.getType()); + filtered.setBody(filteredBody); + return filtered; + } + + private static List removeDownlinkDuplicates(List edgeEvents) { + Set seen = new HashSet<>(); + return edgeEvents.stream() + .filter(e -> seen.add(new EventKey( + e.getTenantId(), + e.getAction(), + e.getEntityId(), + e.getType().name(), + (e.getBody() != null ? e.getBody().toString() : "null")))) + .collect(Collectors.toList()); + } + + private record EventKey(TenantId tenantId, + EdgeEventActionType action, + UUID entityId, + String type, + String body) { + } + + private record AttrsTs(long ts, List attrs) { + } + + private record AttrUpdateMsg(UUID entityId, JsonNode body) { + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index e9512504c9..a46a204cee 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -54,16 +54,18 @@ public class RelatedEdgesSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - executorService.submit(() -> { - log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); - try { - switch (event.getActionType()) { - case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); - } - } catch (Exception e) { - log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); + switch (event.getActionType()) { + case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> { + executorService.submit(() -> { + log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); + try { + relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); + } catch (Exception e) { + log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); + } + }); } - }); + } } @TransactionalEventListener( diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java new file mode 100644 index 0000000000..a83e3036f4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; + +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class DownlinkMessageMapper { + + private final EdgeContextComponent ctx; + + public List convertToDownlinkMsgsPack(EdgeSessionState state, List edgeEvents) { + List result = new ArrayList<>(); + List filtered = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(edgeEvents); + for (EdgeEvent edgeEvent : filtered) { + log.trace("[{}][{}] converting edge event to downlink msg [{}]", state.getTenantId(), state.getEdgeId(), edgeEvent); + DownlinkMsg downlinkMsg = null; + try { + switch (edgeEvent.getAction()) { + case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, + ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, + ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { + downlinkMsg = convertEntityEventToDownlink(state, edgeEvent); + if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { + log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", + state.getTenantId(), state.getEdgeId(), downlinkMsg.getDownlinkMsgId()); + } else { + log.trace("[{}][{}] entity message processed [{}]", state.getTenantId(), state.getEdgeId(), downlinkMsg); + } + } + case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> + downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(state.getEdge(), edgeEvent); + default -> log.warn("[{}][{}] Unsupported action type [{}]", state.getTenantId(), state.getEdgeId(), edgeEvent.getAction()); + } + } catch (Exception e) { + log.trace("[{}][{}] Exception during converting edge event to downlink msg", state.getTenantId(), state.getEdgeId(), e); + } + if (downlinkMsg != null) { + result.add(downlinkMsg); + } + } + return result; + } + + protected DownlinkMsg convertEntityEventToDownlink(EdgeSessionState state, EdgeEvent edgeEvent) { + log.trace("[{}] Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", edgeEvent.getTenantId(), edgeEvent, edgeEvent.getAction()); + if ((EdgeEventType.OAUTH2_CLIENT.equals(edgeEvent.getType()) || EdgeEventType.DOMAIN.equals(edgeEvent.getType())) && + (EdgeVersionUtils.isEdgeVersionOlderThan(state.getEdgeVersion(), EdgeVersion.V_3_8_0))) { + return null; + } + + return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, state.getEdgeVersion()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java index 915e2613a8..b7153a4918 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java @@ -29,4 +29,6 @@ public class EdgeEventStorageSettings { private long noRecordsSleepInterval; @Value("${edges.storage.sleep_between_batches}") private long sleepIntervalBetweenBatches; + @Value("${edges.storage.misordering_compensation_millis:60000}") + private long misorderingCompensationMillis; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java deleted file mode 100644 index e1211c53a7..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ /dev/null @@ -1,742 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import io.grpc.Server; -import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; -import io.grpc.stub.StreamObserver; -import jakarta.annotation.Nullable; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.rule.engine.api.AttributesSaveRequest; -import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; -import org.thingsboard.server.cache.TbTransactionalCache; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.transport.config.ssl.PemSslCredentials; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger; -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.edge.EdgeEventUpdateMsg; -import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; -import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; -import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; -import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; -import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; -import org.thingsboard.server.gen.edge.v1.RequestMsg; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.KafkaAdmin; -import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.queue.util.AfterStartUp; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; -import java.util.function.Function; - -import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE; -import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; -import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; - -@Service -@Slf4j -@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") -@TbCoreComponent -public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { - - private final ConcurrentMap sessions = new ConcurrentHashMap<>(); - private final ConcurrentMap sessionsById = new ConcurrentHashMap<>(); - private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); - private final Map sessionNewEvents = new HashMap<>(); - private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); - private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); - private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); - private final List zombieSessions = new ArrayList<>(); - - @Value("${edges.rpc.port}") - private int rpcPort; - @Value("${edges.rpc.ssl.enabled}") - private boolean sslEnabled; - @Value("${edges.rpc.ssl.cert}") - private String certFileResource; - @Value("${edges.rpc.ssl.private_key}") - private String privateKeyResource; - @Value("${edges.rpc.ssl.key_password:}") - private String keyPassword; - @Value("${edges.state.persistToTelemetry:false}") - private boolean persistToTelemetry; - @Value("${edges.rpc.client_max_keep_alive_time_sec:1}") - private int clientMaxKeepAliveTimeSec; - @Value("${edges.rpc.max_inbound_message_size:4194304}") - private int maxInboundMessageSize; - @Value("${edges.rpc.keep_alive_time_sec:10}") - private int keepAliveTimeSec; - @Value("${edges.rpc.keep_alive_timeout_sec:5}") - private int keepAliveTimeoutSec; - @Value("${edges.scheduler_pool_size}") - private int schedulerPoolSize; - - @Value("${edges.send_scheduler_pool_size}") - private int sendSchedulerPoolSize; - - @Value("${edges.max_high_priority_queue_size_per_session:10000}") - private int maxHighPriorityQueueSizePerSession; - - @Autowired - @Lazy - private EdgeContextComponent ctx; - - @Autowired - private TelemetrySubscriptionService tsSubService; - - @Autowired - private TbClusterService clusterService; - - @Autowired - private TbServiceInfoProvider serviceInfoProvider; - - @Autowired - private TbTransactionalCache edgeIdServiceIdCache; - - @Autowired - private TopicService topicService; - - @Autowired - private TbCoreQueueFactory tbCoreQueueFactory; - - @Autowired - private Optional kafkaAdmin; - - private Server server; - - private ScheduledExecutorService edgeEventProcessingExecutorService; - - private ScheduledExecutorService sendDownlinkExecutorService; - - private ScheduledExecutorService executorService; - - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void onStartUp() { - log.info("Initializing Edge RPC service!"); - NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) - .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) - .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) - .keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS) - .permitKeepAliveWithoutCalls(true) - .maxInboundMessageSize(maxInboundMessageSize) - .addService(this); - if (sslEnabled) { - try { - setupSsl(builder); - } catch (Exception e) { - log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); - throw new RuntimeException("Unable to set up SSL context!", e); - } - } - server = builder.build(); - log.info("Going to start Edge RPC server using port: {}", rpcPort); - try { - server.start(); - } catch (IOException e) { - log.error("Failed to start Edge RPC server!", e); - throw new RuntimeException("Failed to start Edge RPC server!"); - } - this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); - this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); - this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); - this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS); - log.info("Edge RPC service initialized!"); - } - - /** - * Configures TLS for the Edge gRPC server. - *

- * Delegates PEM parsing and key management to {@link PemSslCredentials} — the same - * class used by MQTT, CoAP, and LwM2M transports — which supports: - *

    - *
  • Separate certificate and private key files (classic two-file setup)
  • - *
  • Combined PEM: certificate chain + private key in a single {@code cert} file - * ({@code private_key} left empty)
  • - *
  • Encrypted private keys (password supplied via {@code key_password})
  • - *
- * Path resolution (for both {@code cert} and {@code private_key}) is handled by - * {@link org.thingsboard.server.common.data.ResourceUtils#getInputStream ResourceUtils}: - * absolute path → relative / working-dir → classpath → {@code classpath:} prefix. - */ - void setupSsl(NettyServerBuilder builder) throws Exception { - PemSslCredentials credentials = new PemSslCredentials(); - credentials.setCertFile(certFileResource); - credentials.setKeyFile(StringUtils.isEmpty(privateKeyResource) ? null : privateKeyResource); - credentials.setKeyPassword(keyPassword); - credentials.init(false); - - SslContext sslContext = GrpcSslContexts.configure( - SslContextBuilder.forServer(credentials.createKeyManagerFactory())).build(); - builder.sslContext(sslContext); - } - - @PreDestroy - public void destroy() { - if (server != null) { - server.shutdownNow(); - } - for (Map.Entry> entry : sessionEdgeEventChecks.entrySet()) { - EdgeId edgeId = entry.getKey(); - ScheduledFuture sessionEdgeEventCheck = entry.getValue(); - if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { - sessionEdgeEventCheck.cancel(true); - sessionEdgeEventChecks.remove(edgeId); - } - } - if (edgeEventProcessingExecutorService != null) { - edgeEventProcessingExecutorService.shutdownNow(); - } - if (sendDownlinkExecutorService != null) { - sendDownlinkExecutorService.shutdownNow(); - } - if (executorService != null) { - executorService.shutdownNow(); - } - } - - @Override - public StreamObserver handleMsgs(StreamObserver outputStream) { - EdgeGrpcSession session = createEdgeGrpcSession(outputStream); - return session.getInputStream(); - } - - private EdgeGrpcSession createEdgeGrpcSession(StreamObserver outputStream) { - return kafkaAdmin.isPresent() - ? new KafkaEdgeGrpcSession(ctx, topicService, tbCoreQueueFactory, kafkaAdmin.get(), outputStream, this::onEdgeConnect, this::onEdgeDisconnect, - sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession) - : new PostgresEdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, - sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); - } - - @Override - public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) { - switch (msg.getMsgType()) { - case EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG -> { - EdgeHighPriorityMsg edgeHighPriorityMsg = (EdgeHighPriorityMsg) msg; - log.trace("[{}] edgeEventMsg [{}]", tenantId, msg); - onEdgeHighPriorityEvent(edgeHighPriorityMsg); - } - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG -> { - EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; - log.trace("[{}] onToEdgeEventUpdateMsg [{}]", tenantId, msg); - onEdgeEventUpdate(tenantId, edgeEventUpdateMsg.getEdgeId()); - } - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG -> { - ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; - log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); - startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId(), toEdgeSyncRequest.getServiceId()); - } - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG -> { - FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; - log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); - processSyncResponse(fromEdgeSyncResponse); - } - } - } - - @Override - public void updateEdge(TenantId tenantId, Edge edge) { - if (edge == null) { - log.warn("[{}] Edge is null - edge is removed and outdated notification is in process!", tenantId); - return; - } - EdgeGrpcSession session = sessions.get(edge.getId()); - if (session != null && session.isConnected()) { - log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - session.onConfigurationUpdate(edge); - } else { - log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - } - } - - @Override - public void deleteEdge(TenantId tenantId, EdgeId edgeId) { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); - destroySession(session); - session.cleanUp(); - sessions.remove(edgeId); - sessionsById.remove(session.getSessionId()); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.remove(edgeId); - } finally { - newEventLock.unlock(); - } - cancelScheduleEdgeEventsCheck(edgeId); - } - } - - private void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.trace("[{}] onEdgeEventUpdate [{}]", tenantId, edgeId.getId()); - updateSessionEventsFlag(tenantId, edgeId); - } - } - - private void onEdgeHighPriorityEvent(EdgeHighPriorityMsg msg) { - TenantId tenantId = msg.getTenantId(); - EdgeEvent edgeEvent = msg.getEdgeEvent(); - EdgeId edgeId = edgeEvent.getEdgeId(); - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId); - session.addEventToHighPriorityQueue(edgeEvent); - updateSessionEventsFlag(tenantId, edgeId); - } - } - - private void updateSessionEventsFlag(TenantId tenantId, EdgeId edgeId) { - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) { - log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, true); - } - } finally { - newEventLock.unlock(); - } - } - - private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { - Edge edge = edgeGrpcSession.getEdge(); - TenantId tenantId = edge.getTenantId(); - log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); - if (sessions.containsKey(edgeId)) { - EdgeGrpcSession existing = sessions.get(edgeId); - if (existing != null) { - log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, edgeGrpcSession.getSessionId(), existing.getSessionId(), edgeId); - destroySession(existing); - sessionsById.remove(existing.getSessionId()); - } - } - sessions.put(edgeId, edgeGrpcSession); - sessionsById.put(edgeGrpcSession.getSessionId(), edgeGrpcSession); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.put(edgeId, true); - } finally { - newEventLock.unlock(); - } - save(tenantId, edgeId, ACTIVITY_STATE, true); - long lastConnectTs = System.currentTimeMillis(); - save(tenantId, edgeId, LAST_CONNECT_TIME, lastConnectTs); - edgeIdServiceIdCache.put(edgeId, serviceInfoProvider.getServiceId()); - pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT); - cancelScheduleEdgeEventsCheck(edgeId); - edgeEventsMigrationProcessed.putIfAbsent(edgeId, Boolean.FALSE); - scheduleEdgeEventsCheck(edgeGrpcSession); - } - - private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null) { - if (session.isSyncInProgress()) { - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId); - } else { - boolean success = false; - if (session.isConnected()) { - session.startSyncProcess(true); - success = true; - } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, ""), requestServiceId); - } - } - } - - @Override - public void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer) { - ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId, serviceInfoProvider.getServiceId()); - - UUID requestId = request.getId(); - EdgeGrpcSession session = sessions.get(request.getEdgeId()); - if (session != null && session.isSyncInProgress()) { - responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); - } else { - log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId()); - localSyncEdgeRequests.put(requestId, responseConsumer); - clusterService.pushEdgeSyncRequestToEdge(request); - scheduleSyncRequestTimeout(request, requestId); - } - } - - private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) { - log.trace("[{}] scheduling sync edge request", requestId); - executorService.schedule(() -> { - log.trace("[{}] checking if sync edge request is not processed...", requestId); - Consumer consumer = localSyncEdgeRequests.remove(requestId); - if (consumer != null) { - log.trace("[{}] timeout for processing sync edge request.", requestId); - consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Edge is not connected")); - } - }, 20, TimeUnit.SECONDS); - } - - private void processSyncResponse(FromEdgeSyncResponse response) { - log.trace("[{}] Received response from sync service: [{}]", response.getId(), response); - UUID requestId = response.getId(); - Consumer consumer = localSyncEdgeRequests.remove(requestId); - if (consumer != null) { - consumer.accept(response); - } else { - log.trace("[{}] Unknown or stale sync response received [{}]", requestId, response); - } - } - - private void scheduleEdgeEventsCheck(EdgeGrpcSession session) { - EdgeId edgeId = session.getEdge().getId(); - TenantId tenantId = session.getEdge().getTenantId(); - - cancelScheduleEdgeEventsCheck(edgeId); - - if (sessions.containsKey(edgeId)) { - ScheduledFuture edgeEventCheckTask = edgeEventProcessingExecutorService.schedule(() -> { - try { - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - if (Boolean.TRUE.equals(sessionNewEvents.get(edgeId))) { - log.trace("[{}][{}] set session new events flag to false", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, false); - session.processHighPriorityEvents(); - processEdgeEventMigrationIfNeeded(session, edgeId); - if (Boolean.TRUE.equals(edgeEventsMigrationProcessed.get(edgeId))) { - Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() { - @Override - public void onSuccess(Boolean newEventsAdded) { - if (Boolean.TRUE.equals(newEventsAdded)) { - log.trace("[{}][{}] new events added. set session new events flag to true", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, true); - } - scheduleEdgeEventsCheck(session); - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), t); - scheduleEdgeEventsCheck(session); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - scheduleEdgeEventsCheck(session); - } - } else { - scheduleEdgeEventsCheck(session); - } - } finally { - newEventLock.unlock(); - } - } catch (Exception e) { - log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), e); - } - }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); - sessionEdgeEventChecks.put(edgeId, edgeEventCheckTask); - log.trace("[{}] Check edge event scheduled for edge [{}]", tenantId, edgeId.getId()); - } else { - log.debug("[{}] Session was removed and edge event check schedule must not be started [{}]", - tenantId, edgeId.getId()); - } - } - - private void processEdgeEventMigrationIfNeeded(EdgeGrpcSession session, EdgeId edgeId) throws Exception { - boolean isMigrationProcessed = edgeEventsMigrationProcessed.getOrDefault(edgeId, Boolean.FALSE); - if (!isMigrationProcessed) { - log.info("Starting edge event migration for edge [{}]", edgeId.getId()); - Boolean eventsExist = session.migrateEdgeEvents().get(); - if (Boolean.TRUE.equals(eventsExist)) { - log.info("Migration still in progress for edge [{}]", edgeId.getId()); - sessionNewEvents.put(edgeId, true); - scheduleEdgeEventsCheck(session); - } else if (Boolean.FALSE.equals(eventsExist)) { - log.info("Migration completed for edge [{}]", edgeId.getId()); - edgeEventsMigrationProcessed.put(edgeId, true); - } - } - } - - private void cancelScheduleEdgeEventsCheck(EdgeId edgeId) { - log.trace("[{}] cancelling edge event check for edge", edgeId); - if (sessionEdgeEventChecks.containsKey(edgeId)) { - ScheduledFuture sessionEdgeEventCheck = sessionEdgeEventChecks.get(edgeId); - if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { - sessionEdgeEventCheck.cancel(true); - sessionEdgeEventChecks.remove(edgeId); - } - } - } - - private void onEdgeDisconnect(Edge edge, UUID sessionId) { - EdgeId edgeId = edge.getId(); - log.info("[{}][{}] edge disconnected!", edgeId, sessionId); - EdgeGrpcSession current = sessions.get(edgeId); - if (current != null && current.getSessionId().equals(sessionId)) { - EdgeGrpcSession toRemove = sessions.remove(edgeId); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.remove(edgeId); - } finally { - newEventLock.unlock(); - } - destroySession(toRemove); - sessionsById.remove(sessionId); - TenantId tenantId = toRemove.getEdge().getTenantId(); - save(tenantId, edgeId, ACTIVITY_STATE, false); - long lastDisconnectTs = System.currentTimeMillis(); - save(tenantId, edgeId, LAST_DISCONNECT_TIME, lastDisconnectTs); - pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); - cancelScheduleEdgeEventsCheck(edgeId); - } else { - log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId); - EdgeGrpcSession stale = sessionsById.remove(sessionId); - if (stale != null) { - try { - destroySession(stale); - log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId); - } catch (Exception e) { - log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId, e); - } - } else { - log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId); - } - } - edgeIdServiceIdCache.evict(edgeId); - } - - private void destroySession(EdgeGrpcSession session) { - try (session) { - if (!session.destroy()) { - log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", - session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId()); - zombieSessions.add(session); - } - } catch (Exception e) { - log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]", - session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId(), e); - } - } - - private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { - log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value); - if (persistToTelemetry) { - tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .entry(new LongDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } else { - tsSubService.saveAttributes(AttributesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .scope(AttributeScope.SERVER_SCOPE) - .entry(new LongDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } - } - - private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) { - log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value); - if (persistToTelemetry) { - tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .entry(new BooleanDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } else { - tsSubService.saveAttributes(AttributesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .scope(AttributeScope.SERVER_SCOPE) - .entry(new BooleanDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } - } - - private static class AttributeSaveCallback implements FutureCallback { - - private final TenantId tenantId; - private final EdgeId edgeId; - private final String key; - private final Object value; - - AttributeSaveCallback(TenantId tenantId, EdgeId edgeId, String key, Object value) { - this.tenantId = tenantId; - this.edgeId = edgeId; - this.key = key; - this.value = value; - } - - @Override - public void onSuccess(@Nullable Void result) { - log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value); - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t); - } - - } - - private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) { - try { - EdgeId edgeId = edge.getId(); - ObjectNode edgeState = JacksonUtil.newObjectNode(); - boolean isConnected = TbMsgType.CONNECT_EVENT.equals(msgType); - if (isConnected) { - edgeState.put(ACTIVITY_STATE, true); - edgeState.put(LAST_CONNECT_TIME, ts); - } else { - edgeState.put(ACTIVITY_STATE, false); - edgeState.put(LAST_DISCONNECT_TIME, ts); - } - ctx.getRuleProcessor().process(EdgeConnectionTrigger.builder() - .tenantId(tenantId) - .customerId(edge.getCustomerId()) - .edgeId(edgeId) - .edgeName(edge.getName()) - .connected(isConnected).build()); - String data = JacksonUtil.toString(edgeState); - TbMsgMetaData md = new TbMsgMetaData(); - if (!persistToTelemetry) { - md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); - md.putValue("edgeName", edge.getName()); - md.putValue("edgeType", edge.getType()); - } - TbMsg tbMsg = TbMsg.newMsg() - .type(msgType) - .originator(edgeId) - .copyMetaData(md) - .dataType(TbMsgDataType.JSON) - .data(data) - .build(); - clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); - } catch (Exception e) { - log.warn("[{}][{}] Failed to push {}", tenantId, edge.getId(), msgType, e); - } - } - - private void cleanupZombieSessions() { - try { - tryToDestroyZombieSessions(getZombieSessions(sessions.values()), s -> sessions.remove(s.getEdge().getId())); - tryToDestroyZombieSessions(getZombieSessions(sessionsById.values()), s -> sessionsById.remove(s.getSessionId())); - - zombieSessions.removeIf(zombie -> { - if (zombie.destroy()) { - log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", - zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); - return true; - } else { - log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].", - zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); - return false; - } - }); - } catch (Exception e) { - log.warn("Failed to cleanup kafka sessions", e); - } - } - - private List getZombieSessions(Collection sessions) { - List result = new ArrayList<>(); - for (EdgeGrpcSession session : sessions) { - if (isKafkaSessionAndZombie(session)) { - result.add(session); - } - } - return result; - } - - private void tryToDestroyZombieSessions(List sessionsToRemove, Function removeFunc) { - for (EdgeGrpcSession toRemove : sessionsToRemove) { - log.info("[{}] Destroying session for edge because edge is not connected", toRemove.getEdge().getId()); - if (toRemove.destroy()) { - removeFunc.apply(toRemove); - } - } - } - - private boolean isKafkaSessionAndZombie(EdgeGrpcSession session) { - if (session instanceof KafkaEdgeGrpcSession kafkaSession) { - log.debug("[{}] kafkaSession.isConnected() = {}, kafkaSession.getConsumer().getConsumer().isStopped() = {}", - kafkaSession.getEdge().getId(), - kafkaSession.isConnected(), - kafkaSession.getConsumer() != null ? kafkaSession.getConsumer().getConsumer() != null ? kafkaSession.getConsumer().getConsumer().isStopped() : null : null); - return !kafkaSession.isConnected() && - kafkaSession.getConsumer() != null && - kafkaSession.getConsumer().getConsumer() != null && - !kafkaSession.getConsumer().getConsumer().isStopped(); - } - return false; - - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java deleted file mode 100644 index fb420d080a..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ /dev/null @@ -1,963 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc; - -import com.datastax.oss.driver.api.core.uuid.Uuids; -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.SettableFuture; -import io.grpc.stub.StreamObserver; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.springframework.data.util.Pair; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.AttributesSaveResult; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; -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.msg.edge.EdgeEventUpdateMsg; -import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; -import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; -import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; -import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; -import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; -import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; -import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; -import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; -import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; -import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; -import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.EntityDataProto; -import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; -import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; -import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; -import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RequestMsg; -import org.thingsboard.server.gen.edge.v1.RequestMsgType; -import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; -import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; -import org.thingsboard.server.gen.edge.v1.UplinkMsg; -import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; -import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; -import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; -import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; -import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; -import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; -import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; - -import java.io.Closeable; -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 java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.BiConsumer; - -@Slf4j -@Data -public abstract class EdgeGrpcSession implements Closeable { - - private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; - private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId"; - - private static final int MAX_DOWNLINK_ATTEMPTS = 3; - private static final String RATE_LIMIT_REACHED = "Rate limit reached"; - - protected final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); - - protected UUID sessionId; - private BiConsumer sessionOpenListener; - private BiConsumer sessionCloseListener; - - private final EdgeSessionState sessionState = new EdgeSessionState(); - private final ReentrantLock downlinkMsgLock = new ReentrantLock(); - - protected EdgeContextComponent ctx; - protected Edge edge; - protected TenantId tenantId; - - private Long newStartTs; - private Long previousStartTs; - private Long newStartSeqId; - private Long previousStartSeqId; - - private StreamObserver inputStream; - private StreamObserver outputStream; - - private volatile boolean connected; - private volatile boolean syncInProgress; - - private EdgeVersion edgeVersion; - private int maxInboundMessageSize; - private int clientMaxInboundMessageSize; - private int maxHighPriorityQueueSizePerSession; - - private ScheduledExecutorService sendDownlinkExecutorService; - - public EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, - BiConsumer sessionOpenListener, - BiConsumer sessionCloseListener, - ScheduledExecutorService sendDownlinkExecutorService, - int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { - this.sessionId = UUID.randomUUID(); - this.ctx = ctx; - this.outputStream = outputStream; - this.sessionOpenListener = sessionOpenListener; - this.sessionCloseListener = sessionCloseListener; - this.sendDownlinkExecutorService = sendDownlinkExecutorService; - this.maxInboundMessageSize = maxInboundMessageSize; - this.maxHighPriorityQueueSizePerSession = maxHighPriorityQueueSizePerSession; - initInputStream(); - } - - protected abstract ListenableFuture migrateEdgeEvents() throws Exception; - - public void initInputStream() { - inputStream = new StreamObserver<>() { - @Override - public void onNext(RequestMsg requestMsg) { - if (!connected && requestMsg.getMsgType().equals(RequestMsgType.CONNECT_RPC_MESSAGE)) { - ConnectResponseMsg responseMsg = processConnect(requestMsg.getConnectRequestMsg()); - outputStream.onNext(ResponseMsg.newBuilder() - .setConnectResponseMsg(responseMsg) - .build()); - if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { - outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); - } else { - if (requestMsg.getConnectRequestMsg().hasMaxInboundMessageSize()) { - log.debug("[{}][{}] Client max inbound message size: {}", tenantId, sessionId, requestMsg.getConnectRequestMsg().getMaxInboundMessageSize()); - clientMaxInboundMessageSize = requestMsg.getConnectRequestMsg().getMaxInboundMessageSize(); - } - connected = true; - } - } - if (connected) { - if (requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { - if (requestMsg.hasSyncRequestMsg()) { - boolean fullSync = false; - if (requestMsg.getSyncRequestMsg().hasFullSync()) { - fullSync = requestMsg.getSyncRequestMsg().getFullSync(); - } - startSyncProcess(fullSync); - } else { - syncInProgress = false; - } - } - if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) { - if (requestMsg.hasUplinkMsg()) { - onUplinkMsg(requestMsg.getUplinkMsg()); - } - if (requestMsg.hasDownlinkResponseMsg()) { - onDownlinkResponse(requestMsg.getDownlinkResponseMsg()); - } - } - } - } - - @Override - public void onError(Throwable t) { - log.trace("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); - closeSession(); - } - - @Override - public void onCompleted() { - log.info("[{}][{}] Stream was closed and completed successfully!", tenantId, sessionId); - closeSession(); - } - - private void closeSession() { - connected = false; - if (edge != null) { - try { - sessionCloseListener.accept(edge, sessionId); - } catch (Exception ignored) { - } - } - try { - outputStream.onCompleted(); - } catch (Exception ignored) { - } - } - }; - } - - public void onConfigurationUpdate(Edge edge) { - log.debug("[{}] onConfigurationUpdate [{}]", sessionId, edge); - this.tenantId = edge.getTenantId(); - this.edge = edge; - EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() - .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build(); - ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() - .setEdgeUpdateMsg(edgeConfig) - .build(); - sendDownlinkMsg(edgeConfigMsg); - } - - public void startSyncProcess(boolean fullSync) { - if (!syncInProgress) { - log.info("[{}][{}][{}] Staring edge sync process", tenantId, edge.getId(), sessionId); - syncInProgress = true; - interruptGeneralProcessingOnSync(); - doSync(new EdgeSyncCursor(ctx, edge, fullSync)); - } else { - log.info("[{}][{}][{}] Sync is already started, skipping starting it now", tenantId, edge.getId(), sessionId); - } - } - - private void doSync(EdgeSyncCursor cursor) { - if (cursor.hasNext()) { - EdgeEventFetcher next = cursor.getNext(); - log.debug("[{}][{}] starting sync process, cursor current idx = {}, class = {}", - tenantId, edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); - ListenableFuture> future = startProcessingEdgeEvents(next); - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Pair result) { - doSync(cursor); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Exception during sync process", tenantId, edge.getId(), t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - log.info("[{}][{}] sync process completed", tenantId, edge.getId()); - DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) - .build(); - Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { - @Override - public void onSuccess(Boolean isInterrupted) { - markSyncCompletedSendEdgeEventUpdate(); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Exception during sending sync complete", tenantId, edge.getId(), t); - markSyncCompletedSendEdgeEventUpdate(); - } - }, ctx.getGrpcCallbackExecutorService()); - } - } - - protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { - try { - log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); - processHighPriorityEvents(); - PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); - if (isConnected() && !pageData.getData().isEmpty()) { - if (fetcher instanceof GeneralEdgeEventFetcher) { - long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.setDownlinkMsgsLag(edge.getTenantId(), edge.getId(), queueSize)); - } - log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); - List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); - Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Boolean isInterrupted) { - if (Boolean.TRUE.equals(isInterrupted)) { - log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), sessionId); - result.set(null); - } else { - if (isConnected() && pageData.hasNext()) { - processEdgeEvents(fetcher, pageLink.nextPageLink(), result); - } else { - EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1); - UUID idOffset = latestEdgeEvent.getUuidId(); - if (idOffset != null) { - Long newStartTs = Uuids.unixTimestamp(idOffset); - long newStartSeqId = latestEdgeEvent.getSeqId(); - result.set(Pair.of(newStartTs, newStartSeqId)); - } else { - result.set(null); - } - } - } - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}] Failed to send downlink msgs pack", edge.getId(), t); - result.setException(t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); - result.set(null); - } - } catch (Exception e) { - log.error("[{}] Failed to fetch edge events", edge.getId(), e); - result.setException(e); - } - } - - private ConnectResponseMsg processConnect(ConnectRequestMsg request) { - log.trace("[{}] processConnect [{}]", sessionId, request); - Optional optional = ctx.getEdgeService().findEdgeByRoutingKey(TenantId.SYS_TENANT_ID, request.getEdgeRoutingKey()); - if (optional.isPresent()) { - edge = optional.get(); - tenantId = edge.getTenantId(); - try { - if (edge.getSecret().equals(request.getEdgeSecret())) { - sessionOpenListener.accept(edge.getId(), this); - edgeVersion = request.getEdgeVersion(); - processSaveEdgeVersionAsAttribute(request.getEdgeVersion().name()); - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.ACCEPTED) - .setErrorMsg("") - .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)) - .setMaxInboundMessageSize(maxInboundMessageSize) - .build(); - } - String error = "Failed to validate the edge!"; - String failureMsg = String.format("%s Provided request secret: %s", error, request.getEdgeSecret()); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) - .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) - .setErrorMsg(failureMsg) - .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); - } catch (Exception e) { - String failureMsg = "Failed to process edge connection!"; - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) - .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); - log.error(failureMsg, e); - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) - .setErrorMsg(failureMsg) - .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); - } - } - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) - .setErrorMsg("Failed to find the edge! Routing key: " + request.getEdgeRoutingKey()) - .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); - } - - private void processSaveEdgeVersionAsAttribute(String edgeVersion) { - AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry(DataConstants.EDGE_VERSION_ATTR_KEY, edgeVersion), System.currentTimeMillis()); - ctx.getAttributesService().save(tenantId, edge.getId(), AttributeScope.SERVER_SCOPE, attributeKvEntry); - } - - private void interruptGeneralProcessingOnSync() { - log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", tenantId, edge.getId(), sessionId); - stopCurrentSendDownlinkMsgsTask(true); - } - - protected ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { - interruptPreviousSendDownlinkMsgsTask(); - - sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); - sessionState.getPendingMsgsMap().clear(); - - downlinkMsgsPack.forEach(msg -> sessionState.getPendingMsgsMap().put(msg.getDownlinkMsgId(), msg)); - scheduleDownlinkMsgsPackSend(1); - - return sessionState.getSendDownlinkMsgsFuture(); - } - - private void interruptPreviousSendDownlinkMsgsTask() { - if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone() - || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) { - log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", tenantId, edge.getId(), sessionId); - stopCurrentSendDownlinkMsgsTask(true); - } else { - log.trace("[{}][{}][{}] Previous send downlink future is not active", tenantId, edge.getId(), sessionId); - } - } - - private void onUplinkMsg(UplinkMsg uplinkMsg) { - if (isRateLimitViolated(uplinkMsg)) { - return; - } - ListenableFuture> future = processUplinkMsg(uplinkMsg); - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable List result) { - sendResponseMessage(uplinkMsg.getUplinkMsgId(), true, null); - } - - @Override - public void onFailure(Throwable t) { - String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t); - sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); - } - }, ctx.getGrpcCallbackExecutorService()); - } - - private boolean isRateLimitViolated(UplinkMsg uplinkMsg) { - if (!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES, tenantId) || - !ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE, tenantId, edge.getId())) { - String errorMsg = String.format("Failed to process uplink message. %s", RATE_LIMIT_REACHED); - sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); - return true; - } - return false; - } - - private void scheduleDownlinkMsgsPackSend(int attempt) { - Runnable sendDownlinkMsgsTask = () -> { - try { - if (!isConnected()) { - stopCurrentSendDownlinkMsgsTask(true); - return; - } - if (!sessionState.getPendingMsgsMap().values().isEmpty()) { - List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); - if (attempt > 1) { - String error = "Failed to deliver the batch"; - String failureMsg = String.format("{%s} (size: {%s})", error, copy.size()); - if (attempt == 2) { - // Send a failure notification only on the second attempt. - // This ensures that failure alerts are sent just once to avoid redundant notifications. - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) - .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); - } - ctx.getStatsCounterService().ifPresent(statsCounterService -> - statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED, edge.getTenantId(), edge.getId(), 1)); - log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt); - log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy); - } - log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size()); - for (DownlinkMsg downlinkMsg : copy) { - if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) { - String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + - "env variable on the edge and restart it.", clientMaxInboundMessageSize); - String message = String.format("Downlink msg size %s exceeds client max inbound message size %s. " + - "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), clientMaxInboundMessageSize); - log.error("[{}][{}][{}] {} Message {}", tenantId, edge.getId(), sessionId, message, downlinkMsg); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) - .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> - statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), 1)); - sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); - } else { - sendDownlinkMsg(ResponseMsg.newBuilder() - .setDownlinkMsg(downlinkMsg) - .build()); - } - } - if (attempt < MAX_DOWNLINK_ATTEMPTS) { - scheduleDownlinkMsgsPackSend(attempt + 1); - } else { - String failureMsg = String.format("Failed to deliver messages: %s", copy); - log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", - tenantId, edge.getId(), MAX_DOWNLINK_ATTEMPTS, copy); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) - .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) - .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size())); - stopCurrentSendDownlinkMsgsTask(false); - } - } else { - stopCurrentSendDownlinkMsgsTask(false); - } - } catch (Exception e) { - log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, edge.getId(), e.getMessage(), e); - stopCurrentSendDownlinkMsgsTask(true); - } - }; - - if (attempt == 1) { - sendDownlinkExecutorService.submit(sendDownlinkMsgsTask); - } else { - sessionState.setScheduledSendDownlinkTask( - sendDownlinkExecutorService.schedule( - sendDownlinkMsgsTask, - ctx.getEdgeEventStorageSettings().getSleepIntervalBetweenBatches(), - TimeUnit.MILLISECONDS) - ); - } - } - - private void sendResponseMessage(int uplinkMsgId, boolean success, String errorMsg) { - UplinkResponseMsg.Builder responseBuilder = UplinkResponseMsg.newBuilder() - .setUplinkMsgId(uplinkMsgId) - .setSuccess(success); - if (errorMsg != null) { - responseBuilder.setErrorMsg(errorMsg); - } - sendDownlinkMsg(ResponseMsg.newBuilder() - .setUplinkResponseMsg(responseBuilder.build()) - .build()); - } - - private void onDownlinkResponse(DownlinkResponseMsg msg) { - try { - if (msg.getSuccess()) { - sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1)); - log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg); - } else { - log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg()); - DownlinkMsg downlinkMsg = sessionState.getPendingMsgsMap().get(msg.getDownlinkMsgId()); - // if NOT timeseries or attributes failures - ack failed downlink - if (downlinkMsg.getEntityDataCount() == 0) { - sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - } - } - if (sessionState.getPendingMsgsMap().isEmpty()) { - log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getId(), sessionId); - stopCurrentSendDownlinkMsgsTask(false); - } - } catch (Exception e) { - log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, edge.getId(), msg, e); - } - } - - public void processHighPriorityEvents() { - try { - if (isConnected() && !isSyncInProgress()) { - if (highPriorityQueue.isEmpty()) { - return; - } - List highPriorityEvents = new ArrayList<>(); - EdgeEvent event; - while ((event = highPriorityQueue.poll()) != null) { - highPriorityEvents.add(event); - } - log.trace("[{}][{}] Sending high priority events {}", tenantId, edge.getId(), highPriorityEvents.size()); - List downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); - sendDownlinkMsgsPack(downlinkMsgsPack).get(); - } - } catch (Exception e) { - log.error("[{}] Failed to process high priority events", edge.getId(), e); - } - } - - public ListenableFuture processEdgeEvents() throws Exception { - SettableFuture result = SettableFuture.create(); - if (isConnected() && !isSyncInProgress()) { - Pair startTsAndSeqId = getQueueStartTsAndSeqId().get(); - previousStartTs = startTsAndSeqId.getFirst(); - previousStartSeqId = startTsAndSeqId.getSecond(); - GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher( - previousStartTs, - previousStartSeqId, - false, - Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), - ctx.getEdgeEventService()); - log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", - tenantId, edge.getId(), previousStartTs, previousStartSeqId); - Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Pair newStartTsAndSeqId) { - if (newStartTsAndSeqId != null) { - ListenableFuture updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); - Futures.addCallback(updateFuture, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable AttributesSaveResult saveResult) { - log.debug("[{}][{}] queue offset was updated [{}]", tenantId, edge.getId(), newStartTsAndSeqId); - boolean newEventsAvailable; - if (fetcher.isSeqIdNewCycleStarted()) { - newEventsAvailable = isNewEdgeEventsAvailable(); - } else { - newEventsAvailable = isSeqIdStartedNewCycle(); - if (!newEventsAvailable) { - newEventsAvailable = isNewEdgeEventsAvailable(); - } - } - result.set(newEventsAvailable); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, edge.getId(), newStartTsAndSeqId, t); - result.setException(t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, edge.getId()); - result.set(Boolean.FALSE); - } - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to process events", tenantId, edge.getId(), t); - result.setException(t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - if (isSyncInProgress()) { - log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, edge.getId()); - result.set(Boolean.TRUE); - } else { - log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, edge.getId()); - result.set(null); - } - } - return result; - } - - protected List convertToDownlinkMsgsPack(List edgeEvents) { - List result = new ArrayList<>(); - for (EdgeEvent edgeEvent : edgeEvents) { - log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, edge.getId(), edgeEvent); - DownlinkMsg downlinkMsg = null; - try { - switch (edgeEvent.getAction()) { - case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, - ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, - ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { - downlinkMsg = convertEntityEventToDownlink(edgeEvent); - if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { - log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, edge.getId(), downlinkMsg.getDownlinkMsgId()); - } else { - log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg); - } - } - case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> - downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); - default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction()); - } - } catch (Exception e) { - log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, edge.getId(), e); - } - if (downlinkMsg != null) { - result.add(downlinkMsg); - } - } - return result; - } - - private ListenableFuture> getQueueStartTsAndSeqId() { - ListenableFuture> future = - ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), AttributeScope.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY)); - return Futures.transform(future, attributeKvEntries -> { - long startTs = 0L; - long startSeqId = 0L; - for (AttributeKvEntry attributeKvEntry : attributeKvEntries) { - if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) { - startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; - } - if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) { - startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; - } - } - if (startSeqId == 0L) { - startSeqId = findStartSeqIdFromOldestEventIfAny(); - } - return Pair.of(startTs, startSeqId); - }, ctx.getGrpcCallbackExecutorService()); - } - - private boolean isSeqIdStartedNewCycle() { - try { - log.trace("[{}][{}][{}] Checking if seq id started new cycle", tenantId, edge.getId(), sessionId); - TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); - PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, previousStartSeqId == 0 ? null : previousStartSeqId - 1, pageLink); - boolean result = !edgeEvents.getData().isEmpty(); - log.trace("[{}][{}][{}] Result of check if seq id started new cycle, result = {}", tenantId, edge.getId(), sessionId, result); - return result; - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", tenantId, edge.getId(), sessionId, e); - } - return false; - } - - private boolean isNewEdgeEventsAvailable() { - try { - log.trace("[{}][{}][{}] Checking if new edge events available", tenantId, edge.getId(), sessionId); - TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); - PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), newStartSeqId, null, pageLink); - boolean result = !edgeEvents.getData().isEmpty() || !highPriorityQueue.isEmpty(); - log.trace("[{}][{}][{}] Result of check if new edge events available, result = {}", tenantId, edge.getId(), sessionId, result); - return result; - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", tenantId, edge.getId(), sessionId, e); - } - return false; - } - - private long findStartSeqIdFromOldestEventIfAny() { - long startSeqId = 0L; - try { - TimePageLink pageLink = new TimePageLink(1, 0, null, null, null, null); - PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink); - if (!edgeEvents.getData().isEmpty()) { - startSeqId = edgeEvents.getData().get(0).getSeqId() - 1; - } - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", tenantId, edge.getId(), sessionId, e); - } - return startSeqId; - } - - private ListenableFuture updateQueueStartTsAndSeqId(Pair pair) { - newStartTs = pair.getFirst(); - newStartSeqId = pair.getSecond(); - log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", sessionId, edge.getId(), newStartTs, newStartSeqId); - List attributes = List.of( - new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()), - new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis()) - ); - return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), AttributeScope.SERVER_SCOPE, attributes); - } - - protected ListenableFuture> startProcessingEdgeEvents(EdgeEventFetcher fetcher) { - SettableFuture> result = SettableFuture.create(); - PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()); - processEdgeEvents(fetcher, pageLink, result); - return result; - } - - private void markSyncCompletedSendEdgeEventUpdate() { - syncInProgress = false; - ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId())); - } - - private void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) { - if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) { - sessionState.getSendDownlinkMsgsFuture().set(isInterrupted); - } - if (sessionState.getScheduledSendDownlinkTask() != null) { - sessionState.getScheduledSendDownlinkTask().cancel(true); - } - } - - private void sendDownlinkMsg(ResponseMsg responseMsg) { - if (isConnected()) { - String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000); - log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, edge.getId(), responseMsgStr); - downlinkMsgLock.lock(); - String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr; - try { - outputStream.onNext(responseMsg); - } catch (Exception e) { - log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, edge.getId(), downlinkMsgStr, e); - connected = false; - sessionCloseListener.accept(edge, sessionId); - } finally { - downlinkMsgLock.unlock(); - } - log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, edge.getId(), downlinkMsgStr); - } - } - - protected DownlinkMsg convertEntityEventToDownlink(EdgeEvent edgeEvent) { - log.trace("[{}] Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", edgeEvent.getTenantId(), edgeEvent, edgeEvent.getAction()); - if ((EdgeEventType.OAUTH2_CLIENT.equals(edgeEvent.getType()) || EdgeEventType.DOMAIN.equals(edgeEvent.getType())) && - (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0))) { - return null; - } - - return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, edgeVersion); - } - - public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { - while (highPriorityQueue.size() > maxHighPriorityQueueSizePerSession) { - EdgeEvent oldestHighPriority = highPriorityQueue.poll(); - if (oldestHighPriority != null) { - log.warn("[{}][{}][{}] High priority queue is full. Removing oldest high priority event from queue {}", - tenantId, edge.getId(), sessionId, oldestHighPriority); - } - } - highPriorityQueue.add(edgeEvent); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1)); - } - - protected ListenableFuture> processUplinkMsg(UplinkMsg uplinkMsg) { - List> result = new ArrayList<>(); - try { - if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { - for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { - result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg)); - } - } - if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { - for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); - } - } - if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { - for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsgFromEdge(edge.getTenantId(), edge.getId(), deviceCredentialsUpdateMsg)); - } - } - if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) { - for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) { - result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg)); - } - } - if (uplinkMsg.getAssetUpdateMsgCount() > 0) { - for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) { - result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); - } - } - if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) { - for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) { - result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg)); - } - } - if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { - for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) { - result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg)); - } - } - if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) { - for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) { - result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg)); - } - } - if (uplinkMsg.getEntityDataCount() > 0) { - for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { - result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData)); - } - } - if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { - for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { - result.add(ctx.getAlarmProcessor().processAlarmMsgFromEdge(edge.getTenantId(), edge.getId(), alarmUpdateMsg)); - } - } - if (uplinkMsg.getAlarmCommentUpdateMsgCount() > 0) { - for (AlarmCommentUpdateMsg alarmCommentUpdateMsg : uplinkMsg.getAlarmCommentUpdateMsgList()) { - result.add(ctx.getAlarmCommentProcessor().processAlarmCommentMsgFromEdge(edge.getTenantId(), edge.getId(), alarmCommentUpdateMsg)); - } - } - if (uplinkMsg.getRelationUpdateMsgCount() > 0) { - for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { - result.add(ctx.getRelationProcessor().processRelationMsgFromEdge(edge.getTenantId(), edge, relationUpdateMsg)); - } - } - if (uplinkMsg.getDashboardUpdateMsgCount() > 0) { - for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) { - result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg)); - } - } - if (uplinkMsg.getResourceUpdateMsgCount() > 0) { - for (ResourceUpdateMsg resourceUpdateMsg : uplinkMsg.getResourceUpdateMsgList()) { - result.add(ctx.getResourceProcessor().processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg)); - } - } - if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { - for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); - } - } - if (uplinkMsg.getAttributesRequestMsgCount() > 0) { - for (AttributesRequestMsg attributesRequestMsg : uplinkMsg.getAttributesRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processAttributesRequestMsg(edge.getTenantId(), edge, attributesRequestMsg)); - } - } - if (uplinkMsg.getRelationRequestMsgCount() > 0) { - for (RelationRequestMsg relationRequestMsg : uplinkMsg.getRelationRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); - } - } - if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) { - for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg)); - } - } - if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { - for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); - } - } - if (uplinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { - for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg : uplinkMsg.getDeviceCredentialsRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processDeviceCredentialsRequestMsg(edge.getTenantId(), edge, deviceCredentialsRequestMsg)); - } - } - if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) { - for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceRpcCallFromEdge(edge.getTenantId(), edge, deviceRpcCallMsg)); - } - } - if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) { - for (WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg : uplinkMsg.getWidgetBundleTypesRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processWidgetBundleTypesRequestMsg(edge.getTenantId(), edge, widgetBundleTypesRequestMsg)); - } - } - if (uplinkMsg.getEntityViewsRequestMsgCount() > 0) { - for (EntityViewsRequestMsg entityViewRequestMsg : uplinkMsg.getEntityViewsRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg)); - } - } - if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { - for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) { - result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); - } - } - } catch (Exception e) { - String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); - log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) - .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); - return Futures.immediateFailedFuture(e); - } - return Futures.allAsList(result); - } - - protected boolean destroy() { - return true; - } - - protected void cleanUp() {} - - @Override - public void close() { - log.debug("[{}][{}] Closing session", tenantId, sessionId); - connected = false; - try { - outputStream.onCompleted(); - } catch (Exception e) { - log.debug("[{}][{}] Failed to close output stream: {}", tenantId, sessionId, e.getMessage()); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java index 7e06c01bd3..af51247720 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -20,7 +20,6 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; -import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import java.util.function.Consumer; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java index be79b51e19..3cb3920bfb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java @@ -16,19 +16,70 @@ package org.thingsboard.server.service.edge.rpc; import com.google.common.util.concurrent.SettableFuture; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; -@Data +@Getter public class EdgeSessionState { - private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); + @Setter private SettableFuture sendDownlinkMsgsFuture; + @Setter private ScheduledFuture scheduledSendDownlinkTask; + @Setter + private volatile boolean connected; + @Setter + private EdgeVersion edgeVersion; + private final UUID sessionId = UUID.randomUUID(); + private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Lock sequenceDependencyLock = new ReentrantLock(); + private final AtomicBoolean syncInProgress = new AtomicBoolean(false); + private TenantId tenantId; + private Edge edge; + + public void setEdge(Edge edge) { + if (edge == null) { + return; + } + this.edge = edge; + this.tenantId = edge.getTenantId(); + } + + public EdgeId getEdgeId() { + return edge != null ? edge.getId() : null; + } + + public boolean tryStartSync() { + return syncInProgress.compareAndSet(false, true); + } + + public void finishSync() { + syncInProgress.set(false); + } + + public boolean isSyncInProgress() { + return syncInProgress.get(); + } + + public void lockSequenceDependency() { + sequenceDependencyLock.lock(); + } + public void unlockSequenceDependency() { + sequenceDependencyLock.unlock(); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index ce175957f6..e9134a0b60 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.AiModelEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; @@ -66,6 +67,12 @@ public class EdgeSyncCursor { fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), TenantId.SYS_TENANT_ID)); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), edge.getTenantId())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); + fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); + fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); + fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); + fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); + fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); + fetchers.add(new AiModelEdgeEventFetcher(ctx.getAiModelService())); } Customer publicCustomer = ctx.getCustomerService().findPublicCustomer(edge.getTenantId()); if (publicCustomer != null) { @@ -86,14 +93,10 @@ public class EdgeSyncCursor { fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); fetchers.add(new NotificationRuleEdgeEventFetcher(ctx.getNotificationRuleService())); - fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); - fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); - fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); - fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); + // sync device profiles twice to update software and hardware fields fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); - fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java new file mode 100644 index 0000000000..e5dd0738ac --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java @@ -0,0 +1,238 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; +import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; +import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeContextComponent; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class EdgeUplinkMessageDispatcher { + + private final EdgeContextComponent ctx; + + public ListenableFuture> processUplinkMsg(EdgeSessionState state, UplinkMsg uplinkMsg) { + Edge edge = state.getEdge(); + List> result = new ArrayList<>(); + try { + if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { + for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { + result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg)); + } + } + if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { + for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { + for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsgFromEdge(edge.getTenantId(), edge.getId(), deviceCredentialsUpdateMsg)); + } + } + if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) { + for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) { + result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg)); + } + } + if (uplinkMsg.getAssetUpdateMsgCount() > 0) { + for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) { + result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) { + for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { + for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg)); + } + } + if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) { + for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) { + result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg)); + } + } + if (uplinkMsg.getEntityDataCount() > 0) { + for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { + result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData)); + } + } + if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { + for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { + result.add(ctx.getAlarmProcessor().processAlarmMsgFromEdge(edge.getTenantId(), edge.getId(), alarmUpdateMsg)); + } + } + if (uplinkMsg.getAlarmCommentUpdateMsgCount() > 0) { + for (AlarmCommentUpdateMsg alarmCommentUpdateMsg : uplinkMsg.getAlarmCommentUpdateMsgList()) { + result.add(ctx.getAlarmCommentProcessor().processAlarmCommentMsgFromEdge(edge.getTenantId(), edge.getId(), alarmCommentUpdateMsg)); + } + } + if (uplinkMsg.getRelationUpdateMsgCount() > 0) { + for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { + result.add(ctx.getRelationProcessor().processRelationMsgFromEdge(edge.getTenantId(), edge, relationUpdateMsg)); + } + } + if (uplinkMsg.getDashboardUpdateMsgCount() > 0) { + for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) { + result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg)); + } + } + if (uplinkMsg.getResourceUpdateMsgCount() > 0) { + for (ResourceUpdateMsg resourceUpdateMsg : uplinkMsg.getResourceUpdateMsgList()) { + result.add(ctx.getResourceProcessor().processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { + for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); + } + } + if (uplinkMsg.getAttributesRequestMsgCount() > 0) { + for (AttributesRequestMsg attributesRequestMsg : uplinkMsg.getAttributesRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processAttributesRequestMsg(edge.getTenantId(), edge, attributesRequestMsg)); + } + } + if (uplinkMsg.getRelationRequestMsgCount() > 0) { + for (RelationRequestMsg relationRequestMsg : uplinkMsg.getRelationRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); + } + } + if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) { + for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg)); + } + } + if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { + for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { + for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg : uplinkMsg.getDeviceCredentialsRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processDeviceCredentialsRequestMsg(edge.getTenantId(), edge, deviceCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) { + for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceRpcCallFromEdge(edge.getTenantId(), edge, deviceRpcCallMsg)); + } + } + if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) { + for (WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg : uplinkMsg.getWidgetBundleTypesRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processWidgetBundleTypesRequestMsg(edge.getTenantId(), edge, widgetBundleTypesRequestMsg)); + } + } + if (uplinkMsg.getEntityViewsRequestMsgCount() > 0) { + for (EntityViewsRequestMsg entityViewRequestMsg : uplinkMsg.getEntityViewsRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg)); + } + } + if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { + for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) { + result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); + } + } + if (uplinkMsg.getAiModelUpdateMsgCount() > 0) { + for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) { + result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg)); + } + } + if (uplinkMsg.getUserUpdateMsgCount() > 0) { + for (UserUpdateMsg userUpdateMsg : uplinkMsg.getUserUpdateMsgList()) { + state.lockSequenceDependency(); + try { + result.add(ctx.getUserProcessor().processUserMsgFromEdge(edge.getTenantId(), edge, userUpdateMsg)); + } finally { + state.unlockSequenceDependency(); + } + } + } + if (uplinkMsg.getUserCredentialsUpdateMsgCount() > 0) { + for (UserCredentialsUpdateMsg userCredentialsUpdateMsg : uplinkMsg.getUserCredentialsUpdateMsgList()) { + state.lockSequenceDependency(); + try { + result.add(ctx.getUserProcessor().processUserCredentialsMsgFromEdge(edge.getTenantId(), edge, userCredentialsUpdateMsg)); + } finally { + state.unlockSequenceDependency(); + } + } + } + if (uplinkMsg.getApiKeyUpdateMsgCount() > 0) { + for (ApiKeyUpdateMsg apiKeyUpdateMsg : uplinkMsg.getApiKeyUpdateMsgList()) { + state.lockSequenceDependency(); + try { + result.add(ctx.getApiKeyProcessor().processApiKeyMsgFromEdge(edge.getTenantId(), edge, apiKeyUpdateMsg)); + } finally { + state.unlockSequenceDependency(); + } + } + } + } catch (Exception e) { + String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); + log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), edge.getId(), uplinkMsg, e); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); + return Futures.immediateFailedFuture(e); + } + return Futures.allAsList(result); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java new file mode 100644 index 0000000000..0b19f10aa4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc; + +import io.grpc.Server; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.transport.config.ssl.PemSslCredentials; +import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc.EdgeRpcServiceImplBase; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class GrpcServer { + + @Value("${edges.rpc.port}") + private int rpcPort; + @Value("${edges.rpc.ssl.enabled}") + private boolean sslEnabled; + @Value("${edges.rpc.ssl.cert}") + private String certFileResource; + @Value("${edges.rpc.ssl.private_key}") + private String privateKeyResource; + @Value("${edges.rpc.ssl.key_password:}") + private String keyPassword; + @Value("${edges.rpc.client_max_keep_alive_time_sec:1}") + private int clientMaxKeepAliveTimeSec; + @Value("${edges.rpc.max_inbound_message_size:4194304}") + private int maxInboundMessageSize; + @Value("${edges.rpc.keep_alive_time_sec:10}") + private int keepAliveTimeSec; + @Value("${edges.rpc.keep_alive_timeout_sec:5}") + private int keepAliveTimeoutSec; + + private final EdgeRpcServiceImplBase edgeGrpcService; + + private Server server; + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void onStartUp() { + log.info("Initializing Edge RPC server!"); + NettyServerBuilder serverBuilder = NettyServerBuilder.forPort(rpcPort) + .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS) + .permitKeepAliveWithoutCalls(true) + .maxInboundMessageSize(maxInboundMessageSize) + .addService(edgeGrpcService); + + if (sslEnabled) { + setupSsl(serverBuilder); + } + server = serverBuilder.build(); + log.info("Going to start Edge RPC server using port: {}", rpcPort); + try { + server.start(); + } catch (IOException e) { + log.error("Failed to start Edge RPC server!", e); + throw new RuntimeException("Failed to start Edge RPC server!", e); + } + log.info("Edge RPC server initialized!"); + } + + /** + * Configures TLS for the Edge gRPC server. + *

+ * Delegates PEM parsing and key management to {@link PemSslCredentials} — the same + * class used by MQTT, CoAP, and LwM2M transports — which supports: + *

    + *
  • Separate certificate and private key files (classic two-file setup)
  • + *
  • Combined PEM: certificate chain + private key in a single {@code cert} file + * ({@code private_key} left empty)
  • + *
  • Encrypted private keys (password supplied via {@code key_password})
  • + *
+ * Path resolution (for both {@code cert} and {@code private_key}) is handled by + * {@link org.thingsboard.server.common.data.ResourceUtils#getInputStream ResourceUtils}: + * absolute path → relative / working-dir → classpath → {@code classpath:} prefix. + */ + void setupSsl(NettyServerBuilder builder) { + try { + PemSslCredentials credentials = new PemSslCredentials(); + credentials.setCertFile(certFileResource); + credentials.setKeyFile(StringUtils.isEmpty(privateKeyResource) ? null : privateKeyResource); + credentials.setKeyPassword(keyPassword); + credentials.init(false); + + SslContext sslContext = GrpcSslContexts.configure( + SslContextBuilder.forServer(credentials.createKeyManagerFactory())).build(); + builder.sslContext(sslContext); + } catch (Exception e) { + log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); + throw new RuntimeException("Unable to set up SSL context!", e); + } + } + + @PreDestroy + public void destroy() { + if (server != null) { + server.shutdownNow(); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java index 0f52af6c4d..e9ba942afb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java @@ -52,7 +52,8 @@ public class KafkaEdgeEventService extends BaseEdgeEventService { TopicPartitionInfo tpi = topicService.getEdgeEventNotificationsTopic(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); ToEdgeEventNotificationMsg msg = ToEdgeEventNotificationMsg.newBuilder().setEdgeEventMsg(ProtoUtils.toProto(edgeEvent)).build(); producerProvider.getTbEdgeEventsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); - statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); + statsCounterService.ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); return Futures.immediateFuture(null); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java deleted file mode 100644 index f7e729c6b7..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import io.grpc.stub.StreamObserver; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.util.ProtoUtils; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; -import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; -import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.KafkaAdmin; -import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.service.edge.EdgeContextComponent; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.BiConsumer; - -@Slf4j -public class KafkaEdgeGrpcSession extends EdgeGrpcSession { - - private final TopicService topicService; - private final TbCoreQueueFactory tbCoreQueueFactory; - private final KafkaAdmin kafkaAdmin; - - private volatile boolean isHighPriorityProcessing; - - @Getter - private QueueConsumerManager> consumer; - - private ExecutorService consumerExecutor; - - public KafkaEdgeGrpcSession(EdgeContextComponent ctx, TopicService topicService, TbCoreQueueFactory tbCoreQueueFactory, - KafkaAdmin kafkaAdmin, StreamObserver outputStream, - BiConsumer sessionOpenListener, BiConsumer sessionCloseListener, - ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { - super(ctx, outputStream, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); - this.topicService = topicService; - this.tbCoreQueueFactory = tbCoreQueueFactory; - this.kafkaAdmin = kafkaAdmin; - } - - private void processMsgs(List> msgs, TbQueueConsumer> consumer) { - log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId()); - if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { - log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + - "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", - tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing); - return; - } - List edgeEvents = new ArrayList<>(); - for (TbProtoQueueMsg msg : msgs) { - EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); - edgeEvents.add(edgeEvent); - } - List downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); - boolean isInterrupted = true; - try { - isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); - if (isInterrupted) { - log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId()); - } - } catch (Exception e) { - log.error("[{}][{}] Failed to process downlink messages", tenantId, edge.getId(), e); - } - if (!isInterrupted) { - consumer.commit(); - } - } - - @Override - public ListenableFuture migrateEdgeEvents() throws Exception { - return super.processEdgeEvents(); - } - - @Override - public ListenableFuture processEdgeEvents() { - if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { - log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer", - tenantId, edge != null ? edge.getId() : null, isConnected(), isSyncInProgress(), isHighPriorityProcessing); - return Futures.immediateFuture(Boolean.FALSE); - } - if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) { - try { - if (consumerExecutor != null && !consumerExecutor.isShutdown()) { - try { - consumerExecutor.shutdown(); - awaitConsumerTermination(); - } catch (Exception e) { - log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edge.getId(), e); - } - } - this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); - this.consumer = QueueConsumerManager.>builder() - .name("TB Edge events [" + edge.getId() + "]") - .msgPackProcessor(this::processMsgs) - .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) - .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId())) - .consumerExecutor(consumerExecutor) - .threadPrefix("edge-events-" + edge.getId()) - .build(); - consumer.subscribe(); - consumer.launch(); - } catch (Exception e) { - destroy(); - log.warn("[{}][{}] Failed to start edge event consumer", sessionId, edge.getId(), e); - } - } - return Futures.immediateFuture(Boolean.FALSE); - } - - @Override - public void processHighPriorityEvents() { - isHighPriorityProcessing = true; - super.processHighPriorityEvents(); - isHighPriorityProcessing = false; - } - - @Override - public boolean destroy() { - try { - if (consumer != null) { - log.info("[{}][{}] Stopping edge event consumer...", tenantId, edge != null ? edge.getId() : null); - consumer.stop(); - } - } catch (Exception e) { - log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e); - return false; - } - consumer = null; - try { - if (consumerExecutor != null && !consumerExecutor.isShutdown()) { - consumerExecutor.shutdown(); - awaitConsumerTermination(); - } - } catch (Exception e) { - log.warn("[{}][{}] Failed to shutdown edge event consumer executor", tenantId, edge.getId(), e); - return false; - } - return true; - } - - private void awaitConsumerTermination() { - try { - consumerExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS); - } catch (InterruptedException ie) { - log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", tenantId, edge.getId()); - } - } - - @Override - public void cleanUp() { - String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic(); - kafkaAdmin.deleteTopic(topic); - kafkaAdmin.deleteConsumerGroup(topic); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/PostgresEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/PostgresEdgeGrpcSession.java deleted file mode 100644 index bfeb3f09aa..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/PostgresEdgeGrpcSession.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import io.grpc.stub.StreamObserver; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.service.edge.EdgeContextComponent; - -import java.util.UUID; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.BiConsumer; - -@Slf4j -public class PostgresEdgeGrpcSession extends EdgeGrpcSession { - - PostgresEdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, - BiConsumer sessionOpenListener, - BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, - int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { - super(ctx, outputStream, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); - } - - @Override - public ListenableFuture migrateEdgeEvents() { - return Futures.immediateFuture(Boolean.FALSE); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java new file mode 100644 index 0000000000..f5ce48f696 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +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.ai.AiModelService; + +@AllArgsConstructor +@Slf4j +public class AiModelEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private final AiModelService aiModelService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return aiModelService.findAiModelsByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, AiModel aiModel) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, + EdgeEventActionType.ADDED, aiModel.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java index 3c8dc6ead6..e5dcc4410f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java @@ -26,13 +26,9 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.edge.EdgeEventService; -import java.util.concurrent.TimeUnit; - @AllArgsConstructor @Slf4j public class GeneralEdgeEventFetcher implements EdgeEventFetcher { - // Subtract from queueStartTs to ensure no data is lost due to potential misordering of edge events by created_time. - private static final long MISORDERING_COMPENSATION_MILLIS = TimeUnit.SECONDS.toMillis(60); private final Long queueStartTs; private Long seqIdStart; @@ -40,6 +36,10 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { private boolean seqIdNewCycleStarted; private Long maxReadRecordsCount; private final EdgeEventService edgeEventService; + // Subtract from queueStartTs to compensate for possible misalignment between `created_time` and `seqId`. + // This ensures early events with lower seqId are not skipped due to partitioning by `created_time`. + // See: edge_event is partitioned by created_time but sorted by seqId during retrieval. + private final long misorderingCompensationMillis; @Override public PageLink getPageLink(int pageSize) { @@ -48,7 +48,7 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { 0, null, null, - queueStartTs > 0 ? queueStartTs - MISORDERING_COMPENSATION_MILLIS : 0, + queueStartTs > 0 ? queueStartTs - misorderingCompensationMillis : 0, System.currentTimeMillis()); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 5cfd16aa90..8455d86833 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -19,12 +19,17 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -37,6 +42,7 @@ import org.thingsboard.server.common.data.id.EdgeId; 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.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -61,6 +67,7 @@ import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -139,7 +146,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { UPDATED_COMMENT, DELETED -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, - WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE, + WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, API_KEY, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; default -> false; }; @@ -348,6 +355,29 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { edgeCtx.getRelationService().saveRelation(tenantId, relation); } + protected > void pushEntityEventToRuleEngine(TenantId tenantId, T entity, TbMsgType msgType) { + pushEntityEventToRuleEngine(tenantId, null, entity, msgType); + } + + protected > void pushEntityEventToRuleEngine(TenantId tenantId, Edge edge, T entity, TbMsgType msgType) { + try { + String entityAsString = JacksonUtil.toString(entity); + CustomerId customerId = getCustomerId(entity); + TbMsgMetaData tbMsgMetaData = edge == null ? TbMsgMetaData.EMPTY : getEdgeActionTbMsgMetaData(edge, customerId); + + pushEntityEventToRuleEngine(tenantId, entity.getId(), customerId, msgType, entityAsString, tbMsgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push entity action for {} to rule engine: {}", tenantId, entity.getId(), entity.getId().getEntityType(), msgType.name(), e); + } + } + + private > CustomerId getCustomerId(T entity) { + if (entity instanceof HasCustomerId hasCustomer) { + return hasCustomer.getCustomerId(); + } + return null; + } + protected TbMsgMetaData getEdgeActionTbMsgMetaData(Edge edge, CustomerId customerId) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("edgeId", edge.getId().toString()); @@ -381,4 +411,29 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { }); } + protected boolean isSaveRequired(HasVersion current, HasVersion updated) { + if (current != null) { + current.setVersion(null); + } + updated.setVersion(null); + return !updated.equals(current); + } + + protected > Optional generateUniqueNameIfDuplicateExists( + TenantId tenantId, I entityId, E entity, @Nullable E entityWithSameName) { + + if (entityWithSameName == null || entityWithSameName.getId().equals(entityId)) { + return Optional.empty(); + } + String currentName = entity.getName(); + String newEntityName = generateRandomAlphabeticString(currentName); + + log.warn("[{}] Entity with name '{}' already exists (id={}). Renaming to '{}'", tenantId, currentName, entityWithSameName.getId(), newEntityName); + return Optional.of(newEntityName); + } + + protected static String generateRandomAlphabeticString(String prefix) { + return prefix + "_" + StringUtils.randomAlphabetic(15); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeEventsDispatcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeEventsDispatcher.java new file mode 100644 index 0000000000..f218148d30 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeEventsDispatcher.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor; + +import com.google.common.util.concurrent.ListenableFuture; + +public interface EdgeEventsDispatcher { + + ListenableFuture processNewEvents() throws Exception; +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/PostgresGeneralEdgeEventsDispatcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/PostgresGeneralEdgeEventsDispatcher.java new file mode 100644 index 0000000000..d483706485 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/PostgresGeneralEdgeEventsDispatcher.java @@ -0,0 +1,218 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor; + +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.SettableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.session.EdgeSession; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +@Slf4j +@RequiredArgsConstructor +public class PostgresGeneralEdgeEventsDispatcher implements EdgeEventsDispatcher { + + private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; + private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId"; + + private final EdgeSession session; + private final EdgeContextComponent ctx; + + private Long newStartTs; + private Long previousStartTs; + private Long newStartSeqId; + private Long previousStartSeqId; + + @Override + public ListenableFuture processNewEvents() throws Exception { + EdgeSessionState state = session.getState(); + TenantId tenantId = state.getTenantId(); + EdgeId edgeId = state.getEdgeId(); + UUID sessionId = state.getSessionId(); + + SettableFuture result = SettableFuture.create(); + if (state.isConnected() && !state.isSyncInProgress()) { + setLocalPrevStartTsAndSeqId(tenantId, edgeId, sessionId); + GeneralEdgeEventFetcher fetcher = initGeneralEdgeEventFetcher(); + log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", + tenantId, edgeId, previousStartTs, previousStartSeqId); + + Futures.addCallback(session.fetchAndSendEdgeEvents(fetcher), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Pair newStartTsAndSeqId) { + if (newStartTsAndSeqId != null) { + Futures.addCallback(updateQueueStartTsAndSeqId(tenantId, edgeId, newStartTsAndSeqId), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable AttributesSaveResult saveResult) { + log.debug("[{}][{}] queue offset was updated [{}]", tenantId, edgeId, newStartTsAndSeqId); + boolean newEventsAvailable; + if (fetcher.isSeqIdNewCycleStarted()) { + newEventsAvailable = isNewEdgeEventsAvailable(tenantId, edgeId, sessionId); + } else { + newEventsAvailable = isSeqIdStartedNewCycle(tenantId, edgeId, sessionId); + if (!newEventsAvailable) { + newEventsAvailable = isNewEdgeEventsAvailable(tenantId, edgeId, sessionId); + } + } + result.set(newEventsAvailable); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, edgeId, newStartTsAndSeqId, t); + result.setException(t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, edgeId); + result.set(Boolean.FALSE); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to process events", tenantId, edgeId, t); + result.setException(t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + if (state.isSyncInProgress()) { + log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, edgeId); + result.set(Boolean.TRUE); + } else { + log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, edgeId); + result.set(null); + } + } + return result; + } + + private boolean isSeqIdStartedNewCycle(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + try { + log.trace("[{}][{}][{}] Checking if seq id started new cycle", tenantId, edgeId, sessionId); + TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); + PageData edgeEvents = ctx.getEdgeEventService() + .findEdgeEvents(tenantId, edgeId, 0L, previousStartSeqId == 0 ? null : previousStartSeqId - 1, pageLink); + + boolean result = !edgeEvents.getData().isEmpty(); + log.trace("[{}][{}][{}] Result of check if seq id started new cycle, result = {}", tenantId, edgeId, sessionId, result); + return result; + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", tenantId, edgeId, sessionId, e); + } + return false; + } + + private boolean isNewEdgeEventsAvailable(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + try { + log.trace("[{}][{}][{}] Checking if new edge events available", tenantId, edgeId, sessionId); + TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); + PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(tenantId, edgeId, newStartSeqId, null, pageLink); + boolean available = !edgeEvents.getData().isEmpty() || session.hasHighPriorityEvents(); + log.trace("[{}][{}][{}] Result of check if new edge events available, result = {}", tenantId, edgeId, sessionId, available); + return available; + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", tenantId, edgeId, sessionId, e); + } + return false; + } + + private ListenableFuture updateQueueStartTsAndSeqId(TenantId tenantId, EdgeId edgeId, Pair pair) { + newStartTs = pair.getFirst(); + newStartSeqId = pair.getSecond(); + log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", session.getState().getSessionId(), edgeId, newStartTs, newStartSeqId); + List attributes = List.of( + new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()), + new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis()) + ); + return ctx.getAttributesService().save(tenantId, edgeId, AttributeScope.SERVER_SCOPE, attributes); + } + + private GeneralEdgeEventFetcher initGeneralEdgeEventFetcher() { + return new GeneralEdgeEventFetcher( + previousStartTs, + previousStartSeqId, + false, + Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), + ctx.getEdgeEventService(), + ctx.getEdgeEventStorageSettings().getMisorderingCompensationMillis()); + } + + private void setLocalPrevStartTsAndSeqId(TenantId tenantId, EdgeId edgeId, UUID sessionId) throws InterruptedException, ExecutionException { + Pair startTsAndSeqId = getQueueStartTsAndSeqId(tenantId, edgeId, sessionId).get(); + previousStartTs = startTsAndSeqId.getFirst(); + previousStartSeqId = startTsAndSeqId.getSecond(); + } + + private ListenableFuture> getQueueStartTsAndSeqId(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + ListenableFuture> future = ctx.getAttributesService() + .find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY)); + + return Futures.transform(future, attributeKvEntries -> { + long startTs = 0L; + long startSeqId = 0L; + for (AttributeKvEntry attributeKvEntry : attributeKvEntries) { + if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) { + startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } + if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) { + startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } + } + if (startSeqId == 0L) { + startSeqId = findStartSeqIdFromOldestEventIfAny(tenantId, edgeId, sessionId); + } + return Pair.of(startTs, startSeqId); + }, ctx.getGrpcCallbackExecutorService()); + } + + private long findStartSeqIdFromOldestEventIfAny(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + long startSeqId = 0L; + try { + TimePageLink pageLink = new TimePageLink(1, 0, null, null, null, null); + PageData edgeEvents = ctx.getEdgeEventService() + .findEdgeEvents(tenantId, edgeId, null, null, pageLink); + if (!edgeEvents.getData().isEmpty()) { + startSeqId = edgeEvents.getData().get(0).getSeqId() - 1; + } + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", tenantId, edgeId, sessionId, e); + } + return startSeqId; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java new file mode 100644 index 0000000000..afb44d4607 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.ai; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Component +@TbCoreComponent +public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiModelProcessor { + + @Override + public ListenableFuture processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg) { + AiModelId aiModelId = new AiModelId(new UUID(aiModelUpdateMsg.getIdMSB(), aiModelUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + return switch (aiModelUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> { + processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge); + yield Futures.immediateFuture(null); + } + case ENTITY_DELETED_RPC_MESSAGE -> { + deleteAiModel(tenantId, edge, aiModelId); + yield Futures.immediateFuture(null); + } + default -> handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType()); + }; + } catch (DataValidationException e) { + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + } + + @Override + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + AiModelId aiModelId = new AiModelId(edgeEvent.getEntityId()); + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + Optional aiModel = edgeCtx.getAiModelService().findAiModelById(edgeEvent.getTenantId(), aiModelId); + if (aiModel.isPresent()) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelUpdatedMsg(msgType, aiModel.get()); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAiModelUpdateMsg(aiModelUpdateMsg) + .build(); + } + } + case DELETED -> { + AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelDeleteMsg(aiModelId); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAiModelUpdateMsg(aiModelUpdateMsg) + .build(); + } + } + return null; + } + + private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg); + Boolean created = resultPair.getFirst(); + if (created) { + Optional aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); + aiModel.ifPresent(model -> pushEntityEventToRuleEngine(tenantId, edge, model, TbMsgType.ENTITY_CREATED)); + } + Boolean aiModelNameUpdated = resultPair.getSecond(); + if (aiModelNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null); + } + } + + @Override + public EdgeEventType getEdgeEventType() { + return EdgeEventType.AI_MODEL; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java new file mode 100644 index 0000000000..c8ae278608 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.ai; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface AiModelProcessor extends EdgeProcessor { + + ListenableFuture processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java new file mode 100644 index 0000000000..ccc54f75d9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.ai; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.Optional; + +@Slf4j +public abstract class BaseAiModelProcessor extends BaseEdgeProcessor { + + @Autowired + private DataValidator aiModelValidator; + + protected Pair saveOrUpdateAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg) { + boolean isCreated = false; + boolean isNameUpdated = false; + try { + AiModel aiModel = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true); + if (aiModel == null) { + throw new RuntimeException("[{" + tenantId + "}] aiModelUpdateMsg {" + aiModelUpdateMsg + " } cannot be converted to aiModel"); + } + + Optional aiModelById = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); + if (aiModelById.isEmpty()) { + aiModel.setCreatedTime(Uuids.unixTimestamp(aiModelId.getId())); + isCreated = true; + aiModel.setId(null); + } else { + aiModel.setId(aiModelId); + } + + String aiModelName = aiModel.getName(); + Optional aiModelByName = edgeCtx.getAiModelService().findAiModelByTenantIdAndName(aiModel.getTenantId(), aiModelName); + if (aiModelByName.isPresent() && !aiModelByName.get().getId().equals(aiModelId)) { + aiModelName = aiModelName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] aiModel with name {} already exists. Renaming aiModel name to {}", + tenantId, aiModel.getName(), aiModelByName.get().getName()); + isNameUpdated = true; + } + aiModel.setName(aiModelName); + + aiModelValidator.validate(aiModel, AiModel::getTenantId); + + if (isCreated) { + aiModel.setId(aiModelId); + } + + edgeCtx.getAiModelService().save(aiModel, false); + } catch (Exception e) { + log.error("[{}] Failed to process aiModel update msg [{}]", tenantId, aiModelUpdateMsg, e); + throw e; + } + return Pair.of(isCreated, isNameUpdated); + } + + protected void deleteAiModel(TenantId tenantId, Edge edge, AiModelId aiModelId) { + Optional aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); + if (aiModel.isPresent()) { + edgeCtx.getAiModelService().deleteByTenantIdAndId(tenantId, aiModelId); + pushEntityEventToRuleEngine(tenantId, edge, aiModel.get(), TbMsgType.ENTITY_DELETED); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index 638ed9693a..bfdc39313d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -77,7 +77,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { case ALARM_CLEAR_RPC_MESSAGE: Alarm alarmToClear = edgeCtx.getAlarmService().findAlarmById(tenantId, alarmId); if (alarmToClear != null) { - edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails()); + edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails(), true); } break; case ENTITY_DELETED_RPC_MESSAGE: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java new file mode 100644 index 0000000000..960c0dd695 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.apikey; + +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.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.ApiKeyId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; + +import java.util.UUID; + +@Slf4j +@Component +@TbCoreComponent +public class ApiKeyEdgeProcessor extends BaseApiKeyProcessor implements ApiKeyProcessor { + + @Override + public ListenableFuture processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg) { + ApiKeyId apiKeyId = new ApiKeyId(new UUID(apiKeyUpdateMsg.getIdMSB(), apiKeyUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + return switch (apiKeyUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> { + boolean created = saveOrUpdateApiKey(tenantId, apiKeyId, apiKeyUpdateMsg); + if (created) { + ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId); + if (apiKey != null) { + pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_CREATED); + } + } + yield Futures.immediateFuture(null); + } + case ENTITY_DELETED_RPC_MESSAGE -> { + deleteApiKey(tenantId, edge, apiKeyId); + yield Futures.immediateFuture(null); + } + default -> handleUnsupportedMsgType(apiKeyUpdateMsg.getMsgType()); + }; + } catch (DataValidationException e) { + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + } + + @Override + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + ApiKeyId apiKeyId = new ApiKeyId(edgeEvent.getEntityId()); + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(edgeEvent.getTenantId(), apiKeyId); + if (apiKey != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addApiKeyUpdateMsg(apiKeyUpdateMsg) + .build(); + } + } + case DELETED -> { + ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyDeleteMsg(apiKeyId); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addApiKeyUpdateMsg(apiKeyUpdateMsg) + .build(); + } + } + return null; + } + + @Override + public EdgeEventType getEdgeEventType() { + return EdgeEventType.API_KEY; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java new file mode 100644 index 0000000000..f50594c9b7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.apikey; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface ApiKeyProcessor extends EdgeProcessor { + + ListenableFuture processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java new file mode 100644 index 0000000000..7d110c3670 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.apikey; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.ApiKeyId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +public abstract class BaseApiKeyProcessor extends BaseEdgeProcessor { + + protected boolean saveOrUpdateApiKey(TenantId tenantId, ApiKeyId apiKeyId, ApiKeyUpdateMsg apiKeyUpdateMsg) { + boolean isCreated = false; + try { + ApiKey apiKey = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true); + if (apiKey == null) { + throw new RuntimeException("[{" + tenantId + "}] apiKeyUpdateMsg {" + apiKeyUpdateMsg + " } cannot be converted to apiKey"); + } + + ApiKey existingApiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId); + if (existingApiKey == null) { + apiKey.setCreatedTime(Uuids.unixTimestamp(apiKeyId.getId())); + isCreated = true; + } + + apiKey.setId(apiKeyId); + edgeCtx.getApiKeyService().saveApiKey(tenantId, apiKey, apiKey.getValue(), false); + } catch (Exception e) { + log.error("[{}] Failed to process apiKey update msg [{}]", tenantId, apiKeyUpdateMsg, e); + throw e; + } + return isCreated; + } + + protected void deleteApiKey(TenantId tenantId, Edge edge, ApiKeyId apiKeyId) { + ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId); + if (apiKey != null) { + edgeCtx.getApiKeyService().deleteApiKey(tenantId, apiKey, false); + pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_DELETED); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index a004f211c4..03f765cfb7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; @@ -55,21 +55,17 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce try { edgeSynchronizationManager.getEdgeId().set(edge.getId()); - switch (assetUpdateMsg.getMsgType()) { - case ENTITY_CREATED_RPC_MESSAGE: - case ENTITY_UPDATED_RPC_MESSAGE: + return switch (assetUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> { saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge); - return Futures.immediateFuture(null); - case ENTITY_DELETED_RPC_MESSAGE: - Asset assetToDelete = edgeCtx.getAssetService().findAssetById(tenantId, assetId); - if (assetToDelete != null) { - edgeCtx.getAssetService().unassignAssetFromEdge(tenantId, assetId, edge.getId()); - } - return Futures.immediateFuture(null); - case UNRECOGNIZED: - default: - return handleUnsupportedMsgType(assetUpdateMsg.getMsgType()); - } + yield Futures.immediateFuture(null); + } + case ENTITY_DELETED_RPC_MESSAGE -> { + deleteAsset(tenantId, edge, assetId); + yield Futures.immediateFuture(null); + } + default -> handleUnsupportedMsgType(assetUpdateMsg.getMsgType()); + }; } catch (DataValidationException e) { if (e.getMessage().contains("limit reached")) { log.warn("[{}] Number of allowed asset violated {}", tenantId, assetUpdateMsg, e); @@ -97,14 +93,8 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce } private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetId assetId) { - try { - Asset asset = edgeCtx.getAssetService().findAssetById(tenantId, assetId); - String assetAsString = JacksonUtil.toString(asset); - TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, asset.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, assetId, asset.getCustomerId(), TbMsgType.ENTITY_CREATED, assetAsString, msgMetaData); - } catch (Exception e) { - log.warn("[{}][{}] Failed to push asset action to rule engine: {}", tenantId, assetId, TbMsgType.ENTITY_CREATED.name(), e); - } + Asset asset = edgeCtx.getAssetService().findAssetById(tenantId, assetId); + pushEntityEventToRuleEngine(tenantId, edge, asset, TbMsgType.ENTITY_CREATED); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java index 53c9f34b8a..cf160abf08 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java @@ -19,11 +19,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; 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.msg.TbMsgType; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -50,22 +51,15 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor { } else { asset.setId(assetId); } - String assetName = asset.getName(); - Asset assetByName = edgeCtx.getAssetService().findAssetByTenantIdAndName(tenantId, assetName); - if (assetByName != null && !assetByName.getId().equals(assetId)) { - assetName = assetName + "_" + StringUtils.randomAlphanumeric(15); - log.warn("[{}] Asset with name {} already exists. Renaming asset name to {}", - tenantId, asset.getName(), assetName); - assetNameUpdated = true; + if (isSaveRequired(assetById, asset)) { + assetNameUpdated = updateAssetNameIfDuplicateExists(tenantId, assetId, asset); + setCustomerId(tenantId, created ? null : assetById.getCustomerId(), asset, assetUpdateMsg); + assetValidator.validate(asset, Asset::getTenantId); + if (created) { + asset.setId(assetId); + } + edgeCtx.getAssetService().saveAsset(asset, false); } - asset.setName(assetName); - setCustomerId(tenantId, created ? null : assetById.getCustomerId(), asset, assetUpdateMsg); - - assetValidator.validate(asset, Asset::getTenantId); - if (created) { - asset.setId(assetId); - } - edgeCtx.getAssetService().saveAsset(asset, false); } catch (Exception e) { log.error("[{}] Failed to process asset update msg [{}]", tenantId, assetUpdateMsg, e); throw e; @@ -75,6 +69,27 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor { return Pair.of(created, assetNameUpdated); } + private boolean updateAssetNameIfDuplicateExists(TenantId tenantId, AssetId assetId, Asset asset) { + Asset assetByName = edgeCtx.getAssetService().findAssetByTenantIdAndName(tenantId, asset.getName()); + + return generateUniqueNameIfDuplicateExists(tenantId, assetId, asset, assetByName).map(uniqueName -> { + asset.setName(uniqueName); + return true; + }).orElse(false); + } + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg); + protected void deleteAsset(TenantId tenantId, AssetId assetId) { + deleteAsset(tenantId, null, assetId); + } + + protected void deleteAsset(TenantId tenantId, Edge edge, AssetId assetId) { + Asset assetById = edgeCtx.getAssetService().findAssetById(tenantId, assetId); + if (assetById != null) { + edgeCtx.getAssetService().deleteAsset(tenantId, assetId); + pushEntityEventToRuleEngine(tenantId, edge, assetById, TbMsgType.ENTITY_DELETED); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index e11b809955..74c02fa667 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java index ddb7d65d9c..9d6b44e661 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java @@ -53,7 +53,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { } String calculatedFieldName = calculatedField.getName(); - CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndName(calculatedField.getEntityId(), calculatedFieldName); + CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndTypeAndName(calculatedField.getEntityId(), calculatedField.getType(), calculatedFieldName); if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) { calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15); log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}", diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java index c7e092acaf..bc085afc08 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java @@ -22,9 +22,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.edge.rpc.EdgeVersionComparator; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -36,7 +38,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; @@ -47,6 +49,9 @@ import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import java.util.UUID; +import static org.thingsboard.server.common.data.cf.CalculatedFieldType.SCRIPT; +import static org.thingsboard.server.common.data.cf.CalculatedFieldType.SIMPLE; + @Slf4j @Component @TbCoreComponent @@ -91,7 +96,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId); - if (calculatedField != null) { + if (calculatedField != null && isValidCfToSend(calculatedField.getType(), edgeVersion)) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField); return DownlinkMsg.newBuilder() @@ -111,6 +116,10 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return null; } + private boolean isValidCfToSend(CalculatedFieldType type, EdgeVersion edgeVersion) { + return EdgeVersionComparator.INSTANCE.compare(edgeVersion, EdgeVersion.V_4_3_0) >= 0 || (type == SIMPLE || type == SCRIPT); + } + @Override public EdgeEventType getEdgeEventType() { return EdgeEventType.CALCULATED_FIELD; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java index 91b7c5b4bd..5c0c6c70e9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java @@ -20,9 +20,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.edge.Edge; 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.msg.TbMsgType; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -56,15 +58,14 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { dashboard.setAssignedCustomers(dashboardById.getAssignedCustomers()); } - dashboardValidator.validate(dashboard, Dashboard::getTenantId); - if (created) { - dashboard.setId(dashboardId); + if (isSaveRequired(dashboardById, dashboard)) { + dashboardValidator.validate(dashboard, Dashboard::getTenantId); + if (created) { + dashboard.setId(dashboardId); + } + dashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false); } - - Dashboard savedDashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false); - - updateDashboardAssignments(tenantId, customerId, dashboardById, savedDashboard, newAssignedCustomers); - + updateDashboardAssignments(tenantId, customerId, dashboardById, dashboard, newAssignedCustomers); return created; } @@ -100,6 +101,18 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { } } + protected void deleteDashboard(TenantId tenantId, DashboardId dashboardId) { + deleteDashboard(tenantId, null, dashboardId); + } + + protected void deleteDashboard(TenantId tenantId, Edge edge, DashboardId dashboardId) { + Dashboard dashboardById = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId); + if (dashboardById != null) { + edgeCtx.getDashboardService().deleteDashboard(tenantId, dashboardId); + pushEntityEventToRuleEngine(tenantId, edge, dashboardById, TbMsgType.ENTITY_DELETED); + } + } + protected abstract Set filterNonExistingCustomers(TenantId tenantId, CustomerId customerId, Set currentAssignedCustomers, Set newAssignedCustomers); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index 1e33f88333..b227334b29 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -19,7 +19,6 @@ 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.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.ShortCustomerInfo; @@ -30,8 +29,7 @@ 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.msg.TbMsgType; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; @@ -62,10 +60,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge); return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: - Dashboard dashboardToDelete = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId); - if (dashboardToDelete != null) { - edgeCtx.getDashboardService().unassignDashboardFromEdge(tenantId, dashboardId, edge.getId()); - } + deleteDashboard(tenantId, edge, dashboardId); return Futures.immediateFuture(null); case UNRECOGNIZED: default: @@ -93,14 +88,8 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da } private void pushDashboardCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DashboardId dashboardId) { - try { - Dashboard dashboard = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId); - String dashboardAsString = JacksonUtil.toString(dashboard); - TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); - pushEntityEventToRuleEngine(tenantId, dashboardId, null, TbMsgType.ENTITY_CREATED, dashboardAsString, msgMetaData); - } catch (Exception e) { - log.warn("[{}][{}] Failed to push dashboard action to rule engine: {}", tenantId, dashboardId, TbMsgType.ENTITY_CREATED.name(), e); - } + Dashboard dashboard = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId); + pushEntityEventToRuleEngine(tenantId, edge, dashboard, TbMsgType.ENTITY_CREATED); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java index 04b18d6774..e4449f9816 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java @@ -20,10 +20,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edge.Edge; 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.msg.TbMsgType; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -52,23 +53,17 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { } else { device.setId(deviceId); } - String deviceName = device.getName(); - Device deviceByName = edgeCtx.getDeviceService().findDeviceByTenantIdAndName(tenantId, deviceName); - if (deviceByName != null && !deviceByName.getId().equals(deviceId)) { - deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); - log.warn("[{}] Device with name {} already exists. Renaming device name to {}", - tenantId, device.getName(), deviceName); - deviceNameUpdated = true; - } - device.setName(deviceName); - setCustomerId(tenantId, created ? null : deviceById.getCustomerId(), device, deviceUpdateMsg); + if (isSaveRequired(deviceById, device)) { + deviceNameUpdated = updateDeviceNameIfDuplicateExists(tenantId, deviceId, device); + setCustomerId(tenantId, created ? null : deviceById.getCustomerId(), device, deviceUpdateMsg); - deviceValidator.validate(device, Device::getTenantId); - if (created) { - device.setId(deviceId); + deviceValidator.validate(device, Device::getTenantId); + if (created) { + device.setId(deviceId); + } + Device savedDevice = edgeCtx.getDeviceService().saveDevice(device, false); + edgeCtx.getClusterService().onDeviceUpdated(savedDevice, created ? null : device); } - Device savedDevice = edgeCtx.getDeviceService().saveDevice(device, false); - edgeCtx.getClusterService().onDeviceUpdated(savedDevice, created ? null : device); } catch (Exception e) { log.error("[{}] Failed to process device update msg [{}]", tenantId, deviceUpdateMsg, e); throw e; @@ -78,6 +73,15 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { return Pair.of(created, deviceNameUpdated); } + private boolean updateDeviceNameIfDuplicateExists(TenantId tenantId, DeviceId deviceId, Device device) { + Device deviceByName = edgeCtx.getDeviceService().findDeviceByTenantIdAndName(tenantId, device.getName()); + + return generateUniqueNameIfDuplicateExists(tenantId, deviceId, device, deviceByName).map(uniqueName -> { + device.setName(uniqueName); + return true; + }).orElse(false); + } + protected void updateDeviceCredentials(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { DeviceCredentials deviceCredentials = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); if (deviceCredentials == null) { @@ -110,4 +114,15 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Device device, DeviceUpdateMsg deviceUpdateMsg); + protected void deleteDevice(TenantId tenantId, DeviceId deviceId) { + deleteDevice(tenantId, null, deviceId); + } + + protected void deleteDevice(TenantId tenantId, Edge edge, DeviceId deviceId) { + Device deviceById = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId); + if (deviceById != null) { + edgeCtx.getDeviceService().deleteDevice(tenantId, deviceId); + pushEntityEventToRuleEngine(tenantId, edge, deviceById, TbMsgType.ENTITY_DELETED); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index dab3efd911..920b86208d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; @@ -76,10 +76,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge); return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: - Device deviceToDelete = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId); - if (deviceToDelete != null) { - edgeCtx.getDeviceService().unassignDeviceFromEdge(tenantId, deviceId, edge.getId()); - } + deleteDevice(tenantId, edge, deviceId); return Futures.immediateFuture(null); case UNRECOGNIZED: default: @@ -125,14 +122,8 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr } private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceId deviceId) { - try { - Device device = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId); - String deviceAsString = JacksonUtil.toString(device); - TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, device.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, deviceId, device.getCustomerId(), TbMsgType.ENTITY_CREATED, deviceAsString, msgMetaData); - } catch (Exception e) { - log.warn("[{}][{}] Failed to push device action to rule engine: {}", tenantId, deviceId, TbMsgType.ENTITY_CREATED.name(), e); - } + Device device = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId); + pushEntityEventToRuleEngine(tenantId, edge, device, TbMsgType.ENTITY_CREATED); } @Override @@ -246,7 +237,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); - builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); + builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile, edgeVersion)); } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index b13614053f..1827302525 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; @@ -102,7 +102,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile, edgeVersion); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) @@ -141,5 +141,4 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple public EdgeEventType getEdgeEventType() { return EdgeEventType.DEVICE_PROFILE; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java index a6d821c2cd..8fdedc106f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java @@ -21,9 +21,13 @@ import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -48,25 +52,39 @@ public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { } else { entityView.setId(entityViewId); } - String entityViewName = entityView.getName(); - EntityView entityViewByName = edgeCtx.getEntityViewService().findEntityViewByTenantIdAndName(tenantId, entityViewName); - if (entityViewByName != null && !entityViewByName.getId().equals(entityViewId)) { - entityViewName = entityViewName + "_" + StringUtils.randomAlphanumeric(15); - log.warn("[{}] Entity view with name {} already exists. Renaming entity view name to {}", - tenantId, entityView.getName(), entityViewName); - entityViewNameUpdated = true; - } - entityView.setName(entityViewName); - setCustomerId(tenantId, created ? null : entityViewById.getCustomerId(), entityView, entityViewUpdateMsg); + if (isSaveRequired(entityViewById, entityView)) { + entityViewNameUpdated = updateEntityViewNameIfDuplicateExists(tenantId, entityViewId, entityView); + setCustomerId(tenantId, created ? null : entityViewById.getCustomerId(), entityView, entityViewUpdateMsg); - entityViewValidator.validate(entityView, EntityView::getTenantId); - if (created) { - entityView.setId(entityViewId); + entityViewValidator.validate(entityView, EntityView::getTenantId); + if (created) { + entityView.setId(entityViewId); + } + edgeCtx.getEntityViewService().saveEntityView(entityView, false); } - edgeCtx.getEntityViewService().saveEntityView(entityView, false); return Pair.of(created, entityViewNameUpdated); } + private boolean updateEntityViewNameIfDuplicateExists(TenantId tenantId, EntityViewId entityViewId, EntityView entityView) { + EntityView entityViewByName = edgeCtx.getEntityViewService().findEntityViewByTenantIdAndName(tenantId, entityView.getName()); + + return generateUniqueNameIfDuplicateExists(tenantId, entityViewId, entityView, entityViewByName).map(uniqueName -> { + entityView.setName(uniqueName); + return true; + }).orElse(false); + } + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg); + protected void deleteEntityView(TenantId tenantId, EntityViewId entityViewId) { + deleteEntityView(tenantId, null, entityViewId); + } + + protected void deleteEntityView(TenantId tenantId, Edge edge, EntityViewId entityViewId) { + EntityView entityViewById = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId); + if (entityViewById != null) { + edgeCtx.getEntityViewService().deleteEntityView(tenantId, entityViewId); + pushEntityEventToRuleEngine(tenantId, edge, entityViewById, TbMsgType.ENTITY_DELETED); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index f91591a079..8e4c9587ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; @@ -54,21 +54,17 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements try { edgeSynchronizationManager.getEdgeId().set(edge.getId()); - switch (entityViewUpdateMsg.getMsgType()) { - case ENTITY_CREATED_RPC_MESSAGE: - case ENTITY_UPDATED_RPC_MESSAGE: + return switch (entityViewUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> { saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge); - return Futures.immediateFuture(null); - case ENTITY_DELETED_RPC_MESSAGE: - EntityView entityViewToDelete = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId); - if (entityViewToDelete != null) { - edgeCtx.getEntityViewService().unassignEntityViewFromEdge(tenantId, entityViewId, edge.getId()); - } - return Futures.immediateFuture(null); - case UNRECOGNIZED: - default: - return handleUnsupportedMsgType(entityViewUpdateMsg.getMsgType()); - } + yield Futures.immediateFuture(null); + } + case ENTITY_DELETED_RPC_MESSAGE -> { + deleteEntityView(tenantId, edge, entityViewId); + yield Futures.immediateFuture(null); + } + default -> handleUnsupportedMsgType(entityViewUpdateMsg.getMsgType()); + }; } catch (DataValidationException e) { if (e.getMessage().contains("limit reached")) { log.warn("[{}] Number of allowed entity views violated {}", tenantId, entityViewUpdateMsg, e); @@ -96,14 +92,8 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements } private void pushEntityViewCreatedEventToRuleEngine(TenantId tenantId, Edge edge, EntityViewId entityViewId) { - try { - EntityView entityView = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId); - String entityViewAsString = JacksonUtil.toString(entityView); - TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, entityView.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, entityViewId, entityView.getCustomerId(), TbMsgType.ENTITY_CREATED, entityViewAsString, msgMetaData); - } catch (Exception e) { - log.warn("[{}][{}] Failed to push entity view action to rule engine: {}", tenantId, entityViewId, TbMsgType.ENTITY_CREATED.name(), e); - } + EntityView entityView = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId); + pushEntityEventToRuleEngine(tenantId, edge, entityView, TbMsgType.ENTITY_CREATED); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index 5246f7a730..fe203349f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 1a304dfe83..86ae1e93c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java new file mode 100644 index 0000000000..f3c710ffed --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -0,0 +1,159 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +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.msg.TbMsgType; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +public abstract class BaseUserProcessor extends BaseEdgeProcessor { + + @Autowired + private DataValidator userValidator; + + protected Pair saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg) { + boolean isCreated = false; + boolean userEmailUpdated = false; + + try { + User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + if (user == null) { + throw new IllegalArgumentException(String.format("[%s] Failed to parse User from UserUpdateMsg: %s", tenantId, userUpdateMsg)); + } + + User userById = edgeCtx.getUserService().findUserById(tenantId, userId); + if (userById == null) { + isCreated = true; + user.setId(null); + } else { + user.setId(userId); + } + if (isSaveRequired(userById, user)) { + userEmailUpdated = updateUserEmailIfDuplicateExists(tenantId, userId, user); + setCustomerId(tenantId, isCreated ? null : userById.getCustomerId(), user, userUpdateMsg); + + userValidator.validate(user, User::getTenantId); + + if (isCreated) { + user.setId(userId); + } + + edgeCtx.getUserService().saveUser(tenantId, user, false); + } + } catch (Exception e) { + log.error("[{}] Failed to process user update msg [{}]", tenantId, userUpdateMsg, e); + throw e; + } + + return Pair.of(isCreated, userEmailUpdated); + } + + private boolean updateUserEmailIfDuplicateExists(TenantId tenantId, UserId userId, User user) { + String email = user.getEmail(); + User userByEmail = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, email); + + if (userByEmail != null && !userByEmail.getId().equals(user.getId())) { + String[] splitEmail = email.split("@"); + String newEmail = splitEmail[0] + "_" + StringUtils.randomAlphanumeric(15) + "@" + splitEmail[1]; + log.warn("[{}] User with email {} already exists. Renaming User email to {}", tenantId, user.getEmail(), newEmail); + user.setEmail(newEmail); + return true; + } + return false; + } + + protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId) { + deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, null); + } + + protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) { + User removedUser = deleteUser(tenantId, userId); + if (removedUser == null) { + return; + } + CustomerId userCustomerId = removedUser.getCustomerId(); + String userAsString = JacksonUtil.toString(removedUser); + TbMsgMetaData msgMetaData = edge == null ? new TbMsgMetaData() : getEdgeActionTbMsgMetaData(edge, userCustomerId); + addRemovedUserMetadata(msgMetaData, removedUser); + pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); + } + + private User deleteUser(TenantId tenantId, UserId userId) { + User userById = edgeCtx.getUserService().findUserById(tenantId, userId); + if (userById == null) { + log.trace("[{}] User with id {} does not exist", tenantId, userId); + return null; + } + edgeCtx.getUserService().deleteUser(tenantId, userById); + return userById; + } + + protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { + UserCredentials userCredentials = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); + if (userCredentials == null) { + throw new IllegalArgumentException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); + } + User user = edgeCtx.getUserService().findUserById(tenantId, userCredentials.getUserId()); + if (user == null) { + log.warn("[{}] Can't find user by id [{}] skipping credentials update. UserCredentialsUpdateMsg [{}]", + tenantId, userCredentials.getUserId(), updateMsg); + return; + } + log.debug("[{}] Updating user credentials for user [{}]. New credentials Id [{}], enabled [{}]", + tenantId, user.getName(), userCredentials.getId(), userCredentials.isEnabled()); + try { + UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); + if (userCredentialsByUserId != null && !userCredentialsByUserId.getId().equals(userCredentials.getId())) { + edgeCtx.getUserService().deleteUserCredentials(tenantId, userCredentialsByUserId); + } + edgeCtx.getUserService().saveUserCredentials(tenantId, userCredentials, false); + } catch (Exception e) { + log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]", + tenantId, user.getName(), updateMsg, e); + throw new RuntimeException(e); + } + } + + private void addRemovedUserMetadata(TbMsgMetaData metaData, User removedUser) { + metaData.putValue("userId", removedUser.getId().toString()); + metaData.putValue("userName", removedUser.getName()); + metaData.putValue("userEmail", removedUser.getEmail()); + if (removedUser.getFirstName() != null) { + metaData.putValue("userFirstName", removedUser.getFirstName()); + } + if (removedUser.getLastName() != null) { + metaData.putValue("userLastName", removedUser.getLastName()); + } + } + + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index f4b0ff039a..03fc29ae3c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -15,26 +15,113 @@ */ package org.thingsboard.server.service.edge.rpc.processor.user; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +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.msg.TbMsgType; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.List; +import java.util.UUID; @Slf4j @Component @TbCoreComponent -public class UserEdgeProcessor extends BaseEdgeProcessor { +public class UserEdgeProcessor extends BaseUserProcessor implements UserProcessor { + + @Override + public ListenableFuture processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg) { + log.trace("[{}] executing processUserMsgFromEdge [{}] from edge [{}]", tenantId, userUpdateMsg, edge.getId()); + UserId userId = new UserId(new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + return switch (userUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> { + saveOrUpdateUser(tenantId, userId, userUpdateMsg, edge); + yield Futures.immediateFuture(null); + } + case ENTITY_DELETED_RPC_MESSAGE -> { + deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, edge); + yield Futures.immediateFuture(null); + } + default -> handleUnsupportedMsgType(userUpdateMsg.getMsgType()); + }; + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed users violated {}", tenantId, userUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + } + + @Override + public ListenableFuture processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg) { + log.debug("[{}] Executing processUserCredentialsMsgFromEdge, userCredentialsUpdateMsg [{}]", tenantId, userCredentialsUpdateMsg); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + super.updateUserCredentials(tenantId, userCredentialsUpdateMsg); + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + return Futures.immediateFuture(null); + } + + private void saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateUser(tenantId, userId, userUpdateMsg); + boolean isCreated = resultPair.getFirst(); + if (isCreated) { + createRelationFromEdge(tenantId, edge.getId(), userId); + pushUserCreatedEventToRuleEngine(tenantId, edge, userId); + } + + boolean userEmailUpdated = resultPair.getSecond(); + + if (userEmailUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.UPDATED, userId, null); + } + } + + private void pushUserCreatedEventToRuleEngine(TenantId tenantId, Edge edge, UserId userId) { + try { + User user = edgeCtx.getUserService().findUserById(tenantId, userId); + if (user != null) { + String userAsString = JacksonUtil.toString(user); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, user.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, userId, user.getCustomerId(), TbMsgType.ENTITY_CREATED, userAsString, msgMetaData); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to push user action to rule engine: {}", tenantId, userId, TbMsgType.ENTITY_CREATED.name(), e); + } + } @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { @@ -48,9 +135,13 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addUserUpdateMsg(EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user)); UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); - if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + if (userCredentialsByUserId != null) { builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)); } + List apiKeys = edgeCtx.getApiKeyService().findApiKeysByUserId(edgeEvent.getTenantId(), userId); + for (ApiKey apiKey : apiKeys) { + builder.addApiKeyUpdateMsg(EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey)); + } return builder.build(); } } @@ -62,11 +153,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { } case CREDENTIALS_UPDATED -> { UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); - if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { - UserCredentialsUpdateMsg userCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + if (userCredentialsByUserId != null) { return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) + .addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)) .build(); } } @@ -79,4 +169,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { return EdgeEventType.USER; } + @Override + protected void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg) { + CustomerId customerUUID = user.getCustomerId() != null ? user.getCustomerId() : customerId; + user.setCustomerId(customerUUID); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java new file mode 100644 index 0000000000..9e6a289386 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.processor.user; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface UserProcessor extends EdgeProcessor { + + ListenableFuture processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg); + + ListenableFuture processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java new file mode 100644 index 0000000000..b50d643634 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java @@ -0,0 +1,392 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.service; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.grpc.stub.StreamObserver; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger; +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.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; +import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; +import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; +import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; +import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.session.EdgeAttributeSaveCallback; +import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE; +import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; +import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { + + @Value("${edges.send_scheduler_pool_size}") + private int sendSchedulerPoolSize; + + @Value("${edges.state.persistToTelemetry:false}") + private boolean persistToTelemetry; + + @Autowired + @Lazy + private EdgeContextComponent ctx; + + private final EdgeSessionsHolder sessions; + private final ApplicationContext applicationContext; + private final TbClusterService clusterService; + private final TbServiceInfoProvider serviceInfoProvider; + private final TelemetrySubscriptionService tsSubService; + private final TbTransactionalCache edgeIdServiceIdCache; + + private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); + private ScheduledExecutorService executorService; + private ScheduledExecutorService sendDownlinkExecutorService; + + @PostConstruct + public void onStartUp() { + this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); + this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); + } + + @PreDestroy + private void preDestroy() { + shutdownExecutorSafely(executorService); + shutdownExecutorSafely(sendDownlinkExecutorService); + sessions.forEach(EdgeGrpcSessionManager::onEdgeDisconnect); + } + + @Override + public StreamObserver handleMsgs(StreamObserver outputStream) { + EdgeGrpcSessionManager sessionManager = applicationContext.getBean(EdgeGrpcSessionManager.class); + return sessionManager.initInputStream(outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService); + } + + @Override + public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) { + switch (msg.getMsgType()) { + case EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG -> { + EdgeHighPriorityMsg edgeHighPriorityMsg = (EdgeHighPriorityMsg) msg; + log.trace("[{}] edgeEventMsg [{}]", tenantId, msg); + onEdgeHighPriorityEvent(edgeHighPriorityMsg); + } + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG -> { + EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; + log.trace("[{}] onToEdgeEventUpdateMsg [{}]", tenantId, msg); + onEdgeEventUpdate(tenantId, edgeEventUpdateMsg.getEdgeId()); + } + case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG -> { + ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; + log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); + startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId(), toEdgeSyncRequest.getServiceId()); + } + case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG -> { + FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; + log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); + processSyncResponse(fromEdgeSyncResponse); + } + } + } + + @Override + public void updateEdge(TenantId tenantId, Edge edge) { + if (edge == null) { + log.warn("[{}] Edge is null - edge is removed and outdated notification is in process!", tenantId); + return; + } + EdgeGrpcSessionManager session = sessions.getByEdgeId(edge.getId()); + if (session != null && session.getState().isConnected()) { + log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + session.onConfigurationUpdate(edge); + } else { + log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + } + } + + @Override + public void deleteEdge(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null && session.getState().isConnected()) { + log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); + session.destroyAndMarkAsZombieIfFailed(); + session.onEdgeRemoval(); + sessions.remove(session); + } + } + + @Override + public void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer) { + ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId, serviceInfoProvider.getServiceId()); + + UUID requestId = request.getId(); + EdgeGrpcSessionManager session = sessions.getByEdgeId(request.getEdgeId()); + if (session != null && session.getState().isSyncInProgress()) { + responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); + } else { + log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId()); + localSyncEdgeRequests.put(requestId, responseConsumer); + clusterService.pushEdgeSyncRequestToEdge(request); + scheduleSyncRequestTimeout(request, requestId); + } + } + + private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSessionManager edgeSession) { + EdgeSessionState state = edgeSession.getState(); + Edge edge = state.getEdge(); + TenantId tenantId = state.getTenantId(); + log.info("[{}][{}] edge [{}] connected successfully.", tenantId, state.getSessionId(), edgeId); + if (sessions.hasByEdgeId(edgeId)) { + EdgeGrpcSessionManager existingSession = sessions.getByEdgeId(edgeId); + if (existingSession != null) { + UUID sessionId = existingSession.getState().getSessionId(); + log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, state.getSessionId(), sessionId, edgeId); + existingSession.destroyAndMarkAsZombieIfFailed(); + sessions.removeBySessionId(sessionId); + } + } + sessions.put(edgeSession); + save(tenantId, edgeId, ACTIVITY_STATE, true); + long lastConnectTs = System.currentTimeMillis(); + save(tenantId, edgeId, LAST_CONNECT_TIME, lastConnectTs); + edgeIdServiceIdCache.put(edgeId, serviceInfoProvider.getServiceId()); + pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT); + edgeSession.onEdgeConnect(); + } + + private void onEdgeDisconnect(Edge edge, UUID sessionId) { + TenantId tenantId = edge.getTenantId(); + EdgeId edgeId = edge.getId(); + log.info("[{}][{}] edge disconnected!", edgeId, sessionId); + EdgeGrpcSessionManager current = sessions.getByEdgeId(edgeId); + if (current != null && current.getState().getSessionId().equals(sessionId)) { + EdgeGrpcSessionManager toRemove = sessions.removeByEdgeId(edgeId); + if (toRemove != null) { + toRemove.onEdgeDisconnect(); + toRemove.destroyAndMarkAsZombieIfFailed(); + } + sessions.removeBySessionId(sessionId); + save(tenantId, edgeId, ACTIVITY_STATE, false); + long lastDisconnectTs = System.currentTimeMillis(); + save(tenantId, edgeId, LAST_DISCONNECT_TIME, lastDisconnectTs); + pushRuleEngineMessage(tenantId, edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); + } else { + log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId); + EdgeGrpcSessionManager stale = sessions.removeBySessionId(sessionId); + if (stale != null) { + try { + stale.destroyAndMarkAsZombieIfFailed(); + log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getState().getTenantId(), sessionId, edgeId); + } catch (Exception e) { + log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getState().getTenantId(), sessionId, edgeId, e); + } + } else { + log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId); + } + } + edgeIdServiceIdCache.evict(edgeId); + } + + private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) { + log.trace("[{}] scheduling sync edge request", requestId); + executorService.schedule(() -> { + log.trace("[{}] checking if sync edge request is not processed...", requestId); + Consumer consumer = localSyncEdgeRequests.remove(requestId); + if (consumer != null) { + log.trace("[{}] timeout for processing sync edge request.", requestId); + consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Edge is not connected")); + } + }, 20, TimeUnit.SECONDS); + } + + private void onEdgeHighPriorityEvent(EdgeHighPriorityMsg msg) { + TenantId tenantId = msg.getTenantId(); + EdgeEvent edgeEvent = msg.getEdgeEvent(); + EdgeId edgeId = edgeEvent.getEdgeId(); + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null && session.getState().isConnected()) { + log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId); + session.addEventToHighPriorityQueue(edgeEvent); + } + } + + private void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null && session.getState().isConnected()) { + log.trace("[{}] onEdgeEventUpdate [{}]", tenantId, edgeId.getId()); + session.onEdgeEventUpdate(); + } + } + + private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) { + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null) { + EdgeSessionState sessionState = session.getState(); + if (sessionState.isSyncInProgress()) { + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId); + } else { + boolean success = false; + if (sessionState.isConnected()) { + session.startSyncProcess(true); + success = true; + } + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, ""), requestServiceId); + } + } + } + + private void processSyncResponse(FromEdgeSyncResponse response) { + log.trace("[{}] Received response from sync service: [{}]", response.getId(), response); + UUID requestId = response.getId(); + Consumer consumer = localSyncEdgeRequests.remove(requestId); + if (consumer != null) { + consumer.accept(response); + } else { + log.trace("[{}] Unknown or stale sync response received [{}]", requestId, response); + } + } + + private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) { + try { + EdgeId edgeId = edge.getId(); + ObjectNode edgeState = JacksonUtil.newObjectNode(); + boolean isConnected = TbMsgType.CONNECT_EVENT.equals(msgType); + if (isConnected) { + edgeState.put(ACTIVITY_STATE, true); + edgeState.put(LAST_CONNECT_TIME, ts); + } else { + edgeState.put(ACTIVITY_STATE, false); + edgeState.put(LAST_DISCONNECT_TIME, ts); + } + ctx.getRuleProcessor().process(EdgeConnectionTrigger.builder() + .tenantId(tenantId) + .customerId(edge.getCustomerId()) + .edgeId(edgeId) + .edgeName(edge.getName()) + .connected(isConnected).build()); + String data = JacksonUtil.toString(edgeState); + TbMsgMetaData md = new TbMsgMetaData(); + if (!persistToTelemetry) { + md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); + md.putValue("edgeName", edge.getName()); + md.putValue("edgeType", edge.getType()); + } + TbMsg tbMsg = TbMsg.newMsg() + .type(msgType) + .originator(edgeId) + .copyMetaData(md) + .dataType(TbMsgDataType.JSON) + .data(data) + .build(); + clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push {}", tenantId, edge.getId(), msgType, e); + } + } + + private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { + log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .entry(new LongDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } else { + tsSubService.saveAttributes(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(new LongDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } + } + + private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) { + log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .entry(new BooleanDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } else { + tsSubService.saveAttributes(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(new BooleanDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } + } + + private void shutdownExecutorSafely(ExecutorService e) { + if (e != null && !e.isShutdown()) { + e.shutdown(); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java new file mode 100644 index 0000000000..780dcbe196 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; +import org.thingsboard.server.service.edge.rpc.session.manager.KafkaBasedEdgeGrpcSessionManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +@Service +@Slf4j +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class DefaultZombieSessionCleanupService implements ZombieSessionCleanupService { + + @Autowired + private EdgeSessionsHolder edgeSessionsHolder; + + private final Queue zombieSessions; + private final ScheduledExecutorService zombieSessionsExecutorService; + + public DefaultZombieSessionCleanupService() { + this.zombieSessions = new ConcurrentLinkedQueue<>(); + this.zombieSessionsExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("zombie-sessions"); + } + + @PostConstruct + public void init() { + this.zombieSessionsExecutorService.scheduleAtFixedRate(this::cleanupZombieSessions, 30, 60, TimeUnit.SECONDS); + } + + @PreDestroy + public void destroy() { + if (zombieSessionsExecutorService != null && !zombieSessionsExecutorService.isShutdown()) { + zombieSessionsExecutorService.shutdown(); + } + } + + @Override + public void add(EdgeGrpcSessionManager session) { + zombieSessions.add(session); + } + + private void cleanupZombieSessions() { + try { + tryToDestroyZombieSessions(getZombieSessions(edgeSessionsHolder.getSessions().values()), + s -> edgeSessionsHolder.removeByEdgeId(s.getState().getEdge().getId())); + + tryToDestroyZombieSessions(getZombieSessions(edgeSessionsHolder.getSessionsById().values()), + s -> edgeSessionsHolder.removeBySessionId(s.getState().getSessionId())); + + zombieSessions.removeIf(zombie -> { + EdgeSessionState state = zombie.getState(); + if (zombie.destroy()) { + log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", + state.getTenantId(), state.getEdgeId(), state.getSessionId(), state.getEdge().getName()); + return true; + } else { + log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].", + state.getTenantId(), state.getEdgeId(), state.getSessionId(), state.getEdge().getName()); + return false; + } + }); + } catch (Exception e) { + log.warn("Failed to cleanup zombie sessions", e); + } + } + + private List getZombieSessions(Collection sessions) { + List result = new ArrayList<>(); + for (EdgeGrpcSessionManager session : sessions) { + if (isKafkaSessionAndZombie(session)) { + result.add(session); + } + } + return result; + } + + private boolean isKafkaSessionAndZombie(EdgeGrpcSessionManager session) { + if (session instanceof KafkaBasedEdgeGrpcSessionManager kafkaSession) { + EdgeSessionState sessionState = kafkaSession.getState(); + log.debug("[{}] kafkaSession.isConnected() = {}, kafkaSession.getConsumer().getConsumer().isStopped() = {}", + sessionState.getEdgeId(), + sessionState.isConnected(), + kafkaSession.getConsumer() != null ? kafkaSession.getConsumer().getConsumer() != null ? kafkaSession.getConsumer().getConsumer().isStopped() : null : null); + return !sessionState.isConnected() && + kafkaSession.getConsumer() != null && + kafkaSession.getConsumer().getConsumer() != null && + !kafkaSession.getConsumer().getConsumer().isStopped(); + } + return false; + } + + private void tryToDestroyZombieSessions(List sessionsToRemove, Function removeFunc) { + for (EdgeGrpcSessionManager toRemove : sessionsToRemove) { + log.info("[{}] Destroying session for edge because edge is not connected", toRemove.getState().getEdge().getId()); + if (toRemove.destroy()) { + removeFunc.apply(toRemove); + } + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeAttributeSaveCallback.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeAttributeSaveCallback.java new file mode 100644 index 0000000000..a5e063de2c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeAttributeSaveCallback.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session; + +import com.google.common.util.concurrent.FutureCallback; +import jakarta.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; + +@Slf4j +public class EdgeAttributeSaveCallback implements FutureCallback { + + private final TenantId tenantId; + private final EdgeId edgeId; + private final String key; + private final Object value; + + public EdgeAttributeSaveCallback(TenantId tenantId, EdgeId edgeId, String key, Object value) { + this.tenantId = tenantId; + this.edgeId = edgeId; + this.key = key; + this.value = value; + } + + @Override + public void onSuccess(@Nullable Void result) { + log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t); + } + +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java new file mode 100644 index 0000000000..73a3b53e4c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java @@ -0,0 +1,621 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +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.SettableFuture; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.data.util.Pair; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +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.StringDataEntry; +import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; +import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; +import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; +import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.RequestMsgType; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.EdgeSyncCursor; +import org.thingsboard.server.service.edge.rpc.EdgeUplinkMessageDispatcher; +import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; + +@Slf4j +@RequiredArgsConstructor +public class EdgeGrpcSession implements EdgeSession { + + private static final int MAX_DOWNLINK_ATTEMPTS = 3; + private static final String RATE_LIMIT_REACHED = "Rate limit reached"; + + private final EdgeGrpcSessionManager parentManagerRef; + private final EdgeContextComponent ctx; + private final StreamObserver outputStream; + private final DownlinkMessageMapper downlinkMessageMapper; + private final EdgeUplinkMessageDispatcher uplinkMessageDispatcher; + private final BiConsumer sessionOpenListener; + private final BiConsumer sessionCloseListener; + private final ScheduledExecutorService sendDownlinkExecutorService; + private final int maxInboundMessageSize; + private final int maxHighPriorityQueueSizePerSession; + + private final EdgeSessionState state = new EdgeSessionState(); + private final Lock downlinkMsgLock = new ReentrantLock(); + private final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); + + private int clientMaxInboundMessageSize; + + @Override + public EdgeSessionState getState() { + return state; + } + + @Override + public StreamObserver initInputStream() { + return new StreamObserver<>() { + @Override + public void onNext(RequestMsg requestMsg) { + if (!state.isConnected() && requestMsg.getMsgType().equals(RequestMsgType.CONNECT_RPC_MESSAGE)) { + ConnectResponseMsg responseMsg = processConnect(requestMsg.getConnectRequestMsg()); + outputStream.onNext(ResponseMsg.newBuilder() + .setConnectResponseMsg(responseMsg) + .build()); + if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { + outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); + } else { + if (requestMsg.getConnectRequestMsg().hasMaxInboundMessageSize()) { + log.debug("[{}][{}] Client max inbound message size: {}", + getTenantId(), getSessionId(), requestMsg.getConnectRequestMsg().getMaxInboundMessageSize()); + clientMaxInboundMessageSize = requestMsg.getConnectRequestMsg().getMaxInboundMessageSize(); + } + state.setConnected(true); + } + } + if (state.isConnected()) { + if (requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { + if (requestMsg.hasSyncRequestMsg()) { + boolean fullSync = false; + if (requestMsg.getSyncRequestMsg().hasFullSync()) { + fullSync = requestMsg.getSyncRequestMsg().getFullSync(); + } + startSyncProcess(fullSync); + } else { + state.finishSync(); + saveSyncInProgressAsAttribute(false); + } + } + if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) { + if (requestMsg.hasUplinkMsg()) { + onUplinkMsg(requestMsg.getUplinkMsg()); + } + if (requestMsg.hasDownlinkResponseMsg()) { + onDownlinkResponse(requestMsg.getDownlinkResponseMsg()); + } + } + } + } + + @Override + public void onError(Throwable t) { + log.trace("[{}][{}] Stream was terminated due to error:", getTenantId(), getSessionId(), t); + closeSession(); + } + + @Override + public void onCompleted() { + log.info("[{}][{}] Stream was closed and completed successfully!", getTenantId(), getSessionId()); + closeSession(); + } + + private void closeSession() { + state.setConnected(false); + if (state.getEdge() != null) { + try { + sessionCloseListener.accept(state.getEdge(), getSessionId()); + } catch (Exception ignored) { + } + } + try { + outputStream.onCompleted(); + } catch (Exception ignored) { + } + } + }; + } + + @Override + public void startSyncProcess(boolean fullSync) { + if (state.tryStartSync()) { + saveSyncInProgressAsAttribute(true); + log.info("[{}][{}][{}] Staring edge sync process", getTenantId(), getEdgeId(), getSessionId()); + interruptGeneralProcessingOnSync(); + doSync(new EdgeSyncCursor(ctx, state.getEdge(), fullSync)); + } else { + log.info("[{}][{}][{}] Sync is already started, skipping starting it now", getTenantId(), getEdgeId(), getSessionId()); + } + } + + @Override + public void sendDownlinkMsg(ResponseMsg responseMsg) { + if (state.isConnected()) { + String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000); + log.trace("[{}][{}] Sending downlink msg [{}]", getTenantId(), getEdgeId(), responseMsgStr); + downlinkMsgLock.lock(); + String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr; + try { + outputStream.onNext(responseMsg); + } catch (Exception e) { + log.trace("[{}][{}] Failed to send downlink message [{}]", getTenantId(), getEdgeId(), downlinkMsgStr, e); + state.setConnected(false); + sessionCloseListener.accept(state.getEdge(), getSessionId()); + } finally { + downlinkMsgLock.unlock(); + } + log.trace("[{}][{}] downlink msg successfully sent [{}]", getTenantId(), getEdgeId(), downlinkMsgStr); + } + } + + @Override + public void addHighPriorityEvent(EdgeEvent edgeEvent) { + while (highPriorityQueue.size() > maxHighPriorityQueueSizePerSession) { + EdgeEvent oldestHighPriority = highPriorityQueue.poll(); + if (oldestHighPriority != null) { + log.warn("[{}][{}][{}] High priority queue is full. Removing oldest high priority event from queue {}", + state.getTenantId(), state.getEdgeId(), getSessionId(), oldestHighPriority); + } + } + highPriorityQueue.add(edgeEvent); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, state.getEdge().getTenantId(), edgeEvent.getEdgeId(), 1)); + } + + @Override + public void processHighPriorityEvents() { + try { + if (state.isConnected() && !state.isSyncInProgress()) { + if (highPriorityQueue.isEmpty()) { + return; + } + List highPriorityEvents = new ArrayList<>(); + EdgeEvent event; + while ((event = highPriorityQueue.poll()) != null) { + highPriorityEvents.add(event); + } + log.trace("[{}][{}] Sending high priority events {}", getTenantId(), getEdgeId(), highPriorityEvents.size()); + List downlinkMsgsPack = downlinkMessageMapper.convertToDownlinkMsgsPack(state, highPriorityEvents); + sendDownlinkMsgsPack(downlinkMsgsPack).get(); + } + } catch (Exception e) { + log.error("[{}] Failed to process high priority events", getEdgeId(), e); + } + } + + @Override + public boolean hasHighPriorityEvents() { + return !highPriorityQueue.isEmpty(); + } + + @Override + public ListenableFuture> fetchAndSendEdgeEvents(EdgeEventFetcher fetcher) { + SettableFuture> result = SettableFuture.create(); + PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()); + fetchAndSendEdgeEvents(fetcher, pageLink, result); + return result; + } + + @Override + public ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { + interruptPreviousSendDownlinkMsgsTask(); + + state.setSendDownlinkMsgsFuture(SettableFuture.create()); + state.getPendingMsgsMap().clear(); + + downlinkMsgsPack.forEach(msg -> state.getPendingMsgsMap().put(msg.getDownlinkMsgId(), msg)); + scheduleDownlinkMsgsPackSend(1); + + return state.getSendDownlinkMsgsFuture(); + } + + @Override + public void close() { + log.debug("[{}][{}] Closing session", getTenantId(), getSessionId()); + state.setConnected(false); + try { + outputStream.onCompleted(); + } catch (Exception e) { + log.debug("[{}][{}] Failed to close output stream: {}", getTenantId(), getSessionId(), e.getMessage()); + } + } + + private void onUplinkMsg(UplinkMsg uplinkMsg) { + if (isRateLimitViolated(uplinkMsg)) { + return; + } + Futures.addCallback(uplinkMessageDispatcher.processUplinkMsg(state, uplinkMsg), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable List result) { + sendResponseMessage(uplinkMsg.getUplinkMsgId(), true, null); + } + + @Override + public void onFailure(Throwable t) { + String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t); + sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); + } + }, ctx.getGrpcCallbackExecutorService()); + } + + private boolean isRateLimitViolated(UplinkMsg uplinkMsg) { + if (!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES, getTenantId()) || + !ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE, getTenantId(), getEdgeId())) { + String errorMsg = String.format("Failed to process uplink message. %s", RATE_LIMIT_REACHED); + sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); + return true; + } + return false; + } + + private void sendResponseMessage(int uplinkMsgId, boolean success, String errorMsg) { + UplinkResponseMsg.Builder responseBuilder = UplinkResponseMsg.newBuilder() + .setUplinkMsgId(uplinkMsgId) + .setSuccess(success); + if (errorMsg != null) { + responseBuilder.setErrorMsg(errorMsg); + } + sendDownlinkMsg(ResponseMsg.newBuilder() + .setUplinkResponseMsg(responseBuilder.build()) + .build()); + } + + private void onDownlinkResponse(DownlinkResponseMsg msg) { + try { + if (msg.getSuccess()) { + state.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, getTenantId(), getEdgeId(), 1)); + log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", getTenantId(), getEdgeId(), getSessionId(), msg.getDownlinkMsgId(), msg); + } else { + log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", getTenantId(), getEdgeId(), getSessionId(), msg.getDownlinkMsgId(), msg.getErrorMsg()); + DownlinkMsg downlinkMsg = state.getPendingMsgsMap().get(msg.getDownlinkMsgId()); + // if NOT timeseries or attributes failures - ack failed downlink + if (downlinkMsg != null && downlinkMsg.getEntityDataCount() == 0) { + state.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); + } + } + if (state.getPendingMsgsMap().isEmpty()) { + log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", getTenantId(), getEdgeId(), getSessionId()); + stopCurrentSendDownlinkMsgsTask(false); + } + } catch (Exception e) { + log.error("[{}][{}] Can't process downlink response message [{}]", getTenantId(), getEdgeId(), msg, e); + } + } + + private void interruptGeneralProcessingOnSync() { + log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", getTenantId(), getEdgeId(), getSessionId()); + stopCurrentSendDownlinkMsgsTask(true); + } + + private void doSync(EdgeSyncCursor cursor) { + if (cursor.hasNext()) { + EdgeEventFetcher next = cursor.getNext(); + log.debug("[{}][{}] starting sync process, cursor current idx = {}, class = {}", + getTenantId(), getEdgeId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); + ListenableFuture> future = fetchAndSendEdgeEvents(next); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Pair result) { + doSync(cursor); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Exception during sync process", getTenantId(), getEdgeId(), t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + log.info("[{}][{}] sync process completed", getTenantId(), getEdgeId()); + DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) + .build(); + Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { + @Override + public void onSuccess(Boolean isInterrupted) { + markSyncCompletedSendEdgeEventUpdate(); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Exception during sending sync complete", getTenantId(), getEdgeId(), t); + markSyncCompletedSendEdgeEventUpdate(); + } + }, ctx.getGrpcCallbackExecutorService()); + } + } + + private void markSyncCompletedSendEdgeEventUpdate() { + state.finishSync(); + saveSyncInProgressAsAttribute(false); + ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(getTenantId(), getEdgeId())); + } + + private void fetchAndSendEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { + Edge edge = state.getEdge(); + TenantId tenantId = getTenantId(); + try { + log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); + processHighPriorityEvents(); + PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); + if (state.isConnected() && !pageData.getData().isEmpty()) { + if (fetcher instanceof GeneralEdgeEventFetcher) { + long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_LAG, tenantId, edge.getId(), queueSize)); + } + log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); + List downlinkMsgsPack = downlinkMessageMapper.convertToDownlinkMsgsPack(state, pageData.getData()); + Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Boolean isInterrupted) { + if (Boolean.TRUE.equals(isInterrupted)) { + log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), getSessionId()); + result.set(null); + } else { + if (state.isConnected() && pageData.hasNext()) { + fetchAndSendEdgeEvents(fetcher, pageLink.nextPageLink(), result); + } else { + EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1); + UUID idOffset = latestEdgeEvent.getUuidId(); + if (idOffset != null) { + Long newStartTs = Uuids.unixTimestamp(idOffset); + long newStartSeqId = latestEdgeEvent.getSeqId(); + result.set(Pair.of(newStartTs, newStartSeqId)); + } else { + result.set(null); + } + } + } + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to send downlink msgs pack", edge.getId(), t); + result.setException(t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); + result.set(null); + } + } catch (Exception e) { + log.error("[{}] Failed to fetch edge events", edge.getId(), e); + result.setException(e); + } + } + + private void scheduleDownlinkMsgsPackSend(int attempt) { + Runnable sendDownlinkMsgsTask = () -> { + try { + if (!state.isConnected()) { + stopCurrentSendDownlinkMsgsTask(true); + return; + } + if (!state.getPendingMsgsMap().values().isEmpty()) { + Edge edge = state.getEdge(); + List copy = new ArrayList<>(state.getPendingMsgsMap().values()); + if (attempt > 1) { + String error = "Failed to deliver the batch"; + String failureMsg = String.format("{%s} (size: {%s})", error, copy.size()); + if (attempt == 2) { + // Send a failure notification only on the second attempt. + // This ensures that failure alerts are sent just once to avoid redundant notifications. + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(getTenantId()) + .edgeId(getEdgeId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); + } + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED, edge.getTenantId(), getEdgeId(), 1)); + log.warn("[{}][{}] {} on attempt {}", getTenantId(), getEdgeId(), failureMsg, attempt); + log.debug("[{}][{}] entities in failed batch: {}", getTenantId(), getEdgeId(), copy); + } + log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", getTenantId(), getEdgeId(), copy.size()); + for (DownlinkMsg downlinkMsg : copy) { + if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) { + String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + + "env variable on the edge and restart it.", clientMaxInboundMessageSize); + String message = String.format("Downlink msg size %s exceeds client max inbound message size %s. " + + "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), clientMaxInboundMessageSize); + log.error("[{}][{}][{}] {} Message {}", getTenantId(), getEdgeId(), getSessionId(), message, downlinkMsg); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(getTenantId()) + .edgeId(getEdgeId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), getEdgeId(), 1)); + state.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); + } else { + sendDownlinkMsg(ResponseMsg.newBuilder() + .setDownlinkMsg(downlinkMsg) + .build()); + } + } + if (attempt < MAX_DOWNLINK_ATTEMPTS) { + scheduleDownlinkMsgsPackSend(attempt + 1); + } else { + String failureMsg = String.format("Failed to deliver messages: %s", copy); + log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", + getTenantId(), getEdgeId(), MAX_DOWNLINK_ATTEMPTS, copy); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(getTenantId()).edgeId(getEdgeId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) + .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), getEdgeId(), copy.size())); + stopCurrentSendDownlinkMsgsTask(false); + } + } else { + stopCurrentSendDownlinkMsgsTask(false); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", getTenantId(), getEdgeId(), e.getMessage(), e); + stopCurrentSendDownlinkMsgsTask(true); + } + }; + + if (attempt == 1) { + sendDownlinkExecutorService.submit(sendDownlinkMsgsTask); + } else { + state.setScheduledSendDownlinkTask( + sendDownlinkExecutorService.schedule( + sendDownlinkMsgsTask, + ctx.getEdgeEventStorageSettings().getSleepIntervalBetweenBatches(), + TimeUnit.MILLISECONDS) + ); + } + } + + private void interruptPreviousSendDownlinkMsgsTask() { + if (state.getSendDownlinkMsgsFuture() != null && !state.getSendDownlinkMsgsFuture().isDone() + || state.getScheduledSendDownlinkTask() != null && !state.getScheduledSendDownlinkTask().isCancelled()) { + log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", + getTenantId(), getEdgeId(), getSessionId()); + stopCurrentSendDownlinkMsgsTask(true); + } else { + log.trace("[{}][{}][{}] Previous send downlink future is not active", + getTenantId(), getEdgeId(), getSessionId()); + } + } + + private void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) { + if (state.getSendDownlinkMsgsFuture() != null && !state.getSendDownlinkMsgsFuture().isDone()) { + state.getSendDownlinkMsgsFuture().set(isInterrupted); + } + if (state.getScheduledSendDownlinkTask() != null) { + state.getScheduledSendDownlinkTask().cancel(true); + } + } + + private ConnectResponseMsg processConnect(ConnectRequestMsg request) { + log.trace("[{}] processConnect [{}]", getSessionId(), request); + Optional optional = ctx.getEdgeService().findEdgeByRoutingKey(TenantId.SYS_TENANT_ID, request.getEdgeRoutingKey()); + if (optional.isPresent()) { + Edge edge = optional.get(); + TenantId tenantId = edge.getTenantId(); + state.setEdge(edge); + try { + if (edge.getSecret().equals(request.getEdgeSecret())) { + sessionOpenListener.accept(edge.getId(), parentManagerRef); + state.setEdgeVersion(request.getEdgeVersion()); + processSaveEdgeVersionAsAttribute(request.getEdgeVersion().name()); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.ACCEPTED) + .setErrorMsg("") + .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)) + .setMaxInboundMessageSize(maxInboundMessageSize) + .build(); + } + String error = "Failed to validate the edge!"; + String failureMsg = String.format("%s Provided request secret: %s", error, request.getEdgeSecret()); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg(failureMsg) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } catch (Exception e) { + String failureMsg = "Failed to process edge connection!"; + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); + log.error(failureMsg, e); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) + .setErrorMsg(failureMsg) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + } + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg("Failed to find the edge! Routing key: " + request.getEdgeRoutingKey()) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + + private void processSaveEdgeVersionAsAttribute(String edgeVersion) { + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry(DataConstants.EDGE_VERSION_ATTR_KEY, edgeVersion), System.currentTimeMillis()); + ctx.getAttributesService().save(getTenantId(), getEdgeId(), AttributeScope.SERVER_SCOPE, attributeKvEntry); + } + + private void saveSyncInProgressAsAttribute(Boolean value) { + ctx.getTsSubService().saveAttributes(AttributesSaveRequest.builder() + .tenantId(getTenantId()) + .entityId(getEdgeId()) + .scope(AttributeScope.SERVER_SCOPE) + .entry(new BooleanDataEntry(DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value)) + .callback(new EdgeAttributeSaveCallback(getTenantId(), getEdgeId(), DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value)) + .build()); + } + + private TenantId getTenantId() { + return state.getTenantId(); + } + + private EdgeId getEdgeId() { + return state.getEdgeId(); + } + + private UUID getSessionId() { + return state.getSessionId(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSessionDelegate.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSessionDelegate.java new file mode 100644 index 0000000000..9c8c5eddb9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSessionDelegate.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session; + +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +public abstract class EdgeGrpcSessionDelegate implements EdgeGrpcSessionManager { + + protected abstract EdgeSession getSession(); + + @Override + public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { + getSession().addHighPriorityEvent(edgeEvent); + } + + @Override + public void startSyncProcess(boolean fullSync) { + getSession().startSyncProcess(fullSync); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSession.java new file mode 100644 index 0000000000..0b7158211d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSession.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session; + +import com.google.common.util.concurrent.ListenableFuture; +import io.grpc.stub.StreamObserver; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; + +import java.io.Closeable; +import java.util.List; + +public interface EdgeSession extends Closeable { + + StreamObserver initInputStream(); + EdgeSessionState getState(); + void startSyncProcess(boolean fullSync); + void sendDownlinkMsg(ResponseMsg responseMsg); + void addHighPriorityEvent(EdgeEvent edgeEvent); + void processHighPriorityEvents(); + boolean hasHighPriorityEvents(); + ListenableFuture> fetchAndSendEdgeEvents(EdgeEventFetcher fetcher); + ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java new file mode 100644 index 0000000000..2d7c3dd8e3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +@Data +@Slf4j +@Component +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class EdgeSessionsHolder { + + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); + private final ConcurrentMap sessionsById = new ConcurrentHashMap<>(); + + public void forEach(Consumer consumer) { + Set unique = new HashSet<>(sessions.values()); + unique.addAll(sessionsById.values()); + + unique.forEach(consumer); + } + + public void put(EdgeGrpcSessionManager session) { + UUID sessionId = session.getState().getSessionId(); + EdgeId edgeId = session.getState().getEdgeId(); + sessionsById.put(sessionId, session); + sessions.put(edgeId, session); + } + + public EdgeGrpcSessionManager getByEdgeId(EdgeId id) { + return sessions.get(id); + } + + public boolean hasByEdgeId(EdgeId id) { + return sessions.containsKey(id); + } + + public EdgeGrpcSessionManager removeByEdgeId(EdgeId id) { + return sessions.remove(id); + } + + public EdgeGrpcSessionManager removeBySessionId(UUID sessionId) { + return sessionsById.remove(sessionId); + } + + public void remove(EdgeGrpcSessionManager session) { + if (session == null) { + log.warn("Can't remove session from holder because it's null"); + return; + } + EdgeSessionState sessionState = session.getState(); + removeByEdgeId(sessionState.getEdgeId()); + removeBySessionId(sessionState.getSessionId()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/ZombieSessionCleanupService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/ZombieSessionCleanupService.java new file mode 100644 index 0000000000..4f65ae38a0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/ZombieSessionCleanupService.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session; + +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +public interface ZombieSessionCleanupService { + + void add(EdgeGrpcSessionManager session); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java new file mode 100644 index 0000000000..0df1af1ada --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session.manager; + +import io.grpc.stub.StreamObserver; +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.context.annotation.Lazy; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.EdgeUplinkMessageDispatcher; +import org.thingsboard.server.service.edge.rpc.session.EdgeGrpcSession; +import org.thingsboard.server.service.edge.rpc.session.EdgeGrpcSessionDelegate; +import org.thingsboard.server.service.edge.rpc.session.EdgeSession; +import org.thingsboard.server.service.edge.rpc.session.ZombieSessionCleanupService; + +import java.util.UUID; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; + +@Slf4j +public abstract class AbstractEdgeGrpcSessionManager extends EdgeGrpcSessionDelegate implements EdgeGrpcSessionManager { + + @Value("${edges.max_high_priority_queue_size_per_session:10000}") + protected int maxHighPriorityQueueSizePerSession; + + @Value("${edges.rpc.max_inbound_message_size:4194304}") + protected int maxInboundMessageSize; + + @Autowired + protected ZombieSessionCleanupService zombieSessionCleanupService; + + @Autowired + @Lazy + protected EdgeContextComponent ctx; + + @Autowired + protected DownlinkMessageMapper downlinkMessageMapper; + + @Autowired + protected EdgeUplinkMessageDispatcher uplinkMessageDispatcher; + + @Getter + protected EdgeSession session; + + private final Lock initLock = new ReentrantLock(); + + @Override + public StreamObserver initInputStream(StreamObserver outputStream, + BiConsumer sessionOpenListener, + BiConsumer sessionCloseListener, + ScheduledExecutorService sendDownlinkExecutorService) { + if (session != null) { + throw new IllegalStateException("Session already initialized"); + } + initLock.lock(); + try { + this.session = new EdgeGrpcSession( + this, ctx, outputStream, downlinkMessageMapper, uplinkMessageDispatcher, + sessionOpenListener, sessionCloseListener, + sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); + return session.initInputStream(); + } finally { + initLock.unlock(); + } + } + + @Override + public EdgeSessionState getState() { + if (session == null) { + return null; + } + return session.getState(); + } + + @Override + public void onConfigurationUpdate(Edge edge) { + EdgeSessionState state = getState(); + if (state == null) { + log.warn("Session state is null, skipping configuration update for edge [{}]", edge.getId()); + return; + } + CustomerId stateCustomerId = state.getEdge().getCustomerId(); + state.setEdge(edge); + if (stateCustomerId != null && !stateCustomerId.equals(edge.getCustomerId())) { + // do not send edge configuration message on customer update + // message send by separate flow from assign_to or unassing_from customer + return; + } + EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() + .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build(); + + ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() + .setEdgeUpdateMsg(edgeConfig) + .build(); + + session.sendDownlinkMsg(edgeConfigMsg); + } + + @Override + public void destroyAndMarkAsZombieIfFailed() { + EdgeSessionState state = getState(); + final var finalSession = session; + + try (finalSession) { + if (!destroy()) { + if (state != null) { + log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", + state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId()); + } else { + log.warn("Session destroy failed (state is null). Adding to zombie queue for later cleanup."); + } + zombieSessionCleanupService.add(this); + } + } catch (Exception e) { + if (state != null) { + log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]", + state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId(), e); + } else { + log.warn("Exception during session destroy (state is null)", e); + } + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/EdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/EdgeGrpcSessionManager.java new file mode 100644 index 0000000000..9bf51107e4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/EdgeGrpcSessionManager.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session.manager; + +import io.grpc.stub.StreamObserver; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; + +import java.util.UUID; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.BiConsumer; + +public interface EdgeGrpcSessionManager { + + StreamObserver initInputStream(StreamObserver outputStream, + BiConsumer sessionOpenListener, + BiConsumer sessionCloseListener, + ScheduledExecutorService sendDownlinkExecutorService); + EdgeSessionState getState(); + void addEventToHighPriorityQueue(EdgeEvent edgeEvent); + void startSyncProcess(boolean fullSync); + void onEdgeConnect(); + void onEdgeEventUpdate(); + void onConfigurationUpdate(Edge edge); + void onEdgeDisconnect(); + void onEdgeRemoval(); + void destroyAndMarkAsZombieIfFailed(); + boolean destroy(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java new file mode 100644 index 0000000000..ebc5531320 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -0,0 +1,302 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session.manager; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.kafka.KafkaAdmin; +import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.util.TbKafkaComponent; + +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.processor.PostgresGeneralEdgeEventsDispatcher; +import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@Slf4j +@TbKafkaComponent +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@RequiredArgsConstructor +public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionManager { + + private static final int NO_INITIAL_DELAY_VALUE = 0; + + private final TbCoreQueueFactory tbCoreQueueFactory; + private final TopicService topicService; + private final KafkaAdmin kafkaAdmin; + private final EdgeSessionsHolder sessions; + + private final AtomicReference> initMigrationAndProcessingFutureRef = new AtomicReference<>(); + private final AtomicReference> highPriorityProcessingFutureRef = new AtomicReference<>(); + private final Lock initLock = new ReentrantLock(); + private volatile boolean migrationProcessed; + private volatile boolean isHighPriorityProcessing; + @Getter + private QueueConsumerManager> consumer; + private PostgresGeneralEdgeEventsDispatcher psqlGeneralEdgeEventsDispatcher; + private ExecutorService consumerExecutor; + + @Override + public void onEdgeConnect() { + scheduleMigrationAndProcessing(); + } + + @Override + public void onEdgeEventUpdate() {} + + @Override + public void onEdgeDisconnect() {} + + @Override + public void onEdgeRemoval() { + EdgeSessionState state = getState(); + String topic = topicService + .buildEdgeEventNotificationsTopicPartitionInfo(state.getTenantId(), state.getEdgeId()) + .getTopic(); + kafkaAdmin.deleteTopic(topic); + kafkaAdmin.deleteConsumerGroup(topic); + } + + @Override + public boolean destroy() { + cancelHighPriorityProcessing(); + EdgeSessionState state = getState(); + try { + if (consumer != null) { + log.info("[{}][{}] Stopping edge event consumer...", state.getTenantId(), state.getEdgeId()); + consumer.stop(); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to stop edge event consumer", state.getTenantId(), state.getEdgeId(), e); + return false; + } + consumer = null; + try { + if (consumerExecutor != null && !consumerExecutor.isShutdown()) { + consumerExecutor.shutdown(); + awaitConsumerTermination(); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown edge event consumer executor", state.getTenantId(), state.getEdgeId(), e); + return false; + } + return true; + } + + private void scheduleMigrationAndProcessing() { + cancelMigrationAndProcessingInit(); + EdgeSessionState state = getSession().getState(); + TenantId tenantId = state.getTenantId(); + EdgeId edgeId = state.getEdgeId(); + + if (!sessions.hasByEdgeId(edgeId)) { + log.debug("[{}] Session was removed and edge event consumer must not be started [{}]", + tenantId, edgeId.getId()); + return; + } + ScheduledFuture initMigrationAndProcessingTask = ctx.getEdgeEventProcessingExecutorService().schedule(() -> { + log.info("[{}] Edge event processing task started for edge [{}]", tenantId, edgeId); + initLock.lock(); + try { + processRemainingPostgresEventsIfRequired(edgeId); + if (migrationProcessed) { + initAndLaunchConsumerOrRetry(tenantId, edgeId, state); + } + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); + } + finally { + initLock.unlock(); + } + }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + + initMigrationAndProcessingFutureRef.set(initMigrationAndProcessingTask); + } + + private void initAndLaunchConsumerOrRetry(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + if (initAndLaunchConsumer(tenantId, edgeId, state)) { + launchProcessingOfHighPriorityEvents(tenantId, edgeId); + } else { + scheduleMigrationAndProcessing(); + } + } + + private void launchProcessingOfHighPriorityEvents(TenantId tenantId, EdgeId edgeId) { + cancelHighPriorityProcessing(); + ScheduledFuture highPriorityProcessingTask = ctx.getEdgeEventProcessingExecutorService().scheduleAtFixedRate(() -> { + try { + isHighPriorityProcessing = true; + session.processHighPriorityEvents(); + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); + } finally { + isHighPriorityProcessing = false; + } + }, NO_INITIAL_DELAY_VALUE, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + highPriorityProcessingFutureRef.set(highPriorityProcessingTask); + } + + private boolean initAndLaunchConsumer(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + if (!state.isConnected() || state.isSyncInProgress() || isHighPriorityProcessing) { + log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer", + tenantId, edgeId, state.isConnected(), state.isSyncInProgress(), isHighPriorityProcessing); + return false; + } + if (isConsumerInitializationRequired()) { + initConsumerAndExecutor(tenantId, edgeId, state); + } + return true; + } + + private void initConsumerAndExecutor(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + try { + if (consumerExecutor != null && !consumerExecutor.isShutdown()) { + try { + consumerExecutor.shutdown(); + awaitConsumerTermination(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edgeId, e); + } + } + this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); + this.consumer = QueueConsumerManager.>builder() + .name("TB Edge events [" + edgeId + "]") + .msgPackProcessor(this::processMsgs) + .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) + .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edgeId)) + .consumerExecutor(consumerExecutor) + .threadPrefix("edge-events-" + edgeId) + .build(); + consumer.subscribe(); + consumer.launch(); + } catch (Exception e) { + destroy(); + log.warn("[{}][{}] Failed to start edge event consumer", state.getSessionId(), edgeId, e); + } + } + + private boolean isConsumerInitializationRequired() { + return consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped()); + } + + private void processMsgs(List> msgs, TbQueueConsumer> consumer) { + EdgeSessionState state = getSession().getState(); + TenantId tenantId = state.getTenantId(); + EdgeId edgeId = state.getEdgeId(); + + log.trace("[{}][{}] starting processing edge events", tenantId, edgeId); + if (!state.isConnected() || state.isSyncInProgress() || isHighPriorityProcessing) { + log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + + "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", + tenantId, edgeId, state.isConnected(), state.isSyncInProgress(), isHighPriorityProcessing); + return; + } + List edgeEvents = new ArrayList<>(); + for (TbProtoQueueMsg msg : msgs) { + EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); + edgeEvents.add(edgeEvent); + } + List downlinkMsgsPack = downlinkMessageMapper.convertToDownlinkMsgsPack(state, edgeEvents); + boolean isInterrupted = true; + try { + isInterrupted = session.sendDownlinkMsgsPack(downlinkMsgsPack).get(); + if (isInterrupted) { + log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edgeId); + } + } catch (Exception e) { + log.error("[{}][{}] Failed to process downlink messages", tenantId, edgeId, e); + } + if (!isInterrupted) { + consumer.commit(); + } + } + + private void cancelMigrationAndProcessingInit() { + EdgeSessionState state = getState(); + log.trace("[{}] cancelling edge migration & processing init for edge", state != null ? state.getEdgeId() : "unknown"); + cancelFuture(initMigrationAndProcessingFutureRef); + } + + private void cancelHighPriorityProcessing() { + EdgeSessionState state = getState(); + log.trace("[{}] cancelling high priority processing task for edge", state != null ? state.getEdgeId() : "unknown"); + cancelFuture(highPriorityProcessingFutureRef); + } + + private void cancelFuture(AtomicReference> futureReference) { + Future f = futureReference.getAndSet(null); + if (f != null && !f.isCancelled() && !f.isDone()) { + f.cancel(true); + } + } + + private void processRemainingPostgresEventsIfRequired(EdgeId edgeId) throws Exception { + log.debug("Processing remaining postgres edge events for edge [{}]", edgeId); + + if (psqlGeneralEdgeEventsDispatcher == null) { + psqlGeneralEdgeEventsDispatcher = new PostgresGeneralEdgeEventsDispatcher(session, ctx); + } + if (migrationProcessed) { + return; + } + Boolean hasMorePostgresEvents = psqlGeneralEdgeEventsDispatcher.processNewEvents().get(); + if (Boolean.TRUE.equals(hasMorePostgresEvents)) { + log.info("Migration still in progress for edge [{}]", edgeId.getId()); + scheduleMigrationAndProcessing(); + } else if (Boolean.FALSE.equals(hasMorePostgresEvents)) { + log.info("Migration completed for edge [{}]", edgeId.getId()); + migrationProcessed = true; + } + } + + private void awaitConsumerTermination() { + EdgeSessionState state = getState(); + try { + if (!consumerExecutor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS)) { + consumerExecutor.shutdownNow(); + } + } catch (InterruptedException ie) { + log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", state.getTenantId(), state.getEdgeId()); + Thread.currentThread().interrupt(); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java new file mode 100644 index 0000000000..0190cd2784 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java @@ -0,0 +1,183 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edge.rpc.session.manager; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.processor.PostgresGeneralEdgeEventsDispatcher; +import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@Slf4j +@ConditionalOnExpression("'${queue.type:null}'!='kafka'") +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@RequiredArgsConstructor +public class PostgresBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionManager { + + private final EdgeSessionsHolder edgeSessions; + + private final Lock newEventsLock = new ReentrantLock(); + private final AtomicReference> edgeEventCheckFutureRef = new AtomicReference<>(); + private PostgresGeneralEdgeEventsDispatcher generalEdgeEventsDispatcher; + private volatile boolean hasNewEvents; + + @Override + public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { + super.addEventToHighPriorityQueue(edgeEvent); + markHasNewEvents(); + } + + @Override + public void onEdgeConnect() { + markHasNewEvents(); + scheduleEdgeEventsCheck(); + } + + @Override + public void onEdgeEventUpdate() { + markHasNewEvents(); + } + + @Override + public void onEdgeDisconnect() { + markHasNoEvents(); + cancelScheduleEdgeEventsCheck(); + } + + @Override + public void onEdgeRemoval() { + markHasNoEvents(); + cancelScheduleEdgeEventsCheck(); + } + + @Override + public boolean destroy() { + markHasNoEvents(); + cancelScheduleEdgeEventsCheck(); + return true; + } + + private void scheduleEdgeEventsCheck() { + cancelScheduleEdgeEventsCheck(); + EdgeId edgeId = getState().getEdgeId(); + TenantId tenantId = getState().getTenantId(); + + if (!edgeSessions.hasByEdgeId(edgeId)) { + log.debug("[{}] Session was removed and edge event check schedule must not be started [{}]", + tenantId, edgeId.getId()); + return; + } + ScheduledFuture edgeEventCheckTask = ctx.getEdgeEventProcessingExecutorService().schedule(() -> { + try { + newEventsLock.lock(); + try { + if (!hasNewEvents) { + scheduleEdgeEventsCheck(); + return; + } + log.trace("[{}][{}] set session new events flag to false", tenantId, edgeId.getId()); + hasNewEvents = false; + session.processHighPriorityEvents(); + processEdgeEventsWithCallback(tenantId, edgeId); + } finally { + newEventsLock.unlock(); + } + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); + } + }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + + edgeEventCheckFutureRef.set(edgeEventCheckTask); + log.trace("[{}] Check edge event scheduled for edge [{}]", tenantId, edgeId.getId()); + } + + private void processEdgeEventsWithCallback(TenantId tenantId, EdgeId edgeId) throws Exception { + Futures.addCallback(processEdgeEvents(), new FutureCallback<>() { + @Override + public void onSuccess(Boolean newEventsAdded) { + if (Boolean.TRUE.equals(newEventsAdded)) { + log.trace("[{}][{}] new events added. set session new events flag to true", tenantId, edgeId.getId()); + hasNewEvents = true; + } + scheduleEdgeEventsCheck(); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, t); + scheduleEdgeEventsCheck(); + } + }, ctx.getGrpcCallbackExecutorService()); + } + + public ListenableFuture processEdgeEvents() throws Exception { + if (generalEdgeEventsDispatcher == null) { + generalEdgeEventsDispatcher = new PostgresGeneralEdgeEventsDispatcher(session, ctx); + } + return generalEdgeEventsDispatcher.processNewEvents(); + } + + private void cancelScheduleEdgeEventsCheck() { + EdgeSessionState state = getState(); + log.trace("[{}] cancelling edge event check for edge", state != null ? state.getEdgeId() : "unknown"); + + ScheduledFuture sf = edgeEventCheckFutureRef.getAndSet(null); + if (sf != null && !sf.isCancelled() && !sf.isDone()) { + sf.cancel(true); + } + } + + private void markHasNoEvents() { + markHasEvents(false); + } + + private void markHasNewEvents() { + markHasEvents(true); + } + + private void markHasEvents(boolean newEventsPresent) { + newEventsLock.lock(); + try { + if (hasNewEvents != newEventsPresent) { + EdgeSessionState state = getState(); + if (state != null) { + log.trace("[{}] set session new events flag to {} [{}]", state.getTenantId(), newEventsPresent, state.getEdgeId()); + } + hasNewEvents = newEventsPresent; + } + } finally { + newEventsLock.unlock(); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java index 26917f845d..3458d8f010 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java @@ -54,7 +54,7 @@ import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_P import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; @TbCoreComponent -@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true") @RequiredArgsConstructor @Service @Slf4j @@ -70,7 +70,6 @@ public class EdgeStatsService { @Value("${edges.stats.report-interval-millis:600000}") private long reportIntervalMillis; - @Scheduled( fixedDelayString = "${edges.stats.report-interval-millis:600000}", initialDelayString = "${edges.stats.report-interval-millis:600000}" @@ -80,13 +79,13 @@ public class EdgeStatsService { long now = System.currentTimeMillis(); long ts = now - (now % reportIntervalMillis); - Map countersByEdge = statsCounterService.getCounterByEdge(); - Map lagByEdgeId = kafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap(); - Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getCounterByEdge()); + Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getMsgCountersByEdge()); + boolean isKafkaStats = kafkaAdmin.isPresent(); + Map lagByEdgeId = isKafkaStats ? getLagByEdgeId(countersByEdgeSnapshot) : Collections.emptyMap(); countersByEdgeSnapshot.forEach((edgeId, counters) -> { TenantId tenantId = counters.getTenantId(); - if (kafkaAdmin.isPresent()) { + if (isKafkaStats) { counters.getMsgsLag().set(lagByEdgeId.getOrDefault(edgeId, 0L)); } List statsEntries = List.of( @@ -102,7 +101,7 @@ public class EdgeStatsService { }); } - private Map getEdgeLagByEdgeId(Map countersByEdge) { + private Map getLagByEdgeId(Map countersByEdge) { Map edgeToTopicMap = countersByEdge.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index a7c1fc67b0..38fbd94b16 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -73,6 +73,7 @@ import org.thingsboard.server.queue.util.AfterStartUp; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -150,12 +151,10 @@ public class DefaultEdqsService implements EdqsService { executor.submit(() -> { try { EdqsSyncState syncState = getSyncState(); - if (edqsSyncService.isSyncNeeded() || syncState == null || syncState.getStatus() != EdqsSyncStatus.FINISHED) { - if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) { - processSystemRequest(ToCoreEdqsRequest.builder() - .syncRequest(new EdqsSyncRequest()) - .build()); - } + if (edqsSyncService.isSyncNeeded() || syncState == null) { + requestEdqsSync(new EdqsSyncRequest()); + } else if (syncState.getStatus() != EdqsSyncStatus.FINISHED) { + requestEdqsSync(new EdqsSyncRequest(syncState.getObjectTypes())); } else { // only if topic/RocksDB is not empty and sync is finished onSyncStatusUpdate(EdqsSyncStatus.FINISHED); @@ -166,11 +165,19 @@ public class DefaultEdqsService implements EdqsService { }); } + private void requestEdqsSync(EdqsSyncRequest syncRequest) { + if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) { + processSystemRequest(ToCoreEdqsRequest.builder() + .syncRequest(syncRequest) + .build()); + } + } + @Override public void processSystemRequest(ToCoreEdqsRequest request) { log.info("Processing system request {}", request); if (request.getSyncRequest() != null) { - saveSyncState(EdqsSyncStatus.REQUESTED); + saveSyncState(EdqsSyncStatus.REQUESTED, request.getSyncRequest().getObjectTypes()); } broadcast(ToCoreEdqsMsg.builder() .syncRequest(request.getSyncRequest()) @@ -198,7 +205,8 @@ public class DefaultEdqsService implements EdqsService { onSyncStatusUpdate(msg.getSyncStatus()); } - if (msg.getSyncRequest() != null) { + EdqsSyncRequest syncRequest = msg.getSyncRequest(); + if (syncRequest != null) { syncLock.lock(); try { EdqsSyncState syncState = getSyncState(); @@ -209,15 +217,15 @@ public class DefaultEdqsService implements EdqsService { return; } } - saveSyncState(EdqsSyncStatus.STARTED); + saveSyncState(EdqsSyncStatus.STARTED, syncRequest.getObjectTypes()); - edqsSyncService.sync(); + edqsSyncService.sync(syncRequest); saveSyncState(EdqsSyncStatus.FINISHED); broadcastSyncStatusUpdate(EdqsSyncStatus.FINISHED); } catch (Exception e) { log.error("Failed to complete sync", e); - saveSyncState(EdqsSyncStatus.FAILED); + saveSyncState(EdqsSyncStatus.FAILED, syncRequest.getObjectTypes()); broadcastSyncStatusUpdate(EdqsSyncStatus.FAILED); } finally { syncLock.unlock(); @@ -369,7 +377,12 @@ public class DefaultEdqsService implements EdqsService { @SneakyThrows private void saveSyncState(EdqsSyncStatus status) { - EdqsSyncState state = new EdqsSyncState(status); + saveSyncState(status, null); + } + + @SneakyThrows + private void saveSyncState(EdqsSyncStatus status, Set objectTypes) { + EdqsSyncState state = new EdqsSyncState(status, objectTypes); log.info("New EDQS sync state: {}", state); attributesService.save(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, AttributeScope.SERVER_SCOPE, new BaseAttributeKvEntry( new JsonDataEntry("edqsSyncState", JacksonUtil.toString(state)), @@ -383,11 +396,4 @@ public class DefaultEdqsService implements EdqsService { eventsProducer.stop(); } - @Data - @AllArgsConstructor - @NoArgsConstructor - private static class EdqsSyncState { - private EdqsSyncStatus status; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java index c4afbbb2ea..e6713fc9d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.AttributeKv; import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.edqs.EdqsObject; +import org.thingsboard.server.common.data.edqs.EdqsSyncRequest; import org.thingsboard.server.common.data.edqs.Entity; import org.thingsboard.server.common.data.edqs.LatestTsKv; import org.thingsboard.server.common.data.edqs.fields.EntityFields; @@ -83,21 +84,38 @@ public abstract class EdqsSyncService { public abstract boolean isSyncNeeded(); - public void sync() { + public void sync(EdqsSyncRequest syncRequest) { log.info("Synchronizing data to EDQS"); long startTs = System.currentTimeMillis(); counters.clear(); - syncTenantEntities(); - syncRelations(); - loadKeyDictionary(); - syncAttributes(); - syncLatestTimeseries(); + if (syncRequest.getObjectTypes() != null && !syncRequest.getObjectTypes().isEmpty()) { + log.info("Sync request for entity types: {}", syncRequest.getObjectTypes()); + for (ObjectType objectType : syncRequest.getObjectTypes()) { + syncObjectType(objectType); + } + } else { + log.info("Sync request for all entity types"); + syncTenantEntities(); + syncRelations(); + loadKeyDictionary(); + syncAttributes(); + syncLatestTimeseries(); + } counters.clear(); log.info("Finished synchronizing data to EDQS in {} ms", (System.currentTimeMillis() - startTs)); } + private void syncObjectType(ObjectType objectType) { + switch (objectType) { + case RELATION -> syncRelations(); + case ATTRIBUTE_KV -> syncAttributes(); + case LATEST_TS_KV -> syncLatestTimeseries(); + default -> syncTenantEntity(objectType); + } + } + private void process(TenantId tenantId, ObjectType type, EdqsObject object) { AtomicInteger counter = counters.computeIfAbsent(type, t -> new AtomicInteger()); if (counter.incrementAndGet() % 10000 == 0) { @@ -108,26 +126,30 @@ public abstract class EdqsSyncService { private void syncTenantEntities() { for (ObjectType type : edqsTenantTypes) { - log.info("Synchronizing {} entities to EDQS", type); - long ts = System.currentTimeMillis(); - EntityType entityType = type.toEntityType(); - Dao dao = entityDaoRegistry.getDao(entityType); - UUID lastId = UUID.fromString("00000000-0000-0000-0000-000000000000"); - while (true) { - var batch = dao.findNextBatch(lastId, entityBatchSize); - if (batch.isEmpty()) { - break; - } - for (EntityFields entityFields : batch) { - TenantId tenantId = TenantId.fromUUID(entityFields.getTenantId()); - entityInfoMap.put(entityFields.getId(), new EntityIdInfo(entityType, tenantId)); - process(tenantId, type, new Entity(entityType, entityFields)); - } - EntityFields lastRecord = batch.get(batch.size() - 1); - lastId = lastRecord.getId(); + syncTenantEntity(type); + } + } + + private void syncTenantEntity(ObjectType type) { + log.info("Synchronizing {} entities to EDQS", type); + long ts = System.currentTimeMillis(); + EntityType entityType = type.toEntityType(); + Dao dao = entityDaoRegistry.getDao(entityType); + UUID lastId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + while (true) { + var batch = dao.findNextBatch(lastId, entityBatchSize); + if (batch.isEmpty()) { + break; + } + for (EntityFields entityFields : batch) { + TenantId tenantId = TenantId.fromUUID(entityFields.getTenantId()); + entityInfoMap.put(entityFields.getId(), new EntityIdInfo(entityType, tenantId)); + process(tenantId, type, new Entity(entityType, entityFields)); } - log.info("Finished synchronizing {} entities to EDQS in {} ms", type, (System.currentTimeMillis() - ts)); + EntityFields lastRecord = batch.get(batch.size() - 1); + lastId = lastRecord.getId(); } + log.info("Finished synchronizing {} entities to EDQS in {} ms", type, (System.currentTimeMillis() - ts)); } private void syncRelations() { diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncState.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncState.java new file mode 100644 index 0000000000..9ea2f7cc8d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncState.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.edqs; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.edqs.EdqsState.EdqsSyncStatus; + +import java.util.Set; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EdqsSyncState { + private EdqsSyncStatus status; + private Set objectTypes; +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 279ed918c8..efa000e521 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -97,6 +97,10 @@ public abstract class AbstractTbEntityService { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } + protected I getOrEmptyId(I entityId, EntityType entityType) { + return entityId == null ? emptyId(entityType) : entityId; + } + protected ListenableFuture autoCommit(User user, EntityId entityId) { if (vcService != null) { return vcService.autoCommit(user, entityId); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java index 05ec142e9d..685e447887 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -60,7 +61,7 @@ public class DefaultTbLogEntityActionService implements TbLogEntityActionService } @Override - public void logEntityAction(TenantId tenantId, I entityId, E entity, + public void logEntityAction(TenantId tenantId, @NotNull I entityId, E entity, CustomerId customerId, ActionType actionType, User user, Exception e, Object... additionalInfo) { if (user != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 0669c32bdd..c3d275883e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -22,8 +22,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.server.cluster.TbClusterService; 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.EntityType; @@ -31,9 +33,11 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.DeviceId; @@ -41,9 +45,11 @@ 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.job.Job; +import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -53,13 +59,18 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.rule.engine.api.JobManager; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.service.cf.CalculatedFieldCache; import java.util.Set; @@ -72,6 +83,7 @@ public class EntityStateSourcingListener { private final TbClusterService tbClusterService; private final EdgeSynchronizationManager edgeSynchronizationManager; private final JobManager jobManager; + private final CalculatedFieldCache calculatedFieldCache; @PostConstruct public void init() { @@ -99,7 +111,7 @@ public class EntityStateSourcingListener { case ASSET -> { onAssetUpdate(event.getEntity(), event.getOldEntity()); } - case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE -> { + case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, USER -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); } case RULE_CHAIN -> { @@ -140,6 +152,9 @@ public class EntityStateSourcingListener { case JOB -> { onJobUpdate((Job) event.getEntity()); } + case CUSTOMER -> { + tbClusterService.onCustomerUpdated((Customer) event.getEntity(), (Customer) event.getOldEntity()); + } default -> { } } @@ -153,7 +168,7 @@ public class EntityStateSourcingListener { return; } EntityType entityType = entityId.getEntityType(); - if (!tenantId.isSysTenantId() && entityType != EntityType.TENANT && !tenantService.tenantExists(tenantId)) { + if (entityType != EntityType.TENANT && !tenantExists(tenantId)) { log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event); return; } @@ -164,7 +179,7 @@ public class EntityStateSourcingListener { Asset asset = (Asset) event.getEntity(); tbClusterService.onAssetDeleted(tenantId, asset, null); } - case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE -> { + case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, USER -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } case NOTIFICATION_REQUEST -> { @@ -216,18 +231,59 @@ public class EntityStateSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); - if (ActionType.CREDENTIALS_UPDATED.equals(event.getActionType()) && - EntityType.DEVICE.equals(event.getEntityId().getEntityType()) - && event.getEntity() instanceof DeviceCredentials) { - tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(), - (DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null); - } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) { - Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); - if (tenant != null) { - tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); + TenantId tenantId = event.getTenantId(); + log.trace("[{}] ActionEntityEvent called: {}", tenantId, event); + switch (event.getActionType()) { + case CREDENTIALS_UPDATED -> { + if (event.getEntityId().getEntityType() == EntityType.DEVICE && event.getEntity() instanceof DeviceCredentials deviceCredentials) { + tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId, + (DeviceId) event.getEntityId(), deviceCredentials), null); + } else if (event.getEntityId().getEntityType() == EntityType.USER) { + tbClusterService.broadcastEntityStateChangeEvent(event.getTenantId(), event.getEntityId(), ComponentLifecycleEvent.UPDATED); + + } + } + case ASSIGNED_TO_TENANT -> { + if (event.getEntity() instanceof Device device) { + Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); + if (tenant != null) { + tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); + } + pushAssignedFromNotification(tenant, tenantId, device); + } + } + case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> { + if (event.getActionType() == ActionType.ALARM_DELETE && !tenantExists(tenantId)) { + return; + } + Alarm alarm = (Alarm) event.getEntity(); + if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) { + ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder() + .setEventMsg(toProto(event)) + .build(); + tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) {} + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to push alarm event to CF queue: {}", tenantId, event, t); + } + }); + } + } + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(RelationActionEvent relationEvent) { + EntityRelation relation = relationEvent.getRelation(); + if (CalculatedField.isSupportedRefEntity(relation.getFrom()) && CalculatedField.isSupportedRefEntity(relation.getTo())) { + if (relationEvent.getActionType() == ActionType.RELATION_ADD_OR_UPDATE) { + tbClusterService.onRelationUpdated(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY); + } else if (relationEvent.getActionType() == ActionType.RELATION_DELETED) { + tbClusterService.onRelationDeleted(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY); } - pushAssignedFromNotification(tenant, event.getTenantId(), device); } } @@ -305,10 +361,12 @@ public class EntityStateSourcingListener { jobManager.onJobUpdate(job); ComponentLifecycleEvent event; - if (job.getResult().getCancellationTs() > 0) { + if (job.getStatus() == JobStatus.CANCELLED) { event = ComponentLifecycleEvent.STOPPED; - } else if (job.getResult().getGeneralError() != null) { + } else if (job.getStatus() == JobStatus.FAILED) { event = ComponentLifecycleEvent.FAILED; + } else if (job.getStatus() == JobStatus.COMPLETED) { + event = ComponentLifecycleEvent.UPDATED; } else { return; } @@ -338,6 +396,10 @@ public class EntityStateSourcingListener { } } + private boolean tenantExists(TenantId tenantId) { + return tenantId.isSysTenantId() || tenantService.tenantExists(tenantId); + } + private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString()); @@ -345,4 +407,13 @@ public class EntityStateSourcingListener { return metaData; } + private EntityActionEventProto toProto(ActionEntityEvent event) { + return EntityActionEventProto.newBuilder() + .setTenantId(ProtoUtils.toProto(event.getTenantId())) + .setEntityId(ProtoUtils.toProto(event.getEntityId())) + .setAction(event.getActionType().name()) + .setEntity(event.getEntity() != null ? JacksonUtil.toString(event.getEntity()) : "") + .build(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java index 712bca5b49..f574d4df23 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy; +import jakarta.validation.constraints.NotNull; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -37,7 +38,7 @@ public interface TbLogEntityActionService { void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, User user, Object... additionalInfo); - void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, + void logEntityAction(TenantId tenantId, @NotNull I entityId, E entity, CustomerId customerId, ActionType actionType, User user, Exception e, Object... additionalInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java index 0fb43a4511..1daab60990 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java @@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.COMMENT_DELETED; + @Service @AllArgsConstructor public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService { @@ -59,9 +61,10 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem if (alarmComment.getType() == AlarmCommentType.OTHER) { alarmComment.setType(AlarmCommentType.SYSTEM); alarmComment.setUserId(null); - alarmComment.setComment(JacksonUtil.newObjectNode().put("text", - String.format("User %s deleted his comment", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))); + alarmComment.setComment(JacksonUtil.newObjectNode() + .put("text", String.format(COMMENT_DELETED.getText(), user.getTitle())) + .put("subtype", COMMENT_DELETED.name()) + .put("userName", user.getTitle())); AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.saveAlarmComment(alarm.getTenantId(), alarmComment)); logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getId(), alarm, alarm.getCustomerId(), ActionType.DELETED_COMMENT, user, savedAlarmComment); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 2a494e9dfa..448fc19852 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentSubType; import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -39,9 +40,17 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.UUID; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.ACKED_BY_USER; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.ASSIGNED_TO_USER; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.CLEARED_BY_USER; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.UNASSIGNED_BY_USER; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.UNASSIGNED_FROM_DELETED_USER; + @Service @AllArgsConstructor @Slf4j @@ -102,8 +111,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { - String systemComment = String.format("Alarm was acknowledged by user %s", user.getTitle()); - addSystemAlarmComment(alarmInfo, user, "ACK", systemComment); + addSystemAlarmComment(alarmInfo, user, ACKED_BY_USER,"userName", user.getTitle()); logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_ACK, user); } else { @@ -125,8 +133,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } AlarmInfo alarmInfo = result.getAlarm(); if (result.isCleared()) { - String systemComment = String.format("Alarm was cleared by user %s", user.getTitle()); - addSystemAlarmComment(alarmInfo, user, "CLEAR", systemComment); + addSystemAlarmComment(alarmInfo, user, CLEARED_BY_USER, "userName", user.getTitle()); logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_CLEAR, user); } else { @@ -144,8 +151,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { AlarmAssignee assignee = alarmInfo.getAssignee(); - String systemComment = String.format("Alarm was assigned by user %s to user %s", user.getTitle(), assignee.getTitle()); - addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment, assignee.getId()); + addSystemAlarmComment(alarmInfo, user, ASSIGNED_TO_USER, "userName", user.getTitle(), "assigneeName", assignee.getTitle()); logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_ASSIGNED, user); } else { @@ -162,8 +168,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { - String systemComment = String.format("Alarm was unassigned by user %s", user.getTitle()); - addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment); + addSystemAlarmComment(alarmInfo, user, UNASSIGNED_BY_USER, "userName", user.getTitle()); logEntityActionService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_UNASSIGNED, user); } else { @@ -182,8 +187,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb continue; } if (result.isModified()) { - String comment = String.format("Alarm was unassigned because user %s - was deleted", userTitle); - addSystemAlarmComment(result.getAlarm(), null, "ASSIGN", comment); + addSystemAlarmComment(result.getAlarm(), null, UNASSIGNED_FROM_DELETED_USER, "userName", userTitle); logEntityActionService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getOriginator(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null); } } @@ -214,20 +218,24 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb return ts > 0 ? ts : System.currentTimeMillis(); } - private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText) { - addSystemAlarmComment(alarm, user, subType, commentText, null); + private void addSystemAlarmComment(Alarm alarm, User user, AlarmCommentSubType subType, String param, String value) { + Map params = new LinkedHashMap<>(1); + params.put(param, value); + addSystemAlarmComment(alarm, user, subType, params); + } + + private void addSystemAlarmComment(Alarm alarm, User user, AlarmCommentSubType subType, String param, String value, String param2, String value2) { + Map params = new LinkedHashMap<>(2); + params.put(param, value); + params.put(param2, value2); + addSystemAlarmComment(alarm, user, subType, params); } - private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText, UserId assigneeId) { + private void addSystemAlarmComment(Alarm alarm, User user, AlarmCommentSubType subType, Map params) { ObjectNode commentNode = JacksonUtil.newObjectNode(); - commentNode.put("text", commentText) - .put("subtype", subType); - if (user != null) { - commentNode.put("userId", user.getId().getId().toString()); - } - if (assigneeId != null) { - commentNode.put("assigneeId", assigneeId.getId().toString()); - } + commentNode.put("text", String.format(subType.getText(), params.values().toArray())) + .put("subtype", subType.name()); + params.forEach(commentNode::put); AlarmComment alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 40e85aa987..32e2fedaab 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; @@ -40,10 +41,15 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb @Override public Asset save(Asset asset, User user) throws Exception { + return save(asset, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception { ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = asset.getTenantId(); try { - Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); + Asset savedAsset = checkNotNull(assetService.saveAsset(asset, nameConflictStrategy)); autoCommit(user, savedAsset.getId()); logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(), actionType, user); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index 2d8a5affd0..7ffd5500f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.entitiy.asset; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; @@ -27,6 +28,8 @@ public interface TbAssetService { Asset save(Asset asset, User user) throws Exception; + Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception; + void delete(Asset asset, User user); Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java index fde88bf084..4adb97f380 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java @@ -15,13 +15,26 @@ */ package org.thingsboard.server.service.entitiy.cf; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; +import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; +import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -30,10 +43,16 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.Optional; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; @TbCoreComponent @Service @@ -41,8 +60,13 @@ import java.util.Optional; @RequiredArgsConstructor public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService { + private static final int TIMEOUT = 20; + private final CalculatedFieldService calculatedFieldService; + @Autowired(required = false) + private TbelInvokeService tbelInvokeService; + @Override public CalculatedField save(CalculatedField calculatedField, SecurityUser user) throws ThingsboardException { ActionType actionType = calculatedField.getId() == null ? ActionType.ADDED : ActionType.UPDATED; @@ -52,7 +76,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp CalculatedField existingCf = calculatedFieldService.findById(tenantId, calculatedField.getId()); checkForEntityChange(existingCf, calculatedField); } - checkEntityExistence(tenantId, calculatedField.getEntityId()); + checkEntity(tenantId, calculatedField.getEntityId(), calculatedField.getType()); CalculatedField savedCalculatedField = checkNotNull(calculatedFieldService.save(calculatedField)); logEntityActionService.logEntityAction(tenantId, savedCalculatedField.getId(), savedCalculatedField, actionType, user); return savedCalculatedField; @@ -68,10 +92,9 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } @Override - public PageData findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink) { - TenantId tenantId = user.getTenantId(); - checkEntityExistence(tenantId, entityId); - return calculatedFieldService.findAllCalculatedFieldsByEntityId(tenantId, entityId, pageLink); + public PageData findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink) { + checkEntity(tenantId, entityId, type); + return calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId, type, pageLink); } @Override @@ -89,17 +112,90 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } } + @Override + public JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams) { + String expression = inputParams.get("expression").asText(); + Map arguments = Objects.requireNonNullElse( + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), + Collections.emptyMap() + ); + + ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(arguments.keySet()); + + String output = ""; + String errorText = ""; + + CalculatedFieldTbelScriptEngine engine = null; + try { + if (tbelInvokeService == null) { + throw new IllegalArgumentException("TBEL script engine is disabled!"); + } + + engine = new CalculatedFieldTbelScriptEngine( + tenantId, + tbelInvokeService, + expression, + ctxAndArgNames.toArray(String[]::new) + ); + + Object[] args = new Object[ctxAndArgNames.size()]; + args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); + for (int i = 1; i < ctxAndArgNames.size(); i++) { + var arg = arguments.get(ctxAndArgNames.get(i)); + if (arg instanceof TbelCfSingleValueArg svArg) { + args[i] = svArg.getValue(); + } else { + args[i] = arg; + } + } + + JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); + output = JacksonUtil.toString(json); + } catch (Exception e) { + log.error("Error evaluating expression", e); + Throwable rootCause = ObjectUtils.firstNonNull(ExceptionUtils.getRootCause(e), e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getClass().getSimpleName()); + } finally { + if (engine != null) { + engine.destroy(); + } + } + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); + } + + private static long getLatestTimestamp(Map arguments) { + long lastUpdateTimestamp = -1; + for (TbelCfArg entry : arguments.values()) { + if (entry instanceof TbelCfSingleValueArg singleValueArg) { + long ts = singleValueArg.getTs(); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); + } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { + long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); + } + } + return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; + } + private void checkForEntityChange(CalculatedField oldCalculatedField, CalculatedField newCalculatedField) { if (!oldCalculatedField.getEntityId().equals(newCalculatedField.getEntityId())) { throw new IllegalArgumentException("Changing the calculated field target entity after initialization is prohibited."); } } - private void checkEntityExistence(TenantId tenantId, EntityId entityId) { - switch (entityId.getEntityType()) { - case ASSET, DEVICE, ASSET_PROFILE, DEVICE_PROFILE -> Optional.ofNullable(entityService.fetchEntity(tenantId, entityId)) - .orElseThrow(() -> new IllegalArgumentException(entityId.getEntityType().getNormalName() + " with id [" + entityId.getId() + "] does not exist.")); - default -> throw new IllegalArgumentException("Entity type '" + entityId.getEntityType() + "' does not support calculated fields."); + private void checkEntity(TenantId tenantId, EntityId entityId, CalculatedFieldType type) { + EntityType entityType = entityId.getEntityType(); + Set supportedTypes = CalculatedField.SUPPORTED_ENTITIES.get(entityType); + if (supportedTypes == null || supportedTypes.isEmpty()) { + throw new IllegalArgumentException("Entity type '" + entityType + "' does not support calculated fields"); + } else if (type != null && !supportedTypes.contains(type)) { + throw new IllegalArgumentException("Entity type '" + entityType + "' does not support '" + type + "' calculated fields"); + } else if (entityService.fetchEntity(tenantId, entityId).isEmpty()) { + throw new IllegalArgumentException(entityType.getNormalName() + " with id [" + entityId.getId() + "] does not exist."); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java index 25b58c1d39..9dbb20dc2f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.service.entitiy.cf; +import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.PageLink; import org.thingsboard.server.service.security.model.SecurityUser; @@ -29,8 +32,10 @@ public interface TbCalculatedFieldService { CalculatedField findById(CalculatedFieldId calculatedFieldId, SecurityUser user); - PageData findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink); + PageData findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); void delete(CalculatedField calculatedField, SecurityUser user); + JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams); + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index 4988880f4d..6dc1db024d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; @@ -32,10 +33,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements @Override public Customer save(Customer customer, SecurityUser user) throws Exception { + return save(customer, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception { ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = customer.getTenantId(); try { - Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer)); + Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer, nameConflictStrategy)); autoCommit(user, savedCustomer.getId()); logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user); return savedCustomer; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java index 63f49783a3..7571947b8c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java @@ -16,8 +16,12 @@ package org.thingsboard.server.service.entitiy.customer; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; public interface TbCustomerService extends SimpleTbEntityService { + Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index 9f637b3beb..f0ead91eb3 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -56,10 +57,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T @Override public Device save(Device device, String accessToken, User user) throws Exception { + return save(device, accessToken, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception { ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = device.getTenantId(); try { - Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); + Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken, nameConflictStrategy)); autoCommit(user, savedDevice.getId()); logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), actionType, user); @@ -73,10 +79,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T @Override public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException { + return saveDeviceWithCredentials(device, credentials, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException { ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = device.getTenantId(); try { - Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); + Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials, nameConflictStrategy)); logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), actionType, user); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java index 63743d30b3..453d4de1b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.device; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; @@ -33,8 +34,12 @@ public interface TbDeviceService { Device save(Device device, String accessToken, User user) throws Exception; + Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception; + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException; + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException; + void delete(Device device, User user); Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java index 8777c82940..9bfda157fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.entitiy.entity.relation; import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -33,7 +32,6 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @Service @TbCoreComponent @AllArgsConstructor -@Slf4j public class DefaultTbEntityRelationService extends AbstractTbEntityService implements TbEntityRelationService { private final RelationService relationService; @@ -71,7 +69,7 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl } @Override - public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException { + public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) { try { relationService.deleteEntityCommonRelations(tenantId, entityId); logEntityActionService.logEntityAction(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user); @@ -81,4 +79,5 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl throw e; } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java index 7e74383f8c..822c5603b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -80,10 +81,15 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen @Override public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception { + return save(entityView, existingEntityView, NameConflictStrategy.DEFAULT, user); + } + + @Override + public EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception { ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = entityView.getTenantId(); try { - EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); + EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView, nameConflictStrategy)); this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user); autoCommit(user, savedEntityView.getId()); logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java index aaf9e65453..0cb789d1db 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.entityview; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -33,6 +34,8 @@ public interface TbEntityViewService extends ComponentLifecycleListener { EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception; + EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception; + void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException; void delete(EntityView entity, User user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index fce44972d8..25dadbe316 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -15,21 +15,28 @@ */ package org.thingsboard.server.service.entitiy.tenant.profile; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TenantProfile; +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.User; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.queue.TbQueueService; import java.util.List; +import static org.thingsboard.server.common.data.EntityType.TENANT_PROFILE; + @Slf4j @Service @TbCoreComponent @@ -41,18 +48,62 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple private final TbTenantProfileCache tenantProfileCache; @Override - public TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException { - TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile)); - tenantProfileCache.put(savedTenantProfile); + public TenantProfile + save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException { + ActionType actionType = tenantProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + try { + TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile)); + tenantProfileCache.put(savedTenantProfile); + logEntityActionService.logEntityAction(tenantId, savedTenantProfile.getId(), savedTenantProfile, null, + actionType, user); + + List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); + tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); - List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); - tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); + return savedTenantProfile; + } catch (ThingsboardException e) { + log.debug("Failed to save tenant profile because ThingsboardException [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e); + throw e; + } catch (DataValidationException e) { + log.debug("Failed to save tenant profile because data validation [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } catch (Exception e) { + log.debug("Failed to save tenant profile because Exception [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.GENERAL); + } - return savedTenantProfile; } @Override - public void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException { - tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId()); + public void delete(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException { + ActionType actionType = ActionType.DELETED; + try { + tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId()); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user, e); + throw e; + } + } + + @Override + public TenantProfile setDefaultTenantProfile(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException { + ActionType actionType = ActionType.UPDATED; + try { + TenantProfile savedTenantProfile = tenantProfileService.setDefaultTenantProfile(tenantId, tenantProfile.getId()); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), savedTenantProfile, null, actionType, user); + return savedTenantProfile; + } catch (DataValidationException e) { + log.debug("Failed to set default tenant profile due to data validation [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } catch (Exception e) { + log.debug("Failed to set default tenant profile [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.GENERAL); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java index 3ca8f0570b..d15e9c84c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.service.entitiy.tenant.profile; +import jakarta.validation.constraints.NotNull; 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.User; public interface TbTenantProfileService { - TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException; + TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException; - void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException; + void delete(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException; + + TenantProfile setDefaultTenantProfile(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 221f3418b9..72307b2159 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -31,7 +31,8 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // map of versions from which the upgrade to the current version is possible // key - supported version prefix, value - display name private static final Map SUPPORTED_VERSIONS_FOR_UPGRADE = Map.of( - "4.2.0", "4.2.0" + "4.3.0", "4.3.0.x", + "4.3.1", "4.3.1.x" ); private final ProjectInfo projectInfo; 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 4864f53e17..d528f10899 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 @@ -49,17 +49,25 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; 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.AlarmConditionFilter; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; -import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; -import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.query.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.debug.DebugSettings; 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.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -74,12 +82,7 @@ import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.query.BooleanFilterPredicate; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; @@ -94,12 +97,13 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration; 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.exception.DataValidationException; import org.thingsboard.server.dao.mobile.MobileAppDao; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationTargetService; @@ -117,7 +121,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; -import java.util.TreeMap; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -155,6 +159,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private final MobileAppDao mobileAppDao; private final NotificationSettingsService notificationSettingsService; private final NotificationTargetService notificationTargetService; + private final CalculatedFieldService calculatedFieldService; @Autowired private BCryptPasswordEncoder passwordEncoder; @@ -306,8 +311,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { if (invalidSignKey) { log.warn("WARNING: {}. A new JWT Signing Key has been added automatically. " + - "You can change the JWT Signing Key using the Web UI: " + - "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); + "You can change the JWT Signing Key using the Web UI: " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); jwtSettings.setTokenSigningKey(generateRandomKey()); jwtSettingsService.saveJwtSettings(jwtSettings); @@ -319,9 +324,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { .filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret())) .forEach(mobileApp -> { log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " + - "A new Application Secret has been added automatically for Mobile Application [{}]. " + - "You can change the Application Secret using the Web UI: " + - "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); + "A new Application Secret has been added automatically for Mobile Application [{}]. " + + "You can change the Application Secret using the Web UI: " + + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); mobileApp.setAppSecret(generateRandomKey()); mobileAppDao.save(TenantId.SYS_TENANT_ID, mobileApp); }); @@ -372,11 +377,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { 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, 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, 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, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + - "Raspberry Pi GPIO control sample application"); + 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"); DeviceProfile thermostatDeviceProfile = new DeviceProfile(); thermostatDeviceProfile.setTenantId(demoTenant.getId()); @@ -398,110 +403,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { deviceProfileData.setProvisionConfiguration(provisionConfiguration); thermostatDeviceProfile.setProfileData(deviceProfileData); - DeviceProfileAlarm highTemperature = new DeviceProfileAlarm(); - highTemperature.setId("highTemperatureAlarmID"); - highTemperature.setAlarmType("High Temperature"); - AlarmRule temperatureRule = new AlarmRule(); - AlarmCondition temperatureCondition = new AlarmCondition(); - temperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter temperatureAlarmFlagAttributeFilter = new AlarmConditionFilter(); - temperatureAlarmFlagAttributeFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "temperatureAlarmFlag")); - temperatureAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); - BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate(); - temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - temperatureAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE)); - temperatureAlarmFlagAttributeFilter.setPredicate(temperatureAlarmFlagAttributePredicate); - - AlarmConditionFilter temperatureTimeseriesFilter = new AlarmConditionFilter(); - temperatureTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); - temperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate temperatureTimeseriesFilterPredicate = new NumericFilterPredicate(); - temperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); - FilterPredicateValue temperatureTimeseriesPredicateValue = - new FilterPredicateValue<>(25.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold")); - temperatureTimeseriesFilterPredicate.setValue(temperatureTimeseriesPredicateValue); - temperatureTimeseriesFilter.setPredicate(temperatureTimeseriesFilterPredicate); - temperatureCondition.setCondition(Arrays.asList(temperatureAlarmFlagAttributeFilter, temperatureTimeseriesFilter)); - temperatureRule.setAlarmDetails("Current temperature = ${temperature}"); - temperatureRule.setCondition(temperatureCondition); - highTemperature.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MAJOR, temperatureRule))); - - AlarmRule clearTemperatureRule = new AlarmRule(); - AlarmCondition clearTemperatureCondition = new AlarmCondition(); - clearTemperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter clearTemperatureTimeseriesFilter = new AlarmConditionFilter(); - clearTemperatureTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); - clearTemperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate clearTemperatureTimeseriesFilterPredicate = new NumericFilterPredicate(); - clearTemperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL); - FilterPredicateValue clearTemperatureTimeseriesPredicateValue = - new FilterPredicateValue<>(25.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold")); - - clearTemperatureTimeseriesFilterPredicate.setValue(clearTemperatureTimeseriesPredicateValue); - clearTemperatureTimeseriesFilter.setPredicate(clearTemperatureTimeseriesFilterPredicate); - clearTemperatureCondition.setCondition(Collections.singletonList(clearTemperatureTimeseriesFilter)); - clearTemperatureRule.setCondition(clearTemperatureCondition); - clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}"); - highTemperature.setClearRule(clearTemperatureRule); - - DeviceProfileAlarm lowHumidity = new DeviceProfileAlarm(); - lowHumidity.setId("lowHumidityAlarmID"); - lowHumidity.setAlarmType("Low Humidity"); - AlarmRule humidityRule = new AlarmRule(); - AlarmCondition humidityCondition = new AlarmCondition(); - humidityCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter(); - humidityAlarmFlagAttributeFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "humidityAlarmFlag")); - humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); - BooleanFilterPredicate humidityAlarmFlagAttributePredicate = new BooleanFilterPredicate(); - humidityAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - humidityAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE)); - humidityAlarmFlagAttributeFilter.setPredicate(humidityAlarmFlagAttributePredicate); - - AlarmConditionFilter humidityTimeseriesFilter = new AlarmConditionFilter(); - humidityTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "humidity")); - humidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate humidityTimeseriesFilterPredicate = new NumericFilterPredicate(); - humidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); - FilterPredicateValue humidityTimeseriesPredicateValue = - new FilterPredicateValue<>(60.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold")); - humidityTimeseriesFilterPredicate.setValue(humidityTimeseriesPredicateValue); - humidityTimeseriesFilter.setPredicate(humidityTimeseriesFilterPredicate); - humidityCondition.setCondition(Arrays.asList(humidityAlarmFlagAttributeFilter, humidityTimeseriesFilter)); - - humidityRule.setCondition(humidityCondition); - humidityRule.setAlarmDetails("Current humidity = ${humidity}"); - lowHumidity.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MINOR, humidityRule))); - - AlarmRule clearHumidityRule = new AlarmRule(); - AlarmCondition clearHumidityCondition = new AlarmCondition(); - clearHumidityCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter clearHumidityTimeseriesFilter = new AlarmConditionFilter(); - clearHumidityTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "humidity")); - clearHumidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate clearHumidityTimeseriesFilterPredicate = new NumericFilterPredicate(); - clearHumidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL); - FilterPredicateValue clearHumidityTimeseriesPredicateValue = - new FilterPredicateValue<>(60.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold")); - - clearHumidityTimeseriesFilterPredicate.setValue(clearHumidityTimeseriesPredicateValue); - clearHumidityTimeseriesFilter.setPredicate(clearHumidityTimeseriesFilterPredicate); - clearHumidityCondition.setCondition(Collections.singletonList(clearHumidityTimeseriesFilter)); - clearHumidityRule.setCondition(clearHumidityCondition); - clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}"); - lowHumidity.setClearRule(clearHumidityRule); - - deviceProfileData.setAlarms(Arrays.asList(highTemperature, lowHumidity)); - DeviceProfile savedThermostatDeviceProfile = deviceProfileService.saveDeviceProfile(thermostatDeviceProfile); + createAlarmRules(demoTenant.getId(), savedThermostatDeviceProfile.getId()); DeviceId t1Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); DeviceId t2Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); @@ -526,6 +429,136 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { installScripts.createDefaultTenantDashboards(demoTenant.getId(), null); } + private void createAlarmRules(TenantId tenantId, DeviceProfileId deviceProfileId) { + CalculatedField highTemperature = new CalculatedField(); + highTemperature.setName("High Temperature"); + highTemperature.setType(CalculatedFieldType.ALARM); + highTemperature.setTenantId(tenantId); + highTemperature.setEntityId(deviceProfileId); + highTemperature.setDebugSettings(DebugSettings.all()); + AlarmCalculatedFieldConfiguration highTemperatureConfig = new AlarmCalculatedFieldConfiguration(); + highTemperature.setConfiguration(highTemperatureConfig); + + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureAlarmThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setDefaultValue("25"); + Argument temperatureAlarmFlagArgument = new Argument(); + temperatureAlarmFlagArgument.setRefEntityKey(new ReferencedEntityKey("temperatureAlarmFlag", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + highTemperatureConfig.setArguments(Map.of( + "temperature", temperatureArgument, + "temperatureAlarmThreshold", temperatureThresholdArgument, + "temperatureAlarmFlag", temperatureAlarmFlagArgument + )); + + AlarmRule temperatureRule = new AlarmRule(); + SimpleAlarmCondition temperatureCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter temperatureAlarmFlagFilter = new AlarmConditionFilter(); + temperatureAlarmFlagFilter.setArgument("temperatureAlarmFlag"); + temperatureAlarmFlagFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate(); + temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + temperatureAlarmFlagAttributePredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); + temperatureAlarmFlagFilter.setPredicates(List.of(temperatureAlarmFlagAttributePredicate)); + + AlarmConditionFilter temperatureFilter = new AlarmConditionFilter(); + temperatureFilter.setArgument("temperature"); + temperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate temperatureFilterPredicate = new NumericFilterPredicate(); + temperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + temperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); + temperatureFilter.setPredicates(List.of(temperatureFilterPredicate)); + temperatureCondition.setExpression(new SimpleAlarmConditionExpression(List.of(temperatureAlarmFlagFilter, temperatureFilter), ComplexOperation.AND)); + temperatureRule.setCondition(temperatureCondition); + temperatureRule.setAlarmDetails("Current temperature = ${temperature}"); + highTemperatureConfig.setCreateRules(Map.of( + AlarmSeverity.MAJOR, temperatureRule + )); + + AlarmRule clearTemperatureRule = new AlarmRule(); + SimpleAlarmCondition clearTemperatureCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter clearTemperatureFilter = new AlarmConditionFilter(); + clearTemperatureFilter.setArgument("temperature"); + clearTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate clearTemperatureFilterPredicate = new NumericFilterPredicate(); + clearTemperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL); + clearTemperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); + clearTemperatureFilter.setPredicates(List.of(clearTemperatureFilterPredicate)); + clearTemperatureCondition.setExpression(new SimpleAlarmConditionExpression(List.of(clearTemperatureFilter), ComplexOperation.AND)); + clearTemperatureRule.setCondition(clearTemperatureCondition); + clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}"); + highTemperatureConfig.setClearRule(clearTemperatureRule); + + calculatedFieldService.save(highTemperature); + + CalculatedField lowHumidity = new CalculatedField(); + lowHumidity.setName("Low Humidity"); + lowHumidity.setType(CalculatedFieldType.ALARM); + lowHumidity.setTenantId(tenantId); + lowHumidity.setEntityId(deviceProfileId); + lowHumidity.setDebugSettings(DebugSettings.all()); + AlarmCalculatedFieldConfiguration lowHumidityConfig = new AlarmCalculatedFieldConfiguration(); + lowHumidity.setConfiguration(lowHumidityConfig); + + Argument humidityArgument = new Argument(); + humidityArgument.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + Argument humidityThresholdArgument = new Argument(); + humidityThresholdArgument.setRefEntityKey(new ReferencedEntityKey("humidityAlarmThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + humidityThresholdArgument.setDefaultValue("60"); + Argument humidityAlarmFlagArgument = new Argument(); + humidityAlarmFlagArgument.setRefEntityKey(new ReferencedEntityKey("humidityAlarmFlag", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + lowHumidityConfig.setArguments(Map.of( + "humidity", humidityArgument, + "humidityAlarmThreshold", humidityThresholdArgument, + "humidityAlarmFlag", humidityAlarmFlagArgument + )); + + AlarmRule humidityRule = new AlarmRule(); + SimpleAlarmCondition humidityCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter(); + humidityAlarmFlagAttributeFilter.setArgument("humidityAlarmFlag"); + humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate humidityAlarmFlagPredicate = new BooleanFilterPredicate(); + humidityAlarmFlagPredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + humidityAlarmFlagPredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); + humidityAlarmFlagAttributeFilter.setPredicates(List.of(humidityAlarmFlagPredicate)); + + AlarmConditionFilter humidityFilter = new AlarmConditionFilter(); + humidityFilter.setArgument("humidity"); + humidityFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate humidityFilterPredicate = new NumericFilterPredicate(); + humidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); + humidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); + humidityFilter.setPredicates(List.of(humidityFilterPredicate)); + humidityCondition.setExpression(new SimpleAlarmConditionExpression(List.of(humidityAlarmFlagAttributeFilter, humidityFilter), ComplexOperation.AND)); + humidityRule.setCondition(humidityCondition); + humidityRule.setAlarmDetails("Current humidity = ${humidity}"); + lowHumidityConfig.setCreateRules(Map.of( + AlarmSeverity.MINOR, humidityRule + )); + + AlarmRule clearHumidityRule = new AlarmRule(); + SimpleAlarmCondition clearHumidityCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter clearHumidityFilter = new AlarmConditionFilter(); + clearHumidityFilter.setArgument("humidity"); + clearHumidityFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate clearHumidityFilterPredicate = new NumericFilterPredicate(); + clearHumidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL); + clearHumidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); + clearHumidityFilter.setPredicates(List.of(clearHumidityFilterPredicate)); + clearHumidityCondition.setExpression(new SimpleAlarmConditionExpression(List.of(clearHumidityFilter), ComplexOperation.AND)); + clearHumidityRule.setCondition(clearHumidityCondition); + clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}"); + lowHumidityConfig.setClearRule(clearHumidityRule); + + calculatedFieldService.save(lowHumidity); + } + @Override public void loadSystemWidgets() throws Exception { installScripts.loadSystemWidgets(); @@ -609,6 +642,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public void onFailure(Throwable t) { log.warn("[{}] Failed to update attribute [{}] with value [{}]", deviceId, key, value, t); } + } private void addTsCallback(ListenableFuture saveFuture, final FutureCallback callback) { 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 364fcb47dd..96f58707b5 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 @@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ResourceService; @@ -406,7 +406,8 @@ public class InstallScripts { Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR); loadSystemResources(resourcesDir.resolve("images"), ResourceType.IMAGE, null); - loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION); + loadSystemResources(resourcesDir.resolve("js_extensions"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION); + loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.MODULE); loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD, null); } 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 6bdcb1b0a2..2eb8df92fe 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,8 +15,6 @@ */ package org.thingsboard.server.service.install.update; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -24,12 +22,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; @@ -129,60 +124,6 @@ public class DefaultDataUpdateService implements DataUpdateService { return ruleNodeIds; } - boolean convertDeviceProfileForVersion330(JsonNode profileData) { - boolean isUpdated = false; - if (profileData.has("alarms") && !profileData.get("alarms").isNull()) { - JsonNode alarms = profileData.get("alarms"); - for (JsonNode alarm : alarms) { - if (alarm.has("createRules")) { - JsonNode createRules = alarm.get("createRules"); - for (AlarmSeverity severity : AlarmSeverity.values()) { - if (createRules.has(severity.name())) { - JsonNode spec = createRules.get(severity.name()).get("condition").get("spec"); - if (convertDeviceProfileAlarmRulesForVersion330(spec)) { - isUpdated = true; - } - } - } - } - if (alarm.has("clearRule") && !alarm.get("clearRule").isNull()) { - JsonNode spec = alarm.get("clearRule").get("condition").get("spec"); - if (convertDeviceProfileAlarmRulesForVersion330(spec)) { - isUpdated = true; - } - } - } - } - return isUpdated; - } - - boolean convertDeviceProfileAlarmRulesForVersion330(JsonNode spec) { - if (spec != null) { - if (spec.has("type") && spec.get("type").asText().equals("DURATION")) { - if (spec.has("value")) { - long value = spec.get("value").asLong(); - var predicate = new FilterPredicateValue<>( - value, null, new DynamicValue<>(null, null, false) - ); - ((ObjectNode) spec).remove("value"); - ((ObjectNode) spec).putPOJO("predicate", predicate); - return true; - } - } else if (spec.has("type") && spec.get("type").asText().equals("REPEATING")) { - if (spec.has("count")) { - int count = spec.get("count").asInt(); - var predicate = new FilterPredicateValue<>( - count, null, new DynamicValue<>(null, null, false) - ); - ((ObjectNode) spec).remove("count"); - ((ObjectNode) spec).putPOJO("predicate", predicate); - return true; - } - } - } - return false; - } - public static boolean getEnv(String name, boolean defaultValue) { String env = System.getenv(name); if (env == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index f17f10bfd2..d14ba2067a 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -15,15 +15,20 @@ */ package org.thingsboard.server.service.job; +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 jakarta.annotation.Nullable; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rule.engine.api.JobManager; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; @@ -33,7 +38,10 @@ import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.Task; import org.thingsboard.server.common.data.job.task.TaskResult; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +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.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; @@ -50,7 +58,10 @@ import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -65,6 +76,8 @@ public class DefaultJobManager implements JobManager { private final Map jobProcessors; private final Map>> taskProducers; private final ExecutorService executor; + private final ConcurrentHashMap finishCallbacks = new ConcurrentHashMap<>(); + private final ScheduledExecutorService cleanupScheduler; public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService, TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig, @@ -76,6 +89,8 @@ public class DefaultJobManager implements JobManager { this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); + this.cleanupScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("job-callback-cleanup"); + this.cleanupScheduler.scheduleWithFixedDelay(this::cleanupStaleCallbacks, 1, 1, TimeUnit.HOURS); } @Override @@ -84,6 +99,25 @@ public class DefaultJobManager implements JobManager { return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor); } + @Override + public ListenableFuture submitJob(Job job, TbCallback finishCallback) { + ListenableFuture saveFuture = submitJob(job); + if (finishCallback != null) { + Futures.addCallback(saveFuture, new FutureCallback<>() { + @Override + public void onSuccess(Job savedJob) { + finishCallbacks.put(savedJob.getId(), finishCallback); + } + + @Override + public void onFailure(Throwable t) { + finishCallback.onFailure(t); + } + }, MoreExecutors.directExecutor()); + } + return saveFuture; + } + @Override public void onJobUpdate(Job job) { JobStatus status = job.getStatus(); @@ -109,6 +143,35 @@ public class DefaultJobManager implements JobManager { } } + @EventListener + public void onJobUpdateEvent(ComponentLifecycleMsg event) { + EntityId entityId = event.getEntityId(); + if (entityId.getEntityType() != EntityType.JOB) { + return; + } + + ComponentLifecycleEvent lifecycleEvent = event.getEvent(); + if (!lifecycleEvent.equals(ComponentLifecycleEvent.STOPPED) && + !lifecycleEvent.equals(ComponentLifecycleEvent.FAILED) && + !lifecycleEvent.equals(ComponentLifecycleEvent.UPDATED)) { + return; + } + JobId jobId = new JobId(entityId.getId()); + TbCallback callback = finishCallbacks.remove(jobId); + if (callback == null) { + return; + } + executor.execute(() -> { + try { + Job job = jobService.findJobById(event.getTenantId(), jobId); + invokeFinishCallback(job, callback); + } catch (Throwable e) { + log.error("[{}] Failed to invoke finish callback", jobId, e); + callback.onFailure(e); + } + }); + } + private void processJob(Job job) { TenantId tenantId = job.getTenantId(); JobId jobId = job.getId(); @@ -195,12 +258,41 @@ public class DefaultJobManager implements JobManager { }); } + private void invokeFinishCallback(@Nullable Job job, TbCallback callback) { + if (job == null) { + callback.onFailure(new RuntimeException("Job not found")); + } else if (job.getStatus() == JobStatus.COMPLETED) { + callback.onSuccess(); + } else { + callback.onFailure(new RuntimeException(job.getError())); + } + } + + private void cleanupStaleCallbacks() { + finishCallbacks.entrySet().removeIf(entry -> { + JobId jobId = entry.getKey(); + try { + Job job = jobService.findJobById(TenantId.SYS_TENANT_ID, jobId); + if (job == null || job.getStatus().isOneOf(JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED)) { + invokeFinishCallback(job, entry.getValue()); + return true; + } + return false; + } catch (Throwable e) { + log.error("[{}] Failed to cleanup stale callback", jobId, e); + entry.getValue().onFailure(e); + return true; + } + }); + } + private JobProcessor getJobProcessor(JobType jobType) { return jobProcessors.get(jobType); } @PreDestroy private void destroy() { + cleanupScheduler.shutdownNow(); executor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java index 98b696125d..585130501f 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java @@ -62,6 +62,7 @@ public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerP .alarmType(alarmInfo.getType()) .alarmOriginator(alarmInfo.getOriginator()) .alarmOriginatorName(alarmInfo.getOriginatorName()) + .alarmOriginatorLabel(alarmInfo.getOriginatorLabel()) .alarmSeverity(alarmInfo.getSeverity()) .alarmStatus(alarmInfo.getStatus()) .alarmCustomerId(alarmInfo.getCustomerId()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java index 4ae158c6e7..155e8e4e3c 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; @@ -57,11 +58,14 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc @Override public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmCommentTrigger trigger) { Alarm alarm = trigger.getAlarm(); - String originatorName; + String originatorName, originatorLabel; if (alarm instanceof AlarmInfo) { originatorName = ((AlarmInfo) alarm).getOriginatorName(); + originatorLabel = ((AlarmInfo) alarm).getOriginatorLabel(); } else { - originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse(""); + var infoOpt = entityService.fetchNameLabelAndCustomerDetails(trigger.getTenantId(), alarm.getOriginator()); + originatorName = infoOpt.map(NameLabelAndCustomerDetails::getName).orElse(null); + originatorLabel = infoOpt.map(NameLabelAndCustomerDetails::getLabel).orElse(null); } return AlarmCommentNotificationInfo.builder() .comment(trigger.getComment().getComment().get("text").asText()) @@ -73,6 +77,7 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc .alarmType(alarm.getType()) .alarmOriginator(alarm.getOriginator()) .alarmOriginatorName(originatorName) + .alarmOriginatorLabel(originatorLabel) .alarmSeverity(alarm.getSeverity()) .alarmStatus(alarm.getStatus()) .alarmCustomerId(alarm.getCustomerId()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java index e91b1002f7..992d86d83a 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.service.notification.rule.trigger; +import com.fasterxml.jackson.databind.JsonNode; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -28,6 +30,9 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.util.HashMap; +import java.util.Map; + import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @@ -106,15 +111,27 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor toDetailsTemplateMap(JsonNode details) { + Map infoMap = JacksonUtil.toFlatMap(details); + Map result = new HashMap<>(); + for (Map.Entry entry : infoMap.entrySet()) { + String key = "details." + entry.getKey(); + result.put(key, entry.getValue()); + } + return result; + } + @Override public NotificationRuleTriggerType getTriggerType() { return NotificationRuleTriggerType.ALARM; 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 index 38efff3d39..92d76693f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -15,31 +15,34 @@ */ package org.thingsboard.server.service.query; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.FutureCallback; +import com.fasterxml.jackson.databind.JsonNode; 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.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.KvUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; 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.DataType; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -56,26 +59,26 @@ import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; 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.sql.query.EntityKeyMapping; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; -import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.Futures.immediateFuture; + @Service @Slf4j @TbCoreComponent @@ -138,20 +141,12 @@ public class DefaultEntityQueryService implements EntityQueryService { } private void resolveDynamicValue(DynamicValue dynamicValue, SecurityUser user, FilterPredicateType predicateType) { - EntityId entityId; - switch (dynamicValue.getSourceType()) { - case CURRENT_TENANT: - entityId = user.getTenantId(); - break; - case CURRENT_CUSTOMER: - entityId = user.getCustomerId(); - break; - case CURRENT_USER: - entityId = user.getId(); - break; - default: - throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}"); - } + EntityId entityId = switch (dynamicValue.getSourceType()) { + case CURRENT_TENANT -> user.getTenantId(); + case CURRENT_CUSTOMER -> user.getCustomerId(); + case CURRENT_USER -> user.getId(); + default -> throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}"); + }; try { Optional valueOpt = attributesService.find(user.getTenantId(), entityId, @@ -226,7 +221,7 @@ public class DefaultEntityQueryService implements EntityQueryService { private EntityDataQuery buildEntityDataQuery(AlarmCountQuery query) { EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME))); - return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) { @@ -238,105 +233,186 @@ public class DefaultEntityQueryService implements EntityQueryService { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } @Override - public DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, - boolean isTimeseries, boolean isAttributes, String attributesScope) { - final DeferredResult response = new DeferredResult<>(); + public ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, + boolean isTimeseries, boolean isAttributes, AttributeScope scope) { if (!isAttributes && !isTimeseries) { - replyWithEmptyResponse(response); - return response; + return immediateFuture(AvailableEntityKeys.none()); } - List ids = this.findEntityDataByQuery(securityUser, query).getData().stream() + List ids = findEntityDataByQuery(securityUser, query).getData().stream() .map(EntityData::getEntityId) - .collect(Collectors.toList()); + .toList(); if (ids.isEmpty()) { - replyWithEmptyResponse(response); - return response; + return immediateFuture(AvailableEntityKeys.none()); } Set types = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet()); - final ListenableFuture> timeseriesKeysFuture; - final ListenableFuture> attributesKeysFuture; + ListenableFuture> timeseriesKeysFuture; + ListenableFuture> attributesKeysFuture; if (isTimeseries) { - timeseriesKeysFuture = dbCallbackExecutor.submit(() -> timeseriesService.findAllKeysByEntityIds(tenantId, ids)); + timeseriesKeysFuture = timeseriesService.findAllKeysByEntityIdsAsync(tenantId, ids); } else { - timeseriesKeysFuture = null; + timeseriesKeysFuture = immediateFuture(Collections.emptyList()); } if (isAttributes) { Map> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType)); List>> futures = new ArrayList<>(typesMap.size()); - typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, attributesScope)))); + typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope)))); attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> { if (CollectionUtils.isEmpty(lists)) { return Collections.emptyList(); } - return lists.stream().flatMap(List::stream).distinct().sorted().collect(Collectors.toList()); + return lists.stream().flatMap(List::stream).distinct().sorted().toList(); }, dbCallbackExecutor); } else { - attributesKeysFuture = null; + attributesKeysFuture = immediateFuture(Collections.emptyList()); } - if (isTimeseries && isAttributes) { - Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture).run(() -> { - try { - replyWithResponse(response, types, timeseriesKeysFuture.get(), attributesKeysFuture.get()); - } catch (Exception e) { - log.error("Failed to fetch timeseries and attributes keys!", e); - AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); - } - }, dbCallbackExecutor); - } else if (isTimeseries) { - addCallback(timeseriesKeysFuture, keys -> replyWithResponse(response, types, keys, null), - error -> { - log.error("Failed to fetch timeseries keys!", error); - AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR); - }); - } else { - addCallback(attributesKeysFuture, keys -> replyWithResponse(response, types, null, keys), - error -> { - log.error("Failed to fetch attributes keys!", error); - AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR); - }); + return Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture) + .call(() -> { + try { + return new AvailableEntityKeys(types, Futures.getDone(timeseriesKeysFuture), Futures.getDone(attributesKeysFuture)); + } catch (ExecutionException e) { + throw new ThingsboardException(e.getCause(), ThingsboardErrorCode.DATABASE); + } + }, dbCallbackExecutor); + } + + @Override + public ListenableFuture findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query, + boolean includeTimeseries, boolean includeAttributes, + Set scopes, boolean includeSamples) { + if (!includeTimeseries && !includeAttributes) { + return Futures.immediateFailedFuture( + new IllegalArgumentException("At least one of 'includeTimeseries' or 'includeAttributes' must be true")); } - return response; + + return Futures.transformAsync(findEntityIdsByQueryAsync(securityUser, query), ids -> { + if (ids.isEmpty()) { + return immediateFuture(new AvailableEntityKeysV2( + Collections.emptySet(), + includeTimeseries ? Collections.emptyList() : null, + includeAttributes ? Collections.emptyMap() : null)); + } + + TenantId tenantId = securityUser.getTenantId(); + Set entityTypes = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet()); + + var tsFuture = includeTimeseries ? fetchTimeseriesKeys(tenantId, ids, includeSamples) : null; + + Set effectiveScopes = includeAttributes + ? resolveAttributeScopes(scopes, entityTypes) : Collections.emptySet(); + var attrFutures = effectiveScopes.stream() + .map(scope -> fetchAttributeKeys(tenantId, ids, scope, includeSamples)) + .toList(); + + return assembleResult(entityTypes, tsFuture, attrFutures); + }, dbCallbackExecutor); } - private void replyWithResponse(DeferredResult response, Set types, List timeseriesKeys, List attributesKeys) { - ObjectNode json = JacksonUtil.newObjectNode(); - addItemsToArrayNode(json.putArray("entityTypes"), types); - addItemsToArrayNode(json.putArray("timeseries"), timeseriesKeys); - addItemsToArrayNode(json.putArray("attribute"), attributesKeys); - response.setResult(new ResponseEntity<>(json, HttpStatus.OK)); + private ListenableFuture> findEntityIdsByQueryAsync(SecurityUser securityUser, EntityDataQuery query) { + return Futures.transform(entityService.findEntityDataByQueryAsync(securityUser.getTenantId(), securityUser.getCustomerId(), query), + page -> page.getData().stream() + .map(EntityData::getEntityId) + .toList(), + dbCallbackExecutor); } - private void replyWithEmptyResponse(DeferredResult response) { - replyWithResponse(response, Collections.emptySet(), Collections.emptyList(), Collections.emptyList()); + private static Set resolveAttributeScopes(Set requestedScopes, Set entityTypes) { + boolean hasDevices = entityTypes.contains(EntityType.DEVICE); + Set scopes; + if (CollectionUtils.isNotEmpty(requestedScopes)) { + scopes = requestedScopes; + } else { // auto-determine scopes + scopes = hasDevices + ? Set.of(AttributeScope.SERVER_SCOPE, AttributeScope.CLIENT_SCOPE, AttributeScope.SHARED_SCOPE) + : Collections.singleton(AttributeScope.SERVER_SCOPE); + } + // Non-device entities only support SERVER_SCOPE + if (!hasDevices) { + return scopes.contains(AttributeScope.SERVER_SCOPE) + ? Collections.singleton(AttributeScope.SERVER_SCOPE) + : Collections.emptySet(); + } + return scopes; } - private void addItemsToArrayNode(ArrayNode arrayNode, Collection collection) { - if (!CollectionUtils.isEmpty(collection)) { - collection.forEach(item -> arrayNode.add(item.toString())); + private ListenableFuture> fetchTimeseriesKeys(TenantId tenantId, List entityIds, boolean includeSamples) { + if (includeSamples) { + return Futures.transform( + timeseriesService.findLatestByEntityIdsAsync(tenantId, entityIds), + entries -> toKeyInfos(entries, true), + dbCallbackExecutor); } + return Futures.transform( + timeseriesService.findAllKeysByEntityIdsAsync(tenantId, entityIds), + keys -> keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList(), + dbCallbackExecutor); } - private void addCallback(ListenableFuture> future, Consumer> success, Consumer error) { - Futures.addCallback(future, new FutureCallback>() { - @Override - public void onSuccess(@Nullable List keys) { - success.accept(keys); - } + private ListenableFuture>> fetchAttributeKeys( + TenantId tenantId, List entityIds, AttributeScope scope, boolean includeSamples) { + if (includeSamples) { + return Futures.transform( + attributesService.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope), + entries -> Map.entry(scope, toKeyInfos(entries, true)), + dbCallbackExecutor); + } + return Futures.transform( + attributesService.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope), + keys -> Map.entry(scope, keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList()), + dbCallbackExecutor); + } - @Override - public void onFailure(Throwable t) { - error.accept(t); - } - }, dbCallbackExecutor); + private ListenableFuture assembleResult( + Set entityTypes, + ListenableFuture> tsFuture, + List>>> attrFutures) { + var allAttrFuture = attrFutures.isEmpty() + ? immediateFuture(List.>>of()) + : Futures.allAsList(attrFutures); + + List> allFutures = new ArrayList<>(); + if (tsFuture != null) { + allFutures.add(tsFuture); + } + allFutures.add(allAttrFuture); + + var finalTsFuture = tsFuture; + return Futures.whenAllComplete(allFutures) + .call(() -> { + List tsKeys = finalTsFuture != null ? Futures.getDone(finalTsFuture) : null; + Map> attrMap = attrFutures.isEmpty() ? null : new TreeMap<>(); + if (attrMap != null) { + for (var entry : Futures.getDone(allAttrFuture)) { + attrMap.put(entry.getKey(), entry.getValue()); + } + } + return new AvailableEntityKeysV2(entityTypes, tsKeys, attrMap); + }, dbCallbackExecutor); + } + + private static List toKeyInfos(List entries, boolean includeSamples) { + return entries.stream() + .map(e -> new KeyInfo(e.getKey(), includeSamples ? toKeySample(e) : null)) + .sorted(Comparator.comparing(KeyInfo::key)) + .toList(); + } + + private static KeySample toKeySample(KvEntry entry) { + long ts = entry instanceof TsKvEntry tsKv ? tsKv.getTs() + : entry instanceof AttributeKvEntry attr ? attr.getLastUpdateTs() + : 0; + JsonNode value = entry.getDataType() == DataType.JSON + ? JacksonUtil.toJsonNode(entry.getJsonValue().get()) + : JacksonUtil.valueToTree(entry.getValue()); + return new KeySample(ts, value); } } 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 index 464fde4410..354f8e9278 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -15,18 +15,22 @@ */ package org.thingsboard.server.service.query; -import org.springframework.http.ResponseEntity; -import org.springframework.web.context.request.async.DeferredResult; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; 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; +import java.util.Set; + public interface EntityQueryService { long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query); @@ -37,7 +41,11 @@ public interface EntityQueryService { long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query); - DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, - boolean isTimeseries, boolean isAttributes, String attributesScope); + ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, + boolean isTimeseries, boolean isAttributes, AttributeScope scope); + + ListenableFuture findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query, + boolean includeTimeseries, boolean includeAttributes, + Set scopes, boolean includeSamples); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index ba4ab74f37..8a3f05f0b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityActionEventMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.common.data.DataConstants; @@ -50,7 +51,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.queue.util.TbRuleEngineComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.cf.CalculatedFieldCache; import org.thingsboard.server.service.cf.CalculatedFieldStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; @@ -145,26 +145,21 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa private void processMsgs(List> msgs, TbQueueConsumer> consumer, Object consumerKey, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); PendingMsgHolder pendingMsgHolder = new PendingMsgHolder<>(); Future packSubmitFuture = consumersExecutor.submit(() -> { orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); TbCallback callback = new TbPackCallback<>(id, ctx); try { ToCalculatedFieldMsg toCfMsg = msg.getValue(); pendingMsgHolder.setMsg(toCfMsg); - if (toCfMsg.hasTelemetryMsg()) { - log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); - forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); - } else if (toCfMsg.hasLinkedTelemetryMsg()) { - forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); - } + processMsg(toCfMsg, id, callback); } catch (Throwable e) { log.warn("[{}] Failed to process message: {}", id, msg, e); callback.onFailure(e); @@ -182,6 +177,17 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa consumer.commit(); } + private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) { + if (toCfMsg.hasTelemetryMsg()) { + log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); + forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); + } else if (toCfMsg.hasLinkedTelemetryMsg()) { + forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); + } else if (toCfMsg.hasEventMsg()) { + actorContext.tell(CalculatedFieldEntityActionEventMsg.fromProto(toCfMsg.getEventMsg(), callback)); + } + } + @Override protected ServiceType getServiceType() { return ServiceType.TB_RULE_ENGINE; 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 718ec2adfa..bed3729328 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 @@ -26,6 +26,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; +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; @@ -55,6 +56,7 @@ 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.queue.Queue; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; @@ -468,6 +470,17 @@ public class DefaultTbClusterService implements TbClusterService { broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED); } + @Override + public void onCustomerUpdated(Customer customer, Customer oldCustomer) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(customer.getTenantId()) + .entityId(customer.getId()) + .event(oldCustomer == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED) + .ownerChanged(false) // for compatibility with PE + .build(); + broadcast(msg); + } + private void broadcastEntityChangeToTransport(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); @@ -597,7 +610,9 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.JOB, - EntityType.TB_RESOURCE) + EntityType.TB_RESOURCE, + EntityType.CUSTOMER, + EntityType.USER) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { @@ -673,6 +688,7 @@ public class DefaultTbClusterService implements TbClusterService { } msg.event(ComponentLifecycleEvent.UPDATED) .oldProfileId(old.getDeviceProfileId()) + .ownerChanged(!entity.getOwnerId().equals(old.getOwnerId())) .oldName(old.getName()); } broadcast(msg.build()); @@ -693,6 +709,7 @@ public class DefaultTbClusterService implements TbClusterService { } else { msg.event(ComponentLifecycleEvent.UPDATED) .oldProfileId(old.getAssetProfileId()) + .ownerChanged(!entity.getOwnerId().equals(old.getOwnerId())) .oldName(old.getName()); } broadcast(msg.build()); @@ -708,6 +725,28 @@ public class DefaultTbClusterService implements TbClusterService { broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED); } + @Override + public void onRelationUpdated(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(tenantId) + .entityId(entityRelation.getFrom()) + .event(ComponentLifecycleEvent.RELATION_UPDATED) + .info(JacksonUtil.valueToTree(entityRelation)) + .build(); + broadcast(msg); + } + + @Override + public void onRelationDeleted(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(tenantId) + .entityId(entityRelation.getFrom()) + .event(ComponentLifecycleEvent.RELATION_DELETED) + .info(JacksonUtil.valueToTree(entityRelation)) + .build(); + broadcast(msg); + } + @Override public void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId originatorEdgeId) { if (!edgesEnabled) { 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 de2e65723e..519601c30a 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 @@ -86,7 +86,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.cf.CalculatedFieldCache; import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -259,15 +258,15 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, Object consumerKey, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); PendingMsgHolder pendingMsgHolder = new PendingMsgHolder<>(); Future packSubmitFuture = consumersExecutor.submit(() -> { orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); TbCallback callback = new TbPackCallback<>(id, ctx); try { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java index f5381f3a3d..8498c3e205 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java @@ -87,8 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, Object consumerKey, QueueConfig edgeQueueConfig) throws InterruptedException { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); PendingMsgHolder pendingMsgHolder = new PendingMsgHolder<>(); Future submitFuture = consumersExecutor.submit(() -> { orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); TbCallback callback = new TbPackCallback<>(id, ctx); try { ToEdgeMsg toEdgeMsg = msg.getValue(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 22e2f05cb6..e79510decf 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -46,7 +46,6 @@ import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.util.TbRuleEngineComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.cf.CalculatedFieldCache; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedConsumerService; @@ -70,6 +69,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo private final TbRuleEngineConsumerContext ctx; private final QueueService queueService; private final TbRuleEngineDeviceRpcService tbDeviceRpcService; + private final TbMsgPackProcessingContextFactory packProcessingContextFactory; private final ConcurrentMap consumers = new ConcurrentHashMap<>(); @@ -84,11 +84,13 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo TbApiUsageStateService apiUsageStateService, PartitionService partitionService, ApplicationEventPublisher eventPublisher, - JwtSettingsService jwtSettingsService) { + JwtSettingsService jwtSettingsService, + TbMsgPackProcessingContextFactory packProcessingContextFactory) { super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.ctx = ctx; this.tbDeviceRpcService = tbDeviceRpcService; this.queueService = queueService; + this.packProcessingContextFactory = packProcessingContextFactory; } @Override @@ -254,6 +256,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) + .packProcessingContextFactory(packProcessingContextFactory) .build(); consumers.put(queueKey, consumer); consumer.init(queue); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java new file mode 100644 index 0000000000..08307a68ca --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.queue; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; + +public interface TbMsgPackProcessingContextFactory { + + TbMsgPackProcessingContext create(String queueName, TbRuleEngineSubmitStrategy submitStrategy, boolean skipTimeouts); + + @Component + class DefaultTbMsgPackProcessingContextFactory implements TbMsgPackProcessingContextFactory { + + @Override + public TbMsgPackProcessingContext create(String queueName, TbRuleEngineSubmitStrategy submitStrategy, boolean skipTimeouts) { + return new TbMsgPackProcessingContext(queueName, submitStrategy, skipTimeouts); + } + + } + +} 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 f1b3925cdd..4243d71ce5 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 @@ -131,13 +131,13 @@ public abstract class AbstractConsumerService> msgs, TbQueueConsumer> consumer) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); orderedMsgList.forEach(element -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); TbCallback callback = new TbPackCallback<>(id, ctx); try { @@ -196,6 +196,7 @@ public abstract class AbstractConsumerService> getPendingMap() { - return orderedMsgList.stream().collect(Collectors.toConcurrentMap(pair -> pair.uuid, pair -> pair.msg)); + return orderedMsgList.stream().collect(Collectors.toConcurrentMap(pair -> pair.uuid(), pair -> pair.msg())); } @Override public void update(ConcurrentMap> reprocessMap) { List> newOrderedMsgList = new ArrayList<>(reprocessMap.size()); for (IdMsgPair pair : orderedMsgList) { - if (reprocessMap.containsKey(pair.uuid)) { - if (StringUtils.isNotEmpty(pair.getMsg().getValue().getFailureMessage())) { - var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.getMsg().getValue()) + if (reprocessMap.containsKey(pair.uuid())) { + if (StringUtils.isNotEmpty(pair.msg().getValue().getFailureMessage())) { + var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.msg().getValue()) .clearFailureMessage() .clearRelationTypes() .build(); - var newMsg = new TbProtoQueueMsg<>(pair.getMsg().getKey(), toRuleEngineMsg, pair.getMsg().getHeaders()); - newOrderedMsgList.add(new IdMsgPair<>(pair.getUuid(), newMsg)); + var newMsg = new TbProtoQueueMsg<>(pair.msg().getKey(), toRuleEngineMsg, pair.msg().getHeaders()); + newOrderedMsgList.add(new IdMsgPair<>(pair.uuid(), newMsg)); } else { newOrderedMsgList.add(pair); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java index 32fcd65ca4..605c5fe977 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java @@ -73,7 +73,7 @@ public class BatchTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS pendingPack.clear(); for (int i = startIdx; i < endIdx; i++) { IdMsgPair pair = orderedMsgList.get(i); - pendingPack.put(pair.uuid, pair.msg); + pendingPack.put(pair.uuid(), pair.msg()); } tmpPack = new LinkedHashMap<>(pendingPack); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java index 9f48c7bb70..5da8b85dc4 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java @@ -34,7 +34,7 @@ public class BurstTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS if (log.isDebugEnabled()) { log.debug("[{}] submitting [{}] messages to rule engine", queueName, orderedMsgList.size()); } - orderedMsgList.forEach(pair -> msgConsumer.accept(pair.uuid, pair.msg)); + orderedMsgList.forEach(pair -> msgConsumer.accept(pair.uuid(), pair.msg())); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java index 81d2854dac..a0e440f7cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java @@ -15,19 +15,9 @@ */ package org.thingsboard.server.service.queue.processing; -import lombok.Getter; +import com.google.protobuf.GeneratedMessageV3; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import java.util.UUID; -public class IdMsgPair { - @Getter - final UUID uuid; - @Getter - final TbProtoQueueMsg msg; - - public IdMsgPair(UUID uuid, TbProtoQueueMsg msg) { - this.uuid = uuid; - this.msg = msg; - } -} +public record IdMsgPair(UUID uuid, TbProtoQueueMsg msg) {} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java index cfc3bb4a38..5acb629824 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java @@ -51,7 +51,7 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs entityIdToListMap.forEach((entityId, queue) -> { IdMsgPair msg = queue.peek(); if (msg != null) { - msgConsumer.accept(msg.uuid, msg.msg); + msgConsumer.accept(msg.uuid(), msg.msg()); } }); } @@ -71,13 +71,13 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs IdMsgPair next = null; synchronized (queue) { IdMsgPair expected = queue.peek(); - if (expected != null && expected.uuid.equals(id)) { + if (expected != null && expected.uuid().equals(id)) { queue.poll(); next = queue.peek(); } } if (next != null) { - msgConsumer.accept(next.uuid, next.msg); + msgConsumer.accept(next.uuid(), next.msg()); } } } @@ -87,9 +87,9 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs msgToEntityIdMap.clear(); entityIdToListMap.clear(); for (IdMsgPair pair : orderedMsgList) { - EntityId entityId = getEntityId(pair.msg.getValue()); + EntityId entityId = getEntityId(pair.msg().getValue()); if (entityId != null) { - msgToEntityIdMap.put(pair.uuid, entityId); + msgToEntityIdMap.put(pair.uuid(), entityId); entityIdToListMap.computeIfAbsent(entityId, id -> new LinkedList<>()).add(pair); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java index 64be9bd378..be32f155dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java @@ -60,11 +60,11 @@ public class SequentialTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSu int idx = msgIdx.get(); if (idx < listSize) { IdMsgPair pair = orderedMsgList.get(idx); - expectedMsgId = pair.uuid; + expectedMsgId = pair.uuid(); if (log.isDebugEnabled()) { - log.debug("[{}] submitting [{}] message to rule engine", queueName, pair.msg); + log.debug("[{}] submitting [{}] message to rule engine", queueName, pair.msg()); } - msgConsumer.accept(pair.uuid, pair.msg); + msgConsumer.accept(pair.uuid(), pair.msg()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index 582205982f..3733b49f1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -38,9 +38,11 @@ import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.DeleteQueueTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; @@ -66,13 +68,15 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager { Integer partitionId = tpi != null ? tpi.getPartition().orElse(-1) : null; @@ -81,6 +85,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager> msgs, TbQueueConsumer> consumer, - Object consumerKey, + ConsumerKey consumerKey, Queue queue) throws Exception { TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue); TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue); submitStrategy.init(msgs); while (!stopped && !consumer.isStopped()) { - TbMsgPackProcessingContext packCtx = new TbMsgPackProcessingContext(queue.getName(), submitStrategy, ackStrategy.isSkipTimeoutMsgs()); + TbMsgPackProcessingContext packCtx = packProcessingContextFactory.create(queue.getName(), submitStrategy, ackStrategy.isSkipTimeoutMsgs()); submitStrategy.submitAttempt((id, msg) -> submitMessage(packCtx, id, msg)); final boolean timeout = !packCtx.await(queue.getPackProcessingTimeout(), TimeUnit.MILLISECONDS); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 1fcf1fcc71..395dc7e955 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -75,7 +75,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements ActionType actionType = resource.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = resource.getTenantId(); try { - if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { + if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) && resource.getId() == null) { toLwm2mResource(resource); } else if (resource.getResourceKey() == null) { resource.setResourceKey(resource.getFileName()); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index afce336d0e..53801383a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -19,8 +19,8 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceDeleteResult; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; 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 2c5e706838..19df974999 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 @@ -69,7 +69,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.resource.ResourceService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractAuthenticationProvider.java new file mode 100644 index 0000000000..4da933e774 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractAuthenticationProvider.java @@ -0,0 +1,95 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; +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.security.Authority; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; + +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractAuthenticationProvider implements AuthenticationProvider { + + private final CustomerService customerService; + private final UserAuthDetailsCache userAuthDetailsCache; + + protected SecurityUser authenticateByPublicId(String publicId, String authContextName, UserPrincipal userPrincipal) { + TenantId systemId = TenantId.SYS_TENANT_ID; + CustomerId customerId; + try { + customerId = new CustomerId(UUID.fromString(publicId)); + } catch (Exception e) { + throw new BadCredentialsException(authContextName + " is not valid"); + } + Customer publicCustomer = customerService.findCustomerById(systemId, customerId); + if (publicCustomer == null) { + throw new UsernameNotFoundException("Public entity not found"); + } + + if (!publicCustomer.isPublic()) { + throw new BadCredentialsException(authContextName + " is not valid"); + } + + User user = new User(new UserId(EntityId.NULL_UUID)); + user.setTenantId(publicCustomer.getTenantId()); + user.setCustomerId(publicCustomer.getId()); + user.setEmail(publicId); + user.setAuthority(Authority.CUSTOMER_USER); + user.setFirstName("Public"); + user.setLastName("Public"); + + UserPrincipal principal = userPrincipal == null ? new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId) : userPrincipal; + + return new SecurityUser(user, true, principal); + } + + protected SecurityUser authenticateByUserId(TenantId tenantId, UserId userId) { + UserAuthDetails userAuthDetails = userAuthDetailsCache.getUserAuthDetails(tenantId, userId); + if (userAuthDetails == null) { + throw new UsernameNotFoundException("User with credentials not found"); + } + if (!userAuthDetails.credentialsEnabled()) { + throw new DisabledException("User is not active"); + } + + User user = userAuthDetails.user(); + if (user.getAuthority() == null) { + throw new InsufficientAuthenticationException("User has no authority assigned"); + } + + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + return new SecurityUser(user, true, userPrincipal); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java index 6da2ca51b2..050a805971 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java @@ -18,8 +18,10 @@ package org.thingsboard.server.service.security.auth; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -32,6 +34,10 @@ public class AuthExceptionHandler extends OncePerRequestFilter { private final ThingsboardErrorResponseHandler errorResponseHandler; + @Value("${server.log_controller_error_stack_trace}") + @Getter + private boolean logControllerErrorStackTrace; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { try { @@ -39,8 +45,15 @@ public class AuthExceptionHandler extends OncePerRequestFilter { } catch (AuthenticationException e) { throw e; } catch (Exception e) { + log(e); errorResponseHandler.handle(e, response); } } + private void log(Exception e) { + if (logControllerErrorStackTrace) { + log.error("Auth error", e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java index 5e183d9bc1..77ac52ff79 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java @@ -49,21 +49,22 @@ public class DefaultTokenOutdatingService implements TokenOutdatingService { @Override public boolean isOutdated(String token, UserId userId) { - Claims claims = tokenFactory.parseTokenClaims(token).getBody(); + Claims claims = tokenFactory.parseTokenClaims(token).getPayload(); long issueTime = claims.getIssuedAt().getTime(); String sessionId = claims.get("sessionId", String.class); - if (isTokenOutdated(issueTime, userId.toString())){ - return true; + if (isTokenOutdated(issueTime, userId.toString())) { + return true; } else { - return sessionId != null && isTokenOutdated(issueTime, sessionId); + return sessionId != null && isTokenOutdated(issueTime, sessionId); } } private Boolean isTokenOutdated(long issueTime, String sessionId) { - return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false); + return Optional.ofNullable(cache.get(sessionId)).map(outdatedTime -> isTokenOutdated(issueTime, outdatedTime.get())).orElse(false); } private boolean isTokenOutdated(long issueTime, Long outdatageTime) { return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java new file mode 100644 index 0000000000..048ba1ff26 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import org.thingsboard.server.service.security.model.SecurityUser; + +public class MfaConfigurationToken extends AbstractJwtAuthenticationToken { + public MfaConfigurationToken(SecurityUser securityUser) { + super(securityUser); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/AbstractHeaderTokenExtractor.java similarity index 74% rename from application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/extractor/AbstractHeaderTokenExtractor.java index 3d9fbc4dd2..533e921995 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/AbstractHeaderTokenExtractor.java @@ -13,32 +13,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.jwt.extractor; +package org.thingsboard.server.service.security.auth.extractor; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; -@Component(value="jwtHeaderTokenExtractor") -public class JwtHeaderTokenExtractor implements TokenExtractor { - public static final String HEADER_PREFIX = "Bearer "; +public abstract class AbstractHeaderTokenExtractor implements TokenExtractor { + + private final String headerPrefix; + + protected AbstractHeaderTokenExtractor(String headerPrefix) { + this.headerPrefix = headerPrefix; + } @Override public String extract(HttpServletRequest request) { - String header = request.getHeader(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM); + String header = request.getHeader(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER); if (StringUtils.isBlank(header)) { - header = request.getHeader(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM_V2); + header = request.getHeader(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2); if (StringUtils.isBlank(header)) { throw new AuthenticationServiceException("Authorization header cannot be blank!"); } } - if (header.length() < HEADER_PREFIX.length()) { + if (header.length() < headerPrefix.length()) { throw new AuthenticationServiceException("Invalid authorization header size."); } - return header.substring(HEADER_PREFIX.length(), header.length()); + return header.substring(headerPrefix.length()); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/ApiKeyHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/ApiKeyHeaderTokenExtractor.java new file mode 100644 index 0000000000..4165726bca --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/ApiKeyHeaderTokenExtractor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.extractor; + +import org.springframework.stereotype.Component; + +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; + +@Component(value = "apiKeyHeaderTokenExtractor") +public class ApiKeyHeaderTokenExtractor extends AbstractHeaderTokenExtractor { + + public ApiKeyHeaderTokenExtractor() { + super(API_KEY_HEADER_PREFIX); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/JwtHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/JwtHeaderTokenExtractor.java new file mode 100644 index 0000000000..d48be8899c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/JwtHeaderTokenExtractor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.extractor; + +import org.springframework.stereotype.Component; + +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.BEARER_HEADER_PREFIX; + +@Component(value = "jwtHeaderTokenExtractor") +public class JwtHeaderTokenExtractor extends AbstractHeaderTokenExtractor { + + public JwtHeaderTokenExtractor() { + super(BEARER_HEADER_PREFIX); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/JwtQueryTokenExtractor.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/extractor/JwtQueryTokenExtractor.java index 41e9594834..9124a5fa6d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/JwtQueryTokenExtractor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.jwt.extractor; +package org.thingsboard.server.service.security.auth.extractor; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.authentication.AuthenticationServiceException; @@ -21,7 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; -@Component(value="jwtQueryTokenExtractor") +@Component(value = "jwtQueryTokenExtractor") public class JwtQueryTokenExtractor implements TokenExtractor { @Override @@ -39,4 +39,5 @@ public class JwtQueryTokenExtractor implements TokenExtractor { return token; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/TokenExtractor.java similarity index 91% rename from application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/extractor/TokenExtractor.java index 46fb71c226..aee3689f27 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/TokenExtractor.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.jwt.extractor; +package org.thingsboard.server.service.security.auth.extractor; import jakarta.servlet.http.HttpServletRequest; public interface TokenExtractor { + String extract(HttpServletRequest request); -} \ No newline at end of file + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java index 914ac9691f..2bffb2b338 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java @@ -39,7 +39,7 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); - SecurityUser securityUser = authenticate(rawAccessToken.getToken()); + SecurityUser securityUser = authenticate(rawAccessToken.token()); return new JwtAuthenticationToken(securityUser); } @@ -58,4 +58,5 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { public boolean supports(Class authentication) { return (JwtAuthenticationToken.class.isAssignableFrom(authentication)); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java index 96aabcf937..76422dabbb 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java @@ -28,12 +28,17 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.util.matcher.RequestMatcher; import org.thingsboard.server.service.security.auth.JwtAuthenticationToken; -import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; +import org.thingsboard.server.service.security.auth.extractor.TokenExtractor; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; import java.io.IOException; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.BEARER_HEADER_PREFIX; + public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { + private final AuthenticationFailureHandler failureHandler; private final TokenExtractor tokenExtractor; @@ -46,8 +51,7 @@ public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticati } @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException, IOException, ServletException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(request)); return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token)); } @@ -61,10 +65,27 @@ public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticati chain.doFilter(request, response); } + @Override + protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { + if (!super.requiresAuthentication(request, response)) { + return false; + } + String header = request.getHeader(AUTHORIZATION_HEADER); + if (header == null) { + header = request.getHeader(AUTHORIZATION_HEADER_V2); + } + if (header == null) { + // If there is NO auth header at all, let the JWT filter try to attempt Authentication and failure in the process. + return true; + } + return header.startsWith(BEARER_HEADER_PREFIX); + } + @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); failureHandler.onAuthenticationFailure(request, response, failed); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index eac622e6fa..16cc507e0b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -15,123 +15,63 @@ */ package org.thingsboard.server.service.security.auth.jwt; -import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.Assert; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.User; -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.security.Authority; -import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.AbstractAuthenticationProvider; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; - -import java.util.UUID; +import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; @Component -@RequiredArgsConstructor -public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { +public class RefreshTokenAuthenticationProvider extends AbstractAuthenticationProvider { + private final JwtTokenFactory tokenFactory; - private final UserService userService; - private final CustomerService customerService; private final TokenOutdatingService tokenOutdatingService; + public RefreshTokenAuthenticationProvider(JwtTokenFactory jwtTokenFactory, UserAuthDetailsCache userAuthDetailsCache, + CustomerService customerService, TokenOutdatingService tokenOutdatingService) { + super(customerService, userAuthDetailsCache); + this.tokenFactory = jwtTokenFactory; + this.tokenOutdatingService = tokenOutdatingService; + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.notNull(authentication, "No authentication data provided"); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); - SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken.getToken()); + SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken.token()); UserPrincipal principal = unsafeUser.getUserPrincipal(); SecurityUser securityUser; if (principal.getType() == UserPrincipal.Type.USER_NAME) { - securityUser = authenticateByUserId(unsafeUser.getId()); + securityUser = authenticateByUserId(TenantId.SYS_TENANT_ID, unsafeUser.getId()); } else { securityUser = authenticateByPublicId(principal.getValue()); } securityUser.setSessionId(unsafeUser.getSessionId()); - if (tokenOutdatingService.isOutdated(rawAccessToken.getToken(), securityUser.getId())) { + if (tokenOutdatingService.isOutdated(rawAccessToken.token(), securityUser.getId())) { throw new CredentialsExpiredException("Token is outdated"); } return new RefreshAuthenticationToken(securityUser); } - private SecurityUser authenticateByUserId(UserId userId) { - TenantId systemId = TenantId.SYS_TENANT_ID; - User user = userService.findUserById(systemId, userId); - if (user == null) { - throw new UsernameNotFoundException("User not found by refresh token"); - } - - UserCredentials userCredentials = userService.findUserCredentialsByUserId(systemId, user.getId()); - if (userCredentials == null) { - throw new UsernameNotFoundException("User credentials not found"); - } - - if (!userCredentials.isEnabled()) { - throw new DisabledException("User is not active"); - } - - if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); - - UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); - - return securityUser; - } - private SecurityUser authenticateByPublicId(String publicId) { - TenantId systemId = TenantId.SYS_TENANT_ID; - CustomerId customerId; - try { - customerId = new CustomerId(UUID.fromString(publicId)); - } catch (Exception e) { - throw new BadCredentialsException("Refresh token is not valid"); - } - Customer publicCustomer = customerService.findCustomerById(systemId, customerId); - if (publicCustomer == null) { - throw new UsernameNotFoundException("Public entity not found by refresh token"); - } - - if (!publicCustomer.isPublic()) { - throw new BadCredentialsException("Refresh token is not valid"); - } - - User user = new User(new UserId(EntityId.NULL_UUID)); - user.setTenantId(publicCustomer.getTenantId()); - user.setCustomerId(publicCustomer.getId()); - user.setEmail(publicId); - user.setAuthority(Authority.CUSTOMER_USER); - user.setFirstName("Public"); - user.setLastName("Public"); - - UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId); - - SecurityUser securityUser = new SecurityUser(user, true, userPrincipal); - - return securityUser; + return super.authenticateByPublicId(publicId, "Refresh token", null); } @Override public boolean supports(Class authentication) { return (RefreshAuthenticationToken.class.isAssignableFrom(authentication)); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java index fedeccf7d6..84eb4295f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java @@ -51,11 +51,10 @@ public class RefreshTokenProcessingFilter extends AbstractAuthenticationProcessi } @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException, IOException, ServletException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!HttpMethod.POST.name().equals(request.getMethod())) { - if(log.isDebugEnabled()) { - log.debug("Authentication method not supported. Request method: " + request.getMethod()); + if (log.isDebugEnabled()) { + log.debug("Authentication method not supported. Request method: {}", request.getMethod()); } throw new AuthMethodNotSupportedException("Authentication method not supported"); } @@ -67,11 +66,11 @@ public class RefreshTokenProcessingFilter extends AbstractAuthenticationProcessi throw new AuthenticationServiceException("Invalid refresh token request payload"); } - if (StringUtils.isBlank(refreshTokenRequest.getRefreshToken())) { + if (refreshTokenRequest == null || StringUtils.isBlank(refreshTokenRequest.refreshToken())) { throw new AuthenticationServiceException("Refresh token is not provided"); } - RawAccessJwtToken token = new RawAccessJwtToken(refreshTokenRequest.getRefreshToken()); + RawAccessJwtToken token = new RawAccessJwtToken(refreshTokenRequest.refreshToken()); return this.getAuthenticationManager().authenticate(new RefreshAuthenticationToken(token)); } @@ -88,4 +87,5 @@ public class RefreshTokenProcessingFilter extends AbstractAuthenticationProcessi SecurityContextHolder.clearContext(); failureHandler.onAuthenticationFailure(request, response, failed); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java index ea5a4d8d0d..4435bc49a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java @@ -18,15 +18,11 @@ package org.thingsboard.server.service.security.auth.jwt; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public class RefreshTokenRequest { - private String refreshToken; +public record RefreshTokenRequest(String refreshToken) { @JsonCreator public RefreshTokenRequest(@JsonProperty("refreshToken") String refreshToken) { this.refreshToken = refreshToken; } - public String getRefreshToken() { - return refreshToken; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java index 2a77d46ddd..e6891e41e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java @@ -25,12 +25,13 @@ import java.util.List; import java.util.stream.Collectors; public class SkipPathRequestMatcher implements RequestMatcher { - private OrRequestMatcher matchers; - private RequestMatcher processingMatcher; + + private final OrRequestMatcher matchers; + private final RequestMatcher processingMatcher; public SkipPathRequestMatcher(List pathsToSkip, String processingPath) { Assert.notNull(pathsToSkip, "List of paths to skip is required."); - List m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList()); + List m = pathsToSkip.stream().map(AntPathRequestMatcher::new).collect(Collectors.toList()); matchers = new OrRequestMatcher(m); processingMatcher = new AntPathRequestMatcher(processingPath); } @@ -40,6 +41,7 @@ public class SkipPathRequestMatcher implements RequestMatcher { if (matchers.matches(request)) { return false; } - return processingMatcher.matches(request) ? true : false; + return processingMatcher.matches(request); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index f0a8257a2c..80b201e656 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -21,7 +21,7 @@ import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.Arrays; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.model.JwtSettings; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.Base64; import java.util.Optional; @@ -66,7 +66,7 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator { throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!"); } - System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory + System.arraycopy(decodedKey, 0, RandomUtils.secure().randomBytes(decodedKey.length), 0, decodedKey.length); // secure memory } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java index 0b6e246b59..e30f542f17 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java @@ -22,9 +22,8 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.model.JwtSettings; /** - * During Install or upgrade the validation is suppressed to keep existing data + * During Install or upgrade, the validation is suppressed to keep existing data * */ - @Primary @Profile("install") @Component diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java index 05e72d8de0..39ccd65738 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java @@ -20,4 +20,5 @@ import org.thingsboard.server.common.data.security.model.JwtSettings; public interface JwtSettingsValidator { void validate(JwtSettings jwtSettings); + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 6c471aeee5..0f81e251cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; @@ -61,12 +62,25 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private static final ThingsboardException TOO_MANY_REQUESTS_ERROR = new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); @Override - public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { - return configManager.getAccountTwoFaSettings(tenantId, userId) + public boolean isTwoFaEnabled(TenantId tenantId, User user) { + return configManager.getAccountTwoFaSettings(tenantId, user) .map(settings -> !settings.getConfigs().isEmpty()) .orElse(false); } + @Override + public boolean isEnforceTwoFaEnabled(TenantId tenantId, User user) { + SystemLevelUsersFilter enforcedUsersFilter = configManager.getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, true) + .filter(PlatformTwoFaSettings::isEnforceTwoFa) + .map(PlatformTwoFaSettings::getEnforcedUsersFilter) + .orElse(null); + if (enforcedUsersFilter == null) { + return false; + } + + return userService.matchesFilter(tenantId, enforcedUsersFilter, user); + } + @Override public void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException { getTwoFaProvider(providerType).check(tenantId); @@ -75,7 +89,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { @Override public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception { - TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) + TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType) .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); prepareVerificationCode(user, accountConfig, checkLimits); } @@ -104,7 +118,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { @Override public boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException { - TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) + TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType) .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); return checkVerificationCode(user, verificationCode, accountConfig, checkLimits); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index dd4ed4d7b7..ee77d5d3a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -18,17 +18,17 @@ package org.thingsboard.server.service.security.auth.mfa; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.service.security.model.SecurityUser; public interface TwoFactorAuthService { - boolean isTwoFaEnabled(TenantId tenantId, UserId userId); + boolean isTwoFaEnabled(TenantId tenantId, User user); - void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException; + boolean isEnforceTwoFaEnabled(TenantId tenantId, User user); + void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException; void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index 5cabbc8e58..eb2342c23d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -16,14 +16,13 @@ package org.thingsboard.server.service.security.auth.mfa.config; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserAuthSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; @@ -34,6 +33,7 @@ import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserAuthSettingsDao; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import java.util.Comparator; @@ -48,16 +48,15 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { private final UserAuthSettingsDao userAuthSettingsDao; private final AdminSettingsService adminSettingsService; private final AdminSettingsDao adminSettingsDao; - @Autowired @Lazy - private TwoFactorAuthService twoFactorAuthService; + @Lazy + private final TwoFactorAuthService twoFactorAuthService; protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; - @Override - public Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId) { + public Optional getAccountTwoFaSettings(TenantId tenantId, User user) { PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null); - return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + return Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId())) .map(userAuthSettings -> { AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings(); if (twoFaSettings == null) return null; @@ -79,17 +78,17 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { } if (updateNeeded) { - twoFaSettings = saveAccountTwoFaSettings(tenantId, userId, twoFaSettings); + twoFaSettings = saveAccountTwoFaSettings(tenantId, user, twoFaSettings); } return twoFaSettings; }); } - protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) { - UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) { + UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId())) .orElseGet(() -> { UserAuthSettings newUserAuthSettings = new UserAuthSettings(); - newUserAuthSettings.setUserId(userId); + newUserAuthSettings.setUserId(user.getId()); return newUserAuthSettings; }); userAuthSettings.setTwoFaSettings(settings); @@ -99,20 +98,19 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { return settings; } - @Override - public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { - return getAccountTwoFaSettings(tenantId, userId) + public Optional getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) { + return getAccountTwoFaSettings(tenantId, user) .map(AccountTwoFaSettings::getConfigs) .flatMap(configs -> Optional.ofNullable(configs.get(providerType))); } @Override - public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig) { + public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, User user, TwoFaAccountConfig accountConfig) { getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) .orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured")); - AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId).orElseGet(() -> { + AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, user).orElseGet(() -> { AccountTwoFaSettings newSettings = new AccountTwoFaSettings(); newSettings.setConfigs(new LinkedHashMap<>()); return newSettings; @@ -128,12 +126,13 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true)); } - return saveAccountTwoFaSettings(tenantId, userId, settings); + checkAccountTwoFaSettings(tenantId, user, settings); + return saveAccountTwoFaSettings(tenantId, user, settings); } @Override - public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { - AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId) + public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) { + AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, user) .orElseThrow(() -> new IllegalArgumentException("2FA not configured")); settings.getConfigs().remove(providerType); if (settings.getConfigs().size() == 1) { @@ -145,7 +144,8 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { .min(Comparator.comparing(TwoFaAccountConfig::getProviderType)) .ifPresent(config -> config.setUseByDefault(true)); } - return saveAccountTwoFaSettings(tenantId, userId, settings); + checkAccountTwoFaSettings(tenantId, user, settings); + return saveAccountTwoFaSettings(tenantId, user, settings); } @@ -166,6 +166,19 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) { twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType()); } + if (tenantId.isSysTenantId()) { + if (twoFactorAuthSettings.isEnforceTwoFa()) { + if (twoFactorAuthSettings.getProviders().isEmpty()) { + throw new DataValidationException("At least one 2FA provider is required if enforcing is enabled"); + } + if (twoFactorAuthSettings.getEnforcedUsersFilter() == null) { + throw new DataValidationException("Users filter to enforce 2FA for is required"); + } + } + } else { + twoFactorAuthSettings.setEnforceTwoFa(false); + twoFactorAuthSettings.setEnforcedUsersFilter(null); + } AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) .orElseGet(() -> { @@ -184,4 +197,12 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { .ifPresent(adminSettings -> adminSettingsDao.removeById(tenantId, adminSettings.getId().getId())); } + private void checkAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) { + if (settings.getConfigs().isEmpty()) { + if (twoFactorAuthService.isEnforceTwoFaEnabled(tenantId, user)) { + throw new DataValidationException("At least one 2FA provider is required"); + } + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java index 6edc580c3c..9557ce9118 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; @@ -27,14 +27,14 @@ import java.util.Optional; public interface TwoFaConfigManager { - Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId); + Optional getAccountTwoFaSettings(TenantId tenantId, User user); - Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); + Optional getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType); - AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig); + AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, User user, TwoFaAccountConfig accountConfig); - AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); + AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType); Optional getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java index 59b2ebb8d3..ccb82e5b70 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java @@ -58,7 +58,7 @@ public class BackupCodeTwoFaProvider implements TwoFaProvider authentication) { + return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java new file mode 100644 index 0000000000..0665f23298 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.token.ApiKeyAuthRequest; + +import java.io.Serial; + +public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { + + @Serial + private static final long serialVersionUID = 2978710889397403536L; + + private ApiKeyAuthRequest apiKeyAuthRequest; + private SecurityUser securityUser; + + public ApiKeyAuthenticationToken(ApiKeyAuthRequest apiKeyAuthRequest) { + super(null); + this.apiKeyAuthRequest = apiKeyAuthRequest; + setAuthenticated(false); + } + + public ApiKeyAuthenticationToken(SecurityUser securityUser) { + super(securityUser.getAuthorities()); + this.eraseCredentials(); + this.securityUser = securityUser; + super.setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return apiKeyAuthRequest; + } + + @Override + public Object getPrincipal() { + return this.securityUser; + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + this.apiKeyAuthRequest = null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java new file mode 100644 index 0000000000..789efe6ad0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java @@ -0,0 +1,88 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.thingsboard.server.service.security.auth.extractor.TokenExtractor; +import org.thingsboard.server.service.security.model.token.ApiKeyAuthRequest; + +import java.io.IOException; + +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2; + +public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { + + private final AuthenticationFailureHandler failureHandler; + private final TokenExtractor tokenExtractor; + + @Autowired + public ApiKeyTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, + @Qualifier("apiKeyHeaderTokenExtractor") TokenExtractor tokenExtractor, RequestMatcher matcher) { + super(matcher); + this.failureHandler = failureHandler; + this.tokenExtractor = tokenExtractor; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + String apiKeyValue = tokenExtractor.extract(request); + ApiKeyAuthRequest apiKeyAuthRequest = new ApiKeyAuthRequest(apiKeyValue); + return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(apiKeyAuthRequest)); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authResult) throws IOException, ServletException { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authResult); + SecurityContextHolder.setContext(context); + chain.doFilter(request, response); + } + + @Override + protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { + if (!super.requiresAuthentication(request, response)) { + return false; + } + String header = request.getHeader(AUTHORIZATION_HEADER); + if (header == null) { + header = request.getHeader(AUTHORIZATION_HEADER_V2); + } + return header != null && header.startsWith(API_KEY_HEADER_PREFIX); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + SecurityContextHolder.clearContext(); + failureHandler.onAuthenticationFailure(request, response, failed); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index e94cacd99c..7bf35e22fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.security.auth.rest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.LockedException; @@ -27,41 +26,34 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.Assert; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; -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.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.AbstractAuthenticationProvider; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; +import org.thingsboard.server.service.security.auth.MfaConfigurationToken; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.exception.UserPasswordNotValidException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.system.SystemSecurityService; -import java.util.UUID; - - @Component @Slf4j @TbCoreComponent -public class RestAuthenticationProvider implements AuthenticationProvider { +public class RestAuthenticationProvider extends AbstractAuthenticationProvider { private final SystemSecurityService systemSecurityService; private final SecuritySettingsService securitySettingsService; private final UserService userService; - private final CustomerService customerService; private final TwoFactorAuthService twoFactorAuthService; @Autowired @@ -70,8 +62,8 @@ public class RestAuthenticationProvider implements AuthenticationProvider { final SystemSecurityService systemSecurityService, SecuritySettingsService securitySettingsService, TwoFactorAuthService twoFactorAuthService) { + super(customerService, null); this.userService = userService; - this.customerService = customerService; this.systemSecurityService = systemSecurityService; this.securitySettingsService = securitySettingsService; this.twoFactorAuthService = twoFactorAuthService; @@ -82,11 +74,10 @@ public class RestAuthenticationProvider implements AuthenticationProvider { Assert.notNull(authentication, "No authentication data provided"); Object principal = authentication.getPrincipal(); - if (!(principal instanceof UserPrincipal)) { + if (!(principal instanceof UserPrincipal userPrincipal)) { throw new BadCredentialsException("Authentication Failed. Bad user principal."); } - UserPrincipal userPrincipal = (UserPrincipal) principal; SecurityUser securityUser; if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { String username = userPrincipal.getValue(); @@ -103,8 +94,10 @@ public class RestAuthenticationProvider implements AuthenticationProvider { } securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password); - if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) { + if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser)) { return new MfaAuthenticationToken(securityUser); + } else if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId(), securityUser)) { + return new MfaConfigurationToken(securityUser); } else { systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null); } @@ -123,7 +116,6 @@ public class RestAuthenticationProvider implements AuthenticationProvider { } try { - UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId()); if (userCredentials == null) { throw new UsernameNotFoundException("User credentials not found"); @@ -136,8 +128,9 @@ public class RestAuthenticationProvider implements AuthenticationProvider { throw e; } - if (user.getAuthority() == null) + if (user.getAuthority() == null) { throw new InsufficientAuthenticationException("User has no authority assigned"); + } return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); } catch (Exception e) { @@ -147,28 +140,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { } private SecurityUser authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { - CustomerId customerId; - try { - customerId = new CustomerId(UUID.fromString(publicId)); - } catch (Exception e) { - throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); - } - Customer publicCustomer = customerService.findCustomerById(TenantId.SYS_TENANT_ID, customerId); - if (publicCustomer == null) { - throw new UsernameNotFoundException("Public entity not found: " + publicId); - } - if (!publicCustomer.isPublic()) { - throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); - } - User user = new User(new UserId(EntityId.NULL_UUID)); - user.setTenantId(publicCustomer.getTenantId()); - user.setCustomerId(publicCustomer.getId()); - user.setEmail(publicId); - user.setAuthority(Authority.CUSTOMER_USER); - user.setFirstName("Public"); - user.setLastName("Public"); - - return new SecurityUser(user, true, userPrincipal); + return super.authenticateByPublicId(publicId, "Public Id", userPrincipal); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java index 2b3a6cc5c9..f774b541ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.auth.rest; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -24,8 +23,6 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand import org.springframework.stereotype.Component; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; -import java.io.IOException; - @Component(value = "defaultAuthenticationFailureHandler") public class RestAwareAuthenticationFailureHandler implements AuthenticationFailureHandler { @@ -37,8 +34,8 @@ public class RestAwareAuthenticationFailureHandler implements AuthenticationFail } @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException e) throws IOException, ServletException { + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { errorResponseHandler.handle(e, response); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index aa8149ea71..ff4435788e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -15,11 +15,11 @@ */ package org.thingsboard.server.service.security.auth.rest; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; @@ -30,6 +30,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; +import org.thingsboard.server.service.security.auth.MfaConfigurationToken; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -38,7 +39,7 @@ import java.io.IOException; import java.util.Optional; import java.util.concurrent.TimeUnit; -@Component(value = "defaultAuthenticationSuccessHandler") +@Slf4j @Component(value = "defaultAuthenticationSuccessHandler") @RequiredArgsConstructor public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final JwtTokenFactory tokenFactory; @@ -46,18 +47,14 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) throws IOException, ServletException { + Authentication authentication) throws IOException { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); - JwtPair tokenPair = new JwtPair(); + JwtPair tokenPair; if (authentication instanceof MfaAuthenticationToken) { - int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true) - .flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification()) - .filter(time -> time > 0)) - .orElse((int) TimeUnit.MINUTES.toSeconds(30)); - tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken()); - tokenPair.setRefreshToken(null); - tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); + tokenPair = createMfaTokenPair(securityUser, Authority.PRE_VERIFICATION_TOKEN); + } else if (authentication instanceof MfaConfigurationToken) { + tokenPair = createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN); } else { tokenPair = tokenFactory.createTokenPair(securityUser); } @@ -69,6 +66,19 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc clearAuthenticationAttributes(request); } + public JwtPair createMfaTokenPair(SecurityUser securityUser, Authority scope) { + log.debug("[{}][{}] Creating {} token", securityUser.getTenantId(), securityUser.getId(), scope); + JwtPair tokenPair = new JwtPair(); + int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true) + .flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification()) + .filter(time -> time > 0)) + .orElse((int) TimeUnit.MINUTES.toSeconds(30)); + tokenPair.setToken(tokenFactory.createMfaToken(securityUser, scope, preVerificationTokenLifetime).token()); + tokenPair.setRefreshToken(null); + tokenPair.setScope(scope); + return tokenPair; + } + /** * Removes temporary authentication-related data which may have been stored * in the session during the authentication process.. @@ -83,4 +93,5 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java index 35bf3367c8..2e59976e86 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java +++ b/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java @@ -17,7 +17,11 @@ package org.thingsboard.server.service.security.exception; import org.springframework.security.core.AuthenticationException; +import java.io.Serial; + public class JwtExpiredTokenException extends AuthenticationException { + + @Serial private static final long serialVersionUID = -5959543783324224864L; private String token; @@ -34,4 +38,5 @@ public class JwtExpiredTokenException extends AuthenticationException { public String token() { return this.token; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java index db0ce87052..a6504fdd73 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java @@ -17,15 +17,6 @@ package org.thingsboard.server.service.security.model.token; import org.thingsboard.server.common.data.security.model.JwtToken; -public final class AccessJwtToken implements JwtToken { - private final String rawToken; - - public AccessJwtToken(String rawToken) { - this.rawToken = rawToken; - } - - public String getToken() { - return this.rawToken; - } +public record AccessJwtToken(String token) implements JwtToken { } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/ApiKeyAuthRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/ApiKeyAuthRequest.java new file mode 100644 index 0000000000..fee06eea55 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/ApiKeyAuthRequest.java @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.model.token; + +public record ApiKeyAuthRequest(String apiKey) {} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 5b4f4dc94a..051e061857 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -115,13 +115,16 @@ public class JwtTokenFactory { throw new IllegalArgumentException("JWT Token doesn't have any scopes"); } + Authority authority = Authority.parse(scopes.get(0)); + SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setEmail(subject); - securityUser.setAuthority(Authority.parse(scopes.get(0))); + securityUser.setAuthority(authority); String tenantId = claims.get(TENANT_ID, String.class); + if (tenantId != null) { securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId))); - } else if (securityUser.getAuthority() == Authority.SYS_ADMIN) { + } else if (authority == Authority.SYS_ADMIN) { securityUser.setTenantId(TenantId.SYS_TENANT_ID); } String customerId = claims.get(CUSTOMER_ID, String.class); @@ -132,18 +135,15 @@ public class JwtTokenFactory { securityUser.setSessionId(claims.get(SESSION_ID, String.class)); } - UserPrincipal principal; - if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { + boolean isPublic = false; + if (authority != Authority.PRE_VERIFICATION_TOKEN && authority != Authority.MFA_CONFIGURATION_TOKEN) { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); - boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); - principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); - } else { - principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, subject); + isPublic = claims.get(IS_PUBLIC, Boolean.class); } + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); securityUser.setUserPrincipal(principal); - return securityUser; } @@ -179,8 +179,8 @@ public class JwtTokenFactory { return securityUser; } - public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) { - JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime) + public JwtToken createMfaToken(SecurityUser user, Authority scope, Integer expirationTime) { + JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(scope.name()), expirationTime) .claim(TENANT_ID, user.getTenantId().toString()); if (user.getCustomerId() != null) { jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString()); @@ -235,7 +235,7 @@ public class JwtTokenFactory { securityUser.setSessionId(UUID.randomUUID().toString()); JwtToken accessToken = createAccessJwtToken(securityUser); JwtToken refreshToken = createRefreshToken(securityUser); - return new JwtPair(accessToken.getToken(), refreshToken.getToken()); + return new JwtPair(accessToken.token(), refreshToken.token()); } private SecretKey getSecretKey(boolean forceReload) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java index 98f9215ebf..c08497bdda 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -20,9 +20,9 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; @@ -43,8 +43,7 @@ public class OAuth2AppTokenFactory { Jws jwsClaims; try { jwsClaims = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(appSecret))).build().parseSignedClaims(appToken); - } - catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { + } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { throw new IllegalArgumentException("Invalid Application token: ", ex); } catch (ExpiredJwtException expiredEx) { throw new IllegalArgumentException("Application token expired", expiredEx); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java index 0caffe21c1..af767cb2ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java @@ -19,18 +19,6 @@ import org.thingsboard.server.common.data.security.model.JwtToken; import java.io.Serializable; -public class RawAccessJwtToken implements JwtToken, Serializable { +public record RawAccessJwtToken(String token) implements JwtToken, Serializable { - private static final long serialVersionUID = -797397445703066079L; - - private String token; - - public RawAccessJwtToken(String token) { - this.token = token; - } - - @Override - public String getToken() { - return token; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java index 6056cb3ba7..46bd079b8e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java @@ -24,6 +24,10 @@ public interface AccessControlService { void checkPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException; + boolean hasPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException; + void checkPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException; + boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException; + } 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 8d5269a332..a3e3ff458b 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 @@ -21,14 +21,16 @@ import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -@Component(value = "customerUserPermissions") +@Component public class CustomerUserPermissions extends AbstractPermissions { public CustomerUserPermissions() { @@ -48,6 +50,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.ASSET_PROFILE, profilePermissionChecker); put(Resource.TB_RESOURCE, customerResourcePermissionChecker); put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.API_KEY, apiKeysPermissionChecker); } private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() { @@ -202,4 +205,19 @@ public class CustomerUserPermissions extends AbstractPermissions { return user.getTenantId().equals(entity.getTenantId()); } }; + + private static final PermissionChecker apiKeysPermissionChecker = new PermissionChecker() { + + @Override + public boolean hasPermission(SecurityUser user, Operation operation) { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public boolean hasPermission(SecurityUser user, Operation operation, ApiKeyId entityId, ApiKeyInfo entity) { + return user.getTenantId().equals(entity.getTenantId()); + } + }; + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java index 946bbb8221..fb9768a920 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.security.permission; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -33,18 +32,18 @@ import java.util.Optional; @Slf4j public class DefaultAccessControlService implements AccessControlService { - private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; private final Map authorityPermissions = new HashMap<>(); - public DefaultAccessControlService( - @Qualifier("sysAdminPermissions") Permissions sysAdminPermissions, - @Qualifier("tenantAdminPermissions") Permissions tenantAdminPermissions, - @Qualifier("customerUserPermissions") Permissions customerUserPermissions) { + public DefaultAccessControlService(SysAdminPermissions sysAdminPermissions, + TenantAdminPermissions tenantAdminPermissions, + CustomerUserPermissions customerUserPermissions, + MfaConfigurationPermissions mfaConfigurationPermissions) { authorityPermissions.put(Authority.SYS_ADMIN, sysAdminPermissions); authorityPermissions.put(Authority.TENANT_ADMIN, tenantAdminPermissions); authorityPermissions.put(Authority.CUSTOMER_USER, customerUserPermissions); + authorityPermissions.put(Authority.MFA_CONFIGURATION_TOKEN, mfaConfigurationPermissions); } @Override @@ -55,23 +54,37 @@ public class DefaultAccessControlService implements AccessControlService { } } + @Override + @SuppressWarnings("unchecked") + public boolean hasPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException { + var permissionChecker = getPermissionChecker(user.getAuthority(), resource); + return permissionChecker.hasPermission(user, operation); + } + @Override @SuppressWarnings("unchecked") public void checkPermission(SecurityUser user, Resource resource, - Operation operation, I entityId, T entity) throws ThingsboardException { + Operation operation, I entityId, T entity) throws ThingsboardException { PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); if (!permissionChecker.hasPermission(user, operation, entityId, entity)) { permissionDenied(); } } + @Override + @SuppressWarnings("unchecked") + public boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException { + var permissionChecker = getPermissionChecker(user.getAuthority(), resource); + return permissionChecker.hasPermission(user, operation, entityId, entity); + } + private PermissionChecker getPermissionChecker(Authority authority, Resource resource) throws ThingsboardException { Permissions permissions = authorityPermissions.get(authority); if (permissions == null) { permissionDenied(); } Optional permissionChecker = permissions.getPermissionChecker(resource); - if (!permissionChecker.isPresent()) { + if (permissionChecker.isEmpty()) { permissionDenied(); } return permissionChecker.get(); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java new file mode 100644 index 0000000000..4fe8b4d127 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.permission; + +import org.springframework.stereotype.Component; + +@Component +public class MfaConfigurationPermissions extends AbstractPermissions { + + public MfaConfigurationPermissions() { + super(); + // for compatibility with PE + } + +} 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 7b2959b0cc..ebeb65df40 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 @@ -53,7 +53,8 @@ public enum Resource { EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), MOBILE_APP_SETTINGS, JOB(EntityType.JOB), - AI_MODEL(EntityType.AI_MODEL); + AI_MODEL(EntityType.AI_MODEL), + API_KEY(EntityType.API_KEY); private final Set entityTypes; 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 7dd220b7b5..801a7bc684 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 @@ -18,12 +18,14 @@ package org.thingsboard.server.service.security.permission; 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.ApiKeyId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -@Component(value = "sysAdminPermissions") +@Component public class SysAdminPermissions extends AbstractPermissions { public SysAdminPermissions() { @@ -45,6 +47,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.NOTIFICATION, systemEntityPermissionChecker); put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker); + put(Resource.API_KEY, PermissionChecker.allowAllPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { @@ -71,4 +74,13 @@ public class SysAdminPermissions extends AbstractPermissions { }; + private static final PermissionChecker apiKeysPermissionChecker = new PermissionChecker<>() { + + @Override + public boolean hasPermission(SecurityUser user, Operation operation) { + 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 89bee32c93..a576ea6f82 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 @@ -20,12 +20,14 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -@Component(value = "tenantAdminPermissions") +@Component public class TenantAdminPermissions extends AbstractPermissions { public TenantAdminPermissions() { @@ -59,10 +61,16 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.JOB, tenantEntityPermissionChecker); put(Resource.AI_MODEL, aiModelPermissionChecker); + put(Resource.API_KEY, apiKeysPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { + @Override + public boolean hasPermission(SecurityUser user, Operation operation) { + return true; + } + @Override public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { if (!user.getTenantId().equals(entity.getTenantId())) { @@ -73,7 +81,7 @@ public class TenantAdminPermissions extends AbstractPermissions { }; private static final PermissionChecker tenantPermissionChecker = - new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) { + new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY, Operation.DELETE) { @Override @SuppressWarnings("unchecked") @@ -163,4 +171,18 @@ public class TenantAdminPermissions extends AbstractPermissions { }; + private static final PermissionChecker apiKeysPermissionChecker = new PermissionChecker<>() { + + @Override + public boolean hasPermission(SecurityUser user, Operation operation) { + return true; + } + + @Override + public boolean hasPermission(SecurityUser user, Operation operation, ApiKeyId entityId, ApiKeyInfo entity) { + return user.getTenantId().equals(entity.getTenantId()); + } + + }; + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 339767f32f..f5c4f40928 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.user.UserService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 2f68f71870..5ea3838a75 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.service.security.model.SecurityUser; public interface SystemSecurityService { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index c2fe743bb4..5b4287acd1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -130,7 +130,7 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index d64ce942d0..27bd88fc1a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.TbLogEntityActionService; @@ -64,7 +64,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final RateLimitService rateLimitService; private final TbLogEntityActionService logEntityActionService; - protected static final List SUPPORTED_ENTITY_TYPES = List.of( + public static final List SUPPORTED_ENTITY_TYPES = List.of( EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE, EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE, @@ -131,7 +131,10 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS @Override public Comparator getEntityTypeComparatorForImport() { - return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf); + return Comparator.comparingInt(type -> { + int index = SUPPORTED_ENTITY_TYPES.indexOf(type); + return index >= 0 ? index : Integer.MAX_VALUE; + }); } @SuppressWarnings("unchecked") diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java index d6ed0754e3..586febd686 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java @@ -42,10 +42,6 @@ public abstract class BaseEntityExportService ctx, E mainEntity, D exportData) { } - protected D newExportData() { - return (D) new EntityExportData(); - } - public abstract Set getSupportedEntityTypes(); protected void replaceUuidsRecursively(EntitiesExportCtx ctx, JsonNode node, Set skippedRootFields, Pattern includedFieldsPattern) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index 8cf852ef2f..a38e2e058e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; @@ -24,6 +25,9 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -66,7 +70,8 @@ public class DefaultEntityExportService ctx, I entityId) throws ThingsboardException { - D exportData = newExportData(); + @SuppressWarnings("unchecked") + D exportData = (D) EntityExportData.newInstance(entityId.getEntityType()); E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId); if (entity == null) { @@ -74,7 +79,6 @@ public class DefaultEntityExportService calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId); calculatedFields.forEach(calculatedField -> { calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId)); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(getExternalIdOrElseInternal(ctx, zoneGroupConfiguration.getRefEntityId())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); + } + }); } - }); + } + if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration alarmCfConfig) { + alarmCfConfig.getAllRules().map(Pair::getValue).forEach(rule -> { + if (rule.getDashboardId() != null) { + rule.setDashboardId(getExternalIdOrElseInternal(ctx, rule.getDashboardId())); + } + }); + } }); return calculatedFields; } @@ -202,8 +223,4 @@ public class DefaultEntityExportService(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index 57beddef59..4a18a23b80 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -48,11 +48,6 @@ public class DeviceExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.DEVICE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java index af29e3c5df..775ab87fcb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java @@ -36,11 +36,6 @@ public class OtaPackageExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.OTA_PACKAGE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java index 52452bcca4..ce93574103 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java @@ -61,11 +61,6 @@ public class RuleChainExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.RULE_CHAIN); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java index cb2df97a57..7e381b39bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java @@ -38,11 +38,6 @@ public class WidgetTypeExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.WIDGET_TYPE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java index 3f579bbc43..3d4b80ce1c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java @@ -45,11 +45,6 @@ public class WidgetsBundleExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.WIDGETS_BUNDLE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index bec5eab531..c2669a6e30 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.sync.ie.importing.csv; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import jakarta.annotation.Nullable; @@ -183,7 +184,13 @@ public abstract class AbstractBulkImportService dataEntry.getKey().getType() == kvType && StringUtils.isNotEmpty(dataEntry.getKey().getKey())) - .forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive())); + .forEach(dataEntry -> { + ParsedValue value = dataEntry.getValue(); + JsonElement kvValue = (value.getDataType() == DataType.JSON) + ? (JsonElement) value.getValue() + : value.toJsonPrimitive(); + kvs.add(dataEntry.getKey().getKey(), kvValue); + }); return Map.entry(kvType, kvs); }) .filter(kvsEntry -> !kvsEntry.getValue().entrySet().isEmpty()) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index e770573cf7..4fc0945bb3 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -22,6 +22,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -35,6 +36,9 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -321,11 +325,28 @@ public abstract class BaseEntityImportService { calculatedField.setTenantId(ctx.getTenantId()); calculatedField.setEntityId(savedEntity.getId()); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(idProvider.getInternalId(zoneGroupConfiguration.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); } - }); + } + if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration alarmCfConfig) { + alarmCfConfig.getAllRules().map(Pair::getValue).forEach(rule -> { + if (rule.getDashboardId() != null) { + rule.setDashboardId(idProvider.getInternalId(rule.getDashboardId(), ctx.isFinalImportAttempt())); + } + }); + } }).toList(); for (CalculatedField existingField : existing) { diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java index 256b230b5a..bb5052fd76 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java @@ -28,6 +28,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; @@ -42,6 +43,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; @@ -65,6 +67,7 @@ public class SystemPatchApplier { private final InstallScripts installScripts; private final DatabaseSchemaSettingsService schemaSettingsService; private final WidgetTypeService widgetTypeService; + private final ImageService imageService; @PostConstruct private void init() { @@ -100,6 +103,9 @@ public class SystemPatchApplier { int updated = updateWidgetTypes(); log.info("Updated {} widget types", updated); + int createdImages = createMissingSystemImages(); + log.info("Created {} new system images", createdImages); + schemaSettingsService.updateSchemaVersion(); log.info("System data patch update completed successfully"); @@ -215,6 +221,39 @@ public class SystemPatchApplier { return false; } + private int createMissingSystemImages() { + AtomicInteger created = new AtomicInteger(); + Path imagesDir = Paths.get(installScripts.getDataDir(), InstallScripts.RESOURCES_DIR, "images"); + + if (!Files.exists(imagesDir)) { + log.warn("System images directory does not exist: {}", imagesDir); + return 0; + } + + Set existingKeys = imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID); + + try (Stream dirStream = listDir(imagesDir).filter(Files::isRegularFile)) { + dirStream.forEach(path -> { + String resourceKey = path.getFileName().toString(); + if (existingKeys.contains(resourceKey)) { + log.trace("System image already exists, skipping: {}", resourceKey); + return; + } + try { + byte[] data = Files.readAllBytes(path); + imageService.createOrUpdateSystemImage(resourceKey, data); + created.incrementAndGet(); + log.trace("Created system image: {}", resourceKey); + } catch (Exception e) { + log.error("Unable to create system image from file: [{}]", path); + throw new RuntimeException("Unable to create system image " + resourceKey, e); + } + }); + } + + return created.get(); + } + private boolean isWidgetTypeChanged(WidgetTypeDetails existing, WidgetTypeDetails file) { if (!isDescriptorEqual(existing.getDescriptor(), file.getDescriptor())) { return true; 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 index c258a30281..79c7868f2b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -61,6 +61,8 @@ import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.Collection; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.SEVERITY_CHANGED; + /** * Created by ashvayka on 27.03.18. */ @@ -102,7 +104,12 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { - return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details)); + return clearAlarm(tenantId, alarmId, clearTs, details, true); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) { + return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details, pushEvent)); } @Override @@ -246,8 +253,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService AlarmComment.AlarmCommentBuilder alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", - String.format("Alarm severity was updated from %s to %s", result.getOldSeverity(), alarm.getSeverity()))); + .comment(JacksonUtil.newObjectNode() + .put("text", String.format(SEVERITY_CHANGED.getText(), result.getOldSeverity(), alarm.getSeverity())) + .put("subtype", SEVERITY_CHANGED.name()) + .put("oldSeverity", result.getOldSeverity().name()) + .put("newSeverity", alarm.getSeverity().name())); if (request != null && request.getUserId() != null) { alarmComment.userId(request.getUserId()); } 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 7396860272..48bd497c6e 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 @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; +import org.thingsboard.server.exception.EntitiesLimitExceededException; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; @@ -76,7 +77,6 @@ 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.exception.EntitiesLimitException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -398,7 +398,7 @@ public class DefaultTransportApiService implements TransportApiService { } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); throw new RuntimeException(e); - } catch (EntitiesLimitException e) { + } catch (EntitiesLimitExceededException e) { log.warn("[{}][{}] API limit exception: [{}]", e.getTenantId(), gatewayId, e.getMessage()); return TransportApiResponseMsg.newBuilder() .setGetOrCreateDeviceResponseMsg( diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java new file mode 100644 index 0000000000..895fed555d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.ttl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.pat.ApiKeyDao; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Slf4j +@Service +@TbCoreComponent +@ConditionalOnExpression("${sql.ttl.api_keys.enabled:true} && ${sql.ttl.api_keys.ttl:0} > 0") +public class ApiKeysCleanUpService extends AbstractCleanUpService { + + public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = + "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.api_keys.checking_interval_ms})}"; + + private final ApiKeyDao apiKeyDao; + + public ApiKeysCleanUpService(PartitionService partitionService, ApiKeyDao apiKeyDao) { + super(partitionService); + this.apiKeyDao = apiKeyDao; + } + + @Scheduled( + initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, + fixedDelayString = "${sql.ttl.api_keys.checking_interval_ms:86400000}" + ) + public void cleanUp() { + long threshold = System.currentTimeMillis(); + if (isSystemTenantPartitionMine()) { + int deleted = apiKeyDao.deleteAllByExpirationTimeBefore(threshold); + if (deleted > 0) { + log.info("API key cleanup removed {} keys (thresholdTs={})", deleted, threshold); + } + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java b/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java new file mode 100644 index 0000000000..e4ed589e16 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.user.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.UserAuthDetails; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.dao.user.UserService; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache { + + private final UserService userService; + + @Value("${cache.userAuthDetails.maxSize:1000}") + private int cacheMaxSize; + @Value("${cache.userAuthDetails.timeToLiveInMinutes:30}") + private int cacheValueTtl; + private Cache cache; + + @PostConstruct + private void init() { + cache = Caffeine.newBuilder() + .maximumSize(cacheMaxSize) + .expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) + .build(); + } + + @EventListener(ComponentLifecycleMsg.class) + public void onComponentLifecycleEvent(ComponentLifecycleMsg event) { + if (event.getEntityId() != null) { + if (event.getEntityId().getEntityType() == EntityType.USER) { + evict(new UserId(event.getEntityId().getId())); + } + } + } + + @Override + public UserAuthDetails getUserAuthDetails(TenantId tenantId, UserId userId) { + log.trace("Retrieving user with enabled credentials status for id {} for tenant {} from cache", userId, tenantId); + return cache.get(userId, id -> userService.findUserAuthDetailsByUserId(tenantId, id)); + } + + public void evict(UserId userId) { + cache.invalidate(userId); + log.trace("Evicted record for user {} from cache", userId); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/user/cache/UserAuthDetailsCache.java b/application/src/main/java/org/thingsboard/server/service/user/cache/UserAuthDetailsCache.java new file mode 100644 index 0000000000..c7614bcc5e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/user/cache/UserAuthDetailsCache.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.user.cache; + +import org.thingsboard.server.common.data.UserAuthDetails; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; + +public interface UserAuthDetailsCache { + + UserAuthDetails getUserAuthDetails(TenantId tenantId, UserId userId); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java index e139498d04..a2dc87e4af 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java @@ -23,11 +23,14 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor public class AuthCmd implements WsCmd { + private int cmdId; private String token; + private String apiKey; @Override public WsCmdType getType() { return WsCmdType.AUTH; } + } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java index f07f155c4e..fcf8c0593b 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -19,6 +19,8 @@ import lombok.NonNull; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -33,6 +35,18 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntryStatus; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry.DEFAULT_VERSION; @@ -42,12 +56,34 @@ public class CalculatedFieldArgumentUtils { return kvEntry.getValue() != null ? ArgumentEntry.createSingleValueArgument(kvEntry) : new SingleValueArgumentEntry(); } - public static TsKvEntry createDefaultTsKvEntry(Argument argument, long ts) { - return new BasicTsKvEntry(ts, createDefaultKvEntry(argument), DEFAULT_VERSION); + public static ArgumentEntry transformTsRollingArgument(List tsRolling, int limit, long argTimeWindow) { + return ArgumentEntry.createTsRollingArgument(tsRolling, limit, argTimeWindow); } - public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) { - return new BaseAttributeKvEntry(createDefaultKvEntry(argument), ts, DEFAULT_VERSION); + public static ArgumentEntry transformAggMetricArgument(List timeSeries, String argKey, AggMetric aggMetric) { + if (timeSeries == null || timeSeries.isEmpty()) { + return createDefaultMetricArgumentEntry(argKey, aggMetric); + } + return ArgumentEntry.createSingleValueArgument(timeSeries.get(0)); + } + + public static ArgumentEntry createDefaultMetricArgumentEntry(String argKey, AggMetric metric) { + Double defaultValue = metric.getDefaultValue(); + if (defaultValue != null) { + return ArgumentEntry.createSingleValueArgument(new DoubleDataEntry(argKey, defaultValue)); + } + return new SingleValueArgumentEntry(); + } + + public static ArgumentEntry transformAggregationArgument(List timeSeries, long startIntervalTs, long endIntervalTs) { + Map aggIntervals = new HashMap<>(); + AggIntervalEntry aggIntervalEntry = new AggIntervalEntry(startIntervalTs, endIntervalTs); + if (timeSeries == null || timeSeries.isEmpty()) { + aggIntervals.put(aggIntervalEntry, new AggIntervalEntryStatus()); + } else { + aggIntervals.put(aggIntervalEntry, new AggIntervalEntryStatus(System.currentTimeMillis())); + } + return new EntityAggregationArgumentEntry(aggIntervals); } private static KvEntry createDefaultKvEntry(Argument argument) { @@ -65,10 +101,22 @@ public class CalculatedFieldArgumentUtils { return new StringDataEntry(key, defaultValue); } - public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { + public static TsKvEntry createDefaultTsKvEntry(Argument argument, long ts) { + return new BasicTsKvEntry(ts, createDefaultKvEntry(argument), DEFAULT_VERSION); + } + public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) { + return new BaseAttributeKvEntry(createDefaultKvEntry(argument), ts, DEFAULT_VERSION); + } + + public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx, EntityId entityId) { return switch (ctx.getCfType()) { - case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); - case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); + case SIMPLE -> new SimpleCalculatedFieldState(entityId); + case SCRIPT -> new ScriptCalculatedFieldState(entityId); + case GEOFENCING -> new GeofencingCalculatedFieldState(entityId); + case ALARM -> new AlarmCalculatedFieldState(entityId); + case PROPAGATION -> new PropagationCalculatedFieldState(entityId); + case RELATED_ENTITIES_AGGREGATION -> new RelatedEntitiesAggregationCalculatedFieldState(entityId); + case ENTITY_AGGREGATION -> new EntityAggregationCalculatedFieldState(entityId); }; } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 66dee78d96..49d3c22c3c 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -15,31 +15,62 @@ */ package org.thingsboard.server.utils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.kv.BasicKvEntry; import org.thingsboard.server.common.util.KvProtoUtil; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.AlarmRuleStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.AlarmStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.ArgumentIntervalProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.EntityIdProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto; import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntryStatus; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmRuleState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; public class CalculatedFieldUtils { @@ -75,15 +106,65 @@ public class CalculatedFieldUtils { .setType(state.getType().name()); state.getArguments().forEach((argName, argEntry) -> { - if (argEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - builder.addSingleValueArguments(toSingleValueArgumentProto(argName, singleValueArgumentEntry)); - } else if (argEntry instanceof TsRollingArgumentEntry rollingArgumentEntry) { - builder.addRollingValueArguments(toRollingArgumentProto(argName, rollingArgumentEntry)); + switch (argEntry.getType()) { + case SINGLE_VALUE -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) argEntry)); + case TS_ROLLING -> builder.addRollingValueArguments(toRollingArgumentProto(argName, (TsRollingArgumentEntry) argEntry)); + case GEOFENCING -> builder.addGeofencingArguments(toGeofencingArgumentProto(argName, (GeofencingArgumentEntry) argEntry)); + case PROPAGATION -> builder.addAllPropagationEntityIds(toPropagationEntityIdsProto((PropagationArgumentEntry) argEntry)); + case RELATED_ENTITIES -> { + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry; + Map entityInputs = relatedEntitiesArgumentEntry.getEntityInputs(); + if (entityInputs.isEmpty()) { + builder.addSingleValueArguments(SingleValueArgumentProto.newBuilder().setArgName(argName).build()); + } else { + entityInputs.forEach((entityId, entry) -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) entry))); + } + } + case ENTITY_AGGREGATION -> { + EntityAggregationArgumentEntry entityAggregationArgumentEntry = (EntityAggregationArgumentEntry) argEntry; + entityAggregationArgumentEntry.getAggIntervals().forEach((interval, argumentStatus) -> builder.addAggregationArguments(toArgumentIntervalProto(argName, interval, argumentStatus))); + } } }); + if (state instanceof AlarmCalculatedFieldState alarmState) { + AlarmStateProto.Builder alarmStateProto = AlarmStateProto.newBuilder(); + alarmState.getCreateRuleStates().forEach((severity, ruleState) -> { + alarmStateProto.addCreateRuleStates(toAlarmRuleStateProto(ruleState)); + }); + if (alarmState.getClearRuleState() != null) { + alarmStateProto.setClearRuleState(toAlarmRuleStateProto(alarmState.getClearRuleState())); + } + builder.setAlarmState(alarmStateProto); + } + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) { + builder.setLastArgsUpdateTs(aggState.getLastArgsRefreshTs()); + builder.setLastMetricsEvalTs(aggState.getLastMetricsEvalTs()); + } return builder.build(); } + private static List toPropagationEntityIdsProto(PropagationArgumentEntry argEntry) { + return argEntry.getEntityIds().stream().map(ProtoUtils::toProto).collect(Collectors.toList()); + } + + private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) { + return AlarmRuleStateProto.newBuilder() + .setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse("")) + .setEventCount(ruleState.getEventCount()) + .setFirstEventTs(ruleState.getFirstEventTs()) + .setLastCheckTs(ruleState.getLastCheckTs()) + .build(); + } + + private static AlarmRuleState fromAlarmRuleStateProto(AlarmRuleStateProto proto, AlarmCalculatedFieldState state) { + AlarmSeverity severity = StringUtils.isNotEmpty(proto.getSeverity()) ? AlarmSeverity.valueOf(proto.getSeverity()) : null; + AlarmRuleState ruleState = new AlarmRuleState(severity, null, state); + ruleState.setEventCount(proto.getEventCount()); + ruleState.setFirstEventTs(proto.getFirstEventTs()); + ruleState.setLastCheckTs(proto.getLastCheckTs()); + return ruleState; + } + public static SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) { SingleValueArgumentProto.Builder builder = SingleValueArgumentProto.newBuilder() .setArgName(argName); @@ -94,9 +175,23 @@ public class CalculatedFieldUtils { Optional.ofNullable(entry.getVersion()).ifPresent(builder::setVersion); + if (entry.getEntityId() != null) { + builder.setEntityId(ProtoUtils.toProto(entry.getEntityId())); + } + return builder.build(); } + public static ArgumentIntervalProto toArgumentIntervalProto(String argName, AggIntervalEntry intervalEntry, AggIntervalEntryStatus argumentStatus) { + return ArgumentIntervalProto.newBuilder() + .setArgName(argName) + .setStartTs(intervalEntry.getStartTs()) + .setEndTs(intervalEntry.getEndTs()) + .setLastArgsRefreshTs(argumentStatus.getLastArgsRefreshTs()) + .setLastMetricsEvalTs(argumentStatus.getLastMetricsEvalTs()) + .build(); + } + public static TsRollingArgumentProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) { TsRollingArgumentProto.Builder builder = TsRollingArgumentProto.newBuilder() .setKey(argName) @@ -108,7 +203,28 @@ public class CalculatedFieldUtils { return builder.build(); } - public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { + private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) { + Map zoneStates = geofencingArgumentEntry.getZoneStates(); + GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder() + .setArgName(argName); + zoneStates.forEach((entityId, zoneState) -> + builder.addZones(toGeofencingZoneProto(entityId, zoneState))); + return builder.build(); + } + + private static GeofencingZoneProto toGeofencingZoneProto(EntityId entityId, GeofencingZoneState zoneState) { + GeofencingZoneProto.Builder builder = GeofencingZoneProto.newBuilder() + .setZoneId(ProtoUtils.toProto(entityId)) + .setTs(zoneState.getTs()) + .setVersion(zoneState.getVersion()) + .setPerimeterDefinition(JacksonUtil.toString(zoneState.getPerimeterDefinition())); + if (zoneState.getLastPresence() != null) { + builder.setInside(zoneState.getLastPresence().equals(GeofencingPresenceStatus.INSIDE)); + } + return builder.build(); + } + + public static CalculatedFieldState fromProto(CalculatedFieldEntityCtxId id, CalculatedFieldStateProto proto) { if (StringUtils.isEmpty(proto.getType())) { return null; } @@ -116,16 +232,71 @@ public class CalculatedFieldUtils { CalculatedFieldType type = CalculatedFieldType.valueOf(proto.getType()); CalculatedFieldState state = switch (type) { - case SIMPLE -> new SimpleCalculatedFieldState(); - case SCRIPT -> new ScriptCalculatedFieldState(); + case SIMPLE -> new SimpleCalculatedFieldState(id.entityId()); + case SCRIPT -> new ScriptCalculatedFieldState(id.entityId()); + case GEOFENCING -> new GeofencingCalculatedFieldState(id.entityId()); + case ALARM -> new AlarmCalculatedFieldState(id.entityId()); + case PROPAGATION -> new PropagationCalculatedFieldState(id.entityId()); + case RELATED_ENTITIES_AGGREGATION -> new RelatedEntitiesAggregationCalculatedFieldState(id.entityId()); + case ENTITY_AGGREGATION -> new EntityAggregationCalculatedFieldState(id.entityId()); }; + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map> arguments = new HashMap<>(); + proto.getSingleValueArgumentsList().forEach(argProto -> { + SingleValueArgumentEntry entry = fromSingleValueArgumentProto(argProto); + Map entityInputs = arguments.computeIfAbsent(argProto.getArgName(), name -> new HashMap<>()); + if (entry.getEntityId() != null) { + entityInputs.put(entry.getEntityId(), entry); + } + }); + arguments.forEach((argName, entityInputs) -> { + relatedEntitiesAggState.getArguments().put(argName, new RelatedEntitiesArgumentEntry(entityInputs, false)); + }); + relatedEntitiesAggState.setLastArgsRefreshTs(proto.getLastArgsUpdateTs()); + relatedEntitiesAggState.setLastMetricsEvalTs(proto.getLastMetricsEvalTs()); + + return relatedEntitiesAggState; + } + + if (state instanceof EntityAggregationCalculatedFieldState entityAggregationState) { + Map arguments = new HashMap<>(); + + proto.getAggregationArgumentsList().forEach(argProto -> { + AggIntervalEntry intervalEntry = new AggIntervalEntry(argProto.getStartTs(), argProto.getEndTs()); + AggIntervalEntryStatus intervalStatus = new AggIntervalEntryStatus(argProto.getLastArgsRefreshTs(), argProto.getLastMetricsEvalTs()); + EntityAggregationArgumentEntry argEntry = arguments.computeIfAbsent(argProto.getArgName(), name -> new EntityAggregationArgumentEntry(new HashMap<>())); + argEntry.getAggIntervals().put(intervalEntry, intervalStatus); + }); + + entityAggregationState.getArguments().putAll(arguments); + + return entityAggregationState; + } + proto.getSingleValueArgumentsList().forEach(argProto -> state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto))); - if (CalculatedFieldType.SCRIPT.equals(type)) { - proto.getRollingValueArgumentsList().forEach(argProto -> + switch (type) { + case SCRIPT -> proto.getRollingValueArgumentsList().forEach(argProto -> state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); + case GEOFENCING -> proto.getGeofencingArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); + case PROPAGATION -> { + List propagationEntityIds = proto.getPropagationEntityIdsList().stream().map(ProtoUtils::fromProto).toList(); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(propagationEntityIds)); + } + case ALARM -> { + AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state; + AlarmStateProto alarmStateProto = proto.getAlarmState(); + for (AlarmRuleStateProto ruleStateProto : alarmStateProto.getCreateRuleStatesList()) { + AlarmRuleState ruleState = fromAlarmRuleStateProto(ruleStateProto, alarmState); + alarmState.getCreateRuleStates().put(ruleState.getSeverity(), ruleState); + } + if (alarmStateProto.hasClearRuleState()) { + alarmState.setClearRuleState(fromAlarmRuleStateProto(alarmStateProto.getClearRuleState(), alarmState)); + } + } } return state; @@ -136,11 +307,14 @@ public class CalculatedFieldUtils { return new SingleValueArgumentEntry(); } TsValueProto tsValueProto = proto.getValue(); - return new SingleValueArgumentEntry( - tsValueProto.getTs(), - (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto), - proto.getVersion() - ); + BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto); + long ts = tsValueProto.getTs(); + long version = proto.getVersion(); + if (proto.hasEntityId()) { + EntityId entityId = ProtoUtils.fromProto(proto.getEntityId()); + return new SingleValueArgumentEntry(entityId, ts, kvEntry, version); + } + return new SingleValueArgumentEntry(ts, kvEntry, version); } public static TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentProto proto) { @@ -149,4 +323,15 @@ public class CalculatedFieldUtils { return new TsRollingArgumentEntry(tsRecords, proto.getLimit(), proto.getTimeWindow()); } + + private static ArgumentEntry fromGeofencingArgumentProto(GeofencingArgumentProto proto) { + Map zoneStates = proto.getZonesList() + .stream() + .map(GeofencingZoneState::new) + .collect(Collectors.toMap(GeofencingZoneState::getZoneId, Function.identity())); + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + geofencingArgumentEntry.setZoneStates(zoneStates); + return geofencingArgumentEntry; + } + } diff --git a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java index fdf9481191..4d63d01060 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java @@ -17,10 +17,15 @@ package org.thingsboard.server.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.SneakyThrows; import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.input.CharSequenceReader; +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -43,4 +48,18 @@ public class CsvUtils { .collect(Collectors.toList()); } + @SneakyThrows + public static byte[] generateCsv(List> rows) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); + CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) { + + for (List row : rows) { + csvPrinter.printRecord(row); + } + csvPrinter.flush(); + } + return out.toByteArray(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java index 450c3bb888..b93568a7e0 100644 --- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; import org.thingsboard.server.common.data.util.TbDDFFileParser; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -73,7 +73,7 @@ public class LwM2mObjectModelUtils { try { List objectModels = ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText()); - if (objectModels.size() == 0) { + if (objectModels.isEmpty()) { return null; } else { ObjectModel obj = objectModels.get(0); @@ -95,7 +95,7 @@ public class LwM2mObjectModelUtils { resources.add(lwM2MResourceObserve); } }); - if (isSave || resources.size() > 0) { + if (isSave || !resources.isEmpty()) { instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); lwM2mObject.setInstances(new LwM2mInstance[]{instance}); return lwM2mObject; diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 0efb669ec2..3904875b34 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -56,6 +56,9 @@ + + + diff --git a/application/src/main/resources/thingsboard-openapi.properties b/application/src/main/resources/thingsboard-openapi.properties new file mode 100644 index 0000000000..410c4072a5 --- /dev/null +++ b/application/src/main/resources/thingsboard-openapi.properties @@ -0,0 +1,51 @@ +# +# Copyright © 2016-2026 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Lightweight startup for OpenAPI spec generation. +# Activated only via: --spring.profiles.active=openapi + +spring.main.banner-mode=off + +# Testcontainers PostgreSQL +database.ts.type=sql +database.ts_latest.type=sql +spring.datasource.url=jdbc:tc:postgresql:16.6:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.hikari.maximumPoolSize=5 +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 + +# Disable transports +transport.http.enabled=false +transport.mqtt.enabled=false +transport.coap.enabled=false +transport.lwm2m.enabled=false +transport.snmp.enabled=false +coap.server.enabled=false + +# Disable edges, integrations, EDQS +edges.enabled=false +integrations.rpc.enabled=false +service.integrations.supported=NONE +transport.gateway.dashboard.sync.enabled=false +queue.edqs.sync.enabled=false +queue.edqs.api.supported=false +usage.stats.report.enabled=false +service.type=monolith diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index bac005d836..2724b9a3ff 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -15,6 +15,7 @@ # # Server common parameters +# Configures HTTP/HTTPS bind address, port, SSL, WebSocket, and REST API settings. server: # Server bind-address address: "${HTTP_BIND_ADDRESS:0.0.0.0}" @@ -104,25 +105,30 @@ server: rate_limits: # Limit that prohibits resetting the password for the user too often. The value of the rate limit. By default, no more than 5 requests per hour reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}" + rule_engine: + # Default timeout for waiting response of REST API request to Rule Engine in milliseconds + response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}" # Application info parameters +# Exposes application metadata such as version string injected at build time. app: # Application version version: "@project.version@" -# Zookeeper connection parameters +# ZooKeeper connection parameters +# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments. zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:false}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. @@ -130,6 +136,7 @@ zk: recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" # Cluster parameters +# Controls cluster statistics — tracks the number of messages exchanged between cluster nodes. cluster: stats: # Enable/Disable the cluster statistics. Calculates the number of messages sent between cluster nodes based on each type @@ -138,11 +145,13 @@ cluster: print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}" # Plugins configuration parameters +# Defines classpath scan packages used to discover and register ThingsBoard extension plugins. plugins: # Comma-separated package list used during classpath scanning for plugins scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}" # Security parameters +# Configures JWT tokens, user login, device claiming, OAuth2, and CA certificate trust store. security: # JWT Token parameters jwt: # Since 3.4.2 values are persisted in the database during installation or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN @@ -152,6 +161,12 @@ security: tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Base64 encoded # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" + # API key parameters + api_key: + # Prefix for the auto-generated API key. For example, tb_Ood4dQMxWvMH-76z3E_Cv0mZaBWT0Clk3hRSO0P_jNQ + value_prefix: "${SECURITY_API_KEY_VALUE_PREFIX:tb_}" + # Length of the auto-generated API key. Max is 255 + value_bytes_size: "${SECURITY_API_KEY_VALUE_PREFIX:64}" # Enable/disable case-sensitive username login user_login_case_sensitive: "${SECURITY_USER_LOGIN_CASE_SENSITIVE:true}" claim: @@ -229,6 +244,7 @@ security: report-only: "${SECURITY_HEADERS_CONTENT_SECURITY_POLICY_REPORT_ONLY:false}" # Mail settings parameters +# Configures mail service OAuth2 token refresh and per-tenant sending rate limits. mail: oauth2: # Interval for checking refresh token expiration in seconds(by default, 1 day). @@ -237,6 +253,7 @@ mail: per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}" # Usage statistics parameters +# Controls collection and reporting intervals for API usage stats at system, tenant, and customer levels. usage: stats: report: @@ -246,6 +263,8 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Statistics reporting interval, set to send summarized data every 10 seconds by default interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates + urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" check: @@ -258,6 +277,7 @@ usage: report_interval: "${DEVICES_STATS_REPORT_INTERVAL:60}" # UI settings parameters +# Configures dashboard data limits and base URL for UI help assets. ui: # Dashboard parameters dashboard: @@ -266,9 +286,10 @@ ui: # Help parameters help: # Base URL for UI help assets - base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.2.2}" + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.4}" # Database telemetry parameters +# Selects the storage backend (SQL, Cassandra, or TimescaleDB) for time-series and latest telemetry data. database: ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records ts: @@ -277,6 +298,7 @@ database: type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) # Cassandra driver configuration parameters +# Configures cluster name, keyspace, SSL, credentials, socket, and query settings for the Cassandra driver. cassandra: # Thingsboard cluster name cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}" @@ -395,6 +417,7 @@ cassandra: print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}" # SQL configuration parameters +# Tunes batch sizes, delays, thread counts, TTL, and partitioning for SQL-backed persistence of telemetry, events, and audit logs. sql: # Specify batch size for persisting attribute updates attributes: @@ -446,6 +469,10 @@ sql: log_tenant_stats: "${SQL_LOG_TENANT_STATS:true}" # Interval in milliseconds for printing the latest statistic information about the tenant log_tenant_stats_interval_ms: "${SQL_LOG_TENANT_STATS_INTERVAL_MS:60000}" + query: + key-filters-or-conditions: + # Enable/disable OR conditions between key filters in entity data queries + enabled: "${SQL_QUERY_KEY_FILTERS_OR_CONDITIONS_ENABLED:true}" postgres: # Specify partitioning size for timestamp key-value storage. Example: DAYS, MONTHS, YEARS, INDEFINITE. ts_key_value_partitioning: "${SQL_POSTGRES_TS_KV_PARTITIONING:MONTHS}" @@ -489,12 +516,16 @@ sql: ttl: "${SQL_TTL_NOTIFICATIONS_SECS:2592000}" # Default value - 30 days checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day removal_batch_size: "${SQL_TTL_NOTIFICATIONS_REMOVAL_BATCH_SIZE:10000}" # Batch size for records removal + api_keys: + enabled: "${SQL_TTL_API_KEYS_ENABLED:true}" # Enable/disable TTL (Time To Live) for expired api keys records + checking_interval_ms: "${SQL_TTL_API_KEYS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day relations: max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # This value has to be reasonably small to prevent infinite recursion as early as possible pool_size: "${SQL_RELATIONS_POOL_SIZE:4}" # This value has to be reasonably small to prevent the relation query from blocking all other DB calls query_timeout: "${SQL_RELATIONS_QUERY_TIMEOUT_SEC:20}" # This value has to be reasonably small to prevent the relation query from blocking all other DB calls # Actor system parameters +# Configures thread pools, timeouts, and behavior for the internal actor system processing devices, rules, and RPCs. actors: system: throughput: "${ACTORS_SYSTEM_THROUGHPUT:5}" # Number of messages the actor system will process per actor before switching to processing of messages for the next actor @@ -588,14 +619,14 @@ actors: response_timeout_ms: "${ACTORS_RPC_RESPONSE_TIMEOUT_MS:30000}" # Close transport session if RPC delivery timed out. If enabled, RPC will be reverted to the queued state. # Note: - # - For MQTT transport: - # - QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered. - # - QoS level 1: This feature applies, as an acknowledgment is expected. - # - QoS level 2: Unsupported. - # - For CoAP transport: - # - Confirmable requests: This feature applies, as delivery confirmation is expected. - # - Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected. - # - For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment. + #
  • For MQTT transport: + #
    • QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered.
    • + #
    • QoS level 1: This feature applies, as an acknowledgment is expected.
    • + #
    • QoS level 2: Unsupported.
  • + #
  • For CoAP transport: + #
    • Confirmable requests: This feature applies, as delivery confirmation is expected.
    • + #
    • Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected.
  • + #
  • For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment.
close_session_on_rpc_delivery_timeout: "${ACTORS_RPC_CLOSE_SESSION_ON_RPC_DELIVERY_TIMEOUT:false}" statistics: # Enable/disable actor statistics @@ -611,6 +642,8 @@ actors: # Time in seconds to receive calculation result. calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}" +# Debug settings parameters +# Configures the global default duration for debug mode used by rule chains and calculated fields. debug: settings: # Default duration (in minutes) for debug mode. Min value is 1 minute. Tenant profile settings override this one. @@ -618,6 +651,7 @@ debug: default_duration: "${DEBUG_SETTINGS_DEFAULT_DURATION_MINUTES:15}" # Cache settings parameters +# Configures cache type (Caffeine or Redis), pool size, and per-entity TTL and max-size limits for all caches. cache: # caffeine or redis(7.2 - latest compatible version) type: "${CACHE_TYPE:caffeine}" @@ -746,6 +780,9 @@ cache: aiModel: timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled + apiKeys: + timeToLiveInMinutes: "${CACHE_SPECS_API_KEYS_TTL:1440}" # API keys cache TTL + maxSize: "${CACHE_SPECS_API_KEYS_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: @@ -766,11 +803,15 @@ cache: tbResourceData: timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_DATA_TTL:10080}" # TB resource data cache TTL maxSize: "${CACHE_SPECS_RESOURCE_DATA_MAX_SIZE:100000}" # 0 means the cache is disabled + userAuthDetails: + timeToLiveInMinutes: "${CACHE_SPECS_USER_AUTH_DETAILS_TTL:120}" # User auth details cache TTL + maxSize: "${CACHE_SPECS_USER_AUTH_DETAILS_MAX_SIZE:200000}" # 0 means the cache is disabled # Spring data parameters spring.data.redis.repositories.enabled: false # Disable this because it is not required. # Redis/Valkey configuration parameters +# Configures standalone, cluster, or sentinel Redis connection, SSL, and connection pool settings used for caching. redis: # standalone or cluster or sentinel connection: @@ -799,17 +840,17 @@ redis: # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" sentinel: - # name of the master node + # Name of the master node master: "${REDIS_MASTER:}" - # comma-separated list of "host:port" pairs of sentinels + # Comma-separated list of "host:port" pairs of sentinels sentinels: "${REDIS_SENTINELS:}" - # password to authenticate with sentinel + # Password to authenticate with sentinel password: "${REDIS_SENTINEL_PASSWORD:}" - # if set false will be used pool config build from values of the pool config section + # If set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" - # db index + # Redis logical database index to select after connecting. db: "${REDIS_DB:0}" - # db password + # Password for Redis authentication (leave empty if not required). password: "${REDIS_PASSWORD:}" # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth username: "${REDIS_USERNAME:}" @@ -854,6 +895,7 @@ redis: # Update version parameters +# Controls whether the platform periodically checks for new ThingsBoard releases. updates: # Enable/disable checks for the new version enabled: "${UPDATES_ENABLED:true}" @@ -882,6 +924,7 @@ spring.mvc.cors: allow-credentials: "${TB_CORS_ALLOW_CREDENTIALS:true}" # General spring parameters +# Miscellaneous Spring Boot settings for circular references, Freemarker, MVC, multipart, and JPA dialect. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.freemarker.checkTemplateLocation: "false" # spring freemarker configuration spring.mvc.async.request-timeout: "${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:30000}" # The default timeout for asynchronous requests in milliseconds @@ -900,6 +943,7 @@ spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PR spring.jpa.properties.hibernate.dialect: "${SPRING_JPA_DIALECT:org.thingsboard.server.dao.ThingsboardPostgreSQLDialect}" # we use custom dialect that contains ilike(arg1, arg2) function (is interpreted to postgres ILIKE operator) # SQL DAO Configuration parameters +# Configures Spring JPA, Hibernate DDL, and HikariCP datasource connections for the primary and events databases. spring: data: jpa: @@ -950,6 +994,7 @@ spring: registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" # Audit log parameters +# Configures audit logging levels per entity type and optional forwarding to an external sink such as Elasticsearch. audit-log: # Enable/disable audit log functionality. enabled: "${AUDIT_LOG_ENABLED:true}" @@ -992,6 +1037,7 @@ audit-log: password: "${AUDIT_LOG_SINK_PASSWORD:}" # Password used to access external sink system # Device state parameters +# Configures device inactivity detection, state persistence strategy, and rate limits for state rule nodes. state: # Device inactivity timeout is a global configuration parameter that defines when the device will be marked as "inactive" by the server. # The parameter value is in seconds. A user can overwrite this parameter for an individual device by setting the “inactivityTimeout” server-side attribute (NOTE: expects value in milliseconds). @@ -1022,7 +1068,8 @@ state: # Refill is set to be greedy. Please refer to Bucket4j library documentation for more details. rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}" -# Tbel parameters +# TBEL parameters +# Configures the ThingsBoard Expression Language (TBEL) engine: limits, timeouts, thread pool, and blacklisting. tbel: # Enable/Disable TBEL feature. enabled: "${TBEL_ENABLED:true}" @@ -1051,6 +1098,7 @@ tbel: print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}" # JS parameters +# Configures the JavaScript execution engine (local Nashorn or remote Node.js): limits, sandboxing, and thread pools. js: # local (Nashorn Engine, deprecated) OR remote JS-Executors (NodeJS) evaluator: "${JS_EVALUATOR:local}" @@ -1098,6 +1146,7 @@ js: print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}" # Transport configuration parameters +# Configures session management, rate limits, and protocol-specific settings for HTTP, MQTT, CoAP, LwM2M, and SNMP transports. transport: sessions: # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. @@ -1114,10 +1163,10 @@ transport: activity: # This property specifies the strategy for reporting activity events within each reporting period. # The accepted values are 'FIRST', 'LAST', 'FIRST_AND_LAST' and 'ALL'. - # - 'FIRST': Only the first activity event in each reporting period is reported. - # - 'LAST': Only the last activity event in the reporting period is reported. - # - 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported. - # - 'ALL': All activity events in the reporting period are reported. + #
  • 'FIRST': Only the first activity event in each reporting period is reported.
  • + #
  • 'LAST': Only the last activity event in the reporting period is reported.
  • + #
  • 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported.
  • + #
  • 'ALL': All activity events in the reporting period are reported.
reporting_strategy: "${TB_TRANSPORT_ACTIVITY_REPORTING_STRATEGY:LAST}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON @@ -1235,16 +1284,16 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:}" + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
+ connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID id: "${LWM2M_SERVER_ID:123}" @@ -1289,7 +1338,7 @@ transport: # Enable/disable Bootstrap Server enabled: "${LWM2M_ENABLED_BS:true}" # Default value in LwM2M client after start in mode Bootstrap for the object : name "LWM2M Security" field: "Short Server ID" (deviceProfile: Bootstrap.BOOTSTRAP SERVER.Short ID) - id: "${LWM2M_SERVER_ID_BS:111}" + id: "${LWM2M_SERVER_ID_BS:0}" # LwM2M bootstrap server bind address. Bind to all interfaces by default bind_address: "${LWM2M_BS_BIND_ADDRESS:0.0.0.0}" # LwM2M bootstrap server bind port @@ -1403,11 +1452,12 @@ transport: # URL of gateways dashboard repository repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}" # Branch of gateways dashboard repository to work with - branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.0.0}" + branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.3.0}" # Fetch frequency in hours for gateways dashboard repository fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" # CoAP server parameters +# Configures the standalone CoAP server bind address, port, DTLS encryption, and credential settings. coap: server: # Enable/disable coap server. @@ -1426,15 +1476,15 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
+ connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. # Default = 1024 @@ -1450,13 +1500,13 @@ coap: # In order to negotiate smaller maximum fragment lengths, # clients MAY include an extension of type "max_fragment_length" in the (extended) client hello. # The "extension_data" field of this extension SHALL contain: - # enum { + #
 enum {
     #   2^9(1) == 512,
     #   2^10(2) == 1024,
     #   2^11(3) == 2048,
     #   2^12(4) == 4096,
     #   (255)
-    # } MaxFragmentLength;
+    # } MaxFragmentLength; 
# TLS already requires clients and servers to support fragmentation of handshake messages. max_fragment_length: "${COAP_DTLS_MAX_FRAGMENT_LENGTH:1024}" # Server DTLS credentials @@ -1492,6 +1542,7 @@ coap: dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" # Device connectivity parameters +# Specifies the hosts, ports, and credentials exposed to the UI for generating device connection check commands. device: connectivity: http: @@ -1542,9 +1593,10 @@ device: pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}" gateway: # The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch - image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.7-stable}" + image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}" # Edges parameters +# Controls Edge instance gRPC communication, event storage, state persistence, and statistics reporting. edges: # Enable/disable Edge instance enabled: "${EDGES_ENABLED:true}" @@ -1588,6 +1640,11 @@ edges: no_read_records_sleep: "${EDGES_NO_READ_RECORDS_SLEEP:1000}" # Number of milliseconds to wait before resending failed batch of edge events to edge sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:60000}" + # Time (in milliseconds) to subtract from the start timestamp when fetching edge events. + # This compensates for possible misordering between `created_time` (used for partitioning) + # and `seqId` (used for sorting). Without this, events with smaller seqId but larger created_time + # might be skipped, especially across partition boundaries. + misordering_compensation_millis: "${EDGES_MISORDERING_COMPENSATION_MILLIS:60000}" # Max number of high priority edge events per edge session. No persistence - stored in memory max_high_priority_queue_size_per_session: "${EDGES_MAX_HIGH_PRIORITY_QUEUE_SIZE_PER_SESSION:10000}" # Number of threads that are used to check DB for edge events @@ -1608,6 +1665,7 @@ edges: report-interval-millis: "${EDGES_STATS_REPORT_INTERVAL_MS:600000}" # Spring doc common parameters +# Enables or disables the OpenAPI/Swagger documentation endpoint and sets the default media type. springdoc: # If false swagger API docs will be unavailable api-docs.enabled: "${SWAGGER_ENABLED:true}" @@ -1615,6 +1673,7 @@ springdoc: default-produces-media-type: "${SWAGGER_DEFAULT_PRODUCES_MEDIA_TYPE:application/json}" # Swagger common parameters +# Configures Swagger UI metadata: title, description, contact, license, version, and API path patterns. swagger: # General swagger match pattern of swagger UI links api_path: "${SWAGGER_API_PATH:/api/**}" @@ -1642,8 +1701,11 @@ swagger: version: "${SWAGGER_VERSION:}" # The group name (definition) on the API doc UI page. group_name: "${SWAGGER_GROUP_NAME:thingsboard}" + # Control the initial display state of API operations and tags (none or list) + doc_expansion: "${SWAGGER_DOC_EXPANSION:list}" # Queue configuration parameters +# Configures the message queue backend (in-memory or Kafka) and all topic/partition/consumer settings for each service. queue: type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). @@ -1983,6 +2045,9 @@ queue: print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" # Max length of the error message that is printed by statistics max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}" + prometheus-stats: + # Enable/disable Prometheus statistics for individual Rule Engine message processing (records time in ms for success/failure). + enabled: "${TB_QUEUE_RULE_ENGINE_PROMETHEUS_STATS_ENABLED:false}" # After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}" # Size of the thread pool that handles such operations as partition changes, config updates, queue deletion @@ -2054,12 +2119,14 @@ queue: processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" # Event configuration parameters +# Controls limits on debug event content size stored for rule chain and rule node execution. event: debug: # Maximum number of symbols per debug event. The event content will be truncated if needed max-symbols: "${TB_MAX_DEBUG_EVENT_SYMBOLS:4096}" # General service parameters +# Sets the deployment type (monolith or microservice) and assigns tenant profiles to specific Rule Engine instances. service: type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine # Unique id for this service (autogenerated if empty) @@ -2073,6 +2140,7 @@ service: executor_thread_pool_size: "${TB_RULE_ENGINE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" # Metrics parameters +# Enables actuator metrics endpoint and configures system info (CPU, memory) persistence frequency and TTL. metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" @@ -2086,6 +2154,7 @@ metrics: ttl: "${METRICS_SYSTEM_INFO_TTL_DAYS:7}" # Version control parameters +# Configures thread pools and Git repository folder for entity version control (export/import) operations. vc: # Pool size for handling export tasks thread_pool_size: "${TB_VC_POOL_SIZE:6}" @@ -2096,6 +2165,7 @@ vc: repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" # Notification system parameters +# Configures thread pool size and deduplication intervals for the notification rule processing engine. notification_system: # Specify thread pool size for Notification System processing notification rules and notification sending. Recommend value <= 10 thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}" @@ -2104,6 +2174,7 @@ notification_system: deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}" # General management parameters +# Configures Spring Boot Actuator endpoint exposure and health indicator settings. management: endpoints: web: @@ -2116,6 +2187,7 @@ management: enabled: "false" # Mobile application settings for Thingsboard mobile application +# Specifies domain name and store links used for the ThingsBoard Live mobile application QR code and deep links. mobileApp: # Server domain name for Thingsboard Live mobile application domain: "${TB_MOBILE_APP_DOMAIN:demo.thingsboard.io}" @@ -2124,6 +2196,8 @@ mobileApp: # Link to App Store for Thingsboard Live mobile application appStoreLink: "${TB_MOBILE_APP_APP_STORE_LINK:https://apps.apple.com/us/app/thingsboard-live/id1594355695}" +# MQTT client parameters +# Configures MQTT client retransmission behavior including max attempts, initial delay, and jitter factor. mqtt: # MQTT client configuration parameters client: diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java new file mode 100644 index 0000000000..366089a04e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -0,0 +1,1014 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.query.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate.StringOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.event.CalculatedFieldDebugEvent; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +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.query.EntityKeyValueType; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.event.EventDao; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +@Slf4j +@DaoSqlTest +public class AlarmRulesTest extends AbstractControllerTest { + + @MockitoSpyBean + private ActorSystemContext actorSystemContext; + + @Autowired + private EventDao eventDao; + + private Device device; + private DeviceId deviceId; + private EntityId originatorId; + private EventId latestEventId; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setCfReevaluationCheckInterval(1); + tenantProfileConfig.setAlarmsReevaluationInterval(1); + }); + + loginTenantAdmin(); + device = createDevice("Device A", "aaa"); + deviceId = device.getId(); + originatorId = deviceId; + } + + @Test + public void testCreateAlarm_severityUpdate_clear() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", null, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) + ); + + Condition clearRule = new Condition("return temperature <= 25;", null, null); + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, clearRule); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":100}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":101}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":20}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + }); + } + + @Test + public void testCreateAlarm_simpleConditionExpression() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter filter = new AlarmConditionFilter(); + filter.setArgument("temperature"); + filter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setOperation(NumericOperation.GREATER_OR_EQUAL); + AlarmConditionValue thresholdValue = new AlarmConditionValue<>(); + thresholdValue.setStaticValue(100.0); + predicate.setValue(thresholdValue); + filter.setPredicates(List.of(predicate)); + simpleExpression.setFilters(List.of(filter)); + simpleExpression.setOperation(ComplexOperation.AND); + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition(simpleExpression, null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":100}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAlarm_eventBeforeDefaultTs() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"values\": {\"temperature\": 50}, \"ts\": " + (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30) + "}")); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAlarm_repeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + int eventsCountMajor = 5; + int eventsCountCritical = 10; + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", eventsCountMajor, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + for (int i = 0; i < 4; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(5); + }); + + for (int i = 0; i < 4; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(alarmRule, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + }); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(10); + }); + } + + @Test + public void testCreateAlarm_dynamicRepeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument eventsCountArgument = new Argument(); + eventsCountArgument.setRefEntityKey(new ReferencedEntityKey("eventsCount", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + eventsCountArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "eventsCount", eventsCountArgument + ); + + int eventsCount = 5; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, + new AlarmConditionValue<>(null, "eventsCount"), null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"eventsCount\":" + eventsCount + "}"); + for (int i = 0; i < eventsCount; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(eventsCount); + }); + } + + @Test + public void testCreateAlarm_durationCondition() throws Exception { + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("0"); + Map arguments = Map.of( + "powerConsumption", argument + ); + + long createDurationMs = 5000L; + long clearDurationMs = 3000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, createDurationMs) + ); + Condition clearRule = new Condition("return powerConsumption < 3000;", null, clearDurationMs); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 5 seconds", + arguments, createRules, clearRule); + postTelemetry(deviceId, "{\"powerConsumption\":3500}"); + Thread.sleep(createDurationMs - 2000); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + + postTelemetry(deviceId, "{\"powerConsumption\":2000}"); + Thread.sleep(clearDurationMs - 2000); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(clearDurationMs, clearDurationMs + 2000); + }); + } + + @Test + public void testCreateAlarm_dynamicDurationCondition() throws Exception { + Argument powerConsumptionArgument = new Argument(); + powerConsumptionArgument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + powerConsumptionArgument.setDefaultValue("0"); + + Argument durationArgument = new Argument(); + durationArgument.setRefEntityKey(new ReferencedEntityKey("duration", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + durationArgument.setDefaultValue("-1"); + Map arguments = Map.of( + "powerConsumption", powerConsumptionArgument, + "duration", durationArgument + ); + + long createDurationMs = 2000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, null, + new AlarmConditionValue(null, "duration"), null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds", + arguments, createRules, null); + postTelemetry(deviceId, "{\"powerConsumption\":3500}"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"duration\":" + createDurationMs + "}"); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + } + + @Test + public void testCreateAlarm_durationCondition_defaultValue() { + Argument powerConsumptionArgument = new Argument(); + powerConsumptionArgument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + powerConsumptionArgument.setDefaultValue("3500"); + Map arguments = Map.of( + "powerConsumption", powerConsumptionArgument + ); + + long createDurationMs = 2000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, null, + new AlarmConditionValue(2000L, null), null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds", + arguments, createRules, null); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + } + + @Test + public void testCreateAlarm_currentOwnerArgument() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + temperatureThresholdArgument.setDefaultValue("1000"); + + Map arguments = Map.of( + "temperature", temperatureArgument, + "temperatureThreshold", temperatureThresholdArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) + ); + + device.setCustomerId(customerId); + device = doPost("/api/device", device, Device.class); + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); + + postTelemetry(deviceId, "{\"temperature\":51}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAndClearAlarm_customerAlarmRule_simpleExpression() throws Exception { + Argument locationArgument = new Argument(); + locationArgument.setRefEntityKey(new ReferencedEntityKey("location", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + locationArgument.setDefaultValue("unknown"); + originatorId = customerId; + + Argument locationFilterArgument = new Argument(); + locationFilterArgument.setRefEntityKey(new ReferencedEntityKey("locationFilter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + locationFilterArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + locationFilterArgument.setDefaultValue("None"); + + Map arguments = Map.of( + "location", locationArgument, + "locationFilter", locationFilterArgument + ); + + Map createRules = Map.of( + AlarmSeverity.INDETERMINATE, new Condition(createSimpleExpression( + "location", StringOperation.CONTAINS, new AlarmConditionValue<>(null, "locationFilter") + ), null, null) + ); + Condition clearRule = new Condition(createSimpleExpression( + "location", StringOperation.NOT_CONTAINS, new AlarmConditionValue<>(null, "locationFilter") + ), null, null); + + AlarmRuleDefinition alarmRule = createAlarmRule(customerId, "New resident", + arguments, createRules, clearRule); + + loginSysAdmin(); + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"locationFilter\":\"Kyiv\"}"); + loginTenantAdmin(); + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Kyiv\"}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Lviv\"}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + }); + } + + @Test + public void testCreateAlarm_dynamicSchedule() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Argument scheduleArgument = new Argument(); + scheduleArgument.setRefEntityKey(new ReferencedEntityKey("schedule", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + scheduleArgument.setDefaultValue("None"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "schedule", scheduleArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null, null, + new AlarmConditionValue<>(null, "schedule")) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + String schedule = """ + {"timezone":"Europe/Kiev","items":[{"enabled":false,"dayOfWeek":1,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":2,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":3,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":4,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":5,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":6,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":7,"startsOn":0,"endsOn":0}]} + """; + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); + postTelemetry(deviceId, "{\"temperature\":50}"); + + Thread.sleep(1000); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); + + schedule = schedule.replace("\"enabled\":false", "\"enabled\":true"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + // checking multiple debug events due to scheduled reevaluation (which also produces debug events) + CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream() + .filter(event -> event.getResult() != null) + .findFirst().orElse(null); + assertThat(debugEvent).isNotNull(); + TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAlarm_noDataPredicate() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + long majorNoDataDuration = 3L; + long criticalNoDataDuration = 10L; + + SimpleAlarmConditionExpression majorExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter majorFilter = new AlarmConditionFilter(); + majorFilter.setArgument("temperature"); + majorFilter.setValueType(EntityKeyValueType.NUMERIC); + majorFilter.setPredicates(List.of( + new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)), + new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue(majorNoDataDuration, null)) + )); + majorExpression.setFilters(List.of(majorFilter)); + + SimpleAlarmConditionExpression criticalExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter criticalFilter = new AlarmConditionFilter(); + criticalFilter.setArgument("temperature"); + criticalFilter.setValueType(EntityKeyValueType.NUMERIC); + criticalFilter.setPredicates(List.of( + new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)), + new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue(criticalNoDataDuration, null)) + )); + criticalExpression.setFilters(List.of(criticalFilter)); + + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition(majorExpression, null, null), + AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "No Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeAlarmType() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + alarmRule.setName("New alarm type"); + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeRuleExpression() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(1000); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); + + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration(); + ((TbelAlarmConditionExpression) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition().getExpression()) + .setExpression("return temperature >= 50;"); + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeRequiredEventsCountForRepeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + int eventsCountMajor = 5; + int eventsCountCritical = 10; + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", eventsCountMajor, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + for (int i = 0; i < eventsCountMajor; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(5); + }); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); + }); + + // decreasing required events count for critical rule + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration(); + ((RepeatingAlarmCondition) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition()) + .setCount(new AlarmConditionValue<>(6, null)); + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); + }); + } + + @Test + public void testChangeConditionArgumentSource() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + temperatureThresholdArgument.setDefaultValue("100"); + loginSysAdmin(); + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":100}"); + loginTenantAdmin(); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); + + Map arguments = Map.of( + "temperature", temperatureArgument, + "temperatureThreshold", temperatureThresholdArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(1000); + // not created because tenant's threshold 100 is used + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); + + ((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getArguments().get("temperatureThreshold") + .setRefDynamicSourceConfiguration(null); + // using threshold 50 on device level + alarmRule = saveAlarmRule(alarmRule); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testAlarmDetails() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Argument humidityArgument = new Argument(); + humidityArgument.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + humidityArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "humidity", humidityArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null) + ); + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature and Humidity Alarm", + arguments, createRules, null, configuration -> { + configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + "temperature is ${temperature}, humidity is ${humidity}" + ); + }); + + postTelemetry(deviceId, "{\"temperature\":50}"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}"); + + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) + .isEqualTo("temperature is 50, humidity is 50"); + }); + + ((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + "UPDATED temperature is ${temperature}, humidity is ${humidity}" + ); + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isFalse(); + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) + .isEqualTo("UPDATED temperature is 50, humidity is 50"); + }); + } + + @Test + public void testCreateAlarm_scheduleStarted() throws Exception { + Argument parkingSpotOccupiedArgument = new Argument(); + parkingSpotOccupiedArgument.setRefEntityKey(new ReferencedEntityKey("parkingSpotOccupied", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + parkingSpotOccupiedArgument.setDefaultValue("false"); + Map arguments = Map.of( + "parkingSpotOccupied", parkingSpotOccupiedArgument + ); + + SpecificTimeSchedule schedule = new SpecificTimeSchedule(); + schedule.setTimezone(ZoneId.systemDefault().getId()); + schedule.setDaysOfWeek(Set.of(1, 2, 3, 4, 5, 6, 7)); + long startsOn = Duration.between(LocalDate.now().atStartOfDay(), LocalDateTime.now()) + .plus(15, ChronoUnit.SECONDS).toMillis(); + schedule.setStartsOn(startsOn); + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return parkingSpotOccupied == true;", null, null, null, + new AlarmConditionValue<>(schedule, null)) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "Illegal parking alarm", + arguments, createRules, null); + + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"parkingSpotOccupied\":true}"); + + Thread.sleep(10000); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream() + .filter(event -> event.getResult() != null) + .findFirst().orElse(null); + assertThat(debugEvent).isNotNull(); + TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testManualClearAlarm() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Alarm alarm = checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }).getAlarm(); + + doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class); + Thread.sleep(1000); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(alarmRule, alarmResult -> { + assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId()); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + // TODO: MSA tests + + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion) { + return checkAlarmResult(alarmRule, null, assertion); + } + + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, + Predicate waitFor, + Consumer assertion) { + TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getLatestAlarmResult(alarmRule.getId()), result -> + result != null && (waitFor == null || waitFor.test(result))); + assertion.accept(alarmResult); + + Alarm alarm = alarmResult.getAlarm(); + assertThat(alarm.getOriginator()).isEqualTo(originatorId); + assertThat(alarm.getType()).isEqualTo(alarmRule.getName()); + return alarmResult; + } + + private TbAlarmResult getLatestAlarmResult(CalculatedFieldId calculatedFieldId) { + List debugEvents = getDebugEvents(calculatedFieldId, 1); + if (debugEvents.isEmpty()) { + return null; + } + CalculatedFieldDebugEvent debugEvent = debugEvents.get(0); + if (debugEvent.getError() != null) { + throw new RuntimeException(debugEvent.getError()); + } + if (debugEvent.getId().equals(latestEventId)) { + return null; + } + latestEventId = debugEvent.getId(); + return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + } + + private AlarmRuleDefinition createAlarmRule(EntityId entityId, + String alarmType, + Map arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { + Map createRules = new HashMap<>(); + createConditions.forEach((severity, condition) -> { + createRules.put(severity, toAlarmRule(condition)); + }); + AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; + + AlarmRuleDefinition alarmRule = new AlarmRuleDefinition(); + alarmRule.setEntityId(entityId); + alarmRule.setName(alarmType); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + configuration.setArguments(arguments); + configuration.setCreateRules(createRules); + configuration.setClearRule(clearRule); + alarmRule.setConfiguration(configuration); + alarmRule.setDebugSettings(DebugSettings.all()); + if (modifier.length > 0) { + modifier[0].accept(configuration); + } + AlarmRuleDefinition savedAlarmRule = saveAlarmRule(alarmRule); + + CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getDebugEvents(savedAlarmRule.getId(), 1), + events -> !events.isEmpty()).get(0); + latestEventId = debugEvent.getId(); + return savedAlarmRule; + } + + private AlarmRule toAlarmRule(Condition condition) { + AlarmRule rule = new AlarmRule(); + AlarmConditionExpression expression; + if (condition.getTbelExpression() != null) { + TbelAlarmConditionExpression tbelExpression = new TbelAlarmConditionExpression(); + tbelExpression.setExpression(condition.getTbelExpression()); + expression = tbelExpression; + } else { + expression = condition.getSimpleExpression(); + } + if (condition.getEventsCount() != null) { + RepeatingAlarmCondition alarmCondition = new RepeatingAlarmCondition(); + alarmCondition.setExpression(expression); + alarmCondition.setCount(condition.getEventsCount()); + rule.setCondition(alarmCondition); + } else if (condition.getDuration() != null) { + DurationAlarmCondition alarmCondition = new DurationAlarmCondition(); + alarmCondition.setExpression(expression); + alarmCondition.setUnit(TimeUnit.MILLISECONDS); + alarmCondition.setValue(condition.getDuration()); + rule.setCondition(alarmCondition); + } else { + SimpleAlarmCondition alarmCondition = new SimpleAlarmCondition(); + alarmCondition.setExpression(expression); + rule.setCondition(alarmCondition); + } + if (condition.getSchedule() != null) { + rule.getCondition().setSchedule(condition.getSchedule()); + } + return rule; + } + + private SimpleAlarmConditionExpression createSimpleExpression(String argument, StringOperation stringOperation, AlarmConditionValue conditionValue) { + SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter filter = new AlarmConditionFilter(); + filter.setArgument(argument); + filter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setOperation(stringOperation); + predicate.setValue(conditionValue); + filter.setPredicates(List.of(predicate)); + simpleExpression.setFilters(List.of(filter)); + return simpleExpression; + } + + private List getDebugEvents(CalculatedFieldId calculatedFieldId, int limit) { + return eventDao.findLatestEvents(tenantId.getId(), calculatedFieldId.getId(), EventType.DEBUG_CALCULATED_FIELD, limit).stream() + .map(e -> (CalculatedFieldDebugEvent) e).toList(); + } + + @Getter + @AllArgsConstructor + private static final class Condition { + + private final String tbelExpression; + private final SimpleAlarmConditionExpression simpleExpression; + private AlarmConditionValue eventsCount; + private AlarmConditionValue duration; + private AlarmConditionValue schedule; + + private Condition(String tbelExpression, Integer eventsCount, Long durationMs) { + this.tbelExpression = tbelExpression; + this.simpleExpression = null; + if (eventsCount != null) { + this.eventsCount = new AlarmConditionValue<>(eventsCount, null); + } + if (durationMs != null) { + this.duration = new AlarmConditionValue<>(durationMs, null); + } + } + + private Condition(SimpleAlarmConditionExpression simpleExpression, Integer eventsCount, Long durationMs) { + this.tbelExpression = null; + this.simpleExpression = simpleExpression; + if (eventsCount != null) { + this.eventsCount = new AlarmConditionValue<>(eventsCount, null); + } + if (durationMs != null) { + this.duration = new AlarmConditionValue<>(durationMs, null); + } + } + + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java new file mode 100644 index 0000000000..52180a888d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java @@ -0,0 +1,198 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class CalculatedFieldCurrentOwnerTest extends AbstractControllerTest { + + public static final int TIMEOUT = 60; + public static final int POLL_INTERVAL = 1; + + @Test + public void testCreateCFWithCurrentOwner() throws Exception { + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + Device testDevice = createDevice("Test device", "1234567890"); + + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":10}"); + + await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("110"); + }); + } + + @Test + public void testChangeOwner() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + Device testDevice = createDevice("Test device", "1234567890"); + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + doDelete("/api/customer/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + await().alias("change owner -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + } + + @Test + public void testCreateCFWithCurrentOwnerWhenEntityIsProfile() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + AssetProfile assetProfile = doPost("/api/assetProfile", createAssetProfile("Test Asset Profile"), AssetProfile.class); + + Asset asset1 = createAsset("Test asset 1", assetProfile.getId()); + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset1.getId().getId()).andExpect(status().isOk()); + + Asset asset2 = createAsset("Test asset 2", assetProfile.getId()); // owner - TENANT + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(assetProfile.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 1 + ObjectNode result1 = getLatestTelemetry(asset1.getId(), "result"); + assertThat(result1).isNotNull(); + assertThat(result1.get("result").get(0).get("value").asText()).isEqualTo("105"); + + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset2.getId().getId()).andExpect(status().isOk()); + + await().alias("change asset2 owner -> recalculate state for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + } + + private CalculatedField buildCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("Test Calculated Field"); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("attrKey", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + argument.setRefEntityKey(refEntityKey); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + + config.setArguments(Map.of("a", argument)); + + config.setExpression("a + 100"); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("result"); + output.setDecimalsByDefault(0); + + config.setOutput(output); + + calculatedField.setConfiguration(config); + return calculatedField; + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return doPost("/api/asset", asset, Asset.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 7f905ec0ae..ba7d11fee9 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -15,40 +15,58 @@ */ package org.thingsboard.server.cf; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; -import org.junit.jupiter.api.BeforeEach; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { @@ -56,16 +74,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public static final int TIMEOUT = 60; public static final int POLL_INTERVAL = 1; - @BeforeEach - void setUp() throws Exception { - loginTenantAdmin(); - } - @Test public void testSimpleCalculatedFieldWhenAllTelemetryPresent() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}")); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + postTelemetry(testDevice.getId(), "{\"temperature\":25}"); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"deviceTemperature\":40}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -83,13 +96,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); - calculatedField.setVersion(1L); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -101,7 +112,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -111,10 +122,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); }); - Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); - savedOutput.setType(OutputType.ATTRIBUTES); - savedOutput.setScope(AttributeScope.SERVER_SCOPE); - savedOutput.setName("temperatureF"); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setScope(AttributeScope.SERVER_SCOPE); + newOutput.setName("temperatureF"); + config.setOutput(newOutput); + savedCalculatedField.setConfiguration(config); + savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -122,10 +135,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .untilAsserted(() -> { ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF"); assertThat(temperatureF).isNotNull(); + assertThat(temperatureF.get(0)).isNotNull(); assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("86.0"); }); - Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); + Argument savedArgument = ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).getArguments().get("T"); savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); @@ -137,7 +151,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("104.0"); }); - savedCalculatedField.getConfiguration().setExpression("1.8 * T + 32"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setExpression("1.8 * T + 32"); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -168,9 +182,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); @@ -186,7 +199,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue(); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -217,9 +230,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); @@ -235,7 +247,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -249,15 +261,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testSimpleCalculatedFieldWhenEntityIdIsProfile() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":40}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":40}"); AssetProfile assetProfile = doPost("/api/assetProfile", createAssetProfile("Test Asset Profile"), AssetProfile.class); Asset asset1 = createAsset("Test asset 1", assetProfile.getId()); - doPost("/api/plugins/telemetry/ASSET/" + asset1.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":11}")); + postAttributes(asset1.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":11}"); Asset asset2 = createAsset("Test asset 2", assetProfile.getId()); - doPost("/api/plugins/telemetry/ASSET/" + asset2.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":12}")); + postAttributes(asset2.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":12}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(assetProfile.getId()); @@ -281,9 +293,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setExpression("x + y"); - Output output = new Output(); + AttributesOutput output = new AttributesOutput(); output.setName("z"); - output.setType(OutputType.ATTRIBUTES); output.setScope(AttributeScope.SERVER_SCOPE); config.setOutput(output); @@ -307,7 +318,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z2.get(0).get("value").asText()).isEqualTo("52.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":25}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":25}"); await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -323,7 +334,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z2.get(0).get("value").asText()).isEqualTo("37.0"); }); - doPost("/api/plugins/telemetry/ASSET/" + asset1.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":15}")); + postAttributes(asset1.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":15}"); await().alias("update asset 1 telemetry -> recalculate state only for asset 1").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -339,7 +350,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z2.get(0).get("value").asText()).isEqualTo("37.0"); }); - doPost("/api/plugins/telemetry/ASSET/" + asset2.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":5}")); + postAttributes(asset2.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":5}"); await().alias("update asset 2 telemetry -> recalculate state only for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -356,7 +367,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); Asset asset3 = createAsset("Test asset 3", assetProfile.getId()); - doPost("/api/plugins/telemetry/ASSET/" + asset3.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":13}")); + postAttributes(asset3.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":13}"); Asset finalAsset3 = asset3; await().alias("add new entity to profile -> calculate state for new entity").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -368,7 +379,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z3.get(0).get("value").asText()).isEqualTo("38.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":20}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":20}"); await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -394,7 +405,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes asset3.setAssetProfileId(newAssetProfile.getId()); asset3 = doPost("/api/asset", asset3, Asset.class); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":15}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":15}"); Asset updatedAsset3 = asset3; await().alias("update device telemetry -> recalculate state for asset 1 and asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -420,7 +431,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testSimpleCalculatedFieldWhenExpressionIsInvalid() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}")); + postTelemetry(testDevice.getId(), "{\"temperature\":25}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -438,9 +449,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/0) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); @@ -456,7 +466,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue(); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -471,7 +481,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public void testSimpleCalculatedFieldWhenUseLatestTsIsTrue() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long ts = System.currentTimeMillis() - 300000L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -488,9 +498,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); config.setUseLatestTs(true); @@ -515,10 +524,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes long ts = System.currentTimeMillis(); long tsA = ts - 300000L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA)); long tsB = ts - 300L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -538,9 +547,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("a", argument1, "b", argument2)); config.setExpression("a + b"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("c"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); config.setUseLatestTs(true); @@ -559,7 +567,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); long tsABeforeTsB = tsB - 300L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":10}}", tsABeforeTsB))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":10}}", tsABeforeTsB)); await().alias("update telemetry with ts less than latest -> save result with latest ts").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -576,7 +584,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Device testDevice = createDevice("Test device", "1234567890"); long ts = System.currentTimeMillis() - 300000L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -593,9 +601,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); calculatedField.setConfiguration(config); @@ -639,9 +645,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("a", argument1, "b", argument2, "c", argument3)); config.setExpression("a + b + c"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("d"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(0); config.setOutput(output); @@ -659,7 +664,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(d.get("d").get(0).get("value").asText()).isEqualTo("600"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}")); + postTelemetry(testDevice.getId(), "{\"a\":10}"); await().alias("update telemetry -> save result with ts of 'a' argument").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -671,8 +676,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("510"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":20}")); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"c\":30}")); + postTelemetry(testDevice.getId(), "{\"b\":20}"); + postTelemetry(testDevice.getId(), "{\"c\":30}"); await().alias("update telemetry -> save result with latest ts of updated arguments").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -714,9 +719,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("m", argument)); config.setExpression("m + 1"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("m1"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(0); config.setOutput(output); @@ -724,7 +728,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}")); + postTelemetry(testDevice.getId(), "{\"m\":1}"); await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -738,7 +742,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes calculatedField.setConfiguration(config); calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}")); + postTelemetry(testDevice.getId(), "{\"m\":2}"); await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -749,10 +753,483 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception { + // --- Arrange entities --- + Device device = createDevice("GF Test Device", "sn-geo-2"); + + // Allowed zone polygon (square) + String allowedPolygon = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + // Restricted zone polygon (square) + String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; + + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "{\"allowedZone\":" + allowedPolygon + "}"); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "{\"restrictedZone\":" + restrictedPolygon + "}"); + + // Initial device coordinates (inside Allowed, outside Restricted) + postTelemetry(device.getId(), "{\"latitude\":50.4730,\"longitude\":30.5050}"); + + // --- Build CF: GEOFENCING --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getDeviceProfileId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST on the device + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone groups: ATTRIBUTE on the device + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("allowedZone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("restrictedZone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + + cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); + + // Output to server attributes + AttributesOutput out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert initial evaluation --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent"); + // --- no transition events as no transitions happened yet + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + + // --- delete attributes reported in previous evaluation + doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesStatus,restrictedZonesStatus", String.class); + + // --- Update restrictedZone by 'restrictedZone' attribute update + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "{\"restrictedZone\":" + restrictedPolygon + "}"); + + // --- Assert no transition --- + // --- Assert attributes updated with the same values for restrictedZones --- + // --- Assert attributes updated with the new values for allowedZones --- + await().alias("evaluation after version bump of geo argument") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", + "restrictedZonesEvent", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + } + + @Test + public void testGeofencingCalculatedField_withoutRelationsCreationAndDynamicRefresh() throws Exception { + // --- Arrange entities --- + Device device = createDevice("GF Device", "sn-geo-1"); + + // Allowed zone polygon (square) + String allowedPolygon = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + // Restricted zone polygon (square) + String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; + + Asset allowedZoneAsset = createAsset("Allowed Zone", null); + postAttributes(allowedZoneAsset.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + allowedPolygon + "}"); + + Asset restrictedZoneAsset = createAsset("Restricted Zone", null); + postAttributes(restrictedZoneAsset.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + restrictedPolygon + "}"); + + // Relations from device to zones + createEntityRelation(device.getId(), allowedZoneAsset.getId(), "AllowedZone"); + createEntityRelation(device.getId(), restrictedZoneAsset.getId(), "RestrictedZone"); + + // Initial device coordinates (inside Allowed, outside Restricted) + postTelemetry(device.getId(), "{\"latitude\":50.4730,\"longitude\":30.5050}"); + + // --- Build CF: GEOFENCING --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST on the device + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone groups: ATTRIBUTE on specific assets (one zone per group) + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone"))); + restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); + + cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); + + // Output to server attributes + AttributesOutput out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert initial evaluation --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + + // --- Move the device into Restricted zone (and outside Allowed) --- + postTelemetry(device.getId(), "{\"latitude\":50.4760,\"longitude\":30.5110}"); + + // --- Assert transition (LEFT / ENTERED) --- + await().alias("transition evaluation after movement") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", + "restrictedZonesEvent", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(4); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("restrictedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "OUTSIDE") + .containsEntry("restrictedZonesStatus", "INSIDE"); + }); + } + + @Test + public void testGeofencingCalculatedField_DynamicRefresh_RebindsZoneArguments() throws Exception { + // --- Update min allowed scheduled update intervals for CFs --- + loginSysAdmin(); + EntityInfo tenantProfileEntityInfo = doGet("/api/tenantProfileInfo/default", EntityInfo.class); + assertThat(tenantProfileEntityInfo).isNotNull(); + TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class); + assertThat(foundTenantProfile).isNotNull(); + assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull(); + int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10; + foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); + assertThat(savedTenantProfile).isNotNull(); + assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF); + loginTenantAdmin(); + + // --- Arrange entities --- + Device device = createDevice("GF Device dyn", "sn-geo-dyn-1"); + + // Allowed Zone A: covers initial point (ENTERED) + String allowedPolygonA = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + + Asset allowedZoneA = createAsset("Allowed Zone A", null); + postAttributes(allowedZoneA.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + allowedPolygonA + "}"); + + // Relation from device to Allowed Zone A + createEntityRelation(device.getId(), allowedZoneA.getId(), "AllowedZone"); + + // Initial device coordinates: INSIDE Zone A + postTelemetry(device.getId(), "{\"latitude\":50.4730,\"longitude\":30.5050}"); + + // --- Build CF: GEOFENCING with dynamic 'allowedZones' and short scheduled refresh --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF (dynamic refresh)"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); + + var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup)); + + // Server attributes output + AttributesOutput out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // Enable scheduled refresh with a 6-second interval + cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF); + cfg.setScheduledUpdateEnabled(true); + + cf.setConfiguration(cfg); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); + assertThat(savedCalculatedField).isNotNull(); + CalculatedFieldConfiguration configuration = savedCalculatedField.getConfiguration(); + assertThat(configuration).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + var geofencingConfiguration = (GeofencingCalculatedFieldConfiguration) configuration; + assertThat(geofencingConfiguration.isScheduledUpdateEnabled()).isTrue(); + + // --- Assert initial evaluation --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE"); + }); + + // --- Move device OUTSIDE Zone A (expect LEFT) --- + postTelemetry(device.getId(), "{\"latitude\":50.4760,\"longitude\":30.5110}"); + + await().alias("outside zone A (LEFT)") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("allowedZonesStatus", "OUTSIDE"); + }); + + // --- Create Allowed Zone B covering the CURRENT location --- + String allowedPolygonB = "[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"; + + Asset allowedZoneB = createAsset("Allowed Zone B", null); + postAttributes(allowedZoneB.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + allowedPolygonB + "}"); + + // Add a new relation + createEntityRelation(device.getId(), allowedZoneB.getId(), "AllowedZone"); + + awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF); + + // --- Same coordinates as before, but now we expect INSIDE group status since a new zone is registered --- + postTelemetry(device.getId(), "{\"latitude\":50.4760,\"longitude\":30.5110}"); + + // --- Assert dynamic refresh picks up a new relation and flips status back to INSIDE on the next telemetry update --- + await().alias("dynamic refresh rebinds allowedZones") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") // attribute from previous eval with outdated ts. + .containsEntry("allowedZonesStatus", "INSIDE"); + }); + } + + @Test + public void testPropagationCalculatedField_withExpression() throws Exception { + // --- Arrange entities --- + Device device = createDevice("Propagation Device With Expression", "sn-prop-1"); + Asset asset1 = createAsset("Propagated Asset 1", null); + Asset asset2 = createAsset("Propagated Asset 2", null); + + // Create relations FROM assets TO device + createEntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + createEntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + + // Telemetry on device + postTelemetry(device.getId(), "{\"temperature\":12.5, \"humidity\":85}"); + + // --- Build CF: PROPAGATION with expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (expr)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(true); + + Argument arg1 = new Argument(); + arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + + Argument arg2 = new Argument(); + arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + + cfg.setArguments(Map.of("t", arg1, "h", arg2)); + cfg.setExpression("return { testResult: (t + h) / 2};"); + + AttributesOutput output = new AttributesOutput(); + output.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert propagated calculation (expression applied) --- + await().alias("propagation expr mode evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = getServerAttributes(asset1.getId(), "testResult"); + ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); + assertThat(attrs1).isNotNull(); + assertThat(attrs2).isNotNull(); + assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(48.75); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(48.75); + }); + + String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + asset1.getId().getId(), EntityType.ASSET, + EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE + ); + doDelete(deleteUrl).andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/SERVER_SCOPE?keys=testResult").andExpect(status().isOk()); + + postTelemetry(device.getId(), "{\"temperature\":25}"); + + // --- Assert propagated calculation (expression applied with new temperature argument and one relation removed) --- + await().alias("propagation expr mode evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = getServerAttributes(asset1.getId(), "testResult"); + ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); + assertThat(attrs1).isNullOrEmpty(); + assertThat(attrs2).isNotNull(); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(55); + }); + } + + @Test + public void testPropagationCalculatedField_withoutExpression() throws Exception { + // --- Arrange entities --- + Device device = createDevice("Propagation Device Without Expression", "sn-prop-2"); + Asset asset1 = createAsset("Propagated Asset 1", null); + Asset asset2 = createAsset("Propagated Asset 2", null); + + // Create relations FROM assets TO device + createEntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + createEntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + + // Telemetry on device + long ts = System.currentTimeMillis() - 300000L; + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5, \"humidity\":85}}", ts)); + + // --- Build CF: PROPAGATION without expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (args-only)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode + + Argument arg1 = new Argument(); + arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + Argument arg2 = new Argument(); + arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + + cfg.setArguments(Map.of("temperatureComputed", arg1, "humidityComputed", arg2)); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setStrategy(new TimeSeriesImmediateOutputStrategy(0, true, true, true, true)); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert propagated calculation (arguments-only mode) --- + await().alias("propagation args-only evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed"); + assertThat(telemetry1).isNotNull(); + assertThat(telemetry2).isNotNull(); + assertThat(telemetry1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry1.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry1.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry1.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); + }); + + String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + asset1.getId().getId(), EntityType.ASSET, + EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE + ); + doDelete(deleteUrl).andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed,humidityComputed&deleteAllDataForKeys=true").andExpect(status().isOk()); + + // Update telemetry on the device + long newTs = ts + 300000L; + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs)); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed"); + assertThat(telemetry1).isNotNull(); + assertThat(telemetry2).isNotNull(); + assertThat(telemetry1.get("temperatureComputed").get(0).get("value")).isEqualTo(NullNode.instance); + assertThat(telemetry1.get("humidityComputed").get(0).get("value")).isEqualTo(NullNode.instance); + + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + // TS for humidity is not updated -> expected + assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); + }); + + Asset asset3 = createAsset("Propagated Asset 3", null); + createEntityRelation(asset3.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only to new entity after relation creation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed,humidityComputed"); + assertThat(telemetry).isNotNull(); + assertThat(telemetry.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + assertThat(telemetry.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); + }); + + } + @Test public void testCalculatedFieldWhenTheSameTelemetryKeysUsed() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":5}")); + postTelemetry(testDevice.getId(), "{\"a\":5}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -770,9 +1247,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("a", argumentA, "b", argumentB)); config.setExpression("a + b"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("c"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(0); config.setOutput(output); @@ -788,7 +1264,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("10"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}")); + postTelemetry(testDevice.getId(), "{\"a\":10}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -803,8 +1279,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public void testCalculatedFieldWhenBatchOfTelemetrySent() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long now = System.currentTimeMillis(); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5, \"b\":10}}", now - TimeUnit.MINUTES.toMillis(3)))); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":20}}", now - TimeUnit.MINUTES.toMillis(1)))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":5, \"b\":10}}", now - TimeUnit.MINUTES.toMillis(3))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"b\":20}}", now - TimeUnit.MINUTES.toMillis(1))); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -830,9 +1306,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }; """); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); calculatedField.setConfiguration(config); @@ -847,7 +1321,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("15.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format(""" + postTelemetry(testDevice.getId(), String.format(""" [{ "ts": %s, "values": { @@ -866,7 +1340,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes "a": 8, "b": 300 } - }]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5)))); + }]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5))); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -878,11 +1352,53 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testSimpleCalculatedFieldWhenSkipRuleEngineOutputProcessing() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + + postTelemetry(testDevice.getId(), "{\"temperature\":24.5}"); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("C to F"); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + config.setArguments(Map.of("T", argument)); + config.setExpression("(T * 9/5) + 32"); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("fahrenheitTemp"); + output.setDecimalsByDefault(1); + output.setStrategy(new TimeSeriesImmediateOutputStrategy(1000L, true, true, true, true)); + + config.setOutput(output); + + config.setUseLatestTs(true); + + calculatedField.setConfiguration(config); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("76.1"); + }); + } + @Test public void testCalculatedFieldsWhenOneIsInvalid() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long now = System.currentTimeMillis(); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5}}", now - TimeUnit.MINUTES.toMillis(3)))); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + AttributeScope.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5}}", now - TimeUnit.MINUTES.toMillis(3)))); // Script CF - invalid CalculatedField invalidCF = new CalculatedField(); @@ -903,9 +1419,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }; """); - Output scriptOutput = new Output(); - scriptOutput.setType(OutputType.TIME_SERIES); - scriptConfig.setOutput(scriptOutput); + scriptConfig.setOutput(new TimeSeriesOutput()); invalidCF.setConfiguration(scriptConfig); @@ -933,9 +1447,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes simpleConfig.setArguments(Map.of("a", argumentA)); simpleConfig.setExpression("a+1"); - Output simpleOutput = new Output(); + TimeSeriesOutput simpleOutput = new TimeSeriesOutput(); simpleOutput.setName("a+1"); - simpleOutput.setType(OutputType.TIME_SERIES); simpleOutput.setDecimalsByDefault(0); simpleConfig.setOutput(simpleOutput); @@ -957,7 +1470,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(result.get("a+1").get(0).get("value").asText()).isEqualTo("6"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":6}")); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + AttributeScope.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":6}")); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -983,4 +1496,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes return doPost("/api/asset", asset, Asset.class); } + private static Map kv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asText()); + } + return m; + } + } diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java new file mode 100644 index 0000000000..c23c59d137 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -0,0 +1,379 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.annotation.DirtiesContext; +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.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.CustomInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL; + +@DaoSqlTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest { + + private final String TZ = "Europe/Kyiv"; + + private Tenant savedTenant; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); + tenantProfileConfig.setMinAllowedAggregationIntervalInSecForCF(1); + tenantProfileConfig.setCfReevaluationCheckInterval(1); + }); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant@thingsboard.org"); + tenantAdmin.setFirstName("John"); + tenantAdmin.setLastName("Doe"); + + createUserAndLogin(tenantAdmin, "testPassword"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + deleteTenant(savedTenant.getId()); + } + + @Test + public void testCreateCfAndNoTelemetryDuringInterval_checkAggregation() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L); + createConsumptionCF(device.getId(), customInterval, null); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + + await().alias("create CF and no telemetry during interval -> save metric with default value") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("9999"); + assertThat(result.get("avgConsumption").get(0).get("value").isNull()).isTrue(); + }); + } + + @Test + public void testCreateCfWithoutWatermark_checkAggregation() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L); + createConsumptionCF(device.getId(), customInterval, null); + + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); + + long tsBeforeInterval = currentIntervalStartTs - 1000; + long tsInInterval_1 = currentIntervalStartTs + 1000; + long tsInInterval_2 = currentIntervalStartTs + 500; + long tsInInterval_3 = currentIntervalStartTs + 200; + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + + await().alias("create CF -> perform aggregation after interval end") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133"); + }); + + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", tsInInterval_1)); + + await().alias("update telemetry that belongs to previous interval -> no aggregation since watermark is not set ") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133"); + }); + } + + @Test + public void testCreateCfWithWatermark_checkAggregationDuringWatermark() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L); + Watermark watermark = new Watermark(10); + createConsumptionCF(device.getId(), customInterval, watermark); + + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); + + long tsBeforeInterval = currentIntervalStartTs - 1000L; + long tsInInterval_1 = currentIntervalStartTs + 1000L; + long tsInInterval_2 = currentIntervalStartTs + 500L; + long tsInInterval_3 = currentIntervalStartTs + 200L; + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + + await().alias("create CF -> perform aggregation after interval end") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133"); + }); + + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":300}}", tsInInterval_1)); + + await().alias("update telemetry during watermark -> perform aggregation") + .atMost(2 * 10, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("600"); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("200"); + }); + } + + @Test + public void testSendFutureTelemetry_checkAggregation() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval(TZ, 0L, 2L); + createConsumptionCF(device.getId(), customInterval, null); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + + // Wait for a fresh interval + long initialIntervalStart = customInterval.getCurrentIntervalStartTs(); + await().alias("wait for fresh interval") + .atMost(interval + 100, TimeUnit.MILLISECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> customInterval.getCurrentIntervalStartTs() != initialIntervalStart); + + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); + + long tsBeforeInterval = currentIntervalStartTs - 1000; + long tsInInterval_1 = currentIntervalStartTs + 1000; + long tsInInterval_2 = currentIntervalStartTs + 500; + long tsInInterval_3 = currentIntervalStartTs + 200; + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); + + await().alias("create CF -> perform aggregation after interval end") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133"); + }); + + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", currentIntervalStartTs + 4500L)); + + await().alias("update telemetry that belongs to future interval -> check aggregation ") + .atMost(3 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("500"); + assertThat(result.get("consumption").get(0).get("ts").asLong()).isEqualTo(currentIntervalStartTs + 4000L); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("500"); + assertThat(result.get("avgConsumption").get(0).get("ts").asLong()).isEqualTo(currentIntervalStartTs + 4000L); + }); + } + + private CalculatedField createConsumptionCF(EntityId entityId, AggInterval aggInterval, Watermark watermark) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null)); + arguments.put("en", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric consumption = new AggMetric(); + consumption.setFunction(AggFunction.SUM); + consumption.setInput(new AggKeyInput("en")); + consumption.setDefaultValue(9999.0); + aggMetrics.put("consumption", consumption); + + AggMetric avgEnergyConsumption = new AggMetric(); + avgEnergyConsumption.setFunction(AggFunction.AVG); + avgEnergyConsumption.setInput(new AggKeyInput("en")); + aggMetrics.put("avgConsumption", avgEnergyConsumption); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + + return createAggCf("Consumption", entityId, + aggInterval, + watermark, + arguments, + aggMetrics, + output); + } + + @Test + public void testCreateCfWith2Arguments_checkAggregation() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + createCFWith2Args(device.getId(), customInterval, null); + + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); + + long tsBeforeInterval = currentIntervalStartTs - 1000; + long tsInInterval_1 = currentIntervalStartTs + 1000; + long tsInInterval_2 = currentIntervalStartTs + 500; + long tsInInterval_3 = currentIntervalStartTs + 200; + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120, \"temperature\":43}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100, \"temperature\":39}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180, \"temperature\":27}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120, \"temperature\":50}}", tsInInterval_3)); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + + await().alias("create CF -> perform aggregation after interval end") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgTemperature"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + assertThat(result.get("avgTemperature").get(0).get("value").asText()).isEqualTo("39"); + }); + } + + private CalculatedField createCFWith2Args(EntityId entityId, AggInterval aggInterval, Watermark watermark) { + Map arguments = new HashMap<>(); + Argument energy = new Argument(); + energy.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null)); + arguments.put("en", energy); + + Argument temperature = new Argument(); + temperature.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + arguments.put("temp", temperature); + + Map aggMetrics = new HashMap<>(); + + AggMetric consumption = new AggMetric(); + consumption.setFunction(AggFunction.SUM); + consumption.setInput(new AggKeyInput("en")); + consumption.setDefaultValue(9999.0); + aggMetrics.put("consumption", consumption); + + AggMetric avgTemperature = new AggMetric(); + avgTemperature.setFunction(AggFunction.AVG); + avgTemperature.setInput(new AggKeyInput("temp")); + aggMetrics.put("avgTemperature", avgTemperature); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + + return createAggCf("CF with 2 args", entityId, + aggInterval, + watermark, + arguments, + aggMetrics, + output); + } + + private CalculatedField createAggCf(String name, + EntityId entityId, + AggInterval aggInterval, + Watermark watermark, + Map inputs, + Map metrics, + Output output) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName(name); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.ENTITY_AGGREGATION); + + EntityAggregationCalculatedFieldConfiguration configuration = new EntityAggregationCalculatedFieldConfiguration(); + + configuration.setArguments(inputs); + configuration.setMetrics(metrics); + configuration.setInterval(aggInterval); + if (watermark != null) { + configuration.setWatermark(watermark); + } + configuration.setOutput(output); + + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java new file mode 100644 index 0000000000..4c708bce56 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java @@ -0,0 +1,978 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.thingsboard.server.common.data.AttributeScope; +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.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +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.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL; + +@DaoSqlTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractControllerTest { + + private Tenant savedTenant; + + private DeviceProfile deviceProfile; + private Device device1; + private String accessToken1 = "1234567890111"; + private Device device2; + private String accessToken2 = "1234567890222"; + + private AssetProfile assetProfile; + private Asset asset; + + private final long deduplicationInterval = 5; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); + tenantProfileConfig.setMinAllowedScheduledUpdateIntervalInSecForCF(1); + }); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant@thingsboard.org"); + tenantAdmin.setFirstName("John"); + tenantAdmin.setLastName("Doe"); + + createUserAndLogin(tenantAdmin, "testPassword"); + + deviceProfile = doPost("/api/deviceProfile", createDeviceProfile("Device Profile"), DeviceProfile.class); + device1 = createDevice("Device 1", deviceProfile.getId(), accessToken1); + device2 = createDevice("Device 2", deviceProfile.getId(), accessToken2); + + postTelemetry(device1.getId(), "{\"occupied\":true}"); + postTelemetry(device2.getId(), "{\"occupied\":false}"); + + assetProfile = doPost("/api/assetProfile", createAssetProfile("Asset Profile"), AssetProfile.class); + asset = createAsset("Asset", assetProfile.getId()); + + createEntityRelation(asset.getId(), device1.getId(), "Contains"); + createEntityRelation(asset.getId(), device2.getId(), "Contains"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + deleteTenant(savedTenant.getId()); + } + + @Test + public void testCreateCfOnProfile_checkInitialAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(assetProfile.getId()); + + await().alias("create CF and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testAddEntityToProfile_checkAggregation() throws Exception { + createOccupancyCF(assetProfile.getId()); + + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + postTelemetry(device4.getId(), "{\"occupied\":true}"); + + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + + await().alias("add entity to profile with no related entities and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode occupancy = getLatestTelemetry(asset2.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); + assertThat(occupancy).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").isNull()).isTrue(); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").isNull()).isTrue(); + assertThat(occupancy.get("totalSpaces").get(0).get("value").isNull()).isTrue(); + }); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + await().alias("create relations and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + postTelemetry(device3.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testChangeEntityProfile_checkAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(assetProfile.getId()); + + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + + AssetProfile newAssetProfile = createAssetProfile("New Asset Profile"); + asset2.setAssetProfileId(newAssetProfile.getId()); + doPost("/api/asset", asset2, Asset.class); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + await().alias("change profile and no aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfOnAssetAndNoTelemetryOnDevices_checkDefaultValueUsed() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform aggregation with default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfAndUpdateTelemetry_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfAndRelationToRuleChain_checkAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("RuleChain"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + postTelemetry(ruleChain.getId(), "{\"occupied\":true}"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), ruleChain.getId(), "Contains"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + + postTelemetry(ruleChain.getId(), "{\"occupied\":true}"); + + await().alias("update telemetry on rule chain and no aggregation performed").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testDeleteCf_checkNoAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + doDelete("/api/calculatedField/" + cf.getId().getId().toString()) + .andExpect(status().isOk()); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("delete cf and update telemetry and no aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testUpdateTelemetry_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry -> no changes").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(this::checkInitialCalculationValues); + + postTelemetry(device2.getId(), "{\"occupied\":false}"); + + await().alias("create CF and perform initial calculation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testDeleteTelemetry_checkAggregationWithPreviousValuesOrDefault() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + long currentTime = System.currentTimeMillis(); + long firstTs = currentTime - 10; + long secondTs = currentTime - 10; + long thirdTs = currentTime - 5; + postTelemetry(device3.getId(), "{\"ts\": " + firstTs + ", \"values\": {\"occupied\":true}}"); + postTelemetry(device4.getId(), "{\"ts\": " + secondTs + ", \"values\": {\"occupied\":true}}"); + postTelemetry(device3.getId(), "{\"ts\": " + thirdTs + ", \"values\": {\"occupied\":true}}"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + doDelete("/api/plugins/telemetry/DEVICE/" + device3.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + thirdTs + "&endTs=" + thirdTs + 1, String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + device4.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + secondTs + "&endTs=" + secondTs + 1, String.class); + + await().alias("delete latest telemetry and perform aggregation with previous or default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testDeleteAttr_checkAggregationWithDefault() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + postAttributes(device3.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); + postAttributes(device4.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); + + createOccupancyCFWithAttr(asset2.getId()); + + await().alias("create CF and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + doDelete("/api/plugins/telemetry/DEVICE/" + device3.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + device4.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); + + await().alias("delete attribute and perform aggregation with default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateRelation_checkAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(assetProfile.getId()); + + await().alias("create CF and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + + Device device5 = createDevice("Device 5", "1234567890555"); + createEntityRelation(asset2.getId(), device5.getId(), "Contains"); + + await().alias("create relation and perform aggregation on asset 2") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "3", + "occupiedSpaces", "0", + "totalSpaces", "3" + )); + }); + } + + @Test + public void testDeleteRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + deleteEntityRelation(new EntityRelation(asset.getId(), device1.getId(), "Contains", RelationTypeGroup.COMMON)); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "0", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testDeleteEntityByRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + doDelete("/api/device/" + device1.getId()).andExpect(status().isOk()); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "0", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testUpdateRelationPath_checkAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + Device device3 = createDevice("Device 3", "1234567890333"); + createEntityRelation(asset.getId(), device3.getId(), "Has"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + configuration.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, "Has")); + saveCalculatedField(cf); + + await().alias("update relation path and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testUpdateArguments_checkAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupiedStatus\":false}"); + postTelemetry(device2.getId(), "{\"occupiedStatus\":false}"); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("oc", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("false"); + configuration.setArguments(Map.of("oc", argument)); + saveCalculatedField(cf); + + await().alias("update arguments and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testUpdateMetrics_checkAggregation() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + AggMetric aggMetric = new AggMetric(); + aggMetric.setInput(new AggKeyInput("temp")); + aggMetric.setFilter("return temp < 100;"); + aggMetric.setFunction(AggFunction.MAX); + configuration.setMetrics(Map.of("maxTemperature", aggMetric)); + saveCalculatedField(cf); + + await().alias("update metrics and perform aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("maxTemperature", "24")); + }); + + postTelemetry(device1.getId(), "{\"temperature\":101.3}"); + postTelemetry(device2.getId(), "{\"temperature\":25.8}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("maxTemperature", "26")); + }); + } + + @Test + public void testUpdateOutput_checkAggregation() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + AttributesOutput output = new AttributesOutput(); + output.setScope(AttributeScope.SERVER_SCOPE); + configuration.setOutput(output); + saveCalculatedField(cf); + + await().alias("update output and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode avgTemperature = getServerAttributes(asset.getId(), "avgTemperature"); + assertThat(avgTemperature).isNotNull(); + assertThat(avgTemperature.get(0)).isNotNull(); + assertThat(avgTemperature.get(0).get("value").asText()).isEqualTo("24.2"); + }); + } + + @Test + public void testUpdateDeduplicationInterval_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + configuration.setDeduplicationIntervalInSec(2 * deduplicationInterval); + saveCalculatedField(cf); + + await().alias("update deduplication interval and perform aggregation").atMost(deduplicationInterval / 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + postTelemetry(device2.getId(), "{\"temperature\":32.1}"); + + await().alias("update telemetry and perform aggregation").atMost(2 * deduplicationInterval + 10, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "28")); + }); + } + + + @Test + public void testTheSameRelationTypeAndKeyButDifferentCfs_checkAggregation() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + postTelemetry(device3.getId(), "{\"temperature\":10.2}"); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName("Average temperature"); + calculatedField.setEntityId(asset2.getId()); + calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + + var config = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + AggMetric avgMetric = new AggMetric(); + avgMetric.setFunction(AggFunction.AVG); + avgMetric.setInput(new AggKeyInput("temp")); + config.setMetrics(Map.of("avgTemperature_2", avgMetric)); + + calculatedField.setConfiguration(config); + calculatedField.setDebugSettings(DebugSettings.all()); + saveCalculatedField(calculatedField); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode avgTemperature = getLatestTelemetry(asset.getId(), "avgTemperature", "avgTemperature_2"); + assertThat(avgTemperature).isNotNull(); + assertThat(avgTemperature.get("avgTemperature").get(0).get("value").asText()).isEqualTo("24"); + assertThat(avgTemperature.get("avgTemperature_2").get(0).get("value").isNull()).isTrue(); + }); + + postTelemetry(device1.getId(), "{\"temperature\":26.2}"); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode avgTemperature = getLatestTelemetry(asset.getId(), "avgTemperature", "avgTemperature_2"); + assertThat(avgTemperature).isNotNull(); + assertThat(avgTemperature.get("avgTemperature").get(0).get("value").asText()).isEqualTo("26"); + assertThat(avgTemperature.get("avgTemperature_2").get(0).get("value").isNull()).isTrue(); + }); + } + + @Test + public void testUpdateMaxRelatedEntitiesPerArgument_checkAggregation() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setMaxRelatedEntitiesToReturnPerCfArgument(1); + }); + + login("tenant@thingsboard.org", "testPassword"); + + createCountCF(asset.getId()); + + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode numberOfDevices = getLatestTelemetry(asset.getId(), "numberOfDevices"); + assertThat(numberOfDevices).isNotNull(); + assertThat(numberOfDevices.get("numberOfDevices").get(0).get("value").asText()).isEqualTo("1"); + }); + + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setMaxRelatedEntitiesToReturnPerCfArgument(10); + }); + + login("tenant@thingsboard.org", "testPassword"); + + await().alias("update max related entities per argument and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode numberOfDevices = getLatestTelemetry(asset.getId(), "numberOfDevices"); + assertThat(numberOfDevices).isNotNull(); + assertThat(numberOfDevices.get("numberOfDevices").get(0).get("value").asText()).isEqualTo("2"); + }); + } + + private void checkInitialCalculation() { + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(this::checkInitialCalculationValues); + } + + private void checkInitialCalculationValues() throws Exception { + ObjectNode occupancy = getLatestTelemetry(asset.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); + assertThat(occupancy).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("1"); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("1"); + assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("2"); + } + + private CalculatedField createAvgTemperatureCF(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("20"); + arguments.put("temp", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric avgMetric = new AggMetric(); + avgMetric.setFunction(AggFunction.AVG); + avgMetric.setFilter("return temp >= 20;"); + avgMetric.setInput(new AggKeyInput("temp")); + aggMetrics.put("avgTemperature", avgMetric); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + + return createAggCf("Average temperature", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createOccupancyCF(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("false"); + arguments.put("oc", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + + return createAggCf("Occupied spaces", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createOccupancyCFWithAttr(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + argument.setDefaultValue("false"); + arguments.put("oc", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + + return createAggCf("Occupied spaces", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createCountCF(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("active", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("true"); + arguments.put("active", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric avgMetric = new AggMetric(); + avgMetric.setFunction(AggFunction.COUNT); + avgMetric.setInput(new AggKeyInput("active")); + aggMetrics.put("numberOfDevices", avgMetric); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + + return createAggCf("Number of devices", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createAggCf(String name, + EntityId entityId, + RelationPathLevel relation, + Map inputs, + Map metrics, + Output output) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName(name); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + + RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + configuration.setRelation(relation); + configuration.setArguments(inputs); + configuration.setDeduplicationIntervalInSec(deduplicationInterval); + configuration.setScheduledUpdateInterval(10); + configuration.setMetrics(metrics); + configuration.setOutput(output); + + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + + private Device createDevice(String name, DeviceProfileId deviceProfileId, String accessToken) { + Device device = new Device(); + device.setName(name); + device.setDeviceProfileId(deviceProfileId); + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + device.setDeviceData(deviceData); + return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return doPost("/api/asset", asset, Asset.class); + } + + private void verifyTelemetry(EntityId entityId, Map expectedResults) throws Exception { + ObjectNode result = getLatestTelemetry(entityId, expectedResults.keySet().toArray(new String[0])); + assertThat(result).isNotNull(); + expectedResults.forEach((key, value) -> assertThat(result.get(key).get(0).get("value").asText()).isEqualTo(value)); + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + + private ArrayNode getServerAttributes(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/attributes/SERVER_SCOPE?keys=" + String.join(",", keys), ArrayNode.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java new file mode 100644 index 0000000000..59314378aa --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java @@ -0,0 +1,168 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AiModel; +import org.thingsboard.client.model.OpenAiChatModelConfig; +import org.thingsboard.client.model.OpenAiProviderConfig; +import org.thingsboard.client.model.PageDataAiModel; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AIModelApiClientTest extends AbstractApiClientTest { + + private static final String AI_PREFIX = "AiTest_"; + + @Test + public void testSaveAndGetAiModel() throws Exception { + long ts = System.currentTimeMillis(); + String name = AI_PREFIX + "save_" + ts; + + AiModel model = buildAiModel(name, "gpt-4o", 0.7); + AiModel saved = client.saveAiModel(model); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(name, saved.getName()); + assertNotNull(saved.getConfiguration()); + + // get by id + AiModel fetched = client.getAiModelById(saved.getId().getId()); + assertNotNull(fetched); + assertEquals(name, fetched.getName()); + assertEquals(saved.getId().getId(), fetched.getId().getId()); + } + + @Test + public void testGetAiModelById() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("getbyid_" + ts); + + AiModel fetched = client.getAiModelById(saved.getId().getId()); + assertNotNull(fetched); + assertEquals(saved.getName(), fetched.getName()); + assertEquals(saved.getId().getId(), fetched.getId().getId()); + } + + @Test + public void testUpdateAiModel() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("update_" + ts); + + saved.setName(AI_PREFIX + "updated_" + ts); + OpenAiChatModelConfig updatedConfig = new OpenAiChatModelConfig(); + updatedConfig.setModelId("gpt-4o-mini"); + updatedConfig.setTemperature(0.3); + updatedConfig.setMaxOutputTokens(2048); + updatedConfig.setMaxRetries(50); + OpenAiProviderConfig providerConfig = new OpenAiProviderConfig(); + providerConfig.setApiKey("test-api-key"); + providerConfig.setBaseUrl("https://api.openai.com/v1"); + updatedConfig.setProviderConfig(providerConfig); + updatedConfig.setProvider("OPENAI"); + saved.setConfiguration(updatedConfig); + + AiModel updated = client.saveAiModel(saved); + assertNotNull(updated); + assertEquals(saved.getId().getId(), updated.getId().getId()); + assertEquals(AI_PREFIX + "updated_" + ts, updated.getName()); + } + + @Test + public void testDeleteAiModel() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("delete_" + ts); + + UUID modelId = saved.getId().getId(); + client.getAiModelById(modelId); + + Boolean deleted = client.deleteAiModelById(modelId); + assertTrue(deleted); + + assertReturns404(() -> client.getAiModelById(modelId)); + } + + @Test + public void testGetAiModels() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 3; i++) { + createAiModel("list_" + ts + "_" + i); + } + + PageDataAiModel page = client.getAiModels(100, 0, AI_PREFIX + "list_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (AiModel m : page.getData()) { + assertTrue(m.getName().startsWith(AI_PREFIX + "list_" + ts)); + } + } + + @Test + public void testGetAiModelById_notFound() { + UUID nonExistentId = UUID.randomUUID(); + assertReturns404(() -> client.getAiModelById(nonExistentId)); + } + + @Test + public void testGetAiModelsPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + createAiModel("paged_" + ts + "_" + i); + } + + PageDataAiModel page1 = client.getAiModels(2, 0, AI_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataAiModel lastPage = client.getAiModels(2, 2, AI_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + private AiModel buildAiModel(String name, String modelId, double temperature) { + OpenAiChatModelConfig config = new OpenAiChatModelConfig(); + config.setModelId(modelId); + config.setTemperature(temperature); + config.setMaxRetries(50); + OpenAiProviderConfig openAiProviderConfig = new OpenAiProviderConfig(); + openAiProviderConfig.setApiKey("test-api-key"); + openAiProviderConfig.setBaseUrl("https://api.openai.com/v1"); + config.setProviderConfig(openAiProviderConfig); + config.setProvider("OPENAI"); + + AiModel model = new AiModel(); + model.setName(name); + model.setConfiguration(config); + return model; + } + + private AiModel createAiModel(String suffix) throws Exception { + return client.saveAiModel(buildAiModel(AI_PREFIX + suffix, "gpt-4o", 0.7)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java new file mode 100644 index 0000000000..6bbda65a59 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.ThingsboardClient; +import org.thingsboard.client.model.ActivateUserRequest; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.JwtPair; +import org.thingsboard.client.model.User; +import org.thingsboard.client.model.UserId; +import org.thingsboard.server.common.data.util.ThrowingRunnable; +import org.thingsboard.server.controller.AbstractControllerTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@Slf4j +public abstract class AbstractApiClientTest extends AbstractControllerTest { + + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + protected static final ObjectMapper MAPPER = new ObjectMapper(); + + protected static final String TEST_PREFIX = "ApiClientTestDevice_"; + protected static final String TEST_PREFIX_2 = "ApiClientTestDevice2_"; + protected static final String CUSTOMER_USERNAME = "javaClientCustomer@thingsboard.org"; + protected static final String TENANT_ADMIN_USERNAME = "javaClientTenant@thingsboard.org"; + protected static final String TEST_PASSWORD = "password123"; + + protected ThingsboardClient client; + + // FQN for Tenant/Customer to avoid collision with AbstractWebTest fields + protected org.thingsboard.client.model.Tenant savedClientTenant; + protected User clientTenantAdmin; + protected org.thingsboard.client.model.Customer savedClientCustomer; + protected User savedClientCustomerUser; + + @Before + public void setUpJavaClient() throws Exception { + client = ThingsboardClient.builder() + .url("http://localhost:" + wsPort) + .build(); + client.login("sysadmin@thingsboard.org", "sysadmin"); + + org.thingsboard.client.model.Tenant tenant = new org.thingsboard.client.model.Tenant(); + tenant.setTitle("Java client test tenant"); + savedClientTenant = client.saveTenant(tenant); + + clientTenantAdmin = new User(); + clientTenantAdmin.setAuthority(Authority.TENANT_ADMIN); + clientTenantAdmin.setTenantId(savedClientTenant.getId()); + clientTenantAdmin.setEmail(TENANT_ADMIN_USERNAME); + clientTenantAdmin = client.saveUser(clientTenantAdmin, "false"); + activateUserAndAuthorize(clientTenantAdmin); + + org.thingsboard.client.model.Customer customer = new org.thingsboard.client.model.Customer(); + customer.setTitle("Java client test customer"); + customer.setTenantId(savedClientTenant.getId()); + savedClientCustomer = client.saveCustomer(customer, null, null, null); + + User customerUser = new User(); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(savedClientTenant.getId()); + customerUser.setCustomerId(savedClientCustomer.getId()); + customerUser.setEmail(CUSTOMER_USERNAME); + savedClientCustomerUser = client.saveUser(customerUser, "false"); + activateUser(savedClientCustomerUser.getId(), "password123", false); + } + + @After + public void tearDownJavaClient() { + client.login("sysadmin@thingsboard.org", "sysadmin"); + client.deleteTenant(savedClientTenant.getId().getId().toString()); + } + + protected String getBaseUrl() { + return "http://localhost:" + wsPort; + } + + protected void activateUserAndAuthorize(User user) throws ApiException { + JwtPair jwtPair = activateUser(user.getId(), TEST_PASSWORD, false); + client.setToken(jwtPair.getToken()); + } + + protected JwtPair activateUser(UserId userId, String password, boolean sendActivationMail) throws ApiException { + ActivateUserRequest activateRequest = new ActivateUserRequest(); + activateRequest.setActivateToken(getActivateToken(userId)); + activateRequest.setPassword(password); + return client.activateUser(activateRequest, sendActivationMail); + } + + protected String getActivateToken(UserId userId) throws ApiException { + String activateTokenRegex = "/api/noauth/activate?activateToken="; + String activationLink = client.getActivationLink(userId.getId().toString()); + return activationLink.substring(activationLink.lastIndexOf(activateTokenRegex) + activateTokenRegex.length()); + } + + protected void assertReturns404(ThrowingRunnable operation) { + try { + operation.run(); + fail("Expected ApiException with 404 status code"); + } catch (ApiException exception) { + assertEquals("Expected 404 status code but got " + exception.getCode(), + 404, exception.getCode()); + } catch (Exception e) { + fail("Expected ApiException but got " + e.getClass().getName() + ": " + e.getMessage()); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java new file mode 100644 index 0000000000..4d11bf3fbe --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.client.model.AdminSettings; +import org.thingsboard.client.model.FeaturesInfo; +import org.thingsboard.client.model.JwtSettings; +import org.thingsboard.client.model.SecuritySettings; +import org.thingsboard.client.model.SystemInfo; +import org.thingsboard.client.model.UpdateMessage; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AdminApiClientTest extends AbstractApiClientTest { + + @Test + public void testAdminSettingsLifecycle() throws Exception { + // authenticate as sysadmin for admin settings management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // get mail settings + AdminSettings mailSettings = client.getAdminSettings("mail"); + assertNotNull(mailSettings); + assertNotNull(mailSettings.getKey()); + assertEquals("mail", mailSettings.getKey()); + assertNotNull(mailSettings.getJsonValue()); + + // get general settings + AdminSettings generalSettings = client.getAdminSettings("general"); + assertNotNull(generalSettings); + assertEquals("general", generalSettings.getKey()); + assertNotNull(generalSettings.getJsonValue()); + assertNotNull(generalSettings.getJsonValue().get("baseUrl").asText()); + + // update general settings and restore + ((ObjectNode) generalSettings.getJsonValue()).put("prohibitDifferentUrl", true); + AdminSettings updatedGeneralSettings = client.saveAdminSettings(generalSettings); + assertTrue(updatedGeneralSettings.getJsonValue().get("prohibitDifferentUrl").asBoolean()); + + // get security settings + SecuritySettings securitySettings = client.getSecuritySettings(); + assertNotNull(securitySettings); + assertNotNull(securitySettings.getPasswordPolicy()); + Integer originalMaxAttempts = securitySettings.getMaxFailedLoginAttempts(); + + // update security settings + securitySettings.setMaxFailedLoginAttempts(10); + SecuritySettings updatedSecurity = client.saveSecuritySettings(securitySettings); + assertNotNull(updatedSecurity); + assertEquals(10, updatedSecurity.getMaxFailedLoginAttempts().intValue()); + + // restore original security settings + updatedSecurity.setMaxFailedLoginAttempts(originalMaxAttempts); + client.saveSecuritySettings(updatedSecurity); + + // get JWT settings + JwtSettings jwtSettings = client.getJwtSettings(); + assertNotNull(jwtSettings); + assertNotNull(jwtSettings.getTokenExpirationTime()); + assertNotNull(jwtSettings.getRefreshTokenExpTime()); + assertEquals("thingsboard.io", jwtSettings.getTokenIssuer()); + assertNotNull(jwtSettings.getTokenSigningKey()); + + // get system info + SystemInfo systemInfo = client.getSystemInfo(); + assertNotNull(systemInfo); + + // get features info + FeaturesInfo featuresInfo = client.getFeaturesInfo(); + assertNotNull(featuresInfo); + assertFalse(featuresInfo.getSmsEnabled()); + assertFalse(featuresInfo.getOauthEnabled()); + + // check updates + UpdateMessage updateMessage = client.checkUpdates(); + assertNotNull(updateMessage); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java new file mode 100644 index 0000000000..0ebc247018 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Alarm; +import org.thingsboard.client.model.AlarmInfo; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.AlarmStatus; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntitySubtype; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.PageDataAlarmInfo; +import org.thingsboard.client.model.PageDataEntitySubtype; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AlarmApiClientTest extends AbstractApiClientTest { + + @Test + public void testAlarmLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdAlarms = new ArrayList<>(); + + // First, create devices to attach alarms to + Device device1 = new Device(); + device1.setName("Device_For_Alarm_" + timestamp + "_1"); + device1.setType("default"); + Device createdDevice1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName("Device_For_Alarm_" + timestamp + "_2"); + device2.setType("thermostat"); + Device createdDevice2 = client.saveDevice(device2, null, null, null, null); + + // Create 2 alarms (1 for each device) + for (int i = 0; i < 2; i++) { + Alarm alarm = new Alarm(); + alarm.setType(((i % 2 == 0) ? "Temperature Alarm" : "Connection Alarm")); + alarm.setSeverity(((i % 2 == 0) ? AlarmSeverity.CRITICAL : AlarmSeverity.WARNING)); + alarm.setOriginator((i % 2 == 0) ? createdDevice1.getId() : createdDevice2.getId()); + + Alarm createdAlarm = client.saveAlarm(alarm); + assertNotNull(createdAlarm); + assertNotNull(createdAlarm.getId()); + assertEquals(alarm.getType(), createdAlarm.getType()); + assertEquals(alarm.getSeverity(), createdAlarm.getSeverity()); + + createdAlarms.add(createdAlarm); + } + + // Get all alarms + PageDataAlarmInfo allAlarms = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null); + + assertNotNull(allAlarms); + assertNotNull(allAlarms.getData()); + int initialSize = allAlarms.getData().size(); + assertEquals("Expected at least 2 alarms, but got " + initialSize, 2, initialSize); + + // Get alarms by entity (device1) + PageDataAlarmInfo device1Alarms = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null); + assertNotNull(device1Alarms); + assertEquals("Expected 1 alarms for device1", 1, device1Alarms.getData().size()); + + // Get alarm by id + Alarm searchAlarm = createdAlarms.get(0); + Alarm fetchedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(searchAlarm.getType(), fetchedAlarm.getType()); + assertEquals(searchAlarm.getSeverity(), fetchedAlarm.getSeverity()); + + // Get alarm info + AlarmInfo alarmInfo = client.getAlarmInfoById(searchAlarm.getId().getId().toString()); + assertNotNull(alarmInfo); + assertEquals(searchAlarm.getId().getId(), alarmInfo.getId().getId()); + + // Acknowledge alarm + client.ackAlarm(searchAlarm.getId().getId().toString()); + + // Verify alarm is acknowledged + Alarm ackedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(AlarmStatus.ACTIVE_ACK, ackedAlarm.getStatus()); + + // Clear alarm + client.clearAlarm(searchAlarm.getId().getId().toString()); + + // Verify alarm is cleared + Alarm clearedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(AlarmStatus.CLEARED_ACK, clearedAlarm.getStatus()); + + // Get highest severity alarm for device + AlarmSeverity highestSeverity = client.getHighestAlarmSeverity(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), null, null, null); + assertNotNull(highestSeverity); + assertEquals(AlarmSeverity.CRITICAL, highestSeverity); + + // Assign alarm to customer + client.assignAlarm(createdAlarms.get(0).getId().getId().toString(), clientTenantAdmin.getId().getId().toString()); + + // Verify assignment + Alarm assignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString()); + assertEquals(clientTenantAdmin.getId().getId(), assignedAlarm.getAssigneeId().getId()); + + // Unassign alarm + client.unassignAlarm(createdAlarms.get(0).getId().getId().toString()); + + // Verify unassignment + Alarm unassignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString()); + assertNull(unassignedAlarm.getAssigneeId()); + + // Get alarm types + PageDataEntitySubtype pageDataEntitySubtype = client.getAlarmTypes(100, 0, null, null); + assertEquals(2, pageDataEntitySubtype.getData().size()); + List alarmTypes = pageDataEntitySubtype.getData().stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + assertTrue(alarmTypes.containsAll(List.of("Temperature Alarm", "Connection Alarm"))); + + // Get alarms V2 (alternative endpoint) + PageDataAlarmInfo alarmsV2 = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null); + assertNotNull(alarmsV2); + assertEquals(1, alarmsV2.getData().size()); + + // Get all alarms V2 + PageDataAlarmInfo allAlarmsV2 = client.getAllAlarmsV2(100, 0, null, null, null, null, null, null, null, null, null); + assertEquals(2, allAlarmsV2.getData().size()); + + // Delete alarm + UUID alarmToDeleteId = createdAlarms.get(0).getId().getId(); + client.deleteAlarm(alarmToDeleteId.toString()); + + // Verify the alarm is deleted (should return 404) + assertReturns404(() -> + client.getAlarmById(alarmToDeleteId.toString()) + ); + + // Verify count after deletion + PageDataAlarmInfo alarmsAfterDelete = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null); + assertEquals(initialSize - 1, alarmsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java new file mode 100644 index 0000000000..31da96b7b4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.client.model.Alarm; +import org.thingsboard.client.model.AlarmComment; +import org.thingsboard.client.model.AlarmCommentInfo; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.PageDataAlarmCommentInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class AlarmCommentApiClientTest extends AbstractApiClientTest { + + @Test + public void testAlarmComments() throws Exception { + long timestamp = System.currentTimeMillis(); + + // Create device for alarm + Device device = new Device(); + device.setName("Device_For_Comments_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + // Create alarm + Alarm alarm = new Alarm(); + alarm.setType("Temperature Alarm"); + alarm.setSeverity(AlarmSeverity.CRITICAL); + alarm.setOriginator(createdDevice.getId()); + + Alarm createdAlarm = client.saveAlarm(alarm); + String alarmId = createdAlarm.getId().getId().toString(); + + List createdComments = new ArrayList<>(); + + // Create multiple comments + for (int i = 0; i < 5; i++) { + AlarmComment alarmComment = new AlarmComment(); + String message = "Test comment #" + i + " at " + timestamp; + ObjectNode comment = OBJECT_MAPPER.createObjectNode().put("message", message); + alarmComment.setComment(comment); + + AlarmComment commentInfo = client.saveAlarmComment(alarmId, alarmComment); + + assertNotNull(commentInfo); + assertNotNull(commentInfo.getId()); + JsonNode commentValue = commentInfo.getComment(); + assertEquals(message, commentValue.get("message").asText()); + assertNotNull(commentInfo.getCreatedTime()); + + createdComments.add(commentInfo); + } + + // Get all comments for the alarm + PageDataAlarmCommentInfo allComments = client.getAlarmComments(alarmId, 100, 0, null, null); + assertEquals("Expected 5 comments", 5, allComments.getData().size()); + + // Update a comment + AlarmComment commentToUpdate = createdComments.get(2); + JsonNode comment = commentToUpdate.getComment(); + ((ObjectNode) comment).put("message", "New comment"); + commentToUpdate.setComment(comment); + + AlarmComment updatedComment = client.saveAlarmComment(alarmId, commentToUpdate); + assertEquals("New comment", updatedComment.getComment().get("message").asText()); + + // Delete a comment + UUID commentToDeleteId = createdComments.get(0).getId().getId(); + + client.deleteAlarmComment(alarmId, commentToDeleteId.toString()); + + // Verify comment was updated to "deleted" + PageDataAlarmCommentInfo commentsAfterDelete = client.getAlarmComments(alarmId, 100, 0, null, null); + List data = commentsAfterDelete.getData(); + AlarmCommentInfo deletedComment = data.stream() + .filter(alarmCommentInfo -> alarmCommentInfo.getId().getId().equals(commentToDeleteId)) + .findFirst() + .get(); + assertEquals("User " + clientTenantAdmin.getEmail() + " deleted his comment", deletedComment.getComment().get("text").asText()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java new file mode 100644 index 0000000000..4b774a442a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.ApiKey; +import org.thingsboard.client.model.ApiKeyInfo; +import org.thingsboard.client.model.PageDataApiKeyInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class ApiKeyApiClientTest extends AbstractApiClientTest { + + @Test + public void testApiKeyLifecycle() throws Exception { + String userId = clientTenantAdmin.getId().getId().toString(); + + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Test API key"); + request.setUserId(clientTenantAdmin.getId()); + request.setEnabled(true); + ApiKey created = client.saveApiKey(request); + + assertNotNull(created); + assertNotNull(created.getId()); + assertNotNull(created.getValue()); + assertFalse(created.getValue().isBlank()); + assertEquals("Test API key", created.getDescription()); + + UUID keyId = created.getId().getId(); + + PageDataApiKeyInfo keysPage = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertNotNull(keysPage); + assertNotNull(keysPage.getData()); + assertTrue("Newly created API key should appear in user's key list", + keysPage.getData().stream() + .anyMatch(k -> k.getId().getId().equals(keyId))); + + client.deleteApiKey(keyId); + + PageDataApiKeyInfo keysAfterDelete = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertTrue("Deleted API key should not appear in user's key list", + keysAfterDelete.getData().stream() + .noneMatch(k -> k.getId().getId().equals(keyId))); + } + + @Test + public void testEnableDisableApiKey() throws Exception { + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Enable/disable test key"); + request.setUserId(clientTenantAdmin.getId()); + request.setEnabled(true); + ApiKey created = client.saveApiKey(request); + assertNotNull(created); + + UUID keyId = created.getId().getId(); + + ApiKeyInfo disabled = client.enableApiKey(keyId, false); + assertNotNull(disabled); + assertEquals(Boolean.FALSE, disabled.getEnabled()); + + ApiKeyInfo enabled = client.enableApiKey(keyId, true); + assertNotNull(enabled); + assertEquals(Boolean.TRUE, enabled.getEnabled()); + + client.deleteApiKey(keyId); + } + + @Test + public void testGetUserApiKeys() throws Exception { + String userId = clientTenantAdmin.getId().getId().toString(); + + int initialCount = client.getUserApiKeys(userId, 100, 0, null, null, null) + .getData().size(); + + UUID[] createdIds = new UUID[3]; + for (int i = 0; i < 3; i++) { + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Paging test key " + i); + request.setUserId(clientTenantAdmin.getId()); + createdIds[i] = client.saveApiKey(request).getId().getId(); + } + + PageDataApiKeyInfo afterCreate = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertEquals(initialCount + 3, afterCreate.getData().size()); + assertEquals(Long.valueOf(initialCount + 3), afterCreate.getTotalElements()); + + PageDataApiKeyInfo page1 = client.getUserApiKeys(userId, 2, 0, null, null, null); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + for (UUID id : createdIds) { + client.deleteApiKey(id); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java new file mode 100644 index 0000000000..070470ac2a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.PageDataAsset; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class AssetApiClientTest extends AbstractApiClientTest { + + @Test + public void testAssetLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdAssets = new ArrayList<>(); + + // create 20 assets + for (int i = 0; i < 20; i++) { + Asset asset = new Asset(); + String assetName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + asset.setName(assetName); + asset.setLabel("Test Asset " + i); + asset.setType(((i % 2 == 0) ? "default" : "building")); + + Asset createdAsset = client.saveAsset(asset, null, null, null); + assertNotNull(createdAsset); + assertNotNull(createdAsset.getId()); + assertEquals(assetName, createdAsset.getName()); + + createdAssets.add(createdAsset); + } + + // find all, check count + PageDataAsset allAssets = client.getTenantAssets(100, 0, null, null, null, null); + + assertNotNull(allAssets); + assertNotNull(allAssets.getData()); + int initialSize = allAssets.getData().size(); + assertEquals("Expected at least 20 assets, but got " + allAssets.getData().size(), 20, initialSize); + + //find all with search text, check count + PageDataAsset allAssetsBySearchText = client.getTenantAssets(100, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 test assets", 10, allAssetsBySearchText.getData().size()); + + // find by id + Asset searchAsset = createdAssets.get(10); + Asset asset = client.getAssetById(searchAsset.getId().getId().toString()); + assertEquals(searchAsset.getName(), asset.getName()); + + // delete asset + UUID assetToDeleteId = createdAssets.get(0).getId().getId(); + client.deleteAsset(assetToDeleteId.toString()); + + // Verify the asset is deleted + PageDataAsset assetsAfterDelete = client.getTenantAssets(100, 0, null, null, null, null); + assertEquals(initialSize - 1, assetsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getAssetById(assetToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java new file mode 100644 index 0000000000..7f6a2f667b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AssetProfile; +import org.thingsboard.client.model.AssetProfileInfo; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataAssetProfile; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AssetProfileApiClientTest extends AbstractApiClientTest { + + @Test + public void testAssetProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // Get initial count (there should be a default profile) + PageDataAssetProfile initialProfiles = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default asset profile", initialSize == 1); + + // Get default asset profile info + AssetProfileInfo defaultProfileInfo = client.getDefaultAssetProfileInfo(); + assertNotNull(defaultProfileInfo); + assertEquals(defaultProfileInfo.getName(), "default"); + + // Create multiple asset profiles + for (int i = 0; i < 5; i++) { + AssetProfile profile = new AssetProfile(); + profile.setName("Test Asset Profile " + timestamp + "_" + i); + profile.setDescription("Test description " + i); + + AssetProfile created = client.saveAssetProfile(profile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(profile.getName(), created.getName()); + assertEquals(profile.getDescription(), created.getDescription()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // Find all, check count + PageDataAssetProfile allProfiles = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // Find all with text search + PageDataAssetProfile filteredProfiles = client.getAssetProfiles(100, 0, "Test Asset Profile " + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // Get by id + AssetProfile searchProfile = createdProfiles.get(2); + AssetProfile fetchedProfile = client.getAssetProfileById(searchProfile.getId().getId().toString(), false); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // Update asset profile + fetchedProfile.setDescription("Updated description"); + AssetProfile updatedProfile = client.saveAssetProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // Get asset profile info by id + AssetProfileInfo profileInfo = client.getAssetProfileInfoById(searchProfile.getId().getId().toString()); + assertNotNull(profileInfo); + assertEquals(searchProfile.getName(), profileInfo.getName()); + + // Get asset profile infos (paginated) + PageDataAssetProfile profileInfos = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // Set a profile as default + AssetProfile profileToSetDefault = createdProfiles.get(1); + AssetProfile newDefault = client.setDefaultAssetProfile(profileToSetDefault.getId().getId().toString()); + assertNotNull(newDefault); + assertTrue(newDefault.getDefault()); + + // Verify default profile info now points to the new default + AssetProfileInfo newDefaultInfo = client.getDefaultAssetProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // Get asset profile names + List profileNames = client.getAssetProfileNames(false); + assertNotNull(profileNames); + assertEquals(createdProfiles.size() + 1, profileNames.size()); + + // Delete asset profile (cannot delete the default one, so delete a non-default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteAssetProfile(profileToDeleteId.toString()); + + // Verify the profile is deleted + assertReturns404(() -> + client.getAssetProfileById(profileToDeleteId.toString(), false)); + + // Verify count after deletion + PageDataAssetProfile profilesAfterDelete = client.getAssetProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + + // Restore original default profile + AssetProfile originalDefault = initialProfiles.getData().stream() + .filter(AssetProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultAssetProfile(originalDefault.getId().getId().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java new file mode 100644 index 0000000000..7594485c14 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java @@ -0,0 +1,286 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AlarmCalculatedFieldConfiguration; +import org.thingsboard.client.model.AlarmConditionValueAlarmSchedule; +import org.thingsboard.client.model.AlarmRule; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.Argument; +import org.thingsboard.client.model.ArgumentType; +import org.thingsboard.client.model.CalculatedField; +import org.thingsboard.client.model.CalculatedFieldType; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.PageDataCalculatedField; +import org.thingsboard.client.model.ReferencedEntityKey; +import org.thingsboard.client.model.SimpleAlarmCondition; +import org.thingsboard.client.model.SimpleCalculatedFieldConfiguration; +import org.thingsboard.client.model.SpecificTimeSchedule; +import org.thingsboard.client.model.TbelAlarmConditionExpression; +import org.thingsboard.client.model.TimeSeriesOutput; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class CalculatedFieldApiClientTest extends AbstractApiClientTest { + + @Test + public void testCalculatedFieldLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdFields = new ArrayList<>(); + + // create devices to attach calculated fields to + Device device1 = new Device(); + device1.setName("CalcFieldDevice1_" + timestamp); + device1.setType("default"); + Device createdDevice1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName("CalcFieldDevice2_" + timestamp); + device2.setType("default"); + Device createdDevice2 = client.saveDevice(device2, null, null, null, null); + + // create calculated fields on device1 + for (int i = 0; i < 5; i++) { + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "CalcField_" + timestamp + "_" + i); + cf.setType(CalculatedFieldType.SIMPLE); + + cf.setEntityId(createdDevice1.getId()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument arg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("temperature"); + refKey.setType(ArgumentType.TS_LATEST); + arg.setRefEntityKey(refKey); + config.putArgumentsItem("temp", arg); + + config.setExpression("temp * " + (i + 1)); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("scaledTemp_" + i); + config.setOutput(output); + + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(cf.getName(), created.getName()); + assertEquals(CalculatedFieldType.SIMPLE, created.getType()); + + createdFields.add(created); + } + + // create calculated fields on device2 + for (int i = 0; i < 3; i++) { + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "CalcField2_" + timestamp + "_" + i); + cf.setType(CalculatedFieldType.SIMPLE); + cf.setEntityId(createdDevice2.getId()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument arg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("humidity"); + refKey.setType(ArgumentType.TS_LATEST); + arg.setRefEntityKey(refKey); + config.putArgumentsItem("hum", arg); + + config.setExpression("hum + " + i); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("adjustedHumidity_" + i); + config.setOutput(output); + + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + createdFields.add(created); + } + + // get calculated fields by entity id for device1 + PageDataCalculatedField device1Fields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), + 100, 0, CalculatedFieldType.SIMPLE, null, null, null); + assertNotNull(device1Fields); + assertEquals(5, device1Fields.getData().size()); + + // get calculated fields by entity id for device2 + PageDataCalculatedField device2Fields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice2.getId().getId().toString(), + 100, 0, CalculatedFieldType.SIMPLE, null, null, null); + assertEquals(3, device2Fields.getData().size()); + + // get by id + CalculatedField searchField = createdFields.get(2); + CalculatedField fetchedField = client.getCalculatedFieldById(searchField.getId().getId().toString()); + assertEquals(searchField.getName(), fetchedField.getName()); + assertEquals(searchField.getType(), fetchedField.getType()); + assertNotNull(fetchedField.getConfiguration()); + SimpleCalculatedFieldConfiguration fetchedConfig = + (SimpleCalculatedFieldConfiguration) fetchedField.getConfiguration(); + assertEquals("temp * 3", fetchedConfig.getExpression()); + + // update calculated field + fetchedField.setName(fetchedField.getName() + "_updated"); + fetchedConfig.setExpression("temp * 100"); + CalculatedField updatedField = client.saveCalculatedField(fetchedField); + assertEquals(fetchedField.getName(), updatedField.getName()); + SimpleCalculatedFieldConfiguration updatedConfig = + (SimpleCalculatedFieldConfiguration) updatedField.getConfiguration(); + assertEquals("temp * 100", updatedConfig.getExpression()); + + // delete calculated field + UUID fieldToDeleteId = createdFields.get(0).getId().getId(); + client.deleteCalculatedField(fieldToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getCalculatedFieldById(fieldToDeleteId.toString()) + ); + + PageDataCalculatedField device1FieldsAfterDelete = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), + 100, 0, null, null, null, null); + assertEquals(4, device1FieldsAfterDelete.getData().size()); + } + + @Test + public void testAlarmCalculatedFieldLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create a device to attach the alarm calculated field to + Device device = new Device(); + device.setName("AlarmCalcFieldDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + // build the alarm calculated field configuration + AlarmCalculatedFieldConfiguration config = new AlarmCalculatedFieldConfiguration(); + + // argument: temperature time-series + Argument tempArg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("temperature"); + refKey.setType(ArgumentType.TS_LATEST); + tempArg.setRefEntityKey(refKey); + config.putArgumentsItem("temp", tempArg); + + // create rule: HIGH_TEMPERATURE when temp > 50 (TBEL expression) + TbelAlarmConditionExpression createExpression = new TbelAlarmConditionExpression(); + createExpression.setExpression("return temp > 50;"); + SimpleAlarmCondition createCondition = new SimpleAlarmCondition(); + createCondition.setExpression(createExpression); + SpecificTimeSchedule specificTimeSchedule = new SpecificTimeSchedule().addDaysOfWeekItem(3); + AlarmConditionValueAlarmSchedule schedule = new AlarmConditionValueAlarmSchedule().staticValue(specificTimeSchedule); + createCondition.setSchedule(schedule); + AlarmRule createRule = new AlarmRule(); + createRule.setCondition(createCondition); + createRule.setAlarmDetails("Temperature is too high: ${temp}"); + config.setCreateRules(Map.of( + AlarmSeverity.CRITICAL.name(), createRule + )); + + // clear rule: when temp drops below 30 + TbelAlarmConditionExpression clearExpression = new TbelAlarmConditionExpression(); + clearExpression.setExpression("return temp < 30;"); + SimpleAlarmCondition clearCondition = new SimpleAlarmCondition(); + clearCondition.setExpression(clearExpression); + AlarmRule clearRule = new AlarmRule(); + clearRule.setCondition(clearCondition); + config.setClearRule(clearRule); + + config.setPropagate(true); + config.setPropagateToOwner(false); + + // create calculated field + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "AlarmCalcField_" + timestamp); + cf.setType(CalculatedFieldType.ALARM); + + cf.setEntityId(createdDevice.getId()); + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(cf.getName(), created.getName()); + assertEquals(CalculatedFieldType.ALARM, created.getType()); + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) created.getConfiguration(); + AlarmConditionValueAlarmSchedule createdSchedule = configuration.getCreateRules().get(AlarmSeverity.CRITICAL.name()).getCondition().getSchedule(); + SpecificTimeSchedule staticSchedule = (SpecificTimeSchedule) createdSchedule.getStaticValue(); + assertEquals(Set.of(3), staticSchedule.getDaysOfWeek()); + + // get by id and verify configuration + CalculatedField fetched = client.getCalculatedFieldById(created.getId().getId().toString()); + assertNotNull(fetched); + assertEquals(created.getName(), fetched.getName()); + assertEquals(CalculatedFieldType.ALARM, fetched.getType()); + assertNotNull(fetched.getConfiguration()); + AlarmCalculatedFieldConfiguration fetchedConfig = + (AlarmCalculatedFieldConfiguration) fetched.getConfiguration(); + assertNotNull(fetchedConfig.getCreateRules()); + assertEquals(1, fetchedConfig.getCreateRules().size()); + assertTrue(fetchedConfig.getCreateRules().containsKey("CRITICAL")); + assertNotNull(fetchedConfig.getClearRule()); + assertEquals(Boolean.TRUE, fetchedConfig.getPropagate()); + + // update: add a second create rule for CRITICAL_TEMPERATURE + TbelAlarmConditionExpression criticalExpression = new TbelAlarmConditionExpression(); + criticalExpression.setExpression("return temp > 80;"); + SimpleAlarmCondition criticalCondition = new SimpleAlarmCondition(); + criticalCondition.setExpression(criticalExpression); + AlarmRule criticalRule = new AlarmRule(); + criticalRule.setCondition(criticalCondition); + fetchedConfig.putCreateRulesItem(AlarmSeverity.INDETERMINATE.name(), criticalRule); + fetched.setConfiguration(fetchedConfig); + + CalculatedField updated = client.saveCalculatedField(fetched); + AlarmCalculatedFieldConfiguration updatedConfig = + (AlarmCalculatedFieldConfiguration) updated.getConfiguration(); + assertEquals(2, updatedConfig.getCreateRules().size()); + assertTrue(updatedConfig.getCreateRules().containsKey("INDETERMINATE")); + + // filter by entity and ALARM type + PageDataCalculatedField deviceFields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice.getId().getId().toString(), + 100, 0, CalculatedFieldType.ALARM, null, null, null); + assertNotNull(deviceFields); + assertEquals(1, deviceFields.getData().size()); + + // delete and verify + UUID fieldId = created.getId().getId(); + client.deleteCalculatedField(fieldId.toString()); + assertReturns404(() -> client.getCalculatedFieldById(fieldId.toString())); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java new file mode 100644 index 0000000000..fff3670c85 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Customer; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.PageDataCustomer; +import org.thingsboard.client.model.PageDataDevice; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class CustomerApiClientTest extends AbstractApiClientTest { + + @Test + public void testCustomerLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdCustomers = new ArrayList<>(); + + // create 20 customers + for (int i = 0; i < 20; i++) { + Customer customer = new Customer(); + String customerTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + customer.setTitle(customerTitle); + customer.setEmail("customer_" + timestamp + "_" + i + "@test.com"); + + Customer createdCustomer = client.saveCustomer(customer, null, null, null); + assertNotNull(createdCustomer); + assertNotNull(createdCustomer.getId()); + assertEquals(customerTitle, createdCustomer.getTitle()); + + createdCustomers.add(createdCustomer); + } + + // find all, check count (includes savedClientCustomer from AbstractApiClientTest setup) + PageDataCustomer allCustomers = client.getCustomers(100, 0, null, null, null); + assertNotNull(allCustomers); + assertNotNull(allCustomers.getData()); + int initialSize = allCustomers.getData().size(); + assertEquals("Expected 21 customers (20 created + 1 from setup), but got " + initialSize, 21, initialSize); + + // find all with search text, check count + PageDataCustomer filteredCustomers = client.getCustomers(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 customers matching prefix", 10, filteredCustomers.getData().size()); + + // find by id + Customer searchCustomer = createdCustomers.get(10); + Customer fetchedCustomer = client.getCustomerById(searchCustomer.getId().getId().toString()); + assertEquals(searchCustomer.getTitle(), fetchedCustomer.getTitle()); + + // find by title + Customer fetchedByTitle = client.getTenantCustomer(searchCustomer.getTitle()); + assertEquals(searchCustomer.getId().getId(), fetchedByTitle.getId().getId()); + + // update customer + fetchedCustomer.setCity("New York"); + fetchedCustomer.setCountry("US"); + Customer updatedCustomer = client.saveCustomer(fetchedCustomer, null, null, null); + assertEquals("New York", updatedCustomer.getCity()); + assertEquals("US", updatedCustomer.getCountry()); + + // assign device to customer and verify + Device device = new Device(); + device.setName("CustomerTestDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + String customerId = createdCustomers.get(0).getId().getId().toString(); + client.assignDeviceToCustomer(customerId, createdDevice.getId().getId().toString()); + + PageDataDevice customerDevices = client.getCustomerDevices(customerId, 100, 0, null, null, null, null); + assertEquals(1, customerDevices.getData().size()); + assertEquals(createdDevice.getName(), customerDevices.getData().get(0).getName()); + + // unassign device from customer + client.unassignDeviceFromCustomer(createdDevice.getId().getId().toString()); + PageDataDevice devicesAfterUnassign = client.getCustomerDevices(customerId, 100, 0, null, null, null, null); + assertEquals(0, devicesAfterUnassign.getData().size()); + + // delete customer + UUID customerToDeleteId = createdCustomers.get(0).getId().getId(); + client.deleteCustomer(customerToDeleteId.toString()); + + // verify deletion + PageDataCustomer customersAfterDelete = client.getCustomers(100, 0, null, null, null); + assertEquals(initialSize - 1, customersAfterDelete.getData().size()); + + assertReturns404(() -> + client.getCustomerById(customerToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java new file mode 100644 index 0000000000..386e2db84f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Dashboard; +import org.thingsboard.client.model.DashboardInfo; +import org.thingsboard.client.model.PageDataDashboardInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class DashboardApiClientTest extends AbstractApiClientTest { + + @Test + public void testDashboardLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create 20 dashboards + for (int i = 0; i < 20; i++) { + Dashboard dashboard = new Dashboard(); + String dashboardTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + dashboard.setTitle(dashboardTitle); + + client.saveDashboard(dashboard, null); + } + + // find all, check count + PageDataDashboardInfo allDashboards = client.getTenantDashboards(100, 0, null, null, null, null); + assertNotNull(allDashboards); + assertNotNull(allDashboards.getData()); + int initialSize = allDashboards.getData().size(); + assertEquals("Expected 20 dashboards, but got " + initialSize, 20, initialSize); + + List createdDashboards = allDashboards.getData(); + + // find all with search text, check count + PageDataDashboardInfo filteredDashboards = client.getTenantDashboards(100, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 dashboards matching prefix", 10, filteredDashboards.getData().size()); + + // find by id + DashboardInfo searchDashboard = createdDashboards.get(10); + DashboardInfo fetchedDashboard = client.getDashboardInfoById(searchDashboard.getId().getId().toString()); + assertEquals(searchDashboard.getTitle(), fetchedDashboard.getTitle()); + + // update dashboard + Dashboard dashboardToUpdate = new Dashboard(); + dashboardToUpdate.setId(fetchedDashboard.getId()); + dashboardToUpdate.setTitle(fetchedDashboard.getTitle() + "_updated"); + dashboardToUpdate.setVersion(fetchedDashboard.getVersion()); + client.saveDashboard(dashboardToUpdate, null); + + DashboardInfo updatedDashboard = client.getDashboardInfoById(fetchedDashboard.getId().getId().toString()); + assertEquals(fetchedDashboard.getTitle() + "_updated", updatedDashboard.getTitle()); + + // assign dashboard to customer and verify + String customerId = savedClientCustomer.getId().getId().toString(); + String dashboardId = createdDashboards.get(0).getId().getId().toString(); + client.assignDashboardToCustomer(customerId, dashboardId); + + PageDataDashboardInfo customerDashboards = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null); + assertEquals(1, customerDashboards.getData().size()); + assertEquals(createdDashboards.get(0).getTitle(), customerDashboards.getData().get(0).getTitle()); + + // unassign dashboard from customer + client.unassignDashboardFromCustomer(customerId, dashboardId); + PageDataDashboardInfo dashboardsAfterUnassign = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null); + assertEquals(0, dashboardsAfterUnassign.getData().size()); + + // make dashboard public and verify + client.assignDashboardToPublicCustomer(dashboardId); + DashboardInfo publicDashboard = client.getDashboardInfoById(dashboardId); + assertNotNull(publicDashboard.getAssignedCustomers()); + assertTrue(publicDashboard.getAssignedCustomers().size() > 0); + + // remove public access + client.unassignDashboardFromPublicCustomer(dashboardId); + + // delete dashboard + UUID dashboardToDeleteId = createdDashboards.get(0).getId().getId(); + client.deleteDashboard(dashboardToDeleteId.toString()); + + // verify deletion + PageDataDashboardInfo dashboardsAfterDelete = client.getTenantDashboards(100, 0, null, null, null, null); + assertEquals(initialSize - 1, dashboardsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getDashboardInfoById(dashboardToDeleteId.toString()) + ); + } + + @Test + public void testGetServerTime() throws Exception { + Long serverTime = client.getServerTime(); + assertNotNull(serverTime); + } + + @Test + public void testGetMaxDatapointsLimit() throws Exception { + Long maxDatapointsLimit = client.getMaxDatapointsLimit(); + assertNotNull(maxDatapointsLimit); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java new file mode 100644 index 0000000000..f3695b9fc1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.DeviceCredentials; +import org.thingsboard.client.model.DeviceCredentialsType; +import org.thingsboard.client.model.PageDataDevice; +import org.thingsboard.client.model.SaveDeviceWithCredentialsRequest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class DeviceApiClientTest extends AbstractApiClientTest { + + @Test + public void testDeviceLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdDevices = new ArrayList<>(); + + // create 20 devices + for (int i = 0; i < 20; i++) { + Device device = new Device(); + String deviceName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + device.setName(deviceName); + device.setLabel("Test Device " + i); + device.setType(((i % 2 == 0) ? "default" : "thermostat")); + + Device createdDevice = client.saveDevice(device, null, null, null, null); + assertNotNull(createdDevice); + assertNotNull(createdDevice.getId()); + assertEquals(deviceName, createdDevice.getName()); + + createdDevices.add(createdDevice); + } + + // find all, check count + PageDataDevice allDevices = client.getTenantDevices(100, 0, null, null, null, null); + + assertNotNull(allDevices); + assertNotNull(allDevices.getData()); + int initialSize = allDevices.getData().size(); + assertEquals("Expected at least 20 devices, but got " + allDevices.getData().size(), 20, initialSize); + + //find all with search text, check count + PageDataDevice allDevicesBySearchText = client.getTenantDevices(10, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 test devices", 10, allDevicesBySearchText.getData().size()); + + // find by id + Device searchDevice = createdDevices.get(10); + Device device = client.getDeviceById(searchDevice.getId().getId().toString()); + assertEquals(searchDevice.getName(), device.getName()); + + // create device with credentials + Device deviceWithCreds = new Device(); + deviceWithCreds.setName("device-with-creds"); + + DeviceCredentials creds = new DeviceCredentials(); + creds.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + creds.setCredentialsId("TEST_ACCESS_TOKEN"); + + SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest(); + request.setDevice(deviceWithCreds); + request.setCredentials(creds); + + Device savedDeviceWithCreds = client.saveDeviceWithCredentials(request, null, null, null); + assertEquals("device-with-creds", savedDeviceWithCreds.getName()); + + // find credentials by device id + DeviceCredentials fetchedCreds = client.getDeviceCredentialsByDeviceId(savedDeviceWithCreds.getId().getId().toString()); + assertEquals(creds.getCredentialsId(), fetchedCreds.getCredentialsId()); + + // delete device + UUID deviceToDeleteId = createdDevices.get(0).getId().getId(); + client.deleteDevice(deviceToDeleteId.toString()); + + // Verify the device is deleted + PageDataDevice devicesAfterDelete = client.getTenantDevices(100, 0, null, null, null, null); + assertEquals(initialSize, devicesAfterDelete.getData().size()); + + assertReturns404(() -> + client.getDeviceById(deviceToDeleteId.toString())); + + // assign device to customer + client.assignDeviceToCustomer(savedClientCustomer.getId().getId().toString(), savedDeviceWithCreds.getId().getId().toString()); + + // check customer devices + PageDataDevice pageDataDevice = client.getCustomerDevices(savedClientCustomer.getId().getId().toString(), 100, 0, null, null, null, null); + List data = pageDataDevice.getData(); + assertEquals(1, data.size()); + assertEquals(savedDeviceWithCreds.getName(), data.get(0).getName()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java new file mode 100644 index 0000000000..d8204cab41 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.thingsboard.client.model.Device; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +@DaoSqlTest +public class DeviceConnectivityApiClientTest extends AbstractApiClientTest { + + @Test + public void testGetDevicePublishTelemetryCommands() throws Exception { + Device device = new Device(); + device.setName(TEST_PREFIX + System.currentTimeMillis()); + device.setType("default"); + + Device savedDevice = client.saveDevice(device, null, null, null, null); + String token = client.getDeviceCredentialsByDeviceId(savedDevice.getId().getId().toString()).getCredentialsId(); + + String deviceId = savedDevice.getId().getId().toString(); + + JsonNode commands = client.getDevicePublishTelemetryCommands(deviceId); + assertEquals("curl -v -X POST http://localhost:8080/api/v1/" + token + "/telemetry --header Content-Type:application/json --data \"{temperature:25}\"", commands.get("http").get("http").asText()); + assertEquals("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry -u \"" + token + "\" -m \"{temperature:25}\"", commands.get("mqtt").get("mqtt").asText()); + assertEquals("coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://localhost:5683/api/v1/" + token + "/telemetry", commands.get("coap").get("coap").asText()); + } + + @Test + public void testGetDevicePublishTelemetryCommands_nonExistentDevice() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getDevicePublishTelemetryCommands(nonExistentId)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java new file mode 100644 index 0000000000..e8fcb4ffd1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java @@ -0,0 +1,157 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.DefaultDeviceProfileConfiguration; +import org.thingsboard.client.model.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.client.model.DeviceProfile; +import org.thingsboard.client.model.DeviceProfileData; +import org.thingsboard.client.model.DeviceProfileInfo; +import org.thingsboard.client.model.DeviceProfileType; +import org.thingsboard.client.model.DeviceTransportType; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataDeviceProfile; +import org.thingsboard.client.model.PageDataDeviceProfileInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class DeviceProfileApiClientTest extends AbstractApiClientTest { + + @Test + public void testDeviceProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // Get initial count (there should be a default profile) + PageDataDeviceProfile initialProfiles = client.getDeviceProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default device profile", initialSize >= 1); + + // Get default device profile info + DeviceProfileInfo defaultProfileInfo = client.getDefaultDeviceProfileInfo(); + assertNotNull(defaultProfileInfo); + assertNotNull(defaultProfileInfo.getName()); + + // Create multiple device profiles + for (int i = 0; i < 5; i++) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName("Test Device Profile " + timestamp + "_" + i); + deviceProfile.setDescription("Test description " + i); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + configuration.setType(DeviceProfileType.DEFAULT.getValue()); + deviceProfileData.setConfiguration(configuration); + DefaultDeviceProfileTransportConfiguration transportConf = new DefaultDeviceProfileTransportConfiguration(); + transportConf.setType(DeviceTransportType.DEFAULT.getValue()); + deviceProfileData.setTransportConfiguration(transportConf); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + + DeviceProfile created = client.saveDeviceProfile(deviceProfile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(deviceProfile.getName(), created.getName()); + assertEquals(deviceProfile.getDescription(), created.getDescription()); + assertEquals(DeviceProfileType.DEFAULT, created.getType()); + assertEquals(DeviceTransportType.DEFAULT, created.getTransportType()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // Find all, check count + PageDataDeviceProfile allProfiles = client.getDeviceProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // Find all with text search + PageDataDeviceProfile filteredProfiles = client.getDeviceProfiles(100, 0, "Test Device Profile " + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // Get by id + DeviceProfile searchProfile = createdProfiles.get(2); + DeviceProfile fetchedProfile = client.getDeviceProfileById(searchProfile.getId().getId().toString(), false); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // Update device profile + fetchedProfile.setDescription("Updated description"); + DeviceProfile updatedProfile = client.saveDeviceProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // Get device profile info by id + DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo(); + assertNotNull(profileInfo); + assertEquals(searchProfile.getType().getValue().toLowerCase(), profileInfo.getName()); + assertEquals(DeviceTransportType.DEFAULT, profileInfo.getTransportType()); + + // Get device profile infos (paginated) + PageDataDeviceProfileInfo profileInfos = client.getDeviceProfileInfos(100, 0, null, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // Set a profile as default + DeviceProfile profileToSetDefault = createdProfiles.get(1); + DeviceProfile newDefault = client.setDefaultDeviceProfile(profileToSetDefault.getId().getId().toString()); + assertNotNull(newDefault); + assertTrue(newDefault.getDefault()); + + // Verify default profile info now points to the new default + DeviceProfileInfo newDefaultInfo = client.getDefaultDeviceProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // Get device profile names + List profileNames = client.getDeviceProfileNames(false); + assertNotNull(profileNames); + assertEquals(createdProfiles.size() + 1, profileNames.size()); + + // Delete device profile (cannot delete the default one, so delete a non-default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteDeviceProfile(profileToDeleteId.toString()); + + // Verify the profile is deleted + assertReturns404(() -> + client.getDeviceProfileById(profileToDeleteId.toString(), false)); + + // Verify count after deletion + PageDataDeviceProfile profilesAfterDelete = client.getDeviceProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + + // Restore original default profile + DeviceProfile originalDefault = initialProfiles.getData().stream() + .filter(DeviceProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultDeviceProfile(originalDefault.getId().getId().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java new file mode 100644 index 0000000000..e9498a1c50 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.After; +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Domain; +import org.thingsboard.client.model.DomainInfo; +import org.thingsboard.client.model.PageDataDomainInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class DomainApiClientTest extends AbstractApiClientTest { + + List createdDomains = new ArrayList<>(); + + @After + public void afterDomainTest() { + createdDomains.forEach(domain -> { + try { + client.deleteDomain(domain.getId().getId()); + } catch (ApiException e) { + // ignore + } + }); + } + + @Test + public void testDomainLifecycle() throws Exception { + client.login("sysadmin@thingsboard.org", "sysadmin"); + + long timestamp = System.currentTimeMillis(); + + // create 5 domains + for (int i = 0; i < 5; i++) { + Domain domain = new Domain(); + domain.setName("domain." + i + ".com"); + domain.setOauth2Enabled(false); + domain.setPropagateToEdge(false); + + Domain created = client.saveDomain(domain, null); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(domain.getName(), created.getName()); + assertEquals(false, created.getOauth2Enabled()); + + createdDomains.add(created); + } + + // list tenant domains with text search + PageDataDomainInfo filteredDomains = client.getDomainInfos(100, 0, + "domain.", null, null); + assertNotNull(filteredDomains); + assertEquals(5, filteredDomains.getData().size()); + + // get domain info by id + Domain searchDomain = createdDomains.get(2); + DomainInfo fetchedInfo = client.getDomainInfoById(searchDomain.getId().getId()); + assertEquals(searchDomain.getName(), fetchedInfo.getName()); + assertEquals(searchDomain.getOauth2Enabled(), fetchedInfo.getOauth2Enabled()); + assertNotNull(fetchedInfo.getOauth2ClientInfos()); + + // update domain + Domain domainToUpdate = createdDomains.get(3); + domainToUpdate.setPropagateToEdge(true); + Domain updatedDomain = client.saveDomain(domainToUpdate, null); + assertEquals(true, updatedDomain.getPropagateToEdge()); + + // delete domain + UUID domainToDeleteId = createdDomains.get(0).getId().getId(); + createdDomains.remove(0); + client.deleteDomain(domainToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getDomainInfoById(domainToDeleteId) + ); + + PageDataDomainInfo domainsAfterDelete = client.getDomainInfos(100, 0, + "domain.", null, null); + assertEquals(4, domainsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java new file mode 100644 index 0000000000..6aeb7f0ef6 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java @@ -0,0 +1,141 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Edge; +import org.thingsboard.client.model.EdgeInfo; +import org.thingsboard.client.model.PageDataEdge; +import org.thingsboard.client.model.PageDataEdgeInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class EdgeApiClientTest extends AbstractApiClientTest { + + @Test + public void testEdgeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdEdges = new ArrayList<>(); + + // create 5 edges + for (int i = 0; i < 5; i++) { + Edge edge = new Edge(); + edge.setName(TEST_PREFIX + "Edge_" + timestamp + "_" + i); + edge.setType("gateway"); + edge.setLabel("Test Edge " + i); + edge.setRoutingKey("routing_key_" + timestamp + "_" + i); + edge.setSecret("secret_key_" + timestamp + "_" + i); + + Edge created = client.saveEdge(edge); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(edge.getName(), created.getName()); + assertEquals("gateway", created.getType()); + assertNotNull(created.getRoutingKey()); + assertNotNull(created.getSecret()); + + createdEdges.add(created); + } + + // list tenant edges with text search + PageDataEdge filteredEdges = client.getTenantEdges(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertNotNull(filteredEdges); + assertEquals(5, filteredEdges.getData().size()); + + // list tenant edges with type filter + PageDataEdge typedEdges = client.getTenantEdges(100, 0, "gateway", + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(5, typedEdges.getData().size()); + + // get tenant edge infos + PageDataEdgeInfo edgeInfos = client.getTenantEdgeInfos(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(5, edgeInfos.getData().size()); + + // get edge by id + Edge searchEdge = createdEdges.get(2); + Edge fetchedEdge = client.getEdgeById(searchEdge.getId().getId().toString()); + assertEquals(searchEdge.getName(), fetchedEdge.getName()); + assertEquals(searchEdge.getType(), fetchedEdge.getType()); + assertEquals(searchEdge.getRoutingKey(), fetchedEdge.getRoutingKey()); + + // get edge by name + Edge fetchedByName = client.getTenantEdgeByName(searchEdge.getName()); + assertEquals(searchEdge.getId().getId(), fetchedByName.getId().getId()); + + // get edges by list of ids + List idsToFetch = List.of( + createdEdges.get(0).getId().getId().toString(), + createdEdges.get(1).getId().getId().toString() + ); + List edgeList = client.getEdgeList(idsToFetch); + assertEquals(2, edgeList.size()); + + // update edge + Edge edgeToUpdate = createdEdges.get(3); + edgeToUpdate.setLabel("Updated Label"); + Edge updatedEdge = client.saveEdge(edgeToUpdate); + assertEquals("Updated Label", updatedEdge.getLabel()); + + // assign edge to customer + String customerId = savedClientCustomer.getId().getId().toString(); + String edgeId = createdEdges.get(1).getId().getId().toString(); + Edge assignedEdge = client.assignEdgeToCustomer(customerId, edgeId); + assertNotNull(assignedEdge.getCustomerId()); + + // get customer edges + PageDataEdge customerEdges = client.getCustomerEdges(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(1, customerEdges.getData().size()); + + // get customer edge infos + PageDataEdgeInfo customerEdgeInfos = client.getCustomerEdgeInfos(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(1, customerEdgeInfos.getData().size()); + EdgeInfo edgeInfo = customerEdgeInfos.getData().get(0); + assertNotNull(edgeInfo.getCustomerTitle()); + + // unassign edge from customer + Edge unassignedEdge = client.unassignEdgeFromCustomer(edgeId); + assertNotNull(unassignedEdge); + + PageDataEdge customerEdgesAfter = client.getCustomerEdges(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(0, customerEdgesAfter.getData().size()); + + // delete edge + UUID edgeToDeleteId = createdEdges.get(0).getId().getId(); + client.deleteEdge(edgeToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getEdgeById(edgeToDeleteId.toString()) + ); + + PageDataEdge edgesAfterDelete = client.getTenantEdges(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(4, edgesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java new file mode 100644 index 0000000000..2571306eb6 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java @@ -0,0 +1,289 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AliasEntityId; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.AssetTypeFilter; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.DeviceTypeFilter; +import org.thingsboard.client.model.Direction; +import org.thingsboard.client.model.EntityData; +import org.thingsboard.client.model.EntityDataPageLink; +import org.thingsboard.client.model.EntityDataQuery; +import org.thingsboard.client.model.EntityDataSortOrder; +import org.thingsboard.client.model.EntityKey; +import org.thingsboard.client.model.EntityKeyType; +import org.thingsboard.client.model.EntityKeyValueType; +import org.thingsboard.client.model.EntityListFilter; +import org.thingsboard.client.model.EntityNameFilter; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.FilterPredicateValueString; +import org.thingsboard.client.model.KeyFilter; +import org.thingsboard.client.model.PageDataEntityData; +import org.thingsboard.client.model.SingleEntityFilter; +import org.thingsboard.client.model.StringFilterPredicate; +import org.thingsboard.client.model.StringOperation; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityQueryApiClientTest extends AbstractApiClientTest { + + private static final String QUERY_TEST_PREFIX = "QueryTest_"; + + private EntityDataPageLink pageLink(int pageSize) { + return new EntityDataPageLink() + .pageSize(pageSize) + .page(0) + .sortOrder(new EntityDataSortOrder() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .direction(Direction.ASC)); + } + + @Test + public void testFindByDeviceTypeFilter() throws Exception { + long ts = System.currentTimeMillis(); + String type1 = "temperatureSensor"; + String type2 = "humiditySensor"; + + for (int i = 0; i < 3; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "temp_" + ts + "_" + i); + d.setType(type1); + client.saveDevice(d, null, null, null, null); + } + for (int i = 0; i < 2; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "hum_" + ts + "_" + i); + d.setType(type2); + client.saveDevice(d, null, null, null, null); + } + + // filter by single device type + EntityDataQuery singleTypeQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(singleTypeQuery); + assertNotNull(result); + assertEquals(3, result.getTotalElements().intValue()); + for (EntityData entity : result.getData()) { + assertNotNull(entity.getEntityId()); + } + + // filter by multiple device types + EntityDataQuery multiTypeQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1, type2))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData multiResult = client.findEntityDataByQuery(multiTypeQuery); + assertNotNull(multiResult); + assertEquals(5, multiResult.getTotalElements().intValue()); + + // filter by device type + name filter + EntityDataQuery nameFilterQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1, type2)) + .deviceNameFilter(QUERY_TEST_PREFIX + "temp_" + ts)) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData nameResult = client.findEntityDataByQuery(nameFilterQuery); + assertNotNull(nameResult); + assertEquals(3, nameResult.getTotalElements().intValue()); + } + + @Test + public void testFindByEntityNameFilter() throws Exception { + long ts = System.currentTimeMillis(); + String prefix = QUERY_TEST_PREFIX + "named_" + ts; + + for (int i = 0; i < 4; i++) { + Device d = new Device(); + d.setName(prefix + "_" + i); + d.setType("default"); + client.saveDevice(d, null, null, null, null); + } + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(prefix)) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(4, result.getTotalElements().intValue()); + assertFalse(result.getHasNext()); + } + + @Test + public void testFindByEntityListFilter() throws Exception { + long ts = System.currentTimeMillis(); + + Device d1 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_1").type("default"), null, null, null, null); + Device d2 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_2").type("default"), null, null, null, null); + client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_3").type("default"), null, null, null, null); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityListFilter() + .entityType(EntityType.DEVICE) + .entityList(List.of( + d1.getId().getId().toString(), + d2.getId().getId().toString()))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(2, result.getTotalElements().intValue()); + + List returnedIds = result.getData().stream() + .map(e -> e.getEntityId().getId().toString()) + .collect(Collectors.toList()); + assertTrue(returnedIds.contains(d1.getId().getId().toString())); + assertTrue(returnedIds.contains(d2.getId().getId().toString())); + } + + @Test + public void testFindBySingleEntityFilter() throws Exception { + long ts = System.currentTimeMillis(); + Device device = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "single_" + ts).type("default"), null, null, null, null); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new SingleEntityFilter() + .singleEntity(new AliasEntityId() + .id(device.getId().getId()) + .entityType(EntityType.DEVICE))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(1, result.getTotalElements().intValue()); + assertEquals(device.getId().getId().toString(), + result.getData().get(0).getEntityId().getId().toString()); + } + + @Test + public void testFindByAssetTypeFilter() throws Exception { + long ts = System.currentTimeMillis(); + String assetType = "building"; + + for (int i = 0; i < 3; i++) { + Asset a = new Asset(); + a.setName(QUERY_TEST_PREFIX + "asset_" + ts + "_" + i); + a.setType(assetType); + client.saveAsset(a, null, null, null); + } + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new AssetTypeFilter() + .assetTypes(List.of(assetType))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(3, result.getTotalElements().intValue()); + } + + @Test + public void testFindWithKeyFilter() throws Exception { + long ts = System.currentTimeMillis(); + String matchName = QUERY_TEST_PREFIX + "kf_match_" + ts; + String noMatchName = QUERY_TEST_PREFIX + "kf_other_" + ts; + + client.saveDevice(new Device().name(matchName).type("default"), null, null, null, null); + client.saveDevice(new Device().name(noMatchName).type("default"), null, null, null, null); + + KeyFilter nameKeyFilter = new KeyFilter() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .valueType(EntityKeyValueType.STRING) + .predicate(new StringFilterPredicate() + .operation(StringOperation.CONTAINS) + .value(new FilterPredicateValueString().defaultValue("kf_match")) + .ignoreCase(true)); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(QUERY_TEST_PREFIX + "kf_")) + .addKeyFiltersItem(nameKeyFilter) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(1, result.getTotalElements().intValue()); + } + + @Test + public void testFindWithPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "page_" + ts + "_" + i); + d.setType("default"); + client.saveDevice(d, null, null, null, null); + } + + EntityDataPageLink smallPage = new EntityDataPageLink() + .pageSize(2) + .page(0) + .sortOrder(new EntityDataSortOrder() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .direction(Direction.ASC)); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(QUERY_TEST_PREFIX + "page_" + ts)) + .pageLink(smallPage) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + // first page + PageDataEntityData page1 = client.findEntityDataByQuery(query); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + // last page + smallPage.setPage(2); + PageDataEntityData lastPage = client.findEntityDataByQuery(query); + assertNotNull(lastPage); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java new file mode 100644 index 0000000000..1792ba9ff1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java @@ -0,0 +1,182 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntityRelation; +import org.thingsboard.client.model.EntityRelationInfo; +import org.thingsboard.client.model.EntityRelationsQuery; +import org.thingsboard.client.model.EntitySearchDirection; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.RelationEntityTypeFilter; +import org.thingsboard.client.model.RelationTypeGroup; +import org.thingsboard.client.model.RelationsSearchParameters; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityRelationApiClientTest extends AbstractApiClientTest { + + @Test + public void testEntityRelationLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create assets and devices to relate + Asset building = new Asset(); + building.setName(TEST_PREFIX + "Building_" + timestamp); + building.setType("building"); + building = client.saveAsset(building, null, null, null); + + Asset floor = new Asset(); + floor.setName(TEST_PREFIX + "Floor_" + timestamp); + floor.setType("floor"); + floor = client.saveAsset(floor, null, null, null); + + Device device1 = new Device(); + device1.setName(TEST_PREFIX + "Sensor_" + timestamp + "_1"); + device1.setType("sensor"); + device1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName(TEST_PREFIX + "Sensor_" + timestamp + "_2"); + device2.setType("sensor"); + device2 = client.saveDevice(device2, null, null, null, null); + + Device device3 = new Device(); + device3.setName(TEST_PREFIX + "Sensor_" + timestamp + "_3"); + device3.setType("sensor"); + device3 = client.saveDevice(device3, null, null, null, null); + + // create relations: building -> Contains -> floor, floor -> Contains -> device1/device2/device3 + EntityRelation buildingToFloor = new EntityRelation(); + buildingToFloor.setFrom(building.getId()); + buildingToFloor.setTo(floor.getId()); + buildingToFloor.setType("Contains"); + buildingToFloor.setTypeGroup(RelationTypeGroup.COMMON); + EntityRelation savedRelation = client.saveRelation(buildingToFloor); + assertNotNull(savedRelation); + assertEquals("Contains", savedRelation.getType()); + + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device1.getId()) + .type("Contains") + .typeGroup(RelationTypeGroup.COMMON)); + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device2.getId()) + .type("Contains").typeGroup(RelationTypeGroup.COMMON)); + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device3.getId()) + .type("Manages") + .typeGroup(RelationTypeGroup.COMMON)); + + // get specific relation + EntityRelation fetched = client.getRelation( + building.getId().getId().toString(), "ASSET", + "Contains", + floor.getId().getId().toString(), "ASSET", + RelationTypeGroup.COMMON.getValue()); + assertNotNull(fetched); + assertEquals("Contains", fetched.getType()); + + // find all relations from floor + List fromFloor = client.findEntityRelationsByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(3, fromFloor.size()); + + // find relations from floor with type filter "Contains" + List containsFromFloor = client.findEntityRelationsByFromAndRelationType("ASSET", + floor.getId().getId().toString(), "Contains", RelationTypeGroup.COMMON.getValue()); + assertEquals(2, containsFromFloor.size()); + + // find relations to device1 + List toDevice1 = client.findEntityRelationsByTo("DEVICE", + device1.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(1, toDevice1.size()); + assertEquals("Contains", toDevice1.get(0).getType()); + + // find relations to device3 with type filter "Manages" + List managesToDevice3 = client.findEntityRelationsByToAndRelationType("DEVICE", + device3.getId().getId().toString(), "Manages", RelationTypeGroup.COMMON.getValue()); + assertEquals(1, managesToDevice3.size()); + + // find info by from (includes entity names) + List infoFromFloor = client.findEntityRelationInfosByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(3, infoFromFloor.size()); + Device finalDevice = device1; + assertTrue(infoFromFloor.stream().anyMatch(info -> + finalDevice.getName().equals(info.getToName()))); + + // find info by to + List infoToDevice2 = client.findEntityRelationInfosByTo("DEVICE", + device2.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(1, infoToDevice2.size()); + assertEquals(floor.getName(), infoToDevice2.get(0).getFromName()); + + // find by query - search from building, direction FROM, max 2 levels + RelationsSearchParameters params = new RelationsSearchParameters(); + params.setRootId(building.getId().getId()); + params.setRootType(EntityType.ASSET); + params.setDirection(EntitySearchDirection.FROM); + params.setRelationTypeGroup(RelationTypeGroup.COMMON); + params.setMaxLevel(2); + + RelationEntityTypeFilter filter = new RelationEntityTypeFilter(); + filter.setRelationType("Contains"); + filter.setEntityTypes(List.of(EntityType.ASSET, EntityType.DEVICE)); + + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(params); + query.setFilters(List.of(filter)); + + List queryResult = client.findEntityRelationsByQuery(query); + assertTrue(queryResult.size() >= 3); + + // find info by query + List infoQueryResult = client.findEntityRelationInfosByQuery(query); + assertTrue(infoQueryResult.size() >= 3); + + // delete single relation + client.deleteRelation( + floor.getId().getId().toString(), "ASSET", + "Manages", + device3.getId().getId().toString(), "DEVICE", + RelationTypeGroup.COMMON.getValue()); + + // verify deletion + List afterDelete = client.findEntityRelationsByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(2, afterDelete.size()); + + // delete all relations for building + client.deleteRelations(building.getId().getId().toString(), "ASSET"); + + List afterDeleteAll = client.findEntityRelationsByFrom("ASSET", + building.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(0, afterDeleteAll.size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java new file mode 100644 index 0000000000..60c0e86662 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java @@ -0,0 +1,267 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AttributesEntityView; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntitySubtype; +import org.thingsboard.client.model.EntityView; +import org.thingsboard.client.model.EntityViewInfo; +import org.thingsboard.client.model.PageDataEntityView; +import org.thingsboard.client.model.PageDataEntityViewInfo; +import org.thingsboard.client.model.TelemetryEntityView; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityViewApiClientTest extends AbstractApiClientTest { + + private static final String EV_PREFIX = "EvTest_"; + + @Test + public void testSaveAndGetEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + EntityView ev = new EntityView(); + ev.setName(EV_PREFIX + "save_" + ts); + ev.setType("testType"); + ev.setEntityId(device.getId()); + ev.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature", "humidity")) + .attributes(new AttributesEntityView() + .cs(List.of("firmware")) + .ss(List.of("active")) + .sh(List.of()))); + ev.setStartTimeMs(1000L); + ev.setEndTimeMs(2000L); + + EntityView saved = client.saveEntityView(ev, null, null, null); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(ev.getName(), saved.getName()); + assertEquals("testType", saved.getType()); + assertEquals(device.getId().getId(), saved.getEntityId().getId()); + assertEquals(List.of("temperature", "humidity"), saved.getKeys().getTimeseries()); + assertEquals(1000L, saved.getStartTimeMs().longValue()); + assertEquals(2000L, saved.getEndTimeMs().longValue()); + + // get by id + String evId = saved.getId().getId().toString(); + EntityView fetched = client.getEntityViewById(evId); + assertNotNull(fetched); + assertEquals(saved.getName(), fetched.getName()); + assertEquals(saved.getType(), fetched.getType()); + assertEquals(saved.getEntityId().getId(), fetched.getEntityId().getId()); + } + + @Test + public void testGetEntityViewInfoById() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "info_" + ts, "infoType", device); + + EntityViewInfo info = client.getEntityViewInfoById(saved.getId().getId().toString()); + assertNotNull(info); + assertEquals(saved.getName(), info.getName()); + assertEquals("infoType", info.getType()); + assertNotNull(info.getEntityId()); + } + + @Test + public void testUpdateEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "update_" + ts, "default", device); + + saved.setName(EV_PREFIX + "updated_" + ts); + saved.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature", "pressure")) + .attributes(new AttributesEntityView() + .cs(List.of()) + .ss(List.of()) + .sh(List.of()))); + + EntityView updated = client.saveEntityView(saved, null, null, null); + assertEquals(EV_PREFIX + "updated_" + ts, updated.getName()); + assertEquals(List.of("temperature", "pressure"), updated.getKeys().getTimeseries()); + assertEquals(saved.getId().getId(), updated.getId().getId()); + } + + @Test + public void testDeleteEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "delete_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + client.getEntityViewById(evId); + + client.deleteEntityView(evId); + + assertReturns404(() -> client.getEntityViewById(evId)); + } + + @Test + public void testGetTenantEntityViews() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + for (int i = 0; i < 3; i++) { + createEntityView(EV_PREFIX + "tenant_" + ts + "_" + i, "tenantViewType", device); + } + + PageDataEntityView page = client.getTenantEntityViews(100, 0, null, EV_PREFIX + "tenant_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (EntityView ev : page.getData()) { + assertTrue(ev.getName().startsWith(EV_PREFIX + "tenant_" + ts)); + } + } + + @Test + public void testGetTenantEntityViewInfos() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + createEntityView(EV_PREFIX + "tinfo_" + ts, "default", device); + + PageDataEntityViewInfo page = client.getTenantEntityViewInfos(100, 0, null, EV_PREFIX + "tinfo_" + ts, null, null); + assertNotNull(page); + assertEquals(1, page.getTotalElements().intValue()); + assertEquals(EV_PREFIX + "tinfo_" + ts, page.getData().get(0).getName()); + } + + @Test + public void testAssignAndUnassignEntityViewToCustomer() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "assign_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + String customerId = savedClientCustomer.getId().getId().toString(); + + // assign to customer + EntityView assigned = client.assignEntityViewToCustomer(customerId, evId); + assertNotNull(assigned); + assertEquals(savedClientCustomer.getId().getId(), assigned.getCustomerId().getId()); + + // verify in customer entity views + PageDataEntityView customerViews = client.getCustomerEntityViews( + customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null); + assertEquals(1, customerViews.getTotalElements().intValue()); + assertEquals(saved.getName(), customerViews.getData().get(0).getName()); + + // unassign from customer + EntityView unassigned = client.unassignEntityViewFromCustomer(evId); + assertNotNull(unassigned); + + PageDataEntityView afterUnassign = client.getCustomerEntityViews( + customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null); + assertEquals(0, afterUnassign.getTotalElements().intValue()); + } + + @Test + public void testGetCustomerEntityViewInfos() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "cinfo_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + String customerId = savedClientCustomer.getId().getId().toString(); + + client.assignEntityViewToCustomer(customerId, evId); + + PageDataEntityViewInfo infos = client.getCustomerEntityViewInfos( + customerId, 100, 0, null, EV_PREFIX + "cinfo_" + ts, null, null); + assertNotNull(infos); + assertEquals(1, infos.getTotalElements().intValue()); + assertEquals(saved.getName(), infos.getData().get(0).getName()); + } + + @Test + public void testGetEntityViewTypes() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + createEntityView(EV_PREFIX + "types_" + ts, "uniqueEvType_" + ts, device); + + List types = client.getEntityViewTypes(); + assertNotNull(types); + assertFalse(types.isEmpty()); + + List typeNames = types.stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + assertTrue(typeNames.contains("uniqueEvType_" + ts)); + } + + @Test + public void testGetEntityViewById_notFound() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getEntityViewById(nonExistentId)); + } + + @Test + public void testGetTenantEntityViewsPagination() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + for (int i = 0; i < 5; i++) { + createEntityView(EV_PREFIX + "paged_" + ts + "_" + i, "default", device); + } + + PageDataEntityView page1 = client.getTenantEntityViews(2, 0, null, EV_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataEntityView lastPage = client.getTenantEntityViews(2, 2, null, EV_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + private Device createTestDevice(String suffix) throws Exception { + Device device = new Device(); + device.setName(EV_PREFIX + "device_" + suffix); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + + private EntityView createEntityView(String name, String type, Device device) throws Exception { + EntityView ev = new EntityView(); + ev.setName(name); + ev.setType(type); + ev.setEntityId(device.getId()); + ev.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature")) + .attributes(new AttributesEntityView() + .cs(List.of()) + .ss(List.of()) + .sh(List.of()))); + return client.saveEntityView(ev, null, null, null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java new file mode 100644 index 0000000000..fd448eaee1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.MobileApp; +import org.thingsboard.client.model.MobileAppBundle; +import org.thingsboard.client.model.MobileAppBundleInfo; +import org.thingsboard.client.model.MobileAppStatus; +import org.thingsboard.client.model.PageDataMobileApp; +import org.thingsboard.client.model.PageDataMobileAppBundleInfo; +import org.thingsboard.client.model.PlatformType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class MobileAppApiClientTest extends AbstractApiClientTest { + + @Test + public void testMobileAppLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdApps = new ArrayList<>(); + + // create 3 Android apps + for (int i = 0; i < 3; i++) { + MobileApp app = new MobileApp(); + app.setPkgName("com.test.android." + timestamp + "." + i); + app.setTitle(TEST_PREFIX + "AndroidApp_" + timestamp + "_" + i); + app.setAppSecret("secret_android_" + timestamp + "_" + i); + app.setPlatformType(PlatformType.ANDROID); + app.setStatus(MobileAppStatus.DRAFT); + + MobileApp created = client.saveMobileApp(app); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(app.getPkgName(), created.getPkgName()); + assertEquals(PlatformType.ANDROID, created.getPlatformType()); + assertEquals(MobileAppStatus.DRAFT, created.getStatus()); + + createdApps.add(created); + } + + // create 2 iOS apps + for (int i = 0; i < 2; i++) { + MobileApp app = new MobileApp(); + app.setPkgName("com.test.ios." + timestamp + "." + i); + app.setTitle(TEST_PREFIX + "IosApp_" + timestamp + "_" + i); + app.setAppSecret("secret_ios_" + timestamp + "_" + i); + app.setPlatformType(PlatformType.IOS); + app.setStatus(MobileAppStatus.DRAFT); + + MobileApp created = client.saveMobileApp(app); + assertNotNull(created); + createdApps.add(created); + } + + // list all tenant mobile apps + PageDataMobileApp allApps = client.getTenantMobileApps(100, 0, null, + null, null, null); + assertNotNull(allApps); + assertEquals(5, allApps.getData().size()); + + // list with platform type filter + PageDataMobileApp androidApps = client.getTenantMobileApps(100, 0, PlatformType.ANDROID, + null, null, null); + assertEquals(3, androidApps.getData().size()); + + PageDataMobileApp iosApps = client.getTenantMobileApps(100, 0, PlatformType.IOS, + null, null, null); + assertEquals(2, iosApps.getData().size()); + + // get mobile app by id + MobileApp searchApp = createdApps.get(1); + MobileApp fetchedApp = client.getMobileAppById(searchApp.getId().getId()); + assertEquals(searchApp.getPkgName(), fetchedApp.getPkgName()); + assertEquals(searchApp.getTitle(), fetchedApp.getTitle()); + assertEquals(searchApp.getPlatformType(), fetchedApp.getPlatformType()); + + // update mobile app + MobileApp appToUpdate = createdApps.get(2); + appToUpdate.setTitle(appToUpdate.getTitle() + "_updated"); + MobileApp updatedApp = client.saveMobileApp(appToUpdate); + assertEquals(appToUpdate.getTitle(), updatedApp.getTitle()); + + // create mobile app bundle with android and ios apps + MobileAppBundle bundle = new MobileAppBundle(); + bundle.setTitle(TEST_PREFIX + "Bundle_" + timestamp); + bundle.setDescription("Test bundle"); + bundle.setAndroidAppId(createdApps.get(0).getId()); + bundle.setIosAppId(createdApps.get(3).getId()); + bundle.setOauth2Enabled(false); + + MobileAppBundle savedBundle = client.saveMobileAppBundle(bundle, null); + assertNotNull(savedBundle); + assertNotNull(savedBundle.getId()); + assertEquals(bundle.getTitle(), savedBundle.getTitle()); + + // get bundle info by id + MobileAppBundleInfo bundleInfo = client.getMobileAppBundleInfoById(savedBundle.getId().getId()); + assertEquals(savedBundle.getTitle(), bundleInfo.getTitle()); + assertEquals("Test bundle", bundleInfo.getDescription()); + assertNotNull(bundleInfo.getAndroidPkgName()); + assertNotNull(bundleInfo.getIosPkgName()); + + // list tenant bundles + PageDataMobileAppBundleInfo bundles = client.getTenantMobileAppBundleInfos(100, 0, + TEST_PREFIX + "Bundle_" + timestamp, null, null); + assertEquals(1, bundles.getData().size()); + + // update bundle + savedBundle.setDescription("Updated description"); + MobileAppBundle updatedBundle = client.saveMobileAppBundle(savedBundle, null); + assertEquals("Updated description", updatedBundle.getDescription()); + + // delete bundle + client.deleteMobileAppBundle(savedBundle.getId().getId()); + + // verify bundle deletion + assertReturns404(() -> + client.getMobileAppBundleInfoById(savedBundle.getId().getId()) + ); + + // delete mobile app + UUID appToDeleteId = createdApps.get(0).getId().getId(); + client.deleteMobileApp(appToDeleteId); + + // verify app deletion + assertReturns404(() -> + client.getMobileAppById(appToDeleteId) + ); + + PageDataMobileApp appsAfterDelete = client.getTenantMobileApps(100, 0, null, + null, null, null); + assertEquals(4, appsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java new file mode 100644 index 0000000000..026b8784ef --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java @@ -0,0 +1,278 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.EntityActionNotificationRuleTriggerConfig; +import org.thingsboard.client.model.EntityActionRecipientsConfig; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.NotificationDeliveryMethod; +import org.thingsboard.client.model.NotificationRequest; +import org.thingsboard.client.model.NotificationRequestInfo; +import org.thingsboard.client.model.NotificationRule; +import org.thingsboard.client.model.NotificationRuleInfo; +import org.thingsboard.client.model.NotificationRuleTriggerType; +import org.thingsboard.client.model.NotificationSettings; +import org.thingsboard.client.model.NotificationTarget; +import org.thingsboard.client.model.NotificationTemplate; +import org.thingsboard.client.model.NotificationTemplateConfig; +import org.thingsboard.client.model.NotificationType; +import org.thingsboard.client.model.PageDataNotification; +import org.thingsboard.client.model.PageDataNotificationRequestInfo; +import org.thingsboard.client.model.PageDataNotificationRuleInfo; +import org.thingsboard.client.model.PageDataNotificationTarget; +import org.thingsboard.client.model.PageDataNotificationTemplate; +import org.thingsboard.client.model.PlatformUsersNotificationTargetConfig; +import org.thingsboard.client.model.TenantAdministratorsFilter; +import org.thingsboard.client.model.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class NotificationApiClientTest extends AbstractApiClientTest { + + @Test + public void testNotificationLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // === 1. Notification Target CRUD === + + // Create target + TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter(); + PlatformUsersNotificationTargetConfig targetConfig = + new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter); + NotificationTarget target = + new NotificationTarget() + .name("Test Target " + timestamp) + ._configuration(targetConfig); + + NotificationTarget savedTarget = client.saveNotificationTarget(target); + assertNotNull(savedTarget); + assertNotNull(savedTarget.getId()); + assertEquals("Test Target " + timestamp, savedTarget.getName()); + + // Get target by ID + NotificationTarget fetchedTarget = + client.getNotificationTargetById(savedTarget.getId().getId()); + assertEquals(savedTarget.getName(), fetchedTarget.getName()); + + // List targets + PageDataNotificationTarget targetsPage = + client.getNotificationTargets(100, 0, null, null, null); + assertNotNull(targetsPage); + assertNotNull(targetsPage.getData()); + assertTrue( + targetsPage.getData().stream() + .anyMatch(t -> t.getName().equals(savedTarget.getName()))); + + // Update target + savedTarget.setName("Updated Target " + timestamp); + NotificationTarget updatedTarget = client.saveNotificationTarget(savedTarget); + assertEquals("Updated Target " + timestamp, updatedTarget.getName()); + + // === 2. Notification Template CRUD === + + // Create template + WebDeliveryMethodNotificationTemplate webTemplate = + new WebDeliveryMethodNotificationTemplate() + .subject("Test Subject") + .body("Test notification body") + .enabled(true); + NotificationTemplateConfig templateConfig = + new NotificationTemplateConfig() + .putDeliveryMethodsTemplatesItem("WEB", webTemplate); + NotificationTemplate template = + new NotificationTemplate() + .name("Test Template " + timestamp) + .notificationType(NotificationType.GENERAL) + ._configuration(templateConfig); + + NotificationTemplate savedTemplate = client.saveNotificationTemplate(template); + assertNotNull(savedTemplate); + assertNotNull(savedTemplate.getId()); + assertEquals("Test Template " + timestamp, savedTemplate.getName()); + + // Get template by ID + NotificationTemplate fetchedTemplate = + client.getNotificationTemplateById(savedTemplate.getId().getId()); + assertEquals(savedTemplate.getName(), fetchedTemplate.getName()); + assertEquals(NotificationType.GENERAL, fetchedTemplate.getNotificationType()); + + // List templates + PageDataNotificationTemplate templatesPage = + client.getNotificationTemplates(100, 0, null, null, null, null); + assertNotNull(templatesPage); + assertTrue( + templatesPage.getData().stream() + .anyMatch(t -> t.getName().equals(savedTemplate.getName()))); + + // Update template + savedTemplate.setName("Updated Template " + timestamp); + NotificationTemplate updatedTemplate = client.saveNotificationTemplate(savedTemplate); + assertEquals("Updated Template " + timestamp, updatedTemplate.getName()); + + // === 3. Send notification & read notifications === + + // Send notification request + NotificationRequest request = + new NotificationRequest() + .targets(List.of(savedTarget.getId().getId())) + .templateId(savedTemplate.getId()); + NotificationRequest sentRequest = client.createNotificationRequest(request); + assertNotNull(sentRequest); + assertNotNull(sentRequest.getId()); + + // Get request by ID + NotificationRequestInfo fetchedRequest = + client.getNotificationRequestById(sentRequest.getId().getId()); + assertNotNull(fetchedRequest); + + // List requests + PageDataNotificationRequestInfo requestsPage = + client.getNotificationRequests(100, 0, null, null, null); + assertNotNull(requestsPage); + assertFalse(requestsPage.getData().isEmpty()); + + // Get notifications for current user + PageDataNotification notificationsPage = + client.getNotifications(100, 0, null, null, null, null, null); + assertNotNull(notificationsPage); + assertFalse(notificationsPage.getData().isEmpty()); + + // Get unread count + Integer unreadCount = client.getUnreadNotificationsCount("WEB"); + assertNotNull(unreadCount); + assertTrue("Expected at least one unread notification", unreadCount > 0); + + // Mark single notification as read + client.markNotificationAsRead( + notificationsPage.getData().get(0).getId().getId()); + + // Mark all as read + client.markAllNotificationsAsRead(null); + Integer unreadAfterMarkAll = client.getUnreadNotificationsCount(null); + assertEquals("Expected no unread notifications after marking all as read", 0, unreadAfterMarkAll.intValue()); + + // === 4. Notification Settings === + + NotificationSettings settings = client.getNotificationSettings(); + assertNotNull(settings); + + List deliveryMethods = client.getAvailableDeliveryMethods(); + assertNotNull(deliveryMethods); + assertTrue(deliveryMethods.contains(NotificationDeliveryMethod.WEB)); + + // === 5. Cleanup === + + // Delete notification request + client.deleteNotificationRequest(sentRequest.getId().getId()); + assertReturns404(() -> client.getNotificationRequestById(sentRequest.getId().getId())); + + // Delete template + client.deleteNotificationTemplateById(savedTemplate.getId().getId()); + assertReturns404(() -> client.getNotificationTemplateById(savedTemplate.getId().getId())); + + // Delete target + client.deleteNotificationTargetById(savedTarget.getId().getId()); + assertReturns404(() -> client.getNotificationTargetById(savedTarget.getId().getId())); + } + + @Test + public void testNotificationRuleLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // Create a target for the rule recipients + TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter(); + PlatformUsersNotificationTargetConfig targetConfig = + new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter); + NotificationTarget target = + new NotificationTarget() + .name("Rule Test Target " + timestamp) + ._configuration(targetConfig); + NotificationTarget savedTarget = client.saveNotificationTarget(target); + + // Create a template of type ENTITY_ACTION + WebDeliveryMethodNotificationTemplate webTemplate = + new WebDeliveryMethodNotificationTemplate() + .subject("Entity action: ${entityType}") + .body("Entity ${entityName} was ${actionType}") + .enabled(true); + NotificationTemplateConfig templateConfig = + new NotificationTemplateConfig() + .putDeliveryMethodsTemplatesItem("WEB", webTemplate); + NotificationTemplate template = + new NotificationTemplate() + .name("Rule Test Template " + timestamp) + .notificationType(NotificationType.ENTITY_ACTION) + ._configuration(templateConfig); + NotificationTemplate savedTemplate = client.saveNotificationTemplate(template); + + // Build trigger config: fire on DEVICE create/update + EntityActionNotificationRuleTriggerConfig triggerConfig = + new EntityActionNotificationRuleTriggerConfig() + .addEntityTypesItem(EntityType.DEVICE) + .created(true) + .updated(true) + .deleted(false); + + // Build recipients config + EntityActionRecipientsConfig recipientsConfig = new EntityActionRecipientsConfig() + .addTargetsItem(savedTarget.getId().getId()); + + // saveNotificationRule - create + NotificationRule rule = new NotificationRule() + .name("Test Rule " + timestamp) + .enabled(true) + .templateId(savedTemplate.getId()) + .triggerType(NotificationRuleTriggerType.ENTITY_ACTION) + .triggerConfig(triggerConfig) + .recipientsConfig(recipientsConfig); + + NotificationRule savedRule = client.saveNotificationRule(rule); + assertNotNull(savedRule); + assertNotNull(savedRule.getId()); + assertEquals("Test Rule " + timestamp, savedRule.getName()); + assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, savedRule.getTriggerType()); + assertEquals(Boolean.TRUE, savedRule.getEnabled()); + + // getNotificationRuleById + NotificationRuleInfo fetchedRule = client.getNotificationRuleById(savedRule.getId().getId()); + assertNotNull(fetchedRule); + assertEquals(savedRule.getName(), fetchedRule.getName()); + assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, fetchedRule.getTriggerType()); + + // getNotificationRules - verify it appears in the list + PageDataNotificationRuleInfo rulesPage = client.getNotificationRules(100, 0, null, null, null); + assertNotNull(rulesPage); + assertTrue(rulesPage.getData().stream() + .anyMatch(r -> r.getId().getId().equals(savedRule.getId().getId()))); + + // deleteNotificationRule + client.deleteNotificationRule(savedRule.getId().getId()); + assertReturns404(() -> client.getNotificationRuleById(savedRule.getId().getId())); + + // Cleanup + client.deleteNotificationTemplateById(savedTemplate.getId().getId()); + client.deleteNotificationTargetById(savedTarget.getId().getId()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java new file mode 100644 index 0000000000..2099f405f0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java @@ -0,0 +1,138 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.MapperType; +import org.thingsboard.client.model.OAuth2BasicMapperConfig; +import org.thingsboard.client.model.OAuth2Client; +import org.thingsboard.client.model.OAuth2ClientInfo; +import org.thingsboard.client.model.OAuth2MapperConfig; +import org.thingsboard.client.model.PageDataOAuth2ClientInfo; +import org.thingsboard.client.model.PlatformType; +import org.thingsboard.client.model.TenantNameStrategyType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class Oauth2ApiClientTest extends AbstractApiClientTest { + + private OAuth2Client createOAuth2Client(String title, String clientId, String clientSecret) { + OAuth2BasicMapperConfig basicConfig = new OAuth2BasicMapperConfig(); + basicConfig.setEmailAttributeKey("email"); + basicConfig.setFirstNameAttributeKey("given_name"); + basicConfig.setLastNameAttributeKey("family_name"); + basicConfig.setTenantNameStrategy(TenantNameStrategyType.DOMAIN); + + OAuth2MapperConfig mapperConfig = new OAuth2MapperConfig(); + mapperConfig.setType(MapperType.BASIC); + mapperConfig.setAllowUserCreation(true); + mapperConfig.setActivateUser(false); + mapperConfig.setBasic(basicConfig); + + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTitle(title); + oAuth2Client.setClientId(clientId); + oAuth2Client.setClientSecret(clientSecret); + oAuth2Client.setAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth"); + oAuth2Client.setAccessTokenUri("https://oauth2.googleapis.com/token"); + oAuth2Client.setScope(List.of("openid", "email", "profile")); + oAuth2Client.setUserInfoUri("https://openidconnect.googleapis.com/v1/userinfo"); + oAuth2Client.setUserNameAttributeName("email"); + oAuth2Client.setClientAuthenticationMethod("POST"); + oAuth2Client.setLoginButtonLabel(title); + oAuth2Client.setMapperConfig(mapperConfig); + oAuth2Client.setPlatforms(List.of(PlatformType.WEB)); + + return oAuth2Client; + } + + @Test + public void testOAuth2ClientLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdClients = new ArrayList<>(); + + // create 5 OAuth2 clients + for (int i = 0; i < 5; i++) { + String title = TEST_PREFIX + "OAuth2_" + timestamp + "_" + i; + OAuth2Client oAuth2Client = createOAuth2Client(title, + "client_id_" + timestamp + "_" + i, + "client_secret_" + timestamp + "_" + i); + + OAuth2Client created = client.saveOAuth2Client(oAuth2Client); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(title, created.getTitle()); + assertEquals("POST", created.getClientAuthenticationMethod()); + assertNotNull(created.getMapperConfig()); + assertEquals(MapperType.BASIC, created.getMapperConfig().getType()); + + createdClients.add(created); + } + + // list tenant OAuth2 client infos + PageDataOAuth2ClientInfo clientInfos = client.findOAuth2ClientInfos(100, 0, + TEST_PREFIX + "OAuth2_" + timestamp, null, null); + assertNotNull(clientInfos); + assertEquals(5, clientInfos.getData().size()); + + // get OAuth2 client by id + OAuth2Client searchClient = createdClients.get(2); + OAuth2Client fetchedClient = client.getOAuth2ClientById(searchClient.getId().getId()); + assertEquals(searchClient.getTitle(), fetchedClient.getTitle()); + assertEquals(searchClient.getClientId(), fetchedClient.getClientId()); + assertEquals(searchClient.getAuthorizationUri(), fetchedClient.getAuthorizationUri()); + assertEquals(3, fetchedClient.getScope().size()); + + // fetch client infos by ids + List idsToFetch = List.of( + createdClients.get(0).getId().getId().toString(), + createdClients.get(1).getId().getId().toString() + ); + List fetchedInfos = client.findTenantOAuth2ClientInfosByIds(idsToFetch); + assertEquals(2, fetchedInfos.size()); + + // update OAuth2 client + OAuth2Client clientToUpdate = client.getOAuth2ClientById(createdClients.get(3).getId().getId()); + clientToUpdate.setTitle(clientToUpdate.getTitle() + "_updated"); + clientToUpdate.setLoginButtonLabel("Updated Login"); + clientToUpdate.setPlatforms(List.of(PlatformType.WEB, PlatformType.ANDROID)); + OAuth2Client updatedClient = client.saveOAuth2Client(clientToUpdate); + assertEquals(clientToUpdate.getTitle(), updatedClient.getTitle()); + assertEquals("Updated Login", updatedClient.getLoginButtonLabel()); + assertEquals(2, updatedClient.getPlatforms().size()); + + // delete OAuth2 client + UUID clientToDeleteId = createdClients.get(0).getId().getId(); + client.deleteOauth2Client(clientToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getOAuth2ClientById(clientToDeleteId) + ); + + PageDataOAuth2ClientInfo clientsAfterDelete = client.findOAuth2ClientInfos(100, 0, + TEST_PREFIX + "OAuth2_" + timestamp, null, null); + assertEquals(4, clientsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java new file mode 100644 index 0000000000..c71f2cb2e3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java @@ -0,0 +1,261 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.ChecksumAlgorithm; +import org.thingsboard.client.model.DeviceProfileId; +import org.thingsboard.client.model.DeviceProfileInfo; +import org.thingsboard.client.model.OtaPackage; +import org.thingsboard.client.model.OtaPackageInfo; +import org.thingsboard.client.model.OtaPackageType; +import org.thingsboard.client.model.PageDataOtaPackageInfo; +import org.thingsboard.client.model.SaveOtaPackageInfoRequest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Files; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class OtaPackageApiClientTest extends AbstractApiClientTest { + + private static final String OTA_PREFIX = "OtaTest_"; + + private DeviceProfileId getDefaultDeviceProfileId() throws Exception { + DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo(); + return (DeviceProfileId) profileInfo.getId(); + } + + private SaveOtaPackageInfoRequest buildOtaPackageInfoRequest( + String title, String version, OtaPackageType type, + DeviceProfileId deviceProfileId, boolean usesUrl, String url) { + SaveOtaPackageInfoRequest request = new SaveOtaPackageInfoRequest(); + request.setTitle(title); + request.setType(type); + request.setUrl(url); + request.setVersion(version); + request.setDeviceProfileId(deviceProfileId); + return request; + } + + private OtaPackageInfo createFirmwareInfo(String suffix) throws Exception { + DeviceProfileId profileId = getDefaultDeviceProfileId(); + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(), + OtaPackageType.FIRMWARE, profileId, false, null); + return client.saveOtaPackageInfo(request); + } + + private OtaPackageInfo createFirmwareWithUrl(String suffix) throws Exception { + DeviceProfileId profileId = getDefaultDeviceProfileId(); + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(), + OtaPackageType.FIRMWARE, profileId, true, "https://example.com/firmware.bin"); + return client.saveOtaPackageInfo(request); + } + + @Test + public void testSaveAndGetOtaPackageInfo() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + String title = OTA_PREFIX + "save_" + ts; + String version = "1.0." + ts; + + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + title, version, OtaPackageType.FIRMWARE, profileId, true, "https://example.com/fw.bin"); + + OtaPackageInfo saved = client.saveOtaPackageInfo(request); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(title, saved.getTitle()); + assertEquals(version, saved.getVersion()); + assertEquals(OtaPackageType.FIRMWARE, saved.getType()); + assertTrue(saved.getUrl().contains("example.com")); + + // get info by id + String pkgId = saved.getId().getId().toString(); + OtaPackageInfo fetched = client.getOtaPackageInfoById(pkgId); + assertNotNull(fetched); + assertEquals(title, fetched.getTitle()); + assertEquals(version, fetched.getVersion()); + } + + @Test + public void testGetOtaPackageById() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("getbyid_" + ts); + + OtaPackage fullPkg = client.getOtaPackageById(saved.getId().getId().toString()); + assertNotNull(fullPkg); + assertEquals(saved.getTitle(), fullPkg.getTitle()); + assertEquals(saved.getVersion(), fullPkg.getVersion()); + } + + @Test + public void testSaveOtaPackageInfoForSoftware() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + String title = OTA_PREFIX + "sw_" + ts; + + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + title, "2.0." + ts, OtaPackageType.SOFTWARE, profileId, true, "https://example.com/sw.bin"); + + OtaPackageInfo saved = client.saveOtaPackageInfo(request); + assertNotNull(saved); + assertEquals(OtaPackageType.SOFTWARE, saved.getType()); + assertEquals(title, saved.getTitle()); + } + + @Test + public void testSaveOtaPackageData() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo info = createFirmwareInfo("data_" + ts); + + File tempFile = Files.createTempFile("ota_test_", ".bin").toFile(); + tempFile.deleteOnExit(); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write("test firmware content " + ts); + } + + OtaPackageInfo updated = client.saveOtaPackageData( + info.getId().getId().toString(), "MD5", tempFile, null); + assertNotNull(updated); + assertTrue(updated.getHasData()); + assertNotNull(updated.getFileName()); + assertNotNull(updated.getDataSize()); + assertTrue(updated.getDataSize() > 0); + assertEquals(ChecksumAlgorithm.MD5, updated.getChecksumAlgorithm()); + } + + @Test + public void testDownloadOtaPackage() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo info = createFirmwareInfo("download_" + ts); + + String content = "downloadable firmware " + ts; + File tempFile = Files.createTempFile("ota_dl_", ".bin").toFile(); + tempFile.deleteOnExit(); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write(content); + } + + client.saveOtaPackageData(info.getId().getId().toString(), "MD5", tempFile, null); + + File downloaded = client.downloadOtaPackage(info.getId().getId().toString()); + assertNotNull(downloaded); + assertTrue(downloaded.length() > 0); + String downloadedContent = Files.readString(downloaded.toPath()); + assertEquals(content, downloadedContent); + } + + @Test + public void testDeleteOtaPackage() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("delete_" + ts); + + String pkgId = saved.getId().getId().toString(); + client.getOtaPackageInfoById(pkgId); + + client.deleteOtaPackage(pkgId); + + assertReturns404(() -> client.getOtaPackageInfoById(pkgId)); + } + + @Test + public void testGetOtaPackages() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 3; i++) { + createFirmwareWithUrl("list_" + ts + "_" + i); + } + + PageDataOtaPackageInfo page = client.getOtaPackages(100, 0, OTA_PREFIX + "list_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (OtaPackageInfo pkg : page.getData()) { + assertTrue(pkg.getTitle().startsWith(OTA_PREFIX + "list_" + ts)); + } + } + + @Test + public void testGetOtaPackagesByDeviceProfileAndType() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + + createFirmwareWithUrl("byprofile_" + ts + "_0"); + createFirmwareWithUrl("byprofile_" + ts + "_1"); + + PageDataOtaPackageInfo page = client.getOtaPackagesByDeviceProfileAndType( + profileId.getId().toString(), "FIRMWARE", 100, 0, + OTA_PREFIX + "byprofile_" + ts, null, null); + assertNotNull(page); + assertEquals(2, page.getTotalElements().intValue()); + } + + @Test + public void testGetOtaPackageInfoById_notFound() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getOtaPackageInfoById(nonExistentId)); + } + + @Test + public void testGetOtaPackagesPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + createFirmwareWithUrl("paged_" + ts + "_" + i); + } + + PageDataOtaPackageInfo page1 = client.getOtaPackages(2, 0, OTA_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataOtaPackageInfo lastPage = client.getOtaPackages(2, 2, OTA_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + @Test + public void testUpdateOtaPackageInfo() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("update_" + ts); + + SaveOtaPackageInfoRequest updateReq = new SaveOtaPackageInfoRequest(); + updateReq.setId(saved.getId()); + updateReq.setTitle(saved.getTitle()); + updateReq.setType(saved.getType()); + updateReq.setVersion(saved.getVersion()); + updateReq.setDeviceProfileId(saved.getDeviceProfileId()); + updateReq.setUrl(saved.getUrl()); + updateReq.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("infoKey", "infoValue")); + + OtaPackageInfo updated = client.saveOtaPackageInfo(updateReq); + assertNotNull(updated); + assertEquals(saved.getId().getId(), updated.getId().getId()); + assertEquals("infoValue", updated.getAdditionalInfo().get("infoKey").asText()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java new file mode 100644 index 0000000000..80719052d1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Device; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.junit.Assert.assertEquals; + +@DaoSqlTest +public class RpcV1ApiClientTest extends AbstractApiClientTest { + + private static final String ONE_WAY_BODY = + "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; + private static final String TWO_WAY_BODY = + "{\"method\":\"getGpio\",\"params\":{\"pin\":7},\"persistent\":true}"; + + @Test + public void testHandleOneWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleOneWayDeviceRPCRequestV1(deviceId, ONE_WAY_BODY); + } catch (ApiException e) { + assertEquals("handleOneWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testHandleTwoWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleTwoWayDeviceRPCRequestV1(deviceId, TWO_WAY_BODY); + } catch (ApiException e) { + assertEquals("handleTwoWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + private Device createNewDevice(String name) throws ApiException { + Device device = new Device(); + device.setName(name); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java new file mode 100644 index 0000000000..02b7e0f0ba --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.Rpc; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class RpcV2ApiClientTest extends AbstractApiClientTest { + + private static final String PERSISTENT_BODY = + "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; + + @Test + public void testHandleOneWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleOneWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY); + } catch (ApiException e) { + assertEquals("handleOneWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testHandleTwoWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleTwoWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY); + } catch (ApiException e) { + assertEquals("handleTwoWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testGetPersistedRpcAndDeleteRpc() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + String rpcId = postPersistentRpcAndGetId(deviceId); + assertNotNull(rpcId); + + Rpc rpc = client.getPersistedRpc(rpcId); + assertNotNull(rpc); + assertNotNull(rpc.getId()); + + client.deleteRpc(rpcId); + + assertReturns404(() -> client.getPersistedRpc(rpcId)); + + client.deleteDevice(deviceId); + } + + @Test + public void testGetPersistedRpcNotFound() { + assertReturns404(() -> client.getPersistedRpc(UUID.randomUUID().toString())); + } + + @Test + public void testGetPersistedRpcByDevice() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + postPersistentRpcAndGetId(deviceId); + + try { + client.getPersistedRpcByDevice(deviceId, 100, 0, null, null, null, null); + } catch (ApiException e) { + assertEquals("getPersistedRpcByDevice got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + private Device createNewDevice(String name) throws ApiException { + Device device = new Device(); + device.setName(name); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + + private String postPersistentRpcAndGetId(String deviceId) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(getBaseUrl() + "/api/plugins/rpc/oneway/" + deviceId)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + client.getToken()) + .POST(HttpRequest.BodyPublishers.ofString(PERSISTENT_BODY)) + .build(); + HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + return OBJECT_MAPPER.readTree(response.body()).get("rpcId").asText(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java new file mode 100644 index 0000000000..8adcecaf24 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.NodeConnectionInfo; +import org.thingsboard.client.model.PageDataRuleChain; +import org.thingsboard.client.model.RuleChain; +import org.thingsboard.client.model.RuleChainMetaData; +import org.thingsboard.client.model.RuleChainType; +import org.thingsboard.client.model.RuleNode; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class RuleChainApiClientTest extends AbstractApiClientTest { + + @Test + public void testRuleChainAndNodeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdChains = new ArrayList<>(); + + // create 5 rule chains + for (int i = 0; i < 5; i++) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(TEST_PREFIX + "RuleChain_" + timestamp + "_" + i); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setDebugMode(false); + + RuleChain created = client.saveRuleChain(ruleChain); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(ruleChain.getName(), created.getName()); + assertEquals(RuleChainType.CORE, created.getType()); + + createdChains.add(created); + } + + // list rule chains with text search + PageDataRuleChain filteredChains = client.getRuleChains(100, 0, null, + TEST_PREFIX + "RuleChain_" + timestamp, null, null); + assertNotNull(filteredChains); + assertEquals(5, filteredChains.getData().size()); + + // get rule chain by id + RuleChain searchChain = createdChains.get(2); + RuleChain fetchedChain = client.getRuleChainById(searchChain.getId().getId().toString()); + assertEquals(searchChain.getName(), fetchedChain.getName()); + assertEquals(searchChain.getType(), fetchedChain.getType()); + + // get metadata (initially has default node) + RuleChainMetaData metadata = client.getRuleChainMetaData(searchChain.getId().getId().toString()); + assertNotNull(metadata); + assertEquals(searchChain.getId().getId(), metadata.getRuleChainId().getId()); + + // save metadata with rule nodes and connections + RuleChainMetaData newMetadata = new RuleChainMetaData(metadata.getRuleChainId()); + newMetadata.setVersion(metadata.getVersion()); + newMetadata.setFirstNodeIndex(0); + + // node 0: message type switch + RuleNode switchNode = new RuleNode(); + switchNode.setName("Message Type Switch"); + switchNode.setType("org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode"); + switchNode.setConfiguration(OBJECT_MAPPER.createObjectNode().put("version", 0)); + switchNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 200).put("layoutY", 150)); + + // node 1: log node for telemetry + RuleNode logNode = new RuleNode(); + logNode.setName("Log Telemetry"); + logNode.setType("org.thingsboard.rule.engine.action.TbLogNode"); + logNode.setConfiguration(OBJECT_MAPPER.createObjectNode() + .put("scriptLang", "TBEL") + .put("jsScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);") + .put("tbelScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);")); + logNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 100)); + + // node 2: save timeseries + RuleNode saveNode = new RuleNode(); + saveNode.setName("Save Timeseries"); + saveNode.setType("org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode"); + saveNode.setConfiguration(OBJECT_MAPPER.createObjectNode() + .put("defaultTTL", 0) + .put("skipLatestPersistence", false) + .put("useServerTs", false)); + saveNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 250)); + + newMetadata.setNodes(List.of(switchNode, logNode, saveNode)); + + // connection: switch -> log (on "Post telemetry") + NodeConnectionInfo conn1 = new NodeConnectionInfo(); + conn1.setFromIndex(0); + conn1.setToIndex(1); + conn1.setType("Post telemetry"); + + // connection: switch -> save timeseries (on "Post telemetry") + NodeConnectionInfo conn2 = new NodeConnectionInfo(); + conn2.setFromIndex(0); + conn2.setToIndex(2); + conn2.setType("Post telemetry"); + + newMetadata.setConnections(List.of(conn1, conn2)); + newMetadata.setRuleChainConnections(List.of()); + + RuleChainMetaData savedMetadata = client.saveRuleChainMetaData(newMetadata, false); + assertNotNull(savedMetadata); + assertEquals(3, savedMetadata.getNodes().size()); + assertEquals(2, savedMetadata.getConnections().size()); + + // verify saved nodes + RuleChainMetaData fetchedMetadata = client.getRuleChainMetaData(searchChain.getId().getId().toString()); + assertEquals(3, fetchedMetadata.getNodes().size()); + assertTrue(fetchedMetadata.getNodes().stream() + .anyMatch(node -> "Log Telemetry".equals(node.getName()))); + assertTrue(fetchedMetadata.getNodes().stream() + .anyMatch(node -> "Save Timeseries".equals(node.getName()))); + + // get output labels + client.getRuleChainOutputLabels(searchChain.getId().getId().toString()); + + // update rule chain + RuleChain chainToUpdate = createdChains.get(3); + chainToUpdate.setName(chainToUpdate.getName() + "_updated"); + chainToUpdate.setDebugMode(true); + RuleChain updatedChain = client.saveRuleChain(chainToUpdate); + assertEquals(chainToUpdate.getName(), updatedChain.getName()); + assertEquals(true, updatedChain.getDebugMode()); + + // delete rule chain + UUID chainToDeleteId = createdChains.get(0).getId().getId(); + client.deleteRuleChain(chainToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getRuleChainById(chainToDeleteId.toString()) + ); + + PageDataRuleChain chainsAfterDelete = client.getRuleChains(100, 0, null, + TEST_PREFIX + "RuleChain_" + timestamp, null, null); + assertEquals(4, chainsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java new file mode 100644 index 0000000000..2a06549554 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.PageDataTbResourceInfo; +import org.thingsboard.client.model.ResourceExportData; +import org.thingsboard.client.model.TbImageDeleteResult; +import org.thingsboard.client.model.TbResourceInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TbImageApiClientTest extends AbstractApiClientTest { + + private File createTempImage(String name, Color color) throws IOException { + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + Graphics2D g = img.createGraphics(); + g.setColor(color); + g.fillRect(0, 0, 100, 100); + g.dispose(); + + File tempFile = File.createTempFile(name, ".png"); + tempFile.deleteOnExit(); + ImageIO.write(img, "png", tempFile); + return tempFile; + } + + @Test + public void testImageLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdImages = new ArrayList<>(); + Color[] colors = {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.CYAN}; + + // upload 5 images + for (int i = 0; i < 5; i++) { + String title = TEST_PREFIX + "Image_" + timestamp + "_" + i; + File imageFile = createTempImage("test_image_" + i, colors[i]); + + TbResourceInfo uploaded = client.uploadImage(imageFile, title, null); + assertNotNull(uploaded); + assertNotNull(uploaded.getResourceKey()); + assertEquals(title, uploaded.getTitle()); + assertNotNull(uploaded.getLink()); + + createdImages.add(uploaded); + } + + // list images with text search + PageDataTbResourceInfo filteredImages = client.getImages(100, 0, null, false, + TEST_PREFIX + "Image_" + timestamp, null, null); + assertNotNull(filteredImages); + assertEquals(5, filteredImages.getData().size()); + + // get image info by type and key + TbResourceInfo searchImage = createdImages.get(2); + TbResourceInfo fetchedInfo = client.getImageInfo("tenant", searchImage.getResourceKey()); + assertEquals(searchImage.getTitle(), fetchedInfo.getTitle()); + assertEquals(searchImage.getResourceKey(), fetchedInfo.getResourceKey()); + + // download image + File downloadedImage = client.downloadImage("tenant", searchImage.getResourceKey(), null, null); + assertNotNull(downloadedImage); + assertTrue(downloadedImage.exists()); + assertTrue(downloadedImage.length() > 0); + + // download image preview + File preview = client.downloadImagePreview("tenant", searchImage.getResourceKey(), null, null); + assertNotNull(preview); + assertTrue(preview.exists()); + assertTrue(preview.length() > 0); + + // update image file + File updatedImageFile = createTempImage("updated_image", Color.MAGENTA); + TbResourceInfo updatedImage = client.updateImage("tenant", searchImage.getResourceKey(), updatedImageFile); + assertNotNull(updatedImage); + assertEquals(searchImage.getResourceKey(), updatedImage.getResourceKey()); + + // update image info (title) + TbResourceInfo infoToUpdate = client.getImageInfo("tenant", createdImages.get(3).getResourceKey()); + infoToUpdate.setTitle(infoToUpdate.getTitle() + "_updated"); + TbResourceInfo updatedInfo = client.updateImageInfo("tenant", infoToUpdate.getResourceKey(), infoToUpdate); + assertEquals(infoToUpdate.getTitle(), updatedInfo.getTitle()); + + // make image public + TbResourceInfo publicImage = client.updateImagePublicStatus("tenant", + createdImages.get(1).getResourceKey(), true); + assertTrue(publicImage.getPublic()); + assertNotNull(publicImage.getPublicResourceKey()); + assertNotNull(publicImage.getPublicLink()); + + // download public image + File publicDownload = client.downloadPublicImage(publicImage.getPublicResourceKey(), null, null); + assertNotNull(publicDownload); + assertTrue(publicDownload.exists()); + assertTrue(publicDownload.length() > 0); + + // make image private again + TbResourceInfo privateImage = client.updateImagePublicStatus("tenant", + createdImages.get(1).getResourceKey(), false); + assertEquals(false, privateImage.getPublic()); + + // export image + ResourceExportData exportData = client.exportImage("tenant", createdImages.get(4).getResourceKey()); + assertNotNull(exportData); + assertNotNull(exportData.getData()); + assertEquals(createdImages.get(4).getTitle(), exportData.getTitle()); + assertEquals(createdImages.get(4).getResourceKey(), exportData.getResourceKey()); + + // delete image + String keyToDelete = createdImages.get(0).getResourceKey(); + TbImageDeleteResult deleteResult = client.deleteImage("tenant", keyToDelete, false); + assertNotNull(deleteResult); + assertTrue(deleteResult.getSuccess()); + + // verify deletion + assertReturns404(() -> + client.getImageInfo("tenant", keyToDelete) + ); + + PageDataTbResourceInfo imagesAfterDelete = client.getImages(100, 0, null, false, + TEST_PREFIX + "Image_" + timestamp, null, null); + assertEquals(4, imagesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java new file mode 100644 index 0000000000..0651ce6cc3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.PageDataTbResourceInfo; +import org.thingsboard.client.model.ResourceType; +import org.thingsboard.client.model.TbResource; +import org.thingsboard.client.model.TbResourceInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.File; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TbResourceApiClientTest extends AbstractApiClientTest { + + @Test + public void testResourceLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdResources = new ArrayList<>(); + + // create 5 JS_MODULE resources + for (int i = 0; i < 5; i++) { + TbResource resource = new TbResource(); + resource.setTitle(TEST_PREFIX + "Resource_" + timestamp + "_" + i); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setResourceKey("test_module_" + timestamp + "_" + i + ".js"); + resource.setFileName("test_module_" + timestamp + "_" + i + ".js"); + + String jsContent = "export default function test" + i + "() { return " + i + "; }"; + resource.setData(Base64.getEncoder().encodeToString(jsContent.getBytes())); + + TbResourceInfo created = client.saveResource(resource); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(resource.getTitle(), created.getTitle()); + assertEquals(ResourceType.JS_MODULE, created.getResourceType()); + + createdResources.add(created); + } + + // get tenant resources, check count + PageDataTbResourceInfo tenantResources = client.getTenantResources(100, 0, null, null, null); + assertNotNull(tenantResources); + assertNotNull(tenantResources.getData()); + int initialSize = tenantResources.getData().size(); + assertTrue("Expected at least 5 resources, but got " + initialSize, initialSize >= 5); + + // find with text search + PageDataTbResourceInfo filteredResources = client.getTenantResources(100, 0, + TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(5, filteredResources.getData().size()); + + // get resources with type filter + PageDataTbResourceInfo jsResources = client.getResources(100, 0, + ResourceType.JS_MODULE.getValue(), null, TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(5, jsResources.getData().size()); + + // get resource info by id + TbResourceInfo searchResource = createdResources.get(2); + TbResourceInfo fetchedInfo = client.getResourceInfoById(searchResource.getId().getId().toString()); + assertEquals(searchResource.getTitle(), fetchedInfo.getTitle()); + assertEquals(searchResource.getResourceKey(), fetchedInfo.getResourceKey()); + + // get full resource by id (includes data) + TbResource fullResource = client.getResourceById(searchResource.getId().getId().toString()); + assertNotNull(fullResource); + assertEquals(searchResource.getTitle(), fullResource.getTitle()); + assertNotNull(fullResource.getData()); + + // download resource + File downloadedFile = client.downloadResource(searchResource.getId().getId().toString()); + assertNotNull(downloadedFile); + assertTrue(downloadedFile.exists()); + assertTrue(downloadedFile.length() > 0); + + // get resources by list of ids + List idsToFetch = List.of( + createdResources.get(0).getId().getId().toString(), + createdResources.get(1).getId().getId().toString() + ); + List resourceList = client.getSystemOrTenantResourcesByIds(idsToFetch); + assertEquals(2, resourceList.size()); + + // update resource + TbResource resourceToUpdate = client.getResourceById(createdResources.get(3).getId().getId().toString()); + resourceToUpdate.setTitle(resourceToUpdate.getTitle() + "_updated"); + String updatedContent = "export default function updated() { return 42; }"; + resourceToUpdate.setData(Base64.getEncoder().encodeToString(updatedContent.getBytes())); + TbResourceInfo updatedResource = client.saveResource(resourceToUpdate); + assertEquals(resourceToUpdate.getTitle(), updatedResource.getTitle()); + + // delete resource + UUID resourceToDeleteId = createdResources.get(0).getId().getId(); + client.deleteResource(resourceToDeleteId.toString(), false); + + // verify deletion + assertReturns404(() -> + client.getResourceInfoById(resourceToDeleteId.toString()) + ); + + PageDataTbResourceInfo resourcesAfterDelete = client.getTenantResources(100, 0, + TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(4, resourcesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java new file mode 100644 index 0000000000..a2dbe0eda4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AttributeData; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.TsData; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TelemetryApiClientTest extends AbstractApiClientTest { + + @Test + public void testTelemetryLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create a device for telemetry operations + Device device = new Device(); + device.setName("TelemetryTestDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + assertNotNull(createdDevice); + + String entityType = "DEVICE"; + String entityId = createdDevice.getId().getId().toString(); + + // save server-side attributes + String serverAttributes = "{\"serverAttr1\": \"value1\", \"serverAttr2\": 42}"; + client.saveEntityAttributesV2(entityType, entityId, "SERVER_SCOPE", serverAttributes); + + // save shared attributes + String sharedAttributes = "{\"sharedAttr1\": \"sharedValue1\", \"sharedAttr2\": true}"; + client.saveEntityAttributesV2(entityType, entityId, "SHARED_SCOPE", sharedAttributes); + + // get attribute keys + List allKeys = client.getAttributeKeys(entityType, entityId); + assertNotNull(allKeys); + assertTrue(allKeys.containsAll(List.of("serverAttr1", "serverAttr2", "sharedAttr1", "sharedAttr2"))); + + // get attribute keys by scope + List serverKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertEquals(2 + 1, serverKeys.size()); //active attribute is automatically added to server scope + assertTrue(serverKeys.containsAll(List.of("serverAttr1", "serverAttr2", "active"))); + + // get attributes by scope + List serverAttrs = client.getAttributesByScope(entityType, entityId, "SERVER_SCOPE", "serverAttr1,serverAttr2", null); + assertNotNull(serverAttrs); + assertEquals(2, serverAttrs.size()); + + // get all attributes + List allAttrs = client.getAttributes(entityType, entityId, "serverAttr1,sharedAttr1", null); + assertEquals(2, allAttrs.size()); + assertEquals("value1", allAttrs.stream().filter(attr -> attr.getKey().equals("serverAttr1")).findFirst().orElseThrow().getValue().toString()); + assertEquals("sharedValue1", allAttrs.stream().filter(attr -> attr.getKey().equals("sharedAttr1")).findFirst().orElseThrow().getValue().toString()); + + // save timeseries data + long ts1 = timestamp - 60000; + long ts2 = timestamp - 30000; + long ts3 = timestamp; + String telemetryBody = "{\"ts\":" + ts1 + ",\"values\":{\"temperature\":25.5,\"humidity\":60}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody); + + String telemetryBody2 = "{\"ts\":" + ts2 + ",\"values\":{\"temperature\":26.0,\"humidity\":58}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody2); + + String telemetryBody3 = "{\"ts\":" + ts3 + ",\"values\":{\"temperature\":27.1,\"humidity\":55}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody3); + + // get timeseries keys + List tsKeys = client.getTimeseriesKeys(entityType, entityId); + assertNotNull(tsKeys); + assertEquals(2, tsKeys.size()); + assertTrue(tsKeys.containsAll(List.of("humidity", "temperature"))); + + // get latest timeseries + Map> latestData = client.getLatestTimeseries(entityType, entityId, "temperature,humidity", false, null); + assertNotNull(latestData); + assertNotNull(latestData.get("temperature")); + assertFalse(latestData.get("temperature").isEmpty()); + assertEquals("27.1", latestData.get("temperature").get(0).getValue().toString()); + + // get timeseries history + Map> historyData = client.getTimeseriesHistory( + entityType, entityId, + ts1 - 1000, ts3 + 1000, "temperature", + null, null, null, null, "NONE", "ASC", false, null); + assertNotNull(historyData); + List tempHistory = historyData.get("temperature"); + assertNotNull(tempHistory); + assertEquals(3, tempHistory.size()); + assertEquals("25.5", tempHistory.get(0).getValue().toString()); + assertEquals("27.1", tempHistory.get(2).getValue().toString()); + + // delete timeseries + client.deleteEntityTimeseries(entityType, entityId, "humidity", true, null, null, true, false, null); + + List keysAfterDelete = client.getTimeseriesKeys(entityType, entityId); + assertFalse(keysAfterDelete.contains("humidity")); + + // delete attributes + client.deleteEntityAttributes(entityType, entityId, "SERVER_SCOPE", "serverAttr1", null); + + List serverKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertFalse(serverKeysAfterDelete.contains("serverAttr1")); + assertTrue(serverKeysAfterDelete.contains("serverAttr2")); + + // save device attributes using device-specific endpoint + client.saveDeviceAttributes(entityId, "SERVER_SCOPE", "{\"deviceSpecificAttr\": \"test\"}"); + + List deviceKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertTrue(deviceKeys.contains("deviceSpecificAttr")); + + // delete device attributes + client.deleteDeviceAttributes(entityId, "SERVER_SCOPE", "deviceSpecificAttr", null); + + List deviceKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertFalse(deviceKeysAfterDelete.contains("deviceSpecificAttr")); + + // save telemetry with TTL + String ttlTelemetry = "{\"ts\":" + timestamp + ",\"values\":{\"shortLived\":99}}"; + client.saveEntityTelemetryWithTTL(entityType, entityId, "ANY", 86400L, ttlTelemetry); + + Map> latestWithTtl = client.getLatestTimeseries(entityType, entityId, "shortLived", false, null); + assertNotNull(latestWithTtl.get("shortLived")); + assertEquals("99", latestWithTtl.get("shortLived").get(0).getValue().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java new file mode 100644 index 0000000000..150486eb8c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.PageDataTenant; +import org.thingsboard.client.model.PageDataUser; +import org.thingsboard.client.model.Tenant; +import org.thingsboard.client.model.User; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class TenantApiClientTest extends AbstractApiClientTest { + + @Test + public void testTenantLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdTenants = new ArrayList<>(); + + // authenticate as sysadmin for tenant management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // create 20 tenants + for (int i = 0; i < 20; i++) { + Tenant tenant = new Tenant(); + String tenantTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + tenant.setTitle(tenantTitle); + tenant.setEmail("tenant_" + timestamp + "_" + i + "@test.com"); + tenant.setCountry("US"); + tenant.setCity("City" + i); + + Tenant createdTenant = client.saveTenant(tenant); + assertNotNull(createdTenant); + assertNotNull(createdTenant.getId()); + assertEquals(tenantTitle, createdTenant.getTitle()); + + createdTenants.add(createdTenant); + } + + try { + // find all with search text, check count + PageDataTenant filteredTenants = client.getTenants(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 tenants matching prefix", 10, filteredTenants.getData().size()); + + // find by id + Tenant searchTenant = createdTenants.get(10); + Tenant fetchedTenant = client.getTenantById(searchTenant.getId().getId().toString()); + assertEquals(searchTenant.getTitle(), fetchedTenant.getTitle()); + assertEquals(searchTenant.getEmail(), fetchedTenant.getEmail()); + + // update tenant + fetchedTenant.setCity("Updated City"); + fetchedTenant.setCountry("DE"); + Tenant updatedTenant = client.saveTenant(fetchedTenant); + assertEquals("Updated City", updatedTenant.getCity()); + assertEquals("DE", updatedTenant.getCountry()); + + // create a tenant admin for one of the tenants and verify listing + Tenant tenantForAdmin = createdTenants.get(0); + User adminUser = new User(); + adminUser.setEmail("tenanttest_admin_" + timestamp + "@test.com"); + adminUser.setAuthority(Authority.TENANT_ADMIN); + adminUser.setTenantId(tenantForAdmin.getId()); + adminUser.setFirstName("TestAdmin"); + User savedAdmin = client.saveUser(adminUser, "false"); + assertNotNull(savedAdmin); + + PageDataUser tenantAdmins = client.getTenantAdmins( + tenantForAdmin.getId().getId().toString(), 100, 0, null, null, null); + assertEquals(1, tenantAdmins.getData().size()); + assertEquals(savedAdmin.getEmail(), tenantAdmins.getData().get(0).getEmail()); + + // delete tenant + UUID tenantToDeleteId = createdTenants.get(0).getId().getId(); + client.deleteTenant(tenantToDeleteId.toString()); + createdTenants.remove(0); + + // verify deletion + PageDataTenant tenantsAfterDelete = client.getTenants(100, 0, TEST_PREFIX_2, null, null); + assertEquals(10, tenantsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getTenantById(tenantToDeleteId.toString()) + ); + } finally { + // clean up all created tenants (deleting tenant cascades to users) + client.login("sysadmin@thingsboard.org", "sysadmin"); + for (Tenant tenant : createdTenants) { + try { + client.deleteTenant(tenant.getId().getId().toString()); + } catch (ApiException ignored) { + } + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java new file mode 100644 index 0000000000..68dd5f7c0e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java @@ -0,0 +1,179 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.DefaultTenantProfileConfiguration; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataEntityInfo; +import org.thingsboard.client.model.PageDataTenantProfile; +import org.thingsboard.client.model.TenantProfile; +import org.thingsboard.client.model.TenantProfileData; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TenantProfileApiClientTest extends AbstractApiClientTest { + + @Test + public void testTenantProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // authenticate as sysadmin for tenant profile management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // get initial count (there should be a default profile) + PageDataTenantProfile initialProfiles = client.getTenantProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default tenant profile", initialSize >= 1); + + // get default tenant profile info + EntityInfo defaultProfileInfo = client.getDefaultTenantProfileInfo(); + assertNotNull(defaultProfileInfo); + assertNotNull(defaultProfileInfo.getName()); + + try { + // create 5 tenant profiles + for (int i = 0; i < 5; i++) { + TenantProfile profile = new TenantProfile(); + profile.setName(TEST_PREFIX + "TenantProfile_" + timestamp + "_" + i); + profile.setDescription("Test tenant profile " + i); + profile.setIsolatedTbRuleEngine(false); + + TenantProfileData profileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration(); + config.setMaxDevices(100L); + config.setMaxAssets(100L); + config.setMaxCustomers(50L); + config.setMaxUsers(50L); + config.setMaxDashboards(50L); + config.setMaxRuleChains(20L); + config.setMaxDataPointsPerRollingArg(20L); + config.setMaxRelatedEntitiesToReturnPerCfArgument(20); + config.setMaxRelationLevelPerCfArgument(20); + profileData.setConfiguration(config); + profile.setProfileData(profileData); + profile.setDefault(false); + + TenantProfile created = client.saveTenantProfile(profile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(profile.getName(), created.getName()); + assertEquals(profile.getDescription(), created.getDescription()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // find all, check count + PageDataTenantProfile allProfiles = client.getTenantProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // find with text search + PageDataTenantProfile filteredProfiles = client.getTenantProfiles(100, 0, + TEST_PREFIX + "TenantProfile_" + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // get by id + TenantProfile searchProfile = createdProfiles.get(2); + TenantProfile fetchedProfile = client.getTenantProfileById(searchProfile.getId().getId().toString()); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // update tenant profile + fetchedProfile.setDescription("Updated description"); + TenantProfile updatedProfile = client.saveTenantProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // get tenant profile infos (paginated) + PageDataEntityInfo profileInfos = client.getTenantProfileInfos(100, 0, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // get profiles by list of ids + List idsToFetch = List.of( + createdProfiles.get(0).getId().getId().toString(), + createdProfiles.get(1).getId().getId().toString() + ); + List profileList = client.getTenantProfileList(idsToFetch); + assertEquals(2, profileList.size()); + + // set a profile as default + TenantProfile profileToSetDefault = createdProfiles.get(1); + client.setDefaultTenantProfile(profileToSetDefault.getId().getId().toString()); + EntityInfo defaultTenantProfileInfo = client.getDefaultTenantProfileInfo(); + assertEquals(profileToSetDefault.getName(), defaultTenantProfileInfo.getName()); + + // verify default profile info now points to the new default + EntityInfo newDefaultInfo = client.getDefaultTenantProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // restore original default profile + TenantProfile originalDefault = initialProfiles.getData().stream() + .filter(TenantProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultTenantProfile(originalDefault.getId().getId().toString()); + + // delete tenant profile (cannot delete the default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteTenantProfile(profileToDeleteId.toString()); + createdProfiles.remove(0); + + // verify deletion + assertReturns404(() -> + client.getTenantProfileById(profileToDeleteId.toString()) + ); + + PageDataTenantProfile profilesAfterDelete = client.getTenantProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + } finally { + // clean up created profiles + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // ensure original default is restored before deleting test profiles + TenantProfile originalDefault = initialProfiles.getData().stream() + .filter(TenantProfile::getDefault) + .findFirst() + .orElseThrow(); + try { + client.setDefaultTenantProfile(originalDefault.getId().getId().toString()); + } catch (ApiException ignored) { + } + + for (TenantProfile profile : createdProfiles) { + try { + client.deleteTenantProfile(profile.getId().getId().toString()); + } catch (ApiException ignored) { + } + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java new file mode 100644 index 0000000000..5f371b5d82 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AccountTwoFaSettings; +import org.thingsboard.client.model.PlatformTwoFaSettings; +import org.thingsboard.client.model.TotpTwoFaAccountConfig; +import org.thingsboard.client.model.TotpTwoFaProviderConfig; +import org.thingsboard.client.model.TwoFaAccountConfig; +import org.thingsboard.client.model.TwoFaProviderType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TwoFactorAuthApiClientTest extends AbstractApiClientTest { + + @Test + public void testTwoFactorAuthLifecycle() throws Exception { + // save original platform 2FA settings as sysadmin + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // configure platform 2FA settings with TOTP provider + TotpTwoFaProviderConfig totpProviderConfig = new TotpTwoFaProviderConfig(); + totpProviderConfig.setIssuerName("TestThingsBoard"); + + PlatformTwoFaSettings newSettings = new PlatformTwoFaSettings(); + newSettings.setProviders(List.of(totpProviderConfig)); + newSettings.setMinVerificationCodeSendPeriod(30); + newSettings.setTotalAllowedTimeForVerification(300); + newSettings.setMaxVerificationFailuresBeforeUserLockout(5); + + PlatformTwoFaSettings savedSettings = client.savePlatformTwoFaSettings(newSettings); + assertNotNull(savedSettings); + assertNotNull(savedSettings.getProviders()); + assertFalse(savedSettings.getProviders().isEmpty()); + assertEquals(30, savedSettings.getMinVerificationCodeSendPeriod().intValue()); + assertEquals(300, savedSettings.getTotalAllowedTimeForVerification().intValue()); + + // get available 2FA providers (should include TOTP) + List providerTypes = client.getAvailableTwoFaProviderTypes(); + assertNotNull(providerTypes); + assertTrue(providerTypes.contains(TwoFaProviderType.TOTP)); + + // get account 2FA settings (should be empty initially) + AccountTwoFaSettings accountSettings = client.getAccountTwoFaSettings(); + assertNull(accountSettings); + + // generate TOTP account config + TwoFaAccountConfig generatedConfig = client.generateTwoFaAccountConfig(TwoFaProviderType.TOTP.getValue()); + assertNotNull(generatedConfig); + TotpTwoFaAccountConfig totpConfig = (TotpTwoFaAccountConfig) generatedConfig; + assertNotNull(totpConfig); + assertNotNull(totpConfig.getAuthUrl()); + assertTrue(totpConfig.getAuthUrl().startsWith("otpauth://totp/")); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java new file mode 100644 index 0000000000..154858405f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.Customer; +import org.thingsboard.client.model.JwtPair; +import org.thingsboard.client.model.PageDataUser; +import org.thingsboard.client.model.User; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class UserApiClientTest extends AbstractApiClientTest { + + @Test + public void testUserLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdUsers = new ArrayList<>(); + + // create 20 tenant admin users + for (int i = 0; i < 20; i++) { + User user = new User(); + String email = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i + "@test.com"; + user.setEmail(email); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(savedClientTenant.getId()); + user.setFirstName("First" + i); + user.setLastName("Last" + i); + + User createdUser = client.saveUser(user, "false"); + assertNotNull(createdUser); + assertNotNull(createdUser.getId()); + assertEquals(email, createdUser.getEmail()); + assertEquals(Authority.TENANT_ADMIN, createdUser.getAuthority()); + + createdUsers.add(createdUser); + } + + // find all tenant admins, check count (20 created + 1 from setup) + PageDataUser allUsers = client.getUsers(100, 0, null, null, null); + assertNotNull(allUsers); + assertNotNull(allUsers.getData()); + int initialSize = allUsers.getData().size(); + assertEquals("Expected 21 users (20 created + 2 from setup), but got " + initialSize, 22, initialSize); + + // find with search text, check count + PageDataUser filteredUsers = client.getUsers(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 users matching prefix", 10, filteredUsers.getData().size()); + + // find by id + User searchUser = createdUsers.get(10); + User fetchedUser = client.getUserById(searchUser.getId().getId().toString()); + assertEquals(searchUser.getEmail(), fetchedUser.getEmail()); + assertEquals(searchUser.getFirstName(), fetchedUser.getFirstName()); + + // update user + fetchedUser.setFirstName("UpdatedFirst"); + fetchedUser.setLastName("UpdatedLast"); + User updatedUser = client.saveUser(fetchedUser, "false"); + assertEquals("UpdatedFirst", updatedUser.getFirstName()); + assertEquals("UpdatedLast", updatedUser.getLastName()); + + // activate user and get token + activateUser(createdUsers.get(0).getId(), "password123", false); + JwtPair userToken = client.getUserToken(createdUsers.get(0).getId().getId().toString()); + assertNotNull(userToken); + assertNotNull(userToken.getToken()); + + // disable user credentials + client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "false"); + + // re-enable user credentials + client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "true"); + + // create customer users and verify listing + Customer customer2 = new Customer(); + customer2.setTitle("User test customer " + timestamp); + customer2.setEmail("usertest_" + timestamp + "@test.com"); + Customer savedCustomer2 = client.saveCustomer(customer2, null, null, null); + + List customerUsers = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + User customerUser = new User(); + customerUser.setEmail("custuser_" + timestamp + "_" + i + "@test.com"); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(savedClientTenant.getId()); + customerUser.setCustomerId(savedCustomer2.getId()); + customerUser.setFirstName("CustFirst" + i); + customerUser.setLastName("CustLast" + i); + + User created = client.saveUser(customerUser, "false"); + assertNotNull(created); + customerUsers.add(created); + } + + // list customer users + PageDataUser customerUserPage = client.getCustomerUsers( + savedCustomer2.getId().getId().toString(), 100, 0, null, null, null); + assertEquals("Expected 5 customer users", 5, customerUserPage.getData().size()); + + // delete user + UUID userToDeleteId = createdUsers.get(0).getId().getId(); + client.deleteUser(userToDeleteId.toString()); + + // verify deletion + PageDataUser usersAfterDelete = client.getUsers(100, 0, null, null, null); + assertEquals(initialSize + 5 - 1, usersAfterDelete.getData().size()); + + assertReturns404(() -> + client.getUserById(userToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java new file mode 100644 index 0000000000..ee3984bb4b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java @@ -0,0 +1,155 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.thingsboard.client.model.PageDataWidgetTypeInfo; +import org.thingsboard.client.model.WidgetTypeDetails; +import org.thingsboard.client.model.WidgetTypeInfo; +import org.thingsboard.client.model.WidgetsBundle; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class WidgetTypeApiClientTest extends AbstractApiClientTest { + + private JsonNode createDescriptor(String type) { + return OBJECT_MAPPER.createObjectNode() + .put("type", type) + .put("sizeX", 7.5) + .put("sizeY", 5) + .put("resources", "[]") + .put("templateHtml", "
Test
") + .put("templateCss", ".test-widget { font-size: 14px; }") + .put("controllerScript", "self.onInit = function() {};") + .put("settingsSchema", "{}") + .put("dataKeySettingsSchema", "{}"); + } + + @Test + public void testWidgetTypeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdWidgetTypes = new ArrayList<>(); + + // create a widgets bundle + WidgetsBundle bundle = new WidgetsBundle(null, null, null, + TEST_PREFIX + "Bundle_" + timestamp, null, false, + "Test bundle description", null, null); + WidgetsBundle savedBundle = client.saveWidgetsBundle(bundle); + assertNotNull(savedBundle); + assertNotNull(savedBundle.getId()); + assertEquals(bundle.getTitle(), savedBundle.getTitle()); + + // create 5 widget types + for (int i = 0; i < 5; i++) { + String name = TEST_PREFIX + "Widget_" + timestamp + "_" + i; + JsonNode descriptor = createDescriptor("latest"); + + WidgetTypeDetails widgetType = new WidgetTypeDetails(null, null, null, name, descriptor); + widgetType.setDescription("Test widget " + i); + widgetType.setDeprecated(false); + widgetType.setTags(List.of("test", "automated")); + + WidgetTypeDetails created = client.saveWidgetType(widgetType, false); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(name, created.getName()); + assertNotNull(created.getFqn()); + + createdWidgetTypes.add(created); + } + + // list widget types with text search (tenant only) + PageDataWidgetTypeInfo filteredTypes = client.getWidgetTypes(100, 0, + TEST_PREFIX + "Widget_" + timestamp, null, null, + true, false, null, null, null); + assertNotNull(filteredTypes); + assertEquals(5, filteredTypes.getData().size()); + + // get widget type details by id + WidgetTypeDetails searchWidget = createdWidgetTypes.get(2); + WidgetTypeDetails fetchedDetails = client.getWidgetTypeById( + searchWidget.getId().getId().toString(), true); + assertEquals(searchWidget.getName(), fetchedDetails.getName()); + assertEquals(searchWidget.getFqn(), fetchedDetails.getFqn()); + assertEquals("Test widget 2", fetchedDetails.getDescription()); + + // get widget type info by id + WidgetTypeInfo fetchedInfo = client.getWidgetTypeInfoById( + searchWidget.getId().getId().toString()); + assertEquals(searchWidget.getName(), fetchedInfo.getName()); + + // add widget types to bundle + List widgetTypeIds = createdWidgetTypes.stream() + .map(wt -> wt.getId().getId().toString()) + .collect(Collectors.toList()); + client.updateWidgetsBundleWidgetTypes(savedBundle.getId().getId().toString(), widgetTypeIds); + + // get bundle widget type fqns + List bundleFqns = client.getBundleWidgetTypeFqns(savedBundle.getId().getId().toString()); + assertEquals(5, bundleFqns.size()); + + // get bundle widget types details + List bundleDetails = client.getBundleWidgetTypesDetails( + savedBundle.getId().getId().toString(), false); + assertEquals(5, bundleDetails.size()); + + // get bundle widget types infos (paginated) + PageDataWidgetTypeInfo bundleInfos = client.getBundleWidgetTypesInfos( + savedBundle.getId().getId().toString(), 100, 0, + null, null, null, null, null, null); + assertEquals(5, bundleInfos.getData().size()); + + // update widget type + WidgetTypeDetails widgetToUpdate = client.getWidgetTypeById( + createdWidgetTypes.get(3).getId().getId().toString(), true); + widgetToUpdate.setDescription("Updated description"); + widgetToUpdate.setDeprecated(true); + widgetToUpdate.setTags(List.of("test", "updated")); + WidgetTypeDetails updatedWidget = client.saveWidgetType(widgetToUpdate, false); + assertEquals("Updated description", updatedWidget.getDescription()); + assertEquals(true, updatedWidget.getDeprecated()); + + // delete widget type + String widgetToDeleteId = createdWidgetTypes.get(0).getId().getId().toString(); + client.deleteWidgetType(widgetToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getWidgetTypeById(widgetToDeleteId, false) + ); + + PageDataWidgetTypeInfo typesAfterDelete = client.getWidgetTypes(100, 0, + TEST_PREFIX + "Widget_" + timestamp, null, null, + true, false, null, null, null); + assertEquals(4, typesAfterDelete.getData().size()); + + // delete widgets bundle + client.deleteWidgetsBundle(savedBundle.getId().getId().toString()); + + assertReturns404(() -> + client.getWidgetsBundleById(savedBundle.getId().getId().toString(), false) + ); + } + +} 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 c25dafa5e6..996efb17be 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java @@ -86,12 +86,12 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { } @Before - public void beforeWsTest() throws Exception { + public void beforeWsTest() { // placeholder } @After - public void afterWsTest() throws Exception { + public void afterWsTest() { if (wsClient != null) { wsClient.close(); } @@ -113,4 +113,11 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { return wsClient; } + protected TbTestWebSocketClient buildAndConnectWebSocketClientWithApiKey(String apiKey) throws URISyntaxException, InterruptedException { + TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws")); + assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); + wsClient.authenticateWithApiKey(apiKey); + return wsClient; + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 58bf4b9798..62a266f26a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -18,6 +18,8 @@ package org.thingsboard.server.controller; import lombok.extern.slf4j.Slf4j; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EdgeUtils; @@ -59,6 +61,9 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { @SpyBean protected AuditLogService auditLogService; + @MockBean + BuildProperties buildProperties; + protected final String msgErrorPermission = "You don't have permission to perform this operation!"; protected final String msgErrorShouldBeSpecified = "should be specified"; protected final String msgErrorNotFound = "Requested item wasn't found!"; @@ -212,8 +217,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { testNotificationMsgToEdgeServiceNeverWithActionType(entityId, actionType); ArgumentMatcher matcherEntityClassEquals = argument -> argument.getClass().equals(entity.getClass()); ArgumentMatcher matcherOriginatorId = argument -> argument.getClass().equals(originatorId.getClass()); - ArgumentMatcher matcherCustomerId = customerId == null ? - argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId); + ArgumentMatcher matcherCustomerId = customerId == null ? argument -> true : actualCustomerId -> actualCustomerId.equals(customerId); ArgumentMatcher matcherUserId = userId == null ? argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId); testLogEntityActionAdditionalInfoAny(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime, @@ -638,7 +642,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { return fieldName + " length must be equal or less than 255"; } - protected String msgErrorNoFound(String entityClassName, String entityIdStr) { + protected static String msgErrorNoFound(String entityClassName, String entityIdStr) { return entityClassName + " with id [" + entityIdStr + "] is not found"; } 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 f104c51ca4..0cf114fa35 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -15,19 +15,13 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EventInfo; -import org.thingsboard.server.common.data.event.EventType; -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.msg.TbMsgType; -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; import org.thingsboard.server.dao.rule.RuleChainService; @@ -61,18 +55,6 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class); } - protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { - return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE.getOldName(), limit); - } - - protected PageData getEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) throws Exception { - TimePageLink pageLink = new TimePageLink(limit); - return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", - new TypeReference>() { - }, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId()); - } - - protected JsonNode getMetadata(EventInfo outEvent) { String metaDataStr = outEvent.getBody().get("metadata").asText(); try { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index bbf3a3467e..055e1e19a0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -69,17 +69,23 @@ import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.actors.DefaultTbActorSystem; import org.thingsboard.server.actors.TbActorId; import org.thingsboard.server.actors.TbActorMailbox; +import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; import org.thingsboard.server.actors.TbEntityActorId; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityActor; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityMessageProcessor; import org.thingsboard.server.actors.device.DeviceActor; import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; import org.thingsboard.server.actors.device.SessionInfo; import org.thingsboard.server.actors.device.ToDeviceRpcRequestMetadata; import org.thingsboard.server.actors.service.DefaultActorService; +import org.thingsboard.server.common.data.AttributeScope; 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.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; @@ -88,6 +94,10 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; 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; @@ -102,6 +112,7 @@ import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeCon import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -113,6 +124,8 @@ import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationType; @@ -141,6 +154,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.session.FeatureType; @@ -153,6 +167,8 @@ import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.service.cf.CfRocksDb; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -164,6 +180,7 @@ import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -191,6 +208,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.BEARER_HEADER_PREFIX; @Slf4j public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @@ -201,13 +220,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected static final String TEST_DIFFERENT_TENANT_NAME = "TEST DIFFERENT TENANT"; protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; - private static final String SYS_ADMIN_PASSWORD = "sysadmin"; + protected static final String SYS_ADMIN_PASSWORD = "sysadmin"; protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; protected static final String TENANT_ADMIN_PASSWORD = "tenant"; protected static final String DIFFERENT_TENANT_ADMIN_EMAIL = "testdifftenant@thingsboard.org"; - private static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant"; + protected static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant"; protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; private static final String CUSTOMER_USER_PASSWORD = "customer"; @@ -236,6 +255,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected String mobileToken; protected String username; + protected String apiKey; + protected TenantId tenantId; protected TenantProfileId tenantProfileId; protected UserId tenantAdminUserId; @@ -417,6 +438,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); jdbcTemplate.execute("TRUNCATE TABLE notification"); + jdbcTemplate.execute("TRUNCATE TABLE audit_log"); log.debug("Executed web test teardown"); } @@ -619,8 +641,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { 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(); + validateAndSetJwtToken(JacksonUtil.treeToValue(tokenInfo, JwtPair.class), username); + } + + protected void validateAndSetJwtToken(JwtPair jwtPair, String username) { + Assert.assertNotNull(jwtPair); + String token = jwtPair.getToken(); + String refreshToken = jwtPair.getRefreshToken(); validateJwtToken(token, username); validateJwtToken(refreshToken, username); this.token = token; @@ -649,13 +676,27 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void setJwtToken(MockHttpServletRequestBuilder request) { if (this.token != null) { - request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); + request.header(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER, BEARER_HEADER_PREFIX + this.token); } if (this.mobileToken != null) { request.header(UserController.MOBILE_TOKEN_HEADER, this.mobileToken); } } + protected void resetApiKey() { + this.apiKey = null; + } + + protected void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + protected void setApiKey(MockHttpServletRequestBuilder request) { + if (this.apiKey != null) { + request.header(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER, API_KEY_HEADER_PREFIX + this.apiKey); + } + } + protected DeviceProfile createDeviceProfile(String name) { return createDeviceProfile(name, null); } @@ -690,15 +731,28 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return assetProfile; } + protected Device createDevice(String name) throws Exception { + return createDevice(name, "default", null, null); + } + protected Device createDevice(String name, String accessToken) throws Exception { + return createDevice(name, "default", null, accessToken); + } + + protected Device createDevice(String name, String type, String label, String accessToken) throws Exception { Device device = new Device(); device.setName(name); - device.setType("default"); + device.setType(type); + device.setLabel(label); DeviceData deviceData = new DeviceData(); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); deviceData.setConfiguration(new DefaultDeviceConfiguration()); device.setDeviceData(deviceData); - return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + if (accessToken != null) { + return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + } else { + return doPost("/api/device", device, Device.class); + } } protected Device assignDeviceToCustomer(DeviceId deviceId, CustomerId customerId) { @@ -749,6 +803,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/device-with-credentials", request, Device.class); } + protected ResultActions doGetAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate).params(params); + setJwtToken(getRequest); + return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); + } + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); setJwtToken(getRequest); @@ -762,6 +822,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(getRequest); } + protected ResultActions doGetWithApiKey(String urlTemplate, Object... urlVariables) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); + setApiKey(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); } @@ -785,6 +851,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); } + protected T doGetWithApiKey(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { + return readResponse(doGetWithApiKey(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); + } + protected T doGetTyped(String urlTemplate, TypeReference responseType, Object... urlVariables) throws Exception { return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); } @@ -864,6 +934,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } } + protected R doPostWithApiKey(String urlTemplate, T content, Class responseClass, String... params) { + try { + return readResponse(doPostWithApiKey(urlTemplate, content, params).andExpect(status().isOk()), responseClass); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + protected R doPostWithResponse(String urlTemplate, T content, Class responseClass, String... params) throws Exception { return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); } @@ -931,6 +1009,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(postRequest); } + protected ResultActions doPostWithApiKey(String urlTemplate, T content, String... params) throws Exception { + MockHttpServletRequestBuilder postRequest = post(urlTemplate, params); + setApiKey(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); @@ -948,6 +1034,22 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(deleteRequest); } + protected ResultActions doDeleteAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate) + .params(params); + setJwtToken(deleteRequest); + MvcResult result = mockMvc.perform(deleteRequest).andReturn(); + result.getAsyncResult(DEFAULT_TIMEOUT); + return mockMvc.perform(asyncDispatch(result)); + } + + protected ResultActions doDeleteWithApiKey(String urlTemplate, String... params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate); + setApiKey(deleteRequest); + populateParams(deleteRequest, params); + return mockMvc.perform(deleteRequest); + } + protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception { MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params); setJwtToken(deleteRequest); @@ -1071,7 +1173,17 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void createEntityRelation(EntityId entityIdFrom, EntityId entityIdTo, String typeRelation) throws Exception { EntityRelation relation = new EntityRelation(entityIdFrom, entityIdTo, typeRelation); - doPost("/api/relation", relation); + doPost("/api/relation", relation).andExpect(status().isOk()); + } + + protected void deleteEntityRelation(EntityRelation entityRelation) throws Exception { + String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + entityRelation.getFrom().getId(), + entityRelation.getFrom().getEntityType(), + entityRelation.getType(), + entityRelation.getTo().getId(), + entityRelation.getTo().getEntityType()); + doDelete(url); } protected List findRelationsByTo(EntityId entityId) throws Exception { @@ -1122,6 +1234,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } + protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) { + CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); + Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); + Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + CalculatedFieldState calculatedFieldState = statesMap.get(cfId); + boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastScheduledRefreshTs() < + System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); + return isReady; + }); + } + protected static String getMapName(FeatureType featureType) { switch (featureType) { case ATTRIBUTES: @@ -1143,6 +1267,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); } + protected CalculatedFieldEntityMessageProcessor getCalculatedFieldEntityMessageProcessor(EntityId entityId) { + DefaultTbActorSystem actorSystem = (DefaultTbActorSystem) ReflectionTestUtils.getField(actorService, "system"); + ConcurrentMap actors = (ConcurrentMap) ReflectionTestUtils.getField(actorSystem, "actors"); + Awaitility.await("CF entity actor was created").atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> actors.containsKey(new TbCalculatedFieldEntityActorId(entityId))); + TbActorMailbox actorMailbox = actors.get(new TbCalculatedFieldEntityActorId(entityId)); + CalculatedFieldEntityActor actor = (CalculatedFieldEntityActor) ReflectionTestUtils.getField(actorMailbox, "actor"); + return (CalculatedFieldEntityMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); + } + protected void updateDefaultTenantProfileConfig(Consumer updater) throws ThingsboardException { updateDefaultTenantProfile(tenantProfile -> { TenantProfileData profileData = tenantProfile.getProfileData(); @@ -1156,7 +1290,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { TenantProfile oldTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); TenantProfile tenantProfile = JacksonUtil.clone(oldTenantProfile); updater.accept(tenantProfile); - tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile); + // user should be sysadmin as this operation allowed only for sysadmins. But for the simplification of the test - already existed variable provided. This affects only an audit log content + tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile, tenantAdminUser); } protected OAuth2Client createOauth2Client(TenantId tenantId, String title) { @@ -1309,6 +1444,72 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { doPost("/api/job/" + jobId + "/reprocess").andExpect(status().isOk()); } + protected void postTelemetry(EntityId entityId, String payload) throws Exception { + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected void postTelemetry(EntityId entityId, TsKvEntry entry) throws Exception { + var values = JacksonUtil.newObjectNode(); + JacksonUtil.addKvEntry(values, entry); + + var payload = JacksonUtil.newObjectNode() + .put("ts", entry.getTs()) + .set("values", values); + + var url = "/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/timeseries/any"; + doPostAsync(url, payload, 30_000L).andExpect(status().isOk()); + } + + protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + + "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected void postAttributes(EntityId entityId, AttributeScope scope, KvEntry... attributes) throws Exception { + postAttributes(entityId, scope, Arrays.asList(attributes)); + } + + protected void postAttributes(EntityId entityId, AttributeScope scope, Collection attributes) throws Exception { + var url = "/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/attributes/" + scope; + var payload = JacksonUtil.newObjectNode(); + for (KvEntry entry : attributes) { + JacksonUtil.addKvEntry(payload, entry); + } + doPostAsync(url, payload, 30_000L).andExpect(status().isOk()); + } + + protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { + return doPost("/api/calculatedField", calculatedField, CalculatedField.class); + } + + protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) { + return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class); + } + + protected PageData getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + } + + protected PageData getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", + new TypeReference>() {}, pageLink); + } + + protected List getCalculatedFields(CalculatedFieldType type, + EntityType entityType, + List entities, + List names) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields?" + + (type != null ? "types=" + type + "&" : "") + + (entityType != null ? "entityType=" + entityType + "&" : "") + + (entities != null ? "entities=" + String.join(",", + entities.stream().map(UUID::toString).toList()) + "&" : "") + + (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), + new TypeReference>() {}, new PageLink(10)).getData(); + } + protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE, limit); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java index ba86d60852..3bb5fd5647 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java @@ -50,6 +50,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.COMMENT_DELETED; @Slf4j @ContextConfiguration(classes = {AlarmCommentControllerTest.Config.class}) @@ -209,8 +210,10 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { AlarmComment expectedAlarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment", - CUSTOMER_USER_EMAIL))) + .comment(JacksonUtil.newObjectNode() + .put("text", String.format(COMMENT_DELETED.getText(), CUSTOMER_USER_EMAIL)) + .put("subtype", COMMENT_DELETED.name()) + .put("userName", CUSTOMER_USER_EMAIL)) .build(); testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment); } @@ -240,8 +243,10 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { AlarmComment expectedAlarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment", - TENANT_ADMIN_EMAIL))) + .comment(JacksonUtil.newObjectNode() + .put("text", String.format(COMMENT_DELETED.getText(), TENANT_ADMIN_EMAIL)) + .put("subtype", COMMENT_DELETED.name()) + .put("userName", TENANT_ADMIN_EMAIL)) .build(); testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java new file mode 100644 index 0000000000..7c64fd6525 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java @@ -0,0 +1,420 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +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.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +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.page.SortOrder; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class AlarmRuleControllerTest extends AbstractControllerTest { + + private Tenant savedTenant; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + deleteTenant(savedTenant.getId()); + } + + @Test + public void testSaveAlarmRule() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "High Temperature"); + + AlarmRuleDefinition saved = saveAlarmRule(alarmRule); + + assertThat(saved).isNotNull(); + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getCreatedTime()).isGreaterThan(0); + assertThat(saved.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(saved.getEntityId()).isEqualTo(testDevice.getId()); + assertThat(saved.getName()).isEqualTo("High Temperature"); + assertThat(saved.getConfiguration()).isNotNull(); + assertThat(saved.getConfiguration().getCreateRules()).containsKey(AlarmSeverity.CRITICAL); + + saved.setName("Updated Alarm Rule"); + AlarmRuleDefinition updated = saveAlarmRule(saved); + + assertThat(updated.getName()).isEqualTo("Updated Alarm Rule"); + assertThat(updated.getVersion()).isEqualTo(saved.getVersion() + 1); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRuleById() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "Test Alarm"); + + AlarmRuleDefinition saved = saveAlarmRule(alarmRule); + AlarmRuleDefinition fetched = doGet("/api/alarm/rule/" + saved.getId().getId(), AlarmRuleDefinition.class); + + assertThat(fetched).isNotNull(); + assertThat(fetched).isEqualTo(saved); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRuleById_notFound() throws Exception { + doGet("/api/alarm/rule/" + UUID.randomUUID()) + .andExpect(status().isNotFound()); + } + + @Test + public void testGetAlarmRuleById_calculatedFieldNotAlarm() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); + CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); + + doGet("/api/alarm/rule/" + savedCf.getId().getId()) + .andExpect(status().isNotFound()); + + doDelete("/api/calculatedField/" + savedCf.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRulesByEntityId() throws Exception { + Device device1 = createDevice("Device 1", "1234567890"); + Device device2 = createDevice("Device 2", "0987654321"); + AlarmRuleDefinition rule1 = saveAlarmRule(createTestAlarmRule(device1.getId(), "Rule 1")); + saveAlarmRule(createTestAlarmRule(device2.getId(), "Rule 2")); + + PageData result = doGetTypedWithPageLink( + "/api/alarm/rules/" + EntityType.DEVICE + "/" + device1.getUuidId() + "?", + new TypeReference<>() {}, new PageLink(10)); + + assertThat(result.getData()).hasSize(1); + assertThat(result.getData().get(0).getId()).isEqualTo(rule1.getId()); + assertThat(result.getData().get(0).getName()).isEqualTo("Rule 1"); + } + + @Test + public void testGetAlarmRules() throws Exception { + Device device = createDevice("Device A", "1234567890"); + AlarmRuleDefinition deviceRule = saveAlarmRule(createTestAlarmRule(device.getId(), "Device Alarm")); + + DeviceProfile profile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class); + AlarmRuleDefinition profileRule = saveAlarmRule(createTestAlarmRule(profile.getId(), "Profile Alarm")); + + // All alarm rules + List all = getAlarmRules(null, null); + assertThat(all).extracting(AlarmRuleDefinition::getName) + .contains("Device Alarm", "Profile Alarm"); + + // Filter by entity type: DEVICE + List deviceRules = getAlarmRules(EntityType.DEVICE, null); + assertThat(deviceRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Device Alarm"); + + // Filter by entity type: DEVICE_PROFILE + List profileRules = getAlarmRules(EntityType.DEVICE_PROFILE, null); + assertThat(profileRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Profile Alarm"); + + // Filter by specific entity IDs + List specificRules = getAlarmRules(EntityType.DEVICE, List.of(device.getUuidId())); + assertThat(specificRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Device Alarm"); + + // Verify entity names are populated + AlarmRuleDefinitionInfo deviceInfo = all.stream() + .filter(r -> r.getName().equals("Device Alarm")).findFirst().orElseThrow(); + assertThat(deviceInfo.getEntityName()).isEqualTo("Device A"); + + AlarmRuleDefinitionInfo profileInfo = all.stream() + .filter(r -> r.getName().equals("Profile Alarm")).findFirst().orElseThrow(); + assertThat(profileInfo.getEntityName()).isEqualTo("Profile A"); + } + + @Test + public void testGetAlarmRules_textSearch() throws Exception { + Device device = createDevice("Device A", "1234567890"); + saveAlarmRule(createTestAlarmRule(device.getId(), "Temperature Alarm")); + saveAlarmRule(createTestAlarmRule(device.getId(), "Humidity Alarm")); + + PageData result = doGetTypedWithPageLink( + "/api/alarm/rules?textSearch=Temp&", + new TypeReference<>() {}, new PageLink(10)); + + assertThat(result.getData()).hasSize(1); + assertThat(result.getData().get(0).getName()).isEqualTo("Temperature Alarm"); + } + + @Test + public void testGetAlarmRuleNames() throws Exception { + Device device = createDevice("Device A", "1234567890"); + saveAlarmRule(createTestAlarmRule(device.getId(), "Alpha Alarm")); + saveAlarmRule(createTestAlarmRule(device.getId(), "Beta Alarm")); + + PageData names = getAlarmRuleNames(new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(2); + assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder()); + assertThat(names.getData()).contains("Alpha Alarm", "Beta Alarm"); + + names = getAlarmRuleNames(new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.DESC))); + assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder()); + + names = getAlarmRuleNames(new PageLink(10, 0, + "Alpha", new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(1); + assertThat(names.getData()).containsOnly("Alpha Alarm"); + } + + @Test + public void testDeleteAlarmRule() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "To Delete")); + + assertThat(saved).isNotNull(); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + doGet("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteAlarmRule_notFound() throws Exception { + doDelete("/api/alarm/rule/" + UUID.randomUUID()) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteAlarmRule_calculatedFieldNotAlarm() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); + CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); + + doDelete("/api/alarm/rule/" + savedCf.getId().getId()) + .andExpect(status().isNotFound()); + + doDelete("/api/calculatedField/" + savedCf.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetLatestAlarmRuleDebugEvent() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "Debug Test")); + + doGet("/api/alarm/rule/" + saved.getId().getId() + "/debug") + .andExpect(status().isOk()); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetLatestAlarmRuleDebugEvent_notFound() throws Exception { + doGet("/api/alarm/rule/" + UUID.randomUUID() + "/debug") + .andExpect(status().isNotFound()); + } + + @Test + public void testTestAlarmRuleScript() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "return temperature > 50;", + "arguments": { + "temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 55 } + } + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.has("output")).isTrue(); + assertThat(result.has("error")).isTrue(); + assertThat(result.get("error").asText()).isEmpty(); + assertThat(result.get("output").asText()).isEqualTo("true"); + } + + @Test + public void testTestAlarmRuleScript_returnsFalse() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "return temperature > 50;", + "arguments": { + "temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 30 } + } + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.get("error").asText()).isEmpty(); + assertThat(result.get("output").asText()).isEqualTo("false"); + } + + @Test + public void testTestAlarmRuleScript_missingExpression() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "arguments": {} + } + """); + + doPost("/api/alarm/rule/testScript", request) + .andExpect(status().isBadRequest()); + } + + @Test + public void testTestAlarmRuleScript_invalidExpression() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "invalid syntax {{{{", + "arguments": {} + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.get("error").asText()).isNotEmpty(); + } + + // --- Helper methods --- + + private AlarmRuleDefinition createTestAlarmRule(EntityId entityId, String name) { + AlarmRuleDefinition alarmRule = new AlarmRuleDefinition(); + alarmRule.setEntityId(entityId); + alarmRule.setName(name); + alarmRule.setConfigurationVersion(1); + alarmRule.setAdditionalInfo(JacksonUtil.newObjectNode()); + + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("0"); + configuration.setArguments(Map.of("temperature", argument)); + + AlarmRule rule = new AlarmRule(); + TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); + expression.setExpression("return temperature >= 50;"); + SimpleAlarmCondition condition = new SimpleAlarmCondition(); + condition.setExpression(expression); + rule.setCondition(condition); + configuration.setCreateRules(Map.of(AlarmSeverity.CRITICAL, rule)); + + alarmRule.setConfiguration(configuration); + return alarmRule; + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId) { + CalculatedField cf = new CalculatedField(); + cf.setEntityId(entityId); + cf.setType(CalculatedFieldType.SIMPLE); + cf.setName("Simple CF"); + cf.setConfigurationVersion(1); + cf.setAdditionalInfo(JacksonUtil.newObjectNode()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + config.setArguments(Map.of("T", arg)); + config.setExpression("T * 2"); + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("result"); + config.setOutput(output); + cf.setConfiguration(config); + + return cf; + } + + private List getAlarmRules(EntityType entityType, List entities) throws Exception { + StringBuilder url = new StringBuilder("/api/alarm/rules?"); + if (entityType != null) { + url.append("entityType=").append(entityType).append("&"); + } + if (entities != null) { + url.append("entities=").append(String.join(",", + entities.stream().map(UUID::toString).toList())).append("&"); + } + return doGetTypedWithPageLink(url.toString(), + new TypeReference>() {}, new PageLink(10)).getData(); + } + + private PageData getAlarmRuleNames(PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/alarm/rules/names?", + new TypeReference>() {}, pageLink); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java new file mode 100644 index 0000000000..836d1cbfad --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class ApiKeyControllerTest extends AbstractControllerTest { + + @Before + public void setUp() throws Exception { + loginTenantAdmin(); + } + + @Test + public void testSaveApiKey() throws Exception { + ApiKeyInfo apiKeyInfo = constructApiKeyInfo("New API key description", true); + + doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + + PageData pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); + Assert.assertEquals(1, pageData.getData().size()); + + ApiKeyInfo savedApiKey = pageData.getData().get(0); + Assert.assertNotNull(savedApiKey); + Assert.assertEquals(apiKeyInfo.getDescription(), savedApiKey.getDescription()); + Assert.assertEquals(apiKeyInfo.isEnabled(), savedApiKey.isEnabled()); + Assert.assertEquals(tenantId, savedApiKey.getTenantId()); + Assert.assertEquals(tenantAdminUser.getId(), savedApiKey.getUserId()); + + String newDescription = "Updated API Key Description"; + savedApiKey.setDescription(newDescription); + ApiKey updatedApiKey = doPost("/api/apiKey", savedApiKey, ApiKey.class); + Assert.assertNotNull(updatedApiKey); + Assert.assertEquals(newDescription, updatedApiKey.getDescription()); + Assert.assertNull("Verify we do not expose API key value on update", updatedApiKey.getValue()); + + doDelete("/api/apiKey/" + updatedApiKey.getId()).andExpect(status().isOk()); + } + + @Test + public void tesFindUserApiKeys() throws Exception { + PageData pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); + Assert.assertTrue(pageData.getData().isEmpty()); + + ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); + int expectedSize = 10; + for (int i = 0; i < expectedSize; i++) { + doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + } + + PageData pageData2 = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); + Assert.assertEquals(expectedSize, pageData2.getData().size()); + + pageData2.getData().forEach(apiKey -> { + try { + doDelete("/api/apiKey/" + apiKey.getId()).andExpect(status().isOk()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testUpdateApiKeyDescription() throws Exception { + ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); + doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + + PageData pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); + Assert.assertEquals(1, pageData.getData().size()); + + ApiKeyInfo savedApiKey = pageData.getData().get(0); + + String newDescription = "Updated API Key Description"; + + ApiKeyInfo updatedApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/description", newDescription, ApiKeyInfo.class); + Assert.assertNotNull(updatedApiKeyInfo); + Assert.assertEquals(newDescription, updatedApiKeyInfo.getDescription()); + + doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); + } + + @Test + public void testEnableApiKey() throws Exception { + ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); + doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + + PageData pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); + Assert.assertEquals(1, pageData.getData().size()); + + ApiKeyInfo savedApiKey = pageData.getData().get(0); + + ApiKeyInfo disabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/false", Boolean.FALSE, ApiKeyInfo.class); + Assert.assertNotNull(disabledApiKeyInfo); + Assert.assertFalse(disabledApiKeyInfo.isEnabled()); + + ApiKeyInfo enabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/true", Boolean.TRUE, ApiKeyInfo.class); + Assert.assertNotNull(enabledApiKeyInfo); + Assert.assertTrue(enabledApiKeyInfo.isEnabled()); + + doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); + } + + @Test + public void testDeleteApiKey() throws Exception { + doDelete("/api/apiKey/" + UUID.randomUUID()).andExpect(status().isNotFound()); + + ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", false); + doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + + PageData pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); + Assert.assertEquals(1, pageData.getData().size()); + ApiKeyInfo savedApiKey = pageData.getData().get(0); + + doDelete("/api/apiKey/" + savedApiKey.getId().getId()).andExpect(status().isOk()); + } + + private ApiKeyInfo constructApiKeyInfo(String description, boolean enabled) { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription(description); + apiKeyInfo.setEnabled(enabled); + apiKeyInfo.setUserId(tenantAdminUserId); + return apiKeyInfo; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java new file mode 100644 index 0000000000..405eecec9b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class ApiKeyWebSocketApiTest extends WebSocketApiTest { + + private ApiKey apiKey; + + @Before + public void setUpApiKey() { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("WS test API key"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(tenantAdminUserId); + apiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + } + + @After + public void tearDownApiKey() throws Exception { + loginTenantAdmin(); + doDelete("/api/apiKey/" + apiKey.getId()).andExpect(status().isOk()); + } + + @Override + protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { + return buildAndConnectWebSocketClientWithApiKey(apiKey.getValue()); + } + + @Test + public void testInvalidApiKeyAuthCmd_connectionClosed() throws Exception { + TbTestWebSocketClient client = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws")); + assertThat(client.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); + try { + client.authenticateWithApiKey("invalid-key"); + assertThat(client.waitForClose()).isTrue(); + } finally { + client.close(); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java index 501b639388..98b06ebbd1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java @@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.asset.AssetDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -1080,6 +1080,30 @@ public class AssetControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId); } + @Test + public void testSaveAssetWithUniquifyStrategy() throws Exception { + Asset asset = new Asset(); + asset.setName("My unique asset"); + asset.setType("default"); + doPost("/api/asset", asset, Asset.class); + + doPost("/api/asset", asset).andExpect(status().isBadRequest()); + + doPost("/api/asset?nameConflictPolicy=FAIL", asset).andExpect(status().isBadRequest()); + + Asset secondAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY", asset, Asset.class); + assertThat(secondAsset.getName()).startsWith("My unique asset_"); + + Asset thirdAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", asset, Asset.class); + assertThat(thirdAsset.getName()).startsWith("My unique asset-"); + + Asset fourthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class); + assertThat(fourthAsset.getName()).isEqualTo("My unique asset_1"); + + Asset fifthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class); + assertThat(fifthAsset.getName()).isEqualTo("My unique asset_2"); + } + private Asset createAsset(String name) { Asset asset = new Asset(); asset.setName(name); diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java index ac2a14fbff..490973f66c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java @@ -42,13 +42,17 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.asset.AssetProfileDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; @@ -195,6 +199,38 @@ public class AssetProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName()); } + @Test + public void testFindAssetProfileInfoByIds() throws Exception { + List assetProfiles = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i); + assetProfile.setTenantId(savedTenant.getId()); + assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfileInfo.class)); + } + + List expected = assetProfiles.subList(5, 15); + + String idsParam = expected.stream() + .map(ap -> ap.getId().getId().toString()) + .collect(Collectors.joining(",")); + AssetProfileInfo[] foundAssetProfileInfos = doGet("/api/assetProfileInfos?assetProfileIds=" + idsParam, AssetProfileInfo[].class); + + Assert.assertNotNull(foundAssetProfileInfos); + Assert.assertEquals(expected.size(), foundAssetProfileInfos.length); + + Map infoById = Arrays.stream(foundAssetProfileInfos) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (AssetProfileInfo assetProfileInfo : expected) { + UUID id = assetProfileInfo.getId().getId(); + AssetProfileInfo info = infoById.get(id); + Assert.assertNotNull("AssetProfileInfo not found for id " + id, info); + + Assert.assertEquals(assetProfileInfo.getId(), info.getId()); + Assert.assertEquals(assetProfileInfo.getName(), info.getName()); + } + } + @Test public void whenGetAssetProfileInfoById_thenPermissionsAreChecked() throws Exception { AssetProfile assetProfile = createAssetProfile("Asset profile 1"); diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index a76da5f161..95582cf261 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -19,6 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -28,6 +29,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -60,6 +62,9 @@ import static org.mockito.Mockito.verify; @DaoSqlTest public class AuditLogControllerTest extends AbstractControllerTest { + // Small enough to force multiple pages, verifying pagination loop correctness + private static final int SMALL_PAGE_SIZE = 5; + private Tenant savedTenant; private User tenantAdmin; @@ -111,48 +116,47 @@ public class AuditLogControllerTest extends AbstractControllerTest { doPost("/api/device", device, Device.class); } - List loadedAuditLogs = new ArrayList<>(); - TimePageLink pageLink = new TimePageLink(5); - PageData pageData; - do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs?", - new TypeReference>() { - }, pageLink); - loadedAuditLogs.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1)); - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1)); - loadedAuditLogs = new ArrayList<>(); - pageLink = new TimePageLink(5); - do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?", - new TypeReference>() { - }, pageLink); - loadedAuditLogs.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1)); + } - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + @Test + public void testAuditLogsSysAdmin() throws Exception { + loginSysAdmin(); + List loadedAuditLogsBefore = getAuditLogs(100, "/api/audit/logs?"); + + for (int i = 0; i < 3; i++) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Profile " + UUID.randomUUID()); + doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + } - loadedAuditLogs = new ArrayList<>(); - pageLink = new TimePageLink(5); + int expectedSize = loadedAuditLogsBefore.size() + 3; + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test", + expectedSize, getAuditLogs(100, "/api/audit/logs?").size())); + } + + private List getAuditLogs(int pageSize, String urlTemplate) throws Exception { + List loadedAuditLogs = new ArrayList<>(); + TimePageLink pageLink = new TimePageLink(pageSize); + PageData pageData; do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?", - new TypeReference>() { + pageData = doGetTypedWithTimePageLink(urlTemplate, + new TypeReference<>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + return loadedAuditLogs; } @Test @@ -166,20 +170,31 @@ public class AuditLogControllerTest extends AbstractControllerTest { savedDevice = doPost("/api/device", savedDevice, Device.class); } - List loadedAuditLogs = new ArrayList<>(); - TimePageLink pageLink = new TimePageLink(5); - PageData pageData; - do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?", - new TypeReference>() { - }, pageLink); - loadedAuditLogs.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); + Device finalSavedDevice = savedDevice; + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1)); + } + + @Test + public void testAuditLogs_byTenantIdAndEntityId_Sysadmin() throws Exception { + loginSysAdmin(); + + //created + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Profile " + UUID.randomUUID()); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + //updated + tenantProfile.setName(tenantProfile.getName() + "(old)"); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + TenantProfile finalTenantProfile = tenantProfile; + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/" + finalTenantProfile.getId().getEntityType() + "/" + finalTenantProfile.getId().getId() + "?")) + .as("Audit logs count by Tenant Profile entity").hasSize(2)); - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + //cleanup + doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java index 46a1dba3a7..0d79882a94 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java @@ -24,8 +24,8 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.DataConstants; @@ -96,15 +96,15 @@ public class BaseQueueControllerTest extends AbstractControllerTest { private RuleEngineStatisticsService ruleEngineStatisticsService; @Autowired private StatsFactory statsFactory; - @SpyBean - private TimeseriesDao timeseriesDao; @Autowired private QueueStatsService queueStatsService; - @SpyBean + @MockitoSpyBean + private TimeseriesDao timeseriesDao; + @MockitoSpyBean private PartitionService partitionService; - @SpyBean + @MockitoSpyBean private TimeseriesService timeseriesService; - @SpyBean + @MockitoSpyBean private ActorSystemContext actorSystemContext; @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 1802bd29fa..18e11faaf9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -18,26 +18,50 @@ package org.thingsboard.server.controller; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; 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.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.HourInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +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.page.SortOrder; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.Comparator; +import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldControllerTest extends AbstractControllerTest { @@ -73,7 +97,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { @Test public void testSaveCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -84,7 +108,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig()); + assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -92,16 +117,111 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo()); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) .andExpect(status().isOk()); } + @Test + public void testSaveGeofencingCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.GEOFENCING); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getGeofencingCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSavePropagationCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.PROPAGATION); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getPropagationCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveEntityAggregationCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.ENTITY_AGGREGATION); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getEntityAggregationCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSavePropagationCalculatedFieldWithNullArguments() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.PROPAGATION, getPropagationCalculatedFieldConfig(null)); + + doPost("/api/calculatedField", calculatedField) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("arguments must not be empty"))); + } + @Test public void testGetCalculatedFieldById() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); CalculatedField fetchedCalculatedField = doGet("/api/calculatedField/" + savedCalculatedField.getId().getId(), CalculatedField.class); @@ -113,10 +233,74 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testGetEntityCalculatedFields() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); + calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(getEntityCalculatedFields(testDevice.getId(), null, new PageLink(10)).getData()) + .singleElement().isEqualTo(calculatedField); + assertThat(getEntityCalculatedFields(testDevice.getId(), CalculatedFieldType.SIMPLE, new PageLink(10)).getData()) + .singleElement().isEqualTo(calculatedField); + } + + @Test + public void testGetCalculatedFieldsByFilter() throws Exception { + Device device = createDevice("Device A", "1234567890"); + CalculatedFieldInfo deviceCalculatedField = new CalculatedFieldInfo( + doPost("/api/calculatedField", getSimpleCalculatedField(device.getId()), CalculatedField.class), + "Device A" + ); + + DeviceProfile deviceProfile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class); + CalculatedFieldInfo profileCalculatedField = new CalculatedFieldInfo( + doPost("/api/calculatedField", getSimpleCalculatedField(deviceProfile.getId()), CalculatedField.class), + "Profile A" + ); + + List allCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + null, null, null); + assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); + allCalculatedFields = getCalculatedFields(null, null, null, null); + assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); + + List profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + EntityType.DEVICE_PROFILE, null, null); + assertThat(profileLevelCalculatedFields).containsOnly(profileCalculatedField); + + List specificDeviceCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + EntityType.DEVICE, List.of(device.getUuidId()), null); + assertThat(specificDeviceCalculatedFields).containsOnly(deviceCalculatedField); + + List byNameCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + null, null, List.of(deviceCalculatedField.getName())); + assertThat(byNameCalculatedFields).containsOnly(deviceCalculatedField); + + byNameCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, null, null, + List.of(deviceCalculatedField.getName(), profileCalculatedField.getName())); + assertThat(byNameCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); + + PageData names = getCalculatedFieldNames(CalculatedFieldType.SIMPLE, new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(2); + assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder()); + assertThat(names.getData()).contains(deviceCalculatedField.getName(), profileCalculatedField.getName()); + + names = getCalculatedFieldNames(CalculatedFieldType.SIMPLE, new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.DESC))); + assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder()); + + names = getCalculatedFieldNames(CalculatedFieldType.SIMPLE, new PageLink(10, 0, + device.getId().toString(), new SortOrder("", SortOrder.Direction.DESC))); + assertThat(names.getTotalElements()).isEqualTo(1); + assertThat(names.getData()).containsOnly(deviceCalculatedField.getName()); + } + @Test public void testDeleteCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -127,18 +311,93 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { doGet("/api/calculatedField/" + savedCalculatedField.getId().getId()).andExpect(status().isNotFound()); } - private CalculatedField getCalculatedField(DeviceId deviceId) { + private CalculatedField getSimpleCalculatedField(EntityId entityId) { + return getCalculatedField(entityId, CalculatedFieldType.SIMPLE); + } + + private CalculatedField getCalculatedField(EntityId entityId, CalculatedFieldType cfType) { + return getCalculatedField(entityId, cfType, null); + } + + private CalculatedField getCalculatedField(EntityId entityId, CalculatedFieldType cfType, CalculatedFieldConfiguration customConfiguration) { CalculatedField calculatedField = new CalculatedField(); - calculatedField.setEntityId(deviceId); - calculatedField.setType(CalculatedFieldType.SIMPLE); - calculatedField.setName("Test Calculated Field"); + calculatedField.setEntityId(entityId); + calculatedField.setType(cfType); + calculatedField.setName("Test Calculated Field for " + entityId); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(getCalculatedFieldConfig()); + calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode()); + if (customConfiguration != null) { + calculatedField.setConfiguration(customConfiguration); + } else switch (cfType) { + case SIMPLE -> calculatedField.setConfiguration(getSimpleCalculatedFieldConfig()); + case GEOFENCING -> calculatedField.setConfiguration(getGeofencingCalculatedFieldConfig()); + case PROPAGATION -> calculatedField.setConfiguration(getPropagationCalculatedFieldConfig()); + case ENTITY_AGGREGATION -> calculatedField.setConfiguration(getEntityAggregationCalculatedFieldConfig()); + } calculatedField.setVersion(1L); return calculatedField; } - private CalculatedFieldConfiguration getCalculatedFieldConfig() { + private CalculatedFieldConfiguration getGeofencingCalculatedFieldConfig() { + var config = new GeofencingCalculatedFieldConfiguration(); + + var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "FromSafeArea"))); + + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + + config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude")); + config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration)); + config.setScheduledUpdateEnabled(false); + config.setOutput(new TimeSeriesOutput()); + + return config; + } + + private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig() { + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + return getPropagationCalculatedFieldConfig(Map.of("t", arg)); + } + + private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig(Map arguments) { + var config = new PropagationCalculatedFieldConfiguration(); + + config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + + config.setApplyExpressionToResolvedArguments(false); + config.setExpression(null); + config.setOutput(new TimeSeriesOutput()); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + config.setArguments(arguments); + + return config; + } + + private CalculatedFieldConfiguration getEntityAggregationCalculatedFieldConfig() { + var config = new EntityAggregationCalculatedFieldConfiguration(); + + Argument energyArgument = new Argument(); + energyArgument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null)); + config.setArguments(Map.of("en", energyArgument)); + + AggMetric metric = new AggMetric(); + metric.setInput(new AggKeyInput("en")); + metric.setDefaultValue(9999.0); + config.setMetrics(Map.of("consumption", metric)); + + config.setWatermark(new Watermark(TimeUnit.DAYS.toSeconds(1))); + config.setInterval(new HourInterval("Europe/Kiev", TimeUnit.MINUTES.toSeconds(15))); + + config.setOutput(new TimeSeriesOutput()); + + return config; + } + + private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); Argument argument = new Argument(); @@ -150,9 +409,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index 74bad63d07..dd3ab07ba0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java @@ -33,6 +33,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -43,12 +44,17 @@ 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.customer.CustomerDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -260,6 +266,40 @@ public class CustomerControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testFindCustomersByIds() throws Exception { + List savedCustomers = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Customer customer = new Customer(); + customer.setTitle("My customer " + i); + savedCustomers.add(doPost("/api/customer", customer, Customer.class)); + } + + String idsParam = savedCustomers.stream() + .map(c -> c.getId().getId().toString()) + .collect(Collectors.joining(",")); + + Customer[] foundCustomers = doGet("/api/customers?customerIds=" + idsParam, Customer[].class); + + Assert.assertNotNull(foundCustomers); + Assert.assertEquals(savedCustomers.size(), foundCustomers.length); + + Map foundById = Arrays.stream(foundCustomers) + .collect(Collectors.toMap(c -> c.getId().getId(), Function.identity())); + + for (Customer savedCustomer : savedCustomers) { + UUID id = savedCustomer.getId().getId(); + Customer foundCustomer = foundById.get(id); + Assert.assertNotNull("Customer not found for id " + id, foundCustomer); + Assert.assertEquals(savedCustomer, foundCustomer); + } + + for (Customer savedCustomer : savedCustomers) { + doDelete("/api/customer/" + savedCustomer.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + @Test public void testDeleteCustomer() throws Exception { Customer customer = new Customer(); @@ -462,6 +502,27 @@ public class CustomerControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId); } + @Test + public void testSaveCustomerWithUniquifyStrategy() throws Exception { + Customer customer = new Customer(); + customer.setTitle("My unique customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + doPost("/api/customer?nameConflictPolicy=FAIL", customer).andExpect(status().isBadRequest()); + + Customer secondCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY", customer, Customer.class); + assertThat(secondCustomer.getName()).startsWith("My unique customer_"); + + Customer thirdCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", customer, Customer.class); + assertThat(thirdCustomer.getName()).startsWith("My unique customer-"); + + Customer fourthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class); + assertThat(fourthCustomer.getName()).isEqualTo("My unique customer_1"); + + Customer fifthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class); + assertThat(fifthCustomer.getName()).isEqualTo("My unique customer_2"); + } + private Customer createCustomer(String title) { Customer customer = new Customer(); customer.setTitle(title); diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index ff904bb948..27fcabbd2c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -51,13 +51,16 @@ 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.dashboard.DashboardDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -155,6 +158,42 @@ public class DashboardControllerTest extends AbstractControllerTest { Assert.assertEquals(savedDashboard, foundDashboard); } + @Test + public void testFindDashboardInfosByIds() throws Exception { + List dashboards = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard " + i); + dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class)); + } + + List expected = dashboards.subList(5, 15); + + String idsParam = expected.stream() + .map(d -> d.getId().getId().toString()) + .collect(Collectors.joining(",")); + + DashboardInfo[] result = doGet( + "/api/dashboards?dashboardIds=" + idsParam, + DashboardInfo[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map infoById = Arrays.stream(result) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (Dashboard dashboard : expected) { + UUID id = dashboard.getId().getId(); + DashboardInfo info = infoById.get(id); + Assert.assertNotNull("DashboardInfo not found for id " + id, info); + + Assert.assertEquals(dashboard.getId(), info.getId()); + Assert.assertEquals(dashboard.getTitle(), info.getTitle()); + } + } + @Test public void testDeleteDashboard() throws Exception { Dashboard dashboard = new Dashboard(); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index 7d78877b9e..9a80ad2e25 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -95,7 +95,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId coapDeviceProfileId; - @Value("${device.connectivity.gateway.image_version:3.7-stable}") + @Value("${device.connectivity.gateway.image_version:3.8-stable}") private String gatewayImageVersion; @Before @@ -321,9 +321,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "\n" + " # Environment variables\n" + " environment:\n" + - " - host=host.docker.internal\n" + - " - port=1883\n" + - " - accessToken=" + credentials.getCredentialsId() + "\n" + + " - TB_GW_HOST=host.docker.internal\n" + + " - TB_GW_PORT=1883\n" + + " - TB_GW_SECURITY_TYPE=accessToken\n" + + " - TB_GW_ACCESS_TOKEN=" + credentials.getCredentialsId() + "\n" + "\n" + " # Volumes bind\n" + " volumes:\n" + diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 0e404b2857..07b68210bb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -38,6 +38,7 @@ import org.springframework.test.context.ContextConfiguration; import org.testcontainers.shaded.org.awaitility.Awaitility; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -59,6 +60,7 @@ 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.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -70,15 +72,19 @@ import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumn import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.utils.CsvUtils; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -1629,6 +1635,57 @@ public class DeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(newAttributeValue, actualAttribute.get("value")); } + @Test + public void testBulkImportDeviceWithJsonAttr() throws Exception { + String deviceName = "some_device"; + String deviceType = "some_type"; + String deviceAttr = "{\"threshold\":45}"; + + List> content = new LinkedList<>(); + content.add(Arrays.asList("NAME", "TYPE", "ATTR")); + content.add(Arrays.asList(deviceName, deviceType, deviceAttr)); + + byte[] bytes = CsvUtils.generateCsv(content); + BulkImportRequest request = new BulkImportRequest(); + request.setFile(new String(bytes, StandardCharsets.UTF_8)); + BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping(); + BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping(); + name.setType(BulkImportColumnType.NAME); + BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping(); + type.setType(BulkImportColumnType.TYPE); + BulkImportRequest.ColumnMapping attr = new BulkImportRequest.ColumnMapping(); + attr.setType(BulkImportColumnType.SERVER_ATTRIBUTE); + attr.setKey("attr"); + List columns = new ArrayList<>(); + columns.add(name); + columns.add(type); + columns.add(attr); + + mapping.setColumns(columns); + mapping.setDelimiter(','); + mapping.setUpdate(true); + mapping.setHeader(true); + request.setMapping(mapping); + + BulkImportResult deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {}); + + Assert.assertEquals(1, deviceBulkImportResult.getCreated().get()); + Assert.assertEquals(0, deviceBulkImportResult.getErrors().get()); + Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get()); + Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); + + Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); + + Assert.assertNotNull(savedDevice); + Assert.assertEquals(deviceName, savedDevice.getName()); + Assert.assertEquals(deviceType, savedDevice.getType()); + + Optional retrieved = await().atMost(5, TimeUnit.SECONDS) + .until(() -> attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get(), Optional::isPresent); + assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr); + assertThat(retrieved.get().getStrValue()).isNotPresent(); + } + @Test public void testSaveDeviceWithOutdatedVersion() throws Exception { Device device = createDevice("Device v1.0"); @@ -1651,11 +1708,28 @@ public class DeviceControllerTest extends AbstractControllerTest { assertThat(device.getVersion()).isEqualTo(3); } - private Device createDevice(String name) { + @Test + public void testSaveDeviceWithUniquifyStrategy() throws Exception { Device device = new Device(); - device.setName(name); + device.setName("My unique device"); device.setType("default"); - return doPost("/api/device", device, Device.class); + Device savedDevice = doPost("/api/device", device, Device.class); + + doPost("/api/device", device).andExpect(status().isBadRequest()); + + doPost("/api/device?nameConflictPolicy=FAIL", device).andExpect(status().isBadRequest()); + + Device secondDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY", device, Device.class); + assertThat(secondDevice.getName()).startsWith("My unique device_"); + + Device thirdDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", device, Device.class); + assertThat(thirdDevice.getName()).startsWith("My unique device-"); + + Device fourthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class); + assertThat(fourthDevice.getName()).isEqualTo("My unique device_1"); + + Device fifthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class); + assertThat(fifthDevice.getName()).isEqualTo("My unique device_2"); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index 225757e451..0ee1a65cd8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -52,13 +53,18 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.device.DeviceProfileDao; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -89,6 +95,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { public DeviceProfileDao deviceProfileDao(DeviceProfileDao deviceProfileDao) { return Mockito.mock(DeviceProfileDao.class, AdditionalAnswers.delegatesTo(deviceProfileDao)); } + } @Before @@ -214,6 +221,42 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType()); } + @Test + public void testFindDeviceProfileInfosByIds() throws Exception { + List deviceProfiles = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile " + i); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); + } + + List expected = deviceProfiles.subList(5, 15); + + String idsParam = expected.stream() + .map(dp -> dp.getId().getId().toString()) + .collect(Collectors.joining(",")); + + DeviceProfileInfo[] result = doGet( + "/api/deviceProfileInfos?deviceProfileIds=" + idsParam, + DeviceProfileInfo[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map infoById = Arrays.stream(result) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (DeviceProfile dp : expected) { + UUID id = dp.getId().getId(); + DeviceProfileInfo info = infoById.get(id); + Assert.assertNotNull("DeviceProfileInfo not found for id " + id, info); + + Assert.assertEquals(dp.getId(), info.getId()); + Assert.assertEquals(dp.getName(), info.getName()); + Assert.assertEquals(dp.getType(), info.getType()); + } + } + @Test public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); @@ -600,364 +643,364 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testSaveProtoDeviceProfileWithInvalidProtoFile() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message SchemaValidationTest {\n" + - " required int32 parameter = 1;\n" + - "}", "[Transport Configuration] failed to parse attributes proto schema due to: Syntax error in :6:4: 'required' label forbidden in proto3 field declarations"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message SchemaValidationTest {\n" + + " required int32 parameter = 1;\n" + + "}", "[Transport Configuration] failed to parse attributes proto schema due to: Syntax error in :6:4: 'required' label forbidden in proto3 field declarations"); } @Test public void testSaveProtoDeviceProfileWithInvalidProtoSyntax() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto2\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message SchemaValidationTest {\n" + - " required int32 parameter = 1;\n" + - "}", "[Transport Configuration] invalid schema syntax: proto2 for attributes proto schema provided! Only proto3 allowed!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message SchemaValidationTest {\n" + + " required int32 parameter = 1;\n" + + "}", "[Transport Configuration] invalid schema syntax: proto2 for attributes proto schema provided! Only proto3 allowed!"); } @Test public void testSaveProtoDeviceProfileOptionsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "option java_package = \"com.test.schemavalidation\";\n" + - "option java_multiple_files = true;\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message SchemaValidationTest {\n" + - " optional int32 parameter = 1;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Schema options don't support!"); + "\n" + + "option java_package = \"com.test.schemavalidation\";\n" + + "option java_multiple_files = true;\n" + + "\n" + + "package schemavalidation;\n" + + "\n" + + "message SchemaValidationTest {\n" + + " optional int32 parameter = 1;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema options don't support!"); } @Test public void testSaveProtoDeviceProfilePublicImportsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "import public \"oldschema.proto\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message SchemaValidationTest {\n" + - " optional int32 parameter = 1;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Schema public imports don't support!"); + "\n" + + "import public \"oldschema.proto\";\n" + + "\n" + + "package schemavalidation;\n" + + "\n" + + "message SchemaValidationTest {\n" + + " optional int32 parameter = 1;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema public imports don't support!"); } @Test public void testSaveProtoDeviceProfileImportsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "import \"oldschema.proto\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message SchemaValidationTest {\n" + - " optional int32 parameter = 1;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Schema imports don't support!"); + "\n" + + "import \"oldschema.proto\";\n" + + "\n" + + "package schemavalidation;\n" + + "\n" + + "message SchemaValidationTest {\n" + + " optional int32 parameter = 1;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema imports don't support!"); } @Test public void testSaveProtoDeviceProfileExtendDeclarationsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "extend google.protobuf.MethodOptions {\n" + - " MyMessage my_method_option = 50007;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Schema extend declarations don't support!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "extend google.protobuf.MethodOptions {\n" + + " MyMessage my_method_option = 50007;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema extend declarations don't support!"); } @Test public void testSaveProtoDeviceProfileEnumOptionsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "enum testEnum {\n" + - " option allow_alias = true;\n" + - " DEFAULT = 0;\n" + - " STARTED = 1;\n" + - " RUNNING = 2;\n" + - "}\n" + - "\n" + - "message testMessage {\n" + - " optional int32 parameter = 1;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Enum definitions options are not supported!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "enum testEnum {\n" + + " option allow_alias = true;\n" + + " DEFAULT = 0;\n" + + " STARTED = 1;\n" + + " RUNNING = 2;\n" + + "}\n" + + "\n" + + "message testMessage {\n" + + " optional int32 parameter = 1;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Enum definitions options are not supported!"); } @Test public void testSaveProtoDeviceProfileNoOneMessageTypeExists() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "enum testEnum {\n" + - " DEFAULT = 0;\n" + - " STARTED = 1;\n" + - " RUNNING = 2;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! At least one Message definition should exists!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "enum testEnum {\n" + + " DEFAULT = 0;\n" + + " STARTED = 1;\n" + + " RUNNING = 2;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! At least one Message definition should exists!"); } @Test public void testSaveProtoDeviceProfileMessageTypeOptionsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message testMessage {\n" + - " option allow_alias = true;\n" + - " optional int32 parameter = 1;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition options don't support!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message testMessage {\n" + + " option allow_alias = true;\n" + + " optional int32 parameter = 1;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition options don't support!"); } @Test public void testSaveProtoDeviceProfileMessageTypeExtensionsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message TestMessage {\n" + - " extensions 100 to 199;\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition extensions don't support!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message TestMessage {\n" + + " extensions 100 to 199;\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition extensions don't support!"); } @Test public void testSaveProtoDeviceProfileMessageTypeReservedElementsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message Foo {\n" + - " reserved 2, 15, 9 to 11;\n" + - " reserved \"foo\", \"bar\";\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition reserved elements don't support!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message Foo {\n" + + " reserved 2, 15, 9 to 11;\n" + + " reserved \"foo\", \"bar\";\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition reserved elements don't support!"); } @Test public void testSaveProtoDeviceProfileMessageTypeGroupsElementsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message TestMessage {\n" + - " repeated group Result = 1 {\n" + - " optional string url = 2;\n" + - " optional string title = 3;\n" + - " repeated string snippets = 4;\n" + - " }\n" + - "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition groups don't support!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message TestMessage {\n" + + " repeated group Result = 1 {\n" + + " optional string url = 2;\n" + + " optional string title = 3;\n" + + " repeated string snippets = 4;\n" + + " }\n" + + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition groups don't support!"); } @Test public void testSaveProtoDeviceProfileOneOfsGroupsElementsNotSupported() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message SampleMessage {\n" + - " oneof test_oneof {\n" + - " string name = 1;\n" + - " group Result = 2 {\n" + - " \tstring url = 3;\n" + - " \tstring title = 4;\n" + - " \trepeated string snippets = 5;\n" + - " }\n" + - " }" + - "}", "[Transport Configuration] invalid attributes proto schema provided! OneOf definition groups don't support!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message SampleMessage {\n" + + " oneof test_oneof {\n" + + " string name = 1;\n" + + " group Result = 2 {\n" + + " \tstring url = 3;\n" + + " \tstring title = 4;\n" + + " \trepeated string snippets = 5;\n" + + " }\n" + + " }" + + "}", "[Transport Configuration] invalid attributes proto schema provided! OneOf definition groups don't support!"); } @Test public void testSaveProtoDeviceProfileWithInvalidTelemetrySchemaTsField() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message PostTelemetry {\n" + - " int64 ts = 1;\n" + - " Values values = 2;\n" + - " \n" + - " message Values {\n" + - " string key1 = 3;\n" + - " bool key2 = 4;\n" + - " double key3 = 5;\n" + - " int32 key4 = 6;\n" + - " JsonObject key5 = 7;\n" + - " }\n" + - " \n" + - " message JsonObject {\n" + - " optional int32 someNumber = 8;\n" + - " repeated int32 someArray = 9;\n" + - " NestedJsonObject someNestedObject = 10;\n" + - " message NestedJsonObject {\n" + - " optional string key = 11;\n" + - " }\n" + - " }\n" + - "}", "[Transport Configuration] invalid telemetry proto schema provided! Field 'ts' has invalid label. Field 'ts' should have optional keyword!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message PostTelemetry {\n" + + " int64 ts = 1;\n" + + " Values values = 2;\n" + + " \n" + + " message Values {\n" + + " string key1 = 3;\n" + + " bool key2 = 4;\n" + + " double key3 = 5;\n" + + " int32 key4 = 6;\n" + + " JsonObject key5 = 7;\n" + + " }\n" + + " \n" + + " message JsonObject {\n" + + " optional int32 someNumber = 8;\n" + + " repeated int32 someArray = 9;\n" + + " NestedJsonObject someNestedObject = 10;\n" + + " message NestedJsonObject {\n" + + " optional string key = 11;\n" + + " }\n" + + " }\n" + + "}", "[Transport Configuration] invalid telemetry proto schema provided! Field 'ts' has invalid label. Field 'ts' should have optional keyword!"); } @Test public void testSaveProtoDeviceProfileWithInvalidTelemetrySchemaTsDateType() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message PostTelemetry {\n" + - " optional int32 ts = 1;\n" + - " Values values = 2;\n" + - " \n" + - " message Values {\n" + - " string key1 = 3;\n" + - " bool key2 = 4;\n" + - " double key3 = 5;\n" + - " int32 key4 = 6;\n" + - " JsonObject key5 = 7;\n" + - " }\n" + - " \n" + - " message JsonObject {\n" + - " optional int32 someNumber = 8;\n" + - " }\n" + - "}", "[Transport Configuration] invalid telemetry proto schema provided! Field 'ts' has invalid data type. Only int64 type is supported!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message PostTelemetry {\n" + + " optional int32 ts = 1;\n" + + " Values values = 2;\n" + + " \n" + + " message Values {\n" + + " string key1 = 3;\n" + + " bool key2 = 4;\n" + + " double key3 = 5;\n" + + " int32 key4 = 6;\n" + + " JsonObject key5 = 7;\n" + + " }\n" + + " \n" + + " message JsonObject {\n" + + " optional int32 someNumber = 8;\n" + + " }\n" + + "}", "[Transport Configuration] invalid telemetry proto schema provided! Field 'ts' has invalid data type. Only int64 type is supported!"); } @Test public void testSaveProtoDeviceProfileWithInvalidTelemetrySchemaValuesDateType() throws Exception { testSaveDeviceProfileWithInvalidProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message PostTelemetry {\n" + - " optional int64 ts = 1;\n" + - " string values = 2;\n" + - " \n" + - "}", "[Transport Configuration] invalid telemetry proto schema provided! Field 'values' has invalid data type. Only message type is supported!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message PostTelemetry {\n" + + " optional int64 ts = 1;\n" + + " string values = 2;\n" + + " \n" + + "}", "[Transport Configuration] invalid telemetry proto schema provided! Field 'values' has invalid data type. Only message type is supported!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaMethodDateType() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional int32 method = 1;\n" + - " optional int32 requestId = 2;\n" + - " optional string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'method' has invalid data type. Only string type is supported!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional int32 method = 1;\n" + + " optional int32 requestId = 2;\n" + + " optional string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'method' has invalid data type. Only string type is supported!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaRequestIdDateType() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional string method = 1;\n" + - " optional int64 requestId = 2;\n" + - " optional string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'requestId' has invalid data type. Only int32 type is supported!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional string method = 1;\n" + + " optional int64 requestId = 2;\n" + + " optional string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'requestId' has invalid data type. Only int32 type is supported!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaMethodLabel() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " repeated string method = 1;\n" + - " optional int32 requestId = 2;\n" + - " optional string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'method' has invalid label!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " repeated string method = 1;\n" + + " optional int32 requestId = 2;\n" + + " optional string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'method' has invalid label!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaRequestIdLabel() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional string method = 1;\n" + - " repeated int32 requestId = 2;\n" + - " optional string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'requestId' has invalid label!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional string method = 1;\n" + + " repeated int32 requestId = 2;\n" + + " optional string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'requestId' has invalid label!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaParamsLabel() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional string method = 1;\n" + - " optional int32 requestId = 2;\n" + - " repeated string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'params' has invalid label!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional string method = 1;\n" + + " optional int32 requestId = 2;\n" + + " repeated string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Field 'params' has invalid label!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaFieldsCount() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional int32 requestId = 2;\n" + - " optional string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! RpcRequestMsg message should always contains 3 fields: method, requestId and params!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional int32 requestId = 2;\n" + + " optional string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! RpcRequestMsg message should always contains 3 fields: method, requestId and params!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaFieldMethodIsNoSet() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional string methodName = 1;\n" + - " optional int32 requestId = 2;\n" + - " optional string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Failed to get field descriptor for field: method!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional string methodName = 1;\n" + + " optional int32 requestId = 2;\n" + + " optional string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Failed to get field descriptor for field: method!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaFieldRequestIdIsNotSet() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional string method = 1;\n" + - " optional int32 requestIdentifier = 2;\n" + - " optional string params = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Failed to get field descriptor for field: requestId!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional string method = 1;\n" + + " optional int32 requestIdentifier = 2;\n" + + " optional string params = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Failed to get field descriptor for field: requestId!"); } @Test public void testSaveProtoDeviceProfileWithInvalidRpcRequestSchemaFieldParamsIsNotSet() throws Exception { testSaveDeviceProfileWithInvalidRpcRequestProtoSchema("syntax =\"proto3\";\n" + - "\n" + - "package schemavalidation;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional string method = 1;\n" + - " optional int32 requestId = 2;\n" + - " optional string parameters = 3;\n" + - " \n" + - "}", "[Transport Configuration] invalid rpc request proto schema provided! Failed to get field descriptor for field: params!"); + "\n" + + "package schemavalidation;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional string method = 1;\n" + + " optional int32 requestId = 2;\n" + + " optional string parameters = 3;\n" + + " \n" + + "}", "[Transport Configuration] invalid rpc request proto schema provided! Failed to get field descriptor for field: params!"); } @Test @@ -1028,11 +1071,17 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(protoTransportPayloadConfiguration, false); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - Mockito.reset(tbClusterService, auditLogService); - - doPost("/api/deviceProfile", deviceProfile) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString(errorMsg))); + // The request may hit a transient TenantNotFoundException right after the @Before tenant creation + // if the tenant profile cache is not yet warmed up for the newly created tenant. Retry until the + // request returns the expected 400 Bad Request for the invalid schema. Mockito.reset is inside the + // retry loop so the subsequent verify* assertions see only the invocations from the last attempt. + Awaitility.await().atMost(10, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS) + .ignoreExceptions().untilAsserted(() -> { + Mockito.reset(tbClusterService, auditLogService); + doPost("/api/deviceProfile", deviceProfile) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(errorMsg))); + }); testNotifyEntityEqualsOneTimeServiceNeverError(deviceProfile, savedTenant.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(errorMsg)); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index fc65d67949..9d50e3a62c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -75,7 +75,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.edge.EdgeDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edge.imitator.EdgeImitator; diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java index 61d821ba83..fbd7d73421 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.ArrayList; import java.util.Collections; @@ -62,8 +64,11 @@ public class EdqsControllerTest extends AbstractControllerTest { @Autowired private JdbcTemplate jdbcTemplate; + @MockitoBean + private EdqsRocksDb edqsRocksDb; + @Before - public void beforeEdqsControllerTest() throws Exception { + public void before() throws Exception { loginTenantAdmin(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 932980a3ca..6c1a63194c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -19,11 +19,11 @@ import org.assertj.core.api.ThrowingConsumer; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; +import org.awaitility.core.ThrowingRunnable; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.edqs.util.EdqsRocksDb; import org.thingsboard.server.queue.discovery.DiscoveryService; import java.util.concurrent.TimeUnit; @@ -59,9 +58,6 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { @Autowired private DiscoveryService discoveryService; - @MockBean // so that we don't do backup for tests - private EdqsRocksDb edqsRocksDb; - @Before public void before() { await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.getState().isApiEnabled()); @@ -91,6 +87,11 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { result -> result == expectedResult); } + @Override + protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(assertion); + } + @Test public void testEdqsState() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index e557626ee7..5de4c9ffcf 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -17,13 +17,19 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.awaitility.core.ThrowingRunnable; 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.test.context.TestPropertySource; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; @@ -42,12 +48,23 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; 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.kv.BaseAttributeKvEntry; +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.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; 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.AliasEntityId; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -75,14 +92,18 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -106,6 +127,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Autowired private QueueStatsService queueStatsService; + @Autowired + private BaseEntityService baseEntityService; + + @MockitoBean + private EdqsRocksDb edqsRocksDb; @Before public void beforeTest() throws Exception { @@ -495,19 +521,22 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List alarmFields = new ArrayList<>(); alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "originatorDisplayName")); EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); - AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); - List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); + List retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); + List retrievedAlarmDisplayName = alarmPageData.getData().stream().map(AlarmData::getOriginatorDisplayName).toList(); + assertThat(retrievedAlarmDisplayName).containsExactlyInAnyOrderElementsOf(assets.stream().map(Asset::getLabel).toList()); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { }); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } @@ -569,16 +598,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); - AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); - PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); + PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { }); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } @@ -634,7 +663,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter deviceTypeFilter = new EntityTypeFilter(); deviceTypeFilter.setEntityType(EntityType.DEVICE); - AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); + AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); PageData alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); List retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); @@ -1132,6 +1161,119 @@ public class EntityQueryControllerTest extends AbstractControllerTest { countByQueryAndCheck(customerEntitiesQuery, 0); } + @Test + public void testFindDevicesByDisplayName() throws Exception { + loginTenantAdmin(); + int numOfDevices = 3; + + for (int i = 0; i < numOfDevices; i++) { + Device device = new Device(); + String name = "Device" + i; + device.setName(name); + device.setLabel("Device Label " + i); + device.setType("testFindDevicesByDisplayName"); + + Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("testFindDevicesByDisplayName")); + filter.setDeviceNameFilter(""); + + KeyFilter displayNameFilter = getEntityFieldEqualFilter("displayName", "Device Label " + 0); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + // all devices with ownerName = TEST TENANT + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), Collections.emptyList()); + checkEntitiesByQuery(query, numOfDevices, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + + // all devices with ownerName = TEST TENANT + EntityDataQuery displayNameFilterQuery = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(displayNameFilter)); + checkEntitiesByQuery(displayNameFilterQuery, 1, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + } + + @Test + public void testFindUsersByDisplayName() throws Exception { + loginTenantAdmin(); + + User userA = new User(); + userA.setAuthority(Authority.TENANT_ADMIN); + userA.setFirstName("John"); + userA.setLastName("Doe"); + userA.setEmail("john.doe@tb.org"); + userA = doPost("/api/user", userA, User.class); + var aId = userA.getId(); + + User userB = new User(); + userB.setAuthority(Authority.TENANT_ADMIN); + userB.setFirstName("John"); + userB.setEmail("john@tb.org"); + userB = doPost("/api/user", userB, User.class); + var bId = userB.getId(); + + User userC = new User(); + userC.setAuthority(Authority.TENANT_ADMIN); + userC.setLastName("Doe"); + userC.setEmail("doe@tb.org"); + userC = doPost("/api/user", userC, User.class); + var cId = userC.getId(); + + User userD = new User(); + userD.setAuthority(Authority.TENANT_ADMIN); + userD.setEmail("noname@tb.org"); + userD = doPost("/api/user", userD, User.class); + var dId = userD.getId(); + + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(EntityType.USER); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(aId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(bId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(cId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "noname@tb.org"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(dId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("noname@tb.org", displayName); + }); + } + @Test public void testFindDevicesByOwnerNameAndOwnerType() throws Exception { loginTenantAdmin(); @@ -1169,19 +1311,30 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // all devices with ownerName = TEST TENANT EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); - checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT"); + BiConsumer checkFunction = (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue(); + String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue(); + String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue(); + + Assert.assertEquals("Device" + i, name); + Assert.assertEquals(TEST_TENANT_NAME, ownerName); + Assert.assertEquals("TENANT", ownerType); + Assert.assertEquals("1" + i, alarmActiveTime); + }; + checkEntitiesByQuery(query, numOfDevices, checkFunction); // all devices with wrong ownerName EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); - checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null); + checkEntitiesByQuery(wrongTenantNameQuery, 0, null); // all devices with owner type = TENANT EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); - checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT"); + checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, checkFunction); // all devices with owner type = CUSTOMER EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); - checkEntitiesByQuery(customerEntitiesQuery, 0, null, null); + checkEntitiesByQuery(customerEntitiesQuery, 0, null); } @Test @@ -1227,6 +1380,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest { findByQueryAndCheck(query, 0); } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { + await() + .alias("data by query") + .atMost(30, TimeUnit.SECONDS) + .until(() -> { + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + return loadedEntities.size() == expectedNumOfDevices; + }); + if (expectedNumOfDevices == 0) { + return; + } + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + + Assert.assertEquals(expectedNumOfDevices, loadedEntities.size()); + + for (int i = 0; i < expectedNumOfDevices; i++) { + checkFunction.accept(i, loadedEntities.get(i)); + } + } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { await() .alias("data by query") @@ -1299,6 +1474,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return result; } + protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) throws Throwable { + assertion.run(); + } + private KeyFilter getEntityFieldEqualFilter(String keyName, String value) { return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.EQUAL); } @@ -1339,4 +1518,740 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return nameFilter; } + // --- findAvailableEntityKeysV2 tests --- + + @Test + public void testFindAvailableKeysByQueryV2() throws Throwable { + // GIVEN — two devices matched by query; a third device should not be matched + var device1 = createDevice("Test device 1"); + var device2 = createDevice("Test device 2"); + var unmatchedDevice = createDevice("Unmatched device"); + + // unmatched device has unique keys that must NOT appear in the result + postTelemetry(unmatchedDevice.getId(), new BasicTsKvEntry(9000, new DoubleDataEntry("unmatchedTs", 999.0))); + postAttributes(unmatchedDevice.getId(), AttributeScope.SHARED_SCOPE, new StringDataEntry("unmatchedAttr", "nope")); + + // device1: timeseries1 (Double) with two data points, and timeseries2 older data point + postTelemetry(device1.getId(), new BasicTsKvEntry(1000, new DoubleDataEntry("timeseries1", 10.0))); + postTelemetry(device1.getId(), new BasicTsKvEntry(2000, new DoubleDataEntry("timeseries1", 20.5))); + postTelemetry(device1.getId(), new BasicTsKvEntry(1000, new LongDataEntry("timeseries2", 100L))); + + // device2: timeseries2 (Long) with a newer data point, and timeseries3 only on this device + postTelemetry(device2.getId(), new BasicTsKvEntry(3000, new LongDataEntry("timeseries2", 300L))); + postTelemetry(device2.getId(), new BasicTsKvEntry(5000, new DoubleDataEntry("timeseries3", 99.9))); + + // device1: SHARED_SCOPE attributes + postAttributes(device1.getId(), AttributeScope.SHARED_SCOPE, + new BooleanDataEntry("sharedAttribute1", true), new DoubleDataEntry("sharedAttribute2", 3.14)); + + // device2: CLIENT_SCOPE attributes (saved via service to bypass API restriction) + attributesService.save(tenantId, device2.getId(), AttributeScope.CLIENT_SCOPE, List.of( + new BaseAttributeKvEntry(new JsonDataEntry("clientAttribute1", "{\"key\":\"val\"}"), System.currentTimeMillis()), + new BaseAttributeKvEntry(new BooleanDataEntry("clientAttribute2", false), System.currentTimeMillis()) + )).get(); + + // device1 also has SERVER_SCOPE attributes (should be omitted by scope filter) + postAttributes(device1.getId(), AttributeScope.SERVER_SCOPE, + new StringDataEntry("serverAttribute1", "sv1"), new LongDataEntry("serverAttribute2", 42L)); + + // WHEN — query matches both devices; request timeseries + only SHARED and CLIENT attribute scopes + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter("Test device"); + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, null); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, List.of(), null, null); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, + true, true, List.of(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE), true); + + assertThat(result.entityTypes()).containsExactly(EntityType.DEVICE); + + // timeseries: keys collected from both devices, samples contain the freshest data points + assertThat(result.timeseries()).extracting(KeyInfo::key) + .containsExactly("timeseries1", "timeseries2", "timeseries3"); + assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNotNull()); + assertKeySample(result.timeseries(), "timeseries1", new DoubleNode(20.5), 2000); // from device1 + assertKeySample(result.timeseries(), "timeseries2", new IntNode(300), 3000); // from device2 (newer) + assertKeySample(result.timeseries(), "timeseries3", new DoubleNode(99.9), 5000); // only on device2 + + // SERVER_SCOPE must be fully omitted from the response + assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE); + + // SHARED_SCOPE: from device1 (alphabetical order) + assertThat(result.attributes().get(AttributeScope.SHARED_SCOPE)) + .extracting(KeyInfo::key).containsExactly("sharedAttribute1", "sharedAttribute2"); + assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute1", BooleanNode.TRUE); + assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute2", new DoubleNode(3.14)); + + // CLIENT_SCOPE: from device2 (alphabetical order) + assertThat(result.attributes().get(AttributeScope.CLIENT_SCOPE)) + .extracting(KeyInfo::key).containsExactly("clientAttribute1", "clientAttribute2"); + assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute1", JacksonUtil.toJsonNode("{\"key\":\"val\"}")); + assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute2", BooleanNode.FALSE); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_withoutSamples() throws Throwable { + // GIVEN + var device = createDevice("Test device"); + postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), true, true, null, false); + + assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNull()); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .allSatisfy(ki -> assertThat(ki.sample()).isNull()); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_timeseriesOnly() throws Throwable { + // GIVEN + var device = createDevice("Test device"); + postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), true, false, null, false); + + assertThat(result.timeseries()).extracting(KeyInfo::key).contains("temperature"); + assertThat(result.attributes()).isNull(); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_attributesOnly() throws Throwable { + // GIVEN + var device = createDevice("Test device"); + postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), false, true, null, false); + + assertThat(result.timeseries()).isNull(); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .extracting(KeyInfo::key).contains("firmware"); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_noMatchingEntities() throws Throwable { + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("NonExistentDevice_" + UUID.randomUUID()), true, true, null, true); + + assertThat(result.entityTypes()).isEmpty(); + assertThat(result.timeseries()).isEmpty(); + assertThat(result.attributes()).isEmpty(); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_assetUsesServerScopeOnly() throws Throwable { + // GIVEN + var asset = new Asset(); + asset.setName("Test asset"); + asset.setType("default"); + asset = doPost("/api/asset", asset, Asset.class); + postAttributes(asset.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("location", "warehouse")); + + // WHEN + var filter = new SingleEntityFilter(); + filter.setSingleEntity(AliasEntityId.fromEntityId(asset.getId())); + var query = new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, false, true, null, false); + + assertThat(result.entityTypes()).containsExactly(EntityType.ASSET); + assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SERVER_SCOPE); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .extracting(KeyInfo::key).containsExactly("location"); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_rejectsWhenNoKeyTypeRequested() throws Exception { + // WHEN / THEN + EntityDataQuery query = buildDeviceQuery("NonExistent"); + + doPostAsync("/api/v2/entitiesQuery/find/keys?includeTimeseries=false&includeAttributes=false", + query, 30_000L).andExpect(status().isBadRequest()); + } + + protected AvailableEntityKeysV2 findAvailableEntityKeysByQueryV2(EntityDataQuery query, + boolean includeTimeseries, boolean includeAttributes, + List scopes, boolean includeSamples) throws Exception { + StringBuilder url = new StringBuilder("/api/v2/entitiesQuery/find/keys?") + .append("includeTimeseries=").append(includeTimeseries) + .append("&includeAttributes=").append(includeAttributes) + .append("&includeSamples=").append(includeSamples); + if (scopes != null) { + for (AttributeScope scope : scopes) { + url.append("&scopes=").append(scope); + } + } + return doPostAsyncWithTypedResponse(url.toString(), query, + new TypeReference<>() {}, status().isOk()); + } + + private static void assertKeySample(List keys, String expectedKey, JsonNode expectedValue, long expectedTs) { + KeyInfo keyInfo = findKeyInfo(keys, expectedKey); + assertThat(keyInfo.sample()).isNotNull(); + assertThat(keyInfo.sample().value()).isEqualTo(expectedValue); + assertThat(keyInfo.sample().ts()).isEqualTo(expectedTs); + } + + private static void assertKeySample(List keys, String expectedKey, JsonNode expectedValue) { + KeyInfo keyInfo = findKeyInfo(keys, expectedKey); + assertThat(keyInfo.sample()).isNotNull(); + assertThat(keyInfo.sample().value()).isEqualTo(expectedValue); + assertThat(keyInfo.sample().ts()).isGreaterThan(0); + } + + private static KeyInfo findKeyInfo(List keys, String key) { + return keys.stream() + .filter(ki -> ki.key().equals(key)).findFirst().orElseThrow(); + } + + private static EntityDataQuery buildDeviceQuery(String deviceName) { + var filter = new DeviceTypeFilter(); + filter.setDeviceTypes(Collections.singletonList("default")); + filter.setDeviceNameFilter(deviceName); + return new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null); + } + + // --- OR conditions tests --- + + private Device createDeviceWithSharedAttributes(String name, String type, String sharedAttributesPayload) throws Exception { + Device device = new Device(); + device.setName(name); + device.setType(type); + device = doPost("/api/device", device, Device.class); + if (sharedAttributesPayload != null) { + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, + sharedAttributesPayload, String.class, status().isOk()); + } + return device; + } + + private Device createDeviceWithTimeseries(String name, String type, String timeseriesPayload) throws Exception { + Device device = new Device(); + device.setName(name); + device.setType(type); + device = doPost("/api/device", device, Device.class); + JsonNode payload = JacksonUtil.toJsonNode(timeseriesPayload); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", payload) + .andExpect(status().isOk()); + return device; + } + + private Alarm createAlarm(DeviceId originator, String type, AlarmSeverity severity) throws Exception { + Alarm alarm = new Alarm(); + alarm.setOriginator(originator); + alarm.setType(type); + alarm.setSeverity(severity); + return doPost("/api/alarm", alarm, Alarm.class); + } + + private static DeviceTypeFilter deviceTypeFilter(String type) { + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of(type)); + filter.setDeviceNameFilter(""); + return filter; + } + + private static KeyFilter numericKeyFilter(EntityKeyType keyType, String key, + NumericFilterPredicate.NumericOperation operation, double value) { + KeyFilter keyFilter = new KeyFilter(); + keyFilter.setKey(new EntityKey(keyType, key)); + keyFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(value)); + predicate.setOperation(operation); + keyFilter.setPredicate(predicate); + return keyFilter; + } + + private static KeyFilter numericAttributeKeyFilter(String key, + NumericFilterPredicate.NumericOperation operation, double value) { + return numericKeyFilter(EntityKeyType.ATTRIBUTE, key, operation, value); + } + + private static KeyFilter stringKeyFilter(EntityKeyType keyType, String key, + StringFilterPredicate.StringOperation operation, String value) { + KeyFilter keyFilter = new KeyFilter(); + keyFilter.setKey(new EntityKey(keyType, key)); + keyFilter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromString(value)); + predicate.setOperation(operation); + keyFilter.setPredicate(predicate); + return keyFilter; + } + + private static KeyFilter stringAttributeKeyFilter(String key, + StringFilterPredicate.StringOperation operation, String value) { + return stringKeyFilter(EntityKeyType.ATTRIBUTE, key, operation, value); + } + + private static EntityDataPageLink pageLinkSortedByName(int pageSize, int page, String textSearch) { + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC); + return new EntityDataPageLink(pageSize, page, textSearch, sortOrder); + } + + private static List nameEntityField() { + return Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + } + + private static List extractNames(PageData result) { + return result.getData().stream() + .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) + .collect(Collectors.toList()); + } + + @Test + public void testCountEntitiesWithOrKeyFiltersOperation() throws Exception { + String type = "orTestType"; + createDeviceWithSharedAttributes("OrTestDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrTestDeviceB", type, "{\"temperature\":5}"); + createDeviceWithSharedAttributes("OrTestDeviceC", type, "{\"temperature\":30}"); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // OR: deviceA (60>50) and deviceB (5<10) match => count=2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); + + // AND: no device has temperature both >50 AND <10 => count=0 + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testFindEntityDataWithOrKeyFiltersOperation() throws Exception { + String type = "orDataType"; + createDeviceWithSharedAttributes("OrDataDeviceX", type, "{\"status\":\"active\"}"); + createDeviceWithSharedAttributes("OrDataDeviceY", type, "{\"humidity\":80}"); + createDeviceWithSharedAttributes("OrDataDeviceZ", type, null); // no matching attribute + + List keyFilters = List.of( + stringAttributeKeyFilter("status", StringFilterPredicate.StringOperation.EQUAL, "active"), + numericAttributeKeyFilter("humidity", NumericFilterPredicate.NumericOperation.GREATER, 70)); + + // OR: deviceX matches status=active, deviceY matches humidity>70 + EntityDataQuery orQuery = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery, 2)); + PageData result = findByQueryAndCheck(orQuery, 2); + assertThat(extractNames(result)).containsExactlyInAnyOrder("OrDataDeviceX", "OrDataDeviceY"); + } + + @Test + public void testFindEntityDataWithOrDoesNotLeakFilterOnlyEntityFields() throws Exception { + // Regression test: under OR, an entity-field filter (e.g. label) that isn't declared in + // entityFields must not leak its value into EntityData.latest[ENTITY_FIELD]. + String type = "orLeakGuardType"; + Device d1 = createDeviceWithSharedAttributes("OrLeakDeviceA", type, "{\"status\":\"active\"}"); + d1.setLabel("leak-label-A"); + doPost("/api/device", d1, Device.class); + + Device d2 = createDeviceWithSharedAttributes("OrLeakDeviceB", type, null); + d2.setLabel("leak-label-B"); + doPost("/api/device", d2, Device.class); + + List keyFilters = List.of( + stringAttributeKeyFilter("status", StringFilterPredicate.StringOperation.EQUAL, "active"), + buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "label", StringFilterPredicate.StringOperation.EQUAL, "leak-label-B")); + + EntityDataQuery orQuery = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery, 2)); + PageData result = findByQueryAndCheck(orQuery, 2); + assertThat(extractNames(result)).containsExactlyInAnyOrder("OrLeakDeviceA", "OrLeakDeviceB"); + for (EntityData entity : result.getData()) { + assertThat(entity.getLatest().get(EntityKeyType.ENTITY_FIELD)).containsOnlyKeys("name"); + } + } + + @Test + public void testFindEntityDataWithOrSameKeyFilters() throws Exception { + String type = "orSameKeyType"; + createDeviceWithSharedAttributes("OrSameKeyDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrSameKeyDeviceB", type, "{\"temperature\":5}"); + + // Same key "temperature" with two different predicates exercises the OR-ungrouping path + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + EntityDataQuery orQuery = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery, 2)); + PageData result = findByQueryAndCheck(orQuery, 2); + assertThat(extractNames(result)).containsExactlyInAnyOrder("OrSameKeyDeviceA", "OrSameKeyDeviceB"); + } + + @Test + public void testCountEntitiesWithoutKeyFiltersOperation() throws Exception { + String type = "backCompatType"; + createDeviceWithSharedAttributes("BackCompatDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("BackCompatDeviceB", type, "{\"temperature\":5}"); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // Await attribute propagation via an OR query that should find 2 when propagated + EntityCountQuery orCheckQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orCheckQuery, 2)); + + // Query without keyFiltersOperation (null) -- should behave as AND + EntityCountQuery nullOpQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters); + Long nullResult = countByQueryAndCheck(nullOpQuery, 0); + + // Query with explicit AND -- should produce the same result + EntityCountQuery andOpQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + Long andResult = countByQueryAndCheck(andOpQuery, 0); + + Assert.assertEquals(nullResult, andResult); + } + + @Test + public void testAlarmDataQueryWithOrKeyFiltersOperation() throws Exception { + loginTenantAdmin(); + + String type = "orAlarmType"; + Device deviceHot = createDeviceWithSharedAttributes("OrAlarmDeviceHot", type, "{\"temperature\":60}"); + Device deviceCold = createDeviceWithSharedAttributes("OrAlarmDeviceCold", type, "{\"temperature\":5}"); + Device deviceMid = createDeviceWithSharedAttributes("OrAlarmDeviceMid", type, "{\"temperature\":30}"); + + createAlarm(deviceHot.getId(), "highTemp", AlarmSeverity.CRITICAL); + createAlarm(deviceCold.getId(), "lowTemp", AlarmSeverity.WARNING); + createAlarm(deviceMid.getId(), "normalTemp", AlarmSeverity.WARNING); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(100); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); + List alarmFields = List.of(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + + // OR query: should return alarms for deviceHot (60>50) and deviceCold (5<10) = 2 alarms + AlarmDataQuery orAlarmQuery = new AlarmDataQuery(deviceTypeFilter(type), pageLink, null, null, keyFilters, alarmFields, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findAlarmsByQueryAndCheck(orAlarmQuery, 2)); + PageData alarmResult = findAlarmsByQueryAndCheck(orAlarmQuery, 2); + List alarmTypes = alarmResult.getData().stream().map(AlarmData::getType).collect(Collectors.toList()); + assertThat(alarmTypes).containsExactlyInAnyOrder("highTemp", "lowTemp"); + + // AND query: no device has temp both >50 AND <10 => 0 alarms + AlarmDataQuery andAlarmQuery = new AlarmDataQuery(deviceTypeFilter(type), pageLink, null, null, keyFilters, alarmFields, ComplexOperation.AND); + findAlarmsByQueryAndCheck(andAlarmQuery, 0); + } + + @Test + public void testCountAlarmsByQueryWithOrKeyFiltersOperation() throws Exception { + loginTenantAdmin(); + + String type = "orAlarmCntType"; + Device deviceHot = createDeviceWithSharedAttributes("OrAlarmCntDeviceHot", type, "{\"temperature\":60}"); + Device deviceCold = createDeviceWithSharedAttributes("OrAlarmCntDeviceCold", type, "{\"temperature\":5}"); + Device deviceMid = createDeviceWithSharedAttributes("OrAlarmCntDeviceMid", type, "{\"temperature\":30}"); + + // 2 alarms for deviceHot, 1 for deviceCold, 1 for deviceMid + createAlarm(deviceHot.getId(), "highTemp1", AlarmSeverity.CRITICAL); + createAlarm(deviceHot.getId(), "highTemp2", AlarmSeverity.CRITICAL); + createAlarm(deviceCold.getId(), "lowTemp", AlarmSeverity.WARNING); + createAlarm(deviceMid.getId(), "normalTemp", AlarmSeverity.WARNING); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // OR: deviceHot (2 alarms) + deviceCold (1 alarm) match => 3 alarms total + AlarmCountQuery orQuery = new AlarmCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countAlarmsByQueryAndCheck(orQuery, 3)); + + // AND: no device matches both filters => 0 + AlarmCountQuery andQuery = new AlarmCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + countAlarmsByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrMixedEntityFieldAndAttribute() throws Exception { + // Exercises the entity field predicate relocation to middle-layer WHERE under OR + String type = "orMixedType"; + createDeviceWithSharedAttributes("OrMixedAlpha", type, "{\"temperature\":10}"); + createDeviceWithSharedAttributes("OrMixedBeta", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrMixedGamma", type, "{\"temperature\":10}"); + + List keyFilters = List.of( + stringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.CONTAINS, "Alpha"), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50)); + + // OR: Alpha matches name contains "Alpha", Beta matches temp>50 => count=2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); + + // AND: only Alpha has name "Alpha" AND temp is 10 (not >50) => count=0 + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrStringAttributes() throws Exception { + String type = "orStrType"; + createDeviceWithSharedAttributes("OrStrDeviceA", type, "{\"color\":\"red\"}"); + createDeviceWithSharedAttributes("OrStrDeviceB", type, "{\"color\":\"blue\"}"); + createDeviceWithSharedAttributes("OrStrDeviceC", type, "{\"color\":\"green\"}"); + + List keyFilters = List.of( + stringAttributeKeyFilter("color", StringFilterPredicate.StringOperation.EQUAL, "red"), + stringAttributeKeyFilter("color", StringFilterPredicate.StringOperation.EQUAL, "blue")); + + // OR: red and blue match => count=2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); + + // AND: no device is both red AND blue => count=0 + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrSingleFilter() throws Exception { + String type = "orSingleType"; + createDeviceWithSharedAttributes("OrSingleDeviceA", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrSingleDeviceB", type, "{\"temperature\":30}"); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50)); + + // Single filter with OR should behave identically to AND + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 1)); + Long orResult = countByQueryAndCheck(orQuery, 1); + + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + Long andResult = countByQueryAndCheck(andQuery, 1); + + Assert.assertEquals(orResult, andResult); + } + + @Test + public void testCountEntitiesWithOrThreeFilters() throws Exception { + String type = "or3fType"; + createDeviceWithSharedAttributes("Or3fDeviceA", type, "{\"temperature\":60,\"humidity\":50,\"pressure\":1000}"); + createDeviceWithSharedAttributes("Or3fDeviceB", type, "{\"temperature\":20,\"humidity\":90,\"pressure\":1000}"); + createDeviceWithSharedAttributes("Or3fDeviceC", type, "{\"temperature\":20,\"humidity\":50,\"pressure\":1050}"); + createDeviceWithSharedAttributes("Or3fDeviceD", type, "{\"temperature\":20,\"humidity\":50,\"pressure\":1000}"); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("humidity", NumericFilterPredicate.NumericOperation.GREATER, 80), + numericAttributeKeyFilter("pressure", NumericFilterPredicate.NumericOperation.GREATER, 1040)); + + // OR: A matches temp>50, B matches hum>80, C matches press>1040, D matches none => 3 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 3)); + + // AND: no device matches all three => 0 + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testFindEntityDataWithOrPagination() throws Exception { + // 5 devices, 4 match OR filters; explicit temperatures keep the setup easy to read. + String type = "orPageType"; + int[] temperatures = {61, 62, 2, 1, 25}; // devices 1,2: temp>50; 3,4: temp<10; 5: no match + for (int i = 0; i < temperatures.length; i++) { + createDeviceWithSharedAttributes( + String.format("OrPageDevice%02d", i + 1), type, + "{\"temperature\":" + temperatures[i] + "}"); + } + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // Page 1: pageSize=2, totalElements=4, data.size()=2 + EntityDataQuery orQuery1 = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(2, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery1, 4)); + PageData page1 = findByQueryAndCheck(orQuery1, 4); + Assert.assertEquals(2, page1.getData().size()); + Assert.assertTrue(page1.hasNext()); + + // Page 2: remaining 2 of 4 + EntityDataQuery orQuery2 = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(2, 1, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + PageData page2 = findByQueryAndCheck(orQuery2, 4); + Assert.assertEquals(2, page2.getData().size()); + Assert.assertFalse(page2.hasNext()); + + // All names across both pages should be from the 4 matching devices (01..04) + List allNames = new ArrayList<>(extractNames(page1)); + allNames.addAll(extractNames(page2)); + assertThat(allNames).hasSize(4).doesNotContain("OrPageDevice05"); + } + + @Test + public void testCountEntitiesWithOrZeroMatches() throws Exception { + String type = "orZeroType"; + createDeviceWithSharedAttributes("OrZeroDeviceA", type, "{\"temperature\":30}"); + createDeviceWithSharedAttributes("OrZeroDeviceB", type, "{\"temperature\":40}"); + + // Await attribute propagation via a filter that actually matches both devices + EntityCountQuery propagationCheck = new EntityCountQuery(deviceTypeFilter(type), List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 20)), + ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(propagationCheck, 2)); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // OR with no matches: neither filter matches any device => count=0 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + countByQueryAndCheck(orQuery, 0); + } + + @Test + public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception { + loginTenantAdmin(); + + ReflectionTestUtils.setField(baseEntityService, "keyFiltersOrConditionsEnabled", false); + try { + DeviceTypeFilter filter = deviceTypeFilter("default"); + + // POST a query with OR operation -- should be rejected with 400 + EntityCountQuery orQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.OR); + String errorMessage = getErrorMessage( + doPost("/api/entitiesQuery/count", orQuery).andExpect(status().isBadRequest()) + ); + assertThat(errorMessage).contains("OR conditions between key filters are disabled"); + + // POST a query without keyFiltersOperation (null/AND) -- should still succeed + EntityCountQuery andQuery = new EntityCountQuery(filter, Collections.emptyList()); + doPost("/api/entitiesQuery/count", andQuery).andExpect(status().isOk()); + + // POST a query with explicit AND -- should also succeed + EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND); + doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk()); + } finally { + ReflectionTestUtils.setField(baseEntityService, "keyFiltersOrConditionsEnabled", true); + } + } + + @Test + public void testFindEntityDataWithOrAndTextSearch() throws Exception { + // 3 devices: 2 match OR filters, but only 1 also matches textSearch at a time + String type = "orTextType"; + createDeviceWithSharedAttributes("OrTextAlpha", type, "{\"temperature\":60}"); + createDeviceWithSharedAttributes("OrTextBeta", type, "{\"temperature\":5}"); + createDeviceWithSharedAttributes("OrTextGamma", type, "{\"temperature\":30}"); + + List keyFilters = List.of( + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericAttributeKeyFilter("temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // OR without textSearch: Alpha and Beta match => 2 + EntityDataQuery orQueryNoText = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, null), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQueryNoText, 2)); + + // OR with textSearch="Alpha": only Alpha matches both OR filter AND text search + EntityDataQuery orQueryWithText = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, "Alpha"), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + assertThat(extractNames(findByQueryAndCheck(orQueryWithText, 1))).containsExactly("OrTextAlpha"); + + // OR with textSearch="Beta": only Beta matches both OR filter AND text search + EntityDataQuery orQueryBeta = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, "Beta"), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + assertThat(extractNames(findByQueryAndCheck(orQueryBeta, 1))).containsExactly("OrTextBeta"); + + // OR with textSearch="Gamma": Gamma doesn't match any OR filter => 0 + EntityDataQuery orQueryGamma = new EntityDataQuery(deviceTypeFilter(type), pageLinkSortedByName(10, 0, "Gamma"), + nameEntityField(), null, keyFilters, ComplexOperation.OR); + findByQueryAndCheck(orQueryGamma, 0); + } + + @Test + public void testCountEntitiesWithOrTimeSeriesKeyFilters() throws Exception { + String type = "orTsType"; + createDeviceWithTimeseries("OrTsDeviceA", type, "{\"temperature\":60}"); + createDeviceWithTimeseries("OrTsDeviceB", type, "{\"temperature\":5}"); + createDeviceWithTimeseries("OrTsDeviceC", type, "{\"temperature\":30}"); + + List keyFilters = List.of( + numericKeyFilter(EntityKeyType.TIME_SERIES, "temperature", NumericFilterPredicate.NumericOperation.GREATER, 50), + numericKeyFilter(EntityKeyType.TIME_SERIES, "temperature", NumericFilterPredicate.NumericOperation.LESS, 10)); + + // OR: deviceA (60>50) and deviceB (5<10) match => count=2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); + + // AND: no device has ts temperature both >50 AND <10 => count=0 + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrComplexFilterPredicate() throws Exception { + // Key-level ComplexFilterPredicate combined with query-level OR + String type = "orCplxType"; + createDeviceWithSharedAttributes("OrCplxDeviceA", type, "{\"temperature\":65,\"humidity\":50}"); + createDeviceWithSharedAttributes("OrCplxDeviceB", type, "{\"temperature\":25,\"humidity\":90}"); + createDeviceWithSharedAttributes("OrCplxDeviceC", type, "{\"temperature\":25,\"humidity\":50}"); + + // Key filter 1: temperature > 50 AND temperature < 70 (complex predicate within key filter) — matches A only + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + NumericFilterPredicate lt70 = new NumericFilterPredicate(); + lt70.setValue(FilterPredicateValue.fromDouble(70)); + lt70.setOperation(NumericFilterPredicate.NumericOperation.LESS); + ComplexFilterPredicate complexTempPred = new ComplexFilterPredicate(); + complexTempPred.setOperation(ComplexOperation.AND); + complexTempPred.setPredicates(List.of(gt50, lt70)); + KeyFilter tempComplexFilter = new KeyFilter(); + tempComplexFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempComplexFilter.setValueType(EntityKeyValueType.NUMERIC); + tempComplexFilter.setPredicate(complexTempPred); + + // Key filter 2: humidity > 80 (simple predicate) — matches B only + List keyFilters = List.of( + tempComplexFilter, + numericAttributeKeyFilter("humidity", NumericFilterPredicate.NumericOperation.GREATER, 80)); + + // Query-level OR: A matches key filter 1, B matches key filter 2 => 2 + EntityCountQuery orQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); + + // Query-level AND: no device matches both key filters => 0 + EntityCountQuery andQuery = new EntityCountQuery(deviceTypeFilter(type), keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java index 62fc1e4b62..b4ce7179d2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java @@ -17,19 +17,15 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import lombok.extern.slf4j.Slf4j; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; 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.Tenant; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -39,8 +35,7 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; -import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.Collections; @@ -48,57 +43,32 @@ import java.util.List; import java.util.UUID; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@Slf4j @DaoSqlTest public class EntityRelationControllerTest extends AbstractControllerTest { public static final String BASE_DEVICE_NAME = "Test dummy device"; @Autowired - RelationService relationService; + private RelationDao relationDao; - private IdComparator idComparator; - private Tenant savedTenant; - private User tenantAdmin; private Device mainDevice; @Before public void beforeTest() throws Exception { - loginSysAdmin(); - idComparator = new IdComparator<>(); + loginTenantAdmin(); - Tenant tenant = new Tenant(); - tenant.setTitle("Test tenant"); - - savedTenant = saveTenant(tenant); - 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"); - - Device device = new Device(); + var device = new Device(); device.setName("Main test device"); device.setType("default"); mainDevice = doPost("/api/device", device, Device.class); } - @After - public void afterTest() throws Exception { - loginSysAdmin(); - - deleteTenant(savedTenant.getId()); - } - @Test public void testSaveAndFindRelation() throws Exception { - Device device = buildSimpleDevice("Test device 1"); + Device device = createDevice("Test device 1"); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); Mockito.reset(tbClusterService, auditLogService); @@ -116,57 +86,117 @@ public class EntityRelationControllerTest extends AbstractControllerTest { Assert.assertEquals("Found relation is not equals origin!", relation, foundRelation); testNotifyEntityAllOneTimeRelation(foundRelation, - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.RELATION_ADD_OR_UPDATE, foundRelation); } @Test - public void testSaveWithDeviceFromNotCreated() throws Exception { - Device device = new Device(); - device.setName("Test device 2"); - device.setType("default"); - EntityRelation relation = createFromRelation(device, mainDevice, "CONTAINS"); - - Mockito.reset(tbClusterService, auditLogService); + public void testSaveRelationFromValidation() throws Exception { + // GIVEN + var relation = new EntityRelation(); + relation.setFrom(null); + relation.setTo(mainDevice.getId()); + relation.setType("Contains"); + + // WHEN-THEN + for (String endpoint : List.of("/api/relation", "/api/v2/relation")) { + doPost(endpoint, relation) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(is("Validation error: from must not be null"))); + } + } - doPost("/api/relation", relation) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Parameter entityId can't be empty!"))); + @Test + public void testSaveRelationToValidation() throws Exception { + // GIVEN + var relation = new EntityRelation(); + relation.setFrom(mainDevice.getId()); + relation.setTo(null); + relation.setType("Contains"); + + // WHEN-THEN + for (String endpoint : List.of("/api/relation", "/api/v2/relation")) { + doPost(endpoint, relation) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(is("Validation error: to must not be null"))); + } + } - testNotifyEntityNever(mainDevice.getId(), null); + @Test + public void testSaveRelationRelationTypeValidation() throws Exception { + // GIVEN + var device = createDevice("Test device"); + + EntityRelation relationTypeNull = createFromRelation(mainDevice, device, null); + EntityRelation relationTypeEmpty = createFromRelation(mainDevice, device, ""); + EntityRelation relationTypeBlank = createFromRelation(mainDevice, device, " "); + EntityRelation relationTypeContainsNullChar = createFromRelation(mainDevice, device, "null char \u0000"); + EntityRelation relationTypeTooLong = createFromRelation(mainDevice, device, "a".repeat(256)); + + // WHEN-THEN + for (String endpoint : List.of("/api/relation", "/api/v2/relation")) { + doPost(endpoint, relationTypeNull) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(is("Validation error: type must not be blank"))); + + doPost(endpoint, relationTypeEmpty) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(is("Validation error: type must not be blank"))); + + doPost(endpoint, relationTypeBlank) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(is("Validation error: type must not be blank"))); + + doPost(endpoint, relationTypeContainsNullChar) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(is("Validation error: type should not contain 0x00 symbol"))); + + doPost(endpoint, relationTypeTooLong) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(is("Validation error: type length must be equal or less than 255"))); + } } @Test - public void testSaveWithDeviceToNotCreated() throws Exception { - Device device = new Device(); - device.setName("Test device 2"); - device.setType("default"); - EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); + public void testSaveRelationFromNonexistentEntity() throws Exception { + // GIVEN + var nonexistentDevice = new Device(); + nonexistentDevice.setId(new DeviceId(UUID.randomUUID())); + nonexistentDevice.setName("Nonexistent device"); + nonexistentDevice.setType("default"); + EntityRelation relation = createFromRelation(nonexistentDevice, mainDevice, "CONTAINS"); Mockito.reset(tbClusterService, auditLogService); - doPost("/api/relation", relation) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Parameter entityId can't be empty!"))); + // WHEN-THEN + for (String endpoint : List.of("/api/relation", "/api/v2/relation")) { + doPost(endpoint, relation) + .andExpect(status().isNotFound()) + .andExpect(statusReason(is(msgErrorNoFound("Device", nonexistentDevice.getId().toString())))); - testNotifyEntityNever(mainDevice.getId(), null); + testNotifyEntityNever(mainDevice.getId(), null); + } } @Test - public void testSaveWithDeviceToMissing() throws Exception { - Device device = new Device(); - device.setName("Test device 2"); - device.setType("default"); - device.setId(new DeviceId(UUID.randomUUID())); - EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); + public void testSaveRelationToNonexistentEntity() throws Exception { + // GIVEN + var nonexistentDevice = new Device(); + nonexistentDevice.setId(new DeviceId(UUID.randomUUID())); + nonexistentDevice.setName("Nonexistent device"); + nonexistentDevice.setType("default"); + EntityRelation relation = createFromRelation(mainDevice, nonexistentDevice, "CONTAINS"); Mockito.reset(tbClusterService, auditLogService); - doPost("/api/relation", relation) - .andExpect(status().isNotFound()) - .andExpect(statusReason(containsString(msgErrorNoFound("Device", device.getId().getId().toString())))); + // WHEN-THEN + for (String endpoint : List.of("/api/relation", "/api/v2/relation")) { + doPost(endpoint, relation) + .andExpect(status().isNotFound()) + .andExpect(statusReason(is(msgErrorNoFound("Device", nonexistentDevice.getId().toString())))); - testNotifyEntityNever(mainDevice.getId(), null); + testNotifyEntityNever(mainDevice.getId(), null); + } } @Test @@ -178,7 +208,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME); EntityRelation relationTest = createFromRelation(mainDevice, mainDevice, "TEST_NOTIFY_ENTITY"); - testNotifyEntityAllManyRelation(relationTest, savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + testNotifyEntityAllManyRelation(relationTest, tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.RELATION_ADD_OR_UPDATE, numOfDevices); String url = String.format("/api/relations?fromId=%s&fromType=%s", @@ -204,7 +234,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { final int numOfDevices = 30; createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME); - Device device = buildSimpleDevice("Unique dummy test device "); + Device device = createDevice("Unique dummy test device "); String relationType = "TEST"; EntityRelation relation = createFromRelation(mainDevice, device, relationType); @@ -221,7 +251,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { final int numOfDevices = 30; createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME); - Device device = buildSimpleDevice("Unique dummy test device "); + Device device = createDevice("Unique dummy test device "); String relationType = "TEST"; EntityRelation relation = createFromRelation(mainDevice, device, relationType); @@ -240,7 +270,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { final int numOfDevices = 30; createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME); - Device device = buildSimpleDevice("Unique dummy test device "); + Device device = createDevice("Unique dummy test device "); String relationType = "TEST"; EntityRelation relation = createFromRelation(device, mainDevice, relationType); @@ -252,13 +282,12 @@ public class EntityRelationControllerTest extends AbstractControllerTest { assertFoundList(url, 1); } - @Test public void testSaveAndFindRelationsByToWithRelationTypeOther() throws Exception { final int numOfDevices = 30; createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME); - Device device = buildSimpleDevice("Unique dummy test device "); + Device device = createDevice("Unique dummy test device "); String relationType = "TEST"; EntityRelation relation = createFromRelation(device, mainDevice, relationType); @@ -280,9 +309,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { mainDevice.getUuidId(), EntityType.DEVICE ); - List relationsInfos = - JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() { - }); + List relationsInfos = JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() {}); Assert.assertNotNull("Relations is not found!", relationsInfos); Assert.assertEquals("List of found relationsInfos is not equal to number of created relations!", @@ -299,57 +326,88 @@ public class EntityRelationControllerTest extends AbstractControllerTest { mainDevice.getUuidId(), EntityType.DEVICE ); - List relationsInfos = - JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() { - }); + List relationsInfos = JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() {}); Assert.assertNotNull("Relations is not found!", relationsInfos); - Assert.assertEquals("List of found relationsInfos is not equal to number of created relations!", - numOfDevices, relationsInfos.size()); + Assert.assertEquals("List of found relationsInfos is not equal to number of created relations!", numOfDevices, relationsInfos.size()); assertRelationsInfosByTo(relationsInfos); } @Test public void testDeleteRelation() throws Exception { - Device device = buildSimpleDevice("Test device 1"); + // GIVEN + Device device = createDevice("Test device 1"); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); relation = doPost("/api/v2/relation", relation, EntityRelation.class); - String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", - mainDevice.getUuidId(), EntityType.DEVICE, - "CONTAINS", device.getUuidId(), EntityType.DEVICE - ); - - EntityRelation foundRelation = doGet(url, EntityRelation.class); - - Assert.assertNotNull("Relation is not found!", foundRelation); - Assert.assertEquals("Found relation is not equals origin!", relation, foundRelation); - Mockito.reset(tbClusterService, auditLogService); + // WHEN String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", mainDevice.getUuidId(), EntityType.DEVICE, "CONTAINS", device.getUuidId(), EntityType.DEVICE ); var deletedRelation = doDelete(deleteUrl, EntityRelation.class); + // THEN testNotifyEntityAllOneTimeRelation(deletedRelation, - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.RELATION_DELETED, deletedRelation); - doGet(url).andExpect(status().is4xxClientError()); + getRelation(relation) + .andExpect(status().isNotFound()) + .andExpect(statusReason(is(msgErrorNotFound))); + } + + @Test + public void testDeleteRelationWithTypeEmptyOrBlank() throws Exception { + // GIVEN + Device device = createDevice("Test device"); + + // WHEN-THEN + for (String endpoint : List.of("/api/relation", "/api/v2/relation")) { + // saving relation with empty type + EntityRelation emptyRelation = createFromRelation(mainDevice, device, ""); + relationDao.saveRelation(tenantId, emptyRelation); + + // saving relation with blank type + EntityRelation blankRelation = createFromRelation(mainDevice, device, " "); + relationDao.saveRelation(tenantId, blankRelation); + + // deleting relation with empty type + String deleteEmptyUrl = String.format(endpoint + "?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + mainDevice.getUuidId(), EntityType.DEVICE, + emptyRelation.getType(), device.getUuidId(), EntityType.DEVICE + ); + doDelete(deleteEmptyUrl).andExpect(status().isOk()); + + getRelation(emptyRelation) + .andExpect(status().isNotFound()) + .andExpect(statusReason(is(msgErrorNotFound))); + + // deleting relation with blank type + String deleteBlankUrl = String.format(endpoint + "?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + mainDevice.getUuidId(), EntityType.DEVICE, + blankRelation.getType(), device.getUuidId(), EntityType.DEVICE + ); + doDelete(deleteBlankUrl).andExpect(status().isOk()); + + getRelation(blankRelation) + .andExpect(status().isNotFound()) + .andExpect(statusReason(is(msgErrorNotFound))); + } } @Test public void testDeleteRelationWithOtherFromDeviceError() throws Exception { - Device device = buildSimpleDevice("Test device 1"); + Device device = createDevice("Test device 1"); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); doPost("/api/relation", relation).andExpect(status().isOk()); - Device device2 = buildSimpleDevice("Test device 2"); + Device device2 = createDevice("Test device 2"); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", device2.getUuidId(), EntityType.DEVICE, "CONTAINS", device.getUuidId(), EntityType.DEVICE @@ -366,12 +424,12 @@ public class EntityRelationControllerTest extends AbstractControllerTest { @Test public void testDeleteRelationWithOtherToDeviceError() throws Exception { - Device device = buildSimpleDevice("Test device 1"); + Device device = createDevice("Test device 1"); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); doPost("/api/relation", relation).andExpect(status().isOk()); - Device device2 = buildSimpleDevice("Test device 2"); + Device device2 = createDevice("Test device 2"); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", mainDevice.getUuidId(), EntityType.DEVICE, "CONTAINS", device2.getUuidId(), EntityType.DEVICE @@ -411,7 +469,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { doDelete(url).andExpect(status().isOk()); testNotifyEntityOneTimeMsgToEdgeServiceNever(null, mainDevice.getId(), mainDevice.getId(), - savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.RELATIONS_DELETED); Assert.assertTrue( @@ -442,8 +500,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { List relations = readResponse( doPost("/api/relations", query).andExpect(status().isOk()), - new TypeReference>() { - } + new TypeReference<>() {} ); assertFoundRelations(relations, numOfDevices); @@ -467,8 +524,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { List relations = readResponse( doPost("/api/relations", query).andExpect(status().isOk()), - new TypeReference<>() { - } + new TypeReference<>() {} ); assertFoundRelations(relations, numOfDevices); @@ -526,11 +582,11 @@ public class EntityRelationControllerTest extends AbstractControllerTest { @Test public void testCreateRelationFromTenantToDevice() throws Exception { - EntityRelation relation = new EntityRelation(tenantAdmin.getTenantId(), mainDevice.getId(), "CONTAINS"); + EntityRelation relation = new EntityRelation(tenantId, mainDevice.getId(), "CONTAINS"); relation = doPost("/api/v2/relation", relation, EntityRelation.class); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", - tenantAdmin.getTenantId(), EntityType.TENANT, + tenantId, EntityType.TENANT, "CONTAINS", mainDevice.getUuidId(), EntityType.DEVICE ); @@ -542,12 +598,12 @@ public class EntityRelationControllerTest extends AbstractControllerTest { @Test public void testCreateRelationFromDeviceToTenant() throws Exception { - EntityRelation relation = new EntityRelation(mainDevice.getId(), tenantAdmin.getTenantId(), "CONTAINS"); + EntityRelation relation = new EntityRelation(mainDevice.getId(), tenantId, "CONTAINS"); relation = doPost("/api/v2/relation", relation, EntityRelation.class); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", mainDevice.getUuidId(), EntityType.DEVICE, - "CONTAINS", tenantAdmin.getTenantId(), EntityType.TENANT + "CONTAINS", tenantId, EntityType.TENANT ); EntityRelation foundRelation = doGet(url, EntityRelation.class); @@ -558,7 +614,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { @Test public void testSaveAndFindRelationDifferentTenant() throws Exception { - Device device = buildSimpleDevice("Test device 1"); + Device device = createDevice("Test device 1"); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); doPost("/api/relation", relation).andExpect(status().isOk()); @@ -577,12 +633,13 @@ public class EntityRelationControllerTest extends AbstractControllerTest { deleteDifferentTenant(); } - private Device buildSimpleDevice(String name) throws Exception { - Device device = new Device(); - device.setName(name); - device.setType("default"); - device = doPost("/api/device", device, Device.class); - return device; + private ResultActions getRelation(EntityRelation relation) throws Exception { + return doGet("/api/relation?" + + "fromId=" + relation.getFrom().getId() + + "&fromType=" + relation.getFrom().getEntityType() + + "&relationType=" + relation.getType() + + "&toId=" + relation.getTo().getId() + + "&toType=" + relation.getTo().getEntityType()); } private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) { @@ -591,7 +648,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { private void createDevicesByFrom(int numOfDevices, String baseName) throws Exception { for (int i = 0; i < numOfDevices; i++) { - Device device = buildSimpleDevice(baseName + i); + Device device = createDevice(baseName + i); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); doPost("/api/relation", relation).andExpect(status().isOk()); @@ -600,7 +657,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { private void createDevicesByTo(int numOfDevices, String baseName) throws Exception { for (int i = 0; i < numOfDevices; i++) { - Device device = buildSimpleDevice(baseName + i); + Device device = createDevice(baseName + i); EntityRelation relation = createFromRelation(device, mainDevice, "CONTAINS"); doPost("/api/relation", relation).andExpect(status().isOk()); } @@ -633,4 +690,5 @@ public class EntityRelationControllerTest extends AbstractControllerTest { Assert.assertEquals("Wrong relationType!", "CONTAINS", info.getType()); } } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java index b65d4178f0..277ec687f1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java @@ -66,16 +66,19 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.entityview.EntityViewDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; import static java.util.concurrent.TimeUnit.HOURS; @@ -166,6 +169,36 @@ public class EntityViewControllerTest extends AbstractControllerTest { assertEquals(savedView, foundView); } + @Test + public void testFindEntityViewByIds() throws Exception { + List assetProfiles = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + assetProfiles.add(getNewSavedEntityView("Test entity view " + i)); + } + + List expected = assetProfiles.subList(5, 15); + + String idsParam = expected.stream() + .map(ap -> ap.getId().getId().toString()) + .collect(Collectors.joining(",")); + EntityView[] foundEntityViews = doGet("/api/entityViews?entityViewIds=" + idsParam, EntityView[].class); + + Assert.assertNotNull(foundEntityViews); + Assert.assertEquals(expected.size(), foundEntityViews.length); + + Map infoById = Arrays.stream(foundEntityViews) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (EntityView entityView : expected) { + UUID id = entityView.getId().getId(); + EntityView view = infoById.get(id); + Assert.assertNotNull("Entity view not found for id " + id, view); + + Assert.assertEquals(entityView.getId(), view.getId()); + Assert.assertEquals(entityView.getName(), view.getName()); + } + } + @Test public void testSaveEntityView() throws Exception { String name = "Test entity view"; @@ -871,4 +904,29 @@ public class EntityViewControllerTest extends AbstractControllerTest { EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId(); testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId); } + + @Test + public void testSaveEntityViewWithUniquifyStrategy() throws Exception { + EntityView view = new EntityView(); + view.setEntityId(testDevice.getId()); + view.setTenantId(tenantId); + view.setType("default"); + view.setName("My unique view"); + + EntityView savedView = doPost("/api/entityView", view, EntityView.class); + + doPost("/api/entityView?nameConflictPolicy=FAIL", view).andExpect(status().isBadRequest()); + + EntityView secondView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY", view, EntityView.class); + assertThat(secondView.getName()).startsWith("My unique view_"); + + EntityView thirdView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", view, EntityView.class); + assertThat(thirdView.getName()).startsWith("My unique view-"); + + EntityView fourthView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class); + assertThat(fourthView.getName()).isEqualTo("My unique view_1"); + + EntityView fifthEntityView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class); + assertThat(fifthEntityView.getName()).isEqualTo("My unique view_2"); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java index 3fbe4bd845..0b8767e65d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -189,7 +189,7 @@ public class ImageControllerTest extends AbstractControllerTest { assertThat(newImageInfo.getTitle()).isEqualTo(newTitle); assertThat(newImageInfo.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor); assertThat(newImageInfo.getResourceKey()).isEqualTo(imageInfo.getResourceKey()); - assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(newImageInfo.getPublicResourceKey()); + assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(imageInfo.getPublicResourceKey()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java index 7e99f6b110..5425c34740 100644 --- a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java @@ -37,7 +37,7 @@ 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.security.Authority; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.nio.ByteBuffer; diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index f8cd8e0f33..b65e500082 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -44,15 +44,21 @@ 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.RuleChainNote; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -231,6 +237,44 @@ public class RuleChainControllerTest extends AbstractControllerTest { Assert.assertEquals(savedRuleChain, foundRuleChain); } + @Test + public void testFindRuleChainsByIds() throws Exception { + List ruleChains = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("RuleChain " + i); + ruleChains.add(doPost("/api/ruleChain", ruleChain, RuleChain.class)); + } + + List expected = ruleChains.subList(5, 15); + + String idsParam = expected.stream() + .map(rc -> rc.getId().getId().toString()) + .collect(Collectors.joining(",")); + + RuleChain[] result = doGet( + "/api/ruleChains?ruleChainIds=" + idsParam, + RuleChain[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map rcById = Arrays.stream(result) + .collect(Collectors.toMap(rc -> rc.getId().getId(), Function.identity())); + + for (RuleChain rc : expected) { + UUID id = rc.getId().getId(); + RuleChain found = rcById.get(id); + Assert.assertNotNull("RuleChain not found for id " + id, found); + + Assert.assertEquals(rc.getId(), found.getId()); + Assert.assertEquals(rc.getName(), found.getName()); + Assert.assertEquals(rc.getTenantId(), found.getTenantId()); + } + } + + @Test public void testDeleteRuleChain() throws Exception { RuleChain ruleChain = new RuleChain(); @@ -399,6 +443,201 @@ public class RuleChainControllerTest extends AbstractControllerTest { assertThat(ruleChain.getVersion()).isEqualTo(5); } + @Test + public void testSaveAndLoadRuleChainMetaDataWithNotes() throws Exception { + RuleChain ruleChain = createRuleChain("RuleChain with notes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainMetaData.setNodes(new ArrayList<>()); + + List notes = new ArrayList<>(); + RuleChainNote note1 = new RuleChainNote(); + note1.setId("note-1"); + note1.setX(100); + note1.setY(200); + note1.setWidth(300); + note1.setHeight(150); + note1.setContent("# Test Note\nSome markdown content"); + note1.setBackgroundColor("#FFF9C4"); + note1.setBorderColor("#E6C800"); + note1.setBorderWidth(2); + note1.setApplyDefaultMarkdownStyle(true); + notes.add(note1); + + RuleChainNote note2 = new RuleChainNote(); + note2.setId("note-2"); + note2.setX(500); + note2.setY(300); + note2.setWidth(200); + note2.setHeight(100); + note2.setContent("Simple note"); + note2.setBackgroundColor("#C8E6C9"); + notes.add(note2); + + ruleChainMetaData.setNotes(notes); + + RuleChainMetaData savedMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + Assert.assertNotNull(savedMetaData); + Assert.assertNotNull(savedMetaData.getNotes()); + Assert.assertEquals(2, savedMetaData.getNotes().size()); + + RuleChainMetaData loadedMetaData = doGet("/api/ruleChain/" + ruleChain.getId().getId() + "/metadata", RuleChainMetaData.class); + Assert.assertNotNull(loadedMetaData); + Assert.assertNotNull(loadedMetaData.getNotes()); + Assert.assertEquals(2, loadedMetaData.getNotes().size()); + + RuleChainNote loadedNote1 = loadedMetaData.getNotes().stream() + .filter(n -> "note-1".equals(n.getId())).findFirst().orElse(null); + Assert.assertNotNull(loadedNote1); + Assert.assertEquals(100, loadedNote1.getX()); + Assert.assertEquals(200, loadedNote1.getY()); + Assert.assertEquals(300, loadedNote1.getWidth()); + Assert.assertEquals(150, loadedNote1.getHeight()); + Assert.assertEquals("# Test Note\nSome markdown content", loadedNote1.getContent()); + Assert.assertEquals("#FFF9C4", loadedNote1.getBackgroundColor()); + Assert.assertEquals("#E6C800", loadedNote1.getBorderColor()); + Assert.assertEquals(Integer.valueOf(2), loadedNote1.getBorderWidth()); + Assert.assertEquals(Boolean.TRUE, loadedNote1.getApplyDefaultMarkdownStyle()); + + RuleChainNote loadedNote2 = loadedMetaData.getNotes().stream() + .filter(n -> "note-2".equals(n.getId())).findFirst().orElse(null); + Assert.assertNotNull(loadedNote2); + Assert.assertEquals(500, loadedNote2.getX()); + Assert.assertEquals(300, loadedNote2.getY()); + Assert.assertEquals("Simple note", loadedNote2.getContent()); + Assert.assertEquals("#C8E6C9", loadedNote2.getBackgroundColor()); + } + + @Test + public void testUpdateRuleChainNotes() throws Exception { + RuleChain ruleChain = createRuleChain("RuleChain update notes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainMetaData.setNodes(new ArrayList<>()); + + RuleChainNote note = new RuleChainNote(); + note.setId("note-1"); + note.setX(10); + note.setY(20); + note.setWidth(100); + note.setHeight(50); + note.setContent("Original content"); + ruleChainMetaData.setNotes(List.of(note)); + + RuleChainMetaData savedMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + + note.setContent("Updated content"); + note.setX(50); + RuleChainNote newNote = new RuleChainNote(); + newNote.setId("note-2"); + newNote.setX(200); + newNote.setY(300); + newNote.setWidth(150); + newNote.setHeight(75); + newNote.setContent("New note"); + savedMetaData.setNotes(List.of(note, newNote)); + + RuleChainMetaData updatedMetaData = doPost("/api/ruleChain/metadata", savedMetaData, RuleChainMetaData.class); + Assert.assertEquals(2, updatedMetaData.getNotes().size()); + + RuleChainMetaData loadedMetaData = doGet("/api/ruleChain/" + ruleChain.getId().getId() + "/metadata", RuleChainMetaData.class); + Assert.assertEquals(2, loadedMetaData.getNotes().size()); + + RuleChainNote updatedNote = loadedMetaData.getNotes().stream() + .filter(n -> "note-1".equals(n.getId())).findFirst().orElse(null); + Assert.assertNotNull(updatedNote); + Assert.assertEquals("Updated content", updatedNote.getContent()); + Assert.assertEquals(50, updatedNote.getX()); + } + + @Test + public void testSaveRuleChainDoesNotOverwriteNotes() throws Exception { + RuleChain ruleChain = createRuleChain("RuleChain preserve notes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainMetaData.setNodes(new ArrayList<>()); + + RuleChainNote note = new RuleChainNote(); + note.setId("note-1"); + note.setX(10); + note.setY(20); + note.setWidth(100); + note.setHeight(50); + note.setContent("Persistent note"); + ruleChainMetaData.setNotes(List.of(note)); + + doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + + // Save the RuleChain itself (not metadata) — e.g. rename + ruleChain = doGet("/api/ruleChain/" + ruleChain.getId().getId(), RuleChain.class); + ruleChain.setName("Renamed RuleChain"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + Assert.assertEquals("Renamed RuleChain", ruleChain.getName()); + + // Notes must still be present + RuleChainMetaData loadedMetaData = doGet("/api/ruleChain/" + ruleChain.getId().getId() + "/metadata", RuleChainMetaData.class); + Assert.assertNotNull(loadedMetaData.getNotes()); + Assert.assertEquals(1, loadedMetaData.getNotes().size()); + Assert.assertEquals("Persistent note", loadedMetaData.getNotes().get(0).getContent()); + } + + @Test + public void testRemoveNotesByUpdatingMetadata() throws Exception { + RuleChain ruleChain = createRuleChain("RuleChain remove notes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainMetaData.setNodes(new ArrayList<>()); + + RuleChainNote note = new RuleChainNote(); + note.setId("note-1"); + note.setX(10); + note.setY(20); + note.setWidth(100); + note.setHeight(50); + note.setContent("Will be removed"); + ruleChainMetaData.setNotes(List.of(note)); + + RuleChainMetaData savedMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + Assert.assertEquals(1, savedMetaData.getNotes().size()); + + // Save metadata without notes — should clear them + savedMetaData.setNotes(null); + RuleChainMetaData updatedMetaData = doPost("/api/ruleChain/metadata", savedMetaData, RuleChainMetaData.class); + + RuleChainMetaData loadedMetaData = doGet("/api/ruleChain/" + ruleChain.getId().getId() + "/metadata", RuleChainMetaData.class); + Assert.assertTrue(loadedMetaData.getNotes() == null || loadedMetaData.getNotes().isEmpty()); + } + + @Test + public void testSaveRuleChainNotesExceedsSizeLimit() throws Exception { + RuleChain ruleChain = createRuleChain("RuleChain oversized notes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + ruleChainMetaData.setNodes(new ArrayList<>()); + + List notes = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + RuleChainNote note = new RuleChainNote(); + note.setId("note-" + i); + note.setX(i * 10); + note.setY(i * 10); + note.setWidth(300); + note.setHeight(150); + note.setContent(StringUtils.randomAlphabetic(60000)); + notes.add(note); + } + ruleChainMetaData.setNotes(notes); + + String error = getErrorMessage(doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isBadRequest())); + assertThat(error).contains("Rule chain notes data is too large"); + } + private RuleChain createRuleChain(String name) { RuleChain ruleChain = new RuleChain(); ruleChain.setName(name); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 22fa9c22d4..7fb316dd7e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -25,11 +25,12 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockPart; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; 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.EntityInfo; import org.thingsboard.server.common.data.EntityType; @@ -47,15 +48,14 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.common.data.widget.WidgetTypeInfo; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Base64; -import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -64,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class TbResourceControllerTest extends AbstractControllerTest { - private IdComparator idComparator = new IdComparator<>(); + private final IdComparator idComparator = new IdComparator<>(); private static final String DEFAULT_FILE_NAME = "test.jks"; private static final String DEFAULT_FILE_NAME_2 = "test2.jks"; @@ -126,13 +126,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey()); Assert.assertArrayEquals(resource.getData(), download(savedResource.getId())); - TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class); - foundResource.setTitle("My new resource"); - foundResource.setData(null); - - savedResource = save(foundResource); - - Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle()); + String resourceTitle = "My new resource"; + savedResource.setTitle(resourceTitle); + savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class); + assertThat(savedResource.getTitle()).isEqualTo(resourceTitle); testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), @@ -501,8 +498,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, cntEntity, cntEntity, cntEntity); - Collections.sort(resources, idComparator); - Collections.sort(loadedResources, idComparator); + resources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(resources, loadedResources); } @@ -549,8 +546,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity); - Collections.sort(resources, idComparator); - Collections.sort(loadedResources, idComparator); + resources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(resources, loadedResources); } @@ -581,8 +578,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(resources, idComparator); - Collections.sort(loadedResources, idComparator); + resources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(resources, loadedResources); @@ -654,8 +651,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(jksResources, idComparator); - Collections.sort(loadedResources, idComparator); + jksResources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(jksResources, loadedResources); @@ -736,8 +733,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(expectedResources, idComparator); - Collections.sort(loadedResources, idComparator); + expectedResources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(expectedResources, loadedResources); @@ -770,7 +767,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { MockHttpServletResponse response = resultActions.andReturn().getResponse(); String eTag = response.getHeader("ETag"); Assert.assertNotNull(eTag); - Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA); + Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray())); //download with if-none-match header HttpHeaders headers = new HttpHeaders(); @@ -814,7 +811,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { MockHttpServletResponse response = resultActions.andReturn().getResponse(); String eTag = response.getHeader("ETag"); Assert.assertNotNull(eTag); - Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA); + Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray())); //download with if-none-match header HttpHeaders headers = new HttpHeaders(); @@ -859,10 +856,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString("can't be updated"))); - foundResource.setData(null); - foundResource.setTitle("Updated resource"); - savedResource = doPost("/api/resource", foundResource, TbResource.class); - assertThat(savedResource.getTitle()).isEqualTo("Updated resource"); + String resourceTitle = "Updated resource"; + savedResource.setTitle(resourceTitle); + savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class); + assertThat(savedResource.getTitle()).isEqualTo(resourceTitle); assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName()); assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag()); assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA); @@ -923,8 +920,20 @@ public class TbResourceControllerTest extends AbstractControllerTest { } private TbResourceInfo save(TbResource tbResource) throws Exception { - return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() { - }); + byte[] data = tbResource.getData() != null ? tbResource.getData() : tbResource.getEncodedData() != null ? Base64.getDecoder().decode(tbResource.getEncodedData()) : null; + List parts = new ArrayList<>(); + parts.add(new MockPart("resourceType", tbResource.getResourceType().name().getBytes())); + if (tbResource.getTitle() != null) { + parts.add(new MockPart("title", tbResource.getTitle().getBytes())); + } + if (tbResource.getDescriptor() != null) { + parts.add(new MockPart("descriptor", tbResource.getDescriptor().toString().getBytes())); + } + if (tbResource.getResourceSubType() != null) { + parts.add(new MockPart("resourceSubType", tbResource.getResourceSubType().name().getBytes())); + } + + return uploadResource(HttpMethod.POST, "/api/resource/upload", tbResource.getFileName(), tbResource.getResourceType().getMediaType(), data, parts); } private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception { @@ -949,7 +958,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { for (String model : models) { String fileName = model + ".xml"; - byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName)); + byte[] bytes = IOUtils.toByteArray(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName))); TbResource resource = new TbResource(); resource.setResourceType(ResourceType.LWM2M_MODEL); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 26705ddacb..671c936585 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -39,7 +39,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; -import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.net.URI; import java.nio.channels.NotYetConnectedException; @@ -53,6 +52,8 @@ public class TbTestWebSocketClient extends WebSocketClient { private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30); + private final CountDownLatch closeLatch = new CountDownLatch(1); + @Getter private volatile String lastMsg; private volatile CountDownLatch reply; @@ -62,6 +63,7 @@ public class TbTestWebSocketClient extends WebSocketClient { super(serverUri); } + @Override public void onOpen(ServerHandshake serverHandshake) { @@ -69,7 +71,13 @@ public class TbTestWebSocketClient extends WebSocketClient { public void authenticate(String token) { WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); - cmdsWrapper.setAuthCmd(new AuthCmd(1, token)); + cmdsWrapper.setAuthCmd(new AuthCmd(1, token, null)); + send(JacksonUtil.toString(cmdsWrapper)); + } + + public void authenticateWithApiKey(String apiKey) { + WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); + cmdsWrapper.setAuthCmd(new AuthCmd(1, null, apiKey)); send(JacksonUtil.toString(cmdsWrapper)); } @@ -88,6 +96,16 @@ public class TbTestWebSocketClient extends WebSocketClient { @Override public void onClose(int i, String s, boolean b) { log.info("CLOSED."); + closeLatch.countDown(); + } + + public boolean waitForClose() { + try { + return closeLatch.await(TIMEOUT, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + log.warn("Failed to await close", e); + return false; + } } @Override @@ -275,7 +293,7 @@ public class TbTestWebSocketClient extends WebSocketClient { public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) { log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope); - TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); + TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); cmd.setEntityId(entityId.getId().toString()); cmd.setEntityType(entityId.getEntityType().toString()); cmd.setCmdId(1); diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java index 4f89d4c838..ce6d291546 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.springframework.test.context.TestPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -99,17 +102,29 @@ public class TelemetryControllerTest extends AbstractControllerTest { Assert.assertEquals(11L, thirdIntervalResult.get("value").asLong()); Assert.assertEquals(thirdIntervalTs, thirdIntervalResult.get("ts").asLong()); - result = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + + ObjectNode resultByMonth = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}", ObjectNode.class, startTs, endTs, "SUM", "MONTH", "Europe/Kyiv"); - Assert.assertNotNull(result); - Assert.assertNotNull(result.get("t")); - Assert.assertEquals(1, result.get("t").size()); + Assert.assertNotNull(resultByMonth); + Assert.assertNotNull(resultByMonth.get("t")); + Assert.assertEquals(1, resultByMonth.get("t").size()); - var monthResult = result.get("t").get(0); + var monthResult = resultByMonth.get("t").get(0); Assert.assertEquals(22L, monthResult.get("value").asLong()); Assert.assertEquals(middleOfTheInterval, monthResult.get("ts").asLong()); + + // check timeseries history with key parameter (instead of keys) has the same result as with keys parameter + ObjectNode resultByKey = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + + "/values/timeseries?key=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}", + ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv"); + Assert.assertEquals(result, resultByKey); + + // check timeseries history without keys and key results into empty object + ObjectNode resultWithoutKeyAndKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + + "/values/timeseries?startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}", + ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv"); + Assert.assertTrue(resultWithoutKeyAndKeys.isEmpty()); } @Test @@ -232,6 +247,63 @@ public class TelemetryControllerTest extends AbstractControllerTest { doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest()); } + @Test + public void testDeleteTelemetryByKeyWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String tsKey = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(tsKey, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", tsKey); + params.add("deleteAllDataForKeys", "true"); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + } + + @Test + public void testDeleteTelemetryByKeysWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String keyWithComma = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(keyWithComma, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + String key = "key3"; + String testBody2 = JacksonUtil.newObjectNode() + .put(key, "value") + .toString(); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", keyWithComma); + params.add("key", key); + params.add("deleteAllDataForKeys", "true"); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody2, String.class, status().isOk()); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + assertThat(tsData.get("key3").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + Assert.assertTrue(tsDataAfterDeletion.get("key3").get(0).get("value").isNull()); + } + private Device createDevice() throws Exception { String testToken = "TEST_TOKEN"; diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index cc217b05d5..aeca72c739 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -27,8 +27,8 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.ActorSystemContext; @@ -69,6 +69,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -78,6 +79,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -110,11 +112,11 @@ public class TenantControllerTest extends AbstractControllerTest { ListeningExecutorService executor; - @SpyBean + @MockitoSpyBean private PartitionService partitionService; - @SpyBean + @MockitoSpyBean private ActorSystemContext actorContext; - @SpyBean + @MockitoSpyBean private TbQueueAdmin queueAdmin; @Before @@ -188,6 +190,51 @@ public class TenantControllerTest extends AbstractControllerTest { deleteTenant(savedTenant.getId()); } + @Test + public void testFindTenantsByIds() throws Exception { + loginSysAdmin(); + + List savedTenants = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant " + i); + savedTenants.add(saveTenant(tenant)); + } + + String idsParam = savedTenants.stream() + .map(t -> t.getId().getId().toString()) + .collect(Collectors.joining(",")); + + Tenant[] foundTenants = doGet("/api/tenants?tenantIds=" + idsParam, Tenant[].class); + + Assert.assertNotNull(foundTenants); + Assert.assertEquals(savedTenants.size(), foundTenants.length); + + Map foundById = Arrays.stream(foundTenants) + .collect(Collectors.toMap(t -> t.getId().getId(), Function.identity())); + + for (Tenant savedTenant : savedTenants) { + UUID id = savedTenant.getId().getId(); + Tenant foundTenant = foundById.get(id); + Assert.assertNotNull("Tenant not found for id " + id, foundTenant); + Assert.assertEquals(savedTenant, foundTenant); + } + + loginTenantAdmin(); + Tenant[] foundTenantsByTenant = doGet("/api/tenants?tenantIds=" + idsParam, Tenant[].class); + assertThat(foundTenantsByTenant).isEmpty(); + + Tenant[] foundCurrentTenant = doGet("/api/tenants?tenantIds=" + tenantId, Tenant[].class); + assertThat(foundCurrentTenant).hasSize(1); + assertThat(foundCurrentTenant[0].getTenantId()).isEqualTo(tenantId); + + loginSysAdmin(); + for (Tenant savedTenant : savedTenants) { + deleteTenant(savedTenant.getId()); + } + } + + @Test public void testFindTenantInfoById() throws Exception { loginSysAdmin(); @@ -244,15 +291,37 @@ public class TenantControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString(msgErrorNoFound("Tenant", tenantIdStr)))); } + @Test + public void testDeleteTenantByTenantAdmin() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = saveTenant(tenant); + + //login as tenant admin + User tenantAdminUser = new User(); + tenantAdminUser.setAuthority(Authority.TENANT_ADMIN); + tenantAdminUser.setTenantId(savedTenant.getId()); + tenantAdminUser.setEmail("tenantToDelete@thingsboard.io"); + + createUserAndLogin(tenantAdminUser, TENANT_ADMIN_PASSWORD); + + String tenantIdStr = savedTenant.getId().getId().toString(); + deleteTenant(savedTenant.getId()); + loginSysAdmin(); + doGet("/api/tenant/" + tenantIdStr) + .andExpect(status().isNotFound()) + .andExpect(statusReason(containsString(msgErrorNoFound("Tenant", tenantIdStr)))); + } + @Test public void testFindTenants() throws Exception { loginSysAdmin(); - List tenants = new ArrayList<>(); PageLink pageLink = new PageLink(17); PageData pageData = doGetTypedWithPageLink("/api/tenants?", PAGE_DATA_TENANT_TYPE_REF, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(1, pageData.getData().size()); - tenants.addAll(pageData.getData()); + List tenants = new ArrayList<>(pageData.getData()); Mockito.reset(tbClusterService); @@ -378,12 +447,11 @@ public class TenantControllerTest extends AbstractControllerTest { @Test public void testFindTenantInfos() throws Exception { loginSysAdmin(); - List tenants = new ArrayList<>(); PageLink pageLink = new PageLink(17); PageData pageData = doGetTypedWithPageLink("/api/tenantInfos?", PAGE_DATA_TENANT_INFO_TYPE_REF, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(1, pageData.getData().size()); - tenants.addAll(pageData.getData()); + List tenants = new ArrayList<>(pageData.getData()); List> createFutures = new ArrayList<>(56); for (int i = 0; i < 56; i++) { diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java index 476428e016..a2e622c8b5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Test; import org.mockito.ArgumentMatcher; @@ -25,9 +26,12 @@ import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.audit.ActionType; 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.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; @@ -42,8 +46,13 @@ import org.thingsboard.server.queue.TbQueueCallback; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; @@ -77,12 +86,17 @@ public class TenantProfileControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.CREATED, 1); + awaitAuditLog("Wait for async audit log to be persisted (ADDED expected)", savedTenantProfile.getId(), ActionType.ADDED); + 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()); testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.UPDATED, 1); + + awaitAuditLog("Wait for async audit log to be persisted (UPDATED expected)", savedTenantProfile.getId(), ActionType.UPDATED); + } @Test @@ -120,6 +134,43 @@ public class TenantProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName()); } + @Test + public void testFindTenantProfilesByIds() throws Exception { + loginSysAdmin(); + List tenantProfiles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile " + i); + tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class)); + } + + List expected = tenantProfiles.subList(2, 7); + + String idsParam = expected.stream() + .map(tp -> tp.getId().getId().toString()) + .collect(Collectors.joining(",")); + + TenantProfile[] result = doGet( + "/api/tenantProfiles?ids=" + idsParam, + TenantProfile[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map tpById = Arrays.stream(result) + .collect(Collectors.toMap(tp -> tp.getId().getId(), Function.identity())); + + for (TenantProfile tp : expected) { + UUID id = tp.getId().getId(); + TenantProfile found = tpById.get(id); + Assert.assertNotNull("TenantProfile not found for id " + id, found); + + Assert.assertEquals(tp.getId(), found.getId()); + Assert.assertEquals(tp.getName(), found.getName()); + } + } + + @Test public void testFindDefaultTenantProfileInfo() throws Exception { loginSysAdmin(); @@ -139,6 +190,8 @@ public class TenantProfileControllerTest extends AbstractControllerTest { Assert.assertNotNull(foundDefaultTenantProfile); Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfile.getName()); Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfile.getId()); + + awaitAuditLog("Wait for async audit log to be persisted (UPDATED expected for NEW DEFAULT Tenant Profile)", savedTenantProfile.getId(), ActionType.UPDATED); } @Test @@ -204,6 +257,8 @@ public class TenantProfileControllerTest extends AbstractControllerTest { doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) .andExpect(status().isOk()); + awaitAuditLog("Wait for async audit log to be persisted for deleted tenant profile", savedTenantProfile.getId(), ActionType.DELETED); + testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.DELETED, 1); doGet("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) @@ -352,6 +407,17 @@ public class TenantProfileControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventNeverTenantProfile(); } + private void awaitAuditLog(String awaitMessage, TenantProfileId tenantProfileId, ActionType expectedAction) { + Awaitility.await(awaitMessage) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doGetTypedWithTimePageLink( + "/api/audit/logs?", + new TypeReference>() {}, + new TimePageLink(100)).getData().stream() + .anyMatch(log -> log.getEntityId().equals(tenantProfileId) && log.getActionType() == expectedAction) + ); + } + private TenantProfile createTenantProfile(String name) { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName(name); @@ -388,7 +454,6 @@ public class TenantProfileControllerTest extends AbstractControllerTest { tenantProfile.setProfileData(profileData); } - private void testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(TenantProfile tenantProfile, ComponentLifecycleEvent event, int cntTime) { ArgumentMatcher matcherTenantProfile = cntTime == 1 ? argument -> argument.equals(tenantProfile) : argument -> argument.getClass().equals(TenantProfile.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index c3eb91db85..c8b5bf06c9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -30,6 +30,8 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; @@ -48,6 +50,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFaP import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -85,7 +88,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaConfigManager.deletePlatformTwoFaSettings(tenantId); } - @Test public void testSavePlatformTwoFaSettings() throws Exception { loginSysAdmin(); @@ -102,15 +104,32 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); twoFaSettings.setTotalAllowedTimeForVerification(3600); + twoFaSettings.setEnforceTwoFa(true); + twoFaSettings.setEnforcedUsersFilter(new AllUsersFilter()); - doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + saveTwoFaSettings(twoFaSettings); - PlatformTwoFaSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class); + PlatformTwoFaSettings savedTwoFaSettings = findTwoFaSettings(); assertThat(savedTwoFaSettings.getProviders()).hasSize(2); assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); } + @Test + public void testSavePlatformTwoFaSettingsWithEnforceTwoFaWithoutProviders() throws Exception { + loginSysAdmin(); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(List.of()); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); + twoFaSettings.setTotalAllowedTimeForVerification(3600); + twoFaSettings.setEnforceTwoFa(true); + + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isBadRequest()); + } + @Test public void testSavePlatformTwoFaSettings_validationError() throws Exception { loginSysAdmin(); @@ -157,17 +176,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { assertThat(errorResponse).containsIgnoringCase("verificationCodeLifetime is required"); } - private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { - PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); - twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); - twoFaSettings.setMinVerificationCodeSendPeriod(5); - twoFaSettings.setTotalAllowedTimeForVerification(100); - - return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) - .andExpect(status().isBadRequest())); - } - - @Test public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { configureSmsTwoFaProvider("${code}"); @@ -268,24 +276,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); } - private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception { - TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") - .andExpect(status().isOk()), TwoFaAccountConfig.class); - assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class); - - assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { - UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); - assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); - assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); - assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); - assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); - assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { - assertDoesNotThrow(() -> Base32.decode(secretKey)); - }); - }); - return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig; - } - @Test public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { testVerifyAndSaveTotpTwoFaAccountConfig(); @@ -419,6 +409,56 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig); } + @Test + public void testIsTwoFaEnabled() throws Exception { + configureSmsTwoFaProvider("${code}"); + SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); + accountConfig.setPhoneNumber("+38050505050"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUser, accountConfig); + + assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUser)).isTrue(); + } + + @Test + public void testDeleteTwoFaAccountConfig() throws Exception { + configureSmsTwoFaProvider("${code}"); + loginTenantAdmin(); + SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); + accountConfig.setPhoneNumber("+38050505050"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUser, accountConfig); + + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); + assertThat(savedAccountConfig).isEqualTo(accountConfig); + + PlatformTwoFaSettings twoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, true).get(); + twoFaSettings.setEnforceTwoFa(true); + TenantAdministratorsFilter enforcedUsersFilter = new TenantAdministratorsFilter(); + enforcedUsersFilter.setTenantsIds(Set.of(tenantId.getId())); + twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter); + twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + String errorMessage = getErrorMessage(doDelete("/api/2fa/account/config?providerType=SMS") + .andExpect(status().isBadRequest())); + assertThat(errorMessage).isEqualTo("At least one 2FA provider is required"); + + twoFaSettings.setEnforceTwoFa(false); + twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk()); + + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) + .doesNotContainKey(TwoFaProviderType.SMS); + } + + private PlatformTwoFaSettings findTwoFaSettings() throws Exception { + return doGet("/api/2fa/settings", PlatformTwoFaSettings.class); + } + + private void saveTwoFaSettings(PlatformTwoFaSettings twoFaSettings) throws Exception { + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + } + private TotpTwoFaProviderConfig configureTotpTwoFaProvider() throws Exception { TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); totpTwoFaProviderConfig.setIssuerName("tb"); @@ -441,37 +481,35 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); twoFaSettings.setMinVerificationCodeSendPeriod(5); twoFaSettings.setTotalAllowedTimeForVerification(100); - doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + saveTwoFaSettings(twoFaSettings); } - @Test - public void testIsTwoFaEnabled() throws Exception { - configureSmsTwoFaProvider("${code}"); - SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); - accountConfig.setPhoneNumber("+38050505050"); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); + private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception { + TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isOk()), TwoFaAccountConfig.class); + assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class); - assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue(); + assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { + UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); + assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); + assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); + assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); + assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); + assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { + assertDoesNotThrow(() -> Base32.decode(secretKey)); + }); + }); + return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig; } - @Test - public void testDeleteTwoFaAccountConfig() throws Exception { - configureSmsTwoFaProvider("${code}"); - SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); - accountConfig.setPhoneNumber("+38050505050"); - - loginTenantAdmin(); - - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); - - AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); - TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); - assertThat(savedAccountConfig).isEqualTo(accountConfig); - - doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk()); + private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); - assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) - .doesNotContainKey(TwoFaProviderType.SMS); + return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isBadRequest())); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index b20dd570fb..ca81b97a48 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -25,6 +25,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; @@ -33,6 +34,8 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; @@ -58,6 +61,7 @@ import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -116,7 +120,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { public void testTwoFa_totp() throws Exception { TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); doPost("/api/auth/2fa/verification/send?providerType=TOTP") .andExpect(status().isOk()); @@ -136,7 +140,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { public void testTwoFa_sms() throws Exception { configureSmsTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); doPost("/api/auth/2fa/verification/send?providerType=SMS") .andExpect(status().isOk()); @@ -160,7 +164,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setTotalAllowedTimeForVerification(65); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); await("expiration of the pre-verification token") .atLeast(Duration.ofSeconds(30).plusMillis(500)) @@ -177,7 +181,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); Stream.generate(() -> StringUtils.randomNumeric(6)) .limit(9) @@ -206,7 +210,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setMinVerificationCodeSendPeriod(10); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); doPost("/api/auth/2fa/verification/send?providerType=TOTP") .andExpect(status().isOk()); @@ -230,7 +234,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); for (int i = 0; i < 3; i++) { String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") @@ -258,7 +262,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { @Test public void testCheckVerificationCode_invalidVerificationCode() throws Exception { configureTotpTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode) @@ -273,7 +277,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { smsTwoFaProviderConfig.setVerificationCodeLifetime(10); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk()); @@ -296,7 +300,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { public void testTwoFa_logLoginAction() throws Exception { TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); await("async audit log saving").during(1, TimeUnit.SECONDS); doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") @@ -334,7 +338,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { @Test public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { configureTotpTwoFa(); - twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), TwoFaProviderType.TOTP); + twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user, TwoFaProviderType.TOTP); assertDoesNotThrow(() -> { login(username, password); @@ -368,17 +372,17 @@ public class TwoFactorAuthTest extends AbstractControllerTest { TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(twoFaUser, TwoFaProviderType.TOTP); totpTwoFaAccountConfig.setUseByDefault(true); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), totpTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, totpTwoFaAccountConfig); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); smsTwoFaAccountConfig.setPhoneNumber("+38012312322"); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), smsTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, smsTwoFaAccountConfig); EmailTwoFaAccountConfig emailTwoFaAccountConfig = new EmailTwoFaAccountConfig(); emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail()); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), emailTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, emailTwoFaAccountConfig); - logInWithPreVerificationToken(twoFaUser.getEmail(), "12345678"); + logInWithMfaToken(twoFaUser.getEmail(), "12345678", Authority.PRE_VERIFICATION_TOKEN); Map providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference>() {}).stream() .collect(Collectors.toMap(TwoFactorAuthController.TwoFaProviderInfo::getType, v -> v)); @@ -395,13 +399,79 @@ public class TwoFactorAuthTest extends AbstractControllerTest { assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse(); } - private void logInWithPreVerificationToken(String username, String password) throws Exception { + @Test + public void testEnforceTwoFa() throws Exception { + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); + twoFaSettings.setEnforceTwoFa(true); + TenantAdministratorsFilter enforcedUsersFilter = new TenantAdministratorsFilter(); + enforcedUsersFilter.setTenantsIds(Set.of(tenantId.getId())); + twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter); + twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + logInWithMfaToken(username, password, Authority.MFA_CONFIGURATION_TOKEN); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class); + + String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String verificationCode = new Totp(secret).now(); + readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class); + + JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class); + assertThat(tokenPair.getToken()).isNotEmpty(); + assertThat(tokenPair.getRefreshToken()).isNotEmpty(); + validateAndSetJwtToken(tokenPair, username); + + doGet("/api/user/" + user.getId()).andExpect(status().isOk()); + + // verifying enforced users filter + createDifferentTenant(); + doGet("/api/user/" + savedDifferentTenantUser.getId()).andExpect(status().isOk()); + } + + @Test + public void testEnforceTwoFa_sysadmin() throws Exception { + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); + twoFaSettings.setEnforceTwoFa(true); + AllUsersFilter enforcedUsersFilter = new AllUsersFilter(); + twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter); + twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + logInWithMfaToken(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD, Authority.MFA_CONFIGURATION_TOKEN); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class); + String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String verificationCode = new Totp(secret).now(); + readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class); + + JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class); + assertThat(tokenPair.getToken()).isNotEmpty(); + assertThat(tokenPair.getRefreshToken()).isNotEmpty(); + validateAndSetJwtToken(tokenPair, SYS_ADMIN_EMAIL); + + doGet("/api/user/" + user.getId()).andExpect(status().isOk()); + } + + private void logInWithMfaToken(String username, String password, Authority expectedScope) throws Exception { LoginRequest loginRequest = new LoginRequest(username, password); JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class); assertThat(response.getToken()).isNotNull(); assertThat(response.getRefreshToken()).isNull(); - assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); + assertThat(response.getScope()).isEqualTo(expectedScope); this.token = response.getToken(); } @@ -418,7 +488,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user, totpTwoFaAccountConfig); return totpTwoFaAccountConfig; } @@ -436,7 +506,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user, smsTwoFaAccountConfig); return smsTwoFaAccountConfig; } diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index 7d7131df96..957b3721e0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -50,13 +50,17 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.settings.StarredDashboardInfo; import org.thingsboard.server.common.data.settings.UserDashboardsInfo; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.user.UserDao; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -263,6 +267,37 @@ public class UserControllerTest extends AbstractControllerTest { Assert.assertEquals(savedUser, foundUser); } + @Test + public void testFindUsersByIds() throws Exception { + loginTenantAdmin(); + List savedUsers = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + User user = createTenantAdminUser(); + savedUsers.add(doPost("/api/user", user, User.class)); + } + + String idsParam = savedUsers.stream() + .map(u -> u.getId().getId().toString()) + .collect(Collectors.joining(",")); + + User[] foundUsers = doGet("/api/users?userIds=" + idsParam, User[].class); + + Assert.assertNotNull(foundUsers); + Assert.assertEquals(savedUsers.size(), foundUsers.length); + + Map foundById = Arrays.stream(foundUsers) + .collect(Collectors.toMap(u -> u.getId().getId(), Function.identity())); + + for (User savedUser : savedUsers) { + User foundUser = foundById.get(savedUser.getId().getId()); + Assert.assertNotNull("User not found for id " + savedUser.getId().getId(), foundUser); + + foundUser.setAdditionalInfo(savedUser.getAdditionalInfo()); + Assert.assertEquals(savedUser, foundUser); + } + } + + @Test public void testSaveUserWithSameEmail() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java similarity index 85% rename from application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java rename to application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java index 6ec0f3ad64..2174b7425f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java @@ -76,13 +76,11 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; -import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -96,7 +94,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. "server.ws.alarms_per_alarm_status_subscription_cache_size=5", "server.ws.dynamic_page_link.refresh_interval=15" }) -public class WebsocketApiTest extends AbstractControllerTest { +public class WebSocketApiTest extends AbstractControllerTest { + @Autowired private TelemetrySubscriptionService tsService; @@ -136,8 +135,8 @@ public class WebsocketApiTest extends AbstractControllerTest { 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); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertEquals(0, pageData.getData().getFirst().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)); @@ -152,8 +151,8 @@ public class WebsocketApiTest extends AbstractControllerTest { 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(device.getId(), dataList.getFirst().getEntityId()); + TsValue[] tsArray = dataList.getFirst().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]); @@ -170,7 +169,7 @@ public class WebsocketApiTest extends AbstractControllerTest { PageData pageData = update.getData(); Assert.assertNotNull(pageData); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); 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)); @@ -184,8 +183,8 @@ public class WebsocketApiTest extends AbstractControllerTest { 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(device.getId(), listData.getFirst().getEntityId()); + TsValue[] tsArray = listData.getFirst().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]); @@ -194,7 +193,7 @@ public class WebsocketApiTest extends AbstractControllerTest { now = System.currentTimeMillis(); TsKvEntry dataPoint4 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 45L)); getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint4)); + sendTelemetry(device, List.of(dataPoint4)); String msg = getWsClient().waitForUpdate(); update = JacksonUtil.fromString(msg, EntityDataUpdate.class); @@ -202,9 +201,9 @@ public class WebsocketApiTest extends AbstractControllerTest { 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.assertEquals(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getTimeseries()); + TsValue[] tsValues = eData.getFirst().getTimeseries().get("temperature"); Assert.assertNotNull(tsValues); Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]); } @@ -529,7 +528,7 @@ public class WebsocketApiTest extends AbstractControllerTest { String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); - // check device + // check a device AlarmStatusCmd deviceCmd = new AlarmStatusCmd(2, device.getId(), null, List.of(AlarmSeverity.CRITICAL)); getWsClient().send(deviceCmd); @@ -597,13 +596,13 @@ public class WebsocketApiTest extends AbstractControllerTest { 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()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().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); + List tsData = List.of(dataPoint1); sendTelemetry(device, tsData); update = getWsClient().subscribeLatestUpdate(keys); @@ -613,36 +612,36 @@ public class WebsocketApiTest extends AbstractControllerTest { 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(device.getId(), listData.getFirst().getEntityId()); + Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + TsValue tsValue = listData.getFirst().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)); getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); 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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + tsValue = eData.getFirst().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; getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint1)); + sendTelemetry(device, List.of(dataPoint1)); String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); } @@ -685,14 +684,14 @@ public class WebsocketApiTest extends AbstractControllerTest { 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()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); getWsClient().registerWaitForUpdate(); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); - List tsData = Arrays.asList(dataPoint1); + List tsData = List.of(dataPoint1); sendTelemetry(device, tsData); update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); @@ -702,34 +701,34 @@ public class WebsocketApiTest extends AbstractControllerTest { 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(device.getId(), listData.getFirst().getEntityId()); + Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + TsValue tsValue = listData.getFirst().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)); getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); 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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES)); + tsValue = eData.getFirst().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; + //Sending update from the past, while the latest value has new timestamp; getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint1)); + sendTelemetry(device, List.of(dataPoint1)); String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendTelemetry(device, Arrays.asList(dataPoint2)); + sendTelemetry(device, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); } @@ -779,21 +778,21 @@ public class WebsocketApiTest extends AbstractControllerTest { 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.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); getWsClient().registerWaitForUpdate(); - // Pushing update with wrong scope and make sure it will not arrive. + // Pushing update with the wrong scope and make sure it will not arrive. AttributeKvEntry invalidDataPoint = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 55L)); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(invalidDataPoint)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(invalidDataPoint)); Assert.assertNull(getWsClient().waitForUpdate(3000)); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); - List tsData = Arrays.asList(dataPoint1); + List tsData = List.of(dataPoint1); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); String msg = getWsClient().waitForUpdate(); @@ -804,16 +803,16 @@ public class WebsocketApiTest extends AbstractControllerTest { 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(device.getId(), listData.getFirst().getEntityId()); + Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + TsValue tsValue = listData.getFirst().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)); getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(); Assert.assertNotNull(msg); @@ -822,20 +821,20 @@ public class WebsocketApiTest extends AbstractControllerTest { 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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + tsValue = eData.getFirst().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; + //Sending update from the past, while the latest value has new timestamp; getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint1)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); } @@ -855,23 +854,23 @@ public class WebsocketApiTest extends AbstractControllerTest { 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()); + Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue()); + Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey")); + Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs()); + Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue()); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); - List tsData = Arrays.asList(dataPoint1); + List tsData = List.of(dataPoint1); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); @@ -882,78 +881,78 @@ public class WebsocketApiTest extends AbstractControllerTest { 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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); + TsValue attrValue = eData.getFirst().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; + //Sending update from the past, while the latest value has new timestamp; getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint1)); + sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint1)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending update from the past, while latest value has new timestamp; getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("sharedAttributeKey", 42L)); - sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint2)); + sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"); Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), attrValue); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint3 = new BaseAttributeKvEntry(now, new LongDataEntry("clientAttributeKey", 42L)); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint3)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint3)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"); Assert.assertEquals(new TsValue(dataPoint3.getLastUpdateTs(), dataPoint3.getValueAsString()), attrValue); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint4 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 42L)); - sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint4)); + sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint4)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); Assert.assertEquals(new TsValue(dataPoint4.getLastUpdateTs(), dataPoint4.getValueAsString()), attrValue); getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint5 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 43L)); - sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint5)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint5)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); update = JacksonUtil.fromString(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(device.getId(), eData.getFirst().getEntityId()); + Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE)); + attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); Assert.assertEquals(new TsValue(dataPoint5.getLastUpdateTs(), dataPoint5.getValueAsString()), attrValue); } @@ -1042,7 +1041,7 @@ public class WebsocketApiTest extends AbstractControllerTest { .tenantId(device.getTenantId()) .entityId(device.getId()) .entries(tsData) - .callback(new FutureCallback() { + .callback(new FutureCallback<>() { @Override public void onSuccess(@Nullable Void result) { log.debug("sendTelemetry callback onSuccess"); diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java index 0bc7cb4f59..917470eea1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java @@ -29,12 +29,17 @@ 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.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -150,6 +155,37 @@ public class WidgetsBundleControllerTest extends AbstractControllerTest { Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle); } + @Test + public void testFindWidgetsBundlesByIds() throws Exception { + List savedWidgetsBundles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTitle("My widgets bundle " + i); + savedWidgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class)); + } + + String idsParam = savedWidgetsBundles.stream() + .map(wb -> wb.getId().getId().toString()) + .collect(Collectors.joining(",")); + + WidgetsBundle[] foundWidgetsBundles = + doGet("/api/widgetsBundles?widgetsBundleIds=" + idsParam, WidgetsBundle[].class); + + Assert.assertNotNull(foundWidgetsBundles); + Assert.assertEquals(savedWidgetsBundles.size(), foundWidgetsBundles.length); + + Map foundById = Arrays.stream(foundWidgetsBundles) + .collect(Collectors.toMap(wb -> wb.getId().getId(), Function.identity())); + + for (WidgetsBundle savedWidgetsBundle : savedWidgetsBundles) { + UUID id = savedWidgetsBundle.getId().getId(); + WidgetsBundle foundWidgetsBundle = foundById.get(id); + Assert.assertNotNull("WidgetsBundle not found for id " + id, foundWidgetsBundle); + Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle); + } + } + + @Test public void testDeleteWidgetsBundle() throws Exception { WidgetsBundle widgetsBundle = new WidgetsBundle(); diff --git a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java index 4dc637725f..970c244fad 100644 --- a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java @@ -36,9 +36,13 @@ 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.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; +import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.WebSocketSessionType; import java.io.IOException; import java.lang.reflect.Method; @@ -262,4 +266,101 @@ class TbWebSocketHandlerTest { return ref; } + private AuthTestFixture createAuthTestFixture() throws IOException { + TbWebSocketHandler handler = spy(new TbWebSocketHandler()); + willDoNothing().given(handler).close(any(), any()); + + ApiKeyAuthenticationProvider apiKeyProvider = mock(ApiKeyAuthenticationProvider.class); + JwtAuthenticationProvider jwtProvider = mock(JwtAuthenticationProvider.class); + WebSocketService wsService = mock(WebSocketService.class); + + ReflectionTestUtils.setField(handler, "apiKeyAuthenticationProvider", apiKeyProvider); + ReflectionTestUtils.setField(handler, "authenticationProvider", jwtProvider); + ReflectionTestUtils.setField(handler, "webSocketService", wsService); + ReflectionTestUtils.setField(handler, "tenantProfileCache", mock(TbTenantProfileCache.class)); + ReflectionTestUtils.setField(handler, "authTimeoutMs", 10000); + ReflectionTestUtils.invokeMethod(handler, "init"); + + WebSocketSessionRef ref = WebSocketSessionRef.builder() + .sessionId(UUID.randomUUID().toString()) + .sessionType(WebSocketSessionType.GENERAL) + .build(); + + NativeWebSocketSession wsSession = mock(NativeWebSocketSession.class); + Session nativeSess = mock(Session.class); + willReturn(nativeSess).given(wsSession).getNativeSession(Session.class); + RemoteEndpoint.Async async = mock(RemoteEndpoint.Async.class); + willReturn(async).given(nativeSess).getAsyncRemote(); + willReturn("test-session-id").given(wsSession).getId(); + + TbWebSocketHandler.SessionMetaData sessionMd = handler.new SessionMetaData(wsSession, ref); + + return new AuthTestFixture(handler, apiKeyProvider, jwtProvider, ref, sessionMd); + } + + @Test + void processMsg_authenticatesWithApiKey() throws Exception { + AuthTestFixture f = createAuthTestFixture(); + + SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS); + willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key"); + + String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\"},\"cmds\":[]}"; + f.handler.processMsg(f.sessionMd, msg); + + verify(f.apiKeyProvider).authenticate("my-api-key"); + verify(f.jwtProvider, never()).authenticate(anyString()); + assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser); + } + + @Test + void processMsg_authenticatesWithJwtToken() throws Exception { + AuthTestFixture f = createAuthTestFixture(); + + SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS); + willReturn(securityUser).given(f.jwtProvider).authenticate("my-jwt-token"); + + String msg = "{\"authCmd\":{\"cmdId\":1,\"token\":\"my-jwt-token\"},\"cmds\":[]}"; + f.handler.processMsg(f.sessionMd, msg); + + verify(f.jwtProvider).authenticate("my-jwt-token"); + verify(f.apiKeyProvider, never()).authenticate(anyString()); + assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser); + } + + @Test + void processMsg_apiKeyTakesPrecedenceOverToken() throws Exception { + AuthTestFixture f = createAuthTestFixture(); + + SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS); + willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key"); + + String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\",\"token\":\"my-jwt-token\"},\"cmds\":[]}"; + f.handler.processMsg(f.sessionMd, msg); + + verify(f.apiKeyProvider).authenticate("my-api-key"); + verify(f.jwtProvider, never()).authenticate(anyString()); + assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser); + } + + @Test + void extractQueryParam_parsesCorrectly() throws Exception { + TbWebSocketHandler handler = new TbWebSocketHandler(); + Method method = TbWebSocketHandler.class.getDeclaredMethod("extractQueryParam", String.class, String.class); + method.setAccessible(true); + + assertThat(method.invoke(handler, "token=jwt123", "token")).isEqualTo("jwt123"); + assertThat(method.invoke(handler, "token=jwt123&other=abc123", "token")).isEqualTo("jwt123"); + assertThat(method.invoke(handler, "other=value", "token")).isNull(); + assertThat(method.invoke(handler, "tokenExtra=value", "token")).isNull(); + } + + private record AuthTestFixture( + TbWebSocketHandler handler, + ApiKeyAuthenticationProvider apiKeyProvider, + JwtAuthenticationProvider jwtProvider, + WebSocketSessionRef ref, + TbWebSocketHandler.SessionMetaData sessionMd + ) {} + } diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index b46811daf5..d75c7b8f59 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -22,6 +22,7 @@ import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -38,6 +39,7 @@ 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.DeviceProfile; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.StringUtils; @@ -105,6 +107,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.UUID; @@ -603,7 +606,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); Assert.assertNotNull(deviceCredentialsMsg); Assert.assertEquals(savedDevice.getId(), deviceCredentialsMsg.getDeviceId()); - Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); + compareHasVersionEntities(deviceCredentials, deviceCredentialsMsg); return savedDevice; } @@ -750,4 +753,33 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { return rpc; } + protected void verifyEdgeDisconnected() { + verifyEdgeActiveFlag(false); + } + + protected void verifyEdgeConnected() { + verifyEdgeActiveFlag(true); + } + + private void verifyEdgeActiveFlag(boolean value) { + Awaitility.await() + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); + Optional> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst(); + if (activeAttrOpt.isEmpty()) { + return false; + } + Map activeAttr = activeAttrOpt.get(); + return Boolean.toString(value).equals(activeAttr.get("value").toString()); + }); + } + + protected void compareHasVersionEntities(HasVersion entity1, HasVersion entity2) { + entity1.setVersion(null); + entity2.setVersion(null); + Assert.assertEquals(entity1, entity2); + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java new file mode 100644 index 0000000000..4b23ec3cf9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java @@ -0,0 +1,216 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; + +@DaoSqlTest +public class AiModelEdgeTest extends AbstractEdgeTest { + + private static final String DEFAULT_AI_MODEL_NAME = "Edge Test AiModel"; + private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel"; + + @Test + public void testAiModel_create_update_delete_fromCloud() throws Exception { + // create AiModel + AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); + + edgeImitator.expectMessageAmount(1); + AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg); + AiModelUpdateMsg aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType()); + Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB()); + Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB()); + AiModel aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true); + Assert.assertNotNull(aiModelFromMsg); + + Assert.assertEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName()); + Assert.assertEquals(savedAiModel.getTenantId(), aiModelFromMsg.getTenantId()); + + // update AiModel + edgeImitator.expectMessageAmount(1); + savedAiModel.setName(UPDATED_AI_MODEL_NAME); + savedAiModel = doPost("/api/ai/model", savedAiModel, AiModel.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg); + aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage; + aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true); + Assert.assertNotNull(aiModelFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_AI_MODEL_NAME, aiModelFromMsg.getName()); + + // delete AiModel + edgeImitator.expectMessageAmount(1); + doDelete("/api/ai/model/" + savedAiModel.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg); + aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType()); + Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB()); + Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB()); + } + + @Test + public void testAiModel_create_update_delete_toCloud() throws Exception { + // create + AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName()); + + // update + aiModel.setName(UPDATED_AI_MODEL_NAME); + UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + + checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName()); + + // delete + UplinkMsg deleteUplinkMsg = getDeleteUplinkMsg(uuid); + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(deleteUplinkMsg); + Assert.assertTrue(edgeImitator.waitForResponses()); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> + doGet("/api/ai/model/" + uuid, AiModel.class, status().isNotFound()) + ); + } + + @Test + public void testAiModelToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); + + edgeImitator.expectMessageAmount(1); + AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional aiModelUpdateMsgOpt = edgeImitator.findMessageByType(AiModelUpdateMsg.class); + Assert.assertTrue(aiModelUpdateMsgOpt.isPresent()); + AiModelUpdateMsg latestAiModelUpdateMsg = aiModelUpdateMsgOpt.get(); + AiModel aiModelFromMsg = JacksonUtil.fromString(latestAiModelUpdateMsg.getEntity(), AiModel.class, true); + Assert.assertNotNull(aiModelFromMsg); + Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName()); + + Assert.assertNotEquals(savedAiModel.getUuidId(), uuid); + + AiModel aiModelFromCloud = doGet("/api/ai/model/" + uuid, AiModel.class); + Assert.assertNotNull(aiModelFromCloud); + Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromCloud.getName()); + } + + private AiModel createSimpleAiModel(String name) { + AiModel aiModel = new AiModel(); + aiModel.setTenantId(tenantId); + aiModel.setName(name); + aiModel.setConfiguration(OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig(null, "test-api-key")) + .modelId("gpt-4o") + .temperature(0.5) + .topP(0.3) + .frequencyPenalty(0.1) + .presencePenalty(0.2) + .maxOutputTokens(1000) + .timeoutSeconds(60) + .maxRetries(2) + .build()); + return aiModel; + } + + private UplinkMsg getDeleteUplinkMsg(UUID uuid) throws InvalidProtocolBufferException { + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + AiModelUpdateMsg.Builder aiModelDeleteMsgBuilder = AiModelUpdateMsg.newBuilder(); + aiModelDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE); + aiModelDeleteMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + aiModelDeleteMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(aiModelDeleteMsgBuilder); + + upLinkMsgBuilder.addAiModelUpdateMsg(aiModelDeleteMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + return upLinkMsgBuilder.build(); + } + + private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException { + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder(); + aiModelUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + aiModelUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + aiModelUpdateMsgBuilder.setEntity(JacksonUtil.toString(aiModel)); + aiModelUpdateMsgBuilder.setMsgType(updateMsgType); + testAutoGeneratedCodeByProtobuf(aiModelUpdateMsgBuilder); + uplinkMsgBuilder.addAiModelUpdateMsg(aiModelUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + return uplinkMsgBuilder.build(); + } + + private void checkAiModelOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception { + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + AiModel aiModel = doGet("/api/ai/model/" + uuid, AiModel.class); + Assert.assertNotNull(aiModel); + Assert.assertEquals(resourceTitle, aiModel.getName()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/ApiKeyEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/ApiKeyEdgeTest.java new file mode 100644 index 0000000000..bd46cb5251 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/ApiKeyEdgeTest.java @@ -0,0 +1,238 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.ApiKeyId; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.pat.ApiKeyService; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; + +@DaoSqlTest +public class ApiKeyEdgeTest extends AbstractEdgeTest { + + @Autowired + private ApiKeyService apiKeyService; + + private static final String DEFAULT_API_KEY_DESCRIPTION = "Edge Test ApiKey"; + private static final String UPDATED_API_KEY_DESCRIPTION = "Updated Edge Test ApiKey"; + + @Test + public void testApiKey_create_update_delete_fromCloud() throws Exception { + // create ApiKey + ApiKeyInfo apiKeyInfo = createSimpleApiKeyInfo(DEFAULT_API_KEY_DESCRIPTION); + + edgeImitator.expectMessageAmount(1); + ApiKey savedApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg); + ApiKeyUpdateMsg apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType()); + Assert.assertEquals(savedApiKey.getUuidId().getMostSignificantBits(), apiKeyUpdateMsg.getIdMSB()); + Assert.assertEquals(savedApiKey.getUuidId().getLeastSignificantBits(), apiKeyUpdateMsg.getIdLSB()); + ApiKey apiKeyFromMsg = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true); + Assert.assertNotNull(apiKeyFromMsg); + + Assert.assertEquals(DEFAULT_API_KEY_DESCRIPTION, apiKeyFromMsg.getDescription()); + Assert.assertEquals(savedApiKey.getTenantId(), apiKeyFromMsg.getTenantId()); + + // update ApiKey + edgeImitator.expectMessageAmount(1); + savedApiKey.setDescription(UPDATED_API_KEY_DESCRIPTION); + savedApiKey = doPost("/api/apiKey", new ApiKeyInfo(savedApiKey), ApiKey.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg); + apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage; + apiKeyFromMsg = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true); + Assert.assertNotNull(apiKeyFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_API_KEY_DESCRIPTION, apiKeyFromMsg.getDescription()); + + // delete ApiKey + edgeImitator.expectMessageAmount(1); + doDelete("/api/apiKey/" + savedApiKey.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg); + apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType()); + Assert.assertEquals(savedApiKey.getUuidId().getMostSignificantBits(), apiKeyUpdateMsg.getIdMSB()); + Assert.assertEquals(savedApiKey.getUuidId().getLeastSignificantBits(), apiKeyUpdateMsg.getIdLSB()); + } + + @Test + public void testApiKey_create_update_delete_toCloud() throws Exception { + // create + ApiKey apiKey = createSimpleApiKey(DEFAULT_API_KEY_DESCRIPTION); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, apiKey, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkApiKeyOnCloud(uplinkMsg, uuid, apiKey.getDescription()); + + // update + apiKey.setDescription(UPDATED_API_KEY_DESCRIPTION); + UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, apiKey, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + + checkApiKeyOnCloud(updatedUplinkMsg, uuid, apiKey.getDescription()); + + // delete + UplinkMsg deleteUplinkMsg = getDeleteUplinkMsg(uuid); + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(deleteUplinkMsg); + Assert.assertTrue(edgeImitator.waitForResponses()); + + ApiKeyId apiKeyId = new ApiKeyId(uuid); + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> + Assert.assertNull(apiKeyService.findApiKeyById(tenantId, apiKeyId)) + ); + } + + @Test + public void testApiKey_pushedDuringUserSync() throws Exception { + // create tenant admin user - expect 3 messages: 1 UserUpdateMsg + 2 UserCredentialsUpdateMsg + User user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(tenantId); + user.setEmail("apiKeyTestUser@thingsboard.org"); + user.setFirstName("ApiKey"); + user.setLastName("TestUser"); + + edgeImitator.expectMessageAmount(3); + User savedUser = createUser(user, "tenant"); + Assert.assertTrue(edgeImitator.waitForMessages()); + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); + + // create API key for this user - expect 1 ApiKeyUpdateMsg + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setTenantId(tenantId); + apiKeyInfo.setUserId(savedUser.getId()); + apiKeyInfo.setDescription("Test API Key for user sync"); + apiKeyInfo.setEnabled(true); + + edgeImitator.expectMessageAmount(1); + doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(ApiKeyUpdateMsg.class).size()); + + // update user - expect 3 messages: UserUpdateMsg + UserCredentialsUpdateMsg + ApiKeyUpdateMsg + savedUser.setLastName("UpdatedLastName"); + edgeImitator.expectMessageAmount(3); + doPost("/api/user", savedUser, User.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(ApiKeyUpdateMsg.class).size()); + + Optional apiKeyUpdateMsgOpt = edgeImitator.findMessageByType(ApiKeyUpdateMsg.class); + Assert.assertTrue(apiKeyUpdateMsgOpt.isPresent()); + ApiKeyUpdateMsg apiKeyUpdateMsg = apiKeyUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType()); + } + + private ApiKeyInfo createSimpleApiKeyInfo(String description) { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setTenantId(tenantId); + apiKeyInfo.setUserId(tenantAdminUserId); + apiKeyInfo.setDescription(description); + apiKeyInfo.setEnabled(true); + return apiKeyInfo; + } + + private ApiKey createSimpleApiKey(String description) { + ApiKey apiKey = new ApiKey(); + apiKey.setTenantId(tenantId); + apiKey.setUserId(tenantAdminUserId); + apiKey.setDescription(description); + apiKey.setEnabled(true); + apiKey.setValue("test-api-key-value-" + UUID.randomUUID()); + return apiKey; + } + + private UplinkMsg getDeleteUplinkMsg(UUID uuid) throws InvalidProtocolBufferException { + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + ApiKeyUpdateMsg.Builder apiKeyDeleteMsgBuilder = ApiKeyUpdateMsg.newBuilder(); + apiKeyDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE); + apiKeyDeleteMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + apiKeyDeleteMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(apiKeyDeleteMsgBuilder); + + upLinkMsgBuilder.addApiKeyUpdateMsg(apiKeyDeleteMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + return upLinkMsgBuilder.build(); + } + + private UplinkMsg getUplinkMsg(UUID uuid, ApiKey apiKey, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException { + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + ApiKeyUpdateMsg.Builder apiKeyUpdateMsgBuilder = ApiKeyUpdateMsg.newBuilder(); + apiKeyUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + apiKeyUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + apiKeyUpdateMsgBuilder.setEntity(JacksonUtil.toString(apiKey)); + apiKeyUpdateMsgBuilder.setMsgType(updateMsgType); + testAutoGeneratedCodeByProtobuf(apiKeyUpdateMsgBuilder); + uplinkMsgBuilder.addApiKeyUpdateMsg(apiKeyUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + return uplinkMsgBuilder.build(); + } + + private void checkApiKeyOnCloud(UplinkMsg uplinkMsg, UUID uuid, String description) throws Exception { + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + ApiKey apiKey = apiKeyService.findApiKeyById(tenantId, new ApiKeyId(uuid)); + Assert.assertNotNull(apiKey); + Assert.assertEquals(description, apiKey.getDescription()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java index 931a02de58..843011c24f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.edge; -import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; @@ -28,8 +27,6 @@ import org.thingsboard.server.common.data.edge.Edge; 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.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; @@ -37,10 +34,11 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; -import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -60,7 +58,7 @@ public class AssetEdgeTest extends AbstractEdgeTest { Asset assetMsg = JacksonUtil.fromString(assetUpdateMsg.getEntity(), Asset.class, true); Assert.assertNotNull(assetMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); - Assert.assertEquals(savedAsset, assetMsg); + compareHasVersionEntities(savedAsset, assetMsg); Optional assetProfileUpdateMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); Assert.assertTrue(assetProfileUpdateMsgOpt.isPresent()); AssetProfileUpdateMsg assetProfileUpdateMsg = assetProfileUpdateMsgOpt.get(); @@ -111,7 +109,7 @@ public class AssetEdgeTest extends AbstractEdgeTest { assetMsg = JacksonUtil.fromString(assetUpdateMsg.getEntity(), Asset.class, true); Assert.assertNotNull(assetMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); - Assert.assertEquals(savedAsset, assetMsg); + compareHasVersionEntities(savedAsset, assetMsg); assetProfileUpdateMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); Assert.assertTrue(assetProfileUpdateMsgOpt.isPresent()); assetProfileUpdateMsg = assetProfileUpdateMsgOpt.get(); @@ -277,12 +275,10 @@ public class AssetEdgeTest extends AbstractEdgeTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); Assert.assertTrue(edgeImitator.waitForResponses()); - AssetInfo assetInfo = doGet("/api/asset/info/" + savedAsset.getUuidId(), AssetInfo.class); - Assert.assertNotNull(assetInfo); - List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/assets?", - new TypeReference>() { - }, new PageLink(100)).getData(); - Assert.assertFalse(edgeAssets.contains(assetInfo)); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> + doGet("/api/asset/info/" + savedAsset.getUuidId(), AssetInfo.class, status().isNotFound()) + ); } private Asset saveAssetOnCloudAndVerifyDeliveryToEdge() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java index 68548558f7..210b2799e0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java @@ -53,7 +53,7 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage; AssetProfile assetProfileMsg = JacksonUtil.fromString(assetProfileUpdateMsg.getEntity(), AssetProfile.class, true); Assert.assertNotNull(assetProfileMsg); - Assert.assertEquals(assetProfile, assetProfileMsg); + compareHasVersionEntities(assetProfile, assetProfileMsg); Assert.assertEquals("Building", assetProfileMsg.getName()); Assert.assertEquals(buildingsRuleChainId, assetProfileMsg.getDefaultEdgeRuleChainId()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 06a4aefd63..52a30f0e85 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -26,10 +26,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -74,6 +73,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); + Assert.assertEquals(calculatedField.getAdditionalInfo(), calculatedFieldFromMsg.getAdditionalInfo()); edgeImitator.expectMessageAmount(1); savedCalculatedField.setName(UPDATED_CF_NAME); @@ -148,7 +148,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; CalculatedField calculatedFieldFromEdge = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); Assert.assertNotNull(calculatedFieldFromEdge); - Assert.assertEquals(savedCalculatedField, calculatedFieldFromEdge); + compareHasVersionEntities(savedCalculatedField, calculatedFieldFromEdge); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); } @@ -224,13 +224,13 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(2); config.setOutput(output); calculatedField.setConfiguration(config); + calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode()); return calculatedField; } @@ -262,6 +262,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class); Assert.assertNotNull(calculatedField); Assert.assertEquals(resourceTitle, calculatedField.getName()); + Assert.assertEquals(JacksonUtil.newObjectNode(), calculatedField.getAdditionalInfo()); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java index f3d12675a3..578175dc46 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java @@ -60,7 +60,7 @@ public class CustomerEdgeTest extends AbstractEdgeTest { CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get(); Customer customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer, customerMsg); + compareHasVersionEntities(savedCustomer, customerMsg); testAutoGeneratedCodeByProtobuf(customerUpdateMsg); // update customer @@ -73,7 +73,7 @@ public class CustomerEdgeTest extends AbstractEdgeTest { customerUpdateMsg = (CustomerUpdateMsg) latestMessage; customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer, customerMsg); + compareHasVersionEntities(savedCustomer, customerMsg); // delete customer edgeImitator.expectMessageAmount(2); diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index cf0cf9cd51..788dff02c0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -40,10 +40,11 @@ import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; -import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -200,7 +201,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest { CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get(); Customer customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer, customerMsg); + compareHasVersionEntities(savedCustomer, customerMsg); Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer); @@ -264,7 +265,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest { } @Test - public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception { + public void testSendDeleteDashboardOnEdgeToCloud() throws Exception { Dashboard savedDashboard = saveDashboardOnCloudAndVerifyDeliveryToEdge(); UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); @@ -281,12 +282,10 @@ public class DashboardEdgeTest extends AbstractEdgeTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); Assert.assertTrue(edgeImitator.waitForResponses()); - DashboardInfo dashboardInfo = doGet("/api/dashboard/info/" + savedDashboard.getUuidId(), DashboardInfo.class); - Assert.assertNotNull(dashboardInfo); - List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/dashboards?", - new TypeReference>() { - }, new PageLink(100)).getData(); - Assert.assertFalse(edgeAssets.contains(dashboardInfo)); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> + doGet("/api/dashboard/info/" + savedDashboard.getUuidId(), DashboardInfo.class, status().isNotFound()) + ); } private Dashboard saveDashboardOnCloudAndVerifyDeliveryToEdge() throws Exception { @@ -300,10 +299,10 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForMessages()); Optional dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class); Assert.assertTrue(dashboardUpdateMsgOpt.isPresent()); - DashboardUpdateMsg entityViewUpdateMsg = dashboardUpdateMsgOpt.get(); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); - Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB()); - Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB()); + DashboardUpdateMsg dashboardUpdateMsg = dashboardUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), dashboardUpdateMsg.getIdMSB()); + Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB()); return savedDashboard; } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index a8f82f8b7d..369cda37aa 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -52,8 +52,6 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.ota.OtaPackageType; -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.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -82,6 +80,7 @@ import java.util.Random; import java.util.UUID; import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -142,7 +141,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); - Assert.assertEquals(savedDevice, deviceFromMsg); + compareHasVersionEntities(savedDevice, deviceFromMsg); Assert.assertEquals(savedDevice.getId(), deviceFromMsg.getId()); Assert.assertEquals(savedDevice.getName(), deviceFromMsg.getName()); Assert.assertEquals(savedDevice.getType(), deviceFromMsg.getType()); @@ -223,7 +222,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Assert.assertTrue(latestMessage instanceof DeviceCredentialsUpdateMsg); DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = (DeviceCredentialsUpdateMsg) latestMessage; DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); - Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); + compareHasVersionEntities(deviceCredentials, deviceCredentialsMsg); // update device credentials - X509_CERTIFICATE edgeImitator.expectMessageAmount(1); @@ -273,7 +272,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device deviceMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); - Assert.assertEquals(savedDevice, deviceMsg); + compareHasVersionEntities(savedDevice, deviceMsg); Assert.assertEquals(firmwareOtaPackageInfo.getId(), deviceMsg.getFirmwareId()); Assert.assertEquals(softwareOtaPackageInfo.getId(), deviceMsg.getSoftwareId()); deviceData = deviceMsg.getDeviceData(); @@ -388,7 +387,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); Assert.assertNotNull(deviceCredentialsMsg); Assert.assertEquals(device.getId(), deviceCredentialsMsg.getDeviceId()); - Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); + compareHasVersionEntities(deviceCredentials, deviceCredentialsMsg); } @Test @@ -423,12 +422,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); Assert.assertTrue(edgeImitator.waitForResponses()); - DeviceInfo deviceInfo = doGet("/api/device/info/" + savedDevice.getUuidId(), DeviceInfo.class); - Assert.assertNotNull(deviceInfo); - List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?", - new TypeReference>() { - }, new PageLink(100)).getData(); - Assert.assertFalse(edgeDevices.contains(deviceInfo)); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> + doGet("/api/device/info/" + savedDevice.getUuidId(), DeviceInfo.class, status().isNotFound()) + ); } @Test @@ -682,8 +679,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { .atMost(10, TimeUnit.SECONDS) .until(() -> { String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/attributes/" + scope; - List actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() { - }); + List actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {}); return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains(expectedKey); }); @@ -904,18 +900,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes); - Awaitility.await() - .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + - "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); - Optional> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst(); - if (activeAttrOpt.isEmpty()) { - return false; - } - Map activeAttr = activeAttrOpt.get(); - return "true".equals(activeAttr.get("value").toString()); - }); + verifyEdgeConnected(); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index 810a03b415..3ac7b3d648 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -83,7 +83,7 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); Assert.assertNotNull(deviceProfileMsg); - Assert.assertEquals(deviceProfile, deviceProfileMsg); + compareHasVersionEntities(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); // update device profile @@ -108,7 +108,7 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); Assert.assertNotNull(deviceProfileMsg); - Assert.assertEquals(deviceProfile, deviceProfileMsg); + compareHasVersionEntities(deviceProfile, deviceProfileMsg); // delete profile edgeImitator.expectMessageAmount(1); @@ -146,19 +146,25 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); Assert.assertNotNull(deviceProfileMsg); - Assert.assertEquals(deviceProfile, deviceProfileMsg); + compareHasVersionEntities(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); // delete profile when edge is offline edgeImitator.disconnect(); + verifyEdgeDisconnected(); + doDelete("/api/deviceProfile/" + deviceProfile.getUuidId()) .andExpect(status().isOk()); - edgeImitator.connect(); // 25 sync message - // + 2 RuleChain and RuleChainMetadata - // + 1 delete DeviceProfile + // + 1 RuleChain Added + // + 1 RuleChainMetadata Added + // + 1 DeviceProfile Delete edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 3); + + edgeImitator.connect(); + verifyEdgeConnected(); + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); @@ -180,7 +186,7 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); Assert.assertNotNull(deviceProfileMsg); - Assert.assertEquals(deviceProfile, deviceProfileMsg); + compareHasVersionEntities(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(DeviceTransportType.SNMP, deviceProfileMsg.getTransportType()); @@ -218,7 +224,7 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); Assert.assertNotNull(deviceProfileMsg); - Assert.assertEquals(deviceProfile, deviceProfileMsg); + compareHasVersionEntities(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(DeviceTransportType.LWM2M, deviceProfileMsg.getTransportType()); @@ -267,7 +273,7 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); Assert.assertNotNull(deviceProfileMsg); - Assert.assertEquals(deviceProfile, deviceProfileMsg); + compareHasVersionEntities(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(DeviceTransportType.COAP, deviceProfileMsg.getTransportType()); diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java new file mode 100644 index 0000000000..e6cad9da6a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java @@ -0,0 +1,146 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; +import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; +import org.thingsboard.server.dao.edge.stats.MsgCounters; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.edge.stats.EdgeStatsService; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PUSHED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; + +@DaoSqlTest +@Slf4j +public class EdgeStatsIntegrationTest extends AbstractEdgeTest { + + private static final String STATISTICS_DEVICE_PROFILE = "STATISTICS"; + + private static final long EXPECTED_MSGS_ADDED = 6L; + private static final long EXPECTED_MSGS_PUSHED = 6L; + private static final long EXPECTED_MSGS_PERMANENTLY_FAILED = 0L; + private static final long EXPECTED_MSGS_TMP_FAILED = 0L; + + @Autowired + private EdgeStatsService edgeStatsService; + @Autowired + private EdgeStatsCounterService statsCounterService; + + @Test + public void testReportStats() throws Exception { + // GIVEN + simulateEdgeEventsAddedDownlinkPushed(); + + // Await Edge Counters Updated + await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { + MsgCounters counters = statsCounterService.getMsgCountersByEdge().get(edge.getId()); + assertEquals(EXPECTED_MSGS_ADDED, counters.getMsgsAdded().get()); + assertEquals(EXPECTED_MSGS_PUSHED, counters.getMsgsPushed().get()); + assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, counters.getMsgsPermanentlyFailed().get()); + assertEquals(EXPECTED_MSGS_TMP_FAILED, counters.getMsgsTmpFailed().get()); + }); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { + List actualStats = fetchLatestStats(); + assertEquals(EXPECTED_MSGS_ADDED, getStatsLongValue(actualStats, DOWNLINK_MSGS_ADDED)); + assertEquals(EXPECTED_MSGS_PUSHED, getStatsLongValue(actualStats, DOWNLINK_MSGS_PUSHED)); + assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, getStatsLongValue(actualStats, DOWNLINK_MSGS_PERMANENTLY_FAILED)); + assertEquals(EXPECTED_MSGS_TMP_FAILED, getStatsLongValue(actualStats, DOWNLINK_MSGS_TMP_FAILED)); + }); + } + + private long getStatsLongValue(List stats, EdgeStatsKey key) { + return stats.stream().filter(e -> e.getKey().equals(key.getKey())).findFirst().get().getLongValue().orElse(0L); + } + + private List fetchLatestStats() throws ExecutionException, InterruptedException { + return tsService.findLatest( + tenantId, + edge.getId(), + Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList()).get(); + } + + private void simulateEdgeEventsAddedDownlinkPushed() throws InterruptedException, ExecutionException { + statsCounterService.clear(edge.getId()); + + // Save device and assign to edge + // 2 DOWNLINK_MSGS_ADDED, EdgeEvents: [{DEVICE_PROFILE: ADDED}, {DEVICE: ASSIGNED_TO_EDGE}] + // 2 DOWNLINK_MSGS_PUSHED, Downlinks: [{deviceProfileUpdateMsg}, {deviceUpdateMsg, deviceProfileUpdateMsg, deviceCredentialsUpdateMsg}] + edgeImitator.expectMessageAmount(4); + Device savedDevice = saveDevice("StatisticDevice", STATISTICS_DEVICE_PROFILE); + doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // Save asset and assign to edge + // 1 DOWNLINK_MSGS_ADDED, EdgeEvents: [{ASSET: ASSIGNED_TO_EDGE}] + // 1 DOWNLINK_MSGS_PUSHED, Downlinks: [{assetUpdateMsg, assetProfileUpdateMsg}] + edgeImitator.expectMessageAmount(2); + Asset savedAsset = saveAsset("Edge Asset"); + doPost("/api/edge/" + edge.getUuidId() + + "/asset/" + savedAsset.getUuidId(), Asset.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // Create customer and assign edge to the customer + // 2 DOWNLINK_MSGS_ADDED, EdgeEvents: [{CUSTOMER: ADDED}, {EDGE: ASSIGNED_TO_CUSTOMER}] + // 2 DOWNLINK_MSGS_PUSHED, Downlinks: [{customerUpdateMsg}, {edgeConfiguration}] + edgeImitator.expectMessageAmount(2); + Customer customer = new Customer(); + customer.setTitle("Edge Customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + doPost("/api/customer/" + savedCustomer.getUuidId() + + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // Send device telemetry downlink for the device + // 1 DOWNLINK_MSGS_ADDED, EdgeEvents: [{DEVICE: TIMESERIES_UPDATED}] + // 1 DOWNLINK_MSGS_PUSHED, Downlinks: [{entityData}] + edgeImitator.expectMessageAmount(1); + String timeseriesData = "{\"data\":{\"temperature\":25},\"ts\":" + System.currentTimeMillis() + "}"; + JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, savedDevice.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + edgeEventService.saveAsync(edgeEvent).get(); + Assert.assertTrue(edgeImitator.waitForMessages()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java index 334680f7a7..e9dbf6d443 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.edge; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -26,10 +28,14 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; +import java.util.List; import java.util.Optional; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThat; + @DaoSqlTest public class EdgeTest extends AbstractEdgeTest { @@ -57,7 +63,7 @@ public class EdgeTest extends AbstractEdgeTest { CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get(); Customer customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer, customerMsg); + compareHasVersionEntities(savedCustomer, customerMsg); // unassign edge from customer edgeImitator.expectMessageAmount(2); @@ -76,4 +82,23 @@ public class EdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB()); Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB()); } + + @Test + public void testSyncEdge_attributeUpdated() throws Exception { + getWsClient().subscribeForAttributes(edge.getId(), TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of(DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY)); + + doPost("/api/edge/sync/" + edge.getId()); + + // wait for sync to start + waitForEdgeSyncInProgressEqualsValue(true); + + // wait for sync to end + waitForEdgeSyncInProgressEqualsValue(false); + } + + private void waitForEdgeSyncInProgressEqualsValue(Boolean value) { + getWsClient().registerWaitForUpdate(); + JsonNode update = JacksonUtil.toJsonNode(getWsClient().waitForUpdate()); + assertThat(update.get("data").get(DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY).get(0).get(1).asBoolean()).isEqualTo(value); + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java index f404508e36..640545c5d4 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.edge; -import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import org.junit.Assert; @@ -31,8 +30,6 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; @@ -40,10 +37,11 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; -import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -70,7 +68,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { EntityViewUpdateMsg entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; EntityView entityView = JacksonUtil.fromString(entityViewUpdateMsg.getEntity(), EntityView.class, true); Assert.assertNotNull(entityView); - Assert.assertEquals(savedEntityView, entityView); + compareHasVersionEntities(savedEntityView, entityView); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); // request entity view(s) for device @@ -256,12 +254,9 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); Assert.assertTrue(edgeImitator.waitForResponses()); - EntityViewInfo entityViewInfo = doGet("/api/entityView/info/" + savedEntityView.getUuidId(), EntityViewInfo.class); - Assert.assertNotNull(entityViewInfo); - List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/entityViews?", - new TypeReference>() { - }, new PageLink(100)).getData(); - Assert.assertFalse(edgeAssets.contains(entityViewInfo)); + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> + doGet("/api/entityView/info/" + savedEntityView.getUuidId(), EntityViewInfo.class, status().isNotFound()) + ); } private void verifyEntityViewUpdateMsg(EntityView entityView, Device device) throws InvalidProtocolBufferException { @@ -270,7 +265,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { EntityViewUpdateMsg entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; EntityView entityViewMsg = JacksonUtil.fromString(entityViewUpdateMsg.getEntity(), EntityView.class, true); Assert.assertNotNull(entityViewMsg); - Assert.assertEquals(entityView, entityViewMsg); + compareHasVersionEntities(entityView, entityViewMsg); Assert.assertEquals(device.getId(), entityViewMsg.getEntityId()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); testAutoGeneratedCodeByProtobuf(entityViewUpdateMsg); diff --git a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java index d5458ce793..193dc57c36 100644 --- a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java @@ -266,7 +266,6 @@ public class NotificationEdgeTest extends AbstractEdgeTest { notificationRule.setTriggerConfig(triggerConfig); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); Map> escalationTable = new HashMap<>(); escalationTable.put(Integer.valueOf("1"), new ArrayList<>()); recipientsConfig.setEscalationTable(escalationTable); diff --git a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java index 054f67585e..cc23440deb 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java @@ -57,7 +57,7 @@ public class RelationEdgeTest extends AbstractEdgeTest { RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; EntityRelation entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); Assert.assertNotNull(entityRelation); - Assert.assertEquals(relation, entityRelation); + compareHasVersionEntities(relation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); // delete relation @@ -76,7 +76,7 @@ public class RelationEdgeTest extends AbstractEdgeTest { relationUpdateMsg = (RelationUpdateMsg) latestMessage; entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); Assert.assertNotNull(entityRelation); - Assert.assertEquals(deletedRelation, entityRelation); + compareHasVersionEntities(deletedRelation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); } @@ -155,7 +155,7 @@ public class RelationEdgeTest extends AbstractEdgeTest { RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; EntityRelation entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); Assert.assertNotNull(entityRelation); - Assert.assertEquals(deviceToAssetRelation, entityRelation); + compareHasVersionEntities(deviceToAssetRelation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); } @@ -177,7 +177,7 @@ public class RelationEdgeTest extends AbstractEdgeTest { RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; EntityRelation entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); Assert.assertNotNull(entityRelation); - Assert.assertEquals(relation, entityRelation); + compareHasVersionEntities(relation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); // delete relation @@ -196,7 +196,7 @@ public class RelationEdgeTest extends AbstractEdgeTest { relationUpdateMsg = (RelationUpdateMsg) latestMessage; entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); Assert.assertNotNull(entityRelation); - Assert.assertEquals(deletedRelation, entityRelation); + compareHasVersionEntities(deletedRelation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); } diff --git a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java index 17bc177d10..5818179f45 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java @@ -223,8 +223,10 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); edgeEventService.saveAsync(failedEdgeEvent).get(); + // add unique body to avoid merge and filter by device id in edge service (mergeAndFilterDownlinkDuplicates) + JsonNode uniqueBody = JacksonUtil.toJsonNode("{\"idx\":" + idx + "}"); EdgeEvent successEdgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.UPDATED, - device.getId().getId(), EdgeEventType.DEVICE, null); + device.getId().getId(), EdgeEventType.DEVICE, uniqueBody); edgeEventService.saveAsync(successEdgeEvent).get(); } diff --git a/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java index 283e8bb66b..812357ac7d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java @@ -50,7 +50,7 @@ public class TenantEdgeTest extends AbstractEdgeTest { TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); Tenant tenantMsg = JacksonUtil.fromString(tenantUpdateMsg.getEntity(), Tenant.class, true); Assert.assertNotNull(tenantMsg); - Assert.assertEquals(savedTenant, tenantMsg); + compareHasVersionEntities(savedTenant, tenantMsg); TenantProfile tenantProfileMsg = JacksonUtil.fromString(tenantProfileUpdateMsg.getEntity(), TenantProfile.class, true); Assert.assertNotNull(tenantProfileMsg); Assert.assertEquals(tenantMsg.getTenantProfileId(), tenantProfileMsg.getId()); @@ -75,7 +75,7 @@ public class TenantEdgeTest extends AbstractEdgeTest { Assert.assertNotNull(tenantProfileMsg); // tenant update Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenant, tenantMsg); + compareHasVersionEntities(savedTenant, tenantMsg); Assert.assertEquals(savedTenant.getTenantProfileId(), tenantProfileMsg.getId()); } diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 6b3bffc883..91dd0669b1 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -15,15 +15,21 @@ */ package org.thingsboard.server.edge; -import com.google.protobuf.AbstractMessage; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.testcontainers.shaded.org.awaitility.Awaitility; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UserCredentialsId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -32,11 +38,15 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.security.model.ChangePasswordRequest; import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.user.UserServiceImpl.DEFAULT_TOKEN_LENGTH; @DaoSqlTest public class UserEdgeTest extends AbstractEdgeTest { @@ -44,216 +54,309 @@ public class UserEdgeTest extends AbstractEdgeTest { @Autowired private BCryptPasswordEncoder passwordEncoder; + private static final String DEFAULT_FIRST_NAME = "Boris"; + private static final String DEFAULT_LAST_NAME = "Johnson"; + private static final String UPDATED_LAST_NAME = "Borisov"; + private static final String DEFAULT_TENANT_ADMIN_EMAIL = "tenantAdmin@thingsboard.org"; + private static final String DEFAULT_CUSTOMER_USER_EMAIL = "customerUser@thingsboard.org"; + @Test public void testCreateUpdateDeleteTenantUser() throws Exception { // create user - edgeImitator.expectMessageAmount(3); - User newTenantAdmin = new User(); - newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); - newTenantAdmin.setTenantId(tenantId); - newTenantAdmin.setEmail("tenantAdmin@thingsboard.org"); - newTenantAdmin.setFirstName("Boris"); - newTenantAdmin.setLastName("Johnson"); - User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); - Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); - User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getId(), userMsg.getId()); - Assert.assertEquals(savedTenantAdmin.getAuthority(), userMsg.getAuthority()); - Assert.assertEquals(savedTenantAdmin.getEmail(), userMsg.getEmail()); - Assert.assertEquals(savedTenantAdmin.getFirstName(), userMsg.getFirstName()); - Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName()); - Optional userCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class); - Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent()); + User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null); + User savedTenantAdmin = createAndVerifyUserOnEdge(newTenantAdmin); // update user - edgeImitator.expectMessageAmount(2); - savedTenantAdmin.setLastName("Borisov"); - savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class); - Assert.assertTrue(edgeImitator.waitForMessages()); - userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - userUpdateMsg = userUpdateMsgOpt.get(); - userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName()); + updateAndVerifyUserLastName(savedTenantAdmin); // update user credentials login(savedTenantAdmin.getEmail(), "tenant"); + updateAndVerifyUserCredentials(savedTenantAdmin); + loginTenantAdmin(); - edgeImitator.expectMessageAmount(1); - ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); - changePasswordRequest.setCurrentPassword("tenant"); - changePasswordRequest.setNewPassword("newTenant"); - doPost("/api/auth/changePassword", changePasswordRequest); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); - Assert.assertNotNull(userCredentialsMsg); - Assert.assertEquals(savedTenantAdmin.getId(), userCredentialsMsg.getUserId()); - Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword())); + // delete user + deleteAndVerifyUser(savedTenantAdmin); + } + @Test + public void testCreateUpdateDeleteCustomerUser() throws Exception { + // create customer + Customer savedCustomer = createAndAssignCustomerToEdge(); + + // create user + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId()); + User savedCustomerUser = createAndVerifyUserOnEdge(customerUser); + + // update user + updateAndVerifyUserLastName(savedCustomerUser); + + // update user credentials + login(savedCustomerUser.getEmail(), "customer"); + updateAndVerifyUserCredentials(savedCustomerUser); loginTenantAdmin(); // delete user + deleteAndVerifyUser(savedCustomerUser); + } + + @Test + public void testSendUserToCloudFromEdge() throws Exception { + // create customer + Customer savedCustomer = createAndAssignCustomerToEdge(); + + // create uplinkMsg with user and userCredentials + UserId userId = new UserId(UUID.randomUUID()); + UserCredentialsId userCredentialsId = new UserCredentialsId(UUID.randomUUID()); + UplinkMsg uplinkMsg = buildUserUplinkMsg(userId, savedCustomer.getId(), userCredentialsId); + + User userFromCloud = verifyMsgOnCloud(uplinkMsg, userId, false); + assertUserCredentialsFlags(userFromCloud, false, false); + + // create uplinkMsg with enabled userCredentials + UplinkMsg uplinkMsgWithEnabledCredentials = constructUserCredentialsUplinkMsg(userCredentialsId, userId); + + User cloudUserWithCredentials = verifyMsgOnCloud(uplinkMsgWithEnabledCredentials, userId, false); + assertUserCredentialsFlags(cloudUserWithCredentials, true, true); + + // create uplinkMsg with user the same email + UserId secondUserId = new UserId(UUID.randomUUID()); + UserCredentialsId secondCredentialsId = new UserCredentialsId(UUID.randomUUID()); + UplinkMsg uplinkMsgForUserExistingEmail = buildUserUplinkMsg(secondUserId, savedCustomer.getId(), secondCredentialsId); + + verifyMsgOnCloud(uplinkMsgForUserExistingEmail, secondUserId, true); + } + + @Test + public void testSendUserCredentialsRequestToCloud() throws Exception { + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); + userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); + userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); + uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); edgeImitator.expectMessageAmount(1); - doDelete("/api/user/" + savedTenantAdmin.getUuidId()) - .andExpect(status().isOk()); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); + + UserCredentialsUpdateMsg userCredentialsUpdateMsg = getLatestUserCredentialsUpdateMsg(); + UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); + Assert.assertNotNull(userCredentialsMsg); + Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); + + testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } @Test - public void testCreateUpdateDeleteCustomerUser() throws Exception { + public void testSendUserDeleteFromEdgeToCloud() throws Exception { // create customer + Customer savedCustomer = createAndAssignCustomerToEdge(); + + // create user + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId()); + User savedCustomerUser = createAndVerifyUserOnEdge(customerUser); + + // simulate user removal event from edge to cloud + UserUpdateMsg.Builder userUpdateMsg = UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(savedCustomerUser.getUuidId().getMostSignificantBits()) + .setIdLSB(savedCustomerUser.getUuidId().getLeastSignificantBits()); + + UplinkMsg uplink = UplinkMsg.newBuilder() + .setUplinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserUpdateMsg(userUpdateMsg).build(); + + testAutoGeneratedCodeByProtobuf(userUpdateMsg); + + // expect edge message sent & cloud message response + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplink); + Assert.assertTrue(edgeImitator.waitForResponses()); + + loginTenantAdmin(); + Awaitility.await().atMost(10, TimeUnit.SECONDS) + .until(() -> { + try { + doGet("/api/user/" + savedCustomerUser.getId(), User.class, status().isNotFound()); + return true; + } catch (Throwable ex) { + return false; + } + }); + } + + private Customer createAndAssignCustomerToEdge() throws Exception { edgeImitator.expectMessageAmount(1); Customer customer = new Customer(); customer.setTitle("Edge Customer"); Customer savedCustomer = doPost("/api/customer", customer, Customer.class); Assert.assertFalse(edgeImitator.waitForMessages(5)); - // assign edge to customer edgeImitator.expectMessageAmount(2); - doPost("/api/customer/" + savedCustomer.getUuidId() - + "/edge/" + edge.getUuidId(), Edge.class); + doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); Assert.assertTrue(edgeImitator.waitForMessages()); - // create user + return savedCustomer; + } + + private User buildUser(Authority authority, CustomerId customerId) { + User user = new User(); + + user.setAuthority(authority); + user.setTenantId(tenantId); + user.setCustomerId(customerId); + user.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL); + user.setFirstName(DEFAULT_FIRST_NAME); + user.setLastName(DEFAULT_LAST_NAME); + + return user; + } + + private User createAndVerifyUserOnEdge(User user) throws Exception { + String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"; + + // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) edgeImitator.expectMessageAmount(3); - User customerUser = new User(); - customerUser.setAuthority(Authority.CUSTOMER_USER); - customerUser.setTenantId(tenantId); - customerUser.setCustomerId(savedCustomer.getId()); - customerUser.setEmail("customerUser@thingsboard.org"); - customerUser.setFirstName("John"); - customerUser.setLastName("Edwards"); - User savedCustomerUser = createUser(customerUser, "customer"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) + User savedUser = createUser(user, password); + Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); - Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); + // The initial USER ADDED edge event may bundle a UserCredentialsUpdateMsg when + // user activation completes before the event is processed, in addition to the 2 + // messages from the CREDENTIALS_UPDATED events fired during activation. Accept 2 or 3. + int credMsgCount = edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size(); + Assert.assertTrue("Expected 2 or 3 UserCredentialsUpdateMsg (ADDED/activation race), got " + credMsgCount, + credMsgCount == 2 || credMsgCount == 3); + + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getId(), userMsg.getId()); - Assert.assertEquals(savedCustomerUser.getCustomerId(), userMsg.getCustomerId()); - Assert.assertEquals(savedCustomerUser.getAuthority(), userMsg.getAuthority()); - Assert.assertEquals(savedCustomerUser.getEmail(), userMsg.getEmail()); - Assert.assertEquals(savedCustomerUser.getFirstName(), userMsg.getFirstName()); - Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); + Assert.assertEquals(savedUser.getId(), userMsg.getId()); + Assert.assertEquals(savedUser.getCustomerId(), userMsg.getCustomerId()); + Assert.assertEquals(savedUser.getAuthority(), userMsg.getAuthority()); + Assert.assertEquals(savedUser.getEmail(), userMsg.getEmail()); + Assert.assertEquals(savedUser.getFirstName(), userMsg.getFirstName()); + Assert.assertEquals(savedUser.getLastName(), userMsg.getLastName()); + + return savedUser; + } + + private void updateAndVerifyUserLastName(User user) throws Exception { + user.setLastName(UPDATED_LAST_NAME); - // update user edgeImitator.expectMessageAmount(2); - savedCustomerUser.setLastName("Addams"); - savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class); + doPost("/api/user", user, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - userUpdateMsg = userUpdateMsgOpt.get(); - userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); + + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); + User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + Assert.assertNotNull(userFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); + Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName()); + } - // update user credentials - login(savedCustomerUser.getEmail(), "customer"); + private void updateAndVerifyUserCredentials(User user) throws Exception { + String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"; + String newPassword = "new" + password; edgeImitator.expectMessageAmount(1); ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); - changePasswordRequest.setCurrentPassword("customer"); - changePasswordRequest.setNewPassword("newCustomer"); + changePasswordRequest.setCurrentPassword(password); + changePasswordRequest.setNewPassword(newPassword); doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); - Assert.assertNotNull(userCredentialsMsg); - Assert.assertEquals(savedCustomerUser.getId(), userCredentialsMsg.getUserId()); - Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword())); - loginTenantAdmin(); + UserCredentialsUpdateMsg msg = getLatestUserCredentialsUpdateMsg(); + UserCredentials creds = JacksonUtil.fromString(msg.getEntity(), UserCredentials.class, true); + Assert.assertNotNull(creds); + Assert.assertEquals(user.getId(), creds.getUserId()); + Assert.assertTrue(passwordEncoder.matches(newPassword, creds.getPassword())); + } - // delete user + private void deleteAndVerifyUser(User savedTenantAdmin) throws Exception { edgeImitator.expectMessageAmount(1); - doDelete("/api/user/" + savedCustomerUser.getUuidId()) + doDelete("/api/user/" + savedTenantAdmin.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; + + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); + Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); + Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); + } + + private UplinkMsg buildUserUplinkMsg(UserId userId, CustomerId customerId, UserCredentialsId userCredentialsUuid) { + User customerUser = buildUser(Authority.CUSTOMER_USER, customerId); + customerUser.setId(userId); + UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, false); + userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUser); + UserCredentialsUpdateMsg userCredentialsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); + + return UplinkMsg.newBuilder() + .addUserUpdateMsg(userUpdateMsg) + .addUserCredentialsUpdateMsg(userCredentialsMsg) + .build(); } - @Test - public void testSendUserCredentialsRequestToCloud() throws Exception { - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); - UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); - userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); - userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); - uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); + private UserCredentials buildCredentials(UserCredentialsId userCredentialsUuid, UserId userId, boolean enabled) { + UserCredentials userCredentials = new UserCredentials(); - testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + userCredentials.setId(userCredentialsUuid); + userCredentials.setUserId(userId); + userCredentials.setEnabled(enabled); + userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + + return userCredentials; + } + private User verifyMsgOnCloud(UplinkMsg uplinkMsg, UserId userId, boolean emailExist) throws Exception { edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.sendUplinkMsg(uplinkMsg); Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); - Assert.assertNotNull(userCredentialsMsg); - Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); + User userFromCloud = doGet("/api/user/" + userId, User.class); + Assert.assertNotNull(userFromCloud); + + if (emailExist) { + Assert.assertNotEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail()); + } else { + Assert.assertEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail()); + } + return userFromCloud; } - @Test - public void sendUserCredentialsRequest() throws Exception { - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); - UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); - userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); - userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); - uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); + private UplinkMsg constructUserCredentialsUplinkMsg(UserCredentialsId userCredentialsUuid, UserId userId) { + UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, true); + userCredentials.setPassword("password"); + UserCredentialsUpdateMsg credsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); - testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + return UplinkMsg.newBuilder() + .addUserCredentialsUpdateMsg(credsMsg) + .build(); + } - edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); + private void assertUserCredentialsFlags(User user, boolean enabled, boolean activated) { + JsonNode info = user.getAdditionalInfo(); + Assert.assertNotNull(info); + Assert.assertEquals(enabled, info.get("userCredentialsEnabled").asBoolean()); + Assert.assertEquals(activated, info.get("userActivated").asBoolean()); + } - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); - Assert.assertNotNull(userCredentialsMsg); - Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); + private UserUpdateMsg getLatestUserUpdateMsg() { + Optional opt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(opt.isPresent()); + return opt.get(); + } - testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); + private UserCredentialsUpdateMsg getLatestUserCredentialsUpdateMsg() { + Optional opt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class); + Assert.assertTrue(opt.isPresent()); + return opt.get(); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java index d6afa81262..ddaa867aee 100644 --- a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java @@ -68,7 +68,7 @@ public class WidgetEdgeTest extends AbstractEdgeTest { WidgetType widgetsType = JacksonUtil.fromString(widgetTypeUpdateMsg.getEntity(), WidgetType.class, true); Assert.assertNotNull(widgetsType); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); - Assert.assertEquals(savedWidgetType, widgetsType); + compareHasVersionEntities(savedWidgetType, widgetsType); // update widget bundle edgeImitator.expectMessageAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 266a4dbb62..8208dc4fc9 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -29,6 +29,8 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient; import org.thingsboard.server.controller.AbstractWebTest; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; @@ -369,6 +371,16 @@ public class EdgeImitator { result.add(saveDownlinkMsg(calculatedFieldUpdateMsg)); } } + if (downlinkMsg.getAiModelUpdateMsgCount() > 0) { + for (AiModelUpdateMsg aiModelUpdateMsg : downlinkMsg.getAiModelUpdateMsgList()) { + result.add(saveDownlinkMsg(aiModelUpdateMsg)); + } + } + if (downlinkMsg.getApiKeyUpdateMsgCount() > 0) { + for (ApiKeyUpdateMsg apiKeyUpdateMsg : downlinkMsg.getApiKeyUpdateMsgList()) { + result.add(saveDownlinkMsg(apiKeyUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } @@ -443,7 +455,7 @@ public class EdgeImitator { Optional result; lock.lock(); try { - result = (Optional) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).findAny(); + result = (Optional) downlinkMsgs.stream().filter(downlinkMsg -> tClass.isAssignableFrom(downlinkMsg.getClass())).findAny(); } finally { lock.unlock(); } @@ -455,7 +467,7 @@ public class EdgeImitator { List result; lock.lock(); try { - result = (List) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).collect(Collectors.toList()); + result = (List) downlinkMsgs.stream().filter(downlinkMsg -> tClass.isAssignableFrom(downlinkMsg.getClass())).collect(Collectors.toList()); } finally { lock.unlock(); } 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 bcd7c25085..f4a913f41d 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 @@ -118,7 +118,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac .pollInterval(10, MILLISECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { - List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT.getOldName(), 1000) + List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT, 1000) .getData().stream().filter(e -> { var body = e.getBody(); return body.has("event") && body.get("event").asText().equals("STARTED") diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java index da73600671..4cd41c314e 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java @@ -19,6 +19,8 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; +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.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; @@ -30,6 +32,7 @@ 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.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.TbUrlConstants; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -46,6 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @TestPropertySource(properties = { "usage.stats.report.enabled=true", "usage.stats.report.interval=2", + "usage.stats.report.urgent_interval=1", "usage.stats.gauge_report_interval=1", }) public class ApiUsageTest extends AbstractControllerTest { @@ -54,9 +58,12 @@ public class ApiUsageTest extends AbstractControllerTest { private User tenantAdmin; private static final int MAX_DP_ENABLE_VALUE = 12; + private static final int MAX_SMS_ENABLE_VALUE = 10; private static final double WARN_THRESHOLD_VALUE = 0.5; @Autowired private ApiUsageStateService apiUsageStateService; + @Autowired + private TbApiUsageReportClient apiUsageReportClient; @Before public void beforeTest() throws Exception { @@ -82,7 +89,7 @@ public class ApiUsageTest extends AbstractControllerTest { } @Test - public void testTelemetryApiCall() throws Exception { + public void testDbStorageApiUsage() throws Exception { Device device = createDevice(); assertNotNull(device); String telemetryPayload = "{\"temperature\":25, \"humidity\":60}"; @@ -94,7 +101,8 @@ public class ApiUsageTest extends AbstractControllerTest { doPostAsync(url, telemetryPayload, String.class, status().isOk()); } - await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState())); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertEquals(ApiUsageStateValue.WARNING, getUsageState().getDbStorageState())); long VALUE_DISABLE = (long) (MAX_DP_ENABLE_VALUE - (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE)) / 2; @@ -104,10 +112,35 @@ public class ApiUsageTest extends AbstractControllerTest { await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { - assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); + assertEquals(ApiUsageStateValue.DISABLED, getUsageState().getDbStorageState()); }); } + @Test + public void testSmsApiUsage() { + long smsWarnThreshold = (long) (MAX_SMS_ENABLE_VALUE * WARN_THRESHOLD_VALUE); + + for (int i = 0; i < smsWarnThreshold; i++) { + apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.SMS_EXEC_COUNT); + } + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertEquals(ApiUsageStateValue.WARNING, getUsageState().getSmsExecState())); + + long smsDisableCount = MAX_SMS_ENABLE_VALUE - smsWarnThreshold; + + for (int i = 0; i < smsDisableCount; i++) { + apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.SMS_EXEC_COUNT); + } + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertEquals(ApiUsageStateValue.DISABLED, getUsageState().getSmsExecState())); + } + + private ApiUsageState getUsageState() { + return apiUsageStateService.findTenantApiUsageState(tenantId); + } + private TenantProfile createTenantProfile() { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName("Tenant Profile"); @@ -116,6 +149,8 @@ public class ApiUsageTest extends AbstractControllerTest { TenantProfileData tenantProfileData = new TenantProfileData(); DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() .maxDPStorageDays(MAX_DP_ENABLE_VALUE) + .maxSms(MAX_SMS_ENABLE_VALUE) + .smsEnabled(true) .warnThreshold(WARN_THRESHOLD_VALUE) .build(); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java b/application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java index ee83f9df64..3ee229e7a0 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java @@ -33,6 +33,7 @@ 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.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -40,15 +41,22 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.profile.TbAssetProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -57,6 +65,12 @@ public class DefaultCalculatedFieldCacheTest { @Mock private CalculatedFieldService calculatedFieldService; @Mock + private TbAssetProfileCache assetProfileCache; + @Mock + private TbDeviceProfileCache deviceProfileCache; + @Mock + private TbTenantProfileCache tenantProfileCache; + @Mock private DeviceService deviceService; @Mock private AssetService assetService; @@ -67,7 +81,11 @@ public class DefaultCalculatedFieldCacheTest { @BeforeEach public void setUp() { - cache = new DefaultCalculatedFieldCache(calculatedFieldService, null, null); + // ActorSystemContext is only used in getCalculatedFieldCtx (not tested here), so null is safe + OwnerService ownerService = new OwnerService(deviceService, assetService, customerService); + cache = new DefaultCalculatedFieldCache(calculatedFieldService, assetProfileCache, + deviceProfileCache, tenantProfileCache, null, ownerService); + } // --- Tenant deletion tests --- @@ -90,6 +108,21 @@ public class DefaultCalculatedFieldCacheTest { assertThat(cache.getCalculatedFieldsByEntityId(device2)).containsExactly(cf2); } + @Test + public void onComponentLifecycleEvent_tenantDeleted_evictsOwnerEntities() { + TenantId tenant = new TenantId(UUID.randomUUID()); + DeviceId device = new DeviceId(UUID.randomUUID()); + stubDeviceOwner(tenant, device, tenant); + + cache.addOwnerEntity(tenant, device); + assertThat(cache.getDynamicEntities(tenant, tenant)).contains(device); + + cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); + + // After eviction, getDynamicEntities triggers a fresh load from ownerService (empty) + assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(device); + } + @Test public void onComponentLifecycleEvent_tenantDeleted_removesLinksToLinkedEntities() { TenantId tenant = new TenantId(UUID.randomUUID()); @@ -141,6 +174,21 @@ public class DefaultCalculatedFieldCacheTest { assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty(); } + @Test + public void onComponentLifecycleEvent_deviceDeleted_evictsDeviceFromOwnerEntities() { + TenantId tenant = new TenantId(UUID.randomUUID()); + CustomerId customer = new CustomerId(UUID.randomUUID()); + DeviceId device = new DeviceId(UUID.randomUUID()); + stubDeviceOwner(tenant, device, customer); + + cache.addOwnerEntity(tenant, device); + assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); + + cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED)); + + assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); + } + @Test public void onComponentLifecycleEvent_assetDeleted_evictsCfsForThatAsset() { TenantId tenant = new TenantId(UUID.randomUUID()); @@ -153,6 +201,36 @@ public class DefaultCalculatedFieldCacheTest { assertThat(cache.getCalculatedFieldsByEntityId(asset)).isEmpty(); } + @Test + public void onComponentLifecycleEvent_deviceCreated_addsDeviceToOwnerEntities() { + TenantId tenant = new TenantId(UUID.randomUUID()); + CustomerId customer = new CustomerId(UUID.randomUUID()); + DeviceId device = new DeviceId(UUID.randomUUID()); + stubDeviceOwner(tenant, device, customer); + + cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.CREATED)); + + assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); + } + + // --- Customer deletion tests --- + + @Test + public void onComponentLifecycleEvent_customerDeleted_evictsCustomerOwnerEntries() { + TenantId tenant = new TenantId(UUID.randomUUID()); + CustomerId customer = new CustomerId(UUID.randomUUID()); + DeviceId device = new DeviceId(UUID.randomUUID()); + stubDeviceOwner(tenant, device, customer); + + cache.addOwnerEntity(tenant, device); + assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); + + cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED)); + + // The customer's owned-entities entry is evicted; fresh load returns empty + assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); + } + // --- DeviceProfile/AssetProfile deletion tests --- @Test @@ -302,6 +380,96 @@ public class DefaultCalculatedFieldCacheTest { assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(updatedCf); } + // --- evictOwner recursive traversal tests --- + + @Test + public void evictOwner_customerDeleted_recursivelyEvictsDevicesOwnedByThatCustomer() { + TenantId tenant = new TenantId(UUID.randomUUID()); + CustomerId customer = new CustomerId(UUID.randomUUID()); + DeviceId device = new DeviceId(UUID.randomUUID()); + + stubDeviceOwner(tenant, device, customer); + when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData()); + + // tenant owns customer (getOwner for CUSTOMER returns tenantId) + cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer} + cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device} + + assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer); + assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); + + // deleting the customer evicts the customer key and recursively cleans its owned set + cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED)); + + assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); + } + + @Test + public void evictOwner_tenantDeleted_recursivelyEvictsCustomerAndItsOwnedDevices() { + TenantId tenant = new TenantId(UUID.randomUUID()); + CustomerId customer = new CustomerId(UUID.randomUUID()); + DeviceId device = new DeviceId(UUID.randomUUID()); + + stubDeviceOwner(tenant, device, customer); + when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData()); + + cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer} + cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device} + + assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer); + assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); + + // deleting the tenant: evictOwner(tenant) finds customer (CUSTOMER type) and recurses into it + cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); + + // both levels must be gone + assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(customer); + assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); + } + + // --- TenantProfile lifecycle tests --- + + @Test + public void onComponentLifecycleEvent_tenantProfileUpdated_callsHandleTenantProfileUpdate() { + TenantId tenant = new TenantId(UUID.randomUUID()); + TenantProfileId profileId = new TenantProfileId(UUID.randomUUID()); + DefaultCalculatedFieldCache spyCache = spy(cache); + + spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED)); + + verify(spyCache).handleTenantProfileUpdate(profileId); + } + + @Test + public void onComponentLifecycleEvent_tenantProfileDeleted_doesNotCallHandleTenantProfileUpdate() { + TenantId tenant = new TenantId(UUID.randomUUID()); + TenantProfileId profileId = new TenantProfileId(UUID.randomUUID()); + DefaultCalculatedFieldCache spyCache = spy(cache); + + spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED)); + + verify(spyCache, never()).handleTenantProfileUpdate(any()); + } + + // --- Helpers --- + + private void stubDeviceOwner(TenantId tenantId, DeviceId deviceId, EntityId ownerId) { + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + if (ownerId instanceof CustomerId customerId) { + device.setCustomerId(customerId); + } + // If ownerId is a TenantId, leaving customerId null means getOwnerId() returns tenantId + when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); + // Stubs for getOwnedEntities iteration (empty pages — device is added explicitly) + when(deviceService.findDeviceInfosByFilter(any(), any())).thenReturn(PageData.emptyPageData()); + when(assetService.findAssetsByTenantIdAndCustomerId(any(), any(), any())).thenReturn(PageData.emptyPageData()); + if (ownerId instanceof TenantId) { + when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData()); + } + } + private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId) { CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID()); CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, simpleCfConfig()); @@ -332,7 +500,7 @@ public class DefaultCalculatedFieldCacheTest { private CalculatedFieldConfiguration simpleCfConfig() { CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class); - when(config.getReferencedEntities()).thenReturn(Collections.emptyList()); + when(config.getReferencedEntities()).thenReturn(Collections.emptySet()); when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(Collections.emptyList()); return config; } @@ -340,7 +508,7 @@ public class DefaultCalculatedFieldCacheTest { private CalculatedFieldConfiguration linkedEntityCfConfig(TenantId tenantId, CalculatedFieldId cfId, EntityId linkedEntity) { CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class); CalculatedFieldLink link = new CalculatedFieldLink(tenantId, linkedEntity, cfId); - when(config.getReferencedEntities()).thenReturn(List.of(linkedEntity)); + when(config.getReferencedEntities()).thenReturn(Set.of(linkedEntity)); when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(List.of(link)); when(config.buildCalculatedFieldLink(any(), eq(linkedEntity), any())).thenReturn(link); return config; diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java new file mode 100644 index 0000000000..a20c7b0253 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -0,0 +1,511 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; + +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.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; + +@ExtendWith(MockitoExtension.class) +public class GeofencingCalculatedFieldStateTest { + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("8f83eeca-b5cd-4955-9241-09d1393768c6")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("688b529d-cfbe-4430-91c5-60b4f4e5d3cf")); + private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); + private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); + + private final SingleValueArgumentEntry latitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("latitude", 50.4730), 145L); + private final SingleValueArgumentEntry longitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("longitude", 30.5050), 165L); + + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, System.currentTimeMillis(), 0L); + private final GeofencingArgumentEntry geofencingAllowedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry)); + + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); + private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, System.currentTimeMillis(), 0L); + private final GeofencingArgumentEntry geofencingRestrictedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + + private GeofencingCalculatedFieldState state; + private CalculatedFieldCtx ctx; + + @Mock + private TenantProfile tenantProfile; + @Mock + private TbTenantProfileCache tenantProfileCache; + @Mock + private RelationService relationService; + @InjectMocks + private ActorSystemContext systemContext; + + @BeforeEach + void setUp() { + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); + ctx.init(); + state = new GeofencingCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); + } + + @Test + void testType() { + assertThat(state.getType()).isEqualTo(CalculatedFieldType.GEOFENCING); + } + + @Test + void testUpdateState() { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry + )); + + Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); + + assertThat(stateUpdated).isTrue(); + assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( + Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry + ) + ); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForLatitudeArgument() { + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() { + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() { + assertThatThrownBy(() -> state.update(Map.of("someArgumentName", latitudeArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for someArgumentName argument: SINGLE_VALUE. Only GEOFENCING type is allowed."); + } + + @Test + void testUpdateStateWhenUpdateExistingSingleValueArgumentEntry() { + state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); + + SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 190L); + Map newArgs = Map.of("latitude", newArgEntry); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); + + assertThat(stateUpdated).isTrue(); + assertThat(state.getArguments()).isEqualTo(newArgs); + } + + @Test + void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithTheSameValue() { + state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); + + Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); + + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); + + assertThat(stateUpdated).isFalse(); + assertThat(state.getArguments()).isEqualTo(newArgs); + } + + @Test + void testUpdateStateWhenUpdateExistingSingleValueArgumentEntryWithValueOfAnotherType() { + state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); + + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for single value argument entry: GEOFENCING"); + } + + + @Test + void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithValueOfAnotherType() { + state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); + + assertThatThrownBy(() -> state.update(Map.of("allowedZones", latitudeArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); + } + + @Test + void testIsReadyWhenNotAllArgPresent() { + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); + } + + @Test + void testIsReadyWhenAllArgPresent() { + state.update(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + ), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + @Test + void testIsReadyWhenEmptyEntryPresents() { + state.update(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", new GeofencingArgumentEntry(Collections.emptyMap()) + ), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones"); + } + + @Test + void testPerformCalculation() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + TelemetryCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesStatus", "INSIDE") + .put("restrictedZonesStatus", "OUTSIDE") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); + + TelemetryCalculatedFieldResult result2 = performCalculation(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesEvent", "LEFT") + .put("allowedZonesStatus", "OUTSIDE") + .put("restrictedZonesEvent", "ENTERED") + .put("restrictedZonesStatus", "INSIDE") + ); + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + List deleteValues = deleteCaptor.getAllValues(); + assertThat(deleteValues).hasSize(2); + + EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0); + assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + + EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1); + assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + } + + @Test + void testPerformCalculationWithOnlyTransitionEventsReportingStrategy() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + + var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_ONLY); + + ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig)); + ctx.init(); + + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + TelemetryCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult().get("values")).isEmpty(); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); + + TelemetryCalculatedFieldResult result2 = performCalculation(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesEvent", "LEFT") + .put("restrictedZonesEvent", "ENTERED") + ); + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + List deleteValues = deleteCaptor.getAllValues(); + assertThat(deleteValues).hasSize(2); + + EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0); + assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + + EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1); + assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + } + + @Test + void testPerformCalculationWithOnlyPresenceStatusReportingStrategy() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + + var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_PRESENCE_STATUS_ONLY); + + ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig)); + ctx.init(); + + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + TelemetryCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesStatus", "INSIDE") + .put("restrictedZonesStatus", "OUTSIDE") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); + + TelemetryCalculatedFieldResult result2 = performCalculation(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesStatus", "OUTSIDE") + .put("restrictedZonesStatus", "INSIDE") + ); + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + List deleteValues = deleteCaptor.getAllValues(); + assertThat(deleteValues).hasSize(2); + + EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0); + assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + + EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1); + assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + } + + private CalculatedField getCalculatedField() { + return getCalculatedField(getCalculatedFieldConfig(REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + } + + private CalculatedField getCalculatedField(CalculatedFieldConfiguration configuration) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(TENANT_ID); + calculatedField.setEntityId(DEVICE_ID); + calculatedField.setType(CalculatedFieldType.GEOFENCING); + calculatedField.setName("Test Geofencing Calculated Field"); + calculatedField.setConfigurationVersion(1); + calculatedField.setConfiguration(configuration); + calculatedField.setVersion(1L); + return calculatedField; + } + + private CalculatedFieldConfiguration getCalculatedFieldConfig(GeofencingReportStrategy reportStrategy) { + var config = new GeofencingCalculatedFieldConfiguration(); + + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + config.setEntityCoordinates(entityCoordinates); + + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "AllowedZone"))); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + allowedZonesGroup.setRelationType("CurrentZone"); + allowedZonesGroup.setDirection(EntitySearchDirection.TO); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); + var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "RestrictedZone"))); + restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); + restrictedZonesGroup.setRelationType("CurrentZone"); + restrictedZonesGroup.setDirection(EntitySearchDirection.TO); + + config.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); + + config.setOutput(new TimeSeriesOutput()); + return config; + } + + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java new file mode 100644 index 0000000000..d7bc39f6e8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -0,0 +1,191 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +public class GeofencingValueArgumentEntryTest { + + private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); + private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); + + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, 363L, 155L); + + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); + private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, 363L, 155L); + + private GeofencingArgumentEntry entry; + + @Mock + private CalculatedFieldCtx ctx; + + @BeforeEach + void setUp() { + entry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + } + + @Test + void testArgumentEntryType() { + assertThat(entry.getType()).isEqualTo(ArgumentEntryType.GEOFENCING); + } + + @Test + void testUpdateEntryWhenSingleEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); + } + + @Test + void testUpdateEntryWhenRollingEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: TS_ROLLING"); + } + + @Test + void testUpdateEntryWithTheSameTs() { + BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 363L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + assertThat(entry.updateEntry(updated, ctx)).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void testUpdateEntryWhenNewVersionIsNull() { + BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, null); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated, ctx)).isTrue(); + assertThat(entry.getValue()).isInstanceOf(Map.class); + + Map value = (Map) entry.getValue(); + assertThat(value).hasSize(2); + assertThat(value.get(ZONE_1_ID).getVersion()).isNull(); + assertThat(value.get(ZONE_1_ID).getTs()).isEqualTo(364L); + assertThat(value.get(ZONE_1_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(differentValueNewVersionIsNull.getJsonValue().get(), PerimeterDefinition.class)); + + assertThat(value.get(ZONE_2_ID).getVersion()).isEqualTo(155L); + assertThat(value.get(ZONE_2_ID).getTs()).isEqualTo(363L); + assertThat(value.get(ZONE_2_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(restrictedZoneAttributeKvEntry.getJsonValue().get(), PerimeterDefinition.class)); + } + + @Test + @SuppressWarnings("unchecked") + void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated, ctx)).isTrue(); + assertThat(entry.getValue()).isInstanceOf(Map.class); + + Map value = (Map) entry.getValue(); + assertThat(value).hasSize(2); + assertThat(value.get(ZONE_1_ID).getVersion()).isEqualTo(156L); + assertThat(value.get(ZONE_1_ID).getTs()).isEqualTo(364L); + assertThat(value.get(ZONE_1_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(differentValueNewVersionIsSet.getJsonValue().get(), PerimeterDefinition.class)); + + assertThat(value.get(ZONE_2_ID).getVersion()).isEqualTo(155L); + assertThat(value.get(ZONE_2_ID).getTs()).isEqualTo(363L); + assertThat(value.get(ZONE_2_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(restrictedZoneAttributeKvEntry.getJsonValue().get(), PerimeterDefinition.class)); + } + + @Test + void testUpdateEntryWhenNewVersionIsLessThanCurrent() { + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 154L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated, ctx)).isFalse(); + } + + @Test + void testUpdateEntryWhenNewTsAndVersionIsGreaterThenCurrentAndValueWasNotChanged() { + BaseAttributeKvEntry newTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, newTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated, ctx)).isTrue(); + } + + @Test + void testUpdateEntryWithOldTs() { + BaseAttributeKvEntry oldTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 362L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, oldTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated, ctx)).isFalse(); + } + + @Test + void testUpdateEntryWithNewZone() { + final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3")); + BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone)); + assertThat(entry.updateEntry(updated, ctx)).isTrue(); + } + + @Test + void testIsEmpty() { + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + @Test + void testIsEmptyWithEmptyMap() { + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of()); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + @Test + void testInvalidKvEntryDataTypeForZoneResultInEmptyArgument() { + BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new StringDataEntry("zone", "someString"), 363L, 155L); + assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid perimeter definition format for Zone with id: " + ZONE_1_ID + ". Failed to parse attribute 'zone'"); + } + + @Test + void testNotParsableToPerimeterJsonKvEntryResultInExceptionTrowed() { + BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L); + assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid perimeter definition format for Zone with id: " + ZONE_1_ID + ". Failed to parse attribute 'zone'"); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java new file mode 100644 index 0000000000..40d1ae83b1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -0,0 +1,169 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingEvalResult; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.ENTERED; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.LEFT; + +public class GeofencingZoneStateTest { + + private final AssetId ZONE_ID = new AssetId(UUID.fromString("628730fd-d625-417f-9c6d-ae9fe4addbdb")); + + private GeofencingZoneState state; + + @BeforeEach + void setUp() { + String POLYGON = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + state = new GeofencingZoneState(ZONE_ID, new BaseAttributeKvEntry(new JsonDataEntry("zone", POLYGON), 100L, 1L)); + } + + @Test + void evaluate_initialInside_thenInsideAgain() { + var inside = new Coordinates(50.4730, 30.5050); + // first evaluation: no prior state -> INSIDE + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + // same position again -> INSIDE (steady state) + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + } + + @Test + void evaluate_initialOutside_thenOutsideAgain() { + var outside = new Coordinates(50.4760, 30.5110); + // first evaluation: no prior state -> OUTSIDE + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + // same position again -> OUTSIDE (steady state) + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + } + + @Test + void evaluate_inside_thenLeave() { + var inside = new Coordinates(50.4730, 30.5050); + var outside = new Coordinates(50.4760, 30.5110); + // initial eval + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + // leave -> LEFT + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE)); + // still outside -> OUTSIDE + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + } + + @Test + void evaluate_outside_thenEnter() { + var outside = new Coordinates(50.4760, 30.5110); + var inside = new Coordinates(50.4730, 30.5050); + // start outside + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + // cross boundary -> ENTERED + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); + // remain inside -> INSIDE + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + } + + @Test + void update_withNewerVersion_updatesState_andResetsPresence() { + // arrange: establish a prior presence to ensure it’s reset on update + var inside = new Coordinates(50.4730, 30.5050); + assertThat(state.evaluate(inside)).isNotNull(); // sets lastPresence internally + + String NEW_POLYGON = "[[50.470000, 30.502000], [50.470000, 30.503000], [50.471000, 30.503000], [50.471000, 30.502000]]"; + GeofencingZoneState newer = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", NEW_POLYGON), 200L, 2L) + ); + + // act + boolean changed = state.update(newer); + + // assert + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(200L); + assertThat(state.getVersion()).isEqualTo(2L); + assertThat(state.getPerimeterDefinition()).isNotNull(); + assertThat(state.getLastPresence()).isNull(); // must be reset on successful update + } + + @Test + void update_withEqualVersion_doesNothing() { + // arrange: same version (1L) but different ts/polygon should still be ignored + String SOME_POLYGON = "[[50.472500, 30.504500], [50.472500, 30.505500], [50.473500, 30.505500], [50.473500, 30.504500]]"; + GeofencingZoneState sameVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", SOME_POLYGON), 300L, 1L) + ); + + // act + boolean changed = state.update(sameVersion); + + // assert: nothing changes + assertThat(changed).isFalse(); + assertThat(state.getTs()).isEqualTo(100L); + assertThat(state.getVersion()).isEqualTo(1L); + } + + @Test + void update_withNullNewVersion_alwaysApplies_andCopiesNull() { + // arrange: the implementation updates if newVersion == null + String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]"; + GeofencingZoneState nullVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, null) + ); + + // act + boolean changed = state.update(nullVersion); + + // assert: applied and version copied as null + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(400L); + assertThat(state.getVersion()).isNull(); + assertThat(state.getLastPresence()).isNull(); + } + + @Test + void update_withNewVersionWhenExistingIsNull_alwaysApplies_andCopiesNew() { + // arrange: the implementation updates if newVersion == null + String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]"; + GeofencingZoneState newVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, 2L) + ); + state.setVersion(null); + + // act + boolean changed = state.update(newVersion); + + // assert: applied and version copied as null + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(400L); + assertThat(state.getVersion()).isEqualTo(2); + assertThat(state.getLastPresence()).isNull(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java new file mode 100644 index 0000000000..7db8bc658a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -0,0 +1,257 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfPropagationArg; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +public class PropagationArgumentEntryTest { + + private final AssetId ENTITY_1_ID = new AssetId(UUID.fromString("b0a8637d-6d67-43d5-a483-c0e391afe805")); + private final AssetId ENTITY_2_ID = new AssetId(UUID.fromString("7bd85073-ded5-414f-a2ef-bd56ad3dbf6a")); + private final AssetId ENTITY_3_ID = new AssetId(UUID.fromString("d64f3e51-2ec2-472f-b475-b095ef8bdc70")); + + private PropagationArgumentEntry entry; + + @Mock + private CalculatedFieldCtx ctx; + + @BeforeEach + void setUp() { + lenient().when(ctx.getMaxRelatedEntitiesPerCfArgument()).thenReturn(1000L); + + List propagationEntityIds = new ArrayList<>(); + propagationEntityIds.add(ENTITY_1_ID); + propagationEntityIds.add(ENTITY_2_ID); + entry = new PropagationArgumentEntry(propagationEntityIds); + } + + @Test + void testArgumentEntryType() { + assertThat(entry.getType()).isEqualTo(ArgumentEntryType.PROPAGATION); + } + + @Test + void testIsEmpty() { + PropagationArgumentEntry emptyEntry = new PropagationArgumentEntry(List.of()); + assertThat(emptyEntry.isEmpty()).isTrue(); + } + + @Test + void testGetValueReturnsPropagationIds() { + assertThat(entry.getValue()).isInstanceOf(Set.class); + @SuppressWarnings("unchecked") + Set value = (Set) entry.getValue(); + assertThat(value).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + } + + @Test + void testUpdateEntryWhenSingleEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE"); + } + + @Test + void testUpdateEntryWhenRollingEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for propagation argument entry: TS_ROLLING"); + } + + @Test + void testUpdateEntryReplacesWithNewIds() { + var newIds = new ArrayList(List.of(ENTITY_3_ID, ENTITY_1_ID)); + var updated = new PropagationArgumentEntry(newIds); + + boolean changed = entry.updateEntry(updated, ctx); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyElementsOf(newIds); + } + + @Test + void testUpdateEntryClearsWhenNewEntryIsEmpty() { + var updatedEmpty = new PropagationArgumentEntry(List.of()); + + boolean changed = entry.updateEntry(updatedEmpty, ctx); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).isEmpty(); + } + + @Test + void testUpdateEntryWhenAdded() { + var added = new PropagationArgumentEntry(); + added.setAdded(List.of(ENTITY_3_ID)); + + boolean changed = entry.updateEntry(added, ctx); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); + assertThat(entry.getAdded()).isEqualTo(List.of(ENTITY_3_ID)); + } + + @Test + void testUpdateEntryWhenAddedExistingEntity() { + var added = new PropagationArgumentEntry(); + added.setAdded(List.of(ENTITY_2_ID)); + + boolean changed = entry.updateEntry(added, ctx); + + assertThat(changed).isFalse(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + assertThat(entry.getAdded()).isNull(); + } + + @Test + void testUpdateEntryWhenRemoved() { + var removed = new PropagationArgumentEntry(); + removed.setRemoved(ENTITY_2_ID); + + boolean changed = entry.updateEntry(removed, ctx); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); + assertThat(entry.getRemoved()).isNull(); + } + + @Test + void testUpdateEntryWhenRemovedNonExistingEntity() { + var removed = new PropagationArgumentEntry(); + removed.setRemoved(ENTITY_3_ID); + + boolean changed = entry.updateEntry(removed, ctx); + + assertThat(changed).isFalse(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + assertThat(entry.getRemoved()).isNull(); + } + + @Test + void testUpdateEntryWhenPartitionStateRestoreAddsMissingIds() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID)); + restore.setSyncWithDb(true); + + boolean changed = entry.updateEntry(restore, ctx); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); + assertThat(entry.getAdded()).containsExactly(ENTITY_3_ID); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isSyncWithDb()).isFalse(); + } + + @Test + void testUpdateEntryWhenPartitionStateRestoreRemovesStaleIds() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); + restore.setSyncWithDb(true); + + boolean changed = entry.updateEntry(restore, ctx); + + assertThat(changed).isTrue(); // expected to be changed, so we re-check readiness for the state + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); + assertThat(entry.getAdded()).isNull(); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isSyncWithDb()).isFalse(); + } + + @Test + void testUpdateEntryWhenPartitionStateRestoreAddsAndRemoves() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_3_ID)); + restore.setSyncWithDb(true); + + boolean changed = entry.updateEntry(restore, ctx); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_3_ID); + assertThat(entry.getAdded()).containsExactly(ENTITY_3_ID); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isSyncWithDb()).isFalse(); + } + + + @Test + void testUpdateEntryWhenPartitionStateRestoreNoChanges() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID)); + restore.setSyncWithDb(true); + + boolean changed = entry.updateEntry(restore, ctx); + + assertThat(changed).isFalse(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + assertThat(entry.getAdded()).isNull(); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isSyncWithDb()).isFalse(); + } + + @Test + void testUpdateEntryWhenPartitionStateRestoreEmptySet() { + var restore = new PropagationArgumentEntry(List.of()); + restore.setSyncWithDb(true); + + boolean changed = entry.updateEntry(restore, ctx); + + assertThat(changed).isTrue(); // expected to be changed, so we re-check readiness for the state + assertThat(entry.getEntityIds()).isEmpty(); + assertThat(entry.getAdded()).isNull(); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isSyncWithDb()).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void testToTbelCfArgWithValues() { + TbelCfArg arg = entry.toTbelCfArg(); + assertThat(arg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) arg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(Set.class); + assertThat((Set) tbelCfPropagationArg.getValue()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + } + + + @Test + @SuppressWarnings("unchecked") + void testToTbelCfArgWithEmptyValues() { + var empty = new PropagationArgumentEntry(List.of()); + TbelCfArg emptyArg = empty.toTbelCfArg(); + assertThat(emptyArg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) emptyArg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(Set.class); + assertThat((Set) tbelCfPropagationArg.getValue()).isEmpty(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java new file mode 100644 index 0000000000..66111caf9c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -0,0 +1,498 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; +import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.stats.DefaultStatsFactory; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; +import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; + +import java.util.ArrayList; +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.concurrent.ExecutionException; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class}) +public class PropagationCalculatedFieldStateTest { + + private static final String TEMPERATURE_ARGUMENT_NAME = "t"; + private static final String HUMIDITY_ARGUMENT_NAME = "h"; + private static final String TEST_RESULT_EXPRESSION_KEY = "testResult"; + private static final double TEMPERATURE_VALUE = 12.5; + private static final double HUMIDITY_VALUE = 85; + + private static final PropagationArgumentEntry EMPTY_PROPAGATION_ARGUMENT = new PropagationArgumentEntry(Collections.emptyList()); + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("6c3513cb-85e7-4510-8746-1ba01859a8ce")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("be960a50-c029-4698-b2ec-c56a543c561c")); + private final AssetId ASSET_ID_1 = new AssetId(UUID.fromString("d26f0e5b-7d7d-4a61-9f5e-08ab97b30734")); + private final AssetId ASSET_ID_2 = new AssetId(UUID.fromString("1933a317-4df5-4d36-9800-68aded74579b")); + + private final SingleValueArgumentEntry EMPTY_SINGLE_VALUE_ARGUMENT = new SingleValueArgumentEntry(); + + private final SingleValueArgumentEntry temperatureArgumentEntry = + new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", TEMPERATURE_VALUE), 99L); + + private final SingleValueArgumentEntry humidityArgumentEntry = + new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("humidity", HUMIDITY_VALUE), 99L); + + private final PropagationArgumentEntry propagationArgEntry = + new PropagationArgumentEntry(new ArrayList<>(List.of(ASSET_ID_2, ASSET_ID_1))); + + private PropagationCalculatedFieldState state; + private CalculatedFieldCtx ctx; + + @Autowired + private TbelInvokeService tbelInvokeService; + + @MockitoBean + private TenantProfile tenantProfile; + + @MockitoBean + private TbTenantProfileCache tenantProfileCache; + + @MockitoBean + private ActorSystemContext actorSystemContext; + + @MockitoBean + private CalculatedFieldProcessingService cfProcessingService; + + @BeforeEach + void setUp() { + when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); + when(actorSystemContext.getCalculatedFieldProcessingService()).thenReturn(cfProcessingService); + when(actorSystemContext.getTenantProfileCache()).thenReturn(tenantProfileCache); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); + } + + void initCtxAndState(boolean applyExpressionToResolvedArguments) { + ctx = spy(new CalculatedFieldCtx(getCalculatedField(applyExpressionToResolvedArguments), actorSystemContext)); + ctx.init(); + + state = new PropagationCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); + } + + @Test + void testType() { + initCtxAndState(false); + assertThat(state.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); + } + + @Test + void testInitAddsRequiredArgument() { + initCtxAndState(false); + assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, HUMIDITY_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); + } + + @Test + void testIsReadyReturnFalseWhenNoArgumentsSet() { + initCtxAndState(false); + assertThat(state.isReady()).isFalse(); + } + + private static Stream provideInvalidPropagationArgs() { + return Stream.of( + null, + EMPTY_PROPAGATION_ARGUMENT + ); + } + + @ParameterizedTest + @MethodSource("provideInvalidPropagationArgs") + void testIsReadyWhenPropagationArgIsNullOrEmpty(ArgumentEntry propagationEntry) { + initCtxAndState(false); + + Map args = new HashMap<>(); + args.put(TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry); // Valid user arg + args.put(HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry); // Valid user arg + + if (propagationEntry != null) { + args.put(PROPAGATION_CONFIG_ARGUMENT, propagationEntry); + } + state.update(args, ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()) + .isEqualTo("No entities found via 'Propagation path to related entities'. Verify the configured relation type and direction."); + } + + @Test + void testIsReadyWithoutExpressionWhenAllArgumentsAreNotEmpty() { + initCtxAndState(false); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(updatedArgs, ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + + @Test + void testIsReadyWithoutExpressionWhenAtLeastOneArgumentIsNotEmpty() { + initCtxAndState(false); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(updatedArgs, ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + @Test + void testIsNotReadyWithExpressionWhenAtLeastOneArgumentIsEmpty() { + initCtxAndState(true); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(updatedArgs, ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).isEqualTo("Required arguments are missing: h"); + } + + @Test + void testPerformCalculationWithEmptyPropagationArg() throws Exception { + initCtxAndState(false); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, EMPTY_PROPAGATION_ARGUMENT); + state.update(initArgs, ctx); + assertThat(state.isReady()).isFalse(); + + // test empty propagation argument calculation + PropagationCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.isEmpty()).isTrue(); + assertThat(result.getEntityIds()).isNullOrEmpty(); + } + + @Test + void testPerformCalculationWithArgumentsOnlyMode() throws Exception { + initCtxAndState(false); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); + + PropagationCalculatedFieldResult propagationResult = performCalculation(); + + assertThat(propagationResult).isNotNull(); + assertThat(propagationResult.isEmpty()).isFalse(); + assertThat(propagationResult.getEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + + TelemetryCalculatedFieldResult result = propagationResult.getResult(); + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); + assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); + + ObjectNode expectedNode = JacksonUtil.newObjectNode(); + JacksonUtil.addKvEntry(expectedNode, temperatureArgumentEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); + + assertThat(result.getResult()).isEqualTo(expectedNode); + } + + @Test + void testPerformCalculationWithExpressionResultMode() throws Exception { + initCtxAndState(true); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); + PropagationCalculatedFieldResult propagationResult = performCalculation(); + + assertThat(propagationResult).isNotNull(); + assertThat(propagationResult.isEmpty()).isFalse(); + assertThat(propagationResult.getEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + + TelemetryCalculatedFieldResult result = propagationResult.getResult(); + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); + assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); + + ObjectNode expectedNode = JacksonUtil.newObjectNode(); + expectedNode.put(TEST_RESULT_EXPRESSION_KEY, (TEMPERATURE_VALUE + HUMIDITY_VALUE) / 2); + + assertThat(result.getResult()).isEqualTo(expectedNode); + } + + @Test + void testPropagationWithUpdatedPropagationArgument() throws ExecutionException, InterruptedException { + initCtxAndState(false); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); + + AssetId newEntityId = new AssetId(UUID.fromString("83e2c962-eeae-4708-984e-e6a24760f9c3")); + PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry(); + propagationArgumentEntry.setAdded(List.of(newEntityId)); + Map updated = state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry), ctx); + assertThat(updated).isNotNull().containsEntry(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry); + + PropagationCalculatedFieldResult propagationCalculatedFieldResult = performCalculation(updated); + assertThat(propagationCalculatedFieldResult).isNotNull(); + assertThat(propagationCalculatedFieldResult.getEntityIds()).isNotNull().containsExactly(newEntityId); + assertThat(propagationCalculatedFieldResult.getResult()).isNotNull(); + assertThat(propagationCalculatedFieldResult.getResult().getResult()).isNotNull(); + assertThat(propagationCalculatedFieldResult.getResult().getResult()).isEqualTo(JacksonUtil.newObjectNode().put(TEMPERATURE_ARGUMENT_NAME, TEMPERATURE_VALUE)); + } + + @Test + void testPropapagationStateInitWithRestoredSetToFalse() { + initCtxAndState(false); + verify(cfProcessingService, never()).fetchPropagationArgumentFromDb(any(), any()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropagationStateInitRestoredTrueNoDbChanges_noReevaluation() { + initCtxAndState(false); + + // existing state already matches DB snapshot + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + + // DB returns the same set + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(propagationArgEntry)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropagationStateInitRestoredTrueOnlyRemovals_noReevaluation() { + initCtxAndState(false); + + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + + // DB snapshot contains only ASSET_ID_1 (ASSET_ID_2 should be removed) + var fromDb = new PropagationArgumentEntry(List.of(ASSET_ID_1)); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropagationStateIsNotReadyInitRestoredTrueAddedEntities_scheduleReevaluation() { + initCtxAndState(false); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList()) + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isFalse(); + + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(propagationArgEntry)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx).scheduleReevaluation(0L, state.getActorCtx()); + } + + @Test + void testPropagationStateInitRestoredTrueAddedEntities_scheduleReevaluation() { + initCtxAndState(false); + + // existing missing ASSET_ID_2 + var existing = new PropagationArgumentEntry(List.of(ASSET_ID_1)); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, existing + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); + + // DB snapshot contains both (ASSET_ID_2 should be "added") + var fromDb = new PropagationArgumentEntry(List.of(ASSET_ID_1, ASSET_ID_2)); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx).scheduleReevaluation(0L, state.getActorCtx()); + } + + @Test + void testPropagationStateInitRestoredTrueDbEmpty_clearsState_noReevaluation() { + initCtxAndState(false); + + // existing has something + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + + // DB snapshot empty -> clear + var fromDb = new PropagationArgumentEntry(Collections.emptyList()); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropagationStateInitRestoredTrueBothEmpty_noReevaluation() { + initCtxAndState(false); + + // Existing propagation argument is empty + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList()) + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isFalse(); + + // DB snapshot is also empty + var fromDb = new PropagationArgumentEntry(Collections.emptyList()); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(TENANT_ID); + calculatedField.setEntityId(DEVICE_ID); + calculatedField.setType(CalculatedFieldType.PROPAGATION); + calculatedField.setName("Test Propagation CF"); + calculatedField.setConfigurationVersion(1); + calculatedField.setConfiguration(getCalculatedFieldConfig(applyExpressionToResolvedArguments)); + calculatedField.setVersion(1L); + return calculatedField; + } + + private CalculatedFieldConfiguration getCalculatedFieldConfig(boolean applyExpressionToResolvedArguments) { + var config = new PropagationCalculatedFieldConfiguration(); + + config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + config.setApplyExpressionToResolvedArguments(applyExpressionToResolvedArguments); + + Argument temperatureArg = new Argument(); + ReferencedEntityKey tempKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + temperatureArg.setRefEntityKey(tempKey); + + Argument humidityArg = new Argument(); + ReferencedEntityKey humidityKey = new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null); + humidityArg.setRefEntityKey(humidityKey); + + config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg, HUMIDITY_ARGUMENT_NAME, humidityArg)); + config.setExpression("return { " + TEST_RESULT_EXPRESSION_KEY + ": (" + TEMPERATURE_ARGUMENT_NAME + " + " + HUMIDITY_ARGUMENT_NAME + ") / 2};"); + + AttributesOutput output = new AttributesOutput(); + output.setScope(AttributeScope.SERVER_SCOPE); + config.setOutput(output); + + return config; + } + + private PropagationCalculatedFieldResult performCalculation() throws ExecutionException, InterruptedException { + return performCalculation(Collections.emptyMap()); + } + + private PropagationCalculatedFieldResult performCalculation(Map updatedArgs) throws ExecutionException, InterruptedException { + return (PropagationCalculatedFieldResult) state.performCalculation(updatedArgs, ctx).get(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java new file mode 100644 index 0000000000..02b063a499 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java @@ -0,0 +1,244 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +import com.fasterxml.jackson.databind.JsonNode; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; +import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +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.BooleanDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.stats.DefaultStatsFactory; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class}) +public class RelatedEntitiesAggregationCalculatedFieldStateTest { + + private final long ts = System.currentTimeMillis(); + + private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("cceba360-71e9-44ab-8596-c600d60ee0d0")); + private final AssetProfileId assetProfileId = new AssetProfileId(UUID.fromString("ccced83b-e5f6-4978-bba0-c2df46de9a35")); + private final AssetId assetId = new AssetId(UUID.fromString("982dfdee-b2bc-4f04-a49d-7ffd70940c69")); + private final DeviceId device1 = new DeviceId(UUID.fromString("47f1fef5-a3b7-46e7-9732-b7669d3ef885")); + private final DeviceId device2 = new DeviceId(UUID.fromString("3b097e5e-9eef-46f4-8997-937075e6f342")); + + private RelatedEntitiesAggregationCalculatedFieldState state; + private CalculatedFieldCtx ctx; + + @Autowired + private TbelInvokeService tbelInvokeService; + + @MockitoBean + private TenantProfile tenantProfile; + + @MockitoBean + private TbTenantProfileCache tenantProfileCache; + + @MockitoBean + private ActorSystemContext actorSystemContext; + + @BeforeEach + void setUp() { + when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); + when(actorSystemContext.getTenantProfileCache()).thenReturn(tenantProfileCache); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); + + initCtxAndState(); + } + + void initCtxAndState() { + ctx = new CalculatedFieldCtx(getCalculatedField(), actorSystemContext); + ctx.init(); + + state = new RelatedEntitiesAggregationCalculatedFieldState(assetId); + state.setCtx(ctx, null); + state.init(false); + } + + @Test + void testType() { + assertThat(state.getType()).isEqualTo(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + } + + @Test + void testInitAddsRequiredArgument() { + assertThat(state.getRequiredArguments()).contains("oc"); + } + + @Test + void testIsReadyWhenNoRelatedEntities() { + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()) + .isEqualTo("No entities found via 'Aggregation path to related entities'. Verify the configured relation type and direction."); + } + + @Test + void testUpdateArguments() { + assertThat(state.getLastArgsRefreshTs()).isEqualTo(-1); + + state.update(arguments(), ctx); + + assertThat(state.getLastArgsRefreshTs()).isGreaterThan(-1); + } + + @Test + void testPerformCalculationWhenArgsUpdatedButIntervalDidNotPass() throws Exception { + // deduplication interval 60 sec + state.setLastMetricsEvalTs(ts - TimeUnit.SECONDS.toMillis(10)); + state.setLastArgsRefreshTs(ts - TimeUnit.SECONDS.toMillis(5)); + + assertThat(state.performCalculation(arguments(), ctx).get()).isEqualTo(TelemetryCalculatedFieldResult.EMPTY); + } + + @Test + void testPerformCalculationWhenIntervalPassedButNoArgsUpdated() throws Exception { + state.setLastMetricsEvalTs(ts - TimeUnit.SECONDS.toMillis(90)); + state.setLastArgsRefreshTs(ts - TimeUnit.SECONDS.toMillis(90)); + + assertThat(state.performCalculation(arguments(), ctx).get()).isEqualTo(TelemetryCalculatedFieldResult.EMPTY); + } + + @Test + void testPerformCalculationWhenIntervalPassedAndArgsUpdated() throws Exception { + state.setLastMetricsEvalTs(ts - TimeUnit.SECONDS.toMillis(70)); + state.setLastArgsRefreshTs(ts - TimeUnit.SECONDS.toMillis(10)); + + Map arguments = arguments(); + state.update(arguments, ctx); + + TelemetryCalculatedFieldResult calculatedFieldResult = (TelemetryCalculatedFieldResult) state.performCalculation(arguments, ctx).get(); + assertThat(calculatedFieldResult).isNotNull(); + assertThat(calculatedFieldResult.isEmpty()).isFalse(); + assertThat(calculatedFieldResult.getType()).isEqualTo(OutputType.TIME_SERIES); + + JsonNode result = calculatedFieldResult.getResult(); + assertThat(result).isNotNull(); + safeAssert(result.get("ts"), String.valueOf(state.getLatestTimestamp())); + JsonNode values = result.get("values"); + safeAssert(values.get("freeSpaces"), "1"); + safeAssert(values.get("occupiedSpaces"), "1"); + safeAssert(values.get("totalSpaces"), "2"); + } + + private void safeAssert(JsonNode node, String expected) { + assertThat(node).isNotNull(); + assertThat(node.asText()).isEqualTo(expected); + } + + private Map arguments() { + Map arguments = new HashMap<>(); + Map entityArguments = new HashMap<>(); + entityArguments.put(device1, new SingleValueArgumentEntry(ts - 100, new BooleanDataEntry("occupied", true), 23L)); + entityArguments.put(device2, new SingleValueArgumentEntry(ts - 80, new BooleanDataEntry("occupied", false), 26L)); + RelatedEntitiesArgumentEntry entry = new RelatedEntitiesArgumentEntry(entityArguments, false); + arguments.put("oc", entry); + return arguments; + } + + private CalculatedField getCalculatedField() { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(tenantId); + calculatedField.setEntityId(assetProfileId); + calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + calculatedField.setName("Test CF"); + calculatedField.setConfigurationVersion(1); + + var config = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + config.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + Argument oc = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey("occupied", ArgumentType.TS_LATEST, null); + oc.setRefEntityKey(refKey); + config.setArguments(Map.of("oc", oc)); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + config.setMetrics(aggMetrics); + + config.setDeduplicationIntervalInSec(60); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + config.setOutput(output); + + config.setUseLatestTs(true); + + config.setScheduledUpdateInterval(10); + + calculatedField.setConfiguration(config); + calculatedField.setVersion(1L); + return calculatedField; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java new file mode 100644 index 0000000000..725860dd80 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java @@ -0,0 +1,110 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +public class RelatedEntitiesArgumentEntryTest { + + private RelatedEntitiesArgumentEntry entry; + + @Mock + private CalculatedFieldCtx ctx; + + private final DeviceId device1 = new DeviceId(UUID.fromString("1984e5f4-9ff0-4187-84ae-e4438bba4c8a")); + private final DeviceId device2 = new DeviceId(UUID.fromString("937fc062-1a9d-438f-aa22-55a93fc908b7")); + + private final long ts = System.currentTimeMillis(); + + @BeforeEach + void setUp() { + lenient().when(ctx.getMaxRelatedEntitiesPerCfArgument()).thenReturn(1000L); + + Map aggInputs = new HashMap<>(); + aggInputs.put(device1, new SingleValueArgumentEntry(device1, new BasicTsKvEntry(ts - 100, new LongDataEntry("key", 12L), 1L))); + aggInputs.put(device2, new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 150, new LongDataEntry("key", 16L), 6L))); + + entry = new RelatedEntitiesArgumentEntry(aggInputs, false); + } + + @Test + void testUpdateEntryWhenNotAggEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for aggregation argument entry: " + ArgumentEntryType.TS_ROLLING); + } + + @Test + void testUpdateEntryWhenAggArgumentEntryPasser() { + DeviceId device3 = new DeviceId(UUID.randomUUID()); + DeviceId device4 = new DeviceId(UUID.randomUUID()); + + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = new RelatedEntitiesArgumentEntry(Map.of( + device3, new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 16L), 13L)), + device4, new SingleValueArgumentEntry(device4, new BasicTsKvEntry(ts - 60, new LongDataEntry("key", 23L), 7L)) + ), false); + + assertThat(entry.updateEntry(relatedEntitiesArgumentEntry, ctx)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(4); + assertThat(aggInputs.get(device3)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device3)); + assertThat(aggInputs.get(device4)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device4)); + } + + @Test + void testUpdateEntryWhenSingleValueArgumentEntryPassedAndNoEntriesById() { + DeviceId device3 = new DeviceId(UUID.randomUUID()); + + SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); + + assertThat(entry.updateEntry(singleEntityArgumentEntry, ctx)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(3); + assertThat(aggInputs.get(device3)).isEqualTo(singleEntityArgumentEntry); + } + + @Test + void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() { + SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); + + assertThat(entry.updateEntry(singleEntityArgumentEntry, ctx)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(2); + assertThat(aggInputs.get(device2)).isEqualTo(singleEntityArgumentEntry); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 1c8c1e160d..e1b881fec1 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -18,20 +18,23 @@ package org.thingsboard.server.service.cf.ctx.state; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; @@ -39,12 +42,15 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.DefaultStatsFactory; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; -import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -72,14 +78,24 @@ public class ScriptCalculatedFieldStateTest { private TbelInvokeService tbelInvokeService; @MockitoBean - private ApiLimitService apiLimitService; + private TenantProfile tenantProfile; + + @MockitoBean + private TbTenantProfileCache tenantProfileCache; @BeforeEach void setUp() { - when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService); + ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class); + when(systemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); + when(systemContext.getTenantProfileCache()).thenReturn(tenantProfileCache); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); + + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); - state = new ScriptCalculatedFieldState(ctx.getArgNames()); + state = new ScriptCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); } @Test @@ -92,7 +108,7 @@ public class ScriptCalculatedFieldStateTest { state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry)); Map newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -109,7 +125,7 @@ public class ScriptCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L); Map newArgs = Map.of("assetHumidity", newArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -124,7 +140,7 @@ public class ScriptCalculatedFieldStateTest { void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -140,7 +156,7 @@ public class ScriptCalculatedFieldStateTest { "assetHumidity", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("a", 45L), 10L) )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -152,20 +168,21 @@ public class ScriptCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - + state.update(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry)); - + state.update(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("deviceTemperature"); } private TsRollingArgumentEntry createRollingArgEntry() { @@ -211,8 +228,7 @@ public class ScriptCalculatedFieldStateTest { config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity / 2 }"); - Output output = new Output(); - output.setType(OutputType.ATTRIBUTES); + AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); config.setOutput(output); @@ -220,4 +236,8 @@ public class ScriptCalculatedFieldStateTest { return config; } + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 0001566abd..4454d41e6e 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -18,17 +18,20 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; @@ -38,11 +41,14 @@ 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.dao.usagerecord.ApiLimitService; -import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -66,14 +72,21 @@ public class SimpleCalculatedFieldStateTest { private CalculatedFieldCtx ctx; @Mock - private ApiLimitService apiLimitService; + private TenantProfile tenantProfile; + @Mock + private TbTenantProfileCache tenantProfileCache; + @InjectMocks + private ActorSystemContext systemContext; @BeforeEach void setUp() { - when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); - state = new SimpleCalculatedFieldState(ctx.getArgNames()); + state = new SimpleCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); } @Test @@ -89,7 +102,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", key3ArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -107,7 +120,7 @@ public class SimpleCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L); Map newArgs = Map.of("key1", newArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry)); @@ -121,9 +134,10 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); - assertThatThrownBy(() -> state.updateState(ctx, newArgs)) + assertThatThrownBy(() -> state.update(newArgs, ctx)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Rolling argument entry is not supported for simple calculated fields."); + .hasMessage("Unsupported argument type detected for argument: key3. " + + "Rolling argument entry is not supported for simple calculated fields."); } @Test @@ -134,7 +148,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -151,7 +165,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - assertThatThrownBy(() -> state.performCalculation(ctx)) + assertThatThrownBy(() -> state.performCalculation(Collections.emptyMap(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Argument 'key2' is not a number."); } @@ -164,7 +178,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -185,7 +199,7 @@ public class SimpleCalculatedFieldStateTest { output.setDecimalsByDefault(3); ctx.setOutput(output); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); @@ -196,28 +210,29 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, "key2", key2ArgEntry, "key3", key3ArgEntry - )); - + ), ctx); assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, - "key2", key2ArgEntry - )); - state.getArguments().put("key3", new SingleValueArgumentEntry()); - + "key2", key2ArgEntry, + "key3", new SingleValueArgumentEntry() + ), ctx); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("key3"); } private CalculatedField getCalculatedField() { @@ -254,9 +269,8 @@ public class SimpleCalculatedFieldStateTest { config.setExpression("key1 + key2 + key3"); - Output output = new Output(); + AttributesOutput output = new AttributesOutput(); output.setName("output"); - output.setType(OutputType.ATTRIBUTES); output.setScope(AttributeScope.SERVER_SCOPE); output.setDecimalsByDefault(0); @@ -265,4 +279,8 @@ public class SimpleCalculatedFieldStateTest { return config; } + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java index 707397f6b1..57bb161688 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.server.common.data.kv.JsonDataEntry; @@ -29,10 +32,14 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(MockitoExtension.class) public class SingleValueArgumentEntryTest { private SingleValueArgumentEntry entry; + @Mock + private CalculatedFieldCtx ctx; + private final long ts = System.currentTimeMillis(); @BeforeEach @@ -47,43 +54,48 @@ public class SingleValueArgumentEntryTest { @Test void testUpdateEntryWhenRollingEntryPassed() { - assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for single value argument entry: " + ArgumentEntryType.TS_ROLLING); } @Test void testUpdateEntryWithTheSameTs() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L))).isFalse(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L), ctx)).isFalse(); + } + + @Test + void testUpdateEntryWithTheSameTsAndDifferentVersion() { + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 364L), ctx)).isTrue(); } @Test void testUpdateEntryWhenNewVersionIsNull() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null))).isTrue(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null), ctx)).isTrue(); assertThat(entry.getValue()).isEqualTo(13L); assertThat(entry.getVersion()).isNull(); } @Test void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 369L))).isTrue(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 369L), ctx)).isTrue(); assertThat(entry.getValue()).isEqualTo(18L); assertThat(entry.getVersion()).isEqualTo(369L); } @Test void testUpdateEntryWhenNewVersionIsLessThanCurrent() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 234L))).isFalse(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 234L), ctx)).isFalse(); } @Test void testUpdateEntryWhenValueWasNotChanged() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L), ctx)).isTrue(); } @Test void testUpdateEntryWithOldTs() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts - 10, new LongDataEntry("key", 14L), 365L))).isFalse(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts - 10, new LongDataEntry("key", 14L), 365L), ctx)).isFalse(); } @Test @@ -115,4 +127,5 @@ public class SingleValueArgumentEntryTest { expectedList.add(Map.of("test2", 20)); assertThat(singleValueArg.getValue()).isEqualTo(expectedList); } + } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java index b0c30f5a84..7dea28e655 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; @@ -26,10 +29,14 @@ import java.util.TreeMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(MockitoExtension.class) public class TsRollingArgumentEntryTest { private TsRollingArgumentEntry entry; + @Mock + private CalculatedFieldCtx ctx; + private final long ts = System.currentTimeMillis(); @BeforeEach @@ -51,7 +58,7 @@ public class TsRollingArgumentEntryTest { void testUpdateEntryWhenSingleValueEntryPassed() { SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new DoubleDataEntry("key", 23.0), 123L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(4); assertThat(entry.getTsRecords().get(ts - 10)).isEqualTo(23.0); } @@ -64,7 +71,7 @@ public class TsRollingArgumentEntryTest { values.put(ts - 5, 1.0); newEntry.setTsRecords(values); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(5); assertThat(entry.getTsRecords()).isEqualTo(Map.of( ts - 40, 10.0, @@ -79,7 +86,7 @@ public class TsRollingArgumentEntryTest { void testUpdateEntryWhenValueIsNotNumber() { SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new StringDataEntry("key", "string"), 123L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords().get(ts - 10)).isNaN(); } @@ -93,7 +100,7 @@ public class TsRollingArgumentEntryTest { newEntry.setTsRecords(values); entry = new TsRollingArgumentEntry(3, 30000L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(1); assertThat(entry.getTsRecords()).isEqualTo(Map.of( ts - 5, 0.0 @@ -111,7 +118,7 @@ public class TsRollingArgumentEntryTest { newEntry.setTsRecords(values); entry = new TsRollingArgumentEntry(3, 30000L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(3); assertThat(entry.getTsRecords()).isEqualTo(Map.of( ts - 18, 0.0, diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index eb16623b30..3bf5fa2f25 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.service.edge; +import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -36,14 +38,37 @@ import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode; import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +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.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +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.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.relation.EntityRelation; +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.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.lang.reflect.Constructor; import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.stream.Stream; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION; @@ -155,4 +180,283 @@ public class EdgeMsgConstructorUtilsTest { String.format("For EdgeVersion '%s', ruleNode '%s' should not be included.", edgeVersion, ruleNode.getType())); } + @Test + @DisplayName("mergeDownlinkDuplicates: latest per attribute key is retained and duplicates removed") + public void testMergeDownlinkDuplicates() { + UUID deviceId = UUID.randomUUID(); + UUID assetId = UUID.randomUUID(); + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + + var deviceAttrUpdate1 = createEdgeEvent(tenantId, 1, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBody(1_000L, "{\"a\":1,\"b\":1,\"d\":1}")); + var deviceAttrUpdate2 = createEdgeEvent(tenantId, 2, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBody(2_000L, "{\"a\":2,\"b\":2,\"c\":2}")); + var deviceAttrUpdate3 = createEdgeEvent(tenantId, 3, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBody(3_000L, "{\"a\":3,\"d\":3}")); + + var deviceUpdate = createEdgeEvent(tenantId, 4, EdgeEventActionType.UPDATED, + deviceId, EdgeEventType.DEVICE, null); + var deviceUpdateDup = createEdgeEvent(tenantId, 5, EdgeEventActionType.UPDATED, + deviceId, EdgeEventType.DEVICE, null); + + var assetAttrUpdate1 = createEdgeEvent(tenantId, 6, EdgeEventActionType.ATTRIBUTES_UPDATED, + assetId, EdgeEventType.ASSET, createAttrBody(6_000L, "{\"a\":6,\"d\":6}")); + var assetAttrUpdate2 = createEdgeEvent(tenantId, 7, EdgeEventActionType.ATTRIBUTES_UPDATED, + assetId, EdgeEventType.ASSET, createAttrBody(7_000L, "{\"a\":7,\"b\":7,\"c\":7}")); + var assetAttrUpdate3 = createEdgeEvent(tenantId, 8, EdgeEventActionType.ATTRIBUTES_UPDATED, + assetId, EdgeEventType.ASSET, createAttrBody(8_000L, "{\"a\":8,\"d\":8}")); + + List input = List.of(deviceAttrUpdate1, deviceAttrUpdate2, deviceAttrUpdate3, + deviceUpdate, deviceUpdateDup, + assetAttrUpdate1, assetAttrUpdate2, assetAttrUpdate3); + List merged = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(input); + + Assertions.assertEquals(5, merged.size()); + + EdgeEvent deviceMergedAttrBC = merged.get(0); + Assertions.assertEquals(2, deviceMergedAttrBC.getSeqId()); + Assertions.assertEquals(deviceId, deviceMergedAttrBC.getEntityId()); + Assertions.assertEquals(2_000L, deviceMergedAttrBC.getBody().get("ts").asLong()); + Assertions.assertEquals(2, getIntValue(deviceMergedAttrBC.getBody(), "b")); + Assertions.assertEquals(2, getIntValue(deviceMergedAttrBC.getBody(), "c")); + Assertions.assertNull(getIntValue(deviceMergedAttrBC.getBody(), "a")); + + EdgeEvent deviceMergedAttrAD = merged.get(1); + Assertions.assertEquals(3, deviceMergedAttrAD.getSeqId()); + Assertions.assertEquals(deviceId, deviceMergedAttrAD.getEntityId()); + Assertions.assertEquals(3_000L, deviceMergedAttrAD.getBody().get("ts").asLong()); + Assertions.assertEquals(3, getIntValue(deviceMergedAttrAD.getBody(), "a")); + Assertions.assertEquals(3, getIntValue(deviceMergedAttrAD.getBody(), "d")); + + EdgeEvent mergedDeviceUpdate = merged.get(2); + Assertions.assertEquals(4, mergedDeviceUpdate.getSeqId()); + Assertions.assertEquals(EdgeEventActionType.UPDATED, mergedDeviceUpdate.getAction()); + + EdgeEvent assetMergedAttrBC = merged.get(3); + Assertions.assertEquals(7, assetMergedAttrBC.getSeqId()); + Assertions.assertEquals(assetId, assetMergedAttrBC.getEntityId()); + Assertions.assertEquals(7_000L, assetMergedAttrBC.getBody().get("ts").asLong()); + Assertions.assertEquals(7, getIntValue(assetMergedAttrBC.getBody(), "b")); + Assertions.assertEquals(7, getIntValue(assetMergedAttrBC.getBody(), "c")); + Assertions.assertNull(getIntValue(assetMergedAttrBC.getBody(), "a")); + + EdgeEvent assetMergedAttrAD = merged.get(4); + Assertions.assertEquals(8, assetMergedAttrAD.getSeqId()); + Assertions.assertEquals(assetId, assetMergedAttrAD.getEntityId()); + Assertions.assertEquals(8_000L, assetMergedAttrAD.getBody().get("ts").asLong()); + Assertions.assertEquals(8, getIntValue(assetMergedAttrAD.getBody(), "a")); + Assertions.assertEquals(8, getIntValue(assetMergedAttrAD.getBody(), "d")); + } + + @Test + public void testMergeDownlinkDuplicates_attrBodyHasNoTs_returnOriginalList() { + UUID deviceId = UUID.randomUUID(); + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + + var deviceAttrUpdate1 = createEdgeEvent(tenantId, 1, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":1,\"b\":1,\"d\":1}")); + var deviceAttrUpdate2 = createEdgeEvent(tenantId, 2, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":2,\"b\":2,\"c\":2}")); + var deviceAttrUpdate3 = createEdgeEvent(tenantId, 3, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":3,\"d\":3}")); + + List input = List.of(deviceAttrUpdate1, deviceAttrUpdate2, deviceAttrUpdate3); + List merged = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(input); + + Assertions.assertEquals(3, merged.size()); + Assertions.assertEquals(deviceAttrUpdate1, merged.get(0)); + Assertions.assertEquals(deviceAttrUpdate2, merged.get(1)); + Assertions.assertEquals(deviceAttrUpdate3, merged.get(2)); + } + + private Integer getIntValue(JsonNode body, String key) { + return body.get("kv").get(key) != null ? body.get("kv").get(key).asInt() : null; + } + + private static JsonNode createAttrBodyWithoutTs(String kvJson) { + return JacksonUtil.toJsonNode("{\"kv\":" + kvJson + "}"); + } + + private static JsonNode createAttrBody(long ts, String kvJson) { + return JacksonUtil.toJsonNode("{\"ts\":" + ts + ",\"kv\":" + kvJson + "}"); + } + + private static EdgeEvent createEdgeEvent(TenantId tenantId, + long seqId, + EdgeEventActionType action, + UUID entityId, + EdgeEventType type, + JsonNode body) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setSeqId(seqId); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(action); + edgeEvent.setEntityId(entityId); + edgeEvent.setType(type); + edgeEvent.setBody(body); + return edgeEvent; + } + + @Test + public void testConstructAssetUpdatedMsg_versionIsReset() { + Asset asset = new Asset(); + asset.setId(new AssetId(UUID.randomUUID())); + asset.setName("Test Asset"); + asset.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructAssetUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, asset).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "Asset version should be null in serialized message"); + } + + @Test + public void testConstructAssetProfileUpdatedMsg_versionIsReset() { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setId(new AssetProfileId(UUID.randomUUID())); + assetProfile.setName("Test Asset Profile"); + assetProfile.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, assetProfile).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "AssetProfile version should be null in serialized message"); + } + + @Test + public void testConstructCustomerUpdatedMsg_versionIsReset() { + Customer customer = new Customer(); + customer.setId(new CustomerId(UUID.randomUUID())); + customer.setTitle("Test Customer"); + customer.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructCustomerUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, customer).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "Customer version should be null in serialized message"); + } + + @Test + public void testConstructDashboardUpdatedMsg_versionIsReset() { + Dashboard dashboard = new Dashboard(); + dashboard.setId(new DashboardId(UUID.randomUUID())); + dashboard.setTitle("Test Dashboard"); + dashboard.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructDashboardUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, dashboard).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "Dashboard version should be null in serialized message"); + } + + @Test + public void testConstructDeviceUpdatedMsg_versionIsReset() { + Device device = new Device(); + device.setId(new DeviceId(UUID.randomUUID())); + device.setName("Test Device"); + device.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructDeviceUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, device).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "Device version should be null in serialized message"); + } + + @Test + public void testConstructDeviceCredentialsUpdatedMsg_versionIsReset() { + DeviceCredentials credentials = new DeviceCredentials(); + credentials.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructDeviceCredentialsUpdatedMsg(credentials).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "DeviceCredentials version should be null in serialized message"); + } + + @Test + public void testConstructEntityViewUpdatedMsg_versionIsReset() { + EntityView entityView = new EntityView(); + entityView.setId(new EntityViewId(UUID.randomUUID())); + entityView.setName("Test EntityView"); + entityView.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructEntityViewUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, entityView).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "EntityView version should be null in serialized message"); + } + + @Test + public void testConstructRelationUpdatedMsg_versionIsReset() { + EntityRelation relation = new EntityRelation(); + relation.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructRelationUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, relation).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "EntityRelation version should be null in serialized message"); + } + + @Test + public void testConstructRuleChainUpdatedMsg_versionIsReset() { + RuleChain ruleChain = new RuleChain(); + ruleChain.setId(new org.thingsboard.server.common.data.id.RuleChainId(UUID.randomUUID())); + ruleChain.setName("Test RuleChain"); + ruleChain.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructRuleChainUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChain, false).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "RuleChain version should be null in serialized message"); + } + + @Test + public void testConstructTenantUpdateMsg_versionIsReset() { + Tenant tenant = new Tenant(); + tenant.setId(TenantId.fromUUID(UUID.randomUUID())); + tenant.setTitle("Test Tenant"); + tenant.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructTenantUpdateMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenant).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "Tenant version should be null in serialized message"); + } + + @Test + public void testConstructUserUpdatedMsg_versionIsReset() { + User user = new User(); + user.setId(new UserId(UUID.randomUUID())); + user.setEmail("test@test.com"); + user.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructUserUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, user).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "User version should be null in serialized message"); + } + + @Test + public void testConstructRuleChainMetadataUpdatedMsg_versionIsReset() { + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setVersion(42L); + + String entity = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg( + UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, metaData, EdgeVersion.V_4_0_0).getEntity(); + JsonNode json = JacksonUtil.toJsonNode(entity); + + Assertions.assertTrue(json.get("version") == null || json.get("version").isNull(), + "RuleChainMetaData version should be null in serialized message"); + } } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java index 01f3a08853..f1f0965a0d 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -44,9 +45,11 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; @@ -58,6 +61,16 @@ import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_T @ExtendWith(MockitoExtension.class) public class EdgeStatsTest { + private static final int TTL_DAYS = 30; + private static final long REPORT_INTERVAL_MILLIS = 600_000L; + + private static final long EXPECTED_MSGS_ADDED = 5L; + private static final long EXPECTED_MSGS_PUSHED = 3L; + private static final long EXPECTED_MSGS_PERMANENTLY_FAILED = 1L; + private static final long EXPECTED_MSGS_TMP_FAILED = 0L; + private static final long EXPECTED_MSGS_LAG = 10L; + private static final long EXPECTED_MSGS_KAFKA_LAG = 15L; + @Mock private TimeseriesService tsService; @Mock @@ -66,108 +79,100 @@ public class EdgeStatsTest { private EdgeStatsCounterService statsCounterService; private EdgeStatsService edgeStatsService; + @Captor + private ArgumentCaptor> captor; + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); private final EdgeId edgeId = new EdgeId(UUID.randomUUID()); @BeforeEach void setUp() { - edgeStatsService = new EdgeStatsService( + edgeStatsService = createEdgeStatsService(Optional.empty()); + } + + private EdgeStatsService createEdgeStatsService(Optional kafkaAdmin) { + EdgeStatsService service = new EdgeStatsService( tsService, statsCounterService, topicService, - Optional.empty() + kafkaAdmin ); - - ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); - ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); + ReflectionTestUtils.setField(service, "edgesStatsTtlDays", TTL_DAYS); + ReflectionTestUtils.setField(service, "reportIntervalMillis", REPORT_INTERVAL_MILLIS); + return service; } @Test public void testReportStatsSavesTelemetry() { - // given + // GIVEN + setupCounters(); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + Map counters = verifyCounters(); + Assertions.assertEquals(EXPECTED_MSGS_LAG, counters.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); + } + + @Test + public void testReportStatsWithKafkaLag() { + // GIVEN + setupCounters(); + setupKafkaLag(); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + Map valuesByKey = verifyCounters(); + Assertions.assertEquals(EXPECTED_MSGS_KAFKA_LAG, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); + } + + private void setupCounters() { MsgCounters counters = new MsgCounters(tenantId); - counters.getMsgsAdded().set(5); - counters.getMsgsPushed().set(3); - counters.getMsgsPermanentlyFailed().set(1); - counters.getMsgsTmpFailed().set(0); - counters.getMsgsLag().set(10); + counters.getMsgsAdded().set(EXPECTED_MSGS_ADDED); + counters.getMsgsPushed().set(EXPECTED_MSGS_PUSHED); + counters.getMsgsPermanentlyFailed().set(EXPECTED_MSGS_PERMANENTLY_FAILED); + counters.getMsgsTmpFailed().set(EXPECTED_MSGS_TMP_FAILED); + counters.getMsgsLag().set(EXPECTED_MSGS_LAG); ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); countersByEdge.put(edgeId, counters); - when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); + when(statsCounterService.getMsgCountersByEdge()).thenReturn(countersByEdge); - ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); + } - // when - edgeStatsService.reportStats(); + private Map verifyCounters() { + verify(tsService, times(1)).save(eq(tenantId), eq(edgeId), anyList(), anyLong()); + verify(statsCounterService, times(1)).clear(edgeId); - // then List entries = captor.getValue(); Assertions.assertEquals(5, entries.size()); Map valuesByKey = entries.stream() .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); - Assertions.assertEquals(5L, valuesByKey.get(DOWNLINK_MSGS_ADDED.getKey()).longValue()); - Assertions.assertEquals(3L, valuesByKey.get(DOWNLINK_MSGS_PUSHED.getKey()).longValue()); - Assertions.assertEquals(1L, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); - Assertions.assertEquals(0L, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); - Assertions.assertEquals(10L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); - - - verify(statsCounterService).clear(edgeId); + Assertions.assertEquals(EXPECTED_MSGS_ADDED, valuesByKey.get(DOWNLINK_MSGS_ADDED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_PUSHED, valuesByKey.get(DOWNLINK_MSGS_PUSHED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_TMP_FAILED, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); + return valuesByKey; } - @Test - public void testReportStatsWithKafkaLag() { - // given - MsgCounters counters = new MsgCounters(tenantId); - counters.getMsgsAdded().set(2); - counters.getMsgsPushed().set(2); - counters.getMsgsPermanentlyFailed().set(0); - counters.getMsgsTmpFailed().set(1); - counters.getMsgsLag().set(0); - - ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); - countersByEdge.put(edgeId, counters); - - // mocks - when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); - + private void setupKafkaLag() { String topic = "edge-topic"; TopicPartitionInfo partitionInfo = new TopicPartitionInfo(topic, tenantId, 0, false); when(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId)).thenReturn(partitionInfo); KafkaAdmin kafkaAdmin = mock(KafkaAdmin.class); when(kafkaAdmin.getTotalLagForGroupsBulk(Set.of(topic))) - .thenReturn(Map.of(topic, 15L)); - - ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); - when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) - .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); - - edgeStatsService = new EdgeStatsService( - tsService, - statsCounterService, - topicService, - Optional.of(kafkaAdmin) - ); - ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); - ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); - - // when - edgeStatsService.reportStats(); - - // then - List entries = captor.getValue(); - Map valuesByKey = entries.stream() - .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); + .thenReturn(Map.of(topic, EXPECTED_MSGS_KAFKA_LAG)); - Assertions.assertEquals(15L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); - verify(statsCounterService).clear(edgeId); + edgeStatsService = createEdgeStatsService(Optional.of(kafkaAdmin)); } } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java index 04a1b87833..7614bdd310 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java @@ -32,7 +32,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.springframework.test.util.ReflectionTestUtils; -import org.thingsboard.server.controller.AbstractWebTest; import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; import java.io.ByteArrayInputStream; @@ -56,7 +55,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; /** - * Tests for Edge gRPC SSL setup using the production {@link EdgeGrpcService#setupSsl} method. + * Tests for Edge gRPC SSL setup using the production {@link GrpcServer#setupSsl} method. *

* Covers: * 1. Separate cert and key PEM inputs @@ -68,6 +67,8 @@ import static org.awaitility.Awaitility.await; */ class EdgeGrpcSslTest { + private static final int TIMEOUT = 30; + static { if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); @@ -170,13 +171,14 @@ class EdgeGrpcSslTest { Path certOnlyFile = writeTempPem("cert-only", cert); assertThatThrownBy(() -> startServer(certOnlyFile.toString(), "", null)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); } // --- Server startup using production EdgeGrpcService.setupSsl() --- private Server startServer(String certFileResource, String privateKeyResource, String keyPassword) throws Exception { - EdgeGrpcService edgeGrpcService = new EdgeGrpcService(); + GrpcServer edgeGrpcService = new GrpcServer(new EdgeRpcServiceGrpc.EdgeRpcServiceImplBase() {}); ReflectionTestUtils.setField(edgeGrpcService, "certFileResource", certFileResource); ReflectionTestUtils.setField(edgeGrpcService, "privateKeyResource", privateKeyResource); ReflectionTestUtils.setField(edgeGrpcService, "keyPassword", keyPassword != null ? keyPassword : ""); @@ -200,7 +202,7 @@ class EdgeGrpcSslTest { .build(); channel.getState(true); // trigger connection attempt - await().atMost(AbstractWebTest.TIMEOUT, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(50, TimeUnit.MILLISECONDS) .untilAsserted(() -> { var state = channel.getState(false); diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index a16cf99c1d..adf664e7cf 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -19,8 +19,8 @@ import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; @@ -61,7 +61,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest { @Autowired private EdqsService edqsService; - @MockBean + @MockitoBean private EdqsRocksDb edqsRocksDb; @Before diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index 4886310750..96e4444ffd 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -62,6 +62,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.ASSIGNED_TO_USER; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.UNASSIGNED_BY_USER; +import static org.thingsboard.server.common.data.alarm.AlarmCommentSubType.UNASSIGNED_FROM_DELETED_USER; @SpringJUnitConfig(DefaultTbAlarmService.class) class DefaultTbAlarmServiceTest { @@ -217,9 +220,10 @@ class DefaultTbAlarmServiceTest { service.unassign(new Alarm(), 0L, user); ObjectNode commentNode = JacksonUtil.newObjectNode(); - commentNode.put("subtype", "ASSIGN"); - commentNode.put("text", "Alarm was unassigned by user " + user.getTitle()); - commentNode.put("userId", user.getId().getId().toString()); + commentNode.put("text", String.format(UNASSIGNED_BY_USER.getText(), user.getTitle())); + commentNode.put("subtype", UNASSIGNED_BY_USER.name()); + commentNode.put("userName", user.getTitle()); + AlarmComment expectedAlarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) @@ -243,8 +247,10 @@ class DefaultTbAlarmServiceTest { service.unassignDeletedUserAlarms(tenantId, user.getId(), user.getTitle(), List.of(alarm.getUuidId()), System.currentTimeMillis()); ObjectNode commentNode = JacksonUtil.newObjectNode(); - commentNode.put("subtype", "ASSIGN"); - commentNode.put("text", String.format("Alarm was unassigned because user %s - was deleted", user.getTitle())); + commentNode.put("text", String.format(UNASSIGNED_FROM_DELETED_USER.getText(), user.getTitle())); + commentNode.put("subtype", UNASSIGNED_FROM_DELETED_USER.name()); + commentNode.put("userName", user.getTitle()); + AlarmComment expectedAlarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) diff --git a/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java deleted file mode 100644 index a82aad5b62..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.update; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ActiveProfiles; -import org.thingsboard.common.util.JacksonUtil; - -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.willCallRealMethod; - -@ActiveProfiles("install") -@SpringBootTest(classes = DefaultDataUpdateService.class) -class DefaultDataUpdateServiceTest { - - @MockBean - DefaultDataUpdateService service; - - @BeforeEach - void setUp() { - willCallRealMethod().given(service).convertDeviceProfileAlarmRulesForVersion330(any()); - willCallRealMethod().given(service).convertDeviceProfileForVersion330(any()); - } - - JsonNode readFromResource(String resourceName) throws IOException { - return JacksonUtil.OBJECT_MAPPER.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName)); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330FirstRun() throws IOException { - JsonNode spec = readFromResource("update/330/device_profile_001_in.json"); - JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); - - assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isTrue(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330SecondRun() throws IOException { - JsonNode spec = readFromResource("update/330/device_profile_001_out.json"); - JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); - - assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330EmptyJson() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ }"); - JsonNode expected = JacksonUtil.toJsonNode("{ }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330AlarmNodeNull() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ \"alarms\" : null }"); - JsonNode expected = JacksonUtil.toJsonNode("{ \"alarms\" : null }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330NoAlarmNode() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); - JsonNode expected = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - -} diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index c17a9f6bfb..63fa4a7da4 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.job; +import com.google.common.util.concurrent.SettableFuture; import lombok.SneakyThrows; import org.junit.After; import org.junit.Before; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -44,10 +46,12 @@ import org.thingsboard.server.queue.task.JobStatsService; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -518,19 +522,114 @@ public class JobManagerTest extends AbstractControllerTest { }); } + @Test + public void testSubmitJob_finishCallback_success() { + SettableFuture future = SettableFuture.create(); + + int tasksCount = 3; + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(100) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThat(future.get()).isNull(); + }); + } + + @Test + public void testSubmitJob_finishCallback_taskFailure() { + SettableFuture future = SettableFuture.create(); + + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(1) + .failedTasksCount(2) + .errors(List.of("task error")) + .retries(0) + .taskProcessingTimeMs(100) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .cause() + .hasMessage("task error; task error"); + }); + } + + @Test + public void testSubmitJob_finishCallback_generalError() { + SettableFuture future = SettableFuture.create(); + + submitJob(DummyJobConfiguration.builder() + .generalError("Something went wrong") + .submittedTasksBeforeGeneralError(0) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .cause() + .hasMessage("Something went wrong"); + }); + } + + @Test + public void testSubmitJob_finishCallback_cancelled() throws Exception { + SettableFuture future = SettableFuture.create(); + + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(200) + .taskProcessingTimeMs(50) + .build(), "test-job", TbCallback.wrap(future)).getId(); + + Thread.sleep(500); + cancelJob(jobId); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .cause() + .hasMessage("The task was cancelled"); + }); + } + + @Test + public void testSubmitJob_finishCallback_zeroTasks() { + SettableFuture future = SettableFuture.create(); + + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(0) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThat(future.get()).isNull(); + }); + } + private Job submitJob(DummyJobConfiguration configuration) { return submitJob(configuration, "test-job"); } @SneakyThrows private Job submitJob(DummyJobConfiguration configuration, String key) { + return submitJob(configuration, key, null); + } + + @SneakyThrows + private Job submitJob(DummyJobConfiguration configuration, String key, TbCallback callback) { return jobManager.submitJob(Job.builder() .tenantId(tenantId) .type(JobType.DUMMY) .key(key) .entityId(jobEntity.getId()) .configuration(configuration) - .build()).get(); + .build(), callback).get(); } private List getFailures(JobResult jobResult) { diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index f5607415fa..cf99669777 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -188,8 +188,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest rule.setTriggerType(triggerConfig.getTriggerType()); rule.setTriggerConfig(triggerConfig); - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(triggerConfig.getTriggerType()); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(triggerConfig.getTriggerType()); recipientsConfig.setTargets(DaoUtil.toUUIDs(targets)); rule.setRecipientsConfig(recipientsConfig); diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index e9e239ec04..fc42ae247c 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -24,8 +24,8 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpEntity; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.springframework.web.client.RestTemplate; import org.thingsboard.common.util.JacksonUtil; @@ -125,7 +125,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { private NotificationCenter notificationCenter; @Autowired private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; - @MockBean + @MockitoBean private FirebaseService firebaseService; private static final String TEST_MOBILE_TOKEN = "tenantFcmToken"; diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index 7175f0981d..94af26d3d7 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -21,13 +21,15 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; +import org.mockito.MockedStatic; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.util.Pair; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.SystemUtil; import org.thingsboard.server.cache.limits.RateLimitService; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -39,17 +41,19 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType; 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.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; 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.profile.AlarmCondition; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; -import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; -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.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; @@ -87,9 +91,6 @@ import org.thingsboard.server.common.data.notification.targets.platform.SystemAd import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.query.BooleanFilterPredicate; -import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.Authority; @@ -106,12 +107,11 @@ import org.thingsboard.server.service.system.DefaultSystemInfoService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; -import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -123,6 +123,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mockStatic; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.ASSIGNED; import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.UNASSIGNED; @@ -137,7 +138,7 @@ import static org.thingsboard.server.common.data.notification.rule.trigger.confi }) public class NotificationRuleApiTest extends AbstractNotificationApiTest { - @SpyBean + @MockitoSpyBean private AlarmSubscriptionService alarmSubscriptionService; @Autowired private DefaultSystemInfoService systemInfoService; @@ -193,7 +194,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRuleProcessing_alarmTrigger() throws Exception { String notificationSubject = "Alarm type: ${alarmType}, status: ${alarmStatus}, " + - "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}"; + "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${details.data}."; String notificationText = "Status: ${alarmStatus}, severity: ${alarmSeverity}"; NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); @@ -210,7 +211,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRule.setTriggerConfig(triggerConfig); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); Map> escalationTable = new HashMap<>(); recipientsConfig.setEscalationTable(escalationTable); Map clients = new HashMap<>(); @@ -221,12 +221,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { clients.put(delay, userAndClient.getSecond()); } notificationRule.setRecipientsConfig(recipientsConfig); - notificationRule = saveNotificationRule(notificationRule); + saveNotificationRule(notificationRule); String alarmType = "myBoolIsTrue"; DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType); - Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); + Device device = createDevice("Device 1", deviceProfile.getName(), "label", "1234"); clients.values().forEach(wsClient -> { wsClient.subscribeForUnreadNotifications(10).waitForReply(true); @@ -234,8 +234,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { }); JsonNode attr = JacksonUtil.newObjectNode() - .set("bool", BooleanNode.TRUE); - doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + .set("createAlarm", BooleanNode.TRUE); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString()); await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); @@ -250,7 +250,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(actualDelay).isCloseTo(expectedDelay, offset(2.0)); assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " + - "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId()); + "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true."); assertThat(notification.getText()).isEqualTo("Status: " + AlarmStatus.ACTIVE_UNACK + ", severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase()); assertThat(notification.getType()).isEqualTo(NotificationType.ALARM); @@ -270,7 +270,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { wsClient.waitForUpdate(true); Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate(); assertThat(updatedNotification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + expectedStatus + ", " + - "severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId()); + "severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true."); assertThat(updatedNotification.getText()).isEqualTo("Status: " + expectedStatus + ", severity: " + expectedSeverity.toString().toLowerCase()); wsClient.close(); @@ -328,7 +328,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRule.setTriggerConfig(triggerConfig); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); Map> escalationTable = new HashMap<>(); recipientsConfig.setEscalationTable(escalationTable); @@ -341,8 +340,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); getWsClient().registerWaitForUpdate(); JsonNode attr = JacksonUtil.newObjectNode() - .set("bool", BooleanNode.TRUE); - doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + .set("createAlarm", BooleanNode.TRUE); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString()); await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); @@ -516,10 +515,10 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { .notifyOn(Set.of(ASSIGNED, UNASSIGNED)) .build(); NotificationTarget target = createNotificationTarget(tenantAdminUserId); - String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}'. Assignee: ${assigneeEmail}"; + String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}' with label '${alarmOriginatorLabel}'. Assignee: ${assigneeEmail}"; createNotificationRule(triggerConfig, "Test", template, target.getId()); - Device device = createDevice("Device A", "123"); + Device device = createDevice("Device A", "default", "test", "123"); Alarm alarm = Alarm.builder() .tenantId(tenantId) .originator(device.getId()) @@ -536,7 +535,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { doPost("/api/alarm/" + alarmId + "/assign/" + tenantAdminUserId).andExpect(status().isOk()); }, notification -> { assertThat(notification.getText()).isEqualTo( - TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL + TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A' with label 'test'. Assignee: " + TENANT_ADMIN_EMAIL ); }); @@ -544,7 +543,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { doDelete("/api/alarm/" + alarmId + "/assign").andExpect(status().isOk()); }, notification -> { assertThat(notification.getText()).isEqualTo( - TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A' with label 'test'. Assignee: " ); }); } @@ -639,8 +638,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); rule.setTriggerConfig(triggerConfig); - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); rule.setRecipientsConfig(recipientsConfig); rule = saveNotificationRule(rule); @@ -670,8 +668,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { triggerConfig.setCreated(true); rule.setTriggerConfig(triggerConfig); NotificationTarget target = createNotificationTarget(tenantAdminUserId); - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); recipientsConfig.setTargets(List.of(target.getUuidId())); rule.setRecipientsConfig(recipientsConfig); rule = saveNotificationRule(rule); @@ -799,9 +796,14 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId()); loginTenantAdmin(); - Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo"); - method.setAccessible(true); - method.invoke(systemInfoService); + // Mock SystemUtil to return 15% memory usage (exceeds 1% threshold) + try (MockedStatic mockedSystemUtil = mockStatic(SystemUtil.class)) { + mockedSystemUtil.when(SystemUtil::getMemoryUsage).thenReturn(Optional.of(15)); + + Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo"); + method.setAccessible(true); + method.invoke(systemInfoService); + } await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1); Notification notification = getMyNotifications(false, 100).get(0); @@ -850,15 +852,23 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { loginSysAdmin(); NotificationTarget sysadmins = createNotificationTarget(new SystemAdministratorsFilter()); ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder() - .ramThreshold(1f) - .cpuThreshold(1f) - .storageThreshold(1f) + .ramThreshold(0.99f) + .cpuThreshold(0.99f) + .storageThreshold(0.99f) .build(); NotificationRule rule = createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", sysadmins.getId()); - Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo"); - method.setAccessible(true); - method.invoke(systemInfoService); + // Mock SystemUtil to return 15% usages (not exceeds 99% threshold) + Method method; + try (MockedStatic mockedSystemUtil = mockStatic(SystemUtil.class)) { + mockedSystemUtil.when(SystemUtil::getMemoryUsage).thenReturn(Optional.of(15)); + mockedSystemUtil.when(SystemUtil::getCpuUsage).thenReturn(Optional.of(15)); + mockedSystemUtil.when(SystemUtil::getDiscSpaceUsage).thenReturn(Optional.of(15)); + + method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo"); + method.setAccessible(true); + method.invoke(systemInfoService); + } TimeUnit.SECONDS.sleep(5); assertThat(getMyNotifications(false, 100)).size().isZero(); @@ -942,35 +952,28 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); deviceProfile.setTenantId(tenantId); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); - List alarms = new ArrayList<>(); - DeviceProfileAlarm alarm = new DeviceProfileAlarm(); - alarm.setAlarmType(alarmType); - alarm.setId(alarmType); + CalculatedField alarmCf = new CalculatedField(); + alarmCf.setType(CalculatedFieldType.ALARM); + alarmCf.setEntityId(deviceProfile.getId()); + alarmCf.setName(alarmType); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("createAlarm", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + configuration.setArguments(Map.of("createAlarm", argument)); AlarmRule alarmRule = new AlarmRule(); - alarmRule.setAlarmDetails("Details"); - AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setSpec(new SimpleAlarmConditionSpec()); - List condition = new ArrayList<>(); - - AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); - alarmConditionFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "bool")); - BooleanFilterPredicate predicate = new BooleanFilterPredicate(); - predicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - predicate.setValue(new FilterPredicateValue<>(true)); - - alarmConditionFilter.setPredicate(predicate); - alarmConditionFilter.setValueType(EntityKeyValueType.BOOLEAN); - condition.add(alarmConditionFilter); - alarmCondition.setCondition(condition); - alarmRule.setCondition(alarmCondition); - TreeMap createRules = new TreeMap<>(); - createRules.put(AlarmSeverity.CRITICAL, alarmRule); - alarm.setCreateRules(createRules); - alarms.add(alarm); - - deviceProfile.getProfileData().setAlarms(alarms); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + alarmRule.setAlarmDetails("attribute is ${createAlarm}"); + SimpleAlarmCondition condition = new SimpleAlarmCondition(); + TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); + expression.setExpression("return createAlarm == true;"); + condition.setExpression(expression); + alarmRule.setCondition(condition); + configuration.setCreateRules(Map.of( + AlarmSeverity.CRITICAL, alarmRule + )); + alarmCf.setConfiguration(configuration); + saveCalculatedField(alarmCf); return deviceProfile; } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java index 3ea827d330..8d349bb858 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.service.queue; -import lombok.extern.slf4j.Slf4j; -import org.junit.After; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; @@ -35,30 +38,241 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@Slf4j -@RunWith(MockitoJUnitRunner.class) -public class TbMsgPackProcessingContextTest { +@ExtendWith(MockitoExtension.class) +class TbMsgPackProcessingContextTest { + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + + @Mock + TbRuleEngineSubmitStrategy submitStrategy; + @Mock + TbProtoQueueMsg mockMsg; + + ConcurrentMap> pendingMap; - public static final int TIMEOUT = 10; ExecutorService executorService; - @After - public void tearDown() { + @BeforeEach + void setup() { + pendingMap = new ConcurrentHashMap<>(); + lenient().when(submitStrategy.getPendingMap()).thenReturn(pendingMap); + } + + @AfterEach + void tearDown() { if (executorService != null) { - executorService.shutdownNow(); + MoreExecutors.shutdownAndAwaitTermination(executorService, 5, TimeUnit.SECONDS); } } @Test - public void testHighConcurrencyCase() throws InterruptedException { - //log.warn("preparing the test..."); + void testAwait_shouldReturnTrue_whenOnSuccessIsCalledBeforeTimeout() throws InterruptedException { + // GIVEN - a context with one pending message + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId = UUID.randomUUID(); + pendingMap.put(msgId, mockMsg); + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - onSuccess() is called in another thread before timeout + executorService.submit(() -> { + try { + Thread.sleep(100); + context.onSuccess(msgId); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should return true (successful completion) + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true when latch is counted down before timeout").isTrue(); + + // Verify the message was moved to success map + assertThat(context.getSuccessMap()).containsKey(msgId); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).isEmpty(); + + + // Verify submit strategy was notified about successful message processing + then(submitStrategy).should().onSuccess(msgId); + } + + @Test + void testAwait_shouldReturnTrue_whenOnFailureIsCalledBeforeTimeout() throws InterruptedException { + // GIVEN - a context with one pending message + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId = UUID.randomUUID(); + pendingMap.put(msgId, mockMsg); + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + var exception = new RuleEngineException("Test exception"); + + // WHEN - onFailure() is called in another thread before timeout + executorService.submit(() -> { + try { + Thread.sleep(100); + context.onFailure(tenantId, msgId, exception); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should return true (successful completion, even if message processing failed) + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true when latch is counted down before timeout").isTrue(); + + // Verify the exception was added to exceptions map + assertThat(context.getSuccessMap()).isEmpty(); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).containsEntry(tenantId, exception); + } + + @Test + void testAwait_shouldReturnFalse_whenTimeoutOccurs() throws InterruptedException { + // GIVEN - a context with one pending message and no processing + UUID msgId = UUID.randomUUID(); + pendingMap.put(msgId, mockMsg); + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - await() is called with short timeout and no message processing happens + long startTime = System.nanoTime(); + boolean result = context.await(100, TimeUnit.MILLISECONDS); + long elapsedTime = System.nanoTime() - startTime; + + // THEN - await() should return false (timeout occurred) + assertThat(result).as("await() should return false when timeout occurs").isFalse(); + assertThat(elapsedTime).as("await() should wait for at least the timeout duration").isGreaterThanOrEqualTo(100L); + + // Message should still be in pending map + assertThat(context.getSuccessMap()).isEmpty(); + assertThat(context.getPendingMap()).containsKey(msgId); + assertThat(context.getExceptionsMap()).isEmpty(); + } + + @Test + void testAwait_shouldHandleMultiplePendingMessages() throws InterruptedException { + // GIVEN - a context with multiple pending messages + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId1 = UUID.randomUUID(); + UUID msgId2 = UUID.randomUUID(); + UUID msgId3 = UUID.randomUUID(); + + pendingMap.put(msgId1, mockMsg); + pendingMap.put(msgId2, mockMsg); + pendingMap.put(msgId3, mockMsg); + + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - messages are processed one by one + executorService.submit(() -> { + try { + Thread.sleep(50); + context.onSuccess(msgId1); + Thread.sleep(50); + context.onSuccess(msgId2); + Thread.sleep(50); + context.onSuccess(msgId3); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should return true only after all messages are processed + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true after all messages are processed").isTrue(); + + // All messages should be in success map + assertThat(context.getSuccessMap()).containsKeys(msgId1, msgId2, msgId3); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).isEmpty(); + } + + @Test + void testAwait_shouldNotCountDownPrematurely_withMultipleMessages() throws InterruptedException { + // GIVEN - a context with multiple pending messages + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId1 = UUID.randomUUID(); + UUID msgId2 = UUID.randomUUID(); + + pendingMap.put(msgId1, mockMsg); + pendingMap.put(msgId2, mockMsg); + + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - only one message is processed + executorService.submit(() -> { + try { + Thread.sleep(100); + context.onSuccess(msgId1); + // msgId2 still in processing + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN: await should timeout because not all messages were processed + boolean result = context.await(2000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should timeout when not all messages are processed").isFalse(); + + // One message in success, one still pending + assertThat(context.getSuccessMap()).containsOnlyKeys(msgId1); + assertThat(context.getPendingMap()).containsOnlyKeys(msgId2); + assertThat(context.getExceptionsMap()).isEmpty(); + } + + @Test + void testAwait_shouldHandleMixedSuccessAndFailure() throws InterruptedException { + // GIVEN - multiple messages + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId1 = UUID.randomUUID(); + UUID msgId2 = UUID.randomUUID(); + + pendingMap.put(msgId1, mockMsg); + pendingMap.put(msgId2, mockMsg); + + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + var exception = new RuleEngineException("Test exception"); + + // WHEN - one succeeds, one fails + executorService.submit(() -> { + try { + Thread.sleep(50); + context.onSuccess(msgId1); + Thread.sleep(50); + context.onFailure(tenantId, msgId2, exception); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should complete successfully + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true when all messages are processed").isTrue(); + + assertThat(context.getSuccessMap()).containsOnlyKeys(msgId1); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).containsEntry(tenantId, exception); + } + + @Test + void testHighConcurrencyCase() throws InterruptedException { int msgCount = 1000; int parallelCount = 5; executorService = Executors.newFixedThreadPool(parallelCount, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope")); @@ -76,28 +290,24 @@ public class TbMsgPackProcessingContextTest { final CountDownLatch startLatch = new CountDownLatch(1); final CountDownLatch finishLatch = new CountDownLatch(parallelCount); for (int i = 0; i < parallelCount; i++) { - //final String taskName = "" + uuid + " " + i; executorService.submit(() -> { - //log.warn("ready {}", taskName); readyLatch.countDown(); try { startLatch.await(); } catch (InterruptedException e) { - Assert.fail("failed to await"); + fail("failed to await"); } - //log.warn("go {}", taskName); - context.onSuccess(uuid); - finishLatch.countDown(); }); } - assertTrue(readyLatch.await(TIMEOUT, TimeUnit.SECONDS)); + assertTrue(readyLatch.await(10, TimeUnit.SECONDS)); Thread.yield(); startLatch.countDown(); //run all-at-once submitted tasks - assertTrue(finishLatch.await(TIMEOUT, TimeUnit.SECONDS)); + assertTrue(finishLatch.await(10, TimeUnit.SECONDS)); } - assertTrue(context.await(TIMEOUT, TimeUnit.SECONDS)); + assertTrue(context.await(10, TimeUnit.SECONDS)); verify(strategyMock, times(msgCount)).onSuccess(any(UUID.class)); } + } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java new file mode 100644 index 0000000000..20ea6fa881 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java @@ -0,0 +1,281 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.queue.ruleengine; + +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +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.common.stats.StatsFactory; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.DefaultTbQueueMsgHeaders; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.memory.DefaultInMemoryStorage; +import org.thingsboard.server.queue.memory.InMemoryStorage; +import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; +import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; +import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; +import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; +import org.thingsboard.server.service.stats.RuleEngineStatisticsService; + +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RuleEngineConsumerLoopTest { + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + InMemoryStorage storage; + + @Mock + ActorSystemContext actorContext; + @Mock + StatsFactory statsFactory; + @Mock + TbRuleEngineQueueFactory queueFactory; + @Mock + RuleEngineStatisticsService statisticsService; + @Mock + TbServiceInfoProvider serviceInfoProvider; + @Mock + PartitionService partitionService; + @Mock + TbQueueProducerProvider producerProvider; + @Mock + TbQueueAdmin queueAdmin; + @Mock + TbMsgPackProcessingContextFactory packProcessingContextFactory; + @Mock + TbMsgPackProcessingContext packCtx; + + Queue mainQueue; + + TbQueueConsumer> consumer; + + TbRuleEngineConsumerContext ruleEngineConsumerContext; + TbRuleEngineQueueConsumerManager consumerManager; + + ExecutorService consumersExecutor; + ScheduledExecutorService scheduler; + ExecutorService mgmtExecutor; + + @BeforeEach + void setup() throws InterruptedException { + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer")); + scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("tb-rule-engine-consumer-scheduler"); + mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(1, "tb-rule-engine-mgmt"); + + mainQueue = new Queue(); + mainQueue.setTenantId(TenantId.SYS_TENANT_ID); + mainQueue.setName("Main"); + mainQueue.setTopic("tb_rule_engine.main"); + mainQueue.setPollInterval(25); + mainQueue.setPartitions(1); + mainQueue.setConsumerPerPartition(false); + mainQueue.setPackProcessingTimeout(2000L); + + var submitStrategy = new SubmitStrategy(); + submitStrategy.setType(SubmitStrategyType.BURST); + submitStrategy.setBatchSize(1000); + mainQueue.setSubmitStrategy(submitStrategy); + + var processingStrategy = new ProcessingStrategy(); + processingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + processingStrategy.setRetries(3); + processingStrategy.setFailurePercentage(0.0); + processingStrategy.setPauseBetweenRetries(3); + processingStrategy.setMaxPauseBetweenRetries(3); + mainQueue.setProcessingStrategy(processingStrategy); + + storage = new DefaultInMemoryStorage(); + + consumer = spy(new InMemoryTbQueueConsumer<>(storage, mainQueue.getTopic())); + given(queueFactory.createToRuleEngineMsgConsumer(eq(mainQueue), isNull())).willReturn(consumer); + + ruleEngineConsumerContext = new TbRuleEngineConsumerContext( + actorContext, statsFactory, new TbRuleEngineSubmitStrategyFactory(), new TbRuleEngineProcessingStrategyFactory(), + queueFactory, statisticsService, serviceInfoProvider, partitionService, producerProvider, queueAdmin + ); + ruleEngineConsumerContext.setPollDuration(25); + ruleEngineConsumerContext.setPackProcessingTimeout(2000); + ruleEngineConsumerContext.setStatsEnabled(false); // true by default + ruleEngineConsumerContext.setPrometheusStatsEnabled(false); + ruleEngineConsumerContext.setTopicDeletionDelayInSec(15); + ruleEngineConsumerContext.setMgmtThreadPoolSize(12); + + // Tell the (mock) context factory to return (mock) message pack context + given(packProcessingContextFactory.create( + eq(mainQueue.getName()), + any(TbRuleEngineSubmitStrategy.class), + eq(false) + )).willAnswer(invocation -> { + TbRuleEngineSubmitStrategy realStrategy = invocation.getArgument(1); + when(packCtx.getPendingMap()).thenAnswer(i -> realStrategy.getPendingMap()); + when(packCtx.getFailedMap()).thenReturn(new ConcurrentHashMap<>()); + return packCtx; + }); + + // Tell the (mock) context's await() to return 'false' (always timeout) immediately + given(packCtx.await(anyLong(), any(TimeUnit.class))).willReturn(false); + + consumerManager = TbRuleEngineQueueConsumerManager.create() + .ctx(ruleEngineConsumerContext) + .queueKey(new QueueKey(ServiceType.TB_RULE_ENGINE, mainQueue)) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .packProcessingContextFactory(packProcessingContextFactory) + .build(); + } + + @AfterEach + void destroy() { + MoreExecutors.shutdownAndAwaitTermination(scheduler, Duration.ofSeconds(30)); + MoreExecutors.shutdownAndAwaitTermination(mgmtExecutor, Duration.ofSeconds(30)); + MoreExecutors.shutdownAndAwaitTermination(consumersExecutor, Duration.ofSeconds(30)); + } + + @Test + void consumerLoopTest_verifyOperationsOrder() throws InterruptedException { + // Create partition + var partition = TopicPartitionInfo.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .topic(mainQueue.getTopic()) + .partition(0) + .myPartition(true) + .useInternalPartition(false) + .build(); + + // Put 10k messages to the queue + for (int i = 0; i < 10_000; i++) { + var tbMsg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(deviceId) + .data("{\"temperature\":123}") + .metaData(TbMsgMetaData.EMPTY) + .build(); + + var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder() + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTbMsgProto(TbMsg.toProto(tbMsg)) + .addAllRelationTypes(Set.of("Success")) + .build(); + + storage.put(partition.getFullTopicName(), new TbProtoQueueMsg<>(UUID.randomUUID(), toRuleEngineMsg, new DefaultTbQueueMsgHeaders())); + } + + // Count how many polls were made + var totalPolls = new AtomicInteger(0); + var emptyPolls = new AtomicInteger(0); + doAnswer(invocation -> { + totalPolls.incrementAndGet(); + @SuppressWarnings("unchecked") + var messages = (List) invocation.callRealMethod(); + if (messages.isEmpty()) { + emptyPolls.incrementAndGet(); + } + return messages; + }).when(consumer).poll(mainQueue.getPollInterval()); + + // Count how many commits were made + var totalCommits = new AtomicInteger(0); + doAnswer(invocation -> { + totalCommits.incrementAndGet(); + return invocation.callRealMethod(); + }).when(consumer).commit(); + + // Initialize consumer + consumerManager.init(mainQueue); + + // Assign partition to the consumer + consumerManager.update(Set.of(partition)); + + // Give some time for the consumer to get all messages + await().atMost(Duration.ofSeconds(10L)).until(() -> storage.getLagTotal() == 0); + + // Stop consumer + consumerManager.stop(); + consumerManager.awaitStop(); + + // Determine number of non-empty consumer iterations made, since polling does not stop immediately after consuming all messages and may do a few empty polls + int nonEmptyPolls = totalPolls.get() - emptyPolls.get(); + + // Verify that there is 10 polls and 10 matching commits + // Each poll consumes 1k messages and queue has 10k total, so that means 10k total msgs / 1k msgs per poll = 10 polls + assertThat(nonEmptyPolls).isEqualTo(10).isEqualTo(totalCommits.get()); + + // Verify that poll-await-commit cycle happened in order with correct await timeout + InOrder inOrder = inOrder(consumer, packCtx); + for (int i = 0; i < nonEmptyPolls; i++) { + inOrder.verify(consumer).poll(mainQueue.getPollInterval()); + inOrder.verify(packCtx).await(mainQueue.getPackProcessingTimeout(), TimeUnit.MILLISECONDS); + inOrder.verify(consumer).commit(); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 9c371ee5fc..e50698200b 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -59,6 +59,7 @@ import org.thingsboard.server.queue.provider.KafkaMonolithQueueFactory; import org.thingsboard.server.queue.provider.KafkaTbRuleEngineQueueFactory; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; import org.thingsboard.server.service.stats.RuleEngineStatisticsService; @@ -194,6 +195,7 @@ public class TbRuleEngineQueueConsumerManagerTest { .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) + .packProcessingContextFactory(new TbMsgPackProcessingContextFactory.DefaultTbMsgPackProcessingContextFactory()) .build(); } @@ -595,7 +597,7 @@ public class TbRuleEngineQueueConsumerManagerTest { await().atMost(5, TimeUnit.SECONDS).until(() -> { for (TopicPartitionInfo partition : expectedPartitions) { if (consumers.stream().noneMatch(consumer -> consumer.subscribed && - consumer.pollingStarted && Set.of(partition).equals(consumer.getPartitions()))) { + consumer.pollingStarted && Set.of(partition).equals(consumer.getPartitions()))) { return false; } } @@ -605,7 +607,7 @@ public class TbRuleEngineQueueConsumerManagerTest { await().atMost(5, TimeUnit.SECONDS).until(() -> { return consumers.size() == 1 && consumers.stream() .anyMatch(consumer -> consumer.subscribed && consumer.pollingStarted && - expectedPartitions.equals(consumer.getPartitions())); + expectedPartitions.equals(consumer.getPartitions())); }); } Mockito.reset(ruleEngineConsumerContext.getSubmitStrategyFactory()); @@ -667,8 +669,8 @@ public class TbRuleEngineQueueConsumerManagerTest { return await().atMost(5, TimeUnit.SECONDS) .until(() -> consumers.stream() .filter(consumer -> consumer.getPartitions() != null && - consumer.getPartitions().size() == 1 && - consumer.getPartitions().contains(tpi)) + consumer.getPartitions().size() == 1 && + consumer.getPartitions().contains(tpi)) .findFirst().orElse(null), Objects::nonNull); } @@ -676,9 +678,9 @@ public class TbRuleEngineQueueConsumerManagerTest { return await().atMost(5, TimeUnit.SECONDS) .until(() -> consumers.stream() .filter(consumer -> consumer.getPartitions() != null && - consumer.getPartitions().size() == 1 && - consumer.getPartitions().stream() - .anyMatch(tpi -> tpi.getPartition().get().equals(partition))) + consumer.getPartitions().size() == 1 && + consumer.getPartitions().stream() + .anyMatch(tpi -> tpi.getPartition().get().equals(partition))) .findFirst().orElse(null), Objects::nonNull); } @@ -778,10 +780,6 @@ public class TbRuleEngineQueueConsumerManagerTest { return false; } - public Set getPartitions() { - return partitions; - } - public void setUpTestMsg() { testMsg = TbMsg.newMsg() .type(TbMsgType.POST_TELEMETRY_REQUEST) @@ -790,6 +788,7 @@ public class TbRuleEngineQueueConsumerManagerTest { .data("{}") .build(); } + } } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java index cdbc6d611f..ca47af7b19 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java @@ -43,7 +43,9 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; @@ -191,9 +193,11 @@ public class TbRuleEngineStrategyTest { queue.setProcessingStrategy(processingStrategy); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); + ConsumerKey consumerKey = new ConsumerKey(queueKey, null); var consumerManager = TbRuleEngineQueueConsumerManager.create() .ctx(ruleEngineConsumerContext) .queueKey(queueKey) + .packProcessingContextFactory(new TbMsgPackProcessingContextFactory.DefaultTbMsgPackProcessingContextFactory()) .build(); consumerManager.init(queue); @@ -238,7 +242,7 @@ public class TbRuleEngineStrategyTest { .map(this::toProto) .toList(); - consumerManager.processMsgs(protoMsgs, consumer, queueKey, queue); + consumerManager.processMsgs(protoMsgs, consumer, consumerKey, queue); processingData.forEach(data -> { verify(actorContext, times(data.attempts)).tell(argThat(msg -> diff --git a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java index c227ec76aa..d33f1755d5 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.resource; +import org.awaitility.Awaitility; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; @@ -29,6 +30,8 @@ import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.concurrent.TimeUnit; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.timeout; @@ -61,6 +64,8 @@ public class DefaultResourceDataCacheTest extends AbstractControllerTest { TbResourceInfo savedResource = tbResourceService.save(resource); verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId()); + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get()).isNotNull()); TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); assertThat(cachedData.getData()).isEqualTo(data); assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor); @@ -76,8 +81,8 @@ public class DefaultResourceDataCacheTest extends AbstractControllerTest { TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId()); tbResourceService.delete(resourceById, true, null); verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId()); - TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); - assertThat(cachedDataAfterDeletion).isEqualTo(null); + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get()).isNull()); } } diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index ed9d9df095..0875bd94ee 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -60,7 +60,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 9a3421b219..a556593800 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -2356,7 +2356,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { expected.put("date_1", d1.toString()); TbDate d2 = new TbDate(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); expected.put("dLocal_2", d2.toLocaleString()); - expected.put("dLocal_2_us", "8/5/23, 10:04:05 PM"); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + expected.put("dLocal_2_us", Runtime.version().feature() >= 21 ? "8/5/23, 10:04:05\u202FPM" : "8/5/23, 10:04:05 PM"); expected.put("dIso_2", d2.toISOString()); expected.put("date_2", d2.toString()); Object actual = invokeScript(evalScript(decoderStr), msgStr); @@ -2388,7 +2389,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { """, s2); LinkedHashMap expected = new LinkedHashMap<>(); TbDate d1 = new TbDate(2023, 8, 6, 4, 4, 5); - expected.put("dLocal_1_us", "8/6/23, 4:04:05 AM"); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + expected.put("dLocal_1_us", Runtime.version().feature() >= 21 ? "8/6/23, 4:04:05\u202FAM" : "8/6/23, 4:04:05 AM"); expected.put("dIso_1", d1.toISOString()); expected.put("d1", d1.toString()); TbDate d2 = new TbDate(s2); @@ -2420,7 +2422,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { TbDate d = new TbDate(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); expected.put("dIso", d.toISOString()); expected.put("dLocal_utc", d.toLocaleString("UTC")); - expected.put("dLocal_us", "8/5/23, 10:04:05 PM"); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + expected.put("dLocal_us", Runtime.version().feature() >= 21 ? "8/5/23, 10:04:05\u202FPM" : "8/5/23, 10:04:05 PM"); expected.put("dLocal_de", "06.08.23, 04:04:05"); Object actual = invokeScript(evalScript(decoderStr), msgStr); assertEquals(expected, actual); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index cb7802034e..950ce4609f 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -60,7 +60,7 @@ public class JwtTokenFactoryTest { public void beforeEach() { jwtSettings = new JwtSettings(); jwtSettings.setTokenIssuer("tb"); - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.secure().nextAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); @@ -89,7 +89,7 @@ public class JwtTokenFactoryTest { AccessJwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); checkExpirationTime(accessToken, jwtSettings.getTokenExpirationTime()); - SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(accessToken.getToken()); + SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(accessToken.token()); assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); assertThat(parsedSecurityUser.getEmail()).isEqualTo(securityUser.getEmail()); assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> { @@ -112,7 +112,7 @@ public class JwtTokenFactoryTest { JwtToken refreshToken = tokenFactory.createRefreshToken(securityUser); checkExpirationTime(refreshToken, jwtSettings.getRefreshTokenExpTime()); - SecurityUser parsedSecurityUser = tokenFactory.parseRefreshToken(refreshToken.getToken()); + SecurityUser parsedSecurityUser = tokenFactory.parseRefreshToken(refreshToken.token()); assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> { return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType()) @@ -125,10 +125,10 @@ public class JwtTokenFactoryTest { public void testCreateAndParsePreVerificationJwtToken() { SecurityUser securityUser = createSecurityUser(); int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30); - JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime); + JwtToken preVerificationToken = tokenFactory.createMfaToken(securityUser, Authority.PRE_VERIFICATION_TOKEN, tokenLifetime); checkExpirationTime(preVerificationToken, tokenLifetime); - SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.getToken()); + SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.token()); assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); assertThat(parsedSecurityUser.getAuthority()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId()); @@ -144,7 +144,7 @@ public class JwtTokenFactoryTest { SecurityUser securityUser = createSecurityUser(); String sessionId = securityUser.getSessionId(); - String accessToken = tokenFactory.createAccessJwtToken(securityUser).getToken(); + String accessToken = tokenFactory.createAccessJwtToken(securityUser).token(); securityUser = tokenFactory.parseAccessJwtToken(accessToken); assertThat(securityUser.getSessionId()).isNotNull().isEqualTo(sessionId); @@ -158,7 +158,7 @@ public class JwtTokenFactoryTest { securityUser.setId(new UserId(UUID.randomUUID())); securityUser.setEmail("tenant@thingsboard.org"); securityUser.setAuthority(Authority.TENANT_ADMIN); - securityUser.setTenantId(new TenantId(UUID.randomUUID())); + securityUser.setTenantId(TenantId.fromUUID(UUID.randomUUID())); securityUser.setEnabled(true); securityUser.setFirstName("A"); securityUser.setLastName("B"); @@ -179,7 +179,7 @@ public class JwtTokenFactoryTest { } private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { - Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getPayload(); + Claims claims = tokenFactory.parseTokenClaims(jwtToken.token()).getPayload(); assertThat(claims.getExpiration()).matches(actualExpirationTime -> { Calendar expirationTime = Calendar.getInstance(); expirationTime.setTime(new Date()); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index 519c04b391..e54e36c33f 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -30,15 +30,14 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; @@ -46,6 +45,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; +import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; import java.util.UUID; @@ -91,20 +91,16 @@ public class TokenOutdatingTest { UserId userId = new UserId(UUID.randomUUID()); securityUser = createMockSecurityUser(userId); - UserService userService = mock(UserService.class); + UserAuthDetailsCache userAuthDetailsCache = mock(UserAuthDetailsCache.class); User user = new User(); user.setId(userId); user.setAuthority(Authority.TENANT_ADMIN); user.setEmail("email"); - when(userService.findUserById(any(), eq(userId))).thenReturn(user); - - UserCredentials userCredentials = new UserCredentials(); - userCredentials.setEnabled(true); - when(userService.findUserCredentialsByUserId(any(), eq(userId))).thenReturn(userCredentials); + when(userAuthDetailsCache.getUserAuthDetails(any(), eq(userId))).thenReturn(new UserAuthDetails(user, true)); accessTokenAuthenticationProvider = new JwtAuthenticationProvider(tokenFactory, tokenOutdatingService); - refreshTokenAuthenticationProvider = new RefreshTokenAuthenticationProvider(tokenFactory, userService, mock(CustomerService.class), tokenOutdatingService); + refreshTokenAuthenticationProvider = new RefreshTokenAuthenticationProvider(tokenFactory, userAuthDetailsCache, mock(CustomerService.class), tokenOutdatingService); } @Test @@ -114,12 +110,12 @@ public class TokenOutdatingTest { // Token outdatage time is rounded to 1 sec. Need to wait before outdating so that outdatage time is strictly after token issue time SECONDS.sleep(1); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); - assertTrue(tokenOutdatingService.isOutdated(jwtToken.getToken(), securityUser.getId())); + assertTrue(tokenOutdatingService.isOutdated(jwtToken.token(), securityUser.getId())); SECONDS.sleep(1); JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser); - assertFalse(tokenOutdatingService.isOutdated(newJwtToken.getToken(), securityUser.getId())); + assertFalse(tokenOutdatingService.isOutdated(newJwtToken.token(), securityUser.getId())); } @Test @@ -229,7 +225,7 @@ public class TokenOutdatingTest { private RawAccessJwtToken getRawJwtToken(JwtToken token) { - return new RawAccessJwtToken(token.getToken()); + return new RawAccessJwtToken(token.token()); } private SecurityUser createMockSecurityUser(UserId userId) { @@ -241,4 +237,5 @@ public class TokenOutdatingTest { securityUser.setSessionId(UUID.randomUUID().toString()); return securityUser; } + } diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java new file mode 100644 index 0000000000..e9c36b1ebd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java @@ -0,0 +1,174 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +@DaoSqlTest +public class ApiKeyAuthenticationProviderTest extends AbstractControllerTest { + + ApiKey savedApiKey; + + @Before + public void setUp() throws Exception { + loginTenantAdmin(); + + ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); + savedApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(savedApiKey.getValue()); + } + + @After + public void cleanUp() throws Exception { + resetApiKey(); + doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); + } + + @Test + public void testSaveEdgeWithApiKey() throws Exception { + Edge edge = constructEdge("My edge", "default"); + + Mockito.reset(tbClusterService, auditLogService); + + Edge savedEdge = doPostWithApiKey("/api/edge", edge, Edge.class); + + Assert.assertNotNull(savedEdge); + Assert.assertNotNull(savedEdge.getId()); + Assert.assertTrue(savedEdge.getCreatedTime() > 0); + Assert.assertEquals(tenantId, savedEdge.getTenantId()); + Assert.assertNotNull(savedEdge.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); + Assert.assertEquals(edge.getName(), savedEdge.getName()); + + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), + ActionType.ADDED, 2); + + savedEdge.setName("My new edge"); + doPostWithApiKey("/api/edge", savedEdge, Edge.class); + + Edge foundEdge = doGetWithApiKey("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); + + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), + ActionType.UPDATED, 1); + + doDeleteWithApiKey("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testUnauthorizedWhenKeyDisabled() throws Exception { + ApiKeyInfo disabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/false", Boolean.FALSE, ApiKeyInfo.class); + Assert.assertFalse(disabledApiKeyInfo.isEnabled()); + doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); + } + + @Test + public void testUnauthorizedWhenKeyExpired() throws Exception { + ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); + apiKeyInfo.setExpirationTime(System.currentTimeMillis() - 1000); + ApiKey savedApiKeyWithBad = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(savedApiKeyWithBad.getValue()); + doPost("/api/apiKey", savedApiKey, ApiKeyInfo.class); + doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); + } + + @Test + public void testUnauthorizedWhenUserCredentialsDisabled() throws Exception { + User newUser = new User(); + newUser.setAuthority(Authority.TENANT_ADMIN); + newUser.setTenantId(tenantId); + newUser.setEmail("testUser" + RandomStringUtils.secure().nextAlphanumeric(10) + "@thingsboard.org"); + newUser.setFirstName("Test"); + newUser.setLastName("User"); + User savedUser = createUser(newUser, "testPassword1"); + + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("Test API key for user credentials test"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(savedUser.getId()); + ApiKey testApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(testApiKey.getValue()); + + doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isOk()); + + doPost("/api/user/" + savedUser.getId().getId() + "/userCredentialsEnabled?userCredentialsEnabled=false").andExpect(status().isOk()); + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isUnauthorized())); + + resetApiKey(); + doDelete("/api/apiKey/" + testApiKey.getId()).andExpect(status().isOk()); + loginSysAdmin(); + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + } + + @Test + public void testUnauthorizedWhenUserDeleted() throws Exception { + User newUser = new User(); + newUser.setAuthority(Authority.TENANT_ADMIN); + newUser.setTenantId(tenantId); + newUser.setEmail("testUser" + RandomStringUtils.secure().nextAlphanumeric(10) + "@thingsboard.org"); + newUser.setFirstName("Test"); + newUser.setLastName("User"); + User savedUser = createUser(newUser, "testPassword1"); + + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("Test API key for user deletion test"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(savedUser.getId()); + ApiKey testApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(testApiKey.getValue()); + + doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isOk()); + + loginSysAdmin(); + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isUnauthorized())); + } + + private ApiKeyInfo constructApiKeyInfo() { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("New API key description"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(tenantAdminUserId); + return apiKeyInfo; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/sms/DefaultSmsServiceTest.java b/application/src/test/java/org/thingsboard/server/service/sms/DefaultSmsServiceTest.java index 445be6363a..28ce4ea46a 100644 --- a/application/src/test/java/org/thingsboard/server/service/sms/DefaultSmsServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sms/DefaultSmsServiceTest.java @@ -52,6 +52,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @TestPropertySource(properties = { "usage.stats.report.enabled=true", "usage.stats.report.interval=1", + "usage.stats.report.urgent_interval=1" }) public class DefaultSmsServiceTest extends AbstractControllerTest { @SpyBean diff --git a/application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java b/application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java index 31283a1165..e207c2325c 100644 --- a/application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java @@ -41,6 +41,7 @@ import static org.awaitility.Awaitility.await; @TestPropertySource(properties = { "usage.stats.report.enabled=true", "usage.stats.report.interval=2", + "usage.stats.report.urgent_interval=1", "usage.stats.gauge_report_interval=1", "usage.stats.devices.report_interval=3", "state.defaultStateCheckIntervalInSec=3", diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index e19efdf7e0..9630dacdd4 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -48,10 +48,9 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; 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.profile.DefaultDeviceProfileConfiguration; @@ -627,9 +626,8 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 7496eaf945..994f326627 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.collect.Streams; +import org.apache.commons.lang3.tuple.Pair; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -45,17 +46,21 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; @@ -244,7 +249,7 @@ public class VersionControlTest extends AbstractControllerTest { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); - Device device = createDevice(null, deviceProfile.getId(), "Device v1.0", "test1", newDevice -> { + Device device = createDevice(deviceProfile.getId(), "Device v1.0", "test1", newDevice -> { newDevice.setFirmwareId(firmware.getId()); newDevice.setSoftwareId(software.getId()); }); @@ -267,7 +272,7 @@ public class VersionControlTest extends AbstractControllerTest { createVersion("profiles", EntityType.DEVICE_PROFILE); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); - Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { + Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { newDevice.setFirmwareId(firmware.getId()); newDevice.setSoftwareId(software.getId()); }); @@ -307,6 +312,54 @@ public class VersionControlTest extends AbstractControllerTest { checkImportedOtaPackageData(software, importedSoftwareOta); } + @Test + public void testDeviceVc_withAlarmRules_betweenTenants() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1"); + Dashboard dashboard = createDashboard(null, "Mobile dashboard"); + createAlarmRule(deviceProfile.getId(), "Profile alarm rule", dashboard.getId()); + Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1"); + createAlarmRule(device.getId(), "Device alarm rule", dashboard.getId()); + String version = createVersion("devices, profiles and dashboards", EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD); + + loginTenant2(); + Map result = loadVersion(version, config -> { + config.setLoadCredentials(false); + }, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD); + assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1); + assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1); + assertThat(result.get(EntityType.DASHBOARD).getCreated()).isEqualTo(1); + + Device importedDevice = findDevice(device.getName()); + checkImportedEntity(tenantId1, device, tenantId2, importedDevice); + checkImportedDeviceData(device, importedDevice); + + DeviceProfile importedDeviceProfile = findDeviceProfile(deviceProfile.getName()); + checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedDeviceProfile); + checkImportedDeviceProfileData(deviceProfile, importedDeviceProfile); + assertThat(importedDevice.getDeviceProfileId()).isEqualTo(importedDeviceProfile.getId()); + + Dashboard importedDashboard = findDashboard(dashboard.getName()); + checkImportedEntity(tenantId1, dashboard, tenantId2, importedDashboard); + checkImportedDashboardData(dashboard, importedDashboard); + + getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE, + List.of(importedDeviceProfile.getUuidId()), null).forEach(alarmRuleCf -> { + assertThat(alarmRuleCf.getName()).isEqualTo("Profile alarm rule"); + AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration(); + config.getAllRules().map(Pair::getValue).forEach(alarmRule -> { + assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId()); + }); + }); + getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE, + List.of(importedDevice.getUuidId()), null).forEach(alarmRuleCf -> { + assertThat(alarmRuleCf.getName()).isEqualTo("Device alarm rule"); + AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration(); + config.getAllRules().map(Pair::getValue).forEach(alarmRule -> { + assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId()); + }); + }); + } + @Test public void testDashboardVc_betweenTenants() throws Exception { Dashboard dashboard = createDashboard(null, "Dashboard of tenant 1"); @@ -369,42 +422,42 @@ public class VersionControlTest extends AbstractControllerTest { String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa"; String entityAliases = "{\n" + - "\"" + aliasId + "\": {\n" + - "\"alias\": \"assets\",\n" + - "\"filter\": {\n" + - " \"entityList\": [\n" + - " \"" + asset1.getId() + "\",\n" + - " \"" + asset2.getId() + "\",\n" + - " \"" + tenantId1.getId() + "\",\n" + - " \"" + existingDeviceProfile.getId() + "\",\n" + - " \"" + unknownUuid + "\"\n" + - " ],\n" + - " \"id\":\"" + asset1.getId() + "\",\n" + - " \"resolveMultiple\": true\n" + - "},\n" + - "\"id\": \"" + aliasId + "\"\n" + - "}\n" + - "}"; + "\"" + aliasId + "\": {\n" + + "\"alias\": \"assets\",\n" + + "\"filter\": {\n" + + " \"entityList\": [\n" + + " \"" + asset1.getId() + "\",\n" + + " \"" + asset2.getId() + "\",\n" + + " \"" + tenantId1.getId() + "\",\n" + + " \"" + existingDeviceProfile.getId() + "\",\n" + + " \"" + unknownUuid + "\"\n" + + " ],\n" + + " \"id\":\"" + asset1.getId() + "\",\n" + + " \"resolveMultiple\": true\n" + + "},\n" + + "\"id\": \"" + aliasId + "\"\n" + + "}\n" + + "}"; String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b"; String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae"; String widgets = "{\n" + - " \"" + widgetId + "\": {\n" + - " \"config\": {\n" + - " \"actions\": {\n" + - " \"rowClick\": [\n" + - " {\n" + - " \"name\": \"go to dashboard\",\n" + - " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + - " \"id\": \"" + actionId + "\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " \"row\": 0,\n" + - " \"col\": 0,\n" + - " \"id\": \"" + widgetId + "\"\n" + - " }\n" + - "}"; + " \"" + widgetId + "\": {\n" + + " \"config\": {\n" + + " \"actions\": {\n" + + " \"rowClick\": [\n" + + " {\n" + + " \"name\": \"go to dashboard\",\n" + + " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + + " \"id\": \"" + actionId + "\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"row\": 0,\n" + + " \"col\": 0,\n" + + " \"id\": \"" + widgetId + "\"\n" + + " }\n" + + "}"; ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); @@ -500,11 +553,12 @@ public class VersionControlTest extends AbstractControllerTest { generatorNodeConfig.setMsgCount(1); generatorNodeConfig.setScriptLang(ScriptLanguage.JS); UUID someUuid = UUID.randomUUID(); - generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" + - "var metadata = { data: 40 };\n" + - "var msgType = \"POST_TELEMETRY_REQUEST\";\n" + - "var someUuid = \"" + someUuid + "\";\n" + - "return { msg: msg, metadata: metadata, msgType: msgType };"); + generatorNodeConfig.setJsScript(""" + var msg = { temp: 42, humidity: 77 }; + var metadata = { data: 40 }; + var msgType = "POST_TELEMETRY_REQUEST"; + var someUuid = "%s"; + return { msg: msg, metadata: metadata, msgType: msgType };""".formatted(someUuid)); generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig)); nodes.add(generatorNode); metaData.setNodes(nodes); @@ -528,7 +582,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithRelations_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); EntityRelation relation = createRelation(asset.getId(), device.getId()); String versionId = createVersion("assets and devices", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -554,11 +608,11 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithRelations_sameTenant() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device1 = createDevice(null, null, "Device 1", "test1"); + Device device1 = createDevice("Device 1", "test1"); EntityRelation relation1 = createRelation(device1.getId(), asset.getId()); String versionId = createVersion("assets", EntityType.ASSET); - Device device2 = createDevice(null, null, "Device 2", "test2"); + Device device2 = createDevice("Device 2", "test2"); EntityRelation relation2 = createRelation(device2.getId(), asset.getId()); List relations = findRelationsByTo(asset.getId()); assertThat(relations).contains(relation1, relation2); @@ -591,7 +645,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -617,7 +671,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithReferencedCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); CalculatedField deviceCalculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); CalculatedField assetCalculatedField = createCalculatedField("CalculatedField2", asset.getId(), device.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -638,7 +692,9 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId()); - assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); + assertThat(importedField.getConfiguration()).isInstanceOf(SimpleCalculatedFieldConfiguration.class); + SimpleCalculatedFieldConfiguration simpleCfg = (SimpleCalculatedFieldConfiguration) importedField.getConfiguration(); + assertThat(simpleCfg.getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); }); List importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId()); @@ -647,7 +703,9 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId()); - assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); + assertThat(importedField.getConfiguration()).isInstanceOf(SimpleCalculatedFieldConfiguration.class); + SimpleCalculatedFieldConfiguration simpleCfg = (SimpleCalculatedFieldConfiguration) importedField.getConfiguration(); + assertThat(simpleCfg.getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); }); } @@ -884,7 +942,6 @@ public class VersionControlTest extends AbstractControllerTest { } return result.getResult().stream().collect(Collectors.toMap(EntityTypeLoadResult::getEntityType, r -> r)); } - private List listVersions() throws Exception { PageData versions = doGetAsyncTyped("/api/entities/vc/version?branch=" + branch + "&pageSize=100&page=0&sortProperty=timestamp&sortOrder=DESC", new TypeReference>() {}); return versions.getData(); @@ -907,9 +964,8 @@ public class VersionControlTest extends AbstractControllerTest { login(tenantAdmin2.getEmail(), tenantAdmin2.getEmail()); } - private Device createDevice(CustomerId customerId, DeviceProfileId deviceProfileId, String name, String accessToken, Consumer... modifiers) { + private Device createDevice(DeviceProfileId deviceProfileId, String name, String accessToken, Consumer... modifiers) { Device device = new Device(); - device.setCustomerId(customerId); device.setName(name); device.setLabel("lbl"); device.setDeviceProfileId(deviceProfileId); @@ -1016,20 +1072,21 @@ public class VersionControlTest extends AbstractControllerTest { protected Dashboard createDashboard(CustomerId customerId, String name, AssetId assetForEntityAlias) { Dashboard dashboard = createDashboard(customerId, name); - String entityAliases = "{\n" + - "\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + - "\t\t\"alias\": \"assets\",\n" + - "\t\t\"filter\": {\n" + - "\t\t\t\"entityList\": [\n" + - "\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" + - "\t\t\t],\n" + - "\t\t\t\"entityType\": \"ASSET\",\n" + - "\t\t\t\"resolveMultiple\": true,\n" + - "\t\t\t\"type\": \"entityList\"\n" + - "\t\t},\n" + - "\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + - "\t}\n" + - "}"; + String entityAliases = """ + { + "23c4185d-1497-9457-30b2-6d91e69a5b2c": { + "alias": "assets", + "filter": { + "entityList": [ + "%s" + ], + "entityType": "ASSET", + "resolveMultiple": true, + "type": "entityList" + }, + "id": "23c4185d-1497-9457-30b2-6d91e69a5b2c" + } + }""".formatted(assetForEntityAlias.getId().toString()); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("description", new TextNode("hallo")); @@ -1132,6 +1189,33 @@ public class VersionControlTest extends AbstractControllerTest { return doPost("/api/calculatedField", calculatedField, CalculatedField.class); } + private CalculatedField createAlarmRule(EntityId entityId, String alarmType, DashboardId mobileDashboardId) { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setName(alarmType); + calculatedField.setType(CalculatedFieldType.ALARM); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + configuration.setArguments(arguments); + SimpleAlarmCondition createCondition = new SimpleAlarmCondition(); + createCondition.setExpression(new TbelAlarmConditionExpression("return temperature >= 50;")); + configuration.setCreateRules(Map.of( + AlarmSeverity.CRITICAL, new AlarmRule(createCondition, "", mobileDashboardId) + )); + SimpleAlarmCondition clearCondition = new SimpleAlarmCondition(); + clearCondition.setExpression(new TbelAlarmConditionExpression("return temperature < 50;")); + configuration.setClearRule(new AlarmRule(clearCondition, "", mobileDashboardId)); + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); @@ -1144,9 +1228,8 @@ public class VersionControlTest extends AbstractControllerTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index dcfe9bd2bd..f278153ed8 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -162,7 +162,7 @@ class DefaultTelemetrySubscriptionServiceTest { apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); lenient().when(apiUsageStateService.getApiUsageState(tenantId)).thenReturn(apiUsageState); - lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi); + lenient().when(partitionService.resolve(eq(ServiceType.TB_CORE), eq(tenantId), any())).thenReturn(tpi); lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null))); @@ -310,8 +310,6 @@ class DefaultTelemetrySubscriptionServiceTest { given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView))); // mock that save latest call for entity view is successful given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); - // mock TPI for entity view - given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi); var request = TimeseriesSaveRequest.builder() .tenantId(tenantId) @@ -373,8 +371,6 @@ class DefaultTelemetrySubscriptionServiceTest { lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView))); // mock that save latest call for entity view is successful lenient().when(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); - // mock TPI for entity view - lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).thenReturn(tpi); var request = TimeseriesSaveRequest.builder() .tenantId(tenantId) diff --git a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java index f3ea4e838c..51e35bd999 100644 --- a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java +++ b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java @@ -32,6 +32,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; import org.thingsboard.server.service.install.InstallScripts; @@ -39,6 +40,8 @@ import org.thingsboard.server.service.system.SystemPatchApplier; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -78,6 +81,9 @@ public class SystemPatchApplierTest { @Mock private WidgetTypeService widgetTypeService; + @Mock + private ImageService imageService; + @InjectMocks private SystemPatchApplier reconciler; @@ -652,4 +658,200 @@ public class SystemPatchApplierTest { return widget; } + // --- createMissingSystemImages tests --- + + @Test + void whenImagesDirDoesNotExist_thenReturnsZeroAndDoesNotCallImageService() { + Path dataDir = tempDir.resolve("data"); + // Intentionally do not create resources/images dir + when(installScripts.getDataDir()).thenReturn(dataDir.toString()); + + Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"); + + assertEquals(0, created); + verify(imageService, never()).getAllImageKeysByTenantId(any()); + verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class)); + } + + @Test + void whenImagesDirIsEmpty_thenReturnsZeroAndDoesNotCallImageService() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet()); + + Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"); + + assertEquals(0, created); + verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class)); + } + + @Test + void whenSystemImageDoesNotExistInDb_thenCreateIt() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + + byte[] imageBytes = new byte[]{1, 2, 3, 4, 5}; + Files.write(imagesDir.resolve("gateway.png"), imageBytes); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet()); + + Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"); + + assertEquals(1, created); + verify(imageService).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID); + verify(imageService).createOrUpdateSystemImage(eq("gateway.png"), eq(imageBytes)); + } + + @Test + void whenSystemImageExistsInDb_thenSkipIt() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + + Files.write(imagesDir.resolve("gateway.png"), new byte[]{1, 2, 3}); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Set.of("gateway.png")); + + Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"); + + assertEquals(0, created); + verify(imageService).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID); + verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class)); + } + + @Test + void whenMixOfNewAndExistingImages_thenOnlyCreateMissingOnes() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + + byte[] newImageBytes = new byte[]{9, 9, 9}; + byte[] existingImageBytes = new byte[]{1, 1, 1}; + Files.write(imagesDir.resolve("new.png"), newImageBytes); + Files.write(imagesDir.resolve("existing.svg"), existingImageBytes); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Set.of("existing.svg")); + + Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"); + + assertEquals(1, created); + verify(imageService, times(1)).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID); + verify(imageService).createOrUpdateSystemImage(eq("new.png"), eq(newImageBytes)); + verify(imageService, never()).createOrUpdateSystemImage(eq("existing.svg"), any(byte[].class)); + } + + @Test + void whenImagesDirContainsSubdirectory_thenSubdirectoryIsIgnored() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + Files.createDirectories(imagesDir.resolve("nested")); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + + byte[] imageBytes = new byte[]{5, 6, 7}; + Files.write(imagesDir.resolve("logo.png"), imageBytes); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet()); + + Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"); + + assertEquals(1, created); + verify(imageService).createOrUpdateSystemImage(eq("logo.png"), eq(imageBytes)); + verify(imageService, never()).createOrUpdateSystemImage(eq("nested"), any(byte[].class)); + } + + @Test + void whenMultipleNewImages_thenCreatesAll() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + + Files.write(imagesDir.resolve("a.png"), new byte[]{1}); + Files.write(imagesDir.resolve("b.svg"), new byte[]{2}); + Files.write(imagesDir.resolve("c.jpg"), new byte[]{3}); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet()); + + Integer created = ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages"); + + assertEquals(3, created); + verify(imageService, times(1)).getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID); + verify(imageService).createOrUpdateSystemImage(eq("a.png"), any(byte[].class)); + verify(imageService).createOrUpdateSystemImage(eq("b.svg"), any(byte[].class)); + verify(imageService).createOrUpdateSystemImage(eq("c.jpg"), any(byte[].class)); + } + + @Test + void whenImageServiceThrows_thenWrapsAndPropagates() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + + Files.write(imagesDir.resolve("broken.png"), new byte[]{1, 2}); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet()); + when(imageService.createOrUpdateSystemImage(eq("broken.png"), any(byte[].class))) + .thenThrow(new RuntimeException("DB error")); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages")); + assertTrue(thrown.getMessage().contains("broken.png")); + } + + @Test + void whenExistingKeysLookupFails_thenDoesNotCreateImage() throws Exception { + Path imagesDir = tempDir.resolve("data").resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + when(installScripts.getDataDir()).thenReturn(tempDir.resolve("data").toString()); + + Files.write(imagesDir.resolve("img.png"), new byte[]{1}); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)) + .thenThrow(new RuntimeException("lookup failed")); + + assertThrows(RuntimeException.class, + () -> ReflectionTestUtils.invokeMethod(reconciler, "createMissingSystemImages")); + verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class)); + } + + // --- applyPatchIfNeeded integration with createMissingSystemImages --- + + @Test + void whenApplyPatchIfNeededRuns_thenCreatesMissingImagesAfterWidgets() throws Exception { + when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0"); + when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0"); + when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true); + when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true); + + Path dataDir = tempDir.resolve("data"); + Path imagesDir = dataDir.resolve(InstallScripts.RESOURCES_DIR).resolve("images"); + Files.createDirectories(imagesDir); + byte[] imgBytes = new byte[]{7, 7, 7}; + Files.write(imagesDir.resolve("new_icon.svg"), imgBytes); + when(installScripts.getDataDir()).thenReturn(dataDir.toString()); + + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + when(imageService.getAllImageKeysByTenantId(TenantId.SYS_TENANT_ID)).thenReturn(Collections.emptySet()); + + ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded"); + + verify(imageService).createOrUpdateSystemImage(eq("new_icon.svg"), eq(imgBytes)); + verify(schemaSettingsService).updateSchemaVersion(); + } + + @Test + void whenVersionNotIncreased_thenImagesAreNotTouched() { + when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.0.0"); + when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0"); + + ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded"); + + verify(imageService, never()).getAllImageKeysByTenantId(any()); + verify(imageService, never()).createOrUpdateSystemImage(anyString(), any(byte[].class)); + } + } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 8d0f0f2af0..f3a3212e67 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import jnr.ffi.annotations.In; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.awaitility.core.ConditionTimeoutException; @@ -125,11 +126,11 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19; -@TestPropertySource(properties = { - "transport.lwm2m.enabled=true", -}) @Slf4j @DaoSqlTest +@TestPropertySource(properties = { + "transport.lwm2m.enabled=true" +}) public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { @SpyBean @@ -164,9 +165,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } public static final Integer shortServerId = 123; - public static final Integer shortServerIdBs0 = 0; - public static final int serverId = 1; - public static final int serverIdBs = 0; public static final String COAP = "coap://"; public static final String COAPS = "coaps://"; @@ -336,7 +334,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte @After public void after() throws Exception { - this.clientDestroy(true); + this.clientDestroy(); if (executor != null && !executor.isShutdown()) { executor.shutdownNow(); } @@ -585,7 +583,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr, Integer value3_0_9) throws Exception { - this.clientDestroy(false); + this.clientDestroy(); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); int clientPort = PortFinder.findAvailableUdpPort(); @@ -671,13 +669,29 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } - private void clientDestroy(boolean isAfter) { + private void clientDestroy() { try { if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { - if (isAfter) { - sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); - awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr()); + boolean serverAlive = false; + List ports = List.of(LWM2M_PORT, LWM2MS_PORT, LWM2MS_BOOTSTRAP_PORT, LWM2MS_BOOTSTRAP_PORT); + for (Integer port : ports) { + if (!PortFinder.isUDPPortAvailable(port)) { + log.debug("Port {} is busy — CoAP server still active.", port); + serverAlive = true; + break; + } + } + if (serverAlive) { + try { + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr()); + } catch (Exception e) { + log.warn("Failed to cleanup LwM2M observations before destroy: {}", e.getMessage()); + } + } else { + log.info("No active CoAP server found on ports 5685–5688. Skipping observe cleanup."); } + lwM2MTestClient.destroy(); } } catch (Exception e) { @@ -725,10 +739,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte return bootstrap; } - private AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) { + protected AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) { AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential(); bootstrapServerCredential.setServerPublicKey(""); - bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); + bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId); bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setHost(isBootstrap ? LWM2M_BOOTSTRAP_HOST : LWM2M_HOST); bootstrapServerCredential.setPort(isBootstrap ? LWM2M_BOOTSTRAP_PORT : LWM2M_PORT); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 1a4af66d20..a307d53054 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -24,8 +24,6 @@ public class Lwm2mTestHelper { public static final int TEMPERATURE_SENSOR = 3303; // Ids in Client - public static final int OBJECT_ID_0 = 0; - public static final int OBJECT_ID_1 = 1; public static final int OBJECT_INSTANCE_ID_0 = 0; public static final int OBJECT_INSTANCE_ID_1 = 1; public static final int OBJECT_INSTANCE_ID_2 = 2; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 908c2d52d3..c9c39bc701 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -33,7 +33,6 @@ import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Server; import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; -import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; import org.eclipse.leshan.client.resource.ObjectsInitializer; import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; @@ -68,6 +67,9 @@ import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandle import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.HashMap; @@ -77,6 +79,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; @@ -90,10 +93,7 @@ import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder; import static org.thingsboard.server.transport.AbstractTransportIntegrationTest.DEFAULT_WAIT_TIMEOUT_SECONDS; -import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId; -import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId; -import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerIdBs0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE; @@ -141,11 +141,11 @@ public class LwM2MTestClient { private Map clientDtlsCid; private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; private LwM2mClientContext clientContext; - private LwM2mTemperatureSensor lwM2mTemperatureSensor12; + private LwM2mTemperatureSensor lwM2MTemperatureSensor12; private String deviceIdStr; private int clientPort; - public void init(Security security, Security securityBs, int clientPort, boolean isRpc, + public void init(Security securityLwm2m, Security securityBs, int clientPort, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode, boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException { @@ -154,55 +154,33 @@ public class LwM2MTestClient { this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; - List models = ObjectLoader.loadAllDefault(); - for (String resourceName : lwm2mClientResources) { - models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); + ObjectsInitializer initializer = createFreshInitializer(); + + // SECURITY + if (securityLwm2m != null && securityLwm2m.getId() != null) { + forceNullSecurityId(securityLwm2m); } - if (this.modelResources != null) { - List modelsRes = new ArrayList<>(); - for (String resourceName : this.modelResources) { - modelsRes.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); - } - Set idsToRemove = new HashSet<>(); - for (ObjectModel model : modelsRes) { - idsToRemove.add(model.id); - } - models.removeIf(model -> idsToRemove.contains(model.id)); - models.addAll(modelsRes); + if (securityBs != null && securityBs.getId() != null) { + forceNullSecurityId(securityBs); } - - LwM2mModel model = new StaticModel(models); - ObjectsInitializer initializer = new ObjectsInitializer(model); - if (securityBs != null && security != null) { - // SECURITY - security.setId(serverId); - securityBs.setId(serverIdBs); - LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{securityBs, security}; - initializer.setClassForObject(SECURITY, Security.class); - initializer.setInstancesForObject(SECURITY, instances); - // SERVER - Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); - lwm2mServer.setId(serverId); - Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60)); - serverBs.setId(serverIdBs); - instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer}; - initializer.setClassForObject(SERVER, Server.class); - initializer.setInstancesForObject(SERVER, instances); + if (securityBs != null && securityLwm2m != null) { + log.warn("Security Both: securityBs: [{}] and security Lwm2m [{}]", securityBs.getId(), securityLwm2m.getId()); + initializer.setInstancesForObject(SECURITY, securityBs, securityLwm2m); } else if (securityBs != null) { - // SECURITY + log.warn("Security BS only: securityBs: [{}] ", securityBs.getId()); initializer.setInstancesForObject(SECURITY, securityBs); - // SERVER - initializer.setClassForObject(SERVER, Server.class); - } else { + } else if (securityLwm2m != null) { // SECURITY - initializer.setInstancesForObject(SECURITY, security); - // SERVER - Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); - lwm2mServer.setId(serverId); - initializer.setInstancesForObject(SERVER, lwm2mServer); + log.warn("Security Lwm2m only: security Lwm2m [{}]", securityLwm2m.getId()); + initializer.setInstancesForObject(SECURITY, securityLwm2m); } - + // SERVER + Server serverLwm2m = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); + initializer.setInstancesForObject(SERVER, serverLwm2m); + // DEVICE initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9)); + + // OTHER t initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); @@ -212,8 +190,8 @@ public class LwM2MTestClient { locationParams.getPos(); initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0)); LwM2mTemperatureSensor lwM2mTemperatureSensor0 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0); - lwM2mTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12); - initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2mTemperatureSensor12); + lwM2MTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12); + initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2MTemperatureSensor12); List enablers = initializer.createAll(); @@ -234,7 +212,9 @@ public class LwM2MTestClient { builder.setSessionListener(new DtlsSessionLogger(clientStates, clientDtlsCid)); return builder; - }; + } + + ; }; } }; @@ -449,28 +429,45 @@ public class LwM2MTestClient { public void destroy() { if (leshanClient != null) { - leshanClient.destroy(true); - } - if (lwM2MDevice != null) { - lwM2MDevice.destroy(); - } - if (fwLwM2MDevice != null) { - fwLwM2MDevice.destroy(); - } - if (swLwM2MDevice != null) { - swLwM2MDevice.destroy(); - } - if (lwM2MBinaryAppDataContainer != null) { - lwM2MBinaryAppDataContainer.destroy(); - } - if (lwM2MTemperatureSensor != null) { - lwM2MTemperatureSensor.destroy(); + try { + leshanClient.destroy(true); + } catch (Exception e) { + log.warn("Failed to destroy Leshan client", e); + } finally { + leshanClient = null; + } } - if (lwM2mTemperatureSensor12 != null) { - lwM2mTemperatureSensor12.destroy(); + + // ThingsBoard custom LwM2M objects + destroySafe(lwM2MDevice); + destroySafe(fwLwM2MDevice); + destroySafe(swLwM2MDevice); + destroySafe(lwM2MBinaryAppDataContainer); + destroySafe(lwM2MTemperatureSensor); + destroySafe(lwM2MTemperatureSensor12); + + lwM2MDevice = null; + fwLwM2MDevice = null; + swLwM2MDevice = null; + lwM2MBinaryAppDataContainer = null; + lwM2MTemperatureSensor = null; + lwM2MTemperatureSensor12 = null; + } + + + private void destroySafe(Object obj) { + if (obj == null) return; + try { + Method destroy = obj.getClass().getMethod("destroy"); + destroy.invoke(obj); + } catch (NoSuchMethodException e) { + // не має destroy() — ігноруємо + } catch (Exception e) { + log.warn("Failed to destroy {}", obj.getClass().getSimpleName(), e); } } + public void start(boolean isStartLw) { if (leshanClient != null) { if (clientPort > 0) { @@ -483,7 +480,7 @@ public class LwM2MTestClient { if (isStartLw) { this.awaitClientAfterStartConnectLw(); } - lwM2mTemperatureSensor12.setLeshanClient(leshanClient); + lwM2MTemperatureSensor12.setLeshanClient(leshanClient); fwLwM2MDevice.setLeshanClient(leshanClient); } } @@ -504,4 +501,60 @@ public class LwM2MTestClient { LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(endpoint); Mockito.doAnswer(invocationOnMock -> null).when(defaultLwM2mUplinkMsgHandlerTest).initAttributes(lwM2MClient, true); } + + private ObjectsInitializer createFreshInitializer() { + List models = new ArrayList<>(ObjectLoader.loadAllDefault()); + for (String resourceName : lwm2mClientResources) { + try (InputStream in = LwM2MTestClient.class.getClassLoader() + .getResourceAsStream("lwm2m/" + resourceName)) { + models.addAll(ObjectLoader.loadDdfFile(in, resourceName)); + } catch (IOException | InvalidDDFFileException e) { + log.warn("Failed to load resource {}", resourceName, e); + } + } + if (this.modelResources != null) { + List modelsRes = new ArrayList<>(); + for (String resourceName : this.modelResources) { + try (InputStream in = LwM2MTestClient.class.getClassLoader() + .getResourceAsStream("lwm2m/" + resourceName)) { + modelsRes.addAll(ObjectLoader.loadDdfFile(in, resourceName)); + } catch (IOException | InvalidDDFFileException e) { + log.warn("Failed to load resource {}", resourceName, e); + } + } + Set idsToRemove = modelsRes.stream() + .map(m -> m.id) + .collect(Collectors.toSet()); + models.removeIf(m -> idsToRemove.contains(m.id)); + models.addAll(modelsRes); + } + LwM2mModel model = new StaticModel(models); + return new ObjectsInitializer(model); + } + + private void forceNullSecurityId(Security security) { + if (security == null) { + return; + } + try { + Field field = security.getClass().getDeclaredField("id"); + field.setAccessible(true); + field.set(security, null); + log.info("[forceNullSecurityId] Set id=null for {}", security); + } catch (NoSuchFieldException e) { + try { + //(SecurityObjectInstance) + Field field = security.getClass().getSuperclass().getDeclaredField("id"); + field.setAccessible(true); + field.set(security, null); + log.info("[forceNullSecurityId] Set id=null for {} (via superclass)", security); + } catch (Exception ex) { + log.error("[forceNullSecurityId] Field 'id' not found for {}", security.getClass(), ex); + } + } catch (Exception e) { + log.error("[forceNullSecurityId] Failed to set id=null for {}", security.getClass(), e); + } + } + } + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index ad02b8f8fc..3c4bdcd3e7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -48,12 +48,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.DEVICE; import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; @@ -116,6 +115,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg initRpc(0); } else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) { initRpc(3303); + } else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationInitReadCompositeAllTest")) { + initRpc(2); + }else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationInitReadCompositeByObjectTest")) { + initRpc(3); } else { initRpc(1); } @@ -141,10 +144,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg }); } }); - String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version; - String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version; - objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0; - objectIdVer_1 = "/" + OBJECT_ID_1 + "_" + ver_Id_1; + String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SECURITY).version; + String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version; + objectIdVer_0 = "/" + SECURITY + "_" + ver_Id_0; + objectIdVer_1 = "/" + SERVER + "_" + ver_Id_1; objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + ACCESS_CONTROL)).findFirst().get(); objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get(); objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); @@ -222,10 +225,67 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg " ],\n" + " \"attributeLwm2m\": {}\n" + " }"; + String INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_ALL = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + + " \"/3_1.2/0/20\": \"batteryStatus\",\n" + + " \"/19_1.1/0/2\": \"dataCreationTime\",\n" + + " \"/5_1.2/0/6\": \"pkgname\",\n" + + " \"/5_1.2/0/7\": \"pkgversion\",\n" + + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/3_1.2/0/20\"\n" + + " ],\n" + + " \"attribute\": [\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\"\n" + + " ],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\",\n" + + " \"/5_1.2/0/9\",\n" + + " \"/19_1.1/0/2\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"initAttrTelAsObsStrategy\": true,\n" + + " \"observeStrategy\": 1\n" + + " }"; + String INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_BY_OBJECT = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + + " \"/3_1.2/0/20\": \"batteryStatus\",\n" + + " \"/19_1.1/0/2\": \"dataCreationTime\",\n" + + " \"/5_1.2/0/6\": \"pkgname\",\n" + + " \"/5_1.2/0/7\": \"pkgversion\",\n" + + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/3_1.2/0/9\"\n" + + " ],\n" + + " \"attribute\": [\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\"\n" + + " ],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\",\n" + + " \"/5_1.2/0/9\",\n" + + " \"/19_1.1/0/2\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"initAttrTelAsObsStrategy\": true,\n" + + " \"observeStrategy\": 2\n" + + " }"; + CONFIG_PROFILE_WITH_PARAMS_RPC = switch (typeConfigProfile) { case 0 -> ATTRIBUTES_TELEMETRY_WITH_PARAMS_RPC_WITH_OBSERVE; case 1 -> TELEMETRY_WITH_PARAMS_RPC_WITHOUT_OBSERVE; + case 2 -> INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_ALL; + case 3 -> INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_BY_OBJECT; case 3303 -> TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE; default -> throw new IllegalStateException("Unexpected value: " + typeConfigProfile); }; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java new file mode 100644 index 0000000000..ea23d2a61d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.rpc.sql; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; + +@Slf4j +public class RpcLwm2mIntegrationInitReadCompositeAllTest extends AbstractRpcLwM2MIntegrationTest { + + /** + " \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry + " \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry + " \"/5_1.2/0/6\": \"pkgname\" - Attributes + " \"/5_1.2/0/7\": \"pkgversion\" - Attributes + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry + " \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry + * "observeStrategy": 1 + */ + @Test + public void testInitReadCompositeAsObserveStrategyCompositeAll() throws Exception { + + + // init test + String RESOURCE_3_9 = "batteryLevel"; + String RESOURCE_3_20 = "batteryStatus"; + String RESOURCE_5_6 = "pkgname"; + String RESOURCE_5_7 = "pkgversion"; + String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod"; + String RESOURCE_19_2 = "dataCreationTime"; + + String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20; + String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6; + String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9; + String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; + countUpdateAttrTelemetryResource(idVer_3_0_9); + countUpdateAttrTelemetryResource(idVwr_3_0_20); + countUpdateAttrTelemetryResource(IdVer5_0_6); + countUpdateAttrTelemetryResource(IdVer5_0_7); + countUpdateAttrTelemetryResource(IdVer5_0_9); + countUpdateAttrTelemetryResource(idVer_19_0_2); + + + AtomicReference actualValues = new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualValues.set(doGetAsync( + "/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys=" + + RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class)); + return actualValues.get() != null && !actualValues.get().isEmpty() + && !actualValues.get().get(RESOURCE_3_9).isEmpty() + && !actualValues.get().get(RESOURCE_3_20).isEmpty() + && !actualValues.get().get(RESOURCE_5_9).isEmpty() + && !actualValues.get().get(RESOURCE_19_2).isEmpty(); + }); + + AtomicReference> actualKeys =new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { + })); + return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty() + && actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7); + }); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java new file mode 100644 index 0000000000..6c5f04da77 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.rpc.sql; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; + +@Slf4j +public class RpcLwm2mIntegrationInitReadCompositeByObjectTest extends AbstractRpcLwM2MIntegrationTest { + + /** + " \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry + " \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry + " \"/5_1.2/0/6\": \"pkgname\" - Attributes + " \"/5_1.2/0/7\": \"pkgversion\" - Attributes + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry + " \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry + * "observeStrategy": 2 + */ + @Test + public void testInitReadCompositeAsObserveStrategyCompositeByObject() throws Exception { + + + // init test + String RESOURCE_3_9 = "batteryLevel"; + String RESOURCE_3_20 = "batteryStatus"; + String RESOURCE_5_6 = "pkgname"; + String RESOURCE_5_7 = "pkgversion"; + String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod"; + String RESOURCE_19_2 = "dataCreationTime"; + + String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20; + String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6; + String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9; + String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; + countUpdateAttrTelemetryResource(idVer_3_0_9); + countUpdateAttrTelemetryResource(idVwr_3_0_20); + countUpdateAttrTelemetryResource(IdVer5_0_6); + countUpdateAttrTelemetryResource(IdVer5_0_7); + countUpdateAttrTelemetryResource(IdVer5_0_9); + countUpdateAttrTelemetryResource(idVer_19_0_2); + + + AtomicReference actualValues = new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualValues.set(doGetAsync( + "/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys=" + + RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class)); + return actualValues.get() != null && !actualValues.get().isEmpty() + && !actualValues.get().get(RESOURCE_3_9).isEmpty() + && !actualValues.get().get(RESOURCE_3_20).isEmpty() + && !actualValues.get().get(RESOURCE_5_9).isEmpty() + && !actualValues.get().get(RESOURCE_19_2).isEmpty(); + }); + + AtomicReference> actualKeys =new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { + })); + return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty() + && actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7); + }); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index 5e28f9bf41..fde11785c3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -21,8 +21,10 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mPath; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -53,18 +55,18 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest try { expectedObjectIdVers.forEach(expected -> { try { - String actualResult = sendRPCById((String) expected); String expectedObjectId = pathIdVerToObjectId((String) expected); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; - if (expectedPath.getObjectId() == 1) { - expectedObjectInstances = "LwM2mObject [id=1, instances={1="; - } else if (expectedPath.getObjectId() == 2) { - expectedObjectInstances = "LwM2mObject [id=2, instances={}]"; + if (expectedPath.getObjectId() > ACCESS_CONTROL) { + String actualResult = sendRPCByIdSync((String) expected); + if (StringUtils.isNoneBlank(actualResult)) { + log.warn(" expectedPath: [{}]", expectedPath); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; + assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances)); + } } - assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances)); } catch (Exception e) { e.printStackTrace(); } @@ -83,7 +85,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception { expectedObjectIdVerInstances.forEach(expected -> { try { - String actualResult = sendRPCById((String) expected); + String actualResult = sendRPCByIdAsync((String) expected); String expectedObjectId = pathIdVerToObjectId((String) expected); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -104,7 +106,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest @Test public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception { String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; - String actualResult = sendRPCById(expectedIdVer); + String actualResult = sendRPCByIdAsync(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expected = "LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; @@ -117,7 +119,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest @Test public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; - String actualResult = sendRPCById(expectedIdVer); + String actualResult = sendRPCByIdAsync(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; @@ -228,10 +230,14 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest assertEquals(actualValue, expectedValue); } - private String sendRPCById(String path) throws Exception { + private String sendRPCByIdAsync(String path) throws Exception { String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); } + private String sendRPCByIdSync(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPost("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); + } private String sendRPCByKey(String key) throws Exception { String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"key\": \"" + key + "\"}}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index 4adc398fa8..52bef912f2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -70,6 +70,7 @@ import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.client.object.Security.noSecBootstrap; import static org.eclipse.leshan.client.object.Security.psk; +import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.junit.Assert.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; @@ -78,7 +79,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClient import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; @DaoSqlTest @@ -96,7 +97,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M protected static final String SERVER_STORE_PWD = "server_ks_password"; protected static final String SERVER_CERT_ALIAS = "server"; protected static final String SERVER_CERT_ALIAS_BS = "bootstrap"; - protected static final Security SECURITY_NO_SEC_BS = noSecBootstrap(URI_BS);; protected final X509Certificate serverX509Cert; // server certificate signed by rootCA protected final X509Certificate serverX509CertBs; // serverBs certificate signed by rootCA protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK @@ -190,13 +190,13 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M LwM2MClientState finishState) throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - this.basicTestConnection(null , SECURITY_NO_SEC_BS, + this.basicTestConnection(null , noSecBootstrap(URI_BS), deviceCredentials, clientEndpoint, transportConfiguration, awaitAlias, expectedStatuses, - true, + false, finishState, false); } @@ -236,12 +236,19 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M } - public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type) throws Exception { - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type)); + public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type, int cnt) throws Exception { + List bootstrapServerCredentialsNoSec = getBootstrapServerCredentialsNoSec(type); + for (int i = 2; i <= cnt; i++) { + AbstractLwM2MBootstrapServerCredential bsCredential = getBootstrapServerCredentialNoSec(false); + bsCredential.setHost("0.0.0." + i); + bsCredential.setShortServerId(bsCredential.getShortServerId() + i); + bootstrapServerCredentialsNoSec.add(bsCredential); + } + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, bootstrapServerCredentialsNoSec); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); this.basicTestConnectionBootstrapRequestTrigger( SECURITY_NO_SEC, - SECURITY_NO_SEC_BS, + noSecBootstrap(URI_BS), deviceCredentials, clientEndpoint, transportConfiguration, @@ -280,8 +287,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M }); Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesLwm2m)); - String executedPath = "/" + OBJECT_ID_1 + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version - + "/" +serverId + "/" + RESOURCE_ID_9; + String executedPath = "/" + SERVER + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version + + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9; lwM2MTestClient.setClientStates(new HashSet<>()); String actualResult = sendRPCSecurityExecuteById(executedPath, deviceIdStr, endpoint); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -357,7 +364,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M default: throw new IllegalStateException("Unexpected value: " + mode); } - bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); + bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId); bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setHost(isBootstrap ? LWM2M_BOOTSTRAP_HOST : LWM2M_HOST); bootstrapServerCredential.setPort(isBootstrap ? LWM2MS_BOOTSTRAP_PORT : LWM2MS_PORT); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java index 1676c04fcc..19e6a5c6b3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java @@ -29,10 +29,12 @@ import org.thingsboard.server.dao.service.DaoSqlTest; public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength0Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + private static final Integer serverDtlsCidLength = 0; + protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { - testNoSecDtlsCidLength(dtlsCidLength, 0); + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { - testPskDtlsCidLength(dtlsCidLength, 0); + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength16Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength16Test.java new file mode 100644 index 0000000000..faa6872b3b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength16Test.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.cid; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.service.DaoSqlTest; + + +@TestPropertySource(properties = { + "transport.lwm2m.dtls.connection_id_length=16" +}) + +@DaoSqlTest +@Slf4j +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength16Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + private static final Integer serverDtlsCidLength = 16; + + protected void testNoSecDtlsCidLength(Integer clientDtlsCidLength) throws Exception { + testNoSecDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); + } + + protected void testPskDtlsCidLength(Integer clientDtlsCidLength) throws Exception { + testPskDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength3Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength1Test.java similarity index 78% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength3Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength1Test.java index c45614ca7d..004702f19f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength3Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength1Test.java @@ -21,17 +21,20 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @TestPropertySource(properties = { - "transport.lwm2m.dtls.connection_id_length=3" + "transport.lwm2m.dtls.connection_id_length=1" }) @DaoSqlTest @Slf4j -public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength3Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength1Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + + private static final Integer serverDtlsCidLength = 1; protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { - testNoSecDtlsCidLength(dtlsCidLength, 3); + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { - testPskDtlsCidLength(dtlsCidLength, 3); + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength2Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength2Test.java new file mode 100644 index 0000000000..3ab6de4cd3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength2Test.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.cid; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.service.DaoSqlTest; + + +@TestPropertySource(properties = { + "transport.lwm2m.dtls.connection_id_length=2" +}) + +@DaoSqlTest +@Slf4j +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength2Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + private static final Integer serverDtlsCidLength = 2; + + protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } + protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength4Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength4Test.java new file mode 100644 index 0000000000..168b260227 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength4Test.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.cid; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.service.DaoSqlTest; + + +@TestPropertySource(properties = { + "transport.lwm2m.dtls.connection_id_length=4" +}) + +@DaoSqlTest +@Slf4j +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength4Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + private static final Integer serverDtlsCidLength = 4; + + protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } + protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java index b543a012d4..b91362e21b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java @@ -28,11 +28,12 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @Slf4j public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + private static final Integer serverDtlsCidLength = null; protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { - testNoSecDtlsCidLength(dtlsCidLength, null); + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { - testPskDtlsCidLength(dtlsCidLength, null); + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java index 2c7212fce9..f07b2d4284 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java @@ -17,14 +17,23 @@ package org.thingsboard.server.transport.lwm2m.security.cid; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.dtls.Connection; +import org.eclipse.californium.scandium.dtls.ConnectionId; +import org.eclipse.californium.scandium.dtls.InMemoryReadWriteLockConnectionStore; +import org.eclipse.californium.scandium.dtls.ResumptionSupportingConnectionStore; import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpoint; import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.peer.IpPeer; import org.junit.Assert; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; @@ -39,13 +48,13 @@ public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLengthTest extends protected String awaitAlias; - protected void testNoSecDtlsCidLength(Integer dtlsCidLength, Integer serverDtlsCidLength) throws Exception { + protected void testNoSecDtlsCidLength(Integer clientDtlsCidLength, Integer serverDtlsCidLength) throws Exception { initDeviceCredentialsNoSek(); - basicTestConnectionDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + basicTestConnectionDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); } - protected void testPskDtlsCidLength(Integer dtlsCidLength, Integer serverDtlsCidLength) throws Exception { + protected void testPskDtlsCidLength(Integer clientDtlsCidLength, Integer serverDtlsCidLength) throws Exception { initDeviceCredentialsPsk(); - basicTestConnectionDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + basicTestConnectionDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); } protected void basicTestConnectionDtlsCidLength(Integer clientDtlsCidLength, @@ -69,19 +78,55 @@ public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLengthTest extends Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().isEmpty()); } else { Assert.assertEquals(2L, lwM2MTestClient.getClientDtlsCid().size()); - Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().keySet().contains(ON_READ_CONNECTION_ID)); - Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().keySet().contains(ON_WRITE_CONNECTION_ID)); - if (serverDtlsCidLength == null) { + Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().containsKey(ON_READ_CONNECTION_ID)); + Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().containsKey(ON_WRITE_CONNECTION_ID)); + + LwM2mServer lwM2mServer = lwM2MTestClient.getLeshanClient().getRegisteredServers().entrySet().stream().findFirst().get().getValue(); + CaliforniumClientEndpoint lwM2mClientEndpoint = (CaliforniumClientEndpoint) lwM2MTestClient.getLeshanClient().getEndpoint(lwM2mServer); + Connection connection = getConnection(lwM2mClientEndpoint, lwM2mServer); + ConnectionId clientCid = connection.getConnectionId(); + ConnectionId readCid = connection.getEstablishedDtlsContext().getReadConnectionId(); + ConnectionId serverCid = connection.getEstablishedDtlsContext().getWriteConnectionId(); + if (serverDtlsCidLength == null || clientDtlsCidLength == null) { + // cid is not used Assert.assertNull(lwM2MTestClient.getClientDtlsCid().get(ON_WRITE_CONNECTION_ID)); Assert.assertNull(lwM2MTestClient.getClientDtlsCid().get(ON_READ_CONNECTION_ID)); + Assert.assertNull(readCid); + Assert.assertNull(serverCid); } else { + Assert.assertEquals(serverDtlsCidLength, lwM2MTestClient.getClientDtlsCid().get(ON_WRITE_CONNECTION_ID)); Assert.assertEquals(clientDtlsCidLength, lwM2MTestClient.getClientDtlsCid().get(ON_READ_CONNECTION_ID)); - if (clientDtlsCidLength == null) { - Assert.assertNull(lwM2MTestClient.getClientDtlsCid().get(ON_READ_CONNECTION_ID)); + // cid used + Assert.assertNotNull(clientCid); + Assert.assertNotNull(readCid); + if (clientDtlsCidLength > 0) { + Assert.assertEquals(clientCid, readCid); + } + Assert.assertNotNull(serverCid); + int actualServerCidLength = serverCid.getBytes().length; + int expectedServerCidLength = serverDtlsCidLength; + Assert.assertEquals(expectedServerCidLength, actualServerCidLength); + } + + if (clientCid != null) { + int actualClientCidLength = clientCid.getBytes().length; + int expectedClientCidLength; + if (clientDtlsCidLength == null || clientDtlsCidLength == 0) { + expectedClientCidLength = 3; } else { - Assert.assertEquals(Integer.valueOf(serverDtlsCidLength), lwM2MTestClient.getClientDtlsCid().get(ON_WRITE_CONNECTION_ID)); + expectedClientCidLength = clientDtlsCidLength; } + Assert.assertEquals(expectedClientCidLength, actualClientCidLength); } } } + + private static Connection getConnection(CaliforniumClientEndpoint lwM2mClientEndpoint, LwM2mServer lwM2mServer) throws NoSuchFieldException, IllegalAccessException { + DTLSConnector connector = (DTLSConnector) lwM2mClientEndpoint.getCoapEndpoint().getConnector(); + Field field = DTLSConnector.class.getDeclaredField("connectionStore"); + field.setAccessible(true); + ResumptionSupportingConnectionStore connectionStore = (InMemoryReadWriteLockConnectionStore) field.get(connector); + InetSocketAddress serverAddr = ((IpPeer) lwM2mServer.getTransportData()).getSocketAddress(); + return connectionStore.get(serverAddr); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java index 2e6d40fc7e..5c2a4ceb29 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 @Before public void setUpNoSecDtlsCidLength() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(NO_SEC, NONE)); - awaitAlias = "await on client state (NoSec_Lwm2m) DtlsCidLength = 0"; + awaitAlias = "await on client state (NoSec_Lwm2m) serverDtlsCidLength = 0"; } @Test @@ -40,8 +40,23 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 testNoSecDtlsCidLength(0); } + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testNoSecDtlsCidLength(1); + } + @Test public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { - testNoSecDtlsCidLength(2); + testNoSecDtlsCidLength(1); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testNoSecDtlsCidLength(4); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testNoSecDtlsCidLength(16); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java index 8dd020c448..d080e1bc65 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI @Before public void createProfileRpc() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); - awaitAlias = "await on client state (Psk_Lwm2m) DtlsCidLength = 0"; + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 0"; } @Test @@ -39,10 +39,24 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { testPskDtlsCidLength(0); } + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } @Test public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testPskDtlsCidLength(2); } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_1/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_1/PskLwm2mIntegrationDtlsCidLengthTest.java new file mode 100644 index 0000000000..d39afdd504 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_1/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.cid.serverDtlsCidLength_1; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength0Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength1Test; + +import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength1Test { + + @Before + public void createProfileRpc() { + transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 1"; + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_Null() throws Exception { + testPskDtlsCidLength(null); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { + testPskDtlsCidLength(0); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { + testPskDtlsCidLength(2); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/NoSecLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/NoSecLwM2MIntegrationDtlsCidLengthTest.java similarity index 69% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/NoSecLwM2MIntegrationDtlsCidLengthTest.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/NoSecLwM2MIntegrationDtlsCidLengthTest.java index 0d74787993..ca36137295 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/NoSecLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/NoSecLwM2MIntegrationDtlsCidLengthTest.java @@ -13,21 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_3; +package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_16; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength3Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength16Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength4Test; import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.NO_SEC; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength3Test { +public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength16Test { @Before public void setUpNoSecDtlsCidLength() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(NO_SEC, NONE)); - awaitAlias = "await on client state (NoSec_Lwm2m) DtlsCidLength = 3"; + awaitAlias = "await on client state (NoSec_Lwm2m) serverDtlsCidLength = 16"; } @Test @@ -40,8 +41,23 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 testNoSecDtlsCidLength(0); } + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testNoSecDtlsCidLength(1); + } + @Test public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testNoSecDtlsCidLength(2); } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testNoSecDtlsCidLength(4); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testNoSecDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/PskLwm2mIntegrationDtlsCidLengthTest.java new file mode 100644 index 0000000000..45b52c6ddd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.cid.serverDtlsCidLength_16; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength16Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength4Test; + +import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength16Test { + + @Before + public void createProfileRpc() { + transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 16"; + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_Null() throws Exception { + testPskDtlsCidLength(null); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { + testPskDtlsCidLength(0); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { + testPskDtlsCidLength(2); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_2/PskLwm2mIntegrationDtlsCidLengthTest.java similarity index 73% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/PskLwm2mIntegrationDtlsCidLengthTest.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_2/PskLwm2mIntegrationDtlsCidLengthTest.java index 5312c872b8..2e0b40da3d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/PskLwm2mIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_2/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_3; +package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_2; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength3Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength2Test; import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength3Test { +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength2Test { @Before public void createProfileRpc() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); - awaitAlias = "await on client state (Psk_Lwm2m) DtlsCidLength = 3"; + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 2"; } @Test @@ -40,9 +40,24 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI testPskDtlsCidLength(0); } + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + @Test public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testPskDtlsCidLength(2); } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_4/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_4/PskLwm2mIntegrationDtlsCidLengthTest.java new file mode 100644 index 0000000000..aa281020a7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_4/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.cid.serverDtlsCidLength_4; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength4Test; + +import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength4Test { + + @Before + public void createProfileRpc() { + transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 4"; + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_Null() throws Exception { + testPskDtlsCidLength(null); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { + testPskDtlsCidLength(0); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { + testPskDtlsCidLength(2); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java index 0c928b2237..88bc5a2622 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 @Before public void setUpNoSecDtlsCidLength() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(NO_SEC, NONE)); - awaitAlias = "await on client state (NoSec_Lwm2m) DtlsCidLength = Null"; + awaitAlias = "await on client state (NoSec_Lwm2m) serverDtlsCidLength = Null"; } @Test @@ -41,7 +41,22 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 } @Test - public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { - testNoSecDtlsCidLength(2); + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testNoSecDtlsCidLength(1); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testNoSecDtlsCidLength(4); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_8() throws Exception { + testNoSecDtlsCidLength(8); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testNoSecDtlsCidLength(16); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java index e84e7fdb8d..37611c2bdb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI @Before public void createProfileRpc() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); - awaitAlias = "await on client state (Psk_Lwm2m) DtlsCidLength = Null"; + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = Null"; } @Test @@ -40,9 +40,24 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI testPskDtlsCidLength(0); } + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + @Test public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testPskDtlsCidLength(2); } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java new file mode 100644 index 0000000000..8b7195141a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; +public class NoSecLwM2MIntegrationBS3SectionTriggerTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTrigger_3_ConnectBsSuccess_UpdateLwm2mSection_3_AndLm2m_1_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger_3" + LWM2M_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 3); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java new file mode 100644 index 0000000000..d386c60135 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; +public class NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 1); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name(); + String awaitAlias = "await on client state (NoSecBS Trigger None section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE, 1); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java new file mode 100644 index 0000000000..46deafd0a4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; + +public class NoSecLwM2MIntegrationBSNoTriggerTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + BOTH.name(); + String awaitAlias = "await on client state (NoSecBS two section)"; + basicTestConnectionStartBS(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); + } + + @Test + public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + LWM2M_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Lwm2m section)"; + basicTestConnectionStartBS(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java new file mode 100644 index 0000000000..deacc8167e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; +public class NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY, 1); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java new file mode 100644 index 0000000000..9ba19fbbfa --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; + +public class NoSecLwM2MIntegrationBSTriggerTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Two section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH, 1); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 633092b9b7..21b4fcbce0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -19,12 +19,6 @@ import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; - public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { //Lwm2m only @@ -34,48 +28,4 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false, false); } - - // Bootstrap + Lwm2m - @Test - public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + BOTH.name(); - String awaitAlias = "await on client state (NoSecBS two section)"; - basicTestConnectionStartBS(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); - } - - @Test - public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + LWM2M_ONLY.name(); - String awaitAlias = "await on client state (NoSecBS Lwm2m section)"; - basicTestConnectionStartBS(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); - } - - // Bs trigger - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name(); - String awaitAlias = "await on client state (NoSecBS Trigger Two section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH); - } - - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name(); - String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY); - } - - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name(); - String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY); - } - - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name(); - String awaitAlias = "await on client state (NoSecBS Trigger None section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index 72d34317bc..a80e9c4643 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -58,8 +58,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Hex.decodeHex(keyPsk.toCharArray())); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -87,8 +86,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); String awaitAlias = "await on client state (Psk_Lwm2m)"; - Device lwm2mDevice = this.basicTestConnection(security, - null, + Device lwm2mDevice = this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -125,8 +123,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); String awaitAlias = "await on client state (Psk_Lwm2m)"; - Device lwm2mDevice = this.basicTestConnection(security, - null, + Device lwm2mDevice = this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -192,12 +189,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setIdentity(identity); clientCredentials.setKey(keyPsk); - Security securityBs = pskBootstrap(SECURE_URI_BS, + Security securityPskBs = pskBootstrap(SECURE_URI_BS, identity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(keyPsk.toCharArray())); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); - this.basicTestConnection(null, securityBs, + this.basicTestConnection(null, securityPskBs, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 666457c7f9..ce942090e9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -113,13 +113,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes RPKClientCredential clientCredentials = new RPKClientCredential(); clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded())); - Security securityBs = rpkBootstrap(SECURE_URI_BS, + Security securityRpkBs = rpkBootstrap(SECURE_URI_BS, certificate.getPublicKey().getEncoded(), privateKey.getEncoded(), serverX509CertBs.getPublicKey().getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, clientPrivateKeyFromCertTrust, certificate, RPK, false); - this.basicTestConnection(null, securityBs, + this.basicTestConnection(null, securityRpkBs, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index e5786388e4..5b385dfea0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -51,15 +51,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg X509ClientCredential clientCredentials = new X509ClientCredential(); clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setCert(Base64.getEncoder().encodeToString(certificate.getEncoded())); - Security security = x509(SECURE_URI, + Security securityX509 = x509(SECURE_URI, shortServerId, certificate.getEncoded(), privateKey.getEncoded(), serverX509Cert.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(securityX509, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -119,8 +118,7 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg serverX509CertBs.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 7c2063301d..44eeb96d5f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -50,8 +50,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra serverX509Cert.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -77,8 +76,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra serverX509CertBs.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security,null, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java new file mode 100644 index 0000000000..1e2273af7c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -0,0 +1,167 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.utils; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +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.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; +import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; + +@ExtendWith(MockitoExtension.class) +class CalculatedFieldUtilsTest { + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("0a69e1e2-fcbc-4234-a4cd-3844bf54035c")); + private static final CalculatedFieldId CF_ID = CalculatedFieldId.fromString("ec0e91b9-6f27-4e93-946a-5fbc2707d8bc"); + private static final DeviceId DEVICE_ID = DeviceId.fromString("1e03bd38-2010-4739-9362-160c288e36c4"); + + @Test + void toProtoAndFromProto_shouldMapGeofencingArgumentsAndZones() { + // given + CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); + given(stateId.tenantId()).willReturn(TENANT_ID); + given(stateId.cfId()).willReturn(CF_ID); + given(stateId.entityId()).willReturn(DEVICE_ID); + + // Build a geofencing argument with two zones (one with inside=true, one with inside=null) + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + Map zoneStates = new LinkedHashMap<>(); + + UUID zoneId1 = UUID.fromString("624a8fff-71a2-4847-a100-ff1cf52dbe71"); + UUID zoneId2 = UUID.fromString("e2adf6ce-9478-40b1-b0e9-4a6860cc46bb"); + + AssetId z1 = new AssetId(zoneId1); + AssetId z2 = new AssetId(zoneId2); + + JsonDataEntry zone1 = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + JsonDataEntry zone2 = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); + + BaseAttributeKvEntry zone1PerimeterAttribute = new BaseAttributeKvEntry(zone1, System.currentTimeMillis(), 0L); + BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L); + + GeofencingZoneState s1 = new GeofencingZoneState(z1, zone1PerimeterAttribute); + s1.setLastPresence(GeofencingPresenceStatus.INSIDE); + GeofencingZoneState s2 = new GeofencingZoneState(z2, zone2PerimeterAttribute); + + zoneStates.put(z1, s1); + zoneStates.put(z2, s2); + geofencingArgumentEntry.setZoneStates(zoneStates); + + // Create cf state with the geofencing argument and add it to the state map + CalculatedFieldState state = new GeofencingCalculatedFieldState(DEVICE_ID); + + CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("geofencingArgumentTest")); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of("geofencingArgumentTest", geofencingArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(1); + assertThat(updatedArguments.get("geofencingArgumentTest")).isEqualTo(geofencingArgumentEntry); + + CalculatedFieldStateProto proto = toProto(stateId, state); + CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(stateId, proto); + + assertThat(fromProto) + .usingRecursiveComparison() + .ignoringFields("ctx", "requiredArguments", "readinessStatus", "latestTimestamp") + .isEqualTo(state); + + ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest"); + assertThat(fromProtoArgument).isInstanceOf(GeofencingArgumentEntry.class); + GeofencingArgumentEntry fromProtoGeoArgument = (GeofencingArgumentEntry) fromProtoArgument; + assertThat(fromProtoGeoArgument.getZoneStates()).hasSize(2); + assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getLastPresence()).isEqualTo(GeofencingPresenceStatus.INSIDE); + assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getLastPresence()).isNull(); + } + + @Test + void toProtoAndFromProto_shouldCreatePropagationStateWithNotEmptyPropagationArgument() { + // given + CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); + given(stateId.tenantId()).willReturn(TENANT_ID); + given(stateId.cfId()).willReturn(CF_ID); + given(stateId.entityId()).willReturn(DEVICE_ID); + + AssetId propagationAssetId = new AssetId(UUID.fromString("17bbf99c-3b87-4d21-b07d-da7409bb2bb7")); + PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry(List.of(propagationAssetId)); + + long lastUpdateTs = System.currentTimeMillis(); + SingleValueArgumentEntry singleValueArgumentEntry = new SingleValueArgumentEntry(new BaseAttributeKvEntry(new StringDataEntry("state", "active"), lastUpdateTs, 1L)); + + CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("state")); + + CalculatedFieldState state = new PropagationCalculatedFieldState(DEVICE_ID); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry, "state", singleValueArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(2); + assertThat(updatedArguments.get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); + assertThat(updatedArguments.get("state")).isEqualTo(singleValueArgumentEntry); + + // when + CalculatedFieldStateProto proto = toProto(stateId, state); + + // then + CalculatedFieldState restored = CalculatedFieldUtils.fromProto(stateId, proto); + + // Propagation argument is not persisted -> should be absent after restore + assertThat(restored).isNotNull(); + assertThat(restored).isInstanceOf(PropagationCalculatedFieldState.class); + + PropagationCalculatedFieldState propagationState = (PropagationCalculatedFieldState) restored; + + assertThat(propagationState.getEntityId()).isEqualTo(DEVICE_ID); + assertThat(propagationState.getArguments()).isNotNull(); + assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); + assertThat(propagationState.getArguments().get("state")).isNotNull().isEqualTo(singleValueArgumentEntry); + assertThat(propagationState.getRequiredArguments()).isNull(); + assertThat(propagationState.getReadinessStatus()).isNull(); + } + +} diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 6faaf97536..9409eed7bd 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -18,9 +18,6 @@ - - - diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 4d5fff8157..6326c45685 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index f3f4f47bb9..774015fc33 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index 7ab81680dc..da04969ed1 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -70,11 +70,10 @@ public class TbCaffeineCacheConfiguration { } private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { - - final Caffeine caffeineBuilder - = Caffeine.newBuilder() + Caffeine caffeineBuilder = Caffeine.newBuilder() .weigher(collectionSafeWeigher()) .maximumWeight(cacheSpec.getMaxSize()) + .recordStats() .ticker(ticker()); if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index bd0c1e6016..5983b96b95 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 8b3ae02c1d..e537b3127a 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.cluster; 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.TbResourceInfo; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; @@ -38,7 +40,6 @@ import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; @@ -81,7 +82,7 @@ public interface TbClusterService extends TbQueueClusterService { void pushNotificationToTransport(String targetServiceId, ToTransportMsg response, TbQueueCallback callback); - void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, TransportProtos.ToCalculatedFieldMsg msg, TbQueueCallback callback); + void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldMsg msg, TbQueueCallback callback); void pushMsgToCalculatedFields(TopicPartitionInfo tpi, UUID msgId, ToCalculatedFieldMsg msg, TbQueueCallback callback); @@ -131,8 +132,14 @@ public interface TbClusterService extends TbQueueClusterService { void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId sourceEdgeId); + void onCustomerUpdated(Customer customer, Customer oldCustomer); + void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback); void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback); + void onRelationUpdated(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback); + + void onRelationDeleted(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback); + } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java index 5ad9babb58..60d4d532be 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java @@ -38,6 +38,8 @@ public interface TbQueueConsumer { boolean isStopped(); + Set getPartitions(); + List getFullTopicNames(); } diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 55f2ee4896..32824826c3 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java index 672de6bacd..af9dc2829f 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -66,7 +66,7 @@ public class TbCoapDtlsSettings { @Value("${coap.dtls.retransmission_timeout:9000}") private int dtlsRetransmissionTimeout; - @Value("${coap.dtls.connection_id_length:}") + @Value("${coap.dtls.connection_id_length:8}") private Integer cIdLength; @Value("${coap.dtls.max_transmission_unit:1024}") diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index f2a1fabfbd..12533cd813 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java index ac87ed2afa..bd0367d871 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java @@ -29,6 +29,8 @@ public interface AiModelService extends EntityDaoService { AiModel save(AiModel model); + AiModel save(AiModel model, boolean doValidate); + Optional findAiModelById(TenantId tenantId, AiModelId modelId); PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink); @@ -37,6 +39,8 @@ public interface AiModelService extends EntityDaoService { FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId); + Optional findAiModelByTenantIdAndName(TenantId tenantId, String name); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); } 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 afbdd4eda7..b9173ef863 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 @@ -51,10 +51,6 @@ import java.util.UUID; public interface AlarmService extends EntityDaoService { - /* - * New API, since 3.5. - */ - /** * Designed for atomic operations over active alarms. * Only one active alarm may exist for the pair {originatorId, alarmType} @@ -74,7 +70,7 @@ public interface AlarmService extends EntityDaoService { AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); - AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent); AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index ad0aebddd2..2294e4fa09 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -62,4 +62,6 @@ public interface AssetProfileService extends EntityDaoService { List findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); + List findAssetProfilesByIds(TenantId tenantId, List assetProfileIds); + } 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 8af1b22604..a785140294 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 @@ -17,6 +17,7 @@ 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.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; @@ -48,6 +49,8 @@ public interface AssetService extends EntityDaoService { Asset saveAsset(Asset asset); + Asset saveAsset(Asset asset, NameConflictStrategy nameConflictStrategy); + Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId); Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index 2cca29e5c7..c5e359e2d5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -27,9 +27,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -/** - * @author Andrew Shvayka - */ public interface AttributesService { ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey); @@ -48,7 +45,13 @@ public interface AttributesService { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); - List findAllKeysByEntityIds(TenantId tenantId, List entityIds, String scope); + List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + + ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); + + List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + + ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); int removeAllByEntityId(TenantId tenantId, EntityId entityId); 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 df5833576a..07ea2243dc 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.audit; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.validation.constraints.NotNull; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -43,7 +44,7 @@ public interface AuditLogService { CustomerId customerId, UserId userId, String userName, - I entityId, + @NotNull I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 420bf01dd2..1603385094 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -16,9 +16,10 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -35,7 +36,7 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - CalculatedField findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name); List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); @@ -45,23 +46,15 @@ public interface CalculatedFieldService extends EntityDaoService { PageData findCalculatedFieldsByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); + PageData findCalculatedFieldsByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink); - void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); - - CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink); + PageData findCalculatedFieldNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink); - CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId); + PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); - List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); - - PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink); + void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); - PageData findAllCalculatedFieldLinks(PageLink pageLink); + int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId); 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 4673f47a7e..b365f41830 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 @@ -17,12 +17,14 @@ package org.thingsboard.server.dao.customer; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; 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.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; +import java.util.List; import java.util.Optional; public interface CustomerService extends EntityDaoService { @@ -37,6 +39,8 @@ public interface CustomerService extends EntityDaoService { Customer saveCustomer(Customer customer); + Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy); + void deleteCustomer(TenantId tenantId, CustomerId customerId); Customer findOrCreatePublicCustomer(TenantId tenantId); @@ -47,4 +51,6 @@ public interface CustomerService extends EntityDaoService { void deleteCustomersByTenantId(TenantId tenantId); + List findCustomersByTenantIdAndIds(TenantId tenantId, List customerIds); + } 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 1866ffe3d1..11f860d6f6 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 @@ -80,4 +80,6 @@ public interface DashboardService extends EntityDaoService { PageData findAllDashboardsIds(PageLink pageLink); + List findDashboardInfoByIds(TenantId tenantId, List dashboardIds); + } 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 index c054653235..be708f9733 100644 --- 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 @@ -64,4 +64,6 @@ public interface DeviceProfileService extends EntityDaoService { List findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); + List findDeviceProfilesByIds(TenantId tenantId, List deviceProfileIds); + } 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 64b9be5149..13bff8d4e4 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 @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; @@ -58,8 +59,12 @@ public interface DeviceService extends EntityDaoService { Device saveDeviceWithAccessToken(Device device, String accessToken); + Device saveDeviceWithAccessToken(Device device, String accessToken, NameConflictStrategy nameConflictStrategy); + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials); + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy); + Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile); Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId); 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 3a67c31b57..fdd722a266 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,6 +16,7 @@ package org.thingsboard.server.dao.entity; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -51,4 +52,6 @@ public interface EntityService { PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + ListenableFuture> findEntityDataByQueryAsync(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 2156cc1860..990a2e4621 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 @@ -19,6 +19,7 @@ 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.NameConflictStrategy; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; @@ -35,6 +36,8 @@ public interface EntityViewService extends EntityDaoService { EntityView saveEntityView(EntityView entityView); + EntityView saveEntityView(EntityView entityView, NameConflictStrategy nameConflictStrategy); + EntityView saveEntityView(EntityView entityView, boolean doValidate); EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId); @@ -65,6 +68,8 @@ public interface EntityViewService extends EntityDaoService { PageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + List findEntityViewsByTenantIdAndIds(TenantId tenantId, List entityViewIds); + PageData findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); PageData findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, PageLink pageLink, String type); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java new file mode 100644 index 0000000000..cdb8c23c41 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import org.thingsboard.server.common.data.id.ApiKeyId; +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.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.List; + +public interface ApiKeyService extends EntityDaoService { + + ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKey); + + ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo, String value, boolean doValidate); + + void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force); + + void deleteByUserId(TenantId tenantId, UserId userId); + + ApiKey findApiKeyByValue(String value); + + ApiKey findApiKeyById(TenantId tenantId, ApiKeyId apiKeyId); + + PageData findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink); + + List findApiKeysByUserId(TenantId tenantId, UserId userId); + + PageData findApiKeysByTenantId(TenantId tenantId, PageLink pageLink); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index 030830d312..21d84f6930 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -20,15 +20,14 @@ 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.EntityRelationInfo; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; import java.util.List; +import java.util.function.Predicate; -/** - * Created by ashvayka on 27.04.17. - */ public interface RelationService { ListenableFuture checkRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); @@ -83,6 +82,12 @@ public interface RelationService { List findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit); + ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + + ListenableFuture> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate relationFilter); + + List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + // TODO: This method may be useful for some validations in the future // ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java index f412c5488a..406263aaed 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import java.util.Collection; +import java.util.Set; public interface ImageService { @@ -38,6 +39,8 @@ public interface ImageService { TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key); + Set getAllImageKeysByTenantId(TenantId tenantId); + TbResourceInfo getPublicImageInfoByKey(String publicResourceKey); PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink); 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 1bfc75e7e7..8f76e7444f 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 @@ -117,4 +117,6 @@ public interface RuleChainService extends EntityDaoService { void deleteRuleNodes(TenantId tenantId, RuleChainId ruleChainId); + List findRuleChainsByIds(TenantId tenantId, List ruleChainIds); + } 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 index 8910ed38b3..a4f4e1d715 100644 --- 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 @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.tenant; -import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; 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 index ccac3cc5c1..f8f308c8f4 100644 --- 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 @@ -46,7 +46,7 @@ public interface TenantProfileService extends EntityDaoService { EntityInfo findDefaultTenantProfileInfo(TenantId tenantId); - boolean setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId); + TenantProfile 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 8b57fd2495..40a3b4bd2a 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 @@ -55,4 +55,6 @@ public interface TenantService extends EntityDaoService { PageData findTenantsIds(PageLink pageLink); + List findTenantsByIds(TenantId callerId, List tenantIds); + } 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 6538a5ea26..6d5727474a 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 @@ -30,9 +30,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -/** - * @author Andrew Shvayka - */ public interface TimeseriesService { ListenableFuture> findAllByQueries(TenantId tenantId, EntityId entityId, List queries); @@ -63,5 +60,12 @@ public interface TimeseriesService { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); + ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds); + + List findLatestByEntityIds(TenantId tenantId, List entityIds); + + ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds); + void cleanup(long systemTtl); + } 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 60a54037ab..29811a3c6d 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 @@ -17,12 +17,15 @@ package org.thingsboard.server.dao.user; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; 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.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.mobile.MobileSessionInfo; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; @@ -45,6 +48,8 @@ public interface UserService extends EntityDaoService { User saveUser(TenantId tenantId, User user); + User saveUser(TenantId tenantId, User user, boolean doValidate); + UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); @@ -53,6 +58,8 @@ public interface UserService extends EntityDaoService { UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); + UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials, boolean doValidate); + UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); UserCredentials requestPasswordReset(TenantId tenantId, String email); @@ -67,6 +74,8 @@ public interface UserService extends EntityDaoService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); + void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials); + void deleteUser(TenantId tenantId, User user); PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); @@ -110,4 +119,14 @@ public interface UserService extends EntityDaoService { void removeMobileSession(TenantId tenantId, String mobileToken); int countTenantAdmins(TenantId tenantId); + + PageData findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink); + + boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user); + + UserAuthDetails findUserAuthDetailsByUserId(TenantId tenantId, UserId userId); + + + List findUsersByTenantIdAndIds(TenantId tenantId, List userIds); + } 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 4838fc176a..180fed9eb3 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 @@ -52,4 +52,6 @@ public interface WidgetsBundleService extends EntityDaoService { void updateSystemWidgets(Stream bundles, Stream widgets); + List findSystemOrTenantWidgetsBundlesByIds(TenantId tenantId, List widgetsBundleIds); + } diff --git a/common/data/pom.xml b/common/data/pom.xml index e14e7072ea..0647e706eb 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common 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 index 5a679ce2cf..ceb657374b 100644 --- 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 @@ -25,8 +25,8 @@ public enum ApiUsageRecordKey { RE_EXEC_COUNT(ApiFeature.RE, "ruleEngineExecutionCount", "ruleEngineExecutionLimit", "Rule Engine execution"), JS_EXEC_COUNT(ApiFeature.JS, "jsExecutionCount", "jsExecutionLimit", "JavaScript execution"), TBEL_EXEC_COUNT(ApiFeature.TBEL, "tbelExecutionCount", "tbelExecutionLimit", "Tbel execution"), - EMAIL_EXEC_COUNT(ApiFeature.EMAIL, "emailCount", "emailLimit", "email message"), - SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit", "SMS message"), + EMAIL_EXEC_COUNT(ApiFeature.EMAIL, "emailCount", "emailLimit", "email message", true, true), + SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit", "SMS message", true, true), CREATED_ALARMS_COUNT(ApiFeature.ALARM, "createdAlarmsCount", "createdAlarmsLimit", "alarm"), ACTIVE_DEVICES("activeDevicesCount"), INACTIVE_DEVICES("inactiveDevicesCount"); @@ -50,21 +50,24 @@ public enum ApiUsageRecordKey { private final String unitLabel; @Getter private final boolean counter; + @Getter + private final boolean urgent; // urgent keys are reported at a shorter interval for quicker usage state updates ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey, String unitLabel) { - this(apiFeature, apiCountKey, apiLimitKey, unitLabel, true); + this(apiFeature, apiCountKey, apiLimitKey, unitLabel, true, false); } ApiUsageRecordKey(String apiCountKey) { - this(null, apiCountKey, null, null, false); + this(null, apiCountKey, null, null, false, false); } - ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey, String unitLabel, boolean counter) { + ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey, String unitLabel, boolean counter, boolean urgent) { this.apiFeature = apiFeature; this.apiCountKey = apiCountKey; this.apiLimitKey = apiLimitKey; this.unitLabel = unitLabel; this.counter = counter; + this.urgent = urgent; } public static ApiUsageRecordKey[] getKeys(ApiFeature feature) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java index da72fb67d7..d854fa3eaf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.validation.NoXss; @@ -35,9 +36,11 @@ import java.util.function.Supplier; * Created by ashvayka on 19.02.18. */ @Slf4j +@Schema public abstract class BaseDataWithAdditionalInfo extends BaseData implements HasAdditionalInfo { @NoXss + @Schema private transient JsonNode additionalInfo; @JsonIgnore private byte[] additionalInfoBytes; 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 942a5d4227..e5ca526938 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 @@ -40,6 +40,7 @@ public final class CacheConstants { public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; public static final String AI_MODEL_CACHE = "aiModel"; + public static final String API_KEYS_CACHE = "apiKeys"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index 61620a77be..956cf054dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -134,13 +134,18 @@ public class Customer extends ContactBased implements HasTenantId, E return super.getPhone(); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com") + @Schema(description = "Email", example = "example@company.com") @Override public String getEmail() { return super.getEmail(); } - @Schema(description = "Additional parameters of the device",implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the customer. " + + "May include: 'description' (string), 'homeDashboardId' (string, UUID of the home dashboard), " + + "'homeDashboardHideToolbar' (boolean, whether to hide the dashboard toolbar), " + + "'isPublic' (boolean, whether this is a public customer).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Regional customer\",\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":false,\"isPublic\":false}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java index 168158d427..2c65a3dfc8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java @@ -33,7 +33,7 @@ import java.util.Optional; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"title", "image", "mobileHide", "mobileOrder", "configuration", "name", "resources"}) +@JsonPropertyOrder({"id", "createdTime", "tenantId", "title", "name", "image", "mobileHide", "mobileOrder", "assignedCustomers", "configuration", "resources", "version"}) public class Dashboard extends DashboardInfo implements ExportableEntity { private static final long serialVersionUID = 872682138346187503L; @@ -70,7 +70,7 @@ public class Dashboard extends DashboardInfo implements ExportableEntity implements HasL this.customerId = customerId; } + @JsonIgnore + public EntityId getOwnerId() { + return customerId != null && !customerId.isNullUid() ? customerId : tenantId; + } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") @Override public String getName() { @@ -170,7 +176,7 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.label = label; } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Device Profile Id.") + @Schema(description = "JSON object with Device Profile Id. If not provided, the type will be used to determine the profile. If neither deviceProfileId nor type is specified, the default device profile will be used.") public DeviceProfileId getDeviceProfileId() { return deviceProfileId; } @@ -225,7 +231,12 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.softwareId = softwareId; } - @Schema(description = "Additional parameters of the device",implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the device. " + + "May include: 'gateway' (boolean, whether the device is a gateway), " + + "'description' (string), " + + "'lastConnectedGateway' (string, UUID of the last gateway that connected this device).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"gateway\":false,\"description\":\"Temperature sensor\",\"lastConnectedGateway\":\"784f394c-42b6-435a-983c-b7beff2784f9\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); 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 index a4317b8aa6..8d0c44800c 100644 --- 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 @@ -114,6 +114,9 @@ public class DeviceProfile extends BaseData implements HasName, this.description = deviceProfile.getDescription(); this.image = deviceProfile.getImage(); this.isDefault = deviceProfile.isDefault(); + this.type = deviceProfile.getType(); + this.transportType = deviceProfile.getTransportType(); + this.provisionType = deviceProfile.getProvisionType(); this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); this.defaultDashboardId = deviceProfile.getDefaultDashboardId(); this.defaultQueueName = deviceProfile.getDefaultQueueName(); 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 index 8bf65873a3..e21b8d3cd0 100644 --- 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 @@ -22,6 +22,7 @@ import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; import org.thingsboard.server.common.data.id.DashboardId; +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.TenantId; @@ -75,4 +76,10 @@ public class DeviceProfileInfo extends EntityInfo { profile.getType(), profile.getTransportType()); } + @Override + @Schema(implementation = DeviceProfileId.class, description = "JSON object with the Device Profile Id.") + public EntityId getId() { + return super.getId(); + } + } 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 a95208e457..deb0f8c66e 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 @@ -17,12 +17,14 @@ package org.thingsboard.server.common.data; import lombok.Getter; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import java.util.Arrays; import java.util.EnumSet; import java.util.List; public enum EntityType { + TENANT(1), CUSTOMER(2), USER(3, "tb_user"), @@ -61,7 +63,7 @@ public enum EntityType { MOBILE_APP(37), MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), - CALCULATED_FIELD_LINK(40), + // CALCULATED_FIELD_LINK(40), - was removed in 4.3 JOB(41), ADMIN_SETTINGS(42), AI_MODEL(43, "ai_model") { @@ -69,14 +71,15 @@ public enum EntityType { public String getNormalName() { return "AI model"; } - }; + }, + API_KEY(44); @Getter private final int protoNumber; // Corresponds to EntityTypeProto @Getter private final String tableName; @Getter - private final String normalName = StringUtils.capitalize(StringUtils.removeStart(name(), "TB_") + private final String normalName = StringUtils.capitalize(Strings.CS.removeStart(name(), "TB_") .toLowerCase().replaceAll("_", " ")); public static final List NORMAL_NAMES = EnumSet.allOf(EntityType.class).stream() 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 9ad0ebf9e0..5da4f2cd91 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 @@ -116,7 +116,10 @@ public class EntityView extends BaseDataWithAdditionalInfo return super.getCreatedTime(); } - @Schema(description = "Additional parameters of the device", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the entity view. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Temperature readings view\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java index 2a3b95ba0c..92a446fcb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java @@ -15,13 +15,29 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@JsonPropertyOrder({ + "emailEnabled", + "smsEnabled", + "notificationEnabled", + "oauthEnabled", + "twoFaEnabled" +}) +@Schema @Data public class FeaturesInfo { + @JsonProperty("emailEnabled") boolean isEmailEnabled; + @JsonProperty("smsEnabled") boolean isSmsEnabled; + @JsonProperty("notificationEnabled") boolean isNotificationEnabled; + @JsonProperty("oauthEnabled") boolean isOauthEnabled; + @JsonProperty("twoFaEnabled") boolean isTwoFaEnabled; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java new file mode 100644 index 0000000000..8386c32387 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 NameConflictPolicy { + + FAIL, + UNIQUIFY; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java new file mode 100644 index 0000000000..ab43448f28 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record NameConflictStrategy(NameConflictPolicy policy, String separator, UniquifyStrategy uniquifyStrategy) { + + public static final NameConflictStrategy DEFAULT = new NameConflictStrategy(NameConflictPolicy.FAIL, null, null); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 246fba56c5..647b584666 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -43,17 +43,17 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp @Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; - @Schema(description = "JSON object with Device Profile Id. Device Profile Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with Device Profile Id. Device Profile Id of the ota package can't be changed.") private DeviceProfileId deviceProfileId; - @Schema(description = "OTA Package type.", example = "FIRMWARE", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package type.", example = "FIRMWARE") private OtaPackageType type; @Length(fieldName = "title") @NoXss - @Schema(description = "OTA Package title.", example = "fw", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package title.", example = "fw") private String title; @Length(fieldName = "version") @NoXss - @Schema(description = "OTA Package version.", example = "1.0", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package version.", example = "1.0") private String version; @Length(fieldName = "tag") @NoXss @@ -61,7 +61,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp private String tag; @Length(fieldName = "url") @NoXss - @Schema(description = "OTA Package url.", example = "http://thingsboard.org/fw/1", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package url.", example = "http://thingsboard.org/fw/1") private String url; @Schema(description = "Indicates OTA Package 'has data'. Field is returned from DB ('true' if data exists or url is set). If OTA Package 'has data' is 'false' we can not assign the OTA Package to the Device or Device Profile.", example = "true", accessMode = Schema.AccessMode.READ_ONLY) private boolean hasData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java index 3d162de1e7..cd467b01d2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java @@ -34,21 +34,23 @@ public class ProfileEntityIdInfo implements Serializable, HasTenantId { private static final long serialVersionUID = 8532058281983868003L; private final TenantId tenantId; + private final EntityId ownerId; private final EntityId profileId; private final EntityId entityId; - private ProfileEntityIdInfo(UUID tenantId, EntityId profileId, EntityId entityId) { + private ProfileEntityIdInfo(UUID tenantId, EntityId ownerId, EntityId profileId, EntityId entityId) { this.tenantId = TenantId.fromUUID(tenantId); + this.ownerId = ownerId; this.profileId = profileId; this.entityId = entityId; } - public static ProfileEntityIdInfo create(UUID tenantId, DeviceProfileId profileId, DeviceId entityId) { - return new ProfileEntityIdInfo(tenantId, profileId, entityId); + public static ProfileEntityIdInfo create(UUID tenantId, EntityId ownerId, DeviceProfileId profileId, DeviceId entityId) { + return new ProfileEntityIdInfo(tenantId, ownerId, profileId, entityId); } - public static ProfileEntityIdInfo create(UUID tenantId, AssetProfileId profileId, AssetId entityId) { - return new ProfileEntityIdInfo(tenantId, profileId, entityId); + public static ProfileEntityIdInfo create(UUID tenantId, EntityId ownerId, AssetProfileId profileId, AssetId entityId) { + return new ProfileEntityIdInfo(tenantId, ownerId, profileId, entityId); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java index 930e4dddca..f1de65f1f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -39,9 +40,15 @@ public class ShortCustomerInfo { @NoXss private String title; + @JsonProperty("public") @Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.") - @Getter @Setter - private boolean isPublic; + private boolean publicCustomer; + + @JsonProperty("public") + public boolean isPublic() { return publicCustomer; } + + @JsonProperty("public") + public void setPublic(boolean publicCustomer) { this.publicCustomer = publicCustomer; } @Override public boolean equals(Object o) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 37242dfa1d..3cb6e024fc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -17,12 +17,14 @@ package org.thingsboard.server.common.data; import com.google.common.base.Splitter; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.Strings; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.List; +import java.util.Objects; import java.util.function.Function; import static org.apache.commons.lang3.StringUtils.repeat; @@ -131,7 +133,7 @@ public class StringUtils { } public static boolean endsWith(String str, String suffix) { - return org.apache.commons.lang3.StringUtils.endsWith(str, suffix); + return Strings.CS.endsWith(str, suffix); } public static boolean hasLength(String str) { @@ -147,7 +149,7 @@ public class StringUtils { } public static String defaultString(String s, String defaultValue) { - return org.apache.commons.lang3.StringUtils.defaultString(s, defaultValue); + return Objects.toString(s, defaultValue); } public static boolean isNumeric(String str) { @@ -155,7 +157,7 @@ public class StringUtils { } public static boolean equals(String str1, String str2) { - return org.apache.commons.lang3.StringUtils.equals(str1, str2); + return Strings.CS.equals(str1, str2); } public static boolean equalsAny(String string, String... otherStrings) { @@ -199,7 +201,7 @@ public class StringUtils { } public static boolean contains(final CharSequence seq, final CharSequence searchSeq) { - return org.apache.commons.lang3.StringUtils.contains(seq, searchSeq); + return Strings.CS.contains(seq, searchSeq); } /** @@ -210,23 +212,23 @@ public class StringUtils { } public static String randomNumeric(int length) { - return RandomStringUtils.randomNumeric(length); + return RandomStringUtils.secure().nextNumeric(length); } public static String random(int length) { - return RandomStringUtils.random(length); + return RandomStringUtils.secure().next(length); } public static String random(int length, String chars) { - return RandomStringUtils.random(length, chars); + return RandomStringUtils.secure().next(length, chars); } public static String randomAlphanumeric(int count) { - return RandomStringUtils.randomAlphanumeric(count); + return RandomStringUtils.secure().nextAlphanumeric(count); } public static String randomAlphabetic(int count) { - return RandomStringUtils.randomAlphabetic(count); + return RandomStringUtils.secure().nextAlphabetic(count); } public static String generateSafeToken(int length) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java index 73bd3cddc4..3fd86d1163 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -23,6 +24,7 @@ import java.util.List; @Data public class SystemInfo { @Schema(description = "Is monolith.") + @JsonProperty("monolith") private boolean isMonolith; @Schema(description = "System data.") private List systemData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index c31b49325e..d3b2e8b73a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -38,5 +38,12 @@ public class SystemParams { String calculatedFieldDebugPerTenantLimitsConfiguration; long maxArgumentsPerCF; long maxDataPointsPerRollingArg; + int minAllowedScheduledUpdateIntervalInSecForCF; + int maxRelationLevelPerCfArgument; + int maxRelatedEntitiesToReturnPerCfArgument; + long minAllowedDeduplicationIntervalInSecForCF; + long minAllowedAggregationIntervalInSecForCF; + long intermediateAggregationIntervalInSecForCF; TrendzSettings trendzSettings; + boolean allowKeyFiltersOrConditions; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 4365357d2d..4343205b74 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -28,6 +28,7 @@ import java.io.Serial; import java.util.Base64; import java.util.Optional; +@Schema @Slf4j @Data @EqualsAndHashCode(callSuper = true) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index 0aa0010231..16e95d79ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; import java.util.function.UnaryOperator; @Schema @@ -36,6 +37,7 @@ import java.util.function.UnaryOperator; @EqualsAndHashCode(callSuper = true) public class TbResourceInfo extends BaseData implements HasName, HasTenantId, ExportableEntity { + @Serial private static final long serialVersionUID = 7282664529021651736L; @Schema(description = "JSON object with Tenant Id. Tenant Id of the resource can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) @@ -44,15 +46,17 @@ public class TbResourceInfo extends BaseData implements HasName, H @Length(fieldName = "title") @Schema(description = "Resource title.", example = "BinaryAppDataContainer id=19 v1.0") private String title; - @Schema(description = "Resource type.", example = "LWM2M_MODEL", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource type.", example = "LWM2M_MODEL") private ResourceType resourceType; - @Schema(description = "Resource sub type.", example = "IOT_SVG", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource sub type.", example = "IOT_SVG") private ResourceSubType resourceSubType; @NoXss @Length(fieldName = "resourceKey") - @Schema(description = "Resource key.", example = "19_1.0", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource key.", example = "19_1.0") private String resourceKey; + @Schema(description = "Whether the resource is public.", example = "false") private boolean isPublic; + @Schema(description = "Public resource key.") private String publicResourceKey; @Schema(description = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = Schema.AccessMode.READ_ONLY) private String searchText; @@ -61,10 +65,12 @@ public class TbResourceInfo extends BaseData implements HasName, H private String etag; @NoXss @Length(fieldName = "file name") - @Schema(description = "Resource file name.", example = "19.xml", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource file name.", example = "19.xml") private String fileName; + @Schema(description = "Resource descriptor.") private JsonNode descriptor; + @Schema(description = "External resource Id used for import/export.") private TbResourceId externalId; public TbResourceInfo() { 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 005e3204cd..374e6b526a 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 @@ -164,7 +164,11 @@ public class Tenant extends ContactBased implements HasTenantId, HasTi return super.getEmail(); } - @Schema(description = "Additional parameters of the device", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the tenant. " + + "May include: 'description' (string), 'homeDashboardId' (string, UUID of the home dashboard), " + + "'homeDashboardHideToolbar' (boolean, whether to hide the dashboard toolbar).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Main tenant\",\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":true}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java new file mode 100644 index 0000000000..addf0b8b07 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 UniquifyStrategy { + + RANDOM, + INCREMENTAL; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index 69ba115af1..e1b40b48f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -91,7 +91,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, return super.getCreatedTime(); } - @Schema(description = "JSON object with the Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with the Tenant Id.") public TenantId getTenantId() { return tenantId; } @@ -100,7 +100,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, this.tenantId = tenantId; } - @Schema(description = "JSON object with the Customer Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with the Customer Id.") public CustomerId getCustomerId() { return customerId; } @@ -161,7 +161,16 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, this.phone = phone; } - @Schema(description = "Additional parameters of the user", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the user. " + + "May include: 'defaultDashboardId' (string, UUID of the default dashboard), " + + "'defaultDashboardFullscreen' (boolean), " + + "'homeDashboardId' (string, UUID of the home dashboard), " + + "'homeDashboardHideToolbar' (boolean), " + + "'lang' (string, user locale, e.g. 'en_US'), " + + "'authProviderName' (string, name of the authentication provider).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"defaultDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"defaultDashboardFullscreen\":false," + + "\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":true,\"lang\":\"en_US\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UserAuthDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/UserAuthDetails.java new file mode 100644 index 0000000000..f71b1a7f73 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UserAuthDetails.java @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 record UserAuthDetails(User user, boolean credentialsEnabled) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java index ffca023b23..4d4fcfa58e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java @@ -46,7 +46,6 @@ public final class AiModel extends BaseData implements HasTenantId, H private static final long serialVersionUID = 9017108678716011604L; @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "JSON object representing the ID of the tenant associated with this AI model", example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" @@ -54,7 +53,6 @@ public final class AiModel extends BaseData implements HasTenantId, H private TenantId tenantId; @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "Version of the AI model record; increments automatically whenever the record is changed", example = "7", diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java index a553d66bf0..f1004790d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java @@ -17,8 +17,17 @@ package org.thingsboard.server.common.data.ai.dto; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + description = "Response from chat API", + discriminatorProperty = "status", + discriminatorMapping = { + @DiscriminatorMapping(value = "SUCCESS", schema = TbChatResponse.Success.class), + @DiscriminatorMapping(value = "FAILURE", schema = TbChatResponse.Failure.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "status", @@ -33,7 +42,8 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes @Schema( description = "Indicates whether the request was successful or not", - example = "SUCCESS" + example = "SUCCESS", + requiredMode = Schema.RequiredMode.REQUIRED ) String getStatus(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java index 55f35a3960..4edf49045b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java @@ -19,11 +19,18 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import dev.langchain4j.data.message.Content; import dev.langchain4j.data.message.TextContent; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import static org.thingsboard.server.common.data.ai.dto.TbContent.TbTextContent; +@Schema( + discriminatorProperty = "contentType", + discriminatorMapping = { + @DiscriminatorMapping(value = "TEXT", schema = TbTextContent.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -46,7 +53,8 @@ public sealed interface TbContent permits TbTextContent { } @Schema( - description = "Text-based content part of a user's prompt" + description = "Text-based content part of a user's prompt", + allOf = TbContent.class ) record TbTextContent( @NotBlank diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java index 2d060dbf4a..e1f0b84a4d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.ai.dto; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; @@ -24,9 +25,12 @@ import java.util.List; public record TbUserMessage( @NotEmpty @Valid - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - description = "A list of content parts that make up the complete user prompt" + @ArraySchema( + arraySchema = @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + description = "A list of content parts that make up the complete user prompt" + ), + schema = @Schema(ref = "#/components/schemas/TbContent") ) List contents ) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index adcc176edc..d036969254 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.ai.model; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig; @@ -55,10 +57,31 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"), @JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA") }) +@Schema( + name = "AiModelConfig", + description = "Root configuration for AI models", + discriminatorProperty = "provider", + discriminatorMapping = { + @DiscriminatorMapping(value = "OPENAI", schema = OpenAiChatModelConfig.class), + @DiscriminatorMapping(value = "AZURE_OPENAI", schema = AzureOpenAiChatModelConfig.class), + @DiscriminatorMapping(value = "GOOGLE_AI_GEMINI", schema = GoogleAiGeminiChatModelConfig.class), + @DiscriminatorMapping(value = "GOOGLE_VERTEX_AI_GEMINI", schema = GoogleVertexAiGeminiChatModelConfig.class), + @DiscriminatorMapping(value = "MISTRAL_AI", schema = MistralAiChatModelConfig.class), + @DiscriminatorMapping(value = "ANTHROPIC", schema = AnthropicChatModelConfig.class), + @DiscriminatorMapping(value = "AMAZON_BEDROCK", schema = AmazonBedrockChatModelConfig.class), + @DiscriminatorMapping(value = "GITHUB_MODELS", schema = GitHubModelsChatModelConfig.class), + @DiscriminatorMapping(value = "OLLAMA", schema = OllamaChatModelConfig.class) + } +) public interface AiModelConfig { + @Schema( + description = "AI Provider", + requiredMode = Schema.RequiredMode.REQUIRED + ) AiProvider provider(); + @Schema(hidden = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index ee80230fee..490ae67eb2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; +@Schema @Builder public record AmazonBedrockChatModelConfig( + @Schema(ref = "#/components/schemas/AmazonBedrockProviderConfig") @NotNull @Valid AmazonBedrockProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 44b55c4c22..77352b06c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; +@Schema @Builder public record AnthropicChatModelConfig( + @Schema(ref = "#/components/schemas/AnthropicProviderConfig") @NotNull @Valid AnthropicProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index 1e0d6d0c18..d4df82d237 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; +@Schema @Builder public record AzureOpenAiChatModelConfig( + @Schema(ref = "#/components/schemas/AzureOpenAiProviderConfig") @NotNull @Valid AzureOpenAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index 606f30599b..264683f7b4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig; +@Schema @Builder public record GitHubModelsChatModelConfig( + @Schema(ref = "#/components/schemas/GitHubModelsProviderConfig") @NotNull @Valid GitHubModelsProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index 422c92ba97..82ba3a0295 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; +@Schema @Builder public record GoogleAiGeminiChatModelConfig( + @Schema(ref = "#/components/schemas/GoogleAiGeminiProviderConfig") @NotNull @Valid GoogleAiGeminiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index 3e298e52e2..4d20a240e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +@Schema @Builder public record GoogleVertexAiGeminiChatModelConfig( + @Schema(ref = "#/components/schemas/GoogleVertexAiGeminiProviderConfig") @NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index 5713217fe9..60c208c181 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; +@Schema @Builder public record MistralAiChatModelConfig( + @Schema(ref = "#/components/schemas/MistralAiProviderConfig") @NotNull @Valid MistralAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java index 3f1856f630..5f7776a0a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; +@Schema @Builder public record OllamaChatModelConfig( + @Schema(ref = "#/components/schemas/OllamaProviderConfig") @NotNull @Valid OllamaProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index f74292cd76..3845aaa543 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +@Schema @Builder public record OpenAiChatModelConfig( + @Schema(ref = "#/components/schemas/OpenAiProviderConfig") @NotNull @Valid OpenAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java index dc38440a66..17e886e946 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record AmazonBedrockProviderConfig( @NotNull String region, @NotNull String accessKeyId, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java index 6e7abeee84..4108a72439 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record AnthropicProviderConfig( @NotNull String apiKey ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java index 05d9e99569..6687f8a08e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record AzureOpenAiProviderConfig( @NotNull String endpoint, String serviceVersion, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java index 56409c81ad..529eb2b989 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record GitHubModelsProviderConfig( @NotNull String personalAccessToken ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java index 3492a1096d..5b20ed3d96 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record GoogleAiGeminiProviderConfig( @NotNull String apiKey ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java index 0e0928e038..72c4b5e940 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +@Schema public record GoogleVertexAiGeminiProviderConfig( @NotBlank String fileName, // not used on BE, but needed for UI @NotNull String projectId, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java index 46165f9843..b0ca42de16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record MistralAiProviderConfig( @NotNull String apiKey ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java index 0a4da6c66c..606cc75b9d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java @@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.ai.provider; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +@Schema public record OllamaProviderConfig( @NotNull String baseUrl, @NotNull @Valid OllamaAuth auth ) implements AiProviderConfig { + @Schema( + description = "Ollama authentication schemes", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "NONE", schema = OllamaAuth.None.class), + @DiscriminatorMapping(value = "BASIC", schema = OllamaAuth.Basic.class), + @DiscriminatorMapping(value = "TOKEN", schema = OllamaAuth.Token.class) + } + ) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 59eabc58a6..900eb6734c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.ai.provider; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import lombok.Builder; import org.apache.commons.lang3.StringUtils; import java.util.Objects; +@Schema @Builder public record OpenAiProviderConfig( String baseUrl, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index abd1515ff2..9f9bdec49d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -84,6 +84,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @Schema(description = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465") private long assignTs; @Schema(description = "JSON object with alarm details") + @JsonProperty private transient JsonNode details; @Schema(description = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java index 0a011c287f..ef7499fa33 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java @@ -45,7 +45,7 @@ public class AlarmComment extends BaseData implements HasName { private AlarmId alarmId; @Schema(description = "JSON object with User id.", accessMode = Schema.AccessMode.READ_ONLY) private UserId userId; - @Schema(description = "Defines origination of comment. System type means comment was created by TB. OTHER type means comment was created by user.", example = "SYSTEM/OTHER", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Defines origination of comment. System type means comment was created by TB. OTHER type means comment was created by user.", example = "SYSTEM/OTHER") private AlarmCommentType type; @Schema(description = "JSON object with text of comment.") @NoXss @@ -56,7 +56,7 @@ public class AlarmComment extends BaseData implements HasName { @Schema(description = "JSON object with the alarm comment Id. " + "Specify this field to update the alarm comment. " + "Referencing non-existing alarm Id will cause error. " + - "Omit this field to create new alarm.", accessMode = Schema.AccessMode.READ_ONLY) + "Omit this field to create new alarm.") @Override public AlarmCommentId getId() { return super.getId(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentSubType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentSubType.java new file mode 100644 index 0000000000..80d08a594d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentSubType.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm; + +import lombok.Getter; + +public enum AlarmCommentSubType { + + ACKED_BY_USER("Alarm was acknowledged by user %s"), + CLEARED_BY_USER("Alarm was cleared by user %s"), + ASSIGNED_TO_USER("Alarm was assigned by user %s to user %s"), + UNASSIGNED_BY_USER("Alarm was unassigned by user %s"), + UNASSIGNED_FROM_DELETED_USER("Alarm was unassigned because user %s - was deleted"), + COMMENT_DELETED("User %s deleted his comment"), + SEVERITY_CHANGED("Alarm severity was updated from %s to %s"); + + @Getter + private final String text; + + AlarmCommentSubType(String text) { + this.text = text; + } +} 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 d1481f756d..bb85f772e7 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 @@ -21,11 +21,14 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import java.io.Serial; + @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Schema public class AlarmInfo extends Alarm { + @Serial private static final long serialVersionUID = 2807343093519543363L; @Getter @@ -38,6 +41,10 @@ public class AlarmInfo extends Alarm { @Schema(description = "Alarm originator label", example = "Thermostat label") private String originatorLabel; + @Setter + @Schema(description = "Originator display name", example = "Thermostat") + private String originatorDisplayName; + @Getter @Setter @Schema(description = "Alarm assignee") @@ -51,10 +58,15 @@ public class AlarmInfo extends Alarm { super(alarm); } + public String getOriginatorDisplayName() { + return originatorDisplayName != null ? originatorDisplayName : (originatorLabel != null ? originatorLabel : originatorName); + } + public AlarmInfo(AlarmInfo alarmInfo) { super(alarmInfo); - this.originatorName = alarmInfo.originatorName; - this.originatorLabel = alarmInfo.originatorLabel; + this.originatorName = alarmInfo.getOriginatorName(); + this.originatorLabel = alarmInfo.getOriginatorLabel(); + this.originatorDisplayName = alarmInfo.getOriginatorDisplayName(); this.assignee = alarmInfo.getAssignee(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java new file mode 100644 index 0000000000..3ab3f0a526 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.id.DashboardId; + +@Schema +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AlarmRule { + + @Valid + @NotNull + private AlarmCondition condition; + private String alarmDetails; + private DashboardId dashboardId; + + @JsonIgnore + public boolean requiresScheduledReevaluation() { + return condition.requiresScheduledReevaluation(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java new file mode 100644 index 0000000000..87bd44ce1f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleAlarmCondition.class), + @DiscriminatorMapping(value = "DURATION", schema = DurationAlarmCondition.class), + @DiscriminatorMapping(value = "REPEATING", schema = RepeatingAlarmCondition.class) + } +) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(name = "SIMPLE", value = SimpleAlarmCondition.class), + @Type(name = "DURATION", value = DurationAlarmCondition.class), + @Type(name = "REPEATING", value = RepeatingAlarmCondition.class), +}) +@Data +@NoArgsConstructor +public abstract class AlarmCondition { + + @NotNull + @Valid + private AlarmConditionExpression expression; + @Valid + private AlarmConditionValue schedule; + + @JsonIgnore + public boolean hasSchedule() { + return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); + } + + @JsonIgnore + public boolean requiresScheduledReevaluation() { + return hasSchedule() || expression.requiresScheduledReevaluation(); + } + + @JsonIgnore + @AssertTrue(message = "Expressions requiring scheduled reevaluation can only be used with simple alarm conditions") + public boolean isValid() { + if (getType() != AlarmConditionType.SIMPLE && expression.requiresScheduledReevaluation()) { + return false; + } + return true; + } + + @JsonIgnore + public abstract AlarmConditionType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java new file mode 100644 index 0000000000..4810a74f99 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition; + +public enum AlarmConditionType { + SIMPLE, + DURATION, + REPEATING +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java new file mode 100644 index 0000000000..d55faf6fbf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AlarmConditionValue { + + private T staticValue; + private String dynamicValueArgument; + + @JsonIgnore + @AssertTrue(message = "Either staticValue or dynamicValueArgument must be set") + public boolean isValid() { + return staticValue != null ^ dynamicValueArgument != null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java new file mode 100644 index 0000000000..3599ee5c3c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.concurrent.TimeUnit; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DurationAlarmCondition extends AlarmCondition { + + @NotNull + private TimeUnit unit; + @Valid + @NotNull + private AlarmConditionValue value; + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.DURATION; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java new file mode 100644 index 0000000000..42cba04ec0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RepeatingAlarmCondition extends AlarmCondition { + + @Valid + @NotNull + private AlarmConditionValue count; + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.REPEATING; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java new file mode 100644 index 0000000000..f91ff1f3f0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public class SimpleAlarmCondition extends AlarmCondition { + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.SIMPLE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java new file mode 100644 index 0000000000..5251f385df --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleAlarmConditionExpression.class), + @DiscriminatorMapping(value = "TBEL", schema = TbelAlarmConditionExpression.class) + } +) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(name = "SIMPLE", value = SimpleAlarmConditionExpression.class), + @Type(name = "TBEL", value = TbelAlarmConditionExpression.class), +}) +public interface AlarmConditionExpression { + + @JsonIgnore + AlarmConditionExpressionType getType(); + + @JsonIgnore + default boolean requiresScheduledReevaluation() { + return false; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java new file mode 100644 index 0000000000..b683188436 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression; + +public enum AlarmConditionExpressionType { + SIMPLE, + TBEL +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java new file mode 100644 index 0000000000..578b7909c0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; +import org.thingsboard.server.common.data.query.EntityKeyValueType; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; +import java.util.List; + +@Schema +@Data +public class AlarmConditionFilter implements Serializable { + + @NotBlank + private String argument; + @NotNull + private EntityKeyValueType valueType; + private ComplexOperation operation; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/AlarmRuleKeyFilterPredicate")) + @Valid + @NotEmpty + private List predicates; + + public boolean hasPredicate(FilterPredicateType type) { + return containsPredicate(predicates, type); + } + + private boolean containsPredicate(List predicates, FilterPredicateType type) { + return predicates.stream().anyMatch(predicate -> { + if (predicate instanceof ComplexFilterPredicate complexPredicate) { + return containsPredicate(complexPredicate.getPredicates(), type); + } else { + return predicate.getType() == type; + } + }); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java new file mode 100644 index 0000000000..a581fa3a41 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; +import org.thingsboard.server.common.data.query.ComplexOperation; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SimpleAlarmConditionExpression implements AlarmConditionExpression { + + @Valid + @NotEmpty + private List filters; + private ComplexOperation operation; + + @Override + public AlarmConditionExpressionType getType() { + return AlarmConditionExpressionType.SIMPLE; + } + + @Override + public boolean requiresScheduledReevaluation() { + return filters.stream().anyMatch(filter -> filter.hasPredicate(FilterPredicateType.NO_DATA)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java new file mode 100644 index 0000000000..e7a24dff72 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TbelAlarmConditionExpression implements AlarmConditionExpression { + + @NotBlank + private String expression; + + @Override + public AlarmConditionExpressionType getType() { + return AlarmConditionExpressionType.TBEL; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java new file mode 100644 index 0000000000..64a73d2f6d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Schema(name = "AlarmRuleBooleanFilterPredicate") +@Data +public class BooleanFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private BooleanOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") + @Override + public FilterPredicateType getType() { + return FilterPredicateType.BOOLEAN; + } + + @Schema(name = "AlarmRuleBooleanOperation") + public enum BooleanOperation { + EQUAL, + NOT_EQUAL + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java new file mode 100644 index 0000000000..34f128d3e2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.thingsboard.server.common.data.query.ComplexOperation; + +import java.util.List; + +@Schema(name = "AlarmRuleComplexFilterPredicate") +@Data +public class ComplexFilterPredicate implements KeyFilterPredicate { + + private ComplexOperation operation; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/AlarmRuleKeyFilterPredicate")) + private List predicates; + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") + @Override + public FilterPredicateType getType() { + return FilterPredicateType.COMPLEX; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java new file mode 100644 index 0000000000..de05017130 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "AlarmRuleFilterPredicateType") +public enum FilterPredicateType { + STRING, + NUMERIC, + BOOLEAN, + NO_DATA, + COMPLEX +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java new file mode 100644 index 0000000000..d6409d5d67 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + +@Schema( + name = "AlarmRuleKeyFilterPredicate", + description = "Filter predicate for alarm rule key-based filtering", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "STRING", schema = StringFilterPredicate.class), + @DiscriminatorMapping(value = "NUMERIC", schema = NumericFilterPredicate.class), + @DiscriminatorMapping(value = "BOOLEAN", schema = BooleanFilterPredicate.class), + @DiscriminatorMapping(value = "NO_DATA", schema = NoDataFilterPredicate.class), + @DiscriminatorMapping(value = "COMPLEX", schema = ComplexFilterPredicate.class) + } +) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(value = StringFilterPredicate.class, name = "STRING"), + @Type(value = NumericFilterPredicate.class, name = "NUMERIC"), + @Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), + @Type(value = NoDataFilterPredicate.class, name = "NO_DATA"), + @Type(value = ComplexFilterPredicate.class, name = "COMPLEX") +}) +public interface KeyFilterPredicate extends Serializable { + + @JsonIgnore + FilterPredicateType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java new file mode 100644 index 0000000000..1f719cb80c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +import java.util.concurrent.TimeUnit; + +@Schema +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NoDataFilterPredicate implements KeyFilterPredicate { + + @NotNull + private TimeUnit unit; + @Valid + @NotNull + private AlarmConditionValue duration; + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NO_DATA; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java new file mode 100644 index 0000000000..5bddbc4387 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Schema(name = "AlarmRuleNumericFilterPredicate") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NumericFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private NumericOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NUMERIC; + } + + @Schema(name = "AlarmRuleNumericOperation") + 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/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java new file mode 100644 index 0000000000..1d2a3785db --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Schema(name = "AlarmRuleSimpleKeyFilterPredicate") +public interface SimpleKeyFilterPredicate extends KeyFilterPredicate { + + AlarmConditionValue getValue(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java new file mode 100644 index 0000000000..602730b7e0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Schema(name = "AlarmRuleStringFilterPredicate") +@Data +public class StringFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private StringOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + private boolean ignoreCase; + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") + @Override + public FilterPredicateType getType() { + return FilterPredicateType.STRING; + } + + @Schema(name= "AlarmRuleStringOperation") + public enum StringOperation { + EQUAL, + NOT_EQUAL, + STARTS_WITH, + ENDS_WITH, + CONTAINS, + NOT_CONTAINS, + IN, + NOT_IN + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java new file mode 100644 index 0000000000..127487b471 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.schedule; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "ANY_TIME", schema = AnyTimeSchedule.class), + @DiscriminatorMapping(value = "SPECIFIC_TIME", schema = SpecificTimeSchedule.class), + @DiscriminatorMapping(value = "CUSTOM", schema = CustomTimeSchedule.class) + } +) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), + @Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), + @Type(value = CustomTimeSchedule.class, name = "CUSTOM") +}) +public interface AlarmSchedule extends Serializable { + + @JsonIgnore + AlarmScheduleType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java new file mode 100644 index 0000000000..28093fd845 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.schedule; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public enum AlarmScheduleType { + ANY_TIME, + SPECIFIC_TIME, + CUSTOM +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java new file mode 100644 index 0000000000..873e1f882d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.schedule; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +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/alarm/rule/condition/schedule/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java new file mode 100644 index 0000000000..aea9db5be4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.schedule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema +@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/alarm/rule/condition/schedule/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java new file mode 100644 index 0000000000..d0756a3195 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.schedule; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + +@Schema +@Data +public class CustomTimeScheduleItem implements Serializable { + + 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/alarm/rule/condition/schedule/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java new file mode 100644 index 0000000000..a23dbe48a7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.alarm.rule.condition.schedule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Set; + +@Schema +@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/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index d2845cdd66..000f99f303 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.asset; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; 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.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @@ -125,6 +127,11 @@ public class Asset extends BaseDataWithAdditionalInfo implements HasLab this.customerId = customerId; } + @JsonIgnore + public EntityId getOwnerId() { + return customerId != null && !customerId.isNullUid() ? customerId : tenantId; + } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Asset Name in scope of Tenant", example = "Empire State Building") @Override public String getName() { @@ -162,7 +169,10 @@ public class Asset extends BaseDataWithAdditionalInfo implements HasLab this.assetProfileId = assetProfileId; } - @Schema(description = "Additional parameters of the asset",implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the asset. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Building A asset\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java index e5d0855989..a2d879ff0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java @@ -23,6 +23,7 @@ import lombok.ToString; import lombok.Value; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -66,4 +67,10 @@ public class AssetProfileInfo extends EntityInfo { this(profile.getId(), profile.getTenantId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId()); } + @Override + @Schema(implementation = AssetProfileId.class, description = "JSON object with the Asset Profile Id.") + public EntityId getId() { + return super.getId(); + } + } 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 fbb5e9cac7..27502eb70e 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 @@ -22,42 +22,203 @@ import java.util.Optional; public enum ActionType { - ADDED(TbMsgType.ENTITY_CREATED), // log entity - DELETED(TbMsgType.ENTITY_DELETED), // log string id - UPDATED(TbMsgType.ENTITY_UPDATED), // log entity - ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values - ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), // log attributes - TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), // log timeseries update - TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), // log timeseries - RPC_CALL, // log method and params - CREDENTIALS_UPDATED, // log new credentials - ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name - UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name - ACTIVATED, // log string id - SUSPENDED, // log string id - CREDENTIALS_READ(true), // log device id - ATTRIBUTES_READ(true), // log attributes + /** + * Entity created. Pushes {@link TbMsgType#ENTITY_CREATED} to rule engine. + * Audit log payload: full entity JSON. + */ + ADDED(TbMsgType.ENTITY_CREATED), + /** + * Entity deleted. Pushes {@link TbMsgType#ENTITY_DELETED} to rule engine. + * Audit log payload: entity string id. + */ + DELETED(TbMsgType.ENTITY_DELETED), + /** + * Entity updated. Pushes {@link TbMsgType#ENTITY_UPDATED} to rule engine. + * Audit log payload: full entity JSON. + */ + UPDATED(TbMsgType.ENTITY_UPDATED), + /** + * Server-side or shared attributes updated via API. + * Pushes {@link TbMsgType#ATTRIBUTES_UPDATED} to rule engine. + * Rule engine msg metadata includes {@code scope} ({@code SERVER_SCOPE} or {@code SHARED_SCOPE}). + * Rule engine msg data: key-value pairs of the updated attributes. + * Audit log payload: updated attributes and their values. + */ + ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED), + /** + * Attributes deleted via API. + * Pushes {@link TbMsgType#ATTRIBUTES_DELETED} to rule engine. + * Rule engine msg metadata includes {@code scope} ({@code SERVER_SCOPE} or {@code SHARED_SCOPE}). + * Rule engine msg data: {@code {"attributes": ["key1", "key2"]}}. + * Audit log payload: list of deleted attribute keys. + */ + ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), + /** + * Timeseries data saved via API (not from device transport). + * Pushes {@link TbMsgType#TIMESERIES_UPDATED} to rule engine. + * Rule engine msg data: {@code {"timeseries": [{"ts": ..., "values": {...}}, ...]}}. + * Audit log payload: timeseries entries. + */ + TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), + /** + * Timeseries data deleted via API. + * Pushes {@link TbMsgType#TIMESERIES_DELETED} to rule engine. + * Rule engine msg data: {@code {"timeseries": ["key1", ...], "startTs": ..., "endTs": ...}}. + * Audit log payload: deleted timeseries keys. + */ + TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), + /** + * RPC call to device. Does not push to rule engine (RPC has its own lifecycle messages). + * Audit log payload: RPC method and params. + */ + RPC_CALL, + /** + * Device credentials updated. Does not push to rule engine. + * Audit log payload: new credentials value. + */ + CREDENTIALS_UPDATED, + /** + * Entity assigned to a customer. Pushes {@link TbMsgType#ENTITY_ASSIGNED} to rule engine. + * Rule engine msg metadata includes {@code assignedCustomerId} and {@code assignedCustomerName}. + * Audit log payload: customer name. + */ + ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), + /** + * Entity unassigned from a customer. Pushes {@link TbMsgType#ENTITY_UNASSIGNED} to rule engine. + * Rule engine msg metadata includes {@code unassignedCustomerId} and {@code unassignedCustomerName}. + * Audit log payload: customer name. + */ + UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), + /** + * User account or integration activated. Does not push to rule engine. + * Audit log payload: entity string id. + */ + ACTIVATED, + /** + * User account or integration suspended. Does not push to rule engine. + * Audit log payload: entity string id. + */ + SUSPENDED, + /** + * Device credentials read. Read-only action. Does not push to rule engine. + * Audit log payload: device id. + */ + CREDENTIALS_READ(true), + /** + * Attributes read. Read-only action. Does not push to rule engine. + * Audit log payload: attribute keys read. + */ + ATTRIBUTES_READ(true), + /** + * Relation created or updated. Pushes {@link TbMsgType#RELATION_ADD_OR_UPDATE} to rule engine. + * Rule engine msg data: relation JSON ({@code from}, {@code to}, {@code type}, {@code typeGroup}). + */ RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE), + /** + * Relation deleted. Pushes {@link TbMsgType#RELATION_DELETED} to rule engine. + * Rule engine msg data: relation JSON ({@code from}, {@code to}, {@code type}, {@code typeGroup}). + */ RELATION_DELETED(TbMsgType.RELATION_DELETED), + /** + * All relations for an entity deleted. Pushes {@link TbMsgType#RELATIONS_DELETED} to rule engine. + * Rule engine msg data: empty JSON object. + */ RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED), - REST_API_RULE_ENGINE_CALL, // log call to rule engine from REST API + /** + * REST API call to rule engine. Does not push to rule engine directly + * (the REST controller creates a {@link TbMsgType#REST_API_REQUEST} message itself). + * Audit log payload: call details. + */ + REST_API_RULE_ENGINE_CALL, + /** + * Alarm acknowledged by a user. Pushes {@link TbMsgType#ALARM_ACK} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_ACK(TbMsgType.ALARM_ACK, true), + /** + * Alarm cleared by a user. Pushes {@link TbMsgType#ALARM_CLEAR} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true), + /** + * Alarm deleted by a user. Pushes {@link TbMsgType#ALARM_DELETE} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_DELETE(TbMsgType.ALARM_DELETE, true), + /** + * Alarm assigned to a user. Pushes {@link TbMsgType#ALARM_ASSIGNED} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true), + /** + * Alarm unassigned from a user. Pushes {@link TbMsgType#ALARM_UNASSIGNED} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true), + /** + * User logged in. Does not push to rule engine. + */ LOGIN, + /** + * User logged out. Does not push to rule engine. + */ LOGOUT, + /** + * User account locked out due to too many failed login attempts. Does not push to rule engine. + */ LOCKOUT, + /** + * Entity assigned from another tenant (incoming side of cross-tenant transfer). + * Pushes {@link TbMsgType#ENTITY_ASSIGNED_FROM_TENANT} to rule engine. + * Rule engine msg metadata includes {@code assignedFromTenantId} and {@code assignedFromTenantName}. + */ ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), + /** + * Entity assigned to another tenant (outgoing side of cross-tenant transfer). + * Pushes {@link TbMsgType#ENTITY_ASSIGNED_TO_TENANT} to rule engine. + * Rule engine msg metadata includes {@code assignedToTenantId} and {@code assignedToTenantName}. + */ ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT), + /** + * Device provisioned successfully. Pushes {@link TbMsgType#PROVISION_SUCCESS} to rule engine. + * Rule engine msg data: full device JSON. + */ PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS), + /** + * Device provisioning failed. Pushes {@link TbMsgType#PROVISION_FAILURE} to rule engine. + * Rule engine msg data: full device JSON. + */ PROVISION_FAILURE(TbMsgType.PROVISION_FAILURE), - ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name + /** + * Entity assigned to an Edge instance. Pushes {@link TbMsgType#ENTITY_ASSIGNED_TO_EDGE} to rule engine. + * Rule engine msg metadata includes {@code assignedEdgeId} and {@code assignedEdgeName}. + * Audit log payload: edge name. + */ + ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE), + /** + * Entity unassigned from an Edge instance. Pushes {@link TbMsgType#ENTITY_UNASSIGNED_FROM_EDGE} to rule engine. + * Rule engine msg metadata includes {@code unassignedEdgeId} and {@code unassignedEdgeName}. + */ UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), + /** + * Comment added to an alarm. Pushes {@link TbMsgType#COMMENT_CREATED} to rule engine. + * Rule engine msg metadata includes {@code comment} (JSON string of the AlarmComment object). + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ADDED_COMMENT(TbMsgType.COMMENT_CREATED), + /** + * Alarm comment updated. Pushes {@link TbMsgType#COMMENT_UPDATED} to rule engine. + * Rule engine msg metadata includes {@code comment} (JSON string of the AlarmComment object). + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED), + /** + * Alarm comment deleted. Does not push to rule engine. + */ DELETED_COMMENT, + /** + * SMS sent. Does not push to rule engine. + */ SMS_SENT; @Getter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java new file mode 100644 index 0000000000..bd0c76bc05 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java @@ -0,0 +1,166 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasAdditionalInfo; +import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmRuleDefinition extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo { + + private TenantId tenantId; + private EntityId entityId; + + @NoXss + @Length(fieldName = "name") + @Schema(description = "User defined name of the alarm rule.") + private String name; + @Deprecated + @Schema(description = "Enable/disable debug. ", example = "false", deprecated = true) + private boolean debugMode; + @Schema(description = "Debug settings object.") + private DebugSettings debugSettings; + @Schema(description = "Version of alarm rule configuration.", example = "0") + private int configurationVersion; + @Schema(implementation = AlarmCalculatedFieldConfiguration.class) + @Valid + @NotNull + private AlarmCalculatedFieldConfiguration configuration; + private Long version; + @NoXss + @Schema(description = "Additional parameters of the alarm rule. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"High temperature alarm rule\"}") + private JsonNode additionalInfo; + + public AlarmRuleDefinition() {} + + public AlarmRuleDefinition(CalculatedFieldId id) { + super(id); + } + + public AlarmRuleDefinition(AlarmRuleDefinition alarmRuleDefinition) { + super(alarmRuleDefinition); + this.tenantId = alarmRuleDefinition.tenantId; + this.entityId = alarmRuleDefinition.entityId; + this.name = alarmRuleDefinition.name; + this.debugMode = alarmRuleDefinition.debugMode; + this.debugSettings = alarmRuleDefinition.debugSettings; + this.configurationVersion = alarmRuleDefinition.configurationVersion; + this.configuration = alarmRuleDefinition.configuration; + this.version = alarmRuleDefinition.version; + this.additionalInfo = alarmRuleDefinition.additionalInfo; + } + + @Schema(description = "JSON object with the Alarm Rule Id. Referencing non-existing Alarm Rule Id will cause error.") + @Override + public CalculatedFieldId getId() { + return super.getId(); + } + + @Schema(description = "Timestamp of the alarm rule creation, in milliseconds", example = "1609459200000", accessMode = Schema.AccessMode.READ_ONLY) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + // Getter is ignored for serialization + @JsonIgnore + public boolean isDebugMode() { + return debugMode; + } + + // Setter is annotated for deserialization + @JsonSetter + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + public CalculatedField toCalculatedField() { + CalculatedField cf = new CalculatedField(); + cf.setId(this.id); + cf.setCreatedTime(this.createdTime); + cf.setTenantId(this.tenantId); + cf.setEntityId(this.entityId); + cf.setType(CalculatedFieldType.ALARM); + cf.setName(this.name); + cf.setDebugMode(this.debugMode); + cf.setDebugSettings(this.debugSettings); + cf.setConfigurationVersion(this.configurationVersion); + cf.setConfiguration(this.configuration); + cf.setVersion(this.version); + cf.setAdditionalInfo(this.additionalInfo); + return cf; + } + + public static AlarmRuleDefinition fromCalculatedField(CalculatedField cf) { + AlarmRuleDefinition def = new AlarmRuleDefinition(); + def.setId(cf.getId()); + def.setCreatedTime(cf.getCreatedTime()); + def.setTenantId(cf.getTenantId()); + def.setEntityId(cf.getEntityId()); + def.setName(cf.getName()); + def.setDebugMode(cf.isDebugMode()); + def.setDebugSettings(cf.getDebugSettings()); + def.setConfigurationVersion(cf.getConfigurationVersion()); + if (!(cf.getConfiguration() instanceof AlarmCalculatedFieldConfiguration config)) { + throw new IllegalArgumentException("Expected ALARM calculated field, got " + cf.getType()); + } + def.setConfiguration(config); + def.setVersion(cf.getVersion()); + def.setAdditionalInfo(cf.getAdditionalInfo()); + return def; + } + + @Override + public String toString() { + return new StringBuilder() + .append("AlarmRuleDefinition[") + .append("tenantId=").append(tenantId) + .append(", entityId=").append(entityId) + .append(", name='").append(name) + .append(", configurationVersion=").append(configurationVersion) + .append(", configuration=").append(configuration) + .append(", additionalInfo=").append(additionalInfo) + .append(", version=").append(version) + .append(", createdTime=").append(createdTime) + .append(", id=").append(id).append(']') + .toString(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java new file mode 100644 index 0000000000..145dfc4081 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class AlarmRuleDefinitionInfo extends AlarmRuleDefinition { + + private String entityName; + + public AlarmRuleDefinitionInfo(AlarmRuleDefinition alarmRuleDefinition, String entityName) { + super(alarmRuleDefinition); + this.entityName = entityName; + } + + public static AlarmRuleDefinitionInfo fromCalculatedFieldInfo(CalculatedFieldInfo cfi) { + AlarmRuleDefinition def = AlarmRuleDefinition.fromCalculatedField(cfi); + return new AlarmRuleDefinitionInfo(def, cfi.getEntityName()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index 5ab8e45b93..2d3928ee88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -17,12 +17,15 @@ package org.thingsboard.server.common.data.cf; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasAdditionalInfo; import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; @@ -36,15 +39,31 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; -import java.io.Serial; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; @Schema @Data @EqualsAndHashCode(callSuper = true) -public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings { - - @Serial - private static final long serialVersionUID = 4491966747773381420L; +public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo { + + public static final Map> SUPPORTED_ENTITIES = Map.of( + EntityType.DEVICE, CalculatedFieldType.all, + EntityType.ASSET, CalculatedFieldType.all, + EntityType.DEVICE_PROFILE, CalculatedFieldType.all, + EntityType.ASSET_PROFILE, CalculatedFieldType.all, + EntityType.CUSTOMER, Set.of(CalculatedFieldType.ALARM) + ); + + public static final Set SUPPORTED_REFERENCED_ENTITIES = Collections.unmodifiableSet(EnumSet.of( + EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT + )); + + public static boolean isSupportedRefEntity(EntityId entity) { + return SUPPORTED_REFERENCED_ENTITIES.contains(entity.getEntityType()); + } private TenantId tenantId; private EntityId entityId; @@ -63,30 +82,21 @@ public class CalculatedField extends BaseData implements HasN private DebugSettings debugSettings; @Schema(description = "Version of calculated field configuration.", example = "0") private int configurationVersion; - @Schema(implementation = SimpleCalculatedFieldConfiguration.class) + @Schema(implementation = CalculatedFieldConfiguration.class) + @Valid + @NotNull private CalculatedFieldConfiguration configuration; - @Getter - @Setter private Long version; + @NoXss + @Schema(description = "Additional parameters of the calculated field") + private JsonNode additionalInfo; - public CalculatedField() { - super(); - } + public CalculatedField() {} public CalculatedField(CalculatedFieldId id) { super(id); } - public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) { - this.tenantId = tenantId; - this.entityId = entityId; - this.type = type; - this.name = name; - this.configurationVersion = configurationVersion; - this.configuration = configuration; - this.version = version; - } - public CalculatedField(CalculatedField calculatedField) { super(calculatedField); this.tenantId = calculatedField.tenantId; @@ -98,6 +108,7 @@ public class CalculatedField extends BaseData implements HasN this.configurationVersion = calculatedField.configurationVersion; this.configuration = calculatedField.configuration; this.version = calculatedField.version; + this.additionalInfo = calculatedField.additionalInfo; } @Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.") @@ -134,6 +145,7 @@ public class CalculatedField extends BaseData implements HasN .append(", name='").append(name) .append(", configurationVersion=").append(configurationVersion) .append(", configuration=").append(configuration) + .append(", additionalInfo=").append(additionalInfo) .append(", version=").append(version) .append(", createdTime=").append(createdTime) .append(", id=").append(id).append(']') diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldEventType.java new file mode 100644 index 0000000000..e671bfd27b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldEventType.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +public enum CalculatedFieldEventType { + + INITIALIZED, + UPDATED, + + TENANT_PROFILE_UPDATED, + OWNER_CHANGED, + RELATION_ADD_OR_UPDATE, + RELATION_DELETED, + + REEVALUATION_MSG + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java new file mode 100644 index 0000000000..81a50e69c4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import lombok.Builder; +import lombok.Data; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Set; +import java.util.UUID; + +@Data +@Builder +public class CalculatedFieldFilter { + + @NonNull + private final Set types; + @NonNull + private final Set entityTypes; + @Nullable + private final Set entityIds; + @Nullable + private final Set names; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldInfo.java new file mode 100644 index 0000000000..68b60d30a5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldInfo.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class CalculatedFieldInfo extends CalculatedField { + + private String entityName; + + public CalculatedFieldInfo(CalculatedField calculatedField, String entityName) { + super(calculatedField); + this.entityName = entityName; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java index bf2e6dcd22..809d5dfb31 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java @@ -15,52 +15,8 @@ */ package org.thingsboard.server.common.data.cf; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -@Schema -@Data -@EqualsAndHashCode(callSuper = true) -public class CalculatedFieldLink extends BaseData { - - private static final long serialVersionUID = 6492846246722091530L; - - private TenantId tenantId; - private EntityId entityId; - - @Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY) - private CalculatedFieldId calculatedFieldId; - - public CalculatedFieldLink() { - super(); - } - - public CalculatedFieldLink(CalculatedFieldLinkId id) { - super(id); - } - - public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) { - this.tenantId = tenantId; - this.entityId = entityId; - this.calculatedFieldId = calculatedFieldId; - } - - @Override - public String toString() { - return new StringBuilder() - .append("CalculatedFieldLink[") - .append("tenantId=").append(tenantId) - .append(", entityId=").append(entityId) - .append(", calculatedFieldId=").append(calculatedFieldId) - .append(", createdTime=").append(createdTime) - .append(", id=").append(id).append(']') - .toString(); - } - -} +public record CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java index c533aad712..fb944344e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java @@ -15,8 +15,20 @@ */ package org.thingsboard.server.common.data.cf; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + public enum CalculatedFieldType { - SIMPLE, SCRIPT + SIMPLE, + SCRIPT, + GEOFENCING, + ALARM, + PROPAGATION, + RELATED_ENTITIES_AGGREGATION, + ENTITY_AGGREGATION; + + public static final Set all = Collections.unmodifiableSet(EnumSet.allOf(CalculatedFieldType.class)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..d22666723b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.apache.commons.lang3.tuple.Pair; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.util.CollectionsUtil; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.stream.Stream; + +import static java.util.Map.Entry.comparingByKey; + +@Schema +@Data +public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { + + private Map arguments; + + @Valid + @NotEmpty + private Map createRules; + @Valid + private AlarmRule clearRule; + + private boolean propagate; + private boolean propagateToOwner; + private boolean propagateToTenant; + private List propagateRelationTypes; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ALARM; + } + + @Override + public Output getOutput() { + return null; + } + + @JsonIgnore + @Override + public boolean requiresScheduledReevaluation() { + return getAllRules().anyMatch(entry -> entry.getValue().requiresScheduledReevaluation()); + } + + @JsonIgnore + public Stream> getAllRules() { + Stream> rules = createRules.entrySet().stream() + .map(entry -> Pair.of(entry.getKey(), entry.getValue())); + if (clearRule != null) { + rules = Stream.concat(rules, Stream.of(Pair.of(null, clearRule))); + } + return rules.sorted(comparingByKey(Comparator.nullsLast(Comparator.naturalOrder()))); + } + + public boolean rulesEqual(AlarmCalculatedFieldConfiguration other, BiPredicate equalityCheck) { + List> thisRules = this.getAllRules().toList(); + List> otherRules = other.getAllRules().toList(); + return CollectionsUtil.elementsEqual(thisRules, otherRules, (thisRule, otherRule) -> { + if (!Objects.equals(thisRule.getKey(), otherRule.getKey())) { + return false; + } + return equalityCheck.test(thisRule.getValue(), otherRule.getValue()); + }); + } + + public boolean propagationSettingsEqual(AlarmCalculatedFieldConfiguration other) { + return this.propagate == other.propagate && + this.propagateToOwner == other.propagateToOwner && + this.propagateToTenant == other.propagateToTenant && + Objects.equals(this.propagateRelationTypes, other.propagateRelationTypes); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index f4e050fa75..b41e296cba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -16,20 +16,39 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.lang.Nullable; import org.thingsboard.server.common.data.id.EntityId; +@Schema @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class Argument { @Nullable private EntityId refEntityId; + private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; private ReferencedEntityKey refEntityKey; private String defaultValue; private Integer limit; private Long timeWindow; + public boolean hasDynamicSource() { + return refDynamicSourceConfiguration != null; + } + + public boolean hasRelationQuerySource() { + return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.RELATION_PATH_QUERY; + } + + public boolean hasOwnerSource() { + return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.CURRENT_OWNER; + } + + public boolean hasTsRollingArgument() { + return ArgumentType.TS_ROLLING.equals(refEntityKey.getType()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..852eefc4a4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + + @Valid + @NotEmpty + Map getArguments(); + + default Set getReferencedEntities() { + var args = getArguments(); + if (args == null) { + return Collections.emptySet(); + } + return args.values().stream() + .map(Argument::getRefEntityId) + .filter(Objects::nonNull) + .collect(toSet()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java new file mode 100644 index 0000000000..4fbac188bd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AttributesImmediateOutputStrategy implements AttributesOutputStrategy { + + private boolean sendAttributesUpdatedNotification; + private boolean updateAttributesOnlyOnValueChange; + + private boolean saveAttribute; + private boolean sendWsUpdate; + private boolean processCfs; + + @Override + public OutputStrategyType getType() { + return OutputStrategyType.IMMEDIATE; + } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean saveTimeSeriesUpdated = saveAttribute != otherStrategy.isSaveAttribute(); + boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate(); + boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean updateAttrOnValueChangedChanged = updateAttributesOnlyOnValueChange != otherStrategy.isUpdateAttributesOnlyOnValueChange(); + boolean sendAttrUpdatedNotificationChanged = sendAttributesUpdatedNotification != otherStrategy.isSendAttributesUpdatedNotification(); + return updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java new file mode 100644 index 0000000000..e672c90ed0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import lombok.Data; +import org.thingsboard.server.common.data.AttributeScope; + +@Data +public class AttributesOutput implements Output { + + private String name; + private AttributeScope scope; + private Integer decimalsByDefault; + + private AttributesOutputStrategy strategy; + + public AttributesOutput() { + this.strategy = new AttributesRuleChainOutputStrategy(); + } + + @Override + public OutputType getType() { + return OutputType.ATTRIBUTES; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java new file mode 100644 index 0000000000..22e566e39d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "IMMEDIATE", schema = AttributesImmediateOutputStrategy.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = AttributesRuleChainOutputStrategy.class) + } +) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AttributesImmediateOutputStrategy.class, name = "IMMEDIATE"), + @JsonSubTypes.Type(value = AttributesRuleChainOutputStrategy.class, name = "RULE_CHAIN"), +}) +public interface AttributesOutputStrategy extends OutputStrategy { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java new file mode 100644 index 0000000000..935d97fabc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema +@Data +@NoArgsConstructor +public class AttributesRuleChainOutputStrategy implements AttributesOutputStrategy { + + @Override + public OutputStrategyType getType() { + return OutputStrategyType.RULE_CHAIN; + } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + return !(other instanceof AttributesRuleChainOutputStrategy); + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + return false; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index dbb7856e16..0ba9db43cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -15,47 +15,34 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; +@Schema @Data -public abstract class BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { +public abstract class BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration { protected Map arguments; protected String expression; - protected Output output; - @Override - public List getReferencedEntities() { - return arguments.values().stream() - .map(Argument::getRefEntityId) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } + @NotNull + protected Output output; @Override - public List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { - return getReferencedEntities().stream() - .filter(referencedEntity -> !referencedEntity.equals(cfEntityId)) - .map(referencedEntityId -> buildCalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId)) - .collect(Collectors.toList()); + public void validate() { + baseCalculatedFieldRestriction(); + if (arguments.values().stream().anyMatch(Argument::hasRelationQuerySource)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support relation query configuration!"); + } } - @Override - public CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { - CalculatedFieldLink link = new CalculatedFieldLink(); - link.setTenantId(tenantId); - link.setEntityId(referencedEntityId); - link.setCalculatedFieldId(calculatedFieldId); - return link; + protected void baseCalculatedFieldRestriction() { + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java new file mode 100644 index 0000000000..192694b32a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +public enum CFArgumentDynamicSourceType { + + CURRENT_OWNER, + RELATION_PATH_QUERY + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 2bf7e0a403..73d130c16f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -18,24 +18,49 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Collections; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "SCRIPT", schema = ScriptCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "GEOFENCING", schema = GeofencingCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "ALARM", schema = AlarmCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "PROPAGATION", schema = PropagationCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "RELATED_ENTITIES_AGGREGATION", schema = RelatedEntitiesAggregationCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "ENTITY_AGGREGATION", schema = EntityAggregationCalculatedFieldConfiguration.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT") + @Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), + @Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT"), + @Type(value = GeofencingCalculatedFieldConfiguration.class, name = "GEOFENCING"), + @Type(value = AlarmCalculatedFieldConfiguration.class, name = "ALARM"), + @Type(value = PropagationCalculatedFieldConfiguration.class, name = "PROPAGATION"), + @Type(value = RelatedEntitiesAggregationCalculatedFieldConfiguration.class, name = "RELATED_ENTITIES_AGGREGATION"), + @Type(value = EntityAggregationCalculatedFieldConfiguration.class, name = "ENTITY_AGGREGATION") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CalculatedFieldConfiguration { @@ -43,19 +68,29 @@ public interface CalculatedFieldConfiguration { @JsonIgnore CalculatedFieldType getType(); - Map getArguments(); - - String getExpression(); - - void setExpression(String expression); - Output getOutput(); + default void validate() {} + @JsonIgnore - List getReferencedEntities(); + default Set getReferencedEntities() { + return Collections.emptySet(); + } + + default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { + return new CalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId); + } - List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId); + default List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { + return getReferencedEntities().stream() + .filter(referencedEntity -> !referencedEntity.equals(cfEntityId)) + .map(referencedEntityId -> buildCalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId)) + .collect(Collectors.toList()); + } - CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId); + @JsonIgnore + default boolean requiresScheduledReevaluation() { + return false; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java new file mode 100644 index 0000000000..72b7f88bf8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "RELATION_PATH_QUERY", schema = RelationPathQueryDynamicSourceConfiguration.class), + @DiscriminatorMapping(value = "CURRENT_OWNER", schema = CurrentOwnerDynamicSourceConfiguration.class) + } +) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY"), + @JsonSubTypes.Type(value = CurrentOwnerDynamicSourceConfiguration.class, name = "CURRENT_OWNER") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface CfArgumentDynamicSourceConfiguration { + + @JsonIgnore + CFArgumentDynamicSourceType getType(); + + default void validate() {} + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java new file mode 100644 index 0000000000..61efa11557 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import lombok.Data; + +@Data +public class CurrentOwnerDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.CURRENT_OWNER; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..5b25d7ca11 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +public interface ExpressionBasedCalculatedFieldConfiguration extends ArgumentsBasedCalculatedFieldConfiguration { + + String getExpression(); + + void setExpression(String expression); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasRelationPathLevel.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasRelationPathLevel.java new file mode 100644 index 0000000000..0a71bb7bad --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasRelationPathLevel.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +public interface HasRelationPathLevel { + + RelationPathLevel getRelation(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasUseLatestTsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasUseLatestTsConfig.java new file mode 100644 index 0000000000..f8fc449171 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasUseLatestTsConfig.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +public interface HasUseLatestTsConfig { + + boolean isUseLatestTs(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index 1339678df7..2ee6901e9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -15,17 +15,69 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.AttributeScope; -@Data +import java.util.Objects; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "TIME_SERIES", schema = TimeSeriesOutput.class), + @DiscriminatorMapping(value = "ATTRIBUTES", schema = AttributesOutput.class) + } +) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TimeSeriesOutput.class, name = "TIME_SERIES"), + @JsonSubTypes.Type(value = AttributesOutput.class, name = "ATTRIBUTES") +}) @JsonInclude(JsonInclude.Include.NON_NULL) -public class Output { +@JsonIgnoreProperties(ignoreUnknown = true) +public interface Output { + + @JsonIgnore + OutputType getType(); + + String getName(); + + OutputStrategy getStrategy(); + + default AttributeScope getScope() { + return null; + } + + Integer getDecimalsByDefault(); + + void setDecimalsByDefault(Integer decimalsByDefault); - private String name; - private OutputType type; - private AttributeScope scope; - private Integer decimalsByDefault; + default boolean hasContextOnlyChanges(Output other) { + if (!getType().equals(other.getType())) { + return true; + } + if (!Objects.equals(getName(), other.getName())) { + return true; + } + if (getScope() != (other.getScope())) { + return true; + } + if (!Objects.equals(getDecimalsByDefault(), other.getDecimalsByDefault())) { + return true; + } + if (getStrategy().hasContextOnlyChanges(other.getStrategy())) { + return true; + } + return false; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java new file mode 100644 index 0000000000..c34c1040e2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public interface OutputStrategy { + + @JsonIgnore + OutputStrategyType getType(); + + boolean hasContextOnlyChanges(OutputStrategy other); + + boolean hasRefreshContextOnlyChanges(OutputStrategy other); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java new file mode 100644 index 0000000000..454f312ebd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +public enum OutputStrategyType { + + IMMEDIATE, RULE_CHAIN + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..7390424b3d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.List; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements HasRelationPathLevel, HasUseLatestTsConfig { + + public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx"; + + @Valid + @NotNull + private RelationPathLevel relation; + + private boolean applyExpressionToResolvedArguments; + + @Override + @JsonIgnore + public boolean isUseLatestTs() { + return output.getType() == OutputType.TIME_SERIES; + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.PROPAGATION; + } + + @Override + public void validate() { + baseCalculatedFieldRestriction(); + propagationRestriction(); + if (!applyExpressionToResolvedArguments) { + arguments.forEach((name, argument) -> { + if (!currentEntitySource(argument)) { + throw new IllegalArgumentException("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + if (argument.getRefEntityKey() == null) { + throw new IllegalArgumentException("Argument: '" + name + "' doesn't have reference entity key configured!"); + } + if (argument.getRefEntityKey().getType() == ArgumentType.TS_ROLLING) { + throw new IllegalArgumentException("Argument type: 'Time series rolling' detected for argument: '" + name + "'. " + + "Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); + } + }); + } else { + boolean noneMatchCurrentEntitySource = arguments.entrySet() + .stream() + .noneMatch(entry -> currentEntitySource(entry.getValue())); + if (noneMatchCurrentEntitySource) { + throw new IllegalArgumentException("At least one argument must be configured with the 'Current entity' " + + "source entity type for 'Expression result' propagation mode!"); + } + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("Expression must be specified for 'Expression result' propagation mode!"); + } + } + } + + public Argument toPropagationArgument() { + var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration.setLevels(List.of(relation)); + var propagationArgument = new Argument(); + propagationArgument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + return propagationArgument; + } + + private void propagationRestriction() { + if (arguments.entrySet().stream().anyMatch(entry -> entry.getKey().equals(PROPAGATION_CONFIG_ARGUMENT))) { + throw new IllegalArgumentException("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); + } + } + + private boolean currentEntitySource(Argument argument) { + return argument.getRefEntityId() == null && argument.getRefDynamicSourceConfiguration() == null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java index 59627751d0..fd36d1bd61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java @@ -16,10 +16,12 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.AttributeScope; +@Schema @Data @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java new file mode 100644 index 0000000000..b59f69afc1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.util.CollectionsUtil; + +import java.util.List; + +@Data +public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + + @ArraySchema(schema = @Schema(implementation = RelationPathLevel.class)) + private List levels; + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.RELATION_PATH_QUERY; + } + + @Override + public void validate() { + if (CollectionsUtil.isEmpty(levels)) { + throw new IllegalArgumentException("At least one relation level must be specified!"); + } + levels.forEach(RelationPathLevel::validate); + } + + public List resolveEntityIds(List relations) { + EntitySearchDirection lastLevelDirection = getLastLevel().direction(); + return switch (lastLevelDirection) { + case FROM -> relations.stream().map(EntityRelation::getTo).toList(); + case TO -> relations.stream().map(EntityRelation::getFrom).toList(); + }; + } + + public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { + if (levels.size() > maxAllowedRelationLevel) { + throw new IllegalArgumentException("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); + } + } + + public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) { + return new EntityRelationPathQuery(entityId, levels); + } + + private RelationPathLevel getLastLevel() { + return levels.get(levels.size() - 1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..f78ff72af0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import jakarta.validation.constraints.PositiveOrZero; + +public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + + boolean isScheduledUpdateEnabled(); + + @PositiveOrZero + Integer getScheduledUpdateInterval(); + + void setScheduledUpdateInterval(Integer interval); + + default void validate(long minAllowedScheduledUpdateInterval) { + if (getScheduledUpdateInterval() < minAllowedScheduledUpdateInterval) { + throw new IllegalArgumentException("Scheduled update interval (" + getScheduledUpdateInterval() + + " seconds) is less than minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval + " seconds"); + } + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java index c4475026de..40c28967b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@Schema @EqualsAndHashCode(callSuper = true) -public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { +public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { @Override public CalculatedFieldType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index cb73f0ae65..e3ca07b5be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@Schema @EqualsAndHashCode(callSuper = true) -public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { +public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration, HasUseLatestTsConfig { private boolean useLatestTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java new file mode 100644 index 0000000000..33be67e5a6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TimeSeriesImmediateOutputStrategy implements TimeSeriesOutputStrategy { + + private long ttl; + + private boolean saveTimeSeries; + private boolean saveLatest; + private boolean sendWsUpdate; + private boolean processCfs; + + @Override + public OutputStrategyType getType() { + return OutputStrategyType.IMMEDIATE; + } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean saveTimeSeriesUpdated = saveTimeSeries != otherStrategy.isSaveTimeSeries(); + boolean saveLatestUpdated = saveLatest != otherStrategy.isSaveLatest(); + boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate(); + boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) { + return true; + } + return ttl != otherStrategy.getTtl(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java new file mode 100644 index 0000000000..942a734908 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema +@Data +public class TimeSeriesOutput implements Output { + + private String name; + private Integer decimalsByDefault; + + private TimeSeriesOutputStrategy strategy; + + public TimeSeriesOutput() { + this.strategy = new TimeSeriesRuleChainOutputStrategy(); + } + + @Override + public OutputType getType() { + return OutputType.TIME_SERIES; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java new file mode 100644 index 0000000000..aca968f422 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "IMMEDIATE", schema = TimeSeriesImmediateOutputStrategy.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = TimeSeriesRuleChainOutputStrategy.class) + } +) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TimeSeriesImmediateOutputStrategy.class, name = "IMMEDIATE"), + @JsonSubTypes.Type(value = TimeSeriesRuleChainOutputStrategy.class, name = "RULE_CHAIN") +}) +public interface TimeSeriesOutputStrategy extends OutputStrategy { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java new file mode 100644 index 0000000000..1d68d3e8ee --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema +@Data +@NoArgsConstructor +public class TimeSeriesRuleChainOutputStrategy implements TimeSeriesOutputStrategy { + + @Override + public OutputStrategyType getType() { + return OutputStrategyType.RULE_CHAIN; + } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + return !(other instanceof TimeSeriesRuleChainOutputStrategy); + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + return false; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java new file mode 100644 index 0000000000..c29ef61beb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation; + +public enum AggFunction { + MIN, MAX, SUM, AVG, COUNT, COUNT_UNIQUE +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java new file mode 100644 index 0000000000..ce29e20ea4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AggFunctionInput implements AggInput { + + private String function; + + @Override + public String getType() { + return "function"; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java new file mode 100644 index 0000000000..4c9c0915f3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation; + +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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "key", schema = AggKeyInput.class), + @DiscriminatorMapping(value = "function", schema = AggFunctionInput.class) + } +) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AggKeyInput.class, name = "key"), + @JsonSubTypes.Type(value = AggFunctionInput.class, name = "function") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface AggInput { + + @JsonIgnore + String getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java new file mode 100644 index 0000000000..55aed3b59a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AggKeyInput implements AggInput { + + private String key; + + @Override + public String getType() { + return "key"; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java new file mode 100644 index 0000000000..8a322bd9ce --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AggMetric { + + private AggFunction function; + private String filter; + private AggInput input; + private Double defaultValue; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..7cb833ab3a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel; +import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.Map; + +@Schema +@Data +public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasRelationPathLevel, HasUseLatestTsConfig { + + @NotNull + private RelationPathLevel relation; + private Map arguments; + private long deduplicationIntervalInSec; + @Valid + @NotEmpty + private Map metrics; + @Valid + @NotNull + private Output output; + private boolean useLatestTs; + + private Integer scheduledUpdateInterval; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + } + + @Override + public boolean isScheduledUpdateEnabled() { + return true; + } + + @Override + public void validate() { + validateRelation(); + validateArguments(); + validateMetrics(); + } + + private void validateRelation() { + if (relation == null) { + throw new IllegalArgumentException("Relation must be specified!"); + } + relation.validate(); + } + + private void validateArguments() { + if (arguments == null || arguments.isEmpty()) { + throw new IllegalArgumentException("Arguments map cannot be empty."); + } + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } + if (arguments.values().stream().anyMatch(Argument::hasTsRollingArgument)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support TS_ROLLING arguments."); + } + } + + private void validateMetrics() { + if (metrics == null || metrics.isEmpty()) { + throw new IllegalArgumentException("Metrics map cannot be empty."); + } + + for (AggMetric metric : metrics.values()) { + if (metric.getInput() instanceof AggKeyInput aggKeyInput) { + if (!arguments.containsKey(aggKeyInput.getKey())) { + throw new IllegalArgumentException( + "Metric references unknown argument: '" + aggKeyInput.getKey() + "'." + ); + } + } + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..70be733928 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; + +import java.util.Map; + +@Schema +@Data +public class EntityAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { + + private Map arguments; + @Valid + @NotEmpty + private Map metrics; + @Valid + @NotNull + private AggInterval interval; + @Valid + private Watermark watermark; + private boolean produceIntermediateResult; + @Valid + @NotNull + private Output output; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ENTITY_AGGREGATION; + } + + @Override + public void validate() { + validateArguments(); + validateMetrics(); + validateInterval(); + } + + private void validateArguments() { + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } + if (arguments.values().stream().anyMatch(argument -> !ArgumentType.TS_LATEST.equals(argument.getRefEntityKey().getType()))) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' support only TS_LATEST arguments."); + } + } + + private void validateMetrics() { + if (metrics == null || metrics.isEmpty()) { + throw new IllegalArgumentException("Metrics map cannot be empty."); + } + + for (AggMetric metric : metrics.values()) { + if (metric.getInput() instanceof AggKeyInput aggKeyInput) { + if (!arguments.containsKey(aggKeyInput.getKey())) { + throw new IllegalArgumentException( + "Metric references unknown argument: '" + aggKeyInput.getKey() + "'." + ); + } + } else { + throw new IllegalArgumentException("Metric key can only refer to argument."); + } + } + } + + private void validateInterval() { + if (interval == null) { + throw new IllegalArgumentException("Interval must be defined."); + } + interval.validate(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java new file mode 100644 index 0000000000..55ffb0243b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "HOUR", schema = HourInterval.class), + @DiscriminatorMapping(value = "DAY", schema = DayInterval.class), + @DiscriminatorMapping(value = "WEEK", schema = WeekInterval.class), + @DiscriminatorMapping(value = "WEEK_SUN_SAT", schema = WeekSunSatInterval.class), + @DiscriminatorMapping(value = "MONTH", schema = MonthInterval.class), + @DiscriminatorMapping(value = "QUARTER", schema = QuarterInterval.class), + @DiscriminatorMapping(value = "YEAR", schema = YearInterval.class), + @DiscriminatorMapping(value = "CUSTOM", schema = CustomInterval.class) + } +) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = HourInterval.class, name = "HOUR"), + @JsonSubTypes.Type(value = DayInterval.class, name = "DAY"), + @JsonSubTypes.Type(value = WeekInterval.class, name = "WEEK"), + @JsonSubTypes.Type(value = WeekSunSatInterval.class, name = "WEEK_SUN_SAT"), + @JsonSubTypes.Type(value = MonthInterval.class, name = "MONTH"), + @JsonSubTypes.Type(value = QuarterInterval.class, name = "QUARTER"), + @JsonSubTypes.Type(value = YearInterval.class, name = "YEAR"), + @JsonSubTypes.Type(value = CustomInterval.class, name = "CUSTOM") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface AggInterval { + + @JsonIgnore + AggIntervalType getType(); + + @JsonIgnore + ZoneId getZoneId(); + + @JsonIgnore + long getCurrentIntervalDurationMillis(); + + @JsonIgnore + long getCurrentIntervalStartTs(); + + long getDateTimeIntervalStartTs(ZonedDateTime dateTime); + + @JsonIgnore + long getCurrentIntervalEndTs(); + + long getDateTimeIntervalEndTs(ZonedDateTime dateTime); + + ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart); + + void validate(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalType.java new file mode 100644 index 0000000000..4f98bc08de --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalType.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +public enum AggIntervalType { + + HOUR, + DAY, + WEEK, + WEEK_SUN_SAT, + MONTH, + QUARTER, + YEAR, + CUSTOM + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java new file mode 100644 index 0000000000..8f77215568 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.TimeUnit; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@AllArgsConstructor +@NoArgsConstructor +public abstract class BaseAggInterval implements AggInterval { + + @NotBlank + protected String tz; + protected Long offsetSec; // delay seconds since start of interval + + @Override + public ZoneId getZoneId() { + return ZoneId.of(tz); + } + + protected long getOffsetSafe() { + return offsetSec != null ? offsetSec : 0L; + } + + @Override + public long getCurrentIntervalDurationMillis() { + return getCurrentIntervalEndTs() - getCurrentIntervalStartTs(); + } + + @Override + public long getCurrentIntervalStartTs() { + ZoneId zoneId = getZoneId(); + ZonedDateTime now = ZonedDateTime.now(zoneId); + return getDateTimeIntervalStartTs(now); + } + + @Override + public long getDateTimeIntervalStartTs(ZonedDateTime dateTime) { + long offset = getOffsetSafe(); + ZonedDateTime shiftedNow = dateTime.minusSeconds(offset); + ZonedDateTime alignedStart = getAlignedBoundary(shiftedNow, false); + ZonedDateTime actualStart = alignedStart.plusSeconds(offset); + return actualStart.toInstant().toEpochMilli(); + } + + @Override + public long getCurrentIntervalEndTs() { + ZoneId zoneId = getZoneId(); + ZonedDateTime now = ZonedDateTime.now(zoneId); + return getDateTimeIntervalEndTs(now); + } + + @Override + public long getDateTimeIntervalEndTs(ZonedDateTime dateTime) { + long offset = getOffsetSafe(); + ZonedDateTime shiftedNow = dateTime.minusSeconds(offset); + ZonedDateTime alignedEnd = getAlignedBoundary(shiftedNow, true); + ZonedDateTime actualEnd = alignedEnd.plusSeconds(offset); + return actualEnd.toInstant().toEpochMilli(); + } + + protected abstract ZonedDateTime alignToIntervalStart(ZonedDateTime reference); + + protected ZonedDateTime getAlignedBoundary(ZonedDateTime reference, boolean next) { + ZonedDateTime base = alignToIntervalStart(reference); + return next ? getNextIntervalStart(base) : base; + } + + @Override + public void validate() { + try { + getZoneId(); + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid timezone in interval: " + ex.getMessage()); + } + if (offsetSec != null) { + if (offsetSec < 0) { + throw new IllegalArgumentException("Offset cannot be negative."); + } + if (TimeUnit.SECONDS.toMillis(offsetSec) >= getCurrentIntervalDurationMillis()) { + throw new IllegalArgumentException("Offset must be greater than interval duration."); + } + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java new file mode 100644 index 0000000000..933f2569fb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Duration; +import java.time.ZonedDateTime; + +@Schema +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class CustomInterval extends BaseAggInterval { + + @NotNull + @Min(1) + private Long durationSec; + + public CustomInterval(String tz, Long offsetSec, Long durationSec) { + super(tz, offsetSec); + this.durationSec = durationSec; + } + + @Override + public AggIntervalType getType() { + return AggIntervalType.CUSTOM; + } + + @Override + public long getCurrentIntervalDurationMillis() { + return getDurationMillis(); + } + + private long getDurationMillis() { + return Duration.ofSeconds(durationSec).toMillis(); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + ZonedDateTime localMidnight = reference.toLocalDate().atStartOfDay(reference.getZone()); + long secondsFromMidnight = Duration.between(localMidnight, reference).getSeconds(); + long alignedSecondsFromMidnight = (secondsFromMidnight / durationSec) * durationSec; + return localMidnight.plusSeconds(alignedSecondsFromMidnight); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusSeconds(durationSec); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java new file mode 100644 index 0000000000..23cb648de2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@Schema +@Data +@NoArgsConstructor +public class DayInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.DAY; + } + + public DayInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusDays(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java new file mode 100644 index 0000000000..7a95f2d387 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@Schema +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class HourInterval extends BaseAggInterval { + + public HourInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + public AggIntervalType getType() { + return AggIntervalType.HOUR; + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.truncatedTo(ChronoUnit.HOURS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusHours(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java new file mode 100644 index 0000000000..d49ff9b335 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@Schema +@Data +@NoArgsConstructor +public class MonthInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.MONTH; + } + + public MonthInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusMonths(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java new file mode 100644 index 0000000000..a6f29f53f4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; + +@Schema +@Data +@NoArgsConstructor +public class QuarterInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.QUARTER; + } + + public QuarterInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + int month = reference.getMonthValue(); + int quarterStartMonth = ((month - 1) / 3) * 3 + 1; // 1, 4, 7, 10 + return ZonedDateTime.of( + LocalDate.of(reference.getYear(), quarterStartMonth, 1), + LocalTime.MIDNIGHT, + reference.getZone()); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusMonths(3); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/Watermark.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/Watermark.java new file mode 100644 index 0000000000..7f1a53fd57 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/Watermark.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Watermark { + + @Min(0) + private long duration; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java new file mode 100644 index 0000000000..fad2f072d3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; + +@Schema +@Data +@NoArgsConstructor +public class WeekInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.WEEK; + } + + public WeekInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusWeeks(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java new file mode 100644 index 0000000000..a3d5876b0e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; + +@Schema +@Data +@NoArgsConstructor +public class WeekSunSatInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.WEEK_SUN_SAT; + } + + public WeekSunSatInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)).truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusWeeks(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java new file mode 100644 index 0000000000..882aaa1e56 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; + +@Schema +@Data +@NoArgsConstructor +public class YearInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.YEAR; + } + + public YearInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return ZonedDateTime.of( + LocalDate.of(reference.getYear(), 1, 1), + LocalTime.MIDNIGHT, + reference.getZone()); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusYears(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java new file mode 100644 index 0000000000..83d6275441 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; + +import java.util.Map; + +@Data +public class EntityCoordinates { + + public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; + public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; + + @NotBlank + private final String latitudeKeyName; + @NotBlank + private final String longitudeKeyName; + + public Map toArguments() { + return Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(latitudeKeyName), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument(longitudeKeyName) + ); + } + + private Argument toArgument(String keyName) { + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey(keyName, ArgumentType.TS_LATEST, null)); + return argument; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..62ece3d2dc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +@Schema +@Data +public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasUseLatestTsConfig { + + @Valid + @NotNull + private EntityCoordinates entityCoordinates; + + @Valid + @NotNull + private Map zoneGroups; + + private boolean scheduledUpdateEnabled; + private Integer scheduledUpdateInterval; + + @NotNull + private Output output; + + @Override + @JsonIgnore + public boolean isUseLatestTs() { + return output.getType() == OutputType.TIME_SERIES; + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.GEOFENCING; + } + + @Override + @JsonIgnore + public Map getArguments() { + Map args = new HashMap<>(entityCoordinates.toArguments()); + zoneGroups.forEach((zgName, zgConfig) -> args.put(zgName, zgConfig.toArgument())); + return args; + } + + + @Override + public Set getReferencedEntities() { + return zoneGroups == null ? Collections.emptySet() : zoneGroups.values().stream() + .map(ZoneGroupConfiguration::getRefEntityId) + .filter(Objects::nonNull) + .collect(toSet()); + } + + @Override + public Output getOutput() { + return output; + } + + @Override + public void validate() { + if (scheduledUpdateEnabled && scheduledUpdateInterval == null) { + throw new IllegalArgumentException("Refresh interval is required when periodic zone group refresh is enabled."); + } + zoneGroups.forEach((key, value) -> value.validate(key)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java new file mode 100644 index 0000000000..973524c9f8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +public sealed interface GeofencingEvent + permits GeofencingTransitionEvent, GeofencingPresenceStatus { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java new file mode 100644 index 0000000000..e17d97aa3f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +import lombok.Getter; + +@Getter +public enum GeofencingPresenceStatus implements GeofencingEvent { + + INSIDE, OUTSIDE; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java new file mode 100644 index 0000000000..3acb67d12a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +public enum GeofencingReportStrategy { + + REPORT_TRANSITION_EVENTS_ONLY, + REPORT_PRESENCE_STATUS_ONLY, + REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java new file mode 100644 index 0000000000..a4af68a519 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +public enum GeofencingTransitionEvent implements GeofencingEvent { + ENTERED, LEFT +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java new file mode 100644 index 0000000000..cf2827a47e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.lang.Nullable; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CfArgumentDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ZoneGroupConfiguration { + + @Nullable + private EntityId refEntityId; + private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; + + @NotBlank + private final String perimeterKeyName; + + @NotNull + private final GeofencingReportStrategy reportStrategy; + private final boolean createRelationsWithMatchedZones; + + private String relationType; + private EntitySearchDirection direction; + + public void validate(String name) { + if (EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY.equals(name) || EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY.equals(name)) { + throw new IllegalArgumentException("Name '" + name + "' is reserved and cannot be used for zone group!"); + } + if (refDynamicSourceConfiguration != null) { + refDynamicSourceConfiguration.validate(); + } + if (!createRelationsWithMatchedZones) { + return; + } + if (StringUtils.isBlank(relationType)) { + throw new IllegalArgumentException("Relation type must be specified for '" + name + "' zone group!"); + } + if (direction == null) { + throw new IllegalArgumentException("Relation direction must be specified for '" + name + "' zone group!"); + } + } + + public boolean hasRelationQuerySource() { + return toArgument().hasRelationQuerySource(); + } + + public boolean hasCurrentOwnerSource() { + return toArgument().hasOwnerSource(); + } + + @JsonIgnore + public boolean isCfEntitySource(EntityId cfEntityId) { + if (refEntityId == null && refDynamicSourceConfiguration == null) { + return true; + } + return refEntityId != null && refEntityId.equals(cfEntityId); + } + + @JsonIgnore + public boolean isLinkedCfEntitySource(EntityId cfEntityId) { + return refEntityId != null && !refEntityId.equals(cfEntityId); + } + + public Argument toArgument() { + var argument = new Argument(); + argument.setRefEntityId(refEntityId); + argument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + argument.setRefEntityKey(new ReferencedEntityKey(perimeterKeyName, ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + return argument; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java new file mode 100644 index 0000000000..33a845074c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java @@ -0,0 +1,112 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.lwm2m; + +/** + * Enum representing predefined LwM2M Short Server Identifiers. + *

+ * See OMA Lightweight M2M Specification for details about the server identifier space. + */ +public enum Lwm2mServerIdentifier { + + /** + * Not used for identifying an LwM2M Server (0). + */ + NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN(0, "Bootstrap Short Server ID", false), + + /** + * Primary LwM2M Server Short Server ID (1). + * Upper boundary for valid LwM2M Server Identifiers (1–65534). + */ + PRIMARY_LWM2M_SERVER(1, "LwM2M Server Short Server ID", true), + + /** + * Maximum valid LwM2M Server ID (65534). + * Upper boundary for valid LwM2M Server Identifiers (1–65534). + */ + LWM2M_SERVER_MAX(65534, "LwM2M Server Short Server ID", true), + + /** + * Not used for identifying an LwM2M Server (65535). + * Reserved sentinel value representing "no server associated" or "invalid ID". + * MUST NOT be assigned to any LwM2M Server according to OMA-TS-LightweightM2M-Core, §6.2.1. + * OMA LwM2M Core / v1.2: Server / Short Server ID): «MAX_ID 65535 is a reserved value and MUST NOT be used for identifying an Object» + */ + NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX(65535, "Reserved sentinel value (no active server)", false); + + private final Integer id; + private final String description; + private final boolean isLwm2mServer; + + Lwm2mServerIdentifier(Integer id, String description, boolean isLwm2mServer) { + this.id = id; + this.description = description; + this.isLwm2mServer = isLwm2mServer; + } + + /** + * @return the integer value of this Short Server ID. + */ + public Integer getId() { + return id; + } + + /** + * @return a human-readable description of this Server ID. + */ + public String getDescription() { + return description; + } + + /** + * @return true if this ID represents a Lwm2m Server. + */ + public boolean isLwm2mServer() { + return isLwm2mServer; + } + + /** + * Checks whether a given ID represents a valid LwM2M Server (1–65534). + * @param id Short Server ID value. + * @return true if the ID belongs to a standard LwM2M Server. + */ + public static boolean isLwm2mServer(Integer id) { + return id != null && id >= PRIMARY_LWM2M_SERVER.id && id <= LWM2M_SERVER_MAX.id; + } + public static boolean isNotLwm2mServer(Integer id) { + return id == null || id < PRIMARY_LWM2M_SERVER.id || id > LWM2M_SERVER_MAX.id; + } + + /** + * Returns a {@link Lwm2mServerIdentifier} instance matching the given ID. + * @param id numeric ID. + * @return corresponding enum constant. + * @throws IllegalArgumentException if no constant matches the given ID. + */ + public static Lwm2mServerIdentifier fromId(Integer id) { + for (Lwm2mServerIdentifier s : values()) { + if (s.id == id) { + return s; + } + } + throw new IllegalArgumentException("Unknown Lwm2mServerIdentifier: " + id); + } + + @Override + public String toString() { + return name() + "(" + id + ") - " + description; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java index 3f23e4fe14..86a61e31b6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java @@ -18,6 +18,7 @@ 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 io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; @@ -25,6 +26,7 @@ import java.util.HashMap; import java.util.Map; @Data +@Schema public class CoapDeviceTransportConfiguration extends PowerSavingConfiguration implements DeviceTransportConfiguration { private static final long serialVersionUID = 6061442236008925609L; 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 index 85c1d83744..a0040adb4b 100644 --- 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 @@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileType; -@Schema +@Schema(description = "Default device configuration") @Data public class DefaultDeviceConfiguration implements DeviceConfiguration { 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 index ae7b2e10ab..022a0ca528 100644 --- 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 @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.device.data; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +@Schema @Data public class DefaultDeviceTransportConfiguration implements DeviceTransportConfiguration { 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 index 9fa449fdfa..6fe689d17c 100644 --- 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 @@ -19,12 +19,19 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceProfileType; import java.io.Serializable; -@Schema +@Schema( + description = "Device configuration", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -34,7 +41,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = DefaultDeviceConfiguration.class, name = "DEFAULT")}) public interface DeviceConfiguration extends Serializable { - @JsonIgnore + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Device profile type") DeviceProfileType getType(); } 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 index 84c8e3b463..fa7df3392d 100644 --- 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 @@ -19,12 +19,23 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceTransportType; import java.io.Serializable; -@Schema +@Schema( + description = "Configuration for device transport", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "MQTT", schema = MqttDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "COAP", schema = CoapDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "LWM2M", schema = Lwm2mDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "SNMP", schema = SnmpDeviceTransportConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -37,6 +48,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = Lwm2mDeviceTransportConfiguration.class, name = "LWM2M"), @JsonSubTypes.Type(value = SnmpDeviceTransportConfiguration.class, name = "SNMP")}) public interface DeviceTransportConfiguration extends Serializable { + @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 index 079ceffada..656262ab45 100644 --- 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 @@ -18,12 +18,14 @@ 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 io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import java.util.HashMap; import java.util.Map; +@Schema @Data public class Lwm2mDeviceTransportConfiguration extends PowerSavingConfiguration implements DeviceTransportConfiguration { 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 index 0adb842ec6..bf83fd31fc 100644 --- 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 @@ -18,12 +18,14 @@ 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 io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import java.util.HashMap; import java.util.Map; +@Schema @Data public class MqttDeviceTransportConfiguration implements DeviceTransportConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java index 5509845855..21f2b619d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.device.data; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; @Data +@Schema public class PowerSavingConfiguration implements Serializable { private static final long serialVersionUID = 2905389805488525362L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java index 8357b65f21..5c732b9e62 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.ToString; import org.thingsboard.server.common.data.DeviceTransportType; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.transport.snmp.AuthenticationProtocol; import org.thingsboard.server.common.data.transport.snmp.PrivacyProtocol; import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion; +@Schema @Data @ToString(of = {"host", "port", "protocolVersion"}) public class SnmpDeviceTransportConfiguration implements DeviceTransportConfiguration { 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 index a4da4ceae0..120df6f3b2 100644 --- 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.Data; @@ -23,13 +24,14 @@ import lombok.Data; import java.io.Serializable; import java.util.List; -@Schema +@Schema(hidden = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class AlarmCondition implements Serializable { @Valid - @Schema(description = "JSON array of alarm condition filters") + @ArraySchema(schema = @Schema(ref = "#/components/schemas/AlarmConditionFilter")) private List condition; @Schema(description = "JSON object representing alarm condition type") private AlarmConditionSpec spec; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index 17ca7c094f..96dfee9703 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -24,8 +24,9 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; -@Schema +@Schema(hidden = true) @Data +@Deprecated public class AlarmConditionFilter implements Serializable { @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java index f656ad2a73..df711fd2a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -23,6 +23,7 @@ import java.io.Serializable; @Schema @Data +@Deprecated public class AlarmConditionFilterKey implements Serializable { @Schema(description = "The key type", example = "TIME_SERIES") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java index 80fdadc68a..7e4ca78de7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmConditionKeyType { ATTRIBUTE, TIME_SERIES, 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 index 5bb1cd752f..fd6a12d223 100644 --- 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 @@ -19,9 +19,20 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; +@Schema( + description = "Specification for alarm conditions", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleAlarmConditionSpec.class), + @DiscriminatorMapping(value = "DURATION", schema = DurationAlarmConditionSpec.class), + @DiscriminatorMapping(value = "REPEATING", schema = RepeatingAlarmConditionSpec.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -31,6 +42,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "SIMPLE"), @JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "DURATION"), @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) +@Deprecated public interface AlarmConditionSpec extends Serializable { @JsonIgnore 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 index f8ce5947ed..b666bf821f 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmConditionSpecType { SIMPLE, 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 index d2401082de..7a9be0f302 100644 --- 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 @@ -21,12 +21,17 @@ import lombok.Data; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; import java.io.Serializable; -@Schema +@Schema(hidden = true) @Data +@Deprecated public class AlarmRule implements Serializable { + @Serial + private static final long serialVersionUID = -7617427132423304707L; + @Valid @Schema(description = "JSON object representing the alarm rule condition") private AlarmCondition condition; 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 index 00faaef712..884d9f13c5 100644 --- 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 @@ -18,10 +18,14 @@ 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; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.query.DynamicValue; import java.io.Serializable; +@Schema( + hidden = true +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -31,6 +35,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), @JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), @JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")}) +@Deprecated public interface AlarmSchedule extends Serializable { 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 index 56148844fa..1183f9ae63 100644 --- 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 @@ -15,6 +15,10 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(hidden = true) +@Deprecated public enum AlarmScheduleType { ANY_TIME, 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 index b33b56ba82..d1672af07e 100644 --- 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 @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data +@Schema public class AllowCreateNewDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { private final String provisionDeviceSecret; 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 index 57003396ef..70463dfbe7 100644 --- 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 @@ -15,8 +15,11 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.query.DynamicValue; +@Schema(hidden = true) +@Deprecated public class AnyTimeSchedule implements AlarmSchedule { @Override 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 index 7a1d6d1a95..935da4cbbe 100644 --- 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 @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data +@Schema public class CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { private final String provisionDeviceSecret; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java index b12d0bdf40..66d7f15fc4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java @@ -15,14 +15,18 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; +@Schema @Data public class CoapDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @Schema(implementation = CoapDeviceTypeConfiguration.class) private CoapDeviceTypeConfiguration coapDeviceTypeConfiguration; + @Schema private PowerSavingConfiguration clientSettings; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java index 66907288e9..12efa4a14c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java @@ -19,10 +19,20 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.CoapDeviceType; import java.io.Serializable; +@Schema( + description = "CoAP device type configuration", + discriminatorProperty = "coapDeviceType", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultCoapDeviceTypeConfiguration.class), + @DiscriminatorMapping(value = "EFENTO", schema = EfentoCoapDeviceTypeConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, 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 index cb22bc775f..cea1d76d36 100644 --- 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 @@ -15,12 +15,15 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.DynamicValue; import java.util.List; +@Schema(hidden = true) @Data +@Deprecated public class CustomTimeSchedule implements AlarmSchedule { private String timezone; 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 index 575f45dc17..d91c978ac5 100644 --- 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 @@ -15,11 +15,14 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; +@Schema(hidden = true) @Data +@Deprecated public class CustomTimeScheduleItem implements Serializable { private boolean enabled; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java index 716f733a0a..0455f9f370 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.CoapDeviceType; @Data +@Schema public class DefaultCoapDeviceTypeConfiguration implements CoapDeviceTypeConfiguration { private static final long serialVersionUID = -4287100699186773773L; + @Schema private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; @Override 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 index 5058024680..13cd6b4a58 100644 --- 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 @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileType; +@Schema(description = "Default device profile configuration") @Data public class DefaultDeviceProfileConfiguration implements DeviceProfileConfiguration { 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 index 012b09c8e1..f00922119a 100644 --- 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 @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +@Schema @Data public class DefaultDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { 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 index 0b18e8cf25..c793f6c452 100644 --- 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 @@ -28,6 +28,7 @@ import java.util.TreeMap; @Schema @Data +@Deprecated public class DeviceProfileAlarm implements Serializable { @Schema(description = "String value representing the alarm rule id", example = "highTemperatureAlarmID") 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 index 98546180e2..69c788539b 100644 --- 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 @@ -19,10 +19,19 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceProfileType; import java.io.Serializable; +@Schema( + description = "Device profile configuration", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceProfileConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, 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 index 636f546386..da7282f48a 100644 --- 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 @@ -36,7 +36,7 @@ public class DeviceProfileData implements Serializable { @Schema(description = "JSON object of provisioning strategy type per device profile") private DeviceProfileProvisionConfiguration provisionConfiguration; @Valid - @Schema(description = "JSON array of alarm rules configuration per device profile") + @Schema(hidden = true) 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 index 45ac5207f5..b94c2c9a28 100644 --- 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 @@ -19,10 +19,22 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import java.io.Serializable; +@Schema( + description = "Device profile provision configuration", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DISABLED", schema = DisabledDeviceProfileProvisionConfiguration.class), + @DiscriminatorMapping(value = "ALLOW_CREATE_NEW_DEVICES", schema = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class), + @DiscriminatorMapping(value = "CHECK_PRE_PROVISIONED_DEVICES", schema = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class), + @DiscriminatorMapping(value = "X509_CERTIFICATE_CHAIN", schema = X509CertificateChainProvisionConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -35,6 +47,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")}) public interface DeviceProfileProvisionConfiguration extends Serializable { + @Schema(description = "Provision device secret", example = "secret123") String getProvisionDeviceSecret(); @JsonIgnore 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 index 17627ecc6b..ea035248ac 100644 --- 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 @@ -19,10 +19,23 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceTransportType; import java.io.Serializable; +@Schema( + description = "Configuration for device profile transport", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "MQTT", schema = MqttDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "LWM2M", schema = Lwm2mDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "COAP", schema = CoapDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "SNMP", schema = SnmpDeviceProfileTransportConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, 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 index 7492c077c8..df5883230a 100644 --- 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 @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data +@Schema public class DisabledDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { private final String provisionDeviceSecret; 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 index b23d5f9a6e..b40b8f2c82 100644 --- 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 @@ -16,16 +16,21 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.FilterPredicateValue; import java.util.concurrent.TimeUnit; +@Schema(description = "Duration Alarm Condition Specification") @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class DurationAlarmConditionSpec implements AlarmConditionSpec { + @Schema(description = "Duration time unit") private TimeUnit unit; + @Schema(description = "Duration predicate") private FilterPredicateValue predicate; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java index b5d6bc36f4..8d6831ebd9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.CoapDeviceType; @Data +@Schema public class EfentoCoapDeviceTypeConfiguration implements CoapDeviceTypeConfiguration { private static final long serialVersionUID = -8523081152598707064L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java index 16d5ca7686..3103aff8c4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.TransportPayloadType; @Data +@Schema public class JsonTransportPayloadConfiguration implements TransportPayloadTypeConfiguration { @Override 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 index ccaa730f45..300ede57c9 100644 --- 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 @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.data.PowerMode; @@ -22,20 +24,25 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguratio import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; -import static org.eclipse.leshan.core.LwM2m.Version.V1_0; -import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; - import java.util.Collections; import java.util.List; +import static org.eclipse.leshan.core.LwM2m.Version.V1_0; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; + @Data +@Schema public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { private static final long serialVersionUID = 6257277825459600068L; + @Schema(description = "Configuration for mapping LwM2M resources to telemetry and attributes") private TelemetryMappingConfiguration observeAttr; + @Schema(description = "Flag indicating whether LwM2M bootstrap server update is enabled") private boolean bootstrapServerUpdateEnable; + @ArraySchema(schema = @Schema(implementation = LwM2MBootstrapServerCredential.class)) private List bootstrap; + @Schema(description = "Other LwM2M client settings") private OtherConfiguration clientLwM2mSettings; public Lwm2mDeviceProfileTransportConfiguration() { @@ -50,6 +57,6 @@ public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTr private void updateDefault(){ this.setBootstrap(Collections.emptyList()); this.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); - this.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), SINGLE)); + this.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), false, SINGLE)); } } 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 index 4e165e7745..a5df705800 100644 --- 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 @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; @@ -23,19 +25,27 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.util.Objects; import java.util.Set; +@Schema @Data public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @Schema @NoXss private String deviceTelemetryTopic = MqttTopics.DEVICE_TELEMETRY_TOPIC; + @Schema @NoXss private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Schema @NoXss private String deviceAttributesSubscribeTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Schema private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; + @Schema private boolean sparkplug; + @ArraySchema(schema = @Schema(implementation = String.class)) private Set sparkplugAttributesMetricNames; + @Schema private boolean sendAckOnValidationException; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java index c13f0b2d09..a1e55f3452 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java @@ -18,12 +18,14 @@ package org.thingsboard.server.common.data.device.profile; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.squareup.wire.schema.Location; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.DynamicProtoUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; +@Schema @Slf4j @Data public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeConfiguration { @@ -43,6 +45,9 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC private boolean enableCompatibilityWithJsonPayloadFormat; private boolean useJsonPayloadFormatForDefaultDownlinkTopics; + @Schema( + description = "Transport payload type", requiredMode = Schema.RequiredMode.REQUIRED + ) @Override public TransportPayloadType getTransportPayloadType() { return TransportPayloadType.PROTOBUF; 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 index cdbb7a80df..1685b5da8e 100644 --- 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 @@ -16,16 +16,21 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.FilterPredicateValue; +@Schema @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { + @Schema(description = "Repeating predicate") private FilterPredicateValue predicate; @Override + @Schema(description = "Type of the Alarm Condition Specification", requiredMode = Schema.RequiredMode.REQUIRED) 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 index 5f492060d0..caa65777e9 100644 --- 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 @@ -16,10 +16,13 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class SimpleAlarmConditionSpec implements AlarmConditionSpec { @Override public AlarmConditionSpecType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java index ee1656c04a..a83920b777 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java @@ -16,16 +16,20 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import java.util.List; +@Schema @Data public class SnmpDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { private Integer timeoutMs; private Integer retries; + @ArraySchema(schema = @Schema(implementation = SnmpCommunicationConfig.class)) private List communicationConfigs; @Override 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 index 5cb546ebd8..8bd56d07be 100644 --- 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 @@ -15,12 +15,15 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.DynamicValue; import java.util.Set; +@Schema(hidden = true) @Data +@Deprecated public class SpecificTimeSchedule implements AlarmSchedule { private String timezone; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java index c7111f8ffd..38176bd740 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java @@ -19,10 +19,20 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.TransportPayloadType; import java.io.Serializable; +@Schema( + description = "Configuration for transport payload type", + discriminatorProperty = "transportPayloadType", + discriminatorMapping = { + @DiscriminatorMapping(value = "JSON", schema = JsonTransportPayloadConfiguration.class), + @DiscriminatorMapping(value = "PROTOBUF", schema = ProtoTransportPayloadConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java index ad0cdaa6bf..ab95dae195 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.DeviceProfileProvisionType; +@Schema @Data @NoArgsConstructor public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java index 393e08efee..6e83e82692 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java @@ -16,11 +16,13 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.eclipse.leshan.core.LwM2m; import java.io.Serializable; +@Schema @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class ObjectAttributes implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java index 0b0d4095b1..cdf9ed89df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -23,6 +24,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; +@Schema @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java index a166d992a4..6513e2fe55 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; @@ -27,15 +28,23 @@ import java.util.Set; @Data @NoArgsConstructor +@Schema public class TelemetryMappingConfiguration implements Serializable { private static final long serialVersionUID = -7594999741305410419L; + @Schema(description = "Map of LwM2M resource paths to telemetry key names") private Map keyName; + @Schema(description = "Set of resources to observe") private Set observe; + @Schema(description = "Set of attribute keys") private Set attribute; + @Schema(description = "Set of telemetry keys") private Set telemetry; + @Schema(description = "Map of resource paths to specific LwM2M object attributes") private Map attributeLwm2m; + private Boolean initAttrTelAsObsStrategy; + @Schema(description = "Observation strategy for telemetry") private TelemetryObserveStrategy observeStrategy; @JsonCreator @@ -45,6 +54,7 @@ public class TelemetryMappingConfiguration implements Serializable { @JsonProperty("attribute") Set attribute, @JsonProperty("telemetry") Set telemetry, @JsonProperty("attributeLwm2m") Map attributeLwm2m, + @JsonProperty("initAttrTelAsObsStrategy") Boolean initAttrTelAsObsStrategy, @JsonProperty("observeStrategy") TelemetryObserveStrategy observeStrategy) { this.keyName = keyName != null ? keyName : Collections.emptyMap(); @@ -52,6 +62,7 @@ public class TelemetryMappingConfiguration implements Serializable { this.attribute = attribute != null ? attribute : Collections.emptySet(); this.telemetry = telemetry != null ? telemetry : Collections.emptySet(); this.attributeLwm2m = attributeLwm2m != null ? attributeLwm2m : Collections.emptyMap(); + this.initAttrTelAsObsStrategy = initAttrTelAsObsStrategy != null ? initAttrTelAsObsStrategy : false; this.observeStrategy = observeStrategy != null ? observeStrategy : TelemetryObserveStrategy.SINGLE; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java index b160c735a9..8d826b6e5c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java @@ -26,7 +26,7 @@ public class LwM2MServerSecurityConfig implements Serializable { @Schema(description = "Server short Id. Used as link to associate server Object Instance. This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. " + "This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. " + - "The values ID:1 and ID:65534 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY) + "The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY) protected Integer shortServerId = 123; /** Security -> ObjectId = 0 'LWM2M Security' */ @Schema(description = "Is Bootstrap Server or Lwm2m Server. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index d50882f699..6866271aec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.edge; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -153,4 +154,13 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel return this.secret; } + @Schema(description = "Additional parameters of the edge. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Edge at location A\"}") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 5f8d57f6f8..474a0270c1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -47,7 +47,9 @@ public enum EdgeEventType { TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), DOMAIN(true, EntityType.DOMAIN), - CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD); + CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD), + AI_MODEL(true, EntityType.AI_MODEL), + API_KEY(true, EntityType.API_KEY); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java index 7d5e268ba1..ee0ff30778 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java @@ -16,9 +16,17 @@ package org.thingsboard.server.common.data.edqs; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.ObjectType; + +import java.util.Set; @Data +@AllArgsConstructor +@NoArgsConstructor @JsonIgnoreProperties public class EdqsSyncRequest { + Set objectTypes; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java index 5aa9cf9ebe..f57bf8861c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.UUID; -@ToString +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class CalculatedFieldDebugEvent extends Event { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java index 8779416106..01463dade8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.event; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.StringUtils; @@ -26,10 +27,17 @@ public abstract class DebugEventFilter implements EventFilter { @Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") protected String server; @Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"}) + @JsonProperty("isError") protected boolean isError; @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") protected String errorStr; + @JsonProperty("isError") + public boolean isError() { + return isError; + } + + @JsonProperty("isError") public void setIsError(boolean isError) { this.isError = isError; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java index 07789b9208..4d955f1817 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java @@ -17,9 +17,21 @@ package org.thingsboard.server.common.data.event; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; -@Schema +@Schema( + description = "Filter for various event types", + discriminatorProperty = "eventType", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEBUG_RULE_NODE", schema = RuleNodeDebugEventFilter.class), + @DiscriminatorMapping(value = "DEBUG_RULE_CHAIN", schema = RuleChainDebugEventFilter.class), + @DiscriminatorMapping(value = "ERROR", schema = ErrorEventFilter.class), + @DiscriminatorMapping(value = "LC_EVENT", schema = LifeCycleEventFilter.class), + @DiscriminatorMapping(value = "STATS", schema = StatisticsEventFilter.class), + @DiscriminatorMapping(value = "DEBUG_CALCULATED_FIELD", schema = CalculatedFieldDebugEventFilter.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index 4974b550e4..9750e5edf3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -31,6 +31,7 @@ public enum ThingsboardErrorCode { TOO_MANY_UPDATES(34), VERSION_CONFLICT(35), SUBSCRIPTION_VIOLATION(40), + ENTITIES_LIMIT_EXCEEDED(41), PASSWORD_VIOLATION(45), DATABASE(46); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java index da7ade45e1..5923b8c02c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class AdminSettingsId extends UUIDBased implements EntityId { @Serial @@ -33,7 +34,7 @@ public class AdminSettingsId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") @Override public EntityType getEntityType() { return EntityType.ADMIN_SETTINGS; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java index dbb8b25635..70476564d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class AiModelId extends UUIDBased implements EntityId { @Serial @@ -36,6 +37,7 @@ public final class AiModelId extends UUIDBased implements EntityId { @Override @Schema( requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_ONLY, description = "Entity type of the AI model", example = "AI_MODEL", allowableValues = "AI_MODEL" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java index 444492c328..db2c63d614 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class AlarmId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -36,7 +36,7 @@ public class AlarmId extends UUIDBased implements EntityId { return new AlarmId(UUID.fromString(alarmId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ALARM", allowableValues = "ALARM") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ALARM", allowableValues = "ALARM") @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java similarity index 63% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java index 29bee49ef7..186dcaca40 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java @@ -20,26 +20,28 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; -@Schema -public class CalculatedFieldLinkId extends UUIDBased implements EntityId { +@Schema(allOf = EntityId.class) +public class ApiKeyId extends UUIDBased implements EntityId { - private static final long serialVersionUID = 1L; + @Serial + private static final long serialVersionUID = -273913539653684641L; @JsonCreator - public CalculatedFieldLinkId(@JsonProperty("id") UUID id) { + public ApiKeyId(@JsonProperty("id") UUID id) { super(id); } - public static CalculatedFieldLinkId fromString(String calculatedFieldLinkId) { - return new CalculatedFieldLinkId(UUID.fromString(calculatedFieldLinkId)); + public static ApiKeyId fromString(String secretId) { + return new ApiKeyId(UUID.fromString(secretId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD_LINK", allowableValues = "CALCULATED_FIELD_LINK") @Override + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_KEY", allowableValues = "API_KEY") public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; + return EntityType.API_KEY; } } 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 index 78e5c90bee..02d6d387a2 100644 --- 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 @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class ApiUsageStateId extends UUIDBased implements EntityId { @JsonCreator @@ -34,7 +34,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId { return new ApiUsageStateId(UUID.fromString(userId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") @Override public EntityType getEntityType() { return EntityType.API_USAGE_STATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java index dc42fffeb5..2d57cc365d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class AssetId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -36,7 +36,7 @@ public class AssetId extends UUIDBased implements EntityId { return new AssetId(UUID.fromString(assetId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET", allowableValues = "ASSET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET", allowableValues = "ASSET") @Override public EntityType getEntityType() { return EntityType.ASSET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java index f4f7a3de70..981f381a4a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class AssetProfileId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class AssetProfileId extends UUIDBased implements EntityId { return new AssetProfileId(UUID.fromString(assetProfileId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java index 5168515cd8..094df0e935 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class CalculatedFieldId extends UUIDBased implements EntityId { @Serial @@ -38,7 +38,7 @@ public class CalculatedFieldId extends UUIDBased implements EntityId { return new CalculatedFieldId(UUID.fromString(calculatedFieldId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD") @Override public EntityType getEntityType() { return EntityType.CALCULATED_FIELD; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java index a3b1e28147..4de326f26c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public final class CustomerId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -32,7 +32,7 @@ public final class CustomerId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") @Override public EntityType getEntityType() { return EntityType.CUSTOMER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java index bd326257a6..c98e70177e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class DashboardId extends UUIDBased implements EntityId { @JsonCreator @@ -34,7 +34,7 @@ public class DashboardId extends UUIDBased implements EntityId { return new DashboardId(UUID.fromString(dashboardId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java index 7dd840815b..a7b9c12e19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class DeviceId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -37,7 +37,7 @@ public class DeviceId extends UUIDBased implements EntityId { } @Override - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE", allowableValues = "DEVICE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE", allowableValues = "DEVICE") public EntityType getEntityType() { return EntityType.DEVICE; } 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 index 2adba3640c..dea0dd2a65 100644 --- 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class DeviceProfileId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId { return new DeviceProfileId(UUID.fromString(deviceProfileId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java index 0768eec45e..223ea8d8b5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java @@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class DomainId extends UUIDBased implements EntityId { @JsonCreator @@ -32,6 +34,7 @@ public class DomainId extends UUIDBased implements EntityId { return new DomainId(UUID.fromString(oauth2DomainId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DOMAIN", allowableValues = "DOMAIN") @Override public EntityType getEntityType() { return EntityType.DOMAIN; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 05019a608f..45a95d6632 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class EdgeId extends UUIDBased implements EntityId { @Serial @@ -43,7 +44,7 @@ public class EdgeId extends UUIDBased implements EntityId { return new EdgeId(UUID.fromString(edgeId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "EDGE", allowableValues = "EDGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "EDGE", allowableValues = "EDGE") @Override public EntityType getEntityType() { return EntityType.EDGE; 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 c57dc79514..c96b4c38d5 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 @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; @@ -26,7 +27,47 @@ import java.util.UUID; @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) -@Schema +@Schema( + discriminatorProperty = "entityType", + discriminatorMapping = { + @DiscriminatorMapping(value = "ADMIN_SETTINGS", schema = AdminSettingsId.class), + @DiscriminatorMapping(value = "AI_MODEL", schema = AiModelId.class), + @DiscriminatorMapping(value = "ALARM", schema = AlarmId.class), + @DiscriminatorMapping(value = "API_KEY", schema = ApiKeyId.class), + @DiscriminatorMapping(value = "API_USAGE_STATE", schema = ApiUsageStateId.class), + @DiscriminatorMapping(value = "ASSET", schema = AssetId.class), + @DiscriminatorMapping(value = "ASSET_PROFILE", schema = AssetProfileId.class), + @DiscriminatorMapping(value = "CALCULATED_FIELD", schema = CalculatedFieldId.class), + @DiscriminatorMapping(value = "CUSTOMER", schema = CustomerId.class), + @DiscriminatorMapping(value = "DASHBOARD", schema = DashboardId.class), + @DiscriminatorMapping(value = "DEVICE", schema = DeviceId.class), + @DiscriminatorMapping(value = "DEVICE_PROFILE", schema = DeviceProfileId.class), + @DiscriminatorMapping(value = "DOMAIN", schema = DomainId.class), + @DiscriminatorMapping(value = "EDGE", schema = EdgeId.class), + @DiscriminatorMapping(value = "ENTITY_VIEW", schema = EntityViewId.class), + @DiscriminatorMapping(value = "JOB", schema = JobId.class), + @DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppId.class), + @DiscriminatorMapping(value = "MOBILE_APP_BUNDLE", schema = MobileAppBundleId.class), + @DiscriminatorMapping(value = "NOTIFICATION", schema = NotificationId.class), + @DiscriminatorMapping(value = "NOTIFICATION_REQUEST", schema = NotificationRequestId.class), + @DiscriminatorMapping(value = "NOTIFICATION_RULE", schema = NotificationRuleId.class), + @DiscriminatorMapping(value = "NOTIFICATION_TARGET", schema = NotificationTargetId.class), + @DiscriminatorMapping(value = "NOTIFICATION_TEMPLATE", schema = NotificationTemplateId.class), + @DiscriminatorMapping(value = "OAUTH2_CLIENT", schema = OAuth2ClientId.class), + @DiscriminatorMapping(value = "OTA_PACKAGE", schema = OtaPackageId.class), + @DiscriminatorMapping(value = "QUEUE", schema = QueueId.class), + @DiscriminatorMapping(value = "QUEUE_STATS", schema = QueueStatsId.class), + @DiscriminatorMapping(value = "RPC", schema = RpcId.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = RuleChainId.class), + @DiscriminatorMapping(value = "RULE_NODE", schema = RuleNodeId.class), + @DiscriminatorMapping(value = "TB_RESOURCE", schema = TbResourceId.class), + @DiscriminatorMapping(value = "TENANT", schema = TenantId.class), + @DiscriminatorMapping(value = "TENANT_PROFILE", schema = TenantProfileId.class), + @DiscriminatorMapping(value = "USER", schema = UserId.class), + @DiscriminatorMapping(value = "WIDGETS_BUNDLE", schema = WidgetsBundleId.class), + @DiscriminatorMapping(value = "WIDGET_TYPE", schema = WidgetTypeId.class) + } +) 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 65432f6b7f..ae033f769c 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 @@ -80,10 +80,10 @@ public class EntityIdFactory { case DOMAIN -> new DomainId(uuid); case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); case JOB -> new JobId(uuid); case ADMIN_SETTINGS -> new AdminSettingsId(uuid); case AI_MODEL -> new AiModelId(uuid); + case API_KEY -> new ApiKeyId(uuid); }; } @@ -113,6 +113,8 @@ public class EntityIdFactory { case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); case DOMAIN -> new DomainId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + case AI_MODEL -> new AiModelId(uuid); + case API_KEY -> new ApiKeyId(uuid); case ADMIN_SETTINGS -> new AdminSettingsId(uuid); default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java index cd5b0164db..8bd357a4fc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java @@ -25,6 +25,7 @@ import java.util.UUID; /** * Created by Victor Basanets on 8/27/2017. */ +@Schema(allOf = EntityId.class) public class EntityViewId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -38,7 +39,7 @@ public class EntityViewId extends UUIDBased implements EntityId { return new EntityViewId(UUID.fromString(entityViewID)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java index 51e6b8e539..2d369e8b71 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class JobId extends UUIDBased implements EntityId { @Serial @@ -33,7 +34,7 @@ public class JobId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "JOB", allowableValues = "JOB") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "JOB", allowableValues = "JOB") @Override public EntityType getEntityType() { return EntityType.JOB; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java index 6a9c11207a..cfd0fd109f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java @@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class MobileAppBundleId extends UUIDBased implements EntityId{ @JsonCreator @@ -32,6 +34,7 @@ public class MobileAppBundleId extends UUIDBased implements EntityId{ return new MobileAppBundleId(UUID.fromString(mobileAppId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP_BUNDLE", allowableValues = "MOBILE_APP_BUNDLE") @Override public EntityType getEntityType() { return EntityType.MOBILE_APP_BUNDLE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java index f19e52cc46..bf3571bf7e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java @@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class MobileAppId extends UUIDBased implements EntityId{ @JsonCreator @@ -32,6 +34,7 @@ public class MobileAppId extends UUIDBased implements EntityId{ return new MobileAppId(UUID.fromString(mobileAppId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP", allowableValues = "MOBILE_APP") @Override public EntityType getEntityType() { return EntityType.MOBILE_APP; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java index 3ba6bf4ca4..a702648a52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java index 5ff42bc0e2..3caa5f1f8c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationRequestId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java index ff57e4d252..39e2ccf54c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationRuleId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java index d1a1cfd00b..593ce1c1aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationTargetId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java index ae2ea582fd..5a1635044a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationTemplateId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java index 9ef5871135..491643b921 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java @@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class OAuth2ClientId extends UUIDBased implements EntityId { @JsonCreator @@ -32,6 +34,7 @@ public class OAuth2ClientId extends UUIDBased implements EntityId { return new OAuth2ClientId(UUID.fromString(oauth2ClientId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OAUTH2_CLIENT", allowableValues = "OAUTH2_CLIENT") @Override public EntityType getEntityType() { return EntityType.OAUTH2_CLIENT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 0261b2973e..f9f508f13f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class OtaPackageId extends UUIDBased implements EntityId { @Serial @@ -37,7 +38,7 @@ public class OtaPackageId extends UUIDBased implements EntityId { return new OtaPackageId(UUID.fromString(firmwareId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java index c7ee46da2a..3c00c08bc1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class QueueId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class QueueId extends UUIDBased implements EntityId { return new QueueId(UUID.fromString(queueId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE", allowableValues = "QUEUE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE", allowableValues = "QUEUE") @Override public EntityType getEntityType() { return EntityType.QUEUE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java index 7a921e8755..7869b7bbfe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class QueueStatsId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class QueueStatsId extends UUIDBased implements EntityId { return new QueueStatsId(UUID.fromString(queueId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") @Override public EntityType getEntityType() { return EntityType.QUEUE_STATS; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java index 559855e194..a03b0138d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class RpcId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public final class RpcId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RPC", allowableValues = "RPC") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RPC", allowableValues = "RPC") @Override public EntityType getEntityType() { return EntityType.RPC; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java index 0512f518af..c77cec808e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class RuleChainId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class RuleChainId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java index 645acfe5e2..6be3371969 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class RuleNodeId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class RuleNodeId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") @Override public EntityType getEntityType() { return EntityType.RULE_NODE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java index cf171855b2..b08dcb56b4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class TbResourceId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public class TbResourceId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index f219452c07..47be35fb84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore @@ -54,7 +55,7 @@ public final class TenantId extends UUIDBased implements EntityId { return this.equals(SYS_TENANT_ID); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT", allowableValues = "TENANT") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT", allowableValues = "TENANT") @Override public EntityType getEntityType() { return EntityType.TENANT; 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 index 5b46b772ba..7ced9faa43 100644 --- 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class TenantProfileId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class TenantProfileId extends UUIDBased implements EntityId { return new TenantProfileId(UUID.fromString(tenantProfileId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java index d4f113bef5..0bb939d958 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class UserId extends UUIDBased implements EntityId { @JsonCreator @@ -34,7 +34,7 @@ public class UserId extends UUIDBased implements EntityId { return new UserId(UUID.fromString(userId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "USER", allowableValues = "USER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "USER", allowableValues = "USER") @Override public EntityType getEntityType() { return EntityType.USER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java index fe8b40a7b9..cd9dcec8b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class WidgetTypeId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public final class WidgetTypeId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java index f88d81bf74..295ac8c59d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class WidgetsBundleId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public final class WidgetsBundleId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java index 28ebcd2095..4de12e3eeb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -24,6 +25,7 @@ import lombok.ToString; import java.util.List; +@Schema(description = "Dummy job configuration") @Data @EqualsAndHashCode(callSuper = true) @AllArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index f68012d793..d99fdf1961 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.task.TaskResult; import java.util.Set; import java.util.UUID; @@ -82,4 +84,36 @@ public class Job extends BaseData implements HasTenantId { return (C) configuration; } + @JsonIgnore + public String getError() { + if (status == JobStatus.CANCELLED) { + return "The task was cancelled"; + } + if (result.getGeneralError() != null) { + return result.getGeneralError(); + } + if (result.getFailedCount() > 0 && result.getResults() != null) { + StringBuilder errorMessage = new StringBuilder(); + for (TaskResult taskResult : result.getResults()) { + if (taskResult.isSuccess() || taskResult.isDiscarded()) { + continue; + } + String error = taskResult.getError(); + if (error == null) { + continue; + } + if (!errorMessage.isEmpty()) { + if (errorMessage.length() + 2 + error.length() > 256) { + errorMessage.append("..."); + break; + } + errorMessage.append("; "); + } + errorMessage.append(error); + } + return errorMessage.toString(); + } + return "Task failed"; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index 9480ba11c3..6a10dc7ea1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; import org.thingsboard.server.common.data.job.task.TaskResult; @@ -27,6 +30,12 @@ import org.thingsboard.server.common.data.job.task.TaskResult; import java.io.Serializable; import java.util.List; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DUMMY", schema = DummyJobConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @@ -37,6 +46,7 @@ public abstract class JobConfiguration implements Serializable { @NotBlank private String tasksKey; // internal + @ArraySchema(schema = @Schema(ref = "#/components/schemas/TaskResult")) private List toReprocess; // internal @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 2009f10f9f..05c2d51879 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.job.task.TaskResult; @@ -28,6 +31,13 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +@Schema( + description = "Job execution result", + discriminatorProperty = "jobType", + discriminatorMapping = { + @DiscriminatorMapping(value = "DUMMY", schema = DummyJobResult.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ @@ -37,15 +47,24 @@ import java.util.List; @NoArgsConstructor public abstract class JobResult implements Serializable { + @Schema(description = "Count of successfully completed tasks") private int successfulCount; + @Schema(description = "Count of failed tasks") private int failedCount; + @Schema(description = "Count of discarded tasks") private int discardedCount; - private Integer totalCount = null; // set when all tasks are submitted + @Schema(description = "Total number of tasks, set when all tasks are submitted", nullable = true) + private Integer totalCount = null; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/TaskResult")) private List results = new ArrayList<>(); + @Schema(description = "General error message if the job failed") private String generalError; + @Schema(description = "Timestamp of the job start, in milliseconds") private long startTs; + @Schema(description = "Timestamp of the job finish, in milliseconds") private long finishTs; + @Schema(description = "Timestamp of the job cancellation, in milliseconds") private long cancellationTs; @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java index e6a4702b81..37f851f611 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java @@ -64,6 +64,11 @@ public class DummyTaskResult extends TaskResult { return JobType.DUMMY; } + @Override + public String getError() { + return failure != null ? failure.getError() : null; + } + @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java index 761b518e98..e882c74eb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java @@ -46,4 +46,6 @@ public abstract class TaskResult { @JsonIgnore public abstract JobType getJobType(); + public abstract String getError(); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java index 97f28ba853..925cc73898 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.kv; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; +@Schema public enum DataType { BOOLEAN(0), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java index f29863d731..877c727381 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.Views; +@Schema @Data public abstract class AbstractMobilePage implements MobilePage { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java index 83f82a571b..156436225f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java index 9f6114e4a4..232049adbb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java index 710d951edc..20f25afe19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java index a45fd90838..783d3f68a9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.mobile.layout; import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.AllArgsConstructor; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.Views; import java.util.ArrayList; import java.util.List; +@Schema @Data @Builder @NoArgsConstructor @@ -35,7 +37,7 @@ import java.util.List; @EqualsAndHashCode public class MobileLayoutConfig { - @Schema(description = "List of pages") + @ArraySchema(schema = @Schema(implementation = MobilePage.class)) @JsonView(Views.Public.class) @Valid private List pages = new ArrayList<>(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java index 61fa5a0c46..4ab8388f8b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java @@ -19,10 +19,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.Views; import java.io.Serializable; +@Schema( + description = "Configuration for a mobile page", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultMobilePage.class), + @DiscriminatorMapping(value = "DASHBOARD", schema = DashboardPage.class), + @DiscriminatorMapping(value = "WEB_VIEW", schema = WebViewPage.class), + @DiscriminatorMapping(value = "CUSTOM", schema = CustomMobilePage.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -36,6 +48,7 @@ import java.io.Serializable; }) public interface MobilePage extends Serializable { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @JsonView(Views.Private.class) MobilePageType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java index c8eafbf58c..89e82ab90e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java index 72e185327b..20420e3971 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.mobile.qrCodeSettings; + public enum BadgePosition { RIGHT, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java index 28720a3335..3e60576c27 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -38,7 +38,10 @@ public enum TbMsgType { ENTITY_UNASSIGNED("Entity Unassigned"), ATTRIBUTES_UPDATED("Attributes Updated"), ATTRIBUTES_DELETED("Attributes Deleted"), - ALARM, + ALARM("Alarm"), + ALARM_CREATED("Alarm Created"), + ALARM_UPDATED("Alarm Updated"), + ALARM_SEVERITY_UPDATED("Alarm Severity Updated"), ALARM_ACK("Alarm Acknowledged"), ALARM_CLEAR("Alarm Cleared"), ALARM_DELETE, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java index b6a4fec9f8..26b0d10efb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.notification; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp import java.util.List; import java.util.UUID; +@Schema @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java index 06b02c6183..c54e845a0d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java @@ -15,18 +15,23 @@ */ package org.thingsboard.server.common.data.notification; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.util.List; +@Schema @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class NotificationRequestInfo extends NotificationRequest { + @Schema private String templateName; + @ArraySchema(schema = @Schema(implementation = NotificationDeliveryMethod.class)) private List deliveryMethods; public NotificationRequestInfo(NotificationRequest request, String templateName, List deliveryMethods) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java index c102cfcae4..0e742b6fb1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.notification; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; @@ -27,14 +28,19 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +@Schema(description = "Notification request processing statistics") @Data public class NotificationRequestStats { + @Schema(description = "Number of successfully sent notifications per delivery method", example = "{\"WEB\": 10, \"EMAIL\": 5}") private final Map sent; @JsonIgnore private final AtomicInteger totalSent; + @Schema(description = "Errors per delivery method. Each entry maps recipient name to error message") private final Map> errors; + @Schema(description = "Total number of errors across all delivery methods") private final AtomicInteger totalErrors; + @Schema(description = "General error message if the entire request failed") private String error; @JsonIgnore private final Map> processedRecipients; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java index 921a972225..3584b92441 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java @@ -32,6 +32,7 @@ public enum NotificationType { ALARM_ASSIGNMENT, NEW_PLATFORM_VERSION, ENTITIES_LIMIT, + ENTITIES_LIMIT_INCREASE_REQUEST, API_USAGE_LIMIT, RULE_NODE, RATE_LIMITS, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java index 3fa21a2b7f..941bb6a0a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java @@ -53,6 +53,7 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private CustomerId alarmCustomerId; @@ -77,7 +78,8 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati "alarmStatus", alarmStatus.toString(), "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), "alarmOriginatorId", alarmOriginator.getId().toString(), - "alarmOriginatorName", alarmOriginatorName + "alarmOriginatorName", alarmOriginatorName, + "alarmOriginatorLabel", alarmOriginatorLabel ); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java index dfd59e4e03..b2514c128b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java @@ -48,6 +48,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private CustomerId alarmCustomerId; @@ -68,7 +69,8 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI "alarmStatus", alarmStatus.toString(), "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), "alarmOriginatorId", alarmOriginator.getId().toString(), - "alarmOriginatorName", alarmOriginatorName + "alarmOriginatorName", alarmOriginatorName, + "alarmOriginatorLabel", alarmOriginatorLabel ); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java index 6001de0d30..3adcf12f55 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java @@ -25,11 +25,10 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; +import java.util.HashMap; import java.util.Map; import java.util.UUID; -import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; - @Data @NoArgsConstructor @AllArgsConstructor @@ -41,25 +40,28 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo { private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private boolean acknowledged; private boolean cleared; private CustomerId alarmCustomerId; private DashboardId dashboardId; + private Map details; @Override public Map getTemplateData() { - return mapOf( - "alarmType", alarmType, - "action", action, - "alarmId", alarmId.toString(), - "alarmSeverity", alarmSeverity.name().toLowerCase(), - "alarmStatus", alarmStatus.toString(), - "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), - "alarmOriginatorName", alarmOriginatorName, - "alarmOriginatorId", alarmOriginator.getId().toString() - ); + Map templateData = details != null ? new HashMap<>(details) : new HashMap<>(); + templateData.put("alarmType", alarmType); + templateData.put("action", action); + templateData.put("alarmId", alarmId.toString()); + templateData.put("alarmSeverity", alarmSeverity.name().toLowerCase()); + templateData.put("alarmStatus", alarmStatus.toString()); + templateData.put("alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName()); + templateData.put("alarmOriginatorName", alarmOriginatorName); + templateData.put("alarmOriginatorLabel", alarmOriginatorLabel); + templateData.put("alarmOriginatorId", alarmOriginator.getId().toString()); + return templateData; } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java new file mode 100644 index 0000000000..f00014567d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.notification.info; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Map; + +import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EntitiesLimitIncreaseRequestNotificationInfo implements NotificationInfo { + + private EntityType entityType; + private String userEmail; + private String increaseLimitActionLabel; + private String increaseLimitLink; + private String baseUrl; + + @Override + public Map getTemplateData() { + return mapOf( + "entityType", entityType.getNormalName(), + "userEmail", userEmail, + "increaseLimitActionLabel", increaseLimitActionLabel, + "increaseLimitLink", increaseLimitLink, + "baseUrl", baseUrl + ); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java index 9223f53509..bff983f2f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java @@ -15,17 +15,20 @@ */ package org.thingsboard.server.common.data.notification.rule; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import java.util.List; import java.util.Map; import java.util.UUID; +@Schema(description = "Default notification rule recipients configuration") @Data -@EqualsAndHashCode(callSuper = true) -public class DefaultNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig { +@EqualsAndHashCode +public abstract class DefaultNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig { @NotEmpty private List targets; @@ -35,4 +38,114 @@ public class DefaultNotificationRuleRecipientsConfig extends NotificationRuleRec return Map.of(0, targets); } + public static DefaultNotificationRuleRecipientsConfig forTriggerType(NotificationRuleTriggerType triggerType) { + return switch (triggerType) { + case ENTITY_ACTION -> new EntityActionRecipientsConfig(); + case ALARM_COMMENT -> new AlarmCommentRecipientsConfig(); + case ALARM_ASSIGNMENT -> new AlarmAssignmentRecipientsConfig(); + case DEVICE_ACTIVITY -> new DeviceActivityRecipientsConfig(); + case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT -> new RuleEngineComponentLifecycleEventRecipientsConfig(); + case EDGE_CONNECTION -> new EdgeConnectionRecipientsConfig(); + case EDGE_COMMUNICATION_FAILURE -> new EdgeCommunicationFailureRecipientsConfig(); + case NEW_PLATFORM_VERSION -> new NewPlatformVersionRecipientsConfig(); + case ENTITIES_LIMIT -> new EntitiesLimitRecipientsConfig(); + case API_USAGE_LIMIT -> new ApiUsageLimitRecipientsConfig(); + case RATE_LIMITS -> new RateLimitsRecipientsConfig(); + case TASK_PROCESSING_FAILURE -> new TaskProcessingFailureRecipientsConfig(); + case RESOURCES_SHORTAGE -> new ResourceShortageRecipientsConfig(); + default -> throw new IllegalArgumentException("Unsupported trigger type for default recipients config: " + triggerType); + }; + } + + public static class EntityActionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ENTITY_ACTION; + } + } + + public static class AlarmCommentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM_COMMENT; + } + } + + public static class AlarmAssignmentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM_ASSIGNMENT; + } + } + + public static class DeviceActivityRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.DEVICE_ACTIVITY; + } + } + + public static class RuleEngineComponentLifecycleEventRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT; + } + } + + public static class EdgeConnectionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_CONNECTION; + } + } + + public static class EdgeCommunicationFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE; + } + } + + public static class NewPlatformVersionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.NEW_PLATFORM_VERSION; + } + } + + public static class EntitiesLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ENTITIES_LIMIT; + } + } + + public static class ApiUsageLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.API_USAGE_LIMIT; + } + } + + public static class RateLimitsRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RATE_LIMITS; + } + } + + public static class TaskProcessingFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.TASK_PROCESSING_FAILURE; + } + } + + public static class ResourceShortageRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RESOURCES_SHORTAGE; + } + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java index 459a15c0fd..fa5a10f983 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java @@ -15,21 +15,29 @@ */ package org.thingsboard.server.common.data.notification.rule; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import java.util.List; import java.util.Map; import java.util.UUID; +@Schema(description = "Escalated notification rule recipients configuration") @Data -@EqualsAndHashCode(callSuper = true) -public class EscalatedNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig { +@EqualsAndHashCode +public class EscalatedNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig { @NotEmpty private Map> escalationTable; + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM; + } + @Override public Map> getTargetsTable() { return escalationTable; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java index de4e05a2cb..0475a9a02d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java @@ -17,12 +17,11 @@ package org.thingsboard.server.common.data.notification.rule; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import jakarta.validation.constraints.NotNull; -import lombok.Data; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import java.io.Serializable; @@ -30,19 +29,47 @@ import java.util.List; import java.util.Map; import java.util.UUID; +@Schema( + discriminatorProperty = "triggerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "ENTITY_ACTION", schema = DefaultNotificationRuleRecipientsConfig.EntityActionRecipientsConfig.class), + @DiscriminatorMapping(value = "ALARM", schema = EscalatedNotificationRuleRecipientsConfig.class), + @DiscriminatorMapping(value = "ALARM_COMMENT", schema = DefaultNotificationRuleRecipientsConfig.AlarmCommentRecipientsConfig.class), + @DiscriminatorMapping(value = "ALARM_ASSIGNMENT", schema = DefaultNotificationRuleRecipientsConfig.AlarmAssignmentRecipientsConfig.class), + @DiscriminatorMapping(value = "DEVICE_ACTIVITY", schema = DefaultNotificationRuleRecipientsConfig.DeviceActivityRecipientsConfig.class), + @DiscriminatorMapping(value = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT", schema = DefaultNotificationRuleRecipientsConfig.RuleEngineComponentLifecycleEventRecipientsConfig.class), + @DiscriminatorMapping(value = "EDGE_CONNECTION", schema = DefaultNotificationRuleRecipientsConfig.EdgeConnectionRecipientsConfig.class), + @DiscriminatorMapping(value = "EDGE_COMMUNICATION_FAILURE", schema = DefaultNotificationRuleRecipientsConfig.EdgeCommunicationFailureRecipientsConfig.class), + @DiscriminatorMapping(value = "NEW_PLATFORM_VERSION", schema = DefaultNotificationRuleRecipientsConfig.NewPlatformVersionRecipientsConfig.class), + @DiscriminatorMapping(value = "ENTITIES_LIMIT", schema = DefaultNotificationRuleRecipientsConfig.EntitiesLimitRecipientsConfig.class), + @DiscriminatorMapping(value = "API_USAGE_LIMIT", schema = DefaultNotificationRuleRecipientsConfig.ApiUsageLimitRecipientsConfig.class), + @DiscriminatorMapping(value = "RATE_LIMITS", schema = DefaultNotificationRuleRecipientsConfig.RateLimitsRecipientsConfig.class), + @DiscriminatorMapping(value = "TASK_PROCESSING_FAILURE", schema = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class), + @DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class) + }) @JsonIgnoreProperties(ignoreUnknown = true) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY, defaultImpl = DefaultNotificationRuleRecipientsConfig.class) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", include = JsonTypeInfo.As.EXISTING_PROPERTY) @JsonSubTypes({ @Type(name = "ALARM", value = EscalatedNotificationRuleRecipientsConfig.class), + @Type(name = "ENTITY_ACTION", value = DefaultNotificationRuleRecipientsConfig.EntityActionRecipientsConfig.class), + @Type(name = "ALARM_COMMENT", value = DefaultNotificationRuleRecipientsConfig.AlarmCommentRecipientsConfig.class), + @Type(name = "ALARM_ASSIGNMENT", value = DefaultNotificationRuleRecipientsConfig.AlarmAssignmentRecipientsConfig.class), + @Type(name = "DEVICE_ACTIVITY", value = DefaultNotificationRuleRecipientsConfig.DeviceActivityRecipientsConfig.class), + @Type(name = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT", value = DefaultNotificationRuleRecipientsConfig.RuleEngineComponentLifecycleEventRecipientsConfig.class), + @Type(name = "EDGE_CONNECTION", value = DefaultNotificationRuleRecipientsConfig.EdgeConnectionRecipientsConfig.class), + @Type(name = "EDGE_COMMUNICATION_FAILURE", value = DefaultNotificationRuleRecipientsConfig.EdgeCommunicationFailureRecipientsConfig.class), + @Type(name = "NEW_PLATFORM_VERSION", value = DefaultNotificationRuleRecipientsConfig.NewPlatformVersionRecipientsConfig.class), + @Type(name = "ENTITIES_LIMIT", value = DefaultNotificationRuleRecipientsConfig.EntitiesLimitRecipientsConfig.class), + @Type(name = "API_USAGE_LIMIT", value = DefaultNotificationRuleRecipientsConfig.ApiUsageLimitRecipientsConfig.class), + @Type(name = "RATE_LIMITS", value = DefaultNotificationRuleRecipientsConfig.RateLimitsRecipientsConfig.class), + @Type(name = "TASK_PROCESSING_FAILURE", value = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class), + @Type(name = "RESOURCES_SHORTAGE", value = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class) }) -@Data -public abstract class NotificationRuleRecipientsConfig implements Serializable { +public interface NotificationRuleRecipientsConfig extends Serializable { - @NotNull - @JsonProperty("triggerType") - private NotificationRuleTriggerType triggerType; + NotificationRuleTriggerType getTriggerType(); @JsonIgnore - public abstract Map> getTargetsTable(); + Map> getTargetsTable(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java index 2a07250a4c..db945b9041 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java @@ -25,10 +25,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; + @Data @Builder public class AlarmCommentTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -8614770559491757202L; + private final TenantId tenantId; private final AlarmComment comment; private final Alarm alarm; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java index cfc8d6b00b..ed2fd7b6c1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java @@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; + @Data @Builder public class AlarmTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -466810297904938644L; + private final TenantId tenantId; private final AlarmApiCallResult alarmUpdate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java index fd6b089a42..7511fd7f2a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; @@ -66,6 +67,7 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger { return NotificationRuleTriggerType.RESOURCES_SHORTAGE; } + @Schema public enum Resource { CPU, RAM, STORAGE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java index d9acca4930..0cd2e441ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -23,18 +25,27 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.util.Set; @Data @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -5313556049809972096L; + + @ArraySchema(schema = @Schema(implementation = String.class)) private Set alarmTypes; + @ArraySchema(schema = @Schema(implementation = AlarmSeverity.class)) private Set alarmSeverities; + @ArraySchema(schema = @Schema(implementation = AlarmSearchStatus.class)) private Set alarmStatuses; @NotEmpty + @ArraySchema(schema = @Schema(implementation = Action.class)) private Set notifyOn; @Override @@ -42,6 +53,7 @@ public class AlarmAssignmentNotificationRuleTriggerConfig implements Notificatio return NotificationRuleTriggerType.ALARM_ASSIGNMENT; } + @Schema public enum Action { ASSIGNED, UNASSIGNED } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java index f220002fc0..c651077bed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -22,18 +24,28 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.util.Set; @Data @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -9164282098882339645L; + + @ArraySchema(schema = @Schema(implementation = String.class)) private Set alarmTypes; + @ArraySchema(schema = @Schema(implementation = AlarmSeverity.class)) private Set alarmSeverities; + @ArraySchema(schema = @Schema(implementation = AlarmSearchStatus.class)) private Set alarmStatuses; + @Schema private boolean onlyUserComments; + @Schema private boolean notifyOnCommentUpdate; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java index 041d10ba90..f2be38d6f9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -23,6 +25,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.io.Serializable; import java.util.Set; @@ -30,13 +33,20 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -7382883720381542344L; + + @ArraySchema(schema = @Schema(implementation = String.class)) private Set alarmTypes; + @ArraySchema(schema = @Schema(implementation = AlarmSeverity.class)) private Set alarmSeverities; @NotEmpty + @ArraySchema(schema = @Schema(implementation = AlarmAction.class)) private Set notifyOn; - + @Schema private ClearRule clearRule; @Override @@ -46,9 +56,12 @@ public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTrigg @Data public static class ClearRule implements Serializable { + @Serial + private static final long serialVersionUID = 7922533150038105124L; private Set alarmStatuses; } + @Schema public enum AlarmAction { CREATED, SEVERITY_CHANGED, ACKNOWLEDGED, CLEARED } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java index e7051778b2..77c75dfce0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,9 +30,12 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class ApiUsageLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = ApiFeature.class)) private Set apiFeatures; + @ArraySchema(schema = @Schema(implementation = ApiUsageStateValue.class)) private Set notifyOn; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java index 2ad8586f75..568ec50d51 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,11 +30,15 @@ import java.util.UUID; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class DeviceActivityNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set devices; + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set deviceProfiles; // set either devices or profiles @NotEmpty + @ArraySchema(schema = @Schema(implementation = DeviceEvent.class)) private Set notifyOn; @Override @@ -40,6 +46,7 @@ public class DeviceActivityNotificationRuleTriggerConfig implements Notification return NotificationRuleTriggerType.DEVICE_ACTIVITY; } + @Schema public enum DeviceEvent { ACTIVE, INACTIVE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java index 6def4c1799..8d180ae5bf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -27,8 +29,10 @@ import java.util.UUID; @NoArgsConstructor @AllArgsConstructor @Builder +@Schema public class EdgeCommunicationFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set edges; // if empty - all edges @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java index 264fa9aa55..3caa45f32a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -27,9 +29,12 @@ import java.util.UUID; @NoArgsConstructor @AllArgsConstructor @Builder +@Schema public class EdgeConnectionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set edges; // if empty - all edges + @ArraySchema(schema = @Schema(implementation = EdgeConnectivityEvent.class)) private Set notifyOn; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java index 7c8997dae5..9b51687a6c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,8 +30,10 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class EntitiesLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = EntityType.class)) private Set entityTypes; @Max(1) private float threshold; // in percents, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java index bdc871ad96..d838d899f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -27,8 +29,10 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class EntityActionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = EntityType.class)) private Set entityTypes; // maybe add name filter ? private boolean created; private boolean updated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java index b625efb506..64154b6841 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data +@Schema public class NewPlatformVersionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java index 6fb4bbd48b..a58b3d5eef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java @@ -20,9 +20,32 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; +@Schema( + name = "NotificationRuleTriggerConfig", + description = "Configuration for notification rule trigger", + discriminatorProperty = "triggerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "ALARM", schema = AlarmNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "DEVICE_ACTIVITY", schema = DeviceActivityNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ENTITY_ACTION", schema = EntityActionNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ALARM_COMMENT", schema = AlarmCommentNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT", schema = RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ALARM_ASSIGNMENT", schema = AlarmAssignmentNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "NEW_PLATFORM_VERSION", schema = NewPlatformVersionNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ENTITIES_LIMIT", schema = EntitiesLimitNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "API_USAGE_LIMIT", schema = ApiUsageLimitNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "RATE_LIMITS", schema = RateLimitsNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "EDGE_CONNECTION", schema = EdgeConnectionNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "EDGE_COMMUNICATION_FAILURE", schema = EdgeCommunicationFailureNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "TASK_PROCESSING_FAILURE", schema = TaskProcessingFailureNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = ResourcesShortageNotificationRuleTriggerConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType") @JsonSubTypes({ @@ -43,6 +66,7 @@ import java.io.Serializable; }) public interface NotificationRuleTriggerConfig extends Serializable { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) NotificationRuleTriggerType getTriggerType(); @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java index 982e20284c..8f2ae1cdc9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,8 +30,10 @@ import java.util.stream.Collectors; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class RateLimitsNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = LimitedApi.class)) private Set apis; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java index 77782a73cf..f62ed241ca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import lombok.AllArgsConstructor; import lombok.Builder; @@ -27,6 +28,7 @@ import java.io.Serial; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class ResourcesShortageNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Serial diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java index 4b46c26d65..7e987f1d0b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,14 +30,17 @@ import java.util.UUID; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set ruleChains; // if empty - all rule chains - + @ArraySchema(schema = @Schema(implementation = ComponentLifecycleEvent.class)) private Set ruleChainEvents; // available options: STARTED, UPDATED, STOPPED. if empty - all events private boolean onlyRuleChainLifecycleFailures; private boolean trackRuleNodeEvents; + @ArraySchema(schema = @Schema(implementation = ComponentLifecycleEvent.class)) private Set ruleNodeEvents; // available options: STARTED, UPDATED, STOPPED. if empty - all events private boolean onlyRuleNodeLifecycleFailures; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java index 6a88000124..59fd219705 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @Data @Builder +@Schema public class TaskProcessingFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java index b5b573a452..2a708ed998 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.notification.settings; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +@Schema @Data public class MobileAppNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java index 5eecb63b1e..eb3236ec00 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java @@ -20,10 +20,19 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import java.io.Serializable; +@Schema( + discriminatorProperty = "method", + discriminatorMapping = { + @DiscriminatorMapping(value = "SLACK", schema = SlackNotificationDeliveryMethodConfig.class), + @DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppNotificationDeliveryMethodConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java index fbfc9f9d95..d5facf4d00 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.settings; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import java.io.Serializable; import java.util.Map; +@Schema @Data public class NotificationSettings implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java index f2a8a0d6da..283c09bbd8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.notification.settings; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +@Schema @Data public class SlackNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java index a241616712..d551aab4c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.notification.targets; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class MicrosoftTeamsNotificationTargetConfig extends NotificationTargetConfig implements NotificationRecipient { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java index 74a2544d11..932d954d58 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.targets; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class NotificationTarget extends BaseData implements HasTenantId, HasName, ExportableEntity { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java index 98468b9521..2f7f2615bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java @@ -20,12 +20,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "PLATFORM_USERS", schema = PlatformUsersNotificationTargetConfig.class), + @DiscriminatorMapping(value = "SLACK", schema = SlackNotificationTargetConfig.class), + @DiscriminatorMapping(value = "MICROSOFT_TEAMS", schema = MicrosoftTeamsNotificationTargetConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java index 7dc10cd6ee..96dda12884 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class AffectedTenantAdministratorsFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java index c1e600d5bb..b2c5348d3b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class AffectedUserFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java index 3b603f43e9..119859db7c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data -public class AllUsersFilter implements UsersFilter { +public class AllUsersFilter implements SystemLevelUsersFilter { @Override public UsersFilterType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java index 34df18244f..bc63801124 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.UUID; +@Schema @Data public class CustomerUsersFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java index a7dfdff074..375b47cc03 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class OriginatorEntityOwnerUsersFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java index 1d97552c2e..df26d6ca65 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -22,10 +23,12 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class PlatformUsersNotificationTargetConfig extends NotificationTargetConfig { + @Schema(implementation = UsersFilter.class) @NotNull @Valid private UsersFilter usersFilter; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java index a0b49b1072..8daf518089 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data -public class SystemAdministratorsFilter implements UsersFilter { +public class SystemAdministratorsFilter implements SystemLevelUsersFilter { @Override public UsersFilterType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java new file mode 100644 index 0000000000..9d5cfed5a8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.notification.targets.platform; + +public interface SystemLevelUsersFilter extends UsersFilter { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java index 09641cf069..3b551b4fbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java @@ -15,15 +15,20 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.Set; import java.util.UUID; +@Schema @Data -public class TenantAdministratorsFilter implements UsersFilter { +public class TenantAdministratorsFilter implements SystemLevelUsersFilter { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set tenantsIds; + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set tenantProfilesIds; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java index e0adb26d59..2534bc8dbc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java @@ -15,15 +15,19 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import java.util.List; import java.util.UUID; +@Schema @Data public class UserListFilter implements UsersFilter { + @ArraySchema(schema = @Schema(implementation = UUID.class)) @NotEmpty private List usersIds; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java index 596972be80..2b3628012c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java @@ -20,7 +20,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "USER_LIST", schema = UserListFilter.class), + @DiscriminatorMapping(value = "CUSTOMER_USERS", schema = CustomerUsersFilter.class), + @DiscriminatorMapping(value = "TENANT_ADMINISTRATORS", schema = TenantAdministratorsFilter.class), + @DiscriminatorMapping(value = "AFFECTED_TENANT_ADMINISTRATORS", schema = AffectedTenantAdministratorsFilter.class), + @DiscriminatorMapping(value = "SYSTEM_ADMINISTRATORS", schema = SystemAdministratorsFilter.class), + @DiscriminatorMapping(value = "ALL_USERS", schema = AllUsersFilter.class), + @DiscriminatorMapping(value = "ORIGINATOR_ENTITY_OWNER_USERS", schema = OriginatorEntityOwnerUsersFilter.class), + @DiscriminatorMapping(value = "AFFECTED_USER", schema = AffectedUserFilter.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java index 4febcc1d72..0b8992309e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.notification.targets.slack; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip import static org.apache.commons.lang3.StringUtils.isEmpty; +@Schema @Data @NoArgsConstructor @AllArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java index 11c84549d2..ba21291465 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.targets.slack; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -22,6 +23,7 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class SlackNotificationTargetConfig extends NotificationTargetConfig { @@ -31,6 +33,7 @@ public class SlackNotificationTargetConfig extends NotificationTargetConfig { @Valid private SlackConversation conversation; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the notification target") @Override public NotificationTargetType getType() { return NotificationTargetType.SLACK; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index a679d0e6e7..e8a9784310 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.NoArgsConstructor; @@ -27,6 +29,18 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import java.util.List; +@Schema( + description = "Base template for different delivery methods", + discriminatorProperty = "method", + discriminatorMapping = { + @DiscriminatorMapping(value = "WEB", schema = WebDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "EMAIL", schema = EmailDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "SMS", schema = SmsDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "SLACK", schema = SlackDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "MICROSOFT_TEAMS", schema = MicrosoftTeamsDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppDeliveryMethodNotificationTemplate.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java index 59293e9429..261c127ce4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.template; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -30,6 +31,7 @@ import java.util.List; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) +@Schema public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { @NoXss(fieldName = "email subject") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java index 97def8310e..4d03afd7e2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.template; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -30,8 +31,11 @@ import java.util.UUID; @ToString(callSuper = true) public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @Schema private String subject; + @Schema private String themeColor; + @Schema private Button button; private final List templatableValues = List.of( @@ -82,6 +86,7 @@ public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMe this.setEntityIdInState = other.setEntityIdInState; } + @Schema public enum LinkType { LINK, DASHBOARD } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java index 248d2dedf5..c7d8689f45 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.notification.template; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -31,8 +32,10 @@ import java.util.List; @ToString(callSuper = true) public class MobileAppDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @Schema(description = "Subject line for the mobile notification", example = "New Message Received") @NotEmpty private String subject; + @Schema(description = "Additional JSON configuration for web buttons/actions", type = "object") private JsonNode additionalConfig; private final List templatableValues = List.of( diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java index 9385c00abf..c0229678f6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -37,10 +38,12 @@ import java.util.Optional; @ToString(callSuper = true) public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @Schema(description = "Subject line for the web notification", example = "New Message Received") @NoXss(fieldName = "web notification subject") @Length(fieldName = "web notification subject", max = 150, message = "cannot be longer than 150 chars") @NotEmpty private String subject; + @Schema(description = "Additional JSON configuration for web buttons/actions") private JsonNode additionalConfig; private final List templatableValues = List.of( diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java index 23c0241d51..d5f61fdbeb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java @@ -93,7 +93,10 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp @Schema(description = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") @Length(fieldName = "platforms") private List platforms; - @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Additional info of OAuth2 client. " + + "Must include: 'providerName' (string, name of the OAuth2 provider).", + requiredMode = Schema.RequiredMode.REQUIRED, + example = "{\"providerName\":\"Google\"}") private JsonNode additionalInfo; public OAuth2Client() { @@ -128,4 +131,5 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp public String getName() { return title; } + } 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 index 68f06d94d4..25cd734d77 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.page; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @@ -36,6 +37,7 @@ public class SortOrder { return new SortOrder(property, direction); } + @Schema(name = "SortOrderDirection") public static enum Direction { ASC, DESC } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKey.java new file mode 100644 index 0000000000..d305c218ce --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKey.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.ApiKeyId; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.Serial; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +public class ApiKey extends ApiKeyInfo { + + @Serial + private static final long serialVersionUID = -2313196723950490263L; + + @NoXss + @Schema(description = "API key value", requiredMode = Schema.RequiredMode.REQUIRED) + private String value; + + public ApiKey() { + super(); + } + + public ApiKey(ApiKeyId id) { + super(id); + } + + public ApiKey(ApiKey apiKey) { + super(apiKey); + this.value = apiKey.getValue(); + } + + public ApiKey(ApiKeyInfo apiKeyInfo) { + super(apiKeyInfo); + this.value = null; + } + + public ApiKey(ApiKeyInfo apiKeyInfo, String value) { + super(apiKeyInfo); + this.value = value; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java new file mode 100644 index 0000000000..bfb9e3cabb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.ApiKeyId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.Serial; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +public class ApiKeyInfo extends BaseData implements HasTenantId { + + @Serial + private static final long serialVersionUID = -2313196723950490263L; + + @Schema(description = "JSON object with Tenant Id. Tenant Id of the API key cannot be changed.", accessMode = Schema.AccessMode.READ_ONLY) + private TenantId tenantId; + + @Schema(description = "JSON object with User Id. User Id of the API key cannot be changed.") + private UserId userId; + + @Schema(description = "Expiration time of the API key.") + private long expirationTime; + + @NoXss + @NotBlank + @Length(fieldName = "description") + @Schema(description = "API Key description.", example = "API Key description") + private String description; + + @Schema(description = "Enabled/disabled API key.", example = "true") + private boolean enabled; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @Schema(description = "Indicates if the API key is expired based on current time. Returns false if expirationTime is 0 (no expiry).", + example = "false", + accessMode = Schema.AccessMode.READ_ONLY) + public boolean isExpired() { + if (expirationTime == 0) { + return false; + } + return System.currentTimeMillis() > expirationTime; + } + + @Schema(description = "JSON object with the API Key Id. " + + "Specify this field to update the API Key. " + + "Referencing non-existing API Key Id will cause error. " + + "Omit this field to create new API Key.") + @Override + public ApiKeyId getId() { + return super.getId(); + } + + public ApiKeyInfo() { + super(); + } + + public ApiKeyInfo(ApiKeyId id) { + super(id); + } + + public ApiKeyInfo(ApiKeyInfo apiKeyInfo) { + super(apiKeyInfo); + this.tenantId = apiKeyInfo.getTenantId(); + this.userId = apiKeyInfo.getUserId(); + this.expirationTime = apiKeyInfo.getExpirationTime(); + this.enabled = apiKeyInfo.isEnabled(); + this.description = apiKeyInfo.getDescription(); + } + +} 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 4a4c5d89bb..59bce53ab9 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 @@ -32,7 +32,9 @@ public enum ComponentLifecycleEvent implements Serializable { STOPPED(5), DELETED(6), FAILED(7), - DEACTIVATED(8); + DEACTIVATED(8), + RELATION_UPDATED(9), + RELATION_DELETED(10); @Getter private final int protoNumber; // corresponds to ComponentLifecycleEvent proto 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 index a678338864..3aabbae0a8 100644 --- 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 @@ -49,4 +49,16 @@ public abstract class AbstractDataQuery extends En this.latestValues = latestValues; } + public AbstractDataQuery(EntityFilter entityFilter, + T pageLink, + List entityFields, + List latestValues, + List keyFilters, + ComplexOperation keyFiltersOperation) { + super(entityFilter, keyFilters, keyFiltersOperation); + this.pageLink = pageLink; + this.entityFields = entityFields; + this.latestValues = latestValues; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java index f9d2e8d3d8..d45335021f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -31,6 +32,7 @@ import java.util.List; @AllArgsConstructor @Data @ToString +@Schema public class AlarmCountQuery extends EntityCountQuery { private long startTs; private long endTs; @@ -49,4 +51,8 @@ public class AlarmCountQuery extends EntityCountQuery { super(entityFilter, keyFilters); } + public AlarmCountQuery(EntityFilter entityFilter, List keyFilters, ComplexOperation keyFiltersOperation) { + super(entityFilter, keyFilters, keyFiltersOperation); + } + } 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 index 8d2ae6fae3..072f56abd1 100644 --- 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 @@ -39,8 +39,13 @@ public class AlarmDataQuery extends AbstractDataQuery { this.alarmFields = alarmFields; } + public AlarmDataQuery(EntityFilter entityFilter, AlarmDataPageLink pageLink, List entityFields, List latestValues, List keyFilters, List alarmFields, ComplexOperation keyFiltersOperation) { + super(entityFilter, pageLink, entityFields, latestValues, keyFilters, keyFiltersOperation); + this.alarmFields = alarmFields; + } + @JsonIgnore public AlarmDataQuery next() { - return new AlarmDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters, alarmFields); + return new AlarmDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters, alarmFields, getKeyFiltersOperation()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java new file mode 100644 index 0000000000..a20d0dca55 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; + +import java.util.List; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Objects.requireNonNullElse; + +@Schema( + description = "Contains unique time series and attribute key names discovered from entities matching a query. Used primarily for UI hints such as autocomplete suggestions." +) +public record AvailableEntityKeys( + @Schema( + description = "Set of entity types found among the matched entities.", + example = "[\"DEVICE\", \"ASSET\"]", + requiredMode = Schema.RequiredMode.REQUIRED + ) + Set entityTypes, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @ArraySchema( + arraySchema = @Schema(description = "List of unique time series key names available on the matched entities."), + schema = @Schema(implementation = String.class, example = "temperature"), + uniqueItems = true + ) + List timeseries, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @ArraySchema( + arraySchema = @Schema(description = "List of unique attribute key names available on the matched entities."), + schema = @Schema(implementation = String.class, example = "serialNumber"), + uniqueItems = true + ) + List attribute +) { + + public AvailableEntityKeys { + entityTypes = requireNonNullElse(entityTypes, emptySet()); + timeseries = requireNonNullElse(timeseries, emptyList()); + attribute = requireNonNullElse(attribute, emptyList()); + } + + public static AvailableEntityKeys none() { + return new AvailableEntityKeys(emptySet(), emptyList(), emptyList()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeysV2.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeysV2.java new file mode 100644 index 0000000000..536b70e4ff --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeysV2.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import org.jspecify.annotations.Nullable; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Schema( + description = """ + Contains unique time series and attribute key names discovered from entities matching a query, + optionally including a sample value for each key.""" +) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record AvailableEntityKeysV2( + @Schema( + description = "Set of entity types found among the matched entities.", + example = "[\"DEVICE\", \"ASSET\"]", + requiredMode = Schema.RequiredMode.REQUIRED + ) + Set entityTypes, + + @ArraySchema( + arraySchema = @Schema( + description = """ + List of unique time series keys available on the matched entities, sorted alphabetically. + Omitted when timeseries keys were not requested.""", + nullable = true + ), + schema = @Schema(implementation = KeyInfo.class) + ) + @Nullable List timeseries, + + @Schema( + description = """ + Map of attribute scope to the list of unique attribute keys available on the matched entities. + Only scopes supported by the matched entity types are included. + Omitted when attribute keys were not requested or when none of the requested scopes apply to the matched entity types.""", + nullable = true + ) + @Nullable Map> attributes +) { + + @Schema(description = "Key name with an optional sample value.") + @JsonInclude(JsonInclude.Include.NON_NULL) + public record KeyInfo( + @Schema( + description = "Key name.", + example = "temperature", + requiredMode = Schema.RequiredMode.REQUIRED + ) + String key, + + @Schema( + description = "Most recent sample value for this key across the matched entities. Omitted when samples were not requested.", + nullable = true + ) + @Nullable KeySample sample + ) {} + + @Schema(description = "Most recent value and its timestamp.") + public record KeySample( + @Schema( + description = "Timestamp in milliseconds since epoch.", example = "1707000000000", + requiredMode = Schema.RequiredMode.REQUIRED + ) + long ts, + + @Schema( + description = "Sample value.", + example = "23.5", + requiredMode = Schema.RequiredMode.REQUIRED, + implementation = Object.class + ) + JsonNode value + ) {} + +} 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 index 74c4f87c25..c469a7ef75 100644 --- 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 @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class BooleanFilterPredicate implements SimpleKeyFilterPredicate { @@ -28,6 +30,7 @@ public class BooleanFilterPredicate implements SimpleKeyFilterPredicate return FilterPredicateType.BOOLEAN; } + @Schema 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 index 4fb663c4c1..248211ad92 100644 --- 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 @@ -15,14 +15,18 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; @Data +@Schema public class ComplexFilterPredicate implements KeyFilterPredicate { private ComplexOperation operation; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/KeyFilterPredicate")) private List predicates; @Override @@ -30,8 +34,4 @@ public class ComplexFilterPredicate implements KeyFilterPredicate { return FilterPredicateType.COMPLEX; } - public enum ComplexOperation { - AND, - OR - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java new file mode 100644 index 0000000000..f08646ec4e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "AlarmRuleComplexOperation") +public enum ComplexOperation { + AND, + OR +} 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 index 97fe9972ba..5451e557e7 100644 --- 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 @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; +@Schema @Data @RequiredArgsConstructor public class DynamicValue implements Serializable { 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 index b4680bfd5b..ee39fe7d77 100644 --- 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 @@ -34,6 +34,9 @@ public class EntityCountQuery { @Getter protected List keyFilters; + @Getter + private ComplexOperation keyFiltersOperation; + public EntityCountQuery() { } @@ -45,4 +48,14 @@ public class EntityCountQuery { this.entityFilter = entityFilter; this.keyFilters = keyFilters; } + + public EntityCountQuery(EntityFilter entityFilter, List keyFilters, ComplexOperation keyFiltersOperation) { + this.entityFilter = entityFilter; + this.keyFilters = keyFilters; + this.keyFiltersOperation = keyFiltersOperation; + } + + public ComplexOperation getKeyFiltersOperationOrDefault() { + return keyFiltersOperation != null ? keyFiltersOperation : ComplexOperation.AND; + } } 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 index 959e3e3af6..f24ffd5821 100644 --- 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 @@ -34,9 +34,13 @@ public class EntityDataQuery extends AbstractDataQuery { super(entityFilter, pageLink, entityFields, latestValues, keyFilters); } + public EntityDataQuery(EntityFilter entityFilter, EntityDataPageLink pageLink, List entityFields, List latestValues, List keyFilters, ComplexOperation keyFiltersOperation) { + super(entityFilter, pageLink, entityFields, latestValues, keyFilters, keyFiltersOperation); + } + @JsonIgnore public EntityDataQuery next() { - return new EntityDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters); + return new EntityDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters, getKeyFiltersOperation()); } } 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 index dae84fa598..c7b933add5 100644 --- 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 @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class EntityDataSortOrder { @@ -34,6 +36,7 @@ public class EntityDataSortOrder { this.direction = direction; } + @Schema 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 index 4263a4aeea..bf75f7e9ca 100644 --- 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 @@ -19,12 +19,34 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; 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 static org.thingsboard.server.common.data.query.AliasEntityId.resolveAliasEntityId; +@Schema( + description = "Filter for selecting entities", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "singleEntity", schema = SingleEntityFilter.class), + @DiscriminatorMapping(value = "entityList", schema = EntityListFilter.class), + @DiscriminatorMapping(value = "entityName", schema = EntityNameFilter.class), + @DiscriminatorMapping(value = "entityType", schema = EntityTypeFilter.class), + @DiscriminatorMapping(value = "assetType", schema = AssetTypeFilter.class), + @DiscriminatorMapping(value = "deviceType", schema = DeviceTypeFilter.class), + @DiscriminatorMapping(value = "edgeType", schema = EdgeTypeFilter.class), + @DiscriminatorMapping(value = "entityViewType", schema = EntityViewTypeFilter.class), + @DiscriminatorMapping(value = "apiUsageState", schema = ApiUsageStateFilter.class), + @DiscriminatorMapping(value = "relationsQuery", schema = RelationsQueryFilter.class), + @DiscriminatorMapping(value = "assetSearchQuery", schema = AssetSearchQueryFilter.class), + @DiscriminatorMapping(value = "deviceSearchQuery", schema = DeviceSearchQueryFilter.class), + @DiscriminatorMapping(value = "entityViewSearchQuery", schema = EntityViewSearchQueryFilter.class), + @DiscriminatorMapping(value = "edgeSearchQuery", schema = EdgeSearchQueryFilter.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, 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 index 8501ddca12..3103a1a161 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; 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 index 6a080e3b28..cb844c5c44 100644 --- 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 @@ -18,6 +18,7 @@ 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 io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.Data; import lombok.Getter; @@ -25,6 +26,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; +@Schema @Data public class FilterPredicateValue implements Serializable { 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 index 949ccc9bc5..cb5c7a835f 100644 --- 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 @@ -18,9 +18,21 @@ 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; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; +@Schema( + description = "Filter predicate for key-based filtering", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "STRING", schema = StringFilterPredicate.class), + @DiscriminatorMapping(value = "NUMERIC", schema = NumericFilterPredicate.class), + @DiscriminatorMapping(value = "BOOLEAN", schema = BooleanFilterPredicate.class), + @DiscriminatorMapping(value = "COMPLEX", schema = ComplexFilterPredicate.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, 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 index 0b19d56ea6..0d7953defc 100644 --- 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 @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class NumericFilterPredicate implements SimpleKeyFilterPredicate { @@ -28,6 +30,7 @@ public class NumericFilterPredicate implements SimpleKeyFilterPredicate return FilterPredicateType.NUMERIC; } + @Schema public enum NumericOperation { EQUAL, NOT_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 index 8c063201f7..027c4a7c5c 100644 --- 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 @@ -15,15 +15,17 @@ */ package org.thingsboard.server.common.data.query; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; import java.util.Set; +@Schema @Data public class RelationsQueryFilter implements EntityFilter { @@ -33,6 +35,7 @@ public class RelationsQueryFilter implements EntityFilter { } private AliasEntityId rootEntity; + @JsonProperty("multiRoot") private boolean isMultiRoot; private EntityType multiRootEntitiesType; private Set multiRootEntityIds; 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 index 2a06eb57c2..f85c259034 100644 --- 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 @@ -15,8 +15,12 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Base interface for simple key filter predicates") public interface SimpleKeyFilterPredicate extends KeyFilterPredicate { + @Schema(description = "The value associated with the filter predicate") 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 index 046f86f414..c0bbd43378 100644 --- 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 @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.query; import lombok.Data; -import org.thingsboard.server.common.data.id.EntityId; @Data public class SingleEntityFilter implements EntityFilter { 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 index 915dc6ef5f..da127a58b3 100644 --- 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 @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.Data; @Data +@Schema public class StringFilterPredicate implements SimpleKeyFilterPredicate { private StringOperation operation; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index e775dc6ddf..fc24327a6d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -18,11 +18,11 @@ package org.thingsboard.server.common.data.relation; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; import lombok.ToString; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.ObjectType; @@ -30,16 +30,19 @@ import org.thingsboard.server.common.data.edqs.EdqsObject; import org.thingsboard.server.common.data.edqs.EdqsObjectKey; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoNullChar; +import java.io.Serial; import java.io.Serializable; import java.util.UUID; -@Slf4j +@Data @Schema @EqualsAndHashCode(exclude = "additionalInfoBytes") @ToString(exclude = {"additionalInfoBytes"}) public class EntityRelation implements HasVersion, Serializable, EdqsObject { + @Serial private static final long serialVersionUID = 2807343040519543363L; public static final String EDGE_TYPE = "ManagedByEdge"; @@ -47,18 +50,26 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject { public static final String MANAGES_TYPE = "Manages"; public static final String USES_TYPE = "Uses"; - @Setter + @NotNull + @Schema(description = "JSON object with [from] Entity Id.", accessMode = Schema.AccessMode.READ_WRITE) private EntityId from; - @Setter + + @NotNull + @Schema(description = "JSON object with [to] Entity Id.", accessMode = Schema.AccessMode.READ_WRITE) private EntityId to; - @Setter - @Length(fieldName = "type") + + @NotBlank + @NoNullChar + @Length(max = 255, fieldName = "type") + @Schema(description = "String value of relation type.", example = "Contains") private String type; - @Setter + + @NotNull + @Schema(description = "Represents the type group of the relation.", example = "COMMON") private RelationTypeGroup typeGroup; - @Getter - @Setter + private Long version; + private transient JsonNode additionalInfo; @JsonIgnore private byte[] additionalInfoBytes; @@ -92,27 +103,9 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject { this.version = entityRelation.getVersion(); } - @Schema(description = "JSON object with [from] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY) - public EntityId getFrom() { - return from; - } - - @Schema(description = "JSON object with [to] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY) - public EntityId getTo() { - return to; - } - - @Schema(description = "String value of relation type.", example = "Contains") - public String getType() { - return type; - } - - @Schema(description = "Represents the type group of the relation.", example = "COMMON") - public RelationTypeGroup getTypeGroup() { - return typeGroup; - } - - @Schema(description = "Additional parameters of the relation", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the relation.", + implementation = JsonNode.class, + example = "{\"description\":\"Power supply connection\"}") public JsonNode getAdditionalInfo() { return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java new file mode 100644 index 0000000000..3a4d8f0f99 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.relation; + +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + +public record EntityRelationPathQuery(EntityId rootEntityId, List levels) { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java new file mode 100644 index 0000000000..cff16586e5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.relation; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.thingsboard.server.common.data.StringUtils; + +public record RelationPathLevel(@NotNull EntitySearchDirection direction, @NotBlank String relationType) { + + public void validate() { + if (direction == null) { + throw new IllegalArgumentException("Direction must be specified!"); + } + if (StringUtils.isBlank(relationType)) { + throw new IllegalArgumentException("Relation type must be specified!"); + } + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 4d8bb35919..f81bf841e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -41,7 +41,7 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement private static final long serialVersionUID = -5656679015121935465L; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; @NoXss @Length(fieldName = "name") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java new file mode 100644 index 0000000000..439fcc01d1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.ArrayList; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RuleChainDetails extends RuleChain { + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @Schema(description = "List of sticky notes placed on the rule chain canvas") + private List notes; + + public RuleChainDetails() { + super(); + } + + public RuleChainDetails(RuleChain ruleChain) { + super(ruleChain); + } + + public RuleChainDetails(RuleChainDetails ruleChainDetails) { + super(ruleChainDetails); + this.notes = ruleChainDetails.getNotes() != null ? new ArrayList<>(ruleChainDetails.getNotes()) : null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index c9fdd1a4c6..9109df0f88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.rule; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.HasVersion; @@ -48,6 +49,10 @@ public class RuleChainMetaData implements HasVersion { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of JSON objects that represent connections between rule nodes and other rule chains.") private List ruleChainConnections; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "List of sticky notes placed on the rule chain canvas") + private List notes; + public void addConnectionInfo(int fromIndex, int toIndex, String type) { NodeConnectionInfo connectionInfo = new NodeConnectionInfo(); connectionInfo.setFromIndex(fromIndex); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java new file mode 100644 index 0000000000..97caa0ffbe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +/** + * Represents a sticky note on the rule chain canvas. + * Notes are purely visual metadata and are stored as part of the rule chain. + */ +@Schema +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RuleChainNote { + + @Schema(description = "Unique identifier of the note on the canvas") + private String id; + + @Schema(description = "Horizontal position of the note on the canvas, in pixels") + private int x; + + @Schema(description = "Vertical position of the note on the canvas, in pixels") + private int y; + + @Schema(description = "Width of the note, in pixels") + private int width; + + @Schema(description = "Height of the note, in pixels") + private int height; + + @ToString.Exclude + @Schema(description = "Markdown or HTML content of the note") + private String content; + + @Schema(description = "Background color of the note in CSS hex format, e.g. '#FFF9C4'") + private String backgroundColor; + + @Schema(description = "Border color of the note in CSS hex format, e.g. '#E6C800'") + private String borderColor; + + @Schema(description = "Border width of the note in pixels") + private Integer borderWidth; + + @Schema(description = "Whether to apply the default markdown stylesheet to the note content") + private Boolean applyDefaultMarkdownStyle; + + @ToString.Exclude + @Schema(description = "Custom CSS styles applied to the note content") + private String markdownCss; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java index 981d5c2598..ac1e85dbeb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java @@ -105,7 +105,12 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements return super.getCreatedTime(); } - @Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = JsonNode.class) + @Schema(description = "Additional parameters of the rule node. " + + "May include: 'layoutX' (number, X coordinate for visualization), " + + "'layoutY' (number, Y coordinate for visualization), " + + "'description' (string).", + implementation = JsonNode.class, + example = "{\"layoutX\":320,\"layoutY\":160,\"description\":\"Filter temperature data\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java index 171bb18077..9c7a3bd189 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java @@ -20,8 +20,10 @@ public enum Authority { SYS_ADMIN(0), TENANT_ADMIN(1), CUSTOMER_USER(2), + REFRESH_TOKEN(10), - PRE_VERIFICATION_TOKEN(11); + PRE_VERIFICATION_TOKEN(11), + MFA_CONFIGURATION_TOKEN(12); private int code; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtToken.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtToken.java index 4ced536626..fa35c35735 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtToken.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtToken.java @@ -18,5 +18,7 @@ package org.thingsboard.server.common.data.security.model; import java.io.Serializable; public interface JwtToken extends Serializable { - String getToken(); + + String token(); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java index 6528f581cd..e26287708b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java @@ -16,11 +16,14 @@ package org.thingsboard.server.common.data.security.model.mfa; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Data; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -28,9 +31,11 @@ import java.util.List; import java.util.Optional; @Data +@Schema(description = "Platform Two-Factor Authentication settings") @JsonIgnoreProperties(ignoreUnknown = true) public class PlatformTwoFaSettings { + @ArraySchema(schema = @Schema(implementation = TwoFaProviderConfig.class)) @Valid @NotNull private List providers; @@ -46,6 +51,8 @@ public class PlatformTwoFaSettings { @Min(value = 60) private Integer totalAllowedTimeForVerification; + private boolean enforceTwoFa; + private SystemLevelUsersFilter enforcedUsersFilter; public Optional getProviderConfig(TwoFaProviderType providerType) { return Optional.ofNullable(providers) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java index b24bd68c52..f613b37220 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import java.util.LinkedHashMap; @Data +@Schema(description = "Account Two-Factor Authentication Settings") public class AccountTwoFaSettings { private LinkedHashMap configs; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java index 3c673c1e8e..74a804688d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.security.model.mfa.account; import com.fasterxml.jackson.annotation.JsonGetter; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi import java.util.Set; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class BackupCodeTwoFaAccountConfig extends TwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java index 5960fa8a34..5e87cf5ce7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class EmailTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java index 574a8e01b3..e5903fecf5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +@Schema @Data @EqualsAndHashCode(callSuper = true) public abstract class OtpBasedTwoFaAccountConfig extends TwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java index 97755f6195..1a9b2b718e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +@Schema @EqualsAndHashCode(callSuper = true) @Data public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java index 384ca39e3b..799e381b4f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class TotpTwoFaAccountConfig extends TwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java index d8783178e0..06a5088117 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -35,6 +37,16 @@ import java.io.Serializable; @Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class), @Type(name = "BACKUP_CODE", value = BackupCodeTwoFaAccountConfig.class) }) +@Schema( + description = "Base configuration for two-factor authentication accounts", + discriminatorProperty = "providerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "TOTP", schema = TotpTwoFaAccountConfig.class), + @DiscriminatorMapping(value = "SMS", schema = SmsTwoFaAccountConfig.class), + @DiscriminatorMapping(value = "EMAIL", schema = EmailTwoFaAccountConfig.class), + @DiscriminatorMapping(value = "BACKUP_CODE", schema = BackupCodeTwoFaAccountConfig.class) + } +) @Data public abstract class TwoFaAccountConfig implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java index 18da5115f2..5346f996fd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import lombok.Data; @Data +@Schema public class BackupCodeTwoFaProviderConfig implements TwoFaProviderConfig { @Min(value = 1, message = "must be greater than 0") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java index 56527ebf8a..f9b6d2e618 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Data +@Schema @EqualsAndHashCode(callSuper = true) public class EmailTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java index 35f18d50e0..bd6b8f4332 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import lombok.Data; @Data +@Schema public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig { @Min(value = 1, message = "is required") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java index e848c7b942..dba2372989 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; @@ -22,6 +23,7 @@ import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) @Data +@Schema public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig { @NotBlank(message = "is required") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java index de91a5b732..64cabcbbbd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data +@Schema public class TotpTwoFaProviderConfig implements TwoFaProviderConfig { @NotBlank diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java index 42c3556b08..1bb883b375 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java @@ -20,7 +20,19 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + description = "Two-factor authentication provider configuration", + discriminatorProperty = "providerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "TOTP", schema = TotpTwoFaProviderConfig.class), + @DiscriminatorMapping(value = "SMS", schema = SmsTwoFaProviderConfig.class), + @DiscriminatorMapping(value = "EMAIL", schema = EmailTwoFaProviderConfig.class), + @DiscriminatorMapping(value = "BACKUP_CODE", schema = BackupCodeTwoFaProviderConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java index 02f1db557b..e35f8da2e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.sms.config; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { @Schema(description = "SMPP version", allowableValues = "3.3, 3.4", requiredMode = Schema.RequiredMode.REQUIRED) @@ -89,7 +90,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { @Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String addressRange; - @Schema(allowableValues = {"0-10" ,"13-14"}, + @Schema(minimum = "0", maximum = "14", description = "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free, used as default)\n" + "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))\n" + "2 - Octet Unspecified (8-bit binary)\n" + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java index 5bba9a578b..5b6bceec35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java @@ -19,6 +19,8 @@ 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 io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( @@ -30,6 +32,15 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = TwilioSmsProviderConfiguration.class, name = "TWILIO"), @JsonSubTypes.Type(value = SmppSmsProviderConfiguration.class, name = "SMPP") }) +@Schema( + description = "Base configuration for SMS providers", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "AWS_SNS", schema = AwsSnsSmsProviderConfiguration.class), + @DiscriminatorMapping(value = "TWILIO", schema = TwilioSmsProviderConfiguration.class), + @DiscriminatorMapping(value = "SMPP", schema = SmppSmsProviderConfiguration.class) + } +) public interface SmsProviderConfiguration { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java index 883a8e383b..2373f53283 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java @@ -18,17 +18,23 @@ package org.thingsboard.server.common.data.sync.ie; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.security.DeviceCredentials; +@Schema @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Data public class DeviceExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.DEVICE; } + @JsonProperty(index = 3) @JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"}) private DeviceCredentials credentials; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index 01b07026fa..3e04074236 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -23,11 +23,25 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.JsonTbEntity; @@ -36,17 +50,50 @@ import java.util.List; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true, defaultImpl = EntityExportData.class) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true) @JsonSubTypes({ @Type(name = "DEVICE", value = DeviceExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class), - @Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class) + @Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class), + @Type(name = "CUSTOMER", value = EntityExportData.CustomerExportData.class), + @Type(name = "TB_RESOURCE", value = EntityExportData.TbResourceExportData.class), + @Type(name = "DASHBOARD", value = EntityExportData.DashboardExportData.class), + @Type(name = "ASSET_PROFILE", value = EntityExportData.AssetProfileExportData.class), + @Type(name = "ASSET", value = EntityExportData.AssetExportData.class), + @Type(name = "DEVICE_PROFILE", value = EntityExportData.DeviceProfileExportData.class), + @Type(name = "ENTITY_VIEW", value = EntityExportData.EntityViewExportData.class), + @Type(name = "NOTIFICATION_TEMPLATE", value = EntityExportData.NotificationTemplateExportData.class), + @Type(name = "NOTIFICATION_TARGET", value = EntityExportData.NotificationTargetExportData.class), + @Type(name = "NOTIFICATION_RULE", value = EntityExportData.NotificationRuleExportData.class), + @Type(name = "AI_MODEL", value = EntityExportData.AiModelExportData.class) }) @JsonInclude(JsonInclude.Include.NON_NULL) +@Schema( + description = "Base export container for ThingsBoard entities", + discriminatorProperty = "entityType", + discriminatorMapping = { + @DiscriminatorMapping(value = "CUSTOMER", schema = EntityExportData.CustomerExportData.class), + @DiscriminatorMapping(value = "DEVICE", schema = DeviceExportData.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = RuleChainExportData.class), + @DiscriminatorMapping(value = "WIDGET_TYPE", schema = WidgetTypeExportData.class), + @DiscriminatorMapping(value = "WIDGETS_BUNDLE", schema = WidgetsBundleExportData.class), + @DiscriminatorMapping(value = "OTA_PACKAGE", schema = OtaPackageExportData.class), + @DiscriminatorMapping(value = "TB_RESOURCE", schema = EntityExportData.TbResourceExportData.class), + @DiscriminatorMapping(value = "DASHBOARD", schema = EntityExportData.DashboardExportData.class), + @DiscriminatorMapping(value = "ASSET_PROFILE", schema = EntityExportData.AssetProfileExportData.class), + @DiscriminatorMapping(value = "ASSET", schema = EntityExportData.AssetExportData.class), + @DiscriminatorMapping(value = "DEVICE_PROFILE", schema = EntityExportData.DeviceProfileExportData.class), + @DiscriminatorMapping(value = "ENTITY_VIEW", schema = EntityExportData.EntityViewExportData.class), + @DiscriminatorMapping(value = "NOTIFICATION_TEMPLATE", schema = EntityExportData.NotificationTemplateExportData.class), + @DiscriminatorMapping(value = "NOTIFICATION_TARGET", schema = EntityExportData.NotificationTargetExportData.class), + @DiscriminatorMapping(value = "NOTIFICATION_RULE", schema = EntityExportData.NotificationRuleExportData.class), + @DiscriminatorMapping(value = "AI_MODEL", schema = EntityExportData.AiModelExportData.class) + } +) @Data -public class EntityExportData> { +public abstract class EntityExportData> { public static final Comparator relationsComparator = Comparator .comparing(EntityRelation::getFrom, Comparator.comparing(EntityId::getId)) @@ -61,18 +108,45 @@ public class EntityExportData> { @JsonProperty(index = 2) @JsonTbEntity + @Schema(implementation = ExportableEntity.class) private E entity; @JsonProperty(index = 1) - private EntityType entityType; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + public abstract EntityType getEntityType(); @JsonProperty(index = 100) + @ArraySchema(schema = @Schema(implementation = EntityRelation.class)) private List relations; @JsonProperty(index = 101) + @Schema(description = "Map of attributes where key is the scope of attributes and value is the list of attributes for that scope") private Map> attributes; @JsonProperty(index = 102) @JsonIgnoreProperties({"id", "entityId", "createdTime", "version"}) + @ArraySchema(schema = @Schema(implementation = CalculatedField.class)) private List calculatedFields; + public static EntityExportData newInstance(EntityType entityType) { + return switch (entityType) { + case DEVICE -> new DeviceExportData(); + case RULE_CHAIN -> new RuleChainExportData(); + case WIDGET_TYPE -> new WidgetTypeExportData(); + case WIDGETS_BUNDLE -> new WidgetsBundleExportData(); + case OTA_PACKAGE -> new OtaPackageExportData(); + case CUSTOMER -> new CustomerExportData(); + case TB_RESOURCE -> new TbResourceExportData(); + case DASHBOARD -> new DashboardExportData(); + case ASSET_PROFILE -> new AssetProfileExportData(); + case ASSET -> new AssetExportData(); + case DEVICE_PROFILE -> new DeviceProfileExportData(); + case ENTITY_VIEW -> new EntityViewExportData(); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateExportData(); + case NOTIFICATION_TARGET -> new NotificationTargetExportData(); + case NOTIFICATION_RULE -> new NotificationRuleExportData(); + case AI_MODEL -> new AiModelExportData(); + default -> throw new IllegalArgumentException("Unsupported entity type: " + entityType); + }; + } + public EntityExportData sort() { if (relations != null && !relations.isEmpty()) { relations.sort(relationsComparator); @@ -111,4 +185,92 @@ public class EntityExportData> { return calculatedFields != null && !calculatedFields.isEmpty(); } + @Schema + public static class CustomerExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.CUSTOMER; + } + } + + @Schema + public static class TbResourceExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.TB_RESOURCE; + } + } + + @Schema + public static class DashboardExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.DASHBOARD; + } + } + + @Schema + public static class AssetProfileExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.ASSET_PROFILE; + } + } + + @Schema + public static class AssetExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.ASSET; + } + } + + @Schema + public static class DeviceProfileExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.DEVICE_PROFILE; + } + } + + @Schema + public static class EntityViewExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.ENTITY_VIEW; + } + } + + @Schema + public static class NotificationTemplateExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TEMPLATE; + } + } + + @Schema + public static class NotificationTargetExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TARGET; + } + } + + @Schema + public static class NotificationRuleExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_RULE; + } + } + + @Schema + public static class AiModelExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java index ebab6164b3..727c026bc3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -16,12 +16,18 @@ package org.thingsboard.server.common.data.sync.ie; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; +@Schema @EqualsAndHashCode(callSuper = true) public class OtaPackageExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.OTA_PACKAGE; } + /* * OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking) * We override both methods to ensure 'version' is not ignored during (de)serialization. diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java index c00e6a59a2..7b9ec8e0a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java @@ -17,17 +17,23 @@ package org.thingsboard.server.common.data.sync.ie; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +@Schema @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Data public class RuleChainExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.RULE_CHAIN; } + @JsonProperty(index = 3) @JsonIgnoreProperties({"ruleChainId", "version"}) private RuleChainMetaData metaData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java index 1da9a64eb1..f69b3603db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java @@ -15,12 +15,18 @@ */ package org.thingsboard.server.common.data.sync.ie; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class WidgetTypeExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.WIDGET_TYPE; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java index 978b44357a..62140abdd1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java @@ -16,18 +16,27 @@ package org.thingsboard.server.common.data.sync.ie; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.util.ArrayList; import java.util.List; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class WidgetsBundleExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; } + + @ArraySchema(arraySchema = @Schema(description = "List of widgets in the bundle"), schema = @Schema(implementation = JsonNode.class)) @JsonProperty(index = 3) private List widgets; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java index 71d715d586..c3ca21cdaf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.common.data.sync.vc; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.sync.ie.EntityExportData; +@Schema @Data @AllArgsConstructor public class EntityDataDiff { + @Schema(implementation = EntityExportData.class) private EntityExportData currentVersion; + @Schema(implementation = EntityExportData.class) private EntityExportData otherVersion; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java index d9ba1817cf..13fb3f08d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.sync.vc.request.create; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.EntityType; import java.util.Map; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class ComplexVersionCreateRequest extends VersionCreateRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java index 8a1ebb06e0..4798393cb4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.sync.vc.request.create; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityId; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class SingleEntityVersionCreateRequest extends VersionCreateRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java index fe30c1d6ae..81b4aca9a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.sync.vc.request.create; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serial; import java.io.Serializable; +@Schema @Data public class VersionCreateConfig implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java index 1939f2e222..a3e3071474 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java @@ -18,8 +18,18 @@ package org.thingsboard.server.common.data.sync.vc.request.create; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema( + description = "Request for creating a version", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SINGLE_ENTITY", schema = SingleEntityVersionCreateRequest.class), + @DiscriminatorMapping(value = "COMPLEX", schema = ComplexVersionCreateRequest.class) + } +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(name = "SINGLE_ENTITY", value = SingleEntityVersionCreateRequest.class), @@ -31,6 +41,7 @@ public abstract class VersionCreateRequest { private String versionName; private String branch; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the version to create") public abstract VersionCreateRequestType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java index 7156232eba..dfc783bffb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Data +@Schema @EqualsAndHashCode(callSuper = true) public class EntityTypeVersionLoadConfig extends VersionLoadConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java index 8bb4902f43..c3a1260fb0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.EntityType; import java.util.Map; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class EntityTypeVersionLoadRequest extends VersionLoadRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java index 5379fc69e9..46c760a60f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityId; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class SingleEntityVersionLoadRequest extends VersionLoadRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java index d7620d9ea5..7f0e3158d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data +@Schema public class VersionLoadConfig { private boolean loadRelations; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java index dffa3bdf3d..85b2def3e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java @@ -17,10 +17,20 @@ package org.thingsboard.server.common.data.sync.vc.request.load; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import static com.fasterxml.jackson.annotation.JsonSubTypes.Type; +@Schema( + description = "Request for loading a version", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SINGLE_ENTITY", schema = SingleEntityVersionLoadRequest.class), + @DiscriminatorMapping(value = "ENTITY_TYPE", schema = EntityTypeVersionLoadRequest.class) + } +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(name = "SINGLE_ENTITY", value = SingleEntityVersionLoadRequest.class), @@ -31,6 +41,7 @@ public abstract class VersionLoadRequest { private String versionId; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the version to load") public abstract VersionLoadRequestType getType(); } 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 index e8dbfd1584..297f92cffb 100644 --- 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 @@ -16,7 +16,8 @@ package org.thingsboard.server.common.data.tenant.profile; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -172,14 +173,38 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; + @Schema(example = "10") + @PositiveOrZero + private int minAllowedScheduledUpdateIntervalInSecForCF = 10; + @Builder.Default + @Schema(example = "2") + @Positive + private int maxRelationLevelPerCfArgument = 2; + @Builder.Default + @Schema(example = "100") + @Positive + private int maxRelatedEntitiesToReturnPerCfArgument = 100; @Builder.Default - @Min(value = 1, message = "must be at least 1") + @Positive @Schema(example = "1000") private long maxDataPointsPerRollingArg = 1000; @Schema(example = "32") private long maxStateSizeInKBytes = 32; @Schema(example = "2") private long maxSingleValueArgumentSizeInKBytes = 2; + @Schema(example = "10") + private long minAllowedDeduplicationIntervalInSecForCF = 10; + @Schema(example = "60") + private long minAllowedAggregationIntervalInSecForCF = 60; + @Builder.Default + @Schema(example = "300") + private long intermediateAggregationIntervalInSecForCF = 300; + @Builder.Default + @Schema(example = "60") + private long cfReevaluationCheckInterval = 60; + @Builder.Default + @Schema(example = "60") + private long alarmsReevaluationInterval = 60; @Override public long getProfileThreshold(ApiUsageRecordKey key) { @@ -235,4 +260,16 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura return maxRuleNodeExecutionsPerMessage; } + public long getCfReevaluationCheckInterval() { + return cfReevaluationCheckInterval <= 0 ? 60 : cfReevaluationCheckInterval; + } + + public long getAlarmsReevaluationInterval() { + return alarmsReevaluationInterval <= 0 ? 60 : alarmsReevaluationInterval; + } + + public long getIntermediateAggregationIntervalInSecForCF() { + return intermediateAggregationIntervalInSecForCF <= 0 ? 300 : intermediateAggregationIntervalInSecForCF; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java index 973b4807d3..008988e6cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.transport.resource; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(hidden = true) public enum ResourceType { LWM2M_MODEL, JKS, PKCS_12 } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java index 0fa4d8f053..d38733af24 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec; import org.thingsboard.server.common.data.transport.snmp.SnmpMapping; import org.thingsboard.server.common.data.transport.snmp.SnmpMethod; @@ -31,6 +33,17 @@ import org.thingsboard.server.common.data.transport.snmp.config.impl.ToDeviceRpc import java.io.Serializable; import java.util.List; +@Schema( + description = "SNMP communication configuration", + discriminatorProperty = "spec", + discriminatorMapping = { + @DiscriminatorMapping(value = "TELEMETRY_QUERYING", schema = TelemetryQueryingSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "CLIENT_ATTRIBUTES_QUERYING", schema = ClientAttributesQueryingSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "SHARED_ATTRIBUTES_SETTING", schema = SharedAttributesSettingSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "TO_DEVICE_RPC_REQUEST", schema = ToDeviceRpcRequestSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "TO_SERVER_RPC_REQUEST", schema = ToServerRpcRequestSnmpCommunicationConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "spec") @JsonSubTypes({ @@ -42,6 +55,7 @@ import java.util.List; }) public interface SnmpCommunicationConfig extends Serializable { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Specification of the SNMP communication") SnmpCommunicationSpec getSpec(); @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java index d0d8828649..7541be5078 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java @@ -18,9 +18,11 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiPredicate; import java.util.stream.Collectors; public class CollectionsUtil { @@ -95,6 +97,33 @@ public class CollectionsUtil { return false; } + public static boolean elementsEqual(Iterable iterable1, Iterable iterable2, BiPredicate equalityCheck) { + if (iterable1 instanceof Collection collection1 && iterable2 instanceof Collection collection2) { + if (collection1.size() != collection2.size()) { + return false; + } + } + + Iterator iterator1 = iterable1.iterator(); + Iterator iterator2 = iterable2.iterator(); + while (true) { + if (iterator1.hasNext()) { + if (!iterator2.hasNext()) { + return false; + } + + T o1 = iterator1.next(); + T o2 = iterator2.next(); + if (equalityCheck.test(o1, o2)) { + continue; + } else { + return false; + } + } + return !iterator2.hasNext(); + } + } + public static Set addToSet(Set existing, T value) { if (existing == null || existing.isEmpty()) { return Set.of(value); @@ -108,4 +137,12 @@ public class CollectionsUtil { return (Set) Set.of(newSet.toArray()); } + public static boolean isEmpty(Map map) { + return map == null || map.isEmpty(); + } + + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/KeyValueEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/KeyValueEntry.java new file mode 100644 index 0000000000..a4ce7c77e4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/KeyValueEntry.java @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.util; + +public record KeyValueEntry(K key, V value) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java index 78ff4b0eba..9fec33b482 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.util; +import com.google.gson.JsonParser; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.server.common.data.kv.DataType; @@ -40,6 +41,11 @@ public class TypeCastUtil { } catch (RuntimeException ignored) {} } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value)); + } else if (looksLikeJson(value)) { + try { + return Pair.of(DataType.JSON, JsonParser.parseString(value)); + } catch (Exception ignored) { + } } return Pair.of(DataType.STRING, value); } @@ -70,4 +76,10 @@ public class TypeCastUtil { return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e"); } + private static boolean looksLikeJson(String value) { + String trimmed = value.trim(); + return (trimmed.startsWith("{") && trimmed.endsWith("}")) || + (trimmed.startsWith("[") && trimmed.endsWith("]")); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index 398f4b6929..1cbaa957f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java @@ -33,7 +33,7 @@ import java.util.List; @Data @EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId", "resources"}) +@JsonPropertyOrder({"id", "createdTime", "tenantId", "fqn", "name", "deprecated", "scada", "version", "descriptor", "image", "description", "tags", "externalId", "resources"}) public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity { @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java b/common/data/src/main/java/org/thingsboard/server/exception/DataValidationException.java similarity index 88% rename from dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java rename to common/data/src/main/java/org/thingsboard/server/exception/DataValidationException.java index 47cbee7f63..8a5d0ad965 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java +++ b/common/data/src/main/java/org/thingsboard/server/exception/DataValidationException.java @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.exception; +package org.thingsboard.server.exception; public class DataValidationException extends RuntimeException { - private static final long serialVersionUID = 7659985660312721830L; - public DataValidationException(String message) { super(message); } @@ -26,4 +24,5 @@ public class DataValidationException extends RuntimeException { public DataValidationException(String message, Throwable cause) { super(message, cause); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java b/common/data/src/main/java/org/thingsboard/server/exception/EntitiesLimitExceededException.java similarity index 77% rename from dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java rename to common/data/src/main/java/org/thingsboard/server/exception/EntitiesLimitExceededException.java index 96e2bd6890..a7ed2f37d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java +++ b/common/data/src/main/java/org/thingsboard/server/exception/EntitiesLimitExceededException.java @@ -13,23 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.exception; +package org.thingsboard.server.exception; import lombok.Getter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; -public class EntitiesLimitException extends DataValidationException { - private static final long serialVersionUID = -9211462514373279196L; +public class EntitiesLimitExceededException extends DataValidationException { @Getter private final TenantId tenantId; @Getter private final EntityType entityType; - public EntitiesLimitException(TenantId tenantId, EntityType entityType) { + @Getter + private final long limit; + + public EntitiesLimitExceededException(TenantId tenantId, EntityType entityType, long limit) { super(entityType.getNormalName() + "s limit reached"); this.tenantId = tenantId; this.entityType = entityType; + this.limit = limit; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/exception/ThingsboardRuntimeException.java b/common/data/src/main/java/org/thingsboard/server/exception/ThingsboardRuntimeException.java new file mode 100644 index 0000000000..3ae5f23e82 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/exception/ThingsboardRuntimeException.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; + +public class ThingsboardRuntimeException extends RuntimeException { + + private ThingsboardErrorCode errorCode; + + public ThingsboardRuntimeException() { + super(); + } + + public ThingsboardRuntimeException(ThingsboardErrorCode errorCode) { + this.errorCode = errorCode; + } + + public ThingsboardRuntimeException(String message, ThingsboardErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + public ThingsboardRuntimeException(String message, Throwable cause, ThingsboardErrorCode errorCode) { + super(message, cause); + this.errorCode = errorCode; + } + + public ThingsboardRuntimeException(Throwable cause, ThingsboardErrorCode errorCode) { + super(cause); + this.errorCode = errorCode; + } + + public ThingsboardErrorCode getErrorCode() { + return errorCode; + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java new file mode 100644 index 0000000000..2fbbd66478 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArgumentTest { + + @Test + void validateShouldReturnFalseIfDynamicSourceConfigurationIsNull() { + var argument = new Argument(); + assertThat(argument.hasDynamicSource()).isFalse(); + } + + @Test + void validateWhenRelationQuerySourceConfigurationIsNotNull() { + var argument = new Argument(); + argument.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); + assertThat(argument.hasDynamicSource()).isTrue(); + assertThat(argument.hasRelationQuerySource()).isTrue(); + assertThat(argument.hasOwnerSource()).isFalse(); + } + + @Test + void validateWhenCurrentOwnerSourceConfigurationIsNotNull() { + var argument = new Argument(); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + assertThat(argument.hasDynamicSource()).isTrue(); + assertThat(argument.hasOwnerSource()).isTrue(); + assertThat(argument.hasRelationQuerySource()).isFalse(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java new file mode 100644 index 0000000000..ebf56824fe --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.AttributeScope; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class CalculatedFieldOutputTest { + + @Test + public void testHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + TimeSeriesOutput output = new TimeSeriesOutput(); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenNameChanged_shouldReturnTrue() { + TimeSeriesOutput output = new TimeSeriesOutput(); + TimeSeriesOutput newOutput = new TimeSeriesOutput(); + newOutput.setName("new"); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenScopeChanged_shouldReturnTrue() { + AttributesOutput output = new AttributesOutput(); + output.setScope(AttributeScope.SHARED_SCOPE); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setScope(AttributeScope.SERVER_SCOPE); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenDecimalsByDefaultChanged_shouldReturnTrue() { + AttributesOutput output = new AttributesOutput(); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setDecimalsByDefault(2); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenStrategyHasContextOnlyChanges_shouldReturnTrue() { + AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class); + given(outputStrategy.hasContextOnlyChanges(any())).willReturn(true); + + AttributesOutput output = new AttributesOutput(); + output.setStrategy(outputStrategy); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenStrategyDoesNotHaveContextOnlyChanges_shouldReturnTrue() { + AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class); + given(outputStrategy.hasContextOnlyChanges(any())).willReturn(false); + + AttributesOutput output = new AttributesOutput(); + output.setStrategy(outputStrategy); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isFalse(); + } + + /* .hasContextOnlyChanges() tests*/ + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + AttributesRuleChainOutputStrategy newStrategy = new AttributesRuleChainOutputStrategy(); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSaveAttributesChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, false); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesRuleChainOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + AttributesRuleChainOutputStrategy strategy = new AttributesRuleChainOutputStrategy(); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true); + TimeSeriesRuleChainOutputStrategy newStrategy = new TimeSeriesRuleChainOutputStrategy(); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSaveLatestChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, false, true, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, false, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesRuleChainOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + TimeSeriesRuleChainOutputStrategy strategy = new TimeSeriesRuleChainOutputStrategy(); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + /* .hasRefreshContextOnlyChanges() tests*/ + + @Test + public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenUpdateAttrOnValueChangedChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, false, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenSendAttrUpdatedNotificationChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(false, true, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenTtlChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(300, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..7430c6eec0 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +@ExtendWith(MockitoExtension.class) +public class PropagationCalculatedFieldConfigurationTest { + + @Test + void typeShouldBePropagation() { + var cfg = new PropagationCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); + } + + @Test + void validateShouldThrowWhenConfigurationDisallowArgumentsWithReferencedEntity() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithRefEntityIdSet = new Argument(); + argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("bda14084-f40e-4acc-9b85-9d1dd209bb64"))); + cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + + @Test + void validateShouldThrowWhenConfigurationDisallowArgumentsWithDynamicReferenceConfiguration() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithDynamicRefEntitySource = new Argument(); + argumentWithDynamicRefEntitySource.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + cfg.setArguments(Map.of("argumentWithDynamicRefEntitySource", argumentWithDynamicRefEntitySource)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + + @Test + void validateShouldThrowWhenConfigurationHasNoArgumentsWithCurrentEntitySource() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithRefEntityIdSet = new Argument(); + argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("3703e895-3f9b-4b75-a715-b68f1ad51944"))); + cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); + cfg.setApplyExpressionToResolvedArguments(true); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one argument must be configured with the 'Current entity' " + + "source entity type for 'Expression result' propagation mode!"); + } + + @Test + void validateShouldThrowWhenUsedReservedPropagationArgumentName() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of(PROPAGATION_CONFIG_ARGUMENT, new Argument())); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); + } + + @Test + void validateShouldThrowWhenUsedReservedCtxArgumentName() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("ctx", new Argument())); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument name 'ctx' is reserved and cannot be used."); + } + + @Test + void validateShouldThrowWhenReferencedEntityKeyIsNotSet() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + Argument argument = new Argument(); + cfg.setArguments(Map.of("someArgumentName", argument)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument: 'someArgumentName' doesn't have reference entity key configured!"); + } + + @Test + void validateShouldThrowWhenReferencedEntityKeyTypeIsTsRolling() { + var cfg = new PropagationCalculatedFieldConfiguration(); + ReferencedEntityKey referencedEntityKey = new ReferencedEntityKey("someKey", ArgumentType.TS_ROLLING, null); + Argument argument = new Argument(); + argument.setRefEntityKey(referencedEntityKey); + cfg.setArguments(Map.of("someArgumentName", argument)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument type: 'Time series rolling' detected for argument: 'someArgumentName'. " + + "Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); + } + + @Test + void validateShouldThrowWhenExpressionIsNotSet() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("someArgumentName", new Argument())); + cfg.setApplyExpressionToResolvedArguments(true); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Expression must be specified for 'Expression result' propagation mode!"); + } + + @Test + void validateToPropagationArgumentMethodCallReturnCorrectArgument() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + + Argument propagationArgument = cfg.toPropagationArgument(); + assertThat(propagationArgument).isNotNull(); + assertThat(propagationArgument.getRefEntityId()).isNull(); + assertThat(propagationArgument.getRefEntityKey()).isNull(); + assertThat(propagationArgument.getDefaultValue()).isNull(); + assertThat(propagationArgument.getTimeWindow()).isNull(); + assertThat(propagationArgument.getLimit()).isNull(); + + assertThat(propagationArgument.getRefDynamicSourceConfiguration()) + .isNotNull() + .isInstanceOf(RelationPathQueryDynamicSourceConfiguration.class); + var refDynamicSourceConfiguration = (RelationPathQueryDynamicSourceConfiguration) propagationArgument.getRefDynamicSourceConfiguration(); + assertThat(refDynamicSourceConfiguration.getLevels()).isNotEmpty().hasSize(1); + + var relationPathLevel = refDynamicSourceConfiguration.getLevels().get(0); + assertThat(relationPathLevel.direction()).isEqualTo(EntitySearchDirection.TO); + assertThat(relationPathLevel.relationType()).isEqualTo(EntityRelation.CONTAINS_TYPE); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java new file mode 100644 index 0000000000..9e89bf7045 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java @@ -0,0 +1,126 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RelationPathQueryDynamicSourceConfigurationTest { + + @Test + void typeShouldBeRelationQuery() { + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_PATH_QUERY); + } + + @ParameterizedTest + @NullAndEmptySource + void validateShouldThrowWhenLevelsIsNull(List levels) { + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one relation level must be specified!"); + } + + @Test + void validateShouldCallValidateForPathLevels() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + + verify(lvl1).validate(); + verify(lvl2).validate(); + } + + @Test + void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + when(lvl2.direction()).thenReturn(EntitySearchDirection.FROM); + + EntityRelation rel1 = mock(EntityRelation.class); + EntityRelation rel2 = mock(EntityRelation.class); + + when(rel1.getTo()).thenReturn(mock(EntityId.class)); + when(rel2.getTo()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); + } + + @Test + void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + when(lvl2.direction()).thenReturn(EntitySearchDirection.TO); + + EntityRelation rel1 = mock(EntityRelation.class); + EntityRelation rel2 = mock(EntityRelation.class); + + when(rel1.getFrom()).thenReturn(mock(EntityId.class)); + when(rel2.getFrom()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..a2df32b483 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +public class ScheduledUpdateSupportedCalculatedFieldConfigurationTest { + + @Test + void validateDoesNotThrowAnyExceptionWhenScheduledUpdateIntervalIsGreaterThanMinAllowedIntervalInTenantProfile() { + int scheduledUpdateInterval = 60; + int minAllowedInterval = scheduledUpdateInterval - 1; + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(scheduledUpdateInterval); + assertThatCode(() -> cfg.validate(minAllowedInterval)).doesNotThrowAnyException(); + } + + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsLessThanMinAllowedIntervalInTenantProfile() { + int minAllowedInterval = (int) TimeUnit.HOURS.toSeconds(2); + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(1); + + assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval (1 seconds) is less than " + + "minimum allowed interval in tenant profile: " + minAllowedInterval + " seconds"); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..f68bef5ef2 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfigurationTest.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class RelatedEntitiesAggregationCalculatedFieldConfigurationTest { + + @Test + void typeShouldBeEntityAggregation() { + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + } + + @Test + void validateShouldThrowWhenRelationIsNotSet() { + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation must be specified!"); + } + + @Test + void validateShouldThrowWhenRelationIsNotValid() { + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + cfg.setRelation(new RelationPathLevel(null, null)); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Direction must be specified!"); + } + + @Test + void validateShouldThrowWhenArgumentsMapIsEmpty() { + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Arguments map cannot be empty."); + } + + @Test + void validateShouldThrowWhenTsRollingArgumentUsed() { + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("key", ArgumentType.TS_ROLLING, null)); + cfg.setArguments(Map.of("k", argument)); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Calculated field with type: '" + CalculatedFieldType.RELATED_ENTITIES_AGGREGATION + "' doesn't support TS_ROLLING arguments."); + } + + @Test + void validateShouldThrowWhenMetricMapIsEmpty() { + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + cfg.setMetrics(Map.of()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metrics map cannot be empty."); + } + + @Test + void validateShouldThrowWhenMetricReferencesUnknownArgument() { + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + + AggMetric metric = new AggMetric(); + metric.setInput(new AggKeyInput("unknown")); + cfg.setMetrics(Map.of("m", metric)); + + cfg.setOutput(new TimeSeriesOutput()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metric references unknown argument: 'unknown'."); + } + + private Argument validArgument(ArgumentType type) { + Argument a = new Argument(); + a.setRefEntityKey(new ReferencedEntityKey("key", type, null)); + return a; + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..8a1509b650 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.HourInterval; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EntityAggregationCalculatedFieldConfigurationTest { + + @Test + void typeShouldBeEntityAggregation() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.ENTITY_AGGREGATION); + } + + @ParameterizedTest + @ValueSource(strings = {"ATTRIBUTE", "TS_ROLLING"}) + void validateShouldThrowWhenNotTsLatestArgumentUsed(String argumentType) { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("k", validArgument(ArgumentType.valueOf(argumentType)))); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Calculated field with type: '" + cfg.getType() + "' support only TS_LATEST arguments."); + } + + @Test + void validateShouldThrowWhenMetricMapIsEmpty() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + cfg.setMetrics(Map.of()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metrics map cannot be empty."); + } + + @Test + void validateShouldThrowWhenMetricInputIsNotAggKeyInput() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + + AggMetric metric = new AggMetric(); + metric.setInput(new AggFunctionInput()); // cannot be function + cfg.setMetrics(Map.of("m", metric)); + + cfg.setInterval(new HourInterval("Europe/Kiev", null)); + cfg.setOutput(new TimeSeriesOutput()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metric key can only refer to argument."); + } + + @Test + void validateShouldThrowWhenMetricReferencesUnknownArgument() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + + AggMetric metric = new AggMetric(); + metric.setInput(new AggKeyInput("unknown")); + cfg.setMetrics(Map.of("m", metric)); + + cfg.setInterval(new HourInterval("Europe/Kiev", null)); + cfg.setOutput(new TimeSeriesOutput()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metric references unknown argument: 'unknown'."); + } + + @Test + void validateShouldThrowWhenIntervalIsNull() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + cfg.setMetrics(Map.of("m", validMetric())); + cfg.setInterval(null); + cfg.setOutput(new TimeSeriesOutput()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Interval must be defined."); + } + + private Argument validArgument(ArgumentType type) { + Argument a = new Argument(); + a.setRefEntityKey(new ReferencedEntityKey("key", type, null)); + return a; + } + + private AggMetric validMetric() { + AggMetric metric = new AggMetric(); + metric.setInput(new AggKeyInput("k")); + return metric; + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java new file mode 100644 index 0000000000..fd1a70d02f --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java @@ -0,0 +1,168 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.aggregation.single.interval; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.LongFunction; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AggIntervalTest { + + private static final String TZ = "Europe/Kiev"; + + @Test + void validateShouldThrowWhenInvalidTimZone() { + AggInterval interval = new HourInterval("TimeZone", null); + + assertThatThrownBy(interval::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid timezone in interval: "); + } + + @Test + void validateShouldThrowWhenOffsetIsNegative() { + AggInterval interval = new CustomInterval(TZ, -100L, TimeUnit.HOURS.toSeconds(2)); + + assertThatThrownBy(interval::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Offset cannot be negative."); + } + + @Test + void validateShouldThrowWhenOffsetGreaterThanIntervalDuration() { + AggInterval interval = new CustomInterval(TZ, TimeUnit.HOURS.toSeconds(2), TimeUnit.HOURS.toSeconds(2)); + + assertThatThrownBy(interval::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Offset must be greater than interval duration."); + } + + @ParameterizedTest + @MethodSource("intervals") + void testGetStartAndEndWithoutOffset(LongFunction intervalCreator, long expectedDuration) { + AggInterval interval = intervalCreator.apply(0L); + + ZonedDateTime dateTime = ZonedDateTime.of( + // 2025.11.11 00:00:00 + 2025, 11, 11, 0, 0, 0, 0, ZoneId.of(TZ) + ); + long startTs = interval.getDateTimeIntervalStartTs(dateTime); + long endTs = interval.getDateTimeIntervalEndTs(dateTime); + + assertThat(endTs).isGreaterThan(startTs); + assertThat(endTs - startTs).isEqualTo(expectedDuration); + } + + @ParameterizedTest + @MethodSource("intervals") + void testApplyOffset(LongFunction intervalCreator) { + long offsetSec = TimeUnit.MINUTES.toSeconds(15); + AggInterval intervalWithOffset = intervalCreator.apply(offsetSec); + AggInterval intervalNoOffset = intervalCreator.apply(0L); + + ZonedDateTime dateTime = ZonedDateTime.of( + // 2025.11.11 11:20:00 - chosen so 15m offset shifts into a new interval + 2025, 6, 6, 6, 20, 0, 0, ZoneId.of(TZ) + ); + + long startWithOffsetTs = intervalWithOffset.getDateTimeIntervalStartTs(dateTime); + long startNoOffsetTs = intervalNoOffset.getDateTimeIntervalStartTs(dateTime); + + ZonedDateTime startWithOffset = Instant.ofEpochMilli(startWithOffsetTs).atZone(intervalWithOffset.getZoneId()); + ZonedDateTime startNoOffset = Instant.ofEpochMilli(startNoOffsetTs).atZone(intervalNoOffset.getZoneId()); + + long actualOffset = Duration.between(startNoOffset, startWithOffset).toSeconds(); + assertThat(actualOffset).isEqualTo(offsetSec); + } + + private static Stream intervals() { + return Stream.of( + Arguments.of((LongFunction) offset -> new HourInterval(TZ, offset), TimeUnit.HOURS.toMillis(1)), + Arguments.of((LongFunction) offset -> new DayInterval(TZ, offset), TimeUnit.DAYS.toMillis(1)), + Arguments.of((LongFunction) offset -> new WeekInterval(TZ, offset), TimeUnit.DAYS.toMillis(7)), + Arguments.of((LongFunction) offset -> new WeekSunSatInterval(TZ, offset), TimeUnit.DAYS.toMillis(7)), + Arguments.of((LongFunction) offset -> new MonthInterval(TZ, offset), TimeUnit.DAYS.toMillis(30)), + Arguments.of((LongFunction) offset -> new QuarterInterval(TZ, offset), TimeUnit.DAYS.toMillis(92) + TimeUnit.HOURS.toMillis(1)),// Includes DST fallback (2025-10-26), so duration = 92 days + 1 hour(expected for Europe/Kyiv timezone). + Arguments.of((LongFunction) offset -> new YearInterval(TZ, offset), TimeUnit.DAYS.toMillis(365)), + Arguments.of((LongFunction) offset -> new CustomInterval(TZ, offset, TimeUnit.HOURS.toSeconds(4)), TimeUnit.HOURS.toMillis(4)) + ); + } + + @ParameterizedTest + @MethodSource("nextIntervalFromExactDate") + void testNextIntervalFromExactDate(LongFunction intervalCreator, Function expectedDateTimeFunction) { + AggInterval interval = intervalCreator.apply(0L); + + ZonedDateTime currentStart = ZonedDateTime.of( + 2025, 11, 11, 0, 0, 0, 0, ZoneId.of(TZ) + ); + + ZonedDateTime nextStart = interval.getNextIntervalStart(currentStart); + + assertThat(nextStart).isEqualTo(expectedDateTimeFunction.apply(currentStart)); + } + + private static Stream nextIntervalFromExactDate() { + return Stream.of( + Arguments.of( + (LongFunction) offset -> new HourInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusHours(1) + ), + Arguments.of( + (LongFunction) offset -> new DayInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusDays(1) + ), + Arguments.of( + (LongFunction) offset -> new WeekInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusWeeks(1) + ), + Arguments.of( + (LongFunction) offset -> new WeekSunSatInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusWeeks(1) + ), + Arguments.of( + (LongFunction) offset -> new MonthInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusMonths(1) + ), + Arguments.of( + (LongFunction) offset -> new QuarterInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusMonths(3) + ), + Arguments.of( + (LongFunction) offset -> new YearInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusYears(1) + ), + Arguments.of( + (LongFunction) offset -> new CustomInterval(TZ, offset, TimeUnit.HOURS.toSeconds(4)), + (Function) currentInterval -> currentInterval.plusHours(4) + ) + ); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java new file mode 100644 index 0000000000..5f4655dd46 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; + +public class EntityCoordinatesTest { + + @Test + void validateToArgumentsMethodCallWithoutRefEntityId() { + var entityCoordinates = new EntityCoordinates("xPos", "yPos"); + + var arguments = entityCoordinates.toArguments(); + assertThat(arguments).isNotNull().hasSize(2); + + Argument latitudeArgument = arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY); + assertThat(latitudeArgument).isNotNull(); + assertThat(latitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("xPos", ArgumentType.TS_LATEST, null)); + assertThat(latitudeArgument.getRefEntityId()).isNull(); + assertThat(latitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + + Argument longitudeArgument = arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + assertThat(longitudeArgument).isNotNull(); + assertThat(longitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("yPos", ArgumentType.TS_LATEST, null)); + assertThat(longitudeArgument.getRefEntityId()).isNull(); + assertThat(longitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..2c68bcd8c5 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -0,0 +1,120 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; + +@ExtendWith(MockitoExtension.class) +public class GeofencingCalculatedFieldConfigurationTest { + + @Test + void typeShouldBeGeofencing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.GEOFENCING); + } + + @Test + void validateShouldCallValidateOnZoneGroups() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); + cfg.setEntityCoordinates(entityCoordinatesMock); + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfiguration)); + + cfg.validate(); + verify(zoneGroupConfiguration).validate("someGroupName"); + } + + @Test + void validateShouldCallValidateOnZoneGroupsWithoutAnyExceptions() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); + cfg.setEntityCoordinates(entityCoordinatesMock); + var zoneGroupConfigurationA = mock(ZoneGroupConfiguration.class); + var zoneGroupConfigurationB = mock(ZoneGroupConfiguration.class); + + String zoneGroupAName = "zoneGroupA"; + String zoneGroupBName = "zoneGroupB"; + + cfg.setZoneGroups(Map.of("zoneGroupA", zoneGroupConfigurationA, "zoneGroupB", zoneGroupConfigurationB)); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + + verify(zoneGroupConfigurationA).validate(zoneGroupAName); + verify(zoneGroupConfigurationB).validate(zoneGroupBName); + } + + @Test + void testGetArgumentsOverride() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); + cfg.setZoneGroups(Map.of("allowedZones", new ZoneGroupConfiguration("perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false))); + + Map arguments = cfg.getArguments(); + + assertThat(arguments).isNotNull().hasSize(3); + assertThat(arguments).containsKeys(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, "allowedZones"); + + Argument latitudeArgument = arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY); + assertThat(latitudeArgument).isNotNull(); + assertThat(latitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(latitudeArgument.getRefEntityId()).isNull(); + assertThat(latitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ArgumentType.TS_LATEST, null)); + + Argument longitudeArgument = arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + assertThat(longitudeArgument).isNotNull(); + assertThat(longitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(longitudeArgument.getRefEntityId()).isNull(); + assertThat(longitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ArgumentType.TS_LATEST, null)); + + Argument allowedZonesArgument = arguments.get("allowedZones"); + assertThat(allowedZonesArgument).isNotNull(); + assertThat(allowedZonesArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(allowedZonesArgument.getRefEntityId()).isNull(); + assertThat(allowedZonesArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + } + + @Test + void validateShouldThrowWhenScheduledUpdateEnabledButIntervalNotSet() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(mock(EntityCoordinates.class)); + cfg.setZoneGroups(Map.of("zone", mock(ZoneGroupConfiguration.class))); + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(null); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Refresh interval is required when periodic zone group refresh is enabled."); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java new file mode 100644 index 0000000000..158eb9762a --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -0,0 +1,112 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf.configuration.geofencing; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; + +public class ZoneGroupConfigurationTest { + + @ParameterizedTest + @ValueSource(strings = {EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY, EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY}) + void validateShouldThrowWhenUsedReservedEntityCoordinateNames(String name) { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + assertThatThrownBy(() -> zoneGroupConfiguration.validate(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Name '" + name + "' is reserved and cannot be used for zone group!"); + } + + @ParameterizedTest + @ValueSource(strings = " ") + @NullAndEmptySource + void validateShouldThrowWhenRelationCreationEnabledAndRelationTypeIsNullEmptyOrBlank(String relationType) { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true); + zoneGroupConfiguration.setRelationType(relationType); + assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation type must be specified for 'allowedZonesGroup' zone group!"); + } + + @Test + void validateShouldThrowWhenRelationCreationEnabledAndDirectionIsNull() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true); + zoneGroupConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setDirection(null); + assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation direction must be specified for 'allowedZonesGroup' zone group!"); + } + + @Test + void validateShouldDoesNotThrowAnyExceptionWhenRelationCreationDisabledAndConfigValid() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + assertThatCode(() -> zoneGroupConfiguration.validate("allowedZonesGroup")).doesNotThrowAnyException(); + } + + @Test + void validateShouldDoesNotThrowAnyExceptionWhenRelationCreationEnabledAndConfigValid() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true); + zoneGroupConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setDirection(EntitySearchDirection.TO); + assertThatCode(() -> zoneGroupConfiguration.validate("allowedZonesGroup")).doesNotThrowAnyException(); + } + + @Test + void whenHasRelationQuerySourceCalled_shouldReturnTrueIfRelationQuerySourceConfigurationIsNotNull() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isTrue(); + } + + @Test + void whenHasRelationQuerySourceCalled_shouldReturnFalseIfRelationQuerySourceConfigurationIsNull() { + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + assertThat(zoneGroupConfiguration.getRefDynamicSourceConfiguration()).isNull(); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isFalse(); + } + + @Test + void whenHasRelationQuerySourceCalled_shouldReturnFalseIfCurrentOwnerSourceConfigured() { + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isFalse(); + } + + @Test + void validateToArgumentsMethodCallWithoutRefEntityId() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + Argument zoneGroupArgument = zoneGroupConfiguration.toArgument(); + assertThat(zoneGroupArgument).isNotNull(); + assertThat(zoneGroupArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java index d645d40dcc..9d6434a7c2 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java @@ -15,8 +15,18 @@ */ package org.thingsboard.server.common.data.id; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; public class EntityIdTest { @@ -25,4 +35,44 @@ public class EntityIdTest { Assertions.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString()); } + @Test + public void allEntityIdImplementors_shouldBeInDiscriminatorMapping() { + Schema schemaAnnotation = EntityId.class.getAnnotation(Schema.class); + assertThat(schemaAnnotation).as("EntityId must have @Schema annotation").isNotNull(); + + DiscriminatorMapping[] mappings = schemaAnnotation.discriminatorMapping(); + Map> discriminatorMap = Arrays.stream(mappings) + .collect(Collectors.toMap(DiscriminatorMapping::value, DiscriminatorMapping::schema)); + + UUID testUuid = UUID.randomUUID(); + for (EntityType entityType : EntityType.values()) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid); + String typeName = entityType.name(); + + assertThat(discriminatorMap) + .as("EntityId @Schema discriminatorMapping is missing entry for EntityType." + typeName) + .containsKey(typeName); + assertThat(discriminatorMap.get(typeName)) + .as("Discriminator mapping for " + typeName + " should point to " + entityId.getClass().getSimpleName()) + .isEqualTo(entityId.getClass()); + } + } + + @Test + public void allEntityIdImplementors_shouldHaveAllOfEntityId() { + UUID testUuid = UUID.randomUUID(); + for (EntityType entityType : EntityType.values()) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid); + Class idClass = entityId.getClass(); + Schema schemaAnnotation = idClass.getAnnotation(Schema.class); + + assertThat(schemaAnnotation) + .as(idClass.getSimpleName() + " must have @Schema annotation") + .isNotNull(); + assertThat(schemaAnnotation.allOf()) + .as(idClass.getSimpleName() + " @Schema must include allOf = EntityId.class") + .contains(EntityId.class); + } + } + } \ No newline at end of file diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java index 95368e3240..fbcb92e4dd 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; @@ -39,7 +38,6 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL; class TbMsgTypeTest { private static final List typesWithNullRuleNodeConnection = List.of( - ALARM, ALARM_DELETE, ENTITY_ASSIGNED_TO_EDGE, ENTITY_UNASSIGNED_FROM_EDGE, diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java new file mode 100644 index 0000000000..dd932fa2c5 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.sync.ie; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EntityExportDataTest { + + @Test + public void newInstance_shouldSupportAllJsonSubTypes() { + JsonSubTypes subTypes = EntityExportData.class.getAnnotation(JsonSubTypes.class); + assertThat(subTypes).as("EntityExportData must have @JsonSubTypes annotation").isNotNull(); + + Set jsonSubTypeNames = Arrays.stream(subTypes.value()) + .map(JsonSubTypes.Type::name) + .collect(Collectors.toSet()); + + for (String typeName : jsonSubTypeNames) { + EntityType entityType = EntityType.valueOf(typeName); + EntityExportData instance = EntityExportData.newInstance(entityType); + + assertThat(instance) + .as("newInstance(%s) should not return null", typeName) + .isNotNull(); + assertThat(instance.getEntityType()) + .as("newInstance(%s).getEntityType() should return %s", typeName, entityType) + .isEqualTo(entityType); + } + } + +} diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 0d75e57b9a..c7658bd738 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index bb0d5cb192..379e4fb8fb 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java index 267fd2b3f2..a1a7b5462f 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -41,4 +41,5 @@ public interface EdgeRpcClient { void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg); int getServerMaxInboundMessageSize(); + } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 70f90cc45b..bcea3c30d9 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -44,10 +44,16 @@ enum EdgeVersion { V_4_0_0 = 10; V_4_1_0 = 11; V_4_2_0 = 12; + V_4_3_0 = 13; V_4_2_1_2 = 14; V_4_2_2 = 4220; V_4_2_2_1 = 4221; V_4_2_2_2 = 4222; + V_4_3_0_1 = 15; + V_4_3_1 = 4310; + V_4_3_1_1 = 4311; + V_4_3_1_2 = 4312; + V_4_4_0 = 4400; V_LATEST = 99999; } @@ -137,6 +143,19 @@ message CalculatedFieldUpdateMsg{ string entity = 4; } +message AiModelUpdateMsg{ + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string entity = 4; +} + +message ApiKeyUpdateMsg{ + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string entity = 4; +} message EntityDataProto { int64 entityIdMSB = 1; @@ -445,6 +464,10 @@ message UplinkMsg { repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; + repeated AiModelUpdateMsg aiModelUpdateMsg = 27; + repeated UserUpdateMsg userUpdateMsg = 28; + repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 29; + repeated ApiKeyUpdateMsg apiKeyUpdateMsg = 30; } message UplinkResponseMsg { @@ -495,4 +518,6 @@ message DownlinkMsg { repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35; + repeated AiModelUpdateMsg aiModelUpdateMsg = 36; + repeated ApiKeyUpdateMsg apiKeyUpdateMsg = 37; } diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 4b2cd36592..549ee1515f 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java index 7ebf93fae5..1385e616a0 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java @@ -20,6 +20,7 @@ import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.permission.QueryContext; @@ -139,11 +140,32 @@ public abstract class BaseEntityData implements EntityDa case "name" -> getEntityName(); case "ownerName" -> getOwnerName(); case "ownerType" -> getOwnerType(); + case "displayName" -> getDisplayName(); case "entityType" -> Optional.ofNullable(getEntityType()).map(EntityType::name).orElse(""); default -> fields.getAsString(name); }; } + public String getDisplayName(){ + return switch (getEntityType()) { + case DEVICE, ASSET -> StringUtils.isNotBlank(fields.getLabel()) ? fields.getLabel() : fields.getName(); + case USER -> { + boolean firstNameSet = StringUtils.isNotBlank(fields.getFirstName()); + boolean lastNameSet = StringUtils.isNotBlank(fields.getLastName()); + if(firstNameSet && lastNameSet) { + yield fields.getFirstName() + " " + fields.getLastName(); + } else if(firstNameSet) { + yield fields.getFirstName(); + } else if (lastNameSet) { + yield fields.getLastName(); + } else { + yield fields.getEmail(); + } + } + default -> fields.getName(); + }; + } + public String getEntityName() { return getFields().getName(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java index 90143a0465..ab183bc3bd 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.data; import lombok.ToString; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.edqs.DataPoint; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java index 8ae3499d63..2d9e77633d 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java @@ -33,6 +33,11 @@ public class StringDataPoint extends AbstractDataPoint { this.value = deduplicate ? TbStringPool.intern(value) : value; } + @Override + public boolean getBool() { + return Boolean.parseBoolean(value); + } + @Override public double getDouble() { return Double.parseDouble(value); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java index 0d2ed67987..d338362aeb 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.query; import lombok.Builder; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityFilter; import java.util.List; @@ -23,8 +24,8 @@ import java.util.List; public class EdqsCountQuery extends EdqsQuery { @Builder - EdqsCountQuery(EntityFilter entityFilter, boolean hasKeyFilters, List keyFilters) { - super(entityFilter, hasKeyFilters, keyFilters); + EdqsCountQuery(EntityFilter entityFilter, boolean hasKeyFilters, List keyFilters, ComplexOperation keyFiltersOperation) { + super(entityFilter, hasKeyFilters, keyFilters, keyFiltersOperation); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java index 72ccb68515..bd93eca2f1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java @@ -19,6 +19,7 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.EntityKeyType; @@ -41,10 +42,10 @@ public class EdqsDataQuery extends EdqsQuery { private final List latestValues; @Builder - public EdqsDataQuery(EntityFilter entityFilter, List keyFilters, + public EdqsDataQuery(EntityFilter entityFilter, List keyFilters, ComplexOperation keyFiltersOperation, int pageSize, int page, String textSearch, DataKey sortKey, EntityDataSortOrder.Direction sortDirection, List entityFields, List latestValues) { - super(entityFilter, CollectionsUtil.isNotEmpty(keyFilters), keyFilters); + super(entityFilter, CollectionsUtil.isNotEmpty(keyFilters), keyFilters, keyFiltersOperation); this.pageSize = pageSize; this.page = page; this.hasTextSearch = StringUtils.isNotBlank(textSearch); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java index 9cfc5d442c..8d29ec89b8 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.query; import lombok.Data; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityFilter; import java.util.List; @@ -26,5 +27,6 @@ public abstract class EdqsQuery { private final EntityFilter entityFilter; private final boolean hasKeyFilters; private final List keyFilters; + private final ComplexOperation keyFiltersOperation; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index a0a030ba08..6da25c260a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -60,8 +61,8 @@ import java.util.stream.Stream; import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; import static org.thingsboard.server.common.data.StringUtils.equalsAny; import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; -import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation.AND; -import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation.OR; +import static org.thingsboard.server.common.data.query.ComplexOperation.AND; +import static org.thingsboard.server.common.data.query.ComplexOperation.OR; @Slf4j public class RepositoryUtils { @@ -139,6 +140,7 @@ public class RepositoryUtils { } query.entityFilter(oldQuery.getEntityFilter()); query.keyFilters(toKeyFilters(oldQuery.getKeyFilters())); + query.keyFiltersOperation(oldQuery.getKeyFiltersOperationOrDefault()); query.entityFields(toNewKeys(oldQuery.getEntityFields())); query.latestValues(toNewKeys(oldQuery.getLatestValues())); return query.build(); @@ -149,6 +151,7 @@ public class RepositoryUtils { .entityFilter(oldQuery.getEntityFilter()) .hasKeyFilters(CollectionsUtil.isNotEmpty(oldQuery.getKeyFilters())) .keyFilters(toKeyFilters(oldQuery.getKeyFilters())) + .keyFiltersOperation(oldQuery.getKeyFiltersOperationOrDefault()) .build(); } @@ -196,38 +199,57 @@ public class RepositoryUtils { } public static boolean checkKeyFilters(EntityData entity, List keyFilters) { - for (EdqsFilter keyFilter : keyFilters) { - EntityKeyValueType valueType = keyFilter.valueType(); - if (valueType == null) { - valueType = switch (keyFilter.predicate().getType()) { - case STRING -> EntityKeyValueType.STRING; - case NUMERIC -> EntityKeyValueType.NUMERIC; - case BOOLEAN -> EntityKeyValueType.BOOLEAN; - default -> throw new IllegalStateException(); - }; - } - DataKey dataKey = keyFilter.key(); - DataPoint dp = entity.getDataPoint(dataKey, null); - boolean checkResult = switch (valueType) { - case STRING -> { - String str = dp != null ? dp.valueToString() : null; - yield (dataKey.type() == EntityKeyType.ENTITY_FIELD) ? (str == null || checkKeyFilter(str, keyFilter.predicate())) : - (str != null && checkKeyFilter(str, keyFilter.predicate())); - } - case BOOLEAN -> { - Boolean booleanValue = dp != null ? dp.getBool() : null; - yield booleanValue != null && checkKeyFilter(booleanValue, keyFilter.predicate()); + return checkKeyFilters(entity, keyFilters, ComplexOperation.AND); + } + + public static boolean checkKeyFilters(EntityData entity, List keyFilters, ComplexOperation operation) { + ComplexOperation op = operation != null ? operation : ComplexOperation.AND; + if (op == ComplexOperation.OR) { + for (EdqsFilter keyFilter : keyFilters) { + if (evaluateSingleFilter(entity, keyFilter)) { + return true; } - case DATE_TIME, NUMERIC -> { - Double doubleValue = dp != null ? dp.getDouble() : null; - yield doubleValue != null && checkKeyFilter(doubleValue, keyFilter.predicate()); + } + // Vacuously true when OR is called with an empty filter list. Unreachable via checkFilters + // (which guards on isHasKeyFilters()), but kept defensive for any future direct caller. + return keyFilters.isEmpty(); + } else { + for (EdqsFilter keyFilter : keyFilters) { + if (!evaluateSingleFilter(entity, keyFilter)) { + return false; } - }; - if (!checkResult) { - return false; } + return true; } - return true; + } + + private static boolean evaluateSingleFilter(EntityData entity, EdqsFilter keyFilter) { + EntityKeyValueType valueType = keyFilter.valueType(); + if (valueType == null) { + valueType = switch (keyFilter.predicate().getType()) { + case STRING -> EntityKeyValueType.STRING; + case NUMERIC -> EntityKeyValueType.NUMERIC; + case BOOLEAN -> EntityKeyValueType.BOOLEAN; + default -> throw new IllegalStateException(); + }; + } + DataKey dataKey = keyFilter.key(); + DataPoint dp = entity.getDataPoint(dataKey, null); + return switch (valueType) { + case STRING -> { + String str = dp != null ? dp.valueToString() : null; + yield (dataKey.type() == EntityKeyType.ENTITY_FIELD) ? (str == null || checkKeyFilter(str, keyFilter.predicate())) : + (str != null && checkKeyFilter(str, keyFilter.predicate())); + } + case BOOLEAN -> { + Boolean booleanValue = dp != null ? dp.getBool() : null; + yield booleanValue != null && checkKeyFilter(booleanValue, keyFilter.predicate()); + } + case DATE_TIME, NUMERIC -> { + Double doubleValue = dp != null ? dp.getDouble() : null; + yield doubleValue != null && checkKeyFilter(doubleValue, keyFilter.predicate()); + } + }; } public static boolean checkKeyFilter(String value, KeyFilterPredicate keyFilterPredicate) { @@ -376,7 +398,7 @@ public class RepositoryUtils { if (entity == null || entity.getFields() == null) { return false; // Entity was already removed or not arrived yet; } - if (query.isHasKeyFilters() && !checkKeyFilters(entity, query.getKeyFilters())) { + if (query.isHasKeyFilters() && !checkKeyFilters(entity, query.getKeyFilters(), query.getKeyFiltersOperation())) { return false; } if (query instanceof EdqsDataQuery dataQuery) { diff --git a/common/message/pom.xml b/common/message/pom.xml index 69c4e72e2a..478368afdc 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java new file mode 100644 index 0000000000..948c68108c --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; + +@Data +public class CalculatedFieldStatePartitionRestoreMsg implements ToCalculatedFieldSystemMsg { + + private final TopicPartitionInfo partition; + + @Override + public TenantId getTenantId() { + return TenantId.SYS_TENANT_ID; + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_STATE_PARTITION_RESTORE_MSG; + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 591662b136..7e7aee226f 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -138,16 +138,24 @@ public enum MsgType { CF_CACHE_INIT_MSG, // Sent to init caches for CF actor; CF_STATE_RESTORE_MSG, // Sent to restore particular calculated field entity state; + CF_STATE_PARTITION_RESTORE_MSG, CF_PARTITIONS_CHANGE_MSG, // Sent when cluster event occures; CF_ENTITY_LIFECYCLE_MSG, // Sent on CF/Device/Asset create/update/delete; + CF_ENTITY_ACTION_EVENT_MSG, + CF_ALARM_ACTION_MSG, CF_TELEMETRY_MSG, // Sent from queue to actor system; CF_LINKED_TELEMETRY_MSG, // Sent from queue to actor system; /* CF Manager Actor -> CF Entity actor */ CF_ENTITY_TELEMETRY_MSG, CF_ENTITY_INIT_CF_MSG, - CF_ENTITY_DELETE_MSG; + CF_ENTITY_DELETE_MSG, + + CF_RELATION_ACTION_MSG, + + CF_ARGUMENT_RESET_MSG, // Sent to reset argument; + CF_REEVALUATE_MSG; @Getter private final boolean ignoreOnStart; 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 79059a9a56..abfbdcbe30 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 @@ -41,9 +41,6 @@ import java.util.Objects; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; -/** - * Created by ashvayka on 13.01.18. - */ @Data @Slf4j public final class TbMsg implements Serializable { @@ -491,11 +488,11 @@ public final class TbMsg implements Serializable { public String toString() { return "TbMsg.TbMsgBuilder(queueName=" + this.queueName + ", id=" + this.id + ", ts=" + this.ts + - ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + - ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + - ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + - ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + - ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; + ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + + ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + + ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + + ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + + ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java index 4f73f0c480..ab22653f1e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java @@ -16,12 +16,7 @@ package org.thingsboard.server.common.msg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; public interface ToCalculatedFieldSystemMsg extends TenantAwareMsg { - default TbCallback getCallback() { - return TbCallback.EMPTY; - } - } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java index 5728940087..4bccc3fbbd 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java @@ -17,9 +17,14 @@ package org.thingsboard.server.common.msg.aware; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; public interface TenantAwareMsg extends TbActorMsg { TenantId getTenantId(); - + + default TbCallback getCallback() { + return TbCallback.EMPTY; + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index b4444543bf..cccd39a9d8 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java @@ -30,9 +30,6 @@ import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; import java.io.Serial; import java.util.Optional; -/** - * @author Andrew Shvayka - */ @Data public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { @@ -46,14 +43,15 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { private final String name; private final EntityId oldProfileId; private final EntityId profileId; + private final boolean ownerChanged; private final JsonNode info; public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) { - this(tenantId, entityId, event, null, null, null, null, null); + this(tenantId, entityId, event, null, null, null, null, false, null); } @Builder - private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId, JsonNode info) { + private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId, boolean ownerChanged, JsonNode info) { this.tenantId = tenantId; this.entityId = entityId; this.event = event; @@ -61,6 +59,7 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { this.name = name; this.oldProfileId = oldProfileId; this.profileId = profileId; + this.ownerChanged = ownerChanged; this.info = info; } diff --git a/common/pom.xml b/common/pom.xml index ab2388eb9d..d241646816 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 023b2bc46a..e24705c25a 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 4e0604239f..784bd97c79 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -129,6 +129,7 @@ public class ProtoUtils { builder.setOldProfileIdMSB(msg.getOldProfileId().getId().getMostSignificantBits()); builder.setOldProfileIdLSB(msg.getOldProfileId().getId().getLeastSignificantBits()); } + builder.setOwnerChanged(msg.isOwnerChanged()); if (msg.getName() != null) { builder.setName(msg.getName()); } @@ -165,6 +166,7 @@ public class ProtoUtils { var profileType = EntityType.DEVICE.equals(entityId.getEntityType()) ? EntityType.DEVICE_PROFILE : EntityType.ASSET_PROFILE; builder.oldProfileId(EntityIdFactory.getByTypeAndUuid(profileType, new UUID(proto.getOldProfileIdMSB(), proto.getOldProfileIdLSB()))); } + builder.ownerChanged(proto.getOwnerChanged()); if (proto.hasInfo()) { builder.info(JacksonUtil.toJsonNode(proto.getInfo())); } @@ -1377,6 +1379,18 @@ public class ProtoUtils { return TbMsg.fromProto(queueName, getTbMsgProto(ruleEngineMsg), callback); } + public static TransportProtos.EntityIdProto toProto(EntityId entityId) { + return TransportProtos.EntityIdProto.newBuilder() + .setEntityIdMSB(getMsb(entityId)) + .setEntityIdLSB(getLsb(entityId)) + .setType(toProto(entityId.getEntityType())) + .build(); + } + + public static EntityId fromProto(TransportProtos.EntityIdProto entityIdProto) { + return EntityIdFactory.getByTypeAndUuid(fromProto(entityIdProto.getType()), new UUID(entityIdProto.getEntityIdMSB(), entityIdProto.getEntityIdLSB())); + } + private static boolean isNotNull(Object obj) { return obj != null; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 7ad4d2883d..68f7052c69 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -62,10 +62,11 @@ enum EntityTypeProto { MOBILE_APP = 37; MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; - CALCULATED_FIELD_LINK = 40; + // CALCULATED_FIELD_LINK = 40; - was removed in 4.3 JOB = 41; ADMIN_SETTINGS = 42; AI_MODEL = 43; + API_KEY = 44; } enum ApiUsageRecordKeyProto { @@ -82,6 +83,12 @@ enum ApiUsageRecordKeyProto { INACTIVE_DEVICES = 10; } +message EntityIdProto { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + EntityTypeProto type = 4; +} + /** * Service Discovery Data Structures; */ @@ -881,6 +888,7 @@ message SingleValueArgumentProto { string argName = 1; TsValueProto value = 2; int64 version = 3; + EntityIdProto entityId = 4; } message TsDoubleValProto { @@ -895,11 +903,38 @@ message TsRollingArgumentProto { repeated TsDoubleValProto tsValue = 4; } +message GeofencingZoneProto { + EntityIdProto zoneId = 1; + int64 ts = 2; + string perimeterDefinition = 3; + int64 version = 4; + optional bool inside = 5; +} + +message GeofencingArgumentProto { + string argName = 1; + repeated GeofencingZoneProto zones = 2; +} + +message ArgumentIntervalProto { + string argName = 1; + int64 startTs = 2; + int64 endTs = 3; + int64 lastArgsRefreshTs = 4; + int64 lastMetricsEvalTs = 5; +} + message CalculatedFieldStateProto { CalculatedFieldEntityCtxIdProto id = 1; string type = 2; repeated SingleValueArgumentProto singleValueArguments = 3; repeated TsRollingArgumentProto rollingValueArguments = 4; + repeated GeofencingArgumentProto geofencingArguments = 5; + AlarmStateProto alarmState = 6; + int64 lastArgsUpdateTs = 7; + int64 lastMetricsEvalTs = 8; + repeated ArgumentIntervalProto aggregationArguments = 9; + repeated EntityIdProto propagationEntityIds = 10; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. @@ -1252,6 +1287,8 @@ enum ComponentLifecycleEvent { DELETED = 6; FAILED = 7; DEACTIVATED = 8; + RELATION_UPDATED = 9; + RELATION_DELETED = 10; } message ComponentLifecycleMsgProto { @@ -1269,6 +1306,7 @@ message ComponentLifecycleMsgProto { int64 profileIdMSB = 11; int64 profileIdLSB = 12; optional string info = 13; + bool ownerChanged = 100; } message EdgeEventMsgProto { @@ -1700,6 +1738,7 @@ message ToEdgeEventNotificationMsg { message ToCalculatedFieldMsg { CalculatedFieldTelemetryMsgProto telemetryMsg = 1; CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; + EntityActionEventProto eventMsg = 3; } message ToCalculatedFieldNotificationMsg { @@ -1873,3 +1912,22 @@ message JobStatsMsg { message TaskResultProto { string value = 1; } + +message EntityActionEventProto { + EntityIdProto tenantId = 1; + EntityIdProto entityId = 2; + string entity = 3; + string action = 4; +} + +message AlarmStateProto { + repeated AlarmRuleStateProto createRuleStates = 1; + AlarmRuleStateProto clearRuleState = 2; +} + +message AlarmRuleStateProto { + string severity = 1; + int64 eventCount = 2; + int64 firstEventTs = 3; + int64 lastCheckTs = 4; +} diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index e779632088..78e30aa480 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.common.util.JacksonUtil; @@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKey; @@ -344,4 +346,24 @@ class ProtoUtilsTest { assertThat(toDeviceRpcRequestActorMsg.getMsg()).isEqualTo(request); } + @ParameterizedTest + @EnumSource(EntityType.class) + void testEntityIdProto_toProto_fromProto(EntityType entityType) { + UUID uuid = UUID.fromString("51a514d7-ea8f-496d-b567-f6e76f0f9b83"); + + EntityId original = EntityIdFactory.getByTypeAndUuid(entityType, uuid); + assertThat(original).isNotNull(); + + // toProto + TransportProtos.EntityIdProto proto = ProtoUtils.toProto(original); + assertThat(proto).isNotNull(); + assertThat(proto.getType().getNumber()).isEqualTo(entityType.getProtoNumber()); + assertThat(proto.getEntityIdMSB()).isEqualTo(uuid.getMostSignificantBits()); + assertThat(proto.getEntityIdLSB()).isEqualTo(uuid.getLeastSignificantBits()); + + // fromProto + EntityId restored = ProtoUtils.fromProto(proto); + assertThat(restored).isNotNull().isEqualTo(original); + } + } diff --git a/common/queue/pom.xml b/common/queue/pom.xml index a7d4d5b568..1cb159b3af 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 07726f0254..87f566c90f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -194,6 +194,11 @@ public abstract class AbstractTbQueueConsumerTemplate i abstract protected void doUnsubscribe(); + @Override + public Set getPartitions() { + return partitions; + } + @Override public List getFullTopicNames() { if (partitions == null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 1a5cae1109..62bd6460a9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -25,6 +25,7 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdateConfigTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdatePartitionsTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import java.util.Collection; @@ -218,7 +219,7 @@ public class MainQueueConsumerManager consumer) { + private void consumerLoop(ConsumerKey consumerKey, TbQueueConsumer consumer) { try { while (!stopped && !consumer.isStopped()) { try { @@ -250,7 +251,7 @@ public class MainQueueConsumerManager msgs, TbQueueConsumer consumer, Object consumerKey, C config) throws Exception { + protected void processMsgs(List msgs, TbQueueConsumer consumer, ConsumerKey consumerKey, C config) throws Exception { log.trace("Processing {} messages", msgs.size()); msgPackProcessor.process(msgs, consumer, consumerKey, config); log.trace("Processed {} messages", msgs.size()); @@ -273,7 +274,7 @@ public class MainQueueConsumerManager { - void process(List msgs, TbQueueConsumer consumer, Object consumerKey, C config) throws Exception; + void process(List msgs, TbQueueConsumer consumer, ConsumerKey consumerKey, C config) throws Exception; } public interface ConsumerWrapper { @@ -285,6 +286,7 @@ public class MainQueueConsumerManager { + private final Map> consumers = new HashMap<>(); @Override @@ -307,8 +309,7 @@ public class MainQueueConsumerManager partitions, Consumer onStop, Function startOffsetProvider) { partitions.forEach(tpi -> { - Integer partitionId = tpi.getPartition().orElse(-1); - String key = queueKey + "-" + partitionId; + ConsumerKey key = new ConsumerKey(queueKey, tpi); Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null; TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> { @@ -328,9 +329,11 @@ public class MainQueueConsumerManager> getConsumers() { return consumers.values(); } + } class SingleConsumerWrapper implements ConsumerWrapper { + private TbQueueConsumerTask consumer; @Override @@ -346,7 +349,7 @@ public class MainQueueConsumerManager(queueKey, () -> consumerCreator.apply(config, null), null); // no partitionId passed + consumer = new TbQueueConsumerTask<>(new ConsumerKey(queueKey, null), () -> consumerCreator.apply(config, null), null); // no partitionId passed } consumer.subscribe(partitions); if (!consumer.isRunning()) { @@ -361,5 +364,7 @@ public class MainQueueConsumerManager { @Getter - private final Object key; + private final ConsumerKey key; private volatile TbQueueConsumer consumer; private volatile Supplier> consumerSupplier; @Getter @@ -41,7 +41,7 @@ public class TbQueueConsumerTask { @Setter private Future task; - public TbQueueConsumerTask(Object key, Supplier> consumerSupplier, Runnable callback) { + public TbQueueConsumerTask(ConsumerKey key, Supplier> consumerSupplier, Runnable callback) { this.key = key; this.consumer = null; this.consumerSupplier = consumerSupplier; @@ -97,4 +97,18 @@ public class TbQueueConsumerTask { return task != null; } + public record ConsumerKey(Object queueKey, TopicPartitionInfo partition) { + + @Override + public String toString() { + if (partition != null) { + Integer partitionId = partition.getPartition().orElse(-1); + return queueKey + "-" + partitionId; + } else { + return queueKey.toString(); + } + } + + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java index ea15ac5c18..2a83ef9b72 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.queue.common.state; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; import java.util.Collections; +import java.util.Set; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; public class DefaultQueueStateService extends QueueStateService { @@ -26,4 +31,18 @@ public class DefaultQueueStateService partitions, RestoreCallback callback) { + if (callback != null) { + for (TopicPartitionInfo partition : partitions) { + callback.onPartitionRestored(partition); + } + callback.onAllPartitionsRestored(); + } + eventConsumer.addPartitions(partitions); + for (PartitionedQueueConsumerManager consumer : otherConsumers) { + consumer.addPartitions(withTopic(partitions, consumer.getTopic())); + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java index c586da8842..d5ce0a55b8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java @@ -50,7 +50,7 @@ public class KafkaQueueStateService } @Override - protected void addPartitions(QueueKey queueKey, Set partitions, Runnable whenAllProcessed) { + protected void addPartitions(QueueKey queueKey, Set partitions, RestoreCallback callback) { Map eventsStartOffsets = eventsStartOffsetsProvider != null ? eventsStartOffsetsProvider.get() : null; // remembering the offsets before subscribing to states Set statePartitions = withTopic(partitions, stateConsumer.getTopic()); @@ -61,10 +61,13 @@ public class KafkaQueueStateService try { partitionsInProgress.remove(statePartition); log.info("Finished partition {} (still in progress: {})", statePartition, partitionsInProgress); + if (callback != null) { + callback.onPartitionRestored(statePartition); + } if (partitionsInProgress.isEmpty()) { log.info("All partitions processed"); - if (whenAllProcessed != null) { - whenAllProcessed.run(); + if (callback != null) { + callback.onAllPartitionsRestored(); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java index 1cbf7ee016..234d20c32e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java @@ -49,7 +49,7 @@ public abstract class QueueStateService newPartitions, Runnable whenAllProcessed) { + public void update(QueueKey queueKey, Set newPartitions, RestoreCallback callback) { newPartitions = withTopic(newPartitions, eventConsumer.getTopic()); var writeLock = partitionsLock.writeLock(); writeLock.lock(); @@ -71,23 +71,15 @@ public abstract class QueueStateService partitions, Runnable whenAllProcessed) { - if (whenAllProcessed != null) { - whenAllProcessed.run(); - } - eventConsumer.addPartitions(partitions); - for (PartitionedQueueConsumerManager consumer : otherConsumers) { - consumer.addPartitions(withTopic(partitions, consumer.getTopic())); - } - } + protected abstract void addPartitions(QueueKey queueKey, Set partitions, RestoreCallback callback) ; protected void removePartitions(QueueKey queueKey, Set partitions) { eventConsumer.removePartitions(partitions); @@ -122,4 +114,12 @@ public abstract class QueueStateService implements TbQueueCon return stopped; } + @Override + public Set getPartitions() { + return partitions; + } + @Override public List getFullTopicNames() { return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index b6a785316b..50e131103b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -63,8 +63,10 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { private boolean enabled; @Value("${usage.stats.report.enabled_per_customer:false}") private boolean enabledPerCustomer; - @Value("${usage.stats.report.interval:10}") + @Value("${usage.stats.report.interval:60}") private int interval; + @Value("${usage.stats.report.urgent_interval:10}") + private int urgentInterval; @Value("${usage.stats.report.pack_size:1024}") private int packSize; @@ -83,20 +85,30 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { stats.put(key, new ConcurrentHashMap<>()); } + Random random = new Random(); scheduler.scheduleWithFixedDelay(() -> { try { - reportStats(); + reportStats(false); } catch (Exception e) { log.warn("Failed to report statistics: ", e); } - }, new Random().nextInt(interval), interval, TimeUnit.SECONDS); + }, random.nextInt(interval), interval, TimeUnit.SECONDS); + scheduler.scheduleWithFixedDelay(() -> { + try { + reportStats(true); + } catch (Exception e) { + log.warn("Failed to report urgent statistics: ", e); + } + }, random.nextInt(urgentInterval), urgentInterval, TimeUnit.SECONDS); } } - private void reportStats() { + private void reportStats(boolean urgent) { ConcurrentMap report = new ConcurrentHashMap<>(); for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.isUrgent() != urgent) continue; + ConcurrentMap statsForKey = stats.get(key); statsForKey.forEach((reportLevel, statsValue) -> { long value = statsValue.get(); diff --git a/common/script/pom.xml b/common/script/pom.xml index 3527a1c695..a11d320416 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index 6746100ef2..202cc20bd3 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 6a55f3cc71..b074db321a 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index e206d2d78d..ec8eacbc67 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -26,6 +26,7 @@ import org.mvel2.execution.ExecutionHashMap; import org.mvel2.execution.ExecutionLinkedHashSet; import org.mvel2.util.MethodStub; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.NumberUtils; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.GeoUtil; import org.thingsboard.common.util.geo.RangeUnit; @@ -35,7 +36,6 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; -import java.math.RoundingMode; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.ByteBuffer; @@ -258,13 +258,13 @@ public class TbUtils { byte[].class, int.class, int.class))); parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", byte[].class, int.class, int.class, boolean.class))); - parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", + parserConfig.addImport("toFixed", new MethodStub(NumberUtils.class.getMethod("toFixed", double.class, int.class))); - parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", + parserConfig.addImport("toFixed", new MethodStub(NumberUtils.class.getMethod("toFixed", float.class, int.class))); - parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", + parserConfig.addImport("toInt", new MethodStub(NumberUtils.class.getMethod("toInt", double.class))); - parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN", + parserConfig.addImport("isNaN", new MethodStub(NumberUtils.class.getMethod("isNaN", double.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", ExecutionContext.class, String.class))); @@ -1174,22 +1174,6 @@ public class TbUtils { return new String(hexChars, StandardCharsets.UTF_8); } - public static double toFixed(double value, int precision) { - return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue(); - } - - public static float toFixed(float value, int precision) { - return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue(); - } - - public static int toInt(double value) { - return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); - } - - public static boolean isNaN(double value) { - return Double.isNaN(value); - } - public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json) { return toFlatMap(ctx, json, new ArrayList<>(), true); } @@ -1208,7 +1192,6 @@ public class TbUtils { return map; } - public static String encodeURI(String uri) { String encoded = URLEncoder.encode(uri, StandardCharsets.UTF_8); for (var entry : mdnEncodingReplacements.entrySet()) { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java index 47fb994089..cacd9b9426 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java @@ -26,7 +26,10 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; ) @JsonSubTypes({ @JsonSubTypes.Type(value = TbelCfSingleValueArg.class, name = "SINGLE_VALUE"), - @JsonSubTypes.Type(value = TbelCfTsRollingArg.class, name = "TS_ROLLING") + @JsonSubTypes.Type(value = TbelCfTsRollingArg.class, name = "TS_ROLLING"), + @JsonSubTypes.Type(value = TbelCfGeofencingArg.class, name = "GEOFENCING_CF_ARGUMENT_VALUE"), + @JsonSubTypes.Type(value = TbelCfPropagationArg.class, name = "PROPAGATION_CF_ARGUMENT_VALUE"), + @JsonSubTypes.Type(value = TbelCfRelatedEntitiesArgumentValue.class, name = "RELATED_ENTITIES_ARGUMENT_VALUE") }) public interface TbelCfArg extends TbelCfObject { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java new file mode 100644 index 0000000000..16748f79f9 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class TbelCfGeofencingArg implements TbelCfArg { + + private final Object value; + + @JsonCreator + public TbelCfGeofencingArg(@JsonProperty("value") Object value) { + this.value = value; + } + + @Override + public String getType() { + return "GEOFENCING_CF_ARGUMENT_VALUE"; + } + + + @Override + public long memorySize() { + return OBJ_SIZE; + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java new file mode 100644 index 0000000000..b6cfa2c863 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class TbelCfPropagationArg implements TbelCfArg { + + private final Object value; + + @JsonCreator + public TbelCfPropagationArg(@JsonProperty("value") Object value) { + this.value = value; + } + + @Override + public String getType() { + return "PROPAGATION_CF_ARGUMENT_VALUE"; + } + + @Override + public long memorySize() { + return OBJ_SIZE; + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java new file mode 100644 index 0000000000..58a310b3f5 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +@Data +public class TbelCfRelatedEntitiesArgumentValue implements TbelCfArg { + + private final Map entityInputs; + + @JsonCreator + public TbelCfRelatedEntitiesArgumentValue(@JsonProperty("entityInputs") Map values) { + this.entityInputs = Collections.unmodifiableMap(values); + } + + @Override + public String getType() { + return "RELATED_ENTITIES_ARGUMENT_VALUE"; + } + + @Override + public long memorySize() { + return OBJ_SIZE; + } +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java index 3529c94ee4..494eb64cde 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -209,7 +209,9 @@ class TbDateTest { .put("timeZone", "America/New_York") .put("dateStyle", "full") .toString())); - Assertions.assertEquals("середа, 6 вересня 2023 р.", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() + // Java 21+ uses narrow no-break space (U+202F) before "р." per CLDR 42+ + String expectedDate_ukUA = Runtime.version().feature() >= 21 ? "середа, 6 вересня 2023\u202Fр." : "середа, 6 вересня 2023 р."; + Assertions.assertEquals(expectedDate_ukUA, d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("dateStyle", "full") .toString())); @@ -244,12 +246,18 @@ class TbDateTest { Assertions.assertNotNull(d.toLocaleTimeString()); Assertions.assertNotNull(d.toLocaleTimeString("en-US")); - Assertions.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", "America/New_York")); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + String expectedTime_enUS = Runtime.version().feature() >= 21 ? "9:04:05\u202FPM" : "9:04:05 PM"; + Assertions.assertEquals(expectedTime_enUS, d.toLocaleTimeString("en-US", "America/New_York")); Assertions.assertEquals("오후 9:04:05", d.toLocaleTimeString("ko-KR", "America/New_York")); Assertions.assertEquals("04:04:05", d.toLocaleTimeString( "uk-UA", "Europe/Kiev")); Assertions.assertEquals("9:04:05 م", d.toLocaleTimeString( "ar-EG", "America/New_York")); - Assertions.assertEquals("9:04:05 PM Eastern Daylight Time", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + String expectedFullTime_enUS = Runtime.version().feature() >= 21 + ? "9:04:05\u202FPM Eastern Daylight Time" + : "9:04:05 PM Eastern Daylight Time"; + Assertions.assertEquals(expectedFullTime_enUS, d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("timeStyle", "full") .toString())); @@ -292,14 +300,27 @@ class TbDateTest { Assertions.assertNotNull(d.toLocaleString()); Assertions.assertNotNull(d.toLocaleString("en-US")); - Assertions.assertEquals("9/5/23, 9:04:05 PM", d.toLocaleString("en-US", "America/New_York")); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + String expectedLocale_enUS = Runtime.version().feature() >= 21 ? "9/5/23, 9:04:05\u202FPM" : "9/5/23, 9:04:05 PM"; + Assertions.assertEquals(expectedLocale_enUS, d.toLocaleString("en-US", "America/New_York")); Assertions.assertEquals("23. 9. 5. 오후 9:04:05", d.toLocaleString("ko-KR", "America/New_York")); Assertions.assertEquals("06.09.23, 04:04:05", d.toLocaleString( "uk-UA", "Europe/Kiev")); - String expected_ver = Runtime.version().feature() == 11 ? "5\u200F/9\u200F/2023 9:04:05 م" : - "5\u200F/9\u200F/2023, 9:04:05 م"; - Assertions.assertEquals(expected_ver, d.toLocaleString( "ar-EG", "America/New_York")); + // Java 11 has no comma, Java 17 uses ASCII comma, Java 21+ uses Arabic comma (U+060C) + String expected_ar; + if (Runtime.version().feature() == 11) { + expected_ar = "5\u200F/9\u200F/2023 9:04:05 م"; + } else if (Runtime.version().feature() >= 21) { + expected_ar = "5\u200F/9\u200F/2023\u060C 9:04:05 م"; + } else { + expected_ar = "5\u200F/9\u200F/2023, 9:04:05 م"; + } + Assertions.assertEquals(expected_ar, d.toLocaleString( "ar-EG", "America/New_York")); - Assertions.assertEquals("Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time", d.toLocaleString("en-US", JacksonUtil.newObjectNode() + // Java 21+ changed "at" to "," and uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_full = Runtime.version().feature() >= 21 + ? "Tuesday, September 5, 2023, 9:04:05\u202FPM Eastern Daylight Time" + : "Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time"; + Assertions.assertEquals(expected_enUS_full, d.toLocaleString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") .put("timeStyle", "full") @@ -309,15 +330,26 @@ class TbDateTest { .put("dateStyle", "full") .put("timeStyle", "full") .toString())); - Assertions.assertEquals("середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() + // Java 21+ changed Ukrainian locale: "о" replaced with "," and NNBSP before "р." per CLDR 42+ + String expected_ukUA_full_summer = Runtime.version().feature() >= 21 + ? "середа, 6 вересня 2023\u202Fр., 04:04:05 за східноєвропейським літнім часом" + : "середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом"; + Assertions.assertEquals(expected_ukUA_full_summer, d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("dateStyle", "full") .put("timeStyle", "full") .toString())); - expected_ver = Runtime.version().feature() == 11 ? "الثلاثاء، 5 سبتمبر 2023 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية" : - "الثلاثاء، 5 سبتمبر 2023 في 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; - Assertions.assertEquals(expected_ver, d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() + // Java 11 has no connector, Java 17 uses "في", Java 21+ uses Arabic comma "،" + String expected_ar_full; + if (Runtime.version().feature() == 11) { + expected_ar_full = "الثلاثاء، 5 سبتمبر 2023 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; + } else if (Runtime.version().feature() >= 21) { + expected_ar_full = "الثلاثاء، 5 سبتمبر 2023\u060C 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; + } else { + expected_ar_full = "الثلاثاء، 5 سبتمبر 2023 في 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; + } + Assertions.assertEquals(expected_ar_full, d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") .put("timeStyle", "full") @@ -825,16 +857,44 @@ class TbDateTest { @Test public void toStringAsJs() { TbDate d1 = new TbDate(1975, 12, 31, 23,15,30, 567,"-04:00"); - Assertions.assertEquals("четвер, 1 січня 1976 р. о 06:15:30 за східноєвропейським стандартним часом", d1.toString("uk-UA", "Europe/Kyiv")); - Assertions.assertEquals("Thursday, January 1, 1976 at 6:15:30 AM Eastern European Standard Time", d1.toString("en-US", "Europe/Kyiv")); - Assertions.assertEquals("1976 Jan 1, Thu 06:15:30 Eastern European Time", d1.toString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("Wednesday, December 31, 1975 at 10:15:30 PM Eastern Standard Time", d1.toString("en-US", "America/New_York")); - Assertions.assertEquals("1975 Dec 31, Wed 22:15:30 Eastern Standard Time", d1.toString("GMT", "America/New_York")); - Assertions.assertEquals("1975 Dec 31, Wed 22:15:30 Eastern Standard Time", d1.toString("UTC", "America/New_York")); + // Java 21+ changed Ukrainian locale: "о" (at) replaced with "," and NNBSP before "р." per CLDR 42+ + String expected_ukUA_full = Runtime.version().feature() >= 21 + ? "четвер, 1 січня 1976\u202Fр., 06:15:30 за східноєвропейським стандартним часом" + : "четвер, 1 січня 1976 р. о 06:15:30 за східноєвропейським стандартним часом"; + Assertions.assertEquals(expected_ukUA_full, d1.toString("uk-UA", "Europe/Kyiv")); + // Java 21+ changed "at" to "," and uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_Kyiv = Runtime.version().feature() >= 21 + ? "Thursday, January 1, 1976, 6:15:30\u202FAM Eastern European Standard Time" + : "Thursday, January 1, 1976 at 6:15:30 AM Eastern European Standard Time"; + Assertions.assertEquals(expected_enUS_Kyiv, d1.toString("en-US", "Europe/Kyiv")); + // Java 21+ changed timezone display for UTC locale + String expected_UTC_Kyiv = Runtime.version().feature() >= 21 + ? "1976 Jan 1, Thu 06:15:30 Kyiv (+0)" + : "1976 Jan 1, Thu 06:15:30 Eastern European Time"; + Assertions.assertEquals(expected_UTC_Kyiv, d1.toString("UTC", "Europe/Kyiv")); + // Java 21+ changed "at" to "," and uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_NY = Runtime.version().feature() >= 21 + ? "Wednesday, December 31, 1975, 10:15:30\u202FPM Eastern Standard Time" + : "Wednesday, December 31, 1975 at 10:15:30 PM Eastern Standard Time"; + Assertions.assertEquals(expected_enUS_NY, d1.toString("en-US", "America/New_York")); + // Java 21+ changed timezone display for GMT/UTC locales + String expected_GMT_NY = Runtime.version().feature() >= 21 + ? "1975 Dec 31, Wed 22:15:30 New York (+0)" + : "1975 Dec 31, Wed 22:15:30 Eastern Standard Time"; + Assertions.assertEquals(expected_GMT_NY, d1.toString("GMT", "America/New_York")); + Assertions.assertEquals(expected_GMT_NY, d1.toString("UTC", "America/New_York")); Assertions.assertEquals(d1.toUTCString("UTC"), d1.toUTCString()); - Assertions.assertEquals("четвер, 1 січня 1976 р., 03:15:30", d1.toUTCString("uk-UA")); - Assertions.assertEquals("Thursday, January 1, 1976, 3:15:30 AM", d1.toUTCString("en-US")); + // Java 21+ uses NNBSP before "р." per CLDR 42+ + String expected_ukUA_utc = Runtime.version().feature() >= 21 + ? "четвер, 1 січня 1976\u202Fр., 03:15:30" + : "четвер, 1 січня 1976 р., 03:15:30"; + Assertions.assertEquals(expected_ukUA_utc, d1.toUTCString("uk-UA")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_utc = Runtime.version().feature() >= 21 + ? "Thursday, January 1, 1976, 3:15:30\u202FAM" + : "Thursday, January 1, 1976, 3:15:30 AM"; + Assertions.assertEquals(expected_enUS_utc, d1.toUTCString("en-US")); Assertions.assertEquals("1976-01-01T03:15:30.567Z", d1.toJSON()); Assertions.assertEquals("1976-01-01T03:15:30.567Z", d1.toISOString()); @@ -842,22 +902,44 @@ class TbDateTest { Assertions.assertEquals("1976-01-01 06:15:30", d1.toLocaleString("UTC", "Europe/Kyiv")); Assertions.assertEquals("01.01.76, 06:15:30", d1.toLocaleString("uk-UA", "Europe/Kyiv")); Assertions.assertEquals("1975-12-31 22:15:30", d1.toLocaleString("UTC", "America/New_York")); - Assertions.assertEquals("12/31/75, 10:15:30 PM", d1.toLocaleString("en-US", "America/New_York")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_locale_NY = Runtime.version().feature() >= 21 + ? "12/31/75, 10:15:30\u202FPM" + : "12/31/75, 10:15:30 PM"; + Assertions.assertEquals(expected_enUS_locale_NY, d1.toLocaleString("en-US", "America/New_York")); Assertions.assertEquals("1976 Jan 1, Thu", d1.toDateString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("четвер, 1 січня 1976 р.", d1.toDateString("uk-UA", "Europe/Kyiv")); + // Java 21+ uses NNBSP before "р." per CLDR 42+ + String expected_ukUA_date = Runtime.version().feature() >= 21 + ? "четвер, 1 січня 1976\u202Fр." + : "четвер, 1 січня 1976 р."; + Assertions.assertEquals(expected_ukUA_date, d1.toDateString("uk-UA", "Europe/Kyiv")); Assertions.assertEquals("1975 Dec 31, Wed", d1.toDateString("UTC", "America/New_York")); Assertions.assertEquals("Wednesday, December 31, 1975", d1.toDateString("en-US", "America/New_York")); Assertions.assertEquals("06:15:30", d1.toLocaleTimeString("uk-UA", "Europe/Kyiv")); Assertions.assertEquals("06:15:30", d1.toLocaleTimeString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("10:15:30 PM", d1.toLocaleTimeString("en-US", "America/New_York")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_time = Runtime.version().feature() >= 21 ? "10:15:30\u202FPM" : "10:15:30 PM"; + Assertions.assertEquals(expected_enUS_time, d1.toLocaleTimeString("en-US", "America/New_York")); Assertions.assertEquals("22:15:30", d1.toLocaleTimeString("UTC", "America/New_York")); Assertions.assertEquals("06:15:30 за східноєвропейським стандартним часом", d1.toTimeString("uk-UA", "Europe/Kyiv")); - Assertions.assertEquals("06:15:30 Eastern European Time", d1.toTimeString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("10:15:30 PM Eastern Standard Time", d1.toTimeString("en-US", "America/New_York")); - Assertions.assertEquals("22:15:30 Eastern Standard Time", d1.toTimeString("UTC", "America/New_York")); + // Java 21+ changed timezone display for UTC locale + String expected_UTC_time_Kyiv = Runtime.version().feature() >= 21 + ? "06:15:30 Kyiv (+0)" + : "06:15:30 Eastern European Time"; + Assertions.assertEquals(expected_UTC_time_Kyiv, d1.toTimeString("UTC", "Europe/Kyiv")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_timeStr = Runtime.version().feature() >= 21 + ? "10:15:30\u202FPM Eastern Standard Time" + : "10:15:30 PM Eastern Standard Time"; + Assertions.assertEquals(expected_enUS_timeStr, d1.toTimeString("en-US", "America/New_York")); + // Java 21+ changed timezone display for UTC locale + String expected_UTC_time_NY = Runtime.version().feature() >= 21 + ? "22:15:30 New York (+0)" + : "22:15:30 Eastern Standard Time"; + Assertions.assertEquals(expected_UTC_time_NY, d1.toTimeString("UTC", "America/New_York")); } @Test diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 622d298bbb..ef1729ab73 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -374,13 +374,6 @@ public class TbUtilsTest { Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev))); } - @Test - public void toFixedFloat() { - float actualF = TbUtils.toFixed(floatVal, 3); - Assertions.assertEquals(1, Float.compare(floatVal, actualF)); - Assertions.assertEquals(0, Float.compare(29.298f, actualF)); - } - @Test public void parseBytesToFloat() { byte[] floatValByte = {0x0A}; @@ -556,13 +549,6 @@ public class TbUtilsTest { Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev))); } - @Test - public void toFixedDouble() { - double actualD = TbUtils.toFixed(doubleVal, 3); - Assertions.assertEquals(-1, Double.compare(doubleVal, actualD)); - Assertions.assertEquals(0, Double.compare(1729.173, actualD)); - } - @Test public void parseBytesToDouble() { byte[] doubleValByte = {0x0A}; @@ -1242,19 +1228,6 @@ public class TbUtilsTest { Assertions.assertEquals(expected, actual); } - @Test - public void toInt() { - Assertions.assertEquals(1729, TbUtils.toInt(doubleVal)); - Assertions.assertEquals(13, TbUtils.toInt(12.8)); - Assertions.assertEquals(28, TbUtils.toInt(28.0)); - } - - @Test - public void isNaN() { - assertFalse(TbUtils.isNaN(doubleVal)); - assertTrue(TbUtils.isNaN(Double.NaN)); - } - @Test public void isInsidePolygon() { // outside the polygon diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 75d15a89d6..da650623f9 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 3e63520ce2..76ac33b450 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 3a06449bd9..275a497b6e 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.4.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 9a7cfe43ff..56e90e4b7c 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 @@ -213,7 +213,7 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } - @Operation(summary = "Save claiming information (claimDevice)", + @Operation(summary = "Save claiming information (saveClaimingInfo)", description = "Saves the information required for user to claim the device. " + "See more info about claiming in the corresponding 'Claiming devices' platform documentation." + "\n Example of the request payload: " @@ -224,7 +224,7 @@ public class DeviceApiController implements TbTransportService { "In case the secretKey is not specified, the empty string as a default value is used. In case the durationMs is not specified, the system parameter device.claim.duration is used.\n\n" + REQUIRE_ACCESS_TOKEN) @RequestMapping(value = "/{deviceToken}/claim", method = RequestMethod.POST) - public DeferredResult claimDevice( + public DeferredResult saveClaimingInfo( @Parameter(description = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true , schema = @Schema(defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")) @PathVariable("deviceToken") String deviceToken, @RequestBody(required = false) String json) { diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 9267d74dd2..f47255ac7e 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java index a0f9300d81..45b3148219 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java @@ -127,7 +127,9 @@ public class LwM2MBootstrapConfig implements Serializable { private BootstrapConfig.ServerConfig setServerConfig (AbstractLwM2MBootstrapServerCredential serverCredential) { BootstrapConfig.ServerConfig serverConfig = new BootstrapConfig.ServerConfig(); - serverConfig.shortId = serverCredential.getShortServerId(); + if (serverCredential.getShortServerId() != null) { + serverConfig.shortId = serverCredential.getShortServerId(); + } serverConfig.lifetime = serverCredential.getLifetime(); serverConfig.defaultMinPeriod = serverCredential.getDefaultMinPeriod(); serverConfig.notifIfDisabled = serverCredential.isNotifIfDisabled(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java index 8104a62abf..28d4c5b7b7 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.secure; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.Request; import org.eclipse.leshan.core.peer.IpPeer; import org.eclipse.leshan.core.peer.LwM2mPeer; import org.eclipse.leshan.core.peer.PskIdentity; @@ -112,8 +113,10 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession } catch (InvalidConfigurationException e){ log.error("Failed put to lwM2MBootstrapSessionClients by endpoint [{}]", request.getEndpointName(), e); } + String msg = String.format("Bootstrap session started... %s", ((Request) request.getCoapRequest()).getLocalAddress().toString()); + log.warn(String.format("%s: %s", request.getEndpointName(), msg)); this.sendLogs(request.getEndpointName(), - String.format("%s: Bootstrap session started...", LOG_LWM2M_INFO, request.getEndpointName())); + String.format("%s: %s", LOG_LWM2M_INFO, msg)); } return session; } @@ -135,7 +138,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession session.setModel(modelProvider.getObjectModel(session, tasks.supportedObjects)); // set Requests to Send - log.info("tasks.requestsToSend = [{}]", tasks.requestsToSend); + log.warn("tasks.requestsToSend = [{}]", tasks.requestsToSend); session.setRequests(tasks.requestsToSend); // prepare list where we will store Responses @@ -182,14 +185,16 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession session.getResponses().add(response); String msg = String.format("%s: receives success response for: %s %s %s", LOG_LWM2M_INFO, request.getClass().getSimpleName(), request.getPath().toString(), response.toString()); + log.warn(msg); this.sendLogs(bsSession.getEndpoint(), msg); // on success for NOT bootstrap finish request we send next request return BootstrapPolicy.continueWith(nextRequest(bsSession)); } else { // on success for bootstrap finish request we stop the session - this.sendLogs(bsSession.getEndpoint(), - String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO)); + String msg = String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO); + log.info(msg); + this.sendLogs(bsSession.getEndpoint(), msg); this.tasksProvider.remove(bsSession.getEndpoint()); return BootstrapPolicy.finished(); } @@ -228,7 +233,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession @Override public void end(BootstrapSession bsSession) { - this.sendLogs(bsSession.getEndpoint(), String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO)); + String msg = String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO); + log.warn(msg); + this.sendLogs(bsSession.getEndpoint(), msg); this.tasksProvider.remove(bsSession.getEndpoint()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java index cd54e9a0f1..a40bf9ec27 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java @@ -17,15 +17,12 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.store; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.link.Link; -import org.eclipse.leshan.core.node.LwM2mObject; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; -import org.eclipse.leshan.core.request.BootstrapReadRequest; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.response.BootstrapDiscoverResponse; -import org.eclipse.leshan.core.response.BootstrapReadResponse; import org.eclipse.leshan.core.response.LwM2mResponse; import org.eclipse.leshan.server.bootstrap.BootstrapConfig; import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore; @@ -33,7 +30,6 @@ import org.eclipse.leshan.server.bootstrap.BootstrapSession; import org.eclipse.leshan.server.bootstrap.BootstrapUtil; import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -43,14 +39,18 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.stream.Collectors; -import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; +import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest; -import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID_0; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isLwm2mServer; @Slf4j public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider { @@ -77,11 +77,11 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask @Override public Tasks getTasks(BootstrapSession session, List previousResponse) { // BootstrapConfig config = store.get(session.getEndpoint(), session.getClientTransportData().getIdentity(), session); - BootstrapConfig config = store.get(session); - if (config == null) { + BootstrapConfig configNew = store.get(session); + if (configNew == null) { return null; } - if (previousResponse == null && shouldStartWithDiscover(config)) { + if (previousResponse == null && shouldStartWithDiscover(configNew)) { Tasks tasks = new Tasks(); tasks.requestsToSend = new ArrayList<>(1); tasks.requestsToSend.add(new BootstrapDiscoverRequest()); @@ -96,47 +96,27 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask tasks.supportedObjects = this.supportedObjects; // handle bootstrap discover response if (previousResponse != null) { - if (previousResponse.get(0) instanceof BootstrapDiscoverResponse) { - BootstrapDiscoverResponse discoverResponse = (BootstrapDiscoverResponse) previousResponse.get(0); + if (previousResponse.get(0) instanceof BootstrapDiscoverResponse discoverResponse) { if (discoverResponse.isSuccess()) { - this.initAfterBootstrapDiscover(discoverResponse); - findSecurityInstanceId(discoverResponse.getObjectLinks(), session.getEndpoint()); - } else { + this.initAfterBootstrapDiscover(discoverResponse); + /// Short Server Ids - in old config + findInstancesIdOldByServerId(discoverResponse, session.getEndpoint()); log.warn( - "Bootstrap Discover return error {} : to continue bootstrap session without autoIdForSecurityObject mode. {}", + "Bootstrap server instance successfully found in Security Object (0) in response {}. Continuing bootstrap session. Session: {}", discoverResponse, session); - } - if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0) == null) { - log.error( - "Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}", + } else { + log.warn( + "Unable to find bootstrap server instance in Security Object (0) in response {}. Continuing bootstrap session with autoIdForSecurityObject mode, ignoring information from discoverResponse. Session: {}", discoverResponse, session); - return null; - } - tasks.requestsToSend = new ArrayList<>(1); - tasks.requestsToSend.add(new BootstrapReadRequest("/1")); - tasks.last = false; - return tasks; - } - BootstrapReadResponse readResponse = (BootstrapReadResponse) previousResponse.get(0); - Integer bootstrapServerIdOld = null; - if (readResponse.isSuccess()) { - findServerInstanceId(readResponse, session.getEndpoint()); - if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().size() > 0 && this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getServerInstances().size() > 0) { - bootstrapServerIdOld = this.findBootstrapServerId(session.getEndpoint()); } - } else { - log.warn( - "Bootstrap ReadResponse return error {} : to continue bootstrap session without find Server Instance Id. {}", - readResponse, session); } // create requests from config - tasks.requestsToSend = this.toRequests(config, - config.contentFormat != null ? config.contentFormat : session.getContentFormat(), - bootstrapServerIdOld, session.getEndpoint()); + tasks.requestsToSend = this.toRequests(configNew, + configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat(), session.getEndpoint()); } else { // create requests from config - tasks.requestsToSend = BootstrapUtil.toRequests(config, - config.contentFormat != null ? config.contentFormat : session.getContentFormat()); + tasks.requestsToSend = BootstrapUtil.toRequests(configNew, + configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat()); } return tasks; } @@ -148,81 +128,57 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask /** * "Short Server ID": This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. - * The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server. - * "Short Server ID": + * "Short Lwm2m Server ID": * - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1). - * - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.0). + * The values ID:0 values MUST NOT be used for identifying the LwM2M Server only BS. */ - protected void findSecurityInstanceId(Link[] objectLinks, String endpoint) { - log.info("Object after discover: [{}]", objectLinks); - for (Link link : objectLinks) { - if (link.getUriReference().startsWith("/0/")) { - try { - LwM2mPath path = new LwM2mPath(link.getUriReference()); - if (path.isObjectInstance()) { - if (link.getAttributes().get("ssid") != null) { - int serverId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); - if (!lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(serverId)) { - lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); - } else { - log.error("Invalid lwm2mSecurityInstance by [{}]", path.getObjectInstanceId()); - } - lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); + protected void findInstancesIdOldByServerId(BootstrapDiscoverResponse discoverResponses, String endpoint) { + log.info("Object after discover: [{}]", Arrays.toString(discoverResponses.getObjectLinks())); + for (Link link : discoverResponses.getObjectLinks()) { + LwM2mPath path = new LwM2mPath(link.getUriReference()); + if (path.isObjectInstance()) { + int lwm2mShortServerId = 0; + if (path.getObjectId() == 0) { + if (link.getAttributes().get("ssid") != null) { + lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); + if (validateLwm2mShortServerId(lwm2mShortServerId)) { + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId()); + } else { + log.error("Invalid lwm2mSecurityInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId); + } + } else { + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(null, path.getObjectInstanceId()); + } + } else if (path.getObjectId() == 1) { + if (link.getAttributes().get("ssid") != null) { + lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); + if (validateLwm2mShortServerId(lwm2mShortServerId)) { + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId()); } else { - if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) { - this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID_0, path.getObjectInstanceId()); - } else { - log.error("Invalid bootstrapSecurityInstance by [{}]", path.getObjectInstanceId()); - } + log.error("Invalid lwm2mServerInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId); } } - } catch (Exception e) { - // ignore if this is not a LWM2M path - log.error("Invalid LwM2MPath starting by \"/0/\""); } } } } - protected void findServerInstanceId(BootstrapReadResponse readResponse, String endpoint) { - try { - ((LwM2mObject) readResponse.getContent()).getInstances().values().forEach(instance -> { - var shId = OPAQUE.equals(instance.getResource(0).getType()) ? new BigInteger((byte[]) instance.getResource(0).getValue()).intValue() : instance.getResource(0).getValue(); - int shortId; - if (shId instanceof Long) { - shortId = ((Long) shId).intValue(); - } else { - shortId = (int) shId; - } - this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().put(shortId, instance.getId()); - }); - } catch (Exception e) { - log.error("Failed find Server Instance Id. ", e); - } - } - - protected Integer findBootstrapServerId(String endpoint) { - Integer bootstrapServerIdOld = null; - Map filteredMap = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().entrySet() - .stream().filter(x -> !this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(x.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - if (filteredMap.size() > 0) { - bootstrapServerIdOld = filteredMap.keySet().stream().findFirst().get(); - } - return bootstrapServerIdOld; - } - public BootstrapConfigStore getStore() { return this.store; } private void initAfterBootstrapDiscover(BootstrapDiscoverResponse response) { Link[] links = response.getObjectLinks(); + AtomicReference verDefault = new AtomicReference<>("1.0"); Arrays.stream(links).forEach(link -> { LwM2mPath path = new LwM2mPath(link.getUriReference()); - if (!path.isRoot() && path.getObjectId() < 3) { + if (path.isRoot()) { + if (link.hasAttribute() && link.getAttributes().get("lwm2m") != null) { + verDefault.set(link.getAttributes().get("lwm2m").getValue().toString()); + } + } else if (path.getObjectId() <= ACCESS_CONTROL) { if (path.isObject()) { - String ver = link.getAttributes().get("ver") != null ? link.getAttributes().get("ver").getCoreLinkValue() : "1.0"; + String ver = (link.hasAttribute() && link.getAttributes().get("ver") != null) ? link.getAttributes().get("ver").getCoreLinkValue() : verDefault.get(); this.supportedObjects.put(path.getObjectId(), ver); } } @@ -230,94 +186,124 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask } - public List> toRequests(BootstrapConfig bootstrapConfig, + /** Map => LwM2MBootstrapClientInstanceIds + * 1) Both + * - (Short) Server ID == null bs) + * SECURITY = 0; InstanceId = 0 + * - Short Server ID == 1 - 65534 lwm2m) + * SECURITY = 0; InstanceId = 1 + * SERVER = 1; InstanceId = 0 + * 2) Only BS Server + * - Short Server ID == null bs) + * SECURITY = 0; InstanceId = 0 + * 3) Only Lwm2m Server + * - Short Server ID == 1 - 65534 lwm2m) + * SECURITY = 0; InstanceId = 0 + * SERVER = 1; InstanceId = 0 + * */ + public List> toRequests(BootstrapConfig bootstrapConfigNew, ContentFormat contentFormat, - Integer bootstrapServerIdOld, String endpoint) { + Integer bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null) == null ? + -2 : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null); List> requests = new ArrayList<>(); Set pathsDelete = new HashSet<>(); - List> requestsWrite = new ArrayList<>(); - boolean isBsServer = false; - boolean isLwServer = false; - /** Map */ - Map instances = new HashMap<>(); - Integer bootstrapServerIdNew = null; - // handle security - int lwm2mSecurityInstanceId = 0; - int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0); - for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) { - if (security.bootstrapServer) { - requestsWrite.add(toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat)); - isBsServer = true; - bootstrapServerIdNew = security.serverId; - instances.put(security.serverId, bootstrapSecurityInstanceId); - } else { - if (lwm2mSecurityInstanceId == bootstrapSecurityInstanceId) { - lwm2mSecurityInstanceId++; - } - requestsWrite.add(toWriteRequest(lwm2mSecurityInstanceId, security, contentFormat)); - instances.put(security.serverId, lwm2mSecurityInstanceId); - isLwServer = true; - if (!isBsServer && this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(security.serverId) && - lwm2mSecurityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId)) { - pathsDelete.add("/0/" + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId)); - } - /** - * If there is an instance in the serverInstances with serverId which we replace in the securityInstances - */ - // find serverId in securityInstances by id (instance) - Integer serverIdOld = null; - for (Map.Entry entry : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().entrySet()) { - if (entry.getValue().equals(lwm2mSecurityInstanceId)) { - serverIdOld = entry.getKey(); - } - } - if (!isBsServer && serverIdOld != null && this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(serverIdOld)) { - pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(serverIdOld)); - } - lwm2mSecurityInstanceId++; + ConcurrentHashMap> requestsWrite = new ConcurrentHashMap<>(); + + /// handle security & handle + // bootstrap Security new - There can only be one instance of bootstrap at a time. + /// bs: handle security only + for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) { + if (security.bootstrapServer && bootstrapSecurityInstanceId > -1) { + // delete old bootstrap Security + String path = "/" + SECURITY + "/" + bootstrapSecurityInstanceId; + pathsDelete.add(path); + security.serverId = null; + requestsWrite.put(path, toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat)); } } - // handle server - for (Map.Entry server : bootstrapConfig.servers.entrySet()) { - int securityInstanceId = instances.get(server.getValue().shortId); - requestsWrite.add(toWriteRequest(securityInstanceId, server.getValue(), contentFormat)); - if (!isBsServer) { - /** Delete instance if bootstrapServerIdNew not equals bootstrapServerIdOld or securityInstanceBsIdNew not equals serverInstanceBsIdOld */ - if (bootstrapServerIdNew != null && server.getValue().shortId == bootstrapServerIdNew && - (bootstrapServerIdNew != bootstrapServerIdOld || securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld))) { - pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld)); - /** Delete instance if serverIdNew is present in serverInstances and securityInstanceIdOld by serverIdNew not equals serverInstanceIdOld */ - } else if (this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(server.getValue().shortId) && - securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)) { - pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)); - } + + /** lwm2m servers: Multiple instances of lwm2m servers can run simultaneously by SHORT_ID + if update -> delete and write by InstanceId + if new -> only write with InstanceIdMax++ + */ + + /// lwm2m server: handle security & server + //max Lwm2m Security instance old id if new + int lwm2mSecurityInstanceIdMax = -1; + for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().keySet()) { + if (isLwm2mServer(shortId)) { + lwm2mSecurityInstanceIdMax = Math.max( + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(shortId), + lwm2mSecurityInstanceIdMax); } } - // handle acl - for (Map.Entry acl : bootstrapConfig.acls.entrySet()) { - requestsWrite.add(toWriteRequest(acl.getKey(), acl.getValue(), contentFormat)); + //max Lwm2m Server instance old id if new + int lwm2mServerInstanceIdMax = -1; + for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().keySet()) { + if (isLwm2mServer(shortId)) { + lwm2mServerInstanceIdMax = Math.max( + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(shortId), + lwm2mServerInstanceIdMax); + } } - // handle delete - if (isBsServer && isLwServer) { - requests.add(new BootstrapDeleteRequest("/0")); - requests.add(new BootstrapDeleteRequest("/1")); - } else { - pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete))); + // Lwm2m update or new + for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) { + if (!security.bootstrapServer) { + // Security + Integer secureInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId); + if (secureInstanceId != null) { + pathsDelete.add("/" + SECURITY + "/" + secureInstanceId); + requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat)); + } else { + secureInstanceId = ++lwm2mSecurityInstanceIdMax; + if (bootstrapSecurityInstanceId.equals(secureInstanceId)) { + secureInstanceId = ++lwm2mSecurityInstanceIdMax; + } + requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat)); + } + Integer serverInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(security.serverId); + if (serverInstanceId != null) { + pathsDelete.add("/" + SERVER + "/" + serverInstanceId); + } else { + serverInstanceId = ++lwm2mServerInstanceIdMax; + } + Integer finalServerInstanceId = serverInstanceId; + new TreeMap<>(bootstrapConfigNew.servers).values().stream() + .filter(server -> server.shortId == security.serverId) + .findFirst() + .ifPresent(server -> + requestsWrite.put( + "/" + SERVER + "/" + finalServerInstanceId, + toWriteRequest(finalServerInstanceId, server, contentFormat) + ) + ); + } } - // handle write - if (requestsWrite.size() > 0) { - requests.addAll(requestsWrite); + + /// handle acl + for (Map.Entry acl : bootstrapConfigNew.acls.entrySet()) { + requestsWrite.put("/" + ACCESS_CONTROL + "/" + acl.getKey(), toWriteRequest(acl.getKey(), acl.getValue(), contentFormat)); + } + /// handle delete + pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete))); + + /// handle write + if (!requestsWrite.isEmpty()) { + requests.addAll(requestsWrite.values()); } return (requests); } - private void initSupportedObjectsDefault() { this.supportedObjects = new HashMap<>(); - this.supportedObjects.put(0, "1.1"); - this.supportedObjects.put(1, "1.1"); - this.supportedObjects.put(2, "1.0"); + this.supportedObjects.put(SECURITY, "1.1"); + this.supportedObjects.put(SERVER, "1.1"); + this.supportedObjects.put(ACCESS_CONTROL, "1.0"); + } + + private boolean validateLwm2mShortServerId(int id){ + return id >= PRIMARY_LWM2M_SERVER.getId() && id <= LWM2M_SERVER_MAX.getId(); } @Override diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java index 6bb8d5ac26..dec5170069 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java @@ -21,6 +21,10 @@ import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; import java.util.Map; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isNotLwm2mServer; + public class LwM2MConfigurationChecker extends ConfigurationChecker { @Override @@ -74,15 +78,16 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker { * This Resource MUST be set when the Bootstrap-Server Resource has false value. * Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification). */ - if (!security.bootstrapServer && (srvCfg.shortId < 1 && srvCfg.shortId > 65534 )) { - throw new InvalidConfigurationException("Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server"); + if (!security.bootstrapServer && isNotLwm2mServer(srvCfg.shortId)) { + throw new InvalidConfigurationException("Specific ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN.getId() + " and ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId() + " values MUST NOT be used for identifying the LwM2M Server"); } } } protected static BootstrapConfig.ServerSecurity getSecurityEntry(BootstrapConfig config, int shortId) { for (Map.Entry es : config.security.entrySet()) { - if (es.getValue().serverId == shortId) { + if ((es.getValue().serverId == null && shortId == 0) || + (es.getValue().serverId != null && es.getValue().serverId == shortId)) { return es.getValue(); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index d2b24fe9a8..5373c56320 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -43,7 +43,7 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { private int dtlsRetransmissionTimeout; @Getter - @Value("${transport.lwm2m.dtls.connection_id_length:}") + @Value("${transport.lwm2m.dtls.connection_id_length:8}") private Integer dtlsCidLength; @Getter diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index 65916971ae..234ef11d0b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -38,6 +38,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.time.Instant; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; @@ -180,8 +181,16 @@ public class LwM2mTransportServerHelper { case BOOLEAN: kvProto.setType(BOOLEAN_V).setBoolV((Boolean) value).build(); break; - case STRING: case TIME: + if (value instanceof Date) { + kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV(((Date) value).getTime()); + } else if (value instanceof Integer || value instanceof Long) { + kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV((long) (value)); + } else { + kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV(value.toString()); + } + break; + case STRING: case OPAQUE: case OBJLNK: kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV((String) value); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 67d2838e1d..dea3b901e1 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -66,7 +66,9 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_TRIGGER_PARAMS_ID; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.REGISTRATION_TRIGGER_PARAMS_ID; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @@ -342,6 +344,10 @@ public class LwM2mClient { public String isValidObjectVersion(String path) { LwM2mPath pathIds = getLwM2mPathFromString(path); + if (pathIds.isResource() && (pathIds.toString().equals(REGISTRATION_TRIGGER_PARAMS_ID ) || + pathIds.toString().equals(BOOTSTRAP_TRIGGER_PARAMS_ID))) { + return ""; + } LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId()); if (verSupportedObject == null) { return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java index c4aa473a39..5db88ce306 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java @@ -23,6 +23,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MUplinkTarge import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; +import java.util.concurrent.CountDownLatch; + @Slf4j public class TbLwM2MObserveCompositeCallback extends TbLwM2MUplinkTargetedCallback { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index f906e1e9bf..cb922e5905 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -94,6 +94,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttrib import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService; @@ -502,14 +504,30 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void sendReadRequests(LwM2mClient lwM2MClient, Lwm2mDeviceProfileTransportConfiguration profile, Set supportedObjects) { try { Set targetIds = new HashSet<>(profile.getObserveAttr().getAttribute()); + Boolean initAttrTelAsObsStrategy = profile.getObserveAttr().getInitAttrTelAsObsStrategy(); targetIds.addAll(profile.getObserveAttr().getTelemetry()); targetIds = diffSets(profile.getObserveAttr().getObserve(), targetIds); targetIds = targetIds.stream().filter(target -> isSupportedTargetId(supportedObjects, target)).collect(Collectors.toSet()); - - CountDownLatch latch = new CountDownLatch(targetIds.size()); - targetIds.forEach(versionedId -> sendReadRequest(lwM2MClient, versionedId, - new TbLwM2MLatchCallback<>(latch, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId)))); - latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); + if (!targetIds.isEmpty()) { + TelemetryObserveStrategy observeStrategy = profile.getObserveAttr().getObserveStrategy(); + long timeoutMs = config.getTimeout(); + if (initAttrTelAsObsStrategy && observeStrategy != SINGLE) { + switch (observeStrategy) { + case COMPOSITE_ALL -> { + sendReadCompositeRequest(lwM2MClient, targetIds.toArray(new String[0])); + } + case COMPOSITE_BY_OBJECT -> { + Map versionedObjectIds = groupByObjectIdVersionedIds(targetIds); + versionedObjectIds.forEach((k, v) -> sendReadCompositeRequest(lwM2MClient, v)); + } + } + } else { + CountDownLatch latch = new CountDownLatch(targetIds.size()); + targetIds.forEach(versionedId -> sendReadSingleRequest(lwM2MClient, versionedId, + new TbLwM2MLatchCallback<>(latch, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId)))); + latch.await(timeoutMs, TimeUnit.MILLISECONDS); + } + } } catch (InterruptedException e) { log.error("[{}] Failed to await Read requests!", lwM2MClient.getEndpoint(), e); } catch (Exception e) { @@ -536,17 +554,11 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl if (!completed) log.trace("[{}] Timeout occurred during SINGLE observe init", lwM2MClient.getEndpoint()); } case COMPOSITE_ALL -> { - CountDownLatch latch = new CountDownLatch(targetIds.size()); sendObserveCompositeRequest(lwM2MClient, targetIds.toArray(new String[0])); - boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); - if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_ALL observe init", lwM2MClient.getEndpoint()); } case COMPOSITE_BY_OBJECT -> { Map versionedObjectIds = groupByObjectIdVersionedIds(targetIds); - CountDownLatch latch = new CountDownLatch(versionedObjectIds.size()); versionedObjectIds.forEach((k, v) -> sendObserveCompositeRequest(lwM2MClient, v)); - boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); - if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_BY_OBJECT observe init", lwM2MClient.getEndpoint()); } } } @@ -569,23 +581,22 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } - private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId) { - sendReadRequest(lwM2MClient, versionedId, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId)); - } - - private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { + private void sendReadSingleRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendReadRequest(lwM2MClient, request, callback); } - private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId) { - sendObserveRequest(lwM2MClient, versionedId, new TbLwM2MObserveCallback(this, logService, lwM2MClient, versionedId)); + private void sendReadCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) { + TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); + var mainCallback = new TbLwM2MReadCompositeCallback(this, logService, lwM2MClient, versionedIds); + defaultLwM2MDownlinkMsgHandler.sendReadCompositeRequest(lwM2MClient, request, mainCallback ); } private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendObserveRequest(lwM2MClient, request, callback); } + private void sendObserveCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) { TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); @@ -829,7 +840,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); request.getObjectInstances().forEach(instance -> instance.getResources().forEach((resId, lwM2mResource) ->{ - this.updateResourcesValue(updateResource, lwM2mResource, versionId + "/" + resId, Mode.REPLACE, 0); + String path = versionId.endsWith("/") ? versionId + resId : versionId + "/" + resId; + this.updateResourcesValue(updateResource, lwM2mResource, path, Mode.REPLACE, 0); }) ); clientContext.update(lwM2MClient); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index 8eb1e3e8cd..cfa21a3bec 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -81,7 +81,8 @@ public class LwM2MTransportUtil { public static final String LOG_LWM2M_INFO = "info"; public static final String LOG_LWM2M_ERROR = "error"; public static final String LOG_LWM2M_WARN = "warn"; - public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0; + public static final String REGISTRATION_TRIGGER_PARAMS_ID = "/1/0/8"; + public static final String BOOTSTRAP_TRIGGER_PARAMS_ID = "/1/0/9";; public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) { String path = fromVersionedIdToObjectId(pathIdVer); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index b9e11db3e0..43867564ae 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -33,6 +33,7 @@ import java.util.Date; import static org.eclipse.leshan.core.model.ResourceModel.Type.NONE; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; +import static org.eclipse.leshan.core.model.ResourceModel.Type.TIME; @Slf4j public class LwM2mValueConverterImpl implements LwM2mValueConverter { @@ -58,7 +59,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { currentType = OPAQUE; } - if (currentType == expectedType || currentType == NONE) { + if (currentType == expectedType || currentType == NONE || currentType == TIME) { /** expected type */ return value; } @@ -135,7 +136,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { **/ } catch (IllegalArgumentException e) { log.debug("Unable to convert string to date", e); - throw new CodecException("Unable to convert string (%s) to date for resource %s", value, + throw new CodecException("Unable to convert string (%s) to %s for resource %s", value, TIME.name(), resourcePath); } default: @@ -149,7 +150,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { case FLOAT: return String.valueOf(value); case TIME: - String DATE_FORMAT = "MMM d, yyyy HH:mm a"; + String DATE_FORMAT = "yyyy-MM-dd[[ ]['T']HH:mm[:ss[.SSS]][ ][XXX][Z][z][VV][O]]"; Long timeValue; try { timeValue = ((Date) value).getTime(); diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index b63ad8a1ac..985c2a82f6 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport 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 3d05e999e0..10245c5a24 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 @@ -88,20 +88,30 @@ public class MqttTransportContext extends TransportContext { @Value("${transport.mqtt.proxy_enabled:false}") private boolean proxyEnabled; - private final AtomicInteger connectionsCounter = new AtomicInteger(); + private final AtomicInteger connectionsActiveCounterMQTT = new AtomicInteger(); + private final AtomicInteger connectionsActiveCounterMQTTS = new AtomicInteger(); @PostConstruct public void init() { super.init(); - transportService.createGaugeStats("openConnections", connectionsCounter); + transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTT, "protocol", "MQTT"); + transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTTS, "protocol", "MQTTS"); } - public void channelRegistered() { - connectionsCounter.incrementAndGet(); + public void channelRegistered(boolean isSSL) { + if (isSSL) { + connectionsActiveCounterMQTTS.incrementAndGet(); + } else { + connectionsActiveCounterMQTT.incrementAndGet(); + } } - public void channelUnregistered() { - connectionsCounter.decrementAndGet(); + public void channelUnregistered(boolean isSSL) { + if (isSSL) { + connectionsActiveCounterMQTTS.decrementAndGet(); + } else { + connectionsActiveCounterMQTT.decrementAndGet(); + } } public boolean checkAddress(InetSocketAddress address) { 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 f753579292..48fbad3032 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 @@ -180,16 +180,20 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.rpcAwaitingAck = new ConcurrentHashMap<>(); } + boolean isSSL() { + return sslHandler != null; + } + @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { super.channelRegistered(ctx); - context.channelRegistered(); + context.channelRegistered(isSSL()); } @Override public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { super.channelUnregistered(ctx); - context.channelUnregistered(); + context.channelUnregistered(isSSL()); } @Override @@ -254,6 +258,17 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + String getClientAddr(ChannelHandlerContext ctx) { + try { + InetSocketAddress remote = getAddress(ctx); + if (remote == null) return "unknown"; + String host = remote.getAddress() != null ? remote.getAddress().getHostAddress() : remote.getHostString(); + return host + ":" + remote.getPort(); + } catch (Exception ignored) { + return "unknown"; + } + } + InetSocketAddress getAddress(ChannelHandlerContext ctx) { var address = ctx.channel().attr(MqttTransportService.ADDRESS).get(); if (address == null) { @@ -1150,19 +1165,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof IOException) { if (log.isDebugEnabled()) { - log.debug("[{}][{}][{}] IOException: {}", sessionId, + String clientAddr = getClientAddr(ctx); + log.debug("[{}][{}][{}][{}] {}: {}", sessionId, Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), + clientAddr, + cause.getClass().getSimpleName(), cause.getMessage(), cause); } else if (log.isInfoEnabled()) { - log.info("[{}][{}][{}] IOException: {}", sessionId, + String clientAddr = getClientAddr(ctx); + log.info("[{}][{}][{}][{}] {}: {}", sessionId, Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), + clientAddr, + cause.getClass().getSimpleName(), cause.getMessage()); } } else { - log.error("[{}] Unexpected Exception", sessionId, cause); + log.error("[{}][{}] Unexpected Exception", sessionId, getClientAddr(ctx), cause); } closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN); diff --git a/common/transport/pom.xml b/common/transport/pom.xml index d77303b40c..810b22670a 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index f746b58b7d..f98b2426dc 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 10ce5ede63..73e0bdeec0 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 - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport 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 5c7d552855..a8f8a8b863 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 @@ -161,5 +161,5 @@ public interface TransportService { boolean hasSession(SessionInfoProto sessionInfo); - void createGaugeStats(String openConnections, AtomicInteger connectionsCounter); + void createGaugeStats(String statsName, AtomicInteger number, String... tags); } 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 260957c990..b5384eada7 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 @@ -508,7 +508,9 @@ public class DefaultTransportService extends TransportActivityManager implements @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionEventMsg msg, TransportServiceCallback callback) { if (checkLimits(sessionInfo, msg, callback)) { - recordActivityInternal(sessionInfo); + if (msg.getEvent() != TransportProtos.SessionEvent.CLOSED) { + recordActivityInternal(sessionInfo); + } sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo) .setSessionEvent(msg).build(), callback); } @@ -1257,9 +1259,21 @@ public class DefaultTransportService extends TransportActivityManager implements } @Override - public void createGaugeStats(String statsName, AtomicInteger number) { - statsFactory.createGauge(StatsType.TRANSPORT + "." + statsName, number); - statsMap.put(statsName, number); + public void createGaugeStats(String statsName, AtomicInteger number, String... tags) { + String key = "thingsboard" + "." + StatsType.TRANSPORT.getName() + "." + statsName; + statsFactory.createGauge(key, number, tags); + statsMap.put(statsName + tagsKey(tags), number); + } + + private String tagsKey(String... tags) { + if (tags == null || tags.length < 2) return ""; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < tags.length; i += 2) { + if (i > 0) sb.append(','); + sb.append(tags[i]).append('=').append(i + 1 < tags.length ? tags[i + 1] : ""); + } + sb.append(']'); + return sb.toString(); } @Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}") diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java index 3b3ce6e600..a656b22241 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java @@ -26,12 +26,14 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.activity.ActivityReportCallback; import org.thingsboard.server.common.transport.activity.ActivityState; import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategy; import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategyType; +import org.thingsboard.server.common.transport.limits.TransportRateLimitService; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.UUID; @@ -41,7 +43,12 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -514,4 +521,55 @@ public class TransportActivityManagerTest { verify(transportServiceMock, never()).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); } + @Test + void givenSessionClosedEvent_whenProcessingSessionEvent_thenShouldNotRecordActivity() { + // GIVEN - simulates the session-expiry path: session already removed from sessions map, + // then process(SESSION_CLOSED) is called. Activity must NOT be recorded to avoid active + // status update from false to true + var rateLimitServiceMock = mock(TransportRateLimitService.class); + ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock); + when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null); + + TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() + .setSessionIdMSB(SESSION_ID.getMostSignificantBits()) + .setSessionIdLSB(SESSION_ID.getLeastSignificantBits()) + .build(); + doCallRealMethod().when(transportServiceMock).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); + + // WHEN + transportServiceMock.process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); + + // THEN + verify(transportServiceMock, never()).onActivity(any(), any(), anyLong()); + verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull()); + } + + @Test + void givenSessionOpenEvent_whenProcessingSessionEvent_thenShouldRecordActivity() { + // GIVEN + var rateLimitServiceMock = mock(TransportRateLimitService.class); + ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock); + when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null); + + TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() + .setSessionIdMSB(SESSION_ID.getMostSignificantBits()) + .setSessionIdLSB(SESSION_ID.getLeastSignificantBits()) + .build(); + var sessionOpenMsg = TransportProtos.SessionEventMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC) + .setEvent(TransportProtos.SessionEvent.OPEN) + .build(); + when(transportServiceMock.toSessionId(sessionInfo)).thenReturn(SESSION_ID); + long expectedActivityTime = 100L; + when(transportServiceMock.getCurrentTimeMillis()).thenReturn(expectedActivityTime); + doCallRealMethod().when(transportServiceMock).process(sessionInfo, sessionOpenMsg, null); + + // WHEN + transportServiceMock.process(sessionInfo, sessionOpenMsg, null); + + // THEN + verify(transportServiceMock).onActivity(SESSION_ID, sessionInfo, expectedActivityTime); + verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull()); + } + } diff --git a/common/util/pom.xml b/common/util/pom.xml index 4748d75d92..f3077d49c3 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java similarity index 87% rename from common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java rename to common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java index 07ffdaa2a1..529c017990 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java @@ -15,13 +15,16 @@ */ package org.thingsboard.common.util; +import net.objecthunter.exp4j.Expression; +import net.objecthunter.exp4j.ExpressionBuilder; import net.objecthunter.exp4j.function.Function; import net.objecthunter.exp4j.function.Functions; import java.util.ArrayList; import java.util.List; +import java.util.Set; -public class ExpressionFunctionsUtil { +public class ExpressionUtils { public static final List userDefinedFunctions = new ArrayList<>(); @@ -75,4 +78,13 @@ public class ExpressionFunctionsUtil { userDefinedFunctions.add(Functions.getBuiltinFunction("signum")); } + public static Expression createExpression(String expression, Set variables) { + return new ExpressionBuilder(expression) + .functions(userDefinedFunctions) + .implicitMultiplication(true) + .operator() + .variables(variables) + .build(); + } + } diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 9913210c1a..cf547cb9d4 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -115,10 +115,15 @@ public class JacksonUtil { @Contract("null, _ -> null") // so that IDE doesn't show NPE warning when input is not null public static T fromString(String string, Class clazz) { + return fromString(string, clazz, "The given string value cannot be transformed to Json object: " + string); + } + + @Contract("null, _, _ -> null") // so that IDE doesn't show NPE warning when input is not null + public static T fromString(String string, Class clazz, String errorMsg) { try { return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; } catch (IOException e) { - throw new IllegalArgumentException("The given string value cannot be transformed to Json object: " + string, e); + throw new IllegalArgumentException(errorMsg, e); } } @@ -238,6 +243,14 @@ public class JacksonUtil { } } + public static T treeToValue(JsonNode node, TypeReference type) { + try { + return OBJECT_MAPPER.treeToValue(node, type); + } catch (IOException e) { + throw new IllegalArgumentException("Can't convert value: " + node.toString(), e); + } + } + public static JsonNode toJsonNode(String value) { return toJsonNode(value, OBJECT_MAPPER); } diff --git a/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java b/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java index 1baff75a79..bcf028abf7 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java @@ -61,6 +61,37 @@ public class KvUtil { } } + public static Long getLongValue(KvEntry entry) { + switch (entry.getDataType()) { + case LONG -> { + return entry.getLongValue().orElse(null); + } + case DOUBLE -> { + return entry.getDoubleValue().map(Double::longValue).orElse(null); + } + case BOOLEAN -> { + return entry.getBooleanValue().map(b -> b ? 1L : 0L).orElse(null); + } + case STRING -> { + try { + return Long.parseLong(entry.getStrValue().orElse("")); + } catch (RuntimeException e) { + return null; + } + } + case JSON -> { + try { + return Long.parseLong(entry.getJsonValue().orElse("")); + } catch (RuntimeException e) { + return null; + } + } + default -> { + return null; + } + } + } + public static Boolean getBoolValue(KvEntry entry) { switch (entry.getDataType()) { case LONG: diff --git a/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java b/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java new file mode 100644 index 0000000000..6681557c87 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class NumberUtils { + + public static boolean isNaN(double value) { + return Double.isNaN(value); + } + + public static double toFixed(double value, int precision) { + return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue(); + } + + public static float toFixed(float value, int precision) { + return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue(); + } + + public static int toInt(double value) { + return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); + } + + public static Object roundResult(double value, Integer precision) { + if (precision == null) { + return value; + } + if (precision.equals(0)) { + return toInt(value); + } + return toFixed(value, precision); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java index a6218b2eb8..272e44ba7e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java @@ -33,9 +33,7 @@ public class ThingsBoardThreadFactory implements ThreadFactory { } private ThingsBoardThreadFactory(String name) { - SecurityManager s = System.getSecurityManager(); - group = (s != null) ? s.getThreadGroup() : - Thread.currentThread().getThreadGroup(); + group = Thread.currentThread().getThreadGroup(); namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-"; diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java new file mode 100644 index 0000000000..15b91f5a2e --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util.geo; + +import lombok.Data; + +@Data +public class CirclePerimeterDefinition implements PerimeterDefinition { + + private final Double latitude; + private final Double longitude; + private final Double radius; + + @Override + public PerimeterType getType() { + return PerimeterType.CIRCLE; + } + + @Override + public boolean checkMatches(Coordinates entityCoordinates) { + Coordinates perimeterCoordinates = new Coordinates(latitude, longitude); + return radius > GeoUtil.distance(entityCoordinates, perimeterCoordinates, RangeUnit.METER); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java new file mode 100644 index 0000000000..218596b543 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util.geo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = PerimeterDefinitionDeserializer.class) +@JsonSerialize(using = PerimeterDefinitionSerializer.class) +public interface PerimeterDefinition extends Serializable { + + @JsonIgnore + PerimeterType getType(); + + @JsonIgnore + boolean checkMatches(Coordinates entityCoordinates); +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java new file mode 100644 index 0000000000..1c22d6a007 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util.geo; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +public class PerimeterDefinitionDeserializer extends JsonDeserializer { + + @Override + public PerimeterDefinition deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + ObjectCodec codec = p.getCodec(); + JsonNode node = codec.readTree(p); + + if (node.isObject()) { + if (!node.has("latitude") || !node.has("longitude") || !node.has("radius")) { + throw JsonMappingException.from(p, "CirclePerimeterDefinition missing required fields. Received: " + node); + } + double latitude = node.get("latitude").asDouble(); + double longitude = node.get("longitude").asDouble(); + double radius = node.get("radius").asDouble(); + return new CirclePerimeterDefinition(latitude, longitude, radius); + } + if (node.isArray()) { + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + String polygonStrDefinition = mapper.writeValueAsString(node); + return new PolygonPerimeterDefinition(polygonStrDefinition); + } + throw JsonMappingException.from(p, "Unknown JSON format for PerimeterDefinition. Expected OBJECT (Circle) or ARRAY (Polygon), but found: " + node.getNodeType()); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java new file mode 100644 index 0000000000..76b86ee2a4 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util.geo; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.thingsboard.server.common.data.StringUtils; + +import java.io.IOException; + +public class PerimeterDefinitionSerializer extends JsonSerializer { + + @Override + public void serialize(PerimeterDefinition value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value instanceof CirclePerimeterDefinition c) { + gen.writeStartObject(); + gen.writeNumberField("latitude", c.getLatitude()); + gen.writeNumberField("longitude", c.getLongitude()); + gen.writeNumberField("radius", c.getRadius()); + gen.writeEndObject(); + return; + } + if (value instanceof PolygonPerimeterDefinition p) { + String raw = p.getPolygonDefinition(); + if (StringUtils.isBlank(raw)) { + throw new IOException("Failed to serialize PolygonPerimeterDefinition with blank: " + value); + } + ObjectMapper mapper = (ObjectMapper) gen.getCodec(); + gen.writeTree(mapper.readTree(raw)); + return; + } + throw new IOException("Failed to serialize PerimeterDefinition from value: " + value); + } +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java new file mode 100644 index 0000000000..b0720ae04a --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util.geo; + +import lombok.Data; + +@Data +public class PolygonPerimeterDefinition implements PerimeterDefinition { + + private final String polygonDefinition; + + @Override + public PerimeterType getType() { + return PerimeterType.POLYGON; + } + + @Override + public boolean checkMatches(Coordinates entityCoordinates) { + return GeoUtil.contains(polygonDefinition, entityCoordinates); + } + +} diff --git a/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java b/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java new file mode 100644 index 0000000000..1a1bf326e6 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NumberUtilsTest { + + private final Float floatVal = 29.29824f; + private final double doubleVal = 1729.1729; + + @Test + public void isNaN() { + assertThat(NumberUtils.isNaN(doubleVal)).isFalse(); + assertThat(NumberUtils.isNaN(Double.NaN)).isTrue(); + } + + @Test + public void toFixedFloat() { + float actualF = NumberUtils.toFixed(floatVal, 3); + assertThat(Float.compare(floatVal, actualF)).isEqualTo(1); + assertThat(Float.compare(29.298f, actualF)).isEqualTo(0); + } + + @Test + public void toFixedDouble() { + double actualD = NumberUtils.toFixed(doubleVal, 3); + assertThat(Double.compare(doubleVal, actualD)).isEqualTo(-1); + assertThat(Double.compare(1729.173, actualD)).isEqualTo(0); + } + + @Test + public void toInt() { + assertThat(NumberUtils.toInt(doubleVal)).isEqualTo(1729); + assertThat(NumberUtils.toInt(12.8)).isEqualTo(13); + assertThat(NumberUtils.toInt(28.0)).isEqualTo(28); + } + + @Test + public void roundResult() { + assertThat(NumberUtils.roundResult(doubleVal, null)).isEqualTo(1729.1729); + assertThat(NumberUtils.roundResult(doubleVal, 0)).isEqualTo(1729); + assertThat(NumberUtils.roundResult(doubleVal, 2)).isEqualTo(1729.17); + } + +} diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java new file mode 100644 index 0000000000..df5d90336f --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util.geo; + +import com.fasterxml.jackson.databind.JsonMappingException; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PerimeterDefinitionDeserializerTest { + + @Test + void shouldDeserializeCircle() { + String json = """ + {"latitude":50.45,"longitude":30.52,"radius":100.0}"""; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isNotNull().isInstanceOf(CirclePerimeterDefinition.class); + + CirclePerimeterDefinition circle = (CirclePerimeterDefinition) def; + assertThat(circle.getLatitude()).isEqualTo(50.45); + assertThat(circle.getLongitude()).isEqualTo(30.52); + assertThat(circle.getRadius()).isEqualTo(100.0); + } + + @Test + void shouldDeserializePolygon() { + String json = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isInstanceOf(PolygonPerimeterDefinition.class); + PolygonPerimeterDefinition poly = (PolygonPerimeterDefinition) def; + assertThat(poly.getPolygonDefinition()).isEqualTo(json); + } + + @Test + void shouldThrowWhenCircleFieldIsMissing() { + // Missing "radius" + String badJson = """ + { + "latitude": 48.8566, + "longitude": 2.3522 + } + """; + + String customError = "Custom error context"; + + assertThatThrownBy(() -> JacksonUtil.fromString(badJson, PerimeterDefinition.class, customError)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(customError) + .hasCauseInstanceOf(JsonMappingException.class) + .rootCause() + .hasMessageContaining("CirclePerimeterDefinition missing required fields"); + } + + @Test + void shouldThrowWhenJsonIsGarbage() { + String garbageJson = "\"NotAnObjectOrArray\""; + String customError = "Garbage check"; + + assertThatThrownBy(() -> JacksonUtil.fromString(garbageJson, PerimeterDefinition.class, customError)) + .isInstanceOf(IllegalArgumentException.class) + .hasCauseInstanceOf(JsonMappingException.class) + .rootCause() + .hasMessageContaining("Unknown JSON format"); + } + + @Test + void shouldReturnNullWhenInputIsNull() { + //noinspection ConstantConditions + PerimeterDefinition result = JacksonUtil.fromString(null, PerimeterDefinition.class, "Error"); + assertThat(result).isNull(); + } + +} diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java new file mode 100644 index 0000000000..bed15db25b --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util.geo; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PerimeterDefinitionSerializerTest { + + @Test + void shouldSerializeCircle() { + PerimeterDefinition circle = new CirclePerimeterDefinition(50.45, 30.52, 120.0); + + String json = JacksonUtil.writeValueAsString(circle); + + JsonNode actual = JacksonUtil.toJsonNode(json); + assertThat(actual.get("latitude").asDouble()).isEqualTo(50.45); + assertThat(actual.get("longitude").asDouble()).isEqualTo(30.52); + assertThat(actual.get("radius").asDouble()).isEqualTo(120.0); + } + + @Test + void shouldSerializePolygon() throws Exception { + String rawArray = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + PerimeterDefinition polygon = new PolygonPerimeterDefinition(rawArray); + + String json = JacksonUtil.writeValueAsString(polygon); + + JsonNode actual = JacksonUtil.toJsonNode(json); + JsonNode expected = JacksonUtil.toJsonNode(rawArray); + assertThat(actual).isEqualTo(expected); + assertThat(actual.isArray()).isTrue(); + assertThat(actual.size()).isEqualTo(3); + } + +} diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 66c87eb5d1..d9e134128d 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java similarity index 100% rename from application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java rename to common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java diff --git a/application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java similarity index 100% rename from application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java rename to common/version-control/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java diff --git a/dao/pom.xml b/dao/pom.xml index bc172eeb45..d526eeed4f 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard dao diff --git a/dao/src/main/java/org/thingsboard/server/dao/Dao.java b/dao/src/main/java/org/thingsboard/server/dao/Dao.java index 893688a7d1..08561d8b68 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/Dao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/Dao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.id.TenantId; @@ -32,6 +33,10 @@ public interface Dao { ListenableFuture findByIdAsync(TenantId tenantId, UUID id); + default List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + throw new UnsupportedOperationException(); + } + boolean existsById(TenantId tenantId, UUID id); ListenableFuture existsByIdAsync(TenantId tenantId, UUID id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java index 31fa250a2a..35cbbfe71a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java @@ -29,12 +29,15 @@ 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.CachedVersionedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.model.sql.AiModelEntity; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; import java.util.Optional; import java.util.Set; +import java.util.UUID; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -64,11 +67,23 @@ class AiModelServiceImpl extends CachedVersionedEntityService findAiModelByTenantIdAndId(tenantId, modelId))); } + @Override + public Optional findAiModelByTenantIdAndName(TenantId tenantId, String name) { + return Optional.ofNullable(aiModelDao.findByTenantIdAndName(tenantId.getId(), name)); + } + @Override @Transactional public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) { - return deleteByTenantIdAndIdInternal(tenantId, modelId); + return deleteByTenantIdAndIdInternal(tenantId, modelId.getId()); } @Override @@ -130,14 +150,21 @@ class AiModelServiceImpl extends CachedVersionedEntityService, ExportableEntityDao< List findTenantAssetProfileNames(UUID tenantId, boolean activeOnly); + List findAssetProfilesByTenantIdAndIds(UUID tenantId, List assetProfileIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 49b21976f4..829a60621b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -37,7 +37,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -51,6 +51,7 @@ import java.util.Optional; import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AssetProfileDaoService") @@ -345,6 +346,12 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService findAssetProfilesByIds(TenantId tenantId, List assetProfileIds) { + log.trace("Executing findAssetProfilesByIds, tenantId [{}], assetProfileIds [{}]", tenantId, assetProfileIds); + return assetProfileDao.findAssetProfilesByTenantIdAndIds(tenantId.getId(), toUUIDs(assetProfileIds)); + } + private final PaginatedRemover tenantAssetProfilesRemover = new PaginatedRemover<>() { 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 a4e9226a0f..b4ff58120d 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,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; @@ -51,7 +53,7 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.sql.JpaExecutorService; @@ -147,14 +149,24 @@ public class BaseAssetService extends AbstractCachedEntityService saveAsset(asset, true, nameConflictStrategy)); + } + @Override public Asset saveAsset(Asset asset, boolean doValidate) { + return saveEntity(asset, () -> saveAsset(asset, doValidate, NameConflictStrategy.DEFAULT)); + } + + private Asset saveAsset(Asset asset, boolean doValidate, NameConflictStrategy nameConflictStrategy) { log.trace("Executing saveAsset [{}]", asset); - Asset oldAsset = null; + Asset oldAsset = (asset.getId() != null) ? assetDao.findById(asset.getTenantId(), asset.getId().getId()) : null; + if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldAsset == null || !oldAsset.getName().equals(asset.getName()))) { + uniquifyEntityName(asset, oldAsset, asset::setName, EntityType.ASSET, nameConflictStrategy); + } if (doValidate) { - oldAsset = assetValidator.validate(asset, Asset::getTenantId); - } else if (asset.getId() != null) { - oldAsset = findAssetById(asset.getTenantId(), asset.getId()); + assetValidator.validate(asset, Asset::getTenantId); } AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null); Asset savedAsset; diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index d2033d4535..7c563debd6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -53,7 +53,13 @@ public interface AttributesDao { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); - List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List entityIds, String attributeType); + List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + + ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); + + List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + + ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); List> removeAllByEntityId(TenantId tenantId, EntityId entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index a6a052e247..ca4c937d4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -28,7 +28,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.AttributeKv; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -46,9 +45,6 @@ import java.util.Optional; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; -/** - * @author Andrew Shvayka - */ @Service @ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "false", matchIfMissing = true) @Primary @@ -93,14 +89,29 @@ public class BaseAttributesService implements AttributesService { } @Override - public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, String scope) { - if (StringUtils.isEmpty(scope)) { + public List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + if (scope == null) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); } else { - return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope); + return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope); } } + @Override + public ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + + @Override + public List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScope(tenantId, entityIds, scope); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index cc36c1f76e..1c99dfc495 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -66,6 +66,7 @@ import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; @Primary @Slf4j public class CachedAttributesService implements AttributesService { + private static final String STATS_NAME = "attributes.cache"; public static final String LOCAL_CACHE_TYPE = "caffeine"; @@ -212,14 +213,29 @@ public class CachedAttributesService implements AttributesService { } @Override - public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, String scope) { - if (StringUtils.isEmpty(scope)) { + public List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + if (scope == null) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); } else { - return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope); + return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope); } } + @Override + public ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + + @Override + public List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScope(tenantId, entityIds, scope); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); 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 27a52e1cc7..8aef814657 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 @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -117,9 +118,9 @@ public class AuditLogServiceImpl implements AuditLogService { @Override public ListenableFuture - logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, + logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, @NotNull I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) { - if (canLog(entityId.getEntityType(), actionType)) { + if (canLog(entityId.getEntityType(), actionType) || (tenantId != null && tenantId.isSysTenantId())) { JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo); ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 76b8158074..075e2d8a9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -19,25 +19,35 @@ import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; -import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -52,19 +62,19 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements public static final String INCORRECT_CALCULATED_FIELD_ID = "Incorrect calculatedFieldId "; public static final String INCORRECT_ENTITY_ID = "Incorrect entityId "; + private final EntityService entityService; private final CalculatedFieldDao calculatedFieldDao; - private final CalculatedFieldLinkDao calculatedFieldLinkDao; - private final DataValidator calculatedFieldDataValidator; - private final DataValidator calculatedFieldLinkDataValidator; + private final CalculatedFieldDataValidator calculatedFieldDataValidator; + private final ApiLimitService apiLimitService; @Override public CalculatedField save(CalculatedField calculatedField) { - CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); - return doSave(calculatedField, oldCalculatedField); + return save(calculatedField, true); } @Override public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { + setConfigurationDefaults(calculatedField); CalculatedField oldCalculatedField = null; if (doValidate) { oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); @@ -74,6 +84,15 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return doSave(calculatedField, oldCalculatedField); } + private void setConfigurationDefaults(CalculatedField calculatedField) { + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config + && config.getScheduledUpdateInterval() == null) { + int minScheduledUpdateInterval = (int) apiLimitService.getLimit( + calculatedField.getTenantId(), DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF + ); + config.setScheduledUpdateInterval(minScheduledUpdateInterval); + } + } private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { @@ -81,14 +100,19 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements log.trace("Executing save calculated field, [{}]", calculatedField); updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis()); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); - createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(savedCalculatedField.getTenantId()) + .entityId(savedCalculatedField.getId()) + .entity(savedCalculatedField) + .oldEntity(oldCalculatedField) + .created(calculatedField.getId() == null) + .build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, - "calculated_field_unq_key", "Calculated Field with such name is already in exists!", - "calculated_field_external_id_unq_key", "Calculated Field with such external id already exists!"); + "calculated_field_unq_key", calculatedField.getType() == CalculatedFieldType.ALARM ? + "Alarm rule with such type already exists" : "Calculated field with such name and type already exists", + "calculated_field_external_id_unq_key", "Calculated field with such external id already exists"); throw e; } } @@ -102,10 +126,10 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { - log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); + public CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name) { + log.trace("Executing findByEntityIdAndTypeAndName entityId [{}], type [{}], name [{}]", entityId, type, name); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByEntityIdAndName(entityId, name); + return calculatedFieldDao.findByEntityIdAndTypeAndName(entityId, type, name); } @Override @@ -138,11 +162,36 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public PageData findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { + public PageData findCalculatedFieldsByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink) { + PageData calculatedFields = calculatedFieldDao.findByTenantIdAndFilter(tenantId, filter, pageLink); + Set entityIds = calculatedFields.getData().stream() + .map(CalculatedField::getEntityId) + .collect(Collectors.toSet()); + Map entityInfos = entityService.fetchEntityInfos(tenantId, null, entityIds); + return calculatedFields.mapData(calculatedField -> { + EntityInfo entityInfo = entityInfos.get(calculatedField.getEntityId()); + return new CalculatedFieldInfo(calculatedField, entityInfo.getName()); + }); + } + + @Override + public PageData findCalculatedFieldNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink) { + return calculatedFieldDao.findNamesByTenantIdAndType(tenantId, type, pageLink); + } + + @Override + public PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink) { log.trace("Executing findAllByEntityId, entityId [{}], pageLink [{}]", entityId, pageLink); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); validatePageLink(pageLink); - return calculatedFieldDao.findAllByEntityId(tenantId, entityId, pageLink); + Set types; + if (type == null) { + types = EnumSet.allOf(CalculatedFieldType.class); + types.remove(CalculatedFieldType.ALARM); + } else { + types = Set.of(type); + } + return calculatedFieldDao.findByEntityIdAndTypes(tenantId, entityId, types, pageLink); } @Override @@ -180,48 +229,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFields.size(); } - @Override - public CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - calculatedFieldLinkDataValidator.validate(calculatedFieldLink, CalculatedFieldLink::getTenantId); - log.trace("Executing save calculated field link, [{}]", calculatedFieldLink); - return calculatedFieldLinkDao.save(tenantId, calculatedFieldLink); - } - - @Override - public CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) { - log.trace("Executing findCalculatedFieldLinkById, tenantId [{}], calculatedFieldLinkId [{}]", tenantId, calculatedFieldLinkId); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id); - return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId()); - } - - @Override - public List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - log.trace("Executing findAllCalculatedFieldLinksById, calculatedFieldId [{}]", calculatedFieldId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByCalculatedFieldId(tenantId, calculatedFieldId); - } - - @Override - public List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - log.trace("Executing findAllCalculatedFieldLinksByEntityId, entityId [{}]", entityId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId); - } - - @Override - public PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinksByTenantId, tenantId[{}] pageLink [{}]", tenantId, pageLink); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAllByTenantId(tenantId, pageLink); - } - - @Override - public PageData findAllCalculatedFieldLinks(PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAll(pageLink); - } - @Override public boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId) { return calculatedFieldDao.findAllByTenantId(tenantId).stream() @@ -247,9 +254,4 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return EntityType.CALCULATED_FIELD; } - private void createOrUpdateCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) { - List links = calculatedField.getConfiguration().buildCalculatedFieldLinks(tenantId, calculatedField.getEntityId(), calculatedField.getId()); - links.forEach(link -> saveCalculatedFieldLink(tenantId, link)); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index 633b77407b..8da1697c1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -24,6 +26,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; +import java.util.Set; public interface CalculatedFieldDao extends Dao { @@ -35,16 +38,20 @@ public interface CalculatedFieldDao extends Dao { List findAll(); - CalculatedField findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name); PageData findAll(PageLink pageLink); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); + PageData findByEntityIdAndTypes(TenantId tenantId, EntityId entityId, Set types, PageLink pageLink); List removeAllByEntityId(TenantId tenantId, EntityId entityId); - long countCFByEntityId(TenantId tenantId, EntityId entityId); + long countByEntityIdAndTypeNot(TenantId tenantId, EntityId entityId, CalculatedFieldType type); + + PageData findByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink); + + PageData findNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink); } 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 ccf145be21..5e9292af5f 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 @@ -30,7 +30,7 @@ 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.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; 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 551a9139b7..2505121448 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 @@ -24,6 +24,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -78,4 +79,6 @@ public interface CustomerDao extends Dao, TenantEntityDao, E */ PageData findCustomersWithTheSameTitle(PageLink pageLink); + List findCustomersByTenantIdAndIds(UUID tenantId, List customerIds); + } 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 0d06a51c97..f872a823e1 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 @@ -29,6 +29,8 @@ import org.thingsboard.server.cache.customer.CustomerCacheEvictEvent; import org.thingsboard.server.cache.customer.CustomerCacheKey; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -43,7 +45,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -55,6 +57,7 @@ import org.thingsboard.server.dao.user.UserService; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -141,19 +144,29 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true, nameConflictStrategy)); } private Customer saveCustomer(Customer customer, boolean doValidate) { + return saveCustomer(customer, doValidate, NameConflictStrategy.DEFAULT); + } + + private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) { log.trace("Executing saveCustomer [{}]", customer); - String oldCustomerTitle = null; + Customer oldCustomer = (customer.getId() != null) ? customerDao.findById(customer.getTenantId(), customer.getId().getId()) : null; + if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldCustomer == null || !oldCustomer.getTitle().equals(customer.getTitle()))) { + uniquifyEntityName(customer, oldCustomer, customer::setTitle, EntityType.CUSTOMER, nameConflictStrategy); + } if (doValidate) { - Customer oldCustomer = customerValidator.validate(customer, Customer::getTenantId); - if (oldCustomer != null) { - oldCustomerTitle = oldCustomer.getTitle(); - } + customerValidator.validate(customer, Customer::getTenantId); } - var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomerTitle); + var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomer != null ? oldCustomer.getTitle() : null); try { Customer savedCustomer = customerDao.saveAndFlush(customer.getTenantId(), customer); if (!savedCustomer.isPublic()) { @@ -163,8 +176,13 @@ public class CustomerServiceImpl extends AbstractCachedEntityService findCustomersByTenantIdAndIds(TenantId tenantId, List customerIds) { + log.trace("Executing findCustomersByTenantIdAndIds, tenantId [{}], customerIds [{}]", tenantId, customerIds); + return customerDao.findCustomersByTenantIdAndIds(tenantId.getId(), customerIds.stream().map(CustomerId::getId).collect(Collectors.toList())); + } + @Override public void deleteByTenantId(TenantId tenantId) { deleteCustomersByTenantId(tenantId); 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 578f7fd19d..b47e87ae4d 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ImageContainerDao; import org.thingsboard.server.dao.ResourceContainerDao; +import java.util.List; import java.util.UUID; /** @@ -81,4 +82,6 @@ public interface DashboardInfoDao extends Dao, ImageContainerDao< String findTitleById(UUID tenantId, UUID dashboardId); + List findDashboardsByIds(UUID tenantId, List dashboardIds); + } 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 de0168514d..acbc9d724d 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 @@ -50,7 +50,7 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.service.DataValidator; @@ -63,6 +63,7 @@ import java.util.Map; import java.util.Optional; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("DashboardDaoService") @@ -160,6 +161,10 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) { + return saveEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); + } + + private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) { log.trace("Executing saveDashboard [{}]", dashboard); if (doValidate) { dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); @@ -409,6 +414,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return dashboardDao.findAllIds(pageLink); } + @Override + public List findDashboardInfoByIds(TenantId tenantId, List dashboardIds) { + log.trace("Executing findDashboardInfoByIds, dashboardIds [{}]", dashboardIds); + return dashboardInfoDao.findDashboardsByIds(tenantId.getId(), toUUIDs(dashboardIds)); + } + private final PaginatedRemover tenantDashboardsRemover = new PaginatedRemover<>() { @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index 89cb538c28..70f5f48272 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -85,7 +85,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService private String mqttsPemCertFile; @Value("${device.connectivity.coaps.pem_cert_file:}") private String coapsPemCertFile; - @Value("${device.connectivity.gateway.image_version:3.7-stable}") + @Value("${device.connectivity.gateway.image_version:3.8-stable}") private String gatewayImageVersion; @Override 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 37fc1be448..899df92a77 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 @@ -43,9 +43,9 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; +import org.thingsboard.server.exception.DataValidationException; import java.util.Objects; 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 index 045ddd1002..567abf7e79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -53,4 +53,6 @@ public interface DeviceProfileDao extends Dao, ExportableEntityDa List findTenantDeviceProfileNames(UUID tenantId, boolean activeOnly); + List findDeviceProfilesByTenantIdAndIds(UUID tenantId, List deviceProfileIds); + } 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 index 6bb341df03..cfaf76f05c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -46,11 +46,11 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.DeviceProfileDataValidator; +import org.thingsboard.server.exception.DataValidationException; import java.io.ByteArrayInputStream; import java.security.cert.Certificate; @@ -66,6 +66,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -391,6 +392,12 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService findDeviceProfilesByIds(TenantId tenantId, List deviceProfileIds) { + log.trace("Executing findDeviceProfilesByIds, tenantId [{}], deviceProfileIds [{}]", tenantId, deviceProfileIds); + return deviceProfileDao.findDeviceProfilesByTenantIdAndIds(tenantId.getId(), toUUIDs(deviceProfileIds)); + } + private final PaginatedRemover tenantDeviceProfilesRemover = new PaginatedRemover<>() { 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 3f58d01722..b83f7b3ef7 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 @@ -40,6 +40,8 @@ import org.thingsboard.server.common.data.DeviceTransportType; 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.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; @@ -78,7 +80,7 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.validator.DeviceDataValidator; @@ -167,6 +169,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService doSaveWithCredentials(device, deviceCredentials, nameConflictStrategy)); + } + + private Device doSaveWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy) { + Device savedDevice = doSaveDeviceWithoutCredentials(device, true, nameConflictStrategy); deviceCredentials.setDeviceId(savedDevice.getId()); if (device.getId() == null) { deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); @@ -198,7 +216,15 @@ public class DeviceServiceImpl extends CachedVersionedEntityService saveWithoutCredentials(device, accessToken, doValidate, nameConflictStrategy)); + } + + private Device saveWithoutCredentials(Device device, String accessToken, boolean doValidate, NameConflictStrategy nameConflictStrategy) { + Device savedDevice = doSaveDeviceWithoutCredentials(device, doValidate, nameConflictStrategy); if (device.getId() == null) { DeviceCredentials deviceCredentials = new DeviceCredentials(); deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); @@ -209,13 +235,14 @@ public class DeviceServiceImpl extends CachedVersionedEntityService doSaveEdge(edge)); + } + + private Edge doSaveEdge(Edge edge) { log.trace("Executing saveEdge [{}]", edge); Edge oldEdge = edgeValidator.validate(edge, Edge::getTenantId); EdgeCacheEvictEvent evictEvent = new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), oldEdge != null ? oldEdge.getName() : null); @@ -235,9 +239,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService() { @Override public void onSuccess(Void result) { - statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); + statsCounterService.ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); eventPublisher.publishEvent(SaveEntityEvent.builder() .tenantId(edgeEvent.getTenantId()) .entityId(edgeEvent.getEdgeId()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java index 118978dbe0..e85c78646f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java @@ -24,13 +24,13 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.concurrent.ConcurrentHashMap; -@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true") @Service @Slf4j @Getter public class EdgeStatsCounterService { - private final ConcurrentHashMap counterByEdge = new ConcurrentHashMap<>(); + private final ConcurrentHashMap msgCountersByEdge = new ConcurrentHashMap<>(); public void recordEvent(EdgeStatsKey type, TenantId tenantId, EdgeId edgeId, long value) { MsgCounters counters = getOrCreateCounters(tenantId, edgeId); @@ -39,19 +39,16 @@ public class EdgeStatsCounterService { case DOWNLINK_MSGS_PUSHED -> counters.getMsgsPushed().addAndGet(value); case DOWNLINK_MSGS_PERMANENTLY_FAILED -> counters.getMsgsPermanentlyFailed().addAndGet(value); case DOWNLINK_MSGS_TMP_FAILED -> counters.getMsgsTmpFailed().addAndGet(value); + case DOWNLINK_MSGS_LAG -> counters.getMsgsLag().set(value); } } - public void setDownlinkMsgsLag(TenantId tenantId, EdgeId edgeId, long value) { - getOrCreateCounters(tenantId, edgeId).getMsgsLag().set(value); + public MsgCounters getOrCreateCounters(TenantId tenantId, EdgeId edgeId) { + return msgCountersByEdge.computeIfAbsent(edgeId, id -> new MsgCounters(tenantId)); } public void clear(EdgeId edgeId) { - counterByEdge.remove(edgeId); - } - - public MsgCounters getOrCreateCounters(TenantId tenantId, EdgeId edgeId) { - return counterByEdge.computeIfAbsent(edgeId, id -> new MsgCounters(tenantId)); + msgCountersByEdge.remove(edgeId); } } 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 45453d6f7f..95575f26f8 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 @@ -21,21 +21,30 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.DebugModeUtil; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.EdgeId; 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.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.housekeeper.CleanUpService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -44,7 +53,15 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentMap; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.UniquifyStrategy.RANDOM; @Slf4j public abstract class AbstractEntityService { @@ -52,6 +69,8 @@ public abstract class AbstractEntityService { public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + private final ConcurrentMap entityCreationLocks = new ConcurrentReferenceHashMap<>(16); + @Autowired protected ApplicationEventPublisher eventPublisher; @@ -81,11 +100,28 @@ public abstract class AbstractEntityService { @Autowired @Lazy - private TbTenantProfileCache tbTenantProfileCache; + protected TbTenantProfileCache tbTenantProfileCache; + + @Autowired + protected EntityDaoRegistry entityDaoRegistry; @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + protected E saveEntity(E entity, Supplier saveFunction) { + if (entity.getId() == null) { + ReentrantLock lock = entityCreationLocks.computeIfAbsent(entity.getTenantId(), id -> new ReentrantLock()); + lock.lock(); + try { + return saveFunction.get(); + } finally { + lock.unlock(); + } + } else { + return saveFunction.get(); + } + } + protected void createRelation(TenantId tenantId, EntityRelation relation) { log.debug("Creating relation: {}", relation); relationService.saveRelation(tenantId, relation); @@ -155,4 +191,33 @@ public abstract class AbstractEntityService { private long getMaxDebugAllUntil(TenantId tenantId, long now) { return now + TimeUnit.MINUTES.toMillis(DebugModeUtil.getMaxDebugAllDuration(tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxDebugModeDurationMinutes(), defaultDebugDurationMinutes)); } + + protected & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer setName, EntityType entityType, NameConflictStrategy strategy) { + Dao dao = entityDaoRegistry.getDao(entityType); + List existingEntities = dao.findEntityInfosByNamePrefix(entity.getTenantId(), entity.getName()); + Set existingNames = existingEntities.stream() + .filter(e -> (oldEntity == null || !e.getId().equals(oldEntity.getId()))) + .map(EntityInfo::getName) + .collect(Collectors.toSet()); + + if (existingNames.contains(entity.getName())) { + String uniqueName = generateUniqueName(entity.getName(), existingNames, strategy); + setName.accept(uniqueName); + } + } + + private String generateUniqueName(String baseName, Set existingNames, NameConflictStrategy strategy) { + String newName; + int index = 1; + String separator = strategy.separator(); + boolean isRandom = strategy.uniquifyStrategy() == RANDOM; + + do { + String suffix = isRandom ? StringUtils.randomAlphanumeric(6) : String.valueOf(index++); + newName = baseName + separator + suffix; + } while (existingNames.contains(newName)); + + return newName; + } + } 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 dc950cebd4..be49e64d10 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 @@ -16,8 +16,12 @@ package org.thingsboard.server.dao.entity; import com.google.common.util.concurrent.FluentFuture; +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.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -37,6 +41,7 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -55,6 +60,7 @@ import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.sql.JpaExecutorService; import java.util.ArrayList; import java.util.Collections; @@ -87,6 +93,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe private static final Set EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of( EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY); + @Value("${sql.query.key-filters-or-conditions.enabled:true}") + private boolean keyFiltersOrConditionsEnabled; + @Autowired private EntityQueryDao entityQueryDao; @@ -104,6 +113,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Autowired private EdqsStatsService edqsStatsService; + @Autowired + private JpaExecutorService jpaExecutorService; + @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); @@ -142,41 +154,78 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe EdqsResponse response = processEdqsRequest(tenantId, customerId, request); result = response.getEntityDataQueryResult(); } else { - if (!isValidForOptimization(query)) { - result = entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); - } else { - // 1 step - find entity data by filter and sort columns - PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); - if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { - result = entityDataByQuery; - } else { - // 2 step - find entity data by entity ids from the 1st step - List entities = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); - result = new PageData<>(entities, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); - } - } + result = findEntityDataByQueryInternal(tenantId, customerId, query); } edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); return result; } + @Override + public ListenableFuture> findEntityDataByQueryAsync(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + log.trace("Executing findEntityDataByQueryAsync, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); + + try { + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateEntityDataQuery(query); + } catch (Exception e) { + return Futures.immediateFailedFuture(e); + } + + if (edqsService.isApiEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { + EdqsRequest request = EdqsRequest.builder() + .entityDataQuery(query) + .build(); + long startNs = System.nanoTime(); + return Futures.transform(processEdqsRequestAsync(tenantId, customerId, request), response -> { + edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); + return response.getEntityDataQueryResult(); + }, MoreExecutors.directExecutor()); + } + + return jpaExecutorService.submit(() -> { + long startNs = System.nanoTime(); + PageData result = findEntityDataByQueryInternal(tenantId, customerId, query); + edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); + return result; + }); + } + + private PageData findEntityDataByQueryInternal(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + if (!isValidForOptimization(query)) { + return entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); + } + // 1 step - find entity data by filter and sort columns + PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); + if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { + return entityDataByQuery; + } + // 2 step - find entity data by entity ids from the 1st step + List entities = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); + return new PageData<>(entities, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); + } + private boolean validForEdqs(EntityCountQuery query) { // for compatibility with PE return true; } private EdqsResponse processEdqsRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { - EdqsResponse response; try { - log.debug("[{}] Sending request to EDQS: {}", tenantId, request); - response = edqsApiService.processRequest(tenantId, customerId, request).get(); + return processEdqsRequestAsync(tenantId, customerId, request).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } - log.debug("[{}] Received response from EDQS: {}", tenantId, response); - if (response.getError() != null) { - throw new RuntimeException(response.getError()); - } - return response; + } + + private ListenableFuture processEdqsRequestAsync(TenantId tenantId, CustomerId customerId, EdqsRequest request) { + log.debug("[{}] Sending request to EDQS: {}", tenantId, request); + return Futures.transform(edqsApiService.processRequest(tenantId, customerId, request), response -> { + log.debug("[{}] Received response from EDQS: {}", tenantId, response); + if (response.getError() != null) { + throw new RuntimeException(response.getError()); + } + return response; + }, MoreExecutors.directExecutor()); } @Override @@ -284,7 +333,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return new NameLabelAndCustomerDetails(getName(entity), getLabel(entity), getCustomerId(entity)); } - private static void validateEntityCountQuery(EntityCountQuery query) { + private void validateEntityCountQuery(EntityCountQuery query) { if (query == null) { throw new IncorrectParameterException("Query must be specified."); } else if (query.getEntityFilter() == null) { @@ -298,9 +347,14 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } else if (query.getEntityFilter().getType().equals(ENTITY_NAME)) { validateEntityNameQuery((EntityNameFilter) query.getEntityFilter()); } + // Intentionally using the nullable getKeyFiltersOperation() (not getKeyFiltersOperationOrDefault()): + // a null value encodes "classic AND" and must pass this guard even when the OR feature flag is off. + if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) { + throw new IncorrectParameterException("OR conditions between key filters are disabled by the system administrator."); + } } - private static void validateEntityDataQuery(EntityDataQuery query) { + private void validateEntityDataQuery(EntityDataQuery query) { validateEntityCountQuery(query); validateEntityDataPageLink(query.getPageLink()); } @@ -365,7 +419,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe .collect(Collectors.toList()); } } - EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters()); + EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters(), query.getKeyFiltersOperation()); return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java index de8374d211..0516294d79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java @@ -43,9 +43,6 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry { if (EntityType.RULE_CHAIN.equals(entityType)) { entityDaoServicesMap.put(EntityType.RULE_NODE, entityDaoService); } - if (EntityType.CALCULATED_FIELD.equals(entityType)) { - entityDaoServicesMap.put(EntityType.CALCULATED_FIELD_LINK, entityDaoService); - } }); log.debug("Initialized EntityServiceRegistry total [{}] entries", entityDaoServicesMap.size()); } 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 69f41104de..5a43cb8a05 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 @@ -169,6 +169,8 @@ public interface EntityViewDao extends Dao, ExportableEntityDao findEntityViewsByTenantIdAndIds(UUID tenantId, List entityViewIds); + /** * Find entity views by tenantId, edgeId, type and page link. * 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 5fc2d25811..7867801107 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 @@ -30,6 +30,8 @@ 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.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -49,7 +51,7 @@ import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.validator.EntityViewDataValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; @@ -62,6 +64,7 @@ import java.util.Optional; import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -109,14 +112,24 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService findEntityViewsByTenantIdAndIds(TenantId tenantId, List entityViewIds) { + log.trace("Executing findEntityViewsByTenantIdAndIds, tenantId [{}], entityViewIds [{}]", tenantId, entityViewIds); + return entityViewDao.findEntityViewsByTenantIdAndIds(tenantId.getId(), toUUIDs(entityViewIds)); + } + @Override public PageData findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findEntityViewInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}]," + diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java index 77ca4a0da2..0d2549e67f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java @@ -15,8 +15,12 @@ */ package org.thingsboard.server.dao.exception; +import org.thingsboard.server.exception.DataValidationException; + public class DeviceCredentialsValidationException extends DataValidationException { + public DeviceCredentialsValidationException(String message) { super(message); } + } 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 9594e2624e..0edc757be1 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 @@ -36,9 +36,6 @@ import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; -/** - * Created by ashvayka on 13.07.17. - */ @Data @MappedSuperclass public abstract class BaseSqlEntity implements BaseEntity { 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 de1a04e81e..6f971e61f7 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 @@ -26,8 +26,7 @@ import java.util.UUID; public class ModelConstants { - private ModelConstants() { - } + private ModelConstants() {} public static final UUID NULL_UUID = Uuids.startOf(0); public static final TenantId SYSTEM_TENANT = TenantId.fromUUID(ModelConstants.NULL_UUID); @@ -258,6 +257,7 @@ public class ModelConstants { public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; public static final String ALARM_ORIGINATOR_LABEL_PROPERTY = "originator_label"; public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; + public static final String ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY = "originator_display_name"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; public static final String ALARM_ASSIGNEE_ID_PROPERTY = "assignee_id"; public static final String ALARM_ASSIGNEE_FIRST_NAME_PROPERTY = "assignee_first_name"; @@ -420,6 +420,7 @@ public class ModelConstants { public static final String RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY = "first_rule_node_id"; public static final String RULE_CHAIN_ROOT_PROPERTY = "root"; public static final String RULE_CHAIN_CONFIGURATION_PROPERTY = "configuration"; + public static final String RULE_CHAIN_NOTES_PROPERTY = "notes"; /** * Rule node constants. @@ -730,15 +731,7 @@ public class ModelConstants { public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version"; public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_VERSION = "version"; - - /** - * Calculated field links constants. - */ - public static final String CALCULATED_FIELD_LINK_TABLE_NAME = "calculated_field_link"; - public static final String CALCULATED_FIELD_LINK_TENANT_ID_COLUMN = TENANT_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_TYPE = ENTITY_TYPE_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; + public static final String CALCULATED_FIELD_ADDITIONAL_INFO = ADDITIONAL_INFO_PROPERTY; /** * Tasks constants. @@ -760,6 +753,17 @@ public class ModelConstants { public static final String AI_MODEL_NAME_COLUMN_NAME = NAME_PROPERTY; public static final String AI_MODEL_CONFIGURATION_COLUMN_NAME = "configuration"; + /** + * Api Key constants. + */ + public static final String API_KEY_TABLE_NAME = "api_key"; + public static final String API_KEY_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; + public static final String API_KEY_USER_ID_COLUMN_NAME = USER_ID_PROPERTY; + public static final String API_KEY_VALUE_COLUMN_NAME = "value"; + public static final String API_KEY_EXPIRATION_TIME_COLUMN_NAME = "expiration_time"; + public static final String API_KEY_ENABLED_COLUMN_NAME = "enabled"; + public static final String API_KEY_DESCRIPTION_COLUMN_NAME = "description"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractApiKeyInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractApiKeyInfoEntity.java new file mode 100644 index 0000000000..6fe6ce625f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractApiKeyInfoEntity.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.ApiKeyId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_DESCRIPTION_COLUMN_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_ENABLED_COLUMN_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_EXPIRATION_TIME_COLUMN_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TENANT_ID_COLUMN_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_USER_ID_COLUMN_NAME; + +@Data +@EqualsAndHashCode(callSuper = true) +@MappedSuperclass +public abstract class AbstractApiKeyInfoEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = API_KEY_TENANT_ID_COLUMN_NAME) + private UUID tenantId; + + @Column(name = API_KEY_USER_ID_COLUMN_NAME) + private UUID userId; + + @Column(name = API_KEY_EXPIRATION_TIME_COLUMN_NAME) + private long expirationTime; + + @Column(name = API_KEY_ENABLED_COLUMN_NAME) + private boolean enabled; + + @Column(name = API_KEY_DESCRIPTION_COLUMN_NAME) + private String description; + + public AbstractApiKeyInfoEntity() { + super(); + } + + public AbstractApiKeyInfoEntity(ApiKeyInfo apiKeyInfo) { + super(apiKeyInfo); + this.tenantId = apiKeyInfo.getTenantId().getId(); + this.userId = apiKeyInfo.getUserId().getId(); + this.expirationTime = apiKeyInfo.getExpirationTime(); + this.description = apiKeyInfo.getDescription(); + this.enabled = apiKeyInfo.isEnabled(); + } + + protected ApiKeyInfo toApiKeyInfo() { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(new ApiKeyId(getUuid())); + apiKeyInfo.setCreatedTime(createdTime); + apiKeyInfo.setTenantId(TenantId.fromUUID(tenantId)); + apiKeyInfo.setUserId(new UserId(userId)); + apiKeyInfo.setEnabled(enabled); + apiKeyInfo.setExpirationTime(expirationTime); + apiKeyInfo.setDescription(description); + return apiKeyInfo; + } + +} 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 index d0b1dfaae7..9641054070 100644 --- 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 @@ -40,10 +40,6 @@ 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) @MappedSuperclass diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractRuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractRuleChainEntity.java new file mode 100644 index 0000000000..71b729ae76 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractRuleChainEntity.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; +import lombok.EqualsAndHashCode; +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.common.data.rule.RuleChainType; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.BaseVersionedEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@MappedSuperclass +public abstract class AbstractRuleChainEntity extends BaseVersionedEntity { + + @Column(name = ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) + private String name; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) + private RuleChainType type; + + @Column(name = ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) + private UUID firstRuleNodeId; + + @Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY) + private boolean root; + + @Column(name = ModelConstants.DEBUG_MODE) + private boolean debugMode; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY) + private JsonNode configuration; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + + public AbstractRuleChainEntity() { + super(); + } + + public AbstractRuleChainEntity(T ruleChain) { + super(ruleChain); + this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); + this.name = ruleChain.getName(); + this.type = ruleChain.getType(); + if (ruleChain.getFirstRuleNodeId() != null) { + this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); + } + this.root = ruleChain.isRoot(); + this.debugMode = ruleChain.isDebugMode(); + this.configuration = ruleChain.getConfiguration(); + this.additionalInfo = ruleChain.getAdditionalInfo(); + if (ruleChain.getExternalId() != null) { + this.externalId = ruleChain.getExternalId().getId(); + } + } + + public AbstractRuleChainEntity(AbstractRuleChainEntity ruleChainEntity) { + super(ruleChainEntity); + this.tenantId = ruleChainEntity.getTenantId(); + this.name = ruleChainEntity.getName(); + this.type = ruleChainEntity.getType(); + this.firstRuleNodeId = ruleChainEntity.getFirstRuleNodeId(); + this.root = ruleChainEntity.isRoot(); + this.debugMode = ruleChainEntity.isDebugMode(); + this.configuration = ruleChainEntity.getConfiguration(); + this.additionalInfo = ruleChainEntity.getAdditionalInfo(); + this.externalId = ruleChainEntity.getExternalId(); + } + + protected RuleChain toRuleChain() { + RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); + ruleChain.setCreatedTime(createdTime); + ruleChain.setVersion(version); + ruleChain.setTenantId(TenantId.fromUUID(tenantId)); + ruleChain.setName(name); + ruleChain.setType(type); + if (firstRuleNodeId != null) { + ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); + } + ruleChain.setRoot(root); + ruleChain.setDebugMode(debugMode); + ruleChain.setConfiguration(configuration); + ruleChain.setAdditionalInfo(additionalInfo); + if (externalId != null) { + ruleChain.setExternalId(new RuleChainId(externalId)); + } + return ruleChain; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiKeyEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiKeyEntity.java new file mode 100644 index 0000000000..47a12e9a71 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiKeyEntity.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.pat.ApiKey; + +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TABLE_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_VALUE_COLUMN_NAME; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = API_KEY_TABLE_NAME) +public class ApiKeyEntity extends AbstractApiKeyInfoEntity { + + @Column(name = API_KEY_VALUE_COLUMN_NAME) + private String value; + + public ApiKeyEntity() { + super(); + } + + public ApiKeyEntity(ApiKey apiKey) { + super(apiKey); + this.value = apiKey.getValue(); + } + + @Override + public ApiKey toData() { + return new ApiKey(super.toApiKeyInfo(), value); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiKeyInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiKeyInfoEntity.java new file mode 100644 index 0000000000..8889d0d2f1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiKeyInfoEntity.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; + +import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TABLE_NAME; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = API_KEY_TABLE_NAME) +public class ApiKeyInfoEntity extends AbstractApiKeyInfoEntity { + + public ApiKeyInfoEntity() { + super(); + } + + public ApiKeyInfoEntity(ApiKey apiKey) { + super(apiKey); + } + + @Override + public ApiKeyInfo toData() { + return super.toApiKeyInfo(); + } + +} 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 index aba85d75a2..93c7ee4e8f 100644 --- 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 @@ -33,9 +33,6 @@ import org.thingsboard.server.dao.model.ModelConstants; import java.util.UUID; -/** - * Created by Valerii Sosliuk on 4/21/2017. - */ @Data @EqualsAndHashCode(callSuper = true) @Entity diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java index 5a507b7fd3..47c1520ac0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ADDITIONAL_INFO; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID; @@ -81,9 +82,11 @@ public class CalculatedFieldEntity extends BaseVersionedEntity @Column(name = DEBUG_SETTINGS) private String debugSettings; - public CalculatedFieldEntity() { - super(); - } + @Convert(converter = JsonConverter.class) + @Column(name = CALCULATED_FIELD_ADDITIONAL_INFO) + private JsonNode additionalInfo; + + public CalculatedFieldEntity() {} public CalculatedFieldEntity(CalculatedField calculatedField) { this.setUuid(calculatedField.getUuidId()); @@ -97,6 +100,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration()); this.version = calculatedField.getVersion(); this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings()); + this.additionalInfo = calculatedField.getAdditionalInfo(); } @Override @@ -111,6 +115,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); + calculatedField.setAdditionalInfo(additionalInfo); return calculatedField; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java deleted file mode 100644 index 80459962d8..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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 jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -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 java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TENANT_ID_COLUMN; - -@Data -@EqualsAndHashCode(callSuper = true) -@Entity -@Table(name = CALCULATED_FIELD_LINK_TABLE_NAME) -public class CalculatedFieldLinkEntity extends BaseSqlEntity implements BaseEntity { - - @Column(name = CALCULATED_FIELD_LINK_TENANT_ID_COLUMN) - private UUID tenantId; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_TYPE) - private String entityType; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_ID) - private UUID entityId; - - @Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID) - private UUID calculatedFieldId; - - public CalculatedFieldLinkEntity() { - super(); - } - - public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) { - super(calculatedFieldLink); - this.tenantId = calculatedFieldLink.getTenantId().getId(); - this.entityType = calculatedFieldLink.getEntityId().getEntityType().name(); - this.entityId = calculatedFieldLink.getEntityId().getId(); - this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId(); - } - - @Override - public CalculatedFieldLink toData() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - return calculatedFieldLink; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java index 0519130ab4..5e850542a8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java @@ -32,7 +32,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_ import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME; - @Data @Entity @Table(name = MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME) @@ -63,4 +62,5 @@ public final class MobileAppBundleOauth2ClientEntity implements ToData { + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.RULE_CHAIN_NOTES_PROPERTY) + private JsonNode notes; + + public RuleChainDetailsEntity() { + super(); + } + + public RuleChainDetailsEntity(RuleChainDetails ruleChainDetails) { + super(ruleChainDetails); + if (ruleChainDetails.getNotes() != null) { + this.notes = JacksonUtil.valueToTree(ruleChainDetails.getNotes()); + } + } + + @Override + public RuleChainDetails toData() { + RuleChain ruleChain = super.toRuleChain(); + RuleChainDetails details = new RuleChainDetails(ruleChain); + if (notes != null && notes.isArray()) { + details.setNotes(JacksonUtil.treeToValue(notes, new TypeReference<>() {})); + } + return details; + } + +} 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 5b83eb6f72..ca6651a965 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,102 +15,30 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.databind.JsonNode; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; -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.common.data.rule.RuleChainType; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.util.mapping.JsonConverter; - -import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.RULE_CHAIN_TABLE_NAME) -public class RuleChainEntity extends BaseVersionedEntity { - - @Column(name = ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY) - private UUID tenantId; - - @Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) - private String name; - - @Enumerated(EnumType.STRING) - @Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) - private RuleChainType type; - - @Column(name = ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) - private UUID firstRuleNodeId; - - @Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY) - private boolean root; - - @Column(name = ModelConstants.DEBUG_MODE) - private boolean debugMode; - - @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY) - private JsonNode configuration; - - @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; - - @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) - private UUID externalId; +public class RuleChainEntity extends AbstractRuleChainEntity { public RuleChainEntity() { + super(); } public RuleChainEntity(RuleChain ruleChain) { super(ruleChain); - this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); - this.name = ruleChain.getName(); - this.type = ruleChain.getType(); - if (ruleChain.getFirstRuleNodeId() != null) { - this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); - } - this.root = ruleChain.isRoot(); - this.debugMode = ruleChain.isDebugMode(); - this.configuration = ruleChain.getConfiguration(); - this.additionalInfo = ruleChain.getAdditionalInfo(); - if (ruleChain.getExternalId() != null) { - this.externalId = ruleChain.getExternalId().getId(); - } } @Override public RuleChain toData() { - RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); - ruleChain.setCreatedTime(createdTime); - ruleChain.setVersion(version); - ruleChain.setTenantId(TenantId.fromUUID(tenantId)); - ruleChain.setName(name); - ruleChain.setType(type); - if (firstRuleNodeId != null) { - ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); - } - ruleChain.setRoot(root); - ruleChain.setDebugMode(debugMode); - ruleChain.setConfiguration(configuration); - ruleChain.setAdditionalInfo(additionalInfo); - if (externalId != null) { - ruleChain.setExternalId(new RuleChainId(externalId)); - } - return ruleChain; + return super.toRuleChain(); } } 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 889b4df318..3f4a9d2e74 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 @@ -35,9 +35,6 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.util.UUID; -/** - * Created by Valerii Sosliuk on 4/21/2017. - */ @Data @EqualsAndHashCode(callSuper = true) @Entity 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 44a38bf445..971c06c7a0 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 @@ -65,6 +65,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; query = SearchTsKvLatestRepository.FIND_ALL_BY_ENTITY_ID_QUERY, resultSetMapping = "tsKvLatestFindMapping", resultClass = TsKvLatestEntity.class + ), + @NamedNativeQuery( + name = SearchTsKvLatestRepository.FIND_LATEST_BY_ENTITY_IDS, + query = SearchTsKvLatestRepository.FIND_LATEST_BY_ENTITY_IDS_QUERY, + resultSetMapping = "tsKvLatestFindMapping", + resultClass = TsKvLatestEntity.class ) }) public final class TsKvLatestEntity extends AbstractTsKvEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index 36a09378a2..76f29a0163 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -177,6 +177,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForTenant, affectedTenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitIncreaseRequest, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForTenant, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForSysadmin, sysAdmins.getId()); @@ -225,6 +226,9 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS if (!isNotificationConfigured(tenantId, NotificationType.RESOURCES_SHORTAGE)) { defaultNotifications.create(tenantId, DefaultNotifications.resourcesShortage, sysAdmins.getId()); } + if (!isNotificationConfigured(tenantId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST)) { + defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitIncreaseRequest, sysAdmins.getId()); + } } else { var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE); var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes( diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index 7f6948fccb..81094b7f00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -26,17 +26,13 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.NotificationTargetId; 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.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; -import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; -import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; -import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.page.PageData; @@ -50,9 +46,6 @@ import org.thingsboard.server.dao.user.UserService; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; - -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @@ -115,49 +108,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl @Override public PageData findRecipientsForNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, PageLink pageLink) { UsersFilter usersFilter = targetConfig.getUsersFilter(); - switch (usersFilter.getType()) { - case USER_LIST: { - List users = ((UserListFilter) usersFilter).getUsersIds().stream() - .limit(pageLink.getPageSize()) - .map(UserId::new).map(userId -> userService.findUserById(tenantId, userId)) - .filter(Objects::nonNull).collect(Collectors.toList()); - return new PageData<>(users, 1, users.size(), false); - } - case CUSTOMER_USERS: { - if (tenantId.equals(TenantId.SYS_TENANT_ID)) { - throw new IllegalArgumentException("Customer users target is not supported for system administrator"); - } - CustomerUsersFilter filter = (CustomerUsersFilter) usersFilter; - return userService.findCustomerUsers(tenantId, new CustomerId(filter.getCustomerId()), pageLink); - } - case TENANT_ADMINISTRATORS: { - TenantAdministratorsFilter filter = (TenantAdministratorsFilter) usersFilter; - if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { - return userService.findTenantAdmins(tenantId, pageLink); - } else { - if (isNotEmpty(filter.getTenantsIds())) { - return userService.findTenantAdminsByTenantsIds(filter.getTenantsIds().stream() - .map(TenantId::fromUUID).collect(Collectors.toList()), pageLink); - } else if (isNotEmpty(filter.getTenantProfilesIds())) { - return userService.findTenantAdminsByTenantProfilesIds(filter.getTenantProfilesIds().stream() - .map(TenantProfileId::new).collect(Collectors.toList()), pageLink); - } else { - return userService.findAllTenantAdmins(pageLink); - } - } - } - case SYSTEM_ADMINISTRATORS: - return userService.findSysAdmins(pageLink); - case ALL_USERS: { - if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { - return userService.findUsersByTenantId(tenantId, pageLink); - } else { - return userService.findAllUsers(pageLink); - } - } - default: - throw new IllegalArgumentException("Recipient type not supported"); - } + return userService.findUsersByFilter(tenantId, usersFilter, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 92a36f8def..fb8d5eb1d8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -52,12 +52,15 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.RateL import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -102,7 +105,40 @@ public class DefaultNotifications { .description("Send notification to tenant admins when count of entities of some type reached 80% threshold of the limit") .build()) .build(); - + public static final DefaultNotification entitiesLimitIncreaseRequest = DefaultNotification.builder() + .name("Entities limit increase request") + .type(NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST) + .subject("${entityType} limit increase request") + .text("${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.") + .button("${increaseLimitActionLabel}").link("${increaseLimitLink}") + .emailTemplate(DefaultEmailTemplate.builder() + .subject("${entityType} limit increase request") + .body(""" + + + + + + +
+ + + + + + + + + + + + +
${entityType} limit increase request
${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.
+ ${increaseLimitActionLabel} +
+
""") + .build()) + .build(); public static final DefaultNotification apiFeatureWarningForSysadmin = DefaultNotification.builder() .name("API feature warning notification for sysadmin") .type(NotificationType.API_USAGE_LIMIT) @@ -415,6 +451,8 @@ public class DefaultNotifications { private final String button; private final String link; + private final DefaultEmailTemplate emailTemplate; + private final DefaultRule rule; public NotificationTemplate toTemplate() { @@ -448,9 +486,17 @@ public class DefaultNotifications { } webTemplate.setAdditionalConfig(additionalConfig); webTemplate.setEnabled(true); - templateConfig.setDeliveryMethodsTemplates(Map.of( - NotificationDeliveryMethod.WEB, webTemplate - )); + + Map deliveryMethodsTemplates = new HashMap<>(); + deliveryMethodsTemplates.put(NotificationDeliveryMethod.WEB, webTemplate); + if (this.emailTemplate != null) { + EmailDeliveryMethodNotificationTemplate emailTemplate = new EmailDeliveryMethodNotificationTemplate(); + emailTemplate.setSubject(this.emailTemplate.getSubject()); + emailTemplate.setBody(this.emailTemplate.getBody()); + emailTemplate.setEnabled(true); + deliveryMethodsTemplates.put(NotificationDeliveryMethod.EMAIL, emailTemplate); + } + templateConfig.setDeliveryMethodsTemplates(deliveryMethodsTemplates); template.setConfiguration(templateConfig); return template; } @@ -465,12 +511,10 @@ public class DefaultNotifications { rule.setTriggerConfig(defaultRule.getTriggerConfig()); if (rule.getTriggerType() == NotificationRuleTriggerType.ALARM) { EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(rule.getTriggerType()); recipientsConfig.setEscalationTable(Map.of(0, toUUIDs(List.of(targets)))); rule.setRecipientsConfig(recipientsConfig); } else { - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(rule.getTriggerType()); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(rule.getTriggerType()); recipientsConfig.setTargets(toUUIDs(List.of(targets))); rule.setRecipientsConfig(recipientsConfig); } @@ -482,6 +526,13 @@ public class DefaultNotifications { } + @Data + @Builder(toBuilder = true) + public static class DefaultEmailTemplate { + private final String subject; + private final String body; + } + @Data @Builder(toBuilder = true) public static class DefaultRule { 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 index d4753d045d..86708e9e76 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 54577ff9eb..2e30c0d598 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -41,7 +41,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCacheKey.java new file mode 100644 index 0000000000..ea733cdef3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCacheKey.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.Serializable; + +import static java.util.Objects.requireNonNull; + +record ApiKeyCacheKey(String value) implements Serializable { + + ApiKeyCacheKey { + requireNonNull(value); + } + + static ApiKeyCacheKey of(String value) { + return new ApiKeyCacheKey(value); + } + + @NonNull + @Override + public String toString() { + return /* cache name */ "_" + value; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCaffeineCache.java new file mode 100644 index 0000000000..7ffc48ee86 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.pat.ApiKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("ApiKeyCache") +public class ApiKeyCaffeineCache extends CaffeineTbTransactionalCache { + + public ApiKeyCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.API_KEYS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java similarity index 51% rename from dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java rename to dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java index 1d705e2d75..f5ff79fb52 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java @@ -13,30 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cf; +package org.thingsboard.server.dao.pat; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -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.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.dao.Dao; import java.util.List; +import java.util.Set; -public interface CalculatedFieldLinkDao extends Dao { +public interface ApiKeyDao extends Dao { - List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId); + ApiKey findByValue(String value); - List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); + PageData findByTenantId(TenantId tenantId, PageLink pageLink); - List findCalculatedFieldLinksByTenantId(TenantId tenantId); + List findByTenantIdAndUserId(TenantId tenantId, UserId userId); - List findAll(); + Set deleteByTenantId(TenantId tenantId); - PageData findAll(PageLink pageLink); + Set deleteByUserId(TenantId tenantId, UserId userId); - PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); + int deleteAllByExpirationTimeBefore(long ts); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyEvictEvent.java new file mode 100644 index 0000000000..681e89e122 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyEvictEvent.java @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +public record ApiKeyEvictEvent(String value) {} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyInfoDao.java new file mode 100644 index 0000000000..8b7e1fd2ef --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyInfoDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +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.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.Dao; + +public interface ApiKeyInfoDao extends Dao { + + PageData findByUserId(TenantId tenantId, UserId userId, PageLink pageLink); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyRedisCache.java new file mode 100644 index 0000000000..e79ddb69f2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.pat.ApiKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("ApiKeyCache") +public class ApiKeyRedisCache extends RedisTbTransactionalCache { + + public ApiKeyRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.API_KEYS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(ApiKey.class)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java new file mode 100644 index 0000000000..36d8736f78 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java @@ -0,0 +1,201 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import com.google.common.util.concurrent.FluentFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.common.data.id.ApiKeyId; +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.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.service.validator.ApiKeyDataValidator; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.user.UserServiceImpl.INCORRECT_TENANT_ID; +import static org.thingsboard.server.dao.user.UserServiceImpl.INCORRECT_USER_ID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ApiKeyServiceImpl extends AbstractCachedEntityService implements ApiKeyService { + + private static final String INCORRECT_API_KEY_ID = "Incorrect ApiKeyId "; + private static final int MAX_API_KEY_VALUE_LENGTH = 255; + + private final ApiKeyDao apiKeyDao; + private final ApiKeyInfoDao apiKeyInfoDao; + @Lazy + private final ApiKeyDataValidator apiKeyValidator; + + @Value("${security.api_key.value_prefix:}") + private String prefix; + + @Value("${security.api_key.value_bytes_size:64}") + private int valueBytesSize; + + @Override + @TransactionalEventListener + public void handleEvictEvent(ApiKeyEvictEvent event) { + cache.evict(ApiKeyCacheKey.of(event.value())); + } + + @Override + public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo) { + return saveApiKey(tenantId, apiKeyInfo, null, true); + } + + @Override + public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo, String value, boolean doValidate) { + log.trace("Executing saveApiKey [{}]", apiKeyInfo); + try { + var apiKey = new ApiKey(apiKeyInfo); + ApiKey old = doValidate ? apiKeyValidator.validate(apiKey, ApiKeyInfo::getTenantId) : + (apiKey.getId() != null ? apiKeyDao.findById(tenantId, apiKey.getUuidId()) : null); + if (value != null) { + apiKey.setValue(value); + } else if (old == null) { + apiKey.setValue(generateApiKeySecret()); + } else { + apiKey.setValue(old.getValue()); + } + var savedApiKey = apiKeyDao.save(tenantId, apiKey); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedApiKey.getId()).entity(savedApiKey).created(apiKey.getId() == null).build()); + if (old != null && old.isEnabled() != apiKey.isEnabled()) { + publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue())); + } + return savedApiKey; + } catch (Exception e) { + checkConstraintViolation(e, "api_key_value_unq_key", "API Key with such value already exists!"); + throw e; + } + } + + @Override + public ApiKey findApiKeyById(TenantId tenantId, ApiKeyId apiKeyId) { + log.trace("Executing findApiKeyById [{}] [{}]", tenantId, apiKeyId); + validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); + return apiKeyDao.findById(tenantId, apiKeyId.getId()); + } + + @Override + public PageData findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink) { + log.trace("Executing findApiKeysByUserId [{}][{}]", tenantId, userId); + validateId(userId, id -> INCORRECT_USER_ID + id); + return apiKeyInfoDao.findByUserId(tenantId, userId, pageLink); + } + + @Override + public List findApiKeysByUserId(TenantId tenantId, UserId userId) { + log.trace("Executing findApiKeysByUserId [{}][{}]", tenantId, userId); + validateId(userId, id -> INCORRECT_USER_ID + id); + return apiKeyDao.findByTenantIdAndUserId(tenantId, userId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(findApiKeyById(tenantId, new ApiKeyId(entityId.getId()))); + } + + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(apiKeyDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + + @Override + public void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force) { + UUID apiKeyId = apiKey.getUuidId(); + validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); + apiKeyDao.removeById(tenantId, apiKeyId); + publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue())); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(apiKey.getId()).build()); + } + + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + ApiKey apiKey = findApiKeyById(tenantId, new ApiKeyId(id.getId())); + if (apiKey == null) { + if (force) { + return; + } else { + throw new DataValidationException("Unable to delete non-existent API key."); + } + } + deleteApiKey(tenantId, apiKey, force); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + log.trace("Executing deleteApiKeysByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Set values = apiKeyDao.deleteByTenantId(tenantId); + values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value))); + } + + @Override + public void deleteByUserId(TenantId tenantId, UserId userId) { + log.trace("Executing deleteApiKeysByUserId, tenantId [{}]", tenantId); + validateId(userId, id -> INCORRECT_USER_ID + id); + Set values = apiKeyDao.deleteByUserId(tenantId, userId); + values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value))); + } + + @Override + public PageData findApiKeysByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findApiKeysByTenantId [{}]", tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + return apiKeyDao.findByTenantId(tenantId, pageLink); + } + + @Override + public ApiKey findApiKeyByValue(String value) { + log.trace("Executing findApiKeyByValue [{}]", value); + var cacheKey = ApiKeyCacheKey.of(value); + return cache.getAndPutInTransaction(cacheKey, () -> apiKeyDao.findByValue(value), true); + } + + private String generateApiKeySecret() { + return prefix + StringUtils.generateSafeToken(Math.min(valueBytesSize, MAX_API_KEY_VALUE_LENGTH)); + } + + @Override + public EntityType getEntityType() { + return EntityType.API_KEY; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index b75cfec1ed..0d9d25ff10 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index d55969b6b4..6f404e1aca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -19,7 +19,6 @@ 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 com.google.common.util.concurrent.SettableFuture; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; @@ -32,29 +31,36 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,15 +71,15 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Predicate; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber; -/** - * Created by ashvayka on 28.04.17. - */ -@Service @Slf4j -public class BaseRelationService implements RelationService { +@Service +class BaseRelationService implements RelationService { private final RelationDao relationDao; private final EntityService entityService; @@ -81,7 +87,9 @@ public class BaseRelationService implements RelationService { private final ApplicationEventPublisher eventPublisher; private final JpaExecutorService executor; private final JpaRelationQueryExecutorService relationsExecutor; - protected ScheduledExecutorService timeoutExecutorService; + private final ApiLimitService apiLimitService; + + private ScheduledExecutorService timeoutExecutorService; @Value("${sql.relations.query_timeout:20}") private Integer relationQueryTimeout; @@ -89,13 +97,14 @@ public class BaseRelationService implements RelationService { public BaseRelationService(RelationDao relationDao, @Lazy EntityService entityService, TbTransactionalCache cache, ApplicationEventPublisher eventPublisher, JpaExecutorService executor, - JpaRelationQueryExecutorService relationsExecutor) { + JpaRelationQueryExecutorService relationsExecutor, ApiLimitService apiLimitService) { this.relationDao = relationDao; this.entityService = entityService; this.cache = cache; this.eventPublisher = eventPublisher; this.executor = executor; this.relationsExecutor = relationsExecutor; + this.apiLimitService = apiLimitService; } @PostConstruct @@ -179,7 +188,11 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelationAsync [{}]", relation); - validate(relation); + try { + validate(relation); + } catch (DataValidationException e) { + return Futures.immediateFailedFuture(e); + } var future = relationDao.saveRelationAsync(tenantId, relation); return Futures.transform(future, savedRelation -> { if (savedRelation != null) { @@ -187,7 +200,7 @@ public class BaseRelationService implements RelationService { eventPublisher.publishEvent(new RelationActionEvent(tenantId, savedRelation, ActionType.RELATION_ADD_OR_UPDATE)); } return savedRelation != null; - }, MoreExecutors.directExecutor()); + }, directExecutor()); } @Override @@ -205,7 +218,11 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation) { log.trace("Executing deleteRelationAsync [{}]", relation); - validate(relation); + try { + validate(relation); + } catch (DataValidationException e) { + return Futures.immediateFailedFuture(e); + } var future = relationDao.deleteRelationAsync(tenantId, relation); return Futures.transform(future, deletedRelation -> { if (deletedRelation != null) { @@ -213,7 +230,7 @@ public class BaseRelationService implements RelationService { eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedRelation, ActionType.RELATION_DELETED)); } return deletedRelation != null; - }, MoreExecutors.directExecutor()); + }, directExecutor()); } @Override @@ -239,7 +256,7 @@ public class BaseRelationService implements RelationService { eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedEvent, ActionType.RELATION_DELETED)); } return deletedEvent != null; - }, MoreExecutors.directExecutor()); + }, directExecutor()); } @Transactional @@ -316,17 +333,10 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByFrom [{}][{}]", from, typeGroup); validate(from); validateTypeGroup(typeGroup); - ListenableFuture> relations = executor.submit(() -> relationDao.findAllByFrom(tenantId, from, typeGroup)); - return Futures.transformAsync(relations, - relations1 -> { - List> futures = new ArrayList<>(); - relations1.forEach(relation -> - futures.add(fetchRelationInfoAsync(tenantId, relation, - EntityRelation::getTo, - EntityRelationInfo::setToName)) - ); - return Futures.successfulAsList(futures); - }, MoreExecutors.directExecutor()); + return Futures.transform(executor.submit(() -> relationDao.findAllByFrom(tenantId, from, typeGroup)), + relations -> relations.stream() + .map(relation -> fetchRelationInfo(tenantId, relation, EntityRelation::getTo, EntityRelationInfo::setToName)) + .toList(), directExecutor()); } @Override @@ -372,26 +382,10 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup); validate(to); validateTypeGroup(typeGroup); - ListenableFuture> relations = findByToAsync(tenantId, to, typeGroup); - return Futures.transformAsync(relations, - relations1 -> { - List> futures = new ArrayList<>(); - relations1.forEach(relation -> - futures.add(fetchRelationInfoAsync(tenantId, relation, - EntityRelation::getFrom, - EntityRelationInfo::setFromName)) - ); - return Futures.successfulAsList(futures); - }, MoreExecutors.directExecutor()); - } - - private ListenableFuture fetchRelationInfoAsync(TenantId tenantId, EntityRelation relation, - Function entityIdGetter, - BiConsumer entityNameSetter) { - EntityRelationInfo relationInfo = new EntityRelationInfo(relation); - entityNameSetter.accept(relationInfo, - entityService.fetchEntityName(tenantId, entityIdGetter.apply(relation)).orElse("N/A")); - return Futures.immediateFuture(relationInfo); + return Futures.transform(findByToAsync(tenantId, to, typeGroup), + relations -> relations.stream() + .map(relation -> fetchRelationInfo(tenantId, relation, EntityRelation::getFrom, EntityRelationInfo::setFromName)) + .toList(), directExecutor()); } @Override @@ -443,7 +437,7 @@ public class BaseRelationService implements RelationService { } } return relations; - }, MoreExecutors.directExecutor()); + }, directExecutor()); } catch (Exception e) { log.warn("Failed to query relations: [{}]", query, e); throw new RuntimeException(e); @@ -453,24 +447,31 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture> findInfoByQuery(TenantId tenantId, EntityRelationsQuery query) { log.trace("Executing findInfoByQuery [{}]", query); - ListenableFuture> relations = findByQuery(tenantId, query); + EntitySearchDirection direction = query.getParameters().getDirection(); - return Futures.transformAsync(relations, - relations1 -> { - List> futures = new ArrayList<>(); - relations1.forEach(relation -> - futures.add(fetchRelationInfoAsync(tenantId, relation, - relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(), - (EntityRelationInfo relationInfo, String entityName) -> { - if (direction == EntitySearchDirection.FROM) { - relationInfo.setToName(entityName); - } else { - relationInfo.setFromName(entityName); - } - })) - ); - return Futures.successfulAsList(futures); - }, MoreExecutors.directExecutor()); + + Function entityIdGetter = relation -> direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + + BiConsumer entityNameSetter = (EntityRelationInfo relationInfo, String entityName) -> { + if (direction == EntitySearchDirection.FROM) { + relationInfo.setToName(entityName); + } else { + relationInfo.setFromName(entityName); + } + }; + + return Futures.transform(findByQuery(tenantId, query), + relations -> relations.stream() + .map(relation -> fetchRelationInfo(tenantId, relation, entityIdGetter, entityNameSetter)) + .toList(), directExecutor()); + } + + private EntityRelationInfo fetchRelationInfo(TenantId tenantId, EntityRelation relation, + Function entityIdGetter, + BiConsumer entityNameSetter) { + var relationInfo = new EntityRelationInfo(relation); + entityNameSetter.accept(relationInfo, entityService.fetchEntityName(tenantId, entityIdGetter.apply(relation)).orElse("N/A")); + return relationInfo; } @Override @@ -495,15 +496,90 @@ public class BaseRelationService implements RelationService { return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit); } - protected void validate(EntityRelation relation) { + @Override + public ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery) { + return findFilteredRelationsByPathQueryAsync(tenantId, relationPathQuery, null); + } + + @Override + public ListenableFuture> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate relationFilter) { + log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); + validateId(tenantId, id -> "Invalid tenant id: " + id); + validate(relationPathQuery); + int limit = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument); + validatePositiveNumber(limit, "Max related entities limit for relation path query must be positive!"); + if (relationPathQuery.levels().size() == 1) { + RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0); + var relationsFuture = switch (relationPathLevel.direction()) { + case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + }; + return Futures.transform(relationsFuture, entityRelations -> { + if (entityRelations == null || entityRelations.isEmpty()) { + return Collections.emptyList(); + } + List relations = relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations; + if (relations.size() > limit) { + List limitedRelations = new ArrayList<>(relations); + limitedRelations.sort(Comparator.comparing(r -> r.getFrom().getId())); + return limitedRelations.subList(0, limit); + } else { + return relations; + } + }, directExecutor()); + } + return executor.submit(() -> { + List entityRelations = relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit); + return relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations; + }); + } + + private List filterRelations(List entityRelations, Predicate relationFilter) { + return entityRelations.stream() + .filter(relationFilter) + .toList(); + } + + @Override + public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery) { + log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); + validateId(tenantId, id -> "Invalid tenant id: " + id); + validate(relationPathQuery); + int limit = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument); + if (relationPathQuery.levels().size() == 1) { + RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0); + var relations = switch (relationPathLevel.direction()) { + case FROM -> findByFromAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + case TO -> findByToAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + }; + if (relations.size() > limit) { + List limitedRelations = new ArrayList<>(relations); + limitedRelations.sort(Comparator.comparing(r -> r.getFrom().getId())); + return limitedRelations.subList(0, limit); + } else { + return relations; + } + } + return relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit); + } + + private void validate(EntityRelationPathQuery relationPathQuery) { + validateId((UUIDBased) relationPathQuery.rootEntityId(), id -> "Invalid root entity id: " + id); + List levels = relationPathQuery.levels(); + if (CollectionUtils.isEmpty(levels)) { + throw new DataValidationException("Validation error: relation path levels should be specified!"); + } + levels.forEach(RelationPathLevel::validate); + } + + private static void validate(EntityRelation relation) { if (relation == null) { - throw new DataValidationException("Relation type should be specified!"); + throw new DataValidationException("Validation error: relation must not be null"); } ConstraintValidator.validateFields(relation); - validate(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); } - protected void validate(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) { + private static void validate(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) { validateType(type); validateTypeGroup(typeGroup); if (from == null) { @@ -514,25 +590,25 @@ public class BaseRelationService implements RelationService { } } - private void validateType(String type) { - if (StringUtils.isEmpty(type)) { + private static void validateType(String type) { + if (type == null) { throw new DataValidationException("Relation type should be specified!"); } } - private void validateTypeGroup(RelationTypeGroup typeGroup) { + private static void validateTypeGroup(RelationTypeGroup typeGroup) { if (typeGroup == null) { throw new DataValidationException("Relation type group should be specified!"); } } - protected void validate(EntityId entity) { + private static void validate(EntityId entity) { if (entity == null) { throw new DataValidationException("Entity should be specified!"); } } - private boolean matchFilters(List filters, EntityRelation relation, EntitySearchDirection direction) { + private static boolean matchFilters(List filters, EntityRelation relation, EntitySearchDirection direction) { for (RelationEntityTypeFilter filter : filters) { if (match(filter, relation, direction)) { return true; @@ -541,7 +617,7 @@ public class BaseRelationService implements RelationService { return false; } - private boolean match(RelationEntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) { + private static boolean match(RelationEntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) { if (StringUtils.isEmpty(filter.getRelationType()) || filter.getRelationType().equals(relation.getType())) { if (filter.getEntityTypes() == null || filter.getEntityTypes().isEmpty()) { return true; @@ -556,6 +632,7 @@ public class BaseRelationService implements RelationService { @RequiredArgsConstructor private static class RelationQueueCtx { + final SettableFuture> future = SettableFuture.create(); final Set result = ConcurrentHashMap.newKeySet(); final Queue tasks = new ConcurrentLinkedQueue<>(); @@ -569,12 +646,7 @@ public class BaseRelationService implements RelationService { } - @RequiredArgsConstructor - private static class RelationTask { - private final int currentLvl; - private final EntityId root; - private final List prevRelations; - } + private record RelationTask(int currentLvl, EntityId root, List prevRelations) {} private void processQueue(RelationQueueCtx ctx) { RelationTask task = ctx.tasks.poll(); @@ -648,4 +720,5 @@ public class BaseRelationService implements RelationService { handleEvictEvent(event); } } + } 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 f8f2dbd9e8..e9cc33de50 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.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.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -71,4 +72,6 @@ public interface RelationDao { List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); + List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery, int limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 639c60e1d6..11bbe797cf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -188,6 +188,12 @@ public class BaseImageService extends BaseResourceService implements ImageServic return findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, key); } + @Override + public Set getAllImageKeysByTenantId(TenantId tenantId) { + log.trace("Executing getAllImageKeysByTenantId [{}]", tenantId); + return resourceInfoDao.findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix(tenantId, ResourceType.IMAGE, ""); + } + @Override public TbResourceInfo getPublicImageInfoByKey(String publicResourceKey) { return resourceInfoDao.findPublicResourceByKey(ResourceType.IMAGE, publicResourceKey); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 40b5a9778b..432329a621 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -58,7 +58,7 @@ import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -91,7 +91,9 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Primary public class BaseResourceService extends AbstractCachedEntityService implements ResourceService { - public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; + protected static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; + protected static final int MAX_ENTITIES_TO_FIND = 10; + protected final TbResourceDao resourceDao; protected final TbResourceInfoDao resourceInfoDao; protected final ResourceDataValidator resourceValidator; @@ -100,7 +102,6 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceLinkContainerDaoMap = new HashMap<>(); private final Map> generalResourceContainerDaoMap = new HashMap<>(); - protected static final int MAX_ENTITIES_TO_FIND = 10; @PostConstruct public void init() { 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 c96ba0542f..fce0cec732 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 @@ -51,6 +51,7 @@ 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.RuleChainDetails; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -63,11 +64,11 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -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.service.validator.RuleChainDataValidator; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.Collection; @@ -85,6 +86,7 @@ import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.DataConstants.TENANT; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -103,6 +105,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Autowired private RuleChainDao ruleChainDao; + @Autowired + private RuleChainDetailsDao ruleChainDetailsDao; + @Autowired private RuleNodeDao ruleNodeDao; @@ -127,6 +132,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { + return saveEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, doValidate)); + } + + private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { log.trace("Executing doSaveRuleChain [{}]", ruleChain); if (doValidate) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); @@ -311,9 +320,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } - ruleChain = ruleChainDao.save(tenantId, ruleChain); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain) - .entityId(ruleChain.getId()).broadcastEvent(publishSaveEvent).build()); + RuleChainDetails ruleChainDetails = new RuleChainDetails(ruleChain); + ruleChainDetails.setNotes(ruleChainMetaData.getNotes()); + ruleChainDetails = ruleChainDetailsDao.save(tenantId, ruleChainDetails); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChainDetails) + .entityId(ruleChainDetails.getId()).broadcastEvent(publishSaveEvent).build()); return RuleChainUpdateResult.successful(updatedRuleNodes); } @@ -336,13 +347,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId) { Validator.validateId(ruleChainId, "Incorrect rule chain id."); - RuleChain ruleChain = findRuleChainById(tenantId, ruleChainId); - if (ruleChain == null) { + RuleChainDetails ruleChainDetails = ruleChainDetailsDao.findById(tenantId, ruleChainId.getId()); + if (ruleChainDetails == null) { return null; } RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChainId); - ruleChainMetaData.setVersion(ruleChain.getVersion()); + ruleChainMetaData.setVersion(ruleChainDetails.getVersion()); List ruleNodes = getRuleChainNodes(tenantId, ruleChainId); Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId))); Map ruleNodeIndexMap = new HashMap<>(); @@ -350,8 +361,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ruleNodeIndexMap.put(node.getId(), ruleNodes.indexOf(node)); } ruleChainMetaData.setNodes(ruleNodes); - if (ruleChain.getFirstRuleNodeId() != null) { - ruleChainMetaData.setFirstNodeIndex(ruleNodeIndexMap.get(ruleChain.getFirstRuleNodeId())); + if (ruleChainDetails.getFirstRuleNodeId() != null) { + ruleChainMetaData.setFirstNodeIndex(ruleNodeIndexMap.get(ruleChainDetails.getFirstRuleNodeId())); } for (RuleNode node : ruleNodes) { int fromIndex = ruleNodeIndexMap.get(node.getId()); @@ -371,6 +382,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Collections.sort(ruleChainMetaData.getConnections(), Comparator.comparingInt(NodeConnectionInfo::getFromIndex) .thenComparing(NodeConnectionInfo::getToIndex).thenComparing(NodeConnectionInfo::getType)); } + ruleChainMetaData.setNotes(ruleChainDetails.getNotes()); return ruleChainMetaData; } @@ -869,6 +881,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } + @Override + public List findRuleChainsByIds(TenantId tenantId, List ruleChainIds) { + log.trace("Executing findRuleChainsByIds, tenantId [{}], ruleChainIds [{}]", tenantId, ruleChainIds); + return ruleChainDao.findRuleChainsByTenantIdAndIds(tenantId.getId(), toUUIDs(ruleChainIds)); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { 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 index 1981879e76..aaae87bd4e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java @@ -26,7 +26,7 @@ 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; +import org.thingsboard.server.exception.DataValidationException; @Service @Slf4j 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 ec6a4fb14f..b88ed2294b 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 @@ -21,13 +21,13 @@ 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.RuleChainType; -import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ResourceContainerDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Collection; +import java.util.List; import java.util.UUID; /** @@ -83,4 +83,6 @@ public interface RuleChainDao extends Dao, TenantEntityDao Collection findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name); + List findRuleChainsByTenantIdAndIds(UUID tenantId, List ruleChainIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDetailsDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDetailsDao.java new file mode 100644 index 0000000000..f7ab9d8662 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDetailsDao.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.rule.RuleChainDetails; +import org.thingsboard.server.dao.Dao; + +public interface RuleChainDetailsDao extends Dao { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index 5df895ac80..a9b9f4ec6a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.RateLimit; import org.thingsboard.server.common.data.validation.ValidJsonSchema; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.Collection; import java.util.Set; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index b71db2744c..088c24d810 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.exception.EntitiesLimitExceededException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -27,8 +29,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.TenantEntityWithDataDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.EntitiesLimitException; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.HashSet; @@ -51,7 +51,8 @@ public abstract class DataValidator> { private static final String NAME = "name"; private static final String TOPIC = "topic"; - @Autowired @Lazy + @Autowired + @Lazy private ApiLimitService apiLimitService; // Returns old instance of the same object that is fetched during validation. @@ -93,7 +94,7 @@ public abstract class DataValidator> { } public void validateString(String exceptionPrefix, String name) { - if (StringUtils.isEmpty(name) || name.trim().length() == 0) { + if (StringUtils.isBlank(name)) { throw new DataValidationException(exceptionPrefix + " should be specified!"); } if (StringUtils.contains0x00(name)) { @@ -123,7 +124,8 @@ public abstract class DataValidator> { protected void validateNumberOfEntitiesPerTenant(TenantId tenantId, EntityType entityType) { if (!apiLimitService.checkEntitiesLimit(tenantId, entityType)) { - throw new EntitiesLimitException(tenantId, entityType); + long limit = apiLimitService.getLimit(tenantId, profileConfiguration -> profileConfiguration.getEntitiesLimit(entityType)); + throw new EntitiesLimitExceededException(tenantId, entityType, limit); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java index d03fd3153f..5e90ceae53 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java index 9c4b70ef79..f29fb3ea15 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java @@ -19,7 +19,7 @@ import lombok.AllArgsConstructor; 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.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.settings.AdminSettingsService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java index 9b138fe279..b955bed043 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.ai.AiModelDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java index 86a6e5224b..b70251027d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java @@ -21,8 +21,8 @@ import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.alarm.AlarmCommentDao; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.exception.DataValidationException; @Component @AllArgsConstructor @@ -54,4 +54,5 @@ public class AlarmCommentDataValidator extends DataValidator { } return oldAlarmComment; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java index 0fbd787d52..ed7a6d50d6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java @@ -19,7 +19,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java new file mode 100644 index 0000000000..2420a54cdf --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.dao.pat.ApiKeyDao; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.user.UserService; + +@Component +@RequiredArgsConstructor +public class ApiKeyDataValidator extends DataValidator { + + private final ApiKeyDao apiKeyDao; + private final TenantService tenantService; + private final UserService userService; + + @Override + protected void validateDataImpl(TenantId tenantId, ApiKey apiKey) { + if (apiKey.getId() != null) { + if (apiKey.getUuidId() == null) { + throw new DataValidationException("API Key UUID should be specified!"); + } + if (apiKey.getId().isNullUid()) { + throw new DataValidationException("API key UUID must not be the reserved null value!"); + } + } + + if (apiKey.getTenantId() == null || apiKey.getTenantId().getId() == null) { + throw new DataValidationException("API key should be assigned to tenant!"); + } + if (!TenantId.SYS_TENANT_ID.equals(apiKey.getTenantId()) && !tenantService.tenantExists(apiKey.getTenantId())) { + throw new DataValidationException("API key reference a non-existent tenant!"); + } + + if (apiKey.getUserId() == null || apiKey.getUserId().getId() == null) { + throw new DataValidationException("API key should be assigned to user!"); + } + if (userService.findUserById(apiKey.getTenantId(), apiKey.getUserId()) == null) { + throw new DataValidationException("API key reference a non-existent user!"); + } + } + + @Override + protected ApiKey validateUpdate(TenantId tenantId, ApiKey apiKey) { + ApiKey old = apiKeyDao.findById(tenantId, apiKey.getUuidId()); + if (old == null) { + throw new DataValidationException("Cannot update non-existent API key!"); + } + if (!old.getUserId().equals(apiKey.getUserId())) { + throw new DataValidationException("Cannot update API key user id!"); + } + if (old.getExpirationTime() != apiKey.getExpirationTime()) { + throw new DataValidationException("Cannot update API key expiration time!"); + } + return old; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java index ac7cdca282..c62ff4ad6b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java @@ -21,7 +21,7 @@ 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.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java index 9eccc17cc6..b4a75455fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.customer.CustomerDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java index 292741f85c..27674823cc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java index 258475825a..781c7e6654 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java @@ -18,7 +18,7 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java index c008d60898..a5e11e5f07 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java @@ -23,9 +23,9 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.device.DeviceProfileDao; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.exception.DataValidationException; import java.util.Objects; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index c387414571..82f90a6286 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -18,14 +18,23 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.cf.CalculatedFieldDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + @Component public class CalculatedFieldDataValidator extends DataValidator { @@ -36,46 +45,110 @@ public class CalculatedFieldDataValidator extends DataValidator private ApiLimitService apiLimitService; @Override - protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { - validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId()); + protected void validateDataImpl(TenantId tenantId, CalculatedField calculatedField) { validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateArgumentNames(calculatedField); + validateCalculatedFieldConfiguration(calculatedField); + validateSchedulingConfiguration(tenantId, calculatedField); + validateRelationQuerySourceArguments(tenantId, calculatedField); + validateRelatedAggregationConfiguration(tenantId, calculatedField); + validateEntityAggregationConfiguration(tenantId, calculatedField); } @Override - protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) { - CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId()); - if (old == null) { - throw new DataValidationException("Can't update non existing calculated field!"); + protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { + if (calculatedField.getType() == CalculatedFieldType.ALARM) { + return; } - validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateArgumentNames(calculatedField); - return old; - } - - private void validateNumberOfCFsPerEntity(TenantId tenantId, EntityId entityId) { long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); if (maxCFsPerEntity <= 0) { return; } - if (calculatedFieldDao.countCFByEntityId(tenantId, entityId) >= maxCFsPerEntity) { + if (calculatedFieldDao.countByEntityIdAndTypeNot(tenantId, calculatedField.getEntityId(), CalculatedFieldType.ALARM) >= maxCFsPerEntity) { throw new DataValidationException("Calculated fields per entity limit reached!"); } } + @Override + protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) { + CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing calculated field!"); + } + return old; + } + private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { + return; + } long maxArgumentsPerCF = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxArgumentsPerCF); if (maxArgumentsPerCF <= 0) { return; } - if (calculatedField.getConfiguration().getArguments().size() > maxArgumentsPerCF) { + if (argumentsBasedCfg.getArguments().size() > maxArgumentsPerCF) { throw new DataValidationException("Calculated field arguments limit reached!"); } } - private void validateArgumentNames(CalculatedField calculatedField) { - if (calculatedField.getConfiguration().getArguments().containsKey("ctx")) { - throw new DataValidationException("Argument name 'ctx' is reserved and cannot be used."); + private void validateCalculatedFieldConfiguration(CalculatedField calculatedField) { + wrapAsDataValidation(calculatedField.getConfiguration()::validate); + } + + private void validateSchedulingConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledUpdateCfg) + || !scheduledUpdateCfg.isScheduledUpdateEnabled()) { + return; + } + long minAllowedScheduledUpdateInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF); + wrapAsDataValidation(() -> scheduledUpdateCfg.validate(minAllowedScheduledUpdateInterval)); + } + + private void validateRelationQuerySourceArguments(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { + return; + } + Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() + .stream() + .filter(entry -> entry.getValue().hasRelationQuerySource()) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationPathQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration())); + if (relationQueryBasedArguments.isEmpty()) { + return; + } + int maxRelationLevel = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelationLevelPerCfArgument); + relationQueryBasedArguments.forEach((argumentName, relationQueryDynamicSourceConfiguration) -> + wrapAsDataValidation(() -> relationQueryDynamicSourceConfiguration.validateMaxRelationLevel(argumentName, maxRelationLevel))); + } + + private void validateRelatedAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfiguration)) { + return; + } + long minDeduplicationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedDeduplicationIntervalInSecForCF); + if (aggConfiguration.getDeduplicationIntervalInSec() < minDeduplicationInterval) { + throw new IllegalArgumentException("Deduplication interval is less than configured " + + "minimum allowed interval in tenant profile: " + minDeduplicationInterval); + } + } + + private void validateEntityAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration aggConfiguration)) { + return; + } + long minAggregationIntervalInSec = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedAggregationIntervalInSecForCF); + if (minAggregationIntervalInSec <= 0) { + return; + } + if (aggConfiguration.getInterval().getCurrentIntervalDurationMillis() < TimeUnit.SECONDS.toMillis(minAggregationIntervalInSec)) { + throw new IllegalArgumentException("Aggregation interval duration is less than configured " + + "minimum allowed aggregation interval in tenant profile: " + minAggregationIntervalInSec + " sec."); + } + } + + private static void wrapAsDataValidation(Runnable validation) { + try { + validation.run(); + } catch (IllegalArgumentException e) { + throw new DataValidationException(e.getMessage(), e); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java deleted file mode 100644 index aae76d0909..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.validator; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.DataValidator; - -@Component -public class CalculatedFieldLinkDataValidator extends DataValidator { - - @Autowired - private CalculatedFieldLinkDao calculatedFieldLinkDao; - - @Override - protected CalculatedFieldLink validateUpdate(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - CalculatedFieldLink old = calculatedFieldLinkDao.findById(calculatedFieldLink.getTenantId(), calculatedFieldLink.getId().getId()); - if (old == null) { - throw new DataValidationException("Can't update non existing calculated field link!"); - } - return old; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java index c15655c36c..36be32d0f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java index 825224cde3..0ae9eb1645 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java index 05ea4b390e..50ff63d686 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.customer.CustomerServiceImpl; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java index 3236b75b2c..12c4c62f94 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java index d604a8438f..9d47ade7de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantService; import java.util.Optional; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 6402ec0d1f..74fa56dc87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -52,11 +52,11 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.exception.DataValidationException; import java.io.FileInputStream; import java.security.KeyStore; @@ -69,6 +69,10 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isNotLwm2mServer; + @Slf4j @Component public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator { @@ -182,6 +186,8 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator 65535) { - throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be in range [0 - 65535]!"); - } - } else { - if (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534) { - throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [1 - 65534]!"); + if (serverConfig.isBootstrapServerIs()) { + if (serverConfig.getShortServerId() != null) { + if (serverConfig.getShortServerId() == 0) { + serverConfig.setShortServerId(null); + } else { + throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null!"); } } } else { - String serverName = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; - throw new DeviceCredentialsValidationException(serverName + " ShortServerId must not be null!"); + if (serverConfig.getShortServerId() != null) { + if (isNotLwm2mServer(serverConfig.getShortServerId())) { + throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!"); + } + } else { + throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must not be null!"); + } } String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java index f1ebb64759..e972175ee6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.edge.EdgeDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeEventDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeEventDataValidator.java index f91f37644f..359e124bc9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeEventDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeEventDataValidator.java @@ -18,7 +18,7 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java index 88c03bbe5c..ca29854f46 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entityview.EntityViewDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java index 466eaf96f6..e1c3fd6bd5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java index 568739ccbd..1bff2b9891 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.mobile.MobileAppDao; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java index e64d35afe9..bc445e7cc9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java index d5b965face..289220710c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import java.net.URI; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java index 55a4d963d4..5eec0d670e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageDao; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java index 557a9c3b47..2b8e3eb113 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.mobile.MobileAppDao; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueStatsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueStatsDataValidator.java index 3de9d666a9..519ab54721 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueStatsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueStatsDataValidator.java @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.QueueStats; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java index 8776d87027..afe6730f00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueDao; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java index 5bcd4c0613..1ee469a9c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java @@ -17,14 +17,16 @@ package org.thingsboard.server.dao.service.validator; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.TbResourceDao; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -54,8 +56,8 @@ public class ResourceDataValidator extends DataValidator { @Override protected TbResource validateUpdate(TenantId tenantId, TbResource resource) { - if (resource.getData() != null && !resource.getResourceType().isUpdatable() && - tenantId != null && !tenantId.isSysTenantId()) { + if ((resource.getData() != null && !resource.getResourceType().isUpdatable() && tenantId != null && !tenantId.isSysTenantId()) + || resource.getResourceType().equals(ResourceType.LWM2M_MODEL)) { throw new DataValidationException("This type of resource can't be updated"); } return resource; @@ -81,7 +83,7 @@ public class ResourceDataValidator extends DataValidator { if (StringUtils.isEmpty(resource.getFileName())) { throw new DataValidationException("Resource file name should be specified!"); } - if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) { + if (Strings.CS.containsAny(resource.getFileName(), "/", "\\")) { throw new DataValidationException("File name contains forbidden symbols"); } if (StringUtils.isEmpty(resource.getResourceKey())) { @@ -104,4 +106,5 @@ public class ResourceDataValidator extends DataValidator { validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE); } } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index ecc0c6cc83..5f8a6db3cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.util.ReflectionUtils; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java index 4950e6af0f..7f6087963b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java index 94c0330920..b0337cb345 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantProfileDao; import org.thingsboard.server.dao.tenant.TenantProfileService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java index a8e0ad05f9..0f3470fa34 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.user.UserCredentialsDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java index 79cf13f441..64038fffd4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.customer.CustomerDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java index d7cb4fe98b..2cfebfbdb5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java @@ -20,7 +20,7 @@ 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.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java index ecbab2e83f..775c2c1464 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java @@ -19,7 +19,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; 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 f812d13f65..67e71ebcdf 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 @@ -98,10 +98,8 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -119,10 +117,8 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -183,10 +179,8 @@ public interface AlarmRepository extends JpaRepository { "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -199,10 +193,8 @@ public interface AlarmRepository extends JpaRepository { "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -263,10 +255,8 @@ public interface AlarmRepository extends JpaRepository { "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 ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -280,10 +270,8 @@ public interface AlarmRepository extends JpaRepository { "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 ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index 9d93b5f83c..f5fbcf2e23 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -87,4 +87,10 @@ public interface AssetProfileRepository extends JpaRepository :id ORDER BY a.id") List findNextBatch(@Param("id") UUID id, Limit limit); + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " + + "FROM AssetProfileEntity a WHERE " + + "a.tenantId = :tenantId AND a.id IN :assetProfileIds") + List findAssetProfileInfosByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, + @Param("assetProfileIds") List assetProfileIds); + } 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 42d9a85c1c..10243dd959 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 @@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.AssetFields; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.ExportableEntityRepository; @@ -100,6 +101,10 @@ public interface AssetRepository extends JpaRepository, Expor AssetEntity findByTenantIdAndName(UUID tenantId, String name); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ASSET', a.name) " + + "FROM AssetEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')") + List findEntityInfosByNamePrefix(UUID tenantId, String prefix); + @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.type = :type " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + 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 afd1fed0ce..e37608d136 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 @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; @@ -267,6 +268,12 @@ public class JpaAssetDao extends JpaAbstractDao implements A return nativeAssetRepository.findProfileEntityIdInfosByTenantId(tenantId, DaoUtil.toPageable(pageLink)); } + @Override + public List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + log.debug("Find asset entity infos by name [{}]", name); + return assetRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public Long countByTenantId(TenantId tenantId) { return assetRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index 819c578789..7561802e0a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -105,6 +105,11 @@ public class JpaAssetProfileDao extends JpaAbstractDao findAssetProfilesByTenantIdAndIds(UUID tenantId, List assetProfileIds) { + return assetProfileRepository.findAssetProfileInfosByTenantIdAndIdIn(tenantId, assetProfileIds); + } + @Override public AssetProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(assetProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); 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 aa7f0490d7..8a3405549e 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 @@ -20,6 +20,14 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +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.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.dao.model.sql.AttributeKvCompositeKey; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; @@ -60,6 +68,19 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, @Param("attributeType") int attributeType); + @Query(value = """ + SELECT DISTINCT ON (a.attribute_key) + kd.key AS strKey, + a.bool_v AS boolV, a.str_v AS strV, a.long_v AS longV, + a.dbl_v AS dblV, a.json_v AS jsonV, + a.last_update_ts AS lastUpdateTs, a.version AS version + FROM attribute_kv a + INNER JOIN key_dictionary kd ON a.attribute_key = kd.key_id + WHERE a.entity_id IN :entityIds AND a.attribute_type = :attributeType + ORDER BY a.attribute_key, a.last_update_ts DESC""", nativeQuery = true) + List findLatestByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, + @Param("attributeType") int attributeType); + @Query(value = "SELECT attribute_key, attribute_type, entity_id, bool_v, dbl_v, json_v, last_update_ts, long_v, str_v, version FROM attribute_kv WHERE (entity_id, attribute_type, attribute_key) > " + "(:entityId, :attributeType, :attributeKey) ORDER BY entity_id, attribute_type, attribute_key LIMIT :batchSize", nativeQuery = true) List findNextBatch(@Param("entityId") UUID entityId, @@ -67,4 +88,40 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List entityIds, String attributeType) { + public List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { return attributeKvRepository - .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId()) - .stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList()); + .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).toList(), scope.getId()) + .stream() + .map(keyDictionaryDao::getKey) + .toList(); + } + + @Override + public ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return service.submit(() -> findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope)); + } + + @Override + public List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + if (CollectionUtils.isEmpty(entityIds)) { + return Collections.emptyList(); + } + var uniqueIds = entityIds.stream().map(EntityId::getId).distinct().toList(); + return attributeKvRepository.findLatestByEntityIdsAndAttributeType(uniqueIds, scope.getId()) + .stream() + .map(AttributeKvRepository.AttributeKvProjection::toAttributeKvEntry) + .toList(); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return service.submit(() -> findLatestByEntityIdsAndScope(tenantId, entityIds, scope)); } @Override 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 d685d9ee78..4266003706 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 @@ -33,8 +33,7 @@ public interface AuditLogRepository extends JpaRepository "a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#actionTypes == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityType, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.userName, CONCAT('%', :textSearch, '%')) = true " + @@ -54,8 +53,7 @@ public interface AuditLogRepository extends JpaRepository "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 == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.userName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.actionType, CONCAT('%', :textSearch, '%')) = true " + @@ -75,8 +73,7 @@ public interface AuditLogRepository extends JpaRepository "AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#actionTypes == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityType, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.userName, CONCAT('%', :textSearch, '%')) = true " + @@ -96,8 +93,7 @@ public interface AuditLogRepository extends JpaRepository "AND a.userId = :userId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#actionTypes == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityType, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.actionType, CONCAT('%', :textSearch, '%')) = true " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 9b3767115b..b7b94c5b8c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -23,13 +23,14 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity; import java.util.List; +import java.util.Set; import java.util.UUID; public interface CalculatedFieldRepository extends JpaRepository { boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId); - CalculatedFieldEntity findByEntityIdAndName(UUID entityId, String name); + CalculatedFieldEntity findByEntityIdAndTypeAndName(UUID entityId, String type, String name); List findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); @@ -38,14 +39,28 @@ public interface CalculatedFieldRepository extends JpaRepository findAllByTenantId(UUID tenantId, Pageable pageable); @Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + - "AND cf.entityId = :entityId " + - "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") - Page findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, String textSearch, Pageable pageable); + "AND cf.entityId = :entityId AND cf.type IN :types " + + "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findByTenantIdAndEntityIdAndTypes(UUID tenantId, UUID entityId, List types, String textSearch, Pageable pageable); + + @Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + + "AND cf.type IN :types " + + "AND cf.entityType IN :entityTypes " + + "AND (:entityIds IS NULL OR cf.entityId IN :entityIds) " + + "AND (:names IS NULL OR cf.name IN :names) " + + "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findByTenantIdAndFilter(UUID tenantId, List types, List entityTypes, + Set entityIds, Set names, String textSearch, Pageable pageable); + + @Query("SELECT DISTINCT cf.name FROM CalculatedFieldEntity cf " + + "WHERE cf.tenantId = :tenantId AND cf.type = :type AND " + + "(:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findNamesByTenantIdAndType(UUID tenantId, String type, String textSearch, Pageable pageable); List findAllByTenantId(UUID tenantId); List removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); - long countByTenantIdAndEntityId(UUID tenantId, UUID entityId); + long countByTenantIdAndEntityIdAndTypeNot(UUID tenantId, UUID entityId, String type); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java index 9734af7d7a..0f9dcc20be 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java @@ -25,12 +25,10 @@ import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -49,9 +47,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF private final String CF_COUNT_QUERY = "SELECT count(id) FROM calculated_field;"; private final String CF_QUERY = "SELECT * FROM calculated_field ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final String CFL_COUNT_QUERY = "SELECT count(id) FROM calculated_field_link;"; - private final String CFL_QUERY = "SELECT * FROM calculated_field_link ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; @@ -79,6 +74,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF JsonNode configuration = JacksonUtil.toJsonNode((String) row.get("configuration")); long version = row.get("version") != null ? (long) row.get("version") : 0; String debugSettings = (String) row.get("debug_settings"); + JsonNode additionalInfo = JacksonUtil.toJsonNode((String) row.get("additional_info")); CalculatedField calculatedField = new CalculatedField(); calculatedField.setId(new CalculatedFieldId(id)); @@ -96,6 +92,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF } calculatedField.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); + calculatedField.setAdditionalInfo(additionalInfo); return calculatedField; }).collect(Collectors.toList()); @@ -103,37 +100,4 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF }); } - @Override - public PageData findCalculatedFieldLinks(Pageable pageable) { - return transactionTemplate.execute(status -> { - long startTs = System.currentTimeMillis(); - int totalElements = jdbcTemplate.queryForObject(CFL_COUNT_QUERY, Collections.emptyMap(), Integer.class); - log.debug("Count query took {} ms", System.currentTimeMillis() - startTs); - startTs = System.currentTimeMillis(); - List> rows = jdbcTemplate.queryForList(String.format(CFL_QUERY, pageable.getPageSize(), pageable.getOffset()), Collections.emptyMap()); - log.debug("Main query took {} ms", System.currentTimeMillis() - startTs); - int totalPages = pageable.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageable.getPageSize()) : 1; - boolean hasNext = pageable.getPageSize() > 0 && totalElements > pageable.getOffset() + rows.size(); - var data = rows.stream().map(row -> { - - UUID id = (UUID) row.get("id"); - long createdTime = (long) row.get("created_time"); - UUID tenantId = (UUID) row.get("tenant_id"); - EntityType entityType = EntityType.valueOf((String) row.get("entity_type")); - UUID entityId = (UUID) row.get("entity_id"); - UUID calculatedFieldId = (UUID) row.get("calculated_field_id"); - - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setId(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(new TenantId(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - - return calculatedFieldLink; - }).collect(Collectors.toList()); - return new PageData<>(data, totalPages, totalElements, hasNext); - }); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 81293690fb..a374531a47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.dao.sql.cf; +import com.google.common.base.Strings; import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -34,6 +38,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.Set; import java.util.UUID; @Slf4j @@ -66,8 +71,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { - log.debug("Try to find calculated fields by entityId[{}] and pageLink [{}]", entityId, pageLink); - return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + public PageData findByEntityIdAndTypes(TenantId tenantId, EntityId entityId, Set types, PageLink pageLink) { + log.debug("Try to find calculated fields by entityId [{}] and type [{}] and pageLink [{}]", entityId, types, pageLink); + return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndEntityIdAndTypes(tenantId.getId(), entityId.getId(), + types.stream().map(Enum::name).toList(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override @@ -95,8 +101,24 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink) { + return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndFilter(tenantId.getId(), + filter.getTypes().stream().map(Enum::name).toList(), + filter.getEntityTypes().stream().map(Enum::name).toList(), + CollectionUtils.isNotEmpty(filter.getEntityIds()) ? filter.getEntityIds() : null, + CollectionUtils.isNotEmpty(filter.getNames()) ? filter.getNames() : null, + pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink) { + return DaoUtil.pageToPageData(calculatedFieldRepository.findNamesByTenantIdAndType(tenantId.getId(), type.name(), + Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink, false))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java deleted file mode 100644 index cda10a8730..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.cf; - -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -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.PageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; -import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; - -import java.util.List; -import java.util.UUID; - -@Slf4j -@Component -@AllArgsConstructor -@SqlDao -public class JpaCalculatedFieldLinkDao extends JpaAbstractDao implements CalculatedFieldLinkDao { - - private final CalculatedFieldLinkRepository calculatedFieldLinkRepository; - private final NativeCalculatedFieldRepository nativeCalculatedFieldRepository; - - @Override - public List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId())); - } - - @Override - public List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId())); - } - - @Override - public List findCalculatedFieldLinksByTenantId(TenantId tenantId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId())); - } - - @Override - public List findAll() { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll()); - } - - @Override - public PageData findAll(PageLink pageLink) { - log.debug("Try to find calculated field links by pageLink [{}]", pageLink); - return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink)); - } - - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink); - return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); - } - - @Override - protected Class getEntityClass() { - return CalculatedFieldLinkEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return calculatedFieldLinkRepository; - } - - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java index 64593b7a84..59bf1fa6ef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java @@ -17,13 +17,10 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Pageable; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.page.PageData; public interface NativeCalculatedFieldRepository { PageData findCalculatedFields(Pageable pageable); - PageData findCalculatedFieldLinks(Pageable pageable); - } 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 5ad61bd856..13fe86123f 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 @@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.CustomerEntity; @@ -41,6 +42,10 @@ public interface CustomerRepository extends JpaRepository, CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'CUSTOMER', a.title) " + + "FROM CustomerEntity a WHERE a.tenantId = :tenantId AND a.title LIKE CONCAT(:prefix, '%')") + List findEntityInfosByNamePrefix(UUID tenantId, String prefix); + @Query(value = "SELECT * FROM customer c WHERE c.tenant_id = :tenantId " + "AND c.is_public IS TRUE ORDER BY c.id ASC LIMIT 1", nativeQuery = true) CustomerEntity findPublicCustomerByTenantId(@Param("tenantId") UUID tenantId); @@ -62,4 +67,6 @@ public interface CustomerRepository extends JpaRepository, "c.title, c.version, c.additionalInfo, c.country, c.state, c.city, c.address, c.address2, c.zip, c.phone, c.email) " + "FROM CustomerEntity c WHERE c.id > :id ORDER BY c.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + List findCustomersByTenantIdAndIdIn(UUID tenantId, List customerIds); } 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 075ea4ae33..3d9df8d6ec 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 @@ -20,6 +20,7 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.common.data.id.CustomerId; @@ -107,6 +108,11 @@ public class JpaCustomerDao extends JpaAbstractDao imp ); } + @Override + public List findCustomersByTenantIdAndIds(UUID tenantId, List customerIds) { + return DaoUtil.convertDataList(customerRepository.findCustomersByTenantIdAndIdIn(tenantId, customerIds)); + } + @Override public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { return findByTenantId(tenantId.getId(), pageLink); @@ -117,6 +123,11 @@ public class JpaCustomerDao extends JpaAbstractDao imp return customerRepository.findNextBatch(id, Limit.of(batchSize)); } + @Override + public List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + return customerRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public EntityType getEntityType() { return EntityType.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 2ba46780c9..f67ad1e056 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 @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; @@ -99,4 +100,6 @@ public interface DashboardInfoRepository extends JpaRepository findDashboardInfosByResourceLink(@Param("link") String link, Pageable pageable); + List findByIdIn(List dashboardIds); + } 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 ea60d959ed..59e82a0b81 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,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.dashboard; +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; @@ -126,6 +127,11 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findDashboardsByIds(UUID tenantId, List dashboardIds) { + return DaoUtil.convertDataList(dashboardInfoRepository.findByIdIn(dashboardIds)); + } + @Override public List findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) { return DaoUtil.convertDataList(dashboardInfoRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, limit)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java index e7ba0c206c..4775d5d35b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java @@ -23,6 +23,8 @@ import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +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; @@ -41,13 +43,13 @@ public class DefaultNativeAssetRepository extends AbstractNativeRepository imple @Override public PageData findProfileEntityIdInfos(Pageable pageable) { - String PROFILE_ASSET_ID_INFO_QUERY = "SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s"; + String PROFILE_ASSET_ID_INFO_QUERY = "SELECT tenant_id as tenantId, customer_id as customerId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s"; return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, DefaultNativeAssetRepository::toInfo); } @Override public PageData findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) { - String PROFILE_ASSET_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); + String PROFILE_ASSET_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, customer_id as customerId, asset_profile_id as profileId, id as id FROM asset WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); String COUNT_QUERY_BY_TENANT = String.format("SELECT count(id) FROM asset WHERE tenant_id = '%s';", tenantId); return find(COUNT_QUERY_BY_TENANT, PROFILE_ASSET_ID_INFO_QUERY, pageable, DefaultNativeAssetRepository::toInfo); } @@ -56,8 +58,10 @@ public class DefaultNativeAssetRepository extends AbstractNativeRepository imple var tenantIdObj = row.get("tenantId"); UUID tenantId = tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(); AssetId id = new AssetId((UUID) row.get("id")); + CustomerId customerId = new CustomerId((UUID) row.get("customerId")); + EntityId ownerId = !customerId.isNullUid() ? customerId : TenantId.fromUUID(tenantId); AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId")); - return ProfileEntityIdInfo.create(tenantId, profileId, id); + return ProfileEntityIdInfo.create(tenantId, ownerId, profileId, id); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java index c4ba36ce2e..2a15aee984 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java @@ -22,8 +22,10 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo; +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.PageData; @@ -53,13 +55,13 @@ public class DefaultNativeDeviceRepository extends AbstractNativeRepository impl @Override public PageData findProfileEntityIdInfos(Pageable pageable) { - String PROFILE_DEVICE_ID_INFO_QUERY = "SELECT tenant_id as tenantId, device_profile_id as profileId, id as id FROM device ORDER BY created_time ASC LIMIT %s OFFSET %s"; + String PROFILE_DEVICE_ID_INFO_QUERY = "SELECT tenant_id as tenantId, customer_id as customerId, device_profile_id as profileId, id as id FROM device ORDER BY created_time ASC LIMIT %s OFFSET %s"; return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, DefaultNativeDeviceRepository::toInfo); } @Override public PageData findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) { - String PROFILE_DEVICE_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, device_profile_id as profileId, id as id FROM device WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); + String PROFILE_DEVICE_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, customer_id as customerId, device_profile_id as profileId, id as id FROM device WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); String COUNT_QUERY_BY_TENANT = String.format("SELECT count(id) FROM device WHERE tenant_id = '%s';", tenantId); return find(COUNT_QUERY_BY_TENANT, PROFILE_DEVICE_ID_INFO_QUERY, pageable, DefaultNativeDeviceRepository::toInfo); } @@ -68,8 +70,10 @@ public class DefaultNativeDeviceRepository extends AbstractNativeRepository impl var tenantIdObj = row.get("tenantId"); UUID tenantId = tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(); DeviceId id = new DeviceId((UUID) row.get("id")); + CustomerId customerId = new CustomerId((UUID) row.get("customerId")); + EntityId ownerId = !customerId.isNullUid() ? customerId : TenantId.fromUUID(tenantId); DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId")); - return ProfileEntityIdInfo.create(tenantId, profileId, id); + return ProfileEntityIdInfo.create(tenantId, ownerId, profileId, id); } } 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 index 7fb4d5296a..a07d6b732f 100644 --- 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 @@ -59,6 +59,11 @@ public interface DeviceProfileRepository extends JpaRepository findDeviceProfileInfosByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("deviceProfileIds") List deviceProfileIds); + @Query("SELECT d FROM DeviceProfileEntity d " + "WHERE d.tenantId = :tenantId AND d.isDefault = true") DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId); 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 662dbd83bf..147a030147 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 @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceEntity; @@ -151,6 +152,10 @@ public interface DeviceRepository extends JpaRepository, Exp DeviceEntity findByTenantIdAndName(UUID tenantId, String name); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'DEVICE', a.name) " + + "FROM DeviceEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')") + List findEntityInfosByNamePrefix(UUID tenantId, String prefix); + List findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List deviceIds); List findDevicesByTenantIdAndIdIn(UUID tenantId, List deviceIds); 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 982763c31d..e3721c6584 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 @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfoFilter; 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.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; @@ -114,6 +115,11 @@ public class JpaDeviceDao extends JpaAbstractDao implement DaoUtil.toPageable(pageLink))); } + @Override + public List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + return deviceRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public ListenableFuture> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List deviceIds) { return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(tenantId, deviceIds))); 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 index ced6b8b01a..d07f60aa1e 100644 --- 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 @@ -123,6 +123,11 @@ public class JpaDeviceProfileDao extends JpaAbstractDao findDeviceProfilesByTenantIdAndIds(UUID tenantId, List deviceProfileIds) { + return deviceProfileRepository.findDeviceProfileInfosByTenantIdAndIdIn(tenantId, deviceProfileIds); + } + @Override public DeviceProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); 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 97c8aff01c..fb78aef774 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 @@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.EntityViewFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.EntityViewEntity; @@ -115,10 +116,16 @@ public interface EntityViewRepository extends JpaRepository findEntityInfosByNamePrefix(UUID tenantId, String prefix); + List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId); + List findEntityViewsByTenantIdAndIdIn(UUID tenantId, List entityViewIds); + @Query("SELECT DISTINCT ev.type FROM EntityViewEntity ev WHERE ev.tenantId = :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 26894b2190..3d6fd3f8fe 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 @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; @@ -184,6 +185,11 @@ public class JpaEntityViewDao extends JpaAbstractDao findEntityViewsByTenantIdAndIds(UUID tenantId, List entityViewIds) { + return DaoUtil.convertDataList(entityViewRepository.findEntityViewsByTenantIdAndIdIn(tenantId, entityViewIds)); + } + @Override public PageData findEntityViewsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { log.debug("Try to find entity views by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); @@ -227,6 +233,11 @@ public class JpaEntityViewDao extends JpaAbstractDao findEntityInfosByNamePrefix(TenantId tenantId, String name) { + return entityViewRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyInfoRepository.java similarity index 50% rename from dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyInfoRepository.java index f4ee630dcc..30ad09db0a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyInfoRepository.java @@ -13,24 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.cf; +package org.thingsboard.server.dao.sql.pat; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.ApiKeyInfoEntity; -import java.util.List; import java.util.UUID; -public interface CalculatedFieldLinkRepository extends JpaRepository { +public interface ApiKeyInfoRepository extends JpaRepository { - List findAllByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId); - - List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); - - List findAllByTenantId(UUID tenantId); - - Page findAllByTenantId(UUID tenantId, Pageable pageable); + @Query("SELECT ak FROM ApiKeyInfoEntity ak WHERE ak.tenantId = :tenantId AND ak.userId = :userId AND " + + "(:searchText is NULL OR ilike(ak.description, concat('%', :searchText, '%')) = true)") + Page findByUserId(@Param("tenantId") UUID tenantId, + @Param("userId") UUID userId, + @Param("searchText") String searchText, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java new file mode 100644 index 0000000000..a83223d726 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.ApiKeyEntity; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public interface ApiKeyRepository extends JpaRepository { + + ApiKeyEntity findByValue(String value); + + Page findByTenantId(UUID tenantId, Pageable pageable); + + List findByTenantIdAndUserId(UUID tenantId, UUID userId); + + @Transactional + @Modifying + @Query(value = """ + DELETE FROM api_key + WHERE tenant_id = :tenantId + RETURNING value + """, nativeQuery = true + ) + Set deleteByTenantId(@Param("tenantId") UUID tenantId); + + @Transactional + @Modifying + @Query(value = """ + DELETE FROM api_key + WHERE tenant_id = :tenantId AND user_id = :userId + RETURNING value + """, nativeQuery = true + ) + Set deleteByUserId(@Param("tenantId") UUID tenantId, + @Param("userId") UUID userId); + + @Transactional + @Modifying + @Query("DELETE FROM ApiKeyEntity ak WHERE ak.expirationTime > 0 AND ak.expirationTime < :ts") + int deleteAllByExpirationTimeBefore(@Param("ts") long ts); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java new file mode 100644 index 0000000000..69de36384e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +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.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.ApiKeyEntity; +import org.thingsboard.server.dao.pat.ApiKeyDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Slf4j +@SqlDao +@Component +public class JpaApiKeyDao extends JpaAbstractDao implements ApiKeyDao { + + @Autowired + private ApiKeyRepository apiKeyRepository; + + @Override + public ApiKey findByValue(String value) { + return DaoUtil.getData(apiKeyRepository.findByValue(value)); + } + + @Override + public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(apiKeyRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public List findByTenantIdAndUserId(TenantId tenantId, UserId userId) { + return DaoUtil.convertDataList(apiKeyRepository.findByTenantIdAndUserId(tenantId.getId(), userId.getId())); + } + + @Override + public Set deleteByTenantId(TenantId tenantId) { + return apiKeyRepository.deleteByTenantId(tenantId.getId()); + } + + @Override + public Set deleteByUserId(TenantId tenantId, UserId userId) { + return apiKeyRepository.deleteByUserId(tenantId.getId(), userId.getId()); + } + + @Override + public int deleteAllByExpirationTimeBefore(long ts) { + return apiKeyRepository.deleteAllByExpirationTimeBefore(ts); + } + + @Override + protected Class getEntityClass() { + return ApiKeyEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return apiKeyRepository; + } + + @Override + public EntityType getEntityType() { + return EntityType.API_KEY; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyInfoDao.java new file mode 100644 index 0000000000..0ce3d0d1bc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyInfoDao.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pat; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +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.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.ApiKeyInfoEntity; +import org.thingsboard.server.dao.pat.ApiKeyInfoDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; + +@Slf4j +@SqlDao +@Component +public class JpaApiKeyInfoDao extends JpaAbstractDao implements ApiKeyInfoDao { + + @Autowired + private ApiKeyInfoRepository apiKeyInfoRepository; + + @Override + public PageData findByUserId(TenantId tenantId, UserId userId, PageLink pageLink) { + return DaoUtil.toPageData(apiKeyInfoRepository.findByUserId(tenantId.getId(), userId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + protected Class getEntityClass() { + return ApiKeyInfoEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return apiKeyInfoRepository; + } + +} 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 index 3bbfcc2158..ced8de8e66 100644 --- 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 @@ -117,10 +117,13 @@ public class AlarmDataAdapter { String originatorName = originatorNameObj != null ? originatorNameObj.toString() : null; Object originatorLabelObj = row.get(ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY); String originatorLabel = originatorLabelObj != null ? originatorLabelObj.toString() : null; + Object originatorDisplayNameObj = row.get(ModelConstants.ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY); + String originatorDisplayName = originatorDisplayNameObj != null ? originatorDisplayNameObj.toString() : null; AlarmData alarmData = new AlarmData(alarm, entityId); alarmData.setOriginatorName(originatorName); alarmData.setOriginatorLabel(originatorLabel); + alarmData.setOriginatorDisplayName(originatorDisplayName); if (alarm.getAssigneeId() != null) { alarmData.setAssignee(new AlarmAssignee(alarm.getAssigneeId(), assigneeFirstName, assigneeLastName, assigneeEmail)); } 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 index 673c94c7ec..9d5ae1889b 100644 --- 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 @@ -83,6 +83,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { alarmFieldColumnMap.put(ASSIGNEE_FIRST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY); alarmFieldColumnMap.put(ASSIGNEE_LAST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY); alarmFieldColumnMap.put(ASSIGNEE_EMAIL_KEY, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY); + alarmFieldColumnMap.put("originatorDisplayName", ModelConstants.ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY); } private static final String FIELDS_SELECTION = "select a.id as id," + @@ -106,6 +107,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " a.type as type, " + " a.originator_name as originator_name, " + " a.originator_label as originator_label, " + + " coalesce(a.originator_label, a.originator_name) as originator_display_name, " + " a.assignee_first_name as assignee_first_name, " + " a.assignee_last_name as assignee_last_name, " + " a.assignee_email as assignee_email, " + @@ -319,7 +321,11 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { SqlQueryContext ctx = new SqlQueryContext(new QueryContext(tenantId, null, EntityType.ALARM)); if (query.isSearchPropagatedAlarms()) { - ctx.append("select count(distinct(a.id)) from alarm_info a "); + if (query.getEntityFilter() == null) { + ctx.append("select count(distinct(a.id)) from alarm_info a "); + } else { + ctx.append("select count(a.id) from alarm_info a "); + } ctx.append(JOIN_ENTITY_ALARMS); if (orderedEntityIds != null) { if (orderedEntityIds.isEmpty()) { 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 index a9b04618c8..3417b81e25 100644 --- 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 @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.ApiUsageStateFilter; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.AssetTypeFilter; import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter; @@ -54,6 +55,7 @@ import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -351,25 +353,56 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } }); } else { - List mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); + boolean isOr = operation == ComplexOperation.OR; - List selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection) - .collect(Collectors.toList()); - List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) - .collect(Collectors.toList()); + List mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query); + List selectionMapping = new ArrayList<>(mappings.stream().filter(EntityKeyMapping::isSelection) + .collect(Collectors.toList())); List filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter) .collect(Collectors.toList()); List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() != null) .collect(Collectors.toList()); + // Under OR: entity field filter columns must be in inner SELECT for outer WHERE reference. + // Mirror the ignore=true fix from findEntityDataByQuery so the inner subquery still emits the + // extra column but downstream response shape (benign for count) stays symmetric with the data path. + if (isOr) { + for (EntityKeyMapping m : entityFieldsFiltersMapping) { + if (!selectionMapping.contains(m)) { + m.setIgnore(true); + selectionMapping.add(m); + } + } + } + + List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) .collect(Collectors.toList()); + // Under OR: entity field filters move to outer WHERE (not inner WHERE) + List innerEntityFieldsFilters = isOr ? Collections.emptyList() : entityFieldsFiltersMapping; + + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), innerEntityFieldsFilters); + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr); + + // Under OR: combine ALL filter mappings (entity fields + aliases) with OR joiner + // Place at the middle layer (after JOINs) where alias table references are visible. + // Under AND: place at the outer result layer (existing behavior). + String aliasWhereQuery; + if (isOr) { + String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR); + if (!combinedFilterQuery.isEmpty()) { + latestJoinsCnt += " where (" + combinedFilterQuery + ")"; + } + aliasWhereQuery = ""; + } else { + aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, ""); + } - String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); - String aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, ""); - String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); String entityTypeStr; if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { @@ -419,13 +452,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { EntityType entityType = resolveEntityType(query.getEntityFilter()); SqlQueryContext ctx = new SqlQueryContext(new QueryContext(tenantId, customerId, entityType, ignorePermissionCheck)); EntityDataPageLink pageLink = query.getPageLink(); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); + boolean isOr = operation == ComplexOperation.OR; List mappings = EntityKeyMapping.prepareKeyMapping(entityType, query); - List selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection) - .collect(Collectors.toList()); - List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) - .collect(Collectors.toList()); + List selectionMapping = new ArrayList<>(mappings.stream().filter(EntityKeyMapping::isSelection) + .collect(Collectors.toList())); List latestSelectionMapping = selectionMapping.stream().filter(EntityKeyMapping::isLatest) .collect(Collectors.toList()); @@ -434,14 +467,52 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() != null) .collect(Collectors.toList()); + // Under OR: entity field filter columns must be in inner SELECT for outer WHERE reference. + // Mark force-added filter-only mappings as ignored so EntityDataAdapter does not expose + // them in EntityData.latest — keeps the response shape identical to AND. + if (isOr) { + for (EntityKeyMapping m : entityFieldsFiltersMapping) { + if (!selectionMapping.contains(m)) { + m.setIgnore(true); + selectionMapping.add(m); + } + } + } + + List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) .collect(Collectors.toList()); + // Under OR: entity field filters move to outer WHERE (not inner WHERE) + List innerEntityFieldsFilters = isOr ? Collections.emptyList() : entityFieldsFiltersMapping; + + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), innerEntityFieldsFilters); + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr); + String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false, isOr); + + // Under OR: combine ALL filter mappings (entity fields + aliases) with OR joiner + // Place at the middle layer (after JOINs) where alias table references are visible. + // Under AND: place at the outer result layer (existing behavior). + String aliasWhereQuery; + if (isOr) { + String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR); + String middleWhere = ""; + if (!combinedFilterQuery.isEmpty()) { + middleWhere = " where (" + combinedFilterQuery + ")"; + } + String searchTextQuery = buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch()); + if (!searchTextQuery.isEmpty()) { + middleWhere += (middleWhere.isEmpty() ? " where " : " and ") + "(" + searchTextQuery + ") "; + } + latestJoinsCnt += middleWhere; + latestJoinsData += middleWhere; + aliasWhereQuery = ""; + } else { + aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, pageLink.getTextSearch()); + } - 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 aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, pageLink.getTextSearch()); String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); String entityTypeStr; if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { 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 index c259d48372..ed4bb8eeb2 100644 --- 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 @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -65,6 +66,7 @@ public class EntityKeyMapping { public static final String NAME = "name"; public static final String TYPE = "type"; public static final String LABEL = "label"; + public static final String DISPLAY_NAME = "displayName"; public static final String FIRST_NAME = "firstName"; public static final String LAST_NAME = "lastName"; public static final String EMAIL = "email"; @@ -83,6 +85,8 @@ public class EntityKeyMapping { public static final String SERVICE_ID = "serviceId"; public static final String OWNER_NAME = "ownerName"; public static final String OWNER_TYPE = "ownerType"; + public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(e." + LABEL + "), ''), e." + NAME + ")"; + public static final String USER_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(CONCAT_WS(' ', e.first_name, e.last_name)), ''), e.email)"; public static final String OWNER_NAME_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + "then (select title from tenant where id = e.tenant_id) " + "else (select title from customer where id = e.customer_id) end"; @@ -94,6 +98,16 @@ public class EntityKeyMapping { OWNER_NAME, OWNER_NAME_SELECT_QUERY, OWNER_TYPE, OWNER_TYPE_SELECT_QUERY ); + public static final Map labeledPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY + ); + public static final Map userPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, USER_DISPLAY_NAME_SELECT_QUERY + ); public static final Map queueStatsPropertiesFunctions = Map.of(NAME, QUEUE_STATS_NAME_QUERY); public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); @@ -153,20 +167,24 @@ public class EntityKeyMapping { Map contactBasedAliases = new HashMap<>(); contactBasedAliases.put(NAME, TITLE); contactBasedAliases.put(LABEL, TITLE); + contactBasedAliases.put(DISPLAY_NAME, TITLE); aliases.put(EntityType.TENANT, contactBasedAliases); aliases.put(EntityType.CUSTOMER, contactBasedAliases); aliases.put(EntityType.DASHBOARD, contactBasedAliases); + Map deviceAndAssetAliases = new HashMap<>(); + deviceAndAssetAliases.put(TITLE, NAME); + aliases.put(EntityType.DEVICE, deviceAndAssetAliases); + aliases.put(EntityType.ASSET, deviceAndAssetAliases); Map commonEntityAliases = new HashMap<>(); commonEntityAliases.put(TITLE, NAME); - aliases.put(EntityType.DEVICE, commonEntityAliases); - aliases.put(EntityType.ASSET, commonEntityAliases); + commonEntityAliases.put(DISPLAY_NAME, NAME); aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases); aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases); - propertiesFunctions.put(EntityType.DEVICE, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.ASSET, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.DEVICE, labeledPropertiesFunctions); + propertiesFunctions.put(EntityType.ASSET, labeledPropertiesFunctions); propertiesFunctions.put(EntityType.ENTITY_VIEW, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.USER, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.USER, userPropertiesFunctions); propertiesFunctions.put(EntityType.DASHBOARD, ownerPropertiesFunctions); propertiesFunctions.put(EntityType.QUEUE_STATS, queueStatsPropertiesFunctions); @@ -266,16 +284,39 @@ public class EntityKeyMapping { } public Stream toQueries(SqlQueryContext ctx, EntityFilterType filterType) { - if (hasFilter()) { - String keyAlias = (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) ? "e" : alias; - return keyFilters.stream().map(keyFilter -> - this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType)); - } else { + return toQueries(ctx, filterType, false); + } + + public Stream toQueries(SqlQueryContext ctx, EntityFilterType filterType, boolean outerContext) { + if (!hasFilter()) { return Stream.empty(); } + String keyAlias; + boolean useAliasDirectly = false; + if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) { + if (outerContext) { + // In the middle layer (OR relocation), entity field columns are exposed + // by their alias name from the inner subquery SELECT (e.g., "alias2" from + // "cast(e.name as varchar) as alias2"), so buildSimplePredicateQuery must + // use the alias directly as the field instead of appending entityKeyColumn. + keyAlias = alias; + useAliasDirectly = true; + } else { + keyAlias = "e"; + } + } else { + keyAlias = alias; + } + final boolean aliasAsField = useAliasDirectly; + return keyFilters.stream().map(keyFilter -> + this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType, aliasAsField)); } public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType) { + return toLatestJoin(ctx, entityFilter, entityType, false); + } + + public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, boolean forceLeftJoin) { String entityTypeStr; if (entityFilter.getType().equals(EntityFilterType.RELATIONS_QUERY)) { entityTypeStr = "entities.entity_type"; @@ -283,20 +324,23 @@ public class EntityKeyMapping { entityTypeStr = "'" + entityType.name() + "'"; } ctx.addStringParameter(getKeyId(), entityKey.getKey()); - String filterQuery = toQueries(ctx, entityFilter.getType()) + // Under OR (forceLeftJoin=true) the filter predicate is re-emitted by buildQuery at the middle + // layer with disjunction semantics. Inlining it in the ON clause here would duplicate the + // predicate with a different bound parameter name and double-filter the LEFT JOIN. + String filterQuery = forceLeftJoin ? "" : toQueries(ctx, entityFilter.getType()) .filter(StringUtils::isNotEmpty) .collect(Collectors.joining(" and ")); if (StringUtils.isNotEmpty(filterQuery)) { filterQuery = " AND (" + filterQuery + ")"; } if (entityKey.getType().equals(EntityKeyType.TIME_SERIES)) { - String join = (hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join"; + String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "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 key_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() && hasFilterValues(ctx)) ? "inner join" : "left join"; + String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join"; query = String.format("%s attribute_kv %s ON %s.entity_id=entities.id AND %s.attribute_key=(select key_id from key_dictionary where key = :%s_key_id) ", join, alias, alias, alias, alias); int scope; @@ -309,7 +353,7 @@ public class EntityKeyMapping { } query = String.format("%s AND %s.attribute_type=%s %s", query, alias, scope, filterQuery); } else { - String join = (hasFilter() && hasFilterValues(ctx)) ? "join LATERAL" : "left join LATERAL"; + String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "join LATERAL" : "left join LATERAL"; query = String.format("%s (select * from attribute_kv %s WHERE %s.entity_id=entities.id AND %s.attribute_key=(select key_id from key_dictionary where key = :%s_key_id) %s " + "ORDER BY %s.last_update_ts DESC limit 1) as %s ON true", join, alias, alias, alias, alias, filterQuery, alias, alias); @@ -334,27 +378,47 @@ public class EntityKeyMapping { } public static String buildLatestJoins(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, List latestMappings, boolean countQuery) { + return buildLatestJoins(ctx, entityFilter, entityType, latestMappings, countQuery, false); + } + + public static String buildLatestJoins(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, + List latestMappings, boolean countQuery, boolean forceLeftJoin) { return latestMappings.stream() .filter(mapping -> !countQuery || mapping.hasFilter()) - .map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType)) + .map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType, forceLeftJoin)) .collect(Collectors.joining(" ")); } public static String buildQuery(SqlQueryContext ctx, List mappings, EntityFilterType filterType) { + return buildQuery(ctx, mappings, filterType, ComplexOperation.AND); + } + + public static String buildQuery(SqlQueryContext ctx, List mappings, + EntityFilterType filterType, ComplexOperation operation) { + String joiner = (operation == ComplexOperation.OR) ? " OR " : " AND "; + // Vacuously-true predicates (e.g. a ComplexFilterPredicate with zero nested predicates producing + // an empty string) are dropped here. Under AND this is safe — TRUE is the identity for AND — but + // under OR it silently narrows the disjunction. Callers must not emit meaningful empty predicates. return mappings.stream() - .flatMap(mapping -> mapping.toQueries(ctx, filterType)) + .flatMap(mapping -> mapping.toQueries(ctx, filterType, operation == ComplexOperation.OR)) .filter(StringUtils::isNotEmpty) - .collect(Collectors.joining(" AND ")); + .collect(Collectors.joining(joiner)); } public static List prepareKeyMapping(EntityType entityType, EntityDataQuery query) { EntityFilterType entityFilterType = query.getEntityFilter().getType(); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); 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(); + Map> filters; + if (operation == ComplexOperation.OR) { + // Under OR, don't group same-key filters; handle individually below + filters = Collections.emptyMap(); + } else { + 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; @@ -411,14 +475,37 @@ public class EntityKeyMapping { for (EntityKeyMapping mapping : mappings) { mapping.setIndex(index); mapping.setAlias(String.format("alias%s", index)); - mapping.setKeyFilters(filters.remove(mapping.entityKey)); + if (operation != ComplexOperation.OR) { + // Under AND: assign grouped filters to matching selection mappings + mapping.setKeyFilters(filters.remove(mapping.entityKey)); + } if (mapping.getEntityKey().getType().equals(EntityKeyType.ENTITY_FIELD)) { index++; } else { index += 2; } } - if (!filters.isEmpty()) { + if (operation == ComplexOperation.OR && query.getKeyFilters() != null) { + // Under OR: each KeyFilter gets its own EntityKeyMapping (no same-key grouping) + for (KeyFilter keyFilter : query.getKeyFilters()) { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setIndex(index); + mapping.setAlias(String.format("alias%s", index)); + mapping.setKeyFilters(Collections.singletonList(keyFilter)); + EntityKey filterKey = keyFilter.getKey(); + mapping.setLatest(!filterKey.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setEntityKey(filterKey); + mapping.setEntityKeyColumn(entityType, entityFilterType); + mapping.setSelection(mapping.getEntityKeyColumn() == null); + mappings.add(mapping); + if (filterKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { + index += 1; + } else { + index += 2; + } + } + } else if (!filters.isEmpty()) { + // Under AND: create mappings for filter-only keys not in selection for (EntityKey filterField : filters.keySet()) { EntityKeyMapping mapping = new EntityKeyMapping(); mapping.setIndex(index); @@ -447,25 +534,45 @@ public class EntityKeyMapping { public static List prepareEntityCountKeyMapping(EntityCountQuery query) { EntityType entityType = resolveEntityType(query.getEntityFilter()); EntityFilterType entityFilterType = query.getEntityFilter().getType(); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); - Map> filters = - query.getKeyFilters() != null ? - query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); int index = 2; List mappings = new ArrayList<>(); - if (!filters.isEmpty()) { - for (EntityKey filterField : filters.keySet()) { + + if (operation == ComplexOperation.OR && query.getKeyFilters() != null && !query.getKeyFilters().isEmpty()) { + // Under OR: each KeyFilter gets its own mapping (no same-key grouping) + for (KeyFilter keyFilter : query.getKeyFilters()) { 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.setEntityKey(filterField); + mapping.setKeyFilters(Collections.singletonList(keyFilter)); + EntityKey filterKey = keyFilter.getKey(); + mapping.setLatest(!filterKey.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setEntityKey(filterKey); mapping.setEntityKeyColumn(entityType, entityFilterType); mapping.setSelection(mapping.getEntityKeyColumn() == null); mappings.add(mapping); index += 1; } + } else { + // Under AND (or null/default): group same-key filters together + Map> filters = + query.getKeyFilters() != null ? + query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); + 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.setEntityKey(filterField); + mapping.setEntityKeyColumn(entityType, entityFilterType); + mapping.setSelection(mapping.getEntityKeyColumn() == null); + mappings.add(mapping); + index += 1; + } + } } return mappings; @@ -512,22 +619,27 @@ public class EntityKeyMapping { private String buildKeyQuery(SqlQueryContext ctx, String alias, KeyFilter keyFilter, EntityFilterType filterType) { - return this.buildPredicateQuery(ctx, alias, keyFilter.getKey(), keyFilter.getPredicate(), filterType); + return this.buildKeyQuery(ctx, alias, keyFilter, filterType, false); + } + + private String buildKeyQuery(SqlQueryContext ctx, String alias, KeyFilter keyFilter, + EntityFilterType filterType, boolean useAliasAsField) { + return this.buildPredicateQuery(ctx, alias, keyFilter.getKey(), keyFilter.getPredicate(), filterType, useAliasAsField); } private String buildPredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, - KeyFilterPredicate predicate, EntityFilterType filterType) { + KeyFilterPredicate predicate, EntityFilterType filterType, boolean useAliasAsField) { if (predicate.getType().equals(FilterPredicateType.COMPLEX)) { - return this.buildComplexPredicateQuery(ctx, alias, key, (ComplexFilterPredicate) predicate, filterType); + return this.buildComplexPredicateQuery(ctx, alias, key, (ComplexFilterPredicate) predicate, filterType, useAliasAsField); } else { - return this.buildSimplePredicateQuery(ctx, alias, key, predicate, filterType); + return this.buildSimplePredicateQuery(ctx, alias, key, predicate, filterType, useAliasAsField); } } private String buildComplexPredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, - ComplexFilterPredicate predicate, EntityFilterType filterType) { + ComplexFilterPredicate predicate, EntityFilterType filterType, boolean useAliasAsField) { String result = predicate.getPredicates().stream() - .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType)) + .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType, useAliasAsField)) .filter(StringUtils::isNotEmpty) .collect(Collectors.joining(" " + predicate.getOperation().name() + " ")); if (!result.trim().isEmpty()) { @@ -537,9 +649,9 @@ public class EntityKeyMapping { } private String buildSimplePredicateQuery(SqlQueryContext ctx, String alias, EntityKey key, - KeyFilterPredicate predicate, EntityFilterType filterType) { + KeyFilterPredicate predicate, EntityFilterType filterType, boolean useAliasAsField) { if (key.getType().equals(EntityKeyType.ENTITY_FIELD)) { - String field = (getEntityKeyColumn() != null) ? alias + "." + getEntityKeyColumn() : alias; + String field = useAliasAsField || getEntityKeyColumn() == null ? alias : alias + "." + getEntityKeyColumn(); if (predicate.getType().equals(FilterPredicateType.NUMERIC)) { return this.buildNumericPredicateQuery(ctx, field, (NumericFilterPredicate) predicate); } else if (predicate.getType().equals(FilterPredicateType.STRING)) { 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 18d773a6ca..4648f53e19 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 @@ -25,6 +25,9 @@ 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.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; @@ -43,6 +46,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY; @@ -288,4 +292,106 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple return DaoUtil.convertDataList(relationRepository.findRuleNodeToRuleChainRelations(ruleChainType, PageRequest.of(0, limit))); } + @Override + public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery query, int limit) { + List levels = query.levels(); + if (levels == null || levels.isEmpty()) { + return List.of(); + } + if (limit <= 0) { + return List.of(); + } + String sql = buildRelationPathSql(query); + Object[] params = buildRelationPathParams(query, limit); + + log.trace("[{}] relation path query: {}", tenantId, sql); + + return jdbcTemplate.queryForList(sql, params).stream() + .map(row -> { + var entityRelation = new EntityRelation(); + var fromId = (UUID) row.get(RELATION_FROM_ID_PROPERTY); + var fromType = (String) row.get(RELATION_FROM_TYPE_PROPERTY); + var toId = (UUID) row.get(RELATION_TO_ID_PROPERTY); + var toType = (String) row.get(RELATION_TO_TYPE_PROPERTY); + var grp = (String) row.get(RELATION_TYPE_GROUP_PROPERTY); + var type = (String) row.get(RELATION_TYPE_PROPERTY); + var version = (Long) row.get(VERSION_COLUMN); + + entityRelation.setFrom(EntityIdFactory.getByTypeAndUuid(fromType, fromId)); + entityRelation.setTo(EntityIdFactory.getByTypeAndUuid(toType, toId)); + entityRelation.setType(type); + entityRelation.setTypeGroup(RelationTypeGroup.valueOf(grp)); + entityRelation.setVersion(version); + return entityRelation; + }) + .collect(Collectors.toList()); + } + + private Object[] buildRelationPathParams(EntityRelationPathQuery query, int limit) { + final List params = new ArrayList<>(); + // seed + params.add(query.rootEntityId().getId()); + params.add(query.rootEntityId().getEntityType().name()); + + // levels + for (var lvl : query.levels()) { + params.add(lvl.relationType()); + } + + // limit + params.add(limit); + + return params.toArray(); + } + + private static String buildRelationPathSql(EntityRelationPathQuery query) { + List levels = query.levels(); + StringBuilder sb = new StringBuilder(); + + sb.append("WITH seed AS (\n") + .append(" SELECT ?::uuid AS id, ?::varchar AS type\n") + .append(")"); + + String prev = "seed"; + for (int i = 0; i < levels.size() - 1; i++) { + RelationPathLevel lvl = levels.get(i); + boolean down = lvl.direction() == EntitySearchDirection.FROM; + + String cur = "lvl" + (i + 1); + String joinCond = down + ? "r.from_id = p.id AND r.from_type = p.type" + : "r.to_id = p.id AND r.to_type = p.type"; + String selectNext = down + ? "r.to_id AS id, r.to_type AS type" + : "r.from_id AS id, r.from_type AS type"; + + sb.append(",\n").append(cur).append(" AS (\n") + .append(" SELECT ").append(selectNext).append("\n") + .append(" FROM ").append(RELATION_TABLE_NAME).append(" r\n") + .append(" JOIN ").append(prev).append(" p ON ").append(joinCond).append("\n") + .append(" WHERE r.relation_type_group = '").append(RelationTypeGroup.COMMON).append("'\n") + .append(" AND r.relation_type = ?\n") + .append(")"); + prev = cur; + } + + RelationPathLevel last = levels.get(levels.size() - 1); + boolean lastDown = last.direction() == EntitySearchDirection.FROM; + String prevForLast = (levels.size() == 1) ? "seed" : prev; + String lastJoin = lastDown + ? "r.from_id = p.id AND r.from_type = p.type" + : "r.to_id = p.id AND r.to_type = p.type"; + + sb.append("\n") + .append("SELECT r.from_id, r.from_type, r.to_id, r.to_type,\n") + .append(" r.relation_type_group, r.relation_type, r.version\n") + .append("FROM ").append(RELATION_TABLE_NAME).append(" r\n") + .append("JOIN ").append(prevForLast).append(" p ON ").append(lastJoin).append("\n") + .append("WHERE r.relation_type_group = '").append(RelationTypeGroup.COMMON).append("'\n") + .append(" AND r.relation_type = ?\n") + .append("LIMIT ?"); + + return sb.toString(); + } + } 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 691711da08..0e821a41b2 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 @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.relation; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -96,4 +95,5 @@ public interface RelationRepository @Param("toId") UUID toId, @Param("toType") String toType, @Param("batchSize") int batchSize); + } 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 cba859e767..a72b053a98 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 @@ -112,6 +112,11 @@ public class JpaRuleChainDao extends JpaAbstractDao return DaoUtil.convertDataList(ruleChainRepository.findByTenantIdAndTypeAndName(tenantId.getId(), type, name)); } + @Override + public List findRuleChainsByTenantIdAndIds(UUID tenantId, List ruleChainIds) { + return DaoUtil.convertDataList(ruleChainRepository.findRuleChainsByTenantIdAndIdIn(tenantId, ruleChainIds)); + } + @Override public Long countByTenantId(TenantId tenantId) { return ruleChainRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDetailsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDetailsDao.java new file mode 100644 index 0000000000..3381bae386 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDetailsDao.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChainDetails; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.RuleChainDetailsEntity; +import org.thingsboard.server.dao.rule.RuleChainDetailsDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.exception.DataValidationException; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Component +@SqlDao +@RequiredArgsConstructor +public class JpaRuleChainDetailsDao extends JpaAbstractDao implements RuleChainDetailsDao { + + private final RuleChainDetailsRepository ruleChainDetailsRepository; + + @Override + public RuleChainDetails findById(TenantId tenantId, UUID key) { + Optional entity = getRepository().findById(key); + // Detaching to avoid stale version conflict with RuleChainEntity which maps to the same table. + // Without detach, a loaded RuleChainDetailsEntity stays in the persistence context and becomes stale + // when the same row is updated via RuleChainEntity (e.g. during rule chain import with circular references). + entity.ifPresent(e -> getEntityManager().detach(e)); + return DaoUtil.getData(entity); + } + + @Override + protected RuleChainDetailsEntity doSave(RuleChainDetailsEntity entity, boolean isNew, boolean flush) { + try { + return super.doSave(entity, isNew, flush); + } catch (Exception e) { + String rootMsg = ExceptionUtils.getRootCauseMessage(e); + if (StringUtils.contains(rootMsg, "value too long")) { + throw new DataValidationException("Rule chain notes data is too large. Please reduce the number or size of notes."); + } + throw e; + } + } + + @Override + protected Class getEntityClass() { + return RuleChainDetailsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return ruleChainDetailsRepository; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainDetailsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainDetailsRepository.java new file mode 100644 index 0000000000..df54720984 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainDetailsRepository.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.jpa.repository.JpaRepository; +import org.thingsboard.server.dao.model.sql.RuleChainDetailsEntity; + +import java.util.UUID; + +public interface RuleChainDetailsRepository extends JpaRepository { + +} 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 3ace8a585d..66e2bfd7b0 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 @@ -90,4 +90,7 @@ public interface RuleChainRepository extends JpaRepository :id ORDER BY r.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + List findRuleChainsByTenantIdAndIdIn(UUID tenantId, List ruleChainIds); + } 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 a41d719b49..4a10efdcec 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 @@ -98,6 +98,11 @@ public class JpaTenantDao extends JpaAbstractDao implement return DaoUtil.getData(tenantRepository.findFirstByTitle(name)); } + @Override + public List findTenantsByIds(UUID tenantId, List tenantIds) { + return DaoUtil.convertDataList(tenantRepository.findTenantsByIdIn(tenantIds)); + } + @Override public List findNextBatch(UUID id, int batchSize) { return tenantRepository.findNextBatch(id, Limit.of(batchSize)); 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 d38a5fd513..3b0eb13a93 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 @@ -59,4 +59,6 @@ public interface TenantRepository extends JpaRepository { TenantEntity findFirstByTitle(String name); + List findTenantsByIdIn(List tenantIds); + } 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 e9dbcce733..696e098636 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 @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.edqs.fields.UserFields; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -28,6 +29,7 @@ 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.security.Authority; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.UserEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -136,11 +138,22 @@ public class JpaUserDao extends JpaAbstractDao implements User DaoUtil.toPageable(pageLink))); } + @Override + public UserAuthDetails findUserAuthDetailsByUserId(UUID tenantId, UUID userId) { + TbPair result = userRepository.findUserAuthDetailsByUserId(userId); + return result != null ? new UserAuthDetails(result.getFirst().toData(), result.getSecond()) : null; + } + @Override public int countTenantAdmins(UUID tenantId) { return userRepository.countByTenantIdAndAuthority(tenantId, Authority.TENANT_ADMIN); } + @Override + public List findUsersByTenantIdAndIds(UUID tenantId, List userIds) { + return DaoUtil.convertDataList(userRepository.findUsersByTenantIdAndIdIn(tenantId, userIds)); + } + @Override public Long countByTenantId(TenantId tenantId) { return userRepository.countByTenantId(tenantId.getId()); 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 1d9417fbfa..b63c42aa8c 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 @@ -23,15 +23,13 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.edqs.fields.UserFields; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.model.sql.UserEntity; import java.util.Collection; import java.util.List; import java.util.UUID; -/** - * @author Valerii Sosliuk - */ public interface UserRepository extends JpaRepository { UserEntity findByEmail(String email); @@ -80,4 +78,11 @@ public interface UserRepository extends JpaRepository { List findNextBatch(@Param("id") UUID id, Limit limit); int countByTenantIdAndAuthority(UUID tenantId, Authority authority); + + @Query("SELECT new org.thingsboard.server.common.data.util.TbPair(u, uc.enabled) " + + "FROM UserEntity u JOIN UserCredentialsEntity uc ON u.id = uc.userId WHERE u.id = :userId ") + TbPair findUserAuthDetailsByUserId(@Param("userId") UUID userId); + + List findUsersByTenantIdAndIdIn(UUID tenantId, List userIds); + } 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 7ffdd38092..e1c6341203 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 @@ -108,6 +108,11 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findSystemOrTenantWidgetBundlesByIds(UUID tenantId, List widgetsBundleIds) { + return DaoUtil.convertDataList(widgetsBundleRepository.findSystemOrTenantWidgetsBundlesByIdIn(tenantId, TenantId.NULL_UUID, widgetsBundleIds)); + } + private PageData findTenantWidgetsBundlesByTenantIds(List tenantIds, WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { if (widgetsBundleFilter.isFullSearch()) { return DaoUtil.toPageData( 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 4a4d062006..1830c7fefc 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 @@ -142,4 +142,11 @@ public interface WidgetsBundleRepository extends JpaRepository :id ORDER BY w.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE " + + "wb.id IN (:widgetsBundleIds) AND (wb.tenantId = :tenantId OR wb.tenantId = :systemTenantId)") + List findSystemOrTenantWidgetsBundlesByIdIn(@Param("tenantId") UUID tenantId, + @Param("systemTenantId") UUID systemTenantId, + @Param("widgetsBundleIds") List widgetsBundleIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index a97799a032..143ab53e18 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -167,5 +167,19 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries return sqlDao.findAllKeysByEntityIds(tenantId, entityIds); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return sqlDao.findAllKeysByEntityIdsAsync(tenantId, entityIds); + } + + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + return sqlDao.findLatestByEntityIds(tenantId, entityIds); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return sqlDao.findLatestByEntityIdsAsync(tenantId, entityIds); + } } 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 index 38962b45b1..b33ceca63b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -22,9 +22,9 @@ import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -38,8 +38,6 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; @@ -57,6 +55,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -64,7 +63,6 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.function.Function; -import java.util.stream.Collectors; @Slf4j @Component @@ -185,9 +183,28 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @Override public List findAllKeysByEntityIds(TenantId tenantId, List entityIds) { - return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList())); + return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).toList()); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return service.submit(() -> findAllKeysByEntityIds(tenantId, entityIds)); + } + + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + if (CollectionUtils.isEmpty(entityIds)) { + return Collections.emptyList(); + } + return DaoUtil.convertDataList( + searchTsKvLatestRepository.findLatestByEntityIds(entityIds.stream().map(EntityId::getId).toList()) + ); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return service.submit(() -> findLatestByEntityIds(tenantId, entityIds)); + } private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query, Long version) { ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); @@ -211,7 +228,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme ReadTsKvQueryResult::getData, MoreExecutors.directExecutor()); } - protected TsKvEntry doFindLatestSync(EntityId entityId, String key) { + protected TsKvEntry doFindLatestSync(EntityId entityId, String key) { TsKvLatestCompositeKey compositeKey = new TsKvLatestCompositeKey( entityId.getId(), 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 0ab1994059..8b3a66612c 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 @@ -34,6 +34,20 @@ public class SearchTsKvLatestRepository { " ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, ts_kv_latest.ts AS ts, ts_kv_latest.version AS version FROM ts_kv_latest " + "INNER JOIN key_dictionary ON ts_kv_latest.key = key_dictionary.key_id WHERE ts_kv_latest.entity_id = cast(:id AS uuid)"; + public static final String FIND_LATEST_BY_ENTITY_IDS = "findLatestByEntityIds"; + + public static final String FIND_LATEST_BY_ENTITY_IDS_QUERY = """ + SELECT DISTINCT ON (ts_kv_latest.key) + ts_kv_latest.entity_id AS entityId, ts_kv_latest.key AS key, + key_dictionary.key AS strKey, ts_kv_latest.str_v AS strValue, + ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, + ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, + ts_kv_latest.ts AS ts, ts_kv_latest.version AS version + FROM ts_kv_latest + INNER JOIN key_dictionary ON ts_kv_latest.key = key_dictionary.key_id + WHERE ts_kv_latest.entity_id IN (:entityIds) + ORDER BY ts_kv_latest.key, ts_kv_latest.ts DESC"""; + @PersistenceContext private EntityManager entityManager; @@ -43,4 +57,10 @@ public class SearchTsKvLatestRepository { .getResultList(); } + public List findLatestByEntityIds(List entityIds) { + return entityManager.createNamedQuery(FIND_LATEST_BY_ENTITY_IDS, TsKvLatestEntity.class) + .setParameter("entityIds", entityIds) + .getResultList(); + } + } 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 b1840e75a8..87958eb0cc 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 @@ -42,4 +42,6 @@ public interface TenantDao extends Dao { Tenant findTenantByName(TenantId tenantId, String name); + List findTenantsByIds(UUID tenantId, List tenantIds); + } 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 index db823fd22a..af62feb226 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -35,10 +35,10 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -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.exception.DataValidationException; import java.util.ArrayList; import java.util.List; @@ -190,7 +190,7 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); validateId(tenantProfileId, id -> INCORRECT_TENANT_PROFILE_ID + id); @@ -198,22 +198,18 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService findTenantsByIds(TenantId callerId, List tenantIds) { + log.trace("Executing findTenantsByIds, callerId [{}], tenantIds [{}]", callerId, tenantIds); + return tenantDao.findTenantsByIds(callerId.getId(), toUUIDs(tenantIds)); + } + @Override public boolean tenantExists(TenantId tenantId) { return existsTenantCache.getAndPutInTransaction(tenantId, () -> tenantDao.existsById(tenantId, tenantId.getId()), false); 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 2b14ffdae3..70e04eb5e3 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 @@ -55,9 +55,6 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.StringUtils.isBlank; -/** - * @author Andrew Shvayka - */ @Service @Slf4j public class BaseTimeseriesService implements TimeseriesService { @@ -156,6 +153,21 @@ public class BaseTimeseriesService implements TimeseriesService { return timeseriesLatestDao.findAllKeysByEntityIds(tenantId, entityIds); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return timeseriesLatestDao.findAllKeysByEntityIdsAsync(tenantId, entityIds); + } + + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + return timeseriesLatestDao.findLatestByEntityIds(tenantId, entityIds); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return timeseriesLatestDao.findLatestByEntityIdsAsync(tenantId, entityIds); + } + @Override public void cleanup(long systemTtl) { timeseriesDao.cleanup(systemTtl); @@ -300,13 +312,13 @@ public class BaseTimeseriesService implements TimeseriesService { long interval = query.getInterval(); if (interval < 1) { throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval + - ". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'."); + ". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'."); } long step = Math.max(interval, 1000); long intervalCounts = (query.getEndTs() - query.getStartTs()) / step; if (intervalCounts > maxTsIntervals || intervalCounts < 0) { throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " + - "Please increase 'interval' parameter for your query or reduce the time range of the query."); + "Please increase 'interval' parameter for your query or reduce the time range of the query."); } } } 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 index 72f667bfcc..e9bf3db134 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -36,17 +36,13 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao; import org.thingsboard.server.dao.util.NoSqlTsLatestDao; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; @@ -103,6 +99,20 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return Collections.emptyList(); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return Futures.immediateFuture(Collections.emptyList()); + } + + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + return Collections.emptyList(); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return Futures.immediateFuture(Collections.emptyList()); + } @Override public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { 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 index 6685b862f1..c7582944a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -22,12 +22,8 @@ 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 org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import java.util.List; -import java.util.Map; import java.util.Optional; public interface TimeseriesLatestDao { @@ -54,4 +50,15 @@ public interface TimeseriesLatestDao { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); + ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds); + + /** + * For each unique timeseries key across the given entities, returns the single most recent {@link TsKvEntry} + * (i.e. the entry with the highest timestamp). If the same key exists on multiple entities, + * only the freshest value is kept. Useful for discovering available keys together with a representative sample value. + */ + List findLatestByEntityIds(TenantId tenantId, List entityIds); + + ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds); + } 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 da0df65ef5..fe23c9cc0b 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.user; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; @@ -102,4 +103,9 @@ public interface UserDao extends Dao, TenantEntityDao { PageData findByAuthorityAndTenantProfilesIds(Authority authority, List tenantProfilesIds, PageLink pageLink); int countTenantAdmins(UUID tenantId); + + UserAuthDetails findUserAuthDetailsByUserId(UUID tenantId, UUID userId); + + List findUsersByTenantIdAndIds(UUID tenantId, List userIds); + } 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 a5fb56e7eb..886c9e46c3 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 @@ -35,6 +35,7 @@ import org.thingsboard.server.cache.user.UserCacheKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -45,6 +46,11 @@ import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.mobile.MobileSessionInfo; import org.thingsboard.server.common.data.mobile.UserMobileSessionInfo; +import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; @@ -59,10 +65,12 @@ import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.sql.JpaExecutorService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.ArrayList; import java.util.Collections; @@ -72,9 +80,12 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -86,8 +97,9 @@ public class UserServiceImpl extends AbstractCachedEntityService userValidator; private final DataValidator userCredentialsValidator; private final ApplicationEventPublisher eventPublisher; @@ -161,8 +175,23 @@ public class UserServiceImpl extends AbstractCachedEntityService doSaveUser(tenantId, user, doValidate)); + } + + private User doSaveUser(TenantId tenantId, User user, boolean doValidate) { log.trace("Executing saveUser [{}]", user); - User oldUser = userValidator.validate(user, User::getTenantId); + User oldUser = null; + if (doValidate) { + oldUser = userValidator.validate(user, User::getTenantId); + } else if (user.getId() != null) { + oldUser = findUserById(user.getTenantId(), user.getId()); + } if (!userLoginCaseSensitive) { user.setEmail(user.getEmail().toLowerCase()); } @@ -217,8 +246,15 @@ public class UserServiceImpl extends AbstractCachedEntityService tenantId); + if (doValidate) { + userCredentialsValidator.validate(userCredentials, data -> tenantId); + } UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); eventPublisher.publishEvent(ActionEntityEvent.builder() .tenantId(tenantId) @@ -296,7 +332,7 @@ public class UserServiceImpl extends AbstractCachedEntityService INCORRECT_USER_CREDENTIALS_ID + id); + userCredentialsDao.removeById(tenantId, userCredentialsId.getId()); + } + @Override @Transactional public void deleteUser(TenantId tenantId, User user) { @@ -334,6 +379,7 @@ public class UserServiceImpl extends AbstractCachedEntityService INCORRECT_USER_ID + id); userCredentialsDao.removeByUserId(tenantId, userId); userAuthSettingsDao.removeByUserId(userId); + apiKeyService.deleteByUserId(tenantId, userId); publishEvictEvent(new UserCacheEvictEvent(user.getTenantId(), user.getEmail(), null)); userSettingsDao.removeByUserId(tenantId, userId); userDao.removeById(tenantId, userId.getId()); @@ -492,6 +538,19 @@ public class UserServiceImpl extends AbstractCachedEntityService INCORRECT_USER_ID + id); + return userDao.findUserAuthDetailsByUserId(tenantId.getId(), userId.getId()); + } + + @Override + public List findUsersByTenantIdAndIds(TenantId tenantId, List userIds) { + log.trace("Executing findUsersByTenantIdAndIds, tenantId [{}], userIds [{}]", tenantId, userIds); + return userDao.findUsersByTenantIdAndIds(tenantId.getId(), toUUIDs(userIds)); + } + private Optional findMobileSessionInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); @@ -503,6 +562,80 @@ public class UserServiceImpl extends AbstractCachedEntityService findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink) { + switch (filter.getType()) { + case USER_LIST -> { + List users = ((UserListFilter) filter).getUsersIds().stream() + .limit(pageLink.getPageSize()) + .map(UserId::new).map(userId -> findUserById(tenantId, userId)) + .filter(Objects::nonNull).collect(Collectors.toList()); + return new PageData<>(users, 1, users.size(), false); + } + case CUSTOMER_USERS -> { + if (tenantId.equals(TenantId.SYS_TENANT_ID)) { + throw new IllegalArgumentException("Customer users target is not supported for system administrator"); + } + CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) filter; + return findCustomerUsers(tenantId, new CustomerId(customerUsersFilter.getCustomerId()), pageLink); + } + case TENANT_ADMINISTRATORS -> { + TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter; + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + return findTenantAdmins(tenantId, pageLink); + } else { + if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) { + return findTenantAdminsByTenantsIds(tenantAdministratorsFilter.getTenantsIds().stream() + .map(TenantId::fromUUID).collect(Collectors.toList()), pageLink); + } else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) { + return findTenantAdminsByTenantProfilesIds(tenantAdministratorsFilter.getTenantProfilesIds().stream() + .map(TenantProfileId::new).collect(Collectors.toList()), pageLink); + } else { + return findAllTenantAdmins(pageLink); + } + } + } + case SYSTEM_ADMINISTRATORS -> { + return findSysAdmins(pageLink); + } + case ALL_USERS -> { + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + return findUsersByTenantId(tenantId, pageLink); + } else { + return findAllUsers(pageLink); + } + } + default -> throw new IllegalArgumentException("Recipient type not supported"); + } + } + + @Override + public boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user) { + switch (filter.getType()) { + case TENANT_ADMINISTRATORS -> { + if (user.isSystemAdmin() || user.isCustomerUser()) { + return false; + } + TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter; + if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) { + return tenantAdministratorsFilter.getTenantsIds().contains(user.getTenantId().getId()); + } else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) { + return tenantAdministratorsFilter.getTenantProfilesIds().contains(tenantProfileCache.get(user.getTenantId()).getUuidId()); + } else { + return user.getAuthority() == Authority.TENANT_ADMIN; + } + } + case SYSTEM_ADMINISTRATORS -> { + return user.getAuthority() == Authority.SYS_ADMIN; + } + case ALL_USERS -> { + return true; + } + default -> throw new IllegalArgumentException("Recipient type not supported"); + } + + } + private void updatePasswordHistory(UserCredentials userCredentials) { JsonNode additionalInfo = userCredentials.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java index 893cfce237..89064c2a8b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.entity.AbstractCachedService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.Iterator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index 5b50d062a2..d1d31c4745 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -117,24 +117,26 @@ public class DeviceConnectivityUtil { dockerComposeBuilder.append("\n"); dockerComposeBuilder.append(" # Environment variables\n"); dockerComposeBuilder.append(" environment:\n"); - dockerComposeBuilder.append(" - host=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n"); - dockerComposeBuilder.append(" - port=1883\n"); + dockerComposeBuilder.append(" - TB_GW_HOST=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n"); + dockerComposeBuilder.append(" - TB_GW_PORT=1883\n"); switch (deviceCredentials.getCredentialsType()) { case ACCESS_TOKEN: - dockerComposeBuilder.append(" - accessToken=").append(deviceCredentials.getCredentialsId()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=accessToken\n"); + dockerComposeBuilder.append(" - TB_GW_ACCESS_TOKEN=").append(deviceCredentials.getCredentialsId()).append("\n"); break; case MQTT_BASIC: + dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=usernamePassword\n"); BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); if (credentials != null) { if (StringUtils.isNotEmpty(credentials.getClientId())) { - dockerComposeBuilder.append(" - clientId=").append(credentials.getClientId()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_CLIENT_ID=").append(credentials.getClientId()).append("\n"); } if (StringUtils.isNotEmpty(credentials.getUserName())) { - dockerComposeBuilder.append(" - username=").append(credentials.getUserName()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_USERNAME=").append(credentials.getUserName()).append("\n"); } if (StringUtils.isNotEmpty(credentials.getPassword())) { - dockerComposeBuilder.append(" - password=").append(credentials.getPassword()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_PASSWORD=").append(credentials.getPassword()).append("\n"); } } break; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java index c923ae208b..afe0518208 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java @@ -19,13 +19,21 @@ import com.fasterxml.jackson.databind.JsonNode; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.NoXssValidator; +import org.thingsboard.server.exception.DataValidationException; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; public class KvUtils { @@ -74,4 +82,33 @@ public class KvUtils { } } } + + public static List toTsKvEntryList(Map> tsKvMap) { + List tsKvEntryList = new ArrayList<>(); + for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { + for (KvEntry kvEntry : tsKvEntry.getValue()) { + tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); + } + } + return tsKvEntryList; + } + + public static List filterChangedAttr(List currentAttributes, List newAttributes) { + if (currentAttributes == null || currentAttributes.isEmpty()) { + return newAttributes; + } + + Map currentAttrMap = currentAttributes.stream() + .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); + + return newAttributes.stream() + .filter(item -> { + AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); + return cacheAttr == null + || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type + || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); + }) + .collect(Collectors.toList()); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/TimeUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/TimeUtils.java index c163558ee4..888d0da866 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/TimeUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/TimeUtils.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.util; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.kv.IntervalType; import java.time.Instant; @@ -24,6 +26,7 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.IsoFields; import java.time.temporal.WeekFields; +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class TimeUtils { public static long calculateIntervalEnd(long startTs, IntervalType intervalType, ZoneId tzId) { @@ -42,4 +45,8 @@ public class TimeUtils { } } + public static ZonedDateTime toZonedDateTime(long ts, ZoneId zoneId) { + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(ts), zoneId); + } + } 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 937e8117fc..4d7f30cace 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 @@ -25,6 +25,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ImageContainerDao; +import java.util.List; import java.util.UUID; /** @@ -87,5 +88,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa PageData findAllWidgetsBundles(PageLink pageLink); + List findSystemOrTenantWidgetBundlesByIds(UUID tenantId, List widgetsBundleIds); + } 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 14e04c6090..d950c3eafa 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 @@ -48,6 +48,7 @@ import java.util.Optional; import java.util.stream.Stream; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; @Service("WidgetsBundleDaoService") @@ -252,6 +253,12 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { }); } + @Override + public List findSystemOrTenantWidgetsBundlesByIds(TenantId tenantId, List widgetsBundleIds) { + log.trace("Executing findSystemOrTenantWidgetsBundlesByIds, tenantId [{}], widgetsBundleIds [{}]", tenantId, widgetsBundleIds); + return widgetsBundleDao.findSystemOrTenantWidgetBundlesByIds(tenantId.getId(), toUUIDs(widgetsBundleIds)); + } + private WidgetTypeDetails updateSystemWidget(JsonNode widgetTypeJson) { WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); WidgetType existingWidget = widgetTypeService.findWidgetTypeByTenantIdAndFqn(TenantId.SYS_TENANT_ID, widgetTypeDetails.getFqn()); diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index cf52cb944f..43e64bab95 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -20,7 +20,7 @@ CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); --- Drop index by 'status' column and replace with new indexes that has only active alarms; +-- Drop index by 'status' column and replace with new indexes that have only active alarms; CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active ON alarm USING btree (originator_id, type) WHERE cleared = false; @@ -108,8 +108,6 @@ CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); -CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); - CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(resource_type, public_resource_key); CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id); @@ -117,3 +115,5 @@ CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tena CREATE INDEX IF NOT EXISTS idx_job_tenant_id ON job(tenant_id); CREATE INDEX IF NOT EXISTS idx_ai_model_tenant_id ON ai_model(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_api_key_user_id ON api_key(user_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index b051b187dc..6a66b203a4 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -162,6 +162,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( created_time bigint NOT NULL, additional_info varchar, configuration varchar(10000000), + notes varchar(1000000), name varchar(255), type varchar(255), first_rule_node_id uuid, @@ -709,6 +710,18 @@ CREATE TABLE IF NOT EXISTS api_usage_state ( CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) ); +CREATE TABLE IF NOT EXISTS api_key ( + id uuid NOT NULL CONSTRAINT api_key_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + user_id uuid, + value varchar(512), + enabled boolean NOT NULL DEFAULT TRUE, + expiration_time bigint DEFAULT 0, + description varchar(255), + CONSTRAINT api_key_value_unq_key UNIQUE (value) +); + CREATE TABLE IF NOT EXISTS resource ( id uuid NOT NULL CONSTRAINT resource_pkey PRIMARY KEY, created_time bigint NOT NULL, @@ -923,17 +936,8 @@ CREATE TABLE IF NOT EXISTS calculated_field ( configuration varchar(1000000), version BIGINT DEFAULT 1, debug_settings varchar(1024), - CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name) -); - -CREATE TABLE IF NOT EXISTS calculated_field_link ( - id uuid NOT NULL CONSTRAINT calculated_field_link_pkey PRIMARY KEY, - created_time bigint NOT NULL, - tenant_id uuid NOT NULL, - entity_type VARCHAR(32), - entity_id uuid NOT NULL, - calculated_field_id uuid NOT NULL, - CONSTRAINT fk_calculated_field_id FOREIGN KEY (calculated_field_id) REFERENCES calculated_field(id) ON DELETE CASCADE + additional_info varchar, + CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) ); CREATE TABLE IF NOT EXISTS cf_debug_event ( diff --git a/dao/src/test/java/org/thingsboard/server/dao/TbTimescaleDBContainerProvider.java b/dao/src/test/java/org/thingsboard/server/dao/TbTimescaleDBContainerProvider.java new file mode 100644 index 0000000000..8afcaa1c22 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/TbTimescaleDBContainerProvider.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.TimescaleDBContainerProvider; + +/** + * Extends the upstream {@link TimescaleDBContainerProvider} to disable the + * timescaledb-tune entrypoint script via NO_TS_TUNE=true. + * + * Works around a shell bug in /docker-entrypoint-initdb.d/001_timescaledb_tune.sh + * that crashes the container entrypoint on cgroup v2 hosts (including CI agents) + * when the kernel reports the 64-bit max for memory.max. + * + * Activated by the jdbc:tc:tbtimescaledb:<tag>:///... URL prefix + * registered via META-INF/services. + */ +public class TbTimescaleDBContainerProvider extends TimescaleDBContainerProvider { + + private static final String NAME = "tbtimescaledb"; + + @Override + public boolean supports(String databaseType) { + return NAME.equals(databaseType); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + JdbcDatabaseContainer container = super.newInstance(tag); + container.withEnv("NO_TS_TUNE", "true"); + return container; + } +} 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 d5baec09d5..5ee9af6292 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 @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; 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.TenantProfileId; import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; @@ -92,11 +93,13 @@ public abstract class AbstractServiceTest { @Autowired protected EntityServiceRegistry entityServiceRegistry; + protected Tenant tenant; protected TenantId tenantId; @Before public void beforeAbstractService() { - tenantId = createTenant().getId(); + tenant = createTenant(); + tenantId = tenant.getId(); } @After @@ -185,8 +188,13 @@ public abstract class AbstractServiceTest { } public Tenant createTenant() { + return createTenant(null); + } + + public Tenant createTenant(TenantProfileId tenantProfileId) { Tenant tenant = new Tenant(); tenant.setTitle("My tenant " + UUID.randomUUID()); + tenant.setTenantProfileId(tenantProfileId); Tenant savedTenant = tenantService.saveTenant(tenant); assertNotNull(savedTenant); return savedTenant; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java index 1b7924aad6..50d0b852f1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java @@ -27,8 +27,8 @@ import org.thingsboard.server.common.data.AdminSettings; 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 org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.exception.DataValidationException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java index 2329111e22..46254fbe2b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java @@ -58,9 +58,9 @@ 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.DeviceService; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.exception.DataValidationException; import java.util.Collections; import java.util.List; @@ -215,7 +215,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -323,7 +323,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() @@ -626,7 +626,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .severity(AlarmSeverity.MAJOR) .startTs(System.currentTimeMillis()).build()); AlarmInfo alarm1 = result.getAlarm(); - alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null, true); result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) @@ -636,7 +636,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .startTs(System.currentTimeMillis()).build()); AlarmInfo alarm2 = result.getAlarm(); alarmService.acknowledgeAlarm(tenantId, alarm2.getId(), System.currentTimeMillis()); - alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null, true); result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) @@ -856,7 +856,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(1, alarmsCount); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); @@ -978,13 +978,16 @@ public class AlarmServiceTest extends AbstractServiceTest { alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(parentId)); Assert.assertEquals(1, alarmsCount); + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId, parentId)); + Assert.assertEquals(2, alarmsCount); + created = alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm(); countQuery.setStatusList(List.of(AlarmSearchStatus.UNACK)); alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); Assert.assertEquals(0, alarmsCount); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); countQuery.setStatusList(List.of(AlarmSearchStatus.CLEARED)); alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java new file mode 100644 index 0000000000..f345c34c0d --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java @@ -0,0 +1,220 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.StringUtils; +import org.thingsboard.server.common.data.User; +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.page.PageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.dao.pat.ApiKeyService; +import org.thingsboard.server.dao.user.UserService; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DaoSqlTest +public class ApiKeyServiceTest extends AbstractServiceTest { + + private static final String TEST_API_KEY_DESCRIPTION = "Test API Key Description"; + + @Autowired + ApiKeyService apiKeyService; + + @Autowired + UserService userService; + + private UserId userId; + + @Before + public void before() { + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(tenantId); + tenantAdmin.setEmail("tenant@thingsboard.org"); + User user = userService.saveUser(TenantId.SYS_TENANT_ID, tenantAdmin); + userId = user.getId(); + } + + @After + public void after() { + apiKeyService.deleteByTenantId(tenantId); + User user = userService.findUserById(tenantId, userId); + userService.deleteUser(tenantId, user); + } + + @Test + public void testSaveApiKey() { + ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); + ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); + + Assert.assertNotNull(savedApiKey); + Assert.assertNotNull(savedApiKey.getId()); + Assert.assertEquals(tenantId, savedApiKey.getTenantId()); + Assert.assertEquals(TEST_API_KEY_DESCRIPTION, savedApiKey.getDescription()); + Assert.assertTrue(savedApiKey.isEnabled()); + Assert.assertNotNull(savedApiKey.getValue()); + } + + @Test + public void testSaveApiKeyWithTooLongDescription() { + ApiKeyInfo apiKeyInfo = createApiKeyInfo(StringUtils.randomAlphabetic(300)); + + assertThatThrownBy(() -> apiKeyService.saveApiKey(tenantId, apiKeyInfo)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("description length must be equal or less than 255"); + } + + @Test + public void testUpdateDescriptionApiKey() { + ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); + ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); + + String newDescription = "Updated API Key Description"; + savedApiKey.setDescription(newDescription); + ApiKey updatedApiKey = apiKeyService.saveApiKey(tenantId, savedApiKey); + + Assert.assertNotNull(updatedApiKey); + Assert.assertEquals(savedApiKey.getId(), updatedApiKey.getId()); + Assert.assertEquals(newDescription, updatedApiKey.getDescription()); + Assert.assertEquals(savedApiKey.getValue(), updatedApiKey.getValue()); + } + + @Test + public void testDisableApiKey() { + ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); + ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); + + savedApiKey.setEnabled(false); + ApiKey disabledApiKey = apiKeyService.saveApiKey(tenantId, savedApiKey); + + Assert.assertNotNull(disabledApiKey); + Assert.assertEquals(savedApiKey.getId(), disabledApiKey.getId()); + Assert.assertFalse(disabledApiKey.isEnabled()); + } + + @Test + public void testFindApiKeyById() { + ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); + ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); + + ApiKey foundApiKey = apiKeyService.findApiKeyById(tenantId, savedApiKey.getId()); + + Assert.assertNotNull(foundApiKey); + Assert.assertEquals(savedApiKey.getId(), foundApiKey.getId()); + Assert.assertEquals(savedApiKey.getDescription(), foundApiKey.getDescription()); + Assert.assertEquals(savedApiKey.isEnabled(), foundApiKey.isEnabled()); + Assert.assertEquals(savedApiKey.getValue(), foundApiKey.getValue()); + } + + @Test + public void testFindApiKeyByHash() { + ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); + ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); + + ApiKey foundApiKey = apiKeyService.findApiKeyByValue(savedApiKey.getValue()); + + Assert.assertNotNull(foundApiKey); + Assert.assertEquals(savedApiKey.getId(), foundApiKey.getId()); + Assert.assertEquals(savedApiKey.getDescription(), foundApiKey.getDescription()); + Assert.assertEquals(savedApiKey.isEnabled(), foundApiKey.isEnabled()); + Assert.assertEquals(savedApiKey.getValue(), foundApiKey.getValue()); + } + + @Test + public void testFindApiKeysByUserId() { + int size = 3; + for (int i = 0; i < size; i++) { + ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); + apiKeyService.saveApiKey(tenantId, apiKeyInfo); + } + + PageLink pageLink = new PageLink(10); + PageData pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, pageLink); + + Assert.assertNotNull(pageData); + Assert.assertEquals(size, pageData.getData().size()); + Assert.assertEquals(size, pageData.getTotalElements()); + } + + @Test + public void testDeleteApiKey() { + ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); + ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); + + apiKeyService.deleteApiKey(tenantId, savedApiKey, false); + + ApiKey foundApiKey = apiKeyService.findApiKeyById(tenantId, savedApiKey.getId()); + Assert.assertNull(foundApiKey); + } + + @Test + public void testDeleteByTenantId() { + for (int i = 0; i < 3; i++) { + ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); + apiKeyService.saveApiKey(tenantId, apiKeyInfo); + } + + apiKeyService.deleteByTenantId(tenantId); + + PageLink pageLink = new PageLink(10); + PageData pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, pageLink); + + Assert.assertNotNull(pageData); + Assert.assertEquals(0, pageData.getData().size()); + Assert.assertEquals(0, pageData.getTotalElements()); + } + + @Test + public void testDeleteByUserId() { + int size = 3; + for (int i = 0; i < size; i++) { + ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); + apiKeyService.saveApiKey(tenantId, apiKeyInfo); + } + + PageData pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, new PageLink(10)); + Assert.assertNotNull(pageData); + Assert.assertEquals(size, pageData.getData().size()); + Assert.assertEquals(size, pageData.getTotalElements()); + + apiKeyService.deleteByUserId(tenantId, userId); + + pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, new PageLink(10)); + Assert.assertNotNull(pageData); + Assert.assertEquals(0, pageData.getData().size()); + Assert.assertEquals(0, pageData.getTotalElements()); + } + + private ApiKeyInfo createApiKeyInfo(String description) { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setTenantId(tenantId); + apiKeyInfo.setUserId(userId); + apiKeyInfo.setDescription(description); + apiKeyInfo.setEnabled(true); + return apiKeyInfo; + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java index 46afabfc80..7335cc14a9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.Collections; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index ce01e94d37..204b200482 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -16,17 +16,25 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -34,29 +42,34 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; 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.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -72,13 +85,28 @@ public class AssetServiceTest extends AbstractServiceTest { @Autowired RelationService relationService; @Autowired + TenantProfileService tenantProfileService; + @Autowired private AssetProfileService assetProfileService; @Autowired private CalculatedFieldService calculatedFieldService; @Autowired private PlatformTransactionManager platformTransactionManager; + private static ListeningExecutorService executor; + private IdComparator idComparator = new IdComparator<>(); + private TenantId anotherTenantId; + + @BeforeClass + public static void before() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("AssetServiceTestScope"))); + } + + @AfterClass + public static void after() { + executor.shutdownNow(); + } @Test public void testSaveAsset() { @@ -105,6 +133,39 @@ public class AssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } + @Test + public void testAssetLimitOnTenantProfileLevel() throws InterruptedException { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Test profile"); + tenantProfile.setDescription("Test"); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(DefaultTenantProfileConfiguration.builder().maxAssets(5l).build()); + tenantProfile.setProfileData(profileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbRuleEngine(false); + + tenantProfile = tenantProfileService.saveTenantProfile(anotherTenantId, tenantProfile); + anotherTenantId = createTenant(tenantProfile.getId()).getId(); + + for (int i = 0; i < 20; i++) { + executor.submit(() -> { + Asset asset = new Asset(); + asset.setTenantId(anotherTenantId); + asset.setName(RandomStringUtils.randomAlphabetic(10)); + asset.setType("default"); + assetService.saveAsset(asset); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = assetService.countByTenantId(anotherTenantId); + return countByTenantId == 5; + }); + + Thread.sleep(2000); + assertThat(assetService.countByTenantId(anotherTenantId)).isEqualTo(5); + } + @Test public void testShouldNotPutInCacheRolledbackAssetProfile() { AssetProfile assetProfile = new AssetProfile(); @@ -894,9 +955,8 @@ public class AssetServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 060be564c4..3caf4bb5f4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -15,34 +15,48 @@ */ package org.thingsboard.server.dao.service; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import org.junit.After; -import org.junit.Before; +import org.apache.commons.lang3.RandomUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.exception.DataValidationException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldServiceTest extends AbstractServiceTest { @@ -51,18 +65,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { private CalculatedFieldService calculatedFieldService; @Autowired private DeviceService deviceService; - - private ListeningExecutorService executor; - - @Before - public void before() { - executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); - } - - @After - public void after() { - executor.shutdownNow(); - } + @Autowired + private TbTenantProfileCache tbTenantProfileCache; + @Autowired + private TenantProfileService tenantProfileService; @Test public void testSaveCalculatedField() { @@ -78,6 +84,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration()); + assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -85,9 +92,218 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo()); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + } - calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); + @Test + public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalLessThanMinAllowedIntervalInTenantProfile() { + // Arrange a device + Device device = createTestDevice(); + + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + AttributesOutput out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + + // Enable scheduling with an interval below tenant min + cfg.setScheduledUpdateEnabled(true); + int invalidInterval = RandomUtils.insecure().randomInt(1, min); + cfg.setScheduledUpdateInterval(invalidInterval); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF min allowed scheduled update interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval (" + invalidInterval + + " seconds) is less than minimum allowed interval in tenant profile: " + min + " seconds"); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelGreaterThanMaxAllowedRelationLevelInTenantProfile() { + // Arrange a device + Device device = createTestDevice(); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + int maxRelationLevel = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelationLevelPerCfArgument(); + + // Zone-group argument (ATTRIBUTE) + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + + List levels = new ArrayList<>(); + for (int i = 0; i < maxRelationLevel + 1; i++) { + levels.add(mock(RelationPathLevel.class)); + } + + dynamicSourceConfiguration.setLevels(levels); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + AttributesOutput out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF max relation level test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Max relation level is greater than configured maximum allowed relation level in tenant profile"); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldSaveWithoutDataValidationExceptionOnScheduledUpdateInterval() { + // Arrange a device + Device device = createTestDevice(); + + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + AttributesOutput out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int valueFromConfig = min + 100; + + // Enable scheduling with an interval greater than tenant min + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(valueFromConfig); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF no validation error test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + CalculatedField saved = calculatedFieldService.save(cf); + + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); + assertThat(savedInterval).isEqualTo(valueFromConfig); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldAcceptZeroScheduledUpdateIntervalWhenTenantProfileAllows() { + // GIVEN + var device = createTestDevice(); + + // Store original value and update tenant profile to allow 0 as min scheduled update interval + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); + int originalMinScheduledUpdateInterval = tenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF(); + tenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(0); + tenantProfileService.saveTenantProfile(tenantId, tenantProfile); + tbTenantProfileCache.evict(tenantProfile.getId()); + + try { + // Build a valid Geofencing configuration + var cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + var entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + var zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Enable scheduling with interval = 0 + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(0); + + // Create Calculated Field + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF zero scheduled update interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + var out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var savedConfig = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(0); + } finally { + // Restore original tenant profile value + tenantProfile.getProfileConfiguration().orElseThrow().setMinAllowedScheduledUpdateIntervalInSecForCF(originalMinScheduledUpdateInterval); + tenantProfileService.saveTenantProfile(tenantId, tenantProfile); + tbTenantProfileCache.evict(tenantProfile.getId()); + } } @Test @@ -98,7 +314,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThatThrownBy(() -> calculatedFieldService.save(calculatedField)) .isInstanceOf(DataValidationException.class) - .hasMessage("Calculated Field with such name is already in exists!"); + .hasMessage("Calculated field with such name and type already exists"); } @Test @@ -107,8 +323,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField fetchedCalculatedField = calculatedFieldService.findById(tenantId, savedCalculatedField.getId()); assertThat(fetchedCalculatedField).isEqualTo(savedCalculatedField); - - calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } @Test @@ -120,6 +334,156 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThat(calculatedFieldService.findById(tenantId, savedCalculatedField.getId())).isNull(); } + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldUseMinScheduledUpdateIntervalFromTenantProfileWhenNotSet() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + // Do NOT set scheduledUpdateInterval - it should default to tenant profile min value + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - default scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(RelatedEntitiesAggregationCalculatedFieldConfiguration.class); + + var savedConfig = (RelatedEntitiesAggregationCalculatedFieldConfiguration) saved.getConfiguration(); + int expectedMinScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(expectedMinScheduledUpdateInterval); + } + + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldThrowWhenScheduledUpdateIntervalLessThanMinAllowed() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + int minScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int invalidInterval = RandomUtils.insecure().randomInt(1, minScheduledUpdateInterval); + cfg.setScheduledUpdateInterval(invalidInterval); + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - invalid scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN-THEN + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval (" + invalidInterval + + " seconds) is less than minimum allowed interval in tenant profile: " + minScheduledUpdateInterval + " seconds"); + } + + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldAcceptValidScheduledUpdateInterval() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + int minScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int customScheduledUpdateInterval = minScheduledUpdateInterval + 100; + cfg.setScheduledUpdateInterval(customScheduledUpdateInterval); + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - valid scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(RelatedEntitiesAggregationCalculatedFieldConfiguration.class); + + var savedConfig = (RelatedEntitiesAggregationCalculatedFieldConfiguration) saved.getConfiguration(); + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(customScheduledUpdateInterval); + } + private CalculatedField saveValidCalculatedField() { Device device = createTestDevice(); CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId()); @@ -134,6 +498,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId)); + calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode()); return calculatedField; } @@ -149,9 +514,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java index 46f89ce494..ca54775ea2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.dao.service; import org.junit.Assert; import org.junit.jupiter.api.Test; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; class ConstraintValidatorTest { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java index a587646337..c9eadd2ed5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java @@ -35,17 +35,16 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; 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.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.List; @@ -389,9 +388,8 @@ public class CustomerServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java index 478df46244..9f586503e4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.io.IOException; import java.util.ArrayList; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java index a4f102f4fd..899932fc6e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java @@ -20,7 +20,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.spy; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java index 5f1c502011..bc932f3aef 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; @DaoSqlTest public class DeviceCredentialsServiceTest extends AbstractServiceTest { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java index 54c471fd1f..25cd63ec63 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; 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.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import java.nio.ByteBuffer; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 94745c804e..b9fd609521 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -16,9 +16,13 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.mockito.Mockito; @@ -27,6 +31,8 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -43,10 +49,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; @@ -63,7 +68,7 @@ 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.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; @@ -75,7 +80,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; @@ -108,6 +116,17 @@ public class DeviceServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; + private static ListeningExecutorService executor; + + @BeforeClass + public static void beforeClass() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("DeviceServiceTestScope"))); + } + + @AfterClass + public static void afterClass() { + executor.shutdownNow(); + } @Before public void before() { @@ -140,6 +159,31 @@ public class DeviceServiceTest extends AbstractServiceTest { deleteDevice(tenantId, device); } + @Test + public void testDeviceLimitOnTenantProfileLevel() throws InterruptedException { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(5l).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + for (int i = 0; i < 50; i++) { + executor.submit(() -> { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(StringUtils.randomAlphabetic(10)); + device.setType("default"); + deviceService.saveDevice(device); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = deviceService.countByTenantId(tenantId); + return countByTenantId == 5; + }); + + Thread.sleep(2000); + assertThat(deviceService.countByTenantId(tenantId)).isEqualTo(5); + } + @Test public void testSaveDevicesWithMaxDeviceOutOfLimit() { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); @@ -1248,9 +1292,8 @@ public class DeviceServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java index 8a2ee49ae3..d58c1c8080 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java index 525656bf59..a065939851 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java @@ -20,7 +20,6 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.rule.RuleChainService; @@ -45,8 +44,4 @@ public class EntityServiceRegistryTest extends AbstractServiceTest { Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.RULE_NODE) instanceof RuleChainService); } - @Test - public void givenCalculatedFieldLinkEntityType_whenGetServiceByEntityTypeCalled_thenReturnedCalculatedFieldService() { - Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.CALCULATED_FIELD_LINK) instanceof CalculatedFieldService); - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java index 34f74f292b..4b8c7c0937 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java @@ -26,7 +26,7 @@ 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.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import java.util.Arrays; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java index 2f296d38f2..69fa3fbc5d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java @@ -36,11 +36,11 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 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.ota.OtaPackageDao; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.exception.DataValidationException; import java.nio.ByteBuffer; import java.util.ArrayList; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java index 764d1f8b3e..a2d54e7d37 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.tenant.TenantProfileService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java index cf5fc67ef1..f9564df710 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java @@ -27,7 +27,7 @@ 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.queue.QueueStats; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueStatsService; import static org.assertj.core.api.Assertions.assertThat; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java index e7f6ba74c7..55fe46c06c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java @@ -28,13 +28,16 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.ArrayList; import java.util.Collections; @@ -42,12 +45,17 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; +import static org.assertj.core.api.Assertions.assertThat; + @DaoSqlTest public class RelationServiceTest extends AbstractServiceTest { @Autowired RelationService relationService; + @Autowired + private TbTenantProfileCache tbTenantProfileCache; + @Before public void before() { } @@ -348,14 +356,14 @@ public class RelationServiceTest extends AbstractServiceTest { query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); List relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(expected.size(), relations.size()); - for(EntityRelation r : expected){ + for (EntityRelation r : expected) { Assert.assertTrue(relations.contains(r)); } //Test from cache relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(expected.size(), relations.size()); - for(EntityRelation r : expected){ + for (EntityRelation r : expected) { Assert.assertTrue(relations.contains(r)); } } @@ -623,6 +631,114 @@ public class RelationServiceTest extends AbstractServiceTest { Assert.assertTrue(relations.contains(relationF)); } + @Test + public void testFindByPathQueryWithoutExceedingLimit() throws Exception { + /* + A + └──[firstLevel, TO]→ B + └──[secondLevel, TO]→ C + ├──[thirdLevel, FROM]→ D1 + ├──[thirdLevel, FROM]→ D2 + ├──[thirdLevel, FROM]→ ... + └──[thirdLevel, FROM]→ D{N - 1}, where N is the limit + */ + AssetId assetA = new AssetId(Uuids.timeBased()); + AssetId assetB = new AssetId(Uuids.timeBased()); + AssetId assetC = new AssetId(Uuids.timeBased()); + + // create first and second level + saveRelation(new EntityRelation(assetB, assetA, "firstLevel")); + saveRelation(new EntityRelation(assetC, assetB, "secondLevel")); + + int limit = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelatedEntitiesToReturnPerCfArgument(); + + int totalCreated = limit - 1; + + List allThirdLevelRelations = new ArrayList<>(); + for (int i = 0; i < totalCreated; i++) { + AssetId leaf = new AssetId(Uuids.timeBased()); + allThirdLevelRelations.add(saveRelation(new EntityRelation(assetC, leaf, "thirdLevel"))); + } + + EntityRelationPathQuery query = new EntityRelationPathQuery(assetA, List.of( + new RelationPathLevel(EntitySearchDirection.TO, "firstLevel"), + new RelationPathLevel(EntitySearchDirection.TO, "secondLevel"), + new RelationPathLevel(EntitySearchDirection.FROM, "thirdLevel") + )); + + // call a method that applies the default limit internally + List result = relationService.findByRelationPathQueryAsync(tenantId, query).get(); + + // verify that limit has been applied + assertThat(result).hasSize(totalCreated); + + // verify all returned are valid third-level relations under C + assertThat(result) + .allSatisfy(rel -> { + assertThat(rel.getType()).isEqualTo("thirdLevel"); + assertThat(rel.getFrom()).isEqualTo(assetC); + }); + + // verify the returned subset is part of all created relations + assertThat(result).isEqualTo(allThirdLevelRelations); + } + + @Test + public void testFindByPathQueryWithExceedingLimit() throws Exception { + /* + A + └──[firstLevel, TO]→ B + └──[secondLevel, TO]→ C + ├──[thirdLevel, FROM]→ D1 + ├──[thirdLevel, FROM]→ D2 + ├──[thirdLevel, FROM]→ ... + └──[thirdLevel, FROM]→ D{N + 20}, where N is the limit + */ + AssetId assetA = new AssetId(Uuids.timeBased()); + AssetId assetB = new AssetId(Uuids.timeBased()); + AssetId assetC = new AssetId(Uuids.timeBased()); + + // create first and second level + saveRelation(new EntityRelation(assetB, assetA, "firstLevel")); + saveRelation(new EntityRelation(assetC, assetB, "secondLevel")); + + int limit = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelatedEntitiesToReturnPerCfArgument(); + + int totalCreated = limit + 20; + + List allThirdLevelRelations = new ArrayList<>(); + for (int i = 0; i < totalCreated; i++) { + AssetId leaf = new AssetId(Uuids.timeBased()); + allThirdLevelRelations.add(saveRelation(new EntityRelation(assetC, leaf, "thirdLevel"))); + } + + EntityRelationPathQuery query = new EntityRelationPathQuery(assetA, List.of( + new RelationPathLevel(EntitySearchDirection.TO, "firstLevel"), + new RelationPathLevel(EntitySearchDirection.TO, "secondLevel"), + new RelationPathLevel(EntitySearchDirection.FROM, "thirdLevel") + )); + + // call a method that applies the default limit internally + List result = relationService.findByRelationPathQueryAsync(tenantId, query).get(); + + // verify that limit has been applied + assertThat(result).hasSize(limit); + + // verify all returned are valid third-level relations under C + assertThat(result) + .allSatisfy(rel -> { + assertThat(rel.getType()).isEqualTo("thirdLevel"); + assertThat(rel.getFrom()).isEqualTo(assetC); + }); + + // verify the returned subset is part of all created relations + assertThat(result).isSubsetOf(allThirdLevelRelations); + } + @Test public void testFindByQueryLargeHierarchyFetchAllWithUnlimLvl() throws Exception { AssetId rootAsset = new AssetId(Uuids.timeBased()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java index ca415ea6dc..3bfb2d4c81 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java index 49661c747f..1d8e77f8ce 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java @@ -38,7 +38,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.util.ProtoUtils; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.gen.transport.TransportProtos; @@ -164,13 +164,13 @@ public class TenantProfileServiceTest extends AbstractServiceTest { 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 setDefaultTenantProfile1 = tenantProfileService.setDefaultTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile1.getId()); + Assert.assertNotEquals(savedTenantProfile1.isDefault(), setDefaultTenantProfile1.isDefault()); 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); + TenantProfile setDefaultTenantProfile2 = tenantProfileService.setDefaultTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile2.getId()); + Assert.assertNotEquals(savedTenantProfile2.isDefault(), setDefaultTenantProfile2.isDefault()); defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); Assert.assertNotNull(defaultTenantProfile); Assert.assertEquals(savedTenantProfile2.getId(), defaultTenantProfile.getId()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java index e9f5b4d16f..1873fa9ea9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java @@ -62,7 +62,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rpc.RpcService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java index a3ed1e1ceb..030d6c3bfd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.user.UserService; import java.util.ArrayList; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java index c1052865cc..dee5aee6a6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java index 3d2ad93538..e7b16a2cb2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java @@ -25,7 +25,7 @@ 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.common.data.widget.WidgetsBundleFilter; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.widget.WidgetsBundleService; 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 57dee96462..373112293d 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 @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; 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.AttributeScope; @@ -31,8 +30,12 @@ import org.thingsboard.server.common.data.id.DeviceId; 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.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.attributes.AttributesDao; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -40,6 +43,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Executors; @@ -48,9 +52,6 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; -/** - * @author Andrew Shvayka - */ @Slf4j public abstract class BaseAttributesServiceTest extends AbstractServiceTest { @@ -60,9 +61,8 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { @Autowired private AttributesService attributesService; - @Before - public void before() { - } + @Autowired + private AttributesDao attributesDao; @Test public void saveAndFetch() throws Exception { @@ -223,7 +223,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { - List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name()); + List keys = attributesService.findAllKeysByEntityIdsAndScope(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE); assertThat(keys).containsOnly("key1", "key2"); }); } @@ -241,6 +241,84 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { }); } + @Test + public void findLatestByEntityIdsAndScope_returnsOneEntryPerKey() { + var device1 = new DeviceId(UUID.randomUUID()); + var device2 = new DeviceId(UUID.randomUUID()); + + // Both devices have "temperature", device2 has a newer ts + saveAttribute(tenantId, device1, AttributeScope.SERVER_SCOPE, 1000, new DoubleDataEntry("temperature", 20.0)); + saveAttribute(tenantId, device2, AttributeScope.SERVER_SCOPE, 2000, new DoubleDataEntry("temperature", 25.0)); + // Only device1 has "humidity" + saveAttribute(tenantId, device1, AttributeScope.SERVER_SCOPE, 1500, new LongDataEntry("humidity", 60L)); + // Only device2 has "active" + saveAttribute(tenantId, device2, AttributeScope.SERVER_SCOPE, 3000, new BooleanDataEntry("active", true)); + + List results = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device1, device2), AttributeScope.SERVER_SCOPE); + Map byKey = results.stream().collect(Collectors.toMap(AttributeKvEntry::getKey, e -> e)); + + Assert.assertEquals(3, byKey.size()); + + // "temperature" should pick device2's value (ts=2000 > ts=1000) + AttributeKvEntry temp = byKey.get("temperature"); + Assert.assertNotNull(temp); + Assert.assertEquals(25.0, temp.getDoubleValue().orElseThrow(), 0.0); + Assert.assertEquals(2000, temp.getLastUpdateTs()); + + // "humidity" — only device1 has it + AttributeKvEntry humidity = byKey.get("humidity"); + Assert.assertNotNull(humidity); + Assert.assertEquals(60L, (long) humidity.getLongValue().orElseThrow()); + Assert.assertEquals(1500, humidity.getLastUpdateTs()); + + // "active" — only device2 has it + AttributeKvEntry active = byKey.get("active"); + Assert.assertNotNull(active); + Assert.assertEquals(true, active.getBooleanValue().orElseThrow()); + Assert.assertEquals(3000, active.getLastUpdateTs()); + } + + @Test + public void findLatestByEntityIdsAndScope_emptyList() { + List results = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(), AttributeScope.SERVER_SCOPE); + Assert.assertTrue(results.isEmpty()); + } + + @Test + public void findLatestByEntityIdsAndScope_singleEntity() throws Exception { + var device = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, device, AttributeScope.SERVER_SCOPE, 1000, new StringDataEntry("key1", "value1")); + saveAttribute(tenantId, device, AttributeScope.SERVER_SCOPE, 2000, new StringDataEntry("key2", "value2")); + + // sync + List results = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device), AttributeScope.SERVER_SCOPE); + Assert.assertEquals(2, results.size()); + Map byKey = results.stream().collect(Collectors.toMap(AttributeKvEntry::getKey, e -> e)); + Assert.assertEquals("value1", byKey.get("key1").getStrValue().orElseThrow()); + Assert.assertEquals(1000, byKey.get("key1").getLastUpdateTs()); + Assert.assertEquals("value2", byKey.get("key2").getStrValue().orElseThrow()); + Assert.assertEquals(2000, byKey.get("key2").getLastUpdateTs()); + + // async — same result + List asyncResults = attributesDao.findLatestByEntityIdsAndScopeAsync(tenantId, List.of(device), AttributeScope.SERVER_SCOPE).get(); + Assert.assertEquals(results, asyncResults); + } + + @Test + public void findLatestByEntityIdsAndScope_filtersScope() { + var device = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, device, AttributeScope.SERVER_SCOPE, 1000, new StringDataEntry("serverKey", "sv")); + saveAttribute(tenantId, device, AttributeScope.CLIENT_SCOPE, 1000, new StringDataEntry("clientKey", "cv")); + + List serverResults = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device), AttributeScope.SERVER_SCOPE); + Assert.assertEquals(1, serverResults.size()); + Assert.assertEquals("serverKey", serverResults.get(0).getKey()); + + List clientResults = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device), AttributeScope.CLIENT_SCOPE); + Assert.assertEquals(1, clientResults.size()); + Assert.assertEquals("clientKey", clientResults.get(0).getKey()); + } + private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { var scope = AttributeScope.SERVER_SCOPE; var key = "TEST"; @@ -313,6 +391,15 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { } } + private void saveAttribute(TenantId tenantId, DeviceId deviceId, AttributeScope scope, long ts, KvEntry value) { + try { + attributesService.save(tenantId, deviceId, scope, Collections.singletonList(new BaseAttributeKvEntry(ts, value))).get(10, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("Failed to save attribute", e.getCause()); + throw new RuntimeException(e); + } + } + private void equalsIgnoreVersion(AttributeKvEntry expected, AttributeKvEntry actual) { Assert.assertEquals(expected.getKey(), actual.getKey()); Assert.assertEquals(expected.getValue(), actual.getValue()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java index 540d25578a..eb46a87018 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.customer.CustomerDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantService; import java.util.UUID; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java index 221782b23c..3558f19cb3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java @@ -17,14 +17,14 @@ package org.thingsboard.server.dao.service.validator; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.cf.CalculatedFieldDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.usagerecord.DefaultApiLimitService; import java.util.UUID; @@ -38,11 +38,11 @@ public class CalculatedFieldDataValidatorTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("7b5229e9-166e-41a9-a257-3b1dafad1b04")); private final CalculatedFieldId CALCULATED_FIELD_ID = new CalculatedFieldId(UUID.fromString("060fbe45-fbb2-4549-abf3-f72a6be3cb9f")); - @MockBean + @MockitoBean private CalculatedFieldDao calculatedFieldDao; - @MockBean + @MockitoBean private DefaultApiLimitService apiLimitService; - @SpyBean + @MockitoSpyBean private CalculatedFieldDataValidator validator; @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java deleted file mode 100644 index 41d16a0670..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.validator; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; - -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -@SpringBootTest(classes = CalculatedFieldLinkDataValidator.class) -public class CalculatedFieldLinkDataValidatorTest { - - private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("2ba09d99-6143-43dc-b645-381fc0c43ebe")); - private final CalculatedFieldLinkId CALCULATED_FIELD_LINK_ID = new CalculatedFieldLinkId(UUID.fromString("a5609ef4-cb42-43ce-9b23-e090a4878d1c")); - - @MockBean - private CalculatedFieldLinkDao calculatedFieldLinkDao; - @SpyBean - private CalculatedFieldLinkDataValidator validator; - - @Test - public void testUpdateNonExistingCalculatedField() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(CALCULATED_FIELD_LINK_ID); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(UUID.fromString("136477af-fd07-4498-b9c9-54fe50e82992"))); - - given(calculatedFieldLinkDao.findById(TENANT_ID, CALCULATED_FIELD_LINK_ID.getId())).willReturn(null); - - assertThatThrownBy(() -> validator.validateUpdate(TENANT_ID, calculatedFieldLink)) - .isInstanceOf(DataValidationException.class) - .hasMessage("Can't update non existing calculated field link!"); - } - -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java index 260114abd7..f0aacc91bd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantService; import java.util.UUID; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java index f3f172a5ce..34523c1829 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java @@ -48,6 +48,9 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER; @SpringBootTest(classes = DeviceProfileDataValidator.class) class DeviceProfileDataValidatorTest { @@ -74,8 +77,8 @@ class DeviceProfileDataValidatorTest { " \"clientOnlyObserveAfterConnect\": 1\n" + " }"; - private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [1 - 65534]!"; - private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be in range [0 - 65535]!"; + private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!"; + private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be null!"; private static final String msgErrorNotNull = " Server ShortServerId must not be null!"; private static final String host = "localhost"; private static final String hostBs = "localhost"; @@ -124,7 +127,7 @@ class DeviceProfileDataValidatorTest { @Test void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() { Integer shortServerId = 123; - Integer shortServerIdBs = 0; + Integer shortServerIdBs = null; DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs); validator.validateDataImpl(tenantId, deviceProfile); @@ -132,8 +135,13 @@ class DeviceProfileDataValidatorTest { } @Test - void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_null_Error() { - verifyValidationError(123, null, "Bootstrap" + msgErrorNotNull); + void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_validate_0_to_null_Ok() { + Integer shortServerId = 123; + Integer shortServerIdBs = 0; + DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs); + + validator.validateDataImpl(tenantId, deviceProfile); + verify(validator).validateString("Device profile name", deviceProfile.getName()); } @Test @@ -153,7 +161,7 @@ class DeviceProfileDataValidatorTest { @Test void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() { - verifyValidationError(65535, 111, msgErrorLwm2mRange); + verifyValidationError(NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId(), 111, msgErrorLwm2mRange); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java index 170b452fee..373344cb6d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java @@ -16,29 +16,35 @@ package org.thingsboard.server.dao.service.validator; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.server.common.data.TenantProfile; 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.dao.tenant.TenantProfileDao; import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.exception.DataValidationException; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verify; @SpringBootTest(classes = TenantProfileDataValidator.class) class TenantProfileDataValidatorTest { - @MockBean + @MockitoBean TenantProfileDao tenantProfileDao; - @MockBean + @MockitoBean TenantProfileService tenantProfileService; - @SpyBean + @MockitoSpyBean TenantProfileDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); @Test @@ -53,4 +59,44 @@ class TenantProfileDataValidatorTest { verify(validator).validateString("Tenant profile name", tenantProfile.getName()); } + @ParameterizedTest + @ValueSource(ints = {-1, -100, Integer.MIN_VALUE}) + void minAllowedScheduledUpdateIntervalInSecForCF_shouldRejectNegativeValues(int value) { + // GIVEN + var config = new DefaultTenantProfileConfiguration(); + config.setMinAllowedScheduledUpdateIntervalInSecForCF(value); + + var tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(config); + + var tenantProfile = new TenantProfile(); + tenantProfile.setName("Test"); + tenantProfile.setProfileData(tenantProfileData); + + // WHEN/THEN + assertThatThrownBy(() -> validator.validate(tenantProfile, __ -> TenantId.SYS_TENANT_ID)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("minAllowedScheduledUpdateIntervalInSecForCF") + .hasMessageContaining("must be greater than or equal to 0"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 60, Integer.MAX_VALUE}) + void minAllowedScheduledUpdateIntervalInSecForCF_shouldAcceptValidValues(int value) { + // GIVEN + var config = new DefaultTenantProfileConfiguration(); + config.setMinAllowedScheduledUpdateIntervalInSecForCF(value); + + var tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(config); + + var tenantProfile = new TenantProfile(); + tenantProfile.setName("Test"); + tenantProfile.setProfileData(tenantProfileData); + + // WHEN/THEN + assertThatCode(() -> validator.validate(tenantProfile, __ -> TenantId.SYS_TENANT_ID)) + .doesNotThrowAnyException(); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java index 96f3390b13..02abf1e5fd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java @@ -22,6 +22,9 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.id.DeviceId; 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.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -30,7 +33,9 @@ import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -102,6 +107,69 @@ public class SqlTimeseriesLatestDaoTest extends AbstractServiceTest { } } + @Test + public void findLatestByEntityIds_returnsOneEntryPerKey() throws Exception { + DeviceId device1 = new DeviceId(UUID.randomUUID()); + DeviceId device2 = new DeviceId(UUID.randomUUID()); + + // Both devices have "temperature" key, device2 has a newer ts + timeseriesLatestDao.saveLatest(tenantId, device1, new BasicTsKvEntry(1000, new DoubleDataEntry("temperature", 20.0))).get(); + timeseriesLatestDao.saveLatest(tenantId, device2, new BasicTsKvEntry(2000, new DoubleDataEntry("temperature", 25.0))).get(); + // Only device1 has "humidity" + timeseriesLatestDao.saveLatest(tenantId, device1, new BasicTsKvEntry(1500, new LongDataEntry("humidity", 60L))).get(); + // Only device2 has "active" + timeseriesLatestDao.saveLatest(tenantId, device2, new BasicTsKvEntry(3000, new BooleanDataEntry("active", true))).get(); + + List results = timeseriesLatestDao.findLatestByEntityIds(tenantId, List.of(device1, device2)); + Map byKey = results.stream().collect(Collectors.toMap(TsKvEntry::getKey, e -> e)); + + assertEquals(3, byKey.size()); + + // "temperature" should pick device2's value (ts=2000 > ts=1000) + TsKvEntry temp = byKey.get("temperature"); + assertNotNull(temp); + assertEquals(25.0, temp.getDoubleValue().orElseThrow()); + assertEquals(2000, temp.getTs()); + + // "humidity" — only device1 has it + TsKvEntry humidity = byKey.get("humidity"); + assertNotNull(humidity); + assertEquals(60L, humidity.getLongValue().orElseThrow()); + assertEquals(1500, humidity.getTs()); + + // "active" — only device2 has it + TsKvEntry active = byKey.get("active"); + assertNotNull(active); + assertEquals(true, active.getBooleanValue().orElseThrow()); + assertEquals(3000, active.getTs()); + } + + @Test + public void findLatestByEntityIds_emptyList() { + List results = timeseriesLatestDao.findLatestByEntityIds(tenantId, List.of()); + assertTrue(results.isEmpty()); + } + + @Test + public void findLatestByEntityIds_singleEntity() throws Exception { + DeviceId device = new DeviceId(UUID.randomUUID()); + timeseriesLatestDao.saveLatest(tenantId, device, new BasicTsKvEntry(1000, new StringDataEntry("key1", "value1"))).get(); + timeseriesLatestDao.saveLatest(tenantId, device, new BasicTsKvEntry(2000, new StringDataEntry("key2", "value2"))).get(); + + // sync + List results = timeseriesLatestDao.findLatestByEntityIds(tenantId, List.of(device)); + assertEquals(2, results.size()); + Map byKey = results.stream().collect(Collectors.toMap(TsKvEntry::getKey, e -> e)); + assertEquals("value1", byKey.get("key1").getStrValue().orElseThrow()); + assertEquals(1000, byKey.get("key1").getTs()); + assertEquals("value2", byKey.get("key2").getStrValue().orElseThrow()); + assertEquals(2000, byKey.get("key2").getTs()); + + // async — same result + List asyncResults = timeseriesLatestDao.findLatestByEntityIdsAsync(tenantId, List.of(device)).get(); + assertEquals(results, asyncResults); + } + private TsKvEntry createEntry(String key, long ts) { return new BasicTsKvEntry(ts, new StringDataEntry(key, RandomStringUtils.random(10))); } diff --git a/dao/src/test/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/dao/src/test/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 0000000000..ab36744aa9 --- /dev/null +++ b/dao/src/test/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.thingsboard.server.dao.TbTimescaleDBContainerProvider diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 60605e1710..821317b6b4 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -115,6 +115,9 @@ cache.specs.trendzSettings.maxSize=10000 cache.specs.aiModel.timeToLiveInMinutes=1440 cache.specs.aiModel.maxSize=10000 +cache.specs.apiKeys.timeToLiveInMinutes=1440 +cache.specs.apiKeys.maxSize=10000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 diff --git a/dao/src/test/resources/nosql-test.properties b/dao/src/test/resources/nosql-test.properties index b688c3c40f..921aebd5fe 100644 --- a/dao/src/test/resources/nosql-test.properties +++ b/dao/src/test/resources/nosql-test.properties @@ -13,6 +13,6 @@ spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=none spring.datasource.username=postgres spring.datasource.password=postgres -spring.datasource.url=jdbc:tc:postgresql:16.6:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb +spring.datasource.url=jdbc:tc:postgresql:18:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.hikari.maximumPoolSize=16 diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties index e3f4861aa9..0639c461a3 100644 --- a/dao/src/test/resources/sql-test.properties +++ b/dao/src/test/resources/sql-test.properties @@ -14,7 +14,7 @@ spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=none spring.datasource.username=postgres spring.datasource.password=postgres -spring.datasource.url=jdbc:tc:postgresql:16.6:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb +spring.datasource.url=jdbc:tc:postgresql:18:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.hikari.maximumPoolSize=16 diff --git a/dao/src/test/resources/sql/system-data.sql b/dao/src/test/resources/sql/system-data.sql index 0b17d4f108..2cd038aae7 100644 --- a/dao/src/test/resources/sql/system-data.sql +++ b/dao/src/test/resources/sql/system-data.sql @@ -66,3 +66,5 @@ VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000 ,'13814000-1dd2-1 '{"type": "BURST", "batchSize": 1000}', '{"type": "SKIP_ALL_FAILURES", "retries": 3, "failurePercentage": 0.0, "pauseBetweenRetries": 3, "maxPauseBetweenRetries": 3}' ); + +INSERT INTO tb_schema_settings (schema_version, product) VALUES (999999000000, 'CE'); diff --git a/dao/src/test/resources/timescale-test.properties b/dao/src/test/resources/timescale-test.properties index 2c5552cb75..e0c0bef25e 100644 --- a/dao/src/test/resources/timescale-test.properties +++ b/dao/src/test/resources/timescale-test.properties @@ -13,6 +13,6 @@ spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=none spring.datasource.username=postgres spring.datasource.password=postgres -spring.datasource.url=jdbc:tc:timescaledb:latest-pg12:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.TimescaleSqlInitializer::initDb +spring.datasource.url=jdbc:tc:tbtimescaledb:latest-pg18:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.TimescaleSqlInitializer::initDb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.hikari.maximumPoolSize = 50 diff --git a/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json b/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json new file mode 100644 index 0000000000..f092315e94 --- /dev/null +++ b/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json @@ -0,0 +1,458 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-orange", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Total connections" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 15, + "w": 19, + "x": 0, + "y": 0 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (protocol) (thingsboard_transport_connections_active{})", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{protocol}}", + "refId": "A" + } + ], + "title": "Active Connections by Protocol", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(209, 138, 78, 1)", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 5, + "x": 19, + "y": 0 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum (thingsboard_transport_connections_active)", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Total Connections", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 19, + "x": 0, + "y": 15 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "thingsboard_transport_connections_active", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{protocol}} {{pod_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Active Connections by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(209, 138, 78, 1)", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Connections" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + } + ] + }, + "gridPos": { + "h": 15, + "w": 5, + "x": 19, + "y": 15 + }, + "id": 23, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum by (pod_name) (thingsboard_transport_connections_active)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "__auto", + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Connection Count by Pod", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "app": true, + "controller_revision_hash": true, + "instance": true, + "job": true, + "kubernetes_name": true, + "kubernetes_namespace": true, + "node_name": true, + "pod_name": false, + "statefulset_kubernetes_io_pod_name": true + }, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value": "Connections", + "pod_name": "Pod" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "Pod" + } + ] + } + } + ], + "type": "table" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "thingsboard" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Transport Connections", + "uid": "lewbrlwjerwkj3", + "version": 1, + "weekStart": "" +} diff --git a/edqs/pom.xml b/edqs/pom.xml index 9bf82c7a65..abb4fbe68b 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard edqs diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index 010994dc88..93914fffbe 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -25,19 +25,19 @@ app: # Application version version: "@project.version@" -# Zookeeper connection parameters +# ZooKeeper connection parameters zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:true}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java index 5d4905f0ef..11423d7484 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceTypeFilterTest.java @@ -19,15 +19,20 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.AttributeScope; 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.EntityType; +import org.thingsboard.server.common.data.edqs.AttributeKv; import org.thingsboard.server.common.data.edqs.LatestTsKv; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -39,8 +44,10 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.UUID; public class DeviceTypeFilterTest extends AbstractEDQTest { @@ -119,7 +126,50 @@ public class DeviceTypeFilterTest extends AbstractEDQTest { Assert.assertEquals("42", first.getLatest().get(EntityKeyType.ENTITY_FIELD).get("createdTime").getValue()); } + @Test + public void testFindDeviceByBooleanAttributeWithMixedTypes() { + DeviceId device1Id = createLoraDevice("LoRa-1"); + DeviceId device2Id = createLoraDevice("LoRa-2"); + DeviceId device3Id = createLoraDevice("LoRa-3"); + + long ts = System.currentTimeMillis(); + addOrUpdate(new AttributeKv(device1Id, AttributeScope.SERVER_SCOPE, + new BaseAttributeKvEntry(new BooleanDataEntry("active", true), ts), 1L)); + addOrUpdate(new AttributeKv(device2Id, AttributeScope.SERVER_SCOPE, + new BaseAttributeKvEntry(new BooleanDataEntry("active", false), ts), 1L)); + addOrUpdate(new AttributeKv(device3Id, AttributeScope.SERVER_SCOPE, + new BaseAttributeKvEntry(new StringDataEntry("active", "true"), ts), 1L)); + + KeyFilter activeFilter = new KeyFilter(); + activeFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "active")); + activeFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate predicate = new BooleanFilterPredicate(); + predicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + predicate.setValue(FilterPredicateValue.fromBoolean(true)); + activeFilter.setPredicate(predicate); + + var result = repository.countEntitiesByQuery(tenantId, null, + getDeviceTypeQuery("LoRa", List.of(activeFilter)), false); + Assert.assertEquals(2, result); + } + + private DeviceId createLoraDevice(String name) { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + Device device = new Device(); + device.setId(deviceId); + device.setTenantId(tenantId); + device.setDeviceProfileId(loraProfileId); + device.setName(name); + device.setCreatedTime(42L); + addOrUpdate(EntityType.DEVICE, device); + return deviceId; + } + private static EntityDataQuery getDeviceTypeQuery(String deviceType) { + return getDeviceTypeQuery(deviceType, null); + } + + private static EntityDataQuery getDeviceTypeQuery(String deviceType, List extraFilters) { DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(Collections.singletonList(deviceType)); var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); @@ -135,7 +185,12 @@ public class DeviceTypeFilterTest extends AbstractEDQTest { nameFilter.setPredicate(predicate); nameFilter.setValueType(EntityKeyValueType.STRING); - return new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); + List keyFilters = new ArrayList<>(); + keyFilters.add(nameFilter); + if (extraFilters != null) { + keyFilters.addAll(extraFilters); + } + return new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); } } diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java index a3691a0889..ab1b6c4060 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.edqs.repo; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -26,7 +27,7 @@ import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.BooleanFilterPredicate.BooleanOperation; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; -import org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; @@ -44,6 +45,7 @@ import org.thingsboard.server.edqs.query.DataKey; import org.thingsboard.server.edqs.query.EdqsFilter; import org.thingsboard.server.edqs.util.RepositoryUtils; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.stream.Stream; @@ -469,4 +471,163 @@ public class RepositoryUtilsTest { return complexFilterPredicate; } + // --- OR operation tests for checkKeyFilters --- + + @Test + public void testCheckKeyFiltersWithOrOperation() { + // Entity with temperature=60; filters: temperature>50 (match) and humidity>80 (no data) + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("orDevice1").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter humidityGt80 = getHumidityFilter(NumericOperation.GREATER, 80); + + // OR: temperature>50 matches, short-circuits to true + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), ComplexOperation.OR)).isTrue(); + } + + @Test + public void testCheckKeyFiltersWithOrOperationNoMatch() { + // Entity with temperature=30; filters: temperature>50 (no match) and temperature<10 (no match) + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("orDevice2").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 30.0)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter tempLt10 = getTemperatureFilter(NumericOperation.LESS, 10); + + // OR: neither filter matches + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, tempLt10), ComplexOperation.OR)).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithOrOperationSameKey() { + // Entity with temperature=5; filters: temperature>50 (no match) and temperature<10 (match) + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("orDevice3").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 5.0)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter tempLt10 = getTemperatureFilter(NumericOperation.LESS, 10); + + // OR on same key: second filter matches + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, tempLt10), ComplexOperation.OR)).isTrue(); + } + + @Test + public void testCheckKeyFiltersWithAndOperationUnchanged() { + // Entity with temperature=60 and humidity=90 + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("andDevice1").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData.putTs(6, new LongDataPoint(System.currentTimeMillis(), 90)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter humidityGt80 = getHumidityFilter(NumericOperation.GREATER, 80); + + // AND: both match + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), ComplexOperation.AND)).isTrue(); + + // Now with humidity=70 -- humidity>80 fails + DeviceData deviceData2 = new DeviceData(UUID.randomUUID()); + deviceData2.setCustomerId(UUID.randomUUID()); + deviceData2.setFields(DeviceFields.builder().name("andDevice2").build()); + deviceData2.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData2.putTs(6, new LongDataPoint(System.currentTimeMillis(), 70)); + + // AND: humidity>80 fails + assertThat(RepositoryUtils.checkKeyFilters(deviceData2, List.of(tempGt50, humidityGt80), ComplexOperation.AND)).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithNullOperationDefaultsToAnd() { + // Entity with temperature=60 and humidity=90 + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("nullOpDevice").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData.putTs(6, new LongDataPoint(System.currentTimeMillis(), 90)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter humidityGt80 = getHumidityFilter(NumericOperation.GREATER, 80); + + // null operation should behave like AND + boolean nullResult = RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), null); + boolean andResult = RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), ComplexOperation.AND); + assertThat(nullResult).isEqualTo(andResult); + assertThat(nullResult).isTrue(); + + // Entity with temperature=60 and humidity=70 -- humidity>80 fails + DeviceData deviceData2 = new DeviceData(UUID.randomUUID()); + deviceData2.setCustomerId(UUID.randomUUID()); + deviceData2.setFields(DeviceFields.builder().name("nullOpDevice2").build()); + deviceData2.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData2.putTs(6, new LongDataPoint(System.currentTimeMillis(), 70)); + + boolean nullResult2 = RepositoryUtils.checkKeyFilters(deviceData2, List.of(tempGt50, humidityGt80), null); + boolean andResult2 = RepositoryUtils.checkKeyFilters(deviceData2, List.of(tempGt50, humidityGt80), ComplexOperation.AND); + assertThat(nullResult2).isEqualTo(andResult2); + assertThat(nullResult2).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithOrEmptyFilters() { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("emptyFilterDevice").build()); + + // OR with empty filters = include all (matches SQL behavior) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, Collections.emptyList(), ComplexOperation.OR)).isTrue(); + } + + @Test + public void testCheckKeyFiltersWithOrStringEntityFieldFilters() { + // Entity with name="loranet device 123" + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("loranet device 123").build()); + + // Filter 1: name STARTS_WITH "mqtt" (no match) + EdqsFilter nameStartsMqtt = getStringEntityFieldFilter("name", StringOperation.STARTS_WITH, "mqtt"); + // Filter 2: name CONTAINS "lora" (match) + EdqsFilter nameContainsLora = getStringEntityFieldFilter("name", StringOperation.CONTAINS, "lora"); + + // OR: second filter matches + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(nameStartsMqtt, nameContainsLora), ComplexOperation.OR)).isTrue(); + + // AND: first filter fails + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(nameStartsMqtt, nameContainsLora), ComplexOperation.AND)).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithOrEntityFieldNullStringSpecialCase() { + // Entity with name=null — tests the special case where null string + entity field = match + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(null).build()); + + // Filter on entity field "label" which is not set (null) + EdqsFilter labelFilter = getStringEntityFieldFilter("label", StringOperation.STARTS_WITH, "test"); + // Filter on timeseries (non-entity-field) that doesn't exist + EdqsFilter tempFilter = getTemperatureFilter(NumericOperation.GREATER, 50); + + // Entity field with null string = match (special case in evaluateSingleFilter) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter), ComplexOperation.OR)).isTrue(); + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter), ComplexOperation.AND)).isTrue(); + + // Non-entity-field with missing data = no match + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempFilter), ComplexOperation.OR)).isFalse(); + + // OR: entity field null match + missing timeseries = true (first matches) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter, tempFilter), ComplexOperation.OR)).isTrue(); + + // AND: entity field null match + missing timeseries = false (second fails) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter, tempFilter), ComplexOperation.AND)).isFalse(); + } + } diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 2601ac47fe..c3dfaf00c7 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard @@ -79,6 +79,10 @@ org.apache.httpcomponents httpclient + + com.slack.api + slack-api-client + org.eclipse.leshan leshan-client-cf @@ -118,6 +122,11 @@ ch.qos.logback logback-classic + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml index bbc8e7ad95..50536a7110 100644 --- a/monitoring/src/main/conf/logback.xml +++ b/monitoring/src/main/conf/logback.xml @@ -19,20 +19,6 @@ - - ${pkg.logFolder}/${pkg.name}.log - - ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n @@ -40,11 +26,11 @@ - - + + + - diff --git a/monitoring/src/main/conf/tb-monitoring.conf b/monitoring/src/main/conf/tb-monitoring.conf index 653a25bb54..b4042f1144 100644 --- a/monitoring/src/main/conf/tb-monitoring.conf +++ b/monitoring/src/main/conf/tb-monitoring.conf @@ -14,9 +14,10 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=3,filesize=10M" export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" -export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-monitoring.out export LOADER_PATH=/usr/share/tb-monitoring/conf diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index 836d7de2d8..9218b78bea 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -15,36 +15,43 @@ */ package org.thingsboard.monitoring; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.scheduling.annotation.EnableScheduling; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.monitoring.data.notification.InfoNotification; +import org.thingsboard.monitoring.notification.NotificationService; import org.thingsboard.monitoring.service.BaseMonitoringService; import org.thingsboard.monitoring.service.MonitoringEntityService; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.Map; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @SpringBootApplication @EnableScheduling @Slf4j +@RequiredArgsConstructor public class ThingsboardMonitoringApplication { - @Autowired - private List> monitoringServices; - @Autowired - private MonitoringEntityService entityService; + private final List> monitoringServices; + private final MonitoringEntityService entityService; + private final NotificationService notificationService; @Value("${monitoring.monitoring_rate_ms}") private int monitoringRateMs; + ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring"); + public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) .properties(Map.of("spring.config.name", "tb-monitoring")) @@ -56,12 +63,33 @@ public class ThingsboardMonitoringApplication { entityService.checkEntities(); monitoringServices.forEach(BaseMonitoringService::init); - ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); - scheduler.scheduleWithFixedDelay(() -> { - monitoringServices.forEach(monitoringService -> { - monitoringService.runChecks(); - }); - }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + for (int i = 0; i < monitoringServices.size(); i++) { + int initialDelay = (monitoringRateMs / monitoringServices.size()) * i; + BaseMonitoringService service = monitoringServices.get(i); + log.info("Scheduling initialDelay {}, fixedDelay {} for monitoring '{}' ", initialDelay, monitoringRateMs, service.getClass().getSimpleName()); + scheduler.scheduleWithFixedDelay(service::runChecks, initialDelay, monitoringRateMs, TimeUnit.MILLISECONDS); + } + + String publicDashboardUrl = entityService.getDashboardPublicLink(); + notificationService.sendNotification(new InfoNotification(":rocket: <"+publicDashboardUrl+"|Monitoring> started")); + } + + @EventListener(ContextClosedEvent.class) + public void onShutdown(ContextClosedEvent event) { + log.info("Shutting down monitoring service"); + try { + var futures = notificationService.sendNotification(new InfoNotification(":warning: Monitoring is shutting down")); + for (Future future : futures) { + future.get(5, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.warn("Failed to send shutdown notification", e); + } + } + + @PreDestroy + public void shutdownScheduler() { + scheduler.shutdown(); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java index 251b5e1d85..cc9801dcad 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java @@ -16,13 +16,21 @@ package org.thingsboard.monitoring.config.transport; import lombok.Data; +import org.thingsboard.monitoring.data.notification.ShortNameProvider; @Data -public class TransportInfo { +public class TransportInfo implements ShortNameProvider { private final TransportType type; private final TransportMonitoringTarget target; + public String getShortName() { + if (target.getQueue().equals("Main")) { + return type.getName(); + } + return type.getName() + " " + target.getQueue(); + } + @Override public String toString() { if (target.getQueue().equals("Main")) { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java index 28fa6a9e18..c566e8f896 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java @@ -18,6 +18,9 @@ package org.thingsboard.monitoring.data; public class MonitoredServiceKey { public static final String GENERAL = "Monitoring"; + public static final String LOGIN = "Login"; + public static final String WS_CONNECT = "WS Connect"; + public static final String WS_SUBSCRIBE = "WS Subscribe"; public static final String EDQS = "*EDQS*"; } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/AffectedService.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/AffectedService.java new file mode 100644 index 0000000000..b30a398223 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/AffectedService.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +public record AffectedService(String name, Status status, int failureCount) { + + public enum Status { FAILING, RECOVERED, HIGH_LATENCY } + + public static AffectedService failing(String name, int failureCount) { + return new AffectedService(name, Status.FAILING, failureCount); + } + + public static AffectedService recovered(String name) { + return new AffectedService(name, Status.RECOVERED, 0); + } + + public static AffectedService highLatency(String name) { + return new AffectedService(name, Status.HIGH_LATENCY, 0); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java index 9a319eb6c9..3e739ec4dc 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java @@ -18,6 +18,8 @@ package org.thingsboard.monitoring.data.notification; import org.thingsboard.monitoring.data.Latency; import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; public class HighLatencyNotification implements Notification { @@ -39,4 +41,11 @@ public class HighLatencyNotification implements Notification { return text.toString(); } + @Override + public List getAffectedServices() { + return highLatencies.stream() + .map(latency -> AffectedService.highLatency(latency.getKey())) + .collect(Collectors.toList()); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java new file mode 100644 index 0000000000..6906e8c732 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class InfoNotification implements Notification { + private final String message; + @Override + public String getText() { + return message; + } + + @Override + public boolean isIncident() { + return false; + } +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java index 33ed7f8328..7734fc62ee 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java @@ -15,8 +15,18 @@ */ package org.thingsboard.monitoring.data.notification; +import java.util.List; + public interface Notification { String getText(); + default boolean isIncident() { + return true; + } + + default List getAffectedServices() { + return List.of(); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java index dd93f232a6..69e336f334 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java @@ -18,6 +18,10 @@ package org.thingsboard.monitoring.data.notification; import lombok.Getter; import org.apache.commons.lang3.exception.ExceptionUtils; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + @Getter public class ServiceFailureNotification implements Notification { @@ -43,7 +47,65 @@ public class ServiceFailureNotification implements Notification { if (errorMsg == null) { errorMsg = error.getClass().getSimpleName(); } + errorMsg = stripResponseBody(errorMsg); + errorMsg = linkifyRequestUrl(errorMsg); return String.format("%s - Failure: %s (number of subsequent failures: %s)", serviceKey, errorMsg, failuresCount); } + // Spring RestClient: '... request for ""' + private static final Pattern REQUEST_FOR_URL_PATTERN = Pattern.compile("request for \"(https?://[^\"\\s]+)\""); + // Apache HttpClient wrapped by Spring: 'I/O error on POST request: Connect to failed: ' + private static final Pattern REQUEST_CONNECT_PATTERN = Pattern.compile("request: Connect to (https?://\\S+?) failed:"); + + static String linkifyRequestUrl(String msg) { + if (msg == null) { + return null; + } + // Slack mrkdwn link: + Matcher m = REQUEST_FOR_URL_PATTERN.matcher(msg); + if (m.find()) { + return m.replaceAll("<$1|request>"); + } + Matcher m2 = REQUEST_CONNECT_PATTERN.matcher(msg); + if (m2.find()) { + return m2.replaceAll("<$1|request>:"); + } + return msg; + } + + static String stripResponseBody(String msg) { + if (msg == null) { + return null; + } + int htmlIdx = -1; + for (String marker : new String[]{"= 0 && (htmlIdx < 0 || idx < htmlIdx)) { + htmlIdx = idx; + } + } + if (htmlIdx > 0) { + msg = msg.substring(0, htmlIdx).stripTrailing(); + if (msg.endsWith("\"")) { + msg = msg.substring(0, msg.length() - 1).stripTrailing(); + } + if (msg.endsWith(":")) { + msg = msg.substring(0, msg.length() - 1).stripTrailing(); + } + } + return msg; + } + + @Override + public List getAffectedServices() { + return List.of(AffectedService.failing(shortName(serviceKey), failuresCount)); + } + + static String shortName(Object serviceKey) { + if (serviceKey instanceof ShortNameProvider provider) { + return provider.getShortName(); + } + return serviceKey.toString(); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java index b6e0c7c695..31b32422a1 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java @@ -15,6 +15,8 @@ */ package org.thingsboard.monitoring.data.notification; +import java.util.List; + public class ServiceRecoveryNotification implements Notification { private final Object serviceKey; @@ -28,4 +30,9 @@ public class ServiceRecoveryNotification implements Notification { return String.format("%s is OK", serviceKey); } + @Override + public List getAffectedServices() { + return List.of(AffectedService.recovered(ServiceFailureNotification.shortName(serviceKey))); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ShortNameProvider.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ShortNameProvider.java new file mode 100644 index 0000000000..96c63023af --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ShortNameProvider.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +public interface ShortNameProvider { + + String getShortName(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java index 1d18ab9063..091a331438 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java @@ -22,10 +22,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.monitoring.data.notification.Notification; import org.thingsboard.monitoring.notification.channels.NotificationChannel; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; @Component @RequiredArgsConstructor @@ -38,22 +41,38 @@ public class NotificationService { @Value("${monitoring.notifications.message_prefix}") private String messagePrefix; - public void sendNotification(Notification notification) { + public List> sendNotification(Notification notification) { String message; if (StringUtils.isEmpty(messagePrefix)) { message = notification.getText(); } else { - message = messagePrefix + System.lineSeparator() + notification.getText(); + message = messagePrefix + " " + notification.getText(); } - notificationChannels.forEach(notificationChannel -> { + return notificationChannels.stream().map(notificationChannel -> notificationExecutor.submit(() -> { try { - notificationChannel.sendNotification(message); + notificationChannel.sendNotification(message, notification); } catch (Exception e) { log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e); } - }); - }); + }) + ).toList(); } + @PreDestroy + public void shutdownExecutor() { + try { + notificationExecutor.shutdown(); + if (!notificationExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + var dropped = notificationExecutor.shutdownNow(); + log.warn("Notification executor did not terminate in time. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } + } catch (InterruptedException e) { + var dropped = notificationExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + log.warn("Interrupted during notification executor shutdown. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } catch (Exception e) { + log.warn("Unexpected error while shutting down notification executor", e); + } + } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java index 126942cb3f..614fddda31 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java @@ -15,8 +15,10 @@ */ package org.thingsboard.monitoring.notification.channels; +import org.thingsboard.monitoring.data.notification.Notification; + public interface NotificationChannel { - void sendNotification(String message); + void sendNotification(String message, Notification notification); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackApiClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackApiClient.java new file mode 100644 index 0000000000..4ad74d7811 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackApiClient.java @@ -0,0 +1,125 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.channels.impl; + +import com.slack.api.Slack; +import com.slack.api.SlackConfig; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiTextResponse; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.methods.request.chat.ChatUpdateRequest; +import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import com.slack.api.methods.response.chat.ChatUpdateResponse; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SlackApiClient { + + private static final int DEFAULT_CALL_TIMEOUT_MS = 5000; + + private final Slack slack; + private final String botToken; + + public SlackApiClient(String botToken) { + this(botToken, DEFAULT_CALL_TIMEOUT_MS); + } + + public SlackApiClient(String botToken, int callTimeoutMs) { + this.botToken = botToken; + SlackConfig config = new SlackConfig(); + config.setHttpClientCallTimeoutMillis(callTimeoutMs); + config.setHttpClientReadTimeoutMillis(callTimeoutMs); + config.setHttpClientWriteTimeoutMillis(callTimeoutMs); + this.slack = Slack.getInstance(config); + } + + public String postMessage(String channelId, String text) { + ChatPostMessageRequest request = ChatPostMessageRequest.builder() + .channel(channelId) + .text(text) + .build(); + ChatPostMessageResponse response = sendRequest(request); + return response.getTs(); + } + + public String postThreadReply(String channelId, String threadTs, String text) { + ChatPostMessageRequest request = ChatPostMessageRequest.builder() + .channel(channelId) + .text(text) + .threadTs(threadTs) + .build(); + ChatPostMessageResponse response = sendRequest(request); + return response.getTs(); + } + + public void close() { + try { + slack.close(); + } catch (Exception e) { + log.warn("Failed to close Slack client", e); + } + } + + public void updateMessage(String channelId, String ts, String text) { + ChatUpdateRequest request = ChatUpdateRequest.builder() + .channel(channelId) + .ts(ts) + .text(text) + .build(); + MethodsClient client = slack.methods(botToken); + ChatUpdateResponse response; + try { + response = client.chatUpdate(request); + } catch (Exception e) { + throw new RuntimeException("Failed to update Slack message: " + e.getMessage(), e); + } + checkResponse(response); + } + + private ChatPostMessageResponse sendRequest(ChatPostMessageRequest request) { + MethodsClient client = slack.methods(botToken); + ChatPostMessageResponse response; + try { + response = client.chatPostMessage(request); + } catch (Exception e) { + throw new RuntimeException("Failed to send Slack message: " + e.getMessage(), e); + } + checkResponse(response); + return response; + } + + private void checkResponse(SlackApiTextResponse response) { + if (response.isOk()) { + return; + } + String error = response.getError(); + if (error != null) { + switch (error) { + case "missing_scope" -> { + String neededScope = response.getNeeded(); + error = "bot token scope '" + neededScope + "' is needed"; + } + case "not_in_channel" -> error = "app needs to be added to the channel"; + } + } else if (response.getWarning() != null) { + error = "warning: " + response.getWarning(); + } else { + error = "unknown error"; + } + throw new RuntimeException("Slack API error: " + error); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackIncidentTransport.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackIncidentTransport.java new file mode 100644 index 0000000000..8812714d62 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackIncidentTransport.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.channels.impl; + +import org.thingsboard.monitoring.notification.incident.IncidentTransport; + +public class SlackIncidentTransport implements IncidentTransport { + + private final SlackApiClient slackApiClient; + private final String channelId; + + public SlackIncidentTransport(SlackApiClient slackApiClient, String channelId) { + this.slackApiClient = slackApiClient; + this.channelId = channelId; + } + + @Override + public String postIncident(String text) { + return slackApiClient.postMessage(channelId, text); + } + + @Override + public void postThreadReply(String threadId, String text) { + slackApiClient.postThreadReply(channelId, threadId, text); + } + + @Override + public void updateIncident(String threadId, String text) { + slackApiClient.updateMessage(channelId, threadId, text); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java index c1d25c43c0..06990c5a51 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java @@ -16,13 +16,16 @@ package org.thingsboard.monitoring.notification.channels.impl; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import org.thingsboard.monitoring.data.notification.Notification; import org.thingsboard.monitoring.notification.channels.NotificationChannel; +import org.thingsboard.monitoring.notification.incident.IncidentManager; import java.time.Duration; import java.util.Map; @@ -35,19 +38,73 @@ public class SlackNotificationChannel implements NotificationChannel { @Value("${monitoring.notifications.slack.webhook_url}") private String webhookUrl; + @Value("${monitoring.notifications.slack.bot_token:}") + private String botToken; + + @Value("${monitoring.notifications.slack.channel_id:}") + private String channelId; + + @Value("${monitoring.notifications.incident.enabled:}") + private boolean incidentEnabled; + + @Value("${monitoring.notifications.incident.resolution_timeout_s:}") + private long resolutionTimeoutSeconds; + + @Value("${monitoring.notifications.incident.tag_channel:}") + private boolean tagChannel; + + @Value("${monitoring.notifications.message_prefix:}") + private String messagePrefix; + private RestTemplate restTemplate; + private SlackApiClient slackApiClient; + private IncidentManager incidentManager; @PostConstruct private void init() { - restTemplate = new RestTemplateBuilder() - .setConnectTimeout(Duration.ofSeconds(5)) - .setReadTimeout(Duration.ofSeconds(2)) - .build(); + boolean hasBotConfig = botToken != null && !botToken.isEmpty() && channelId != null && !channelId.isEmpty(); + if (hasBotConfig) { + slackApiClient = new SlackApiClient(botToken); + log.info("Slack API mode enabled (channel: {})", channelId); + if (incidentEnabled) { + incidentManager = new IncidentManager(new SlackIncidentTransport(slackApiClient, channelId), + resolutionTimeoutSeconds, messagePrefix, tagChannel); + log.info("Incident grouping enabled via Slack (resolution timeout: {}s)", resolutionTimeoutSeconds); + } + } else { + if (incidentEnabled) { + log.warn("Incident grouping is enabled but Slack bot_token/channel_id are not set; " + + "falling back to plain webhook mode without incident support"); + } + restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(2)) + .build(); + log.info("Slack webhook mode enabled"); + } } @Override - public void sendNotification(String message) { - restTemplate.postForObject(webhookUrl, Map.of("text", message), String.class); + public void sendNotification(String message, Notification notification) { + if (incidentManager != null && notification.isIncident()) { + // Pass the raw notification text: IncidentManager already puts the prefix into the + // incident header, so pre-prefixing the thread reply would double it up. + incidentManager.sendAlert(notification.getText(), notification.getAffectedServices()); + } else if (slackApiClient != null) { + slackApiClient.postMessage(channelId, message); + } else { + restTemplate.postForObject(webhookUrl, Map.of("text", message), String.class); + } + } + + @PreDestroy + private void destroy() { + if (incidentManager != null) { + incidentManager.shutdown(); + } + if (slackApiClient != null) { + slackApiClient.close(); + } } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/incident/IncidentManager.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/incident/IncidentManager.java new file mode 100644 index 0000000000..c25abfc081 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/incident/IncidentManager.java @@ -0,0 +1,292 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.incident; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.monitoring.data.notification.AffectedService; + +import java.time.Duration; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Thread-safety: all public entry points and scheduled callbacks are {@code synchronized} on the + * manager instance. Transport I/O is performed while the monitor is held. This is safe under the + * assumptions that (a) the transport enforces a short per-call timeout, and (b) notification + * producers are single-threaded; see the Slack client (default 5s) for the Slack-based transport. + */ +@Slf4j +public class IncidentManager { + + private final IncidentTransport transport; + private final long resolutionTimeoutSeconds; + private final String messagePrefix; + private final boolean tagChannel; + private final ScheduledExecutorService scheduler; + + private String activeIncidentThreadId; + private ScheduledFuture resolutionTask; + private ScheduledFuture durationUpdateTask; + private Instant incidentStartTime; + private Instant lastAlertTime; + private final Map failingServices = new LinkedHashMap<>(); + private final Map recoveredServices = new LinkedHashMap<>(); + private final Set highLatencyServices = new LinkedHashSet<>(); + + public IncidentManager(IncidentTransport transport, long resolutionTimeoutSeconds, + String messagePrefix, boolean tagChannel) { + this.transport = transport; + this.resolutionTimeoutSeconds = resolutionTimeoutSeconds; + this.messagePrefix = messagePrefix; + this.tagChannel = tagChannel; + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "incident-manager"); + t.setDaemon(true); + return t; + }); + } + + public synchronized void sendAlert(String message, List affectedServices) { + try { + if (activeIncidentThreadId == null) { + if (affectedServices.stream().allMatch(s -> s.status() == AffectedService.Status.RECOVERED)) { + return; + } + incidentStartTime = Instant.now(); + failingServices.clear(); + recoveredServices.clear(); + highLatencyServices.clear(); + applyAffectedServices(affectedServices); + activeIncidentThreadId = transport.postIncident(buildOngoingMessageText()); + startDurationUpdater(); + log.info("New incident created, thread id: {}", activeIncidentThreadId); + } else if (applyAffectedServices(affectedServices)) { + safeUpdateHeader(); + } + + try { + transport.postThreadReply(activeIncidentThreadId, message); + log.debug("Alert added to incident thread {}", activeIncidentThreadId); + } catch (Exception e) { + log.error("Failed to post alert to incident thread {}", activeIncidentThreadId, e); + } + } finally { + if (activeIncidentThreadId != null) { + lastAlertTime = Instant.now(); + // High latency is a warning only — it has no explicit recovery signal + // (HighLatencyNotification fires only when something is above threshold), + // so resolution hinges on failing services alone. + if (failingServices.isEmpty()) { + resetResolutionTimer(); + } else { + cancelResolutionTimer(); + } + } + } + } + + private boolean applyAffectedServices(List affectedServices) { + boolean changed = false; + Set latencySnapshot = null; + for (AffectedService service : affectedServices) { + String name = service.name(); + switch (service.status()) { + case FAILING -> { + Integer prev = failingServices.put(name, service.failureCount()); + if (prev == null || prev.intValue() != service.failureCount()) { + changed = true; + } + if (recoveredServices.remove(name) != null) { + changed = true; + } + } + case RECOVERED -> { + Integer lastFailureCount = failingServices.remove(name); + if (lastFailureCount != null) { + recoveredServices.put(name, lastFailureCount); + changed = true; + } + } + case HIGH_LATENCY -> { + if (latencySnapshot == null) { + latencySnapshot = new LinkedHashSet<>(); + } + latencySnapshot.add(name); + } + } + } + // HighLatencyNotification carries the full current set of high latencies, so treat it as a + // snapshot: replace highLatencyServices entirely. Without this, a brief spike would stay + // yellow in the header until the incident resolves. + if (latencySnapshot != null && !latencySnapshot.equals(highLatencyServices)) { + highLatencyServices.clear(); + highLatencyServices.addAll(latencySnapshot); + changed = true; + } + return changed; + } + + private String buildOngoingMessageText() { + StringBuilder sb = new StringBuilder(); + if (tagChannel) { + sb.append(" "); + } + if (messagePrefix != null && !messagePrefix.isEmpty()) { + sb.append("*").append(messagePrefix).append("*"); + } + sb.append(" :rotating_light:"); + Duration elapsed = Duration.between(incidentStartTime, Instant.now()); + if (elapsed.toMinutes() >= 1) { + sb.append(" (").append(formatDuration(elapsed)).append(")"); + } + if (hasAffected()) { + sb.append(" | ").append(formatAffectedServices()); + } + return sb.toString(); + } + + private boolean hasAffected() { + return !failingServices.isEmpty() || !recoveredServices.isEmpty() || !highLatencyServices.isEmpty(); + } + + private void safeUpdateHeader() { + try { + transport.updateIncident(activeIncidentThreadId, buildOngoingMessageText()); + } catch (Exception e) { + log.error("Failed to update incident message", e); + } + } + + private void resetResolutionTimer() { + cancelResolutionTimer(); + resolutionTask = scheduler.schedule(this::resolveIncident, resolutionTimeoutSeconds, TimeUnit.SECONDS); + } + + private void cancelResolutionTimer() { + if (resolutionTask != null) { + resolutionTask.cancel(false); + resolutionTask = null; + } + } + + private void startDurationUpdater() { + if (durationUpdateTask != null) { + durationUpdateTask.cancel(false); + } + durationUpdateTask = scheduler.scheduleAtFixedRate(this::updateDuration, 60, 60, TimeUnit.SECONDS); + } + + private synchronized void updateDuration() { + if (activeIncidentThreadId == null) { + return; + } + safeUpdateHeader(); + } + + private void stopDurationUpdater() { + if (durationUpdateTask != null) { + durationUpdateTask.cancel(false); + durationUpdateTask = null; + } + } + + static String formatDuration(Duration duration) { + long totalMinutes = duration.toMinutes(); + if (totalMinutes < 60) { + return totalMinutes + "m"; + } + long hours = totalMinutes / 60; + long minutes = totalMinutes % 60; + return minutes > 0 ? hours + "h" + minutes + "m" : hours + "h"; + } + + synchronized void resolveIncident() { + if (activeIncidentThreadId == null) { + return; + } + String threadId = activeIncidentThreadId; + stopDurationUpdater(); + String resolutionMessage = buildResolutionMessage(); + activeIncidentThreadId = null; + resolutionTask = null; + failingServices.clear(); + recoveredServices.clear(); + highLatencyServices.clear(); + try { + transport.updateIncident(threadId, resolutionMessage); + log.info("Incident resolved (thread was {})", threadId); + } catch (Exception e) { + log.error("Failed to send incident resolution message", e); + } + } + + private String buildResolutionMessage() { + Duration totalDuration = lastAlertTime != null + ? Duration.between(incidentStartTime, lastAlertTime) + : Duration.between(incidentStartTime, Instant.now()); + StringBuilder sb = new StringBuilder(); + if (messagePrefix != null && !messagePrefix.isEmpty()) { + sb.append("*").append(messagePrefix).append("*"); + } + sb.append(" :white_check_mark:"); + sb.append(" (").append(formatDuration(totalDuration)).append(")"); + if (hasAffected()) { + sb.append(" | ").append(formatAffectedServices()).append("\n"); + } + return sb.toString(); + } + + private String formatAffectedServices() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry entry : failingServices.entrySet()) { + if (!first) sb.append(", "); + sb.append(":red_circle: ").append(entry.getKey()).append(" (").append(entry.getValue()).append(")"); + first = false; + } + for (String name : highLatencyServices) { + if (!first) sb.append(", "); + sb.append(":large_yellow_circle: ").append(name); + first = false; + } + for (Map.Entry entry : recoveredServices.entrySet()) { + if (!first) sb.append(", "); + sb.append(":large_green_circle: ").append(entry.getKey()).append(" (").append(entry.getValue()).append(")"); + first = false; + } + return sb.toString(); + } + + public void shutdown() { + scheduler.shutdownNow(); + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Incident scheduler did not terminate in time"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/incident/IncidentTransport.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/incident/IncidentTransport.java new file mode 100644 index 0000000000..0d6b4cac2a --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/incident/IncidentTransport.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.incident; + +public interface IncidentTransport { + + String postIncident(String text); + + void postThreadReply(String threadId, String text); + + void updateIncident(String threadId, String text); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index b73194469f..c0e85da1ac 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -89,11 +89,10 @@ public abstract class BaseHealthChecker { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java index ea154e49c0..9c8c8fc533 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java @@ -117,28 +117,60 @@ public abstract class BaseMonitoringService, T ext } try { log.info("Starting {}", getName()); - stopWatch.start(); - String accessToken = tbClient.logIn(); - reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); - try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { + String accessToken; + try { stopWatch.start(); - wsClient.subscribeForTelemetry(devices, getTestTelemetryKeys()).waitForReply(); - reporter.reportLatency(Latencies.WS_SUBSCRIBE, stopWatch.getTime()); + accessToken = tbClient.logIn(); + reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); + reporter.serviceIsOk(MonitoredServiceKey.LOGIN); + } catch (Exception e) { + reporter.serviceFailure(MonitoredServiceKey.LOGIN, e); + return; + } + + WsClient wsClient; + try { + wsClient = wsClientFactory.createClient(accessToken); + reporter.serviceIsOk(MonitoredServiceKey.WS_CONNECT); + } catch (Exception e) { + reporter.serviceFailure(MonitoredServiceKey.WS_CONNECT, e); + return; + } + + try (WsClient ws = wsClient) { + try { + stopWatch.start(); + ws.subscribeForTelemetry(devices, getTestTelemetryKeys()).waitForReply(); + reporter.reportLatency(Latencies.WS_SUBSCRIBE, stopWatch.getTime()); + reporter.serviceIsOk(MonitoredServiceKey.WS_SUBSCRIBE); + } catch (Exception e) { + reporter.serviceFailure(MonitoredServiceKey.WS_SUBSCRIBE, e); + return; + } for (BaseHealthChecker healthChecker : healthCheckers) { - check(healthChecker, wsClient); + check(healthChecker, ws); } } if (checkEdqs) { - stopWatch.start(); - checkEdqs(); - reporter.reportLatency(Latencies.EDQS_QUERY, stopWatch.getTime()); - reporter.serviceIsOk(MonitoredServiceKey.EDQS); + try { + stopWatch.start(); + checkEdqs(); + reporter.reportLatency(Latencies.EDQS_QUERY, stopWatch.getTime()); + reporter.serviceIsOk(MonitoredServiceKey.EDQS); + } catch (ServiceFailureException e) { + reporter.serviceFailure(e.getServiceKey(), e); + return; + } catch (Exception e) { + reporter.serviceFailure(MonitoredServiceKey.EDQS, e); + return; + } } reporter.reportLatencies(); + reporter.serviceIsOk(MonitoredServiceKey.GENERAL); log.debug("Finished {}", getName()); } catch (ServiceFailureException e) { reporter.serviceFailure(e.getServiceKey(), e); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index 57fd7d280d..d2c275945a 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -30,20 +30,25 @@ import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; import org.thingsboard.monitoring.config.transport.TransportType; import org.thingsboard.monitoring.util.ResourceUtils; import org.thingsboard.server.common.data.DataConstants; +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.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; @@ -57,7 +62,6 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.kv.KvEntry; -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.RuleChainType; @@ -66,6 +70,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_CF_TELEMETRY_KEY; @@ -76,11 +82,16 @@ import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_TELEMETR @RequiredArgsConstructor public class MonitoringEntityService { + private static final String DASHBOARD_TITLE = "[Monitoring] Cloud monitoring"; + private static final String DASHBOARD_RESOURCE_PATH = "dashboard_cloud_monitoring.json"; + private final TbClient tbClient; @Value("${monitoring.calculated_fields.enabled:true}") private boolean calculatedFieldsMonitoringEnabled; + DashboardId dashboardId = null; + public void checkEntities() { RuleChain ruleChain = tbClient.getRuleChains(RuleChainType.CORE, new PageLink(10)).getData().stream() .filter(RuleChain::isRoot) @@ -95,27 +106,34 @@ public class MonitoringEntityService { int currentVersion = Integer.parseInt(attributes.getOrDefault("version", "0")); int newVersion = ruleChainDescriptor.get("version").asInt(); if (currentVersion == newVersion) { - log.info("Not updating rule chain, version is the same ({})", currentVersion); - return; + log.debug("Not updating rule chain, version is the same ({})", currentVersion); } else { log.info("Updating rule chain '{}' from version {} to {}", ruleChain.getName(), currentVersion, newVersion); + + String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), + "\\$\\{MONITORING:(.+?)}", matchResult -> { + String key = matchResult.group(1); + String value = attributes.get(key); + if (value == null) { + throw new IllegalArgumentException("No attribute found for key " + key); + } + log.info("Using {}: {}", key, value); + return value; + }); + RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); + metaData.setRuleChainId(ruleChainId); + tbClient.saveRuleChainMetaData(metaData); + tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() + .put("version", newVersion)); } - String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), - "\\$\\{MONITORING:(.+?)}", matchResult -> { - String key = matchResult.group(1); - String value = attributes.get(key); - if (value == null) { - throw new IllegalArgumentException("No attribute found for key " + key); - } - log.info("Using {}: {}", key, value); - return value; - }); - RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); - metaData.setRuleChainId(ruleChainId); - tbClient.saveRuleChainMetaData(metaData); - tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() - .put("version", newVersion)); + Asset asset = getOrCreateMonitoringAsset(); + Dashboard dashboard = getOrCreateMonitoringDashboard(); + + tbClient.assignAssetToPublicCustomer(asset.getId()); + tbClient.assignDashboardToPublicCustomer(dashboard.getId()); + + this.dashboardId = Optional.ofNullable(dashboard).map(Dashboard::getId).orElse(null); } public Asset getOrCreateMonitoringAsset() { @@ -241,12 +259,77 @@ public class MonitoringEntityService { TEST_TELEMETRY_KEY, testDataArgument )); configuration.setExpression("return { \"" + TEST_CF_TELEMETRY_KEY + "\": " + TEST_TELEMETRY_KEY + " + \"-cf\" };"); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - configuration.setOutput(output); + configuration.setOutput(new TimeSeriesOutput()); calculatedField.setConfiguration(configuration); calculatedField.setDebugMode(true); tbClient.saveCalculatedField(calculatedField); } + public String getDashboardPublicLink() { + String link = ""; + try { + Optional infoOpt = tbClient.getDashboardInfoById(dashboardId); + if (infoOpt.isPresent()) { + String publicCustomerId = null; + Set customers = infoOpt.get().getAssignedCustomers(); + if (customers != null) { + publicCustomerId = customers.stream() + .filter(ShortCustomerInfo::isPublic) + .map(c -> c.getCustomerId().getId().toString()) + .findFirst().orElse(null); + } + if (publicCustomerId != null) { + link = buildPublicDashboardLink(dashboardId, publicCustomerId); + log.info("Public Monitoring dashboard link: {}", link); + } else { + log.warn("Dashboard is not assigned to public customer. Public link can't be generated."); + } + } + } catch (Exception e) { + log.error("Failed to get a public link to Monitoring dashboard ", e); + } + return link; + } + + private Dashboard getOrCreateMonitoringDashboard() { + Dashboard existing = findDashboardByTitle(DASHBOARD_TITLE).orElse(null); + if (existing != null) { + log.debug("Found Monitoring dashboard '{}' with id {}", existing.getTitle(), existing.getId()); + return existing; + } + + Dashboard dashboardFromResource = ResourceUtils.getResource(DASHBOARD_RESOURCE_PATH, Dashboard.class); + dashboardFromResource.setTitle(DASHBOARD_TITLE); + //Optional.ofNullable(existing).map(Dashboard::getId).ifPresent(dashboardFromResource::setId); + Dashboard saved = tbClient.saveDashboard(dashboardFromResource); + log.info("Created Monitoring dashboard '{}' with id {}", saved.getTitle(), saved.getId()); + return saved; + } + + private Optional findDashboardByTitle(String title) { + // Use text search first and then filter by exact title + PageData page = tbClient.getTenantDashboards(new PageLink(10, 0, title)); + return page.getData().stream() + .filter(info -> title.equals(info.getTitle())) + .findFirst() + .flatMap(info -> tbClient.getDashboardById(info.getId())); + } + + private String buildPublicDashboardLink(DashboardId dashboardId, String publicCustomerId) { + String base = getBaseUrl(); + return String.format("%s/dashboard/%s?publicId=%s", base, dashboardId.getId().toString(), publicCustomerId); + } + + private String getBaseUrl() { + // TbClient.baseURL contains the root url, without trailing slash + try { + var baseUrlField = tbClient.getClass().getSuperclass().getDeclaredField("baseURL"); + baseUrlField.setAccessible(true); + return (String) baseUrlField.get(tbClient); + } catch (Exception e) { + log.warn("Unable to access baseURL from RestClient. Falling back to http://localhost:8080"); + return "http://localhost:8080"; + } + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java index b13394dbdc..75a8a819c0 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -25,7 +25,6 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.monitoring.client.TbClient; import org.thingsboard.monitoring.data.Latency; -import org.thingsboard.monitoring.data.MonitoredServiceKey; import org.thingsboard.monitoring.data.notification.HighLatencyNotification; import org.thingsboard.monitoring.data.notification.ServiceFailureNotification; import org.thingsboard.monitoring.data.notification.ServiceRecoveryNotification; @@ -119,9 +118,7 @@ public class MonitoringReporter { public void serviceIsOk(Object serviceKey) { ServiceRecoveryNotification notification = new ServiceRecoveryNotification(serviceKey); - if (!serviceKey.equals(MonitoredServiceKey.GENERAL)) { - log.info(notification.getText()); - } + log.info(notification.getText()); AtomicInteger failuresCounter = failuresCounters.get(serviceKey); if (failuresCounter != null) { if (failuresCounter.get() >= failuresThreshold) { diff --git a/monitoring/src/main/resources/dashboard_cloud_monitoring.json b/monitoring/src/main/resources/dashboard_cloud_monitoring.json new file mode 100644 index 0000000000..8be9677c61 --- /dev/null +++ b/monitoring/src/main/resources/dashboard_cloud_monitoring.json @@ -0,0 +1,580 @@ +{ + "title": "Cloud - Monitoring", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "arrivalLatency", + "type": "timeseries", + "label": "Arrival latency", + "color": "#ffc107", + "settings": {}, + "_hash": 0.7424467450112592, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "processingTime", + "type": "timeseries", + "label": "Processing time", + "color": "#607d8b", + "settings": {}, + "_hash": 0.6065128737901203, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "mqttTransportWsUpdateLatency", + "type": "timeseries", + "label": "MQTT - overall", + "color": "#f44336", + "settings": {}, + "_hash": 0.19638031054010696, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportWsUpdateLatency", + "type": "timeseries", + "label": "CoAP - overall", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.8167481665043521, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportWsUpdateLatency", + "type": "timeseries", + "label": "HTTP - overall", + "color": "#8bc34a", + "settings": {}, + "_hash": 0.45369210935861803, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportWsUpdateLatency", + "type": "timeseries", + "label": "LwM2M - overall", + "color": "#3f51b5", + "settings": {}, + "_hash": 0.3477627917073993, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationWsUpdateLatency", + "type": "timeseries", + "label": "CoAP integration - overall", + "color": "#e91e63", + "settings": {}, + "_hash": 0.8332881819050183, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsSubscribeLatency", + "type": "timeseries", + "label": "WS subscribe latency", + "color": "#03a9f4", + "settings": {}, + "_hash": 0.7843180730206556, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsConnectLatency", + "type": "timeseries", + "label": "WS connect latency", + "color": "#4caf50", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.23140687573220564, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "logInLatency", + "type": "timeseries", + "label": "Log in latency", + "color": "#2196f3", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.6130919996800452, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": null, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 4, + "smoothLines": false, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [], + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "General latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "7db7f580-2aac-d7ee-20f6-3d9315e7003f", + "typeFullFqn": "system.charts.basic_timeseries" + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "mqttTransportRequestLatency", + "type": "timeseries", + "label": "MQTT transport request latency", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8576659620523571, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportRequestLatency", + "type": "timeseries", + "label": "HTTP transport request latency", + "color": "#4caf50", + "settings": {}, + "_hash": 0.9749033105491403, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportRequestLatency", + "type": "timeseries", + "label": "CoAP transport request latency", + "color": "#f44336", + "settings": {}, + "_hash": 0.6977575200148037, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationRequestLatency", + "type": "timeseries", + "label": "CoAP integration request latency", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.18839685494200875, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportRequestLatency", + "type": "timeseries", + "label": "LwM2M transport request latency", + "color": "#ffc107", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.85549409179514, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "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" + }, + "grid": { + "color": "#545454", + "tickColor": "#DDDDDD", + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1 + }, + "legend": { + "show": true, + "position": "nw", + "backgroundColor": "#f0f0f0", + "backgroundOpacity": 0.85, + "labelBoxBorderColor": "rgba(1, 1, 1, 0.45)" + }, + "decimals": 1, + "stack": false, + "tooltipIndividual": false, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "Transport latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1", + "typeFullFqn": "system.charts.basic_timeseries" + } + }, + "states": { + "default": { + "name": "Cloud - Monitoring", + "root": true, + "layouts": { + "main": { + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 0 + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 12 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "outerMargin": true, + "layoutType": "default" + } + } + } + } + }, + "entityAliases": { + "6451461f-d748-9528-e3cd-64078c7fae05": { + "id": "6451461f-d748-9528-e3cd-64078c7fae05", + "alias": "Monitoring stats asset", + "filter": { + "type": "entityName", + "resolveMultiple": true, + "entityType": "ASSET", + "entityNameFilter": "[Monitoring] Latencies" + } + } + }, + "filters": {}, + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 120000, + "timewindowMs": 18000000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 600000, + "timewindowMs": 43200000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 2500 + }, + "timezone": null + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true + } + }, + "name": "Cloud - Monitoring", + "resources": null +} \ No newline at end of file diff --git a/monitoring/src/main/resources/logback.xml b/monitoring/src/main/resources/logback.xml index e2b90894d1..50536a7110 100644 --- a/monitoring/src/main/resources/logback.xml +++ b/monitoring/src/main/resources/logback.xml @@ -17,7 +17,7 @@ --> - + @@ -34,5 +34,4 @@ - diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml index 053e42bb9d..768fd2e4c7 100644 --- a/monitoring/src/main/resources/tb-monitoring.yml +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -121,11 +121,24 @@ monitoring: notifications: message_prefix: '${NOTIFICATION_MESSAGE_PREFIX:}' + # Incident grouping (threads alerts into incidents, auto-resolves after timeout). + # Requires a channel that supports it — currently only Slack API (bot_token + channel_id). + incident: + # Enable incident grouping + enabled: '${INCIDENT_ENABLED:false}' + # Incident resolution timeout in seconds + resolution_timeout_s: '${INCIDENT_RESOLUTION_TIMEOUT_S:90}' + # Tag @channel in incident messages + tag_channel: '${INCIDENT_TAG_CHANNEL:false}' slack: # Enable notifying via Slack enabled: '${SLACK_NOTIFICATION_CHANNEL_ENABLED:false}' # Slack webhook url webhook_url: '${SLACK_WEBHOOK_URL:}' + # Slack Bot OAuth token (xoxb-...) for API-based messaging with incident support, requires chat:write:bot scope + bot_token: '${SLACK_BOT_TOKEN:}' + # Slack channel ID (e.g. C01234ABCDE) - required when incident feature is enabled + channel_id: '${SLACK_CHANNEL_ID:}' latency: # Enable latencies reporting diff --git a/monitoring/src/test/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotificationTest.java b/monitoring/src/test/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotificationTest.java new file mode 100644 index 0000000000..500a713f7b --- /dev/null +++ b/monitoring/src/test/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotificationTest.java @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ServiceFailureNotificationTest { + + @Test + void stripResponseBodyRemovesNginxErrorHtml() { + String msg = "503 Service Temporarily Unavailable on POST request for \"https://domain/api/auth/login\": \"" + + "503 Service Temporarily Unavailable" + + "

503 Service Temporarily Unavailable


nginx
\""; + + String sanitized = ServiceFailureNotification.stripResponseBody(msg); + + assertThat(sanitized) + .isEqualTo("503 Service Temporarily Unavailable on POST request for \"https://domain/api/auth/login\""); + } + + @Test + void stripResponseBodyRemovesDoctypeHtml() { + String msg = "500 Internal Server Error: \"...\""; + + String sanitized = ServiceFailureNotification.stripResponseBody(msg); + + assertThat(sanitized).isEqualTo("500 Internal Server Error"); + } + + @Test + void stripResponseBodyLeavesPlainMessagesUntouched() { + String msg = "Connection refused"; + assertThat(ServiceFailureNotification.stripResponseBody(msg)).isEqualTo(msg); + } + + @Test + void stripResponseBodyHandlesNull() { + assertThat(ServiceFailureNotification.stripResponseBody(null)).isNull(); + } + + @Test + void linkifyReplacesRequestForUrlWithSlackMrkdwnLink() { + String msg = "503 Service Temporarily Unavailable on POST request for \"https://example.com/api/auth/login\""; + + assertThat(ServiceFailureNotification.linkifyRequestUrl(msg)) + .isEqualTo("503 Service Temporarily Unavailable on POST "); + } + + @Test + void linkifyReplacesRequestConnectToUrlFailed() { + String msg = "I/O error on POST request: Connect to https://example.com:443 failed: Connect timed out"; + + assertThat(ServiceFailureNotification.linkifyRequestUrl(msg)) + .isEqualTo("I/O error on POST : Connect timed out"); + } + + @Test + void linkifyLeavesMessagesWithoutRequestUrlUntouched() { + String msg = "Connection refused"; + assertThat(ServiceFailureNotification.linkifyRequestUrl(msg)).isEqualTo(msg); + } + + @Test + void linkifyHandlesNull() { + assertThat(ServiceFailureNotification.linkifyRequestUrl(null)).isNull(); + } + + @Test + void shortNameUsesShortNameProviderWhenAvailable() { + ShortNameProvider provider = () -> "MQTT"; + assertThat(ServiceFailureNotification.shortName(provider)).isEqualTo("MQTT"); + } + + @Test + void shortNameFallsBackToToStringForOtherKeys() { + Object key = new Object() { + @Override public String toString() { return "LOGIN"; } + }; + assertThat(ServiceFailureNotification.shortName(key)).isEqualTo("LOGIN"); + } + +} diff --git a/monitoring/src/test/java/org/thingsboard/monitoring/notification/incident/IncidentManagerTest.java b/monitoring/src/test/java/org/thingsboard/monitoring/notification/incident/IncidentManagerTest.java new file mode 100644 index 0000000000..b7051db534 --- /dev/null +++ b/monitoring/src/test/java/org/thingsboard/monitoring/notification/incident/IncidentManagerTest.java @@ -0,0 +1,183 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.notification.incident; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.monitoring.data.notification.AffectedService; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +class IncidentManagerTest { + + private RecordingTransport transport; + private IncidentManager manager; + + @BeforeEach + void setUp() { + transport = new RecordingTransport(); + manager = new IncidentManager(transport, 3600L, "tbqa", false); + } + + @AfterEach + void tearDown() { + manager.shutdown(); + } + + @Test + void formatDurationRendersMinutesAndHours() { + assertThat(IncidentManager.formatDuration(Duration.ofSeconds(30))).isEqualTo("0m"); + assertThat(IncidentManager.formatDuration(Duration.ofMinutes(5))).isEqualTo("5m"); + assertThat(IncidentManager.formatDuration(Duration.ofMinutes(59))).isEqualTo("59m"); + assertThat(IncidentManager.formatDuration(Duration.ofMinutes(60))).isEqualTo("1h"); + assertThat(IncidentManager.formatDuration(Duration.ofMinutes(75))).isEqualTo("1h15m"); + assertThat(IncidentManager.formatDuration(Duration.ofMinutes(120))).isEqualTo("2h"); + } + + @Test + void firstFailureOpensIncidentAndPostsHeaderAndReply() { + manager.sendAlert("CoAP failure message", + List.of(AffectedService.failing("CoAP", 1))); + + assertThat(transport.incidents).hasSize(1); + assertThat(transport.incidents.get(0)).contains(":rotating_light:").contains(":red_circle: CoAP (1)"); + assertThat(transport.replies).hasSize(1); + assertThat(transport.replies.get(0).text()).isEqualTo("CoAP failure message"); + } + + @Test + void isolatedRecoveryWithoutActiveIncidentIsIgnored() { + manager.sendAlert("Login is OK", + List.of(AffectedService.recovered("Login"))); + + assertThat(transport.incidents).isEmpty(); + assertThat(transport.replies).isEmpty(); + assertThat(transport.updates).isEmpty(); + } + + @Test + void subsequentFailureUpdatesHeaderAndPostsReply() { + manager.sendAlert("CoAP failure", List.of(AffectedService.failing("CoAP", 1))); + manager.sendAlert("CoAP repeat", List.of(AffectedService.failing("CoAP", 3))); + + assertThat(transport.incidents).hasSize(1); + assertThat(transport.replies).hasSize(2); + assertThat(transport.updates).hasSize(1); + assertThat(transport.updates.get(0).text()).contains(":red_circle: CoAP (3)"); + } + + @Test + void recoveryAfterFailureMovesServiceToGreenAndKeepsFailureCount() { + manager.sendAlert("CoAP failure", List.of(AffectedService.failing("CoAP", 4))); + manager.sendAlert("CoAP is OK", List.of(AffectedService.recovered("CoAP"))); + + assertThat(transport.updates).hasSize(1); + String updated = transport.updates.get(0).text(); + assertThat(updated).contains(":large_green_circle: CoAP (4)").doesNotContain(":red_circle:"); + } + + @Test + void highLatencyIsTrackedAsYellow() { + manager.sendAlert("high latency", + List.of(AffectedService.highLatency("logInLatency"))); + + assertThat(transport.incidents.get(0)).contains(":large_yellow_circle: logInLatency"); + } + + @Test + void repeatingSameFailureCountDoesNotTriggerRedundantUpdate() { + manager.sendAlert("CoAP failure", List.of(AffectedService.failing("CoAP", 3))); + manager.sendAlert("CoAP still failing", List.of(AffectedService.failing("CoAP", 3))); + + assertThat(transport.updates).isEmpty(); + assertThat(transport.replies).hasSize(2); + } + + @Test + void fullLifecycleStartFailRecoverResolve() { + manager.sendAlert("Login failure", List.of(AffectedService.failing("Login", 1))); + manager.sendAlert("WS failure", List.of(AffectedService.failing("WS Connect", 1))); + manager.sendAlert("Login is OK", List.of(AffectedService.recovered("Login"))); + + assertThat(transport.incidents).hasSize(1); + + manager.resolveIncident(); + + assertThat(transport.updates).last() + .extracting(RecordingTransport.Message::text) + .asString() + .contains(":white_check_mark:") + .contains(":red_circle: WS Connect") + .contains(":large_green_circle: Login (1)"); + } + + @Test + void resolveWithoutActiveIncidentIsNoOp() { + manager.resolveIncident(); + assertThat(transport.updates).isEmpty(); + } + + @Test + void doesNotAutoResolveWhileServicesAreStillFailing() throws Exception { + manager.shutdown(); + transport = new RecordingTransport(); + manager = new IncidentManager(transport, 1L, "tbqa", false); + + manager.sendAlert("CoAP failure", List.of(AffectedService.failing("CoAP", 1))); + Thread.sleep(1500); + + assertThat(transport.updates) + .extracting(RecordingTransport.Message::text) + .noneMatch(t -> t.contains(":white_check_mark:")); + + manager.sendAlert("CoAP is OK", List.of(AffectedService.recovered("CoAP"))); + Thread.sleep(1500); + + assertThat(transport.updates) + .extracting(RecordingTransport.Message::text) + .anyMatch(t -> t.contains(":white_check_mark:")); + } + + private static class RecordingTransport implements IncidentTransport { + private final AtomicInteger threadCounter = new AtomicInteger(); + final java.util.List incidents = new java.util.ArrayList<>(); + final java.util.List replies = new java.util.ArrayList<>(); + final java.util.List updates = new java.util.ArrayList<>(); + + @Override + public String postIncident(String text) { + incidents.add(text); + return "thread-" + threadCounter.incrementAndGet(); + } + + @Override + public void postThreadReply(String threadId, String text) { + replies.add(new Message(threadId, text)); + } + + @Override + public void updateIncident(String threadId, String text) { + updates.add(new Message(threadId, text)); + } + + record Message(String threadId, String text) {} + } +} diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index c06e98b45e..141ee1adac 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index f95978d5a5..ec1bdcf658 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -26,9 +26,12 @@ import org.apache.http.ssl.SSLContexts; import org.testng.annotations.AfterSuite; import org.testng.annotations.BeforeSuite; import org.testng.annotations.Listeners; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; 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.DeviceProfileData; @@ -39,7 +42,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import java.net.URI; import java.util.Map; import java.util.Random; - +import java.util.function.Consumer; @Slf4j @Listeners(TestListener.class) @@ -166,19 +169,14 @@ public abstract class AbstractContainerTest { DeviceProfileProvisionConfiguration provisionConfiguration; String testProvisionDeviceKey = TEST_PROVISION_DEVICE_KEY; deviceProfile.setProvisionType(provisionType); - switch(provisionType) { - case ALLOW_CREATE_NEW_DEVICES: - provisionConfiguration = new AllowCreateNewDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); - break; - case CHECK_PRE_PROVISIONED_DEVICES: - provisionConfiguration = new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); - break; - default: - case DISABLED: + provisionConfiguration = switch (provisionType) { + case ALLOW_CREATE_NEW_DEVICES -> new AllowCreateNewDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); + case CHECK_PRE_PROVISIONED_DEVICES -> new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); + default -> { testProvisionDeviceKey = null; - provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null); - break; - } + yield new DisabledDeviceProfileProvisionConfiguration(null); + } + }; DeviceProfileData deviceProfileData = deviceProfile.getProfileData(); deviceProfileData.setProvisionConfiguration(provisionConfiguration); deviceProfile.setProfileData(deviceProfileData); @@ -186,4 +184,12 @@ public abstract class AbstractContainerTest { return testRestClient.postDeviceProfile(deviceProfile); } + protected void updateDefaultTenantProfile(Consumer updater) { + EntityInfo defaultTenantProfileInfo = testRestClient.getDefaultTenantProfileInfo(); + TenantProfile oldTenantProfile = testRestClient.getTenantProfileById(defaultTenantProfileInfo.getId().getId().toString()); + TenantProfile tenantProfile = JacksonUtil.clone(oldTenantProfile); + updater.accept(tenantProfile); + testRestClient.postTenantProfile(tenantProfile); + } + } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 7b2cfc68d9..91c4927b3c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -16,6 +16,7 @@ package org.thingsboard.server.msa; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; @@ -33,10 +34,12 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -231,9 +234,9 @@ public class TestRestClient { .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); } - public ValidatableResponse postTelemetryAttribute(EntityId entityId, String scope, JsonNode attribute) { + public ValidatableResponse postTelemetryAttribute(EntityId entityId, AttributeScope scope, JsonNode attribute) { return given().spec(requestSpec).body(attribute) - .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope) + .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope.name()) .then() .statusCode(HTTP_OK); } @@ -256,13 +259,40 @@ public class TestRestClient { .as(JsonNode.class); } - public JsonNode getAttributes(EntityId entityId, AttributeScope scope, String keys) { + public ArrayNode getAttributes(EntityId entityId, AttributeScope scope, String keys) { return given().spec(requestSpec) .get("/api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}?keys={keys}", entityId.getEntityType(), entityId.getId(), scope, keys) .then() .statusCode(HTTP_OK) .extract() - .as(JsonNode.class); + .as(ArrayNode.class); + } + + + public ValidatableResponse deleteEntityAttributes(EntityId entityId, AttributeScope scope, String keys) { + Map pathParams = new HashMap<>(); + pathParams.put("entityId", entityId.getId().toString()); + pathParams.put("entityType", entityId.getEntityType().name()); + pathParams.put("scope", scope.name()); + return given().spec(requestSpec) + .pathParams(pathParams) + .queryParam("keys", keys) + .delete("/api/plugins/telemetry/{entityType}/{entityId}/{scope}") + .then() + .statusCode(HTTP_OK); + } + + public ValidatableResponse deleteEntityTimeseries(EntityId entityId, String keys, boolean deleteAllDataForKeys) { + Map pathParams = new HashMap<>(); + pathParams.put("entityType", entityId.getEntityType().name()); + pathParams.put("entityId", entityId.getId().toString()); + return given().spec(requestSpec) + .pathParams(pathParams) + .queryParam("keys", keys) + .queryParam("deleteAllDataForKeys", Boolean.toString(deleteAllDataForKeys)) + .delete("/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete") + .then() + .statusCode(HTTP_OK); } public JsonNode getLatestTelemetry(EntityId entityId) { @@ -367,6 +397,33 @@ public class TestRestClient { }); } + public EntityRelation postEntityRelation(EntityRelation entityRelation) { + return given().spec(requestSpec) + .body(entityRelation) + .post("/api/v2/relation") + .then() + .statusCode(HTTP_OK) + .extract() + .as(EntityRelation.class); + } + + + public EntityRelation deleteEntityRelation(EntityId fromId, String relationType, EntityId toId) { + Map queryParams = new HashMap<>(); + queryParams.put("fromId", fromId.getId().toString()); + queryParams.put("fromType", fromId.getEntityType().name()); + queryParams.put("relationType", relationType); + queryParams.put("toId", toId.getId().toString()); + queryParams.put("toType", toId.getEntityType().name()); + return given().spec(requestSpec) + .queryParams(queryParams) + .delete("/api/v2/relation") + .then() + .statusCode(HTTP_OK) + .extract() + .as(EntityRelation.class); + } + public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) { return given().spec(requestSpec) .body(serverRpcPayload) @@ -428,7 +485,7 @@ public class TestRestClient { .as(DeviceProfile.class); } - public void deleteDeviseProfile(DeviceProfileId deviceProfileId) { + public void deleteDeviceProfile(DeviceProfileId deviceProfileId) { given().spec(requestSpec) .delete("/api/deviceProfile/{deviceProfileId}", deviceProfileId.getId()) .then() @@ -761,4 +818,33 @@ public class TestRestClient { .then() .statusCode(HTTP_OK); } + + public TenantProfile postTenantProfile(TenantProfile tenantProfile) { + return given().spec(requestSpec).body(tenantProfile) + .post("/api/tenantProfile") + .then() + .statusCode(HTTP_OK) + .extract() + .as(TenantProfile.class); + } + + public EntityInfo getDefaultTenantProfileInfo() { + return given().spec(requestSpec) + .get("/api/tenantProfileInfo/default") + .then() + .statusCode(HTTP_OK) + .extract() + .as(EntityInfo.class); + } + + public TenantProfile getTenantProfileById(String tenantProfileId) { + return given().spec(requestSpec) + .pathParams("tenantProfileId", tenantProfileId) + .get("/api/tenantProfile/{tenantProfileId}") + .then() + .statusCode(HTTP_OK) + .extract() + .as(TenantProfile.class); + } + } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 30a70d5288..af4197d6ec 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -16,43 +16,68 @@ package org.thingsboard.server.msa.cf; import com.fasterxml.jackson.databind.JsonNode; -import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; -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.asset.Asset; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldEventType; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; 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.event.EventType; import org.thingsboard.server.common.data.id.AssetProfileId; 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.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.common.data.AttributeScope.SERVER_SCOPE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultAssetProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; @@ -62,20 +87,22 @@ public class CalculatedFieldTest extends AbstractContainerTest { public final int TIMEOUT = 60; public final int POLL_INTERVAL = 1; - private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; + private final String exampleScript = """ + var avgTemperature = temperature.mean(); // Get average temperature + var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin + + // Estimate air pressure based on altitude + var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); + + // Air density formula + var airDensity = pressure / (287.05 * temperatureK); + + return { + "airDensity": toFixed(airDensity, 2) + } + """; - private final String exampleScript = "var avgTemperature = temperature.mean(); // Get average temperature\n" + - " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + - "\n" + - " // Estimate air pressure based on altitude\n" + - " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + - "\n" + - " // Air density formula\n" + - " var airDensity = pressure / (287.05 * temperatureK);\n" + - "\n" + - " return {\n" + - " \"airDensity\": toFixed(airDensity, 2)\n" + - " };"; + private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; private TenantId tenantId; private UserId tenantAdminId; @@ -88,28 +115,38 @@ public class CalculatedFieldTest extends AbstractContainerTest { public void beforeClass() { testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + updateDefaultTenantProfile(tenantProfile -> { + TenantProfileData profileData = tenantProfile.getProfileData(); + DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); + profileConfiguration.setMinAllowedDeduplicationIntervalInSecForCF(1); + profileConfiguration.setMinAllowedScheduledUpdateIntervalInSecForCF(1); + tenantProfile.setProfileData(profileData); + }); + tenantId = testRestClient.postTenant(EntityPrototypes.defaultTenantPrototype("Tenant")).getId(); tenantAdminId = testRestClient.createUserAndLogin(defaultTenantAdmin(tenantId, "tenantAdmin@thingsboard.org"), "tenant"); + } - deviceProfileId = testRestClient.postDeviceProfile(defaultDeviceProfile("Device Profile 1")).getId(); - device = testRestClient.postDevice(deviceToken, createDevice("Device 1", deviceProfileId)); + @BeforeMethod + public void beforeMethod() { + testRestClient.getAndSetUserToken(tenantAdminId); - assetProfileId = testRestClient.postAssetProfile(defaultAssetProfile("Asset Profile 1")).getId(); - asset = testRestClient.postAsset(createAsset("Asset 1", assetProfileId)); + deviceProfileId = testRestClient.postDeviceProfile(defaultDeviceProfile("Device Profile")).getId(); + device = testRestClient.postDevice(deviceToken, createDevice("Device", deviceProfileId)); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); - testRestClient.postTelemetryAttribute(device.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + assetProfileId = testRestClient.postAssetProfile(defaultAssetProfile("Asset Profile")).getId(); + asset = testRestClient.postAsset(createAsset("Asset", assetProfileId)); + } - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}")); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}")); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}")); + @AfterMethod + public void tearDown() { + testRestClient.getAndSetUserToken(tenantAdminId); - testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); - } + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteDeviceProfile(deviceProfileId); - @BeforeMethod - public void beforeMethod() { - testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + testRestClient.deleteAsset(asset.getId()); + testRestClient.deleteAssetProfile(assetProfileId); } @AfterClass @@ -121,12 +158,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testPerformInitialCalculationForSimpleType() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); - - CalculatedField savedCalculatedField = createSimpleCalculatedField(); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -142,13 +177,25 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testChangeConfigArgument() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); + testRestClient.postTelemetryAttribute(device.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); + assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); - Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); - savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + Argument savedArgument = ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).getArguments().get("T"); + savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, SERVER_SCOPE)); testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -165,21 +212,31 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testChangeConfigOutput() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + + AttributesOutput output = new AttributesOutput(); + output.setScope(SERVER_SCOPE); + output.setName("temperatureF"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setOutput(output); - Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); - savedOutput.setType(OutputType.ATTRIBUTES); - savedOutput.setScope(AttributeScope.SERVER_SCOPE); - savedOutput.setName("temperatureF"); testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - JsonNode temperatureF = testRestClient.getAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "temperatureF"); + ArrayNode temperatureF = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, "temperatureF"); assertThat(temperatureF).isNotNull(); assertThat(temperatureF.get(0)).isNotNull(); assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("77.0"); @@ -190,13 +247,23 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testChangeConfigExpression() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); + assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); savedCalculatedField.setName("F to C"); - savedCalculatedField.getConfiguration().setExpression("(T - 32) / 1.8"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setExpression("(T - 32) / 1.8"); testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -213,10 +280,19 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testTelemetryUpdated() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":30}")); @@ -234,8 +310,8 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testEntityIdIsProfile() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); @@ -253,46 +329,61 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testEntityAddedAndDeleted() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); - String newDeviceToken = "mmmXRIVRsq9lbnTP2XBE"; - Device newDevice = testRestClient.postDevice(newDeviceToken, createDevice("Device 2", deviceProfileId)); + await().alias("create CF -> perform initial calculation for device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + + String deviceToken2 = "beeUmKVqsq3lvnovt6BE"; + Device device2 = testRestClient.postDevice(deviceToken2, createDevice("Device 2", deviceProfileId)); await().alias("create device by profile -> perform initial calculation for new device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // used default value since telemetry is not present - JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device2.getId()); assertThat(fahrenheitTemp).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); DeviceProfile newDeviceProfile = testRestClient.postDeviceProfile(defaultDeviceProfile("Test Profile")); - newDevice.setDeviceProfileId(newDeviceProfile.getId()); - testRestClient.postDevice(newDeviceToken, newDevice); + device2.setDeviceProfileId(newDeviceProfile.getId()); + testRestClient.postDevice(deviceToken2, device2); - testRestClient.postTelemetry(newDeviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + testRestClient.postTelemetry(deviceToken2, JacksonUtil.toJsonNode("{\"temperature\":25}")); await().alias("update telemetry -> no updates").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device2.getId()); assertThat(fahrenheitTemp).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + testRestClient.deleteDeviceIfExists(device2.getId()); + testRestClient.deleteDeviceProfileIfExists(newDeviceProfile); } @Test public void testEntityIdIsProfileAndRefEntityIsCommon() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}")); + waitInitialTelemetry(device.getId(), "temperatureInF"); + + testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); CalculatedField savedCalculatedField = createScriptCalculatedField(deviceProfileId, asset.getId()); @@ -305,7 +396,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("1.05"); }); - testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1531}")); + testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1531}")); await().alias("create CF -> update telemetry for common entity").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -319,15 +410,542 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); } - private CalculatedField createSimpleCalculatedField() { - return createSimpleCalculatedField(device.getId()); + @Test + public void testPerformSerialsOfCalculationsForGeofencingType() { + // Device and initial coords (inside Allowed, outside Restricted) + String deviceToken = "geoDeviceTokenA"; + Device device = testRestClient.postDevice(deviceToken, createDevice("GF Device", deviceProfileId)); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + + // Create zones + Asset allowed = testRestClient.postAsset(createAsset("Allowed Zone", null)); + testRestClient.postTelemetryAttribute(allowed.getId(), SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":[[50.472000,30.504000],[50.472000,30.506000],[50.474000,30.506000],[50.474000,30.504000]]}")); + + Asset restricted = testRestClient.postAsset(createAsset("Restricted Zone", null)); + testRestClient.postTelemetryAttribute(restricted.getId(), SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":[[50.475000,30.510000],[50.475000,30.512000],[50.477000,30.512000],[50.477000,30.510000]]}")); + + // Relations FROM device + testRestClient.postEntityRelation(new EntityRelation(device.getId(), allowed.getId(), "AllowedZone")); + testRestClient.postEntityRelation(new EntityRelation(device.getId(), restricted.getId(), "RestrictedZone")); + + // Build CF: GEOFENCING -> attributes output + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Dynamic groups via relations + ZoneGroupConfiguration allowedZoneGroupConfiguration = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); + allowedZoneGroupConfiguration.setRefDynamicSourceConfiguration(allowedDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone"))); + restrictedZoneGroupConfiguration.setRefDynamicSourceConfiguration(restrictedDynamicSourceConfiguration); + + cfg.setZoneGroups(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); + + AttributesOutput out = new AttributesOutput(); + out.setScope(SERVER_SCOPE); + cfg.setOutput(out); + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // Initial ENTERED/INSIDE and OUTSIDE + await().alias("initial geofencing evaluation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, + "allowedZonesEvent,allowedZonesStatus,restrictedZonesEvent,restrictedZonesStatus"); + assertThat(attrs).isNotNull().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + + // Move device into Restricted zone -> expect LEFT/ENTERED and statuses flipped + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")); + + await().alias("transition after movement").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, + "allowedZonesEvent,allowedZonesStatus,restrictedZonesEvent,restrictedZonesStatus"); + assertThat(attrs).isNotNull().hasSize(4); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("allowedZonesStatus", "OUTSIDE") + .containsEntry("restrictedZonesEvent", "ENTERED") + .containsEntry("restrictedZonesStatus", "INSIDE"); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteAsset(allowed.getId()); + testRestClient.deleteAsset(restricted.getId()); + } + + @Test + public void testPropagationCalculatedField_withExpression() { + // --- Arrange entities --- + String deviceToken = "propagationDeviceTokenA"; + Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device With Expression", deviceProfileId)); + Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 1", null)); + Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 2", null)); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel1); + testRestClient.postEntityRelation(rel2); + + // Telemetry on device + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":12.5}")); + + // --- Build CF: PROPAGATION with expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (expr)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(true); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("t", arg)); + + cfg.setExpression("{\"testResult\": t * 2}"); + + AttributesOutput output = new AttributesOutput(); + output.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // --- Assert propagated calculation (expression applied) --- + await().alias("propagation expr mode evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = testRestClient.getAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs1).isNotNull().hasSize(1); + Map m1 = intKv(attrs1); + assertThat(m1).containsEntry("testResult", 25); + + ArrayNode attrs2 = testRestClient.getAttributes(asset2.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs2).isNotNull().hasSize(1); + Map m2 = intKv(attrs2); + assertThat(m2).containsEntry("testResult", 25); + }); + + testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); + testRestClient.deleteEntityAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + + // --- Assert propagated calculation (expression applied with new temperature argument and one relation removed) --- + await().alias("propagation expr mode evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = testRestClient.getAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs1).isNullOrEmpty(); + + ArrayNode attrs2 = testRestClient.getAttributes(asset2.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs2).isNotNull().hasSize(1); + Map m2 = intKv(attrs2); + assertThat(m2).containsEntry("testResult", 50); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteAsset(asset1.getId()); + testRestClient.deleteAsset(asset2.getId()); + } + + @Test + public void testPropagationCalculatedField_withoutExpression() { + // --- Arrange entities --- + String deviceToken = "propagationDeviceTokenB"; + Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device Without Expression", deviceProfileId)); + Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 3", null)); + Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 4", null)); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel1); + testRestClient.postEntityRelation(rel2); + + // Telemetry on device + long ts = System.currentTimeMillis() - 300000L; + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts))); + + // --- Build CF: PROPAGATION without expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (args-only)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temperatureComputed", arg)); + + cfg.setOutput(new TimeSeriesOutput()); + + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // --- Assert propagated calculation (arguments-only mode) --- + await().alias("propagation args-only evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperature1 = testRestClient.getLatestTelemetry(asset1.getId()); + assertThat(temperature1).isNotNull(); + assertThat(temperature1.get("temperatureComputed")).isNotNull(); + assertThat(temperature1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(temperature1.get("temperatureComputed").get(0).get("value").asText()).isEqualTo("12.5"); + + JsonNode temperature2 = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(temperature2).isNotNull(); + assertThat(temperature2.get("temperatureComputed")).isNotNull(); + assertThat(temperature2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(temperature2.get("temperatureComputed").get(0).get("value").asText()).isEqualTo("12.5"); + }); + + testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); + testRestClient.deleteEntityTimeseries(asset1.getId(), "temperatureComputed", true); + + // Update telemetry on device + long newTs = System.currentTimeMillis() - 300000L; + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs))); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperature1 = testRestClient.getLatestTelemetry(asset1.getId()); + assertThat(temperature1).isNullOrEmpty(); + + JsonNode temperature2 = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(temperature2).isNotNull(); + assertThat(temperature2.get("temperatureComputed")).isNotNull(); + assertThat(temperature2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(temperature2.get("temperatureComputed").get(0).get("value").asInt()).isEqualTo(25); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteAsset(asset1.getId()); + testRestClient.deleteAsset(asset2.getId()); + } + + @Test + public void testRelatedEntitiesAggregationCalculatedField() { + // --- Create entities --- + String device_1_1_token = "000000011"; + Device device_1_1 = testRestClient.postDevice(device_1_1_token, createDevice("Device 1-1", deviceProfileId)); + String device_1_2_token = "000000012"; + Device device_1_2 = testRestClient.postDevice(device_1_2_token, createDevice("Device 1-2", deviceProfileId)); + + // Create relations FROM asset TO devices + EntityRelation rel_1_1 = new EntityRelation(asset.getId(), device_1_1.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel_1_2 = new EntityRelation(asset.getId(), device_1_2.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel_1_1); + testRestClient.postEntityRelation(rel_1_2); + + // Post telemetry + testRestClient.postTelemetry(device_1_1_token, JacksonUtil.toJsonNode("{\"occupied\":true}")); + testRestClient.postTelemetry(device_1_2_token, JacksonUtil.toJsonNode("{\"occupied\":false}")); + + // --- Create CF: Related entities aggregation --- + CalculatedField calculatedField = createOccupancyCF(assetProfileId); + + // --- Assert aggregation --- + await().alias("create cf -> check aggregation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode occupancy = testRestClient.getLatestTelemetry(asset.getId()); + assertThat(occupancy).isNotNull(); + + assertThat(occupancy.get("freeSpaces")).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("1"); + + assertThat(occupancy.get("occupiedSpaces")).isNotNull(); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("1"); + + assertThat(occupancy.get("totalSpaces")).isNotNull(); + assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("2"); + }); + + // Post telemetry + testRestClient.postTelemetry(device_1_2_token, JacksonUtil.toJsonNode("{\"occupied\":true}")); + + // --- Assert aggregation --- + await().alias("update telemetry -> check aggregation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode occupancy = testRestClient.getLatestTelemetry(asset.getId()); + assertThat(occupancy).isNotNull(); + + assertThat(occupancy.get("freeSpaces")).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("0"); + + assertThat(occupancy.get("occupiedSpaces")).isNotNull(); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("2"); + + assertThat(occupancy.get("totalSpaces")).isNotNull(); + assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("2"); + }); + + // Add entity to profile + Asset asset2 = testRestClient.postAsset(createAsset("Asset 2", assetProfileId)); + String device_2_1_token = "000000021"; + Device device_2_1 = testRestClient.postDevice(device_2_1_token, createDevice("Device 2-1", deviceProfileId)); + String device_2_2_token = "000000022"; + Device device_2_2 = testRestClient.postDevice(device_2_2_token, createDevice("Device 2-2", deviceProfileId)); + + // Post telemetry + testRestClient.postTelemetry(device_2_1_token, JacksonUtil.toJsonNode("{\"occupied\":true}")); + testRestClient.postTelemetry(device_2_2_token, JacksonUtil.toJsonNode("{\"occupied\":false}")); + + // --- Assert aggregation --- + await().alias("add entity to profile cf -> no aggregated values since no relations") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode occupancy = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(occupancy).isNullOrEmpty(); + }); + + // Create relations FROM asset TO devices + EntityRelation rel_2_1 = new EntityRelation(asset2.getId(), device_2_1.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel_2_1); + EntityRelation rel_2_2 = new EntityRelation(asset2.getId(), device_2_2.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel_2_2); + + // --- Assert aggregation --- + await().alias("create relation -> check aggregation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode occupancy = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(occupancy).isNotNull(); + + assertThat(occupancy.get("freeSpaces")).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("1"); + + assertThat(occupancy.get("occupiedSpaces")).isNotNull(); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("1"); + + assertThat(occupancy.get("totalSpaces")).isNotNull(); + assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("2"); + }); + + testRestClient.deleteEntityRelation(asset2.getId(), EntityRelation.CONTAINS_TYPE, device_2_2.getId()); + + // --- Assert aggregation --- + await().alias("delete relation -> check aggregation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode occupancy = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(occupancy).isNotNull(); + + assertThat(occupancy.get("freeSpaces")).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("0"); + + assertThat(occupancy.get("occupiedSpaces")).isNotNull(); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("1"); + + assertThat(occupancy.get("totalSpaces")).isNotNull(); + assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("1"); + }); + + testRestClient.deleteCalculatedFieldIfExists(calculatedField.getId()); + testRestClient.deleteDeviceIfExists(device_1_1.getId()); + testRestClient.deleteDeviceIfExists(device_1_2.getId()); + testRestClient.deleteDeviceIfExists(device_2_1.getId()); + testRestClient.deleteDeviceIfExists(device_2_2.getId()); + testRestClient.deleteAsset(asset2.getId()); + } + + @Test + public void testDebugEvents() { + // --- Arrange entities --- + String deviceToken = "12345678901"; + Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device", deviceProfileId)); + Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 1", null)); + + // Create relations FROM asset 1 TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel1); + + // --- Build CF: PROPAGATION --- + CalculatedField saved = createPropagationCF(device.getId()); + + // Wait for INITIALIZED event + waitForDebugEvent(saved, CalculatedFieldEventType.INITIALIZED.name(), 1); + + // Create relations FROM asset 2 TO device + Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 2", null)); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel2); + + // Wait for RELATION_ADD_OR_UPDATE event + waitForDebugEvent(saved, CalculatedFieldEventType.RELATION_ADD_OR_UPDATE.name(), 2); + + // Telemetry on device + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25.1}")); + + // Wait for POST_TELEMETRY_REQUEST event + waitForDebugEvent(saved, TbMsgType.POST_TELEMETRY_REQUEST.name(), 3); + + // Delete relation between asset 1 and device + testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); + + // --- Assert all debug events in correct sequence --- + await().alias("check debug events") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + List eventTypes = testRestClient.getEvents(saved.getId(), EventType.DEBUG_CALCULATED_FIELD, tenantId, new TimePageLink(4, 0, null, SortOrder.BY_CREATED_TIME_DESC)).getData().stream() + .map(e -> e.getBody().get("msgType").asText()) + .toList(); + + assertThat(eventTypes).as("Check sequence of debug events") + .containsSequence( + CalculatedFieldEventType.RELATION_DELETED.name(), + TbMsgType.POST_TELEMETRY_REQUEST.name(), + CalculatedFieldEventType.RELATION_ADD_OR_UPDATE.name(), + CalculatedFieldEventType.INITIALIZED.name() + ); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteAsset(asset1.getId()); + testRestClient.deleteAsset(asset2.getId()); + } + + private void waitForDebugEvent(CalculatedField cf, String expectedEventType, int expectedTotalEvents) { + await().alias("wait for debug event: " + expectedEventType) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + List eventTypes = testRestClient.getEvents(cf.getId(), EventType.DEBUG_CALCULATED_FIELD, tenantId, new TimePageLink(expectedTotalEvents, 0, null, SortOrder.BY_CREATED_TIME_DESC)).getData().stream() + .map(e -> e.getBody().get("msgType").asText()) + .toList(); + assertThat(eventTypes).hasSize(expectedTotalEvents); + assertThat(eventTypes.get(0)).isEqualTo(expectedEventType); + }); + } + + private CalculatedField createPropagationCF(EntityId entityId) { + CalculatedField cf = new CalculatedField(); + cf.setEntityId(entityId); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (args-only)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("deviceTemperature", arg)); + + cfg.setOutput(new TimeSeriesOutput()); + + cf.setConfiguration(cfg); + + cf.setDebugSettings(DebugSettings.all()); + + return testRestClient.postCalculatedField(cf); + } + + private CalculatedField createOccupancyCF(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName("Occupancy"); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + + RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + + configuration.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, "Contains")); + + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("false"); + arguments.put("oc", argument); + configuration.setArguments(arguments); + + configuration.setDeduplicationIntervalInSec(5); + configuration.setScheduledUpdateInterval(10); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + configuration.setMetrics(aggMetrics); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setDecimalsByDefault(0); + configuration.setOutput(output); + + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + + return testRestClient.postCalculatedField(calculatedField); } private CalculatedField createSimpleCalculatedField(EntityId entityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SIMPLE); - calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("C to F" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); @@ -335,14 +953,13 @@ public class CalculatedFieldTest extends AbstractContainerTest { Argument argument = new Argument(); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); - argument.setDefaultValue("12"); // not used because real telemetry value in db is present + argument.setDefaultValue("12"); config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(2); config.setOutput(output); @@ -351,22 +968,18 @@ public class CalculatedFieldTest extends AbstractContainerTest { return testRestClient.postCalculatedField(calculatedField); } - private CalculatedField createScriptCalculatedField() { - return createScriptCalculatedField(device.getId(), asset.getId()); - } - private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SCRIPT); - calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("Air density" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration(); Argument argument1 = new Argument(); argument1.setRefEntityId(refEntityId); - ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, SERVER_SCOPE); argument1.setRefEntityKey(refEntityKey1); Argument argument2 = new Argument(); ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temperatureInF", ArgumentType.TS_ROLLING, null); @@ -378,15 +991,23 @@ public class CalculatedFieldTest extends AbstractContainerTest { config.setExpression(exampleScript); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); calculatedField.setConfiguration(config); return testRestClient.postCalculatedField(calculatedField); } + private void waitInitialTelemetry(EntityId entityId, String key) { + await().alias("wait initial telemetry").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode telemetry = testRestClient.getLatestTelemetry(entityId); + assertThat(telemetry).isNotNull(); + assertThat(telemetry.get(key)).isNotNull(); + }); + } + private Device createDevice(String name, DeviceProfileId deviceProfileId) { Device device = new Device(); device.setName(name); @@ -406,4 +1027,20 @@ public class CalculatedFieldTest extends AbstractContainerTest { return asset; } + private static Map kv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asText()); + } + return m; + } + + private static Map intKv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asInt()); + } + return m; + } + } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java index 73960af516..6e08c41f40 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java @@ -71,7 +71,7 @@ public class CoapClientTest extends AbstractCoapClientTest{ assertThat(provisionResponse2.get("status").asText()).isEqualTo("FAILURE"); // update provision attribute to non-valid value - testRestClient.postTelemetryAttribute(device.getId(), AttributeScope.SERVER_SCOPE.name(), JacksonUtil.valueToTree(Map.of("provisionState", "non-valid"))); + testRestClient.postTelemetryAttribute(device.getId(), AttributeScope.SERVER_SCOPE, JacksonUtil.valueToTree(Map.of("provisionState", "non-valid"))); JsonNode provisionResponse3 = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName())); assertThat(provisionResponse3.get("status").asText()).isEqualTo("FAILURE"); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java index 519373229d..1f60b96edb 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -35,8 +35,7 @@ import java.util.Arrays; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.DataConstants.DEVICE; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; @DisableUIListeners diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java new file mode 100644 index 0000000000..24e7e78039 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -0,0 +1,579 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.msa.connectivity; + +import com.google.gson.JsonObject; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; +import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.ssl.SSLContexts; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rest.client.RestClient; +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.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +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.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +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.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestConfig; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.HasSubject; +import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.oauth2.PlatformType; +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.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; +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.EntityTypeFilter; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.TestProperties; + +import javax.net.ssl.SSLContext; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.EMAIL; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MICROSOFT_TEAMS; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB; +import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; + +public class JavaRestClientTest extends AbstractContainerTest { + + public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; + public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; + private RestClient restClient; + private Tenant tenant1; + private Tenant tenant2; + private User tenantAdmin1; + private User tenantAdmin2; + + @BeforeClass + public void beforeClass() throws Exception { + SSLContext ssl = SSLContexts.custom() + .loadTrustMaterial((chain, authType) -> true) + .build(); + + var tls = new DefaultClientTlsStrategy( + ssl, + HostnameVerificationPolicy.CLIENT, + NoopHostnameVerifier.INSTANCE + ); + + HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() + .setTlsSocketStrategy(tls) + .build(); + + CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(cm) + .build(); + + RestTemplate rt = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); + restClient = new RestClient(rt, TestProperties.getBaseUrl()); + } + + @BeforeMethod + public void setUp() throws Exception { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + // create tenant 1 and tenant admin 1 + tenant1 = new Tenant(); + tenant1.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.insecure().randomAlphabetic(5)); + tenant1 = restClient.saveTenant(tenant1); + + String email1 = RandomStringUtils.insecure().randomAlphabetic(5) + "@gmail.com"; + tenantAdmin1 = restClient.saveUser(defaultTenantAdmin(tenant1.getId(), email1), false); + restClient.activateUser(tenantAdmin1.getId(), "password123", false); + + // create tenant 2 and tenant admin 2 + tenant2 = new Tenant(); + tenant2.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.insecure().randomAlphabetic(5)); + tenant2 = restClient.saveTenant(tenant2); + + String email2 = RandomStringUtils.insecure().randomAlphabetic(5) + "@gmail.com"; + tenantAdmin2 = restClient.saveUser(defaultTenantAdmin(tenant2.getId(), email2), false); + restClient.activateUser(tenantAdmin2.getId(), "password123", false); + + // tenant 1 tenant admin by default + restClient.login(tenantAdmin1.getEmail(), "password123"); + } + + @AfterMethod + public void tearDown() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + if (tenant1 != null) { + restClient.deleteTenant(tenant1.getId()); + } + if (tenant2 != null) { + restClient.deleteTenant(tenant2.getId()); + } + } + + @Test + public void testGetAlarmsV2() { + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.insecure().randomAlphabetic(5))); + assertThat(device).isNotNull(); + + String type = "High temp" + RandomStringUtils.insecure().randomAlphabetic(5); + Alarm alarm = Alarm.builder() + .originator(device.getId()) + .severity(AlarmSeverity.CRITICAL) + .type(type) + .build(); + restClient.saveAlarm(alarm); + + // get /api/v2/alarm + PageData alarmsV2 = restClient.getAlarmsV2(device.getId(), null, null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(alarmsV2.getData()).hasSize(1); + + PageData activeAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(activeAlarms.getData()).hasSize(1); + + PageData cleared = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(cleared.getData()).hasSize(0); + + PageData activeAndClearedAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACTIVE), null, null, null, new TimePageLink(10, 0)); + assertThat(activeAndClearedAlarms.getData()).hasSize(1); + + // get /api/v2/alarms + PageData allAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allAlarmsV2.getData()).hasSize(1); + + PageData allClearedAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allClearedAlarmsV2.getData()).hasSize(0); + + // get /api/alarms + PageData allAlarms = restClient.getAllAlarms(AlarmSearchStatus.ACTIVE, null, new TimePageLink(10, 0), null); + assertThat(allAlarms.getData()).hasSize(1); + + PageData allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null); + assertThat(allClearedAlarms.getData()).hasSize(0); + } + + @Test + public void testTimeSeriesByReadTsKvQueries() { + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.insecure().randomAlphabetic(5))); + assertThat(device).isNotNull(); + + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + for (int i = 0; i < 3; i++) { + JsonObject values = new JsonObject(); + values.addProperty("temperature", i + 25); + testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), JacksonUtil.toJsonNode(createPayload().toString())); + } + + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 25, \"humidity\": 60}")); + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 27, \"humidity\": 59}")); + restClient.saveEntityTelemetry(device.getId(), "ts", JacksonUtil.toJsonNode("{\"temperature\": 33, \"humidity\": 62}")); + + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(EntityType.DEVICE); + var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.DESC), false); + + var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); + + EntityDataQuery entityDataQuery = new EntityDataQuery(filter, pageLink, entityFields, null, null); + AvailableEntityKeys availableEntityKeys = restClient.findAvailableEntityKeysByQuery(entityDataQuery, true, true, null); + assertThat(availableEntityKeys).isNotNull(); + assertThat(availableEntityKeys.timeseries()).contains("temperature", "humidity"); + } + + @Test + public void testFindNotifications() { + NotificationTarget notificationTarget = createNotificationTarget(tenantAdmin1.getId()); + String notificationText1 = "Notification 1"; + NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText1, new NotificationDeliveryMethod[]{WEB}); + NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationTemplate.getId()); + + String notificationText2 = "Notification 2"; + NotificationTemplate notificationTemplate2 = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText2, new NotificationDeliveryMethod[]{WEB}); + NotificationRequest notificationRequest2 = submitNotificationRequest(notificationTarget.getId(), notificationTemplate2.getId()); + + PageData initialRequests = restClient.getNotificationRequests(new PageLink(30)); + assertThat(initialRequests.getTotalElements()).isGreaterThanOrEqualTo(2); + + NotificationRequestInfo notificationRequestInfo = restClient.getNotificationRequestById(notificationRequest.getId()).get(); + assertThat(notificationRequestInfo.getName()).isEqualTo(notificationRequest.getName()); + assertThat(notificationRequestInfo.getTemplateName()).isEqualTo(notificationTemplate.getName()); + + NotificationRequestPreview requestPreview = restClient.getNotificationRequestPreview(notificationRequest, 10); + assertThat(requestPreview.getTotalRecipientsCount()).isEqualTo(1); + assertThat(requestPreview.getRecipientsPreview()).isEqualTo(List.of(tenantAdmin1.getEmail())); + + PageData notifications = restClient.getNotifications(false, WEB, new PageLink(30)); + assertThat(notifications.getTotalElements()).isEqualTo(2); + + Integer unreadCount = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCount).isEqualTo(2); + + restClient.markNotificationAsRead(notifications.getData().get(0).getId()); + + Integer unreadCountAfterRead = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCountAfterRead).isEqualTo(1); + + restClient.markAllNotificationsAsRead(WEB); + + Integer unreadCountAfterAllRead = restClient.getUnreadNotificationsCount(WEB); + assertThat(unreadCountAfterAllRead).isEqualTo(0); + + restClient.deleteNotification(notifications.getData().get(0).getId()); + notifications = restClient.getNotifications(false, WEB, new PageLink(30)); + assertThat(notifications.getTotalElements()).isEqualTo(1); + + restClient.deleteNotificationRequest(notificationRequest.getId()); + PageData requestsAfterUpdate = restClient.getNotificationRequests(new PageLink(30)); + assertThat(requestsAfterUpdate.getTotalElements()).isEqualTo(initialRequests.getTotalElements() - 1); + + List availableDeliveryMethods = restClient.getAvailableDeliveryMethods(); + assertThat(availableDeliveryMethods).contains(WEB, EMAIL, MICROSOFT_TEAMS); + } + + @Test + public void testSaveNotificationSettings() { + NotificationSettings settings = new NotificationSettings(); + SlackNotificationDeliveryMethodConfig slackConfig = new SlackNotificationDeliveryMethodConfig(); + String slackToken = "xoxb-123123123"; + slackConfig.setBotToken(slackToken); + settings.setDeliveryMethodsConfigs(Map.of( + NotificationDeliveryMethod.SLACK, slackConfig + )); + + restClient.saveNotificationSettings(settings); + + NotificationSettings savedSettings = restClient.getNotificationSettings().get(); + assertThat(savedSettings.getDeliveryMethodsConfigs()).hasSize(1); + assertThat(savedSettings.getDeliveryMethodsConfigs().get(slackConfig.getMethod())).isEqualTo(slackConfig); + + // save user notification settings + var entityActionNotificationPref = new UserNotificationSettings.NotificationPref(); + entityActionNotificationPref.setEnabled(true); + entityActionNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + UserNotificationSettings userNotificationSettings = new UserNotificationSettings(Map.of( + NotificationType.ENTITY_ACTION, entityActionNotificationPref + )); + UserNotificationSettings saved = restClient.saveUserNotificationSettings(userNotificationSettings); + UserNotificationSettings retrieved = restClient.getUserNotificationSettings().get(); + assertThat(retrieved).isEqualTo(saved); + } + + @Test + public void testSaveDomain() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + Domain domain = new Domain(); + String prefix = RandomStringUtils.insecure().randomAlphabetic(5).toLowerCase(); + domain.setName(prefix + ".test.com"); + Domain savedDomain = restClient.saveDomain(domain); + assertThat(savedDomain.getName()).isEqualTo(domain.getName()); + + PageData domainInfos = restClient.getTenantDomainInfos(new PageLink(10, 0, prefix)); + assertThat(domainInfos.getData()).hasSize(1); + } + + @Test + public void testSaveMobileApp() { + restClient.login("sysadmin@thingsboard.org", "sysadmin"); + + MobileApp mobileApp = new MobileApp(); + String prefix = RandomStringUtils.insecure().randomAlphabetic(5).toLowerCase(); + mobileApp.setPkgName(prefix + "test.app.apple"); + mobileApp.setPlatformType(PlatformType.ANDROID); + mobileApp.setAppSecret(RandomStringUtils.insecure().randomAlphabetic(20)); + mobileApp.setStatus(MobileAppStatus.DRAFT); + + MobileApp savedMobileApp = restClient.saveMobileApp(mobileApp); + assertThat(savedMobileApp.getName()).isEqualTo(mobileApp.getName()); + + PageData retrieved = restClient.getTenantMobileApps(new PageLink(10, 0, prefix)); + assertThat(retrieved.getData()).hasSize(1); + + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + String bundlePrefix = RandomStringUtils.insecure().randomAlphabetic(5).toLowerCase(); + mobileAppBundle.setTitle(bundlePrefix + "Test Bundle"); + mobileAppBundle.setAndroidAppId(savedMobileApp.getId()); + + MobileAppBundle savedMobileAppBundle = restClient.saveMobileBundle(mobileAppBundle); + PageData bundleInfos = restClient.getTenantMobileBundleInfos(new PageLink(10, 0, bundlePrefix)); + assertThat(bundleInfos.getData()).hasSize(1); + } + + private NotificationTarget createNotificationTarget(UserId... usersIds) { + UserListFilter filter = new UserListFilter(); + filter.setUsersIds(Arrays.stream(usersIds).map(UUIDBased::getId).toList()); + + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setName(filter + RandomStringUtils.insecure().randomNumeric(5)); + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + targetConfig.setUsersFilter(filter); + notificationTarget.setConfiguration(targetConfig); + return restClient.saveNotificationTarget(notificationTarget); + } + + private NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, + String text, NotificationDeliveryMethod... deliveryMethods) { + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setName("Notification template: " + RandomStringUtils.insecure().randomAlphabetic(5)); + notificationTemplate.setNotificationType(notificationType); + NotificationTemplateConfig config = new NotificationTemplateConfig(); + config.setDeliveryMethodsTemplates(new HashMap<>()); + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; + switch (deliveryMethod) { + case WEB: { + deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); + break; + } + case EMAIL: { + deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); + break; + } + case SMS: { + deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + break; + } + case MOBILE_APP: + deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate(); + break; + default: + throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); + } + deliveryMethodNotificationTemplate.setEnabled(true); + deliveryMethodNotificationTemplate.setBody(text); + if (deliveryMethodNotificationTemplate instanceof HasSubject) { + ((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject); + } + config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); + } + notificationTemplate.setConfiguration(config); + return restClient.saveNotificationTemplate(notificationTemplate); + } + + private NotificationRequest submitNotificationRequest(NotificationTargetId targetId, NotificationTemplateId notificationTemplateId) { + NotificationRequestConfig config = new NotificationRequestConfig(); + config.setSendingDelayInSec(0); + NotificationRequest notificationRequest = NotificationRequest.builder() + .targets(Stream.of(targetId).map(UUIDBased::getId).collect(Collectors.toList())) + .templateId(notificationTemplateId) + .additionalConfig(config) + .build(); + return restClient.saveNotificationRequest(notificationRequest); + } + + @Test + public void testApiKeyOperations() { + // Create an API key + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("Test API Key " + RandomStringUtils.insecure().randomAlphabetic(5)); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(tenantAdmin1.getId()); + apiKeyInfo.setExpirationTime(0); + + ApiKey savedApiKey = restClient.saveApiKey(apiKeyInfo); + assertThat(savedApiKey).isNotNull(); + assertThat(savedApiKey.getId()).isNotNull(); + assertThat(savedApiKey.getDescription()).isEqualTo(apiKeyInfo.getDescription()); + assertThat(savedApiKey.isEnabled()).isTrue(); + assertThat(savedApiKey.getUserId()).isEqualTo(tenantAdmin1.getId()); + assertThat(savedApiKey.getTenantId()).isEqualTo(tenant1.getId()); + assertThat(savedApiKey.getValue()).isNotNull(); + + // Get user API keys + PageData apiKeys = restClient.getUserApiKeys(tenantAdmin1.getId(), new PageLink(10)); + assertThat(apiKeys).isNotNull(); + assertThat(apiKeys.getData()).hasSize(1); + assertThat(apiKeys.getData().get(0).getId()).isEqualTo(savedApiKey.getId()); + + // Update API key description + String updatedDescription = "Updated description " + RandomStringUtils.insecure().randomAlphabetic(5); + ApiKeyInfo updatedApiKeyInfo = restClient.updateApiKeyDescription(savedApiKey.getId(), updatedDescription); + assertThat(updatedApiKeyInfo).isNotNull(); + assertThat(updatedApiKeyInfo.getDescription()).isEqualTo(updatedDescription); + + // Disable the API key + ApiKeyInfo disabledApiKey = restClient.enableApiKey(savedApiKey.getId(), false); + assertThat(disabledApiKey).isNotNull(); + assertThat(disabledApiKey.isEnabled()).isFalse(); + + // Enable the API key + ApiKeyInfo enabledApiKey = restClient.enableApiKey(savedApiKey.getId(), true); + assertThat(enabledApiKey).isNotNull(); + assertThat(enabledApiKey.isEnabled()).isTrue(); + + // Delete the API key + restClient.deleteApiKey(savedApiKey.getId()); + + // Verify the API key is deleted + PageData apiKeysAfterDelete = restClient.getUserApiKeys(tenantAdmin1.getId(), new PageLink(10)); + assertThat(apiKeysAfterDelete.getData()).isEmpty(); + } + + @Test + public void testGetDeviceProfileInfosByIds() { + var profileData = new DeviceProfileData(); + profileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + profileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration()); + + // Create a device profile in tenant1 (current tenant) + var deviceProfile1 = new DeviceProfile(); + deviceProfile1.setTenantId(tenant1.getId()); + deviceProfile1.setName("Device Profile 1"); + deviceProfile1.setType(DeviceProfileType.DEFAULT); + deviceProfile1.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile1.setProfileData(profileData); + deviceProfile1 = restClient.saveDeviceProfile(deviceProfile1); + + var deviceProfile2 = new DeviceProfile(); + deviceProfile2.setTenantId(tenant1.getId()); + deviceProfile2.setName("Device Profile 2"); + deviceProfile2.setType(DeviceProfileType.DEFAULT); + deviceProfile2.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile2.setProfileData(profileData); + deviceProfile2 = restClient.saveDeviceProfile(deviceProfile2); + + // Create two more device profiles in tenant2 (different tenant) + restClient.login(tenantAdmin2.getEmail(), "password123"); + + var deviceProfile3 = new DeviceProfile(); + deviceProfile3.setTenantId(tenant2.getId()); + deviceProfile3.setName("Device Profile 3"); + deviceProfile3.setType(DeviceProfileType.DEFAULT); + deviceProfile3.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile3.setProfileData(profileData); + deviceProfile3 = restClient.saveDeviceProfile(deviceProfile3); + + var deviceProfile4 = new DeviceProfile(); + deviceProfile4.setTenantId(tenant2.getId()); + deviceProfile4.setName("Device Profile 4"); + deviceProfile4.setType(DeviceProfileType.DEFAULT); + deviceProfile4.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile4.setProfileData(profileData); + deviceProfile4 = restClient.saveDeviceProfile(deviceProfile4); + + // Attempt to fetch profiles 1 and 3 while acting as Tenant 2. + // - Profile 1: Filtered out because it belongs to a different tenant. + // - Profile 2: Filtered out because it belongs to a different tenant and was not requested. + // - Profile 3: Should be returned. + // - Profile 4: Filtered out; it belongs to the correct tenant, but was not requested by ID. + List profiles = restClient.getDeviceProfileInfosByIds(Set.of(deviceProfile1.getUuidId(), deviceProfile3.getUuidId())); + assertThat(profiles).hasSize(1); + assertThat(profiles.get(0).getId()).isEqualTo(deviceProfile3.getId()); + } + + @Test + public void testGetAssetProfilesByIds() { + // Create two asset profiles in tenant1 (current tenant) + var assetProfile1 = new AssetProfile(); + assetProfile1.setTenantId(tenant1.getId()); + assetProfile1.setName("Asset Profile 1"); + assetProfile1 = restClient.saveAssetProfile(assetProfile1); + + var assetProfile2 = new AssetProfile(); + assetProfile2.setTenantId(tenant1.getId()); + assetProfile2.setName("Asset Profile 2"); + assetProfile2 = restClient.saveAssetProfile(assetProfile2); + + // Create two more asset profiles in tenant2 (different tenant) + restClient.login(tenantAdmin2.getEmail(), "password123"); + + var assetProfile3 = new AssetProfile(); + assetProfile3.setTenantId(tenant2.getId()); + assetProfile3.setName("Asset Profile 3"); + assetProfile3 = restClient.saveAssetProfile(assetProfile3); + + var assetProfile4 = new AssetProfile(); + assetProfile4.setTenantId(tenant2.getId()); + assetProfile4.setName("Asset Profile 4"); + assetProfile4 = restClient.saveAssetProfile(assetProfile4); + + // Attempt to fetch profiles 1 and 3 while acting as Tenant 2. + // - Profile 1: Filtered out because it belongs to a different tenant. + // - Profile 2: Filtered out because it belongs to a different tenant and was not requested. + // - Profile 3: Should be returned. + // - Profile 4: Filtered out; it belongs to the correct tenant, but was not requested by ID. + List profiles = restClient.getAssetProfilesByIds(Set.of(assetProfile1.getUuidId(), assetProfile3.getUuidId())); + assertThat(profiles).hasSize(1); + assertThat(profiles.get(0).getId()).isEqualTo(assetProfile3.getId()); + } + +} 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 8a3596e682..5c5914105b 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 @@ -81,7 +81,7 @@ import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.fail; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; @DisableUIListeners @@ -577,7 +577,7 @@ public class MqttClientTest extends AbstractContainerTest { Awaitility .await() .alias("Check device disconnect.") - .atMost(TIMEOUT*timeoutMultiplier, TimeUnit.SECONDS) + .atMost(TIMEOUT * timeoutMultiplier, TimeUnit.SECONDS) .until(() -> !returnCodeByteValue.isEmpty()); assertThat(returnCodeByteValueSecondClient).isEmpty(); 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 26235cf663..bb464ee52e 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 @@ -64,7 +64,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultGatewayPrototype; @DisableUIListeners diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java index 2416617554..562b872ee7 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java @@ -303,7 +303,7 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { public void deleteDeviceProfileByTitle(String deviceProfileTitle) { DeviceProfile deviceProfile = getDeviceProfileByName(deviceProfileTitle); if (deviceProfile != null) { - testRestClient.deleteDeviseProfile(deviceProfile.getId()); + testRestClient.deleteDeviceProfile(deviceProfile.getId()); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java index bce76debc1..9937975414 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java @@ -52,7 +52,7 @@ public class CreateDeviceProfileImportTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java index 7f307aa48c..7da444e7fa 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java @@ -51,7 +51,7 @@ public class CreateDeviceProfileTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java index dacfc51e8c..d34b69ba78 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java @@ -51,7 +51,7 @@ public class DeviceProfileEditMenuTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java index 6123a6b9f7..eced71c898 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java @@ -46,7 +46,7 @@ public class SearchDeviceProfileTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java index cc8c92a32b..acc1323462 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java @@ -45,7 +45,7 @@ public class SortByNameTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } @@ -83,9 +83,9 @@ public class SortByNameTest extends AbstractDriverBaseTest { profilesPage.setProfileName(2); String thirdDeviceProfile = profilesPage.getProfileName(); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfile).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileNumber).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfile).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileNumber).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); Assert.assertEquals(firstDeviceProfile, deviceProfileSymbol); Assert.assertEquals(secondDeviceProfile, deviceProfileNumber); @@ -126,9 +126,9 @@ public class SortByNameTest extends AbstractDriverBaseTest { profilesPage.setProfileName(lastIndex - 2); String thirdDeviceProfile = profilesPage.getProfileName(); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfile).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileNumber).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfile).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileNumber).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); Assert.assertEquals(firstDeviceProfile, deviceProfileSymbol); Assert.assertEquals(secondDeviceProfile, deviceProfileNumber); diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index f85e9b7691..b1034afe4e 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index be49ece5a6..3a08495069 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -14,46 +14,56 @@ # limitations under the License. # -queue_type: "kafka" -request_topic: "js_eval.requests" -queue_prefix: "" -http_port: "8888" # /livenessProbe +# Queue configuration +# Defines the messaging queue backend used to dispatch JS evaluation requests to the executor and receive results. +queue_type: "kafka" # Queue type used for JS evaluation requests: kafka or in-memory +request_topic: "js_eval.requests" # Kafka topic name for incoming JS evaluation requests +queue_prefix: "" # Optional prefix applied to all queue/topic names; useful for environment isolation +http_port: "8888" # HTTP port exposing the /livenessProbe health check endpoint +# JavaScript execution configuration +# Controls response polling behavior and output size limits for individual JS script executions. js: - response_poll_interval: "25" - max_result_size: "300000" + response_poll_interval: "25" # Interval in milliseconds to poll for JS execution responses + max_result_size: "300000" # Maximum allowed size in bytes of a JS execution result +# Kafka configuration +# Connection, producer, and topic settings for the Kafka message broker used as the JS evaluation queue. kafka: bootstrap: # Kafka Bootstrap Servers servers: "localhost:9092" - replication_factor: "1" + replication_factor: "1" # Number of replicas for each Kafka topic partition acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge - batch_size: "128" # for producer - linger_ms: "5" # for producer + batch_size: "128" # Maximum number of messages to accumulate before triggering a batch send + linger_ms: "5" # Time in milliseconds the producer waits before sending a batch, allowing more messages to accumulate partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency - requestTimeout: "30000" # The default value in kafkajs is: 30000 - connectionTimeout: "1000" # The default value in kafkajs is: 1000 - compression: "none" # gzip or uncompressed - topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" - use_confluent_cloud: false - client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh + requestTimeout: "30000" # Time in milliseconds to wait for a response to a Kafka request before timing out + connectionTimeout: "1000" # Time in milliseconds to wait when establishing a connection to a Kafka broker + compression: "none" # Message compression codec for the producer: none, gzip + topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" # Semicolon-separated Kafka topic configuration properties applied on topic creation + use_confluent_cloud: false # Set to true to enable Confluent Cloud-specific configuration (SSL + SASL) + client_id: "kafkajs" # Kafka client identifier; inject the pod name to easily identify the client via kafka-consumer-groups.sh ssl: - enabled: false + enabled: false # Enable SSL/TLS for Kafka broker connections confluent: sasl: - mechanism: "PLAIN" + mechanism: "PLAIN" # SASL mechanism for Confluent Cloud authentication: PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512 +# Logging configuration +# Controls log verbosity, output directory, and log file naming pattern for the JS executor service. logger: - level: "info" - path: "logs" - filename: "tb-js-executor-%DATE%.log" + level: "info" # Log level: trace, debug, info, warn, or error + path: "logs" # Directory path where log files are written + filename: "tb-js-executor-%DATE%.log" # Log file name pattern; %DATE% is replaced with the current date +# JavaScript execution and monitoring configuration +# Sandboxing, script caching, and observability settings for the JS script runtime. script: - use_sandbox: "true" - memory_usage_trace_frequency: "1000" - script_body_trace_frequency: "10000" - stat_print_frequency: "10000" - max_active_scripts: "1000" - slow_query_log_ms: "5.000000" #millis - slow_query_log_body: "false" + use_sandbox: "true" # Run scripts inside an isolated sandbox to prevent access to Node.js internals + memory_usage_trace_frequency: "1000" # Log memory usage every N script executions + script_body_trace_frequency: "10000" # Log the script body every N script executions + stat_print_frequency: "10000" # Print execution statistics every N script executions + max_active_scripts: "1000" # Maximum number of compiled scripts kept in the cache + slow_query_log_ms: "5.000000" # Execution time threshold in milliseconds above which a script is considered slow and logged + slow_query_log_body: "false" # Log the script body when execution time exceeds slow_query_log_ms threshold diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index b096a3ae99..b857f1e085 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.2.2.2", + "version": "4.4.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index f5ea04fac3..66a629f550 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index ab484e7df8..a968d693df 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index ac7b7b70c3..f852773b8c 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard msa @@ -36,7 +36,7 @@ pkg.package.phase (which is "none" here since these modules don't produce packaging). --> package thingsboard - thingsboard/openjdk17:bookworm-slim + thingsboard/openjdk25:trixie-slim true true 3.5.1 @@ -168,8 +168,8 @@ - 4.2.2-latest - true + 4.4.0-latest + false diff --git a/msa/tb-node/docker/Dockerfile b/msa/tb-node/docker/Dockerfile index 2180cf6870..0f1738ecb1 100644 --- a/msa/tb-node/docker/Dockerfile +++ b/msa/tb-node/docker/Dockerfile @@ -16,6 +16,21 @@ FROM ${docker.base.image} +# For SVG renderer Debian Trixie slim fonts should be installed explicitly +RUN echo "Previously installed font libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz' || echo " (none)") \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libharfbuzz0b fontconfig fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* \ + && echo "Current fonts libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz') \ + && echo "Font config list:" \ + && fc-list && test -n "$(fc-list)" \ + && (fc-list | grep -i dejavu) \ + && echo "Harfbuzz library test with ldconfig:" \ + && (ldconfig -p | grep harfbuzz) + COPY logback.xml start-tb-node.sh ${pkg.name}.deb /tmp/ RUN chmod a+x /tmp/*.sh \ diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 3dd84135cb..6f28a25c51 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 34a90e6fb7..59e3a2898f 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} ENV PG_MAJOR=16 @@ -40,19 +40,33 @@ ENV PATH=$PATH:/usr/lib/postgresql/$PG_MAJOR/bin ENV PGLOG=/var/log/postgres ENV CASSANDRA_LOG=/var/log/cassandra +# For SVG renderer Debian Trixie slim fonts should be installed explicitly +RUN echo "Previously installed font libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz' || echo " (none)") \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libharfbuzz0b fontconfig fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* \ + && echo "Current fonts libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz') \ + && echo "Font config list:" \ + && fc-list && test -n "$(fc-list)" \ + && (fc-list | grep -i dejavu) \ + && echo "Harfbuzz library test with ldconfig:" \ + && (ldconfig -p | grep harfbuzz) + 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 apt-get update \ && apt-get install -y --no-install-recommends wget nmap procps gnupg2 \ - && echo "deb http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null \ - && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | apt-key add - \ - && echo "deb https://debian.cassandra.apache.org 40x main" | tee -a /etc/apt/sources.list.d/cassandra.sources.list > /dev/null \ - && wget -q https://downloads.apache.org/cassandra/KEYS -O- | apt-key add - \ + && mkdir -p /etc/apt/keyrings \ + && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list > /dev/null \ + && wget -q https://downloads.apache.org/cassandra/KEYS -O- | gpg --dearmor -o /etc/apt/keyrings/cassandra.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/cassandra.gpg] https://debian.cassandra.apache.org 40x main" | tee /etc/apt/sources.list.d/cassandra.sources.list > /dev/null \ && apt-get update \ && apt-get install -y --no-install-recommends cassandra cassandra-tools postgresql-${PG_MAJOR} \ && rm -rf /var/lib/apt/lists/* \ - && update-rc.d cassandra disable \ - && update-rc.d postgresql disable \ && apt-get purge -y --auto-remove \ && sed -i.old '/ulimit/d' /etc/init.d/cassandra \ && chmod a+x /tmp/*.sh \ diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index 0a0f61f904..871154a455 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} ENV PG_MAJOR 12 @@ -32,16 +32,31 @@ ENV SPRING_DATASOURCE_USERNAME=${pkg.user} ENV PGLOG=/var/log/postgres +# For SVG renderer Debian Trixie slim fonts should be installed explicitly +RUN echo "Previously installed font libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz' || echo " (none)") \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libharfbuzz0b fontconfig fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* \ + && echo "Current fonts libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz') \ + && echo "Font config list:" \ + && fc-list && test -n "$(fc-list)" \ + && (fc-list | grep -i dejavu) \ + && echo "Harfbuzz library test with ldconfig:" \ + && (ldconfig -p | grep harfbuzz) + 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 apt-get update \ && apt-get install -y --no-install-recommends wget gnupg2 \ - && echo "deb http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null \ - && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | apt-key add - \ + && mkdir -p /etc/apt/keyrings \ + && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list > /dev/null \ && apt-get update \ && apt-get install -y --no-install-recommends postgresql-${PG_MAJOR} \ && rm -rf /var/lib/apt/lists/* \ - && update-rc.d postgresql disable \ && apt-get purge -y --auto-remove \ && chmod a+x /tmp/*.sh \ && mv /tmp/start-tb.sh /usr/bin \ diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 5c10adf67e..86651cd311 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index f081e6fd46..60aa546e1d 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 78e445fda8..b0c0e7b625 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index d65d07f9a9..296a754c38 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 8dfe14b3a6..dc8fc5b39a 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index aa10ff1184..92978fcaa7 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index fb344e3242..ed441a2208 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 0bc90c6ac4..2eef220b7d 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index a0c023b5f8..028b84f98e 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 8314985622..188583dc7e 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -15,30 +15,33 @@ # # Spring common parameters +# Controls Spring Boot startup mode, web environment type, and bean wiring behaviour. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. # Server common parameters +# Configures the embedded HTTP server bind address and port used for actuator and metrics endpoints. server: # Server bind address (has no effect if web-environment is disabled). address: "${HTTP_BIND_ADDRESS:0.0.0.0}" # Server bind port (has no effect if web-environment is disabled). port: "${HTTP_BIND_PORT:8086}" -# Zookeeper connection parameters. Used for service discovery. +# ZooKeeper connection parameters +# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments. zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:true}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. @@ -46,6 +49,7 @@ zk: recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" # Queue configuration parameters +# Defines the message queue type (Kafka or in-memory) and all related producer, consumer, and topic settings. queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). @@ -196,6 +200,7 @@ queue: msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" # Version control parameters +# Configures the Git-based version control executor, including thread pools and local repository storage. vc: # Pool size for handling export tasks thread_pool_size: "${TB_VC_POOL_SIZE:6}" @@ -206,6 +211,7 @@ vc: repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" # Usage statistics parameters +# Controls collection and reporting of API usage statistics at system, tenant, and customer levels. usage: stats: report: @@ -215,10 +221,13 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates + urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters +# Enables actuator-based metrics collection and configures histogram/timer percentile exports. metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" @@ -227,6 +236,7 @@ metrics: percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" # General management parameters +# Configures Spring Boot Actuator endpoint exposure, including the metrics and info endpoints. management: endpoints: web: @@ -235,12 +245,14 @@ management: include: "${METRICS_ENDPOINTS_EXPOSE:info}" # Service common properties +# Identifies this microservice instance by type and optional unique ID for cluster coordination. service: type: "${TB_SERVICE_TYPE:tb-vc-executor}" # service type # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" # Notification system parameters +# Defines deduplication rules for notification triggers to prevent repeated alerts within a time window. notification_system: rules: # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index c51aa6ed05..9e9b643402 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.2.2.2", + "version": "4.4.0", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 00c1703dcd..4ad7aef8c8 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 4b9b9fa21e..c23aaabf80 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard netty-mqtt - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT jar Netty MQTT Client diff --git a/packaging/java/build.gradle b/packaging/java/build.gradle index 16d3db5131..a9984683d2 100644 --- a/packaging/java/build.gradle +++ b/packaging/java/build.gradle @@ -16,12 +16,14 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { - id "nebula.ospackage" version "8.6.3" + id "com.netflix.nebula.ospackage" version "12.1.1" } buildDir = projectBuildDir version = projectVersion -distsDirName = "./" +base { + distsDirectory = layout.buildDirectory.dir("./") +} // OS Package plugin configuration ospackage { @@ -33,8 +35,8 @@ ospackage { into pkgInstallFolder - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the actual .jar file from(mainJar) { @@ -42,19 +44,25 @@ ospackage { rename { String fileName -> "${pkgName}.jar" } - fileMode 0500 + filePermissions { + unix('r-x------') // 0500 + } into "bin" } if("${pkgCopyInstallScripts}".equalsIgnoreCase("true")) { // Copy the install files from("${buildDir}/bin/install/install.sh") { - fileMode 0775 + filePermissions { + unix('rwxrwxr-x') // 0775 + } into "bin/install" } from("${buildDir}/bin/install/upgrade.sh") { - fileMode 0775 + filePermissions { + unix('rwxrwxr-x') // 0775 + } into "bin/install" } @@ -67,14 +75,18 @@ ospackage { from("${buildDir}/conf") { exclude "${pkgName}.conf" fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "conf" } // Copy the data files from("${buildDir}/data") { fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "data" } @@ -92,7 +104,7 @@ buildRpm { archiveVersion = projectVersion.replace('-', '') archiveFileName = "${pkgName}.rpm" - // Support Java 17 (existing), plus Java 21 and Java 25 for RPM-based distros + // Support Java 25 (current), plus Java 21 and Java 17 (backward compatibility) for RPM-based distros // Keep using RPM boolean expression syntax since .or() chaining is for DEB only requires("(java-17 or java-17-headless or jre-17 or jre-17-headless or " + "java-21 or java-21-headless or jre-21 or jre-21-headless or " + @@ -102,7 +114,9 @@ buildRpm { include "${pkgName}.conf" filter(ReplaceTokens, tokens: ['pkg.platform': 'rpm']) fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "${pkgInstallFolder}/conf" } @@ -111,13 +125,15 @@ buildRpm { preUninstall file("${buildDir}/control/rpm/prerm") postUninstall file("${buildDir}/control/rpm/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/usr/lib/systemd/system" rename { String filename -> "${pkgName}.service" @@ -143,7 +159,9 @@ buildDeb { include "${pkgName}.conf" filter(ReplaceTokens, tokens: ['pkg.platform': 'deb']) fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "${pkgInstallFolder}/conf" } @@ -157,13 +175,15 @@ buildDeb { preUninstall file("${buildDir}/control/deb/prerm") postUninstall file("${buildDir}/control/deb/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/lib/systemd/system" rename { String filename -> "${pkgName}.service" diff --git a/packaging/java/scripts/windows/install.bat b/packaging/java/scripts/windows/install.bat index ca17408dfe..566286d4b5 100644 --- a/packaging/java/scripts/windows/install.bat +++ b/packaging/java/scripts/windows/install.bat @@ -7,11 +7,11 @@ setlocal ENABLEEXTENSIONS for /f tokens^=2-5^ delims^=.-_^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j%%k" @ECHO CurrentVersion %jver% -if %jver% NEQ 170 GOTO JAVA_NOT_INSTALLED +if %jver% NEQ 250 GOTO JAVA_NOT_INSTALLED :JAVA_INSTALLED -@ECHO Java 17 found! +@ECHO Java 25 found! @ECHO Installing thingsboard ... SET loadDemo=false @@ -50,8 +50,8 @@ POPD GOTO END :JAVA_NOT_INSTALLED -@ECHO Java 17 is not installed. Only Java 17 is supported -@ECHO Please go to https://adoptopenjdk.net/index.html and install Java 17. Then retry installation. +@ECHO Java 25 is not installed. Only Java 25 is supported +@ECHO Please go to https://adoptopenjdk.net/index.html and install Java 25. Then retry installation. PAUSE GOTO END diff --git a/packaging/js/build.gradle b/packaging/js/build.gradle index 009d1aa460..73020cf4f8 100644 --- a/packaging/js/build.gradle +++ b/packaging/js/build.gradle @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import org.apache.tools.ant.filters.ReplaceTokens plugins { - id "nebula.ospackage" version "8.6.3" + id "com.netflix.nebula.ospackage" version "12.1.1" } buildDir = projectBuildDir version = projectVersion -distsDirName = "./" +base { + distsDirectory = layout.buildDirectory.dir("./") +} // OS Package plugin configuration ospackage { @@ -33,18 +34,22 @@ ospackage { into pkgInstallFolder - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the executable file from("${buildDir}/package/linux/bin/${pkgName}") { - fileMode 0500 + filePermissions { + unix('r-x------') // 0500 + } into "bin" } // Copy the init file from("${buildDir}/package/linux/init/template") { - fileMode 0500 + filePermissions { + unix('r-x------') // 0500 + } into "init" rename { String filename -> "${pkgName}" @@ -54,7 +59,9 @@ ospackage { // Copy the config files from("${buildDir}/package/linux/conf") { fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "conf" } @@ -78,13 +85,15 @@ buildRpm { preUninstall file("${buildDir}/control/rpm/prerm") postUninstall file("${buildDir}/control/rpm/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/usr/lib/systemd/system" rename { String filename -> "${pkgName}.service" @@ -111,13 +120,15 @@ buildDeb { preUninstall file("${buildDir}/control/deb/prerm") postUninstall file("${buildDir}/control/deb/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/lib/systemd/system" rename { String filename -> "${pkgName}.service" diff --git a/pom.xml b/pom.xml index 5604ca39d7..b720468858 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT pom Thingsboard @@ -28,8 +28,8 @@ 2016 - 17 - 17 + 25 + 25 ${basedir} none none @@ -62,6 +62,7 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} + 4.3.1.2-SNAPSHOT 3.5.13 2.4.0-b180830.0359 0.12.5 @@ -137,6 +138,7 @@ 4.0.2 1.7.5 3.8.0 + 1.18.4 1.8.0-TB 2.38.0 1.24 @@ -165,7 +167,7 @@ 0.4.8 1.0.0 - 1.39.0 + 1.48.0 6.6.0 1.35.0 1.6.1 @@ -619,7 +621,6 @@ org.apache.maven.plugins maven-assembly-plugin - ${pkg.skip.zip} ${pkg.name} ${main.dir}/packaging/${pkg.type}/assembly/windows.xml @@ -632,6 +633,9 @@ single + + ${pkg.skip.zip} + @@ -678,7 +682,7 @@ maven-compiler-plugin 3.11.0 - 17 + 25 -Xlint:deprecation -Xlint:removal @@ -722,7 +726,7 @@ org.thingsboard gradle-maven-plugin - 1.0.12 + 1.0.15 com.github.eirslett @@ -736,6 +740,7 @@ -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=200 + -XX:+EnableDynamicAgentLoading --add-opens=java.base/java.lang.reflect=ALL-UNNAMED -Dqueue.edqs.local.rocksdb_path="target/rocksdb/fork_${surefire.forkNumber}/edqs" -Dqueue.calculated_fields.rocks_db_path="target/rocksdb/fork_${surefire.forkNumber}/cf" @@ -1026,6 +1031,16 @@ pom import + + net.bytebuddy + byte-buddy + ${bytebuddy.version} + + + net.bytebuddy + byte-buddy-agent + ${bytebuddy.version} + org.thingsboard.langchain4j langchain4j-bom @@ -1185,6 +1200,12 @@ ${project.version} test + + org.thingsboard.client + thingsboard-ce-client + ${thingsboard.client.version} + test + org.thingsboard.msa js-executor @@ -2026,7 +2047,7 @@ https://repo1.maven.org/maven2/ - thingsboard-repo + thingsboard-public-repo https://repo.thingsboard.io/artifactory/libs-release-public @@ -2052,4 +2073,10 @@ + + + thingsboard-repo + https://repo.thingsboard.io/artifactory/libs-release-public + + diff --git a/rest-client/pom.xml b/rest-client/pom.xml index d108c49cda..9996b19d00 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard rest-client @@ -51,6 +51,10 @@ com.auth0 java-jwt + + org.apache.httpcomponents.core5 + httpcore5 + 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 f82b815d65..f133527ddc 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 @@ -19,9 +19,11 @@ import com.auth0.jwt.JWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; +import lombok.Getter; import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.concurrent.LazyInitializer; +import org.apache.hc.core5.net.URIBuilder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; @@ -38,10 +40,12 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rest.client.utils.RestJsonConverter; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -74,6 +78,7 @@ import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.UserEmailInfo; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; @@ -89,6 +94,8 @@ 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.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.domain.DomainInfo; @@ -98,8 +105,10 @@ import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -113,6 +122,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.NotificationId; +import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.OtaPackageId; @@ -132,6 +143,15 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; @@ -143,11 +163,15 @@ 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.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -190,29 +214,32 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.io.Closeable; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static java.util.stream.Collectors.joining; import static org.thingsboard.server.common.data.StringUtils.isEmpty; -/** - * @author Andrew Shvayka - */ public class RestClient implements Closeable { - private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; + + private static final String TOKEN_HEADER_PARAM = "X-Authorization"; private static final long AVG_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(30); protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; private final LazyInitializer executor = LazyInitializer.builder() .setInitializer(() -> ThingsBoardExecutors.newWorkStealingPool(10, getClass())) .get(); + @Getter protected final RestTemplate restTemplate; protected final RestTemplate loginRestTemplate; protected final String baseURL; @@ -220,56 +247,70 @@ public class RestClient implements Closeable { private String username; private String password; private String mainToken; + @Getter private String refreshToken; private long mainTokenExpTs; private long refreshTokenExpTs; private long clientServerTimeDiff; + public enum AuthType {JWT, API_KEY} + public RestClient(String baseURL) { this(new RestTemplate(), baseURL); } public RestClient(RestTemplate restTemplate, String baseURL) { - this(restTemplate, baseURL, null); + this(restTemplate, baseURL, AuthType.JWT, null); } public RestClient(RestTemplate restTemplate, String baseURL, String accessToken) { + this(restTemplate, baseURL, AuthType.JWT, accessToken); + } + + public RestClient(RestTemplate restTemplate, String baseURL, AuthType authType, String token) { this.restTemplate = restTemplate; this.loginRestTemplate = new RestTemplate(restTemplate.getRequestFactory()); this.baseURL = baseURL; this.restTemplate.getInterceptors().add((request, bytes, execution) -> { HttpRequest wrapper = new HttpRequestWrapper(request); - if (accessToken == null) { - long calculatedTs = System.currentTimeMillis() + clientServerTimeDiff + AVG_REQUEST_TIMEOUT; - if (calculatedTs > mainTokenExpTs) { - synchronized (RestClient.this) { + switch (authType) { + case JWT -> { + if (token == null) { + long calculatedTs = System.currentTimeMillis() + clientServerTimeDiff + AVG_REQUEST_TIMEOUT; if (calculatedTs > mainTokenExpTs) { - if (calculatedTs < refreshTokenExpTs) { - refreshToken(); - } else { - doLogin(); + synchronized (RestClient.this) { + if (calculatedTs > mainTokenExpTs) { + if (calculatedTs < refreshTokenExpTs) { + refreshToken(); + } else { + doLogin(); + } + } } } + } else { + mainToken = token; } + wrapper.getHeaders().set(TOKEN_HEADER_PARAM, "Bearer " + mainToken); + } + case API_KEY -> { + wrapper.getHeaders().set(TOKEN_HEADER_PARAM, "ApiKey " + token); } - } else { - mainToken = accessToken; } - wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + mainToken); return execution.execute(wrapper, bytes); }); } - public RestTemplate getRestTemplate() { - return restTemplate; + public static RestClient withApiKey(String baseURL, String token) { + return withApiKey(new RestTemplate(), baseURL, token); } - public String getToken() { - return mainToken; + public static RestClient withApiKey(RestTemplate rt, String baseURL, String token) { + return new RestClient(rt, baseURL, AuthType.API_KEY, token); } - public String getRefreshToken() { - return refreshToken; + public String getToken() { + return mainToken; } public void refreshToken() { @@ -509,6 +550,99 @@ public class RestClient implements Closeable { params).getBody(); } + public PageData getAllAlarms(AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { + String urlSecondPart = "/api/alarms?"; + Map params = new HashMap<>(); + if (fetchOriginator != null) { + params.put("fetchOriginator", String.valueOf(fetchOriginator)); + urlSecondPart += "&fetchOriginator={fetchOriginator}"; + } + if (searchStatus != null) { + params.put("searchStatus", searchStatus.name()); + urlSecondPart += "&searchStatus={searchStatus}"; + } + if (status != null) { + params.put("status", status.name()); + urlSecondPart += "&status={status}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAlarmsV2(EntityId entityId, List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarm/{entityType}/{entityId}?"; + Map params = new HashMap<>(); + params.put("entityType", entityId.getEntityType().name()); + params.put("entityId", entityId.getId().toString()); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAllAlarmsV2(List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarms?"; + Map params = new HashMap<>(); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + public Optional getHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); @@ -1462,6 +1596,33 @@ public class RestClient implements Closeable { }, activeOnly).getBody(); } + public List getDeviceProfileInfosByIds(Set ids) { + URIBuilder builder; + try { + builder = new URIBuilder(baseURL); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid base URL: " + baseURL, e); + } + + builder.appendPath("/api/deviceProfileInfos"); + + String commaSeparatedIds = ids.stream() + .filter(Objects::nonNull) + .map(UUID::toString) + .collect(joining(",")); + + builder.addParameter("deviceProfileIds", commaSeparatedIds); + + URI uri; + try { + uri = builder.build(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Failed to construct API URI from base URL and provided params", e); + } + + return restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() {}).getBody(); + } + public JsonNode claimDevice(String deviceName, ClaimRequest claimRequest) { return restTemplate.exchange( baseURL + "/api/customer/device/{deviceName}/claim", @@ -1698,6 +1859,33 @@ public class RestClient implements Closeable { }, params).getBody(); } + public List getAssetProfilesByIds(Set ids) { + URIBuilder builder; + try { + builder = new URIBuilder(baseURL); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid base URL: " + baseURL, e); + } + + builder.appendPath("/api/assetProfileInfos"); + + String commaSeparatedIds = ids.stream() + .filter(Objects::nonNull) + .map(UUID::toString) + .collect(joining(",")); + + builder.addParameter("assetProfileIds", commaSeparatedIds); + + URI uri; + try { + uri = builder.build(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Failed to construct API URI from base URL and provided params", e); + } + + return restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() {}).getBody(); + } + public Long countEntitiesByQuery(EntityCountQuery query) { return restTemplate.postForObject(baseURL + "/api/entitiesQuery/count", query, Long.class); } @@ -1710,6 +1898,37 @@ public class RestClient implements Closeable { }).getBody(); } + /** + * @deprecated Use {@link #findAvailableEntityKeysV2(EntityDataQuery, boolean, boolean, Set, boolean)} instead. + */ + @Deprecated(forRemoval = true) + public AvailableEntityKeys findAvailableEntityKeysByQuery(EntityDataQuery query, boolean includeTimeseries, boolean includeAttributes, AttributeScope scope) { + var uri = UriComponentsBuilder.fromUriString(baseURL) + .path("/api/entitiesQuery/find/keys") + .queryParam("timeseries", includeTimeseries) + .queryParam("attributes", includeAttributes) + .queryParamIfPresent("scope", Optional.ofNullable(scope)) + .build() + .toUri(); + return restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference() {}).getBody(); + } + + @SneakyThrows(URISyntaxException.class) + public AvailableEntityKeysV2 findAvailableEntityKeysV2( + EntityDataQuery query, boolean includeTimeseries, boolean includeAttributes, Set scopes, boolean includeSamples + ) { + var builder = new URIBuilder(baseURL).appendPath("/api/v2/entitiesQuery/find/keys") + .addParameter("includeTimeseries", String.valueOf(includeTimeseries)) + .addParameter("includeAttributes", String.valueOf(includeAttributes)) + .addParameter("includeSamples", String.valueOf(includeSamples)); + if (scopes != null) { + for (AttributeScope scope : scopes) { + builder.addParameter("scopes", scope.name()); + } + } + return restTemplate.exchange(builder.build(), HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference() {}).getBody(); + } + public PageData findAlarmDataByQuery(AlarmDataQuery query) { return restTemplate.exchange( baseURL + "/api/alarmsQuery/find", @@ -2158,13 +2377,16 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId()); } - public PageData getTenantDomainInfos() { + public PageData getTenantDomainInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/domain/infos", + baseURL + "/api/domain/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getDomainInfoById(DomainId domainId) { @@ -2192,13 +2414,16 @@ public class RestClient implements Closeable { restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId()); } - public PageData getTenantMobileApps() { + public PageData getTenantMobileApps(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/mobile/app", + baseURL + "/api/mobile/app?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getMobileAppById(MobileAppId mobileAppId) { @@ -2222,13 +2447,16 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId()); } - public PageData getTenantMobileBundleInfos() { + public PageData getTenantMobileBundleInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/mobile/bundle/infos", + baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, + params).getBody(); } public Optional getMobileBundleById(MobileAppBundleId mobileAppBundleId) { @@ -2846,6 +3074,17 @@ public class RestClient implements Closeable { }, params).getBody(); } + public PageData getUsersByQuery(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/users/info?" + 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()); @@ -2893,6 +3132,44 @@ public class RestClient implements Closeable { userCredentialsEnabled); } + public ApiKey saveApiKey(ApiKeyInfo apiKeyInfo) { + return restTemplate.postForEntity(baseURL + "/api/apiKey", apiKeyInfo, ApiKey.class).getBody(); + } + + public PageData getUserApiKeys(UserId userId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("userId", userId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/apiKeys/{userId}?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() {}, params).getBody(); + } + + public ApiKeyInfo updateApiKeyDescription(ApiKeyId apiKeyId, String description) { + return restTemplate.exchange( + baseURL + "/api/apiKey/{id}/description", + HttpMethod.PUT, + new HttpEntity<>(description), + ApiKeyInfo.class, + apiKeyId.getId()).getBody(); + } + + public ApiKeyInfo enableApiKey(ApiKeyId apiKeyId, boolean enabled) { + return restTemplate.exchange( + baseURL + "/api/apiKey/{id}/enabled/{enabledValue}", + HttpMethod.PUT, + HttpEntity.EMPTY, + ApiKeyInfo.class, + apiKeyId.getId(), + enabled).getBody(); + } + + public void deleteApiKey(ApiKeyId apiKeyId) { + restTemplate.delete(baseURL + "/api/apiKey/{id}", apiKeyId.getId()); + } + public Optional getWidgetsBundleById(WidgetsBundleId widgetsBundleId) { try { ResponseEntity widgetsBundle = @@ -3006,7 +3283,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypes?" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -3094,7 +3371,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypesInfos?widgetsBundleId={widgetsBundleId}&" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -4114,6 +4391,46 @@ public class RestClient implements Closeable { } + @SneakyThrows(URISyntaxException.class) + public PageData getCalculatedFields(PageLink pageLink, + Set types, + EntityType entityType, + Set entities, + Set names) { + var urlBuilder = new URIBuilder(baseURL).appendPath("/api/calculatedFields"); + urlBuilder.addParameter("pageSize", String.valueOf(pageLink.getPageSize())); + urlBuilder.addParameter("page", String.valueOf(pageLink.getPage())); + if (!isEmpty(pageLink.getTextSearch())) { + urlBuilder.addParameter("textSearch", pageLink.getTextSearch()); + } + if (pageLink.getSortOrder() != null) { + urlBuilder.addParameter("sortProperty", pageLink.getSortOrder().getProperty()); + urlBuilder.addParameter("sortOrder", pageLink.getSortOrder().getDirection().name()); + } + if (!CollectionUtils.isEmpty(types)) { + for (CalculatedFieldType type : types) { + urlBuilder.addParameter("types", type.name()); + } + } + if (entityType != null) { + urlBuilder.addParameter("entityType", entityType.name()); + } + if (!CollectionUtils.isEmpty(entities)) { + for (UUID entity : entities) { + urlBuilder.addParameter("entities", entity.toString()); + } + } + if (!CollectionUtils.isEmpty(names)) { + for (String name : names) { + urlBuilder.addParameter("name", name); + } + } + return restTemplate.exchange( + urlBuilder.build(), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() {}).getBody(); + } + public void deleteCalculatedField(CalculatedFieldId calculatedFieldId) { restTemplate.delete(baseURL + "/api/calculatedField/{calculatedFieldId}", calculatedFieldId.getId()); } @@ -4144,6 +4461,182 @@ public class RestClient implements Closeable { } } + public PageData getNotifications(Boolean unreadOnly, NotificationDeliveryMethod deliveryMethod, PageLink pageLink) { + Map params = new HashMap<>(); + + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(baseURL).append("/api/notifications?").append(getUrlParams(pageLink)); + addPageLinkToParam(params, pageLink); + + if (unreadOnly != null) { + urlBuilder.append("&unreadOnly={unreadOnly}"); + params.put("unreadOnly", unreadOnly.toString()); + } + if (deliveryMethod != null) { + urlBuilder.append("&deliveryMethod={deliveryMethod}"); + params.put("deliveryMethod", deliveryMethod.name()); + } + + return restTemplate.exchange(urlBuilder.toString(), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Integer getUnreadNotificationsCount(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/unread/count?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + return restTemplate.exchange( + baseURL + uri, + HttpMethod.GET, + HttpEntity.EMPTY, + Integer.class, params).getBody(); + } + + public void markNotificationAsRead(NotificationId notificationId) { + restTemplate.exchange( + baseURL + "/api/notification/{id}/read", + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class, + notificationId.getId()); + } + + public void markAllNotificationsAsRead(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/read?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + restTemplate.exchange( + baseURL + uri, + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class, + params); + } + + + public void deleteNotification(NotificationId notificationId) { + restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId()); + } + + public NotificationRequest saveNotificationRequest(NotificationRequest notificationRequest) { + return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody(); + } + + public NotificationRequestPreview getNotificationRequestPreview(NotificationRequest notificationRequest, int recipientsPreviewSize) { + return restTemplate.postForEntity(baseURL + "/api/notification/request/preview?recipientsPreviewSize={recipientsPreviewSize}", notificationRequest, NotificationRequestPreview.class, recipientsPreviewSize).getBody(); + } + + public Optional getNotificationRequestById(NotificationRequestId notificationRequestId) { + try { + ResponseEntity notificationRequest = restTemplate.getForEntity(baseURL + "/api/notification/request/{id}", NotificationRequestInfo.class, notificationRequestId.getId()); + return Optional.ofNullable(notificationRequest.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getNotificationRequests(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/notification/requests?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public void deleteNotificationRequest(NotificationRequestId notificationRequestId) { + restTemplate.delete(baseURL + "/api/notification/request/{id}", notificationRequestId.getId()); + } + + public NotificationSettings saveNotificationSettings(NotificationSettings notificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings", notificationSettings, NotificationSettings.class).getBody(); + } + + public Optional getNotificationSettings() { + try { + ResponseEntity notificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings", NotificationSettings.class); + return Optional.ofNullable(notificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getAvailableDeliveryMethods() { + return restTemplate.exchange(URI.create( + baseURL + "/api/notification/deliveryMethods"), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public UserNotificationSettings saveUserNotificationSettings(UserNotificationSettings userNotificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings/user", userNotificationSettings, UserNotificationSettings.class).getBody(); + } + + public Optional getUserNotificationSettings() { + try { + ResponseEntity userNotificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings/user", UserNotificationSettings.class); + return Optional.ofNullable(userNotificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) { + return restTemplate.postForEntity(baseURL + "/api/notification/target", notificationTarget, NotificationTarget.class).getBody(); + } + + public NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) { + return restTemplate.postForEntity(baseURL + "/api/notification/template", notificationTemplate, NotificationTemplate.class).getBody(); + } + + public AiModel saveAiModel(AiModel aiModel) { + return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody(); + } + + public Optional getAiModel(AiModelId aiModelId) { + try { + ResponseEntity response = restTemplate.getForEntity( + baseURL + "/api/ai/model/{aiModelId}", AiModel.class, aiModelId.getId()); + return Optional.ofNullable(response.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public void deleteAiModel(AiModelId aiModelId) { + restTemplate.delete(baseURL + "/api/ai/model/{aiModelId}", aiModelId.getId()); + } + + private String getTimeUrlParams(TimePageLink pageLink) { String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index de69670863..bba58e92a8 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 3810c46a99..a9f58a08b3 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 - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java index e4cb573cfe..f12d59cd0a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java @@ -19,11 +19,14 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.msg.queue.TbCallback; public interface JobManager { ListenableFuture submitJob(Job job); // TODO: rate limits + ListenableFuture submitJob(Job job, TbCallback finishCallback); + void cancelJob(TenantId tenantId, JobId jobId); void reprocessJob(TenantId tenantId, JobId jobId); 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 index 1d49d00113..a49e28db4b 100644 --- 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 @@ -63,6 +63,8 @@ public interface RuleEngineAlarmService { AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent); + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); 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 464c2df532..f827dd6e0b 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 @@ -76,6 +76,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; @@ -375,6 +376,8 @@ public interface TbContext { JobManager getJobManager(); + ApiKeyService getApiKeyService(); + boolean isExternalNodeForceAck(); /** diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 4a0f1883c0..8fac42e029 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.api.util; +import com.fasterxml.jackson.core.io.JsonStringEncoder; import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -57,8 +58,12 @@ public final class TbNodeUtils { } public static String processPattern(String pattern, TbMsg tbMsg) { + return processPattern(pattern, tbMsg, false); + } + + public static String processPattern(String pattern, TbMsg tbMsg, boolean escapeJsonValues) { try { - String result = processPattern(pattern, tbMsg.getMetaData()); + String result = processPattern(pattern, tbMsg.getMetaData(), escapeJsonValues); JsonNode json = JacksonUtil.toJsonNode(tbMsg.getData()); result = result.replace(ALL_DATA_TEMPLATE, JacksonUtil.toString(json)); @@ -79,7 +84,11 @@ public final class TbNodeUtils { } if (jsonNode != null && jsonNode.isValueNode()) { - result = result.replace(formatDataVarTemplate(group), jsonNode.asText()); + String value = jsonNode.asText(); + if (escapeJsonValues) { + value = escapeJsonValue(value); + } + result = result.replace(formatDataVarTemplate(group), value); } } } @@ -89,22 +98,31 @@ public final class TbNodeUtils { } } - private static String processPattern(String pattern, TbMsgMetaData metaData) { + private static String processPattern(String pattern, TbMsgMetaData metaData, boolean escapeJsonValues) { String replacement = metaData.isEmpty() ? "{}" : JacksonUtil.toString(metaData.getData()); pattern = pattern.replace(ALL_METADATA_TEMPLATE, replacement); - return processTemplate(pattern, metaData.values()); + return processTemplate(pattern, metaData.values(), escapeJsonValues); + } + + private static String processPattern(String pattern, TbMsgMetaData metaData) { + return processPattern(pattern, metaData, false); } public static String processTemplate(String template, Map data) { + return processTemplate(template, data, false); + } + + private static String processTemplate(String template, Map data, boolean escapeJsonValues) { String result = template; for (Map.Entry kv : data.entrySet()) { - result = processVar(result, kv.getKey(), kv.getValue()); + String value = escapeJsonValues ? escapeJsonValue(kv.getValue()) : kv.getValue(); + result = result.replace(formatMetadataVarTemplate(kv.getKey()), value); } return result; } - private static String processVar(String pattern, String key, String val) { - return pattern.replace(formatMetadataVarTemplate(key), val); + private static String escapeJsonValue(String value) { + return new String(JsonStringEncoder.getInstance().quoteAsString(value)); } static String formatDataVarTemplate(String key) { diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java index 18003d6b70..882e2080d7 100644 --- a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java @@ -305,6 +305,50 @@ public class TbNodeUtilsTest { assertThat(actual, is(expected)); } + @Test + public void testProcessPatternWithJsonEscaping() { + String pattern = "{\"name\":\"${user}\",\"desc\":\"$[description]\"}"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("user", "John \"Doe\""); + + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("description", "line1\nline2"); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TenantId.SYS_TENANT_ID) + .copyMetaData(md) + .data(JacksonUtil.toString(node)) + .build(); + + String result = TbNodeUtils.processPattern(pattern, msg, true); + Assertions.assertEquals("{\"name\":\"John \\\"Doe\\\"\",\"desc\":\"line1\\nline2\"}", result); + + // Verify the result is valid JSON + Assertions.assertDoesNotThrow(() -> JacksonUtil.toJsonNode(result)); + } + + @Test + public void testProcessPatternWithoutJsonEscaping() { + String pattern = "Hello ${user}, desc: $[description]"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("user", "John \"Doe\""); + + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("description", "line1\nline2"); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TenantId.SYS_TENANT_ID) + .copyMetaData(md) + .data(JacksonUtil.toString(node)) + .build(); + + // Without escaping, raw values are substituted as-is + String result = TbNodeUtils.processPattern(pattern, msg, false); + Assertions.assertEquals("Hello John \"Doe\", desc: line1\nline2", result); + } + @Test public void testMixedAllDataMetadataAndNormalTemplates() { // GIVEN diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index a156bb22a2..5e7cb48970 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 - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT rule-engine org.thingsboard.rule-engine @@ -161,6 +161,10 @@ org.thingsboard.langchain4j langchain4j + + org.apache.httpcomponents.core5 + httpcore5 + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java index 0f4dca8934..1c896199e3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.EnumSet; import java.util.NoSuchElementException; 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 054182479a..e1cc6b0894 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,7 +35,7 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.EnumSet; import java.util.NoSuchElementException; 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 index 571c8de122..a3c29f549d 100644 --- 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 @@ -16,19 +16,27 @@ package org.thingsboard.rule.engine.action; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @Data @AllArgsConstructor +@NoArgsConstructor +@Builder public class TbAlarmResult { + boolean isCreated; boolean isUpdated; boolean isSeverityUpdated; boolean isCleared; Alarm alarm; + Long conditionRepeats; + Long conditionDuration; + public TbAlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { this.isCreated = isCreated; this.isUpdated = isUpdated; @@ -38,11 +46,13 @@ public class TbAlarmResult { public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) { boolean isSeverityChanged = result.isSeverityChanged(); - return new TbAlarmResult( - result.isCreated(), - result.isModified() && !isSeverityChanged, - isSeverityChanged, - result.isCleared(), - result.getAlarm()); + return TbAlarmResult.builder() + .isCreated(result.isCreated()) + .isUpdated(result.isModified() && !isSeverityChanged) + .isSeverityUpdated(isSeverityChanged) + .isCleared(result.isCleared()) + .alarm(result.getAlarm()) + .build(); } + } 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 4366d79e4e..ed024a3b2d 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 @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.action; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -30,17 +29,21 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.Futures.transform; +import static com.google.common.util.concurrent.Futures.transformAsync; + @RuleNode( type = ComponentType.ACTION, name = "clear alarm", relationTypes = {"Cleared", "False"}, configClazz = TbClearAlarmNodeConfiguration.class, nodeDescription = "Clear Alarm", - nodeDetails = - "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + - "Node output:\n" + - "If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'metadata' will contains 'isClearedAlarm' property. " + - "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + - "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", + nodeDetails = """ + Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field. + Node output: + If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'metadata' will contains 'isClearedAlarm' property. + Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. + Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.""", configDirective = "tbActionNodeClearAlarmConfig", icon = "notifications_off", docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/" @@ -54,22 +57,26 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { - String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg); - Alarm alarm; - if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { - alarm = ctx.getAlarmService().findAlarmById(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); + String alarmType = TbNodeUtils.processPattern(config.getAlarmType(), msg); + + ListenableFuture alarmFuture; + if (msg.getOriginator().getEntityType() == EntityType.ALARM) { + alarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); } else { - alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); - } - if (alarm != null && !alarm.getStatus().isCleared()) { - return clearAlarm(ctx, msg, alarm); + alarmFuture = ctx.getAlarmService().findLatestActiveByOriginatorAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), alarmType); } - return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); + + return transformAsync(alarmFuture, alarm -> { + if (alarm != null && !alarm.getStatus().isCleared()) { + return clearAlarmAsync(ctx, msg, alarm); + } + return immediateFuture(new TbAlarmResult(false, false, false, null)); + }, ctx.getDbCallbackExecutor()); } - private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + private ListenableFuture clearAlarmAsync(TbContext ctx, TbMsg msg, Alarm alarm) { ListenableFuture asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); - return Futures.transform(asyncDetails, details -> { + return transform(asyncDetails, details -> { AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); if (result.isSuccessful()) { return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); 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 1649b6b842..8df155aa78 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 @@ -16,21 +16,16 @@ package org.thingsboard.rule.engine.action; 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.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; -import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -50,8 +45,6 @@ import org.thingsboard.server.dao.nosql.TbResultSetFuture; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.common.util.DonAsynchron.withCallback; @@ -85,7 +78,6 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { private CassandraCluster cassandraCluster; private ConsistencyLevel defaultWriteLevel; private PreparedStatement saveStmt; - private ExecutorService readResultsProcessingExecutor; private Map fieldsMap; @Override @@ -98,31 +90,19 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (!isTableExists()) { throw new TbNodeException("Table '" + TABLE_PREFIX + config.getTableName() + "' does not exist in Cassandra cluster."); } - startExecutor(); saveStmt = getSaveStmt(); } @Override public void onMsg(TbContext ctx, TbMsg msg) { - withCallback(save(msg, ctx), aVoid -> ctx.tellSuccess(msg), e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); + withCallback(save(msg, ctx), success -> ctx.tellSuccess(msg), e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); } @Override public void destroy() { - stopExecutor(); saveStmt = null; } - private void startExecutor() { - readResultsProcessingExecutor = Executors.newCachedThreadPool(); - } - - private void stopExecutor() { - if (readResultsProcessingExecutor != null) { - readResultsProcessingExecutor.shutdownNow(); - } - } - private boolean isTableExists() { var keyspaceMdOpt = getSession().getMetadata().getKeyspace(cassandraCluster.getKeyspaceName()); return keyspaceMdOpt.map(keyspaceMetadata -> @@ -183,7 +163,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { return query.toString(); } - private ListenableFuture save(TbMsg msg, TbContext ctx) { + private TbResultSetFuture save(TbMsg msg, TbContext ctx) { JsonElement data = JsonParser.parseString(msg.getData()); if (!data.isJsonObject()) { throw new IllegalStateException("Invalid message structure, it is not a JSON Object: " + data); @@ -224,7 +204,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (config.getDefaultTtl() > 0) { stmtBuilder.setInt(i.get(), config.getDefaultTtl()); } - return getFuture(executeAsyncWrite(ctx, stmtBuilder.build()), rs -> null); + return executeAsyncWrite(ctx, stmtBuilder.build()); } } @@ -254,16 +234,6 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { } } - private ListenableFuture getFuture(TbResultSetFuture future, java.util.function.Function transformer) { - return Futures.transform(future, new Function() { - @Nullable - @Override - public T apply(@Nullable AsyncResultSet input) { - return transformer.apply(input); - } - }, readResultsProcessingExecutor); - } - @Override public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { boolean hasChanges = false; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java index 3295c338b1..4b2a4776fb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java @@ -24,12 +24,10 @@ import java.util.Map; @Data public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfiguration { - private String tableName; private Map fieldsMapping; private int defaultTtl; - @Override public TbSaveToCustomCassandraTableNodeConfiguration defaultConfiguration() { TbSaveToCustomCassandraTableNodeConfiguration configuration = new TbSaveToCustomCassandraTableNodeConfiguration(); @@ -40,4 +38,5 @@ public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfig configuration.setFieldsMapping(map); return configuration; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index bff72beb9b..7495276723 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -54,7 +54,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.TbResourceDataCache; import java.nio.charset.StandardCharsets; @@ -92,7 +92,6 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie configClazz = TbAiNodeConfiguration.class, configDirective = "tbExternalNodeAiConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDkiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OSA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOC42MzExIDE3LjA3OTVDNDAuMTcwNSAxNy4wNzk2IDQxLjY1MTggMTcuNjg3MiA0Mi43NDc4IDE4Ljc3NjNDNDMuODQ0OCAxOS44NjYzIDQ0LjQ2NTkgMjEuMzUwMSA0NC40NjU5IDIyLjkwMjlWMzUuNDY1MkM0NC40NjU5IDM2LjM1MDkgNDQuMzU2NyAzNy4wNzY5IDQ0LjA5NzMgMzcuNzUxN0M0My44NDE0IDM4LjQxNjcgNDMuNDY1MSAzOC45NjE0IDQzLjA0NDggMzkuNTAyOEM0Mi40NjY3IDQwLjI0NzIgNDEuNjU2MyA0MC42ODU5IDQwLjg5MTkgNDAuOTM4OEM0MC4xMjExIDQxLjE5MzcgMzkuMzE0MyA0MS4yODg1IDM4LjYzMTEgNDEuMjg4NUgzMS4wMjU5TDIzLjM4MTIgNDUuODQ2NEMyMy4wNDMxIDQ2LjA0NzggMjIuNjI0MSA0Ni4wNTA3IDIyLjI4MzkgNDUuODUyOUMyMS45NDM3IDQ1LjY1NDcgMjEuNzMzOCA0NS4yODU5IDIxLjczMzcgNDQuODg3MlY0MS4yODg1SDE5LjY2NjNDMTguMTI2OSA0MS4yODg0IDE2LjY0NTUgNDAuNjgwOSAxNS41NDk2IDM5LjU5MThDMTQuNDUyNyAzOC41MDE5IDEzLjgzMTUgMzcuMDE3OSAxMy44MzE1IDM1LjQ2NTJWMjIuOTAyOUMxMy44MzE1IDIyLjMyMDIgMTMuOTE4NSAyMS43NDY4IDE0LjA4NTggMjEuMjAwN0wxNi4yODg5IDIxLjgxMDFMMTcuMjA5OSAyNS4yNTAyQzE3Ljk0MTYgMjcuOTg0NSAyMS43NTYyIDI3Ljk4NDQgMjIuNDg4IDI1LjI1MDJMMjMuNDA3OSAyMS44MTAxTDI2Ljc5MTcgMjAuODc0OUMyOC41NzkxIDIwLjM4MDUgMjkuMTc3IDE4LjUwMjYgMjguNTg4OCAxNy4wNzk1SDM4LjYzMTFaTTIyLjU4NDIgMzEuNTM5NUMyMS45OCAzMS41Mzk3IDIxLjQ5MDEgMzIuMDM3NiAyMS40OTAxIDMyLjY1MTlDMjEuNDkwMiAzMy4yNjYgMjEuOTgwMSAzMy43NjQgMjIuNTg0MiAzMy43NjQySDM0LjYxOTFDMzUuMjIzMyAzMy43NjQyIDM1LjcxMzEgMzMuMjY2MSAzNS43MTMyIDMyLjY1MTlDMzUuNzEzMiAzMi4wMzc1IDM1LjIyMzQgMzEuNTM5NSAzNC42MTkxIDMxLjUzOTVIMjIuNTg0MlpNMjQuNzcyMyAyNC44NjU3QzI0LjE2ODIgMjQuODY1OCAyMy42NzgzIDI1LjM2MzggMjMuNjc4MyAyNS45NzhDMjMuNjc4NCAyNi41OTIyIDI0LjE2ODMgMjcuMDkwMiAyNC43NzIzIDI3LjA5MDNIMzcuOTAxNEMzOC41MDU1IDI3LjA5MDMgMzguOTk1MyAyNi41OTIyIDM4Ljk5NTQgMjUuOTc4QzM4Ljk5NTQgMjUuMzYzNyAzOC41MDU2IDI0Ljg2NTcgMzcuOTAxNCAyNC44NjU3SDI0Ljc3MjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0xOC43ODkxIDExLjI5NzVDMTkuMDY5MSAxMC4xODA4IDIwLjYyOTkgMTAuMTgwOCAyMC45MDk5IDExLjI5NzVMMjEuOTE0MyAxNS4zMDM2QzIyLjAxMTYgMTUuNjkxOCAyMi4zMDY1IDE1Ljk5NzggMjIuNjg2NyAxNi4xMDNMMjYuMzYxMSAxNy4xMTg3QzI3LjQzNyAxNy40MTYyIDI3LjQzNyAxOC45Njc2IDI2LjM2MTEgMTkuMjY1MUwyMi42NzYxIDIwLjI4NEMyMi4zMDE4IDIwLjM4NzQgMjIuMDA4NyAyMC42ODQ1IDIxLjkwNjggMjEuMDY1TDIwLjkwNDYgMjQuODEyNUMyMC42MTE3IDI1LjkwNTggMTkuMDg2MSAyNS45MDU5IDE4Ljc5MzMgMjQuODEyNUwxNy43OTExIDIxLjA2NUMxNy42ODkzIDIwLjY4NDcgMTcuMzk3IDIwLjM4NzUgMTcuMDIyOSAyMC4yODRMMTMuMzM2OCAxOS4yNjUxQzEyLjI2MTQgMTguOTY3MyAxMi4yNjE1IDE3LjQxNjUgMTMuMzM2OCAxNy4xMTg3TDE3LjAxMTIgMTYuMTAzQzE3LjM5MTYgMTUuOTk3OCAxNy42ODc0IDE1LjY5MTkgMTcuNzg0NyAxNS4zMDM2TDE4Ljc4OTEgMTEuMjk3NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPHBhdGggZD0iTTEwLjAzNDMgNy4wMjQyNUMxMC4zMDY4IDUuODk0NDQgMTEuODg2OCA1Ljg5NDQ0IDEyLjE1OTQgNy4wMjQyNUwxMi42OTg5IDkuMjYyOThDMTIuNzkyNyA5LjY1MTc0IDEzLjA4NTEgOS45NTg4NyAxMy40NjQgMTAuMDY3OUwxNS41NzczIDEwLjY3NTFDMTYuNjM5MyAxMC45ODAzIDE2LjYzOTMgMTIuNTEwOSAxNS41NzczIDEyLjgxNjFMMTMuNDUzMyAxMy40MjY1QzEzLjA4MDIgMTMuNTMzOCAxMi43OTA4IDEzLjgzMzkgMTIuNjkyNSAxNC4yMTUxTDEyLjE1NTEgMTYuMzA0QzExLjg3IDE3LjQxMTYgMTAuMzIzNiAxNy40MTE2IDEwLjAzODUgMTYuMzA0TDkuNTAwMDMgMTQuMjE1MUM5LjQwMTczIDEzLjgzMzkgOS4xMTIzNSAxMy41MzM3IDguNzM5MyAxMy40MjY1TDYuNjE1MjQgMTIuODE2MUM1LjU1Mzc4IDEyLjUxMDYgNS41NTM2NCAxMC45ODA0IDYuNjE1MjQgMTAuNjc1MUw4LjcyODYyIDEwLjA2NzlDOS4xMDc2IDkuOTU4OTggOS4zOTk3OCA5LjY1MTg0IDkuNDkzNjIgOS4yNjI5OEwxMC4wMzQzIDcuMDI0MjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0yNS45MDI4IDYuNzMzMTNDMjYuMTg3OCA1LjYyNTQxIDI3LjczNDMgNS42MjU0MSAyOC4wMTkzIDYuNzMzMTNMMjguMjAzMSA3LjQ0Njc5QzI4LjMwMyA3LjgzNDMxIDI4LjYwMDEgOC4xMzcwNSAyOC45ODA5IDguMjM5NzVMMjkuNTM0NCA4LjM4OTY1QzMwLjYxOTIgOC42ODIxMiAzMC42MTkzIDEwLjI0NjkgMjkuNTM0NCAxMC41MzkzTDI4Ljk2OTIgMTAuNjkxNEMyOC41OTQ0IDEwLjc5MjUgMjguMjk5OSAxMS4wODgzIDI4LjE5NTYgMTEuNDY4TDI4LjAxNTEgMTIuMTI4NUMyNy43MTc0IDEzLjIxMjggMjYuMjA0NyAxMy4yMTI4IDI1LjkwNyAxMi4xMjg1TDI1LjcyNTQgMTEuNDY4QzI1LjYyMTEgMTEuMDg4MiAyNS4zMjY4IDEwLjc5MjQgMjQuOTUxOCAxMC42OTE0TDI0LjM4NzcgMTAuNTM5M0MyMy4zMDI2IDEwLjI0NyAyMy4zMDI2IDguNjgxOTggMjQuMzg3NyA4LjM4OTY1TDI0Ljk0MDEgOC4yMzk3NUMyNS4zMjExIDguMTM3MDkgMjUuNjE5MSA3LjgzNDQ2IDI1LjcxOSA3LjQ0Njc5TDI1LjkwMjggNi43MzMxM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9zdmc+Cg==", - ruleChainTypes = RuleChainType.CORE, docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/" ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 13dfe74902..6ebf1a3d57 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.StringUtils; 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.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index be38d70e30..21b8ea1846 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -41,6 +41,9 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CLEAR; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CREATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_SEVERITY_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.CONNECT_EVENT; @@ -78,7 +81,7 @@ public abstract class AbstractTbMsgPushNode (active && eval(alarmRule.getCondition(), data)) ? + AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; + case DURATION -> evalDuration(data, active); + case REPEATING -> evalRepeating(data, active); + }; } private boolean isActive(DataSnapshot data, long eventTs) { @@ -600,4 +596,5 @@ class AlarmRuleState { 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 index 81897e4bc5..89cbf8b207 100644 --- 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 @@ -48,6 +48,7 @@ import java.util.function.BiFunction; @Data @Slf4j +@Deprecated class AlarmState { public static final String ERROR_MSG = "Failed to process alarm rule for Device [%s]: %s"; 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 index cf74563d0d..0e588fe6cb 100644 --- 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 @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +@Deprecated class DataSnapshot { private volatile boolean ready; 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 index c6c03a0269..7977d1e150 100644 --- 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 @@ -80,6 +80,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED; @Slf4j +@Deprecated class DeviceState { private final boolean persistState; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java index 41908963b7..367a19ecaa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.profile; +@Deprecated public interface DynamicPredicateValueCtx { EntityKeyValue getTenantValue(String key); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java index 50f649baa6..2a960e75c8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java @@ -29,6 +29,7 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; @Slf4j +@Deprecated public class DynamicPredicateValueCtxImpl implements DynamicPredicateValueCtx { private final TenantId tenantId; private CustomerId customerId; 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 index 724d96d87d..c1aab735ab 100644 --- 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 @@ -20,6 +20,7 @@ import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; @EqualsAndHashCode +@Deprecated class EntityKeyValue { @Getter 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 index 7430f8f0d8..3e3978518c 100644 --- 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 @@ -45,6 +45,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +@Deprecated class ProfileState { private DeviceProfile deviceProfile; 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 index 462f539381..d9847ec110 100644 --- 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 @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import java.util.Set; +@Deprecated class SnapshotUpdate { @Getter 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 index b56a4c314b..e9d0ace4a8 100644 --- 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 @@ -51,17 +51,18 @@ import java.util.concurrent.TimeUnit; @Slf4j @RuleNode( type = ComponentType.ACTION, - name = "device profile", + name = "device profile (deprecated)", customRelations = true, relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, version = 1, configClazz = TbDeviceProfileNodeConfiguration.class, - nodeDescription = "Process device messages based on device profile settings", + nodeDescription = "Process device messages based on device profile settings (deprecated)", nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. The output relation type is either " + - "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", + "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", configDirective = "tbActionNodeDeviceProfileConfig", docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/" ) +@Deprecated public class TbDeviceProfileNode implements TbNode { private TbDeviceProfileNodeConfiguration config; 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 index e58a5110ff..b01201820f 100644 --- 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 @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class TbDeviceProfileNodeConfiguration implements NodeConfiguration { private boolean persistAlarmRulesState; 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 index 7a6277ed27..35d8950c33 100644 --- 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 @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor +@Deprecated public class PersistedAlarmRuleState { private long lastEventTs; 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 index a0759763dd..ec5ba6fd6f 100644 --- 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 @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; import java.util.Map; @Data +@Deprecated public class PersistedAlarmState { private Map createRuleStates; 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 index 974aad1f25..58dbf44c0f 100644 --- 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 @@ -20,6 +20,7 @@ import lombok.Data; import java.util.Map; @Data +@Deprecated 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 de1e73cb66..8e017e21f3 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 @@ -21,6 +21,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.timeout.ReadTimeoutHandler; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.hc.core5.net.URIBuilder; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -41,6 +42,7 @@ import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.util.KeyValueEntry; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import reactor.core.scheduler.Schedulers; @@ -50,10 +52,12 @@ import reactor.netty.transport.ProxyProvider; import javax.net.ssl.SSLException; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -166,10 +170,6 @@ public class TbHttpClient { SslContext sslContext = config.getCredentials().initSslContext(); httpClient = httpClient.secure(t -> t.sslContext(sslContext)); } - } else if (config.isUseSimpleClientHttpFactory()) { - if (CredentialsType.CERT_PEM == config.getCredentials().getType()) { - throw new TbNodeException("Simple HTTP Factory does not support CERT PEM credentials!"); - } } else { SslContext sslContext = config.getCredentials().initSslContext(); httpClient = httpClient.secure(t -> t.sslContext(sslContext)); @@ -223,10 +223,7 @@ public class TbHttpClient { } EventLoopGroup getSharedOrCreateEventLoopGroup(EventLoopGroup eventLoopGroupShared) { - if (eventLoopGroupShared != null) { - return eventLoopGroupShared; - } - return this.eventLoopGroup = new NioEventLoopGroup(); + return Objects.requireNonNullElseGet(eventLoopGroupShared, () -> this.eventLoopGroup = new NioEventLoopGroup()); } private void checkSystemProxyProperties() throws TbNodeException { @@ -341,8 +338,22 @@ public class TbHttpClient { boolean asyncStarted = false; try { String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), task.msg()); + + List> processedQueryParams; + if (config.getQueryParams() != null) { + processedQueryParams = config.getQueryParams().stream() + .map(param -> { + var processedParamName = TbNodeUtils.processPattern(param.key(), task.msg()); + var processedParamValue = TbNodeUtils.processPattern(param.value(), task.msg()); + return new KeyValueEntry<>(processedParamName, processedParamValue); + }) + .toList(); + } else { + processedQueryParams = null; + } + HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); - URI uri = buildEncodedUri(endpointUrl); + URI uri = buildEncodedUri(endpointUrl, processedQueryParams); RequestBodySpec req = webClient .method(method) @@ -352,7 +363,7 @@ public class TbHttpClient { if ((HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) || HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method)) && !config.isIgnoreRequestBody()) { - req.body(BodyInserters.fromValue(getData(task.msg(), config.isParseToPlainText()))); + req.body(BodyInserters.fromValue(getRequestBody(task.msg()))); } req.retrieve() @@ -398,13 +409,13 @@ public class TbHttpClient { if (origin instanceof WebClientResponseException restClientResponseException && restClientResponseException.getStatusCode().is2xxSuccessful()) { // return cause instead of original exception in case 2xx status code - // this will provide meaningful error message to the user + // this will provide a meaningful error message to the user return new RuntimeException(restClientResponseException.getCause()); } return origin; } - public URI buildEncodedUri(String endpointUrl) { + public URI buildEncodedUri(String endpointUrl, List> queryParams) { if (endpointUrl == null) { throw new RuntimeException("Url string cannot be null!"); } @@ -412,7 +423,13 @@ public class TbHttpClient { throw new RuntimeException("Url string cannot be empty!"); } - URI uri = UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri(); + URI uri; + if (queryParams != null) { + uri = buildEncodedUriNew(endpointUrl, queryParams); + } else { + uri = buildEncodedUriLegacy(endpointUrl); + } + if (uri.getScheme() == null || uri.getScheme().isEmpty()) { throw new RuntimeException("Transport scheme(protocol) must be provided!"); } @@ -428,6 +445,51 @@ public class TbHttpClient { return uri; } + private URI buildEncodedUriNew(String endpointUrl, List> queryParams) { + try { + URIBuilder builder = new URIBuilder(endpointUrl); + queryParams.forEach(param -> builder.addParameter(param.key(), param.value())); + return builder.build(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException( + "Invalid request URL: '" + endpointUrl + "'. The URL must be valid and properly encoded. " + + "If URL contains special characters (e.g., spaces), they must be percent-encoded (e.g., '/my device/' should be '/my%20device/').", e); + } + } + + /** + * @deprecated Query params embedded in the URL string are not encoded correctly. + *

+ * In query strings, {@code +} is interpreted as a space (URL form encoding), + * so {@code email=user+tag@test.com} becomes {@code email=user tag@test.com} on the server. + *

+ * Pre-encoding the URL (e.g., {@code email=user%2Btag@test.com}) doesn't help + * because {@code .encode()} will double-encode it to {@code email=user%252Btag@test.com}. + *

+ * Use {@link #buildEncodedUriNew} with a separate query params list, + * where values are encoded exactly once: {@code +} → {@code %2B}, {@code @} → {@code %40}. + */ + @Deprecated(forRemoval = true) + private URI buildEncodedUriLegacy(String endpointUrl) { + return UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri(); + } + + private Object getRequestBody(TbMsg msg) { + if (StringUtils.isNotBlank(config.getRequestBodyTemplate())) { + boolean escapeJson = !config.isParseToPlainText(); + String processedTemplate = TbNodeUtils.processPattern(config.getRequestBodyTemplate(), msg, escapeJson); + if (config.isParseToPlainText()) { + return processedTemplate; + } + try { + return JacksonUtil.toJsonNode(processedTemplate); + } catch (Exception e) { + throw new RuntimeException("Request body template produced invalid JSON: " + processedTemplate, e); + } + } + return getData(msg, config.isParseToPlainText()); + } + private Object getData(TbMsg tbMsg, boolean parseToPlainText) { String data = tbMsg.getData(); return parseToPlainText ? JacksonUtil.toPlainText(data) : JacksonUtil.toJsonNode(data); @@ -451,7 +513,7 @@ public class TbHttpClient { headers.forEach((key, values) -> { if (values != null && !values.isEmpty()) { if (values.size() == 1) { - consumer.accept(key, values.get(0)); + consumer.accept(key, values.getFirst()); } else { consumer.accept(key, JacksonUtil.toString(values)); } 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 02ed33138d..4120dd7ad8 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 @@ -27,17 +27,24 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.exception.DataValidationException; +import java.util.Iterator; import java.util.List; +import java.util.Set; + +import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; @RuleNode( type = ComponentType.EXTERNAL, name = "rest api call", configClazz = TbRestApiCallNodeConfiguration.class, - version = 3, + version = 4, nodeDescription = "Invoke REST API calls to external REST server", nodeDetails = "Will invoke REST API call GET | POST | PUT | DELETE to external REST server. " + - "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." + + "Message payload is used as the request body by default. " + + "Optionally, a request body template can be configured with ${metadataKey} and $[messageKey] placeholders. " + + "Configured attributes can be added into Headers from Message Metadata." + " Outbound message will contain response fields " + "(status, statusCode, statusReason and response headers) in the Message Metadata." + " Response body saved in outbound Message payload. " + @@ -52,13 +59,23 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { static final String PARSE_TO_PLAIN_TEXT = "parseToPlainText"; static final String MAX_IN_MEMORY_BUFFER_SIZE_IN_KB = "maxInMemoryBufferSizeInKb"; + static final String REQUEST_BODY_TEMPLATE = "requestBodyTemplate"; static final String TRIM_DOUBLE_QUOTES = "trimDoubleQuotes"; protected TbHttpClient httpClient; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); + TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); + + String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; + try { + validateFields(config, errorPrefix); + } catch (DataValidationException e) { + throw new TbNodeException(e, true); + } + TbHttpClientSettings httpClientSettings = ctx.getTbHttpClientSettings(); httpClient = new TbHttpClient(config, ctx.getSharedEventLoop(), ctx.getTenantId() != null ? ctx.getTenantId().getId().toString() : "n/a", @@ -101,6 +118,25 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { hasChanges = true; ((ObjectNode) oldConfiguration).put(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB, 256); } + case 3: + if (!oldConfiguration.has(REQUEST_BODY_TEMPLATE)) { + hasChanges = true; + ((ObjectNode) oldConfiguration).putNull(REQUEST_BODY_TEMPLATE); + } + Set knownProperties = Set.of( + "restEndpointUrlPattern", "requestMethod", "headers", + "readTimeoutMs", "maxParallelRequestsCount", "parseToPlainText", + "enableProxy", "useSystemProxyProperties", "proxyHost", "proxyPort", + "proxyUser", "proxyPassword", "credentials", "ignoreRequestBody", + "requestBodyTemplate", "maxInMemoryBufferSizeInKb" + ); + Iterator fieldNames = oldConfiguration.fieldNames(); + while (fieldNames.hasNext()) { + if (!knownProperties.contains(fieldNames.next())) { + hasChanges = true; + fieldNames.remove(); + } + } break; default: break; 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 8413c49c7d..0994ec19e3 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,25 +15,31 @@ */ package org.thingsboard.rule.engine.rest; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.credentials.AnonymousCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; +import org.thingsboard.server.common.data.util.KeyValueEntry; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Objects; -@JsonIgnoreProperties(ignoreUnknown = true) @Data public class TbRestApiCallNodeConfiguration implements NodeConfiguration { private String restEndpointUrlPattern; private String requestMethod; + // null for legacy configs - triggers different URL encoding logic in TbHttpClient to preserve backward compatibility + // all new/modified configs since introduction of this property are forced to have non-null value by UI (and use new encoding logic) + private List<@NotNull KeyValueEntry> queryParams; private Map headers; - private boolean useSimpleClientHttpFactory; private int readTimeoutMs; private int maxParallelRequestsCount; private boolean parseToPlainText; @@ -43,18 +49,32 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParam : queryParams) { + if (queryParam == null || queryParam.key() == null || queryParam.value() == null) { + return false; + } + } + return true; + } + @Override public TbRestApiCallNodeConfiguration defaultConfiguration() { - TbRestApiCallNodeConfiguration configuration = new TbRestApiCallNodeConfiguration(); + var configuration = new TbRestApiCallNodeConfiguration(); configuration.setRestEndpointUrlPattern("http://localhost/api"); configuration.setRequestMethod("POST"); configuration.setHeaders(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); - configuration.setUseSimpleClientHttpFactory(false); + configuration.setQueryParams(Collections.emptyList()); configuration.setReadTimeoutMs(0); configuration.setMaxParallelRequestsCount(0); configuration.setParseToPlainText(false); @@ -66,10 +86,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfigurationsave attributes and save time series nodes. " + + nodeDescription = "Pushes incoming messages to calculated fields and alarm rules services", + nodeDetails = "Node enables the processing of calculated fields and alarm rules without persisting incoming messages to the database. " + + "By default, the processing of calculated fields and alarm rules is triggered by the save attributes and save time series nodes. " + "This rule node accepts the same messages as these nodes but allows you to trigger the processing of calculated " + - "fields independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", + "fields or alarm rules independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", configDirective = "tbNodeEmptyConfig", icon = "published_with_changes", docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/" @@ -101,11 +102,15 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.tellSuccess(msg); return; } + AttributeScope scope = resolveScope(ctx, msg); + if (scope == null) { + return; + } AttributesSaveRequest attributesSaveRequest = AttributesSaveRequest.builder() .tenantId(ctx.getTenantId()) .entityId(msg.getOriginator()) - .scope(AttributeScope.valueOf(msg.getMetaData().getValue(SCOPE))) + .scope(scope) .entries(newAttributes) .strategy(AttributesSaveRequest.Strategy.CF_ONLY) .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) @@ -116,4 +121,20 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.getTelemetryService().saveAttributes(attributesSaveRequest); } + private AttributeScope resolveScope(TbContext ctx, TbMsg msg) { + String scopeStr = msg.getMetaData().getValue(SCOPE); + + if (StringUtils.isEmpty(scopeStr)) { + ctx.tellFailure(msg, new IllegalArgumentException("Attribute scope is missing")); + return null; + } + + try { + return AttributeScope.valueOf(scopeStr); + } catch (IllegalArgumentException e) { + ctx.tellFailure(msg, new IllegalArgumentException("Invalid attribute scope: " + scopeStr)); + return 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 b9ed731276..3a882ea317 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 @@ -41,10 +41,7 @@ import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.UUID; -import java.util.function.Function; import java.util.stream.Collectors; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.Advanced; @@ -54,6 +51,7 @@ import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessin import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.dao.util.KvUtils.filterChangedAttr; @RuleNode( type = ComponentType.ACTION, @@ -216,24 +214,6 @@ public class TbMsgAttributesNode implements TbNode { .build()); } - private List filterChangedAttr(List currentAttributes, List newAttributes) { - if (currentAttributes == null || currentAttributes.isEmpty()) { - return newAttributes; - } - - Map currentAttrMap = currentAttributes.stream() - .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); - - return newAttributes.stream() - .filter(item -> { - AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); - return cacheAttr == null - || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type - || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); - }) - .collect(Collectors.toList()); - } - private boolean checkSendNotification(AttributeScope scope) { return config.isSendAttributesUpdatedNotification() && AttributeScope.CLIENT_SCOPE != scope; } 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 8621236735..c707a1372d 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 @@ -31,7 +31,6 @@ import org.thingsboard.rule.engine.telemetry.strategy.ProcessingStrategy; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -39,7 +38,6 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; @@ -50,6 +48,7 @@ import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessin import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.OnEveryMessage; import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.WebSocketsOnly; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; +import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList; @RuleNode( type = ComponentType.ACTION, @@ -148,12 +147,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src)); return; } - List tsKvEntryList = new ArrayList<>(); - for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { - for (KvEntry kvEntry : tsKvEntry.getValue()) { - tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); - } - } + List tsKvEntryList = toTsKvEntryList(tsKvMap); String ttlValue = msg.getMetaData().getValue("TTL"); long ttl = !StringUtils.isEmpty(ttlValue) ? Long.parseLong(ttlValue) : config.getDefaultTTL(); if (ttl == 0L) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index ae83a41856..74d2c7ffb1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -18,14 +18,13 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -170,20 +169,15 @@ public class TenantIdLoader { case CALCULATED_FIELD: tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, new CalculatedFieldId(id)); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = ctx.getCalculatedFieldService().findCalculatedFieldLinkById(ctxTenantId, new CalculatedFieldLinkId(id)); - if (calculatedFieldLink != null) { - tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, calculatedFieldLink.getCalculatedFieldId()); - } else { - tenantEntity = null; - } - break; case JOB: tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); break; case AI_MODEL: tenantEntity = ctx.getAiModelService().findAiModelById(ctxTenantId, new AiModelId(id)).orElse(null); break; + case API_KEY: + tenantEntity = ctx.getApiKeyService().findApiKeyById(ctxTenantId, new ApiKeyId(id)); + break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java index 7d7bff22d5..b55cf63fab 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.FluentFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.function.Consumer; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -112,8 +113,8 @@ class TbClearAlarmNodeTest { .endTs(oldEndDate) .build(); - when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, "SomeType")).thenReturn(activeAlarm); + when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(immediateFuture(null)); + when(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, "SomeType")).thenReturn(FluentFuture.from(immediateFuture(activeAlarm))); when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) .thenReturn(AlarmApiCallResult.builder() .successful(true) @@ -172,8 +173,8 @@ class TbClearAlarmNodeTest { .build(); expectedAlarm.setId(id); - when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmServiceMock.findAlarmById(tenantId, id)).thenReturn(activeAlarm); + when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(immediateFuture(null)); + when(alarmServiceMock.findAlarmByIdAsync(tenantId, id)).thenReturn(immediateFuture(activeAlarm)); when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) .thenReturn(AlarmApiCallResult.builder() .successful(true) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java index a8f2f9b021..e8aad50463 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java @@ -67,7 +67,7 @@ import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.ai.AiModelService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.TbResourceDataCache; 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 index fd8c931a5e..ae45d83684 100644 --- 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 @@ -264,7 +264,6 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { node.onMsg(ctx, msg2); verify(ctx).tellSuccess(msg2); verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated"); - } @Test diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java index 8b31c5a5ac..e8cbdb52b6 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -15,41 +15,49 @@ */ package org.thingsboard.rule.engine.rest; - import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.ResourceLock; -import org.mockito.ArgumentCaptor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; -import org.mockserver.integration.ClientAndServer; import org.springframework.util.LinkedMultiValueMap; import org.thingsboard.common.util.DirectListeningExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.util.KeyValueEntry; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import java.net.URI; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockserver.integration.ClientAndServer.startClientAndServer; @@ -89,124 +97,462 @@ public class TbHttpClientTest { @Test public void testBuildSimpleUri() { - Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod(); + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); String url = "http://localhost:8080/"; - URI uri = client.buildEncodedUri(url); + URI uri = client.buildEncodedUri(url, null); Assertions.assertEquals(url, uri.toString()); } @Test public void testBuildUriWithoutProtocol() { - Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod(); + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); String url = "localhost:8080/"; - assertThatThrownBy(() -> client.buildEncodedUri(url)); + assertThatThrownBy(() -> client.buildEncodedUri(url, null)); } @Test public void testBuildInvalidUri() { - Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod(); + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); String url = "aaa"; - assertThatThrownBy(() -> client.buildEncodedUri(url)); + assertThatThrownBy(() -> client.buildEncodedUri(url, null)); } @Test public void testBuildUriWithSpecialSymbols() { - Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod(); + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); String url = "http://192.168.1.1/data?d={\"a\": 12}"; String expected = "http://192.168.1.1/data?d=%7B%22a%22:%2012%7D"; - URI uri = client.buildEncodedUri(url); + URI uri = client.buildEncodedUri(url, null); Assertions.assertEquals(expected, uri.toString()); } + @Test + public void testProcessMessageWithQueryParamsPatternProcessing() throws Exception { + // GIVEN + String path = "/api/notify"; + try (var server = startClientAndServer("localhost", 0)) { + server.when( + request() + .withMethod("GET") + .withPath(path) + .withQueryStringParameter("email", "user+tag@test.com") + .withQueryStringParameter("device", "sensor-1") + .withQueryStringParameter("custom-header", "header-value") + .withQueryStringParameter("temp", "25.5") + .withQueryStringParameter("location", "room-1/zone-A") + ).respond( + response().withStatusCode(200) + ); + + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRestEndpointUrlPattern("http://localhost:" + server.getPort() + path); + config.setRequestMethod("GET"); + config.setQueryParams(List.of( + new KeyValueEntry<>("email", "${userEmail}"), // ${} from metadata + new KeyValueEntry<>("device", "${deviceName}"), // ${} from metadata + new KeyValueEntry<>("${dynamicParam}", "${dynamicValue}"), // ${} in both key and value + new KeyValueEntry<>("temp", "$[temperature]"), // $[] from data + new KeyValueEntry<>("location", "$[sensor.location]") // $[] from nested data + )); + + var metaData = new TbMsgMetaData(); + metaData.putValue("userEmail", "user+tag@test.com"); + metaData.putValue("deviceName", "sensor-1"); + metaData.putValue("dynamicParam", "custom-header"); + metaData.putValue("dynamicValue", "header-value"); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(new DeviceId(EntityId.NULL_UUID)) + .metaData(metaData) + .data(""" + {"temperature": 25.5, "sensor": {"location": "room-1/zone-A"}} + """) + .build(); + + // WHEN-THEN + processMessageAndWait(config, msg); + } + } + @Test public void testProcessMessageWithJsonInUrlVariable() throws Exception { - String host = "localhost"; + // GIVEN String path = "/api"; - String paramKey = "data"; - String paramVal = "[{\"test\":\"test\"}]"; - String successResponseBody = "SUCCESS"; - - var server = setUpDummyServer(host, path, paramKey, paramVal, successResponseBody); - - String endpointUrl = String.format( - "http://%s:%d%s?%s=%s", - host, server.getPort(), path, paramKey, paramVal - ); - String method = "GET"; - - - var config = new TbRestApiCallNodeConfiguration() - .defaultConfiguration(); - config.setRequestMethod(method); - config.setRestEndpointUrlPattern(endpointUrl); - config.setUseSimpleClientHttpFactory(true); + String paramValue = "[{\"test\":\"test\"}]"; + + try (var server = startClientAndServer("localhost", 0)) { + server.when( + request() + .withMethod("GET") + .withPath(path) + .withQueryStringParameter("data", paramValue) + ).respond( + response().withStatusCode(200) + ); + + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRestEndpointUrlPattern("http://localhost:" + server.getPort() + path + "?data=" + paramValue); + config.setRequestMethod("GET"); + config.setQueryParams(null); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(new DeviceId(UUID.randomUUID())) + .metaData(new TbMsgMetaData()) + .data(TbMsg.EMPTY_JSON_OBJECT) + .build(); + + // WHEN-THEN + processMessageAndWait(config, msg); + } + } + private void processMessageAndWait(TbRestApiCallNodeConfiguration config, TbMsg msg) throws Exception { var httpClient = new TbHttpClient(config, eventLoop); - - var msg = TbMsg.newMsg() - .type(TbMsgType.POST_TELEMETRY_REQUEST) - .originator(new DeviceId(EntityId.NULL_UUID)) - .copyMetaData(TbMsgMetaData.EMPTY) - .data(TbMsg.EMPTY_JSON_OBJECT) - .build(); - var successMsg = TbMsg.newMsg() - .type(TbMsgType.POST_TELEMETRY_REQUEST) - .originator(msg.getOriginator()) - .copyMetaData(msg.getMetaData()) - .data(msg.getData()) - .build(); - var ctx = mock(TbContext.class); when(ctx.getExternalCallExecutor()).thenReturn(DirectListeningExecutor.INSTANCE); - when(ctx.transformMsg( - eq(msg), - eq(msg.getMetaData()), - eq(msg.getData()) - )).thenReturn(successMsg); - - var capturedData = ArgumentCaptor.forClass(String.class); + when(ctx.transformMsg(eq(msg), any(), any())).thenReturn(msg); - when(ctx.transformMsg( - eq(msg), - any(), - capturedData.capture() - )).thenReturn(successMsg); - - CountDownLatch latch = new CountDownLatch(1); + var latch = new CountDownLatch(1); + var error = new AtomicReference(); httpClient.processMessage(ctx, msg, m -> { - ctx.tellSuccess(msg); + ctx.tellSuccess(m); latch.countDown(); }, (m, t) -> { ctx.tellFailure(m, t); + error.set(t); latch.countDown(); }); - latch.await(5, TimeUnit.SECONDS); + assertTrue(latch.await(5, TimeUnit.SECONDS), "Request should complete within timeout"); + assertNull(error.get(), "Request should succeed, but got: " + error.get()); - verify(ctx, times(1)).tellSuccess(any()); - verify(ctx, times(0)).tellFailure(any(), any()); - Assertions.assertEquals(successResponseBody, capturedData.getValue()); + verify(ctx).tellSuccess(any()); + verify(ctx, never()).tellFailure(any(), any()); } - private ClientAndServer setUpDummyServer(String host, String path, String paramKey, String paramVal, String successResponseBody) { - var server = startClientAndServer(host, 1080); - createGetMethodExpectations(server, path, paramKey, paramVal, successResponseBody); - return server; + + @ParameterizedTest(name = "{0}") + @MethodSource + public void testQueryParamsEncoding(String endpointUrl, List> queryParams, String expectedEncodedUrl) { + // GIVEN + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); + + // WHEN + URI uri = client.buildEncodedUri(endpointUrl, queryParams); + + // THEN + Assertions.assertEquals(expectedEncodedUrl, uri.toASCIIString()); } - private void createGetMethodExpectations(ClientAndServer server, String path, String paramKey, String paramVal, String successResponseBody) { - server.when( - request() - .withMethod("GET") - .withPath(path) - .withQueryStringParameter(paramKey, paramVal) - ).respond( - response() - .withStatusCode(200) - .withBody(successResponseBody) + private static Stream testQueryParamsEncoding() { + return Stream.of( + Arguments.of( + Named.named("ISO 8601 date-time in value", "http://somecompany/api/data/fetch"), + List.of(new KeyValueEntry<>("ts", "2016-08-01T09:06:06.0+02:00")), + "http://somecompany/api/data/fetch?ts=2016-08-01T09%3A06%3A06.0%2B02%3A00" + ), + Arguments.of( + Named.named("email with plus sign in value", "http://localhost:8080/api/user/sendActivationMail"), + List.of(new KeyValueEntry<>("email", "someperson+test1289@thingsboard.io")), + "http://localhost:8080/api/user/sendActivationMail?email=someperson%2Btest1289%40thingsboard.io" + ), + Arguments.of( + Named.named("plus mixed with spaces in value", "http://url/api"), + List.of(new KeyValueEntry<>("q", "a + b")), + "http://url/api?q=a%20%2B%20b" + ), + Arguments.of( + Named.named("colon in value", "http://url/api"), + List.of(new KeyValueEntry<>("time", "12:00")), + "http://url/api?time=12%3A00" + ), + Arguments.of( + Named.named("slash in value", "http://url"), + List.of(new KeyValueEntry<>("ref", "/home/user")), + "http://url?ref=%2Fhome%2Fuser" + ), + Arguments.of( + Named.named("comma and semicolon in value", "http://url"), + List.of(new KeyValueEntry<>("l", "a,b;c")), + "http://url?l=a%2Cb%3Bc" + ), + Arguments.of( + Named.named("ampersand and equals in value", "http://url"), + List.of(new KeyValueEntry<>("q", "key1=value1&key2=value2")), + "http://url?q=key1%3Dvalue1%26key2%3Dvalue2" + ), + Arguments.of( + Named.named("JSON in value", "http://url"), + List.of(new KeyValueEntry<>("json", """ + { + "string": "hello", + "integer": 42, + "float": 3.14, + "boolTrue": true, + "boolFalse": false, + "null": null, + "array": [ + 1, + "two", + true, + null + ], + "object": { + "nested": "value" + } + }""")), + "http://url?json=%7B%0A%20%20%20%20%22string%22%3A%20%22hello%22%2C%0A%20%20%20%20%22integer%22%3A%2042%2C%0A%20%20%20%20%22float%22%3A%203.14%2C%0A%20%20%20%20%22boolTrue%22%3A%20true%2C%0A%20%20%20%20%22boolFalse%22%3A%20false%2C%0A%20%20%20%20%22null%22%3A%20null%2C%0A%20%20%20%20%22array%22%3A%20%5B%0A%20%20%20%20%20%20%20%201%2C%0A%20%20%20%20%20%20%20%20%22two%22%2C%0A%20%20%20%20%20%20%20%20true%2C%0A%20%20%20%20%20%20%20%20null%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22object%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22nested%22%3A%20%22value%22%0A%20%20%20%20%7D%0A%7D" + ), + Arguments.of( + Named.named("UTF-8 in query", "http://url/cafes"), + List.of( + new KeyValueEntry<>("nom", "Le Goût Moderne"), + new KeyValueEntry<>("назва", "У Миколи \uD83D\uDE0B") + ), + "http://url/cafes?nom=Le%20Go%C3%BBt%20Moderne&%D0%BD%D0%B0%D0%B7%D0%B2%D0%B0=%D0%A3%20%D0%9C%D0%B8%D0%BA%D0%BE%D0%BB%D0%B8%20%F0%9F%98%8B" + ), + Arguments.of( + Named.named("empty value", "http://url/empty"), + List.of(new KeyValueEntry<>("name", "")), + "http://url/empty?name=" + ), + Arguments.of( + Named.named("blank value (spaces)", "http://url/empty"), + List.of(new KeyValueEntry<>("name", " ")), + "http://url/empty?name=%20%20" + ), + Arguments.of( + Named.named("empty key", "http://url/empty"), + List.of(new KeyValueEntry<>("", "value")), + "http://url/empty?=value" + ), + Arguments.of( + Named.named("blank key (spaces)", "http://url/empty"), + List.of(new KeyValueEntry<>(" ", "value")), + "http://url/empty?%20%20=value" + ), + Arguments.of( + Named.named("blank key with value that needs to be encoded", "http://url/empty"), + List.of(new KeyValueEntry<>(" ", "value1+value2")), + "http://url/empty?%20%20=value1%2Bvalue2" + ), + Arguments.of( + Named.named("blank key and value", "http://url/empty"), + List.of(new KeyValueEntry<>(" ", " ")), + "http://url/empty?%20%20=%20%20" + ), + Arguments.of( + Named.named("fragment with query params", "http://url#frag"), + List.of(new KeyValueEntry<>("docs", "مستندات عقدة القاعدة\n")), + "http://url?docs=%D9%85%D8%B3%D8%AA%D9%86%D8%AF%D8%A7%D8%AA%20%D8%B9%D9%82%D8%AF%D8%A9%20%D8%A7%D9%84%D9%82%D8%A7%D8%B9%D8%AF%D8%A9%0A#frag" + ), + Arguments.of( + Named.named("fragment only", "http://url#frag"), + Collections.emptyList(), + "http://url#frag" + ), + Arguments.of( + Named.named("multiple query params (ordered)", "http://url/api"), + List.of( + new KeyValueEntry<>("param1", "value1"), + new KeyValueEntry<>("param2", "value2"), + new KeyValueEntry<>("param3", "value3") + ), + "http://url/api?param1=value1¶m2=value2¶m3=value3" + ), + Arguments.of( + Named.named("hash (#) in value", "http://url/api"), + List.of(new KeyValueEntry<>("color", "#ff0000")), + "http://url/api?color=%23ff0000" + ), + Arguments.of( + Named.named("question mark (?) in value", "http://url/api"), + List.of(new KeyValueEntry<>("query", "what?")), + "http://url/api?query=what%3F" + ), + Arguments.of( + Named.named("percent sign (%) in value", "http://url/api"), + List.of(new KeyValueEntry<>("discount", "50%")), + "http://url/api?discount=50%25" + ), + Arguments.of( + Named.named("already encoded string - double encoding", "http://url/api"), + List.of(new KeyValueEntry<>("encoded", "%20")), + "http://url/api?encoded=%2520" + ), + Arguments.of( + Named.named("URL with port number", "http://localhost:8080/api/v1/data"), + List.of(new KeyValueEntry<>("key", "value")), + "http://localhost:8080/api/v1/data?key=value" + ), + Arguments.of( + Named.named("URL with userinfo (auth)", "http://user:password@hostname/path"), + List.of(new KeyValueEntry<>("secure", "true")), + "http://user:password@hostname/path?secure=true" + ), + Arguments.of( + Named.named("IPv4 address in URL", "http://192.168.1.100:9090/endpoint"), + List.of(new KeyValueEntry<>("ip", "test")), + "http://192.168.1.100:9090/endpoint?ip=test" + ), + Arguments.of( + Named.named("IPv6 address in URL", "http://[::1]:8080/api"), + List.of(new KeyValueEntry<>("ipv6", "true")), + "http://[::1]:8080/api?ipv6=true" + ), + Arguments.of( + Named.named("HTTPS protocol", "https://secure.example.com/api"), + List.of(new KeyValueEntry<>("token", "abc123")), + "https://secure.example.com/api?token=abc123" + ), + Arguments.of( + Named.named("pipe (|) in value", "http://url/api"), + List.of(new KeyValueEntry<>("filter", "a|b|c")), + "http://url/api?filter=a%7Cb%7Cc" + ), + Arguments.of( + Named.named("caret (^) and backtick (`) in value", "http://url/api"), + List.of(new KeyValueEntry<>("special", "a^b`c")), + "http://url/api?special=a%5Eb%60c" + ), + Arguments.of( + Named.named("tab character in value", "http://url/api"), + List.of(new KeyValueEntry<>("data", "col1\tcol2\tcol3")), + "http://url/api?data=col1%09col2%09col3" + ), + Arguments.of( + Named.named("CRLF in value", "http://url/api"), + List.of(new KeyValueEntry<>("text", "line1\r\nline2")), + "http://url/api?text=line1%0D%0Aline2" + ), + Arguments.of( + Named.named("square brackets in value", "http://url/api"), + List.of(new KeyValueEntry<>("array", "[1,2,3]")), + "http://url/api?array=%5B1%2C2%2C3%5D" + ), + Arguments.of( + Named.named("single and double quotes in value", "http://url/api"), + List.of(new KeyValueEntry<>("quoted", "He said \"Hello\" and 'Hi'")), + "http://url/api?quoted=He%20said%20%22Hello%22%20and%20%27Hi%27" + ), + Arguments.of( + Named.named("angle brackets in value (XSS test)", "http://url/api"), + List.of(new KeyValueEntry<>("html", "")), + "http://url/api?html=%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E" + ), + Arguments.of( + Named.named("backslash in value", "http://url/api"), + List.of(new KeyValueEntry<>("path", "C:\\Users\\test")), + "http://url/api?path=C%3A%5CUsers%5Ctest" + ), + Arguments.of( + Named.named("at sign (@) in value", "http://url/api"), + List.of(new KeyValueEntry<>("contact", "user@domain.com")), + "http://url/api?contact=user%40domain.com" + ), + Arguments.of( + Named.named("URL as value", "http://url/api"), + List.of(new KeyValueEntry<>("redirect", "https://example.com/path?foo=bar")), + "http://url/api?redirect=https%3A%2F%2Fexample.com%2Fpath%3Ffoo%3Dbar" + ), + Arguments.of( + Named.named("empty list (not null)", "http://url/api"), + Collections.emptyList(), + "http://url/api" + ), + Arguments.of( + Named.named("underscore and hyphen in key/value", "http://url/api"), + List.of(new KeyValueEntry<>("my-key_name", "my-value_data")), + "http://url/api?my-key_name=my-value_data" + ), + Arguments.of( + Named.named("dots in key and value", "http://url/api"), + List.of(new KeyValueEntry<>("version.major", "1.2.3")), + "http://url/api?version.major=1.2.3" + ), + Arguments.of( + Named.named("multiple reserved chars combined", "http://url/api"), + List.of(new KeyValueEntry<>("complex", "a=1&b=2#section?query")), + "http://url/api?complex=a%3D1%26b%3D2%23section%3Fquery" + ), + Arguments.of( + Named.named("trailing slash in URL", "http://url/api/"), + List.of(new KeyValueEntry<>("param", "value")), + "http://url/api/?param=value" + ), + Arguments.of( + Named.named("long value (1000 chars)", "http://url/api"), + List.of(new KeyValueEntry<>("long", "a".repeat(1000))), + "http://url/api?long=" + "a".repeat(1000) + ), + Arguments.of( + Named.named("Chinese characters in value", "http://url/api"), + List.of(new KeyValueEntry<>("greeting", "你好世界")), + "http://url/api?greeting=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C" + ), + Arguments.of( + Named.named("Japanese characters in value", "http://url/api"), + List.of(new KeyValueEntry<>("text", "こんにちは")), + "http://url/api?text=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF" + ), + Arguments.of( + Named.named("existing query params in URL", "http://url/api?existing=param"), + List.of(new KeyValueEntry<>("new", "value")), + "http://url/api?existing=param&new=value" + ), + Arguments.of( + Named.named("existing query and fragment in URL", "http://url/api?existing=param#section"), + List.of(new KeyValueEntry<>("additional", "data")), + "http://url/api?existing=param&additional=data#section" + ), + Arguments.of( + Named.named("null byte in value", "http://url/api"), + List.of(new KeyValueEntry<>("data", "before\u0000after")), + "http://url/api?data=before%00after" + ), + Arguments.of( + Named.named("form feed and vertical tab in value", "http://url/api"), + List.of(new KeyValueEntry<>("whitespace", "a\fb\u000Bc")), + "http://url/api?whitespace=a%0Cb%0Bc" + ), + Arguments.of( + Named.named("tilde (unreserved, not encoded)", "http://url/api"), + List.of(new KeyValueEntry<>("pattern", "~user")), + "http://url/api?pattern=~user" + ), + Arguments.of( + Named.named("asterisk in value", "http://url/api"), + List.of(new KeyValueEntry<>("wildcard", "*.txt")), + "http://url/api?wildcard=%2A.txt" + ), + Arguments.of( + Named.named("curly braces in key", "http://url/api"), + List.of(new KeyValueEntry<>("{key}", "value")), + "http://url/api?%7Bkey%7D=value" + ), + Arguments.of( + Named.named("curly braces in value", "http://url/api"), + List.of(new KeyValueEntry<>("data", "{value}")), + "http://url/api?data=%7Bvalue%7D" + ), + Arguments.of( + Named.named("curly braces in both key and value", "http://url/api"), + List.of(new KeyValueEntry<>("{param}", "{data}")), + "http://url/api?%7Bparam%7D=%7Bdata%7D" + ), + Arguments.of( + Named.named("duplicate param names with different values", "http://url/api"), + List.of( + new KeyValueEntry<>("test", "a+b"), + new KeyValueEntry<>("test", "b c") + ), + "http://url/api?test=a%2Bb&test=b%20c" + ) ); } @@ -225,8 +571,8 @@ public class TbHttpClientTest { Map data = metaData.getData(); Assertions.assertEquals(2, data.size()); - Assertions.assertEquals(data.get("Content-Type"), "binary"); - Assertions.assertEquals(data.get("Set-Cookie"), "[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]"); + Assertions.assertEquals("binary", data.get("Content-Type")); + Assertions.assertEquals("[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]", data.get("Set-Cookie")); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index cc2daa2421..7d0e85b849 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -25,6 +25,7 @@ import org.apache.http.impl.bootstrap.ServerBootstrap; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.parallel.ResourceLock; @@ -49,23 +50,34 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.util.KeyValueEntry; 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.exception.DataValidationException; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -82,11 +94,11 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { @Mock private TbContext ctx; - private EntityId originator = new DeviceId(Uuids.timeBased()); - private TbMsgMetaData metaData = new TbMsgMetaData(); + private final EntityId originator = new DeviceId(Uuids.timeBased()); + private final TbMsgMetaData metaData = new TbMsgMetaData(); - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); private HttpServer server; @@ -109,6 +121,14 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { } } + @BeforeEach + public void setup() { + RuleNode ruleNode = new RuleNode(); + ruleNode.setId(ruleNodeId); + ruleNode.setName("Test REST API call node"); + lenient().when(ctx.getSelf()).thenReturn(ruleNode); + } + @AfterEach public void teardown() { if (server != null) { @@ -116,26 +136,113 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { } } + @Test + public void shouldNotAllowNullQueryParamNames() { + // GIVEN + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setQueryParams(List.of(new KeyValueEntry<>(null, "value"))); + + // WHEN-THEN + assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("query parameter names and values must be non-null"); + } + + @Test + public void shouldNotAllowNullQueryParamValues() { + // GIVEN + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setQueryParams(List.of(new KeyValueEntry<>("key", null))); + + // WHEN-THEN + assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("query parameter names and values must be non-null"); + } + + @Test + public void shouldNotAllowNullQueryParamEntries() { + // GIVEN + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + + var queryParams = new ArrayList>(); + queryParams.add(new KeyValueEntry<>("key", "value")); + queryParams.add(null); + config.setQueryParams(queryParams); + + // WHEN-THEN + assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("must not be null"); + } + + @Test + public void shouldUseNewEncodingForNewNodesByDefault() { + // GIVEN-WHEN + var defaultConfig = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + + // THEN + assertEquals(Collections.emptyList(), defaultConfig.getQueryParams()); + } + + @Test + public void shouldDefaultToOldEncodingForLegacyConfigs() { + // GIVEN + String configJson = """ + { + "restEndpointUrlPattern": "http://url?param=value", + "requestMethod": "GET", + "parseToPlainText": false, + "ignoreRequestBody": false, + "enableProxy": false, + "useSystemProxyProperties": false, + "proxyHost": null, + "proxyPort": 0, + "proxyUser": null, + "proxyPassword": null, + "readTimeoutMs": 0, + "maxParallelRequestsCount": 0, + "headers": { + "Content-Type": "application/json", + "X-Authorization": "Bearer eyJhbGciOi...." + }, + "credentials": { + "type": "anonymous" + }, + "maxInMemoryBufferSizeInKb": 256 + }"""; + + // WHEN + var config = JacksonUtil.fromString(configJson, TbRestApiCallNodeConfiguration.class); + + // THEN + assertNull(config.getQueryParams()); + } + @Test public void deleteRequestWithoutBody() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/path/to/delete"; - setupServer("*", new HttpRequestHandler() { - - @Override - public void handle(HttpRequest request, HttpResponse response, HttpContext context) - throws HttpException, IOException { - try { - assertEquals(request.getRequestLine().getUri(), path, "Request path matches"); - assertTrue(request.containsHeader("Foo"), "Custom header included"); - assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); - response.setStatusCode(200); - latch.countDown(); - } catch (Exception e) { - System.out.println("Exception handling request: " + e.toString()); - e.printStackTrace(); - latch.countDown(); - } + setupServer("*", (request, response, _) -> { + try { + assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); + assertTrue(request.containsHeader("Foo"), "Custom header included"); + assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); + response.setStatusCode(200); + latch.countDown(); + } catch (Exception e) { + System.out.println("Exception handling request: " + e); + e.printStackTrace(); + latch.countDown(); } }); @@ -174,28 +281,23 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { public void deleteRequestWithBody() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/path/to/delete"; - setupServer("*", new HttpRequestHandler() { - - @Override - public void handle(HttpRequest request, HttpResponse response, HttpContext context) - throws HttpException, IOException { - try { - assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); - assertTrue(request.containsHeader("Content-Type"), "Content-Type included"); - assertEquals("application/json", - request.getFirstHeader("Content-Type").getValue(), "Content-Type value"); - assertTrue(request.containsHeader("Content-Length"), "Content-Length included"); - assertEquals("2", - request.getFirstHeader("Content-Length").getValue(), "Content-Length value"); - assertTrue(request.containsHeader("Foo"), "Custom header included"); - assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); - response.setStatusCode(200); - latch.countDown(); - } catch (Exception e) { - System.out.println("Exception handling request: " + e.toString()); - e.printStackTrace(); - latch.countDown(); - } + setupServer("*", (request, response, _) -> { + try { + assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); + assertTrue(request.containsHeader("Content-Type"), "Content-Type included"); + assertEquals("application/json", + request.getFirstHeader("Content-Type").getValue(), "Content-Type value"); + assertTrue(request.containsHeader("Content-Length"), "Content-Length included"); + assertEquals("2", + request.getFirstHeader("Content-Length").getValue(), "Content-Length value"); + assertTrue(request.containsHeader("Foo"), "Custom header included"); + assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); + response.setStatusCode(200); + latch.countDown(); + } catch (Exception e) { + System.out.println("Exception handling request: " + e); + e.printStackTrace(); + latch.countDown(); } }); @@ -361,9 +463,147 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { verify(ctx, timeout(TIMEOUT).times(2)).tellSuccess(any()); } + @Test + public void postRequestWithBodyTemplate() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/token"; + final AtomicReference capturedBody = new AtomicReference<>(); + setupServerWithBodyCapture(capturedBody, latch); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + config.setRequestBodyTemplate("{\"grant_type\":\"client_credentials\",\"client_id\":\"${clientId}\",\"value\":\"$[token]\"}"); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + metaData.putValue("clientId", "my-client-123"); + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"token\":\"abc-xyz\"}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + assertEquals("{\"grant_type\":\"client_credentials\",\"client_id\":\"my-client-123\",\"value\":\"abc-xyz\"}", capturedBody.get()); + } + + @Test + public void postRequestWithBodyTemplateAndParseToPlainText() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/text"; + final AtomicReference capturedBody = new AtomicReference<>(); + setupServerWithBodyCapture(capturedBody, latch); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + config.setParseToPlainText(true); + config.setRequestBodyTemplate("Hello ${name}, your token is $[token]!"); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + metaData.putValue("name", "World"); + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"token\":\"abc-xyz\"}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + assertEquals("Hello World, your token is abc-xyz!", capturedBody.get()); + } + + @Test + public void postRequestWithBodyTemplateEscapesJsonSpecialChars() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/token"; + final AtomicReference capturedBody = new AtomicReference<>(); + setupServerWithBodyCapture(capturedBody, latch); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + config.setRequestBodyTemplate("{\"name\":\"${userName}\",\"desc\":\"$[description]\"}"); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + metaData.putValue("userName", "John \"Doe\""); + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"description\":\"line1\\nline2\"}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + assertEquals("{\"name\":\"John \\\"Doe\\\"\",\"desc\":\"line1\\nline2\"}", capturedBody.get()); + } + + @Test + public void postRequestWithEmptyBodyTemplateUsesMessageData() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/data"; + final AtomicReference capturedBody = new AtomicReference<>(); + setupServerWithBodyCapture(capturedBody, latch); + + given(ctx.getExternalCallExecutor()).willReturn(DirectListeningExecutor.INSTANCE); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + // requestBodyTemplate is null by default — should use msg.getData() + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"temperature\":25}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctx, timeout(10_000)).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + assertEquals("{\"temperature\":25}", capturedBody.get()); + } + + private void setupServerWithBodyCapture(AtomicReference capturedBody, CountDownLatch latch) throws IOException { + setupServer("*", (request, response, _) -> { + try { + if (request instanceof org.apache.http.HttpEntityEnclosingRequest entityRequest) { + InputStream is = entityRequest.getEntity().getContent(); + capturedBody.set(new String(is.readAllBytes(), StandardCharsets.UTF_8)); + } + response.setStatusCode(200); + latch.countDown(); + } catch (Exception e) { + e.printStackTrace(); + latch.countDown(); + } + }); + } + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { return Stream.of( - // config for version 2 with upgrade from version 0 Arguments.of(0, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," + "\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," + @@ -373,13 +613,12 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":false}", true, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + - "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + - "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"credentials\": {\"type\": \"anonymous\"}," + - "\"maxInMemoryBufferSizeInKb\": 256}"), - // config for version 2 with upgrade from version 1 + "\"maxInMemoryBufferSizeInKb\": 256,\"requestBodyTemplate\": null}"), Arguments.of(1, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + @@ -390,13 +629,12 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"credentials\": {\"type\": \"anonymous\"}}", true, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + - "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + - "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"credentials\": {\"type\": \"anonymous\"}," + - "\"maxInMemoryBufferSizeInKb\": 256}"), - // config for version 3 with upgrade from version 2 + "\"maxInMemoryBufferSizeInKb\": 256,\"requestBodyTemplate\": null}"), Arguments.of(2, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + @@ -406,12 +644,63 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"credentials\": {\"type\": \"anonymous\"}}", true, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + - "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + - "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"credentials\": {\"type\": \"anonymous\"}," + - "\"maxInMemoryBufferSizeInKb\": 256}") + "\"maxInMemoryBufferSizeInKb\": 256,\"requestBodyTemplate\": null}"), + Arguments.of(3, """ + { + "restEndpointUrlPattern": "http://localhost/api", + "requestMethod": "POST", + "useSimpleClientHttpFactory": true, + "parseToPlainText": false, + "ignoreRequestBody": false, + "enableProxy": false, + "useSystemProxyProperties": false, + "proxyScheme": null, + "proxyHost": null, + "proxyPort": 0, + "proxyUser": null, + "proxyPassword": null, + "readTimeoutMs": 0, + "maxParallelRequestsCount": 0, + "headers": { + "Content-Type": "application/json" + }, + "credentials": { + "type": "anonymous" + }, + "maxInMemoryBufferSizeInKb": 256, + "trimQueue": true, + "maxQueueSize": 100, + "trimDoubleQuotes": false, + "useRedisQueueForMsgPersistence": false + }""", + true, """ + { + "restEndpointUrlPattern": "http://localhost/api", + "requestMethod": "POST", + "parseToPlainText": false, + "ignoreRequestBody": false, + "enableProxy": false, + "useSystemProxyProperties": false, + "proxyHost": null, + "proxyPort": 0, + "proxyUser": null, + "proxyPassword": null, + "readTimeoutMs": 0, + "maxParallelRequestsCount": 0, + "headers": { + "Content-Type": "application/json" + }, + "credentials": { + "type": "anonymous" + }, + "requestBodyTemplate": null, + "maxInMemoryBufferSizeInKb": 256 + }""") ); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java index 7b00d249ff..062e2d0a2e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java @@ -51,7 +51,7 @@ public class TbSendRestApiCallReplyNodeTest { private TbSendRestApiCallReplyNode node; private TbSendRestApiCallReplyNodeConfiguration config; - + @Mock private TbContext ctxMock; @Mock @@ -140,4 +140,5 @@ public class TbSendRestApiCallReplyNodeTest { TbMsg.EMPTY_STRING, "Request body is empty!") ); } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNodeTest.java new file mode 100644 index 0000000000..e4203d6497 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNodeTest.java @@ -0,0 +1,198 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.telemetry; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +class TbCalculatedFieldsNodeTest { + + final TenantId tenantId = TenantId.fromUUID(UUID.fromString("7361ca62-7688-4d78-b374-bc3d77e12dba")); + final DeviceId deviceId = new DeviceId(UUID.fromString("21c55f8d-0c5c-47b3-a344-9657e194b0f6")); + + @Spy + TbCalculatedFieldsNode node; + + @Mock + TbContext ctxMock; + @Mock + RuleEngineTelemetryService telemetryServiceMock; + + @BeforeEach + void setUp() { + lenient().when(ctxMock.getTenantId()).thenReturn(tenantId); + lenient().when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + var config = new EmptyNodeConfiguration().defaultConfiguration(); + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @ParameterizedTest + @EnumSource(TbMsgType.class) + void givenInvalidMsgType_whenOnMsg_thenTellFailure(TbMsgType msgType) { + if (TbMsgType.POST_TELEMETRY_REQUEST == msgType || TbMsgType.POST_ATTRIBUTES_REQUEST == msgType) { + return; + } + + // GIVEN + var msg = TbMsg.newMsg() + .type(msgType) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor actualError = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().tellFailure(eq(msg), actualError.capture()); + assertThat(actualError.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported msg type: " + msg.getType()); + } + + @Test + void givenTelemetryMsg_whenOnMsg_thenPushToTelemetryService() { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(telemetryServiceMock).should().saveTimeseries(assertArg( + actualSaveRequest -> assertThat(actualSaveRequest.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.CF_ONLY) + )); + } + + @ParameterizedTest + @ValueSource(strings = {"POST_TELEMETRY_REQUEST", "POST_ATTRIBUTES_REQUEST"}) + void givenEmptyTelemetryOrAttributesMsg_whenOnMsg_thenTellSuccess(String msgType) { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.valueOf(msgType)) + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(ctxMock).should().tellSuccess(msg); + } + + @Test + void givenEmptyAttributeScope_whenOnMsg_thenTellFailure() { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_ATTRIBUTES_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("active", true).toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor actualError = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().tellFailure(eq(msg), actualError.capture()); + assertThat(actualError.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Attribute scope is missing"); + } + + @Test + void givenInvalidAttributeScope_whenOnMsg_thenTellFailure() { + // GIVEN + String invalidScope = "INVALID_SCOPE"; + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_ATTRIBUTES_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("active", true).toString()) + .metaData(new TbMsgMetaData(Map.of("scope", invalidScope))) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor actualError = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().tellFailure(eq(msg), actualError.capture()); + assertThat(actualError.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid attribute scope: " + invalidScope); + } + + @Test + void givenAttributesMsg_whenOnMsg_thenPushToTelemetryService() { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_ATTRIBUTES_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("active", true).toString()) + .metaData(new TbMsgMetaData(Map.of("scope", AttributeScope.SERVER_SCOPE.name()))) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(telemetryServiceMock).should().saveAttributes(assertArg( + actualSaveRequest -> assertThat(actualSaveRequest.getStrategy()).isEqualTo(AttributesSaveRequest.Strategy.CF_ONLY) + )); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java index c71d096045..d7aa914818 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java @@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.List; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java index 416a60a961..3ff3f76cd0 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java @@ -49,7 +49,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.ArrayList; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index ef5e9db38c..23bc172402 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -29,7 +29,6 @@ import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -46,14 +45,12 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetProfileId; 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.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.job.Job; @@ -64,6 +61,7 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.rpc.Rpc; @@ -80,6 +78,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.notification.NotificationRequestService; @@ -88,11 +87,11 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -170,6 +169,8 @@ public class TenantIdLoaderTest { private JobService jobService; @Mock private AiModelService aiModelService; + @Mock + private ApiKeyService apiKeyService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -208,159 +209,119 @@ public class TenantIdLoaderTest { case CUSTOMER: Customer customer = new Customer(); customer.setTenantId(tenantId); - when(ctx.getCustomerService()).thenReturn(customerService); doReturn(customer).when(customerService).findCustomerById(eq(tenantId), any()); - break; case USER: User user = new User(); user.setTenantId(tenantId); - when(ctx.getUserService()).thenReturn(userService); doReturn(user).when(userService).findUserById(eq(tenantId), any()); - break; case ASSET: Asset asset = new Asset(); asset.setTenantId(tenantId); - when(ctx.getAssetService()).thenReturn(assetService); doReturn(asset).when(assetService).findAssetById(eq(tenantId), any()); - break; case DEVICE: Device device = new Device(); device.setTenantId(tenantId); - when(ctx.getDeviceService()).thenReturn(deviceService); doReturn(device).when(deviceService).findDeviceById(eq(tenantId), any()); - break; case ALARM: Alarm alarm = new Alarm(); alarm.setTenantId(tenantId); - when(ctx.getAlarmService()).thenReturn(alarmService); doReturn(alarm).when(alarmService).findAlarmById(eq(tenantId), any()); - break; case RULE_CHAIN: RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); - when(ctx.getRuleChainService()).thenReturn(ruleChainService); doReturn(ruleChain).when(ruleChainService).findRuleChainById(eq(tenantId), any()); - break; case ENTITY_VIEW: EntityView entityView = new EntityView(); entityView.setTenantId(tenantId); - when(ctx.getEntityViewService()).thenReturn(entityViewService); doReturn(entityView).when(entityViewService).findEntityViewById(eq(tenantId), any()); - break; case DASHBOARD: Dashboard dashboard = new Dashboard(); dashboard.setTenantId(tenantId); - when(ctx.getDashboardService()).thenReturn(dashboardService); doReturn(dashboard).when(dashboardService).findDashboardById(eq(tenantId), any()); - break; case EDGE: Edge edge = new Edge(); edge.setTenantId(tenantId); - when(ctx.getEdgeService()).thenReturn(edgeService); doReturn(edge).when(edgeService).findEdgeById(eq(tenantId), any()); - break; case OTA_PACKAGE: OtaPackage otaPackage = new OtaPackage(); otaPackage.setTenantId(tenantId); - when(ctx.getOtaPackageService()).thenReturn(otaPackageService); doReturn(otaPackage).when(otaPackageService).findOtaPackageInfoById(eq(tenantId), any()); - break; case ASSET_PROFILE: AssetProfile assetProfile = new AssetProfile(); assetProfile.setTenantId(tenantId); - when(ctx.getAssetProfileCache()).thenReturn(assetProfileCache); doReturn(assetProfile).when(assetProfileCache).get(eq(tenantId), any(AssetProfileId.class)); - break; case DEVICE_PROFILE: DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setTenantId(tenantId); - when(ctx.getDeviceProfileCache()).thenReturn(deviceProfileCache); doReturn(deviceProfile).when(deviceProfileCache).get(eq(tenantId), any(DeviceProfileId.class)); - break; case WIDGET_TYPE: WidgetType widgetType = new WidgetType(); widgetType.setTenantId(tenantId); - when(ctx.getWidgetTypeService()).thenReturn(widgetTypeService); doReturn(widgetType).when(widgetTypeService).findWidgetTypeById(eq(tenantId), any()); - break; case WIDGETS_BUNDLE: WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); - when(ctx.getWidgetBundleService()).thenReturn(widgetsBundleService); doReturn(widgetsBundle).when(widgetsBundleService).findWidgetsBundleById(eq(tenantId), any()); - break; case RPC: Rpc rpc = new Rpc(); rpc.setTenantId(tenantId); - when(ctx.getRpcService()).thenReturn(rpcService); doReturn(rpc).when(rpcService).findRpcById(eq(tenantId), any()); - break; case QUEUE: Queue queue = new Queue(); queue.setTenantId(tenantId); - when(ctx.getQueueService()).thenReturn(queueService); doReturn(queue).when(queueService).findQueueById(eq(tenantId), any()); - break; case API_USAGE_STATE: ApiUsageState apiUsageState = new ApiUsageState(); apiUsageState.setTenantId(tenantId); - when(ctx.getRuleEngineApiUsageStateService()).thenReturn(ruleEngineApiUsageStateService); doReturn(apiUsageState).when(ruleEngineApiUsageStateService).findApiUsageStateById(eq(tenantId), any()); - break; case TB_RESOURCE: TbResource tbResource = new TbResource(); tbResource.setTenantId(tenantId); - when(ctx.getResourceService()).thenReturn(resourceService); doReturn(tbResource).when(resourceService).findResourceInfoById(eq(tenantId), any()); - break; case RULE_NODE: RuleNode ruleNode = new RuleNode(); - when(ctx.getRuleChainService()).thenReturn(ruleChainService); doReturn(ruleNode).when(ruleChainService).findRuleNodeById(eq(tenantId), any()); - break; case TENANT_PROFILE: TenantProfile tenantProfile = new TenantProfile(tenantProfileId); - when(ctx.getTenantProfile()).thenReturn(tenantProfile); - break; case NOTIFICATION_TARGET: NotificationTarget notificationTarget = new NotificationTarget(); @@ -422,12 +383,6 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedField).when(calculatedFieldService).findById(eq(tenantId), any()); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setTenantId(tenantId); - when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); - doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); - break; case JOB: Job job = new Job(); job.setTenantId(tenantId); @@ -440,6 +395,12 @@ public class TenantIdLoaderTest { when(ctx.getAiModelService()).thenReturn(aiModelService); doReturn(Optional.of(aiModel)).when(aiModelService).findAiModelById(eq(tenantId), any()); break; + case API_KEY: + ApiKey apiKey = new ApiKey(); + apiKey.setTenantId(tenantId); + when(ctx.getApiKeyService()).thenReturn(apiKeyService); + doReturn(apiKey).when(apiKeyService).findApiKeyById(eq(tenantId), any()); + break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); } diff --git a/tools/pom.xml b/tools/pom.xml index 46c56c634b..39c52f3b59 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 723f9a1dd2..b17cc4e888 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.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 9ab06ca996..c618a921aa 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -15,30 +15,33 @@ # # Spring common parameters +# Controls core Spring Boot application settings such as web environment and application type. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. # Server common parameters +# Configures the HTTP server bind address and port for the actuator/metrics web endpoint. server: # Server bind address (has no effect if web-environment is disabled). address: "${HTTP_BIND_ADDRESS:0.0.0.0}" # Server bind port (has no effect if web-environment is disabled). port: "${HTTP_BIND_PORT:8083}" -# Zookeeper connection parameters. Used for service discovery. +# ZooKeeper connection parameters. Used for service discovery. +# Defines how this service registers itself and discovers other nodes via Apache ZooKeeper. zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:false}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. @@ -46,6 +49,7 @@ zk: recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" # Cache parameters +# Configures the caching backend and per-entity cache settings used by the transport service. cache: # caffeine or redis type: "${CACHE_TYPE:redis}" @@ -55,6 +59,7 @@ cache: maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis/Valkey configuration parameters +# Defines connection mode, authentication, SSL, and connection pool settings for Redis or Valkey. redis: connection: # standalone or cluster or sentinel @@ -90,9 +95,9 @@ redis: password: "${REDIS_SENTINEL_PASSWORD:}" # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" - # db index + # Redis logical database index to select after connecting. db: "${REDIS_DB:0}" - # db password + # Password for Redis authentication (leave empty if not required). password: "${REDIS_PASSWORD:}" # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth username: "${REDIS_USERNAME:}" @@ -172,6 +177,7 @@ transport: print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" # CoAP server parameters +# Configures the CoAP and CoAP/DTLS server endpoints, including bind addresses, timeouts, and TLS credentials. coap: # CoAP bind-address bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" @@ -187,15 +193,15 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" + #

  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
+ connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. # Default = 1024 @@ -253,6 +259,7 @@ coap: dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" # Queue configuration parameters +# Defines the message queue backend (Kafka) and all topic, partition, and consumer settings for inter-service communication. queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). @@ -406,12 +413,14 @@ queue: poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" # Service common properties +# Identifies this service instance within the ThingsBoard cluster. service: type: "${TB_SERVICE_TYPE:tb-transport}" # service type # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" # Usage statistics parameters +# Controls reporting of API usage metrics at system, tenant, and customer levels. usage: stats: report: @@ -421,15 +430,19 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates + urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters +# Enables Micrometer/Actuator metrics collection and configures the exposed management endpoints. metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" # General management parameters +# Configures Spring Boot Actuator endpoint exposure for health checks and metrics scraping. management: endpoints: web: @@ -438,6 +451,7 @@ management: include: "${METRICS_ENDPOINTS_EXPOSE:info}" # Notification system parameters +# Configures deduplication rules for platform notifications to prevent repeated alerts within a time window. notification_system: rules: # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' diff --git a/transport/http/pom.xml b/transport/http/pom.xml index d4e19705c9..ae2af1e29b 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.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 f869534088..5173f5c26d 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -15,6 +15,7 @@ # # Server common properties +# Configures the HTTP server bind address, port, SSL/TLS settings, and HTTP/2 support. server: # Server bind address address: "${HTTP_BIND_ADDRESS:0.0.0.0}" @@ -54,23 +55,25 @@ server: enabled: "${HTTP2_ENABLED:true}" # Spring common parameters +# Controls Spring Boot framework-level settings such as circular references and multipart upload limits. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.servlet.multipart.max-file-size: "${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}" # Total file size cannot exceed 50MB when configuring file uploads spring.servlet.multipart.max-request-size: "${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}" # Total request size for a multipart/form-data cannot exceed 50MB -# Zookeeper connection parameters. Used for service discovery. +# ZooKeeper connection parameters +# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments. zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:false}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. @@ -78,6 +81,7 @@ zk: recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" # Cache parameters +# Defines the cache backend type and per-entity cache configuration such as TTL and maximum size. cache: # caffeine or redis type: "${CACHE_TYPE:redis}" @@ -87,6 +91,7 @@ cache: maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis/Valkey configuration parameters +# Configures connection mode (standalone, cluster, sentinel), credentials, SSL, and connection pool settings. redis: # standalone or cluster or sentinel connection: @@ -123,9 +128,9 @@ redis: password: "${REDIS_SENTINEL_PASSWORD:}" # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" - # db index + # Redis logical database index to select after connecting. db: "${REDIS_DB:0}" - # db password + # Password for Redis authentication (leave empty if not required). password: "${REDIS_PASSWORD:}" # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth username: "${REDIS_USERNAME:}" @@ -166,6 +171,7 @@ redis: blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" # HTTP server parameters +# Configures HTTP transport request timeouts, payload limits, session management, JSON processing, logging, and statistics. transport: http: # HTTP request processing timeout in milliseconds @@ -203,6 +209,7 @@ transport: print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" # Queue configuration parameters +# Configures the messaging queue backend (Kafka), topic definitions, partitioning, and per-service consumer settings. queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) . @@ -355,12 +362,14 @@ queue: poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" # General service parameters +# Defines the service type and unique identifier used for service discovery and cluster coordination. service: type: "${TB_SERVICE_TYPE:tb-transport}" # type of service # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" # Usage statistics parameters +# Controls reporting of API usage statistics at the system, tenant, and optional customer level. usage: stats: report: @@ -370,15 +379,19 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates + urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters +# Enables or disables actuator metrics collection and exposure for monitoring integrations such as Prometheus. metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" # General management parameters +# Configures Spring Boot Actuator management endpoints, including which endpoints are exposed over HTTP. management: endpoints: web: @@ -387,6 +400,7 @@ management: include: "${METRICS_ENDPOINTS_EXPOSE:info}" # Notification system parameters +# Configures notification rules, including deduplication windows to prevent repeated alerts for the same trigger. notification_system: rules: # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index fac60f7edc..045c7181f2 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index d22bd34505..2711eae3ec 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -15,30 +15,33 @@ # # Spring common parameters +# Controls Spring Boot web environment and application type settings used for optional metrics exposure. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. # Server common parameters +# HTTP server bind address and port, effective only when the web environment is enabled. server: # Server bind address (has no effect if web-environment is disabled). address: "${HTTP_BIND_ADDRESS:0.0.0.0}" # Server bind port (has no effect if web-environment is disabled). port: "${HTTP_BIND_PORT:8083}" -# Zookeeper connection parameters. Used for service discovery. +# ZooKeeper connection parameters +# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments. zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:false}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. @@ -46,6 +49,7 @@ zk: recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" # Cache parameters +# Configures the cache backend type and per-entity cache specifications such as TTL and maximum size. cache: # caffeine or redis type: "${CACHE_TYPE:redis}" @@ -55,6 +59,7 @@ cache: maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis/Valkey configuration parameters +# Connection, authentication, SSL, and connection pool settings for the Redis or Valkey cache backend. redis: connection: # standalone or cluster or sentinel @@ -90,9 +95,9 @@ redis: password: "${REDIS_SENTINEL_PASSWORD:}" # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" - # db index + # Redis logical database index to select after connecting. db: "${REDIS_DB:0}" - # db password + # Password for Redis authentication (leave empty if not required). password: "${REDIS_PASSWORD:}" # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth username: "${REDIS_USERNAME:}" @@ -133,6 +138,7 @@ redis: blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" # LWM2M server parameters +# Session, JSON processing, RPC, logging, and LwM2M-specific transport settings including DTLS, bootstrap, and OTA. transport: sessions: # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. @@ -166,16 +172,16 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:}" + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
+ connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID id: "${LWM2M_SERVER_ID:123}" @@ -303,6 +309,7 @@ transport: print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" # Queue configuration properties +# Defines the message queue backend (Kafka) and per-topic settings for transport, core, rule-engine, and JS evaluation. queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). @@ -456,12 +463,14 @@ queue: poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" # Service common parameters +# Identifies the service type and its unique instance ID within a ThingsBoard cluster. service: type: "${TB_SERVICE_TYPE:tb-transport}" # service type # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" # Usage statistics parameters +# Controls reporting of API usage statistics at system, tenant, and optionally customer level. usage: stats: report: @@ -471,15 +480,19 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates + urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters +# Enables or disables Actuator-based metrics collection (e.g., Prometheus endpoint). metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" # General management parameters +# Configures Spring Boot Actuator endpoint exposure, e.g. for Prometheus metrics scraping. management: endpoints: web: @@ -488,6 +501,7 @@ management: include: "${METRICS_ENDPOINTS_EXPOSE:info}" # Notification system parameters +# Configures deduplication rules for notification triggers to prevent repeated alerts within a time window. notification_system: rules: # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 7b61c03200..4e68cbb49e 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.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 ccbd3901ce..02c451fd69 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -15,30 +15,33 @@ # # Spring common parameters +# Controls Spring Boot application type, web environment, and bean dependency settings. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. # Server common parameters +# Configures the embedded HTTP server bind address and port for actuator/metrics endpoints. server: # Server bind address (has no effect if web-environment is disabled). address: "${HTTP_BIND_ADDRESS:0.0.0.0}" # Server bind port (has no effect if web-environment is disabled). port: "${HTTP_BIND_PORT:8083}" -# Zookeeper connection parameters. Used for service discovery. +# ZooKeeper connection parameters. Used for service discovery. +# Defines connection, session, retry settings and the node directory path for ZooKeeper-based cluster coordination. zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:false}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. @@ -46,6 +49,7 @@ zk: recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" # Cache parameters +# Configures the cache backend type (Caffeine or Redis) and per-cache TTL and size limits. cache: # caffeine or redis type: "${CACHE_TYPE:redis}" @@ -55,6 +59,7 @@ cache: maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis/Valkey configuration parameters +# Covers connection mode (standalone, cluster, sentinel), SSL, credentials, and connection pool tuning. redis: # standalone or cluster or sentinel connection: @@ -91,9 +96,9 @@ redis: password: "${REDIS_SENTINEL_PASSWORD:}" # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" - # db index + # Redis logical database index to select after connecting. db: "${REDIS_DB:0}" - # db password + # Password for Redis authentication (leave empty if not required). password: "${REDIS_PASSWORD:}" # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth username: "${REDIS_USERNAME:}" @@ -134,6 +139,7 @@ redis: blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" # MQTT server parameters +# Configures the MQTT transport layer including bind address/port, SSL, Netty tuning, session management, rate limits, and message processing options. transport: mqtt: # MQTT bind-address @@ -236,6 +242,7 @@ transport: ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}" # Queue configuration parameters +# Defines the messaging queue backend (Kafka), topic names, partitioning, and poll/processing settings for all platform microservices. queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). @@ -389,12 +396,14 @@ queue: poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" # Service common properties +# Identifies this service instance within the cluster by type and optional unique ID. service: type: "${TB_SERVICE_TYPE:tb-transport}" # service type # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" # Usage statistics parameters +# Controls reporting of API usage statistics at system, tenant, and customer levels with configurable intervals. usage: stats: report: @@ -404,15 +413,19 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates + urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters +# Enables or disables Micrometer actuator metrics collection for monitoring this service. metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" # General management parameters +# Configures Spring Boot Actuator endpoint exposure, including which endpoints are accessible over HTTP. management: endpoints: web: @@ -421,6 +434,7 @@ management: include: "${METRICS_ENDPOINTS_EXPOSE:info}" # Notification system parameters +# Configures deduplication rules for notification triggers to prevent repeated alerts within a defined time window. notification_system: rules: # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' diff --git a/transport/pom.xml b/transport/pom.xml index 3645cf7d73..3b6195f9d8 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 683c0e3e2d..1575d25bdb 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT transport diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 656c01524d..028e9e2714 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -15,30 +15,33 @@ # # Spring common parameters +# Controls Spring Boot application type and web environment settings for the SNMP transport service. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. # Server common parameters +# Configures the embedded HTTP server bind address and port (used when web environment is enabled). server: # Server bind address (has no effect if web-environment is disabled). address: "${HTTP_BIND_ADDRESS:0.0.0.0}" # Server bind port (has no effect if web-environment is disabled). port: "${HTTP_BIND_PORT:8083}" -# Zookeeper connection parameters. Used for service discovery. +# ZooKeeper connection parameters. Used for service discovery. +# Defines ZooKeeper connectivity, timeouts, and cluster node registration settings for service coordination. zk: - # Enable/disable zookeeper discovery service. + # Enable/disable ZooKeeper discovery service. enabled: "${ZOOKEEPER_ENABLED:false}" - # Zookeeper connect string + # ZooKeeper connect string url: "${ZOOKEEPER_URL:localhost:2181}" - # Zookeeper retry interval in milliseconds + # ZooKeeper retry interval in milliseconds retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" - # Zookeeper connection timeout in milliseconds + # ZooKeeper connection timeout in milliseconds connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" - # Zookeeper session timeout in milliseconds + # ZooKeeper session timeout in milliseconds session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" - # Name of the directory in zookeeper 'filesystem' + # Name of the directory in ZooKeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. @@ -46,6 +49,7 @@ zk: recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" # Cache parameters +# Specifies the caching backend (Caffeine or Redis) and per-entity cache TTL and size limits. cache: # caffeine or redis type: "${CACHE_TYPE:redis}" @@ -55,6 +59,7 @@ cache: maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis/Valkey configuration parameters +# Covers standalone, cluster, and sentinel Redis connection modes, SSL, authentication, and connection pool tuning. redis: connection: # standalone or cluster or sentinel @@ -90,9 +95,9 @@ redis: password: "${REDIS_SENTINEL_PASSWORD:}" # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" - # db index + # Redis logical database index to select after connecting. db: "${REDIS_DB:0}" - # db password + # Password for Redis authentication (leave empty if not required). password: "${REDIS_PASSWORD:}" # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth username: "${REDIS_USERNAME:}" @@ -133,6 +138,7 @@ redis: blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" # Snmp server parameters +# Configures the SNMP transport protocol, bind port, PDU limits, session management, JSON handling, logging, and statistics. transport: snmp: # Enable/disable SNMP transport protocol @@ -184,6 +190,7 @@ transport: print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" # Queue configuration parameters +# Defines the message queue backend (Kafka) and topic/consumer settings for transport, core, rule-engine, and JS evaluation. queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). @@ -344,12 +351,14 @@ queue: poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" # Service common parameters +# Identifies this service instance by type and unique ID within a ThingsBoard cluster. service: type: "${TB_SERVICE_TYPE:tb-transport}" # service type # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" # Usage statistics parameters +# Controls collection and reporting intervals for API usage statistics at system, tenant, and customer levels. usage: stats: report: @@ -359,15 +368,19 @@ usage: enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Reporting interval for urgent keys (e.g. SMS, Email) that require quicker usage state updates + urgent_interval: "${USAGE_STATS_REPORT_URGENT_INTERVAL:10}" # Amount of statistic messages in pack pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters +# Enables or disables actuator-based metrics collection (e.g., Prometheus) for the SNMP transport service. metrics: # Enable/disable actuator metrics. enabled: "${METRICS_ENABLED:false}" # General management parameters +# Configures Spring Boot Actuator endpoint exposure, including which management endpoints are accessible over HTTP. management: endpoints: web: @@ -376,6 +389,7 @@ management: include: "${METRICS_ENDPOINTS_EXPOSE:info}" # Notification system parameters +# Defines deduplication rules for notification triggers to prevent duplicate alerts within configured time windows. notification_system: rules: # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b88a0583e2..4d001423cd 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "4.2.2.2", + "version": "4.4.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", @@ -71,7 +71,7 @@ "ngx-clipboard": "^16.0.0", "ngx-daterangepicker-material": "^6.0.4", "ngx-drag-drop": "^20.0.1", - "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/4.0.0", + "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/4.1.0", "ngx-hm-carousel": "^19.0.0", "ngx-markdown": "^20.1.0", "ngx-sharebuttons": "^17.0.0", diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch new file mode 100644 index 0000000000..b0087a67e9 --- /dev/null +++ b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +index a372799..f64a6f8 100644 +--- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs ++++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +@@ -516,7 +516,7 @@ class Color { + const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l); + const value = (l + s) / 2; + const saturation = (2 * s) / (l + s) || 0; +- return new Hsva(hue, saturation, value, color.alpha); ++ return new Hsva(hue, saturation * 100, value * 100, color.alpha); + } + rgbaToHsva(color) { + const red = color.red / 255; diff --git a/ui-ngx/patches/@mat-datetimepicker+core+16.0.1.patch b/ui-ngx/patches/@mat-datetimepicker+core+16.0.1.patch index 2c65c021d3..72e42dc318 100644 --- a/ui-ngx/patches/@mat-datetimepicker+core+16.0.1.patch +++ b/ui-ngx/patches/@mat-datetimepicker+core+16.0.1.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs -index 83a6413..70d9c3e 100644 +index 83a6413..6ebae81 100644 --- a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs +++ b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs @@ -935,9 +935,9 @@ class MatDatetimepickerClockComponent { @@ -15,3 +15,30 @@ index 83a6413..70d9c3e 100644 date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); } this._timeChanged = true; +@@ -1889,7 +1889,7 @@ class MatDatetimepickerContentComponent { + } + } + /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: MatDatetimepickerContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.3", type: MatDatetimepickerContentComponent, isStandalone: false, selector: "mat-datetimepicker-content", host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "class.mat-datetimepicker-content-touch": "datetimepicker?.touchUi" }, classAttribute: "mat-datetimepicker-content" }, viewQueries: [{ propertyName: "_calendar", first: true, predicate: MatDatetimepickerCalendarComponent, descendants: true, static: true }], ngImport: i0, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;z-index:1000;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48}.cdk-overlay-dark-backdrop{background:#0009}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"], dependencies: [{ kind: "directive", type: i1$1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: MatDatetimepickerCalendarComponent, selector: "mat-datetimepicker-calendar", inputs: ["multiYearSelector", "startView", "twelvehour", "timeInterval", "dateFilter", "ariaLabel", "ariaNextMonthLabel", "ariaPrevMonthLabel", "ariaNextYearLabel", "ariaPrevYearLabel", "ariaNextMultiYearLabel", "ariaPrevMultiYearLabel", "preventSameDateTimeSelection", "type", "startAt", "selected", "minDate", "maxDate"], outputs: ["_userSelection", "selectedChange", "viewChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } ++ /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.3", type: MatDatetimepickerContentComponent, isStandalone: false, selector: "mat-datetimepicker-content", host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "class.mat-datetimepicker-content-touch": "datetimepicker?.touchUi" }, classAttribute: "mat-datetimepicker-content" }, viewQueries: [{ propertyName: "_calendar", first: true, predicate: MatDatetimepickerCalendarComponent, descendants: true, static: true }], ngImport: i0, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"], dependencies: [{ kind: "directive", type: i1$1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: MatDatetimepickerCalendarComponent, selector: "mat-datetimepicker-calendar", inputs: ["multiYearSelector", "startView", "twelvehour", "timeInterval", "dateFilter", "ariaLabel", "ariaNextMonthLabel", "ariaPrevMonthLabel", "ariaNextYearLabel", "ariaPrevYearLabel", "ariaNextMultiYearLabel", "ariaPrevMultiYearLabel", "preventSameDateTimeSelection", "type", "startAt", "selected", "minDate", "maxDate"], outputs: ["_userSelection", "selectedChange", "viewChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: MatDatetimepickerContentComponent, decorators: [{ + type: Component, +@@ -1897,7 +1897,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImpor + class: 'mat-datetimepicker-content', + '[class.mat-datetimepicker-content-touch]': 'datetimepicker?.touchUi', + '(keydown)': '_handleKeydown($event)', +- }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;z-index:1000;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48}.cdk-overlay-dark-backdrop{background:#0009}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"] }] ++ }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"] }] + }], propDecorators: { _calendar: [{ + type: ViewChild, + args: [MatDatetimepickerCalendarComponent, { static: true }] +@@ -2150,7 +2150,7 @@ class MatDatetimepickerComponent { + const overlayConfig = new OverlayConfig({ + positionStrategy: this._createPopupPositionStrategy(), + hasBackdrop: true, +- backdropClass: 'mat-overlay-transparent-backdrop', ++ backdropClass: 'cdk-overlay-transparent-backdrop', + direction: this._dir ? this._dir.value : 'ltr', + scrollStrategy: this._scrollStrategy(), + panelClass: 'mat-datetimepicker-popup', diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index cc58f728a0..03416b1c32 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard org.thingsboard diff --git a/ui-ngx/src/app/core/api/alarm-data-subscription.ts b/ui-ngx/src/app/core/api/alarm-data-subscription.ts index 16dafd53eb..ad6abb6573 100644 --- a/ui-ngx/src/app/core/api/alarm-data-subscription.ts +++ b/ui-ngx/src/app/core/api/alarm-data-subscription.ts @@ -23,6 +23,7 @@ import { DatasourceType } from '@shared/models/widget.models'; import { AlarmData, AlarmDataPageLink, + ComplexOperation, EntityFilter, EntityKey, EntityKeyType, @@ -47,6 +48,7 @@ export interface AlarmDataSubscriptionOptions { pageLink?: AlarmDataPageLink; keyFilters?: Array; additionalKeyFilters?: Array; + keyFiltersOperation?: ComplexOperation; subscriptionTimewindow?: SubscriptionTimewindow; } @@ -120,6 +122,7 @@ export class AlarmDataSubscription { entityFilter: this.alarmDataSubscriptionOptions.entityFilter, pageLink: deepClone(this.alarmDataSubscriptionOptions.pageLink), keyFilters, + keyFiltersOperation: this.alarmDataSubscriptionOptions.keyFiltersOperation, alarmFields, entityFields, latestValues diff --git a/ui-ngx/src/app/core/api/alarm-data.service.ts b/ui-ngx/src/app/core/api/alarm-data.service.ts index 901155dec5..c0c5a928bf 100644 --- a/ui-ngx/src/app/core/api/alarm-data.service.ts +++ b/ui-ngx/src/app/core/api/alarm-data.service.ts @@ -85,6 +85,7 @@ export class AlarmDataService { alarmDataSubscriptionOptions.entityFilter = alarmSource.entityFilter; alarmDataSubscriptionOptions.pageLink = pageLink; alarmDataSubscriptionOptions.keyFilters = alarmSource.keyFilters; + alarmDataSubscriptionOptions.keyFiltersOperation = alarmSource.keyFiltersOperation; 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 index ffd06a5289..de6863ee56 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -314,7 +314,15 @@ export class AliasController implements IAliasController { || newDatasource.type === DatasourceType.entityCount || newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.filterId) { - newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); + const filterInfo = this.getFilterInfo(newDatasource.filterId); + if (filterInfo) { + newDatasource.keyFilters = filterInfoToKeyFilters(filterInfo); + if (filterInfo.keyFiltersOperation) { + newDatasource.keyFiltersOperation = filterInfo.keyFiltersOperation; + } + } else { + newDatasource.keyFilters = []; + } } if (newDatasource.type === DatasourceType.alarmCount) { newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 021c5e81e9..6f7949d259 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -34,6 +34,7 @@ import { import { AlarmFilter, ComparisonTsValue, + ComplexOperation, EntityData, EntityDataPageLink, EntityFilter, @@ -112,6 +113,7 @@ export interface EntityDataSubscriptionOptions { pageLink?: EntityDataPageLink; keyFilters?: Array; additionalKeyFilters?: Array; + keyFiltersOperation?: ComplexOperation; subscriptionTimewindow?: SubscriptionTimewindow; latestTsOffset?: number; } @@ -356,7 +358,6 @@ export class EntityDataSubscription { this.subscriber = new TelemetrySubscriber(this.telemetryService); this.dataCommand = new EntityDataCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; if (this.entityDataSubscriptionOptions.additionalKeyFilters) { if (keyFilters) { @@ -370,6 +371,7 @@ export class EntityDataSubscription { entityFilter: this.entityDataSubscriptionOptions.entityFilter, pageLink: this.entityDataSubscriptionOptions.pageLink, keyFilters, + keyFiltersOperation: this.entityDataSubscriptionOptions.keyFiltersOperation, entityFields, latestValues: this.latestValues }; @@ -494,7 +496,8 @@ export class EntityDataSubscription { } this.countCommand.query = { entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters + keyFilters, + keyFiltersOperation: this.entityDataSubscriptionOptions.keyFiltersOperation }; this.subscriber.subscriptionCommands.push(this.countCommand); @@ -569,7 +572,8 @@ export class EntityDataSubscription { } this.alarmCountCommand.query = { entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters + keyFilters, + keyFiltersOperation: this.entityDataSubscriptionOptions.keyFiltersOperation }; if (this.entityDataSubscriptionOptions.alarmFilter) { this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter}; diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index 9ed064403c..697187b61e 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -183,6 +183,7 @@ export class EntityDataService { entityDataSubscriptionOptions.alarmFilter = datasource.alarmFilter; entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + entityDataSubscriptionOptions.keyFiltersOperation = datasource.keyFiltersOperation; if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { entityDataSubscriptionOptions.pageLink = pageLink; } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 5572730923..12f5bc60d8 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -67,6 +67,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { MatDialogRef } from '@angular/material/dialog'; import { TbUnit } from '@shared/models/unit.models'; import { UnitService } from '@core/services/unit.service'; +import { HttpOptionsResult, HttpUploadOptionsResult, QueryParams, RequestConfig } from '@core/http/http-utils'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -94,6 +95,14 @@ export interface IWidgetUtils { getEntityDetailsPageURL: (id: string, entityType: EntityType) => string; } +export interface IWidgetHttpUtils { + defaultHttpOptions: (ignoreLoading?: boolean, ignoreErrors?: boolean, resendRequest?: boolean, queryParams?: QueryParams) => HttpOptionsResult; + defaultHttpOptionsFromConfig: (config?: RequestConfig) => HttpOptionsResult; + defaultHttpOptionsFromParams: (queryParams?: QueryParams, config?: RequestConfig) => HttpOptionsResult; + defaultHttpUploadOptions: (ignoreLoading?: boolean, ignoreErrors?: boolean, resendRequest?: boolean, queryParams?: QueryParams) => HttpUploadOptionsResult; + createDefaultHttpOptions: (queryParamsOrConfig?: QueryParams | RequestConfig, config?: RequestConfig) => HttpOptionsResult; +} + export interface PlaceMapItemActionData { action: WidgetAction | WidgetActionDescriptor; additionalParams?: any; @@ -192,6 +201,7 @@ export interface IStateController { getStateIdAtIndex(index: number): string; getEntityId(entityParamName: string): EntityId; getCurrentStateName(): string; + reInit(): void; } export interface SubscriptionInfo { diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 213b327892..2194835dfe 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -1456,20 +1456,18 @@ export class WidgetSubscription implements IWidgetSubscription { 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 (datasource.latestDataKeys) { - datasource.latestDataKeys.forEach((dataKey) => { - if (datasource.generated || datasource.isAdditional) { - dataKey._hash = Math.random(); - // dataKey.color = this.ctx.utils.getMaterialColor(index); - } - // index++; - }); - } + // if (datasource.latestDataKeys) { + // datasource.latestDataKeys.forEach((dataKey) => { + // if (datasource.generated || datasource.isAdditional) { + // // dataKey.color = this.ctx.utils.getMaterialColor(index); + // } + // // index++; + // }); + // } }); if (this.comparisonEnabled) { this.datasourcePages.forEach(datasourcePage => { @@ -1499,9 +1497,9 @@ export class WidgetSubscription implements IWidgetSubscription { private entityDataToDatasourceData(datasource: Datasource, data: Array): Array { let datasourceDataArray: Array = []; datasourceDataArray = datasourceDataArray.concat(datasource.dataKeys.map((dataKey, keyIndex) => { - dataKey.hidden = !!dataKey.settings.hideDataByDefault; - dataKey.inLegend = dataKey.settings.showInLegend || - (isUndefined(dataKey.settings.showInLegend) && !dataKey.settings.removeFromLegend); + dataKey.hidden = !!dataKey.settings?.hideDataByDefault; + dataKey.inLegend = dataKey.settings?.showInLegend || + (isUndefined(dataKey.settings?.showInLegend) && !dataKey.settings?.removeFromLegend); dataKey.label = this.ctx.utils.customTranslation(dataKey.label, dataKey.label); const datasourceData: DatasourceData = { datasource, diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 344c44f6c7..d353ae8c8c 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -31,9 +31,16 @@ export interface SysParamsState { maxDebugModeDurationMinutes: number; maxDataPointsPerRollingArg: number; maxArgumentsPerCF: number; + minAllowedDeduplicationIntervalInSecForCF: number; + minAllowedAggregationIntervalInSecForCF: number; + minAllowedScheduledUpdateIntervalInSecForCF: number; + maxRelationLevelPerCfArgument: number; + maxRelatedEntitiesToReturnPerCfArgument: number; ruleChainDebugPerTenantLimitsConfiguration?: string; calculatedFieldDebugPerTenantLimitsConfiguration?: string; + intermediateAggregationIntervalInSecForCF: number; trendzSettings: TrendzSettings; + allowKeyFiltersOrConditions: boolean; } export interface SysParams extends SysParamsState { diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index ca46344bbb..aaf8844863 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -33,10 +33,17 @@ const emptyUserAuthState: AuthPayload = { mobileQrEnabled: false, maxResourceSize: 0, maxArgumentsPerCF: 0, + minAllowedDeduplicationIntervalInSecForCF: 0, + minAllowedAggregationIntervalInSecForCF: 0, + minAllowedScheduledUpdateIntervalInSecForCF: 0, + maxRelationLevelPerCfArgument: 0, + maxRelatedEntitiesToReturnPerCfArgument: 0, maxDataPointsPerRollingArg: 0, maxDebugModeDurationMinutes: 0, + intermediateAggregationIntervalInSecForCF: 0, userSettings: initialUserSettings, - trendzSettings: initialTrendzSettings + trendzSettings: initialTrendzSettings, + allowKeyFiltersOrConditions: true }; export const initialState: AuthState = { diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 1c6ac90f3b..07a8c5b7a2 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -68,6 +68,7 @@ export class AuthService { redirectUrl: string; oauth2Clients: Array = null; twoFactorAuthProviders: Array = null; + forceTwoFactorAuthProviders: Array = null; private refreshTokenSubject: ReplaySubject = null; private jwtHelper = new JwtHelperService(); @@ -117,6 +118,9 @@ export class AuthService { if (loginResponse.scope === Authority.PRE_VERIFICATION_TOKEN) { this.router.navigateByUrl(`login/mfa`); } + if (loginResponse.scope === Authority.MFA_CONFIGURATION_TOKEN) { + this.router.navigateByUrl(`login/force-mfa`); + } } )); } @@ -164,8 +168,8 @@ export class AuthService { )); } - public getUserPasswordPolicy() { - return this.http.get(`/api/noauth/userPasswordPolicy`, defaultHttpOptions()); + public getUserPasswordPolicy(config?: RequestConfig) { + return this.http.get(`/api/noauth/userPasswordPolicy`, defaultHttpOptionsFromConfig(config)); } public activateByEmailCode(emailCode: string): Observable { @@ -239,6 +243,15 @@ export class AuthService { ); } + public getAvailableTwoFaProviders(): Observable> { + return this.http.get>(`/api/2fa/providers`, defaultHttpOptions()).pipe( + catchError(() => of([])), + tap((providers) => { + this.forceTwoFactorAuthProviders = providers; + }) + ); + } + public 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) { @@ -266,6 +279,8 @@ export class AuthService { if (isAuthenticated) { if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { result = this.router.parseUrl('login/mfa'); + } else if (authState.authUser.authority === Authority.MFA_CONFIGURATION_TOKEN) { + result = this.router.parseUrl('login/force-mfa'); } else if (!path || path === 'login' || this.forceDefaultPlace(authState, path, params)) { if (this.redirectUrl) { const redirectUrl = this.redirectUrl; @@ -399,7 +414,7 @@ export class AuthService { loadUserSubject.error(err); } ); - } else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) { + } else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN || authPayload.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) { loadUserSubject.next(authPayload); loadUserSubject.complete(); } else if (authPayload.authUser?.userId) { diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts index b72df84c9b..0937ed58f6 100644 --- a/ui-ngx/src/app/core/guards/auth.guard.ts +++ b/ui-ngx/src/app/core/guards/auth.guard.ts @@ -104,6 +104,16 @@ export class AuthGuard { } this.authService.logout(); return of(this.authService.defaultUrl(false)); + } else if (path === 'login.force-mfa') { + if (authState.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) { + return this.authService.getAvailableTwoFaProviders().pipe( + map(() => { + return true; + }) + ); + } + this.authService.logout(); + return of(this.authService.defaultUrl(false)); } else { return of(true); } @@ -121,7 +131,7 @@ export class AuthGuard { } } if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) { - this.mobileService.handleMobileNavigation(path, params); + this.mobileService.handleMobileNavigation(path, params, lastChild.queryParams); return of(false); } if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { diff --git a/ui-ngx/src/app/core/http/alarm-rules.service.ts b/ui-ngx/src/app/core/http/alarm-rules.service.ts new file mode 100644 index 0000000000..18a51115f3 --- /dev/null +++ b/ui-ngx/src/app/core/http/alarm-rules.service.ts @@ -0,0 +1,73 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageData } from '@shared/models/page/page-data'; +import { + CalculatedFieldAlarmRule, + CalculatedFieldAlarmRuleInfo, + CalculatedFieldsQuery, + CalculatedFieldTestScriptInputParams +} from '@shared/models/calculated-field.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityTestScriptResult } from '@shared/models/entity.models'; +import { CalculatedFieldEventBody } from '@shared/models/event.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AlarmRulesService { + + constructor( + private http: HttpClient + ) { } + + public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAlarmRule(alarmRule: CalculatedFieldAlarmRule, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + + public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + + public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/names${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/api-key.service.ts b/ui-ngx/src/app/core/http/api-key.service.ts new file mode 100644 index 0000000000..a1b85ae1a6 --- /dev/null +++ b/ui-ngx/src/app/core/http/api-key.service.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { ApiKeyInfo, ApiKey } from '@shared/models/api-key.models'; + +@Injectable({ + providedIn: 'root' +}) +export class ApiKeyService { + + constructor( + private http: HttpClient + ) { + } + + public saveApiKey(apiKey: ApiKeyInfo, config?: RequestConfig): Observable { + return this.http.post('/api/apiKey', apiKey, defaultHttpOptionsFromConfig(config)); + } + + public deleteApiKey(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/apiKey/${id}`, defaultHttpOptionsFromConfig(config)); + } + + public updateApiKeyDescription(id: string, description: string, config?: RequestConfig): Observable { + return this.http.put(`/api/apiKey/${id}/description`, description, defaultHttpOptionsFromConfig(config)); + } + + public enableApiKey(id: string, enabledValue: boolean, config?: RequestConfig): Observable { + return this.http.put(`/api/apiKey/${id}/enabled/${enabledValue}`, defaultHttpOptionsFromConfig(config)); + } + + public getUserApiKeys(userId: string, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/apiKeys/${userId}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/asset-profile.service.ts b/ui-ngx/src/app/core/http/asset-profile.service.ts index d98486b827..f86a0f35f9 100644 --- a/ui-ngx/src/app/core/http/asset-profile.service.ts +++ b/ui-ngx/src/app/core/http/asset-profile.service.ts @@ -38,6 +38,11 @@ export class AssetProfileService { return this.http.get>(`/api/assetProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getAssetProfilesByIds(assetProfileIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/assetProfileInfos?assetProfileIds=${assetProfileIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getAssetProfile(assetProfileId: string, config?: RequestConfig): Observable { return this.http.get(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/asset.service.ts b/ui-ngx/src/app/core/http/asset.service.ts index 0d3bd97850..4773121e59 100644 --- a/ui-ngx/src/app/core/http/asset.service.ts +++ b/ui-ngx/src/app/core/http/asset.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; @@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data'; import { EntitySubtype } from '@shared/models/entity-type.models'; import { Asset, AssetInfo, AssetSearchQuery } from '@shared/models/asset.models'; import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -69,8 +70,10 @@ export class AssetService { 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 saveAsset(asset: Asset, config?: RequestConfig): Observable; + public saveAsset(asset: Asset, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveAsset(asset: Asset, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/asset', asset, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteAsset(assetId: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index fab486b3c8..a574d06fb5 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { EntityId } from '@shared/models/id/entity-id'; @@ -35,47 +35,37 @@ export class AttributeService { public getEntityAttributes(entityId: EntityId, attributeScope?: AttributeScope, keys?: Array, config?: RequestConfig): Observable> { let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes`; - + let queryParams: object = null; if (attributeScope) { url += `/${attributeScope}`; } if (keys && keys.length) { - url += `?keys=${keys.join(',')}`; + queryParams = {key: keys}; } - return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + return this.http.get>(url, defaultHttpOptionsFromParams(queryParams, config)); } public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, config?: RequestConfig): Observable { - const keys = attributes.map(attribute => encodeURIComponent(attribute.key)).join(','); - return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` + - `?keys=${keys}`, - defaultHttpOptionsFromConfig(config)); + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`, + defaultHttpOptionsFromParams({key: attributes.map(attribute => attribute.key)}, config)); } public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, deleteAllDataForKeys = false, startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true, config?: RequestConfig): Observable { - const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(','); - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`; - if (isDefinedAndNotNull(deleteAllDataForKeys)) { - url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`; - } - if (isDefinedAndNotNull(rewriteLatestIfDeleted)) { - url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`; - } - if (isDefinedAndNotNull(deleteLatest)) { - url += `&deleteLatest=${deleteLatest}`; - } - if (isDefinedAndNotNull(startTs)) { - url += `&startTs=${startTs}`; - } - if (isDefinedAndNotNull(endTs)) { - url += `&endTs=${endTs}`; - } - return this.http.delete(url, defaultHttpOptionsFromConfig(config)); + const queryParams = { + key: timeseries.map(key => key.key), + deleteAllDataForKeys: deleteAllDataForKeys, + rewriteLatestIfDeleted: rewriteLatestIfDeleted, + deleteLatest: deleteLatest, + startTs: startTs, + endTs: endTs + }; + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete`, + defaultHttpOptionsFromParams(queryParams, config)); } public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, @@ -138,32 +128,29 @@ export class AttributeService { 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)); + const queryParams = { + key: keys, + startTs: startTs, + endTs: endTs, + limit: limit, + agg: agg, + interval: interval, + orderBy: orderBy, + useStrictDataTypes: useStrictDataTypes + } + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, config)); } public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array, useStrictDataTypes = false, config?: RequestConfig): Observable { - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`; + const queryParams: Record = { + useStrictDataTypes: useStrictDataTypes + } if (isDefinedAndNotNull(keys) && keys.length) { - url += `&keys=${keys.join(',')}`; + queryParams.key = keys; } - return this.http.get(url, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, 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 index f4ab583426..7c209c2219 100644 --- a/ui-ngx/src/app/core/http/audit-log.service.ts +++ b/ui-ngx/src/app/core/http/audit-log.service.ts @@ -15,12 +15,12 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, 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 { AuditLog, AuditLogFilter } from '@shared/models/audit-log.models'; import { EntityId } from '@shared/models/id/entity-id'; @Injectable({ @@ -32,28 +32,58 @@ export class AuditLogService { private http: HttpClient ) { } - public getAuditLogs(pageLink: TimePageLink, - config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getAuditLogs(pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogs(pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogs( + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, config) + ); } - public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, - config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogsByCustomerId( + customerId: string, + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, config) + ); } - public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, - config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/user/${userId}${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogsByUserId( + userId: string, + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs/user/${userId}${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, 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)); + public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogsByEntityId( + entityId: EntityId, + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, config) + ); } - } diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index b262f7c3e2..1fa9abd29b 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -15,11 +15,17 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; -import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models'; +import { + CalculatedField, + CalculatedFieldInfo, + CalculatedFieldsQuery, + CalculatedFieldTestScriptInputParams, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; import { PageLink } from '@shared/models/page/page-link'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityTestScriptResult } from '@shared/models/entity.models'; @@ -46,9 +52,12 @@ export class CalculatedFieldsService { return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getCalculatedFields(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + + public getCalculatedFieldsByEntityId({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> { + return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type} , config)); } public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { @@ -58,4 +67,8 @@ export class CalculatedFieldsService { public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable { return this.http.get(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config)); } + + public getCalculatedFieldNames(pageLink: PageLink, type: CalculatedFieldType, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields/names${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type}, config)); + } } diff --git a/ui-ngx/src/app/core/http/customer.service.ts b/ui-ngx/src/app/core/http/customer.service.ts index 03177f2f87..34f370ad80 100644 --- a/ui-ngx/src/app/core/http/customer.service.ts +++ b/ui-ngx/src/app/core/http/customer.service.ts @@ -15,12 +15,13 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, 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'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -40,8 +41,14 @@ export class CustomerService { 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 getCustomersByIds(customerIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/customers?customerIds=${customerIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + + public saveCustomer(customer: Customer, config?: RequestConfig): Observable; + public saveCustomer(customer: Customer, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveCustomer(customer: Customer, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/customer', customer, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteCustomer(customerId: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index 512a34fc7f..f3281b863f 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -83,6 +83,11 @@ export class DashboardService { return this.http.get(`/api/dashboard/info/${dashboardId}`, defaultHttpOptionsFromConfig(config)); } + public getDashboards(dashboardIds: string[], config?: RequestConfig): Observable> { + return this.http.get>(`/api/dashboards?dashboardIds=${dashboardIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public saveDashboard(dashboard: Dashboard, config?: RequestConfig): Observable { return this.http.post('/api/dashboard', dashboard, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 7daedd85cb..a49375d933 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -50,6 +50,11 @@ export class DeviceProfileService { return this.http.get>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getDeviceProfilesByIds(deviceProfileIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfileInfos?deviceProfileIds=${deviceProfileIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index ad2e12852a..f6126f02ad 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -15,8 +15,9 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import { Observable, ReplaySubject } from 'rxjs'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { catchError, Observable, of, ReplaySubject, throwError, timeout } from 'rxjs'; +import { map, switchMap } from "rxjs/operators"; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; @@ -28,13 +29,15 @@ import { DeviceInfo, DeviceInfoQuery, DeviceSearchQuery, - PublishTelemetryCommand + PublishTelemetryCommand, + SaveDeviceParams } from '@shared/models/device.models'; import { EntitySubtype } from '@shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; import { ResourcesService } from '@core/services/resources.service'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -87,15 +90,19 @@ export class DeviceService { 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 saveDevice(device: Device, config?: RequestConfig): Observable; + public saveDevice(device: Device, saveParams?: SaveDeviceParams, config?: RequestConfig): Observable; + public saveDevice(device: Device, saveParamsOrConfig?: SaveDeviceParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/device', device, createDefaultHttpOptions(saveParamsOrConfig, config)); } - public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable { + public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable; + public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { return this.http.post('/api/device-with-credentials', { device, credentials - }, defaultHttpOptionsFromConfig(config)); + }, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteDevice(deviceId: string, config?: RequestConfig) { @@ -219,4 +226,71 @@ export class DeviceService { public downloadGatewayDockerComposeFile(deviceId: string): Observable { return this.resourcesService.downloadResource(`/api/device-connectivity/gateway-launch/${deviceId}/docker-compose/download`); } + + public rebootDevice(deviceId: string, isBootstrapServer: boolean, config?: RequestConfig): Observable<{ + result: string, + msg: string + }> { + const rebootName = isBootstrapServer ? 'Bootstrap-Request Trigger' : 'Registration Update Trigger'; + return this.sendTwoWayRpcCommand(deviceId, {method: 'DiscoverAll'}, config).pipe( + timeout(10000), + switchMap((response: any) => { + if (response.result && response.result.toUpperCase() === 'CONTENT') { + const resourceId = isBootstrapServer ? 9 : 8; + const resourcePath = `/1/0/${resourceId}`; + return this.rebootTrigger(deviceId, resourcePath, config).pipe( + map((responseReboot: any) => { + if (responseReboot.result === 'CHANGED') { + return { + result: 'SUCCESS', + msg: `"${rebootName}" - Started Successfully.` + }; + } else { + return { + result: 'ERROR', + msg: `"${rebootName}" failed:
${JSON.stringify(responseReboot, null, 2)}
` + } + } + }), + catchError(err => + of({ + result: 'ERROR', + msg: `"${rebootName}" failed.
Error: ${err.message || err}` + }) + ) + ); + } else { + return of({ + result: 'ERROR', + msg: `"${rebootName}" failed.
Bad registration device with id = ${deviceId}.
"DiscoverAll" - RPC result is not "CONTENT"` + }); + } + }), + catchError(err => + of({ + result: 'ERROR', + msg: `"${rebootName}" failed.
Bad registration device with id = ${deviceId}.
Error: ${err.message || err}` + }) + ) + ); + } + + private rebootTrigger(deviceId: string, resourcePath: string, config?: RequestConfig): Observable<{ result: string, msg?: string }> { + return this.sendTwoWayRpcCommand(deviceId, {method: 'Execute', params: {id: resourcePath}}, config).pipe( + timeout(10000), + map(res => { + if (res?.result?.toUpperCase() === 'CHANGED') { + return {result: 'CHANGED'}; + } else { + return { + result: `${res?.result}`, + msg: `${res?.error}` + } + } + }), + catchError(err => { + return throwError(() => err); + }) + ); + } } diff --git a/ui-ngx/src/app/core/http/entity-view.service.ts b/ui-ngx/src/app/core/http/entity-view.service.ts index 04ab621e53..5741921c41 100644 --- a/ui-ngx/src/app/core/http/entity-view.service.ts +++ b/ui-ngx/src/app/core/http/entity-view.service.ts @@ -15,13 +15,14 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, 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'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -47,12 +48,19 @@ export class EntityViewService { return this.http.get(`/api/entityView/${entityViewId}`, defaultHttpOptionsFromConfig(config)); } + public getEntityViews(entityViewIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/entityViews?entityViewIds=${entityViewIds.join(',')}`, + 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 saveEntityView(entityView: EntityView, config?: RequestConfig): Observable; + public saveEntityView(entityView: EntityView, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveEntityView(entityView: EntityView, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/entityView', entityView, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteEntityView(entityViewId: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 868485653e..6f4db51e24 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -189,6 +189,12 @@ export class EntityService { case EntityType.AI_MODEL: observable = this.aiModelService.getAiModelById(entityId, config); break; + case EntityType.DEVICE_PROFILE: + observable = this.deviceProfileService.getDeviceProfile(entityId, config); + break; + case EntityType.ASSET_PROFILE: + observable = this.assetProfileService.getAssetProfile(entityId, config); + break; } return observable; } @@ -239,50 +245,34 @@ export class EntityService { observable = this.edgeService.getEdges(entityIds, config); break; case EntityType.ENTITY_VIEW: - observable = this.getEntitiesByIdsObservable( - (id) => this.entityViewService.getEntityView(id, config), - entityIds); + observable = this.entityViewService.getEntityViews(entityIds, config); break; case EntityType.TENANT: - observable = this.getEntitiesByIdsObservable( - (id) => this.tenantService.getTenant(id, config), - entityIds); + observable = this.tenantService.getTenantsByIds(entityIds, config); break; case EntityType.CUSTOMER: - observable = this.getEntitiesByIdsObservable( - (id) => this.customerService.getCustomer(id, config), - entityIds); + observable = this.customerService.getCustomersByIds(entityIds, config); break; case EntityType.DASHBOARD: - observable = this.getEntitiesByIdsObservable( - (id) => this.dashboardService.getDashboardInfo(id, config), - entityIds); + observable = this.dashboardService.getDashboards(entityIds, config); break; case EntityType.USER: - observable = this.getEntitiesByIdsObservable( - (id) => this.userService.getUser(id, config), - entityIds); + observable = this.userService.getUsersByIds(entityIds, config); break; case EntityType.ALARM: console.error('Get Alarm Entity is not implemented!'); break; case EntityType.DEVICE_PROFILE: - observable = this.getEntitiesByIdsObservable( - (id) => this.deviceProfileService.getDeviceProfileInfo(id, config), - entityIds); + observable = this.deviceProfileService.getDeviceProfilesByIds(entityIds, config); break; case EntityType.TENANT_PROFILE: observable = this.tenantProfileService.getTenantProfilesByIds(entityIds, config); break; case EntityType.ASSET_PROFILE: - observable = this.getEntitiesByIdsObservable( - (id) => this.assetProfileService.getAssetProfileInfo(id, config), - entityIds); + observable = this.assetProfileService.getAssetProfilesByIds(entityIds, config); break; case EntityType.WIDGETS_BUNDLE: - observable = this.getEntitiesByIdsObservable( - (id) => this.widgetService.getWidgetsBundle(id, config), - entityIds); + observable = this.widgetService.getWidgetsBundlesByIds(entityIds, config); break; case EntityType.NOTIFICATION_TARGET: observable = this.notificationService.getNotificationTargetsByIds(entityIds, config); @@ -294,9 +284,7 @@ export class EntityService { observable = this.oauth2Service.findTenantOAuth2ClientInfosByIds(entityIds, config); break; case EntityType.RULE_CHAIN: - observable = this.getEntitiesByIdsObservable( - (id) => this.ruleChainService.getRuleChain(id, config), - entityIds); + observable = this.ruleChainService.getRuleChainsByIds(entityIds, config); break; case EntityType.TB_RESOURCE: observable = this.resourceService.getResourcesByIds(entityIds, config); @@ -817,6 +805,7 @@ export class EntityService { switch (entityType) { case EntityType.USER: entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.displayName.keyName); entityFieldKeys.push(entityFields.email.keyName); entityFieldKeys.push(entityFields.firstName.keyName); entityFieldKeys.push(entityFields.lastName.keyName); @@ -846,6 +835,7 @@ export class EntityService { case EntityType.EDGE: case EntityType.ASSET: entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.displayName.keyName); entityFieldKeys.push(entityFields.type.keyName); entityFieldKeys.push(entityFields.label.keyName); entityFieldKeys.push(entityFields.ownerName.keyName); diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts index 9853d67b85..98f65f924a 100644 --- a/ui-ngx/src/app/core/http/http-utils.ts +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -15,35 +15,93 @@ /// import { InterceptorHttpParams } from '../interceptors/interceptor-http-params'; -import { HttpHeaders } from '@angular/common/http'; +import { HttpHeaders, HttpParams } from '@angular/common/http'; import { InterceptorConfig } from '../interceptors/interceptor-config'; +export type QueryParams = { [param:string]: any }; + export interface RequestConfig { ignoreLoading?: boolean; ignoreErrors?: boolean; resendRequest?: boolean; + queryParams?: QueryParams; +} + +export interface HttpOptionsResult { + headers: HttpHeaders; + params: HttpParams; +} + +export interface HttpUploadOptionsResult { + params: HttpParams; +} + +export function hasRequestConfig(config?: any): boolean { + if (!config) { + return false; + } + return config.hasOwnProperty('ignoreLoading') || config.hasOwnProperty('ignoreErrors') || config.hasOwnProperty('resendRequest') || config.hasOwnProperty('queryParams'); +} + +export function createDefaultHttpOptions(queryParamsOrConfig?: QueryParams | RequestConfig, config?: RequestConfig) { + if (hasRequestConfig(queryParamsOrConfig)) { + return defaultHttpOptionsFromConfig(queryParamsOrConfig as RequestConfig); + } + return defaultHttpOptionsFromParams(queryParamsOrConfig as QueryParams, config); +} + +export function defaultHttpOptionsFromParams(queryParams?: QueryParams, config?: RequestConfig) { + const finalConfig = { + ...config, + ...(queryParams && { queryParams }), + }; + return defaultHttpOptionsFromConfig(finalConfig); } export function defaultHttpOptionsFromConfig(config?: RequestConfig) { if (!config) { config = {}; } - return defaultHttpOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest); + return defaultHttpOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest, config.queryParams); } export function defaultHttpOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, - resendRequest: boolean = false) { + resendRequest: boolean = false, + queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { headers: new HttpHeaders({'Content-Type': 'application/json'}), - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } export function defaultHttpUploadOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, - resendRequest: boolean = false) { + resendRequest: boolean = false, + queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } + +function cleanQueryParams(params?: QueryParams): QueryParams | undefined { + if (!params) { + return undefined; + } + + const entries = Object.entries(params); + + const cleanedEntries = entries.filter( + ([_, value]) => value !== null && value !== undefined + ); + + if (!cleanedEntries.length) { + return undefined; + } + + return Object.fromEntries(cleanedEntries); +} diff --git a/ui-ngx/src/app/core/http/notification.service.ts b/ui-ngx/src/app/core/http/notification.service.ts index 6501e88330..574ad9003c 100644 --- a/ui-ngx/src/app/core/http/notification.service.ts +++ b/ui-ngx/src/app/core/http/notification.service.ts @@ -37,6 +37,7 @@ import { } from '@shared/models/notification.models'; import { User } from '@shared/models/user.model'; import { isNotEmptyStr } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; @Injectable({ providedIn: 'root' @@ -69,6 +70,10 @@ export class NotificationService { return this.http.post('/api/notification/request', notification, defaultHttpOptionsFromConfig(config)); } + public sendEntitiesLimitIncreaseRequest(entityType: EntityType, config?: RequestConfig): Observable { + return this.http.post(`/api/notification/entitiesLimitIncreaseRequest/${entityType}`, defaultHttpOptionsFromConfig(config)); + } + public getNotificationRequestById(id: string, config?: RequestConfig): Observable { return this.http.get(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 76511385f2..810f6ed7c5 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; -import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope } from '@shared/models/resource.models'; @@ -90,6 +90,54 @@ export class ResourceService { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } + public uploadResources(resources: Resource[], config?: RequestConfig): Observable { + let partSize = 100; + partSize = resources.length > partSize ? partSize : resources.length; + const resourceObservables: Observable[] = []; + for (let i = 0; i < partSize; i++) { + resourceObservables.push(this.uploadResource(resources[i], config).pipe(catchError(() => of({} as Resource)))); + } + return forkJoin(resourceObservables).pipe( + mergeMap((resource) => { + resources.splice(0, partSize); + if (resources.length) { + return this.uploadResources(resources, config); + } else { + return of(resource); + } + }) + ); + } + + public uploadResource(resource: Resource, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', resource.data); + formData.append('title', resource.title); + formData.append('resourceType', resource.resourceType); + if (resource.resourceSubType) { + formData.append('resourceSubType', resource.resourceSubType); + } + return this.http.post('/api/resource/upload', formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + + public updatedResourceInfo(resourceId: string, updatedResources: Partial>, config?: RequestConfig): Observable { + return this.http.put(`/api/resource/${resourceId}/info`, updatedResources, defaultHttpOptionsFromConfig(config)); + } + + public updatedResourceData(resourceId: string, data: File, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', data); + return this.http.put(`/api/resource/${resourceId}/data`, formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + public deleteResource(resourceId: string, force = false, config?: RequestConfig) { return this.http.delete(`/api/resource/${resourceId}?force=${force}`, 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 index 5450071ee5..545d29c6af 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -68,6 +68,11 @@ export class RuleChainService { defaultHttpOptionsFromConfig(config)); } + public getRuleChainsByIds(ruleChainIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChains?&ruleChainIds=${ruleChainIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getRuleChain(ruleChainId: string, config?: RequestConfig): Observable { return this.http.get(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/tenant.service.ts b/ui-ngx/src/app/core/http/tenant.service.ts index 361922986f..650032b9f7 100644 --- a/ui-ngx/src/app/core/http/tenant.service.ts +++ b/ui-ngx/src/app/core/http/tenant.service.ts @@ -35,6 +35,10 @@ export class TenantService { return this.http.get>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getTenantsByIds(tenantIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenants?tenantIds=${tenantIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public getTenantInfos(pageLink: PageLink, config?: RequestConfig): Observable> { return this.http.get>(`/api/tenantInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts index f1c1e1d72b..ce16facdb3 100644 --- a/ui-ngx/src/app/core/http/user.service.ts +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -61,6 +61,10 @@ export class UserService { return this.http.get(`/api/user/${userId}`, defaultHttpOptionsFromConfig(config)); } + public getUsersByIds(userIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/users?userIds=${userIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public saveUser(user: User, sendActivationMail: boolean = false, config?: RequestConfig): Observable { let url = '/api/user'; diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 06a6c86ddd..b6a677f22b 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -331,6 +331,11 @@ export class WidgetService { this.widgetsInfoInMemoryCache.delete(fullFqn); } + public getWidgetsBundlesByIds(widgetsBundleIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/widgetsBundles?widgetsBundleIds=${widgetsBundleIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + private loadWidgetsBundleCache(config?: RequestConfig): Observable { if (!this.allWidgetsBundles) { if (!this.loadWidgetsBundleCacheSubject) { diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 9d48e3b2ab..69791908b3 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -109,6 +109,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } else if (errorCode !== Constants.serverErrorCode.credentialsExpired) { unhandled = true; } + } else if (errorCode && errorCode === Constants.serverErrorCode.entitiesLimitExceeded) { + if (!ignoreErrors) { + this.dialogService.entitiesLimitExceeded(errorResponse.error); + } } else if (errorResponse.status === 429) { if (resendRequest) { return this.retryRequest(req, next); diff --git a/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts index ba6e3199ba..332e6cac14 100644 --- a/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts +++ b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts @@ -20,7 +20,7 @@ import { InterceptorConfig } from './interceptor-config'; export class InterceptorHttpParams extends HttpParams { constructor( public interceptorConfig: InterceptorConfig, - params?: { [param: string]: string | string[] } + params?: { [param: string]: string | number | boolean | ReadonlyArray; } ) { super({ fromObject: params }); } diff --git a/ui-ngx/src/app/core/services/calculated-field-form.service.ts b/ui-ngx/src/app/core/services/calculated-field-form.service.ts new file mode 100644 index 0000000000..404676d383 --- /dev/null +++ b/ui-ngx/src/app/core/services/calculated-field-form.service.ts @@ -0,0 +1,117 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DestroyRef, inject, Injectable } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { pairwise, switchMap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { + CalculatedField, + CalculatedFieldConfiguration, + CalculatedFieldEventArguments, + CalculatedFieldType, + OutputStrategyType +} from '@shared/models/calculated-field.models'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { isDefined } from '@core/utils'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { CalculatedFieldsTableEntity } from '@home/components/calculated-fields/calculated-fields-table-config'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Injectable({ providedIn: 'root' }) +export class CalculatedFieldFormService { + private fb = inject(FormBuilder); + private calculatedFieldsService = inject(CalculatedFieldsService); + + buildForm(): FormGroup { + return this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(255)]], + entityId: [null, Validators.required], + type: [CalculatedFieldType.SIMPLE], + debugSettings: [], + configuration: this.fb.control({} as CalculatedFieldConfiguration), + }); + } + + buildAlarmRuleForm(): FormGroup { + return this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(255)]], + entityId: [null, Validators.required], + type: [CalculatedFieldType.ALARM], + debugSettings: [], + configuration: this.fb.group({ + type: [CalculatedFieldType.ALARM], + arguments: this.fb.control({}, Validators.required), + propagate: [false], + propagateToOwner: [false], + propagateToTenant: [false], + propagateRelationTypes: [null], + createRules: [null, Validators.required], + clearRule: [null], + }), + }); + } + + setupTypeChange(form: FormGroup, destroyRef: DestroyRef, isEditActive?: () => boolean): void { + form.get('type').valueChanges.pipe( + pairwise(), + takeUntilDestroyed(destroyRef) + ).subscribe(([prevType, nextType]) => { + const shouldCheck = isEditActive ? isEditActive() : true; + if (shouldCheck && prevType !== nextType) { + if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) || + ![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) { + form.get('configuration').setValue({} as CalculatedFieldConfiguration, { emitEvent: false }); + } + } + }); + } + + prepareConfig(configuration: CalculatedFieldConfiguration): CalculatedFieldConfiguration { + const config = configuration || ({} as CalculatedFieldConfiguration); + if (config.type !== CalculatedFieldType.ALARM) { + if (isDefined(config?.output) && !config?.output?.strategy) { + config.output.strategy = { type: OutputStrategyType.RULE_CHAIN }; + } + } + return config; + } + + testScript( + calculatedFieldId: string, + formValue: CalculatedField, + testDialogFn: (calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit?: boolean, expression?: string) => Observable, + destroyRef: DestroyRef, + expression?: string, + ): Observable { + if (calculatedFieldId) { + return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) + .pipe( + switchMap(event => { + let args = null; + if (event?.arguments) { + try { + args = JSON.parse(event.arguments); + } catch (e) {} + } + return testDialogFn(formValue, args, false, expression); + }), + takeUntilDestroyed(destroyRef) + ); + } + return testDialogFn(formValue, null, false, expression); + } +} diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index b185c77c2d..3ea1923aa3 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -35,9 +35,18 @@ import { LayoutType, WidgetLayout } from '@shared/models/dashboard.models'; -import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils'; +import { + deepClean, + deepClone, + isDefined, + isDefinedAndNotNull, + isNotEmptyStr, + isString, + isUndefined +} from '@core/utils'; import { Datasource, + datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, DatasourceType, defaultLegendConfig, @@ -62,7 +71,11 @@ import { MediaBreakpoints } from '@shared/models/constants'; import { TranslateService } from '@ngx-translate/core'; import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models'; import { maxGridsterCol, maxGridsterRow } from '@home/models/dashboard-component.models'; -import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; +import { + findWidgetModelDefinition, + widgetHasTimewindow, + WidgetModelDefinition +} from '@shared/models/widget/widget-model.definition'; @Injectable({ providedIn: 'root' @@ -78,7 +91,10 @@ export class DashboardUtilsService { public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard { if (!dashboard.configuration) { - dashboard.configuration = {}; + dashboard.configuration = { + entityAliases: {}, + filters: {}, + }; } if (isUndefined(dashboard.configuration.widgets)) { dashboard.configuration.widgets = {}; @@ -166,9 +182,8 @@ export class DashboardUtilsService { dashboard.configuration.filters = {}; } - if (isUndefined(dashboard.configuration.timewindow)) { - dashboard.configuration.timewindow = this.timeService.defaultTimewindow(); - } + dashboard.configuration.timewindow = initModelFromDefaultTimewindow(dashboard.configuration.timewindow, + false, false, this.timeService, true, true); if (isUndefined(dashboard.configuration.settings)) { dashboard.configuration.settings = {}; dashboard.configuration.settings.stateControllerId = 'entity'; @@ -228,12 +243,19 @@ export class DashboardUtilsService { row: widget.row, col: widget.col, }; + dashboard.configuration.timewindow = initModelFromDefaultTimewindow(null, false, false, this.timeService, true, false); return dashboard; } public validateAndUpdateWidget(widget: Widget): Widget { - widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); + const widgetDefinition = widget.config ? findWidgetModelDefinition(widget) : null; + if (widgetDefinition) { + widget.config = this.validateAndUpdateWidgetConfigWithModelDefinition(widget, widgetDefinition); + } else { + widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); + } widget = this.validateAndUpdateWidgetTypeFqn(widget); + this.removeTimewindowConfigIfUnused(widget); if (isDefined((widget as any).title)) { delete (widget as any).title; } @@ -294,8 +316,11 @@ export class DashboardUtilsService { } widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources); if (type === widgetType.latest) { - const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); - widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService); + if (datasourcesHasAggregation(widgetConfig.datasources)) { + const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, + onlyHistoryTimewindow, this.timeService, false); + } } else if (type === widgetType.rpc) { if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) { widgetConfig.targetDevice = { @@ -345,7 +370,34 @@ export class DashboardUtilsService { } } } - return widgetConfig; + return deepClean(widgetConfig, {cleanKeys: ['_hash'], cleanOnlyKey: true}); + } + + public validateAndUpdateWidgetConfigWithModelDefinition(widget: Widget, widgetDefinition: WidgetModelDefinition): WidgetConfig { + const widgetConfig = widget.config; + if (widget.type === widgetType.latest && widgetDefinition.hasTimewindow(widget)) { + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, + widgetDefinition.datasourcesHasOnlyComparisonAggregation(widget), this.timeService, false); + } + return deepClean(widgetConfig, {cleanKeys: ['_hash'], cleanOnlyKey: true}); + } + + private removeTimewindowConfigIfUnused(widget: Widget) { + const hasTimewindow = widgetHasTimewindow(widget); + if (!hasTimewindow || widget.config.useDashboardTimewindow) { + delete widget.config.displayTimewindow; + delete widget.config.timewindow; + + if (!hasTimewindow) { + delete widget.config.useDashboardTimewindow; + } + } + } + + public prepareWidgetForSaving(widget: Widget): Widget { + this.removeTimewindowConfigIfUnused(widget); + widget = deepClean(widget, {cleanKeys: ['_hash'], cleanOnlyKey: true}); + return widget; } public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { diff --git a/ui-ngx/src/app/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts index d87cdfc2db..c73de0435b 100644 --- a/ui-ngx/src/app/core/services/dialog.service.ts +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -36,6 +36,11 @@ import { ErrorAlertDialogData } from '@shared/components/dialog/error-alert-dialog.component'; import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { + EntityLimitExceededDialogComponent, + EntityLimitExceededDialogData +} from '@shared/components/dialog/entity-limit-exceeded-dialog.component'; @Injectable({ providedIn: 'root' @@ -125,6 +130,16 @@ export class DialogService { }).afterClosed(); } + entitiesLimitExceeded(entityLimitData: {entityType: EntityType, limit: number}): Observable { + return this.dialog.open(EntityLimitExceededDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: entityLimitData, + autoFocus: false + }).afterClosed(); + } + private permissionDenied() { this.alert( this.translate.instant('access.permission-denied'), diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index 7b7c828cf3..1dc7146883 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -16,7 +16,13 @@ import { Injectable } from '@angular/core'; import { BreakpointId, Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; -import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; +import { + AliasesInfo, + EntityAlias, + EntityAliases, + EntityAliasInfo, + getEntityAliasId +} from '@shared/models/alias.models'; import { Datasource, DatasourceType, @@ -31,7 +37,7 @@ import { deepClone, isDefinedAndNotNull, 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 { FcRuleNode, FcRuleNote, 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, getFilterId } from '@shared/models/query/query.models'; @@ -72,8 +78,11 @@ export interface RuleNodeConnection { export interface RuleNodesReference { nodes: FcRuleNode[]; connections: RuleNodeConnection[]; + notes?: FcRuleNote[]; originX?: number; originY?: number; + minX?: number; + minY?: number; } @Injectable({ @@ -312,10 +321,11 @@ export class ItemBufferService { return of(theDashboard); } - public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) { + public copyRuleChainObjects(nodes: FcRuleNode[], connections: RuleNodeConnection[], notes: FcRuleNote[] = []) { const ruleNodes: RuleNodesReference = { nodes: [], - connections: [] + connections: [], + notes: [] }; let top = -1; let left = -1; @@ -364,23 +374,51 @@ export class ItemBufferService { right = Math.max(right, node.x + 170); } } + for (const origNote of notes) { + ruleNodes.notes.push({ + id: '', + x: origNote.x, + y: origNote.y, + width: origNote.width, + height: origNote.height, + content: origNote.content, + backgroundColor: origNote.backgroundColor, + borderWidth: origNote.borderWidth, + borderColor: origNote.borderColor, + applyDefaultMarkdownStyle: origNote.applyDefaultMarkdownStyle, + markdownCss: origNote.markdownCss + }); + if (top === -1) { + top = origNote.y; + left = origNote.x; + bottom = origNote.y + (origNote.height || 120); + right = origNote.x + (origNote.width || 200); + } else { + top = Math.min(top, origNote.y); + left = Math.min(left, origNote.x); + bottom = Math.max(bottom, origNote.y + (origNote.height || 120)); + right = Math.max(right, origNote.x + (origNote.width || 200)); + } + } ruleNodes.originX = left + (right - left) / 2; ruleNodes.originY = top + (bottom - top) / 2; + ruleNodes.minX = left; + ruleNodes.minY = top; connections.forEach(connection => { ruleNodes.connections.push(connection); }); this.storeSet(RULE_NODES, ruleNodes); } - public hasRuleNodes(): boolean { + public hasRuleChainObjects(): boolean { return this.storeHas(RULE_NODES); } - public pasteRuleNodes(x: number, y: number): RuleNodesReference { + public pasteRuleChainObjects(x: number, y: number): RuleNodesReference { const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES); if (ruleNodes) { - const deltaX = x - ruleNodes.originX; - const deltaY = y - ruleNodes.originY; + const deltaX = isDefinedAndNotNull(ruleNodes.minX) ? Math.max(x - ruleNodes.originX, -ruleNodes.minX) : x - ruleNodes.originX; + const deltaY = isDefinedAndNotNull(ruleNodes.minY) ? Math.max(y - ruleNodes.originY, -ruleNodes.minY) : y - ruleNodes.originY; for (const node of ruleNodes.nodes) { const component = this.ruleChainService.getRuleNodeComponentByClazz(node.ruleChainType, node.componentClazz); if (component) { @@ -404,6 +442,10 @@ export class ItemBufferService { return null; } } + for (const note of (ruleNodes.notes || [])) { + note.x = Math.round(note.x + deltaX); + note.y = Math.round(note.y + deltaY); + } return ruleNodes; } return null; diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index a3863f8b5e..ab2160ec0f 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -85,7 +85,9 @@ export enum MenuId { domains = 'domains', clients = 'clients', audit_log = 'audit_log', + alarms_center = 'alarms_center', alarms = 'alarms', + alarm_rules = 'alarm_rules', dashboards = 'dashboards', entities = 'entities', devices = 'devices', @@ -96,6 +98,7 @@ export enum MenuId { device_profiles = 'device_profiles', asset_profiles = 'asset_profiles', customers = 'customers', + calculated_fields = 'calculated_fields', rule_chains = 'rule_chains', edge_management = 'edge_management', edges = 'edges', @@ -495,15 +498,35 @@ export const menuSectionMap = new Map([ } ], [ - MenuId.alarms, + MenuId.alarms_center, { - id: MenuId.alarms, + id: MenuId.alarms_center, name: 'alarm.alarms', type: 'link', path: '/alarms', icon: 'mdi:alert-outline' } ], + [ + MenuId.alarms, + { + id: MenuId.alarms, + name: 'alarm.alarm-list', + type: 'link', + path: '/alarms/alarms', + icon: 'mdi:alert-outline' + } + ], + [ + MenuId.alarm_rules, + { + id: MenuId.alarm_rules, + name: 'alarm-rule.alarm-rules', + type: 'link', + path: '/alarms/alarm-rules', + icon: 'tune' + } + ], [ MenuId.dashboards, { @@ -604,6 +627,16 @@ export const menuSectionMap = new Map([ icon: 'supervisor_account' } ], + [ + MenuId.calculated_fields, + { + id: MenuId.calculated_fields, + name: 'entity.type-calculated-fields', + type: 'link', + path: '/calculatedFields', + icon: 'mdi:function-variant', + } + ], [ MenuId.rule_chains, { @@ -783,7 +816,8 @@ const defaultUserMenuMap = new Map([ {id: MenuId.domains}, {id: MenuId.clients} ] - } + }, + {id: MenuId.audit_log} ] } ] @@ -792,7 +826,13 @@ const defaultUserMenuMap = new Map([ Authority.TENANT_ADMIN, [ {id: MenuId.home}, - {id: MenuId.alarms}, + { + id: MenuId.alarms_center, + pages: [ + {id: MenuId.alarms}, + {id: MenuId.alarm_rules} + ] + }, {id: MenuId.dashboards}, { id: MenuId.entities, @@ -811,6 +851,7 @@ const defaultUserMenuMap = new Map([ ] }, {id: MenuId.customers}, + {id: MenuId.calculated_fields}, {id: MenuId.rule_chains}, { id: MenuId.edge_management, diff --git a/ui-ngx/src/app/core/services/mobile.service.ts b/ui-ngx/src/app/core/services/mobile.service.ts index 295605bab1..5d2f82bf37 100644 --- a/ui-ngx/src/app/core/services/mobile.service.ts +++ b/ui-ngx/src/app/core/services/mobile.service.ts @@ -106,9 +106,9 @@ export class MobileService { } } - public handleMobileNavigation(path?: string, params?: Params) { + public handleMobileNavigation(path?: string, params?: Params, queryParams?: Params) { if (this.mobileApp) { - this.mobileChannel.callHandler(navigationHandler, path, params); + this.mobileChannel.callHandler(navigationHandler, path, params, queryParams); } } diff --git a/ui-ngx/src/app/core/services/time.service.ts b/ui-ngx/src/app/core/services/time.service.ts index 1b9c90d13b..aedf250758 100644 --- a/ui-ngx/src/app/core/services/time.service.ts +++ b/ui-ngx/src/app/core/services/time.service.ts @@ -146,8 +146,8 @@ export class TimeService { return this.boundMaxInterval(max); } - public defaultTimewindow(): Timewindow { - return defaultTimewindow(this); + public defaultTimewindow(isDashboard = false): Timewindow { + return defaultTimewindow(this, isDashboard); } private toBound(value: number, min: number, max: number, defValue: number): number { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 5caf198996..66baf3f9a9 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -97,7 +97,6 @@ export class UtilsService { color: this.getMaterialColor(0), funcBody: this.getPredefinedFunctionBody('Sin'), settings: {}, - _hash: Math.random() }; defaultDatasource: Datasource = { @@ -153,8 +152,7 @@ export class UtilsService { type: DataKeyType.alarm, label: this.translate.instant(alarmFields[name].name), color: this.getMaterialColor(i), - settings: {}, - _hash: Math.random() + settings: {} }; this.defaultAlarmDataKeys.push(dataKey); } @@ -267,8 +265,7 @@ export class UtilsService { type, label, funcBody: keyInfo.funcBody, - settings: {}, - _hash: Math.random() + settings: {} }; if (keyInfo.units) { dataKey.units = keyInfo.units; @@ -457,11 +454,11 @@ export class UtilsService { return base64toObj(b64Encoded); } - public applyCssToElement(renderer: Renderer2, element: any, cssClassPrefix: string, css: string): string { + public applyCssToElement(renderer: Renderer2, element: any, cssClassPrefix: string, css: string, addTbDefaultClass: boolean = false): string { const cssParser = new cssjs(); cssParser.testMode = false; const cssClass = `${cssClassPrefix}-${guid()}`; - cssParser.cssPreviewNamespace = cssClass; + cssParser.cssPreviewNamespace = addTbDefaultClass ? 'tb-default .' + cssClass : cssClass; cssParser.createStyleElement(cssClass, css); renderer.addClass(element, cssClass); return cssClass; diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index a858865b0c..466652040f 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -201,6 +201,23 @@ export function deleteNullProperties(obj: any) { }); } +export function deleteFalseProperties(obj: Record): void { + if (isUndefinedOrNull(obj)) { + return; + } + Object.keys(obj).forEach((propName) => { + if (obj[propName] === false || isUndefinedOrNull(obj[propName])) { + delete obj[propName]; + } else if (isObject(obj[propName])) { + deleteFalseProperties(obj[propName]); + } else if (Array.isArray(obj[propName])) { + (obj[propName] as any[]).forEach((elem) => { + deleteFalseProperties(elem); + }); + } + }); +} + export function objToBase64(obj: any): string { const json = JSON.stringify(obj); return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, @@ -789,6 +806,74 @@ export function deepTrim(obj: T): T { }, (Array.isArray(obj) ? [] : {}) as T); } +const isValidValue = (value: any): boolean => { + return ( + value !== undefined && + value !== null && + value !== '' && + !Number.isNaN(value) + ); +}; + +export function deepClean | any[]>(obj: T, { + cleanKeys = [], + cleanOnlyKey = false +} = {}): T { + const keysToRemove = new Set(cleanKeys); + + const clean = (input: any): any => { + if (Array.isArray(input)) { + const result: any[] = []; + for (const item of input) { + const value = clean(item); + + if (cleanOnlyKey) { + result.push(value); + continue; + } + + if (isValidValue(value)) { + const isEmptyArray = Array.isArray(value) && value.length === 0; + const isEmptyObj = isLiteralObject(value) && Object.keys(value).length === 0; + + if (!isEmptyArray && !isEmptyObj) { + result.push(value); + } + } + } + return result; + } + + if (isLiteralObject(input)) { + const result: Record = {}; + + for (const key in input) { + if (keysToRemove.has(key)) continue; + + const value = clean(input[key]); + + if (cleanOnlyKey) { + result[key] = value; + continue; + } + + if (isValidValue(value)) { + const isEmptyArray = Array.isArray(value) && value.length === 0; + const isEmptyObj = isLiteralObject(value) && Object.keys(value).length === 0; + + if (!isEmptyArray && !isEmptyObj) { + result[key] = value; + } + } + } + return result; + } + return input; + }; + + return clean(obj); +} + export function generateSecret(length?: number): string { if (isUndefined(length) || length == null) { length = 1; diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 7ccad1c91e..8f721b87dd 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -124,6 +124,7 @@ import * as TbErrorComponent from '@shared/components/tb-error.component'; import * as TbCheatSheetComponent from '@shared/components/cheatsheet.component'; import * as BreadcrumbComponent from '@shared/components/breadcrumb.component'; import * as UserMenuComponent from '@shared/components/user-menu.component'; +import * as TimeUnitInputComponent from '@shared/components/time-unit-input.component'; import * as TimewindowComponent from '@shared/components/time/timewindow.component'; import * as TimewindowPanelComponent from '@shared/components/time/timewindow-panel.component'; import * as TimeintervalComponent from '@shared/components/time/timeinterval.component'; @@ -181,6 +182,10 @@ import * as OtaPackageAutocompleteComponent from '@shared/components/ota-package import * as WidgetsBundleSearchComponent from '@shared/components/widgets-bundle-search.component'; import * as CopyButtonComponent from '@shared/components/button/copy-button.component'; import * as TogglePasswordComponent from '@shared/components/button/toggle-password.component'; +import * as WidgetButtonComponent from '@shared/components/button/widget-button.component'; +import * as WidgetButtonToggleComponent from '@shared/components/button/widget-button-toggle.component'; +import * as PhoneInputComponent from '@shared/components/phone-input.component'; +import * as TbPopoverService from '@shared/components/popover.service'; import * as ProtobufContentComponent from '@shared/components/protobuf-content.component'; import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component'; import * as StringItemsListComponent from '@shared/components/string-items-list.component'; @@ -193,8 +198,6 @@ import * as HintTooltipIconComponent from '@shared/components/hint-tooltip-icon. import * as ScrollGridComponent from '@shared/components/grid/scroll-grid.component'; import * as GalleryImageInputComponent from '@shared/components/image/gallery-image-input.component'; import * as MultipleGalleryImageInputComponent from '@shared/components/image/multiple-gallery-image-input.component'; -import * as TbPopoverService from '@shared/components/popover.service'; - import * as CssUnitSelectComponent from '@home/components/widget/lib/settings/common/css-unit-select.component'; import * as WidgetActionsPanelComponent from '@home/components/widget/config/basic/common/widget-actions-panel.component'; @@ -465,6 +468,7 @@ class ModulesMap implements IModulesMap { '@shared/components/cheatsheet.component': TbCheatSheetComponent, '@shared/components/breadcrumb.component': BreadcrumbComponent, '@shared/components/user-menu.component': UserMenuComponent, + '@shared/components/time-unit-input.component': TimeUnitInputComponent, '@shared/components/time/timewindow.component': TimewindowComponent, '@shared/components/time/timewindow-panel.component': TimewindowPanelComponent, '@shared/components/time/timeinterval.component': TimeintervalComponent, @@ -529,6 +533,10 @@ class ModulesMap implements IModulesMap { '@shared/components/widgets-bundle-search.component': WidgetsBundleSearchComponent, '@shared/components/button/copy-button.component': CopyButtonComponent, '@shared/components/button/toggle-password.component': TogglePasswordComponent, + '@shared/components/button/widget-button.component': WidgetButtonComponent, + '@shared/components/button/widget-button-toggle.component': WidgetButtonToggleComponent, + '@shared/components/popover.service': TbPopoverService, + '@shared/components/phone-input.component': PhoneInputComponent, '@shared/components/protobuf-content.component': ProtobufContentComponent, '@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent, '@shared/components/string-items-list.component': StringItemsListComponent, @@ -541,8 +549,6 @@ class ModulesMap implements IModulesMap { '@shared/components/grid/scroll-grid.component': ScrollGridComponent, '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, - '@shared/components/popover.service': TbPopoverService, - '@home/components/alarm/alarm-filter-config.component': AlarmFilterConfigComponent, '@home/components/alarm/alarm-comment-dialog.component': AlarmCommentDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 165f83e8e5..6802eaaf8b 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+@if (isLoading$ | async) { + + +} @else { +
+}
@@ -35,12 +38,16 @@ ai-models.name - - {{ 'ai-models.name-required' | translate }} - - - {{ 'ai-models.name-max-length' | translate }} - + @if (aiModelForms.get('name').hasError('required') || aiModelForms.get('name').hasError('pattern')) { + + {{ 'ai-models.name-required' | translate }} + + } + @if (aiModelForms.get('name').hasError('maxlength')) { + + {{ 'ai-models.name-max-length' | translate }} + + }
@@ -50,9 +57,11 @@ ai-models.ai-provider - - {{providerTranslationMap.get(provider) | translate}} - + @for (provider of providerMap; track provider) { + + {{providerTranslationMap.get(provider) | translate}} + + }
@@ -61,30 +70,36 @@ ai-models.personal-access-token - - {{ 'ai-models.personal-access-token-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('required') || + aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('pattern')) { + + {{ 'ai-models.personal-access-token-required' | translate }} + + } } @if (providerFieldsList.includes('projectId')) { ai-models.project-id - - {{ 'ai-models.project-id-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.projectId').hasError('required') || + aiModelForms.get('configuration.providerConfig.projectId').hasError('pattern')) { + + {{ 'ai-models.project-id-required' | translate }} + + } } @if (providerFieldsList.includes('location')) { ai-models.location - - {{ 'ai-models.location-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.location').hasError('required') || + aiModelForms.get('configuration.providerConfig.location').hasError('pattern')) { + + {{ 'ai-models.location-required' | translate }} + + } } @if (providerFieldsList.includes('serviceAccountKey')) { @@ -104,10 +119,12 @@ ai-models.endpoint - - {{ 'ai-models.endpoint-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.endpoint').hasError('required') || + aiModelForms.get('configuration.providerConfig.endpoint').hasError('pattern')) { + + {{ 'ai-models.endpoint-required' | translate }} + + } } @if (providerFieldsList.includes('serviceVersion')) { @@ -120,10 +137,12 @@ ai-models.baseurl - - {{ 'ai-models.baseurl-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.baseUrl').hasError('required') || + aiModelForms.get('configuration.providerConfig.baseUrl').hasError('pattern')) { + + {{ 'ai-models.baseurl-required' | translate }} + + } } @if (providerFieldsList.includes('apiKey')) { @@ -131,30 +150,36 @@ ai-models.api-key - - {{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }} - + @if (aiModelForms.get('configuration.providerConfig.apiKey').hasError('required') || + aiModelForms.get('configuration.providerConfig.apiKey').hasError('pattern')) { + + {{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }} + + } } @if (providerFieldsList.includes('region')) { ai-models.region - - {{ 'ai-models.region-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.region').hasError('required') || + aiModelForms.get('configuration.providerConfig.region').hasError('pattern')) { + + {{ 'ai-models.region-required' | translate }} + + } } @if (providerFieldsList.includes('accessKeyId')) { ai-models.access-key-id - - {{ 'ai-models.access-key-id-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('required') || + aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('pattern')) { + + {{ 'ai-models.access-key-id-required' | translate }} + + } } @if (providerFieldsList.includes('secretAccessKey')) { @@ -162,10 +187,12 @@ ai-models.secret-access-key - - {{ 'ai-models.secret-access-key-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('required') || + aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('pattern')) { + + {{ 'ai-models.secret-access-key-required' | translate }} + + } } @if (provider === aiProvider.OLLAMA) { @@ -185,19 +212,23 @@ ai-models.username - - {{ 'ai-models.username-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.auth.username').hasError('required') || + aiModelForms.get('configuration.providerConfig.auth.username').hasError('pattern')) { + + {{ 'ai-models.username-required' | translate }} + + } ai-models.password - - {{ 'ai-models.password-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.auth.password').hasError('required') || + aiModelForms.get('configuration.providerConfig.auth.password').hasError('pattern')) { + + {{ 'ai-models.password-required' | translate }} + + } } @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) { @@ -205,10 +236,12 @@ ai-models.token - - {{ 'ai-models.token-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.auth.token').hasError('required') || + aiModelForms.get('configuration.providerConfig.auth.token').hasError('pattern')) { + + {{ 'ai-models.token-required' | translate }} + + } }
@@ -225,6 +258,7 @@ additionalClass="tb-suffix-show-on-hover" appearance="outline" panelWidth="" + showInlineError required [label]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name': 'ai-models.model-id') | translate" [errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate" @@ -239,14 +273,15 @@ - - warning - + @if (aiModelForms.get('configuration').get('temperature').hasError('min')) { + + warning + + }
} @@ -258,15 +293,16 @@ - - warning - + @if (aiModelForms.get('configuration').get('topP').hasError('min') || + aiModelForms.get('configuration').get('topP').hasError('max')) { + + warning + + }
} @@ -278,14 +314,15 @@ - - warning - + @if (aiModelForms.get('configuration').get('topK').hasError('min')) { + + warning + + } } diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index d5d5f20ed8..77948333a1 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -43,6 +43,7 @@ import { TranslateService } from '@ngx-translate/core'; export interface AIModelDialogData { AIModel?: AiModel; isAdd?: boolean; + name?: string; } @Component({ @@ -130,6 +131,10 @@ export class AIModelDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html index 8655789594..28d0fa3904 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html @@ -26,25 +26,31 @@
- - -
-
- check_circle + @if (!showCheckSuccess && !checkErrMsg) { + + + } + @if (showCheckSuccess) { +
+
+ check_circle +
+ {{ "ai-models.check-connectivity-success" | translate }}
- {{ "ai-models.check-connectivity-success" | translate }} -
-
-
- cancel + } + @if (checkErrMsg) { +
+
+ cancel +
+
{{ "ai-models.check-connectivity-failed" | translate }}
+ +
-
{{ "ai-models.check-connectivity-failed" | translate }}
- - -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html new file mode 100644 index 0000000000..413b72e614 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html @@ -0,0 +1,51 @@ + + + +

{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}

+ +
+
+
+ + + +
+ +
+
+
+
+ + @if (!data.readonly) { + + } +
+ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts new file mode 100644 index 0000000000..5e36be0893 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts @@ -0,0 +1,61 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; + +export interface AlarmRuleDetailsDialogData { + alarmDetails: string; + readonly: boolean; +} + +@Component({ + selector: 'tb-edit-alarm-details-dialog', + templateUrl: './alarm-rule-details-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + standalone: false +}) +export class AlarmRuleDetailsDialogComponent extends DialogComponent { + + alarmDetailsControl: FormControl; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.alarmDetailsControl = this.fb.control(this.data.alarmDetails); + if (this.data.readonly) { + this.alarmDetailsControl.disable(); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.alarmDetailsControl.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html new file mode 100644 index 0000000000..48f4e8a5af --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -0,0 +1,173 @@ + +
+ +

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

+ +
+ +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'alarm-rule.alarm-type' | translate }} + + alarm-rule.alarm-type-hint + @if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) { + + @if (fieldFormGroup.get('name').hasError('required')) { + {{ 'alarm-rule.alarm-type-required' | translate }} + } @else if (fieldFormGroup.get('name').hasError('maxlength')) { + {{ 'alarm-rule.alarm-type-max-length' | translate }} + } + + } + + +
+ @if (!data.entityId) { + + + } +
+ +
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
{{ 'alarm-rule.create-conditions' | translate }}
+
+ + +
+
+
+
{{ 'alarm-rule.clear-condition' | translate }}
+
+
+ + +
+ +
+ @if (!configFormGroup.get('clearRule').value) { +
+ alarm-rule.no-clear-alarm-rule +
+ } +
+ +
+
+
+ + + {{ 'alarm-rule.advanced-settings' | translate }} + + +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + + } +
+
+ + {{ 'alarm-rule.propagate-alarm-to-owner' | translate }} + +
+
+ + {{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} + +
+
+
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss new file mode 100644 index 0000000000..fc9590f115 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.calculated-field-dialog-container { + width: 869px; + max-width: 100%; + + .tb-disabled { + color: rgba(0, 0, 0, 0.38); + } +} + +.clear-alarm-rule { + border: 1px solid rgba(0, 0, 0, 0.12); + border-left-width: 4px; + border-left-color: green; + border-radius: 4px; + padding: 8px; + min-width: 0; +} + +.button-icon { + color: rgba(0, 0, 0, 0.38); + min-width: 40px; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts new file mode 100644 index 0000000000..10793723db --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -0,0 +1,243 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { + CalculatedFieldAlarmRule, + CalculatedFieldArgument, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; +import { + AlarmRule, + AlarmRuleConditionType, + alarmRuleEntityTypeList, + AlarmRuleExpressionType, + AlarmRuleTestScriptFn +} from "@shared/models/alarm-rule.models"; +import { deepTrim } from "@core/utils"; +import { combineLatest, Observable } from "rxjs"; +import { debounceTime, startWith } from "rxjs/operators"; +import { RelationTypes } from "@shared/models/relation.models"; +import { StringItemsOption } from "@shared/components/string-items-list.component"; +import { BaseData } from "@shared/models/base-data"; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { EntitySelectComponent } from '@shared/components/entity/entity-select.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; + +export interface AlarmRuleDialogData { + value?: CalculatedFieldAlarmRule; + buttonTitle: string; + entityId: EntityId; + tenantId: string; + entityName?: string; + ownerId: EntityId; + additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedFieldAlarmRule) => void>; + isDirty?: boolean; + getTestScriptDialogFn: AlarmRuleTestScriptFn, +} + +@Component({ + selector: 'tb-alarm-rule-dialog', + templateUrl: './alarm-rule-dialog.component.html', + styleUrls: ['./alarm-rule-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class AlarmRuleDialogComponent extends DialogComponent { + + @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; + + fieldFormGroup: FormGroup ; + + additionalDebugActionConfig = this.data.value?.id ? { + ...this.data.additionalDebugActionConfig, + action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), + } : null; + + readonly EntityType = EntityType; + readonly entityTypeTranslations = entityTypeTranslations; + readonly alarmRuleEntityTypeList = alarmRuleEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + readonly ScriptLanguage = ScriptLanguage; + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + entityName = this.data.entityName; + ownerId = this.data.ownerId; + + disabledClearRuleButton = false; + disabledArguments = false; + isLoading = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, + protected dialogRef: MatDialogRef, + private alarmRulesService: AlarmRulesService, + private destroyRef: DestroyRef, + private cfFormService: CalculatedFieldFormService) { + super(store, router, dialogRef); + this.fieldFormGroup = this.cfFormService.buildAlarmRuleForm(); + this.applyDialogData(); + this.updateRulesValidators(); + + if (this.data.isDirty) { + this.fieldFormGroup.markAsDirty(); + } + + this.fieldFormGroup.get('configuration.arguments').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateRulesValidators(); + }); + + if (!this.data.entityId) { + combineLatest([ + this.fieldFormGroup.get('entityId')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('entityId')!.value)), + this.fieldFormGroup.get('name')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('name')!.value)) + ]).pipe( + debounceTime(50), + takeUntilDestroyed() + ).subscribe(([entityId, name]) => { + this.disabledArguments = !entityId || !name?.length; + const argsControl = this.fieldFormGroup.get('configuration.arguments')!; + if (this.disabledArguments) { + argsControl.disable({ emitEvent: false }); + } else { + argsControl.enable({ emitEvent: false }); + } + }); + } + } + + get configFormGroup(): FormGroup { + return this.fieldFormGroup.get('configuration') as FormGroup; + } + + get arguments(): Record { + return this.fieldFormGroup.get('configuration.arguments').value; + } + + public removeClearAlarmRule() { + this.configFormGroup.patchValue({clearRule: null}); + this.fieldFormGroup.markAsDirty(); + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + type: AlarmRuleConditionType.SIMPLE, + expression: { + type: AlarmRuleExpressionType.SIMPLE + } + } + }; + this.configFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + get fromGroupValue(): CalculatedFieldAlarmRule { + return deepTrim(this.fieldFormGroup.value as CalculatedFieldAlarmRule); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + if (this.fieldFormGroup.valid && Object.keys(this.arguments ?? {}).length > 0) { + this.isLoading = true; + const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}; + alarmRule.configuration.type = CalculatedFieldType.ALARM; + + this.alarmRulesService.saveAlarmRule(alarmRule) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: calculatedField => this.dialogRef.close(calculatedField), + error: () => this.isLoading = false + }); + } else { + this.fieldFormGroup.get('name').markAsTouched(); + this.entitySelect?.entityAutocompleteMarkAsTouched(); + } + } + + private applyDialogData(): void { + const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {}; + this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false}); + } + + onTestScript(expression: string): Observable { + return this.cfFormService.testScript( + this.data.value?.id?.id, + this.fromGroupValue, + this.data.getTestScriptDialogFn, + this.destroyRef, + expression + ); + } + + private updateRulesValidators(): void { + if (Object.keys(this.arguments ?? {}).length > 0) { + this.fieldFormGroup.get('configuration.createRules').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration.clearRule').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagate').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagateToOwner').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagateToTenant').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagateRelationTypes').enable({emitEvent: false}); + this.disabledClearRuleButton = false; + } else { + this.fieldFormGroup.get('configuration.createRules').disable({emitEvent: false}); + this.fieldFormGroup.get('configuration.clearRule').disable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagate').disable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagateToOwner').disable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagateToTenant').disable({emitEvent: false}); + this.fieldFormGroup.get('configuration.propagateRelationTypes').disable({emitEvent: false}); + this.disabledClearRuleButton = true; + } + } + get predefinedTypeValues(): StringItemsOption[] { + return RelationTypes.map(type => ({ + name: type, + value: type + })); + } + + changeEntity(entity: BaseData): void { + this.entityName = entity.name; + if (this.isAssignedToCustomer(entity as AssetInfo | DeviceInfo)) { + this.ownerId = (entity as AssetInfo | DeviceInfo).customerId; + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html new file mode 100644 index 0000000000..929e7d581e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html @@ -0,0 +1,134 @@ + +@if (panelMode) { + +} @else { + @if (buttonMode) { + + } @else { +
+
+
alarm-rule.alarm-rule-type-list
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (alarmRuleFilterConfigForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+ } +} + +
+ +
+ + + + +
+
+
+ +
+
+
alarm-rule.alarm-rule-type-list
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (alarmRuleFilterConfigForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss new file mode 100644 index 0000000000..c34e56296f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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; + max-width: 100%; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +::ng-deep { + .tb-alarm-filter-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + + + tb-entity-subtype-list { + flex: 1; + @media #{$mat-gt-xs} { + width: 180px; + } + .mdc-evolution-chip-set__chips { + width: 100%; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts new file mode 100644 index 0000000000..15e92c7ee6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts @@ -0,0 +1,292 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + DestroyRef, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { TranslateService } from '@ngx-translate/core'; +import { deepClone, isArraysEqualIgnoreUndefined, isDefinedAndNotNull, isEmpty, isUndefinedOrNull } from '@core/utils'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { fromEvent, Subscription } from 'rxjs'; +import { POSITION_MAP } from '@shared/models/overlay.models'; +import { UtilsService } from '@core/services/utils.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { alarmRuleEntityTypeList, AlarmRuleFilterConfig } from "@shared/models/alarm-rule.models"; + +export const ALARM_FILTER_CONFIG_DATA = new InjectionToken('AlarmRuleFilterConfigData'); + +export interface AlarmRuleFilterConfigData { + panelMode: boolean; + userMode: boolean; + alarmRuleFilterConfig: AlarmRuleFilterConfig; + initialAlarmRuleFilterConfig?: AlarmRuleFilterConfig; +} + +@Component({ + selector: 'tb-alarm-rule-filter-config', + templateUrl: './alarm-rule-filter-config.component.html', + styleUrls: ['./alarm-rule-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterConfigComponent), + multi: true + } + ], + standalone: false +}) +export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAccessor { + + @ViewChild('alarmRuleFilterPanel') + alarmRuleFilterPanel: TemplateRef; + + @Input() disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @Input() + initialAlarmRuleFilterConfig: AlarmRuleFilterConfig = { + name: [], + entityType: null, + entities: [] + }; + + panelMode = false; + + buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title'); + + alarmRuleFilterConfigForm: FormGroup; + + alarmFilterOverlayRef: OverlayRef; + + panelResult: AlarmRuleFilterConfig = null; + + entityType = EntityType; + + listEntityTypes = alarmRuleEntityTypeList; + entityTypeTranslations = entityTypeTranslations; + + private alarmRuleFilterConfig: AlarmRuleFilterConfig; + private resizeWindows: Subscription; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA) + private data: AlarmRuleFilterConfigData | undefined, + @Optional() + private overlayRef: OverlayRef, + private fb: FormBuilder, + private translate: TranslateService, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.alarmRuleFilterConfig = this.data.alarmRuleFilterConfig; + this.initialAlarmRuleFilterConfig = this.data.initialAlarmRuleFilterConfig; + if (this.panelMode && !this.initialAlarmRuleFilterConfig) { + this.initialAlarmRuleFilterConfig = deepClone(this.alarmRuleFilterConfig); + } + } + this.alarmRuleFilterConfigForm = this.fb.group({ + name: [null, []], + entityType: [null, []], + entities: [null, []] + }); + this.alarmRuleFilterConfigForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + if (!this.buttonMode) { + this.alarmRuleConfigUpdated(this.alarmRuleFilterConfigForm.value); + } + } + ); + if (this.panelMode) { + this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleFilterConfigForm.disable({emitEvent: false}); + } else { + this.alarmRuleFilterConfigForm.enable({emitEvent: false}); + } + } + + writeValue(alarmRuleFilterConfig?: AlarmRuleFilterConfig): void { + this.alarmRuleFilterConfig = alarmRuleFilterConfig; + if (!this.initialAlarmRuleFilterConfig && alarmRuleFilterConfig) { + this.initialAlarmRuleFilterConfig = deepClone(alarmRuleFilterConfig); + } + this.updateButtonDisplayValue(); + this.updateAlarmRuleConfigForm(alarmRuleFilterConfig); + } + + toggleAlarmRuleFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); + + this.alarmFilterOverlayRef = this.overlay.create(config); + this.alarmFilterOverlayRef.backdropClick().subscribe(() => { + this.alarmFilterOverlayRef.dispose(); + }); + this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmRuleFilterPanel, + this.viewContainerRef)); + this.resizeWindows = fromEvent(window, 'resize').subscribe(() => { + this.alarmFilterOverlayRef.updatePosition(); + }); + } + + cancel() { + this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig); + this.alarmRuleFilterConfigForm.markAsPristine(); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.alarmFilterOverlayRef.dispose(); + } + } + + update() { + this.alarmRuleConfigUpdated(this.alarmRuleFilterConfigForm.value); + this.alarmRuleFilterConfigForm.markAsPristine(); + if (this.panelMode) { + this.panelResult = this.alarmRuleFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.alarmFilterOverlayRef.dispose(); + } + } + + reset() { + const alarmRuleFilterConfig = this.alarmRuleFilterConfigFromFormValue(this.alarmRuleFilterConfigForm.value); + if (!this.alarmRuleFilterConfigEquals(alarmRuleFilterConfig, this.initialAlarmRuleFilterConfig)) { + this.updateAlarmRuleConfigForm(this.initialAlarmRuleFilterConfig); + this.alarmRuleFilterConfigForm.markAsDirty(); + } + } + + private alarmRuleFilterConfigEquals = (filter1?: AlarmRuleFilterConfig, filter2?: AlarmRuleFilterConfig): boolean => { + if (filter1 === filter2) { + return true; + } + if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) { + return true; + } else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) { + if (!isArraysEqualIgnoreUndefined(filter1.name, filter2.name)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) { + return false; + } + return filter1.entityType === filter2.entityType; + } + return false; + }; + + private updateAlarmRuleConfigForm(alarmRuleFilterConfig?: AlarmRuleFilterConfig) { + this.alarmRuleFilterConfigForm.patchValue({ + name: alarmRuleFilterConfig?.name ?? [], + entityType: alarmRuleFilterConfig?.entityType ?? null, + entities: alarmRuleFilterConfig?.entities ?? [], + }, {emitEvent: false}); + } + + private alarmRuleConfigUpdated(formValue: any) { + this.alarmRuleFilterConfig = this.alarmRuleFilterConfigFromFormValue(formValue); + this.updateButtonDisplayValue(); + this.propagateChange(this.alarmRuleFilterConfig); + } + + private alarmRuleFilterConfigFromFormValue(formValue: any): AlarmRuleFilterConfig { + return { + name: formValue?.name ?? [], + entityType: formValue?.entityType ?? null, + entities: formValue?.entities ?? [], + }; + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (this.alarmRuleFilterConfig?.name?.length) { + filterTextParts.push(this.alarmRuleFilterConfig.name.map((type) => this.customTranslate(type)).join(', ')); + } + if (this.alarmRuleFilterConfig?.entityType) { + filterTextParts.push(this.translate.instant( entityTypeTranslations.get(this.alarmRuleFilterConfig.entityType).type)); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('alarm-rule.filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + + private customTranslate(entity: string) { + return this.utils.customTranslation(entity, entity); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html new file mode 100644 index 0000000000..3c357b842d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html @@ -0,0 +1,20 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss new file mode 100644 index 0000000000..c6571e8450 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + padding-right: 8px; + overflow: hidden; + max-width: 100%; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts new file mode 100644 index 0000000000..4dc6c6dbb9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { CalculatedFieldAlarmRule, CalculatedFieldsQuery } from "@shared/models/calculated-field.models"; +import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; + +@Component({ + selector: 'tb-alarm-rule-table-header', + templateUrl: './alarm-rule-table-header.component.html', + styleUrls: ['./alarm-rule-table-header.component.scss'], + standalone: false +}) +export class AlarmRuleTableHeaderComponent extends EntityTableHeaderComponent { + + get alarmRuleTableConfig(): AlarmRulesTableConfig { + return this.entitiesTableConfig as AlarmRulesTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + alarmRuleFilterChanged(alarmRuleFilterConfig: CalculatedFieldsQuery) { + this.alarmRuleTableConfig.alarmRuleFilterConfig = alarmRuleFilterConfig; + this.alarmRuleTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts new file mode 100644 index 0000000000..858b23f3dd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { AlarmRuleDialogComponent } from "@home/components/alarm-rules/alarm-rule-dialog.component"; +import { CreateCfAlarmRulesComponent } from "@home/components/alarm-rules/create-cf-alarm-rules.component"; +import { CfAlarmRuleComponent } from "@home/components/alarm-rules/cf-alarm-rule.component"; +import { CfAlarmRuleConditionComponent } from "@home/components/alarm-rules/cf-alarm-rule-condition.component"; +import { + CfAlarmRuleConditionDialogComponent +} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; +import { CfAlarmScheduleComponent } from "@home/components/alarm-rules/cf-alarm-schedule.component"; +import { CfAlarmScheduleDialogComponent } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; +import { + EntityDebugSettingsButtonComponent +} from "@home/components/entity/debug/entity-debug-settings-button.component"; +import { AlarmRuleFilterTextComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-text.component"; +import { + CalculatedFieldArgumentsTableModule +} from "@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module"; +import { + AlarmRuleFilterPredicateListComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component"; +import { + AlarmRuleFilterPredicateComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate.component"; +import { + AlarmRuleFilterPredicateValueComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component"; +import { + AlarmRuleComplexFilterPredicateDialogComponent +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component"; +import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; +import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; +import { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.component"; +import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; +import { + AlarmRuleFilterPredicateNoDataValueComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component"; +import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component'; + +@NgModule({ + declarations: [ + AlarmRuleDialogComponent, + CreateCfAlarmRulesComponent, + CfAlarmRuleComponent, + CfAlarmRuleConditionComponent, + CfAlarmRuleConditionDialogComponent, + CfAlarmScheduleComponent, + CfAlarmScheduleDialogComponent, + AlarmRuleFilterTextComponent, + AlarmRuleFilterListComponent, + AlarmRuleFilterDialogComponent, + AlarmRuleFilterPredicateListComponent, + AlarmRuleFilterPredicateComponent, + AlarmRuleFilterPredicateValueComponent, + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleDetailsDialogComponent, + AlarmRuleFilterConfigComponent, + AlarmRuleTableHeaderComponent, + AlarmRuleFilterPredicateNoDataValueComponent, + AlarmRulesComponent + ], + imports: [ + CommonModule, + SharedModule, + EntityDebugSettingsButtonComponent, + CalculatedFieldArgumentsTableModule + ], + exports: [ + AlarmRuleDialogComponent, + AlarmRulesComponent + ] +}) +export class AlarmRuleModule { } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts new file mode 100644 index 0000000000..f5043bdcee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -0,0 +1,434 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + checkBoxCell, + DateEntityTableColumn, + EntityLinkTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Direction } from '@shared/models/page/sort-order'; +import { MatDialog } from '@angular/material/dialog'; +import { PageLink } from '@shared/models/page/page-link'; +import { EMPTY, Observable, of } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { DestroyRef, Renderer2 } from '@angular/core'; +import { EntityDebugSettings } from '@shared/models/entity.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; +import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; +import { + ArgumentEntityType, + ArgumentType, + CalculatedFieldAlarmRule, + CalculatedFieldAlarmRuleInfo, + CalculatedFieldEventArguments, + CalculatedFieldsQuery, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, +} from '@shared/models/calculated-field.models'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { DatePipe } from '@angular/common'; +import { + AlarmRuleDialogComponent, + AlarmRuleDialogData +} from "@home/components/alarm-rules/alarm-rule-dialog.component"; +import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; +import { UtilsService } from "@core/services/utils.service"; +import { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils"; +import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; +import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; +import { DebugEventType, EventType } from '@shared/models/event.models'; +import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestScriptDialogData +} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; +import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.component'; +import { Router } from '@angular/router'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component'; + +export type AlarmRuleTableEntity = CalculatedFieldAlarmRule | CalculatedFieldAlarmRuleInfo; + +export class AlarmRulesTableConfig extends EntityTableConfig { + + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + additionalDebugActionConfig = { + title: this.translate.instant('action.see-debug-events'), + action: (calculatedField: AlarmRuleTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField), + }; + + alarmRuleFilterConfig: CalculatedFieldsQuery; + + constructor(private alarmRulesService: AlarmRulesService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe, + private entityId: EntityId = null, + private store: Store, + private destroyRef: DestroyRef, + private renderer: Renderer2, + private entityName: string, + private ownerId: EntityId = null, + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, + private router: Router, + public pageMode: boolean = false, + ) { + super(); + if (this.pageMode) { + this.headerComponent = AlarmRuleTableHeaderComponent; + this.entityComponent = AlarmRulesComponent; + this.entityTabsComponent = AlarmRulesTabsComponent; + this.rowPointer = true; + } else { + this.addAsTextButton = false; + } + this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules'); + this.detailsPanelEnabled = this.pageMode; + this.entityResources = { + helpLinkId: 'alarmRules' + }; + this.entityType = EntityType.CALCULATED_FIELD; + this.entityTranslations = { + type: 'alarm-rule.alarm-rule', + typePlural: 'alarm-rule.alarm-rules', + list: 'alarm-rule.list', + add: 'alarm-rule.add', + details: 'alarm-rule.details', + noEntities: 'alarm-rule.no-found', + search: 'action.search', + selectedEntities: 'alarm-rule.selected-fields' + }; + + this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); + this.addEntity = this.getCalculatedAlarmDialog.bind(this); + this.loadEntity = id => this.alarmRulesService.getAlarmRuleById(id.id); + this.saveEntity = (alarmRule) => this.alarmRulesService.saveAlarmRule(alarmRule); + + this.deleteEntityTitle = (field) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); + this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); + this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); + this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); + this.deleteEntity = id => this.alarmRulesService.deleteAlarmRule(id.id); + + this.onEntityAction = action => this.onCFAction(action); + + this.addActionDescriptors = [ + { + name: this.translate.instant('alarm-rule.create'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.getTable().addEntity($event) + }, + { + name: this.translate.instant('alarm-rule.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: () => this.importCalculatedField() + } + ]; + + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); + if (this.pageMode) { + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', + entity => this.utilsService.customTranslation(entity.entityName, entity.entityName), + entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); + } + this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '23%' : '67%', + entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), + () => ({}), false)); + this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '90px', + entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); + + this.cellActionDescriptors.push( + { + name: this.translate.instant('alarm-rule.copy'), + icon: 'content_copy', + isEnabled: () => true, + onAction: ($event, entity) => this.copyCalculatedField($event, entity) + } + ); + this.cellActionDescriptors.push( + { + name: this.translate.instant('action.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.exportAlarmRule($event, entity), + }, + { + name: this.translate.instant('entity-view.events'), + icon: 'mdi:clipboard-text-clock', + isEnabled: () => true, + onAction: ($event, entity) => + this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity), + }, + { + name: '', + nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings), + icon: 'mdi:bug', + isEnabled: () => true, + iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', + onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), + } + ); + if (!this.pageMode) { + this.cellActionDescriptors.push( + { + name: this.translate.instant('action.edit'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.editCalculatedField($event, entity), + } + ) + } + } + + fetchCalculatedFields(pageLink: PageLink): Observable> { + return this.pageMode ? + this.alarmRulesService.getAlarmRules(pageLink, this.alarmRuleFilterConfig) : + this.alarmRulesService.getAlarmRulesByEntityId(this.entityId, pageLink); + } + + onOpenDebugConfig($event: Event, calculatedField: AlarmRuleTableEntity): void { + $event?.stopPropagation(); + const { debugSettings = {}, id } = calculatedField; + const additionalActionConfig = { + ...this.additionalDebugActionConfig, + action: () => this.openDebugEventsDialog($event, calculatedField) + }; + + const { viewContainerRef, renderer } = this.entityDebugSettingsService; + if (!viewContainerRef || !renderer) { + this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef; + this.entityDebugSettingsService.renderer = this.renderer; + } + + this.entityDebugSettingsService.openDebugStrategyPanel({ + debugSettings, + debugConfig: { + entityType: EntityType.CALCULATED_FIELD, + entityLabel: 'alarm-rule.alarm-rule', + additionalActionConfig, + }, + onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) + }, $event.target as Element); + } + + private editCalculatedField($event: Event, calculatedField: AlarmRuleTableEntity, isDirty = false): void { + $event?.stopPropagation(); + this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + + private copyCalculatedField($event: Event, calculatedField: AlarmRuleTableEntity, isDirty = false): void { + $event?.stopPropagation(); + const copyCalculatedAlarmRule = deepClone(calculatedField); + if (this.pageMode) { + copyCalculatedAlarmRule.entityId = null; + delete (copyCalculatedAlarmRule as CalculatedFieldAlarmRuleInfo).entityName; + } + delete copyCalculatedAlarmRule.id; + this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + + private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false): Observable { + const entityId = this.entityId || value?.entityId; + const entityName = this.entityName || (value as CalculatedFieldAlarmRuleInfo)?.entityName; + return this.dialog.open(AlarmRuleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + value, + buttonTitle, + entityId, + entityName, + tenantId: this.tenantId, + ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, + additionalDebugActionConfig: this.additionalDebugActionConfig, + isDirty, + getTestScriptDialogFn: this.getTestScriptDialog.bind(this), + }, + enterAnimationDuration: isDirty ? 0 : null, + }) + .afterClosed() + .pipe(filter(Boolean)); + } + + private openDebugEventsDialog($event: Event, calculatedField: AlarmRuleTableEntity): void { + $event?.stopPropagation(); + this.dialog.open(EventsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + title: 'alarm-rule.debugging', + tenantId: this.tenantId, + entityId: calculatedField.id, + debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], + disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], + defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, + debugActionDisabled: true + } + }) + .afterClosed() + .subscribe(); + } + + private openDebugTab($event: Event, calculatedField: AlarmRuleTableEntity) { + const table = this.getTable(); + if (!table.isDetailsOpen) { + table.toggleEntityDetails($event, calculatedField); + if (table.entityDetailsPanel.matTabGroup._tabs.length > 1) { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + } else { + table.entityDetailsPanel.matTabGroup._tabs.changes.pipe( + first() + ).subscribe(() => { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + }) + } + } + } + + private exportAlarmRule($event: Event, calculatedField: AlarmRuleTableEntity): void { + $event?.stopPropagation(); + this.importExportService.exportCalculatedField(calculatedField.id.id); + } + + private importCalculatedField(): void { + this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file') + .pipe( + filter(Boolean), + switchMap(calculatedField => { + if (calculatedField.type !== CalculatedFieldType.ALARM) { + this.store.dispatch(new ActionNotificationShow({ + message: this.translate.instant('alarm-rule.import-invalid-alarm-rule-type'), + type: 'error', + verticalPosition: 'top', + horizontalPosition: 'left', + duration: 5000 + })); + return EMPTY; + } + return of(calculatedField); + }), + switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), + filter(Boolean), + switchMap(calculatedField => this.alarmRulesService.saveAlarmRule(calculatedField)), + filter(Boolean), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => this.updateData()); + } + + private updateImportedCalculatedField(calculatedField: CalculatedFieldAlarmRule): CalculatedFieldAlarmRule { + if (calculatedField.type === CalculatedFieldType.ALARM) { + calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const arg = calculatedField.configuration.arguments[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } + + return calculatedField; + } + + private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { + this.alarmRulesService.getAlarmRuleById(id).pipe( + switchMap(field => this.alarmRulesService.saveAlarmRule({ ...field, debugSettings })), + catchError(() => of(null)), + takeUntilDestroyed(this.destroyRef), + ).subscribe(() => this.updateData()); + } + + getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const type = calculatedField.configuration.arguments[key].refEntityKey.type; + acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) + ? {...argumentsObj[key], type} + : type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()}; + return acc; + }, {}); + return this.dialog.open(CalculatedFieldScriptTestDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], + data: { + arguments: resultArguments, + expression, + argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), + argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), + openCalculatedFieldEdit + } + }).afterClosed() + .pipe( + filter(Boolean), + tap(expression => { + if (openCalculatedFieldEdit) { + this.editCalculatedField(null, { + entityId: this.entityId, ...calculatedField, + configuration: {...calculatedField.configuration, expression} as any + }, true) + } + }), + ); + } + + private openCalculatedField($event: Event, entity: AlarmRuleTableEntity) { + $event?.stopPropagation(); + const url = this.router.createUrlTree(['alarms', 'alarm-rules', entity.id.id]); + this.router.navigateByUrl(url); + } + + private onCFAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openCalculatedField(action.event, action.entity); + return true; + case 'export': + this.exportAlarmRule(action.event, action.entity); + return true; + } + return false; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html new file mode 100644 index 0000000000..01a3475345 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html @@ -0,0 +1,20 @@ + +@if (alarmRulesTableConfig) { + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss new file mode 100644 index 0000000000..175df5b779 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + .white-background { + --mat-sidenav-content-background-color: white; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts new file mode 100644 index 0000000000..ce0aecb598 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + effect, + input, + Renderer2, + ViewChild, +} from '@angular/core'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { DatePipe } from '@angular/common'; +import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; +import { UtilsService } from "@core/services/utils.service"; +import { ActivatedRoute, Router } from "@angular/router"; + +@Component({ + selector: 'tb-alarm-rules-table', + templateUrl: './alarm-rules-table.component.html', + styleUrls: ['./alarm-rules-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService], + standalone: false +}) +export class AlarmRulesTableComponent { + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + active = input(); + entityId = input(); + entityName = input(); + ownerId = input(); + + alarmRulesTableConfig: AlarmRulesTableConfig; + + pageMode: boolean = false; + + constructor(private alarmRulesService: AlarmRulesService, + private translate: TranslateService, + private dialog: MatDialog, + private store: Store, + private datePipe: DatePipe, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, + private destroyRef: DestroyRef, + private route: ActivatedRoute, + private router: Router + ) { + this.pageMode = !!this.route.snapshot.data.isPage; + effect(() => { + if (this.active() || this.pageMode) { + this.alarmRulesTableConfig = new AlarmRulesTableConfig( + this.alarmRulesService, + this.translate, + this.dialog, + this.datePipe, + this.entityId(), + this.store, + this.destroyRef, + this.renderer, + this.entityName(), + this.ownerId(), + this.importExportService, + this.entityDebugSettingsService, + this.utilsService, + this.router, + this.pageMode, + ); + this.cd.markForCheck(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html new file mode 100644 index 0000000000..daee0a6e8c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html @@ -0,0 +1,166 @@ + +@if (!standalone) { +
+ + + +
+} +
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + +
+ +
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
{{ 'alarm-rule.create-conditions' | translate }}
+
+ + +
+
+
+
{{ 'alarm-rule.clear-condition' | translate }}
+
+
+ + +
+ @if (isEditValue) { + + } +
+ @if (!configFormGroup.get('clearRule').value) { +
+ alarm-rule.no-clear-alarm-rule +
+ } +
+ +
+
+
+ + + {{ 'alarm-rule.advanced-settings' | translate }} + + +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + + } +
+
+ + {{ 'alarm-rule.propagate-alarm-to-owner' | translate }} + +
+
+ + {{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts new file mode 100644 index 0000000000..740dd30440 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts @@ -0,0 +1,194 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldAlarmRuleConfiguration, + CalculatedFieldAlarmRuleInfo, + CalculatedFieldArgument, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import type { + AlarmRulesTableConfig, + AlarmRuleTableEntity +} from '@home/components/alarm-rules/alarm-rules-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { RelationTypes } from '@shared/models/relation.models'; +import { + AlarmRule, + AlarmRuleConditionType, + alarmRuleEntityTypeList, + AlarmRuleExpressionType +} from '@shared/models/alarm-rule.models'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-alarm-rules', + templateUrl: './alarm-rules.component.html', + styleUrls: ['./alarm-rule-dialog.component.scss'], + standalone: false +}) +export class AlarmRulesComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly alarmRuleEntityTypeList = alarmRuleEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldAlarmRuleInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: AlarmRulesTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action({id: this.entity.id, ...this.entityFormValue()}) + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): AlarmRulesTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldAlarmRuleInfo): FormGroup { + return inject(CalculatedFieldFormService).buildAlarmRuleForm(); + } + + updateForm(entity: CalculatedFieldAlarmRuleInfo) { + const { configuration = {} as CalculatedFieldAlarmRuleConfiguration, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + this.entityForm.patchValue({ configuration, debugSettings, entityId, ...value }, {emitEvent: false}); + if (!entityId) { + this.entityForm.get('configuration').disable({emitEvent: false}); + } + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityFormValue(), + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + get arguments(): Record { + return this.entityForm.get('configuration.arguments').value; + } + + get predefinedTypeValues(): StringItemsOption[] { + return RelationTypes.map(type => ({ + name: type, + value: type + })); + } + + get configFormGroup(): FormGroup { + return this.entityForm.get('configuration') as FormGroup; + } + + public removeClearAlarmRule() { + this.configFormGroup.patchValue({clearRule: null}); + this.entityForm.markAsDirty(); + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + type: AlarmRuleConditionType.SIMPLE, + expression: { + type: AlarmRuleExpressionType.SIMPLE + } + }, + alarmDetails: null + }; + this.configFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html new file mode 100644 index 0000000000..3426347595 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -0,0 +1,257 @@ + +
+ +

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

+ + +
+
+
+
+
+
{{ 'alarm-rule.mode' | translate }}
+ + {{ 'alarm-rule.expression-type.simple' | translate }} + {{ 'alarm-rule.expression-type.script' | translate }} + +
+
+
+ @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+
+
{{ 'alarm-rule.argument-filters' | translate }}
+ + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} + +
+ + +
+ } @else { +
+
{{ 'alarm-rule.script' | translate }}
+ +
{{ 'alarm-rule.tbel' | translate }} +
+ +
+
+ +
+
+ } +
+ + @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+ + + {{ 'alarm-rule.filter-preview' | translate }} + + +
+ @if (specText) { + {{ specText }} + } + @if (conditionFormGroup.get('expression.filters').value?.length) { + + + } @else { + {{ 'alarm-rule.no-filter-preview' | translate }} + } +
+
+
+
+ } +
+
+ +
+
+
{{ 'alarm-rule.type' | translate }}
+ + @for (alarmConditionType of alarmConditionTypes; track alarmConditionType) { + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + } + +
+ @if (isNoData) { +
+ {{ 'alarm-rule.condition-type-hint' | translate }} +
+ } + @if (conditionFormGroup.get('type').value === AlarmConditionType.DURATION) { +
+
+
{{ 'alarm-rule.value' | translate }}
+ + {{ 'alarm-rule.static' | translate }} + {{ 'alarm-rule.dynamic' | translate }} + +
+
+
+ +
+
+ +
+
+ + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + } + + @if (conditionFormGroup.get('unit').hasError('required')) { + + {{ 'alarm-rule.condition-duration-time-unit-required' | translate }} + + } + +
+
+
+ } @else if (conditionFormGroup.get('type').value === AlarmConditionType.REPEATING) { +
+
+
{{ 'alarm-rule.value' | translate }}
+ + {{ 'alarm-rule.static' | translate }} + {{ 'alarm-rule.dynamic' | translate }} + +
+
+
+ +
+
+ +
+
+
+ } +
+
+
+
+ + @if (!readonly) { + + } +
+ + +
+ + + {{ defaultValuePlaceholder | translate }} + @if (conditionFormGroup.get(groupName).get('staticValue').hasError('required')) { + {{ defaultValueRequiredError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('min')) { + {{ defaultValueRangeError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('max')) { + {{ defaultValueRangeError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { + {{ defaultValuePatternError | translate }} + } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } + +
+
+ + + + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) { + + {{ 'calculated-fields.hint.argument-name-required' | translate }} + + } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } + + + +
+ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts new file mode 100644 index 0000000000..f2f27c3b8b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -0,0 +1,349 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from "@shared/models/rule-node.models"; +import { + CalculatedFieldArgument, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights +} from "@shared/models/calculated-field.models"; +import { TbEditorCompleter } from "@shared/models/ace/completion.models"; +import { AceHighlightRules } from "@shared/models/ace/ace.models"; +import { ComplexOperation } from "@shared/models/query/query.models"; +import { Observable } from "rxjs"; +import { TranslateService } from "@ngx-translate/core"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleConditionTypeTranslationMap, + alarmRuleDefaultScript, + AlarmRuleExpressionType, + AlarmRuleFilter, + areFiltersAndPredicateArgumentsValid, + filterOperationTranslationMap +} from "@shared/models/alarm-rule.models"; + +export interface CfAlarmRuleConditionDialogData { + readonly: boolean; + condition: AlarmRuleCondition; + arguments?: Record; + testScript: (expression: string) => Observable; +} + +@Component({ + selector: 'tb-cf-alarm-rule-condition-dialog', + templateUrl: './cf-alarm-rule-condition-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + standalone: false +}) +export class CfAlarmRuleConditionDialogComponent extends DialogComponent { + + AlarmRuleExpressionType = AlarmRuleExpressionType; + + timeUnits = Object.values(TimeUnit); + timeUnitTranslations = timeUnitTranslationMap; + alarmConditionTypes = Object.values(AlarmRuleConditionType); + AlarmConditionType = AlarmRuleConditionType; + alarmConditionTypeTranslation = AlarmRuleConditionTypeTranslationMap; + readonly = this.data.readonly; + condition = this.data.condition; + + conditionFormGroup = this.fb.group({ + expression: this.fb.group({ + type: [AlarmRuleExpressionType.SIMPLE], + expression: ['', [Validators.required]], + operation: [ComplexOperation.AND], + filters: [null], + }), + type: this.fb.control(AlarmRuleConditionType.SIMPLE, Validators.required), + unit: this.fb.control(TimeUnit.SECONDS, Validators.required), + value: this.fb.group({ + staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), + dynamicValueArgument: this.fb.control('', Validators.required), + }), + count: this.fb.group({ + staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), + dynamicValueArgument: this.fb.control('', Validators.required), + }), + }); + + readonly scriptLanguage = ScriptLanguage; + + defaultValuePlaceholder = ''; + defaultValueRequiredError = ''; + defaultValueRangeError = ''; + defaultValuePatternError = ''; + + durationDynamicModeControl = this.fb.control(false); + repeatingDynamicModeControl = this.fb.control(false); + + ComplexOperation = ComplexOperation; + complexOperationTranslationMap = filterOperationTranslationMap; + + specText = ''; + + filtersValid: boolean = false; + + functionArgs: Array; + argumentsEditorCompleter: TbEditorCompleter; + argumentsHighlightRules: AceHighlightRules; + + arguments = this.data.arguments; + argumentsList: Array; + + isNoData: boolean = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService) { + super(store, router, dialogRef); + + this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; + this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments); + this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments); + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + + this.conditionFormGroup.patchValue({ + expression: { + type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE, + expression: this.condition?.expression?.expression ?? alarmRuleDefaultScript, + filters: this.condition?.expression?.filters ?? [], + operation: this.condition?.expression?.operation ?? ComplexOperation.AND + }, + type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE, + unit: this.condition?.unit ?? TimeUnit.SECONDS, + value: { + staticValue: this.condition?.value?.staticValue ?? null, + dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.value?.dynamicValueArgument) ? this.condition?.value?.dynamicValueArgument : null, + }, + count: { + staticValue: this.condition?.count?.staticValue ?? null, + dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.count?.dynamicValueArgument) ? this.condition?.count?.dynamicValueArgument : null + } + }, {emitEvent: false}); + + + this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false}); + this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false}); + + this.filtersValid = areFiltersAndPredicateArgumentsValid(this.condition?.expression?.filters, this.data.arguments); + this.checkIsNoData(this.condition?.expression?.filters); + + this.conditionFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type) => { + this.updateValidators(type); + }); + + this.conditionFormGroup.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value) => { + this.updateSpecText(value.type); + }) + + this.conditionFormGroup.get('expression.filters').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((filters) => { + this.filtersValid = areFiltersAndPredicateArgumentsValid(filters, this.data.arguments); + this.checkIsNoData(filters); + }); + + this.conditionFormGroup.get('expression.type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type) => { + this.updateExpressionTypeValidator(type); + this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); + }); + + this.durationDynamicModeControl.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((mode) => { + this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode); + this.updateSpecText(this.conditionFormGroup.get('type').value); + }); + this.repeatingDynamicModeControl.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((mode) => { + this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode); + this.updateSpecText(this.conditionFormGroup.get('type').value); + }); + + this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); + this.updateSpecText(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); + this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); + + if (this.readonly) { + this.conditionFormGroup.disable({emitEvent: false}); + } + } + + updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) { + const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count'); + if (dynamicValue) { + control.get('staticValue').disable({emitEvent: false}); + control.get('dynamicValueArgument').enable({emitEvent: false}); + } else { + control.get('staticValue').enable({emitEvent: false}); + control.get('dynamicValueArgument').disable({emitEvent: false}); + } + } + + updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') { + if (type === 'SIMPLE') { + this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false}); + this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false}); + } else { + this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false}); + this.conditionFormGroup.get(`expression.filters`).disable({emitEvent: false}); + } + } + + private checkIsNoData(filters: Array) { + this.isNoData = this.hasNoData(filters); + if (this.isNoData && this.conditionFormGroup.get('type').value !== AlarmRuleConditionType.SIMPLE) { + this.conditionFormGroup.get('type').patchValue(AlarmRuleConditionType.SIMPLE); + } + if (this.isNoData) { + this.conditionFormGroup.get('type').disable({emitEvent: false}); + } else { + this.conditionFormGroup.get('type').enable({emitEvent: false}); + } + } + + private hasNoData(data: Array) { + const search = (filter) => { + if (!filter) return false; + if (Array.isArray(filter)) return filter.some(search); + if (typeof filter !== 'object') return false; + if (filter.type === 'NO_DATA') return true; + if (filter.predicates?.length) return filter.predicates.some(search); + return false; + }; + return search(data); + }; + + private updateValidators(type: AlarmRuleConditionType) { + switch (type) { + case AlarmRuleConditionType.DURATION: + this.conditionFormGroup.get('unit').enable({emitEvent: false}); + this.conditionFormGroup.get('value').enable({emitEvent: false}); + this.conditionFormGroup.get('count').disable({emitEvent: false}); + this.updateStaticValueValidator(type, this.durationDynamicModeControl.value); + this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value'; + this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required'; + this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range'; + this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern'; + break; + case AlarmRuleConditionType.REPEATING: + this.conditionFormGroup.get('count').enable({emitEvent: false}); + this.conditionFormGroup.get('value').disable({emitEvent: false}); + this.conditionFormGroup.get('unit').disable({emitEvent: false}); + this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value); + this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value'; + this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required'; + this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range'; + this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern'; + break; + case AlarmRuleConditionType.SIMPLE: + this.conditionFormGroup.get('value').disable({emitEvent: false}); + this.conditionFormGroup.get('count').disable({emitEvent: false}); + this.conditionFormGroup.get('unit').disable({emitEvent: false}); + break; + } + } + + private updateSpecText(type: AlarmRuleConditionType) { + this.specText = ''; + const value = this.conditionFormGroup.get('value').value; + const count = this.conditionFormGroup.get('count').value; + switch (type) { + case AlarmRuleConditionType.SIMPLE: + break; + case AlarmRuleConditionType.DURATION: + let duringText = ''; + switch (this.conditionFormGroup.get('unit').value) { + case TimeUnit.SECONDS: + duringText = this.translate.instant('timewindow.seconds', {seconds: value.staticValue}); + break; + case TimeUnit.MINUTES: + duringText = this.translate.instant('timewindow.minutes', {minutes: value.staticValue}); + break; + case TimeUnit.HOURS: + duringText = this.translate.instant('timewindow.hours', {hours: value.staticValue}); + break; + case TimeUnit.DAYS: + duringText = this.translate.instant('timewindow.days', {days: value.staticValue}); + break; + } + if (this.durationDynamicModeControl.value) { + this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { + attribute: `${value.dynamicValueArgument ?? ''}` + }) + ' ' + this.translate.instant(this.timeUnitTranslations.get(this.conditionFormGroup.get('unit').value)).toLowerCase(); + } else { + this.specText = this.translate.instant('alarm-rule.condition-during', { + during: duringText.trim() + }); + } + break; + case AlarmRuleConditionType.REPEATING: + if (this.repeatingDynamicModeControl.value) { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', { + attribute: `${count.dynamicValueArgument ?? ''}` + }); + } else { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times', + {count: count.staticValue}); + } + break; + } + if (this.specText.length > 0) { + this.specText = this.specText + ':'; + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close({ + ...this.conditionFormGroup.value, + type: this.conditionFormGroup.get('type').value, + } as AlarmRuleCondition); + } + + onTestScript($event: Event) { + $event?.preventDefault(); + this.data.testScript(this.conditionFormGroup.get('expression.expression').value).subscribe( + (expression) => { + this.conditionFormGroup.get('expression.expression').setValue(expression); + this.conditionFormGroup.get('expression.expression').markAsDirty(); + }) + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html new file mode 100644 index 0000000000..a3b44ba886 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -0,0 +1,58 @@ + +
+
+
{{ 'alarm-rule.condition' | translate }}
+ +
+
+
{{ 'alarm-rule.schedule-title' | translate }}
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss new file mode 100644 index 0000000000..96494ab98d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + flex: 1; + + .tb-alarm-rule-condition { + display: flex; + flex: 1; + &-button { + --mat-button-outlined-horizontal-padding: 3px 0px 12px; + display: block; + width: 100%; + } + &-label { + display: block; + text-align: start; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts new file mode 100644 index 0000000000..fe0670b4e7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -0,0 +1,348 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, OnChanges, SimpleChanges } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { + dayOfWeekTranslations, + getAlarmScheduleRangeText, + utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; +import { + CfAlarmRuleConditionDialogComponent, + CfAlarmRuleConditionDialogData +} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleExpressionType, + AlarmRuleSchedule, + AlarmRuleScheduleType, + checkPredicates +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { + AlarmRuleScheduleDialogData, + CfAlarmScheduleDialogComponent +} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; +import { coerceBoolean } from "@shared/decorators/coercion"; +import { Observable } from "rxjs"; + +@Component({ + selector: 'tb-cf-alarm-rule-condition', + templateUrl: './cf-alarm-rule-condition.component.html', + styleUrls: ['./cf-alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true, + } + ], + standalone: false +}) +export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + arguments: Record; + + @Input() + @coerceBoolean() + isClearCondition = false; + + @Input({required: true}) + testScript: (expression: string) => Observable; + + alarmRuleConditionFormGroup = this.fb.group({ + type: ['SIMPLE'], + expression: [{type: AlarmRuleExpressionType.SIMPLE}], + schedule: [null], + }); + + specText = ''; + + filtersArgumentsValid: boolean = true; + schedulerArgumentsValid: boolean = true; + + scheduleText = ''; + + private modelValue: AlarmRuleCondition; + + private propagateChange = (v: any) => { }; + private onValidatorChange = () => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private cd: ChangeDetectorRef, + private translate: TranslateService) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + registerOnValidatorChange(fn: () => void): void { + this.onValidatorChange = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleConditionFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleConditionFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRuleCondition): void { + this.modelValue = value; + this.updateConditionInfo(); + this.recalculateArgumentValidity(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.arguments) { + if (changes.arguments && !changes.arguments.firstChange) { + this.recalculateArgumentValidity(); + } + } + } + + private recalculateArgumentValidity(): void { + if (!this.modelValue || !this.arguments) { + this.filtersArgumentsValid = true; + this.schedulerArgumentsValid = true; + return; + } + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); + this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); + } + + private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean { + const arg = obj?.schedule?.dynamicValueArgument; + return !arg || validArguments.includes(arg); + } + + private areFilterAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); + const filters = obj?.expression?.filters || obj?.filters || []; + for (const filter of filters) { + if (filter.argument && !validSet.has(filter.argument)) { + return false; + } + } + for (const filter of filters) { + if (Array.isArray(filter.predicates)) { + if (!checkPredicates(filter.predicates, validSet)) { + return false; + } + } + } + return true; + } + + public conditionSet(): boolean { + return !!this.modelValue && !!(this.modelValue.expression?.expression || this.modelValue.expression?.filters); + } + + public validate(control: AbstractControl): ValidationErrors | null { + const hasCondition = this.conditionSet(); + const argsValid = this.filtersArgumentsValid && this.schedulerArgumentsValid; + return (hasCondition && argsValid) ? null : { alarmRuleCondition: { valid: false } }; + } + + public openFilterDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(CfAlarmRuleConditionDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + condition: this.disabled ? this.modelValue : deepClone(this.modelValue), + arguments: this.arguments, + testScript: this.testScript + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue = {...this.modelValue, ...result}; + this.updateModel(); + } + }); + } + + private updateConditionInfo() { + this.alarmRuleConditionFormGroup.patchValue( + this.modelValue ? { + type: this.modelValue?.type, + expression: this.modelValue?.expression, + schedule: this.modelValue?.schedule, + } : null, {emitEvent: false} + ); + this.updateScheduleText(); + this.updateSpecText(); + } + + private updateSpecText() { + this.specText = ''; + if (this.modelValue && this.modelValue.type) { + const type = this.modelValue.type; + switch (type) { + case AlarmRuleConditionType.SIMPLE: + break; + case AlarmRuleConditionType.DURATION: + let duringText = ''; + switch (this.modelValue.unit) { + case TimeUnit.SECONDS: + duringText = this.translate.instant('timewindow.seconds', {seconds: this.modelValue.value.staticValue}); + break; + case TimeUnit.MINUTES: + duringText = this.translate.instant('timewindow.minutes', {minutes: this.modelValue.value.staticValue}); + break; + case TimeUnit.HOURS: + duringText = this.translate.instant('timewindow.hours', {hours: this.modelValue.value.staticValue}); + break; + case TimeUnit.DAYS: + duringText = this.translate.instant('timewindow.days', {days: this.modelValue.value.staticValue}); + break; + } + if (this.modelValue.value.dynamicValueArgument) { + this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { + attribute: `${this.modelValue.value.dynamicValueArgument}` + }) + ' ' + this.translate.instant(timeUnitTranslationMap.get(this.modelValue.unit)).toLowerCase(); + } else { + this.specText = this.translate.instant('alarm-rule.condition-during', { + during: duringText + }); + } + break; + case AlarmRuleConditionType.REPEATING: + if (this.modelValue.count.dynamicValueArgument) { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', { + attribute: `${this.modelValue.count.dynamicValueArgument}` + }); + } else { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times', + {count: this.modelValue.count.staticValue}); + } + break; + } + } + } + + private updateModel() { + this.updateConditionInfo(); + this.recalculateArgumentValidity(); + this.propagateChange(this.modelValue); + this.onValidatorChange(); + this.cd.detectChanges(); + } + + public openScheduleDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(CfAlarmScheduleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + alarmSchedule: this.disabled ? this.modelValue?.schedule : deepClone(this.modelValue?.schedule), + arguments: this.arguments + } + }).afterClosed().subscribe((result) => { + if (result) { + if (!this.modelValue) { + this.modelValue = {} as AlarmRuleCondition; + } + this.modelValue.schedule = result; + this.updateModel(); + } + }); + } + + private updateScheduleText() { + const schedule = this.modelValue?.schedule; + this.scheduleText = ''; + if (isDefinedAndNotNull(schedule)) { + if (schedule.dynamicValueArgument) { + this.scheduleText = this.translate.instant('alarm-rule.value-argument') + ': ' + schedule?.dynamicValueArgument + } else { + switch (schedule.staticValue.type) { + case AlarmRuleScheduleType.ANY_TIME: + this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); + break; + case AlarmRuleScheduleType.SPECIFIC_TIME: + for (const day of schedule.staticValue.daysOfWeek) { + if (this.scheduleText.length) { + this.scheduleText += ', '; + } + this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]); + } + this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.staticValue.startsOn), + utcTimestampToTimeOfDay(schedule.staticValue.endsOn)) + ''; + break; + case AlarmRuleScheduleType.CUSTOM: + for (const item of schedule.staticValue.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; + } + } + } + if (!this.scheduleText.length) { + this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html new file mode 100644 index 0000000000..47b6281fdd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -0,0 +1,46 @@ + +
+ + +
+
+ alarm-rule.alarm-rule-additional-info +
+ + + + +
+
+
+ alarm-rule.alarm-rule-mobile-dashboard +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss new file mode 100644 index 0000000000..53283aad68 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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%; + .tb-alarm-rule-details, .tb-alarm-rule-dashboard { + padding: 4px; + &.title { + opacity: 0.7; + overflow: visible; + } + } + .tb-alarm-rule-details { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts new file mode 100644 index 0000000000..706477f72a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -0,0 +1,158 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { isDefinedAndNotNull } from '@core/utils'; +import { DashboardId } from '@shared/models/id/dashboard-id'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AlarmRule, AlarmRuleCondition } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { + AlarmRuleDetailsDialogComponent, + AlarmRuleDetailsDialogData +} from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; +import { coerceBoolean } from "@shared/decorators/coercion"; +import { Observable } from "rxjs"; + +@Component({ + selector: 'tb-cf-alarm-rule', + templateUrl: './cf-alarm-rule.component.html', + styleUrls: ['./cf-alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true, + } + ], + standalone: false +}) +export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + arguments: Record; + + @Input() + @coerceBoolean() + isClearCondition = false; + + @Input({required: true}) + testScript: (expression: string) => Observable; + + private modelValue: AlarmRule; + + alarmRuleFormGroup = this.fb.group({ + condition: this.fb.control(null, Validators.required), + alarmDetails: [null], + dashboardId: [null] + }); + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).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; + const model = this.modelValue ? { + ...this.modelValue, + dashboardId: this.modelValue.dashboardId?.id + } : null; + this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); + } + + public openEditDetailsDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AlarmRuleDetailsDialogComponent, { + 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(): ValidationErrors | null { + return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { + alarmRule: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.alarmRuleFormGroup.value; + this.modelValue = {...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null} as AlarmRule; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss new file mode 100644 index 0000000000..f38df65c29 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + width: 900px; + max-width: 100%; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + } + + .spec-text { + font-size: 14px; + color: rgba(0, 0, 0, 0.54); + } +} +.tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; +} +.tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html new file mode 100644 index 0000000000..3908d919b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html @@ -0,0 +1,60 @@ + +
+ +

{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}

+ + +
+
+
+
+
+
{{ 'alarm-rule.mode' | translate }}
+ + {{ 'alarm-rule.static-schedule' | translate }} + {{ 'alarm-rule.dynamic-schedule' | translate }} + +
+ + +
+
+
+
+ + @if (!readonly) { + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts new file mode 100644 index 0000000000..d603e9b8b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { AlarmRuleSchedule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +export interface AlarmRuleScheduleDialogData { + readonly: boolean; + alarmSchedule: AlarmRuleSchedule; + arguments: Record; +} + +@Component({ + selector: 'tb-cf-alarm-schedule-dialog', + templateUrl: './cf-alarm-schedule-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + standalone: false +}) +export class CfAlarmScheduleDialogComponent extends DialogComponent{ + + readonly = this.data.readonly; + alarmSchedule = this.data.alarmSchedule; + arguments = this.data.arguments; + + alarmScheduleControl = this.fb.control(null); + + dynamicModeControl = this.fb.control(false); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.alarmScheduleControl.patchValue(this.alarmSchedule, {emitEvent: false}); + this.dynamicModeControl.patchValue(!!this.alarmSchedule?.dynamicValueArgument, {emitEvent: false}); + if (this.readonly) { + this.alarmScheduleControl.disable({emitEvent: false}); + this.dynamicModeControl.disable({emitEvent: false}); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.alarmScheduleControl.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html new file mode 100644 index 0000000000..8858198d34 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html @@ -0,0 +1,129 @@ + +
+ @if (!dynamicMode) { + + + + @for (scheduleType of alarmScheduleTypes; track scheduleType) { + {{ alarmScheduleTypeTranslate.get(scheduleType) | translate }} + } + + @if (alarmScheduleForm.get('staticValue.type').hasError('required')) { + {{ 'alarm-rule.schedule-type-required' | translate }} + } + + + @if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) { +
+
+ + + @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) { +
+ + @for (day of allDays; track day; let i = $index) { + + {{ dayOfWeekTranslationsArray[day] | translate }} + + } + + +
+
+ + alarm-rule.schedule-time-from + + + + + + alarm-rule.schedule-time-to + + + + +
+
+
+
+
+
+ } + @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) { +
+ @for (day of allDays; track day) { +
+
+ + {{ dayOfWeekTranslationsArray[day] | translate }} + +
+ + alarm-rule.schedule-time-from + + + + + + alarm-rule.schedule-time-to + + + + +
+
+
+
+
+ } + +
+ } +
+
+ } + } @else { +
+ + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (alarmScheduleForm.get('dynamicValueArgument').hasError('required')) { + {{ 'calculated-fields.hint.argument-name-required' | translate }} + } + +
+
+
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts new file mode 100644 index 0000000000..7cc06c37aa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts @@ -0,0 +1,323 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + CustomTimeSchedulerItem, + dayOfWeekTranslations, + getAlarmScheduleRangeText, + timeOfDayToUTCTimestamp, + utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { isDefined } from '@core/utils'; +import { getDefaultTimezone } from '@shared/models/time/time.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + AlarmRuleSchedule, + AlarmRuleScheduleType, + AlarmRuleScheduleTypeTranslationMap +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { MatChipSelectionChange } from "@angular/material/chips"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-cf-alarm-schedule', + templateUrl: './cf-alarm-schedule.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }], + standalone: false +}) +export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + @Input() + @coerceBoolean() + dynamicMode: boolean; + + alarmScheduleForm = this.fb.group({ + staticValue: this.fb.group({ + type: [AlarmRuleScheduleType.ANY_TIME, Validators.required], + timezone: ['', Validators.required], + daysOfWeek: this.fb.control(null, Validators.required), + startsOn: this.fb.control(0, Validators.required), + endsOn: this.fb.control(0, Validators.required), + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems), + }), + dynamicValueArgument: ['', Validators.required] + }); + + alarmScheduleTypes = Object.keys(AlarmRuleScheduleType) as Array; + alarmScheduleType = AlarmRuleScheduleType; + alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap; + dayOfWeekTranslationsArray = dayOfWeekTranslations; + + argumentsList: Array; + + allDays = Array(7).fill(0).map((x, i) => i); + + private modelValue: AlarmRuleSchedule; + + private defaultItems = Array.from({length: 7}, (value, i) => ({ + enabled: true, + dayOfWeek: i + 1 + })) as CustomTimeSchedulerItem[]; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((type) => { + const defaultTimezone = getDefaultTimezone(); + const staticValue = {...this.alarmScheduleForm.get('staticValue').value, type, items: this.defaultItems, timezone: defaultTimezone}; + this.alarmScheduleForm.get('staticValue').patchValue(staticValue, {emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false}); + this.updateValidators(type); + }); + this.alarmScheduleForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + + this.alarmScheduleForm.get('staticValue.items').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((items) => { + items.forEach((item, index) => this.disabledSelectedTime(item.enabled, index, false)) + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.dynamicMode) { + const dynamicModeChanges = changes.dynamicMode; + if (!dynamicModeChanges.firstChange && dynamicModeChanges.currentValue !== dynamicModeChanges.previousValue) { + this.updateModeValidators(dynamicModeChanges.currentValue); + this.updateModel(); + } + } + } + + 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.updateModeValidators(this.dynamicMode); + } + } + + writeValue(value: AlarmRuleSchedule): void { + if (value) { + this.modelValue = value; + if (this.modelValue.dynamicValueArgument) { + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(Object.keys(this.arguments).includes(this.modelValue.dynamicValueArgument) ? this.modelValue.dynamicValueArgument : null, {emitEvent: false}); + } else { + switch (this.modelValue.staticValue.type) { + case AlarmRuleScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + timezone: this.modelValue.staticValue.timezone, + daysOfWeek: this.modelValue.staticValue.daysOfWeek, + startsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.startsOn), + endsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.endsOn), + }, + }, {emitEvent: false}); + break; + case AlarmRuleScheduleType.CUSTOM: + if (this.modelValue?.dynamicValueArgument) { + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + }, + }, {emitEvent: false}); + } else if (this.modelValue.staticValue?.items) { + const alarmDays = []; + this.modelValue.staticValue.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({ + staticValue: { + type: this.modelValue.staticValue.type, + timezone: this.modelValue.staticValue.timezone, + items: alarmDays, + }, + }, {emitEvent: false}); + } + break; + default: + this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); + } + this.updateValidators(this.modelValue.staticValue.type); + } + this.updateModeValidators(this.dynamicMode); + } + } + + validate(control: FormGroup): ValidationErrors | null { + return this.alarmScheduleForm.valid ? null : { + alarmScheduler: { + valid: false + } + }; + } + + private updateModeValidators(mode: boolean) { + if (mode) { + this.alarmScheduleForm.get('staticValue').disable({emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').enable({emitEvent: false}); + } else { + this.alarmScheduleForm.get('staticValue').enable({emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').disable({emitEvent: false}); + this.updateValidators(this.alarmScheduleForm.get('staticValue.type').value); + } + } + + private updateValidators(type: AlarmRuleScheduleType){ + switch (type){ + case AlarmRuleScheduleType.ANY_TIME: + this.alarmScheduleForm.get('staticValue.timezone').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); + break; + case AlarmRuleScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); + break; + case AlarmRuleScheduleType.CUSTOM: + this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').enable({emitEvent: false}); + break; + } + } + + private updateModel() { + const value = this.alarmScheduleForm.value as AlarmRuleSchedule; + if (!this.dynamicMode) { + if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) { + value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn); + } + if (isDefined(value.staticValue.endsOn) && value.staticValue.endsOn !== 0) { + value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn); + } + if (isDefined(value.staticValue.items)){ + value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items as CustomTimeSchedulerItem[]; + value.staticValue.items = value.staticValue.items.map((item) => { + return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)}; + }); + } + } + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + + private defaultItemsScheduler(index: number): FormGroup { + return this.fb.group({ + enabled: [true], + dayOfWeek: [index + 1], + startsOn: [0, Validators.required], + endsOn: [0, Validators.required] + }); + } + + changeCustomScheduler($event: MatChipSelectionChange, index: number) { + const value = $event.selected; + 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('staticValue.items') as FormArray; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html new file mode 100644 index 0000000000..bcf044e998 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -0,0 +1,67 @@ + +
+ @for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) { +
+
+
+
alarm.severity
+ + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + } + + +
+ + +
+ @if (!disabled) { + + } +
+ } + @if (!createAlarmRulesFormArray().controls.length) { + + alarm-rule.add-create-alarm-rule-prompt + + } +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss new file mode 100644 index 0000000000..7250c86ab2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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: 1px solid rgba(0, 0, 0, .12); + border-left-width: 4px; + border-radius: 4px; + padding: 8px; + min-width: 0; + } +} + +:host ::ng-deep { + .mat-mdc-form-field.severity { + .mat-mdc-form-field-infix { + width: 160px; + } + } + .button-icon { + color: rgba(0, 0, 0, 0.38); + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts new file mode 100644 index 0000000000..4d9884f851 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -0,0 +1,190 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { AlarmSeverity, alarmSeverityColors, alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { coerceBoolean } from "@shared/decorators/coercion"; +import { Observable } from "rxjs"; + +@Component({ + selector: 'tb-create-cf-alarm-rules', + templateUrl: './create-cf-alarm-rules.component.html', + styleUrls: ['./create-cf-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true, + } + ], + standalone: false +}) +export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + @Input({required: true}) + testScript: (expression: string) => Observable; + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + alarmSeverityTranslationMap = alarmSeverityTranslations; + + AlarmSeverityNotificationColors = alarmSeverityColors; + + createAlarmRulesFormGroup = this.fb.group({ + createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([]) + }); + + private usedSeverities: AlarmSeverity[] = []; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + this.createAlarmRulesFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + createAlarmRulesFormArray(): UntypedFormArray { + return this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(createAlarmRules: Record): void { + 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: [{value: createAlarmRule, disabled: this.disabled}, Validators.required] + })); + }); + } + const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + formArray.clear({emitEvent: false}); + createAlarmRulesControls.forEach(c => formArray.push(c, {emitEvent: false})); + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + 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 createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + createAlarmRulesArray.push(this.fb.group({ + severity: [this.getFirstUnusedSeverity(), Validators.required], + alarmRule: [null, Validators.required] + })); + 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(): ValidationErrors | null { + return this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormArray().length > 0 ? 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 = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + value.forEach((rule, index) => { + this.usedSeverities[index] = AlarmSeverity[rule.severity]; + }); + } + + private updateModel() { + const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + const createAlarmRules = {} as Record; + value.forEach(v => createAlarmRules[v.severity] = v.alarmRule); + this.updateUsedSeverities(); + this.propagateChange(createAlarmRules); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html new file mode 100644 index 0000000000..5ca3cde37b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -0,0 +1,63 @@ + +
+ +

filter.complex-filter

+ +
+
+
+
+
+ {{ 'alarm-rule.filters' | translate }} +
+ + {{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }} + {{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }} + +
+ + +
+
+
+ + @if (!readonly) { + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts new file mode 100644 index 0000000000..951aa1a083 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { ComplexOperation, EntityKeyValueType } from '@shared/models/query/query.models'; +import { + AlarmRuleFilterPredicate, + AlarmRuleFilterPredicateType, + ComplexAlarmRuleFilterPredicate, + filterOperationTranslationMap +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +export interface AlarmRuleComplexFilterPredicateDialogData { + complexPredicate: ComplexAlarmRuleFilterPredicate; + isAdd: boolean; + valueType: EntityKeyValueType; + arguments: Record; + argumentInUse: string; + readonly: boolean; +} + +@Component({ + selector: 'tb-alarm-rule-complex-filter-predicate-dialog', + templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html', + providers: [], + styleUrls: [], + standalone: false +}) + +export class AlarmRuleComplexFilterPredicateDialogComponent extends + DialogComponent { + + complexFilterFormGroup = this.fb.group( + { + operation: [ComplexOperation.AND, [Validators.required]], + predicates: this.fb.control(null, Validators.required) + } + ); + + EntityKeyValueType = EntityKeyValueType; + complexOperationEnum = ComplexOperation; + complexOperationTranslations = filterOperationTranslationMap; + + isAdd: boolean; + + arguments = this.data.arguments; + + readonly = this.data.readonly; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.isAdd = this.data.isAdd; + + this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false}); + + if (this.readonly) { + this.complexFilterFormGroup.disable({emitEvent: false}); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate; + predicate.type = AlarmRuleFilterPredicateType.COMPLEX; + this.dialogRef.close(predicate); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html new file mode 100644 index 0000000000..2d971f98ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html @@ -0,0 +1,108 @@ + +
+ +

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

+ +
+
+
+
+
{{ 'alarm-rule.general' | translate }}
+
+ + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (filterFormGroup.get('argument').touched && filterFormGroup.get('argument').hasError('required')) { + + warning + + } + + + filter.value-type.value-type + + + + {{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }} + + @for (valueType of entityKeyValueTypesKeys; track valueType) { + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} + + } + + @if (filterFormGroup.get('valueType').hasError('required')) { + + {{ 'filter.value-type-required' | translate }} + + } + +
+
+ +
+
+
+ {{ 'alarm-rule.filters' | translate }} +
+ + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} + +
+ + +
+
+
+
+ + @if (!readonly) { + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss new file mode 100644 index 0000000000..4330e0090f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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-mdc-form-field.tb-value-type { + mat-select-trigger { + .mat-icon { + vertical-align: middle; + margin-right: 8px; + svg { + vertical-align: initial; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts new file mode 100644 index 0000000000..8dcb00daae --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts @@ -0,0 +1,138 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, Inject } 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 { ComplexOperation, EntityKeyValueType, entityKeyValueTypesMap } from '@shared/models/query/query.models'; + import { DialogService } from '@core/services/dialog.service'; + import { TranslateService } from '@ngx-translate/core'; + import { + AlarmRuleFilter, + AlarmRuleFilterPredicate, + filterOperationTranslationMap, + isPredicateArgumentsValid + } from "@shared/models/alarm-rule.models"; + import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + import { FormControlsFrom } from "@shared/models/tenant.model"; + import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + + export interface AlarmRuleFilterDialogData { + filter: AlarmRuleFilter; + isAdd: boolean; + arguments: Record; + usedArguments: Array; + readonly: boolean; +} + +@Component({ + selector: 'tb-alarm-rule-filter-dialog', + templateUrl: './alarm-rule-filter-dialog.component.html', + providers: [], + styleUrls: ['./alarm-rule-filter-dialog.component.scss'], + standalone: false +}) +export class AlarmRuleFilterDialogComponent extends DialogComponent { + + filterFormGroup: FormGroup>; + + entityKeyValueTypesKeys = Object.keys(EntityKeyValueType); + + entityKeyValueTypeEnum = EntityKeyValueType; + + entityKeyValueTypes = entityKeyValueTypesMap; + + complexOperationTranslationMap = filterOperationTranslationMap; + + predicatesValid: boolean = false; + + ComplexOperation = ComplexOperation; + + arguments = this.data.arguments; + argumentsList: Array; + + readonly = this.data.readonly; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData, + public dialogRef: MatDialogRef, + private dialogs: DialogService, + private translate: TranslateService, + private fb: FormBuilder, + private destroyRef: DestroyRef) { + super(store, router, dialogRef); + + this.argumentsList = this.arguments ? Object.keys(this.arguments) : []; + + this.filterFormGroup = this.fb.group( + { + argument: [this.argumentsList.includes(this.data.filter.argument) ? this.data.filter.argument : '' , [Validators.required]], + valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]], + predicates: [this.data.filter.predicates, [Validators.required]], + operation: [this.data.filter.operation ?? ComplexOperation.AND] + } + ); + + this.predicatesValid = isPredicateArgumentsValid(this.data.filter.predicates, this.arguments); + this.filterFormGroup.get('predicates').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(predicates => { + this.predicatesValid = isPredicateArgumentsValid(predicates, this.arguments); + }); + + this.filterFormGroup.get('valueType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((valueType: EntityKeyValueType) => { + const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType; + const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value; + if (prevValueType && prevValueType !== valueType) { + if (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.filterFormGroup.get('predicates').setValue([]); + } else { + this.filterFormGroup.get('valueType').setValue(prevValueType, {emitEvent: false}); + } + } + ); + } + } + }); + + if (this.readonly) { + this.filterFormGroup.disable({emitEvent: false}); + } + } + + argumentInUse(argument: string): boolean { + return this.data.usedArguments.includes(argument); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.filterFormGroup.value as AlarmRuleFilter); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html new file mode 100644 index 0000000000..950d19abd0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html @@ -0,0 +1,85 @@ + +@if (filtersFormArray.length) { +
+
+ +
+ + +   +
+
+ +
+ @for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) { +
+
+ @if ($index) { +
+ {{ complexOperationTranslationMap.get(operation) | translate }} +
+ } +
+
+
+
{{ filterControl.value?.argument }}
+
{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[filterControl.value?.valueType]).name | translate }}
+ + +
+
+ @if (index) { + + } +
+ } +
+
+} @else { + alarm-rule.no-filter +} + + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss new file mode 100644 index 0000000000..fea71605c8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + .filter-title { + padding: 12px 0; + font-size: 14px; + font-weight: 500; + } + .no-data-found { + height: 50px; + font-size: 16px; + } + .filter-list { + overflow: auto; + max-height: 300px; + + &-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + + .filters-text { + font-size: 14px; + } + } + .filters-operation { + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + background-color: white; + } + &-label { + font-size: 15px; + font-weight: 400; + color: $tb-primary-color; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); + } + } + + .tb-budge-button { + --mat-badge-legacy-small-size-container-size: 8px; + --mat-badge-small-size-container-overlap-offset: -5px; + --mat-badge-small-size-text-size: 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts new file mode 100644 index 0000000000..706e2c4a86 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts @@ -0,0 +1,191 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Observable } from 'rxjs'; +import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + entityKeyValueTypesMap +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone } from '@core/utils'; +import { + AlarmRuleFilterDialogComponent, + AlarmRuleFilterDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; +import { AlarmRuleFilter, areFilterAndPredicateArgumentsValid } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Component({ + selector: 'tb-alarm-rule-filter-list', + templateUrl: './alarm-rule-filter-list.component.html', + styleUrls: ['./alarm-rule-filter-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + } + ], + standalone: false +}) +export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator { + + @Input() + arguments: Record; + + @Input() + operation: ComplexOperation = ComplexOperation.AND; + + @Input() + readonly = false; + + filterListFormGroup = this.fb.group({ + filters: this.fb.array([]) + }); + + disabled = false; + + areFilterAndPredicateArgumentsValid = areFilterAndPredicateArgumentsValid; + + complexOperationTranslationMap = complexOperationTranslationMap; + entityKeyValueTypes = entityKeyValueTypesMap; + entityKeyValueTypeEnum = EntityKeyValueType; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterListFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + + get filtersFormArray(): FormArray { + return this.filterListFormGroup.get('filters') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterListFormGroup.valid && this.filterListFormGroup.get('filters').value?.length ? null : { + filterList: {valid: false} + }; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.filterListFormGroup.disable({emitEvent: false}); + } else { + this.filterListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(filters: Array): void { + const keyFilterControls: Array = []; + if (filters) { + for (const filter of filters) { + keyFilterControls.push(this.fb.control(filter, [Validators.required])); + } + } + this.filtersFormArray.clear(); + keyFilterControls.forEach(c => this.filtersFormArray.push(c)); + } + + public removeFilter(index: number) { + (this.filterListFormGroup.get('filters') as FormArray).removeAt(index); + } + + public addFilter() { + const filtersFormArray = this.filterListFormGroup.get('filters') as FormArray; + this.openFilterDialog(null).subscribe(result => { + if (result) { + filtersFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + public editFilter(index: number, readonly = false) { + const filter: AlarmRuleFilter = + (this.filterListFormGroup.get('filters') as FormArray).at(index).value; + this.openFilterDialog(filter, readonly).subscribe(result => { + if (result) { + (this.filterListFormGroup.get('filters') as FormArray).at(index).patchValue(result); + } + }); + } + + private openFilterDialog(filter?: AlarmRuleFilter, readonly = false): Observable { + const isAdd = !filter; + if (isAdd) { + filter = { + argument: null, + valueType: EntityKeyValueType.STRING, + operation: ComplexOperation.AND, + predicates: [] + }; + } + return this.dialog.open(AlarmRuleFilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + filter: filter ? deepClone(filter) : null, + isAdd, + arguments: this.arguments, + usedArguments: this.getUsedArguments, + readonly + } + }).afterClosed(); + } + + get getUsedArguments(): Array { + const filters = this.filterListFormGroup.get('filters').value as Array; + return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : []; + } + + private updateModel() { + const filters = this.filterListFormGroup.value.filters as Array; + this.propagateChange(filters); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html new file mode 100644 index 0000000000..1dd6f5f453 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html @@ -0,0 +1,80 @@ + +
+ @if (predicatesFormArray.length) { +
+
+
+
+ + + +
+   +
+
+ +
+ @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { +
+ @if (index) { +
+ {{ complexOperationTranslations.get(operation) | translate }} +
+ } +
+
+ + + +
+
+
+ } +
+
+ } @else { + alarm-rule.no-filter + } +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss new file mode 100644 index 0000000000..e5e6dbeba4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + .filter-title { + padding: 12px 8px; + font-size: 14px; + font-weight: 500; + } + .no-data-found { + height: 50px; + font-size: 16px; + } + + .key-filter-list-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + .filters-operation { + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + position: absolute; + top: -12px; + left: 10px; + background-color: white; + } + &-label { + font-size: 15px; + font-weight: 400; + color: $tb-primary-color; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts new file mode 100644 index 0000000000..12fce94fb3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts @@ -0,0 +1,214 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + entityKeyValueTypeToFilterPredicateType +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { map } from 'rxjs/operators'; +import { + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleComplexFilterPredicateDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { + AlarmRuleBooleanOperation, + AlarmRuleFilterPredicate, + AlarmRuleFilterPredicateType, + AlarmRuleNumericOperation, + AlarmRulePredicateInfo, + AlarmRuleStringOperation, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate-list', + templateUrl: './alarm-rule-filter-predicate-list.component.html', + styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + } + ], + standalone: false +}) +export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator { + + @Input() disabled: boolean; + + @Input() valueType: EntityKeyValueType; + + @Input() operation: ComplexOperation = ComplexOperation.AND; + + @Input() arguments: Record; + + @Input() argumentInUse: string; + + @Input() readonly: boolean; + + filterListFormGroup = this.fb.group({ + predicates: this.fb.array([]) + }); + + valueTypeEnum = EntityKeyValueType; + + complexOperationTranslations = complexOperationTranslationMap; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterListFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + get 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}); + } + } + + validate(control: AbstractControl): ValidationErrors | null { + return this.filterListFormGroup.valid ? null : { + filterList: {valid: false} + }; + } + + writeValue(predicates: Array): void { + const predicateControls: Array = []; + if (predicates) { + for (const predicate of predicates) { + predicateControls.push(this.fb.control(predicate, [Validators.required])); + } + } + this.predicatesFormArray.clear(); + predicateControls.forEach(predicate => this.predicatesFormArray.push(predicate)); + } + + public removePredicate(index: number) { + this.predicatesFormArray.removeAt(index); + } + + public addPredicate(complex: boolean) { + const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray; + const predicate = this.createDefaultFilterPredicate(this.valueType, complex); + let observable: Observable; + if (complex) { + observable = this.openComplexFilterDialog(predicate as ComplexAlarmRuleFilterPredicate); + } else { + observable = of(predicate); + } + observable.subscribe((result) => { + if (result) { + predicatesFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate { + const predicate = { + type: complex ? AlarmRuleFilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType) + } as AlarmRuleFilterPredicate; + switch (predicate.type) { + case AlarmRuleFilterPredicateType.STRING: + predicate.operation = AlarmRuleStringOperation.STARTS_WITH; + predicate.value = { + staticValue: '' + }; + predicate.ignoreCase = false; + break; + case AlarmRuleFilterPredicateType.NUMERIC: + predicate.operation = AlarmRuleNumericOperation.EQUAL; + predicate.value = { + staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0 + }; + break; + case AlarmRuleFilterPredicateType.BOOLEAN: + predicate.operation = AlarmRuleBooleanOperation.EQUAL; + predicate.value = { + staticValue: false + }; + break; + case AlarmRuleFilterPredicateType.COMPLEX: + predicate.operation = ComplexOperation.AND; + predicate.predicates = []; + break; + } + return predicate; + } + + private openComplexFilterDialog(predicate: ComplexAlarmRuleFilterPredicate): Observable { + return this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: predicate as ComplexAlarmRuleFilterPredicate, + valueType: this.valueType, + isAdd: true, + arguments: this.arguments, + argumentInUse: this.argumentInUse, + readonly: this.readonly + } + }).afterClosed().pipe( + map(result => result) + ); + } + + private updateModel() { + this.propagateChange(this.filterListFormGroup.get('predicates').value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html new file mode 100644 index 0000000000..b5206437c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html @@ -0,0 +1,79 @@ + +
+
+ + + {{'alarm-rule.static' | translate}} + {{'alarm-rule.dynamic' | translate}} + + + + @if (!dynamicModeControl.value) { + + + @if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) { + + warning + + } + + } @else { + + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').touched && filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').errors) { + + warning + + } + @if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').hasError('argumentInUse')) { + + warning + + } + + } + + + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + } + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts new file mode 100644 index 0000000000..977bb5749f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts @@ -0,0 +1,167 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { TimeUnit, timeUnitTranslations } from "@home/components/rule-node/rule-node-config.models"; +import { AlarmRuleFilterPredicateType, NoDataAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; +import { isDefinedAndNotNull } from "@core/utils"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate-no-data-value', + templateUrl: './alarm-rule-filter-predicate-no-data-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), + multi: true + } + ], + standalone: false +}) +export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { + + @Input() + arguments: Record; + + @Input() + valueType: AlarmRuleFilterPredicateType; + + @Input() + argumentInUse: string; + + valueTypeEnum = AlarmRuleFilterPredicateType; + + filterPredicateValueNoDataFormGroup = this.fb.group({ + type: ['NO_DATA'], + unit: [TimeUnit.MINUTES, Validators.required], + duration: this.fb.group({ + staticValue: [null as null | number, [Validators.required, Validators.min(1)]], + dynamicValueArgument: ['', Validators.required] + }) + }); + + timeUnits = [TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS]; + timeUnitsTranslationMap = timeUnitTranslations; + + + dynamicModeControl = this.fb.control(false); + + argumentsList: Array; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + this.filterPredicateValueNoDataFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + this.dynamicModeControl.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => this.updateValueModeValidators(value)); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.argumentInUse) { + const argumentInUseChanges = changes.argumentInUse; + if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) { + if (this.dynamicModeControl.value) { + if (this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value) { + this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true}); + this.filterPredicateValueNoDataFormGroup.updateValueAndValidity(); + } + } + } + } + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.filterPredicateValueNoDataFormGroup.disable({emitEvent: false}); + this.dynamicModeControl.disable({emitEvent: false}); + } else { + this.filterPredicateValueNoDataFormGroup.enable({emitEvent: false}); + this.dynamicModeControl.enable({emitEvent: false}); + this.updateValueModeValidators(this.dynamicModeControl.value); + } + } + + private updateValueModeValidators(isDynamicMode: boolean): void { + if (isDynamicMode) { + this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').disable({emitEvent: false}); + this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').enable(); + setTimeout(()=> { + if (this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('dynamicValueArgument').value) { + this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true}); + this.filterPredicateValueNoDataFormGroup.updateValueAndValidity(); + } + }, 0); + } else { + this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').disable({emitEvent: false}); + this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').enable(); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterPredicateValueNoDataFormGroup.valid ? null : { + filterPredicateValue: {valid: false} + }; + } + + writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void { + if (isDefinedAndNotNull(predicateValue?.duration?.dynamicValueArgument)) { + const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse); + if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) { + predicateValue.duration.dynamicValueArgument = ''; + } + this.dynamicModeControl.patchValue(true, {emitEvent: false}); + } + this.filterPredicateValueNoDataFormGroup.patchValue(predicateValue, {emitEvent: false}); + } + + private updateModel() { + this.propagateChange(this.filterPredicateValueNoDataFormGroup.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html new file mode 100644 index 0000000000..9db8a856bf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html @@ -0,0 +1,82 @@ + +
+
+ + + {{'alarm-rule.static' | translate}} + {{'alarm-rule.dynamic' | translate}} + + + @if (!dynamicModeControl.value) { + @switch (valueType) { + @case (valueTypeEnum.STRING) { + + + + } + @case (valueTypeEnum.NUMERIC) { + + + + } + @case (valueTypeEnum.BOOLEAN) { + + {{ (filterPredicateValueFormGroup.get('staticValue').value ? 'value.true' : 'value.false') | translate }} + + } + @case (valueTypeEnum.DATE_TIME) { + + } + } + } @else { + + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (filterPredicateValueFormGroup.get('dynamicValueArgument').touched && filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('required')) { + + warning + + } + @if (filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('argumentInUse')) { + + warning + + } + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts new file mode 100644 index 0000000000..8db5bc7325 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts @@ -0,0 +1,183 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { EntityKeyValueType } from '@shared/models/query/query.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { AlarmRuleValue } from "@shared/models/alarm-rule.models"; +import { FormControlsFrom } from "@shared/models/tenant.model"; +import { isDefinedAndNotNull } from "@core/utils"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate-value', + templateUrl: './alarm-rule-filter-predicate-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + } + ], + standalone: false +}) +export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { + + @Input() + arguments: Record; + + @Input() + valueType: EntityKeyValueType; + + @Input() + argumentInUse: string; + + valueTypeEnum = EntityKeyValueType; + + filterPredicateValueFormGroup: FormGroup>>; + + dynamicModeControl = this.fb.control(false); + + argumentsList: Array; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + 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({ + staticValue: [defaultValue, defaultValueValidators], + dynamicValueArgument: ['', Validators.required] + }); + this.filterPredicateValueFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + this.dynamicModeControl.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => this.updateValueModeValidators(value)); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.argumentInUse) { + const argumentInUseChanges = changes.argumentInUse; + if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) { + if (this.dynamicModeControl.value) { + if (this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) { + this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true}); + this.filterPredicateValueFormGroup.updateValueAndValidity(); + } + } + } + } + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.filterPredicateValueFormGroup.disable({emitEvent: false}); + this.dynamicModeControl.disable({emitEvent: false}); + } else { + this.filterPredicateValueFormGroup.enable({emitEvent: false}); + this.dynamicModeControl.enable({emitEvent: false}); + this.updateValueModeValidators(this.dynamicModeControl.value); + } + } + + private updateValueModeValidators(isDynamicMode: boolean): void { + if (isDynamicMode) { + this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable(); + setTimeout(()=> { + if (this.filterPredicateValueFormGroup.get('dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) { + this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true}); + this.filterPredicateValueFormGroup.updateValueAndValidity(); + } + }, 0); + } else { + this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('staticValue').enable(); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterPredicateValueFormGroup.valid ? null : { + filterPredicateValue: {valid: false} + }; + } + + writeValue(predicateValue: AlarmRuleValue): void { + if (isDefinedAndNotNull(predicateValue?.dynamicValueArgument)) { + const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse); + if (!availableArgument.includes(predicateValue.dynamicValueArgument)) { + predicateValue.dynamicValueArgument = ''; + } + this.dynamicModeControl.patchValue(true, {emitEvent: false}); + } + this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); + } + + private updateModel() { + this.propagateChange(this.filterPredicateValueFormGroup.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html new file mode 100644 index 0000000000..904e115225 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html @@ -0,0 +1,92 @@ + +
+
+ @switch (type) { + @case (filterPredicateType.STRING) { +
+ + + @for (operation of stringOperations; track operation) { + + {{stringOperationTranslationMap.get(stringOperation[operation]) | translate}} + + } + + + @if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) { + + {{ 'alarm-rule.ignore-case' | translate }} + + } +
+ } + @case (filterPredicateType.NUMERIC) { +
+ + + @for (operation of numericOperations; track operation) { + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} + + } + + +
+ } + @case (filterPredicateType.BOOLEAN) { +
+ + + @for (operation of booleanOperations; track operation) { + + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} + + } + + +
+ } + @case (filterPredicateType.COMPLEX) { +
+ +
+ } + } + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts new file mode 100644 index 0000000000..c458cd64ed --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts @@ -0,0 +1,217 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { EntityKeyValueType } from '@shared/models/query/query.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + AlarmRuleBooleanOperation, + alarmRuleBooleanOperationTranslationMap, + AlarmRuleFilterPredicate, + AlarmRuleFilterPredicateType, + AlarmRuleNumericOperation, + alarmRuleNumericOperationTranslationMap, + AlarmRuleStringOperation, + alarmRuleStringOperationTranslationMap, + checkPredicates, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; +import { MatDialog } from "@angular/material/dialog"; +import { + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleComplexFilterPredicateDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate', + templateUrl: './alarm-rule-filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + } + ], + standalone: false +}) +export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator { + + @Input({ transform: booleanAttribute }) + disabled: boolean; + + @Input() + valueType: EntityKeyValueType; + + @Input() + arguments: Record; + + @Input() + argumentInUse: string; + + filterPredicateFormGroup = this.fb.group({ + operation: [], + ignoreCase: false, + predicates: [], + value: [], + duration: [] + }); + + type: AlarmRuleFilterPredicateType; + + filterPredicateType = AlarmRuleFilterPredicateType; + + stringOperations = Object.keys(AlarmRuleStringOperation); + stringOperation = AlarmRuleStringOperation; + stringOperationTranslationMap = alarmRuleStringOperationTranslationMap; + + numericOperations = Object.keys(AlarmRuleNumericOperation); + numericOperationEnum = AlarmRuleNumericOperation; + numericOperationTranslations = alarmRuleNumericOperationTranslationMap; + + booleanOperations = Object.keys(AlarmRuleBooleanOperation); + booleanOperationEnum = AlarmRuleBooleanOperation; + booleanOperationTranslations = alarmRuleBooleanOperationTranslationMap; + + predicateValid: boolean = false; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterPredicateFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + + this.filterPredicateFormGroup.get('operation').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => { + if (value === 'NO_DATA') { + this.filterPredicateFormGroup.get('duration').enable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.get('duration').disable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').enable({emitEvent: false}); + } + }) + + this.filterPredicateFormGroup.get('predicates').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(predicates => { + this.predicateValid = this.isPredicateArgumentsValid(predicates); + }) + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterPredicateFormGroup.valid ? null : { + filterPredicate: {valid: false} + }; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.filterPredicateFormGroup.disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.enable({emitEvent: false}); + if (this.filterPredicateFormGroup.get('operation').value === 'NO_DATA') { + this.filterPredicateFormGroup.get('duration').enable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.get('duration').disable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').enable({emitEvent: false}); + } + } + } + + private isPredicateArgumentsValid(predicates: AlarmRuleFilterPredicate[]): boolean { + const validSet = new Set(Object.keys(this.arguments)); + if (Array.isArray(predicates)) { + if (!checkPredicates(predicates, validSet)) { + return false; + } + } + return true; + } + + writeValue(predicate: AlarmRuleFilterPredicate): void { + this.type = predicate.type; + if ((predicate as ComplexAlarmRuleFilterPredicate)?.predicates) { + this.predicateValid = this.isPredicateArgumentsValid((predicate as ComplexAlarmRuleFilterPredicate)?.predicates); + } + if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) { + this.type = AlarmRuleFilterPredicateType[this.valueType]; + this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false}); + } else { + this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); + } + } + + private updateModel() { + const predicate = this.filterPredicateFormGroup.value; + if (predicate.operation === 'NO_DATA') { + this.propagateChange(predicate.duration); + } else { + this.propagateChange({type: this.type, ...predicate}); + } + } + + public openComplexFilterDialog() { + this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: this.filterPredicateFormGroup.value as ComplexAlarmRuleFilterPredicate, + valueType: this.valueType, + isAdd: false, + arguments: this.arguments, + argumentInUse: this.argumentInUse, + readonly: this.disabled + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.patchValue(result); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html new file mode 100644 index 0000000000..9e20a079b4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html @@ -0,0 +1,25 @@ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss new file mode 100644 index 0000000000..5f621d7bb1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + text-overflow: ellipsis; + overflow: hidden; + .tb-filter-text { + overflow-y: auto; + text-align: start; + &.required { + color: #f44336; + padding: 0; + } + &.disabled { + color: rgba(0,0,0,0.38); + } + &.nowrap { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + &.wrap { + white-space: pre-wrap !important; + } + } +} + +: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: 400; + font-style: italic; + } + .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/alarm-rules/filter/alarm-rule-filter-text.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts new file mode 100644 index 0000000000..0758b09e38 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts @@ -0,0 +1,205 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType +} from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { + alarmRuleBooleanOperationTranslationMap, + AlarmRuleExpression, + AlarmRuleExpressionType, + AlarmRuleFilter, + AlarmRuleFilterPredicate, + AlarmRuleFilterPredicateType, + alarmRuleNumericOperationTranslationMap, + AlarmRuleStringOperation, + alarmRuleStringOperationTranslationMap, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { coerceBoolean } from "@shared/decorators/coercion"; +import { timeUnitTranslationMap } from "@shared/models/time/time.models"; + +@Component({ + selector: 'tb-alarm-rule-filter-text', + templateUrl: './alarm-rule-filter-text.component.html', + styleUrls: ['./alarm-rule-filter-text.component.scss'], + providers: [], + standalone: false +}) +export class AlarmRuleFilterTextComponent { + + @Input() + @coerceBoolean() + required = false; + + @Input() + noFilterText = this.translate.instant('filter.no-filter-text'); + + @Input() + addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); + + @Input() + @coerceBoolean() + nowrap = false; + + @Input() + arguments: Record; + + @Input() + @coerceBoolean() + disabled = false; + + private alarmRuleExpressionValue: AlarmRuleExpression; + get alarmRuleExpression(): AlarmRuleExpression { + return this.alarmRuleExpressionValue; + } + + @Input() + set alarmRuleExpression(value: AlarmRuleExpression) { + if (value !== this.alarmRuleExpressionValue) { + this.alarmRuleExpressionValue = value; + this.updateFilterText(value); + } + }; + + private specTextValue: string; + get specText(): string { + return this.specTextValue; + } + @Input() + set specText(value: string) { + if (value !== this.specTextValue) { + this.specTextValue = value; + this.updateFilterText(this.alarmRuleExpression); + } + } + + isRequired = false; + + public filterText: string; + + constructor(private translate: TranslateService, + private datePipe: DatePipe) { + } + + private updateFilterText(value: AlarmRuleExpression) { + this.isRequired = false; + if (value && (value.expression || value.filters?.length)) { + if (value.type === AlarmRuleExpressionType.SIMPLE) { + this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation); + } else { + this.filterText = 'function expression(ctx, ' + (this.arguments ? Object.keys(this.arguments).join(', ') : '' ) + ')'; + } + if (this.specText?.length) { + this.filterText = this.specText + ': ' + this.filterText; + } + } else { + if (this.required) { + this.filterText = this.addFilterPrompt; + this.isRequired = true; + } else { + this.filterText = this.noFilterText; + } + } + } + + private keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array, operation: ComplexOperation): string { + const filtersText = keyFilters.map(keyFilter => + this.filterPredicateToText(translate, datePipe, keyFilter, keyFilter.predicates)); + let result: string; + if (filtersText.length > 1) { + const operationText = translate.instant(complexOperationTranslationMap.get(operation)); + result = filtersText.join(' ' + operationText + ' '); + } else { + result = filtersText[0]; + } + return result; + } + + private filterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: AlarmRuleFilter, + keyFilterPredicates: AlarmRuleFilterPredicate[], + complexOperation?: ComplexOperation): string { + const key = keyFilter.argument; + const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND); + + const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => { + if (keyFilterPredicate.type === AlarmRuleFilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate; + const complexOperation = complexPredicate.operation ?? ComplexOperation.AND; + return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation); + } else { + let operation: string; + let value: string; + const val = keyFilterPredicate.type === AlarmRuleFilterPredicateType.NO_DATA ? keyFilterPredicate.duration : keyFilterPredicate.value; + const dynamicValue = val?.dynamicValueArgument?.length; + if (dynamicValue) { + value = '' + val?.dynamicValueArgument + ''; + } + switch (keyFilterPredicate.type) { + case AlarmRuleFilterPredicateType.STRING: + operation = translate.instant(alarmRuleStringOperationTranslationMap.get(keyFilterPredicate.operation)); + if (keyFilterPredicate.ignoreCase) { + operation += ' ' + translate.instant('filter.ignore-case'); + } + if (!dynamicValue) { + value = `'${keyFilterPredicate.value.staticValue}'`; + } + break; + case AlarmRuleFilterPredicateType.NUMERIC: + operation = translate.instant(alarmRuleNumericOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { + value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm'); + } else { + value = keyFilterPredicate.value.staticValue + ''; + } + } + break; + case AlarmRuleFilterPredicateType.BOOLEAN: + operation = translate.instant(alarmRuleBooleanOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false'); + } + break; + case AlarmRuleFilterPredicateType.NO_DATA: + operation = translate.instant(alarmRuleStringOperationTranslationMap.get(AlarmRuleStringOperation.NO_DATA)); + if (!dynamicValue) { + value = keyFilterPredicate.duration.staticValue + ' ' + translate.instant(timeUnitTranslationMap.get(keyFilterPredicate.unit)).toLowerCase(); + } + break; + } + if (!dynamicValue) { + value = `${value}`; + } + return `${key} ${operation} ${value}` + } + }); + if (predicates.length > 1) { + return '(' + predicates.join(` ${translate.instant(complexOperationTranslationMap.get(filterOperation))} `)+ ')'; + } else { + return predicates.toString(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html index fbbce2a469..4c7ebc7ae2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html @@ -26,32 +26,42 @@ #userAutocomplete="matAutocomplete" [displayWith]="displayUserFn" (optionSelected)="selected($event)"> - - account_circle - {{ assigneeNotSetText | translate }} - - - account_circle - {{ assignedToCurrentUserText | translate }} - - - - -
- - -
-
- -
- - {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + @if (displayAssigneeNotSet) { + + account_circle + {{ assigneeNotSetText | translate }} + + } + @if (displayAssignedToCurrentUser) { + + account_circle + {{ assignedToCurrentUserText | translate }} + + } + @for (user of filteredUsers | async; track user) { + + -
-
+
+ @if (user.firstName || user.lastName) { + + } + +
+ + } + @if (!(filteredUsers | async)?.length) { + +
+ + {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + +
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html index 8367d0eb4a..6148e955ad 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html @@ -19,13 +19,21 @@ - alarm.assignee + @if (!inline) { + alarm.assignee + } - - {{ getUserInitials() }} - - account_circle - arrow_drop_down + @if (assignee) { + + {{ getUserInitials() }} + + } + @if (!assignee) { + account_circle + } + @if (!disabled) { + arrow_drop_down + } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html index 783a763031..a566e9afb7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html @@ -20,12 +20,13 @@ subscriptSizing="dynamic"> alarm.assignee - - {{ getUserInitials(alarm.assignee) }} - - + @if (userAssigned) { + + {{ getUserInitials(alarm.assignee) }} + + } @else { {{ alarm?.assigneeId ? 'no_accounts' : 'account_circle' }} - + } arrow_drop_down diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html index 651a1dd186..a5935e1f2b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -22,9 +22,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html index 25db54641d..31bf4dda43 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -49,104 +49,110 @@
- + @if (!isDirectionAscending()) { - + }
-
-
- - {{ displayDataElement.commentText }} - - - {{ displayDataElement.createdDateAgo }} - -
- -
-
- {{ getUserInitials(displayDataElement.displayName) }} -
- - no_accounts - -
-
- {{ displayDataElement.displayName }} - - {{ displayDataElement.createdDateAgo }} - - - Edited - -
- {{ displayDataElement.commentText }} -
-
- - + @for (displayDataElement of displayData; track displayDataElement; let i = $index) { +
+ @if (displayDataElement.isSystemComment) { +
+ + {{ displayDataElement.commentText }} + + + {{ displayDataElement.createdDateAgo }} +
-
- -
-
- {{ getUserInitials(displayDataElement.displayName) }} -
- - -
+ } @else { + @if (!displayDataElement.edit) { +
+ @if (displayDataElement.userExists) { +
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ } @else { + no_accounts + } +
+
+ {{ displayDataElement.displayName }} + + {{ displayDataElement.createdDateAgo }} + + @if (displayDataElement.isEdited) { + + Edited + + } +
+ {{ displayDataElement.commentText }} +
+
- -
- - -
+
+ } @else { +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ + +
+ + +
+
+
+ } + } +
+ }
- + @if (isDirectionAscending()) { - + }
-
- {{ getUserInitials(userDisplayName) }} -
+ @if (userDisplayName$ | async; as userDisplayName) { +
+ {{ getUserInitials(userDisplayName) }} +
+ } - + @if (getAlarmCommentValue()) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts index 1121b53d3d..cce31840ab 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts @@ -27,12 +27,20 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; import { map } from 'rxjs/operators'; -import { AlarmComment, AlarmCommentType, getUserDisplayName } from '@shared/models/alarm.models'; +import { + AlarmComment, + AlarmCommentInfo, + AlarmCommentType, + AlarmMessage, + AlarmSeverity, + alarmSeverityTranslations, + getUserDisplayName +} from '@shared/models/alarm.models'; import { UtilsService } from '@core/services/utils.service'; import { EntityType } from '@shared/models/entity-type.models'; import { DatePipe } from '@angular/common'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { isNotEmptyStr } from '@core/utils'; +import { deepClone, isNotEmptyStr } from '@core/utils'; interface AlarmCommentsDisplayData { commentId?: string; @@ -122,7 +130,7 @@ export class AlarmCommentComponent implements OnInit { const displayDataElement = {} as AlarmCommentsDisplayData; displayDataElement.createdTime = this.datePipe.transform(alarmComment.createdTime, 'yyyy-MM-dd HH:mm:ss'); displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime); - displayDataElement.commentText = alarmComment.comment.text; + displayDataElement.commentText = this.parseSystemComment(alarmComment); displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM; if (alarmComment.type === AlarmCommentType.OTHER) { displayDataElement.commentId = alarmComment.id.id; @@ -145,6 +153,28 @@ export class AlarmCommentComponent implements OnInit { ); } + private parseSystemComment(alarm: AlarmCommentInfo): string { + const subTypeKey = alarm.comment?.subtype; + if (subTypeKey && AlarmMessage[subTypeKey]) { + const alarmComment = deepClone(alarm.comment); + const translationKey = AlarmMessage[subTypeKey]; + if (alarmComment?.newSeverity) { + alarmComment.newSeverity = + (alarmSeverityTranslations.has(alarmComment.newSeverity) + ? this.translate.instant(alarmSeverityTranslations.get(alarmComment.newSeverity)) + : alarmComment.newSeverity) as AlarmSeverity; + } + if (alarmComment?.oldSeverity) { + alarmComment.oldSeverity = + (alarmSeverityTranslations.has(alarmComment.oldSeverity) + ? this.translate.instant(alarmSeverityTranslations.get(alarmComment.oldSeverity)) + : alarmComment.oldSeverity) as AlarmSeverity; + } + return this.translate.instant(translationKey, alarmComment); + } + return alarm.comment.text; + } + changeSortDirection() { const currentDirection = this.alarmCommentSortOrder.direction; this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC; 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 index dbf649ec37..a47de5b3f7 100644 --- 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 @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
@@ -43,14 +47,18 @@
- - alarm.start-time - - - - alarm.duration - - + @if (alarmFormGroup.get('startTime').value) { + + alarm.start-time + + + } + @if (alarmFormGroup.get('duration').value) { + + alarm.duration + + + }
@@ -71,12 +79,13 @@ - - + @if (displayDetails) { + + + }
@@ -88,25 +97,31 @@
-
- - -
+ @if (alarm$ | async; as alarm;) { +
+ @if (allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK || + alarm.status === alarmStatuses.CLEARED_UNACK)) { + + } + @if (allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK || + alarm.status === alarmStatuses.ACTIVE_UNACK)) { + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html index 966b3374b8..ca1578c054 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -15,11 +15,10 @@ limitations under the License. --> - +@if (panelMode) { - - - +} @else { + @if (buttonMode) { - - + } @else { +
+
+
alarm.alarm-status-list
+ + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + } + +
+
+
alarm.alarm-severity-list
+ + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + } + +
+
+
alarm.alarm-type-list
+ + +
+
+
alarm.assignee
+ + +
+ @if (propagatedFilter) { + + {{ 'alarm.search-propagated-alarms' | translate }} + + } +
+ } +}
@@ -59,17 +102,21 @@
alarm.alarm-status-list
- - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + }
alarm.alarm-severity-list
- - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + }
@@ -85,11 +132,13 @@
alarm.assignee
+ formControlName="assigneeId">
- - {{ 'alarm.search-propagated-alarms' | translate }} - + @if (propagatedFilter) { + + {{ 'alarm.search-propagated-alarms' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts index c296f70455..f236873806 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts @@ -108,7 +108,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal alarmSeverityTranslationMap = alarmSeverityTranslations; - buttonDisplayValue = this.translate.instant('alarm.alarm-filter'); + buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title'); alarmFilterConfigForm: UntypedFormGroup; @@ -225,6 +225,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal cancel() { this.updateAlarmConfigForm(this.alarmFilterConfig); + this.alarmFilterConfigForm.markAsPristine(); if (this.overlayRef) { this.overlayRef.dispose(); } else { 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 index 7dd028db41..adf52b3d39 100644 --- 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 @@ -22,23 +22,29 @@ formControlName="entityInfo" [required]="required" [matAutocomplete]="entityInfoAutocomplete"> - + @if (selectEntityInfoFormGroup.get('entityInfo').value && !disabled) { + + } - - - - - + @for (entityInfo of filteredEntityInfos | async; track entityInfo) { + + + + } + @if (!(filteredEntityInfos | async)?.length) { + + {{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} - - + + + } 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 index d65b790222..4dafef12e4 100644 --- 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 @@ -16,13 +16,15 @@ -->
-
- - -
+ @for (alias of entityAliasesInfo | keyvalue; track alias) { +
+ + +
+ }
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 index add27f87d3..fc1610dc8a 100644 --- 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 @@ -15,18 +15,20 @@ limitations under the License. --> -
- + - devices_other - - - {{displayValue}} - -
+ {{displayValue}} + + +} 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 index 4689bec48f..3394114d88 100644 --- 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 @@ -25,20 +25,26 @@ close - - + @if (isLoading$ | async) { + + + }
alias.name - - {{ 'alias.name-required' | translate }} - - - {{ 'alias.duplicate-alias' | translate }} - + @if (entityAliasFormGroup.get('alias').hasError('required')) { + + {{ 'alias.name-required' | translate }} + + } + @if (entityAliasFormGroup.get('alias').hasError('duplicateAliasName')) { + + {{ 'alias.duplicate-alias' | translate }} + + }
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 index 3ed3b218bf..05aba8344a 100644 --- 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 @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
@@ -40,45 +44,49 @@
-
- {{$index + 1}}. -
- - - - {{ 'entity.alias-required' | translate }} - - - - -
- - -
- - + @for (entityAliasControl of entityAliasesFormArray().controls; track entityAliasControl) { +
+ {{$index + 1}}. +
+ + + @if (entityAliasControl.get('alias').hasError('required')) { + + {{ 'entity.alias-required' | translate }} + + } + + + +
+ + +
+ + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html new file mode 100644 index 0000000000..05549f5260 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html @@ -0,0 +1,89 @@ + + +

{{ 'api-key.generate-title' | translate }}

+ +
+ +
+@if (isLoading$ | async) { + +} +
+
+
+ api-key.generate-text +
+ + api-key.description + + @if (apiKeyForm.get('description').hasError('required')) { + + {{ 'asset.description-required' | translate }} + + } + + + {{ 'api-key.enable' | translate }} + +
+ + api-key.expiration-date + + @for (value of expirationTimeOptions; track value) { + + {{ value | dateExpiration }} + + } + {{'api-key.expiration-time-never' | translate}} + {{'api-key.expiration-time-custom' | translate}} + + + @if (isCustomExpirationTime()) { + + api-key.date + + + + + } +
+
+
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.scss b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.scss new file mode 100644 index 0000000000..7f05b2ca75 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../src/scss/constants'; + +:host { + display: grid; + width: 700px; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content 4px minmax(auto, 1fr) min-content; + + .mat-mdc-dialog-content { + grid-row: 3; + } + .mat-mdc-dialog-actions { + grid-row: 4; + } + .api-key-text { + position: relative; + padding: 8px 16px 8px 16px; + &::before { + content: ''; + position: absolute; + inset: 0; + background-color: $tb-primary-color; + border-radius: 6px; + opacity: 0.04; + } + + span { + font-size: 12px; + color: rgba(0, 0, 0, 0.54); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.ts b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.ts new file mode 100644 index 0000000000..1ac2fa10ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { FormBuilder, Validators } from '@angular/forms'; +import { deepTrim } from '@core/utils'; +import { ApiKeyService } from '@core/http/api-key.service'; +import { ApiKeyInfo } from '@shared/models/api-key.models'; +import { ApiKeysTableDialogData } from '@home/components/api-key/api-keys-table-dialog.component'; +import { DAY } from '@shared/models/time/time.models'; + +@Component({ + selector: 'tb-add-api-key-dialog', + templateUrl: './add-api-key-dialog.component.html', + styleUrls: ['./add-api-key-dialog.component.scss'], + standalone: false +}) +export class AddApiKeyDialogComponent extends DialogComponent { + + readonly startDate = new Date(); + readonly expirationTimeOptions: Array = [7, 30, 60, 90].map(days => days * DAY); + readonly apiKeyForm = this.fb.group({ + description: [{value: null, disabled: false}, [Validators.required]], + enabled: [{value: true, disabled: false}, []], + expirationTime: [{value: this.expirationTimeOptions[1] as string | number, disabled: false}, [Validators.required]], + customExpirationTime: [{value: null, disabled: true}, []], + }); + + constructor( + protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private apiKeyService: ApiKeyService, + @Inject(MAT_DIALOG_DATA) public data: ApiKeysTableDialogData, + ) { + super(store, router, dialogRef); + } + + close(): void { + this.dialogRef.close(null); + } + + add(): void { + const formValue = this.apiKeyForm.value; + const userId = this.data.userId; + const expirationTime = this.calcExpirationTime(); + const apiKey = { + ...deepTrim(formValue), + expirationTime, + userId, + } as ApiKeyInfo; + this.apiKeyService.saveApiKey(apiKey).subscribe( + (res) => { + this.dialogRef.close(res); + } + ); + } + + isCustomExpirationTime() { + return this.apiKeyForm.value?.expirationTime === 'custom'; + } + + onExpirationDateChange() { + const customExpirationTimeControl = this.apiKeyForm.get('customExpirationTime'); + if (this.isCustomExpirationTime()) { + customExpirationTimeControl.enable({emitEvent: false}); + } else { + customExpirationTimeControl.disable({emitEvent: false}); + } + } + + private calcExpirationTime(): number { + const expirationTimeValue = this.apiKeyForm.get('expirationTime').value; + let value: number; + if (this.isCustomExpirationTime()) { + value = this.apiKeyForm.get('customExpirationTime').value.getTime(); + } else if (expirationTimeValue === 'never') { + value = 0; + } else { + value = expirationTimeValue as number + Date.now(); + } + return value; + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html new file mode 100644 index 0000000000..70e7ae7396 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html @@ -0,0 +1,105 @@ + + +

{{ 'api-key.generated-api-key-title' | translate }}

+ + +
+
+
+
api-key.generated-api-key-copy
+
+ +
+
+
api-key.generated-api-key-command
+ + + + + Windows + + +
+
+
device.connectivity.install-necessary-client-tools
+
device.connectivity.install-curl-windows
+
+ +
+
+
+ + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+
device.connectivity.install-curl-macos
+
+ +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ +
+
+
+
+
+
+
+
+ +
+ + +
+
device.connectivity.execute-following-command
+
+ warning + {{ 'api-key.generated-api-key-insecure-url' | translate }} +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.scss b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.scss new file mode 100644 index 0000000000..4c4e2e1f30 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.scss @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../src/scss/constants'; + +:host { + display: grid; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + .tb-install-instruction-text { + min-height: 42px; + } + + .insecure-url-warning { + .mat-icon { + color: #FAA405; + } + } + + @media #{$mat-sm} { + width: 470px; + } + + @media #{$mat-gt-sm} { + width: 720px; + } +} + +:host ::ng-deep { + .tb-markdown-view { + .tb-command-code { + .code-wrapper { + padding: 0; + pre[class*=language-] { + margin: 0; + background: #F3F6FA; + border-color: $tb-primary-color; + padding-right: 38px; + overflow: hidden; + overflow-x: auto; + padding-bottom: 4px; + min-height: 42px; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + } + } + button.clipboard-btn { + right: -2px; + p { + color: $tb-primary-color; + } + p, div { + background-color: #F3F6FA; + } + div { + img { + display: none; + } + &:after { + content: ""; + position: initial; + display: block; + width: 18px; + height: 18px; + background: $tb-primary-color; + mask-image: url(/assets/copy-code-icon.svg); + -webkit-mask-image: url(/assets/copy-code-icon.svg); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + } + } + } + } + } + .mdc-button__label > span { + .mat-icon { + vertical-align: text-bottom; + box-sizing: initial; + } + } + + .tabs-icon { + margin-right: 8px; + } + + .tb-form-panel.tb-tab-body { + padding: 16px 0 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts new file mode 100644 index 0000000000..d080d16451 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts @@ -0,0 +1,81 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { 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 { userInfoCommand, ApiKey } from '@shared/models/api-key.models'; +import { getOS } from '@core/utils'; + +export interface ApiKeyGeneratedDialogData { + apiKey: ApiKey; +} + +@Component({ + selector: 'tb-api-key-generated-dialog', + templateUrl: './api-key-generated-dialog.component.html', + styleUrls: ['api-key-generated-dialog.component.scss'], + standalone: false +}) +export class ApiKeyGeneratedDialogComponent extends DialogComponent { + + private baseUrl = window.location.origin; + + apiKeyCommand = userInfoCommand(this.baseUrl, this.data.apiKey.value); + secureUrl = this.baseUrl.startsWith('https'); + selectedTab: number; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ApiKeyGeneratedDialogData) { + super(store, router, dialogRef); + this.selectTabIndexForUserOS(); + } + + close(): void { + this.dialogRef.close(null); + } + + createMarkDownCommand(command: string): string { + return '```bash\n' + + command + + '{:copy-code}\n' + + '```'; + } + + private selectTabIndexForUserOS() { + const currentOS = getOS(); + switch (currentOS) { + case 'linux': + case 'android': + this.selectedTab = 2; + break; + case 'macos': + case 'ios': + this.selectedTab = 1; + break; + case 'windows': + this.selectedTab = 0; + break; + default: + this.selectedTab = 2; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts new file mode 100644 index 0000000000..1522990d6b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + CellActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Injectable, Renderer2, ViewContainerRef } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { Observable } from 'rxjs'; +import { ApiKeyInfo, ApiKey } from '@shared/models/api-key.models'; +import { ApiKeyService } from '@core/http/api-key.service'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { map } from 'rxjs/operators'; +import { UserId } from '@shared/models/id/user-id'; +import { AddApiKeyDialogComponent } from '@home/components/api-key/add-api-key-dialog.component'; +import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/edit-api-key-description-panel.component'; +import { ApiKeysTableDialogData } from '@home/components/api-key/api-keys-table-dialog.component'; +import { + ApiKeyGeneratedDialogComponent, + ApiKeyGeneratedDialogData +} from '@home/components/api-key/api-key-generated-dialog.component'; + +@Injectable() +export class ApiKeysTableConfig extends EntityTableConfig { + + constructor( + private apiKeyService: ApiKeyService, + private translate: TranslateService, + private customTranslate: CustomTranslatePipe, + private dialog: MatDialog, + private datePipe: DatePipe, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private userId: UserId, + ) { + super(); + + this.entityType = EntityType.API_KEY; + this.detailsPanelEnabled = false; + this.pageMode = false; + + this.entityTranslations = entityTypeTranslations.get(EntityType.API_KEY); + this.entityResources = {} as EntityTypeResource; + this.tableTitle = this.translate.instant('api-key.api-keys'); + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.entitiesFetchFunction = pageLink => this.apiKeyService.getUserApiKeys(this.userId.id, pageLink); + this.addEntity = () => this.addApiKey(); + + this.deleteEntityTitle = entity => this.translate.instant('api-key.delete-api-key-title', {name: entity.description}); + this.deleteEntityContent = () => this.translate.instant('api-key.delete-api-key-text'); + this.deleteEntitiesTitle = count => this.translate.instant('api-key.delete-api-keys-title', {count}); + this.deleteEntitiesContent = () => this.translate.instant('api-key.delete-api-keys-text'); + this.deleteEntity = id => this.apiKeyService.deleteApiKey(id.id); + + this.cellActionDescriptors = this.configureCellActions(); + this.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('description', 'api-key.description', '100%', + (entity) => this.customTranslate.transform(entity?.description), () => ({}), true, () => ({}), + (entity) => entity?.description.length > 40 ? this.customTranslate.transform(entity.description) : undefined, false, + { + name: this.translate.instant('api-key.edit-description'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.updateApiKeyDescription($event, entity) + }), + new EntityTableColumn('active', 'api-key.status', '80px', + entity => this.apiKeyStatus(entity), entity => this.apiKeyStatusStyle(entity), false), + new EntityTableColumn('expirationTime', 'api-key.expiration-time', '120px', + (entity) => entity.expirationTime != 0 ? + this.datePipe.transform(entity.expirationTime, 'dd/MM/yyyy, HH:mm') : + this.translate.instant('api-key.expiration-time-never'), + ), + ); + } + + private configureCellActions(): Array> { + const actions: Array> = []; + actions.push( + { + name: '', + nameFunction: (entity) => this.translate.instant(entity.enabled ? 'api-key.disable' : 'api-key.enable'), + icon: 'mdi:toggle-switch', + isEnabled: (entity) => !entity.expired, + iconFunction: (entity) => entity.enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', + onAction: ($event, entity) => this.toggleEnableMode($event, entity) + } + ) + return actions; + } + + private addApiKey(): Observable { + return this.dialog.open(AddApiKeyDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + userId: this.userId + } + }).afterClosed().pipe(map(res => { + if (res) { + this.apiKeyGenerated(res); + } else { + return null; + } + })); + } + + private apiKeyGenerated(apiKey: ApiKey) { + this.dialog.open(ApiKeyGeneratedDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + apiKey + } + }).afterClosed() + .subscribe(() => { + this.updateData(); + }); + } + + private toggleEnableMode($event: Event, entity: ApiKeyInfo): void { + if ($event) { + $event.stopPropagation(); + } + this.apiKeyService.enableApiKey(entity.id.id, !entity.enabled, {ignoreLoading: true}) + .subscribe( + () => this.updateData() + ); + } + + private apiKeyStatus(apiKey: ApiKeyInfo): string { + let translateKey = 'api-key.status-active'; + let backgroundColor = 'rgba(25, 128, 56, 0.08)'; + if (apiKey.expired) { + translateKey = 'api-key.status-expired'; + backgroundColor = 'rgba(0, 0, 0, 0.04)'; + } else if (!apiKey.enabled) { + translateKey = 'api-key.status-inactive'; + backgroundColor = 'rgba(209, 39, 48, 0.08)'; + } + return `
+ ${this.translate.instant(translateKey)} +
`; + } + + private apiKeyStatusStyle(apiKey: ApiKeyInfo): object { + const styleObj = { + fontSize: '14px', + color: '#198038', + cursor: 'pointer' + }; + if (apiKey.expired) { + styleObj.color = 'rgba(0, 0, 0, 0.54)'; + } else if (!apiKey.enabled) { + styleObj.color = '#d12730'; + } + return styleObj; + } + + private updateApiKeyDescription($event: Event, entity: ApiKeyInfo) { + if ($event) { + $event.stopPropagation(); + } + const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const editSecretDescriptionPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: EditApiKeyDescriptionPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['right', 'bottom', 'top'], + context: { + apiKeyId: entity.id.id, + description: entity.description + }, + isModal: true + }); + editSecretDescriptionPanelPopover.tbComponentRef.instance.descriptionApplied.subscribe(() => { + editSecretDescriptionPanelPopover.hide(); + this.updateData(); + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.html b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.html new file mode 100644 index 0000000000..22aea2c7a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.html @@ -0,0 +1,39 @@ + +
+ +

{{ 'api-key.manage-api-keys' | translate }}

+ +
+ +
+
+ +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.scss similarity index 81% rename from ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.scss index 7b620f8c2f..ebd123b857 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.scss @@ -13,16 +13,15 @@ * 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; - } +:host { + .api-keys-dialog-container { + width: 1080px; + max-width: 100%; - .editable-label { - margin: 5px 0; + .api-keys-dialog-content { + height: 65vh; + border-radius: 0; } } } diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.ts b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.ts new file mode 100644 index 0000000000..208e1ac646 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.ts @@ -0,0 +1,48 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { Router } from '@angular/router'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { UserId } from '@shared/models/id/user-id'; + +export interface ApiKeysTableDialogData { + userId: UserId; +} + +@Component({ + selector: 'tb-api-keys-table-dialog', + templateUrl: './api-keys-table-dialog.component.html', + styleUrls: ['api-keys-table-dialog.component.scss'], + standalone: false +}) +export class ApiKeysTableDialogComponent { + + constructor( + protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ApiKeysTableDialogData, + ) { + } + + close(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.html b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.html new file mode 100644 index 0000000000..969ae31ad5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.html @@ -0,0 +1,20 @@ + +@if (apiKeysTableConfig) { + +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.scss b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.scss new file mode 100644 index 0000000000..78646c14fe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.scss @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../src/scss/constants.scss'; +@import '../../src/scss/mixins'; + +:host ::ng-deep { + tb-entities-table { + .mat-drawer-container { + background-color: white; + + mat-cell.mat-column-description { + white-space: nowrap; + .mat-mdc-icon-button { + vertical-align: middle; + margin-left: 8px; + @include tb-mat-icon-button-size(32); + .mat-icon { + @include tb-mat-icon-size(20); + } + } + span { + display: inline-block; + max-width: 40ch; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + pointer-events: none; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts new file mode 100644 index 0000000000..b27da1456e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts @@ -0,0 +1,81 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + effect, + input, + Renderer2, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DatePipe } from '@angular/common'; +import { ApiKeysTableConfig } from '@home/components/api-key/api-keys-table-config'; +import { ApiKeyService } from '@core/http/api-key.service'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { UserId } from '@shared/models/id/user-id'; + +@Component({ + selector: 'tb-api-keys-table', + templateUrl: './api-keys-table.component.html', + styleUrls: ['./api-keys-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false +}) +export class ApiKeysTableComponent { + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + active = input(); + userId = input(); + + apiKeysTableConfig: ApiKeysTableConfig; + + constructor( + private apiKeyService: ApiKeyService, + private translate: TranslateService, + private customTranslate: CustomTranslatePipe, + private dialog: MatDialog, + private datePipe: DatePipe, + private cd: ChangeDetectorRef, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + ) { + effect(() => { + if (this.active()) { + this.apiKeysTableConfig = new ApiKeysTableConfig( + this.apiKeyService, + this.translate, + this.customTranslate, + this.dialog, + this.datePipe, + this.popoverService, + this.renderer, + this.viewContainerRef, + this.userId(), + ); + this.cd.markForCheck(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html new file mode 100644 index 0000000000..ff93fd0ea7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html @@ -0,0 +1,45 @@ + +
+
{{ 'api-key.edit-description' | translate }}
+ + api-key.description + + @if (descriptionFormControl.hasError('required')) { + + {{ 'asset.description-required' | translate }} + + } + {{ input.value?.length || 0 }}/255 + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.scss b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.scss new file mode 100644 index 0000000000..bf6c450d94 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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-edit-api-key-description-panel { + --mat-form-field-outlined-outline-color: rgba(0,0,0,0.12); + + width: 400px; + max-width: 90vw; + display: flex; + flex-direction: column; + gap: 16px; + .tb-edit-api-key-description-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-edit-api-key-description-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts new file mode 100644 index 0000000000..8f05a755f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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'; +import { FormBuilder, Validators } from '@angular/forms'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { ApiKeyService } from '@core/http/api-key.service'; + +@Component({ + selector: 'tb-edit-api-key-description-panel', + templateUrl: './edit-api-key-description-panel.component.html', + styleUrls: ['./edit-api-key-description-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class EditApiKeyDescriptionPanelComponent implements OnInit { + + @Input() + apiKeyId: string; + + @Input() + description: string; + + @Output() + descriptionApplied = new EventEmitter(); + + descriptionFormControl = this.fb.control(null, Validators.required); + + constructor(private fb: FormBuilder, + private popover: TbPopoverComponent, + private apiKeyService: ApiKeyService) {} + + ngOnInit(): void { + this.descriptionFormControl.setValue(this.description, {emitEvent: false}); + } + + cancel() { + this.popover.hide(); + } + + applyDescription() { + const description = this.descriptionFormControl.value.trim(); + this.apiKeyService.updateApiKeyDescription(this.apiKeyId, description).subscribe(() => { + this.descriptionApplied.emit(description); + }); + } + +} 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 index 9cbd85e15b..eddcf3f9b2 100644 --- 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 @@ -25,21 +25,26 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
-
- - attribute.key - - - {{ (isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required') | translate }} - - - {{ 'attribute.key-max-length' | 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 index 8300e0696c..9c20df523f 100644 --- 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 @@ -23,13 +23,23 @@ import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Valida import { EntityId } from '@shared/models/id/entity-id'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; -import { AttributeData, AttributeScope, LatestTelemetry, TelemetryType } from '@shared/models/telemetry/telemetry.models'; +import { + AttributeData, + AttributeScope, + LatestTelemetry, + TelemetryType +} from '@shared/models/telemetry/telemetry.models'; import { AttributeService } from '@core/http/attribute.service'; import { Observable } from 'rxjs'; +import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; +import { map } from 'rxjs/operators'; +import { ErrorMessageConfig } from '@shared/components/string-autocomplete.component'; +import { TranslateService } from '@ngx-translate/core'; export interface AddAttributeDialogData { entityId: EntityId; attributeScope: TelemetryType; + datasource?: AttributeDatasource; } @Component({ @@ -48,19 +58,22 @@ export class AddAttributeDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData, private attributeService: AttributeService, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - public fb: FormBuilder) { + public fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); } ngOnInit(): void { this.attributeFormGroup = this.fb.group({ - key: ['', [Validators.required, Validators.maxLength(255)]], + key: ['', this.keyValidators], value: [null, [Validators.required]] }); this.isTelemetry = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY; @@ -98,4 +111,18 @@ export class AddAttributeDialogComponent extends DialogComponent this.dialogRef.close(true)); } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return this.data.datasource?.getAllAttributes(this.data.entityId,this.data.attributeScope).pipe( + map(attributes => attributes?.filter(attribute => attribute.key.toLowerCase().includes(search)).map(a => a.key)), + ) + } + + get attributeErrorMessages(): ErrorMessageConfig { + return { + required: this.translate.instant(this.isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required'), + maxlength: this.translate.instant('attribute.key-max-length') + } + } } 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 index 18145ac432..41d5d69edb 100644 --- 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 @@ -25,30 +25,35 @@ close - - + @if (isLoading$ | async) { + + + }
-
+
dashboard.select-existing
-
+
dashboard.create-new - + dashboard.new-dashboard-title - - {{ 'dashboard.title-required' | translate }} - + @if (addWidgetFormGroup.get('newDashboardTitle').hasError('required')) { + + {{ 'dashboard.title-required' | translate }} + + }
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 index c7d204884b..9ac6ac1846 100644 --- 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 @@ -21,34 +21,40 @@
{{telemetryTypeTranslationsMap.get(attributeScope) | translate}} - - attribute.attributes-scope - - - {{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }} - - - + @if (!disableAttributeScopeSelection && !attributeScopeSelectionReadonly) { + + attribute.attributes-scope + + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }} + + } + + + }
- - + @if (attributeScope !== attributeScopeTypes.CLIENT_SCOPE && !disableAddTelemetry) { + + } + @if (!isClientSideTelemetryTypeMap.get(attributeScope)) { + + } - - - - - - -
-
- - - widgets-bundle.empty - widget.select-widgets-bundle + showFirstLastButtons> + @if (mode === 'widget' && widgetsList.length > 0) { + +
+ @for (widgets of widgetsList; track widgets) { +
+ + +
+ } +
+ + @if (widgetsCarouselIndex > 0) { + + } + + + @if (widgetsCarouselIndex < widgetsList.length - 1) { + + } + + +
+
+
+ } + @if (mode === 'widget' && widgetsLoaded && + widgetsList.length === 0 && + widgetBundleSet) { + widgets-bundle.empty + } + @if (mode === 'widget' && !widgetBundleSet) { + widget.select-widgets-bundle + }
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 index fe53f4d988..717ff08020 100644 --- 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 @@ -40,7 +40,7 @@ import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { forkJoin, merge, Observable, Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators'; import { EntityId } from '@shared/models/id/entity-id'; import { AttributeData, @@ -91,6 +91,8 @@ import { hidePageSizePixelValue } from '@shared/models/constants'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; import { FormBuilder } from '@angular/forms'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { AggregationType, defaultTimewindow } from '@shared/models/time/time.models'; +import { TimeService } from '@core/services/time.service'; @Component({ selector: 'tb-attribute-table', @@ -193,6 +195,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI isSysAdmin = false; private destroy$ = new Subject(); + selectAllModel: boolean = false; constructor(protected store: Store, private attributeService: AttributeService, @@ -209,7 +212,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private zone: NgZone, private cd: ChangeDetectorRef, private elementRef: ElementRef, - private fb: FormBuilder) { + private fb: FormBuilder, + private timeService: TimeService) { super(store); this.isSysAdmin = getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN; this.dirtyValue = !this.activeValue; @@ -229,6 +233,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI }); }); this.widgetResize$.observe(this.elementRef.nativeElement); + + this.dataSource.selection.changed.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + this.dataSource.isAllSelected().pipe(take(1)).subscribe(allSelected => { + this.selectAllModel = allSelected; + }); + }); } ngOnDestroy() { @@ -243,6 +255,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.attributeScope = attributeScope; this.mode = 'default'; this.paginator.pageIndex = 0; + this.selectAllModel = false; this.updateData(true); } @@ -324,13 +337,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if ($event) { $event.stopPropagation(); } + const data: AddAttributeDialogData = { + entityId: this.entityIdValue, + attributeScope: this.attributeScope, + }; + + if(this.attributeScope === LatestTelemetry.LATEST_TELEMETRY) { + data.datasource = this.dataSource; + } + this.dialog.open(AddAttributeDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - entityId: this.entityIdValue, - attributeScope: this.attributeScope - } + data }).afterClosed().subscribe( (res) => { if (res) { @@ -550,7 +569,6 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI type: dataKeyType, color: this.utils.getMaterialColor(i), settings: {}, - _hash: Math.random() }; this.widgetDatasource.dataKeys.push(dataKey); } @@ -577,7 +595,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.widgetsLoaded = false; this.widgetService.getBundleWidgetTypes(widgetsBundle.id.id).subscribe( (widgetTypes) => { - widgetTypes = widgetTypes.sort((a, b) => { + widgetTypes = widgetTypes.filter(widget => !widget.deprecated).sort((a, b) => { let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); if (result === 0) { result = b.createdTime - a.createdTime; @@ -601,6 +619,11 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI }; widget.config.title = widgetInfo.widgetName; widget.config.datasources = [this.widgetDatasource]; + if (widget.type === widgetType.timeseries && widget.config.useDashboardTimewindow) { + widget.config.useDashboardTimewindow = false; + widget.config.timewindow = defaultTimewindow(this.timeService); + widget.config.timewindow.aggregation.type = AggregationType.NONE; + } if ((this.attributeScope === LatestTelemetry.LATEST_TELEMETRY && widgetInfo.type !== widgetType.rpc) || widgetInfo.type === widgetType.latest) { const length = this.widgetsListCache.push([widget]); diff --git a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html index 600d3f2a93..251496096b 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html @@ -25,31 +25,37 @@ - + attribute.delete-timeseries.strategy - - {{ strategiesTranslationsMap.get(strategy) | translate }} - + @for (strategy of strategiesTranslationsMap.keys(); track strategy) { + + {{ strategiesTranslationsMap.get(strategy) | translate }} + + } -
- - attribute.delete-timeseries.start-time - - - - - - attribute.delete-timeseries.ends-on - - - - -
- - {{ "attribute.delete-timeseries.rewrite-latest-value" | translate }} - + @if (isPeriodStrategy()) { +
+ + attribute.delete-timeseries.start-time + + + + + + attribute.delete-timeseries.ends-on + + + + +
+ } + @if (isPeriodStrategy() || isDeleteLatestStrategy()) { + + {{ "attribute.delete-timeseries.rewrite-latest-value" | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss index c42ab55735..4a5969afe7 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss @@ -26,7 +26,7 @@ display: flex; flex-direction: column; gap: 16px; - padding-top: 0; + padding-top: 4px; } .tb-select-interval { 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 index a27409c21c..9db295dc26 100644 --- 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 @@ -24,13 +24,14 @@ close -
- -
+
+
+ audit-log.action-data +
- - -
+
+ audit-log.failure-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 index 14ea242150..98805e29a5 100644 --- 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 @@ -13,13 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +@import '../../../../../scss/constants'; + :host { - .tb-audit-log-action-data, - .tb-audit-log-failure-details { - width: 100%; - min-width: 400px; - height: 100%; - min-height: 50px; + display: grid; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + @media #{$mat-gt-xs} { + .mat-mdc-dialog-content { + max-height: 80vh; + } + } + + .tb-audit-log { + min-width: max(400px, 100%); border: 1px solid #c0c0c0; + @media #{$mat-xs} { + min-width: 100%; + } } } 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 index d430e97e1b..eeb976ebf8 100644 --- 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 @@ -22,7 +22,11 @@ import { ActionStatus, AuditLog } from '@shared/models/audit-log.models'; import { Ace } from 'ace-builds'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; -import { getAce } from '@shared/models/ace/ace.models'; +import { getAce, updateEditorSize } from '@shared/models/ace/ace.models'; +import { Observable, of } from 'rxjs'; +import { isObject } from '@core/utils'; +import { ContentType, contentTypesMap } from '@shared/models/constants'; +import { beautifyJs } from '@shared/models/beautify.models'; export interface AuditLogDetailsDialogData { auditLog: AuditLog; @@ -42,11 +46,10 @@ export class AuditLogDetailsDialogComponent extends DialogComponent, protected router: Router, @@ -59,12 +62,10 @@ export class AuditLogDetailsDialogComponent extends DialogComponent = { - 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}; - getAce().subscribe( - (ace) => { - const editor = ace.edit(editorElement, editorOptions); - this.aceEditors.push(editor); - editor.session.setUseWrapMode(false); - editor.setValue(content, -1); - this.updateEditorSize(editorElement, content, editor); - } - ); - } - - updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { - let newHeight = 200; - let newWidth = 600; - if (content && content.length > 0) { - const lines = content.split('\n'); - newHeight = 18 * 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 = 9 * maxLineLength + 16; + let mode = 'java'; + let content$: Observable = null; + let contentType = ContentType.TEXT; + if (content && isObject(content)) { + contentType = ContentType.JSON; + mode = contentTypesMap.get(contentType).code; + content$ = beautifyJs(JSON.stringify(content), {indent_size: 2}); + } + if (!content$) { + content$ = of(content as string); } - // 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(); + content$.subscribe((processedContent) => { + const isJSON = contentType === ContentType.JSON + const editorOptions: Partial = { + mode: `ace/mode/${mode}`, + theme: 'ace/theme/github', + showGutter: isJSON, + showFoldWidgets: true, + foldStyle: 'markbeginend', + showPrintMargin: false, + readOnly: true, + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + }; + getAce().subscribe( + (ace) => { + const editor = ace.edit(editorElement, editorOptions); + this.aceEditors.push(editor); + editor.session.setUseWrapMode(false); + editor.setValue(processedContent, -1); + updateEditorSize(editorElement, processedContent, editor, this.renderer, {showGutter: isJSON}); + } + ) + }); } } diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html new file mode 100644 index 0000000000..fd5311ba42 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html @@ -0,0 +1,60 @@ + + + +
+
+
+
audit-log.filter-types
+ + +
+
+
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss new file mode 100644 index 0000000000..501a5e2419 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + max-width: 100%; + padding-right: 20px; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +::ng-deep { + .tb-audit-log-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts new file mode 100644 index 0000000000..631a795f7d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts @@ -0,0 +1,212 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { + ChangeDetectorRef, + Component, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnInit, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { isDefined, isDefinedAndNotNull } from '@app/core/utils'; +import { StringItemsOption } from '@app/shared/components/string-items-list.component'; +import { + ActionType, + actionTypeTranslations, + AuditLogFilter, + auditLogFilterEquals, + POSITION_MAP, +} from '@app/shared/public-api'; +import { TranslateService } from '@ngx-translate/core'; + +// @dynamic +@Component({ + selector: 'tb-audit-log-filter', + templateUrl: './audit-log-filter.component.html', + styleUrls: ['./audit-log-filter.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AuditLogFilterComponent), + multi: true + } + ], + standalone: false +}) +export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { + + @ViewChild('auditLogFilterPanel') + auditLogFilterPanel: TemplateRef; + + @Input() disabled: boolean; + + ActionType = ActionType; + + actionTypes = Object.values(ActionType); + + actionTypeTranslations = actionTypeTranslations; + + buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title'); + + auditLogFilterForm: FormGroup; + + auditLogOverlayRef: OverlayRef; + + initialAuditLogFilter: AuditLogFilter; + + private auditLogFilter: AuditLogFilter; + + private propagateChange = (_: any) => {}; + + constructor(private fb: FormBuilder, + private translate: TranslateService, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) {} + + ngOnInit(): void { + this.auditLogFilterForm = this.fb.group({ + actionTypes: [[]] + }); + this.auditLogFilterForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + this.updateValidators(); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.auditLogFilterForm.disable({emitEvent: false}); + } else { + this.auditLogFilterForm.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(auditLogFilter?: AuditLogFilter): void { + this.auditLogFilter = auditLogFilter; + if(!this.initialAuditLogFilter && isDefined(this.auditLogFilter)) { + this.initialAuditLogFilter = this.auditLogFilterForm.getRawValue(); + } + this.updateButtonDisplayValue(); + this.updateAuditLogFilterForm(auditLogFilter); + } + + get predefinedTypeValues (): StringItemsOption[] { + return [...this.actionTypes].map(type => ({ + name: this.translate.instant(this.actionTypeTranslations.get(type)), + value: type + })); + } + + private updateValidators() { + } + + toggleAuditLogFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); + + this.auditLogOverlayRef = this.overlay.create(config); + this.auditLogOverlayRef.backdropClick().subscribe(() => { + this.auditLogOverlayRef.dispose(); + }); + this.auditLogOverlayRef.attach(new TemplatePortal(this.auditLogFilterPanel, + this.viewContainerRef)); + } + + cancel() { + this.updateAuditLogFilterForm(this.auditLogFilter); + this.auditLogFilterForm.markAsPristine(); + this.auditLogOverlayRef.dispose(); + } + + update() { + this.auditLogFilterUpdated(this.auditLogFilterForm.value); + this.auditLogFilterForm.markAsPristine(); + this.auditLogOverlayRef.dispose(); + } + + reset() { + if (this.initialAuditLogFilter) { + if (!auditLogFilterEquals(this.auditLogFilterForm.value, this.initialAuditLogFilter)) { + this.updateAuditLogFilterForm(this.initialAuditLogFilter); + this.auditLogFilterForm.markAsDirty(); + } + } + } + + private updateAuditLogFilterForm(auditLogFilter?: AuditLogFilter) { + this.auditLogFilterForm.patchValue({ + actionTypes: auditLogFilter?.actionTypes, + }, {emitEvent: false}); + this.updateValidators(); + } + + private auditLogFilterUpdated(auditLogFilter: AuditLogFilter) { + this.auditLogFilter = auditLogFilter; + this.updateButtonDisplayValue(); + this.propagateChange(this.auditLogFilter); + } + + private updateButtonDisplayValue() { + const filterTextParts: string[] = []; + if (isDefinedAndNotNull(this.auditLogFilter?.actionTypes)) { + const actionTypes = this.auditLogFilter.actionTypes; + actionTypes.forEach(actionType => filterTextParts.push(this.translate.instant(this.actionTypeTranslations.get(actionType as ActionType)))) + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('audit-log.filter-title') + `: ${filterTextParts.join(', ')}`; + } + this.cd.detectChanges(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html new file mode 100644 index 0000000000..c82257803d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html @@ -0,0 +1,20 @@ + + diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss similarity index 87% rename from application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java rename to ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss index edd3a35220..0d057f16ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf; -public interface CalculatedFieldInitService { +:host { + padding-right: 8px; + overflow: hidden; + max-width: 100%; } diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts new file mode 100644 index 0000000000..1fdc68cdfa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { AppState } from '@app/core/public-api'; +import { AuditLog, AuditLogFilter, TimePageLink } from '@app/shared/public-api'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; + +@Component({ + selector: 'tb-audit-log-header', + templateUrl: './audit-log-header.component.html', + styles: ``, + standalone: false +}) +export class AuditLogHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store) + } + + auditLogFiltersChanged(auditLogFilter: AuditLogFilter) { + this.entitiesTableConfig.componentsData.auditLogFilter = auditLogFilter; + this.entitiesTableConfig.getTable().resetSortAndFilter(true, true); + } +} 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 index 5b2a48e24a..a5905fd13c 100644 --- 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 @@ -25,7 +25,8 @@ import { ActionType, actionTypeTranslations, AuditLog, - AuditLogMode + AuditLogMode, + AuditLogFilter } from '@shared/models/audit-log.models'; import { AliasEntityType, @@ -48,6 +49,8 @@ import { AuditLogDetailsDialogComponent, AuditLogDetailsDialogData } from '@home/components/audit-log/audit-log-details-dialog.component'; +import { deepClone } from '@app/core/utils'; +import { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.component'; export class AuditLogTableConfig extends EntityTableConfig { @@ -71,6 +74,7 @@ export class AuditLogTableConfig extends EntityTableConfig; + this.componentsData = { + auditLogFilter: [] + }; + this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink); this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; @@ -139,15 +147,16 @@ export class AuditLogTableConfig extends EntityTableConfig> { + const auditLogFilter: AuditLogFilter = deepClone(this.componentsData?.auditLogFilter) || {}; switch (this.auditLogMode) { case AuditLogMode.TENANT: - return this.auditLogService.getAuditLogs(pageLink); + return this.auditLogService.getAuditLogs(pageLink, auditLogFilter); case AuditLogMode.ENTITY: - return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink); + return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink, auditLogFilter); case AuditLogMode.USER: - return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink); + return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink, auditLogFilter); case AuditLogMode.CUSTOMER: - return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink); + return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink, auditLogFilter); } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html new file mode 100644 index 0000000000..86d183dcfd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html @@ -0,0 +1,151 @@ + +@if (!standalone) { +
+ + + +
+} +
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + + + {{ 'common.type' | translate }} + + + {{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value) + ? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate) + : entityForm.get('type').value }} + + @for (type of fieldTypes; track type) { + + + {{ CalculatedFieldTypeTranslations.get(type).name | translate }} + +
+ + {{ CalculatedFieldTypeTranslations.get(type).hint | translate }} + +
+ } +
+
+
+ @switch (entityForm.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + + } + @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } + @default { + + } + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss new file mode 100644 index 0000000000..695df5063a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + .tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; + } + + .tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts new file mode 100644 index 0000000000..4790789080 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -0,0 +1,169 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldConfiguration, + CalculatedFieldInfo, + calculatedFieldsEntityTypeList, + CalculatedFieldType, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-calculated-field', + templateUrl: './calculated-field.component.html', + styleUrls: ['./calculated-field.component.scss'], + standalone: false +}) +export class CalculatedFieldComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + disabledConfiguration = false; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + readonly fieldTypes = calculatedFieldTypes; + readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action( + { id: this.entity.id, ...this.entityFormValue() }, false, + (expression) => { + if (expression) { + this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); + this.entityForm.get('configuration').markAsDirty(); + } + }), + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldInfo): FormGroup { + const form = inject(CalculatedFieldFormService).buildForm(); + inject(CalculatedFieldFormService).setupTypeChange(form, inject(DestroyRef), () => this.isEditValue); + return form; + } + + updateForm(entity: CalculatedFieldInfo) { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + const preparedConfig = this.cfFormService.prepareConfig(configuration); + this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true}); + setTimeout(() => { + this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); + this.entityForm.get('type').updateValueAndValidity(); + }); + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityValue, + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts new file mode 100644 index 0000000000..c58b595ba1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -0,0 +1,80 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldDialogComponent +} from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; +import { + CalculatedFieldScriptTestDialogComponent +} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component'; +import { + CalculatedFieldTestArgumentsComponent +} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; +import { + EntityDebugSettingsButtonComponent +} from '@home/components/entity/debug/entity-debug-settings-button.component'; +import { + GeofencingConfigurationModule +} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module'; +import { + SimpleConfigurationModule +} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module'; +import { + PropagationConfigurationModule +} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module'; +import { + RelatedEntitiesAggregationComponentModule +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module'; +import { + EntityAggregationComponentModule +} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module'; +import { + CalculatedFieldsHeaderComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; +import { + CalculatedFieldsFilterConfigComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; + +@NgModule({ + declarations: [ + CalculatedFieldDialogComponent, + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestArgumentsComponent, + CalculatedFieldsHeaderComponent, + CalculatedFieldsFilterConfigComponent, + CalculatedFieldComponent, + ], + imports: [ + CommonModule, + SharedModule, + GeofencingConfigurationModule, + EntityDebugSettingsButtonComponent, + SimpleConfigurationModule, + PropagationConfigurationModule, + RelatedEntitiesAggregationComponentModule, + EntityAggregationComponentModule, + ], + exports: [ + CalculatedFieldDialogComponent, + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldComponent, + ] +}) +export class CalculatedFieldsModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 72805454ff..aac49a3f37 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -16,15 +16,16 @@ import { DateEntityTableColumn, + EntityLinkTableColumn, EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { PageLink } from '@shared/models/page/page-link'; -import { Observable, of } from 'rxjs'; +import { EMPTY, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; import { Store } from '@ngrx/store'; @@ -34,64 +35,102 @@ import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; -import { catchError, filter, switchMap, tap } from 'rxjs/operators'; +import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; import { ArgumentEntityType, ArgumentType, CalculatedField, CalculatedFieldEventArguments, + CalculatedFieldInfo, + CalculatedFieldScriptConfiguration, + CalculatedFieldsQuery, CalculatedFieldType, CalculatedFieldTypeTranslations, + debugCfActionEnabled, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, + PropagationWithExpression, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, - CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData } from './components/public-api'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { isObject } from '@core/utils'; +import { deepClone, getEntityDetailsPageURL, isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; +import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; +import { + CalculatedFieldsHeaderComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; +import { Router } from '@angular/router'; +import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component'; + +export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; -export class CalculatedFieldsTableConfig extends EntityTableConfig { +export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { - title: this.translate.instant('calculated-fields.see-debug-events'), - action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), + title: this.translate.instant('action.see-debug-events'), + action: (calculatedField: CalculatedFieldsTableEntity, + openCalculatedFieldEdit = true, + afterCloseCallback?: (expression: string) => void) => this.openDebugEventsDialog.call(this, null, calculatedField, openCalculatedFieldEdit, afterCloseCallback), }; + calculatedFieldFilterConfig: CalculatedFieldsQuery; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, private datePipe: DatePipe, - public entityId: EntityId = null, + private entityId: EntityId = null, private store: Store, private destroyRef: DestroyRef, private renderer: Renderer2, - public entityName: string, + private entityName: string, + private ownerId: EntityId = null, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, + private router: Router, + public pageMode = false, ) { super(); + if (this.pageMode) { + this.headerComponent = CalculatedFieldsHeaderComponent; + + this.entityComponent = CalculatedFieldComponent; + this.entityTabsComponent = CalculatedFieldsTabsComponent; + this.rowPointer = true; + } else { + this.addAsTextButton = false; + } this.tableTitle = this.translate.instant('entity.type-calculated-fields'); - this.detailsPanelEnabled = false; - this.pageMode = false; + this.detailsPanelEnabled = this.pageMode; this.entityType = EntityType.CALCULATED_FIELD; this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD); + this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD); this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedFieldDialog.bind(this); - this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); + this.saveEntity = (cf) => this.calculatedFieldsService.saveCalculatedField(cf); + this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id); + this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + + this.onEntityAction = action => this.onCFAction(action); + this.addActionDescriptors = [ { name: this.translate.instant('calculated-fields.create'), @@ -109,34 +148,37 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('expression', 'calculated-fields.expression', '300px'); - expressionColumn.sortable = false; - expressionColumn.cellContentFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`; - } - expressionColumn.cellTooltipFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel.length < 45 ? null : expressionLabel - }; - this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'common.name', '33%')); - this.columns.push(new EntityTableColumn('type', 'common.type', '50px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type)))); - this.columns.push(expressionColumn); + this.columns.push(new EntityTableColumn('name', 'common.name', this.pageMode ? '33%' : '60%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); + if (this.pageMode) { + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', + entity => this.utilsService.customTranslation(entity.entityName, entity.entityName), + entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); + } + this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '23%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.cellActionDescriptors.push( + { + name: this.translate.instant('action.copy'), + icon: 'content_copy', + isEnabled: () => true, + onAction: ($event, entity) => this.copyCalculatedField($event, entity), + }, { name: this.translate.instant('action.export'), icon: 'file_download', isEnabled: () => true, - onAction: (event$, entity) => this.exportCalculatedField(event$, entity), + onAction: ($event, entity) => this.exportCalculatedField($event, entity), }, { name: this.translate.instant('entity-view.events'), icon: 'mdi:clipboard-text-clock', isEnabled: () => true, - onAction: (_, entity) => this.openDebugEventsDialog(entity), + onAction: ($event, entity) => + this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity), }, { name: '', @@ -145,37 +187,31 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), - }, - { + } + ); + if (!this.pageMode) { + this.cellActionDescriptors.push({ name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, - onAction: (_, entity) => this.editCalculatedField(entity), - } - ); - } - - private getExpressionLabel(entity: CalculatedField): string { - if (entity.type === CalculatedFieldType.SCRIPT) { - return 'function calculate(ctx, ' + Object.keys(entity.configuration.arguments).join(', ') + ')'; - } else { - return entity.configuration.expression; + onAction: ($event, entity) => this.editCalculatedField($event, entity), + }) } } - fetchCalculatedFields(pageLink: PageLink): Observable> { - return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); + fetchCalculatedFields(pageLink: PageLink): Observable> { + return this.pageMode ? + this.calculatedFieldsService.getCalculatedFields(pageLink, this.calculatedFieldFilterConfig): + this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink); } - onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + private onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void { + $event?.stopPropagation(); const { debugSettings = {}, id } = calculatedField; const additionalActionConfig = { ...this.additionalDebugActionConfig, - action: () => this.openDebugEventsDialog(calculatedField) + action: () => this.openDebugEventsDialog($event, calculatedField) }; - if ($event) { - $event.stopPropagation(); - } const { viewContainerRef, renderer } = this.entityDebugSettingsService; if (!viewContainerRef || !renderer) { @@ -193,7 +229,24 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig 1) { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + } else { + table.entityDetailsPanel.matTabGroup._tabs.changes.pipe( + first() + ).subscribe(() => { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + }) + } + } + } + + private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void { + $event?.stopPropagation(); this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty) .subscribe((res) => { if (res) { @@ -202,19 +255,23 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false, disabledSelectType = false): Observable { + const entityId = this.entityId || value?.entityId; + const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; return this.dialog.open(CalculatedFieldDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, buttonTitle, - entityId: this.entityId, + entityId, + entityName, tenantId: this.tenantId, - entityName: this.entityName, + ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, additionalDebugActionConfig: this.additionalDebugActionConfig, getTestScriptDialogFn: this.getTestScriptDialog.bind(this), isDirty, + disabledSelectType }, enterAnimationDuration: isDirty ? 0 : null, }) @@ -222,32 +279,75 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig(CalculatedFieldDebugDialogComponent, { + private openDebugEventsDialog($event: Event, calculatedField: CalculatedFieldsTableEntity, openCalculatedFieldEdit = true, afterCloseCallback?: (expression: string) => void ): void { + $event?.stopPropagation(); + + const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef) => { + this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit) + .subscribe(expression => dialogRef.close(expression)); + }; + + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + title: 'calculated-fields.debugging', tenantId: this.tenantId, - value: calculatedField, - getTestScriptDialogFn: this.getTestScriptDialog.bind(this), + entityId: calculatedField.id, + debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], + disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], + defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, + onDebugEventSelected, + debugActionDisabled: !debugCfActionEnabled(calculatedField) } }) .afterClosed() - .subscribe(); + .subscribe(value => { + if (afterCloseCallback) { + afterCloseCallback(value) + } + }); } - private exportCalculatedField($event: Event, calculatedField: CalculatedField): void { - if ($event) { - $event.stopPropagation(); - } + private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void { + $event?.stopPropagation(); this.importExportService.exportCalculatedField(calculatedField.id.id); } + private copyCalculatedField($event: Event, calculatedField: CalculatedField): void { + $event?.stopPropagation(); + const copyCalculatedField = deepClone(calculatedField); + if (this.pageMode) { + copyCalculatedField.entityId = null; + delete (copyCalculatedField as CalculatedFieldInfo).entityName; + } + delete copyCalculatedField.id; + this.getCalculatedFieldDialog(copyCalculatedField, 'action.apply', false) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + private importCalculatedField(): void { this.importExportService.openCalculatedFieldImportDialog() .pipe( filter(Boolean), - switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), + switchMap(calculatedField => { + if (calculatedField.type === CalculatedFieldType.ALARM || !CalculatedFieldType[calculatedField.type]) { + this.store.dispatch(new ActionNotificationShow({ + message: this.translate.instant('calculated-fields.hint.import-invalid-calculated-field-type'), + type: 'error', + verticalPosition: 'top', + horizontalPosition: 'left', + duration: 5000 + })); + return EMPTY; + } + return of(calculatedField); + }), + switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true, true)), filter(Boolean), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), filter(Boolean), @@ -257,13 +357,23 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - const arg = calculatedField.configuration.arguments[key]; - acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant - ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } - : arg; - return acc; - }, {}); + if (calculatedField.type === CalculatedFieldType.GEOFENCING) { + calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => { + const arg = calculatedField.configuration.zoneGroups[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } else { + calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const arg = calculatedField.configuration.arguments[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } return calculatedField; } @@ -276,33 +386,62 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable { - const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { - const type = calculatedField.configuration.arguments[key].refEntityKey.type; - acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) - ? { ...argumentsObj[key], type } - : type === ArgumentType.Rolling ? { values: [], type } : { value: '', type, ts: new Date().getTime() }; - return acc; - }, {}); - return this.dialog.open(CalculatedFieldScriptTestDialogComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], - data: { - arguments: resultArguments, - expression: calculatedField.configuration.expression, - argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), - argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), - openCalculatedFieldEdit - } - }).afterClosed() - .pipe( - filter(Boolean), - tap(expression => { - if (openCalculatedFieldEdit) { - this.editCalculatedField({ entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression } }, true) + getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + if ( + calculatedField.type === CalculatedFieldType.SCRIPT || + calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION || + (calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true) + ) { + const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const type = calculatedField.configuration.arguments[key].refEntityKey.type; + acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) + ? {...argumentsObj[key], type} + : type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()}; + return acc; + }, {}); + return this.dialog.open(CalculatedFieldScriptTestDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], + data: { + arguments: resultArguments, + expression: expression ?? (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression, + argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), + argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), + openCalculatedFieldEdit } - }), - ); + }).afterClosed() + .pipe( + filter(Boolean), + tap(expression => { + if (openCalculatedFieldEdit) { + this.editCalculatedField(null, { + entityId: this.entityId, ...calculatedField, + configuration: {...calculatedField.configuration, expression} as any + }, true) + } + }), + ); + } else { + return of(null); + } + } + + private openCalculatedField($event: Event, entity: CalculatedFieldsTableEntity) { + $event?.stopPropagation(); + const url = this.router.createUrlTree(['calculatedFields', entity.id.id]); + this.router.navigateByUrl(url); + } + + private onCFAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openCalculatedField(action.event, action.entity); + return true; + case 'export': + this.exportCalculatedField(action.event, action.entity); + return true; + } + return false; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html index 7c2317dbac..102a334402 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html @@ -16,5 +16,5 @@ --> @if (calculatedFieldsTableConfig) { - + } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss index f7fd0d1dc6..175df5b779 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss @@ -14,9 +14,7 @@ * limitations under the License. */ :host ::ng-deep { - tb-entities-table { - .mat-drawer-container { - background-color: white; - } + .white-background { + --mat-sidenav-content-background-color: white; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 741e9851e1..feba407f52 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -35,6 +35,8 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'tb-calculated-fields-table', @@ -51,9 +53,12 @@ export class CalculatedFieldsTableComponent { active = input(); entityId = input(); entityName = input(); + ownerId = input(); calculatedFieldsTableConfig: CalculatedFieldsTableConfig; + pageMode: boolean = false; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, @@ -63,10 +68,14 @@ export class CalculatedFieldsTableComponent { private renderer: Renderer2, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, - private destroyRef: DestroyRef) { - + private utilsService: UtilsService, + private destroyRef: DestroyRef, + private route: ActivatedRoute, + private router: Router + ) { + this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { - if (this.active()) { + if (this.active() || this.pageMode) { this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( this.calculatedFieldsService, this.translate, @@ -77,8 +86,12 @@ export class CalculatedFieldsTableComponent { this.destroyRef, this.renderer, this.entityName(), + this.ownerId(), this.importExportService, this.entityDebugSettingsService, + this.utilsService, + this.router, + this.pageMode, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html new file mode 100644 index 0000000000..56f6cb0331 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -0,0 +1,252 @@ + +
+
{{ 'calculated-fields.argument-settings' | translate }}
+
+ @if (hint) { +
+ {{ hint | translate }} +
+ } +
+ @if (!watchKeyChange) { + + } + + @if (!hiddenEntityTypes) { +
+
{{ 'entity.entity-type' | translate }}
+ + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + @if (argumentType.touched && argumentType.hasError('required')) { + + warning + + } + +
+ } + @if (ArgumentEntityTypeParamsMap.has(entityType)) { +
+
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
+ +
+ } +
+ + @if (!hiddenEntityKeyTypes) { +
+
{{ 'calculated-fields.argument-type' | translate }}
+ + + @for (type of argumentTypes; track type) { + {{ ArgumentTypeTranslations.get(type) | translate }} + } + + @if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) { + + warning + + } + +
+ } + @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) { + @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { +
+
{{ 'calculated-fields.timeseries-key' | translate }}
+ @if (refEntityKeyFormGroup.get('type').value === ArgumentType.LatestTelemetry) { + + } @else { + + } + + + +
+ } @else { + @if (enableAttributeScopeSelection) { +
+
{{ 'calculated-fields.attribute-scope' | translate }}
+ + + + {{ 'calculated-fields.server-attributes' | translate }} + + + {{ 'calculated-fields.client-attributes' | translate }} + + + {{ 'calculated-fields.shared-attributes' | translate }} + + + +
+ } +
+
{{ 'calculated-fields.attribute-key' | translate }}
+ +
+ } + } +
+ @if (watchKeyChange) { + + } + @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { + @if (!hiddenDefaultValue) { +
+
{{ 'calculated-fields.default-value' | translate }}
+ + + @if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) { + + warning + + } + +
+ } + } @else { +
+
{{ 'calculated-fields.time-window' | translate }}
+ +
+ @if (maxDataPointsPerRollingArg) { +
+
{{ 'calculated-fields.limit' | translate }}
+
+ + + + + + +
+
+ } + } +
+
+
+ + @if (!readonly) { + + } +
+
+ + +
+
{{ label | translate }}
+ + + @if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { + + warning + + } + +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss similarity index 79% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss index 1254a30aee..36280dafb7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss @@ -15,22 +15,9 @@ */ @use '../../../../../../../scss/constants' as constants; -$panel-width: 520px; - :host { - display: flex; - width: $panel-width; - max-width: 100%; - max-height: 100vh; - - .fixed-title-width { - @media #{constants.$mat-xs} { - min-width: 120px; - } - } - .limit-field-row { - @media screen and (max-width: $panel-width) { + @media screen and (max-width: 520px) { display: flex; flex-direction: column; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts similarity index 56% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 494d083b66..c4d4552999 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -14,9 +14,18 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + DestroyRef, + Input, + OnInit, + output, + ViewChild +} from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { ArgumentEntityType, @@ -25,10 +34,12 @@ import { ArgumentType, ArgumentTypeTranslations, CalculatedFieldArgumentValue, - CalculatedFieldType, - getCalculatedFieldCurrentEntityFilter + FORBIDDEN_NAMES, + forbiddenNamesValidator, + getCalculatedFieldCurrentEntityFilter, + uniqueNameValidator } from '@shared/models/calculated-field.models'; -import { debounceTime, delay, distinctUntilChanged, filter } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DatasourceType } from '@shared/models/widget.models'; @@ -43,11 +54,12 @@ import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { TenantId } from '@shared/models/id/tenant-id'; @Component({ selector: 'tb-calculated-field-argument-panel', templateUrl: './calculated-field-argument-panel.component.html', - styleUrls: ['./calculated-field-argument-panel.component.scss'], + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-argument-panel.component.scss'], standalone: false }) export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit { @@ -57,22 +69,40 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; - @Input() calculatedFieldType: CalculatedFieldType; + @Input() ownerId: EntityId; + @Input() isScript: boolean; @Input() usedArgumentNames: string[]; + @Input() watchKeyChange = false; + @Input() hiddenEntityTypes = false; + @Input() hiddenEntityKeyTypes = false; + @Input() hiddenDefaultValue = false; + @Input() defaultValueRequired = false; + @Input() hint: string; + @Input() predefinedEntityFilter: EntityFilter; + @Input() forbiddenNames = FORBIDDEN_NAMES; + @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; + @Input() argumentNameContext: {[key: string]: string} = { + label: 'calculated-fields.argument-name', + required: 'calculated-fields.hint.argument-name-required', + duplicate: 'calculated-fields.hint.argument-name-duplicate', + pattern: 'calculated-fields.hint.argument-name-pattern', + maxlength: 'calculated-fields.hint.argument-name-max-length', + forbidden: 'calculated-fields.hint.argument-name-forbidden' + }; + @Input() readonly = false; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; argumentsDataApplied = output(); + argumentType = this.fb.control(ArgumentEntityType.Current, Validators.required); + readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], - refEntityId: this.fb.group({ - entityType: [ArgumentEntityType.Current], - id: [''] - }), + argumentName: ['', [Validators.required, Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + refEntityId: [null], refEntityKey: this.fb.group({ type: [ArgumentType.LatestTelemetry, [Validators.required]], key: ['', [Validators.pattern(oneSpaceInsideRegex)]], @@ -87,7 +117,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI entityFilter: EntityFilter; entityNameSubject = new BehaviorSubject(null); - readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; + enableAutocomplete = false; + readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentType = ArgumentType; readonly DataKeyType = DataKeyType; @@ -104,20 +135,16 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private fb: FormBuilder, private cd: ChangeDetectorRef, private popover: TbPopoverComponent, - private store: Store + private store: Store, + private destroyRef: DestroyRef ) { this.observeEntityFilterChanges(); - this.observeEntityTypeChanges(); + this.observeArgumentTypeChanges(); this.observeEntityKeyChanges(); - this.observeUpdatePosition(); } get entityType(): ArgumentEntityType { - return this.argumentFormGroup.get('refEntityId').get('entityType').value; - } - - get refEntityIdFormGroup(): FormGroup { - return this.argumentFormGroup.get('refEntityId') as FormGroup; + return this.argumentType.value; } get refEntityKeyFormGroup(): FormGroup { @@ -131,14 +158,31 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } ngOnInit(): void { + this.updatedFormValidators(); + this.updatedArgumentType(); this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); - this.updateEntityFilter(this.argument.refEntityId?.entityType, true); + this.updateEntityFilter(this.entityType, true); + this.updatedRefEntityIdState(this.entityType, false); this.toggleByEntityKeyType(this.argument.refEntityKey?.type); this.setInitialEntityKeyType(); + this.setInitialEntityType(); + if (this.watchKeyChange) { + this.setWatchKeyChange(); + } + + if (this.defaultValueRequired) { + this.argumentFormGroup.get('defaultValue').addValidators(Validators.required); + this.argumentFormGroup.get('defaultValue').updateValueAndValidity({onlySelf: true}); + } this.argumentTypes = Object.values(ArgumentType) - .filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT); + .filter(type => type !== ArgumentType.Rolling || this.isScript); + + if (this.readonly) { + this.argumentType.disable({emitEvent: false}); + this.argumentFormGroup.disable({emitEvent: false}); + } } ngAfterViewInit(): void { @@ -148,12 +192,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } saveArgument(): void { - const { refEntityId, ...restConfig } = this.argumentFormGroup.value; - const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue; - if (refEntityId.entityType === ArgumentEntityType.Tenant) { - refEntityId.id = this.tenantId; + const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue; + if (this.entityType === ArgumentEntityType.Owner) { + value.refDynamicSourceConfiguration = {type: ArgumentEntityType.Owner}; + } else if (this.entityType === ArgumentEntityType.Tenant) { + value.refEntityId = new TenantId(this.tenantId) as any; } - if (refEntityId.entityType !== ArgumentEntityType.Current && refEntityId.entityType !== ArgumentEntityType.Tenant) { + if (this.entityType !== ArgumentEntityType.Current && this.entityType !== ArgumentEntityType.Tenant) { value.entityName = this.entityNameSubject.value; } if (value.defaultValue) { @@ -167,6 +212,23 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.popover.hide(); } + private updatedFormValidators(): void { + this.argumentFormGroup.get('argumentName').addValidators( + [uniqueNameValidator(this.usedArgumentNames), forbiddenNamesValidator(this.forbiddenNames)]); + this.argumentFormGroup.get('argumentName').updateValueAndValidity({emitEvent: false}); + } + + private updatedArgumentType(): void { + let argumentType = ArgumentEntityType.Current; + if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE); + argumentType = ArgumentEntityType.Owner; + } else if (this.argument.refEntityId?.entityType) { + argumentType = this.argument.refEntityId.entityType; + } + this.argumentType.setValue(argumentType, {emitEvent: false}); + } + private toggleByEntityKeyType(type: ArgumentType): void { const isAttribute = type === ArgumentType.Attribute; const isRolling = type === ArgumentType.Rolling; @@ -182,6 +244,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI case ArgumentEntityType.Current: entityFilter = this.currentEntityFilter; break; + case ArgumentEntityType.Owner: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.ownerId + }; + break; case ArgumentEntityType.Tenant: entityFilter = { type: AliasFilterType.singleEntity, @@ -198,7 +266,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI }; } if (!onInit) { - this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); + this.argumentFormGroup.get('refEntityKey').get('key').setValue('', {emitEvents: !this.watchKeyChange}); + if (this.watchKeyChange && this.argumentFormGroup.get('argumentName').pristine) { + this.argumentFormGroup.get('argumentName').markAsUntouched({emitEvent: false}); + this.argumentFormGroup.get('argumentName').setValue('', {emitEvent: false}); + } + } else if (this.predefinedEntityFilter) { + entityFilter = this.predefinedEntityFilter; } this.entityFilter = entityFilter; this.cd.markForCheck(); @@ -206,41 +280,28 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private observeEntityFilterChanges(): void { merge( - this.refEntityIdFormGroup.get('entityType').valueChanges, + this.argumentType.valueChanges, this.refEntityKeyFormGroup.get('type').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), + this.argumentFormGroup.get('refEntityId').valueChanges.pipe(filter(Boolean)), this.refEntityKeyFormGroup.get('scope').valueChanges, ) .pipe(debounceTime(50), takeUntilDestroyed()) .subscribe(() => this.updateEntityFilter(this.entityType)); } - private observeEntityTypeChanges(): void { - this.refEntityIdFormGroup.get('entityType').valueChanges + private observeArgumentTypeChanges(): void { + this.argumentType.valueChanges .pipe(distinctUntilChanged(), takeUntilDestroyed()) .subscribe(type => { - this.argumentFormGroup.get('refEntityId').get('id').setValue(''); - const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current; - this.argumentFormGroup.get('refEntityId') - .get('id')[isEntityWithId ? 'enable' : 'disable'](); - if (!isEntityWithId) { - this.entityNameSubject.next(null); - } + this.argumentFormGroup.get('refEntityId').setValue(null); + this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner; + this.updatedRefEntityIdState(type); if (!this.enableAttributeScopeSelection) { this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE); } }); } - private uniqNameRequired(): ValidatorFn { - return (control: FormControl) => { - const newName = control.value.trim().toLowerCase(); - const isDuplicate = this.usedArgumentNames?.some(name => name.toLowerCase() === newName); - - return isDuplicate ? { duplicateName: true } : null; - }; - } - private observeEntityKeyChanges(): void { this.argumentFormGroup.get('refEntityKey').get('type').valueChanges .pipe(takeUntilDestroyed()) @@ -248,29 +309,36 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } private setInitialEntityKeyType(): void { - if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argument.refEntityKey?.type === ArgumentType.Rolling) { + if (!this.isScript && this.argument.refEntityKey?.type === ArgumentType.Rolling) { const typeControl = this.argumentFormGroup.get('refEntityKey').get('type'); typeControl.setValue(null); typeControl.markAsTouched(); } } - private forbiddenArgumentNameValidator(): ValidatorFn { - return (control: FormControl) => { - const trimmedValue = control.value.trim().toLowerCase(); - const forbiddenArgumentNames = ['ctx', 'e', 'pi']; - return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null; - }; + private setInitialEntityType() { + if (!this.argumentEntityTypes.includes(this.entityType)) { + this.argumentType.setValue(null); + this.argumentType.markAsTouched(); + } } - private observeUpdatePosition(): void { - merge( - this.refEntityIdFormGroup.get('entityType').valueChanges, - this.refEntityKeyFormGroup.get('type').valueChanges, - this.argumentFormGroup.get('timeWindow').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), - ) - .pipe(delay(50), takeUntilDestroyed()) - .subscribe(() => this.popover.updatePosition()); + private setWatchKeyChange(): void { + this.refEntityKeyFormGroup.get('key').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((key) => { + if (this.argumentFormGroup.get('argumentName').pristine) { + this.argumentFormGroup.get('argumentName').setValue(key); + this.argumentFormGroup.get('argumentName').markAsTouched({emitEvent: false}); + } + }); + } + + private updatedRefEntityIdState(type: ArgumentEntityType, emitEvent = true): void { + const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type); + this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']({emitEvent}); + if (!isEntityWithId) { + this.entityNameSubject.next(null); + } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html similarity index 69% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index 93f1d58d53..fc3c6ac7b3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -15,13 +15,15 @@ limitations under the License. --> -
-
+
+
-
{{ 'common.name' | translate }}
+
{{ argumentNameColumn | translate }}
@@ -29,7 +31,7 @@ @@ -37,13 +39,15 @@ - + {{ 'entity.entity-type' | translate }}
@if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) { {{ 'calculated-fields.argument-current-tenant' | translate }} + } @else if (argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + {{ 'calculated-fields.argument-owner' | translate }} } @else if (argument.refEntityId?.id) { {{ entityTypeTranslations.get(argument.refEntityId.entityType).type | translate }} } @else { @@ -59,7 +63,7 @@
@if (argument.refEntityId?.id && argument.refEntityId?.entityType !== ArgumentEntityType.Tenant) { - {{ entityNameMap.get(argument.refEntityId.id) ?? '' }} @@ -88,25 +92,37 @@ -
- +
+ @if (!isEditValue) { + + } @else { + + }
- - + +
-
- {{ 'calculated-fields.no-arguments' | translate }} -
- @if (errorText) { + @if (errorText || (dataSource.isEmpty() | async)) { }
+
+ {{ 'calculated-fields.no-arguments' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss similarity index 89% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss index 04e72eee84..7a2b96f497 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss @@ -41,6 +41,10 @@ } } + .no-data-found { + font-size: 16px; + } + .max-args-warning { .mat-icon { color: #FAA405; @@ -51,6 +55,9 @@ --mat-badge-legacy-small-size-container-size: 8px; --mat-badge-small-size-container-overlap-offset: -5px; --mat-badge-small-size-text-size: 0; + width: 100%; + display: flex; + justify-content: end; } } @@ -62,7 +69,7 @@ } .arguments-table { - .mat-mdc-header-row.mat-row-select .mat-mdc-header-cell.entity-type-header { + .mat-mdc-header-row.mat-row-select .mat-mdc-header-cell:nth-child(2) { padding: 0 28px 0 0; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts similarity index 80% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index 304d658399..14cfde23fe 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, DestroyRef, @@ -43,17 +44,19 @@ import { CalculatedFieldArgumentValue, CalculatedFieldType, } from '@shared/models/calculated-field.models'; -import { CalculatedFieldArgumentPanelComponent } from '@home/components/calculated-fields/components/public-api'; +import { + CalculatedFieldArgumentPanelComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component'; import { MatIconButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { getEntityDetailsPageURL, isEqual } from '@core/utils'; +import { getEntityDetailsPageURL, isDefined, isEqual } from '@core/utils'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; import { EntityService } from '@core/http/entity.service'; -import { MatSort } from '@angular/material/sort'; +import { MatSort, SortDirection } from '@angular/material/sort'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -84,16 +87,26 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; - @Input() calculatedFieldType: CalculatedFieldType; + @Input() ownerId: EntityId; + @Input() isScript: boolean; + @Input({transform: booleanAttribute}) disable = false; + @Input({transform: booleanAttribute}) isEditValue = true; + @Input() watchKeyChange = false; @ViewChild(MatSort, { static: true }) sort: MatSort; errorText = ''; argumentsFormArray = this.fb.array([]); entityNameMap = new Map(); - sortOrder = { direction: 'asc', property: '' }; + sortOrder: { direction: SortDirection; property: string } = {direction: 'asc', property: ''}; dataSource = new CalculatedFieldArgumentDatasource(); + argumentNameColumn = 'common.name'; + argumentNameColumnCopy = 'calculated-fields.copy-argument-name'; + displayColumns = ['name', 'entityType', 'target', 'type', 'key', 'actions']; + + protected panelAdditionalCtx: Record + readonly entityTypeTranslations = entityTypeTranslations; readonly ArgumentTypeTranslations = ArgumentTypeTranslations; readonly ArgumentEntityType = ArgumentEntityType; @@ -106,14 +119,14 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private propagateChange: (argumentsObj: Record) => void = () => {}; constructor( - private fb: FormBuilder, - private popoverService: TbPopoverService, - private viewContainerRef: ViewContainerRef, - private cd: ChangeDetectorRef, - private renderer: Renderer2, - private entityService: EntityService, - private destroyRef: DestroyRef, - private store: Store + protected fb: FormBuilder, + protected popoverService: TbPopoverService, + protected viewContainerRef: ViewContainerRef, + protected cd: ChangeDetectorRef, + protected renderer: Renderer2, + protected entityService: EntityService, + protected destroyRef: DestroyRef, + protected store: Store ) { this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { this.updateDataSource(value); @@ -122,9 +135,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } ngOnChanges(changes: SimpleChanges): void { - if (changes.calculatedFieldType?.previousValue - && changes.calculatedFieldType.currentValue !== changes.calculatedFieldType.previousValue) { - this.argumentsFormArray.updateValueAndValidity(); + if (isDefined(changes.isScript?.previousValue) && changes.isScript.currentValue !== changes.isScript.previousValue) { + this.changeIsScriptMode(); } } @@ -140,11 +152,15 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.propagateChange = fn; } - registerOnTouched(_): void {} + registerOnTouched(_: any): void {} validate(): ValidationErrors | null { this.updateErrorText(); - return this.errorText ? { argumentsFormArray: false } : null; + return this.errorText || !this.argumentsFormArray.controls.length ? { argumentsFormArray: false } : null; + } + + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; } onDelete($event: Event, argument: CalculatedFieldArgumentValue): void { @@ -154,7 +170,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.argumentsFormArray.markAsDirty(); } - manageArgument($event: Event, matButton: MatIconButton, argument = {} as CalculatedFieldArgumentValue): void { + manageArgument($event: Event, matButton: MatIconButton, argument = {} as CalculatedFieldArgumentValue, readonly: boolean = false): void { $event?.stopPropagation(); if (this.popoverComponent && !this.popoverComponent.tbHidden) { this.popoverComponent.hide(); @@ -169,19 +185,22 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces index, argument, entityId: this.entityId, - calculatedFieldType: this.calculatedFieldType, + isScript: this.isScript, buttonTitle: isExists ? 'action.apply' : 'action.add', tenantId: this.tenantId, entityName: this.entityName, + ownerId: this.ownerId, + watchKeyChange: this.watchKeyChange, usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName), + readonly }; this.popoverComponent = this.popoverService.displayPopover({ trigger, renderer: this.renderer, componentType: CalculatedFieldArgumentPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'], - context: ctx, + preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'], + context: Object.assign(ctx, this.panelAdditionalCtx), isModal: true }); this.popoverComponent.tbComponentRef.instance.argumentsDataApplied.subscribe(({ entityName, ...value }) => { @@ -204,14 +223,11 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.dataSource.loadData(sortedValue); } - private updateErrorText(): void { - if (this.calculatedFieldType === CalculatedFieldType.SIMPLE - && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { + protected updateErrorText(): void { + if (!this.isScript && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; } else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; - } else if (!this.argumentsFormArray.controls.length) { - this.errorText = 'calculated-fields.hint.arguments-empty'; } else { this.errorText = ''; } @@ -226,7 +242,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } writeValue(argumentsObj: Record): void { - this.argumentsFormArray.clear(); + this.argumentsFormArray.clear({emitEvent: false}); this.populateArgumentsFormArray(argumentsObj); this.updateEntityNameMap(this.argumentsFormArray.value); } @@ -235,6 +251,14 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces return getEntityDetailsPageURL(id, type); } + protected changeIsScriptMode(): void { + this.argumentsFormArray.updateValueAndValidity({emitEvent: !this.disable}); + } + + protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { + return !(argument.refEntityKey.type === ArgumentType.Rolling && !this.isScript) && argument.refEntityId?.id !== NULL_UUID + } + private populateArgumentsFormArray(argumentsObj: Record): void { Object.keys(argumentsObj).forEach(key => { const value: CalculatedFieldArgumentValue = { @@ -243,7 +267,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces }; this.argumentsFormArray.push(this.fb.control(value), { emitEvent: false }); }); - this.argumentsFormArray.updateValueAndValidity(); + this.updateDataSource(this.argumentsFormArray.value); } private updateEntityNameMap(values: CalculatedFieldArgumentValue[]): void { @@ -256,7 +280,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces return acc; }, {} as Record); const tasks = Object.entries(entitiesByType).map(([entityType, ids]) => - this.entityService.getEntities(entityType as EntityType, ids) + this.entityService.getEntities(entityType as EntityType, ids, {ignoreLoading: true}) ); if (!tasks.length) { return; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts new file mode 100644 index 0000000000..2b392564f9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldArgumentPanelComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component'; +import { + CalculatedFieldArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; +import { + PropagateArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component'; +import { + RelatedAggregationArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component'; +import { + EntityAggregationArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + CalculatedFieldArgumentPanelComponent, + CalculatedFieldArgumentsTableComponent, + PropagateArgumentsTableComponent, + RelatedAggregationArgumentsTableComponent, + EntityAggregationArgumentsTableComponent, + ], + exports: [ + CalculatedFieldArgumentsTableComponent, + PropagateArgumentsTableComponent, + RelatedAggregationArgumentsTableComponent, + EntityAggregationArgumentsTableComponent, + ] +}) +export class CalculatedFieldArgumentsTableModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts new file mode 100644 index 0000000000..62dcff420e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts @@ -0,0 +1,73 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, Renderer2, ViewContainerRef, } from '@angular/core'; +import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { EntityService } from '@core/http/entity.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + CalculatedFieldArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; +import { ArgumentEntityType } from '@shared/models/calculated-field.models'; + +@Component({ + selector: 'tb-entity-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + } + ], + standalone: false +}) +export class EntityAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { + + constructor( + protected fb: FormBuilder, + protected popoverService: TbPopoverService, + protected viewContainerRef: ViewContainerRef, + protected cd: ChangeDetectorRef, + protected renderer: Renderer2, + protected entityService: EntityService, + protected destroyRef: DestroyRef, + protected store: Store + ) { + super(fb, popoverService, viewContainerRef, cd, renderer, entityService, destroyRef, store); + + this.argumentNameColumn = 'calculated-fields.argument-name'; + this.displayColumns = ['name', 'type', 'key', 'actions']; + this.panelAdditionalCtx = { + hiddenEntityTypes: true, + argumentEntityTypes: [ArgumentEntityType.Current], + hint: 'calculated-fields.entity-aggregation.argument-setting-hint', + hiddenDefaultValue: true, + hiddenEntityKeyTypes: true, + watchKeyChange: true, + }; + + this.isScript = false; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts new file mode 100644 index 0000000000..b7208d0cbb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + DestroyRef, + forwardRef, + OnInit, + Renderer2, + ViewContainerRef, +} from '@angular/core'; +import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { EntityService } from '@core/http/entity.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + CalculatedFieldArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; +import { + ArgumentEntityType, + ArgumentType, + CalculatedFieldArgumentValue, + FORBIDDEN_NAMES +} from '@shared/models/calculated-field.models'; +import { isDefined, isUndefinedOrNull } from '@core/utils'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-propagate-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagateArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagateArgumentsTableComponent), + multi: true + } + ], + standalone: false +}) +export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent implements OnInit { + + constructor( + protected fb: FormBuilder, + protected popoverService: TbPopoverService, + protected viewContainerRef: ViewContainerRef, + protected cd: ChangeDetectorRef, + protected renderer: Renderer2, + protected entityService: EntityService, + protected destroyRef: DestroyRef, + protected store: Store + ) { + super(fb, popoverService, viewContainerRef, cd, renderer, entityService, destroyRef, store) + } + + ngOnInit() { + this.updatedValue(); + } + + protected changeIsScriptMode(): void { + this.updatedValue(); + super.changeIsScriptMode(); + } + + private updatedValue() { + if (this.isScript) { + this.argumentNameColumn = 'common.name'; + this.argumentNameColumnCopy = 'calculated-fields.copy-argument-name'; + this.displayColumns = ['name', 'entityType', 'target', 'type', 'key', 'actions']; + this.panelAdditionalCtx = null; + } else { + this.argumentNameColumn = 'calculated-fields.output-key'; + this.argumentNameColumnCopy = 'calculated-fields.copy-output-key'; + this.displayColumns = ['name', 'type', 'key', 'actions']; + this.panelAdditionalCtx = { + argumentEntityTypes: [ArgumentEntityType.Current], + argumentNameContext: { + label: 'calculated-fields.output-key', + required: 'calculated-fields.hint.output-key-required', + duplicate: 'calculated-fields.hint.output-key-duplicate', + pattern: 'calculated-fields.hint.output-key-pattern', + maxlength: 'calculated-fields.hint.output-key-max-length', + forbidden: 'calculated-fields.hint.output-key-forbidden' + }, + watchKeyChange: true, + forbiddenNames: [...FORBIDDEN_NAMES, 'propagationCtx'], + }; + } + } + + protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { + if (!this.isScript && (isDefined(argument?.refEntityId) || isDefined(argument?.refDynamicSourceConfiguration))) { + return false; + } + return super.isEditButtonShowBadge(argument); + } + + protected updateErrorText(): void { + if (!this.isScript && this.argumentsFormArray.controls.some(control => isDefined(control.value?.refEntityId) || isDefined(control.value.refDynamicSourceConfiguration))) { + this.errorText = 'calculated-fields.hint.arguments-propagate-argument-entity-type'; + } else if (!this.isScript && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { + this.errorText = 'calculated-fields.hint.arguments-propagate-arguments-with-rolling'; + } else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { + this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; + } else if (!this.argumentsFormArray.controls.length) { + this.errorText = 'calculated-fields.hint.arguments-empty'; + } else if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) { + this.errorText = 'calculated-fields.hint.arguments-propagate-argument-must-current-entity'; + } else { + this.errorText = ''; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts new file mode 100644 index 0000000000..52650bbf1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + DestroyRef, + forwardRef, + Input, + Renderer2, + ViewContainerRef, +} from '@angular/core'; +import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { EntityService } from '@core/http/entity.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + CalculatedFieldArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; +import { ArgumentEntityType, RelationPathLevel } from '@shared/models/calculated-field.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { EntityType } from '@shared/models/entity-type.models'; + +@Component({ + selector: 'tb-related-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + } + ], + standalone: false +}) +export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { + + @Input({required: true}) + set relation(value: RelationPathLevel) { + this.panelAdditionalCtx.predefinedEntityFilter = { + type: AliasFilterType.relationsQuery, + rootStateEntity: false, + rootEntity: this.entityId, + direction: value.direction, + filters: [{ + relationType: value.relationType, + entityTypes: [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT] + }], + maxLevel: 1, + }; + } + + constructor( + protected fb: FormBuilder, + protected popoverService: TbPopoverService, + protected viewContainerRef: ViewContainerRef, + protected cd: ChangeDetectorRef, + protected renderer: Renderer2, + protected entityService: EntityService, + protected destroyRef: DestroyRef, + protected store: Store + ) { + super(fb, popoverService, viewContainerRef, cd, renderer, entityService, destroyRef, store); + + this.argumentNameColumn = 'calculated-fields.argument-name'; + this.displayColumns = ['name', 'type', 'key', 'actions']; + this.panelAdditionalCtx = { + hiddenEntityTypes: true, + defaultValueRequired: true, + argumentEntityTypes: [ArgumentEntityType.Current], + hint: 'calculated-fields.hint.setting-arguments-aggregation', + watchKeyChange: true, + }; + + this.isScript = false; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss new file mode 100644 index 0000000000..f3218dbab7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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-config-panel { + width: 520px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + max-width: fit-content; + } + @media #{$mat-xs} { + width: 90vw; + } + + .tb-config-panel-title { + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + font-weight: 500; + font-size: 16px; + } + + .tb-config-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + + .fixed-title-width { + @media #{$mat-xs} { + min-width: 120px; + } + } + } + + .tb-config-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts deleted file mode 100644 index 579433b5ca..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ /dev/null @@ -1,71 +0,0 @@ -/// -/// Copyright © 2016-2026 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES 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, 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 { Router } from '@angular/router'; -import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models'; -import { EventTableComponent } from '@home/components/event/event-table.component'; -import { - CalculatedField, - CalculatedFieldTestScriptFn, - CalculatedFieldType -} from '@shared/models/calculated-field.models'; - -export interface CalculatedFieldDebugDialogData { - tenantId: string; - value: CalculatedField; - getTestScriptDialogFn: CalculatedFieldTestScriptFn; -} - -@Component({ - selector: 'tb-calculated-field-debug-dialog', - styleUrls: ['calculated-field-debug-dialog.component.scss'], - templateUrl: './calculated-field-debug-dialog.component.html', - standalone: false -}) -export class CalculatedFieldDebugDialogComponent extends DialogComponent implements AfterViewInit { - - @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; - - readonly DebugEventType = DebugEventType; - readonly debugEventTypes = DebugEventType; - readonly EventType = EventType; - - constructor(protected store: Store, - protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData, - protected dialogRef: MatDialogRef) { - super(store, router, dialogRef); - } - - ngAfterViewInit(): void { - this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments); - this.eventsTable.entitiesTable.updateData(); - } - - cancel(): void { - this.dialogRef.close(null); - } - - onDebugEventSelected(event: CalculatedFieldEventBody): void { - this.data.getTestScriptDialogFn(this.data.value, JSON.parse(event.arguments)) - .subscribe(expression => this.dialogRef.close(expression)); - } -} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 38c6991c1f..503927c71a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -15,12 +15,13 @@ limitations under the License. --> -
+

{{ 'entity.type-calculated-field' | translate}}

- -
- -
-
-
-
-
{{ 'calculated-fields.output' | translate }}
-
- - {{ 'calculated-fields.output-type' | translate }} - - @for (type of outputTypes; track type) { - {{ OutputTypeTranslations.get(type) | translate}} - } - - - @if (outputFormGroup.get('type').value === OutputType.Attribute - && (data.entityId.entityType === EntityType.DEVICE || data.entityId.entityType === EntityType.DEVICE_PROFILE)) { - - {{ 'calculated-fields.attribute-scope' | translate }} - - - {{ 'calculated-fields.server-attributes' | translate }} - - - {{ 'calculated-fields.shared-attributes' | translate }} - - - - } -
- @if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { -
- - - {{ (outputFormGroup.get('type').value === OutputType.Timeseries - ? 'calculated-fields.timeseries-key' - : 'calculated-fields.attribute-key') - | translate }} - - - @if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) { - - @if (outputFormGroup.get('name').hasError('required')) { - {{ 'common.hint.key-required' | translate }} - } @else if (outputFormGroup.get('name').hasError('maxlength')) { - {{ 'common.hint.key-max-length' | translate }} - } - - } - - - {{ 'calculated-fields.decimals-by-default' | translate }} - - @if (outputFormGroup.get('decimalsByDefault').errors && outputFormGroup.get('decimalsByDefault').touched) { - {{ 'calculated-fields.hint.decimals-range' | translate }} - } - -
-
- -
- calculated-fields.use-latest-timestamp -
-
-
- } -
- + @switch (fieldFormGroup.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + + } + @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } + @default { + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss index ab9638b7f5..b34e717714 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss @@ -16,6 +16,11 @@ .calculated-field-dialog-container { width: 869px; max-width: 100%; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + --mat-form-field-outlined-outline-color: rgba(0,0,0,0.12); + --mat-form-field-outlined-container-shape: 6px; + --mat-form-field-trailing-icon-color: rgba(0, 0, 0, 0.56); } .tbel-script-lang-chip { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 3dcc759fc4..4e37e67c71 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -18,31 +18,30 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } 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 '@shared/components/dialog.component'; import { CalculatedField, CalculatedFieldConfiguration, - calculatedFieldDefaultScript, + calculatedFieldsEntityTypeList, CalculatedFieldTestScriptFn, CalculatedFieldType, - CalculatedFieldTypeTranslations, - getCalculatedFieldArgumentsEditorCompleter, - getCalculatedFieldArgumentsHighlights, - OutputType, - OutputTypeTranslations + calculatedFieldTypes, + CalculatedFieldTypeTranslations } from '@shared/models/calculated-field.models'; -import { digitsRegex } from '@shared/models/regex.constants'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { EntityType } from '@shared/models/entity-type.models'; -import { map, startWith, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { deepTrim } from '@core/utils'; +import { BaseData } from '@shared/models/base-data'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { FormGroup } from '@angular/forms'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -50,9 +49,11 @@ export interface CalculatedFieldDialogData { entityId: EntityId; tenantId: string; entityName?: string; + ownerId: EntityId; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; getTestScriptDialogFn: CalculatedFieldTestScriptFn; isDirty?: boolean; + disabledSelectType?: boolean; } @Component({ @@ -64,55 +65,23 @@ export interface CalculatedFieldDialogData { }) export class CalculatedFieldDialogComponent extends DialogComponent { - fieldFormGroup = this.fb.group({ - name: ['', [Validators.required, Validators.maxLength(255)]], - type: [CalculatedFieldType.SIMPLE], - debugSettings: [], - configuration: this.fb.group({ - arguments: this.fb.control({}), - expressionSIMPLE: ['', [Validators.required, Validators.maxLength(255)]], - expressionSCRIPT: [calculatedFieldDefaultScript], - output: this.fb.group({ - name: ['', [Validators.required, Validators.maxLength(255)]], - scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }], - type: [OutputType.Timeseries], - decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], - }), - useLatestTs: [false] - }), - }); - - functionArgs$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) - ); - - argumentsEditorCompleter$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj)) - ); - - argumentsHighlightRules$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) - ); + fieldFormGroup: FormGroup; additionalDebugActionConfig = this.data.value?.id ? { ...this.data.additionalDebugActionConfig, action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), } : null; - readonly OutputTypeTranslations = OutputTypeTranslations; - readonly OutputType = OutputType; - readonly AttributeScope = AttributeScope; + entityName = this.data.entityName; + ownerId = this.data.ownerId; + + disabledConfiguration = false; + isLoading = false; + readonly EntityType = EntityType; + readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList; readonly CalculatedFieldType = CalculatedFieldType; - readonly ScriptLanguage = ScriptLanguage; - readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[]; - readonly outputTypes = Object.values(OutputType) as OutputType[]; + readonly fieldTypes = calculatedFieldTypes; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; constructor(protected store: Store, @@ -121,34 +90,37 @@ export class CalculatedFieldDialogComponent extends DialogComponent, private calculatedFieldsService: CalculatedFieldsService, private destroyRef: DestroyRef, - private fb: FormBuilder) { + private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); - this.observeIsLoading(); + this.fieldFormGroup = this.cfFormService.buildForm(); + this.cfFormService.setupTypeChange(this.fieldFormGroup, this.destroyRef); this.applyDialogData(); - this.observeTypeChanges(); - } - get configFormGroup(): FormGroup { - return this.fieldFormGroup.get('configuration') as FormGroup; - } + if (this.data.isDirty) { + this.fieldFormGroup.markAsDirty(); + } + + if (!this.data.entityId) { + this.fieldFormGroup.get('entityId').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((entityId) => { + this.disabledConfiguration = !entityId; + if (this.disabledConfiguration) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + } else { + this.fieldFormGroup.get('configuration').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration').updateValueAndValidity({emitEvent: false}); + } + }); + } - get outputFormGroup(): FormGroup { - return this.fieldFormGroup.get('configuration').get('output') as FormGroup; + if (this.data.disabledSelectType) { + this.fieldFormGroup.get('type').disable({emitEvent: false}); + } } get fromGroupValue(): CalculatedField { - const { configuration, type, name, ...rest } = this.fieldFormGroup.value; - const { expressionSIMPLE, expressionSCRIPT, output, ...restConfig } = configuration; - return { - configuration: { - ...restConfig, - type, expression: configuration['expression'+type].trim(), - output: { ...output, name: output.name?.trim() ?? '' } - }, - name: name.trim(), - type, - ...rest, - } as CalculatedField; + return deepTrim(this.fieldFormGroup.getRawValue() as CalculatedField); } cancel(): void { @@ -157,101 +129,51 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.dialogRef.close(calculatedField)); - } - } - - onTestScript(): void { - const calculatedFieldId = this.data.value?.id?.id; - let testScriptDialogResult$: Observable; - - if (calculatedFieldId) { - testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) - .pipe( - switchMap(event => { - const args = event?.arguments ? JSON.parse(event.arguments) : null; - return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false); - }), - takeUntilDestroyed(this.destroyRef) - ) + .subscribe({ + next: calculatedField => this.dialogRef.close(calculatedField), + error: () => this.isLoading = false + }); } else { - testScriptDialogResult$ = this.data.getTestScriptDialogFn(this.fromGroupValue, null, false); + this.fieldFormGroup.get('name').markAsTouched(); } - - testScriptDialogResult$.subscribe(expression => { - this.configFormGroup.get('expressionSCRIPT').setValue(expression); - this.configFormGroup.get('expressionSCRIPT').markAsDirty(); - }); } - private applyDialogData(): void { - const { configuration = {}, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; - const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration; - const updatedConfig = { ...restConfig , ['expression'+type]: expression }; - this.fieldFormGroup.patchValue({ configuration: updatedConfig, type, debugSettings, ...value }, {emitEvent: false}); + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.data.value?.id?.id, + this.fromGroupValue, + this.data.getTestScriptDialogFn, + this.destroyRef, + expression + ); } - private observeTypeChanges(): void { - this.toggleKeyByCalculatedFieldType(this.fieldFormGroup.get('type').value); - this.toggleScopeByOutputType(this.outputFormGroup.get('type').value); - - this.outputFormGroup.get('type').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(type => this.toggleScopeByOutputType(type)); - this.fieldFormGroup.get('type').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(type => this.toggleKeyByCalculatedFieldType(type)); + changeEntity(entity: BaseData): void { + this.entityName = entity.name; + if (this.isAssignedToCustomer(entity as AssetInfo | DeviceInfo)) { + this.ownerId = (entity as AssetInfo | DeviceInfo).customerId; + } } - private toggleScopeByOutputType(type: OutputType): void { - if (type === OutputType.Attribute) { - this.outputFormGroup.get('scope').enable({emitEvent: false}); - } else { - this.outputFormGroup.get('scope').disable({emitEvent: false}); - } - if (this.fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { - if (type === OutputType.Attribute) { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } else { - this.configFormGroup.get('useLatestTs').enable({emitEvent: false}); - } - } else { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } + get entityId(): EntityId { + return this.data.entityId || this.fieldFormGroup.get('entityId').value; } - private toggleKeyByCalculatedFieldType(type: CalculatedFieldType): void { - if (type === CalculatedFieldType.SIMPLE) { - this.outputFormGroup.get('name').enable({emitEvent: false}); - this.configFormGroup.get('expressionSIMPLE').enable({emitEvent: false}); - this.configFormGroup.get('expressionSCRIPT').disable({emitEvent: false}); - if (this.outputFormGroup.get('type').value === OutputType.Attribute) { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } else { - this.configFormGroup.get('useLatestTs').enable({emitEvent: false}); - } - } else { - this.outputFormGroup.get('name').disable({emitEvent: false}); - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - this.configFormGroup.get('expressionSIMPLE').disable({emitEvent: false}); - this.configFormGroup.get('expressionSCRIPT').enable({emitEvent: false}); + private applyDialogData(): void { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {}; + const preparedConfig = this.cfFormService.prepareConfig(configuration); + this.fieldFormGroup.patchValue({ configuration: preparedConfig, type, debugSettings, entityId, ...value }, {emitEvent: false}); + setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); + if (!this.data.entityId) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + this.disabledConfiguration = true; } } - private observeIsLoading(): void { - this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { - if (loading) { - this.fieldFormGroup.disable({emitEvent: false}); - } else { - this.fieldFormGroup.enable({emitEvent: false}); - this.toggleScopeByOutputType(this.outputFormGroup.get('type').value); - this.toggleKeyByCalculatedFieldType(this.fieldFormGroup.get('type').value); - if (this.data.isDirty) { - this.fieldFormGroup.markAsDirty(); - } - } - }); + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html new file mode 100644 index 0000000000..2288d5403d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -0,0 +1,146 @@ + +
+
+
+ {{ 'calculated-fields.arguments' | translate }} +
+
+ {{ 'calculated-fields.entity-aggregation.argument-hint' | translate }} +
+ +
+
+
+ {{ 'calculated-fields.metrics.metrics' | translate }} +
+ +
+
+
+ {{ 'calculated-fields.entity-aggregation.aggregation-interval' | translate }} +
+ +
+ + calculated-fields.aggregate-interval-type + + @for (type of AggIntervalTypes; track type) { + + {{ AggIntervalTypeTranslations.get(type) | translate }} + + } + + + + +
+ @if (entityAggregationConfiguration.get('interval.type').value === AggIntervalType.CUSTOM) { + + + } +
+ +
+ {{ 'calculated-fields.entity-aggregation.apply-offset' | translate }} +
+
+ @if (entityAggregationConfiguration.get('interval.allowOffsetSec').value) { + + +
+ {{ hint }} +
+ } +
+
+
+ +
+ {{ 'calculated-fields.entity-aggregation.wait-delay' | translate }} +
+
+ @if (entityAggregationConfiguration.get('allowWatermark').value) { + + + + + } +
+
+ +
+ {{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }} +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts new file mode 100644 index 0000000000..8386555d2b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts @@ -0,0 +1,462 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + AggInterval, + AggIntervalType, + AggIntervalTypeTranslations, + CalculatedFieldEntityAggregationConfiguration, + CalculatedFieldOutput, + CalculatedFieldType, + defaultCalculatedFieldOutput, + notEmptyObjectValidator, +} from '@shared/models/calculated-field.models'; +import { filter, map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AVG_MONTH, AVG_QUARTER, DAY, HOUR, MINUTE, SECOND, WEEK, YEAR } from '@shared/models/time/time.models'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { merge, Observable } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import _moment from 'moment'; + +interface CalculatedFieldEntityAggregationConfigurationValue extends CalculatedFieldEntityAggregationConfiguration { + interval: AggInterval & {allowOffsetSec?: boolean}; + allowWatermark: boolean; +} + +enum TimeCategory { + SECONDS = 'SECONDS', + MINUTES = 'MINUTES', + HOURS = 'HOURS', + DAYS = 'DAYS' +} + +@Component({ + selector: 'tb-entity-aggregation-component', + templateUrl: './entity-aggregation-component.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + } + ], + standalone: false +}) +export class EntityAggregationComponentComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input() testScript: (expression?: string) => Observable; + + @Input({transform: booleanAttribute}) isEditValue = true; + + readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; + readonly intermediateAggregationIntervalInSecForCF = getCurrentAuthState(this.store).intermediateAggregationIntervalInSecForCF; + readonly DayInSec = DAY / SECOND; + + entityAggregationConfiguration = this.fb.group({ + arguments: this.fb.control({}, notEmptyObjectValidator()), + metrics: this.fb.control({}, notEmptyObjectValidator()), + interval: this.fb.group({ + type: [AggIntervalType.HOUR], + tz: ['', Validators.required], + durationSec: [this.minAllowedAggregationIntervalInSecForCF, Validators.required], + allowOffsetSec: [false], + offsetSec: [this.minAllowedAggregationIntervalInSecForCF > 60 ? MINUTE / SECOND : 1, Validators.required], + }), + allowWatermark: [false], + watermark: this.fb.group({ + duration: [HOUR/SECOND, Validators.required], + }), + produceIntermediateResult: [false], + output: this.fb.control(defaultCalculatedFieldOutput), + }); + + arguments$ = this.entityAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => Object.keys(argumentsObj)) + ); + + AggIntervalType = AggIntervalType; + AggIntervalTypes = Object.values(AggIntervalType) as AggIntervalType[]; + AggIntervalTypeTranslations = AggIntervalTypeTranslations; + + hint: string; + + private propagateChange: (config: CalculatedFieldEntityAggregationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store, + private translate: TranslateService,) { + + this.entityAggregationConfiguration.get('interval.type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type: AggIntervalType) => { + this.checkAggIntervalType(type); + }); + + this.entityAggregationConfiguration.get('interval.allowOffsetSec').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((allow: boolean) => { + this.checkIntervalDuration(allow); + }); + + this.entityAggregationConfiguration.get('allowWatermark').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((allow: boolean) => { + this.checkWatermark(allow); + }); + + merge( + this.entityAggregationConfiguration.get('interval.type').valueChanges, + this.entityAggregationConfiguration.get('interval.durationSec').valueChanges, + this.entityAggregationConfiguration.get('interval.offsetSec').valueChanges, + this.entityAggregationConfiguration.get('interval.allowOffsetSec').valueChanges, + ).pipe( + filter(() => this.entityAggregationConfiguration.get('interval.allowOffsetSec').value), + takeUntilDestroyed() + ).subscribe(() => { + this.updatedOffsetHint(); + }); + + merge( + this.entityAggregationConfiguration.get('interval.type').valueChanges, + this.entityAggregationConfiguration.get('interval.durationSec').valueChanges + ).pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.checkProduceIntermediate(); + }); + + this.entityAggregationConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { + this.updatedModel(deepClone(value)); + }); + } + + validate(): ValidationErrors | null { + return this.entityAggregationConfiguration.valid || this.entityAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: CalculatedFieldEntityAggregationConfiguration): void { + const data: CalculatedFieldEntityAggregationConfigurationValue = { + ...value, + allowWatermark: isDefinedAndNotNull(value.watermark), + interval: {...value.interval, allowOffsetSec: isDefinedAndNotNull(value?.interval?.offsetSec)} + } + this.entityAggregationConfiguration.patchValue(data, {emitEvent: false}); + if (this.entityAggregationConfiguration.enabled) { + this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); + this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); + this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); + } + this.updatedOffsetHint(); + setTimeout(() => { + this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldEntityAggregationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.entityAggregationConfiguration.disable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.enable({emitEvent: false}); + this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); + this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); + this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); + } + } + + get maxOffsetTime(): number { + switch (this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType) { + case AggIntervalType.HOUR: + return HOUR / SECOND - 1; + case AggIntervalType.DAY: + return DAY / SECOND - 1; + case AggIntervalType.WEEK: + case AggIntervalType.WEEK_SUN_SAT: + return 7 * DAY / SECOND - 1; + case AggIntervalType.MONTH: + return AVG_MONTH / SECOND; + case AggIntervalType.QUARTER: + return AVG_QUARTER / SECOND - 1; + case AggIntervalType.YEAR: + return YEAR / SECOND - 1; + case AggIntervalType.CUSTOM: + return this.entityAggregationConfiguration.get('interval.durationSec').value - 1; + } + } + + private updatedModel(value: CalculatedFieldEntityAggregationConfigurationValue): void { + value.type = CalculatedFieldType.ENTITY_AGGREGATION; + if (!value.interval.allowOffsetSec) { + delete value.interval.offsetSec; + } + delete value.interval.allowOffsetSec; + if (!value.allowWatermark) { + delete value.watermark; + } + delete value.allowWatermark; + this.propagateChange(value); + } + + private checkAggIntervalType(type: AggIntervalType) { + if (type === AggIntervalType.CUSTOM) { + this.entityAggregationConfiguration.get('interval.durationSec').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('interval.durationSec').disable({emitEvent: false}); + } + } + + private checkIntervalDuration(allow: boolean) { + if (allow) { + this.entityAggregationConfiguration.get('interval.offsetSec').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('interval.offsetSec').disable({emitEvent: false}); + this.hint = ''; + } + } + + private checkWatermark(allow: boolean) { + if (allow) { + this.entityAggregationConfiguration.get('watermark').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('watermark').disable({emitEvent: false}); + } + } + + private checkProduceIntermediate() { + const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; + let durationSec = 0; + switch (intervalType) { + case AggIntervalType.CUSTOM: + durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; + break + case AggIntervalType.HOUR: + durationSec = HOUR / SECOND; + break + case AggIntervalType.DAY: + durationSec = DAY / SECOND; + break + case AggIntervalType.WEEK: + case AggIntervalType.WEEK_SUN_SAT: + durationSec = WEEK / SECOND; + break + case AggIntervalType.MONTH: + durationSec = AVG_MONTH / SECOND; + break + case AggIntervalType.QUARTER: + durationSec = AVG_QUARTER / SECOND; + break + case AggIntervalType.YEAR: + durationSec = YEAR / SECOND; + break + } + if (durationSec > this.intermediateAggregationIntervalInSecForCF) { + this.entityAggregationConfiguration.get('produceIntermediateResult').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('produceIntermediateResult').disable({emitEvent: false}); + } + } + + private updatedOffsetHint(): void { + const offset = this.entityAggregationConfiguration.get('interval.offsetSec').value; + const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; + const durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; + const offsetCategory = this.getTimeCategory(offset); + const now = _moment.utc(); + let interval: string; + if (intervalType === AggIntervalType.CUSTOM) { + const formatString = this.getCustomFormatString(offsetCategory, this.getTimeCategory(durationSec)); + const intervals: string[] = []; + let allInterval = durationSec >= HOUR*6/SECOND && durationSec < DAY/SECOND; + now.startOf('year').add(offset, 'seconds'); + + let repeat = 2; + if (allInterval) { + repeat = Math.floor(DAY/SECOND/durationSec); + if (repeat > 4) { + repeat = 2; + allInterval = false; + } + } + + for (let i = 0; i < repeat; i++) { + const s1 = now.clone().add(i * durationSec, 'seconds').format(formatString); + const s2 = now.clone().add((i + 1) * durationSec, 'seconds').format(formatString); + intervals.push(`${s1} - ${s2}`); + } + interval = intervals.join('; '); + + if (allInterval) { + this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset', {interval}); + } else { + interval += '…' + this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset-and-so-on', {interval}); + } + } else { + interval = this.buildStandardIntervalString(now, intervalType, offset, offsetCategory); + this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset-and-so-on', { interval }); + } + } + + private getTimeCategory(seconds: number): TimeCategory { + if (seconds % (DAY / SECOND) === 0) { + return TimeCategory.DAYS; + } + if (seconds % (HOUR / SECOND) === 0) { + return TimeCategory.HOURS; + } + if (seconds % (MINUTE / SECOND) === 0) { + return TimeCategory.MINUTES; + } + return TimeCategory.SECONDS; + } + + private getCustomFormatString(offsetCat: TimeCategory, durationCat: TimeCategory): string { + if (durationCat === TimeCategory.DAYS) { + if (offsetCat === TimeCategory.SECONDS) { + return '[Day] D, HH:mm:ss'; + } + if (offsetCat === TimeCategory.MINUTES || offsetCat === TimeCategory.HOURS) { + return '[Day] D, HH:mm'; + } + return '[Day] D'; + } else { + if (offsetCat === TimeCategory.SECONDS) { + return 'HH:mm:ss'; + } + return 'HH:mm'; + } + } + + private formatAdditiveInterval(now: _moment.Moment, addUnit: 'hour' | 'day' | 'month' | 'quarter', offsetCat: TimeCategory, + formats: { [key in TimeCategory]?: { s1: string, s2: string, s3: string } }): string { + const formatTs = formats[offsetCat] || formats[TimeCategory.SECONDS]; + + if (!formatTs) { + return ''; + } + + const s1 = now.format(formatTs.s1); + const s2 = now.clone().add(1, addUnit).format(formatTs.s2); + const s3 = now.clone().add(2, addUnit).format(formatTs.s3); + + return `${s1} - ${s2}; ${s2} - ${s3}…`; + } + + private formatNextInterval(now: _moment.Moment, offsetCat: TimeCategory, secFmt: string, minHourFmt: string, dayFmt: string): string { + let s1: string; + if (offsetCat === TimeCategory.SECONDS) { + s1 = now.format(secFmt); + } else if (offsetCat === TimeCategory.MINUTES || offsetCat === TimeCategory.HOURS) { + s1 = now.format(minHourFmt); + } else { + s1 = now.format(dayFmt); + } + + const s2 = `Next ${s1}`; + const s3 = `Following ${s1}`; + return `${s1} - ${s2}; ${s2} - ${s3}… `; + } + + private buildStandardIntervalString(now: _moment.Moment, type: AggIntervalType, offset: number, offsetCat: TimeCategory): string { + switch (type) { + case AggIntervalType.HOUR: + now.startOf('day').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'hour', offsetCat, { + [TimeCategory.SECONDS]: { s1: 'HH:mm:ss', s2: 'HH:mm:ss', s3: 'HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: 'HH:mm:ss', s2: 'HH:mm', s3: 'HH:mm' } + }); + + case AggIntervalType.DAY: + now.startOf('month').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'day', offsetCat, { + [TimeCategory.SECONDS]: { s1: '[Day] D, HH:mm:ss', s2: '[Day] D, HH:mm:ss', s3: '[Day] D, HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: '[Day] D, HH:mm:ss', s2: '[Day] D, HH:mm', s3: '[Day] D, HH:mm' }, + [TimeCategory.HOURS]: { s1: 'HH:mm:ss', s2: '[Day] D, HH:mm', s3: '[Day] D, HH:mm' } // Note: Original logic, s1 format is different + }); + + case AggIntervalType.WEEK: + now.isoWeekday(1).startOf('isoWeek').add(offset, 'seconds'); + return this.formatNextInterval(now, offsetCat, 'ddd, HH:mm:ss', 'ddd, HH:mm', 'ddd'); + + case AggIntervalType.WEEK_SUN_SAT: + now.startOf('week').add(offset, 'seconds'); + return this.formatNextInterval(now, offsetCat, 'ddd, HH:mm:ss', 'ddd, HH:mm', 'ddd'); + + case AggIntervalType.MONTH: + now.startOf('year').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'month', offsetCat, { + [TimeCategory.SECONDS]: { s1: 'Do [of month], HH:mm:ss', s2: '[Next] Do, HH:mm:ss', s3: '[Following] Do, HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: 'Do [of month], HH:mm', s2: '[Next] Do, HH:mm', s3: '[Following] Do, HH:mm' }, + [TimeCategory.HOURS]: { s1: 'Do [of month], HH:mm', s2: '[Next] Do, HH:mm', s3: '[Following] Do, HH:mm' }, + [TimeCategory.DAYS]: { s1: 'Do [of month]', s2: '[Next] Do', s3: '[Following] Do' } + }); + + case AggIntervalType.QUARTER: + now.startOf('year').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'quarter', offsetCat, { + [TimeCategory.SECONDS]: { s1: 'MMM Do, HH:mm:ss', s2: 'MMM Do, HH:mm:ss', s3: 'MMM Do, HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: 'MMM Do, HH:mm', s2: 'MMM Do, HH:mm', s3: 'MMM Do, HH:mm' }, + [TimeCategory.HOURS]: { s1: 'MMM Do, HH:mm', s2: 'MMM Do, HH:mm', s3: 'MMM Do, HH:mm' }, + [TimeCategory.DAYS]: { s1: 'MMM Do', s2: 'MMM Do', s3: 'MMM Do' } + }); + + case AggIntervalType.YEAR: + now.startOf('year').add(offset, 'seconds'); + return this.formatNextInterval(now, offsetCat, 'MMM Do, HH:mm:ss', 'MMM Do, HH:mm', 'MMM Do'); + + default: + return ''; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts new file mode 100644 index 0000000000..347d99ed15 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + EntityAggregationComponentComponent +} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component'; +import { + CalculatedFieldMetricsTableModule +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + CalculatedFieldMetricsTableModule, + ], + declarations: [ + EntityAggregationComponentComponent, + ], + exports: [ + EntityAggregationComponentComponent, + ] +}) +export class EntityAggregationComponentModule { +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html new file mode 100644 index 0000000000..2295c17ed1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -0,0 +1,276 @@ + +
+
{{ 'calculated-fields.geofencing-zone-groups-settings' | translate }}
+
+
+
{{ 'calculated-fields.name' | translate }}
+ + + @if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('required')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('duplicateName')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('pattern')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('maxlength')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('forbiddenName')) { + + warning + + } + +
+ +
+
{{ 'entity.entity-type' | translate }}
+ + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + +
+ @if (ArgumentEntityTypeParamsMap.has(entityType)) { +
+
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
+ +
+ } +
+ + @if (entityType === ArgumentEntityType.RelationQuery) { +
+ + + {{ 'calculated-fields.entity-zone-relationship' | translate }} + +
+
+
+
calculated-fields.level
+
calculated-fields.direction-level
+
calculated-fields.relation-type
+
+
+ @if (levelsFormArray()?.controls?.length) { +
+ @for (keyControl of levelsFormArray().controls; track trackByKey; ) { +
+
+ +
+
+
{{ $index + 1 }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionLevelTranslations.get(direction) | translate }} + } + + + + +
+
+ +
+
+ } +
+ } @else { + {{ 'calculated-fields.no-level' | translate }} + } + @if (levelsFormArray().errors) { + + } +
+
+ @if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) { +
+ warning + {{ 'calculated-fields.max-allowed-levels-error' | translate }} +
+ } @else { + + } +
+
+
+ } +
+ + @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Owner) { +
+
+ {{ 'calculated-fields.perimeter-attribute-key' | translate }} +
+ @if (entityType === ArgumentEntityType.RelationQuery) { + + + @if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) { + + warning + + } @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) { + + warning + + } + + } @else { + + } +
+ } +
+
{{ 'calculated-fields.report-strategy' | translate }}
+ + + @for (strategy of GeofencingReportStrategyList; track strategy) { + {{ GeofencingReportStrategyTranslations.get(strategy) | translate }} + } + + +
+
+
+ +
+ {{ 'calculated-fields.create-relation-with-matched-zones' | translate }} +
+
+
+
{{ 'calculated-fields.direction' | translate }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionTranslations.get(direction) | translate }} + } + + +
+
+
{{ 'calculated-fields.relation-type' | translate }}
+ + +
+
+
+
+ + @if (!readonly) { + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss new file mode 100644 index 0000000000..6b3652865d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + .level-text { + display: flex; + justify-content: center; + width: 25px; + color: rgba(0, 0, 0, 0.54); + } + + .tb-form-table { + .tb-form-row { + gap: 12px; + } + + .tb-form-table-body { + gap: unset; + } + + .tb-form-table-header { + padding: 0; + } + + .tb-form-table-header-cell { + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + } + } + + .max-args-warning { + .mat-icon { + color: #FAA405; + } + } + + .limit-field-row { + @media screen and (max-width: 520px) { + display: flex; + flex-direction: column; + + .fixed-title-width { + align-self: flex-start; + padding-top: 8px; + } + } + } +} + +:host ::ng-deep { + tb-entity-autocomplete { + .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { + padding-right: 0 !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts new file mode 100644 index 0000000000..c19dcefcd6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts @@ -0,0 +1,332 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, Input, OnInit, output, ViewChild } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + UntypedFormArray, + ValidatorFn, + Validators +} from '@angular/forms'; +import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { + ArgumentEntityType, + ArgumentEntityTypeParamsMap, + ArgumentEntityTypeTranslations, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingValue, + FORBIDDEN_NAMES, + forbiddenNamesValidator, + GeofencingDirectionLevelTranslations, + GeofencingDirectionTranslations, + GeofencingReportStrategy, + GeofencingReportStrategyTranslations, + getCalculatedFieldCurrentEntityFilter, + uniqueNameValidator +} from '@shared/models/calculated-field.models'; +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; +import { EntityType } from '@shared/models/entity-type.models'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { BehaviorSubject, merge, Observable, of } from 'rxjs'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { CdkDragDrop } from "@angular/cdk/drag-drop"; + +@Component({ + selector: 'tb-calculated-field-geofencing-zone-groups-panel', + templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html', + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-geofencing-zone-groups-panel.component.scss'], + standalone: false +}) +export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit, AfterViewInit { + + @Input() buttonTitle: string; + @Input() zone: CalculatedFieldGeofencing; + @Input() entityId: EntityId; + @Input() tenantId: string; + @Input() entityName: string; + @Input() ownerId: EntityId; + @Input() usedNames: string[]; + @Input() readonly = false; + + @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; + + geofencingDataApplied = output(); + + readonly maxRelationLevelPerCfArgument = getCurrentAuthState(this.store).maxRelationLevelPerCfArgument; + + geofencingFormGroup = this.fb.group({ + name: ['', [Validators.required, forbiddenNamesValidator(FORBIDDEN_NAMES), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + refEntityId: this.fb.group({ + entityType: [ArgumentEntityType.Current], + id: [''] + }), + refDynamicSourceConfiguration: this.fb.group({ + levels: this.fb.array([], [this.levelsRequired()]) + }), + perimeterKeyName: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex)]], + reportStrategy: [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS], + createRelationsWithMatchedZones: [false], + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]] + }); + + entityFilter: EntityFilter; + entityNameSubject = new BehaviorSubject(null); + + enableAutocomplete = false; + + readonly ArgumentEntityType = ArgumentEntityType; + readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; + readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; + readonly DataKeyType = DataKeyType; + readonly ArgumentEntityTypeParamsMap = ArgumentEntityTypeParamsMap; + readonly GeofencingReportStrategyList = Object.values(GeofencingReportStrategy) as Array; + readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; + readonly GeofencingDirectionList = Object.values(EntitySearchDirection) as Array; + readonly GeofencingDirectionTranslations = GeofencingDirectionTranslations; + readonly GeofencingDirectionLevelTranslations = GeofencingDirectionLevelTranslations; + readonly AttributeScope = AttributeScope; + + private currentEntityFilter: EntityFilter; + + constructor( + private fb: FormBuilder, + private cd: ChangeDetectorRef, + private popover: TbPopoverComponent, + private store: Store + ) { + + this.observeEntityFilterChanges(); + this.observeEntityTypeChanges(); + this.observeCreateRelationZonesChanges(); + } + + get entityType(): ArgumentEntityType { + return this.geofencingFormGroup.get('refEntityId').get('entityType').value; + } + + get refEntityIdFormGroup(): FormGroup { + return this.geofencingFormGroup.get('refEntityId') as FormGroup; + } + + get refDynamicSourceFormGroup(): FormGroup { + return this.geofencingFormGroup.get('refDynamicSourceConfiguration') as FormGroup; + } + + ngOnInit(): void { + this.updatedFormValidators(); + this.geofencingFormGroup.patchValue(this.zone, {emitEvent: false}); + if (this.zone.refDynamicSourceConfiguration?.type) { + this.refEntityIdFormGroup.get('entityType').setValue(this.zone.refDynamicSourceConfiguration.type, {emitEvent: false}); + } + if (this.zone?.refDynamicSourceConfiguration?.levels?.length > 0) { + this.zone.refDynamicSourceConfiguration.levels.forEach(level => { + this.levelsFormArray().push(this.fb.group({ + direction: [level.direction], + relationType: [level.relationType, [Validators.required]] + })); + }) + } else { + this.addKey(); + } + this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && + this.refEntityIdFormGroup.get('entityType').value === ArgumentEntityType.Owner; + this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones); + this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type); + + this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); + this.updateEntityFilter(this.zone.refEntityId?.entityType); + + if (this.readonly) { + this.geofencingFormGroup.disable({emitEvent: false}); + } + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedFormValidators(): void { + this.geofencingFormGroup.get('name').addValidators(uniqueNameValidator(this.usedNames)); + this.geofencingFormGroup.get('name').updateValueAndValidity({emitEvent: false}); + } + + private observeCreateRelationZonesChanges(): void { + this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateDirectionAndRelationType(value)); + } + + private validateDirectionAndRelationType(createRelation = false): void { + if (createRelation) { + this.geofencingFormGroup.get('direction').enable({emitEvent: false}); + this.geofencingFormGroup.get('relationType').enable({emitEvent: false}); + } else { + this.geofencingFormGroup.get('direction').disable({emitEvent: false}); + this.geofencingFormGroup.get('relationType').disable({emitEvent: false}); + } + } + + private validateRefDynamicSourceConfiguration(type: ArgumentEntityType = ArgumentEntityType.Current): void { + if (type === ArgumentEntityType.RelationQuery) { + this.refDynamicSourceFormGroup.enable({emitEvent: false}); + } else { + this.refDynamicSourceFormGroup.disable({emitEvent: false}); + } + } + + ngAfterViewInit(): void { + if (this.zone.refEntityId?.id === NULL_UUID) { + this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched(); + } + } + + saveZone(): void { + const value = this.geofencingFormGroup.value as CalculatedFieldGeofencingValue; + const argumentType = value.refEntityId.entityType; + switch (argumentType) { + case ArgumentEntityType.Current: + delete value.refEntityId; + break; + case ArgumentEntityType.Owner: + delete value.refEntityId; + value.refDynamicSourceConfiguration = {type: ArgumentEntityType.Owner}; + break; + case ArgumentEntityType.RelationQuery: + delete value.refEntityId; + value.refDynamicSourceConfiguration.type = ArgumentEntityType.RelationQuery; + break; + case ArgumentEntityType.Tenant: + value.refEntityId.id = this.tenantId; + break + default: + value.entityName = this.entityNameSubject.value; + } + this.geofencingDataApplied.emit(value); + } + + cancel(): void { + this.popover.hide(); + } + + private updateEntityFilter(entityType: ArgumentEntityType = ArgumentEntityType.Current): void { + let entityFilter: EntityFilter; + switch (entityType) { + case ArgumentEntityType.Current: + case ArgumentEntityType.RelationQuery: + entityFilter = this.currentEntityFilter; + break; + case ArgumentEntityType.Owner: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.ownerId + }; + break; + case ArgumentEntityType.Tenant: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: { + id: this.tenantId, + entityType: EntityType.TENANT + }, + }; + break; + default: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.geofencingFormGroup.get('refEntityId').value as unknown as EntityId, + }; + } + this.entityFilter = entityFilter; + this.cd.markForCheck(); + } + + private observeEntityFilterChanges(): void { + merge( + this.refEntityIdFormGroup.get('entityType').valueChanges, + this.refEntityIdFormGroup.get('id').valueChanges, + ) + .pipe(debounceTime(50), takeUntilDestroyed()) + .subscribe(() => this.updateEntityFilter(this.entityType)); + } + + private observeEntityTypeChanges(): void { + this.refEntityIdFormGroup.get('entityType').valueChanges + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(type => { + this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner; + this.geofencingFormGroup.get('refEntityId').get('id').setValue(null); + this.geofencingFormGroup.get('perimeterKeyName').reset(''); + const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner, ArgumentEntityType.RelationQuery].includes(type); + this.geofencingFormGroup.get('refEntityId').get('id')[isEntityWithId ? 'enable' : 'disable'](); + if (!isEntityWithId) { + this.entityNameSubject.next(null); + } + this.validateRefDynamicSourceConfiguration(type); + }); + } + + private levelsRequired(): ValidatorFn { + return (control: FormControl) => { + return control.value.length ? null : { levelsRequired: true }; + }; + } + + levelsFormArray(): UntypedFormArray { + return this.refDynamicSourceFormGroup.get('levels') as UntypedFormArray; + } + + trackByKey(_index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + this.levelsFormArray().removeAt(index); + this.levelsFormArray().markAsDirty(); + } + + addKey() { + this.levelsFormArray().push(this.fb.group({ + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]] + })); + } + + keyDrop(event: CdkDragDrop) { + const keysArray = this.levelsFormArray(); + const key = keysArray.at(event.previousIndex); + keysArray.removeAt(event.previousIndex); + keysArray.insert(event.currentIndex, key); + } + + get dragEnabled(): boolean { + return this.levelsFormArray().controls.length > 1 && !this.readonly; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html new file mode 100644 index 0000000000..92fded8a99 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -0,0 +1,163 @@ + +
+
+ + + +
{{ 'common.name' | translate }}
+
+ +
+
{{ geofenceZone.name }}
+ +
+
+
+ + + {{ 'entity.entity-type' | translate }} + + +
+ @if (geofenceZone.refEntityId?.entityType === ArgumentEntityType.Tenant) { + {{ 'calculated-fields.argument-current-tenant' | translate }} + } @else if (geofenceZone.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + {{ 'calculated-fields.argument-owner' | translate }} + } @else if (geofenceZone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery) { + {{ 'calculated-fields.argument-relation-query' | translate }} + } @else if (geofenceZone.refEntityId?.id) { + {{ entityTypeTranslations.get(geofenceZone.refEntityId.entityType).type | translate }} + } @else { + {{ 'calculated-fields.argument-current' | translate }} + } +
+
+
+ + + {{ 'calculated-fields.target-zone' | translate }} + + +
+ @if (geofenceZone.refEntityId?.id && geofenceZone.refEntityId?.entityType !== ArgumentEntityType.Tenant) { + + {{ entityNameMap.get(geofenceZone.refEntityId.id) ?? '' }} + + } +
+
+
+ + + + {{ 'calculated-fields.perimeter-key' | translate }} + + + +
{{ geofenceZone.perimeterKeyName }}
+
+
+
+ + + + {{ 'calculated-fields.report-strategy' | translate }} + + +
{{ GeofencingReportStrategyTranslations.get(geofenceZone.reportStrategy) | translate }}
+
+
+ + + + +
+ @if (!isEditValue) { + + } @else { + + } + +
+
+
+ + +
+ @if (errorText) { + + } +
+
+ {{ 'calculated-fields.no-zone-configured' | translate }} +
+
+ + @if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) { +
+ warning + {{ 'calculated-fields.hint.max-geofencing-zone' | translate }} +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts new file mode 100644 index 0000000000..4afbc83dc5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts @@ -0,0 +1,314 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + booleanAttribute, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + Renderer2, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + ArgumentEntityType, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingValue, + GeofencingReportStrategyTranslations, +} from '@shared/models/calculated-field.models'; +import { MatIconButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { getEntityDetailsPageURL, isEqual } from '@core/utils'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; +import { EntityService } from '@core/http/entity.service'; +import { MatSort } from '@angular/material/sort'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { forkJoin, Observable } from 'rxjs'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BaseData } from '@shared/models/base-data'; +import { + CalculatedFieldGeofencingZoneGroupsPanelComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; + +@Component({ + selector: 'tb-calculated-field-geofencing-zone-groups-table', + templateUrl: './calculated-field-geofencing-zone-groups-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + } + ], + standalone: false +}) +export class CalculatedFieldGeofencingZoneGroupsTableComponent implements ControlValueAccessor, Validator, AfterViewInit { + + @Input({required: true}) entityId: EntityId; + @Input({required: true}) tenantId: string; + @Input({required: true}) entityName: string; + @Input({required: true}) ownerId: EntityId; + @Input({transform: booleanAttribute}) isEditValue = true; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + errorText = ''; + zoneGroupsFormArray = this.fb.array([]); + entityNameMap = new Map(); + sortOrder = { direction: 'asc', property: '' }; + dataSource = new CalculatedFieldZoneDatasource(); + disable = false; + + readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; + readonly entityTypeTranslations = entityTypeTranslations; + readonly ArgumentEntityType = ArgumentEntityType; + readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF - 2; + readonly NULL_UUID = NULL_UUID; + + private popoverComponent: TbPopoverComponent; + private propagateChange: (zonesObj: Record) => void = () => {}; + + constructor( + private fb: FormBuilder, + private popoverService: TbPopoverService, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private entityService: EntityService, + private destroyRef: DestroyRef, + private store: Store + ) { + this.zoneGroupsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + this.updateDataSource(value); + this.propagateChange(this.getZonesObject(value)); + }); + } + + ngAfterViewInit(): void { + this.sort.sortChange.asObservable().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.sortOrder.property = this.sort.active; + this.sortOrder.direction = this.sort.direction; + this.updateDataSource(this.zoneGroupsFormArray.value); + }); + } + + registerOnChange(fn: (zonesObj: Record) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_): void {} + + validate(): ValidationErrors | null { + this.updateErrorText(); + return this.errorText ? { zonesFormArray: false } : null; + } + + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + + onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void { + $event.stopPropagation(); + const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); + this.zoneGroupsFormArray.removeAt(index); + this.zoneGroupsFormArray.markAsDirty(); + } + + manageZone($event: Event, matButton: MatIconButton, zone = {} as CalculatedFieldGeofencingValue, readonly: boolean = false): void { + $event?.stopPropagation(); + if (this.popoverComponent && !this.popoverComponent.tbHidden) { + this.popoverComponent.hide(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); + const isExists = index !== -1; + const ctx = { + index, + zone, + entityId: this.entityId, + buttonTitle: isExists ? 'action.apply' : 'action.add', + tenantId: this.tenantId, + entityName: this.entityName, + ownerId: this.ownerId, + usedNames: this.zoneGroupsFormArray.value.map(({ name }) => name).filter(name => name !== zone.name), + readonly + }; + this.popoverComponent = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: CalculatedFieldGeofencingZoneGroupsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'], + context: ctx, + isModal: true + }); + this.popoverComponent.tbComponentRef.instance.geofencingDataApplied.subscribe(({ entityName, ...value }) => { + this.popoverComponent.hide(); + if (entityName) { + this.entityNameMap.set(value.refEntityId.id, entityName); + } + if (isExists) { + this.zoneGroupsFormArray.at(index).setValue(value); + } else { + this.zoneGroupsFormArray.push(this.fb.control(value)); + } + this.cd.markForCheck(); + }); + } + } + + private updateDataSource(value: CalculatedFieldGeofencingValue[]): void { + const sortedValue = this.sortData(value); + this.dataSource.loadData(sortedValue); + } + + private updateErrorText(): void { + if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { + this.errorText = 'calculated-fields.hint.geofencing-entity-not-found'; + } else { + this.errorText = ''; + } + } + + private getZonesObject(value: CalculatedFieldGeofencingValue[]): Record { + return value.reduce((acc, zoneValue) => { + const { name, ...zone } = zoneValue as CalculatedFieldGeofencingValue; + acc[name] = zone; + return acc; + }, {} as Record); + } + + writeValue(zonesObj: Record): void { + this.zoneGroupsFormArray.clear({emitEvent: false}); + this.populateZonesFormArray(zonesObj); + this.updateEntityNameMap(this.zoneGroupsFormArray.value); + } + + getEntityDetailsPageURL(id: string, type: EntityType): string { + return getEntityDetailsPageURL(id, type); + } + + private populateZonesFormArray(zonesObj: Record): void { + Object.keys(zonesObj).forEach(key => { + const value: CalculatedFieldGeofencingValue = { + ...zonesObj[key], + name: key + }; + this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false }); + }); + this.updateDataSource(this.zoneGroupsFormArray.value); + } + + private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void { + const entitiesByType = values.reduce((acc, { refEntityId = {}}) => { + if (refEntityId.id && refEntityId.entityType !== ArgumentEntityType.Tenant) { + const { id, entityType } = refEntityId as EntityId; + acc[entityType] = acc[entityType] ?? []; + acc[entityType].push(id); + } + return acc; + }, {} as Record); + const tasks = Object.entries(entitiesByType).map(([entityType, ids]) => + this.entityService.getEntities(entityType as EntityType, ids) + ); + if (!tasks.length) { + return; + } + this.fetchEntityNames(tasks, values); + } + + private fetchEntityNames(tasks: Observable[]>[], values: CalculatedFieldGeofencingValue[]): void { + forkJoin(tasks as Observable[]>[]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result: Array>[]) => { + result.forEach((entities: BaseData[]) => entities.forEach((entity: BaseData) => this.entityNameMap.set(entity.id.id, entity.name))); + let updateTable = false; + values.forEach(({ refEntityId }) => { + if (refEntityId?.id && !this.entityNameMap.has(refEntityId.id) && refEntityId.entityType !== ArgumentEntityType.Tenant) { + updateTable = true; + const control = this.zoneGroupsFormArray.controls.find(control => control.value.refEntityId?.id === refEntityId.id); + const value = control.value; + value.refEntityId.id = NULL_UUID; + control.setValue(value, { emitEvent: false }); + } + }); + if (updateTable) { + this.zoneGroupsFormArray.updateValueAndValidity(); + } + }); + } + + private getSortValue(zone: CalculatedFieldGeofencingValue, column: string): string { + switch (column) { + case 'entityType': + if (zone.refEntityId?.entityType === ArgumentEntityType.Tenant) { + return 'calculated-fields.argument-current-tenant'; + } else if (zone.refDynamicSourceConfiguration.type === ArgumentEntityType.RelationQuery) { + return 'calculated-fields.argument-relation-query'; + } else if (zone.refEntityId?.id) { + return entityTypeTranslations.get((zone.refEntityId)?.entityType as unknown as EntityType).type; + } else { + return 'calculated-fields.argument-current'; + } + case 'key': + return zone.perimeterKeyName; + case 'reportStrategy': + return GeofencingReportStrategyTranslations.get(zone.reportStrategy); + default: + return zone.name; + } + } + + private sortData(data: CalculatedFieldGeofencingValue[]): CalculatedFieldGeofencingValue[] { + return data.sort((a, b) => { + const valA = this.getSortValue(a, this.sortOrder.property) ?? ''; + const valB = this.getSortValue(b, this.sortOrder.property) ?? ''; + return (this.sortOrder.direction === 'asc' ? 1 : -1) * valA.localeCompare(valB); + }); + } +} + +class CalculatedFieldZoneDatasource extends TbTableDatasource { + constructor() { + super(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html new file mode 100644 index 0000000000..25f736a222 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html @@ -0,0 +1,70 @@ + +
+
+
+ {{ 'calculated-fields.entity-coordinates' | translate }} +
+
+ + +
+
+ +
+
+ {{ 'calculated-fields.geofencing-zone-groups' | translate }} +
+ +
+ +
+ {{ 'calculated-fields.zone-group-refresh-interval' | translate }} +
+
+
+ + +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts new file mode 100644 index 0000000000..2f621e2a38 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts @@ -0,0 +1,172 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + ArgumentEntityType, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingConfiguration, + CalculatedFieldOutput, + CalculatedFieldType, + defaultCalculatedFieldOutput, + getCalculatedFieldCurrentEntityFilter, + notEmptyObjectValidator +} from '@shared/models/calculated-field.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { EntityId } from '@shared/models/id/entity-id'; + +@Component({ + selector: 'tb-geofencing-configuration', + templateUrl: './geofencing-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GeofencingConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GeofencingConfigurationComponent), + multi: true + } + ], + standalone: false +}) +export class GeofencingConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + ownerId: EntityId; + + @Input({transform: booleanAttribute}) isEditValue = true; + + readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; + readonly DataKeyType = DataKeyType; + + geofencingConfiguration = this.fb.group({ + entityCoordinates: this.fb.group({ + latitudeKeyName: [null, [Validators.required]], + longitudeKeyName: [null, [Validators.required]], + }), + zoneGroups: this.fb.control>({}, notEmptyObjectValidator()), + scheduledUpdateEnabled: [true], + scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], + output: this.fb.control(defaultCalculatedFieldOutput) + }); + + currentEntityFilter: EntityFilter; + isRelatedEntity: boolean; + + private propagateChange: (config: CalculatedFieldGeofencingConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store) { + + this.geofencingConfiguration.get('zoneGroups').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe((zoneGroups: Record) => + this.checkRelatedEntity(zoneGroups) + ); + + this.geofencingConfiguration.get('scheduledUpdateEnabled').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe((value: boolean) => + this.checkScheduledUpdateEnabled(value) + ); + + this.geofencingConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.updatedModel(this.geofencingConfiguration.getRawValue() as any); + }) + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.entityName || changes.entityId) { + const entityNameChanges = changes.entityName; + const entityIdChanges = changes.entityId; + if ((entityNameChanges?.currentValue !== entityNameChanges?.previousValue) || (entityIdChanges?.currentValue !== entityIdChanges?.previousValue)) { + this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); + } + } + } + + validate(): ValidationErrors | null { + return this.geofencingConfiguration.valid || this.geofencingConfiguration.disabled ? null : { geofencingConfigError: false }; + } + + writeValue(config: CalculatedFieldGeofencingConfiguration): void { + this.geofencingConfiguration.patchValue(config, {emitEvent: false}); + this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value); + if (this.geofencingConfiguration.enabled) { + this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); + } + } + + registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.geofencingConfiguration.disable({emitEvent: false}); + } else { + this.geofencingConfiguration.enable({emitEvent: false}); + this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); + } + } + + private updatedModel(value: CalculatedFieldGeofencingConfiguration) { + value.type = CalculatedFieldType.GEOFENCING; + this.propagateChange(value) + } + + private checkScheduledUpdateEnabled(value: boolean) { + if (value) { + this.geofencingConfiguration.get('scheduledUpdateInterval').enable({emitEvent: false}); + } else { + this.geofencingConfiguration.get('scheduledUpdateInterval').disable({emitEvent: false}); + } + } + + private checkRelatedEntity(zoneGroups: Record) { + this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts new file mode 100644 index 0000000000..a7b9b49e1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldGeofencingZoneGroupsTableComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component'; +import { + CalculatedFieldGeofencingZoneGroupsPanelComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; +import { SharedModule } from '@shared/shared.module'; +import { + GeofencingConfigurationComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule + ], + declarations: [ + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, + GeofencingConfigurationComponent + ], + exports: [ + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, + GeofencingConfigurationComponent + ] +}) +export class GeofencingConfigurationModule { + +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html new file mode 100644 index 0000000000..96de7bf113 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html @@ -0,0 +1,226 @@ + +
+
{{ 'calculated-fields.metrics.metric-settings' | translate }}
+
+
+
{{ 'calculated-fields.metrics.metric-name' | translate }}
+ + + @if (metricForm.get('name').touched && metricForm.get('name').hasError('required')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('duplicateName')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('pattern')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('maxlength')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('forbiddenName')) { + + warning + + } + +
+
+
{{ 'calculated-fields.metrics.aggregation' | translate }}
+ + + @for (aggFunction of AggFunctions; track aggFunction) { + {{ AggFunctionTranslations.get(aggFunction) | translate }} + } + + +
+ + @if (!simpleMode) { +
+ + + + +
+ {{ 'calculated-fields.metrics.filter' | translate }} +
+
+
+
+ + +
{{ 'api-usage.tbel' | translate }} +
+ +
+
+ +
+
+
+
+ } + + @if (!simpleMode) { +
+
+ {{ 'calculated-fields.metrics.value-source' | translate }} +
+ + + @for (inputType of AggInputTypes; track inputType) { + {{ AggInputTypeTranslations.get(inputType) | translate }} + } + + +
+ } + @if (metricForm.get('input.type').value === AggInputType.key) { +
+
{{ 'calculated-fields.argument-name' | translate }}
+ + + @for (argument of arguments; track argument) { + {{ argument }} + } + + @if (metricForm.get('input.key').touched && metricForm.get('input.key').hasError('required')) { + + warning + + } + +
+ } @else { + +
{{ 'api-usage.tbel' | translate }} +
+ +
+
+ +
+ } +
+ @if (simpleMode) { +
+
{{ 'calculated-fields.default-value' | translate }}
+ + + +
+ } +
+
+ + @if (!readonly) { + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts new file mode 100644 index 0000000000..7af34df61c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts @@ -0,0 +1,187 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, output } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { FormBuilder, Validators } from '@angular/forms'; +import { charsWithNumRegex } from '@shared/models/regex.constants'; +import { + AggFunction, + AggFunctionTranslations, + AggInputType, + AggInputTypeTranslations, + CalculatedFieldAggMetricValue, + calculatedFieldMetricFilterDefaultScript, + calculatedFieldMetricMapDefaultScript, + FORBIDDEN_NAMES, + forbiddenNamesValidator, + uniqueNameValidator +} from '@shared/models/calculated-field.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; +import { Observable } from "rxjs"; + +interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue { + allowFilter: boolean; +} + +@Component({ + selector: 'tb-calculated-field-metrics-panel', + templateUrl: './calculated-field-metrics-panel.component.html', + styleUrls: ['../common/calculated-field-panel.scss', '../../calculated-field.component.scss'], + standalone: false +}) +export class CalculatedFieldMetricsPanelComponent implements OnInit { + + @Input() buttonTitle: string; + @Input() metric: CalculatedFieldAggMetricValue; + @Input() usedNames: string[]; + @Input() arguments: Array; + @Input() simpleMode: boolean; + @Input() editorCompleter: TbEditorCompleter; + @Input() highlightRules: AceHighlightRules; + @Input({required: true}) testScript: (expression?: string) => Observable; + @Input() readonly = false; + + metricDataApplied = output(); + filterExpanded = false; + functionArgs: Array + + metricForm = this.fb.group({ + name: ['', [Validators.required, forbiddenNamesValidator(FORBIDDEN_NAMES), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + function: [AggFunction.AVG], + allowFilter: [false], + filter: [calculatedFieldMetricFilterDefaultScript, Validators.required], + input: this.fb.group({ + type: [AggInputType.key], + key: ['', Validators.required], + function: [calculatedFieldMetricMapDefaultScript, Validators.required], + }), + defaultValue: [null] + }); + + entityFilter: EntityFilter; + + AggFunctions = Object.values(AggFunction) as AggFunction[]; + readonly AggFunctionTranslations = AggFunctionTranslations; + readonly ScriptLanguage = ScriptLanguage; + readonly AggInputType = AggInputType; + readonly AggInputTypes = Object.values(AggInputType) as AggInputType[]; + readonly AggInputTypeTranslations = AggInputTypeTranslations; + + constructor( + private fb: FormBuilder, + private popover: TbPopoverComponent, + ) { + this.observeFilterAllowChange(); + this.observeInputTypeChange(); + } + + ngOnInit(): void { + this.updatedForm(); + + const data: CalculatedFieldAggMetricValuePanel = { + ...this.metric, + allowFilter: !!this.metric.filter, + } + this.metricForm.patchValue(data, {emitEvent: false}); + + this.validateFilter(data.allowFilter); + this.validateInputTypeFilter(data.input?.type ?? AggInputType.key); + this.validateInputKey(); + + this.functionArgs = ['ctx', ...this.arguments]; + + if (this.simpleMode) { + this.AggFunctions = this.AggFunctions.filter(aggFunc => aggFunc !== AggFunction.COUNT_UNIQUE); + } + + if (this.readonly) { + this.metricForm.disable({emitEvent: false}); + } + } + + saveMetric(): void { + const value = this.metricForm.value as CalculatedFieldAggMetricValuePanel; + if (!value.allowFilter) { + delete value.filter; + } + delete value.allowFilter; + this.metricDataApplied.emit(value); + } + + cancel(): void { + this.popover.hide(); + } + + private updatedForm(): void { + this.metricForm.get('name').addValidators(uniqueNameValidator(this.usedNames)); + this.metricForm.get('name').updateValueAndValidity({emitEvent: false}); + + if (!this.simpleMode) { + this.metricForm.removeControl('defaultValue', {emitEvent: false}); + } + } + + private observeFilterAllowChange(): void { + this.metricForm.get('allowFilter').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateFilter(value)); + } + + private observeInputTypeChange(): void { + this.metricForm.get('input.type').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateInputTypeFilter(value)); + } + + private validateFilter(allowFilter = false): void { + if (allowFilter) { + this.metricForm.get('filter').enable({emitEvent: false}); + } else { + this.metricForm.get('filter').disable({emitEvent: false}); + } + this.filterExpanded = allowFilter; + } + + private validateInputTypeFilter(value: AggInputType): void { + const inputForm = this.metricForm.get('input'); + if (value === AggInputType.key) { + inputForm.get('key').enable({emitEvent: false}); + inputForm.get('function').disable({emitEvent: false}); + } else { + inputForm.get('key').disable({emitEvent: false}); + inputForm.get('function').enable({emitEvent: false}); + } + } + + private validateInputKey() { + if (this.metric.input?.type === AggInputType.key && !this.arguments.includes(this.metric.input.key)) { + this.metricForm.get('input.key').setValue(null); + this.metricForm.get('input.key').markAsTouched(); + } + } + + onTestScript(scriptFunc: 'filter' | 'input.function') { + this.testScript(this.metricForm.get(scriptFunc).value).subscribe(expression => { + this.metricForm.get(scriptFunc).setValue(expression); + this.metricForm.get(scriptFunc).markAsDirty(); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html new file mode 100644 index 0000000000..f26387d5da --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -0,0 +1,151 @@ + +
+
+ + + +
{{ 'calculated-fields.metrics.metric-name' | translate }}
+
+ +
+
{{ metric.name }}
+ +
+
+
+ + + {{ 'calculated-fields.metrics.aggregation' | translate }} + + +
{{ AggFunctionTranslations.get(metric.function) | translate }}
+
+
+ + + {{ 'calculated-fields.metrics.argument-name' | translate }} + + +
{{ metric.input.key }}
+
+
+ + + {{ 'calculated-fields.metrics.filtered' | translate }} + + +
+ {{ metric.filter ? 'check_box' : 'check_box_outline_blank' }} +
+
+
+ + + {{ 'calculated-fields.metrics.value-source' | translate }} + + +
{{ AggInputTypeTranslations.get(metric.input.type) | translate }}
+
+
+ + + + +
+ @if (disable) { + + } @else { + + } + +
+
+
+ + +
+
+
+ {{ 'calculated-fields.metrics.no-metrics-configured' | translate }} +
+
+ + @if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) { +
+ warning + {{ 'calculated-fields.metrics.max-metrics' | translate }} +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts new file mode 100644 index 0000000000..8301c99a44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -0,0 +1,264 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + booleanAttribute, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + OnInit, + Renderer2, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + AggFunctionTranslations, + AggInputTypeTranslations, + CalculatedFieldAggMetric, + CalculatedFieldAggMetricValue, +} from '@shared/models/calculated-field.models'; +import { MatIconButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { isDefinedAndNotNull, isEqual } from '@core/utils'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; +import { MatSort, SortDirection } from '@angular/material/sort'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + CalculatedFieldMetricsPanelComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; +import { Observable } from "rxjs"; + +@Component({ + selector: 'tb-calculated-field-metrics-table', + templateUrl: './calculated-field-metrics-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + } + ], + standalone: false +}) +export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit { + + @Input() arguments: Array; + @Input() editorCompleter: TbEditorCompleter; + @Input() highlightRules: AceHighlightRules; + @Input({transform: booleanAttribute}) simpleMode: boolean = false; + @Input({required: true}) testScript: (expression?: string) => Observable; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + errorText = ''; + metricsFormArray = this.fb.array([]); + sortOrder = { direction: 'asc' as SortDirection, property: '' }; + dataSource = new CalculatedFieldMetricsDatasource(); + disable = false; + + displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions']; + + readonly AggFunctionTranslations = AggFunctionTranslations; + readonly AggInputTypeTranslations = AggInputTypeTranslations; + readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF - 2; + + private popoverComponent: TbPopoverComponent; + private propagateChange: (zonesObj: Record) => void = () => {}; + + constructor( + private fb: FormBuilder, + private popoverService: TbPopoverService, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private destroyRef: DestroyRef, + private store: Store + ) { + this.metricsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + this.updateDataSource(value); + this.propagateChange(this.getMetricsObject(value)); + }); + } + + ngOnInit() { + if (this.simpleMode) { + this.displayColumns = ['name', 'function', 'argumentName', 'actions']; + } + } + + ngAfterViewInit(): void { + this.sort.sortChange.asObservable().pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.sortOrder.property = this.sort.active; + this.sortOrder.direction = this.sort.direction; + this.updateDataSource(this.metricsFormArray.value); + }); + } + + registerOnChange(fn: (zonesObj: Record) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void {} + + validate(): ValidationErrors | null { + this.updateErrorText(); + return this.errorText ? { metricsFormArray: false } : null; + } + + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + + onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void { + $event.stopPropagation(); + const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); + this.metricsFormArray.removeAt(index); + this.metricsFormArray.markAsDirty(); + } + + manageMetrics($event: Event, matButton: MatIconButton, metric = {} as CalculatedFieldAggMetricValue, readonly: boolean = false): void { + $event?.stopPropagation(); + if (this.popoverComponent && !this.popoverComponent.tbHidden) { + this.popoverComponent.hide(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); + const isExists = index !== -1; + const ctx = { + index, + metric, + buttonTitle: isExists ? 'action.apply' : 'action.add', + usedNames: this.metricsFormArray.value.map(({ name }) => name).filter(name => name !== metric.name), + arguments: this.arguments, + editorCompleter: this.editorCompleter, + highlightRules: this.highlightRules, + simpleMode: this.simpleMode, + testScript: this.testScript, + readonly + }; + this.popoverComponent = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: CalculatedFieldMetricsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'], + context: ctx, + isModal: true + }); + this.popoverComponent.tbComponentRef.instance.metricDataApplied.subscribe((value) => { + this.popoverComponent.hide(); + if (isExists) { + this.metricsFormArray.at(index).setValue(value); + } else { + this.metricsFormArray.push(this.fb.control(value)); + } + this.cd.markForCheck(); + }); + } + } + + private updateDataSource(value: CalculatedFieldAggMetricValue[]): void { + const sortedValue = this.sortData(value); + this.dataSource.loadData(sortedValue); + } + + private updateErrorText(): void { + if (!this.metricsFormArray.controls.length) { + this.errorText = 'calculated-fields.metrics.metrics-empty'; + } else { + this.errorText = ''; + } + } + + private getMetricsObject(value: CalculatedFieldAggMetricValue[]): Record { + return value.reduce((acc, metricValue) => { + const { name, ...metric } = metricValue; + acc[name] = metric; + return acc; + }, {} as Record); + } + + writeValue(metrics: Record): void { + this.metricsFormArray.clear({emitEvent: false}); + this.populateZonesFormArray(metrics); + } + + private populateZonesFormArray(metrics: Record): void { + Object.keys(metrics).forEach(key => { + const value: CalculatedFieldAggMetricValue = { + ...metrics[key], + name: key + }; + this.metricsFormArray.push(this.fb.control(value), { emitEvent: false }); + }); + this.updateDataSource(this.metricsFormArray.value); + } + + private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string { + switch (column) { + case 'function': + return metric.function; + case 'valueSource': + return metric.input?.type; + case 'filter': + return isDefinedAndNotNull(metric.filter).toString(); + default: + return metric.name; + } + } + + private sortData(data: CalculatedFieldAggMetricValue[]): CalculatedFieldAggMetricValue[] { + return data.sort((a, b) => { + const valA = this.getSortValue(a, this.sortOrder.property) ?? ''; + const valB = this.getSortValue(b, this.sortOrder.property) ?? ''; + return (this.sortOrder.direction === 'asc' ? 1 : -1) * valA.localeCompare(valB); + }); + } +} + +class CalculatedFieldMetricsDatasource extends TbTableDatasource { + constructor() { + super(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts new file mode 100644 index 0000000000..ee72828bc8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldMetricsTableComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component'; +import { + CalculatedFieldMetricsPanelComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + CalculatedFieldMetricsTableComponent, + CalculatedFieldMetricsPanelComponent + ], + exports: [ + CalculatedFieldMetricsTableComponent + ] +}) +export class CalculatedFieldMetricsTableModule { + +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html new file mode 100644 index 0000000000..aef5126fc3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -0,0 +1,179 @@ + +
+
+ {{ 'calculated-fields.output' | translate }} +
+
+
+ + {{ 'calculated-fields.output-type' | translate }} + + @for (type of outputTypes; track type) { + {{ OutputTypeTranslations.get(type) | translate }} + } + + + @if (outputForm.get('type').value === OutputType.Attribute + && (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.DEVICE_PROFILE)) { + + {{ 'calculated-fields.attribute-scope' | translate }} + + + {{ 'calculated-fields.server-attributes' | translate }} + + + {{ 'calculated-fields.shared-attributes' | translate }} + + + + } +
+ @if (simpleMode) { + @if (hiddenName) { +
+ + +
+ } @else { +
+ + {{ (outputForm.get('type').value === OutputType.Timeseries + ? 'calculated-fields.timeseries-key' + : 'calculated-fields.attribute-key') | translate }} + + @if (outputForm.get('name').errors && outputForm.get('name').touched) { + + @if (outputForm.get('name').hasError('required')) { + {{ 'common.hint.key-required' | translate }} + } @else if (outputForm.get('name').hasError('pattern')) { + {{ 'common.hint.key-pattern' | translate }} + } @else if (outputForm.get('name').hasError('maxlength')) { + {{ 'common.hint.key-max-length' | translate }} + } + + } + + +
+ + } + } +
+
+ + + +
+
+ {{ 'calculated-fields.output-strategy.strategy' | translate }} +
+ + @for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) { + {{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }} + } + +
+
+
+ @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { +
+
calculated-fields.output-strategy.processing-parameters
+ @if (outputForm.get('type').value === OutputType.Timeseries) { + +
+
calculated-fields.output-strategy.save-time-series
+
+
+ +
+
calculated-fields.output-strategy.save-latest-values
+
+
+ } @else { + +
+
calculated-fields.output-strategy.save-database
+
+
+ } + +
+
calculated-fields.output-strategy.send-web-sockets
+
+
+ +
+
calculated-fields.output-strategy.save-calculated-fields
+
+
+
+ @if (outputForm.get('type').value === OutputType.Attribute) { +
+ +
+
calculated-fields.output-strategy.update-attribute-only-on-value-change
+
+
+
+
+ +
+
calculated-fields.output-strategy.send-attributes-updated-notification
+
+
+
+ } @else { +
+ +
+
calculated-fields.output-strategy.ttl
+
+
+ + +
+ } + } +
+
+
+ + + {{ 'calculated-fields.decimals-by-default' | translate }} + + @if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) { + {{ 'calculated-fields.hint.decimals-range' | translate }} + } + + + + + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss new file mode 100644 index 0000000000..b0d282b7f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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-mdc-chip-disabled { + .mdc-evolution-chip__action { + cursor: default + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts new file mode 100644 index 0000000000..0fe3415ac1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -0,0 +1,266 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, forwardRef, inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { + CalculatedFieldOutput, + CalculatedFieldSimpleOutput, + OutputStrategyType, + OutputStrategyTypeTranslations, + OutputType, + OutputTypeTranslations +} from '@shared/models/calculated-field.models'; +import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { merge } from 'rxjs'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-calculate-field-output', + templateUrl: './calculated-field-output.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + } + ], + styleUrls: ['./calculated-field-output.component.scss'], + standalone: false +}) +export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { + + @Input() + @coerceBoolean() + simpleMode = false; + + @Input() + @coerceBoolean() + hiddenName = false; + + @Input() + @coerceBoolean() + disableType = false; + + @Input() + containerInputClass: string | string[] | Record = 'flex flex-col gap-3'; + + @Input({required: true}) + entityId: EntityId; + + disabled = false; + + readonly outputTypes = Object.values(OutputType) as OutputType[]; + readonly OutputType = OutputType; + readonly AttributeScope = AttributeScope; + readonly OutputTypeTranslations = OutputTypeTranslations; + readonly EntityType = EntityType; + + readonly OutputStrategyType = OutputStrategyType; + readonly OutputStrategyTypes = Object.values(OutputStrategyType) as OutputStrategyType[]; + readonly OutputStrategyTypeTranslations = OutputStrategyTypeTranslations; + + private fb = inject(FormBuilder); + private destroyRef = inject(DestroyRef); + + outputForm = this.fb.group({ + name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}], + type: [OutputType.Timeseries], + decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], + strategy: this.fb.group({ + type: [OutputStrategyType.IMMEDIATE], + saveTimeSeries: [true], + saveLatest: [true], + saveAttribute: [true], + sendWsUpdate: [true], + processCfs: [true], + updateAttributesOnlyOnValueChange: [true], + sendAttributesUpdatedNotification: [false], + useCustomTtl: [false], + ttl: [0] + }) + }); + + private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { }; + + ngOnInit() { + this.outputForm.get('type').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(type => { + this.toggleScopeByOutputType(type); + this.updatedStrategy(); + }); + + this.outputForm.get('strategy.saveTimeSeries').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => this.updateTimeSeriesTtl(value)); + + merge( + this.outputForm.get('strategy.type').valueChanges, + this.outputForm.get('strategy.useCustomTtl').valueChanges + ) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.updatedStrategy(); + }); + + this.updatedFormWithMode(); + + this.outputForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => { + this.updatedModel(value); + }) + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'simpleMode' && !this.disabled) { + this.updatedFormWithMode(); + if (!change.firstChange) { + this.outputForm.updateValueAndValidity(); + } + } + } + } + } + + validate(): ValidationErrors | null { + return this.outputForm.valid || this.outputForm.disabled ? null : {outputConfig: false}; + } + + writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void { + this.outputForm.patchValue(value, {emitEvent: false}); + if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) { + this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false}); + } + this.outputForm.get('type').updateValueAndValidity({onlySelf: true, emitEvent: false}); + } + + registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.outputForm.disable({emitEvent: false}); + } else { + this.outputForm.enable({emitEvent: false}); + this.updatedFormWithMode(); + this.toggleScopeByOutputType(this.outputForm.get('type').value); + this.updatedStrategy(); + this.updateTimeSeriesTtl(this.outputForm.get('strategy.saveTimeSeries').value); + if (this.outputForm.invalid) { + this.outputForm.updateValueAndValidity(); + } + } + } + + private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) { + if (this.simpleMode && 'name' in value) { + value.name = value.name?.trim() ?? ''; + } + if (this.disableType) { + value.type = this.outputForm.get('type').value; + } + if (value.type === OutputType.Timeseries && value.strategy.type === OutputStrategyType.IMMEDIATE) { + value = deepClone(value); + delete (value.strategy as any).useCustomTtl + } + this.propagateChange(value); + } + + private toggleScopeByOutputType(type: OutputType): void { + if (type === OutputType.Attribute) { + this.outputForm.get('scope').enable({emitEvent: false}); + } else { + this.outputForm.get('scope').disable({emitEvent: false}); + } + } + + private updatedFormWithMode(): void { + if (this.simpleMode && !this.hiddenName) { + this.outputForm.get('name').enable({emitEvent: false}); + } else { + this.outputForm.get('name').disable({emitEvent: false}); + } + if (this.simpleMode) { + this.outputForm.get('decimalsByDefault').enable({emitEvent: false}); + } else { + this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); + } + if (this.disableType) { + this.outputForm.get('type').disable({emitEvent: false}); + } + } + + private updatedStrategy(): void { + const strategyType = this.outputForm.get('strategy.type').value; + this.outputForm.get('strategy').disable({emitEvent: false}); + this.outputForm.get('strategy.type').enable({emitEvent: false}); + + if (strategyType === OutputStrategyType.IMMEDIATE) { + const outputType = this.outputForm.get('type').value; + this.outputForm.get('strategy.sendWsUpdate').enable({emitEvent: false}); + this.outputForm.get('strategy.processCfs').enable({emitEvent: false}); + if (outputType === OutputType.Attribute) { + this.outputForm.get('strategy.saveAttribute').enable({emitEvent: false}); + this.outputForm.get('strategy.updateAttributesOnlyOnValueChange').enable({emitEvent: false}); + this.outputForm.get('strategy.sendAttributesUpdatedNotification').enable({emitEvent: false}); + } else { + this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false}); + this.outputForm.get('strategy.saveLatest').enable({emitEvent: false}); + this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false}); + if (this.outputForm.get('strategy.useCustomTtl').value) { + this.outputForm.get('strategy.ttl').enable({emitEvent: false}); + } + } + } + } + + private updateTimeSeriesTtl(value: boolean) { + if (value) { + this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false}); + } else { + this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false}); + this.outputForm.get('strategy.ttl').disable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts new file mode 100644 index 0000000000..848aa1d5de --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldOutputComponent +} from '@home/components/calculated-fields/components/output/calculated-field-output.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + CalculatedFieldOutputComponent + ], + exports: [ + CalculatedFieldOutputComponent + ] +}) +export class CalculatedFieldOutputModule { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html deleted file mode 100644 index 08f660b05f..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ /dev/null @@ -1,209 +0,0 @@ - -
-
-
{{ 'calculated-fields.argument-settings' | translate }}
-
-
-
{{ 'calculated-fields.argument-name' | translate }}
- - - @if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { - - warning - - } - -
- -
-
{{ 'entity.entity-type' | translate }}
- - - @for (type of argumentEntityTypes; track type) { - {{ ArgumentEntityTypeTranslations.get(type) | translate }} - } - - -
- @if (ArgumentEntityTypeParamsMap.has(entityType)) { -
-
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
- -
- } -
- -
-
{{ 'calculated-fields.argument-type' | translate }}
- - - @for (type of argumentTypes; track type) { - {{ ArgumentTypeTranslations.get(type) | translate }} - } - - @if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) { - - warning - - } - -
- @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) { - @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { -
-
{{ 'calculated-fields.timeseries-key' | translate }}
- @if (refEntityKeyFormGroup.get('type').value === ArgumentType.LatestTelemetry) { - - } @else { - - } - - - -
- } @else { - @if (enableAttributeScopeSelection) { -
-
{{ 'calculated-fields.attribute-scope' | translate }}
- - - - {{ 'calculated-fields.server-attributes' | translate }} - - - {{ 'calculated-fields.client-attributes' | translate }} - - - {{ 'calculated-fields.shared-attributes' | translate }} - - - -
- } -
-
{{ 'calculated-fields.attribute-key' | translate }}
- -
- } - } -
- @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { -
-
{{ 'calculated-fields.default-value' | translate }}
- - - -
- } @else { -
-
{{ 'calculated-fields.time-window' | translate }}
- -
- @if (maxDataPointsPerRollingArg) { -
-
{{ 'calculated-fields.limit' | translate }}
-
- - - - - - -
-
- } - } -
-
-
- - -
-
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html new file mode 100644 index 0000000000..bb06683d5d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -0,0 +1,100 @@ + +
+
+
+ {{ 'calculated-fields.propagation-path-related-entities' | translate }} +
+
+ + {{ 'calculated-fields.direction' | translate }} + + @for (direction of Directions; track direction) { + {{ PropagationDirectionTranslations.get(direction) | translate }} + } + + + + +
+
+
+
+
+ {{ 'calculated-fields.data-propagate' | translate }} +
+ + {{ 'calculated-fields.propagate-type.arguments-only' | translate }} + {{ 'calculated-fields.propagate-type.expression-result' | translate }} + +
+ +
+
+
{{ 'calculated-fields.script' | translate }}
+
+ +
{{ 'api-usage.tbel' | translate }} +
+ +
+
+ +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts new file mode 100644 index 0000000000..0db1c21c3b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -0,0 +1,191 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of } from 'rxjs'; +import { + calculatedFieldDefaultScript, + CalculatedFieldOutput, + CalculatedFieldPropagationConfiguration, + CalculatedFieldType, + defaultCalculatedFieldOutput, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, + OutputType, + PropagationDirectionTranslations, + PropagationWithExpression +} from '@shared/models/calculated-field.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@app/shared/models/rule-node.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import {getCurrentAuthState} from "@core/auth/auth.selectors"; + +@Component({ + selector: 'tb-propagation-configuration', + templateUrl: './propagation-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + } + ], + standalone: false +}) +export class PropagationConfigurationComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + ownerId: EntityId; + + @Input({required: true}) + testScript: () => Observable; + + @Input({transform: booleanAttribute}) isEditValue = true; + + readonly maxRelatedEntitiesToReturnPerCfArgument = getCurrentAuthState(this.store).maxRelatedEntitiesToReturnPerCfArgument; + + propagateConfiguration = this.fb.group({ + arguments: this.fb.control({}, notEmptyObjectValidator()), + applyExpressionToResolvedArguments: [false], + relation: this.fb.group({ + direction: [EntitySearchDirection.TO, Validators.required], + relationType: ['Contains', Validators.required], + }), + expression: [calculatedFieldDefaultScript], + output: this.fb.control(defaultCalculatedFieldOutput), + }); + + disabled = false; + + readonly ScriptLanguage = ScriptLanguage; + readonly CalculatedFieldType = CalculatedFieldType; + readonly OutputType = OutputType; + readonly Directions = Object.values(EntitySearchDirection) as Array; + readonly PropagationDirectionTranslations = PropagationDirectionTranslations; + + functionArgs$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) + ); + + argumentsEditorCompleter$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store) { + this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.updatedFormWithScript(); + }) + + this.propagateConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldPropagationConfiguration) => { + this.updatedModel(value); + }) + } + + validate(): ValidationErrors | null { + return this.propagateConfiguration.valid || this.propagateConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: PropagationWithExpression): void { + value.expression = value.expression ?? calculatedFieldDefaultScript; + this.propagateConfiguration.patchValue(value, {emitEvent: false}); + if (!this.disabled) { + this.updatedFormWithScript(); + } + setTimeout(() => { + this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldPropagationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.propagateConfiguration.disable({emitEvent: false}); + } else { + this.propagateConfiguration.enable({emitEvent: false}); + this.updatedFormWithScript(); + } + } + + onTestScript() { + this.testScript().subscribe((expression) => { + this.propagateConfiguration.get('expression').setValue(expression); + this.propagateConfiguration.get('expression').markAsDirty(); + }) + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedModel(value: CalculatedFieldPropagationConfiguration): void { + value.type = CalculatedFieldType.PROPAGATION; + this.propagateChange(value); + } + + private updatedFormWithScript() { + if (this.propagateConfiguration.get('applyExpressionToResolvedArguments').value) { + this.propagateConfiguration.get('expression').enable({emitEvent: false}); + } else { + this.propagateConfiguration.get('expression').disable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts new file mode 100644 index 0000000000..209f27eec1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + PropagationConfigurationComponent +} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + PropagationConfigurationComponent, + ], + exports: [ + PropagationConfigurationComponent, + ] +}) +export class PropagationConfigurationModule { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts index 62a8eb603f..476714e3cb 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -15,7 +15,4 @@ /// export * from './dialog/calculated-field-dialog.component'; -export * from './arguments-table/calculated-field-arguments-table.component'; -export * from './panel/calculated-field-argument-panel.component'; -export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './test-dialog/calculated-field-script-test-dialog.component'; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html new file mode 100644 index 0000000000..f3ac0eefd8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html @@ -0,0 +1,86 @@ + +
+
+
+ {{ 'calculated-fields.aggregation-path-related-entities' | translate }} +
+
+ + {{ 'calculated-fields.direction' | translate }} + + @for (direction of Directions; track direction) { + {{ PropagationDirectionTranslations.get(direction) | translate }} + } + + + + +
+
+
+
+ {{ 'calculated-fields.arguments' | translate }} +
+ +
+
+
+ {{ 'calculated-fields.metrics.metrics' | translate }} +
+ + + +
+ + @if (relatedAggregationConfiguration.get('output').value?.type === OutputType.Timeseries) { +
+ +
+
calculated-fields.use-latest-timestamp
+
+
+
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss new file mode 100644 index 0000000000..d012d20732 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + .simpleMode { + min-width: 0; + + .mat-slide { + overflow: hidden; + + .mdc-form-field { + width: 100%; + + .mdc-label { + min-width: 0; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts new file mode 100644 index 0000000000..907bb31abb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -0,0 +1,175 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of } from 'rxjs'; +import { + CalculatedFieldOutput, + CalculatedFieldRelatedAggregationConfiguration, + CalculatedFieldType, + defaultCalculatedFieldOutput, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, + OutputType, + PropagationDirectionTranslations +} from '@shared/models/calculated-field.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@app/shared/models/rule-node.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-related-entities-aggregation-component', + templateUrl: './related-entities-aggregation-component.component.html', + styleUrl: './related-entities-aggregation-component.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + } + ], + standalone: false +}) +export class RelatedEntitiesAggregationComponentComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + testScript: (expression?: string) => Observable; + + @Input({transform: booleanAttribute}) isEditValue = true; + + readonly ScriptLanguage = ScriptLanguage; + readonly CalculatedFieldType = CalculatedFieldType; + readonly OutputType = OutputType; + readonly Directions = Object.values(EntitySearchDirection) as Array; + readonly PropagationDirectionTranslations = PropagationDirectionTranslations; + readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; + readonly maxRelatedEntitiesToReturnPerCfArgument = getCurrentAuthState(this.store).maxRelatedEntitiesToReturnPerCfArgument; + + relatedAggregationConfiguration = this.fb.group({ + relation: this.fb.group({ + direction: [EntitySearchDirection.FROM, Validators.required], + relationType: ['Contains', Validators.required], + }), + arguments: this.fb.control({}, notEmptyObjectValidator()), + metrics: this.fb.control({}, notEmptyObjectValidator()), + deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF], + output: this.fb.control(defaultCalculatedFieldOutput), + useLatestTs: [false] + }); + + arguments$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => Object.keys(argumentsObj)) + ); + + argumentsEditorCompleter$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; + private propagateChange: (config: CalculatedFieldRelatedAggregationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store) { + + this.relatedAggregationConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldRelatedAggregationConfiguration) => { + this.updatedModel(value); + }); + this.relatedAggregationConfiguration.get('output').valueChanges.pipe( + takeUntilDestroyed(), + ).subscribe(() => { + this.toggleScopeByOutputType(); + }); + } + + validate(): ValidationErrors | null { + return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void { + this.relatedAggregationConfiguration.patchValue(value, {emitEvent: false}); + setTimeout(() => { + this.relatedAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldRelatedAggregationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.relatedAggregationConfiguration.disable({emitEvent: false}); + } else { + this.relatedAggregationConfiguration.enable({emitEvent: false}); + } + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedModel(value: CalculatedFieldRelatedAggregationConfiguration): void { + value.type = CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + value.scheduledUpdateInterval = this.minAllowedScheduledUpdateIntervalInSecForCF; + this.propagateChange(value); + } + + private toggleScopeByOutputType(): void { + if (this.relatedAggregationConfiguration.get('output').value.type === OutputType.Attribute) { + this.relatedAggregationConfiguration.get('useLatestTs').disable({emitEvent: false}); + } else { + this.relatedAggregationConfiguration.get('useLatestTs').enable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts new file mode 100644 index 0000000000..aaa7905845 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + RelatedEntitiesAggregationComponentComponent +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component'; +import { + CalculatedFieldMetricsTableModule +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + CalculatedFieldMetricsTableModule, + ], + declarations: [ + RelatedEntitiesAggregationComponentComponent, + ], + exports: [ + RelatedEntitiesAggregationComponentComponent, + ] +}) +export class RelatedEntitiesAggregationComponentModule { +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html new file mode 100644 index 0000000000..161d2dcc25 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -0,0 +1,105 @@ + +
+
+
+ {{ 'calculated-fields.arguments' | translate }} +
+ +
+
+
+ {{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }} +
+ + +
+
+ @if (simpleConfiguration.get('expressionSIMPLE').errors && simpleConfiguration.get('expressionSIMPLE').touched) { + + @if (simpleConfiguration.get('expressionSIMPLE').hasError('required')) { + {{ 'calculated-fields.hint.expression-required' | translate }} + } @else if (simpleConfiguration.get('expressionSIMPLE').hasError('pattern')) { + {{ 'calculated-fields.hint.expression-invalid' | translate }} + } @else if (simpleConfiguration.get('expressionSIMPLE').hasError('maxLength')) { + {{ 'calculated-fields.hint.expression-max-length' | translate }} + } + + } @else { + {{ 'calculated-fields.hint.expression' | translate }} + } +
+
+ +
{{ 'api-usage.tbel' | translate }} +
+ +
+
+ +
+
+
+ +
+ +
+ calculated-fields.use-latest-timestamp +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts new file mode 100644 index 0000000000..f2f9fbac2f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { + calculatedFieldDefaultScript, + CalculatedFieldScriptConfiguration, + CalculatedFieldSimpleConfiguration, + CalculatedFieldSimpleOutput, + CalculatedFieldType, + defaultSimpleCalculatedFieldOutput, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + OutputType +} from '@shared/models/calculated-field.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { deepClone } from '@core/utils'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable } from 'rxjs'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { map } from 'rxjs/operators'; + +type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration; + +@Component({ + selector: 'tb-simple-configuration', + templateUrl: './simple-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + } + ], + standalone: false +}) +export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { + + @Input() + isScript: boolean; + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + ownerId: EntityId; + + @Input({required: true}) + testScript: () => Observable; + + @Input({transform: booleanAttribute}) isEditValue = true; + + simpleConfiguration = this.fb.group({ + arguments: this.fb.control({}), + expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + expressionSCRIPT: [calculatedFieldDefaultScript], + output: this.fb.control(defaultSimpleCalculatedFieldOutput), + useLatestTs: [false] + }); + + readonly ScriptLanguage = ScriptLanguage; + readonly OutputType = OutputType; + + functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) + ); + + argumentsEditorCompleter$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + disabled = false; + + private propagateChange: (config: SimpeConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder) { + this.simpleConfiguration.get('output').valueChanges.pipe( + takeUntilDestroyed(), + ).subscribe(() => { + this.toggleScopeByOutputType(); + }); + + this.simpleConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value) => { + const { expressionSIMPLE, expressionSCRIPT, ...config } = value; + const cfConfig = config as SimpeConfiguration; + cfConfig.expression = this.isScript ? expressionSCRIPT : expressionSIMPLE; + this.updatedModel(cfConfig); + }) + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'isScript' && !this.disabled) { + this.updatedFormWithScript(); + if (!change.firstChange) { + this.simpleConfiguration.updateValueAndValidity(); + } + } + } + } + } + + validate(): ValidationErrors | null { + return this.simpleConfiguration.valid || this.simpleConfiguration.disabled ? null : {invalidSimpleConfig: false}; + } + + writeValue(value: SimpeConfiguration): void { + const formValue: any = deepClone(value); + if (this.isScript) { + formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript; + } else if (value.type === CalculatedFieldType.SIMPLE) { + formValue.expressionSIMPLE = formValue.expression; + } + this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); + setTimeout(() => { + this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + this.simpleConfiguration.get('output').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: SimpeConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.simpleConfiguration.disable({emitEvent: false}); + } else { + this.simpleConfiguration.enable({emitEvent: false}); + this.updatedFormWithScript(); + } + } + + onTestScript() { + this.testScript().subscribe((expression) => { + this.simpleConfiguration.get('expressionSCRIPT').setValue(expression); + this.simpleConfiguration.get('expressionSCRIPT').markAsDirty(); + }) + } + + private updatedModel(value: SimpeConfiguration): void { + value.type = this.isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE; + this.propagateChange(value); + } + + private updatedFormWithScript() { + if (this.isScript) { + this.simpleConfiguration.get('expressionSIMPLE').disable({emitEvent: false}); + this.simpleConfiguration.get('expressionSCRIPT').enable({emitEvent: false}); + } else { + this.simpleConfiguration.get('expressionSIMPLE').enable({emitEvent: false}); + this.simpleConfiguration.get('expressionSCRIPT').disable({emitEvent: false}); + } + this.toggleScopeByOutputType(); + } + + private toggleScopeByOutputType(): void { + if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute || this.disabled) { + this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false}); + } else { + this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts new file mode 100644 index 0000000000..11c6899ae6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + SimpleConfigurationComponent +} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.component'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + SimpleConfigurationComponent, + ], + exports: [ + SimpleConfigurationComponent + ] +}) +export class SimpleConfigurationModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html new file mode 100644 index 0000000000..79062b1a0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html @@ -0,0 +1,138 @@ + +@if (panelMode) { + +} @else { + @if (buttonMode) { + + } @else { +
+
+
calculated-fields.calculated-field-types
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (cfFilterForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+ } +} + +
+ +
+ + + + +
+
+
+ +
+
+
calculated-fields.calculated-field-types
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (cfFilterForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss new file mode 100644 index 0000000000..beb6c4a343 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + max-width: 100%; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +::ng-deep { + .tb-calculated-fields-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts new file mode 100644 index 0000000000..36095579e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts @@ -0,0 +1,295 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + DestroyRef, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { deepClone, isArraysEqualIgnoreUndefined, isDefinedAndNotNull, isEmpty, isUndefinedOrNull } from '@core/utils'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { fromEvent, Subscription } from 'rxjs'; +import { POSITION_MAP } from '@shared/models/overlay.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + calculatedFieldsEntityTypeList, + CalculatedFieldsQuery, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { TranslateService } from '@ngx-translate/core'; + +export const CALCULATED_FIELDS_CONFIG_DATA = new InjectionToken('CalculatedFieldsFilterConfigData'); + +export interface CalculatedFieldsFilterConfigData { + panelMode: boolean; + userMode: boolean; + filterConfig: CalculatedFieldsQuery; + initialFilterConfig?: CalculatedFieldsQuery; +} + +@Component({ + selector: 'tb-calculated-fields-filter-config', + templateUrl: './calculated-fields-filter-config.component.html', + styleUrls: ['./calculated-fields-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldsFilterConfigComponent), + multi: true + } + ], + standalone: false +}) +export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlValueAccessor { + + @ViewChild('calculatedFieldsFilterPanel') + calculatedFieldsFilterPanel: TemplateRef; + + @Input() + disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @Input() + initialCfFilterConfig: CalculatedFieldsQuery = { + types: [], + entityType: null, + entities: [] + }; + + panelMode = false; + + buttonDisplayValue = this.translate.instant('calculated-fields.calculated-field-filter-title'); + + cfFilterForm: FormGroup; + + panelResult: CalculatedFieldsQuery = null; + + entityType = EntityType; + + listEntityTypes = calculatedFieldsEntityTypeList; + entityTypeTranslations = entityTypeTranslations; + + readonly types: StringItemsOption[] = calculatedFieldTypes.map(item => ({ + name: this.translate.instant(CalculatedFieldTypeTranslations.get(item).name), + value: item + })); + + private cfFilterOverlayRef: OverlayRef; + private cfFilterConfig: CalculatedFieldsQuery; + private resizeWindows: Subscription; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(CALCULATED_FIELDS_CONFIG_DATA) + private data: CalculatedFieldsFilterConfigData | undefined, + @Optional() private overlayRef: OverlayRef, + private fb: FormBuilder, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef, + private translate: TranslateService) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.cfFilterConfig = this.data.filterConfig; + this.initialCfFilterConfig = this.data.initialFilterConfig; + if (this.panelMode && !this.initialCfFilterConfig) { + this.initialCfFilterConfig = deepClone(this.cfFilterConfig); + } + } + this.cfFilterForm = this.fb.group({ + types: [null, []], + entityType: [null, []], + entities: [null, []] + }); + this.cfFilterForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + if (!this.buttonMode) { + this.cfConfigUpdated(this.cfFilterForm.value); + } + } + ); + if (this.panelMode) { + this.updateCfConfigForm(this.cfFilterConfig); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.cfFilterForm.disable({emitEvent: false}); + } else { + this.cfFilterForm.enable({emitEvent: false}); + } + } + + writeValue(cfFilterConfig?: CalculatedFieldsQuery): void { + this.cfFilterConfig = cfFilterConfig; + if (!this.initialCfFilterConfig && cfFilterConfig) { + this.initialCfFilterConfig = deepClone(cfFilterConfig); + } + this.updateButtonDisplayValue(); + this.updateCfConfigForm(cfFilterConfig); + } + + toggleCfFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); + + this.cfFilterOverlayRef = this.overlay.create(config); + this.cfFilterOverlayRef.backdropClick().subscribe(() => { + this.cfFilterOverlayRef.dispose(); + }); + this.cfFilterOverlayRef.attach(new TemplatePortal(this.calculatedFieldsFilterPanel, + this.viewContainerRef)); + this.resizeWindows = fromEvent(window, 'resize').subscribe(() => { + this.cfFilterOverlayRef.updatePosition(); + }); + } + + cancel() { + this.updateCfConfigForm(this.cfFilterConfig); + this.cfFilterForm.markAsPristine(); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + update() { + this.cfConfigUpdated(this.cfFilterForm.value); + this.cfFilterForm.markAsPristine(); + if (this.panelMode) { + this.panelResult = this.cfFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + reset() { + const cfFilterConfig = this.cfFilterFromFormValue(this.cfFilterForm.value); + if (!this.cfFilterConfigEquals(cfFilterConfig, this.initialCfFilterConfig)) { + this.updateCfConfigForm(this.initialCfFilterConfig); + this.cfFilterForm.markAsDirty(); + } + } + + private cfFilterConfigEquals = (filter1?: CalculatedFieldsQuery, filter2?: CalculatedFieldsQuery): boolean => { + if (filter1 === filter2) { + return true; + } + if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) { + return true; + } else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) { + if (!isArraysEqualIgnoreUndefined(filter1.types, filter2.types)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) { + return false; + } + return filter1.entityType === filter2.entityType; + } + return false; + }; + + private updateCfConfigForm(cfFilterConfig?: CalculatedFieldsQuery) { + this.cfFilterForm.patchValue({ + types: cfFilterConfig?.types ?? [], + entityType: cfFilterConfig?.entityType ?? null, + entities: cfFilterConfig?.entities ?? [], + }, {emitEvent: false}); + } + + private cfConfigUpdated(formValue: any) { + this.cfFilterConfig = this.cfFilterFromFormValue(formValue); + this.updateButtonDisplayValue(); + this.propagateChange(this.cfFilterConfig); + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (this.cfFilterConfig?.types?.length) { + filterTextParts.push(this.cfFilterConfig.types.map((type) => this.translate.instant(CalculatedFieldTypeTranslations.get(type).name)).join(', ')); + } + if (this.cfFilterConfig?.entityType) { + filterTextParts.push(this.translate.instant( entityTypeTranslations.get(this.cfFilterConfig.entityType).type)); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('calculated-fields.calculated-field-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('calculated-fields.filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + + private cfFilterFromFormValue(formValue: any): CalculatedFieldsQuery { + return { + types: formValue?.types ?? [], + entityType: formValue?.entityType ?? null, + entities: formValue?.entities ?? [], + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html new file mode 100644 index 0000000000..f24c112f27 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html @@ -0,0 +1,20 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss new file mode 100644 index 0000000000..c6571e8450 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + padding-right: 8px; + overflow: hidden; + max-width: 100%; +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts new file mode 100644 index 0000000000..07ed7deeda --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 '@home/components/entity/entity-table-header.component'; +import { CalculatedField, CalculatedFieldsQuery } from "@shared/models/calculated-field.models"; +import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; + +@Component({ + selector: 'tb-calculated-fields-table-header', + templateUrl: './calculated-fields-header.component.html', + styleUrls: ['./calculated-fields-header.component.scss'], + standalone: false +}) +export class CalculatedFieldsHeaderComponent extends EntityTableHeaderComponent { + + get calculatedFieldsTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfig as CalculatedFieldsTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + calculatedFieldsFilterChanged(calculatedFieldFilterConfig: CalculatedFieldsQuery) { + this.calculatedFieldsTableConfig.calculatedFieldFilterConfig = calculatedFieldFilterConfig; + this.calculatedFieldsTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index 0e1b2a7e07..ebd73b5388 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -21,11 +21,13 @@

{{'widget.add' | translate}}:

{{data.widgetInfo.widgetName}}
- - {{ 'widget.basic-mode' | translate }} - {{ 'widget.advanced-mode' | translate }} - + @if (widgetConfig.hasBasicMode) { + + {{ 'widget.basic-mode' | translate }} + {{ 'widget.advanced-mode' | translate }} + + }
@@ -49,22 +51,24 @@ isAdd formControlName="widgetConfig"> - -
- -
-
+ @if (previewMode) { + +
+ +
+
+ }
+ @if (!previewMode) { + + } - - + @if (isLoading$ | async) { + + + }
@@ -40,8 +42,11 @@
-
{{ 'dashboard.no-image' | translate }}
- + @if (!imageUrl) { +
{{ 'dashboard.no-image' | translate }}
+ } @else { + + }
Right % @@ -63,13 +68,15 @@
+ formControlName="dashboardImage">
-
+ @if (takingScreenshot$ | async) { +
-
+
+ }
- - + } + @if (showDashboardLogo()) { + + }
- - + @if (!isPublicUser() && forceFullscreen) { + + + } - + @if (!readonly && !isEdit) { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
- - -
-

{{ translatedDashboardTitle }}

- - dashboard.title - - -
- - - - - - - - -
- {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }} -
-
-
-
- - - -
- -
- - -
- - {{ 'widget.basic-mode' | translate }} - {{ 'widget.advanced-mode' | translate }} - -
-
- + edit{{'dashboard.edit-mode' | translate}} + } + @if (isEdit) { + + + + + + } + @if (currentDashboardId && isEdit && isTenantAdmin()) { + + + } + @if (isEdit) { + + + } + @if (isEdit) { + + + } + @if (isEdit) { + + + } + + @if (isEdit) { + + + + + } @else { + @if (displayDashboardTimewindow()) { + > + + > + + } + } + @if (isEdit) { + + + } + + + + + @if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) { + + + } +
+
+ + +
+ + + @if (!widgetEditMode) { +
+

{{ translatedDashboardTitle }}

+ + dashboard.title + + +
+ } + + @if (layouts.right.show) { + + + + + } + + - - - + + @if (isEdit) { +
+ {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }} +
+ } +
+
+ @if (!readonly && (hideToolbar || widgetEditMode)) { +
+ + + +
+ } + @if (!embedded) { + + } +
+ + @if (!isEditingWidgetClosed) { + +
+ @if (tbEditWidget.widgetConfig?.hasBasicMode) { + + {{ 'widget.basic-mode' | translate }} + {{ 'widget.advanced-mode' | translate }} + + } +
+
+ + +
+ } + @if (!isAddingWidgetClosed && !widgetEditMode) { + -
- -
-
- +
+ +
+ @if (isAddingWidget && dashboardWidgetSelectComponent) { +
+ - -
-
- - - {{ 'widgets-bundle.widgets-bundles' | translate }} - {{ 'widget.all-widgets' | translate }} - - - - {{ 'widget.all' | translate }} - {{ 'widget.actual' | translate }} - {{ 'widget.deprecated' | translate }} - -
- - - - - -
-
+ +
+ } + @if (isAddingWidget) { +
+ @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) { + + } + @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) { + + {{ 'widgets-bundle.widgets-bundles' | translate }} + {{ 'widget.all-widgets' | translate }} + + } + @if (dashboardWidgetSelectComponent?.widgetTypes.size > 1) { + + } + @if (dashboardWidgetSelectComponent?.hasDeprecated) { + + {{ 'widget.all' | translate }} + {{ 'widget.actual' | translate }} + {{ 'widget.deprecated' | translate }} + + } +
+ } + @if (isAddingWidget) { + + + } + + } + + + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 024282e501..3948af744f 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -39,7 +39,7 @@ import { 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 { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { UtilsService } from '@core/services/utils.service'; import { BreakpointId, @@ -274,6 +274,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC {width: '100%', height: '100%', maxWidth: '100%', minWidth: '100%'}; rightLayoutSize: {width: string; height: string} = {width: '100%', height: '100%'}; + dashboardLogoLink = this.getDashboardLogoLink(); + private dashboardLogoCache: SafeUrl; private defaultDashboardLogo = 'assets/logo_title_white.svg'; @@ -548,6 +550,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC const cssParser = new cssjs(); cssParser.testMode = false; this.dashboardPageClass = 'tb-dashboard-page-css-' + guid(); + this.dashboardCtx.dashboardCssClass = this.dashboardPageClass; cssParser.cssPreviewNamespace = 'tb-default .' + this.dashboardPageClass; cssParser.createStyleElement(this.dashboardPageClass, cssString); } @@ -1244,7 +1247,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC widgetEditMode: this.widgetEditMode, singlePageMode: this.singlePageMode }; + const needReInitState = !this.isEdit; this.init(dashboardPageInitData); + if (needReInitState) { + this.dashboardCtx.stateController.reInit(); + } } else { this.dashboard.version = dashboard.version; this.setEditMode(false, false); @@ -1325,6 +1332,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } private addWidgetToDashboard(widget: Widget) { + this.dashboardUtils.prepareWidgetForSaving(widget); if (this.addingLayoutCtx) { this.addWidgetToLayout(widget, this.addingLayoutCtx.id); this.addingLayoutCtx = null; @@ -1412,7 +1420,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC saveWidget() { this.editWidgetComponent.widgetFormGroup.markAsPristine(); - const widget = deepClone(this.editingWidget); + const widget = this.dashboardUtils.prepareWidgetForSaving(deepClone(this.editingWidget)); const widgetLayout = deepClone(this.editingWidgetLayout); const id = this.editingWidgetOriginal.id; this.dashboardConfiguration.widgets[id] = widget; @@ -1827,4 +1835,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } return false; } + + private getDashboardLogoLink(): UrlTree { + return this.forceFullscreen ? null : this.router.createUrlTree([], {relativeTo: this.route}); + } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts index af45b01714..5531f4c346 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts @@ -50,6 +50,7 @@ export interface DashboardContext { stateChanged: Observable; stateId: Observable; runChangeDetection: () => void; + dashboardCssClass?: string; } export interface IDashboardController { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index 70be4a9e55..7fb0f3f68d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -25,224 +25,258 @@ close - - + @if (isLoading$ | async) { + + + }
-
-
-
{{ 'dashboard.state-controller' | translate }}
- - - - {{ getStatesControllerTranslation(stateControllerId) }} - - - -
-
-
dashboard.title-settings
+ @if (settings) { +
- - {{ 'dashboard.title' | translate }} - - - -
-
-
-
dashboard.dashboard-logo-settings
-
- - {{ 'dashboard.display-dashboard-logo' | translate }} - -
- - -
-
-
dashboard.toolbar-settings
- - {{ 'dashboard.hide-toolbar' | translate }} - - - {{ 'dashboard.toolbar-always-open' | 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.display-update-dashboard-image' | translate }} - -
- - - - dashboard.advanced-settings - - - - - - -
-
-
-
dashboard.layout-settings-type
-
-
dashboard.view-format
+
{{ 'dashboard.state-controller' | translate }}
- - - {{ viewFormatTypeTranslationMap.get(type) | translate }} - + + @for (stateControllerId of stateControllerIds; track stateControllerId) { + + {{ getStatesControllerTranslation(stateControllerId) }} + + }
-
-
dashboard.columns-count
- +
dashboard.title-settings
+
+ + {{ 'dashboard.title' | translate }} + + + +
+
+
+
dashboard.dashboard-logo-settings
+
+ + {{ 'dashboard.display-dashboard-logo' | translate }} + +
+ + +
+
+
dashboard.toolbar-settings
+ + {{ 'dashboard.hide-toolbar' | translate }} + + + {{ 'dashboard.toolbar-always-open' | 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.display-update-dashboard-image' | translate }} + +
+ + + + dashboard.advanced-settings + + + + + + +
+ } + @if (gridSettings) { +
+
+
dashboard.layout-settings-type
+ @if (isDefaultLayout && !isDefaultBreakpoint) { +
+
dashboard.view-format
+ + + @for (type of viewFormatTypes; track type) { + + {{ viewFormatTypeTranslationMap.get(type) | translate }} + + } + + +
+ } +
+
dashboard.columns-count
+ @if (!isScada()) { + - - - - - - {{ columns }} - - - -
-
-
dashboard.min-layout-width
- + + } + @if (isScada()) { + + + @for (columns of scadaColumns; track columns) { + + {{ columns }} + + } + + + } +
+
+
dashboard.min-layout-width
+ - - dashboard.columns-suffix - -
-
-
dashboard.widgets-margins
- + dashboard.columns-suffix + +
+ @if (!isScada()) { +
+
dashboard.widgets-margins
+ - - -
-
- - {{ 'dashboard.apply-outer-margin' | translate }} - -
-
- - {{ 'dashboard.autofill-height' | translate }} - -
-
-
dashboard.row-height
- + +
+ } + @if (!isScada()) { +
+ + {{ 'dashboard.apply-outer-margin' | translate }} + +
+ } + @if (!isScada()) { +
+ + {{ 'dashboard.autofill-height' | translate }} + +
+ } + @if (isDefaultLayout && !isDefaultBreakpoint) { +
+
dashboard.row-height
+ - - px - -
-
-
dashboard.background-color
- - + + px + +
+ } +
+
dashboard.background-color
+ + +
+ + +
+
{{ 'dashboard.background-size-mode' | translate }}
+ + + Fit width + Fit height + Cover + Contain + Original size + + +
- - -
-
{{ 'dashboard.background-size-mode' | translate }}
- - - Fit width - Fit height - Cover - Contain - Original size - - -
-
-
-
dashboard.mobile-layout
-
- - {{ 'dashboard.display-first-in-mobile-view' | translate }} - -
-
- - {{ 'dashboard.autofill-height' | translate }} - -
-
-
dashboard.mobile-row-height
- +
dashboard.mobile-layout
+ @if (isRightLayout) { +
+ + {{ 'dashboard.display-first-in-mobile-view' | translate }} + +
+ } + @if (!isScada()) { +
+ + {{ 'dashboard.autofill-height' | translate }} + +
+ } + @if (!isScada()) { +
+
dashboard.mobile-row-height
+ - - px - -
+ + px +
+
+ } +
+ }
-
+ }
-
widget.deprecated
+ @if (item.deprecated) { +
widget.deprecated
+ }
@@ -64,51 +107,10 @@
- {{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}} + {{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}} - - - - - -
-
- {{item.title}} -
-
-
sys
-
i
-
-
-
-
- {{ item.title }} -
-
-
- -
- - {{ 'widgets-bundle.loading-widgets-bundles' | translate }} - - -
-
- - widgets-bundle.no-widgets-bundles-text - -
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html index 9b8e45c6dd..cc53c50fa9 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html @@ -28,56 +28,63 @@ [isDefaultBreakpoint]="isDefaultBreakpoint" formControlName="widgetConfig">
- - + @if (previewMode) { + + } + @if (!previewMode) { + + }
- + @if (!previewMode) { + + }
-
- - -
+ @if (previewMode) { +
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html index 230d930b05..14656f6b7c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html @@ -30,22 +30,26 @@ layout.breakpoint {{ getName(addBreakpointFormGroup.get('newBreakpointId').value) }} - - {{ getIcon(breakpoint) }} -
{{ getName(breakpoint) }}
-
{{ getSizeDescription(breakpoint) }}
-
+ @for (breakpoint of allowBreakpointIds; track breakpoint) { + + {{ getIcon(breakpoint) }} +
{{ getName(breakpoint) }}
+
{{ getSizeDescription(breakpoint) }}
+
+ }
layout.copy-from {{ getName(addBreakpointFormGroup.get('copyFrom').value) }} - - {{ getIcon(breakpoint) }} -
{{ getName(breakpoint) }}
-
{{ getSizeDescription(breakpoint) }}
-
+ @for (breakpoint of selectedBreakpointIds; track breakpoint) { + + {{ getIcon(breakpoint) }} +
{{ getName(breakpoint) }}
+
{{ getSizeDescription(breakpoint) }}
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html index ef892d7ac1..e07be60cb5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html @@ -15,28 +15,36 @@ limitations under the License. --> -
- - -
+@if (layoutCtx.widgets.isLoading()) { +
+ + +
+}
-
- - {{'dashboard.no-widgets' | translate}} - - -
+ style="position: relative; width: 100%; height: 100%;" [tb-hotkeys]="hotKeys" + [cheatSheet]="dashboardCheatSheet" + [style.background-image]="backgroundImage$ | async" + [style]="dashboardStyle"> + @if (layoutCtx.widgets.isEmpty()) { +
+ @if (!isEdit) { + + {{'dashboard.no-widgets' | translate}} + + } + @if (isEdit && !widgetEditMode) { + + } +
+ } layout.manage @@ -31,201 +31,226 @@
dashboard.layout
- - {{ layoutTypeTranslations.get(type) | translate }} - + @for (type of layoutTypes; track type) { + + {{ layoutTypeTranslations.get(type) | translate }} + + }
-
-
-
-
-
layout.breakpoints
-
layout.size
-
-
-
- -
-
- {{ breakpoint.icon }} -
-
- {{ breakpoint.name }} -
-
- {{ breakpoint.descriptionSize }} + @if (!isDividerLayout) { +
+
+
+
+
layout.breakpoints
+
layout.size
+
+
+
+ @for (breakpoint of layoutBreakpoints; track breakpoint; let last = $last) { +
+
+ {{ breakpoint.icon }} +
+
+ {{ breakpoint.name }} +
+
+ {{ breakpoint.descriptionSize }} +
+
+ + +
-
+ @if (!last) { + + } + } +
+
+ +
+ } @else { +
+ + + {{ 'layout.percentage-width' | translate }} + + + {{ 'layout.fixed-width' | translate }} + + +
+
+
+ + + @if (showPreviewInputs('main')) { +
+ @if (layoutsFormGroup.get('type').value !== layoutWidthType.FIXED) { + + } + @if (layoutsFormGroup.get('type').value === layoutWidthType.FIXED && + layoutsFormGroup.get('fixedLayout').value === 'main') { + + } +
+ } +
+ @if (isDividerLayout) { +
- + @if (showPreviewInputs('right')) { +
+ @if (layoutsFormGroup.get('type').value !== layoutWidthType.FIXED) { + + } + @if (layoutsFormGroup.get('type').value === layoutWidthType.FIXED) { + + } +
+ }
-
- - -
-
- -
- - -
- - - {{ 'layout.percentage-width' | translate }} - - - {{ 'layout.fixed-width' | translate }} - - -
-
-
- - -
- - + }
-
- - -
- - + @if (isDividerLayout) { +
+ @if (layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE) { + + + } + @if (layoutsFormGroup.get('type').value !== layoutWidthType.PERCENTAGE) { + + + } +
+ + +
-
-
-
-
- - - - -
- - + }
-
-
- + } +
- - + @if (isLoading$ | async) { + + + }
dashboard.move-by
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html index d9e98fc48a..312cf304cd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html @@ -21,9 +21,11 @@ [(ngModel)]="selectedBreakpoint" (ngModelChange)="selectLayoutChanged()"> {{ getName(selectedBreakpoint) }} - - {{ getIcon(breakpointId) }} -
{{ getName(breakpointId) }}
-
{{ getSizeDescription(breakpointId) }}
-
+ @for (breakpointId of breakpointIds; track breakpointId) { + + {{ getIcon(breakpointId) }} +
{{ getName(breakpointId) }}
+
{{ getSizeDescription(breakpointId) }}
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html index c06d184162..58cb21253d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html @@ -25,26 +25,34 @@ close - - + @if (isLoading$ | async) { + + + }
- + dashboard.state-name - - {{ 'dashboard.state-name-required' | translate }} - + @if (stateFormGroup.get('name').hasError('required')) { + + {{ 'dashboard.state-name-required' | translate }} + + } - + dashboard.state-id - - {{ 'dashboard.state-id-required' | translate }} - - - {{ 'dashboard.state-id-exists' | translate }} - + @if (stateFormGroup.get('id').hasError('required')) { + + {{ 'dashboard.state-id-required' | translate }} + + } + @if (stateFormGroup.get('id').hasError('stateExists')) { + + {{ 'dashboard.state-id-exists' | translate }} + + } {{ 'dashboard.is-root-state' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html index fcf77163c9..a84fb44d26 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html @@ -16,8 +16,10 @@ --> - - {{getStateName(stateKv.key, stateKv.value)}} - + [(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()"> + @for (stateKv of states | keyvalue; track stateKv) { + + {{getStateName(stateKv.key, stateKv.value)}} + + } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html index 394bf14091..a932cd145e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html @@ -16,20 +16,28 @@ -->
-
- + @for (state of stateObject; track state; let i = $index; let isLast = $last) { + - {{getStateName(i)}} + {{getStateName(i)}} > -
- - - {{getStateName(i)}} - - + } +
+ } + @if (isMobile && stateObject.length > 1) { + + @for (state of stateObject; track state; let i = $index) { + + {{getStateName(i)}} + + } + + }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts index a142ae36ce..1ffffcc90e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts @@ -15,7 +15,6 @@ /// import { IStateController, StateObject } from '@core/api/widget-api.models'; -import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { DashboardState } from '@shared/models/dashboard.models'; export declare type StateControllerState = StateObject[]; @@ -29,6 +28,5 @@ export interface IStateControllerComponent extends IStateController { states: {[id: string]: DashboardState }; dashboardId: string; preservedState: any; - reInit(): void; init(): void; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html index 51eebbf7ed..f26be70987 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html @@ -16,8 +16,10 @@ -->
- - {{ 'widget.' + type.type | translate }} - + @for (type of types; track type) { + + {{ 'widget.' + type.type | translate }} + + }
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 index ef9d754ebb..5ba7c784b3 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -24,10 +24,10 @@
+ [class.center-vertical]="centerVertical" + [class.center-horizontal]="centerHorizontal" + (mousedown)="onDashboardMouseDown($event)" + (tbcontextmenu)="openDashboardContextMenu($event)">
- + @for (item of items; track item) { + + }
@@ -54,37 +60,45 @@
- + @for (item of items; track item) { + + }
- - - - + @for (widget of dashboardWidgets; track widget) { + + + + + }
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 index 791aac883a..5288a45fd0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -34,7 +34,7 @@ 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 { initModelFromDefaultTimewindow, Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; import { TimeService } from '@core/services/time.service'; import { GridsterComponent, GridsterConfig, GridType } from 'angular-gridster2'; import { @@ -223,9 +223,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo ngOnInit(): void { this.dashboardWidgets.parentDashboard = this.parentDashboard; this.dashboardWidgets.popoverComponent = this.popoverComponent; - if (!this.dashboardTimewindow) { - this.dashboardTimewindow = this.timeService.defaultTimewindow(); - } + this.dashboardTimewindow = initModelFromDefaultTimewindow(this.dashboardTimewindow, + false, false, this.timeService, true, true); this.gridsterOpts = { gridType: this.gridType || GridType.ScrollVertical, keepFixedHeightInMobile: true, 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 index c7524428e3..bf89db1b8d 100644 --- 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 @@ -20,13 +20,15 @@

layout.select

- - + @if (isLoading$ | async) { + + + }
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 index a1ae9ba969..fdd4b84397 100644 --- 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 @@ -25,16 +25,20 @@ close - - + @if (isLoading$ | async) { + + + }
dashboard.state - - {{ stateItem.value.name }} - + @for (stateItem of states | keyvalue; track stateItem) { + + {{ stateItem.value.name }} + + }
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 index db9caf2cf3..eebb9a6db7 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -28,41 +28,49 @@
-
+ @if (isShowSearch) { +
+ +
+ +
+
+ } + @if (isShowSearch && !showSearchPane) { -
- -
-
- + } - + @if (showCloseDetails) { + + }
-
- - -
+ @if (!isReadOnly) { +
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html index 4aa7c5e2e4..27db006a0f 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html @@ -16,16 +16,18 @@ -->
- + device.lwm2m-security-config.mode - - {{ lwm2mSecurityTypeTranslationMap.get(securityType) }} - + @for (securityType of securityConfigLwM2MTypes; track securityType) { + + {{ lwm2mSecurityTypeTranslationMap.get(securityType) }} + + }
- + {{ 'device.lwm2m-security-config.client-publicKey-or-id' | translate }} help @@ -36,11 +38,11 @@ formControlName="clientPublicKeyOrId" required> - - {{ 'device.lwm2m-security-config.client-publicKey-or-id-required' | translate }} - + @if (serverFormGroup.get('clientPublicKeyOrId').hasError('required')) { + {{ 'device.lwm2m-security-config.client-publicKey-or-id-required' | translate }} + } - + {{ 'device.lwm2m-security-config.client-secret-key' | translate }} help @@ -51,9 +53,9 @@ formControlName="clientSecretKey" required> - - {{ 'device.lwm2m-security-config.client-secret-key-required' | translate }} - + @if (serverFormGroup.get('clientSecretKey').hasError('required')) { + {{ 'device.lwm2m-security-config.client-secret-key-required' | translate }} + }
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html index 150361a876..f45ae6f38c 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html @@ -15,63 +15,78 @@ limitations under the License. --> - + - + device.lwm2m-security-config.endpoint - - {{ 'device.lwm2m-security-config.endpoint-required' | translate }} - + @if (lwm2mConfigFormGroup.get('client.endpoint').hasError('required')) { + {{ 'device.lwm2m-security-config.endpoint-required' | translate }} + } - + device.lwm2m-security-config.mode - - {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityConfigClientMode]) }} - + @for (securityConfigClientMode of securityConfigLwM2MTypes; track securityConfigClientMode) { + + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityConfigClientMode]) }} + + } - - {{ 'device.lwm2m-security-config.identity' | translate }} - help - - - {{ 'device.lwm2m-security-config.identity-required' | translate }} - - - - {{ 'device.lwm2m-security-config.client-key' | translate }} - help - - - {{ 'device.lwm2m-security-config.client-key-required' | translate }} - - - - device.lwm2m-security-config.client-public-key - help - - device.lwm2m-security-config.client-public-key-hint - + @if (lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.PSK) { + + {{ 'device.lwm2m-security-config.identity' | translate }} + help + + @if (lwm2mConfigFormGroup.get('client.identity').hasError('required')) { + {{ 'device.lwm2m-security-config.identity-required' | translate }} + } + + } + @if (lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.RPK || + lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.PSK) { + + {{ 'device.lwm2m-security-config.client-key' | translate }} + help + + @if (lwm2mConfigFormGroup.get('client.key').hasError('required')) { + {{ 'device.lwm2m-security-config.client-key-required' | translate }} + } + + } + @if (lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.X509) { + + device.lwm2m-security-config.client-public-key + @if (lwm2mConfigFormGroup.get('client.cert').value) { + help + } + + device.lwm2m-security-config.client-public-key-hint + + } + @if (deviceId) { + + } @@ -103,5 +118,12 @@
+ @if (deviceId) { + + } diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss index cef379e280..13dd61a4e7 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss @@ -27,4 +27,7 @@ .mat-mdc-tab-body { padding: 16px 0; } + .mat-mdc-tab-body-content { + padding-top: 4px; + } } diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts index 3daa094f8f..3800f2deb8 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts @@ -14,20 +14,21 @@ /// limitations under the License. /// -import { Component, forwardRef, OnDestroy } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy } from '@angular/core'; import { ControlValueAccessor, - UntypedFormBuilder, - UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, ValidationErrors, Validator, Validators } from '@angular/forms'; import { getDefaultClientSecurityConfig, - getDefaultServerSecurityConfig, Lwm2mClientKeyTooltipTranslationsMap, + getDefaultServerSecurityConfig, + Lwm2mClientKeyTooltipTranslationsMap, Lwm2mSecurityConfigModels, Lwm2mSecurityType, Lwm2mSecurityTypeTranslationMap @@ -35,6 +36,11 @@ import { import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { isDefinedAndNotNull } from '@core/utils'; +import { DeviceId } from "@shared/models/id/device-id"; +import { DeviceService } from "@core/http/device.service"; +import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { Store } from "@ngrx/store"; +import { AppState } from "@core/core.state"; @Component({ selector: 'tb-device-credentials-lwm2m', @@ -66,7 +72,12 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va private destroy$ = new Subject(); private propagateChange = (v: any) => {}; - constructor(private fb: UntypedFormBuilder) { + @Input() + deviceId: DeviceId; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private deviceService: DeviceService) { this.lwm2mConfigFormGroup = this.initLwm2mConfigForm(); } @@ -102,6 +113,41 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va this.destroy$.complete(); } + /** + * AbstractRpcController -> rpcController + * - API + * "/api/plugins/rpc/twoway/${this.deviceId.id}" + * - DiscoveryAll + * requestBody = "{\"method\":\"DiscoverAll\"}"; + * - "Registration Update Trigger", + * requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/8\"}} + * - "Bootstrap-Request Trigger" + * requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/9\"}} + */ + + public rebootDevice(isBootstrapServer: boolean): void { + this.deviceService.rebootDevice(this.deviceId.id, isBootstrapServer).subscribe(responseReboot => { + if (responseReboot.result === 'SUCCESS') { + this.store.dispatch(new ActionNotificationShow( + { + message: responseReboot.msg, + type: 'success', + duration: 1500, + verticalPosition: 'top', + horizontalPosition: 'left' + })); + } else { + this.store.dispatch(new ActionNotificationShow( + { + message: responseReboot.msg, + type: 'error', + verticalPosition: 'top', + horizontalPosition: 'left' + })); + } + }); + } + private initClientSecurityConfig(config: Lwm2mSecurityConfigModels): void { this.lwm2mConfigFormGroup.patchValue(config, {emitEvent: false}); this.securityConfigClientUpdateValidators(config.client.securityConfigClientMode); diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html index f36eaa4565..deaaee3063 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html @@ -16,90 +16,87 @@ -->
- + device.client-id - - - - - - - {{ 'device.client-id-pattern' | translate }} - + + @if (!deviceCredentialsMqttFormGroup.get('clientId').value && !disabled) { + + } @else if (deviceCredentialsMqttFormGroup.get('clientId').value) { + + + } + + @if (deviceCredentialsMqttFormGroup.get('clientId').hasError('pattern')) { + {{ 'device.client-id-pattern' | translate }} + } - + device.user-name - - - - - - - {{ 'device.user-name-required' | translate }} - + + @if (!deviceCredentialsMqttFormGroup.get('userName').value && !disabled) { + + } @else if (deviceCredentialsMqttFormGroup.get('userName').value) { + + + } + + @if (deviceCredentialsMqttFormGroup.get('userName').hasError('required')) { + {{ 'device.user-name-required' | translate }} + } - + device.password - - - - - + + @if (!deviceCredentialsMqttFormGroup.get('password').value && !disabled) { + + } @else if (deviceCredentialsMqttFormGroup.get('password').value) { + + + } + 1)" style="margin-bottom: 16px">
device.credentials-type
- - {{ credentialTypeNamesMap.get(credentialsType) }} - + @for (credentialsType of credentialsTypes; track credentialsType) { + + {{ credentialTypeNamesMap.get(credentialsType) }} + + }
-
- - + @switch (deviceCredentialsFormGroup.get('credentialsType').value) { + @case (deviceCredentialsType.ACCESS_TOKEN) { + device.access-token - - + + @if (!deviceCredentialsFormGroup.get('credentialsId').value && !disabled) { + + } @else if (deviceCredentialsFormGroup.get('credentialsId').value) { + + + } + + @if (deviceCredentialsFormGroup.get('credentialsId').hasError('required')) { + {{ 'device.access-token-required' | translate }} + } + @if (deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')) { + {{ 'device.access-token-invalid' | translate }} + } + + } + @case (deviceCredentialsType.X509_CERTIFICATE) { + + device.certificate-pem-format + + @if (deviceCredentialsFormGroup.get('credentialsValue').value) { - - - {{ 'device.access-token-required' | translate }} - - - {{ 'device.access-token-invalid' | translate }} - - - - - - device.certificate-pem-format - - - - - {{ 'device.certificate-pem-format-required' | translate }} - + } + @if (deviceCredentialsFormGroup.get('credentialsValue').hasError('required')) { + {{ 'device.certificate-pem-format-required' | translate }} + } - - + } + @case (deviceCredentialsType.MQTT_BASIC) { - - - + } + @case (deviceCredentialsType.LWM2M_CREDENTIALS) { + - -
+ } + } 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 index 6a663b5d93..1d1838a9ea 100644 --- 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 @@ -36,6 +36,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { generateSecret, isDefinedAndNotNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { DeviceId } from "@shared/models/id/device-id"; @Component({ selector: 'tb-device-credentials', @@ -90,6 +91,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, credentialTypeNamesMap = credentialTypeNames; + deviceId: DeviceId; + private propagateChange = null; private propagateChangePending = false; @@ -128,6 +131,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, writeValue(value: DeviceCredentials | null): void { if (isDefinedAndNotNull(value)) { + this.deviceId = value.deviceId; const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0]; this.deviceCredentialsFormGroup.patchValue({ credentialsType, diff --git a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html index 68128cfd34..0029a3ba3f 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html @@ -15,11 +15,10 @@ limitations under the License. --> - +@if (panelMode) { - - - +} @else { + @if (buttonMode) { - - + } @else { +
+
+
device-profile.device-profile
+ + +
+
+
device.device-state
+ + + {{ 'device.any' | translate }} + {{ 'device.active' | translate }} + {{ 'device.inactive' | translate }} + + +
+
+ } +}
+ + - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts index e0dd1360e5..5d0ac5d5e1 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts @@ -60,6 +60,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor @Input() additionalActionConfig: AdditionalDebugActionConfig; @Input({required: true}) entityType: EntityType; + @Input() entityLabel: string; debugSettingsFormGroup = this.fb.group({ failuresEnabled: [false], @@ -123,6 +124,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor debugConfig: { maxDebugModeDuration: this.maxDebugModeDuration, entityType: this.entityType, + entityLabel: this.entityLabel, additionalActionConfig: this.additionalActionConfig, }, onSettingsAppliedFn: settings => { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html index 4d7ab8ee58..f23f7d00fc 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html @@ -36,14 +36,16 @@ {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
- + @if ((isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts index 6baefe0627..9b2fd42210 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts @@ -52,6 +52,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements @Input({ transform: booleanAttribute }) failuresEnabled = false; @Input({ transform: booleanAttribute }) allEnabled = false; + @Input() entityLabel = ''; @Input() entityType: EntityType; @Input() allEnabledUntil = 0; @Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; @@ -63,7 +64,6 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements maxMessagesCount: string; maxTimeFrameDuration: number; initialAllEnabled: boolean; - entityLabel: string; isDebugAllActive$ = this.debugAllControl.valueChanges.pipe( startWith(this.debugAllControl.value), @@ -108,7 +108,9 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements this.onFailuresControl.patchValue(this.failuresEnabled); this.debugAllControl.patchValue(this.allEnabled); this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime(); - this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; + if (!this.entityLabel) { + this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; + } } onCancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts index b9700c41a7..45039fd15a 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts @@ -28,6 +28,7 @@ export interface EntityDebugSettingPanelConfig { maxDebugModeDuration?: number; additionalActionConfig?: AdditionalDebugActionConfig; entityType: EntityType; + entityLabel?: string; } onSettingsAppliedFn: (settings: EntityDebugSettings) => void; } diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts index 058b3dbbe1..2d8ac49720 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts @@ -22,7 +22,9 @@ import { TranslateService } from '@ngx-translate/core'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { EntityDebugSettingPanelConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class EntityDebugSettingsService { constructor( 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 index 3de62b856d..c59c4cf5a9 100644 --- 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 @@ -16,108 +16,160 @@ --> - - - - + @if (entitiesTableConfig.detailsPanelEnabled) { + + + + + }
- {{ entitiesTableConfig.tableTitle }} + @if (entitiesTableConfig.tableTitle) { + {{ entitiesTableConfig.tableTitle }} + } - + @if (entitiesTableConfig.useTimePageLink) { + + }
-
- - - - - - - - - - + } @else { + @if (entitiesTableConfig.addActionDescriptors.length === 1) { + @if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) { + + } + } @else { - - - - - + } + }
- + @for (actionDescriptor of headerActionDescriptors; track actionDescriptor) { + + } - + @if (entitiesTableConfig.searchEnabled) { + + } +
+ @if (!entitiesTableConfig.addActionDescriptors.length) { + + + } @else { + @if (entitiesTableConfig.addActionDescriptors.length === 1) { + @if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) { + + + } + } @else { + + + } + } +
+ + @for (actionDescriptor of entitiesTableConfig.addActionDescriptors; track actionDescriptor) { + + } +
@@ -140,24 +192,28 @@
- -
- - {{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }} - - - -
-
+ @if (entitiesTableConfig.selectionEnabled) { + +
+ + {{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }} + + + @for (actionDescriptor of groupActionDescriptors; track actionDescriptor) { + + } +
+
+ }
+ matSort [matSortActive]="pageLink.sortOrder?.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> - - + @for (column of entityColumns; track column.key) { + + {{ column.ignoreTranslate ? column.title : (column.title | translate) }} - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ column.title | translate }} - - - - + + @switch (column.type) { + @case ('link') { + @if (column.entityURL(entity); as detailsPageURL) { + + } @else { + + } + } + @case ('entityChips') { + + } + @default { + + } + } + @if (column.actionCell) { + @switch (column.actionCell.type) { + @case (cellActionType.COPY_BUTTON) { + + + } + @default { + + } + } + } + + + } + @for (column of actionColumns; track trackByColumnKey($index, column)) { + + {{ column.title | translate }} + + + + + } - + @if (cellActionDescriptors.length) {
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
@@ -256,21 +320,22 @@ [style.min-width]="(cellActionDescriptors.length * 48) + 'px'"> {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} -
+ }
- + @if (cellActionDescriptors.length) {
- + @for (actionDescriptor of cellActionDescriptors; track actionDescriptor) { + + }
- + @for (actionDescriptor of cellActionDescriptors; track actionDescriptor) { + + }
-
+ }
@@ -299,21 +366,25 @@ [class.mat-selected]="dataSource.selection.isSelected(entity)" [class.tb-current-entity]="dataSource.isCurrentEntity(entity)" [class.tb-pointer]="entitiesTableConfig.rowPointer" - *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"> + *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)">
{{ translations.noEntities | translate }} + class="no-data-found flex items-center justify-center">{{ translations.noEntities | translate }} {{ 'common.loading' | translate }} + class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }}
- - + @if (displayPagination) { + + } + @if (displayPagination) { + + }
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 index 6999c76180..45fcf98eb0 100644 --- 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 @@ -49,7 +49,7 @@ import { CellActionDescriptorType, EntityActionTableColumn, EntityChipsEntityTableColumn, - EntityColumn, + EntityColumn, EntityColumnsType, EntityColumnType, EntityLinkTableColumn, EntityTableColumn, EntityTableConfig, @@ -89,8 +89,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa cellActionDescriptors: Array>>; actionColumns: Array>>; - entityColumns: Array>>; - + entityColumns: EntityColumnsType; displayedColumns: string[]; headerCellStyleCache: Array = []; @@ -594,8 +593,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa columnsUpdated(resetData: boolean = false) { this.entityColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn || - column instanceof EntityChipsEntityTableColumn) - .map(column => column as EntityTableColumn>); + column instanceof EntityChipsEntityTableColumn); this.actionColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityActionTableColumn) .map(column => column as EntityActionTableColumn>); @@ -624,8 +622,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; } - headerCellStyle(column: EntityColumn>) { - const index = this.entitiesTableConfig.columns.indexOf(column); + headerCellStyle(column: EntityColumnType) { + const index = this.entitiesTableConfig.columns.indexOf(column as EntityColumn>); let res = this.headerCellStyleCache[index]; if (!res) { const widthStyle: any = {width: column.width}; @@ -650,7 +648,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.cellStyleCache[index] = undefined; } - cellContent(entity: BaseData, column: EntityColumn>, row: number) { + cellContent(entity: BaseData, column: EntityColumnType, row: number) { if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) { const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; @@ -665,7 +663,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } } - cellTooltip(entity: BaseData, column: EntityColumn>, row: number) { + cellTooltip(entity: BaseData, column: EntityColumnType, row: number) { if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) { const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; @@ -682,8 +680,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } } - cellStyle(entity: BaseData, column: EntityColumn>, row: number) { - const col = this.entitiesTableConfig.columns.indexOf(column); + cellStyle(entity: BaseData, column: EntityColumnType, row: number) { + const col = this.entitiesTableConfig.columns.indexOf(column as EntityColumn>); const index = row * this.entitiesTableConfig.columns.length + col; let res = this.cellStyleCache[index]; if (!res) { @@ -702,7 +700,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa return res; } - trackByColumnKey(index, column: EntityTableColumn>) { + trackByColumnKey(index, column: EntityTableColumn> | EntityActionTableColumn>) { return column.key; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html index b0c7c71e00..856e56b9ea 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html @@ -15,7 +15,9 @@ limitations under the License. --> - - {{ subEntity.name | customTranslate }} - +@for (subEntity of subEntities; track subEntity) { + + {{ subEntity.name | customTranslate }} + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html index 496566ed3a..070d80b4ab 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html @@ -30,25 +30,27 @@
-
- - -
+ @if (!isReadOnly) { +
+ + +
+ }
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 index 333eafd7fa..319bb2fbf0 100644 --- 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 @@ -16,10 +16,11 @@ -->
-
alias.no-entity-filter-specified
- + @if (!filter || !filter.type) { +
alias.no-entity-filter-specified
+ } @else {
{{ 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 index edbea5d1aa..ab517b2501 100644 --- 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 @@ -19,315 +19,330 @@ alias.filter-type - - {{aliasFilterTypeTranslations.get(type) | translate}} - + @for (type of aliasFilterTypes; track type) { + + {{aliasFilterTypeTranslations.get(type) | translate}} + + } - - {{ 'alias.filter-type-required' | translate }} - + @if (entityFilterFormGroup.get('type').hasError('required')) { + + {{ 'alias.filter-type-required' | translate }} + + } -
- - - - - - - - - - - - - - - entity.name-starts-with - help - - - {{ 'entity.entity-name-filter-required' | translate }} - - - - - - - - - - alias.state-entity-parameter-name - - -
-
{{ 'alias.default-state-entity' | translate }}
- + @switch (entityFilterFormGroup.get('type').value) { + @case (aliasFilterType.singleEntity) { + + formControlName="singleEntity"> -
-
- - - - - asset.name-starts-with - help - - - - - - - - device.name-starts-with - help - - - - - - - - entity-view.name-starts-with - help - - - - - - - - edge.name-starts-with - - - - -
-
-
alias.root-entity
- - {{ 'alias.root-state-entity' | translate }} - - @if (filterFormGroup.get('rootStateEntity').value) { -
- - alias.state-entity-parameter-name - - -
-
{{ 'alias.default-state-entity' | translate }}
- - -
-
- } @else { - - + } + @case (aliasFilterType.entityList) { + + + + + } + @case (aliasFilterType.entityName) { + + + + entity.name-starts-with + help + + @if (filterFormGroup.get('entityNameFilter').hasError('required')) { + + {{ 'entity.entity-name-filter-required' | translate }} + } + + } + @case (aliasFilterType.entityType) { + + + } + @case (aliasFilterType.stateEntity) { + + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ +
-
-
alias.query-options
-
- - relation.direction - - - {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} - - - - - alias.max-relation-level - - + } + @case (aliasFilterType.assetType) { + + + + asset.name-starts-with + help + + + } + @case (aliasFilterType.deviceType) { + + + + device.name-starts-with + help + + + } + @case (aliasFilterType.entityViewType) { + + + + entity-view.name-starts-with + help + + + } + @case (aliasFilterType.edgeType) { + + + + edge.name-starts-with + + + } + @case (aliasFilterType.relationsQuery) { +
+
+
alias.root-entity
+ + {{ 'alias.root-state-entity' | translate }} + + @if (filterFormGroup.get('rootStateEntity').value) { +
+ + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ + +
+
+ } @else { + + + }
- - {{ 'alias.last-level-relation' | translate }} - -
-
-
relation.relation-filters
- - -
-
-
- -
-
-
alias.root-entity
- - {{ 'alias.root-state-entity' | translate }} - - @if (filterFormGroup.get('rootStateEntity').value) { -
- - alias.state-entity-parameter-name - +
+
alias.query-options
+
+ + relation.direction + + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + } + -
-
{{ 'alias.default-state-entity' | translate }}
- - + + alias.max-relation-level + + +
+ @if (filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value) { + + {{ 'alias.last-level-relation' | translate }} + + } +
+
+
relation.relation-filters
+ + +
+
+ } + @case (entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery || + entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery || + entityFilterFormGroup.get('type').value === aliasFilterType.edgeSearchQuery || + entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ? + entityFilterFormGroup.get('type').value : '') { +
+
+
alias.root-entity
+ + {{ 'alias.root-state-entity' | translate }} + + @if (filterFormGroup.get('rootStateEntity').value) { +
+ + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ + +
+ } @else { + + + } +
+
+
alias.query-options
+
+ + relation.direction + + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + } + + + + alias.max-relation-level + +
- } @else { - - - } -
-
-
alias.query-options
-
- - relation.direction - - - {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} - - - - - alias.max-relation-level - - + @if (filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value) { + + {{ 'alias.last-level-relation' | translate }} + + }
- - {{ 'alias.last-level-relation' | translate }} - -
-
-
relation.relation-filter
- - - - - - - - - - - - - - - - - - - - -
-
-
- +
+
relation.relation-filter
+ + + @switch (entityFilterFormGroup.get('type').value) { + @case (aliasFilterType.assetSearchQuery) { + + + } + @case (aliasFilterType.deviceSearchQuery) { + + + } + @case (aliasFilterType.edgeSearchQuery) { + + + } + @case (aliasFilterType.entityViewSearchQuery) { + + + } + } +
+
+ } + }
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 index da87474bb0..716d2e8699 100644 --- 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 @@ -17,8 +17,6 @@ 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'; @@ -41,8 +39,8 @@ export abstract class EntityTableHeaderComponent, return this.entitiesTableConfigValue; } - protected constructor(protected store: Store) { - super(store); + protected constructor(...args: unknown[]) { + super(); } ngOnInit() { 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 index ec6e46fb72..ef8fc630a5 100644 --- 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 @@ -16,7 +16,7 @@ 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 { AfterViewInit, Directive, inject, 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'; @@ -100,9 +100,11 @@ export abstract class EntityTabsComponent, entityTabsChanged = this.entityTabsSubject.asObservable(); - protected constructor(protected store: Store) { - super(store); - this.authUser = getCurrentAuthUser(store); + protected store: Store = inject(Store); + + protected constructor(...args: unknown[]) { + super(); + this.authUser = getCurrentAuthUser(this.store); } ngOnInit() { 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 index 0dfcfd43f7..f24d348444 100644 --- 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 @@ -24,8 +24,8 @@ close -
-
+
+
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 index 6de9cfd398..e0441fd914 100644 --- 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 @@ -13,11 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../scss/constants'; + :host { + display: grid; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + @media #{$mat-gt-xs} { + .mat-mdc-dialog-content { + max-height: 80vh; + } + } + .tb-event-content { - width: 100%; min-width: 400px; - height: 100%; - min-height: 50px; + &.border { + border: 1px solid #c0c0c0; + } + @media #{$mat-xs} { + min-width: 100%; + } } } 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 index 5eb15642bb..7fb51c13f6 100644 --- 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 @@ -23,7 +23,7 @@ import { Ace } from 'ace-builds'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { ContentType, contentTypesMap } from '@shared/models/constants'; -import { getAce } from '@shared/models/ace/ace.models'; +import { getAce, updateEditorSize } from '@shared/models/ace/ace.models'; import { Observable } from 'rxjs/internal/Observable'; import { beautifyJs } from '@shared/models/beautify.models'; import { of } from 'rxjs'; @@ -41,40 +41,38 @@ export interface EventContentDialogData { styleUrls: ['./event-content-dialog.component.scss'], standalone: false }) -export class EventContentDialogComponent extends DialogComponent implements OnInit, OnDestroy { +export class EventContentDialogComponent extends DialogComponent implements OnInit, OnDestroy { @ViewChild('eventContentEditor', {static: true}) eventContentEditorElmRef: ElementRef; - content: string; title: string; - contentType: ContentType; - aceEditor: Ace.Editor; + showBorder = false; + + private contentType: ContentType; + private aceEditor: Ace.Editor; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: EventContentDialogData, - public dialogRef: MatDialogRef, + protected 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.createEditor(this.eventContentEditorElmRef, this.content); + this.createEditor(this.eventContentEditorElmRef, this.data.content); } ngOnDestroy(): void { - if (this.aceEditor) { - this.aceEditor.destroy(); - } + this.aceEditor?.destroy(); super.ngOnDestroy(); } - isJson(str) { + private isJson(str: string) { try { return isLiteralObject(JSON.parse(str)); } catch (e) { @@ -82,78 +80,53 @@ export class EventContentDialogComponent extends DialogComponent = null; if (this.contentType) { mode = contentTypesMap.get(this.contentType).code; if (this.contentType === ContentType.JSON && content) { - content$ = beautifyJs(content, {indent_size: 4}); + content$ = beautifyJs(content, {indent_size: 2}); } else if (this.contentType === ContentType.BINARY && content) { try { const decodedData = base64toString(content); if (this.isJson(decodedData)) { mode = 'json'; - content$ = beautifyJs(decodedData, {indent_size: 4}); + content$ = beautifyJs(decodedData, {indent_size: 2}); } else { content$ = of(decodedData); } - } catch (e) {} + } catch (e) {/**/} } } if (!content$) { content$ = of(content); } - content$.subscribe( - (processedContent) => { - 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}; - getAce().subscribe( - (ace) => { - this.aceEditor = ace.edit(editorElement, editorOptions); - this.aceEditor.session.setUseWrapMode(false); - this.aceEditor.setValue(processedContent, -1); - this.updateEditorSize(editorElement, processedContent, this.aceEditor); - } - ); - } - ); - } - - updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { - let newHeight = 400; - let newWidth = 600; - if (content && content.length > 0) { - const lines = content.split('\n'); - newHeight = 17 * 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 = 9 * 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(); + content$.subscribe((processedContent) => { + const isJSON = mode === 'json' && processedContent !== '{}'; + this.showBorder = isJSON; + const editorOptions: Partial = { + mode: `ace/mode/${mode}`, + theme: 'ace/theme/github', + showGutter: isJSON, + showFoldWidgets: true, + foldStyle: 'markbeginend', + showPrintMargin: false, + readOnly: true, + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + }; + getAce().subscribe( + (ace) => { + this.aceEditor = ace.edit(editorElement, editorOptions); + this.aceEditor.session.setUseWrapMode(false); + this.aceEditor.setValue(processedContent, -1); + updateEditorSize(editorElement, processedContent, this.aceEditor, this.renderer, {showGutter: isJSON}); + } + ) + }); } - } diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html index df073b8702..fa9044ed9b 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html @@ -17,48 +17,52 @@ -->
- - - - + @for (column of showColumns; track column) { + @switch (column.key) { + @case (isSelector(column.key)) { + {{ column.title | translate}} {{ 'event.all-events' | translate}} - - {{ value }} - + @for (value of selectorValues(column.key); track value) { + + {{ value }} + + } - - - + } + @case (isNumberFields(column.key)) { + {{ column.title | translate}} - - {{ 'event.min-value' | translate }} - + @if (eventFilterFormGroup.get(column.key).hasError('min')) { + + {{ 'event.min-value' | translate }} + + } - - + } + @case ('isError') { {{ 'event.has-error' | translate }} - - - + } + @case ('errorStr') { + {{ column.title | translate}} - - - + } + @default { + {{ column.title | translate}} - - - + } + } + }
- + @if (!data.readonly) { + + }
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 index 230c8c8bb7..0cd0ea729e 100644 --- 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 @@ -16,54 +16,61 @@ -->
- +

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

-
- - -
-
-
-
- - filter.name - - - {{ 'filter.name-required' | translate }} - - - {{ 'filter.duplicate-filter' | translate }} - - -
- - - -
-
- - -
-
+
+
+ + filter.name + + @if (filterFormGroup.controls.filter.hasError('required')) { + {{ 'filter.name-required' | translate }} + } + @if (filterFormGroup.controls.filter.hasError('duplicateFilterName')) { + {{ 'filter.duplicate-filter' | translate }} + } + + +
+ + + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate | titlecase }} + + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate | titlecase }} + + +
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 index ec3c9b79f0..30e5a791b8 100644 --- 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 @@ -14,25 +14,23 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; -import { ErrorStateMatcher } from '@angular/material/core'; +import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, - FormGroupDirective, - NgForm, - ValidatorFn, - Validators -} from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup, 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'; +import { + ComplexOperation, + complexOperationTranslationMap, + Filter, + FilterInfo, + Filters +} from '@shared/models/query/query.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { FormControlsFrom } from '@shared/models/tenant.model'; export interface FilterDialogData { isAdd: boolean; @@ -41,32 +39,29 @@ export interface FilterDialogData { } @Component({ - selector: 'tb-filter-dialog', - templateUrl: './filter-dialog.component.html', - providers: [{ provide: ErrorStateMatcher, useExisting: FilterDialogComponent }], - styleUrls: ['./filter-dialog.component.scss'], - standalone: false + selector: 'tb-filter-dialog', + templateUrl: './filter-dialog.component.html', + standalone: false }) -export class FilterDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { +export class FilterDialogComponent extends DialogComponent { isAdd: boolean; - filters: Array; - filter: Filter; + filterFormGroup: FormGroup>; - filterFormGroup: UntypedFormGroup; + ComplexOperation = ComplexOperation; + complexOperationTranslationMap = complexOperationTranslationMap; + allowKeyFiltersOrConditions: boolean; - submitted = false; + private readonly filter: Filter; + private filters: Array; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: FilterDialogData, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, - private fb: UntypedFormBuilder, - private utils: UtilsService, - public translate: TranslateService) { + protected dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService) { super(store, router, dialogRef); this.isAdd = data.isAdd; if (Array.isArray(data.filters)) { @@ -87,16 +82,25 @@ export class FilterDialogComponent extends DialogComponent { + return (c: FormControl) => { const newFilter = c.value.trim(); const found = this.filters.find((filter) => filter.filter === newFilter); if (found) { @@ -112,13 +116,11 @@ export class FilterDialogComponent extends DialogComponent -
- - - -
filter.filters
-
-
-
- -
-
-
- - +
+ @if (predicatesFormArray.length) { +
+
+
+
+ +
- + @if (displayUserParameters) { +   + }
- -  
-
- -
-
-
- {{ complexOperationTranslations.get(operation) | translate }} -
-
-
- - - + +
+ @for (predicateControl of predicatesFormArray.controls; track predicateControl) { +
+ @if ($index) { +
+ {{ 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 index 5155866e84..9a895dc7e0 100644 --- 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 @@ -13,18 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../../scss/constants"; + :host { - .predicate-list { - overflow-y: auto; - overflow-x: hidden; - max-height: 350px; - .no-data-found { - height: 50px; - } + .filter-title { + padding: 12px 8px; + font-size: 14px; + font-weight: 500; + } + .no-data-found { + height: 50px; + font-size: 16px; + } + + .key-filter-list-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); } .filters-operation { - margin-top: -18px; - color: #666; - font-weight: 500; + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + position: absolute; + top: -12px; + left: 10px; + background-color: white; + } + &-label { + font-size: 15px; + font-weight: 400; + color: $tb-primary-color; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); + } } } 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 index eb1aa4ce53..63554e8034 100644 --- 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 @@ -15,71 +15,84 @@ limitations under the License. --> -
-
-
- - - - - - - - - - - - - - - - {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} - - -
-
{{ hintText | translate }}
-
-
-
-
- - - - {{'filter.no-dynamic-value' | translate}} - - - {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} - - - -
filter.dynamic-source-type
-
-
- - - -
filter.source-attribute
+
+
+
+
+ @switch (valueType) { + @case (valueTypeEnum.STRING) { + + {{ hintText | translate }} + + + } + @case (valueTypeEnum.NUMERIC) { + + {{ hintText | translate }} + + + } + @case (valueTypeEnum.DATE_TIME) { + + } + @case (valueTypeEnum.BOOLEAN) { + + {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} + + } + }
-
- - {{ 'filter.inherit-owner' | translate}} - -
filter.source-attribute-not-set
+
+
+
+
+ + filter.dynamic-source-type + + + {{'filter.no-dynamic-value' | translate}} + + @for (sourceType of dynamicValueSourceTypes; track sourceType) { + + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} + + } + + +
+
+ + filter.source-attribute + + +
+ @if (!allow && inheritMode) { +
+ + {{ 'filter.inherit-owner' | translate}} + +
filter.source-attribute-not-set
+
+ }
- + + @if (!onlyUserDynamicSource) { + + }
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 index 89db536b53..6c99a7bfcd 100644 --- 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 @@ -15,47 +15,84 @@ limitations under the License. --> -
-
- - +
+ @switch (type) { + @case (filterPredicateType.STRING) { +
+ + + @for (operation of stringOperations; track operation) { + + {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} + + } + + + + {{ 'alarm-rule.ignore-case' | translate }} + +
+ } + @case (filterPredicateType.NUMERIC) { +
+ + + @for (operation of numericOperations; track operation) { + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} + + } + + +
+ } + @case (filterPredicateType.BOOLEAN) { +
+ + + @for (operation of booleanOperations; track operation) { + + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} + + } + + +
+ } + @case (filterPredicateType.COMPLEX) { +
+ filter.complex-filter + +
+ } + } + @if (type !== filterPredicateType.COMPLEX) { + - - - - - - - - - - - - - - + formControlName="value"> + + }
- - + @if (type !== filterPredicateType.COMPLEX && displayUserParameters) { + + }
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 index e4e327bf0a..1311176976 100644 --- 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 @@ -14,19 +14,35 @@ /// limitations under the License. /// -import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, UntypedFormBuilder, - UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, - Validators + Validators, } from '@angular/forms'; -import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models'; +import { + EntityKeyValueType, + FilterPredicateType, + KeyFilterPredicateInfo, + BooleanOperation, booleanOperationTranslationMap, + NumericOperation, numericOperationTranslationMap, + StringOperation, stringOperationTranslationMap, ComplexFilterPredicateInfo, KeyFilterPredicateUserInfo, + KeyFilterPredicate +} from '@shared/models/query/query.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; +import { + ComplexFilterPredicateDialogComponent +} from '@home/components/filter/complex-filter-predicate-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { + FilterUserInfoDialogComponent, + FilterUserInfoDialogData +} from '@home/components/filter/filter-user-info-dialog.component'; @Component({ selector: 'tb-filter-predicate', @@ -46,7 +62,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; ], standalone: false }) -export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { +export class FilterPredicateComponent implements ControlValueAccessor, Validator { @Input() disabled: boolean; @@ -60,23 +76,35 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator @Input() onlyUserDynamicSource = false; - filterPredicateFormGroup: UntypedFormGroup; + filterPredicateFormGroup = this.fb.group({ + operation: [], + ignoreCase: false, + predicates: [], + value: [], + userInfo: [] + }); type: FilterPredicateType; filterPredicateType = FilterPredicateType; + stringOperations = Object.keys(StringOperation); + stringOperationEnum = StringOperation; + stringOperationTranslations = stringOperationTranslationMap; + + numericOperations = Object.keys(NumericOperation); + numericOperationEnum = NumericOperation; + numericOperationTranslations = numericOperationTranslationMap; + + booleanOperations = Object.keys(BooleanOperation); + booleanOperationEnum = BooleanOperation; + booleanOperationTranslations = booleanOperationTranslationMap; + private propagateChange = null; constructor(private fb: UntypedFormBuilder, - private destroyRef: DestroyRef) { - } - - ngOnInit(): void { - this.filterPredicateFormGroup = this.fb.group({ - predicate: [null, [Validators.required]], - userInfo: [null, []] - }); + private destroyRef: DestroyRef, + private dialog: MatDialog) { this.filterPredicateFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { @@ -93,7 +121,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; - if (this.disabled) { + if (isDisabled) { this.filterPredicateFormGroup.disable({emitEvent: false}); } else { this.filterPredicateFormGroup.enable({emitEvent: false}); @@ -108,19 +136,97 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator writeValue(predicate: KeyFilterPredicateInfo): void { this.type = predicate.keyFilterPredicate.type; - this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); + this.updateValidators(); + this.filterPredicateFormGroup.patchValue(predicate.keyFilterPredicate, {emitEvent: false}); this.filterPredicateFormGroup.get('userInfo').patchValue(predicate.userInfo, {emitEvent: false}); } + private updateValidators(): void { + const operationCtrl = this.filterPredicateFormGroup.get('operation'); + const predicatesCtrl = this.filterPredicateFormGroup.get('predicates'); + operationCtrl.setValidators([Validators.required]); + if (this.type === FilterPredicateType.COMPLEX) { + predicatesCtrl.setValidators([Validators.required]); + } else { + predicatesCtrl.clearValidators(); + } + operationCtrl.updateValueAndValidity({emitEvent: false}); + predicatesCtrl.updateValueAndValidity({emitEvent: false}); + } + private updateModel() { let predicate: KeyFilterPredicateInfo = null; if (this.filterPredicateFormGroup.valid) { + const v = this.filterPredicateFormGroup.getRawValue(); + let keyFilterPredicate: KeyFilterPredicate; + if (this.type === FilterPredicateType.COMPLEX) { + keyFilterPredicate = { + type: FilterPredicateType.COMPLEX, + operation: v.operation, + predicates: v.predicates + } as KeyFilterPredicate; + } else { + keyFilterPredicate = { + type: this.type, + value: v.value, + operation: v.operation, + ignoreCase: !!v.ignoreCase + } as KeyFilterPredicate; + } predicate = { - keyFilterPredicate: this.filterPredicateFormGroup.getRawValue().predicate, - userInfo: this.filterPredicateFormGroup.getRawValue().userInfo + keyFilterPredicate, + userInfo: v.userInfo }; } this.propagateChange(predicate); } + public openComplexFilterDialog() { + this.dialog.open(ComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: { + type: FilterPredicateType.COMPLEX, + operation: this.filterPredicateFormGroup.get('operation').value, + predicates: this.filterPredicateFormGroup.get('predicates').value + }, + readonly: this.disabled, + valueType: this.valueType, + isAdd: false, + key: this.key, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource, + onlyUserDynamicSource: this.onlyUserDynamicSource + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.patchValue(result); + } + } + ); + } + + public openFilterUserInfoDialog() { + this.dialog.open(FilterUserInfoDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + keyFilterPredicateUserInfo: this.filterPredicateFormGroup.get('userInfo').value, + valueType: this.valueType, + key: this.key, + operation: this.filterPredicateFormGroup.get('operation').value, + readonly: this.disabled + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.get('userInfo').patchValue(result); + } + } + ); + } } 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 index d2d53c0918..94e246af52 100644 --- 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 @@ -14,37 +14,31 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; -import { KeyFilter, keyFiltersToText } from '@shared/models/query/query.models'; +import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ComplexOperation, 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'; +import { coerceBoolean } from '@shared/decorators/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 - } - ], - standalone: false + selector: 'tb-filter-text', + templateUrl: './filter-text.component.html', + styleUrls: ['./filter-text.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterTextComponent), + multi: true + } + ], + standalone: false }) -export class FilterTextComponent implements ControlValueAccessor, OnInit { +export class FilterTextComponent implements ControlValueAccessor, OnChanges { - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required = false; @Input() disabled: boolean; @@ -58,26 +52,30 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit { @Input() nowrap = false; + @Input() + operation: ComplexOperation = ComplexOperation.AND; + requiredClass = false; public filterText: string; - private propagateChange = (v: any) => { }; + private currentValue: Array; - constructor(private dialog: MatDialog, - private fb: UntypedFormBuilder, - private translate: TranslateService, + constructor(private translate: TranslateService, private datePipe: DatePipe) { } - registerOnChange(fn: any): void { - this.propagateChange = fn; + registerOnChange(_fn: any): void { } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } - ngOnInit() { + ngOnChanges(changes: SimpleChanges) { + if (changes.operation && !changes.operation.firstChange + && changes.operation.currentValue !== changes.operation.previousValue) { + this.updateFilterText(this.currentValue); + } } setDisabledState(isDisabled: boolean): void { @@ -85,13 +83,14 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit { } writeValue(value: Array): void { + this.currentValue = value; this.updateFilterText(value); } private updateFilterText(value: Array) { this.requiredClass = false; if (value && value.length) { - this.filterText = keyFiltersToText(this.translate, this.datePipe, value); + this.filterText = keyFiltersToText(this.translate, this.datePipe, value, this.operation); } else { if (this.required && !this.disabled) { this.filterText = this.addFilterPrompt; 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 index 52df01d33d..1190125501 100644 --- 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 @@ -50,11 +50,13 @@
-
-
filter.unit
- - -
+ @if (showUniInput) { +
+
filter.unit
+ + +
+ }
@@ -65,11 +67,12 @@ cdkFocusInitial> {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!data.readonly) { + + }
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 index 5251d87c4d..452045d164 100644 --- 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 @@ -25,66 +25,61 @@ close - - -
-
- -
-
filter.filter
-
filter.editable
-
+
+
+ + + filter.filter + + {{ control.get('filter').value }} + + + + filter.editable + + + + + + + +
+ + + +
+
+
+ + +
-
-
- -
- {{$index + 1}}. -
- {{filterControl.get('filter').value}} -
- - -
- - - -
+
+ filter.no-filters
-
+
- + @for (filter of filtersInfo | keyvalue; track filter; let last = $last) { +
+
+ {{filter.value.filter}} + +
+ @if (!last) { + + }
- -
+ }
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 index f70b00d863..f669c77b28 100644 --- 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 @@ -15,18 +15,20 @@ limitations under the License. --> -
- + - filter_list - - - {{ 'filter.filters' | translate }} - -
+ {{ 'filter.filters' | translate }} + +
+} 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 index d8e4264a8e..f7624c873f 100644 --- 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 @@ -26,101 +26,128 @@
-
-
-
+
+
{{ 'filter.key-filter' | translate }}
+
+
- - filter.key-type.key-type - - - {{entityKeyTypeTranslations.get(type) | translate}} - - - - - filter.key-name - - - - - - - - - {{ 'filter.key-name-required' | translate }} - - -
- + + filter.key-type.key-type + + @for (type of entityKeyTypes; track type) { + + {{entityKeyTypeTranslations.get(type) | translate}} + + } + + + + filter.key-name + + @if (keyFilterFormGroup.get('key.key').value && showAutocomplete) { + + } + + @for (keyName of filteredKeysName | async; track keyName) { + + + + } + + @if (keyFilterFormGroup.get('key.key').hasError('required')) { + + {{ '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 }} - - -
-
- - - filter.value - - - - - - filter.value - - - - - - - - - {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} - - -
+ filter.value-type.value-type + + + + {{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }} + + @for (valueType of entityKeyValueTypesKeys; track valueType) { + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} + + } + + @if (keyFilterFormGroup.get('valueType').hasError('required')) { + + {{ 'filter.value-type-required' | translate }} + + } + + @if (isConstantKeyType) { +
+
+ @switch (keyFilterFormGroup.get('valueType').value) { + @case (entityKeyValueTypeEnum.STRING) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.NUMERIC) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.DATE_TIME) { + + } + @case (entityKeyValueTypeEnum.BOOLEAN) { + + {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} + + } + } +
+
+ }
- - - + @if (keyFilterFormGroup.get('valueType').value) { +
+
+
{{ 'filter.filters' | translate }} +
+
+ + +
+ } +
- + @if (!data.readonly) { + + }
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 index ba5fbfc250..8bfd7792d5 100644 --- 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 @@ -15,78 +15,86 @@ limitations under the License. --> -
- - - - -
filter.key-filters
-
-
-
- -
- - -   -   -
-
- -
-
-
- filter.operation.and +
+
+
+
filter.key-filters
+ +
+ @if (keyFiltersFormArray.length) { +
+
+ +
+
filter.key-name
+
filter.key-type.key-type
+   +  
-
-
-
{{ keyFilterControl.value.key.key }}
-
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}
- - +
+ +
+ @for (keyFilterControl of keyFiltersFormArray.controls; track keyFilterControl; let index = $index) { +
+
+ @if (index) { +
+ {{ complexOperationTranslationMap.get(operation) | translate }} +
+ } +
+
+
+
{{ keyFilterControl.value.key.key }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}
+ + +
+
- -
+ }
- filter.no-key-filters -
-
- -
- - +
+ } @else { + filter.no-key-filters + } +
+ +
+
+
+ - -
filter.preview
-
+ {{ 'filter.preview' | translate }}
-
- -
+ + @if (keyFiltersFormArray.length) { + + } @else { + filter.no-filter-text + } +
- +
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 index 2d1bcc923a..9ef70c8845 100644 --- 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 @@ -13,30 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../scss/constants"; + :host { - .key-filter-list { + .filter-title { + padding: 12px 0; + font-size: 14px; + font-weight: 500; + } + .no-data-found { + height: 50px; + font-size: 16px; + } + .filter-list { overflow: auto; max-height: 300px; - .no-data-found { - height: 50px; + + &-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + + .filters-text { + font-size: 14px; } } .filters-operation { - margin-top: -18px; - 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; + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + background-color: white; + } + &-label { + font-size: 15px; + font-weight: 400; + color: $tb-primary-color; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); } } } 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 index aad686707d..73804faa7a 100644 --- 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 @@ -14,24 +14,26 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { - AbstractControl, ControlValueAccessor, - UntypedFormArray, - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, + FormArray, + FormBuilder, + FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms'; -import { Observable, Subject } from 'rxjs'; +import { Observable } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { + ComplexOperation, + complexOperationTranslationMap, EntityKeyType, entityKeyTypeTranslationMap, + KeyFilter, KeyFilterInfo, keyFilterInfosToKeyFilters } from '@shared/models/query/query.models'; @@ -39,27 +41,26 @@ import { MatDialog } from '@angular/material/dialog'; import { deepClone } from '@core/utils'; import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; import { EntityId } from '@shared/models/id/entity-id'; -import { takeUntil } from 'rxjs/operators'; @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 - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => KeyFilterListComponent), - multi: true - } - ], - standalone: false + 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 + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true + } + ], + standalone: false }) -export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { +export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -71,39 +72,32 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator, @Input() entityId: EntityId; - keyFilterListFormGroup: UntypedFormGroup; + @Input() operation: ComplexOperation = ComplexOperation.AND; + + complexOperationTranslationMap = complexOperationTranslationMap; entityKeyTypeTranslations = entityKeyTypeTranslationMap; - keyFiltersControl: UntypedFormControl; + keyFiltersFormArray: FormArray>; + + keyFiltersControl: FormControl>; - private destroy$ = new Subject(); private propagateChange = null; - constructor(private fb: UntypedFormBuilder, - private dialog: MatDialog) { + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { } ngOnInit(): void { - this.keyFilterListFormGroup = this.fb.group({ - keyFilters: this.fb.array([]) - }); + this.keyFiltersFormArray = this.fb.array>([]); this.keyFiltersControl = this.fb.control(null); - this.keyFilterListFormGroup.valueChanges.pipe( - takeUntil(this.destroy$) + this.keyFiltersFormArray.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) ).subscribe(() => this.updateModel()); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - get keyFiltersFormArray(): UntypedFormArray { - return this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray; - } - registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -114,61 +108,61 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator, setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { - this.keyFilterListFormGroup.disable({emitEvent: false}); + this.keyFiltersFormArray.disable({emitEvent: false}); this.keyFiltersControl.disable({emitEvent: false}); } else { - this.keyFilterListFormGroup.enable({emitEvent: false}); + this.keyFiltersFormArray.enable({emitEvent: false}); this.keyFiltersControl.enable({emitEvent: false}); } } validate(): ValidationErrors | null { - return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : { + return this.keyFiltersFormArray.valid && this.keyFiltersControl.valid ? null : { keyFilterList: {valid: false} }; } writeValue(keyFilters: Array): void { - if (keyFilters.length === this.keyFiltersFormArray.length) { + if (keyFilters?.length === this.keyFiltersFormArray.length) { this.keyFiltersFormArray.patchValue(keyFilters, {emitEvent: false}); } else { - const keyFilterControls: Array = []; + this.keyFiltersFormArray.clear({emitEvent: false}); if (keyFilters) { for (const keyFilter of keyFilters) { - keyFilterControls.push(this.fb.control(keyFilter, [Validators.required])); + this.keyFiltersFormArray.push( + this.fb.control(keyFilter, [Validators.required]), + {emitEvent: false} + ); } } - this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls), {emitEvent: false}); if (this.disabled) { - this.keyFilterListFormGroup.disable({emitEvent: false}); + this.keyFiltersFormArray.disable({emitEvent: false}); } else { - this.keyFilterListFormGroup.enable({emitEvent: false}); + this.keyFiltersFormArray.enable({emitEvent: false}); } } const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); } - public removeKeyFilter(index: number) { - (this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).removeAt(index); + removeKeyFilter(index: number) { + this.keyFiltersFormArray.removeAt(index); } - public addKeyFilter() { - const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray; + addKeyFilter() { this.openKeyFilterDialog(null).subscribe((result) => { if (result) { - keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); + this.keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); } }); } - public editKeyFilter(index: number) { - const keyFilter: KeyFilterInfo = - (this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).at(index).value; + editKeyFilter(index: number) { + const keyFilter = this.keyFiltersFormArray.at(index).value; this.openKeyFilterDialog(keyFilter).subscribe( (result) => { if (result) { - (this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).at(index).patchValue(result); + this.keyFiltersFormArray.at(index).patchValue(result); } } ); @@ -204,12 +198,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator, } private updateModel() { - const keyFilters: Array = this.keyFilterListFormGroup.getRawValue().keyFilters; - if (keyFilters.length) { - this.propagateChange(keyFilters); - } else { - this.propagateChange(null); - } + const keyFilters = this.keyFiltersFormArray.getRawValue(); + this.propagateChange(keyFilters.length ? keyFilters : 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 index f113bfcde9..76394def54 100644 --- 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 @@ -18,9 +18,11 @@
- - {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} - + @for (operation of numericOperations; track operation) { + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} + + } - - {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} - + @for (operation of stringOperations; track operation) { + + {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} + + } 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 index 8ca43427e9..f1836ae17e 100644 --- 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 @@ -15,64 +15,61 @@ limitations under the License. --> -
- + +

{{ filter.filter | customTranslate }}

-
- -
-
-
-
- - - {{ userInputControl.get('label').value | customTranslate }} - - - - - - {{ userInputControl.get('label').value | customTranslate }} - -
- {{ userInputControl.get('unitSymbol').value }} + @for (userInputControl of userInputsFormArray.controls; track userInputControl) { + @switch (userInputControl.controls.valueType.value) { + @case (valueTypeEnum.STRING) { + + {{ userInputControl.controls.label.value | customTranslate }} + + + } + @case (valueTypeEnum.NUMERIC) { + + {{ userInputControl.controls.label.value | customTranslate }} + + @if (userInputControl.controls.unitSymbol.value) { +
+ {{ userInputControl.controls.unitSymbol.value }}
-
- - - - - - - - {{ userInputControl.get('label').value | customTranslate }} - - -
-
-
+ } + + } + @case (valueTypeEnum.DATE_TIME) { + + } + @case (valueTypeEnum.BOOLEAN) { + + + {{ userInputControl.controls.label.value | customTranslate }} + + + } + } + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss index d3b1d316ab..552c49b56a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss @@ -14,14 +14,14 @@ * limitations under the License. */ :host ::ng-deep { - .user-filter-dialog { - .mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder { + .numeric-field { + &.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder { .mat-mdc-form-field-icon-suffix { place-self: center; } } - .mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix { + &.mat-mdc-form-field-has-icon-suffix { .mat-mdc-form-field-icon-suffix { place-self: baseline; } 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 index 6e068fdcf3..59231f8b80 100644 --- 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 @@ -14,12 +14,11 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core'; -import { ErrorStateMatcher } from '@angular/material/core'; +import { Component, DestroyRef, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormArray, FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { TranslateService } from '@ngx-translate/core'; @@ -39,29 +38,31 @@ export interface UserFilterDialogData { filter: Filter; } +interface UserInputForm { + label: FormControl; + valueType: FormControl; + unitSymbol: FormControl; + value: FormControl; +} + @Component({ selector: 'tb-user-filter-dialog', templateUrl: './user-filter-dialog.component.html', - providers: [{ provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent }], styleUrls: ['./user-filter-dialog.component.scss'], standalone: false }) -export class UserFilterDialogComponent extends DialogComponent - implements ErrorStateMatcher { +export class UserFilterDialogComponent extends DialogComponent { filter: Filter; - userFilterFormGroup: FormGroup; + userInputsFormArray: FormArray>; 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, + protected dialogRef: MatDialogRef, private fb: FormBuilder, private translate: TranslateService, private destroyRef: DestroyRef, @@ -70,19 +71,13 @@ export class UserFilterDialogComponent extends DialogComponent = []; - for (const userInput of userInputs) { - userInputControls.push(this.createUserInputFormControl(userInput)); - } - - this.userFilterFormGroup = this.fb.group({ - userInputs: this.fb.array(userInputControls) - }); + const userInputControls = userInputs.map(input => this.createUserInputFormControl(input)); + this.userInputsFormArray = this.fb.array(userInputControls); } - private createUserInputFormControl(userInput: UserFilterInputInfo): FormGroup { + private createUserInputFormControl(userInput: UserFilterInputInfo): FormGroup { const predicateValue: FilterPredicateValue = (userInput.info.keyFilterPredicate as any).value; - let value = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue; + let value: string | number | boolean = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue; let unitSymbol = ''; let valueConvertor: TbUnitConverter; if (userInput.valueType === EntityKeyValueType.NUMERIC) { @@ -91,42 +86,31 @@ export class UserFilterDialogComponent extends DialogComponent({ + label: this.fb.control(userInput.label), + valueType: this.fb.control(userInput.valueType), + unitSymbol: this.fb.control(unitSymbol), + value: this.fb.control(value, userInput.valueType === EntityKeyValueType.NUMERIC || - userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []] + userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []) }); - userInputControl.get('value').valueChanges.pipe( + userInputControl.controls.value.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(userValue => { - let value = userValue; + let val = userValue; if (valueConvertor) { - value = valueConvertor(value as number); + val = valueConvertor(val as number); } - (userInput.info.keyFilterPredicate as any).value.userValue = value; + (userInput.info.keyFilterPredicate as any).value.userValue = val; }); return userInputControl; } - userInputsFormArray(): FormArray { - return this.userFilterFormGroup.get('userInputs') as FormArray; - } - - 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 index 00c0d3e5e1..a726e4baf7 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -23,6 +23,7 @@ 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 { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableComponent } from '@home/components/event/event-table.component'; import { EventFilterPanelComponent } from '@home/components/event/event-filter-panel.component'; @@ -183,30 +184,24 @@ import { } from '@home/components/dashboard-page/layout/select-dashboard-breakpoint.component'; import { EntityChipsComponent } from '@home/components/entity/entity-chips.component'; import { DashboardViewComponent } from '@home/components/dashboard-view/dashboard-view.component'; -import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; -import { CalculatedFieldDialogComponent } from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; import { EntityDebugSettingsButtonComponent } from '@home/components/entity/debug/entity-debug-settings-button.component'; -import { - CalculatedFieldArgumentsTableComponent -} from '@home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component'; -import { - CalculatedFieldArgumentPanelComponent -} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component'; -import { - CalculatedFieldDebugDialogComponent -} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component'; -import { - CalculatedFieldScriptTestDialogComponent -} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component'; -import { - CalculatedFieldTestArgumentsComponent -} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component'; import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component"; import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; +import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; +import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module'; +import { AlarmRuleModule } from "@home/components/alarm-rules/alarm-rule.module"; +import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component"; +import { ApiKeysTableComponent } from '@home/components/api-key/api-keys-table.component'; +import { AddApiKeyDialogComponent } from '@home/components/api-key/add-api-key-dialog.component'; +import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/edit-api-key-description-panel.component'; +import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component'; +import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component'; +import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component"; +import { EventsDialogComponent } from '@home/dialogs/events-dialog.component'; @NgModule({ declarations: @@ -219,6 +214,8 @@ import { ResourcesLibraryComponent } from "@home/components/resources/resources- EntityDetailsPageComponent, AuditLogTableComponent, AuditLogDetailsDialogComponent, + CalculatedFieldsTableComponent, + AlarmRulesTableComponent, EventContentDialogComponent, EventTableHeaderComponent, EventTableComponent, @@ -351,22 +348,25 @@ import { ResourcesLibraryComponent } from "@home/components/resources/resources- SendNotificationButtonComponent, EntityChipsComponent, DashboardViewComponent, - CalculatedFieldsTableComponent, - CalculatedFieldDialogComponent, - CalculatedFieldArgumentsTableComponent, - CalculatedFieldArgumentPanelComponent, - CalculatedFieldDebugDialogComponent, - CalculatedFieldScriptTestDialogComponent, - CalculatedFieldTestArgumentsComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, ResourcesDialogComponent, ResourcesLibraryComponent, + ApiKeysTableComponent, + ApiKeysTableDialogComponent, + AddApiKeyDialogComponent, + EditApiKeyDescriptionPanelComponent, + ApiKeyGeneratedDialogComponent, + AuditLogHeaderComponent, + AuditLogFilterComponent, + EventsDialogComponent ], imports: [ CommonModule, SharedModule, SharedHomeComponentsModule, + CalculatedFieldsModule, + AlarmRuleModule, WidgetConfigComponentsModule, BasicWidgetConfigModule, Lwm2mProfileComponentsModule, @@ -384,6 +384,8 @@ import { ResourcesLibraryComponent } from "@home/components/resources/resources- EntityDetailsPanelComponent, EntityDetailsPageComponent, AuditLogTableComponent, + CalculatedFieldsTableComponent, + AlarmRulesTableComponent, EventTableComponent, EdgeDownlinkTableHeaderComponent, EdgeDownlinkTableComponent, @@ -500,17 +502,13 @@ import { ResourcesLibraryComponent } from "@home/components/resources/resources- SendNotificationButtonComponent, EntityChipsComponent, DashboardViewComponent, - CalculatedFieldsTableComponent, - CalculatedFieldDialogComponent, - CalculatedFieldArgumentsTableComponent, - CalculatedFieldArgumentPanelComponent, - CalculatedFieldDebugDialogComponent, - CalculatedFieldScriptTestDialogComponent, - CalculatedFieldTestArgumentsComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, ResourcesDialogComponent, ResourcesLibraryComponent, + ApiKeysTableComponent, + ApiKeysTableDialogComponent, + EventsDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html index 0b04505520..ad9f187f76 100644 --- a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html +++ b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html @@ -15,12 +15,14 @@ limitations under the License. --> -
- - -
+@if (show()) { +
+ + +
+} diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html index d58b525d5f..f05d9e372c 100644 --- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html @@ -17,43 +17,52 @@ -->
notification.notification
- + @if ((notifications$ | async)?.length) { + + }
- -
-
-
- - - -
+@if (notifications$ | async) { + @if ((notifications$ | async).length) { +
+
+ @for (notification of (notifications$ | async); track notification.id.id; let last = $last) { +
+ + + @if (!last) { + + } +
+ } +
+
+ } @else { +
+ + + +
+ notification.no-notifications-yet + } + @if ((notifications$ | async).length) { + + } + @if ((notifications$ | async).length) { +
+
-
- -
- -
-
- -
- - - -
- notification.no-notifications-yet -
- + } +} @else {
notification.loading-notifications
-
+} diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts index f9e4969788..7bc4934629 100644 --- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts @@ -46,8 +46,8 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O private notificationSubscriber = NotificationSubscriber.createNotificationsSubscription(this.notificationWsService, this.zone, 6); - notifications$: Observable = this.notificationSubscriber.notifications$.pipe( - filter(value => Array.isArray(value)), + notifications$: Observable = (this.notificationSubscriber.notifications$ as Observable).pipe( + filter((value): value is Notification[] => Array.isArray(value)), shareReplay(1), tap(() => setTimeout(() => this.cd.markForCheck())) ); @@ -91,8 +91,4 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O this.onClose(); this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {}); } - - trackById(index: number, item: NotificationRequest): string { - return item.id.id; - } } 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 index 3e0a267b9e..de39c93a2f 100644 --- 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 @@ -25,9 +25,11 @@ close
- - -
+@if (isLoading$ | async) { + +} @else { +
+}
{{ 'device-profile.device-profile-details' | translate }} -
- +
+ device-profile.name - - {{ 'device-profile.name-required' | translate }} - - - {{ 'device-profile.name-max-length' | translate }} - + @if (deviceProfileDetailsFormGroup.get('name').hasError('required')) { + {{ 'device-profile.name-required' | translate }} + } + @if (deviceProfileDetailsFormGroup.get('name').hasError('maxlength')) { + {{ 'device-profile.name-max-length' | translate }} + } {{'device-profile.mobile-dashboard-hint' | translate}} {{'device-profile.default-edge-rule-chain-hint' | translate}} - + device-profile.type - - {{deviceProfileTypeTranslations.get(type) | translate}} - + @for (type of deviceProfileTypes; track type) { + + {{ deviceProfileTypeTranslations.get(type) | translate }} + + } - - {{ 'device-profile.type-required' | translate }} - + @if (deviceProfileDetailsFormGroup.get('type').hasError('required')) { + {{ 'device-profile.type-required' | translate }} + } - + device-profile.description - +
@@ -92,35 +100,30 @@
{{ '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.transport-type + + @for (type of deviceTransportTypes; track type) { + + {{ deviceTransportTypeTranslations.get(type) | translate }} + + } + + @if (transportConfigFormGroup.get('transportType').value) { + + {{ deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate }} + + } + @if (transportConfigFormGroup.get('transportType').hasError('required')) { + {{ 'device-profile.transport-type-required' | translate }} + } + + + +
@@ -135,15 +138,18 @@
- + @if (selectedIndex > 0) { + + } - + @if (showNext) { + + }
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 index 8363e183bb..efc1d43647 100644 --- 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 @@ -52,6 +52,7 @@ width: 100%;; overflow-y: auto; scrollbar-gutter: stable; + padding-top: 4px; @media #{$mat-gt-sm} { min-width: 500px; } 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 index a8ee087ad7..9891a4f0b3 100644 --- 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 @@ -86,8 +86,6 @@ export class AddDeviceProfileDialogComponent extends transportConfigFormGroup: UntypedFormGroup; - alarmRulesFormGroup: UntypedFormGroup; - provisionConfigFormGroup: UntypedFormGroup; serviceType = ServiceType.TB_RULE_ENGINE; @@ -133,12 +131,6 @@ export class AddDeviceProfileDialogComponent extends this.deviceProfileTransportTypeChanged(); }); - this.alarmRulesFormGroup = this.fb.group( - { - alarms: [null] - } - ); - this.provisionConfigFormGroup = this.fb.group( { provisionConfiguration: [{ @@ -177,8 +169,6 @@ export class AddDeviceProfileDialogComponent extends case 1: return this.transportConfigFormGroup; case 2: - return this.alarmRulesFormGroup; - case 3: return this.provisionConfigFormGroup; } } @@ -200,7 +190,6 @@ export class AddDeviceProfileDialogComponent extends profileData: { configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, - alarms: this.alarmRulesFormGroup.get('alarms').value, provisionConfiguration: deviceProvisionConfiguration } }; @@ -228,8 +217,6 @@ export class AddDeviceProfileDialogComponent extends case 1: return 'device-profile.transport-configuration'; case 2: - return 'device-profile.alarm-rules'; - case 3: return 'device-profile.device-provisioning'; } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html index 0fae730f16..39a5c625e3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html @@ -22,18 +22,26 @@ step="1" min="1" max="2147483647" formControlName="defaultValue" placeholder="{{ defaultValuePlaceholder | translate }}"> - - {{ defaultValueRequiredError | translate }} - - - {{ defaultValueRangeError | translate }} - - - {{ defaultValueRangeError | translate }} - - - {{ defaultValuePatternError | translate }} - + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('required')) { + + {{ defaultValueRequiredError | translate }} + + } + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('min')) { + + {{ defaultValueRangeError | translate }} + + } + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('max')) { + + {{ defaultValueRangeError | translate }} + + } + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('pattern')) { + + {{ defaultValuePatternError | translate }} + + }
@@ -44,9 +52,11 @@ {{'filter.no-dynamic-value' | translate}} - - {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} - + @for (sourceType of dynamicValueSourceTypes; track sourceType) { + + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} + + }
@@ -55,13 +65,15 @@
-
- - {{ 'filter.inherit-owner' | translate}} - -
+ @if (inheritMode) { +
+ + {{ 'filter.inherit-owner' | translate}} + +
+ }
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 index 7fb953620b..c039992035 100644 --- 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 @@ -25,8 +25,10 @@ close - - + @if (isLoading$ | async) { + + + }
@@ -41,35 +43,47 @@ device-profile.condition-type - - {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} - + @for (alarmConditionType of alarmConditionTypes; track alarmConditionType) { + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + } - - {{ 'device-profile.condition-type-required' | translate }} - + @if (conditionFormGroup.get('spec.type').hasError('required')) { + + {{ 'device-profile.condition-type-required' | translate }} + + } -
- - -
- - - - {{ timeUnitTranslations.get(timeUnit) | translate }} - - - - {{ 'device-profile.condition-duration-time-unit-required' | translate }} - - + @if (conditionFormGroup.get('spec.type').value != AlarmConditionType.SIMPLE) { +
+ + + @if (conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION) { +
+ + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + } + + @if (conditionFormGroup.get('spec.unit').hasError('required')) { + + {{ 'device-profile.condition-duration-time-unit-required' | translate }} + + } + +
+ }
-
+ }
@@ -81,11 +95,12 @@ (click)="cancel()" cdkFocusInitial> {{ (readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!readonly) { + + }
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 index 38d632b8ad..31cfb9aebb 100644 --- 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 @@ -23,8 +23,10 @@ required addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> - - + @if (specText) { + + + }
-
-
- - {{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }} - - - {{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}} - -
+ @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { +
+ + {{ ('device-profile.alarm-rule-additional-info' | translate) + ': ' }} + + @if (alarmRuleFormGroup.get('alarmDetails').value) { + + } + +
+ } + @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { +
+ + {{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }} + + + {{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}} + +
+ }
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 index 1e302d9063..141713832f 100644 --- 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 @@ -25,8 +25,10 @@ close - - + @if (isLoading$ | async) { + + + }
@@ -39,11 +41,12 @@ (click)="cancel()" cdkFocusInitial> {{ (readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!readonly) { + + }
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 index 1e0675900f..23226cd17a 100644 --- 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 @@ -18,91 +18,106 @@
- - {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} - + @for (alarmScheduleType of alarmScheduleTypes; track alarmScheduleType) { + + {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} + + } - - {{ 'device-profile.schedule-type-required' | translate }} - + @if (alarmScheduleForm.get('type').hasError('required')) { + + {{ 'device-profile.schedule-type-required' | translate }} + + } -
- - -
- -
device-profile.schedule-days
-
-
- - {{ dayOfWeekTranslationsArray[day] | translate }} - -
-
- - {{ dayOfWeekTranslationsArray[day] | translate }} - -
-
+ @if (alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME) { +
+ + + @if (alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME) { +
+ +
device-profile.schedule-days
+
+
+ @for (day of firstRowDays; track day) { + + {{ dayOfWeekTranslationsArray[day] | translate }} + + } +
+
+ @for (day of secondRowDays; track day) { + + {{ 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 - - - - +
device-profile.schedule-time
+
+
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
-
-
-
-
- +
+ } + @if (alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM) { +
+ +
device-profile.schedule-days
+ @for (day of allDays; track day) { +
+
+ + {{ 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.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts index 90caf04b70..e8224cabf3 100644 --- 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 @@ -63,7 +63,7 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, alarmScheduleForm: UntypedFormGroup; - alarmScheduleTypes = Object.keys(AlarmScheduleType); + alarmScheduleTypes = Object.values(AlarmScheduleType); alarmScheduleType = AlarmScheduleType; alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; dayOfWeekTranslationsArray = dayOfWeekTranslations; 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 index d0c5d25273..e98fe00d2c 100644 --- 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 @@ -16,49 +16,60 @@ -->
-
-
- - alarm.severity - - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - - - - {{ 'device-profile.alarm-severity-required' | translate }} - - - - - + @for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let isLast = $last) { +
+
+ + alarm.severity + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + } + + @if (createAlarmRuleControl.get('severity').hasError('required')) { + + {{ 'device-profile.alarm-severity-required' | translate }} + + } + + + + +
+ @if (!disabled) { + + }
- -
-
+ } + @if (!createAlarmRulesFormArray().controls.length && !disabled) { +
device-profile.add-create-alarm-rule-prompt -
-
- -
+ class="tb-prompt required">device-profile.add-create-alarm-rule-prompt +
+ } + @if (!disabled) { +
+ +
+ }
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 index 3a9577df18..739f9e0716 100644 --- 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 @@ -24,13 +24,15 @@
- + @if (!disabled) { + + }
@@ -39,15 +41,21 @@ {{'device-profile.alarm-type' | translate}} - - {{ 'device-profile.alarm-type-required' | translate }} - - - {{ 'device-profile.alarm-type-unique' | translate }} - - - {{ 'device-profile.alarm-type-max-length' | translate }} - + @if (alarmFormGroup.get('alarmType').hasError('required')) { + + {{ 'device-profile.alarm-type-required' | translate }} + + } + @if (alarmFormGroup.get('alarmType').hasError('unique')) { + + {{ 'device-profile.alarm-type-unique' | translate }} + + } + @if (alarmFormGroup.get('alarmType').hasError('maxlength')) { + + {{ 'device-profile.alarm-type-max-length' | translate }} + + }
@@ -62,25 +70,28 @@ {{ 'device-profile.propagate-alarm' | translate }} -
- - device-profile.alarm-rule-relation-types-list - - - {{key}} - close - - - - - -
+ @if (alarmFormGroup.get('propagate').value === true) { +
+ + device-profile.alarm-rule-relation-types-list + + @for (key of alarmFormGroup.get('propagateRelationTypes').value; track key) { + + {{key}} + close + + } + + + + +
+ } {{ 'device-profile.propagate-alarm-to-owner' | translate }} @@ -103,29 +114,35 @@
- -
-
- device-profile.no-clear-alarm-rule -
-
- + @if (!disabled) { + + }
+ @if (disabled && !alarmFormGroup.get('clearRule').value) { +
+ device-profile.no-clear-alarm-rule +
+ } + @if (!disabled) { +
+ +
+ }
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 index 648217c6f5..71a8ad389e 100644 --- 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 @@ -17,25 +17,30 @@ -->
-
- - -
+ @for (alarmControl of alarmsFormArray.controls; track trackByAlarm($index, alarmControl); let isLast = $last) { +
+ + +
+ }
-
+ @if (!alarmsFormArray.controls.length) { +
device-profile.no-alarm-rules -
-
- -
+ class="tb-prompt flex items-center justify-center">device-profile.no-alarm-rules +
+ } + @if (!disabled) { +
+ +
+ }
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 index 7a94f51423..30caafd1aa 100644 --- 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 @@ -25,14 +25,16 @@ close - - + @if (isLoading$ | async) { + + + }
+ placeholder="{{ 'device-profile.alarm-rule-additional-info-placeholder' | translate }}">
@@ -45,10 +47,12 @@ (click)="cancel()" cdkFocusInitial> {{ 'action.cancel' | translate }} - + @if (!data.readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html index 4f8f3e035b..c699aea0a1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ 'asset-profile.asset-profile' | translate }} - - {{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }} - - - - + @if (showDetailsPageLink && useAssetProfileLink && selectAssetProfileFormGroup.get('assetProfile').value && disabled) { + + {{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }} + + } + @if (selectAssetProfileFormGroup.get('assetProfile').value && !disabled) { + + } + @if (selectAssetProfileFormGroup.get('assetProfile').value?.id && !disabled && editProfileEnabled) { + + } + @if (!selectAssetProfileFormGroup.get('assetProfile').value && !disabled && addNewProfile) { + + } - - - - -
-
- asset-profile.no-asset-profiles-found -
- - + @for (assetProfile of filteredAssetProfiles | async; track assetProfile) { + + + + } + @if (!(filteredAssetProfiles | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText) || !addNewProfile) { +
+ asset-profile.no-asset-profiles-found +
+ } @else { + {{ translate.get('asset-profile.no-asset-profiles-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} - - - asset-profile.create-new-asset-profile - - -
-
+ {entity: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ + asset-profile.create-new-asset-profile + + } +
+
+ }
- - {{ 'asset-profile.asset-profile-required' | translate }} - - {{ hint | translate }} + @if (selectAssetProfileFormGroup.get('assetProfile').hasError('required')) { + + {{ 'asset-profile.asset-profile-required' | translate }} + + } + @if (hint && !disabled) { + {{ hint | translate }} + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts index 79b4c6ed4d..66c84aa27a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts @@ -44,7 +44,7 @@ import { AssetProfileId } from '@shared/models/id/asset-profile-id'; import { AssetProfile, AssetProfileInfo } from '@shared/models/asset.models'; import { AssetProfileService } from '@core/http/asset-profile.service'; import { AssetProfileDialogComponent, AssetProfileDialogData } from './asset-profile-dialog.component'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { coerceBoolean } from '@shared/decorators/coercion'; import { AuthUser } from '@shared/models/user.model'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -98,6 +98,9 @@ export class AssetProfileAutocompleteComponent implements ControlValueAccessor, @Input() hint: string; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Output() assetProfileUpdated = new EventEmitter(); diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html index 724bc80e61..d47b4fa65c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
-
- - - - -
- + + + +
+ +
-
+}
-
- +
+ asset-profile.name - - {{ 'asset-profile.name-required' | translate }} - - - {{ 'asset-profile.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'asset-profile.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'asset-profile.name-max-length' | translate }} + } {{'asset-profile.mobile-dashboard-hint' | translate}} {{'asset-profile.default-edge-rule-chain-hint' | translate}} @@ -87,9 +93,9 @@ label="{{'asset-profile.image' | translate}}" formControlName="image"> - + asset-profile.description - +
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 index 5ed849e0e2..1f1d13b0be 100644 --- 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 @@ -15,8 +15,15 @@ limitations under the License. --> - - {{ 'device-profile.device-profile' | translate }} + + @if (!inlineField) { + {{ 'device-profile.device-profile' | translate }} + } - - {{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }} - - - - + @if (showDetailsPageLink && useDeviceProfileLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled) { + + {{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }} + + } + @if (selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled) { + + } + @if (selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled) { + + } + @if (!selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && addNewProfile) { + + } - - - - -
-
- device-profile.no-device-profiles-found -
- - + @for (deviceProfile of filteredDeviceProfiles | async; track deviceProfile) { + + + + } + @if (!(filteredDeviceProfiles | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText) || !addNewProfile) { +
+ device-profile.no-device-profiles-found +
+ } @else { + {{ translate.get('device-profile.no-device-profiles-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} - - - device-profile.create-new-device-profile - - -
-
+ {entity: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ + device-profile.create-new-device-profile + + } +
+
+ }
- - {{ 'device-profile.device-profile-required' | translate }} - - {{ hint | translate }} + @if (selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')) { + + {{ 'device-profile.device-profile-required' | translate }} + + } + @if (hint && !disabled) { + {{ hint | 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 index 9989eee7ca..c041d0420a 100644 --- 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 @@ -47,7 +47,7 @@ import { MatAutocomplete } from '@angular/material/autocomplete'; import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component'; import { emptyPageData } from '@shared/models/page/page-data'; import { getEntityDetailsPageURL } from '@core/utils'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { coerceBoolean } from '@shared/decorators/coercion'; import { AuthUser } from '@shared/models/user.model'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -70,6 +70,9 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, modelValue: DeviceProfileId | null; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Input() subscriptSizing: SubscriptSizing = 'fixed'; @@ -107,6 +110,10 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, @Input() hint: string; + @Input() + @coerceBoolean() + inlineField: boolean; + @Output() deviceProfileUpdated = new EventEmitter(); 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 index 246912b170..dcb15e48b8 100644 --- 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 @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + device-profile.provision-strategy - - {{deviceProvisionTypeTranslateMap.get(type) | translate}} - + @for (type of deviceProvisionTypes; track type) { + + {{deviceProvisionTypeTranslateMap.get(type) | translate}} + + } - - {{ 'device-profile.provision-strategy-required' | translate }} - + @if (provisionConfigurationFormGroup.get('type').hasError('required')) { + {{ 'device-profile.provision-strategy-required' | translate }} + } -
- - - - - - - + @switch (provisionConfigurationFormGroup.get('type').value) { + @case (deviceProvisionType.ALLOW_CREATE_NEW_DEVICES) { + + } + @case (deviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES) { + + } + @case (deviceProvisionType.X509_CERTIFICATE_CHAIN) {
- + device-profile.provision-strategy-x509.certificate-value - - - {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} - + + @if (provisionConfigurationFormGroup.get('certificateValue').hasError('required')) { + {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} + } - + device-profile.provision-strategy-x509.cn-regex-variable - - {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} - + tb-help-popup="device-profile/x509-chain-regex-examples" + tb-help-popup-placement="top" + trigger-style="letter-spacing:0.25px" + [tb-help-popup-style]="{maxWidth: '820px'}">
+ @if (provisionConfigurationFormGroup.get('certificateRegExPattern').hasError('required')) { + {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} + } device-profile.provision-strategy-x509.cn-regex-variable-hint - - -
- - device-profile.provision-device-key - - - - {{ 'device-profile.provision-device-key-required' | translate }} - - - - device-profile.provision-device-secret - - - - {{ 'device-profile.provision-device-secret-required' | translate }} - - -
-
-
+ } + } + +
+ + device-profile.provision-device-key + + + @if (provisionConfigurationFormGroup.get('provisionDeviceKey').hasError('required')) { + {{ 'device-profile.provision-device-key-required' | translate }} + } + + + device-profile.provision-device-secret + + + @if (provisionConfigurationFormGroup.get('provisionDeviceSecret').hasError('required')) { + {{ '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 index 1da59bbc5c..05aef4cfbb 100644 --- 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 @@ -62,7 +62,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu provisionConfigurationFormGroup: UntypedFormGroup; deviceProvisionType = DeviceProvisionType; - deviceProvisionTypes = Object.keys(DeviceProvisionType); + deviceProvisionTypes = Object.values(DeviceProvisionType); deviceProvisionTypeTranslateMap = deviceProvisionTypeTranslationMap; private requiredValue: boolean; 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 index 863c902853..9b54e59253 100644 --- 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 @@ -15,79 +15,90 @@ limitations under the License. --> -
- - - - -
- + + + +
+ +
-
+}
-
- +
+ device-profile.name - - {{ 'device-profile.name-required' | translate }} - - - {{ 'device-profile.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'device-profile.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'device-profile.name-max-length' | translate }} + } - {{'device-profile.mobile-dashboard-hint' | translate}} + @if (!entityForm.get('defaultDashboardId').disabled) { + {{'device-profile.mobile-dashboard-hint' | translate}} + } - {{'device-profile.default-edge-rule-chain-hint' | translate}} + @if (!entityForm.get('defaultEdgeRuleChainId').disabled) { + {{'device-profile.default-edge-rule-chain-hint' | translate}} + } - + device-profile.type - - {{deviceProfileTypeTranslations.get(type) | translate}} - + @for (type of deviceProfileTypes; track type) { + + {{ deviceProfileTypeTranslations.get(type) | translate }} + + } - - {{ 'device-profile.type-required' | translate }} - + @if (entityForm.get('type').hasError('required')) { + {{ '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
-
-
- - - - -
-
-
+ @if (standalone) { + + device-profile.transport-type + + @for (type of deviceTransportTypes; track type) { + + {{ deviceTransportTypeTranslations.get(type) | translate }} + + } + + @if (entityForm.get('transportType').hasError('required')) { + {{ 'device-profile.transport-type-required' | translate }} + } + +
+ + @if (displayProfileConfiguration) { + + + +
device-profile.profile-configuration
+
+
+ + + + +
+ } + @if (displayTransportConfiguration) { + + + +
device-profile.transport-configuration
+
+
+ + + + +
+ } + @if (entityForm.get('profileData.alarms').value?.length) { + + + +
{{'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/coap-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html index 50f1040052..d345fc789c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html @@ -20,81 +20,81 @@
device-profile.coap-device-type
- + - - {{ coapTransportDeviceTypeTranslations.get(type) | translate}} - + @for (type of coapTransportDeviceTypes; track type) { + + {{ coapTransportDeviceTypeTranslations.get(type) | translate }} + + } - - {{ 'device-profile.coap-device-type-required' | translate }} - + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.coapDeviceType').hasError('required')) { + {{ 'device-profile.coap-device-type-required' | translate }} + }
-
-
- device-profile.coap-device-payload-type -
- - - - {{ transportPayloadTypeTranslations.get(type) | translate}} - - - - {{ 'device-profile.mqtt-payload-type-required' | translate }} - - -
- - - - - {{ 'device-profile.telemetry-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.attributes-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-request-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-response-proto-schema-required' | translate}} - - + @if (coapDeviceTypeDefault) { +
+
+ device-profile.coap-device-payload-type +
+ + + @for (type of transportPayloadTypes; track type) { + + {{ transportPayloadTypeTranslations.get(type) | translate }} + + } + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.transportPayloadType').hasError('required')) { + {{ 'device-profile.mqtt-payload-type-required' | translate }} + } + + @if (protoPayloadType) { +
+ + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')) { + {{ 'device-profile.telemetry-proto-schema-required' | translate }} + } + + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')) { + {{ 'device-profile.attributes-proto-schema-required' | translate }} + } + + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-request-proto-schema-required' | translate }} + } + + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-response-proto-schema-required' | translate }} + } +
+ }
-
-
-
+
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html b/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html index 85328ee0d1..e96c05fb0c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html @@ -16,37 +16,48 @@ -->
- - {{ "device-profile.power-saving-mode" | translate }} + + @if (isDeviceSetting) { + {{ "device-profile.power-saving-mode" | translate }} + } - {{ "device-profile.power-saving-mode-type.default" | translate }} - - {{ powerModeTranslationMap.get(powerMod) | translate}} - + @if (isDeviceSetting) { + {{ "device-profile.power-saving-mode-type.default" | translate }} + } + @for (powerMod of powerMods; track powerMod) { + + {{ powerModeTranslationMap.get(powerMod) | translate}} + + } -
- - - + + + + +
+ } @else if (parentForm.get('powerMode').value === 'PSM') { + + [minText]="'device-profile.psm-activity-timer-min'" + appearance="outline" + formControlName="psmActivityTimer"> -
- - + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html index 132d46ab69..7b1b74bcb5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html @@ -16,25 +16,33 @@ -->
- + {{ labelText | translate }} - - {{ requiredText | translate }} - - - {{ patternText | translate }} - - - {{ (minText || patternText) | translate : {min: minTime/1000} }} - + @if (timeUnitSelectFormGroup.get('time').hasError('required')) { + + {{ requiredText | translate }} + + } + @if (timeUnitSelectFormGroup.get('time').hasError('pattern')) { + + {{ patternText | translate }} + + } + @if (timeUnitSelectFormGroup.get('time').hasError('min')) { + + {{ (minText || patternText) | translate : {min: minTime/1000} }} + + } - + device-profile.condition-duration-time-unit - - {{ timeUnitTranslations.get(timeUnit) | translate }} - + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts index 709d4ececa..29cf614c2e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts @@ -37,6 +37,7 @@ import { timeUnitTranslationMap } from '@shared/models/time/time.models'; import { isDefinedAndNotNull, isNumber } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; interface FormGroupModel { time: number; @@ -105,6 +106,9 @@ export class TimeUnitSelectComponent implements OnInit, OnDestroy, ControlValueA @Input() minText: string; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private propagateChange = (v: any) => { } 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 index 30cbfd8d36..61c5f4f9d4 100644 --- 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 @@ -16,11 +16,13 @@ -->
-
- - - - +
+ @switch (type) { + @case (deviceProfileType.DEFAULT) { + + + } + }
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 index f8fdf1895d..84b7893cee 100644 --- 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 @@ -16,32 +16,34 @@ -->
-
- - - - - - - - - - - - - - - - - - - - +
+ @switch (transportType) { + @case (deviceTransportType.DEFAULT) { + + + } + @case (deviceTransportType.MQTT) { + + + } + @case (deviceTransportType.COAP) { + + + } + @case (deviceTransportType.LWM2M) { + + + } + @case (deviceTransportType.SNMP) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html index 7b4913446c..6a631c8df2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html @@ -27,8 +27,10 @@ close - - + @if (isLoading$ | async) { + + + }
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html index 7d82ef2581..1ad031c3a9 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html @@ -21,51 +21,63 @@
-
-
- - - - {{ attributeNameTranslationMap.get(attributeName) | translate }} - - - - {{ 'device-profile.lwm2m.attribute-name-required' | translate }} - - - - - - {{ 'device-profile.lwm2m.attribute-value-required' | translate }} - - - {{ 'device-profile.lwm2m.attribute-value-pattern' | translate }} - - - + } +
+
+ } +
device-profile.lwm2m.no-attributes-set
+ @if (!disabled && isAddEnabled) { +
+
-
-
device-profile.lwm2m.no-attributes-set
-
- -
+ } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html index 468cb62a60..a966d6197b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html @@ -23,17 +23,23 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
{{ 'device-profile.lwm2m.server-type' | translate }} - - {{ serverConfigTypeNamesMap.get(serverType) | translate }} - + @for (serverType of serverTypes; track serverType) { + + {{ serverConfigTypeNamesMap.get(serverType) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html index d738e0447a..1074428d93 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html @@ -17,25 +17,31 @@ -->
-
- - -
+ @for (serverConfig of serverConfigsFromArray.controls; track trackByParams($index)) { +
+ + +
+ }
-
+ @if (!serverConfigsFromArray.controls.length) { +
device-profile.lwm2m.no-config-servers -
-
-
+ } + @if (!disabled) { +
+ -
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts index 2f0fd69102..3b6d676ab3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts @@ -18,11 +18,12 @@ import { Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output } import { AbstractControl, ControlValueAccessor, - UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, NG_VALIDATORS, - NG_VALUE_ACCESSOR + NG_VALUE_ACCESSOR, + FormControl, + FormArray } from '@angular/forms'; import { of, Subject } from 'rxjs'; import { ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; @@ -104,8 +105,8 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu this.destroy$.complete(); } - get serverConfigsFromArray(): UntypedFormArray { - return this.bootstrapConfigServersFormGroup.get('serverConfigs') as UntypedFormArray; + get serverConfigsFromArray(): FormArray { + return this.bootstrapConfigServersFormGroup.get('serverConfigs') as FormArray; } setDisabledState(isDisabled: boolean): void { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html index 3da66e4ee4..e69cb9660c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html @@ -22,158 +22,180 @@
{{ (serverFormGroup.get('bootstrapServerIs').value ? 'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}
-
-
{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }} - {{ serverFormGroup.get('shortServerId').value }} + @if (!serverPanel.expanded) { +
+
{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }} + {{ serverFormGroup.get('shortServerId').value ? serverFormGroup.get('shortServerId').value : '' }} +
+
{{ ('device-profile.lwm2m.mode' | translate) + ': ' }} + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }} +
-
{{ ('device-profile.lwm2m.mode' | translate) + ': ' }} - {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }} -
-
+ }
- + @if (!disabled) { + + }
- + {{ 'device-profile.lwm2m.mode' | translate }} - - {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityMode]) }} - + @for (securityMode of securityConfigLwM2MTypes; track securityMode) { + + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityMode]) }} + + } - - {{ 'device-profile.lwm2m.short-id' | translate }} - help - - - {{ 'device-profile.lwm2m.short-id-required' | translate }} - - - {{ 'device-profile.lwm2m.short-id-pattern' | translate }} - - - {{ 'device-profile.lwm2m.short-id-range' | translate : {min: shortServerIdMin, max: shortServerIdMax} }} - - + @if (!isBootstrap) { + + {{ 'device-profile.lwm2m.short-id' | translate }} + @if (!disabled) { + help + } + + @if (serverFormGroup.get('shortServerId').hasError('required')) { + {{ 'device-profile.lwm2m.short-id-required' | translate }} + } + @if (serverFormGroup.get('shortServerId').hasError('pattern')) { + {{ (isBootstrap ? 'device-profile.lwm2m.short-id-pattern-bs' : 'device-profile.lwm2m.short-id-pattern') | translate }} + } + @if (serverFormGroup.get('shortServerId').hasError('min') || + serverFormGroup.get('shortServerId').hasError('max')) { + {{ 'device-profile.lwm2m.short-id-range' | translate : {min: shortServerIdMin, max: shortServerIdMax} }} + } + + }
- + {{ 'device-profile.lwm2m.server-host' | translate }} - - {{ 'device-profile.lwm2m.server-host-required' | translate }} - + @if (serverFormGroup.get('host').hasError('required')) { + {{ 'device-profile.lwm2m.server-host-required' | translate }} + } - + {{ 'device-profile.lwm2m.server-port' | translate }} - - {{ 'device-profile.lwm2m.server-port-required' | translate }} - - - {{ 'device-profile.lwm2m.server-port-pattern' | translate }} - - - {{ 'device-profile.lwm2m.server-port-range' | translate }} - + @if (serverFormGroup.get('port').hasError('required')) { + {{ 'device-profile.lwm2m.server-port-required' | translate }} + } + @if (serverFormGroup.get('port').hasError('pattern')) { + {{ 'device-profile.lwm2m.server-port-pattern' | translate }} + } + @if (serverFormGroup.get('port').hasError('min') || + serverFormGroup.get('port').hasError('max')) { + {{ 'device-profile.lwm2m.server-port-range' | translate }} + }
- + {{ 'device-profile.lwm2m.client-hold-off-time' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.client-hold-off-time-required' | translate }} - - - {{ 'device-profile.lwm2m.client-hold-off-time-pattern' | translate }} - + @if (serverFormGroup.get('clientHoldOffTime').hasError('required')) { + {{ 'device-profile.lwm2m.client-hold-off-time-required' | translate }} + } + @if (serverFormGroup.get('clientHoldOffTime').hasError('min') || + serverFormGroup.get('clientHoldOffTime').hasError('pattern')) { + {{ 'device-profile.lwm2m.client-hold-off-time-pattern' | translate }} + } - + {{ 'device-profile.lwm2m.account-after-timeout' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.account-after-timeout-required' | translate }} - - - {{ 'device-profile.lwm2m.account-after-timeout-pattern' | translate }} - + @if (serverFormGroup.get('bootstrapServerAccountTimeout').hasError('required')) { + {{ 'device-profile.lwm2m.account-after-timeout-required' | translate }} + } + @if (serverFormGroup.get('bootstrapServerAccountTimeout').hasError('min') || + serverFormGroup.get('bootstrapServerAccountTimeout').hasError('pattern')) { + {{ 'device-profile.lwm2m.account-after-timeout-pattern' | translate }} + }
-
- + @if (serverFormGroup.get('securityMode').value === securityConfigLwM2MType.RPK || + serverFormGroup.get('securityMode').value === securityConfigLwM2MType.X509) { + {{ 'device-profile.lwm2m.server-public-key' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.server-public-key-required' | translate }} - + @if (serverFormGroup.get('serverPublicKey').hasError('required')) { + {{ 'device-profile.lwm2m.server-public-key-required' | translate }} + } -
+ }
- + {{ 'device-profile.lwm2m.lifetime' | translate }} - - {{ 'device-profile.lwm2m.lifetime-required' | translate }} - - - {{ 'device-profile.lwm2m.lifetime-pattern' | translate }} - + @if (serverFormGroup.get('lifetime').hasError('required')) { + {{ 'device-profile.lwm2m.lifetime-required' | translate }} + } + @if (serverFormGroup.get('lifetime').hasError('pattern') || + serverFormGroup.get('lifetime').hasError('min')) { + {{ 'device-profile.lwm2m.lifetime-pattern' | translate }} + } - + {{ 'device-profile.lwm2m.default-min-period' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.default-min-period-required' | translate }} - - - {{ 'device-profile.lwm2m.default-min-period-pattern' | translate }} - + @if (serverFormGroup.get('defaultMinPeriod').hasError('required')) { + {{ 'device-profile.lwm2m.default-min-period-required' | translate }} + } + @if (serverFormGroup.get('defaultMinPeriod').hasError('pattern') || + serverFormGroup.get('defaultMinPeriod').hasError('min')) { + {{ 'device-profile.lwm2m.default-min-period-pattern' | translate }} + }
- + {{ 'device-profile.lwm2m.binding' | translate }} - help + @if (!disabled) { + help + } - - {{ bindingModeTypeNamesMap.get(bindingMode) | translate }} - + @for (bindingMode of bindingModeTypes; track bindingMode) { + + {{ bindingModeTypeNamesMap.get(bindingMode) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts index 4464880588..ba8a1c3fc1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts @@ -75,8 +75,8 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc currentSecurityMode = null; bootstrapDisabled = false; - shortServerIdMin = 1; - shortServerIdMax = 65534; + readonly shortServerIdMin = 1; + readonly shortServerIdMax = 65534; @Input() @coerceBoolean() @@ -95,18 +95,15 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc } ngOnInit(): void { - if (this.isBootstrap) { - this.shortServerIdMin = 0; - this.shortServerIdMax = 65535; - } this.serverFormGroup = this.fb.group({ host: ['', Validators.required], port: ['', [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern('[0-9]*')]], securityMode: [Lwm2mSecurityType.NO_SEC], serverPublicKey: [''], clientHoldOffTime: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], - shortServerId: ['', - [Validators.required, Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax), Validators.pattern('[0-9]*')]], + shortServerId: ['', this.isBootstrap ? + [] : [Validators.required, Validators.pattern('[0-9]*'), Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax)] + ], bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], binding: [''], lifetime: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], @@ -130,6 +127,7 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc this.serverFormGroup.get('serverPublicKey').patchValue(serverSecurityConfig.serverCertificate, {emitEvent: false}); } }); + this.serverFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index eac4fdd17e..a09f088853 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -19,24 +19,32 @@ -
+
- + + + {{ 'device-profile.lwm2m.init-attr-tel-as-obs-strategy' | translate }} + + + device-profile.lwm2m.observe-strategy.observe-strategy {{ observeStrategyMap.get(lwm2mDeviceProfileFormGroup.get('observeStrategy').value)?.name | translate }} - - {{ observeStrategyMap.get(strategy).name | translate }} - - {{ observeStrategyMap.get(strategy).description | translate }} - - + @for (strategy of observeStrategyList; track strategy) { + + {{ observeStrategyMap.get(strategy).name | translate }} + + {{ observeStrategyMap.get(strategy).description | translate }} + + + }
-
- warning - LwM2M transport was run without bootstrap server -
+ @if (!isTransportWasRunWithBootstrap) { +
+ warning + LwM2M transport was run without bootstrap server +
+ } {{ 'device-profile.lwm2m.include-bootstrap-server' | translate }}
- +
@@ -74,7 +85,7 @@
device-profile.lwm2m.fw-update - + {{ 'device-profile.lwm2m.fw-update-strategy' | translate }} {{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }} @@ -82,30 +93,34 @@ {{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }} - - {{ 'device-profile.lwm2m.fw-update-resource' | translate }} - - - {{ 'device-profile.lwm2m.fw-update-resource-required' | translate }} - - + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateStrategy').value === 2) { + + {{ 'device-profile.lwm2m.fw-update-resource' | translate }} + + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateResource').hasError('required')) { + {{ 'device-profile.lwm2m.fw-update-resource-required' | translate }} + } + + }
device-profile.lwm2m.sw-update - + {{ 'device-profile.lwm2m.sw-update-strategy' | translate }} {{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }} {{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }} - - {{ 'device-profile.lwm2m.sw-update-resource' | translate }} - - - {{ 'device-profile.lwm2m.sw-update-resource-required' | translate }} - - + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateStrategy').value === 2) { + + {{ 'device-profile.lwm2m.sw-update-resource' | translate }} + + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateResource').hasError('required')) { + {{ 'device-profile.lwm2m.sw-update-resource-required' | translate }} + } + + }
@@ -115,11 +130,13 @@
device-profile.lwm2m.default-object-id - + - - {{ objectIDVerTranslationMap.get(objectIDVer) | translate }} - + @for (objectIDVer of objectIDVers; track objectIDVer) { + + {{ objectIDVerTranslationMap.get(objectIDVer) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index 010b3d528b..cf1083e3cc 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -110,6 +110,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro bootstrapServerUpdateEnable: [false], bootstrap: [[]], observeStrategy: [null, []], + initAttrTelAsObsStrategy: [false], clientLwM2mSettings: this.fb.group({ clientOnlyObserveAfterConnect: [1, []], useObject19ForOtaInfo: [false], @@ -185,6 +186,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro takeUntil(this.destroy$) ).subscribe(value => this.updateObserveStrategy(value)); + this.lwm2mDeviceProfileFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value) => { @@ -274,6 +276,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro bootstrap: this.configurationValue.bootstrap, bootstrapServerUpdateEnable: this.configurationValue.bootstrapServerUpdateEnable || false, observeStrategy: this.configurationValue.observeAttr.observeStrategy || ObserveStrategy.SINGLE, + initAttrTelAsObsStrategy: this.configurationValue.observeAttr.initAttrTelAsObsStrategy ?? false, clientLwM2mSettings: { clientOnlyObserveAfterConnect: this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect, useObject19ForOtaInfo: this.configurationValue.clientLwM2mSettings.useObject19ForOtaInfo ?? false, @@ -442,6 +445,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro const attributes: any = {}; const keyNameNew = {}; const observeStrategyValue = this.lwm2mDeviceProfileFormGroup.get('observeStrategy').value; + const initAttrTelAsObsStrategyValue = this.lwm2mDeviceProfileFormGroup.get('initAttrTelAsObsStrategy').value; const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); observeJson.forEach(obj => { if (isDefinedAndNotNull(obj.attributes) && !isEmpty(obj.attributes)) { @@ -483,6 +487,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro telemetry: telemetryArray, keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew), attributeLwm2m: attributes, + initAttrTelAsObsStrategy: initAttrTelAsObsStrategyValue, observeStrategy: observeStrategyValue }; } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html index 41ac62dae2..1f36c01fe4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + {{ 'device-profile.lwm2m.instances-list' | translate }} - - {{instanceId}} - close - + @for (instanceId of instancesId; track instanceId) { + + {{instanceId}} + @if (!disabled) { + close + } + + } - - {{ 'device-profile.lwm2m.instances-list-required' | translate }} - - - {{ 'device-profile.lwm2m.instance-id-pattern' | translate }} - - - {{ 'device-profile.lwm2m.instance-id-max' | translate: {max: instanceIdValueMax} }} - + @if (instancesListFormGroup.get('instanceList').hasError('required')) { + + {{ 'device-profile.lwm2m.instances-list-required' | translate }} + + } + @if (instancesListFormGroup.get('instanceId').hasError('pattern')) { + + {{ 'device-profile.lwm2m.instance-id-pattern' | translate }} + + } + @if (instancesListFormGroup.get('instanceId').hasError('max')) { + + {{ 'device-profile.lwm2m.instance-id-max' | translate: {max: instanceIdValueMax} }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html index 5bd040695a..d56183f562 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html @@ -15,16 +15,19 @@ limitations under the License. --> - + device-profile.lwm2m.object-list - - {{objectLwm2m.name}} #{{objectLwm2m.keyId}} - close - + @for (objectLwm2m of objectsList; track objectLwm2m) { + + {{objectLwm2m.name}} #{{objectLwm2m.keyId}} + @if (!disabled) { + close + } + + } - - - - -
- device-profile.lwm2m.no-objects-found -
- - + @for (objectLwm2m of filteredObjectsList | async; track objectLwm2m) { + + + + } + @if (!(filteredObjectsList | async)?.length) { + + @if (!textIsNotEmpty(searchText)) { +
+ device-profile.lwm2m.no-objects-found +
+ } @else { + {{ 'device-profile.lwm2m.no-objects-matching' | translate:{object: (searchText | truncate: true: 6: '...')} }} - -
-
+ + } + + } - - {{ 'device-profile.lwm2m.object-list-empty' | translate }} - + @if (lwm2mListFormGroup.get('objectsList').hasError('required')) { + + {{ 'device-profile.lwm2m.object-list-empty' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts index 3aafd2d56f..76a12f0f4c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts @@ -35,6 +35,7 @@ import { isDefined, isDefinedAndNotNull, isObject, isString } from '@core/utils' import { PageLink } from '@shared/models/page/page-link'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-profile-lwm2m-object-list', @@ -74,6 +75,9 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V this.updateValidators(); } + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Output() addList = new EventEmitter(); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html index f432533614..a0ab434c9a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html @@ -17,57 +17,58 @@ -->
- - - -
- {{ 'device-profile.lwm2m.instance' | translate }} #{{instances.get('id').value}} -
-
- - -
-
- - -
-
- - -
- - - -
-
- - - - -
+ @for (instances of instancesFormArray.controls; track trackByParams($index, instances.value)) { + + + +
+ {{ 'device-profile.lwm2m.instance' | translate }} #{{ instances.get('id').value }} +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+ + + + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html index c125f345a3..38c3f2e087 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html @@ -34,41 +34,42 @@
-
-
-
- #{{resourceLwM2M.get('id').value}} {{resourceLwM2M.get('name').value}} +
+ @for (resourceLwM2M of resourcesFormArray.controls; track trackByParams($index, resourceLwM2M.value)) { +
+
+ #{{ resourceLwM2M.get('id').value }} {{ resourceLwM2M.get('name').value }} +
+
+ + +
+
+ + +
+
+ + +
+ + + @if (resourceLwM2M.get('keyName').hasError('required') || + resourceLwM2M.get('keyName').hasError('pattern')) { + {{ 'device-profile.lwm2m.key-name-required' | translate }} + } + + + +
-
- - -
-
- - -
-
- - -
- - - - {{ 'device-profile.lwm2m.key-name-required' | translate }} - - - - - -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html index aa3ea82f64..46051f9cc5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html @@ -17,31 +17,33 @@ -->
- - - - {{ objectLwM2M.get('name').value}} #{{ objectLwM2M.get('keyId').value}} - - - - - - - - - - - + @for (objectLwM2M of modelsFormArray.controls; track trackByParams($index, objectLwM2M.value)) { + + + + {{ objectLwM2M.get('name').value }} #{{ objectLwM2M.get('keyId').value }} + + + + @if (!disabled && objectLwM2M.get('multiple').value) { + + } + + + + + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 3c97193480..ac445a3407 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -223,6 +223,7 @@ export interface ObservableAttributes { keyName: {}; attributeLwm2m: AttributesNameValueMap; observeStrategy: ObserveStrategy; + initAttrTelAsObsStrategy?: boolean; } export function getDefaultProfileObserveAttrConfig(): ObservableAttributes { 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 index 71996cc67d..9e2c976c52 100644 --- 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 @@ -19,156 +19,150 @@ {{ 'device-profile.mqtt-device-topic-filters-spark-plug' | translate }} -
- - + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value) { +
+ + + } -
-
- device-profile.mqtt-device-topic-filters -
-
- - 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}} - - - - device-profile.attributes-subscribe-topic-filter - - - {{ 'device-profile.attributes-subscribe-topic-filter-required' | translate}} - - - {{ 'device-profile.not-valid-single-character' | translate}} - - - {{ 'device-profile.not-valid-multi-character' | translate}} - - -
- - {{ 'device-profile.mqtt-device-topic-filters-unique' | translate }} - -
-
-
-
-
-
+@if (!mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value) { +
- device-profile.mqtt-device-payload-type + device-profile.mqtt-device-topic-filters
- - - - {{transportPayloadTypeTranslations.get(type) | translate}} - - - - {{ 'device-profile.mqtt-payload-type-required' | translate }} - - -
- - {{ 'device-profile.mqtt-enable-compatibility-with-json-payload-format' | translate }} - -
-
- - {{ 'device-profile.mqtt-use-json-format-for-default-downlink-topics' | translate }} - -
-
-
-
- - - - - {{ 'device-profile.telemetry-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.attributes-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-request-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-response-proto-schema-required' | translate}} - - +
+ + device-profile.telemetry-topic-filter + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceTelemetryTopic').hasError('required')) { + {{ 'device-profile.telemetry-topic-filter-required' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceTelemetryTopic').hasError('invalidSingleTopicCharacter')) { + {{ 'device-profile.not-valid-single-character' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceTelemetryTopic').hasError('invalidMultiTopicCharacter')) { + {{ 'device-profile.not-valid-multi-character' | translate }} + } + + + device-profile.attributes-topic-filter + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesTopic').hasError('required')) { + {{ 'device-profile.attributes-topic-filter-required' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesTopic').hasError('invalidSingleTopicCharacter')) { + {{ 'device-profile.not-valid-single-character' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesTopic').hasError('invalidMultiTopicCharacter')) { + {{ 'device-profile.not-valid-multi-character' | translate }} + } + + + device-profile.attributes-subscribe-topic-filter + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesSubscribeTopic').hasError('required')) { + {{ 'device-profile.attributes-subscribe-topic-filter-required' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesSubscribeTopic').hasError('invalidSingleTopicCharacter')) { + {{ 'device-profile.not-valid-single-character' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesSubscribeTopic').hasError('invalidMultiTopicCharacter')) { + {{ 'device-profile.not-valid-multi-character' | translate }} + } +
+ @if (mqttDeviceProfileTransportConfigurationFormGroup.hasError('unique')) { + {{ 'device-profile.mqtt-device-topic-filters-unique' | translate }} + } +
+
+
-
-
- - {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} - -
-
-
+
+
+ device-profile.mqtt-device-payload-type +
+ + + @for (type of transportPayloadTypes; track type) { + + {{ getTransportPayloadType(type) | translate }} + + } + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.transportPayloadType').hasError('required')) { + {{ 'device-profile.mqtt-payload-type-required' | translate }} + } + + @if (protoPayloadType) { +
+ + {{ 'device-profile.mqtt-enable-compatibility-with-json-payload-format' | translate }} + +
+ @if (compatibilityWithJsonPayloadFormatEnabled) { + + {{ 'device-profile.mqtt-use-json-format-for-default-downlink-topics' | translate }} + +
+ } +
+
+ + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')) { + {{ 'device-profile.telemetry-proto-schema-required' | translate }} + } + + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')) { + {{ 'device-profile.attributes-proto-schema-required' | translate }} + } + + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-request-proto-schema-required' | translate }} + } + + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-response-proto-schema-required' | translate }} + } +
+ } +
+
+
+
+ + {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} + +
+
+ +} 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 index bc997d6960..d1a6d8ace2 100644 --- 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 @@ -252,4 +252,8 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control } return null; } + + getTransportPayloadType(type: string): string { + return this.transportPayloadTypeTranslations.get(type as TransportPayloadType); + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html index 0d5d03067b..790c13c47e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html @@ -16,57 +16,66 @@ -->
-
-
- - device-profile.snmp.scope - - - {{ snmpSpecTypeTranslationMap.get(snmpSpecType) }} - - - - {{ 'device-profile.snmp.scope-required' | translate }} - - - -
- - device-profile.snmp.querying-frequency - - - {{ 'device-profile.snmp.querying-frequency-required' | translate }} - - - {{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }} - + @for (deviceProfileCommunication of communicationConfigFormArray.controls; track $index) { +
+
+ + device-profile.snmp.scope + + @for (snmpSpecType of snmpSpecTypes; track snmpSpecType) { + + {{ snmpSpecTypeTranslationMap.get(snmpSpecType) }} + + } + + @if (deviceProfileCommunication.get('spec').hasError('required')) { + {{ 'device-profile.snmp.scope-required' | translate }} + } - - -
+ +
+ @if (isShowFrequency(deviceProfileCommunication.get('spec').value)) { + + device-profile.snmp.querying-frequency + + @if (deviceProfileCommunication.get('queryingFrequencyMs').hasError('required')) { + {{ 'device-profile.snmp.querying-frequency-required' | translate }} + } + @if (deviceProfileCommunication.get('queryingFrequencyMs').hasError('pattern') || + deviceProfileCommunication.get('queryingFrequencyMs').hasError('min')) { + {{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }} + } + + } + + +
+
+ @if (!disabled) { + + }
- -
-
+ } + @if (!communicationConfigFormArray.controls.length && !disabled) { +
device-profile.snmp.please-add-communication-config -
-
- -
+
+ } + @if (!disabled && isAddEnabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts index 544fcfdac6..e2a60ad899 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts @@ -24,7 +24,7 @@ import { NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator, - Validators + Validators, FormArray, FormGroup } from '@angular/forms'; import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models'; import { Subject } from 'rxjs'; @@ -79,8 +79,8 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On this.destroy$.complete(); } - get communicationConfigFormArray(): UntypedFormArray { - return this.deviceProfileCommunicationConfig.get('communicationConfig') as UntypedFormArray; + get communicationConfigFormArray(): FormArray { + return this.deviceProfileCommunicationConfig.get('communicationConfig') as FormArray; } registerOnChange(fn: any): void { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html index edc9e44bf8..2f0ef4d0af 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html @@ -25,54 +25,70 @@
-
-
- - - - {{ dataTypesTranslationMap.get(dataType) | translate }} - - - - {{ 'device-profile.snmp.data-type-required' | translate }} - - - - - - {{ 'device-profile.snmp.data-key-required' | translate }} - - - - - - {{ 'device-profile.snmp.oid-required' | translate }} - - - {{ 'device-profile.snmp.oid-pattern' | translate }} - - - + } +
+
+ } + @if (!mappingsConfigFormArray.controls.length && !disabled) { +
+ device-profile.snmp.please-add-mapping-config +
+ } + @if (!disabled) { +
+
- -
- device-profile.snmp.please-add-mapping-config -
-
- -
+ } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html index 5e558a51dd..1813e6d35e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html @@ -17,27 +17,27 @@ -->
- + device-profile.snmp.timeout-ms - - {{ 'device-profile.snmp.timeout-ms-required' | translate }} - - - {{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }} - + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('required')) { + {{ 'device-profile.snmp.timeout-ms-required' | translate }} + } + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('pattern') || + snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('min')) { + {{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }} + } - + device-profile.snmp.retries - - {{ 'device-profile.snmp.retries-required' | translate }} - - - {{ 'device-profile.snmp.retries-invalid-format' | translate }} - + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('required')) { + {{ 'device-profile.snmp.retries-required' | translate }} + } + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('pattern') || + snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('min')) { + {{ 'device-profile.snmp.retries-invalid-format' | translate }} + }
device-profile.snmp.communication-configs
diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html index 18019f3532..1c462bb9b7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html @@ -16,40 +16,46 @@ -->
-
- - -
- - {{ getTitle(queuesControl.value.name) }} - - - -
-
- - - -
-
-
+ @for (queuesControl of queuesFormArray.controls; track trackByQueue($index, queuesControl); let isLast = $last) { +
+ + +
+ + {{ getTitle(queuesControl.value.name) }} + + + @if (!($index === 0) && !disabled) { + + } +
+
+ + + +
+
+ } + @if (!queuesFormArray.controls.length) { +
tenant-profile.no-queue -
-
- -
+ class="tb-prompt flex items-center justify-center">tenant-profile.no-queue +
+ } + @if (!disabled) { +
+ +
+ }
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 index 791b8c1096..4d7b127aa3 100644 --- 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 @@ -15,7 +15,7 @@ limitations under the License. --> - + tenant-profile.tenant-profile - - {{ displayTenantProfileFn(selectTenantProfileFormGroup.get('tenantProfile').value) }} - - - - + @if (showDetailsPageLink && selectTenantProfileFormGroup.get('tenantProfile').value && disabled) { + + {{ displayTenantProfileFn(selectTenantProfileFormGroup.get('tenantProfile').value) }} + + } + @if (selectTenantProfileFormGroup.get('tenantProfile').value && !disabled) { + + } + @if (selectTenantProfileFormGroup.get('tenantProfile').value?.id && !disabled) { + + } + @if (!selectTenantProfileFormGroup.get('tenantProfile').value && !disabled) { + + } - - - - -
-
- tenant-profile.no-tenant-profiles-found -
- - + @for (tenantProfile of filteredTenantProfiles | async; track tenantProfile) { + + + + } + @if (!(filteredTenantProfiles | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ tenant-profile.no-tenant-profiles-found +
+ } @else { + {{ translate.get('tenant-profile.no-tenant-profiles-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + } + + tenant-profile.create-new-tenant-profile - - - tenant-profile.create-new-tenant-profile - -
-
+
+
+ }
- - {{ 'tenant-profile.tenant-profile-required' | translate }} - + @if (selectTenantProfileFormGroup.get('tenantProfile').hasError('required')) { + + {{ '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 index 1bd785cf7a..f59d79cd4a 100644 --- 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 @@ -35,6 +35,7 @@ import { MatDialog } from '@angular/material/dialog'; import { TenantProfileDialogComponent, TenantProfileDialogData } from './tenant-profile-dialog.component'; import { emptyPageData } from '@shared/models/page/page-data'; import { getEntityDetailsPageURL } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-tenant-profile-autocomplete', @@ -71,6 +72,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, @Input() showDetailsPageLink = false; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Output() tenantProfileUpdated = new EventEmitter(); 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 index 7f86c1d259..19ef843762 100644 --- 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 @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
-
- - - -
- + + +
+ +
-
+}
- + tenant-profile.name - - {{ 'tenant-profile.name-required' | translate }} - - - {{ 'tenant-profile.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + + {{ 'tenant-profile.name-required' | translate }} + + } + @if (entityForm.get('name').hasError('maxlength')) { + + {{ 'tenant-profile.name-max-length' | translate }} + + }
@@ -66,20 +72,22 @@
- - - - {{'tenant-profile.queues-with-count' | translate: + @if (entityForm.get('isolatedTbRuleEngine').value) { + + + + {{'tenant-profile.queues-with-count' | translate: {count: entityForm.get('profileData').get('queueConfiguration').value ? - entityForm.get('profileData').get('queueConfiguration').value.length : 0} }} - - - - - - + entityForm.get('profileData').get('queueConfiguration').value.length : 0} }} + + + + + + + } @@ -95,9 +103,9 @@
- + tenant-profile.description - +
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 index e618e02560..ddd159388a 100644 --- 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 @@ -15,64 +15,80 @@ limitations under the License. --> -
+
{{ 'tenant-profile.entities' | translate }} tenant-profile.unlimited
- + tenant-profile.maximum-devices - - {{ 'tenant-profile.maximum-devices-required' | translate}} - - - {{ 'tenant-profile.maximum-devices-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxDevices').hasError('required')) { + + {{ 'tenant-profile.maximum-devices-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDevices').hasError('min')) { + + {{ 'tenant-profile.maximum-devices-range' | translate}} + + } - + tenant-profile.maximum-dashboards - - {{ 'tenant-profile.maximum-dashboards-required' | translate}} - - - {{ 'tenant-profile.maximum-dashboards-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxDashboards').hasError('required')) { + + {{ 'tenant-profile.maximum-dashboards-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDashboards').hasError('min')) { + + {{ 'tenant-profile.maximum-dashboards-range' | translate}} + + }
- + tenant-profile.maximum-assets - - {{ 'tenant-profile.maximum-assets-required' | translate}} - - - {{ 'tenant-profile.maximum-assets-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxAssets').hasError('required')) { + + {{ 'tenant-profile.maximum-assets-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxAssets').hasError('min')) { + + {{ 'tenant-profile.maximum-assets-range' | translate}} + + } - + tenant-profile.maximum-users - - {{ 'tenant-profile.maximum-users-required' | translate}} - - - {{ 'tenant-profile.maximum-users-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxUsers').hasError('required')) { + + {{ 'tenant-profile.maximum-users-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxUsers').hasError('min')) { + + {{ 'tenant-profile.maximum-users-range' | translate}} + + }
@@ -84,45 +100,57 @@
- + tenant-profile.maximum-customers - - {{ 'tenant-profile.maximum-customers-required' | translate}} - - - {{ 'tenant-profile.maximum-customers-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxCustomers').hasError('required')) { + + {{ 'tenant-profile.maximum-customers-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxCustomers').hasError('min')) { + + {{ 'tenant-profile.maximum-customers-range' | translate}} + + } - + tenant-profile.maximum-rule-chains - - {{ 'tenant-profile.maximum-rule-chains-required' | translate}} - - - {{ 'tenant-profile.maximum-rule-chains-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxRuleChains').hasError('required')) { + + {{ 'tenant-profile.maximum-rule-chains-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRuleChains').hasError('min')) { + + {{ 'tenant-profile.maximum-rule-chains-range' | translate}} + + }
- + tenant-profile.maximum-edges - - {{ 'tenant-profile.maximum-edges-required' | translate }} - - - {{ 'tenant-profile.maximum-edges-range' | translate }} - + @if (tenantProfileConfigurationForm.get('maxEdges').hasError('required')) { + + {{ 'tenant-profile.maximum-edges-required' | translate }} + + } + @if (tenantProfileConfigurationForm.get('maxEdges').hasError('min')) { + + {{ 'tenant-profile.maximum-edges-range' | translate }} + + }
@@ -136,30 +164,38 @@ {{ 'tenant-profile.rule-engine' | translate }} tenant-profile.unlimited
- + tenant-profile.max-r-e-executions - - {{ 'tenant-profile.max-r-e-executions-required' | translate}} - - - {{ 'tenant-profile.max-r-e-executions-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxREExecutions').hasError('required')) { + + {{ 'tenant-profile.max-r-e-executions-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxREExecutions').hasError('min')) { + + {{ 'tenant-profile.max-r-e-executions-range' | translate}} + + } - + tenant-profile.max-transport-messages - - {{ 'tenant-profile.max-transport-messages-required' | translate}} - - - {{ 'tenant-profile.max-transport-messages-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxTransportMessages').hasError('required')) { + + {{ 'tenant-profile.max-transport-messages-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxTransportMessages').hasError('min')) { + + {{ 'tenant-profile.max-transport-messages-range' | translate}} + + }
@@ -171,58 +207,74 @@
- + tenant-profile.max-j-s-executions - - {{ 'tenant-profile.max-j-s-executions-required' | translate}} - - - {{ 'tenant-profile.max-j-s-executions-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxJSExecutions').hasError('required')) { + + {{ 'tenant-profile.max-j-s-executions-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxJSExecutions').hasError('min')) { + + {{ 'tenant-profile.max-j-s-executions-range' | translate}} + + } - + tenant-profile.max-tbel-executions - - {{ 'tenant-profile.max-tbel-executions-required' | translate}} - - - {{ 'tenant-profile.max-tbel-executions-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxTbelExecutions').hasError('required')) { + + {{ 'tenant-profile.max-tbel-executions-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxTbelExecutions').hasError('min')) { + + {{ 'tenant-profile.max-tbel-executions-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}} - + @if (tenantProfileConfigurationForm.get('maxRuleNodeExecutionsPerMessage').hasError('required')) { + + {{ 'tenant-profile.max-rule-node-executions-per-message-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRuleNodeExecutionsPerMessage').hasError('min')) { + + {{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}} + + } - + tenant-profile.max-transport-data-points - - {{ 'tenant-profile.max-transport-data-points-required' | translate}} - - - {{ 'tenant-profile.max-transport-data-points-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxTransportDataPoints').hasError('required')) { + + {{ 'tenant-profile.max-transport-data-points-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxTransportDataPoints').hasError('min')) { + + {{ 'tenant-profile.max-transport-data-points-range' | translate}} + + }
@@ -234,45 +286,57 @@ {{ 'tenant-profile.calculated-fields' | translate }} tenant-profile.unlimited
- + tenant-profile.max-calculated-fields - - {{ 'tenant-profile.max-calculated-fields-required' | translate}} - - - {{ 'tenant-profile.max-calculated-fields-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxCalculatedFieldsPerEntity').hasError('required')) { + + {{ 'tenant-profile.max-calculated-fields-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxCalculatedFieldsPerEntity').hasError('min')) { + + {{ 'tenant-profile.max-calculated-fields-range' | translate}} + + } - + tenant-profile.max-data-points-per-rolling-arg - - {{ 'tenant-profile.max-data-points-per-rolling-arg-required' | translate}} - - - {{ 'tenant-profile.max-data-points-per-rolling-arg-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxDataPointsPerRollingArg').hasError('required')) { + + {{ 'tenant-profile.max-data-points-per-rolling-arg-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDataPointsPerRollingArg').hasError('min')) { + + {{ 'tenant-profile.max-data-points-per-rolling-arg-range' | translate}} + + }
- + tenant-profile.max-arguments-per-cf - - {{ 'tenant-profile.max-arguments-per-cf-required' | translate}} - - - {{ 'tenant-profile.max-arguments-per-cf-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxArgumentsPerCF').hasError('required')) { + + {{ 'tenant-profile.max-arguments-per-cf-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxArgumentsPerCF').hasError('min')) { + + {{ 'tenant-profile.max-arguments-per-cf-range' | translate}} + + }
@@ -285,33 +349,169 @@
- + tenant-profile.max-state-size - - {{ 'tenant-profile.max-state-size-required' | translate}} - - - {{ 'tenant-profile.max-state-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxStateSizeInKBytes').hasError('required')) { + + {{ 'tenant-profile.max-state-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxStateSizeInKBytes').hasError('min')) { + + {{ 'tenant-profile.max-state-size-range' | translate}} + + } - + tenant-profile.max-value-argument-size - - {{ 'tenant-profile.max-value-argument-size-required' | translate}} - - - {{ 'tenant-profile.max-value-argument-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxSingleValueArgumentSizeInKBytes').hasError('required')) { + + {{ 'tenant-profile.max-value-argument-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxSingleValueArgumentSizeInKBytes').hasError('min')) { + + {{ 'tenant-profile.max-value-argument-size-range' | translate}} + + }
+
+ + tenant-profile.max-related-level-per-argument + + @if (tenantProfileConfigurationForm.get('maxRelationLevelPerCfArgument').hasError('required')) { + + {{ 'tenant-profile.max-related-level-per-argument-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRelationLevelPerCfArgument').hasError('min')) { + + {{ 'tenant-profile.max-related-level-per-argument-range' | translate}} + + } + + + + tenant-profile.min-allowed-scheduled-update-interval + + @if (tenantProfileConfigurationForm.get('minAllowedScheduledUpdateIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('minAllowedScheduledUpdateIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-range' | translate}} + + } + + +
+
+ + tenant-profile.min-allowed-aggregation-interval + + @if (tenantProfileConfigurationForm.get('minAllowedAggregationIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.min-allowed-aggregation-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('minAllowedAggregationIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.min-allowed-aggregation-interval-range' | translate}} + + } + + + + tenant-profile.min-allowed-deduplication-interval + + @if (tenantProfileConfigurationForm.get('minAllowedDeduplicationIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.min-allowed-deduplication-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('minAllowedDeduplicationIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.min-allowed-deduplication-interval-range' | translate}} + + } + + +
+
+ + tenant-profile.intermediate-aggregation-interval + + @if (tenantProfileConfigurationForm.get('intermediateAggregationIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.intermediate-aggregation-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('intermediateAggregationIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.intermediate-aggregation-interval-range' | translate}} + + } + + + + tenant-profile.reevaluation-check-interval + + @if (tenantProfileConfigurationForm.get('cfReevaluationCheckInterval').hasError('required')) { + + {{ 'tenant-profile.reevaluation-check-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('cfReevaluationCheckInterval').hasError('min')) { + + {{ 'tenant-profile.reevaluation-check-interval-range' | translate}} + + } + + +
+
+ + tenant-profile.relation-search-entity-limit + + @if (tenantProfileConfigurationForm.get('maxRelatedEntitiesToReturnPerCfArgument').hasError('required')) { + + {{ 'tenant-profile.relation-search-entity-limit-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRelatedEntitiesToReturnPerCfArgument').hasError('min')) { + + {{ 'tenant-profile.relation-search-entity-limit-range' | translate}} + + } + tenant-profile.relation-search-entity-limit-hint + +
+
@@ -321,86 +521,110 @@ {{ 'tenant-profile.time-to-live' | translate }} tenant-profile.unlimited
- + 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}} - + @if (tenantProfileConfigurationForm.get('maxDPStorageDays').hasError('required')) { + + {{ 'tenant-profile.max-d-p-storage-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDPStorageDays').hasError('min')) { + + {{ 'tenant-profile.max-d-p-storage-days-range' | translate}} + + } - + tenant-profile.alarms-ttl-days - - {{ 'tenant-profile.alarms-ttl-days-required' | translate}} - - - {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('alarmsTtlDays').hasError('required')) { + + {{ 'tenant-profile.alarms-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('alarmsTtlDays').hasError('min')) { + + {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}} + + }
- + tenant-profile.default-storage-ttl-days - - {{ 'tenant-profile.default-storage-ttl-days-required' | translate}} - - - {{ 'tenant-profile.default-storage-ttl-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('defaultStorageTtlDays').hasError('required')) { + + {{ 'tenant-profile.default-storage-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('defaultStorageTtlDays').hasError('min')) { + + {{ 'tenant-profile.default-storage-ttl-days-range' | translate}} + + } - + tenant-profile.rpc-ttl-days - - {{ 'tenant-profile.rpc-ttl-days-required' | translate}} - - - {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('rpcTtlDays').hasError('required')) { + + {{ 'tenant-profile.rpc-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('rpcTtlDays').hasError('min')) { + + {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}} + + }
- + tenant-profile.queue-stats-ttl-days - - {{ 'tenant-profile.queue-stats-ttl-days-required' | translate}} - - - {{ 'tenant-profile.queue-stats-ttl-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('queueStatsTtlDays').hasError('required')) { + + {{ 'tenant-profile.queue-stats-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('queueStatsTtlDays').hasError('min')) { + + {{ 'tenant-profile.queue-stats-ttl-days-range' | translate}} + + } - + tenant-profile.rule-engine-exceptions-ttl-days - - {{ 'tenant-profile.rule-engine-exceptions-ttl-days-required' | translate}} - - - {{ 'tenant-profile.rule-engine-exceptions-ttl-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('ruleEngineExceptionsTtlDays').hasError('required')) { + + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('ruleEngineExceptionsTtlDays').hasError('min')) { + + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-range' | translate}} + + }
@@ -413,47 +637,81 @@ {{ 'tenant-profile.sms-enabled' | translate }} - - tenant-profile.max-sms - - - {{ 'tenant-profile.max-sms-required' | translate}} - - - {{ 'tenant-profile.max-sms-range' | translate}} - - + @if (tenantProfileConfigurationForm.get('smsEnabled').value) { + + tenant-profile.max-sms + + @if (tenantProfileConfigurationForm.get('maxSms').hasError('required')) { + + {{ 'tenant-profile.max-sms-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxSms').hasError('min')) { + + {{ 'tenant-profile.max-sms-range' | translate}} + + } + + }
- + tenant-profile.max-emails - - {{ 'tenant-profile.max-emails-required' | translate}} - - - {{ 'tenant-profile.max-emails-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxEmails').hasError('required')) { + + {{ 'tenant-profile.max-emails-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxEmails').hasError('min')) { + + {{ 'tenant-profile.max-emails-range' | translate}} + + } - + tenant-profile.max-created-alarms - - {{ 'tenant-profile.max-created-alarms-required' | translate}} - - - {{ 'tenant-profile.max-created-alarms-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxCreatedAlarms').hasError('required')) { + + {{ 'tenant-profile.max-created-alarms-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxCreatedAlarms').hasError('min')) { + + {{ 'tenant-profile.max-created-alarms-range' | translate}} + + }
+
+ + tenant-profile.alarms-reevaluation-interval + + @if (tenantProfileConfigurationForm.get('alarmsReevaluationInterval').hasError('required')) { + + {{ 'tenant-profile.alarms-reevaluation-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('alarmsReevaluationInterval').hasError('min')) { + + {{ 'tenant-profile.alarms-reevaluation-interval-range' | translate}} + + } + + +
+
@@ -461,14 +719,16 @@ {{ 'tenant-profile.debug' | translate }}
- + tenant-profile.maximum-debug-duration-min - - {{ 'tenant-profile.maximum-debug-duration-min-range' | translate }} - + @if (tenantProfileConfigurationForm.get('maxDebugModeDurationMinutes').hasError('min')) { + + {{ 'tenant-profile.maximum-debug-duration-min-range' | translate }} + + }
@@ -480,45 +740,57 @@ {{ 'tenant-profile.ota-files-in-bytes' | translate }} tenant-profile.unlimited
- + tenant-profile.maximum-resources-sum-data-size - - {{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}} - - - {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxResourcesInBytes').hasError('required')) { + + {{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxResourcesInBytes').hasError('min')) { + + {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} + + } - + tenant-profile.maximum-ota-packages-sum-data-size - - {{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}} - - - {{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxOtaPackagesInBytes').hasError('required')) { + + {{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxOtaPackagesInBytes').hasError('min')) { + + {{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}} + + }
- + tenant-profile.maximum-resource-size - - {{ 'tenant-profile.maximum-resource-size-required' | translate}} - - - {{ 'tenant-profile.maximum-resource-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxResourceSize').hasError('required')) { + + {{ 'tenant-profile.maximum-resource-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxResourceSize').hasError('min')) { + + {{ 'tenant-profile.maximum-resource-size-range' | translate}} + + }
@@ -530,35 +802,43 @@ {{ 'tenant-profile.ws-title' | translate }} tenant-profile.unlimited
- + tenant-profile.ws-limit-max-sessions-per-tenant - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerTenant').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } - + tenant-profile.ws-limit-max-subscriptions-per-tenant - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerTenant').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
- + tenant-profile.ws-limit-max-sessions-per-customer - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerCustomer').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } - + tenant-profile.ws-limit-max-subscriptions-per-customer - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerCustomer').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
@@ -569,44 +849,54 @@
- + tenant-profile.ws-limit-max-sessions-per-public-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerPublicUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } - + tenant-profile.ws-limit-max-subscriptions-per-public-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerPublicUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
- + tenant-profile.ws-limit-max-sessions-per-regular-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerRegularUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } - + tenant-profile.ws-limit-max-subscriptions-per-regular-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerRegularUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
- + tenant-profile.ws-limit-queue-per-session - - {{ 'tenant-profile.too-small-value-one' | translate}} - + @if (tenantProfileConfigurationForm.get('wsMsgQueueLimitPerSession').hasError('min')) { + + {{ 'tenant-profile.too-small-value-one' | translate}} + + } {{ 'tenant-profile.queue-size-is-limited-by-system-configuration' | translate}} 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 index 90f38ba13d..9fe6232f4c 100644 --- 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 @@ -14,16 +14,13 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, 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 { Component, forwardRef, Input } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { DefaultTenantProfileConfiguration, FormControlsFrom } from '@shared/models/tenant.model'; import { isDefinedAndNotNull, isUndefinedOrNull} from '@core/utils'; import { RateLimitsType } from './rate-limits/rate-limits.models'; -import { takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-default-tenant-profile-configuration', @@ -36,110 +33,112 @@ import { Subject } from 'rxjs'; }], standalone: false }) -export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { +export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor { - defaultTenantProfileConfigurationFormGroup: UntypedFormGroup; + tenantProfileConfigurationForm: FormGroup>; - private requiredValue: boolean; - private destroy$ = new Subject(); - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() + @coerceBoolean() disabled: boolean; rateLimitsType = RateLimitsType; - private propagateChange = (v: any) => { }; - - constructor(private store: Store, - private fb: UntypedFormBuilder) { - this.defaultTenantProfileConfigurationFormGroup = this.fb.group({ - maxDevices: [null, [Validators.required, Validators.min(0)]], - maxAssets: [null, [Validators.required, Validators.min(0)]], - maxCustomers: [null, [Validators.required, Validators.min(0)]], - maxUsers: [null, [Validators.required, Validators.min(0)]], - maxDashboards: [null, [Validators.required, Validators.min(0)]], - maxRuleChains: [null, [Validators.required, Validators.min(0)]], - maxEdges: [null, [Validators.required, Validators.min(0)]], - maxResourcesInBytes: [null, [Validators.required, Validators.min(0)]], - maxOtaPackagesInBytes: [null, [Validators.required, Validators.min(0)]], - maxResourceSize: [null, [Validators.required, Validators.min(0)]], - transportTenantMsgRateLimit: [null, []], - transportTenantTelemetryMsgRateLimit: [null, []], - transportTenantTelemetryDataPointsRateLimit: [null, []], - transportDeviceMsgRateLimit: [null, []], - transportDeviceTelemetryMsgRateLimit: [null, []], - transportDeviceTelemetryDataPointsRateLimit: [null, []], - transportGatewayMsgRateLimit: [null, []], - transportGatewayTelemetryMsgRateLimit: [null, []], - transportGatewayTelemetryDataPointsRateLimit: [null, []], - transportGatewayDeviceMsgRateLimit: [null, []], - transportGatewayDeviceTelemetryMsgRateLimit: [null, []], - transportGatewayDeviceTelemetryDataPointsRateLimit: [null, []], - tenantEntityExportRateLimit: [null, []], - tenantEntityImportRateLimit: [null, []], - tenantNotificationRequestsRateLimit: [null, []], - tenantNotificationRequestsPerRuleRateLimit: [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)]], - maxTbelExecutions: [null, [Validators.required, Validators.min(0)]], - maxDPStorageDays: [null, [Validators.required, Validators.min(0)]], - maxRuleNodeExecutionsPerMessage: [null, [Validators.required, Validators.min(0)]], - maxEmails: [null, [Validators.required, Validators.min(0)]], - maxSms: [null, []], - smsEnabled: [null, []], - maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]], - maxDebugModeDurationMinutes: [null, [Validators.min(0)]], - defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]], - alarmsTtlDays: [null, [Validators.required, Validators.min(0)]], - rpcTtlDays: [null, [Validators.required, Validators.min(0)]], - queueStatsTtlDays: [null, [Validators.required, Validators.min(0)]], - ruleEngineExceptionsTtlDays: [null, [Validators.required, Validators.min(0)]], - tenantServerRestLimitsConfiguration: [null, []], - customerServerRestLimitsConfiguration: [null, []], - maxWsSessionsPerTenant: [null, [Validators.min(0)]], - maxWsSessionsPerCustomer: [null, [Validators.min(0)]], - maxWsSessionsPerRegularUser: [null, [Validators.min(0)]], - maxWsSessionsPerPublicUser: [null, [Validators.min(0)]], - wsMsgQueueLimitPerSession: [null, [Validators.min(0)]], - maxWsSubscriptionsPerTenant: [null, [Validators.min(0)]], - maxWsSubscriptionsPerCustomer: [null, [Validators.min(0)]], - maxWsSubscriptionsPerRegularUser: [null, [Validators.min(0)]], - maxWsSubscriptionsPerPublicUser: [null, [Validators.min(0)]], - wsUpdatesPerSessionRateLimit: [null, []], - cassandraWriteQueryTenantCoreRateLimits: [null, []], - cassandraReadQueryTenantCoreRateLimits: [null, []], - cassandraWriteQueryTenantRuleEngineRateLimits: [null, []], - cassandraReadQueryTenantRuleEngineRateLimits: [null, []], - edgeEventRateLimits: [null, []], - edgeEventRateLimitsPerEdge: [null, []], - edgeUplinkMessagesRateLimits: [null, []], - edgeUplinkMessagesRateLimitsPerEdge: [null, []], - maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]], - maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]], - maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]], - maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]], - calculatedFieldDebugEventsRateLimit: [null, []], - maxSingleValueArgumentSizeInKBytes: [null, [Validators.required, Validators.min(0)]], + private propagateChange = (_v: any) => { }; + + constructor(private fb: FormBuilder) { + this.tenantProfileConfigurationForm = this.fb.group({ + maxDevices: [0, [Validators.required, Validators.min(0)]], + maxAssets: [0, [Validators.required, Validators.min(0)]], + maxCustomers: [0, [Validators.required, Validators.min(0)]], + maxUsers: [0, [Validators.required, Validators.min(0)]], + maxDashboards: [0, [Validators.required, Validators.min(0)]], + maxRuleChains: [0, [Validators.required, Validators.min(0)]], + maxEdges: [0, [Validators.required, Validators.min(0)]], + maxResourcesInBytes: [0, [Validators.required, Validators.min(0)]], + maxOtaPackagesInBytes: [0, [Validators.required, Validators.min(0)]], + maxResourceSize: [0, [Validators.required, Validators.min(0)]], + transportTenantMsgRateLimit: [''], + transportTenantTelemetryMsgRateLimit: [''], + transportTenantTelemetryDataPointsRateLimit: [''], + transportDeviceMsgRateLimit: [''], + transportDeviceTelemetryMsgRateLimit: [''], + transportDeviceTelemetryDataPointsRateLimit: [''], + transportGatewayMsgRateLimit: [''], + transportGatewayTelemetryMsgRateLimit: [''], + transportGatewayTelemetryDataPointsRateLimit: [''], + transportGatewayDeviceMsgRateLimit: [''], + transportGatewayDeviceTelemetryMsgRateLimit: [''], + transportGatewayDeviceTelemetryDataPointsRateLimit: [''], + tenantEntityExportRateLimit: [''], + tenantEntityImportRateLimit: [''], + tenantNotificationRequestsRateLimit: [''], + tenantNotificationRequestsPerRuleRateLimit: [''], + maxTransportMessages: [0, [Validators.required, Validators.min(0)]], + maxTransportDataPoints: [0, [Validators.required, Validators.min(0)]], + maxREExecutions: [0, [Validators.required, Validators.min(0)]], + maxJSExecutions: [0, [Validators.required, Validators.min(0)]], + maxTbelExecutions: [0, [Validators.required, Validators.min(0)]], + maxDPStorageDays: [0, [Validators.required, Validators.min(0)]], + maxRuleNodeExecutionsPerMessage: [0, [Validators.required, Validators.min(0)]], + maxEmails: [0, [Validators.required, Validators.min(0)]], + maxSms: [0], + smsEnabled: [false], + maxCreatedAlarms: [0, [Validators.required, Validators.min(0)]], + maxDebugModeDurationMinutes: [0, [Validators.min(0)]], + defaultStorageTtlDays: [0, [Validators.required, Validators.min(0)]], + alarmsTtlDays: [0, [Validators.required, Validators.min(0)]], + rpcTtlDays: [0, [Validators.required, Validators.min(0)]], + queueStatsTtlDays: [0, [Validators.required, Validators.min(0)]], + ruleEngineExceptionsTtlDays: [0, [Validators.required, Validators.min(0)]], + tenantServerRestLimitsConfiguration: [''], + customerServerRestLimitsConfiguration: [''], + maxWsSessionsPerTenant: [0, [Validators.min(0)]], + maxWsSessionsPerCustomer: [0, [Validators.min(0)]], + maxWsSessionsPerRegularUser: [0, [Validators.min(0)]], + maxWsSessionsPerPublicUser: [0, [Validators.min(0)]], + wsMsgQueueLimitPerSession: [0, [Validators.min(0)]], + maxWsSubscriptionsPerTenant: [0, [Validators.min(0)]], + maxWsSubscriptionsPerCustomer: [0, [Validators.min(0)]], + maxWsSubscriptionsPerRegularUser: [0, [Validators.min(0)]], + maxWsSubscriptionsPerPublicUser: [0, [Validators.min(0)]], + wsUpdatesPerSessionRateLimit: [''], + cassandraWriteQueryTenantCoreRateLimits: [''], + cassandraReadQueryTenantCoreRateLimits: [''], + cassandraWriteQueryTenantRuleEngineRateLimits: [''], + cassandraReadQueryTenantRuleEngineRateLimits: [''], + edgeEventRateLimits: [''], + edgeEventRateLimitsPerEdge: [''], + edgeUplinkMessagesRateLimits: [''], + edgeUplinkMessagesRateLimitsPerEdge: [''], + maxCalculatedFieldsPerEntity: [0, [Validators.required, Validators.min(0)]], + maxArgumentsPerCF: [0, [Validators.required, Validators.min(0)]], + maxRelationLevelPerCfArgument: [1, [Validators.required, Validators.min(1)]], + minAllowedDeduplicationIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + minAllowedAggregationIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + maxRelatedEntitiesToReturnPerCfArgument: [1, [Validators.required, Validators.min(1)]], + minAllowedScheduledUpdateIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + intermediateAggregationIntervalInSecForCF: [0, [Validators.required, Validators.min(1)]], + cfReevaluationCheckInterval: [0, [Validators.required, Validators.min(1)]], + alarmsReevaluationInterval: [0, [Validators.required, Validators.min(1)]], + maxDataPointsPerRollingArg: [0, [Validators.required, Validators.min(0)]], + maxStateSizeInKBytes: [0, [Validators.required, Validators.min(0)]], + calculatedFieldDebugEventsRateLimit: [''], + maxSingleValueArgumentSizeInKBytes: [0, [Validators.required, Validators.min(0)]], }); - this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe( - takeUntil(this.destroy$) + this.tenantProfileConfigurationForm.get('smsEnabled').valueChanges.pipe( + takeUntilDestroyed() ).subscribe((value: boolean) => { this.maxSmsValidation(value); } ); - this.defaultTenantProfileConfigurationFormGroup.valueChanges.pipe( - takeUntil(this.destroy$) + this.tenantProfileConfigurationForm.valueChanges.pipe( + takeUntilDestroyed() ).subscribe(() => { this.updateModel(); }); @@ -147,34 +146,26 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA private maxSmsValidation(smsEnabled: boolean) { if (smsEnabled) { - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').addValidators([Validators.required, Validators.min(0)]); + this.tenantProfileConfigurationForm.get('maxSms').addValidators([Validators.required, Validators.min(0)]); } else { - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').clearValidators(); + this.tenantProfileConfigurationForm.get('maxSms').clearValidators(); } - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').updateValueAndValidity({emitEvent: false}); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + this.tenantProfileConfigurationForm.get('maxSms').updateValueAndValidity({emitEvent: false}); } registerOnChange(fn: any): void { this.propagateChange = fn; } - registerOnTouched(fn: any): void { - } - - ngOnInit() { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { - this.defaultTenantProfileConfigurationFormGroup.disable({emitEvent: false}); + this.tenantProfileConfigurationForm.disable({emitEvent: false}); } else { - this.defaultTenantProfileConfigurationFormGroup.enable({emitEvent: false}); + this.tenantProfileConfigurationForm.enable({emitEvent: false}); } } @@ -184,14 +175,14 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA value.smsEnabled = true; } this.maxSmsValidation(value.smsEnabled); - this.defaultTenantProfileConfigurationFormGroup.patchValue(value, {emitEvent: false}); + this.tenantProfileConfigurationForm.patchValue(value, {emitEvent: false}); } } private updateModel() { - let configuration: TenantProfileConfiguration = null; - if (this.defaultTenantProfileConfigurationFormGroup.valid) { - configuration = this.defaultTenantProfileConfigurationFormGroup.getRawValue(); + let configuration: DefaultTenantProfileConfiguration = null; + if (this.tenantProfileConfigurationForm.valid) { + configuration = this.tenantProfileConfigurationForm.getRawValue(); } this.propagateChange(configuration); } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html index 5d7fc9876f..370849422e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html @@ -35,9 +35,11 @@ cdkFocusInitial> {{ 'action.cancel' | translate }} - + @if (!data.readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html index 617a8beaee..159dcc6540 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html @@ -16,51 +16,56 @@ -->
-
- @if($index > 0) { -
tenant-profile.rate-limits.and-also-less-than
- } -
- - tenant-profile.rate-limits.number-of-messages - - - @if (rateLimit.get('value').hasError('required')) { - tenant-profile.rate-limits.number-of-messages-required - } @else if(rateLimit.get('value').hasError('min')) { - tenant-profile.rate-limits.number-of-messages-min + @for (rateLimit of rateLimitsFormArray.controls; track rateLimit) { +
+ @if($index > 0) { +
tenant-profile.rate-limits.and-also-less-than
+ } +
+ + tenant-profile.rate-limits.number-of-messages + + + @if (rateLimit.get('value').hasError('required')) { + tenant-profile.rate-limits.number-of-messages-required + } @else if(rateLimit.get('value').hasError('min')) { + tenant-profile.rate-limits.number-of-messages-min + } + + + tenant-profile.rate-limits.per-seconds + + + @if (rateLimit.get('time').hasError('required')) { + tenant-profile.rate-limits.per-seconds-required + } @else if (rateLimit.get('time').hasError('min')) { + tenant-profile.rate-limits.per-seconds-min + } @else if (rateLimit.get('time').hasError('duplicateTime')) { + tenant-profile.rate-limits.per-seconds-duplicate + } + + @if (!rateLimitsFormArray.disabled) { + } - - - tenant-profile.rate-limits.per-seconds - - - @if (rateLimit.get('time').hasError('required')) { - tenant-profile.rate-limits.per-seconds-required - } @else if (rateLimit.get('time').hasError('min')) { - tenant-profile.rate-limits.per-seconds-min - } @else if (rateLimit.get('time').hasError('duplicateTime')) { - tenant-profile.rate-limits.per-seconds-duplicate - } - - +
-
- + } + @if (!rateLimitsFormArray.disabled) { + + }
tenant-profile.rate-limits.preview diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html index 39d97ae424..f0dd188aa6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html @@ -24,10 +24,12 @@
- + @if (!disabled) { + + }
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 index a99101afa3..a72c51f430 100644 --- 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 @@ -16,12 +16,14 @@ -->
-
- - - - +
+ @switch (type) { + @case (tenantProfileType.DEFAULT) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html index 1dd2a79e27..94803a8739 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -16,18 +16,24 @@ -->
- + admin.queue-name - - {{ 'queue.name-required' | translate }} - - - {{ 'queue.name-unique' | translate }} - - - {{ 'queue.name-pattern' | translate }} - + @if (queueFormGroup.get('name').hasError('required')) { + + {{ 'queue.name-required' | translate }} + + } + @if (queueFormGroup.get('name').hasError('unique')) { + + {{ 'queue.name-unique' | translate }} + + } + @if (queueFormGroup.get('name').hasError('pattern')) { + + {{ 'queue.name-pattern' | translate }} + + } @@ -41,30 +47,38 @@
- - {{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }} - - - + @for (strategy of submitStrategies; track strategy) { + + {{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }} + + + + }
-
- - - queue.batch-size - - - {{ 'queue.batch-size-required' | translate }} - - - {{ 'queue.batch-size-min-value' | translate }} - - -
+ @if (hideBatchSize) { +
+ + + queue.batch-size + + @if (queueFormGroup.get('submitStrategy.batchSize').hasError('required')) { + + {{ 'queue.batch-size-required' | translate }} + + } + @if (queueFormGroup.get('submitStrategy.batchSize').hasError('min') && + !queueFormGroup.get('submitStrategy.batchSize').hasError('required')) { + + {{ 'queue.batch-size-min-value' | translate }} + + } + +
+ }
@@ -78,8 +92,9 @@
- - + + @for (strategy of processingStrategies; track strategy) { + {{ queueProcessingStrategyTypesMap.get(queueProcessingStrategyTypes[strategy]).label | translate }} - + } +
- + queue.retries - - {{ 'queue.retries-required' | translate }} - - - {{ 'queue.retries-min-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.retries').hasError('required')) { + + {{ 'queue.retries-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.retries').hasError('min') && + !queueFormGroup.get('processingStrategy.retries').hasError('required')) { + + {{ 'queue.retries-min-value' | translate }} + + } - + queue.failure-percentage - - {{ 'queue.failure-percentage-required' | translate }} - - - {{ 'queue.failure-percentage-min-value' | translate }} - - - {{ 'queue.failure-percentage-max-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.failurePercentage').hasError('required')) { + + {{ 'queue.failure-percentage-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.failurePercentage').hasError('min') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('max')) { + + {{ 'queue.failure-percentage-min-value' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.failurePercentage').hasError('max') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('min')) { + + {{ 'queue.failure-percentage-max-value' | translate }} + + } - + queue.pause-between-retries - - {{ 'queue.pause-between-retries-required' | translate }} - - - {{ 'queue.pause-between-retries-min-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')) { + + {{ 'queue.pause-between-retries-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('min') && + !queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')) { + + {{ 'queue.pause-between-retries-min-value' | translate }} + + } - + queue.max-pause-between-retries - - {{ 'queue.max-pause-between-retries-required' | translate }} - - - {{ 'queue.max-pause-between-retries-min-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')) { + + {{ 'queue.max-pause-between-retries-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('min') && + !queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')) { + + {{ 'queue.max-pause-between-retries-min-value' | translate }} + + }
@@ -160,27 +194,35 @@
- + queue.poll-interval - - {{ 'queue.poll-interval-required' | translate }} - - - {{ 'queue.poll-interval-min-value' | translate }} - + @if (queueFormGroup.get('pollInterval').hasError('required')) { + + {{ 'queue.poll-interval-required' | translate }} + + } + @if (queueFormGroup.get('pollInterval').hasError('min') && + !queueFormGroup.get('pollInterval').hasError('required')) { + + {{ 'queue.poll-interval-min-value' | translate }} + + } - + queue.partitions - - {{ 'queue.partitions-required' | translate }} - - - {{ 'queue.partitions-min-value' | translate }} - + @if (queueFormGroup.get('partitions').hasError('required')) { + + {{ 'queue.partitions-required' | translate }} + + } + @if (queueFormGroup.get('partitions').hasError('min') && + !queueFormGroup.get('partitions').hasError('required')) { + + {{ 'queue.partitions-min-value' | translate }} + + }
@@ -190,16 +232,20 @@ {{ 'queue.consumer-per-partition' | translate }} - + queue.processing-timeout - - {{ 'queue.pack-processing-timeout-required' | translate }} - - - {{ 'queue.pack-processing-timeout-min-value' | translate }} - + @if (queueFormGroup.get('packProcessingTimeout').hasError('required')) { + + {{ 'queue.pack-processing-timeout-required' | translate }} + + } + @if (queueFormGroup.get('packProcessingTimeout').hasError('min') && + !queueFormGroup.get('packProcessingTimeout').hasError('required')) { + + {{ 'queue.pack-processing-timeout-min-value' | translate }} + + }
@@ -211,14 +257,14 @@ {{ 'queue.duplicate-msg-to-all-partitions' | translate }} - + queue.custom-properties - + queue.custom-properties-hint - + queue.description - + queue.description-hint 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 index 6b63c75a35..ffbfb48cf3 100644 --- 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 @@ -25,20 +25,25 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
+ [required]="true"> {{(direction === entitySearchDirection.FROM ? - 'relation.to-entity' : 'relation.from-entity') | translate}} + 'relation.to-entity' : 'relation.from-entity') | translate}} - + @if (submitted && + additionalInfo.invalid) { + + }
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 index 99444c2d2f..71166057d5 100644 --- 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 @@ -18,40 +18,46 @@
-
+ @if (enableNotOption) { +
+ }
{{ 'relation.relation-type' | translate }}
{{ 'entity.entity-types' | translate }}
-
- - {{ 'relation.not' | translate }} - - - - - -
- + @for (relationFilterControl of relationFiltersFormArray.controls; track relationFilterControl) { +
+ @if (enableNotOption) { + + {{ 'relation.not' | translate }} + + } + + + + +
+ +
-
+ }
relation.any-relation
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 index d4f773a8c2..3984d85077 100644 --- 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 @@ -19,17 +19,19 @@
-
+
{{(direction == directions.FROM ? 'relation.from-relations' : 'relation.to-relations') | translate}} - + relation.direction - - {{ directionTypeTranslations.get(directions[type]) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(directions[type]) | translate }} + + }
@@ -87,7 +89,7 @@
+ matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> {{ 'relation.type' | translate }} - - {{ relation.type }} - - - - {{ 'relation.to-entity-type' | translate }} - - {{ relation.toEntityTypeName }} - - - - {{ 'relation.to-entity-name' | translate }} - - - {{ relation.toName | customTranslate }} - - - - - {{ 'relation.from-entity-type' | translate }} - - {{ relation.fromEntityTypeName }} - - - - {{ 'relation.from-entity-name' | translate }} - - - {{ relation.fromName | customTranslate }} - + +
+ {{ relation.type }} + + +
+ @if (direction === directions.FROM) { + + {{ 'relation.to-entity-type' | translate }} + + {{ relation.toEntityTypeName }} + + + } + @if (direction === directions.FROM) { + + {{ 'relation.to-entity-name' | translate }} + + @if(relation.entityURL){ + + {{ relation.toName | customTranslate }} + + } @else { + {{ relation.toName | customTranslate }} + } + + + } + @if (direction === directions.TO) { + + {{ 'relation.from-entity-type' | translate }} + + {{ relation.fromEntityTypeName }} + + + } + @if (direction === directions.TO) { + + {{ 'relation.from-entity-name' | translate }} + + @if(relation.entityURL){ + + {{ relation.fromName | customTranslate }} + + } @else { + {{ relation.fromName | customTranslate }} + } + + + } @@ -159,16 +189,16 @@ + *matRowDef="let relation; columns: displayedColumns;" (click)="dataSource.selection.toggle(relation)">
{{ 'relation.no-relations-text' }} + class="no-data-found flex items-center justify-center" translate>{{ 'relation.no-relations-text' }}
+ [hidePageSize]="hidePageSize">
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 index ec44e6987b..13ca6b5dd3 100644 --- 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 @@ -59,6 +59,17 @@ overflow: hidden; text-overflow: ellipsis; } + + .type-copy { + visibility: hidden; + transition: visibility 0.1s; + } + + .type:hover { + .type-copy { + visibility: visible; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html index fa7efc278a..a5757e7e58 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -25,9 +25,12 @@ close
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
response[0]) ).subscribe(result => this.dialogRef.close(result)); } else { if (resource.resourceType !== ResourceType.GENERAL) { delete resource.descriptor; } - this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result)); + this.resourceService.uploadResource(resource).subscribe(result => this.dialogRef.close(result)); } } } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index 893dbf52f9..b4c2901852 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -15,62 +15,72 @@ limitations under the License. --> -
- - - -
- + + +
+ +
-
+}
- + resource.resource-type - - {{ resourceTypesTranslationMap.get(resourceType) | translate }} - + @for (resourceType of resourceTypes ; track resourceType) { + + {{ resourceTypesTranslationMap.get(resourceType) | translate }} + + } - - resource.title - - - {{ 'resource.title-required' | translate }} - - - {{ 'resource.title-max-length' | translate }} - - + @if (entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd) { + + resource.title + + @if (entityForm.get('title').hasError('required')) { + + {{ 'resource.title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'resource.title-max-length' | translate }} + + } + + } @if (isAdd || ((isAdd || isEdit) && entityForm.get('resourceType').value === resourceType.GENERAL)) { } @else {
- + resource.file-name diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts index 8bb8f05359..535eaf2e5e 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts @@ -90,7 +90,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme return this.fb.group({ title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required], - fileName: [entity ? entity.fileName : null, Validators.required], + fileName: [entity ? entity.fileName : null], data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], descriptor: this.fb.group({ mediaType: [''] diff --git a/ui-ngx/src/app/modules/home/components/router-tabs.component.html b/ui-ngx/src/app/modules/home/components/router-tabs.component.html index 0c9668d848..c71256897d 100644 --- a/ui-ngx/src/app/modules/home/components/router-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/router-tabs.component.html @@ -17,18 +17,24 @@ -->
- + @if (!routerOutlet?.activatedRouteData?.hideTabs && !hideCurrentTabs && (tabs$ | async).length > 1) { + + }
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 index df765fcec8..c753c9cb11 100644 --- 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 @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ labelText | translate }} - - {{ displayRuleChainFn(selectRuleChainFormGroup.get('ruleChainId').value) }} - - + @if (selectRuleChainFormGroup.get('ruleChainId').value && disabled) { + + {{ displayRuleChainFn(selectRuleChainFormGroup.get('ruleChainId').value) }} + + } + @if (selectRuleChainFormGroup.get('ruleChainId').value && !disabled) { + + } - - - - -
-
- rulechain.no-rulechains-text -
- - + @for (ruleChain of filteredRuleChains | async; track ruleChain) { + + + + } + @if (!(filteredRuleChains | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ rulechain.no-rulechains-text +
+ } @else { + {{ translate.get('rulechain.no-rulechains-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + } + + rulechain.create-new-rulechain - - - rulechain.create-new-rulechain - -
-
+
+
+ }
- - {{ 'rulechain.rulechain-required' | translate }} - + @if (selectRuleChainFormGroup.get('ruleChainId').hasError('required')) { + + {{ '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 index b0d83cac46..e3cc793cc8 100644 --- 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 @@ -31,6 +31,7 @@ import { RuleChainService } from '@core/http/rule-chain.service'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { RuleChainType } from '@app/shared/models/rule-chain.models'; import { getEntityDetailsPageURL } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-rule-chain-autocomplete', @@ -58,6 +59,9 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI @Input() ruleChainType: RuleChainType = RuleChainType.CORE; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html index d4213b95e5..7543feb3ad 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html @@ -17,7 +17,7 @@ -->
{{ title }}
- + rule-node-config.save-time-series.strategy @for (strategy of processingStrategies; track strategy) { @@ -34,6 +34,7 @@ maxErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}" [minTime]="1" [maxTime]="maxDeduplicateTime" + [appearance]="appearance" formControlName="deduplicationIntervalSecs"> } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts index 22cdec0c82..c569d3270f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts @@ -32,6 +32,7 @@ import { } from '@home/components/rule-node/action/timeseries-config.models'; import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-advanced-processing-setting-row', @@ -52,6 +53,9 @@ export class AdvancedProcessingSettingRowComponent implements ControlValueAccess @Input() title: string; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + processingSettingRowForm = this.fb.group({ type: [defaultAdvancedProcessingConfig.type], deduplicationIntervalSecs: [{value: 60, disabled: true}] diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html index ab70346ff7..0136d08250 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html @@ -19,26 +19,41 @@ + > - - - - - + @if (timeseries) { + + } + @if (attributes) { + + } + @if (latest) { + + } + @if (webSockets) { + + } + @if (calculatedFields) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html index b50241b5cd..64a23c30e6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html @@ -17,13 +17,15 @@ -->
- + rule-node-config.customer-name-pattern - - {{ 'rule-node-config.customer-name-pattern-required' | translate }} - + @if (assignCustomerConfigForm.get('customerNamePattern').hasError('required') || + assignCustomerConfigForm.get('customerNamePattern').hasError('pattern')) { + + {{ 'rule-node-config.customer-name-pattern-required' | translate }} + + } rule-node-config.customer-name-pattern-hint
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html index ef4c5ee8c2..51ac7c34b6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html @@ -28,7 +28,7 @@
@if(!attributesConfigForm.get('processingSettings.isAdvanced').value) { - + rule-node-config.save-attribute.strategy @for (strategy of processingStrategies; track strategy) { @@ -39,6 +39,7 @@ @if(attributesConfigForm.get('processingSettings.type').value === ProcessingType.DEDUPLICATE) {
- + {{ 'rule-node-config.attributes-scope' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + } - + {{ 'rule-node-config.attributes-scope-value' | translate }}
-
- - {{ 'rule-node-config.send-attributes-updated-notification' | translate }} - -
-
- - {{ 'rule-node-config.notify-device' | translate }} - -
+ @if (attributesConfigForm.get('scope').value !== attributeScopeMap.CLIENT_SCOPE) { +
+ + {{ 'rule-node-config.send-attributes-updated-notification' | translate }} + +
+ } + @if (attributesConfigForm.get('scope').value === attributeScopeMap.SHARED_SCOPE) { +
+ + {{ 'rule-node-config.notify-device' | translate }} + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts index ab43627104..5a3d03ee83 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts @@ -41,7 +41,7 @@ import { export class AttributesConfigComponent extends RuleNodeConfigurationComponent { attributeScopeMap = AttributeScope; - attributeScopes = Object.keys(AttributeScope); + attributeScopes = Object.values(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; ProcessingType = ProcessingType; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html index 328554136a..ab3e54ccdc 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html @@ -16,52 +16,60 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (clearAlarmConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (clearAlarmConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + }
- + rule-node-config.alarm-type - - {{ 'rule-node-config.alarm-type-required' | translate }} - + @if (clearAlarmConfigForm.get('alarmType').hasError('required')) { + + {{ 'rule-node-config.alarm-type-required' | translate }} + + } rule-node-config.general-pattern-hint
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html index f5e933a6d5..6b4ec383a1 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html @@ -19,110 +19,137 @@ {{ 'rule-node-config.use-message-alarm-data' | translate }} - - {{ 'rule-node-config.overwrite-alarm-details' | translate }} - -
- - - - - - - -
- -
-
-
- - rule-node-config.alarm-type - - - {{ 'rule-node-config.alarm-type-required' | translate }} - - rule-node-config.general-pattern-hint - - - {{ 'rule-node-config.use-alarm-severity-pattern' | translate }} - - - rule-node-config.alarm-severity - - - {{ alarmSeverityTranslationMap.get(severity) | translate }} - - - - {{ 'rule-node-config.alarm-severity-required' | translate }} - - - - rule-node-config.alarm-severity-pattern - - - {{ 'rule-node-config.alarm-severity-required' | translate }} - - - - - {{ 'rule-node-config.propagate' | translate }} + @if (createAlarmConfigForm.get('useMessageAlarmData').value === true) { + + {{ 'rule-node-config.overwrite-alarm-details' | translate }} -
- - rule-node-config.relation-types-list - - - {{key}} - close - - - - rule-node-config.relation-types-list-hint + } + @if (createAlarmConfigForm.get('useMessageAlarmData').value === false || + createAlarmConfigForm.get('overwriteAlarmDetails').value === true) { +
+ @if (tbelEnabled) { + + } + @if (createAlarmConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (createAlarmConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + } +
+ +
+
+ } + @if (createAlarmConfigForm.get('useMessageAlarmData').value === false) { +
+ + rule-node-config.alarm-type + + @if (createAlarmConfigForm.get('alarmType').hasError('required')) { + + {{ 'rule-node-config.alarm-type-required' | translate }} + + } + rule-node-config.general-pattern-hint + + {{ 'rule-node-config.use-alarm-severity-pattern' | translate }} + + @if (!createAlarmConfigForm.get('dynamicSeverity').value) { + + rule-node-config.alarm-severity + + @for (severity of alarmSeverities; track severity) { + + {{ alarmSeverityTranslationMap.get(severity) | translate }} + + } + + @if (createAlarmConfigForm.get('severity').hasError('required')) { + + {{ 'rule-node-config.alarm-severity-required' | translate }} + + } + + } + @if (createAlarmConfigForm.get('dynamicSeverity').value) { + + rule-node-config.alarm-severity-pattern + + @if (createAlarmConfigForm.get('severity').hasError('required')) { + + {{ 'rule-node-config.alarm-severity-required' | translate }} + + } + + + } + + {{ 'rule-node-config.propagate' | translate }} + + @if (createAlarmConfigForm.get('propagate').value === true) { +
+ + rule-node-config.relation-types-list + + @for (key of createAlarmConfigForm.get('relationTypes').value; track key) { + + {{key}} + close + + } + + + rule-node-config.relation-types-list-hint + +
+ } + + {{ 'rule-node-config.propagate-to-owner' | translate }} + + + {{ 'rule-node-config.propagate-to-tenant' | translate }} +
- - {{ 'rule-node-config.propagate-to-owner' | translate }} - - - {{ 'rule-node-config.propagate-to-tenant' | translate }} - -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts index cd2122a8dd..06cf94b74d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts @@ -42,7 +42,7 @@ export class CreateAlarmConfigComponent extends RuleNodeConfigurationComponent { @ViewChild('jsFuncComponent', {static: false}) jsFuncComponent: JsFuncComponent; @ViewChild('tbelFuncComponent', {static: false}) tbelFuncComponent: JsFuncComponent; - alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverities = Object.values(AlarmSeverity); alarmSeverityTranslationMap = alarmSeverityTranslations; createAlarmConfigForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html index b7b72ef390..a50b62b26d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html @@ -19,15 +19,18 @@
rule-node-config.relation-parameters
- + relation.direction - - {{ directionTypeTranslations.get(type) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} + + } @@ -38,6 +41,7 @@
rule-node-config.target-entity
- - {{ entityTypeNamePatternTranslation.get(createRelationConfigForm.get('entityType').value) | translate }} - - + @if (createRelationConfigForm.get('entityType').value && + createRelationConfigForm.get('entityType').value !== entityType.TENANT) { + + {{ entityTypeNamePatternTranslation.get(createRelationConfigForm.get('entityType').value) | translate }} + + + } - - rule-node-config.profile-name - - + @if (createRelationConfigForm.get('entityType').value === entityType.DEVICE || + createRelationConfigForm.get('entityType').value === entityType.ASSET) { + + rule-node-config.profile-name + + + }
- + @if (createRelationConfigForm.get('entityType').value === entityType.CUSTOMER || + createRelationConfigForm.get('entityType').value === entityType.DEVICE || + createRelationConfigForm.get('entityType').value === entityType.ASSET) { + + } -
- - {{ 'rule-node-config.create-entity-if-not-exists' | translate }} - -
+ @if (createRelationConfigForm.get('entityType').value === entityType.CUSTOMER || + createRelationConfigForm.get('entityType').value === entityType.DEVICE || + createRelationConfigForm.get('entityType').value === entityType.ASSET) { +
+ + {{ 'rule-node-config.create-entity-if-not-exists' | translate }} + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts index 5d3e85e565..1939b4abbf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts @@ -28,7 +28,7 @@ import { EntityType } from '@app/shared/models/entity-type.models'; }) export class CreateRelationConfigComponent extends RuleNodeConfigurationComponent { - directionTypes = Object.keys(EntitySearchDirection); + directionTypes = Object.values(EntitySearchDirection); directionTypeTranslations = new Map( [ [EntitySearchDirection.FROM, 'rule-node-config.search-direction-from'], diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html index 165510c657..a260f16b96 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html @@ -20,16 +20,18 @@
- + {{ 'rule-node-config.attributes-scope' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + } - + {{ 'rule-node-config.attributes-scope-value' | translate }}
- + {{ 'rule-node-config.attributes-keys' | translate }} - - {{key}} - close - + @for (key of deleteAttributesConfigForm.get('keys').value; track key) { + + {{key}} + close + + } - {{ 'rule-node-config.attributes-keys-required' | translate }} + @if (deleteAttributesConfigForm.get('keys').hasError('required')) { + {{ 'rule-node-config.attributes-keys-required' | translate }} + } rule-node-config.general-pattern-hint @@ -77,13 +82,14 @@ {{ 'rule-node-config.send-attributes-deleted-notification' | translate }}
-
- - {{ 'rule-node-config.notify-device' | translate }} - -
+ @if (deleteAttributesConfigForm.get('scope').value === attributeScopeMap.SHARED_SCOPE) { +
+ + {{ 'rule-node-config.notify-device' | translate }} + +
+ } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts index 565b5e9dea..90818f9ceb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts @@ -33,7 +33,7 @@ export class DeleteAttributesConfigComponent extends RuleNodeConfigurationCompon deleteAttributesConfigForm: UntypedFormGroup; attributeScopeMap = AttributeScope; - attributeScopes = Object.keys(AttributeScope); + attributeScopes = Object.values(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html index dd6c2fab93..5c1517804d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html @@ -19,15 +19,18 @@
rule-node-config.relation-parameters
- + relation.direction - - {{ directionTypeTranslations.get(type) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} + + } @@ -40,23 +43,31 @@ {{ 'rule-node-config.delete-relation-with-specific-entity' | translate }}
-
-
- - - - {{ entityTypeNamePatternTranslation.get(deleteRelationConfigForm.get('entityType').value) | translate }} - - + @if (deleteRelationConfigForm.get('deleteForSingleEntity').value) { +
+
+ + + @if (deleteRelationConfigForm.get('entityType').value && deleteRelationConfigForm.get('entityType').value !== entityType.TENANT) { + + {{ entityTypeNamePatternTranslation.get(deleteRelationConfigForm.get('entityType').value) | translate }} + + + } +
+ @if (deleteRelationConfigForm.get('entityType').value && deleteRelationConfigForm.get('entityType').value !== entityType.TENANT) { + + }
- -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts index 606035bd08..d41def61d3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts @@ -28,7 +28,7 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar }) export class DeleteRelationConfigComponent extends RuleNodeConfigurationComponent { - directionTypes = Object.keys(EntitySearchDirection); + directionTypes = Object.values(EntitySearchDirection); directionTypeTranslations = new Map( [ diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html index c2e9a00a6e..bdb412f385 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html @@ -16,12 +16,14 @@ -->
- - {{ 'rule-node-config.select-device-connectivity-event' | translate }} - - - {{ messageTypeNames.get(eventOption) }} - - - + + {{ 'rule-node-config.select-device-connectivity-event' | translate }} + + @for (eventOption of eventOptions; track eventOption) { + + {{ messageTypeNames.get(eventOption) }} + + } + +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html index 4463973874..d0d91e0bb1 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html @@ -19,25 +19,33 @@
rule-node-config.generation-parameters
- + rule-node-config.message-count - - {{ 'rule-node-config.message-count-required' | translate }} - - - {{ 'rule-node-config.min-message-count-message' | translate }} - + @if (generatorConfigForm.get('msgCount').hasError('required')) { + + {{ 'rule-node-config.message-count-required' | translate }} + + } + @if (generatorConfigForm.get('msgCount').hasError('min')) { + + {{ 'rule-node-config.min-message-count-message' | translate }} + + } - + rule-node-config.generation-frequency-seconds - - {{ 'rule-node-config.generation-frequency-required' | translate }} - - - {{ 'rule-node-config.min-generation-frequency-message' | translate }} - + @if (generatorConfigForm.get('periodInSeconds').hasError('required')) { + + {{ 'rule-node-config.generation-frequency-required' | translate }} + + } + @if (generatorConfigForm.get('periodInSeconds').hasError('min')) { + + {{ 'rule-node-config.min-generation-frequency-message' | translate }} + + }
@@ -45,6 +53,7 @@
rule-node-config.originator
rule-node-config.generator-function - - - - {{ 'rule-node-config.script-lang-tbel' | translate }} - - - {{ 'rule-node-config.script-lang-js' | translate }} - - - - - - - - {{ 'rule-node-config.script-lang-tbel' | translate }} - - - {{ 'rule-node-config.script-lang-js' | translate }} - - - - + @if (generatorConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + @if (tbelEnabled) { + + + {{ 'rule-node-config.script-lang-tbel' | translate }} + + + {{ 'rule-node-config.script-lang-js' | translate }} + + + } + + + } + @if (generatorConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + {{ 'rule-node-config.script-lang-tbel' | translate }} + + + {{ 'rule-node-config.script-lang-js' | translate }} + + + + + }
- - - - + @if (tbelEnabled) { + + } + @if (logConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (logConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + }
- - rule-node-config.customer-name-pattern - - - {{ 'rule-node-config.customer-name-pattern-required' | translate }} - - rule-node-config.customer-name-pattern-hint - + @if (unassignCustomerConfigForm.get('unassignFromCustomer').value) { + + rule-node-config.customer-name-pattern + + @if (unassignCustomerConfigForm.get('customerNamePattern').hasError('required') || + unassignCustomerConfigForm.get('customerNamePattern').hasError('pattern')) { + + {{ 'rule-node-config.customer-name-pattern-required' | translate }} + + } + rule-node-config.customer-name-pattern-hint + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html index 9250c5b544..633c169cf1 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html @@ -23,101 +23,128 @@ [formGroup]="argumentsFormGroup" (cdkDropListDropped)="onDrop($event)" [cdkDropListDisabled]="disabled"> - -
- -
- {{argumentControl.get('name').value}}. -
- - rule-node-config.argument-source-field-input - - - {{ argumentTypeMap.get(argumentControl.get('type').value)?.name | translate }} - - - {{ argumentTypeMap.get(argument).name | translate }} - - {{ argumentTypeMap.get(argument).description | translate }} - - - - - rule-node-config.argument-source-field-input-required - - -
- - rule-node-config.argument-key-field-input - - - help - - - rule-node-config.argument-key-field-input-required - - - - rule-node-config.constant-value-field-input - - - rule-node-config.constant-value-field-input-required - - - - rule-node-config.default-value-field-input - + @for (argumentControl of argumentsFormArray.controls; track argumentControl) { + +
+ @if (!disabled) { + + } +
+ @if (displayArgumentName) { + {{argumentControl.get('name').value}}. + } +
+ + rule-node-config.argument-source-field-input + + + {{ argumentTypeMap.get(argumentControl.get('type').value)?.name | translate }} + + @for (argument of arguments; track argument) { + + {{ argumentTypeMap.get(argument).name | translate }} + + {{ argumentTypeMap.get(argument).description | translate }} + + + } + + @if (argumentControl.get('type').hasError('required')) { + + rule-node-config.argument-source-field-input-required + + } +
+ @if (argumentControl.get('type').value && argumentControl.get('type').value !== ArgumentType.CONSTANT) { + + rule-node-config.argument-key-field-input + + + help + + @if (argumentControl.get('key').hasError('required')) { + + rule-node-config.argument-key-field-input-required + + } + + } + @if (argumentControl.get('type').value === ArgumentType.CONSTANT) { + + rule-node-config.constant-value-field-input + + @if (argumentControl.get('key').hasError('required')) { + + rule-node-config.constant-value-field-input-required + + } + + } + @if (argumentControl.get('type').value && argumentControl.get('type').value !== ArgumentType.CONSTANT) { + + rule-node-config.default-value-field-input + + + } +
+ @if (argumentControl.get('type').value === ArgumentType.ATTRIBUTE) { + + rule-node-config.attribute-scope-field-input + + @for (scope of attributeScope; track scope) { + + {{ attributeScopeMap.get(scope) | translate }} + + } + + @if (argumentControl.get('attributeScope').hasError('required')) { + + rule-node-config.attribute-scope-field-input-required + + } + + }
- - rule-node-config.attribute-scope-field-input - - - {{ attributeScopeMap.get(scope) | translate }} - - - - rule-node-config.attribute-scope-field-input-required - - + @if (!disabled) { + + }
-
-
- + + }
-
+ @if (!argumentsFormArray.length) { +
rule-node-config.no-arguments-prompt -
+ class="tb-prompt flex items-center justify-center">rule-node-config.no-arguments-prompt +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts new file mode 100644 index 0000000000..98a5140de4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts @@ -0,0 +1,84 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { KvMapConfigOldComponent } from '@home/components/rule-node/common/kv-map-config-old.component'; + +@Component({ + selector: 'tb-kv-list-config', + templateUrl: './kv-map-config-old.component.html', + styleUrls: ['./kv-map-config-old.component.scss'], + standalone: false, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KvListConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KvListConfigComponent), + multi: true, + } + ] +}) +export class KvListConfigComponent extends KvMapConfigOldComponent implements ControlValueAccessor, OnInit, Validator { + + override writeValue(kvList: any): void { + const keyValsControls: Array = []; + if (Array.isArray(kvList) && kvList.length > 0) { + for (const property of kvList) { + keyValsControls.push(this.fb.group({ + key: [property.key, [Validators.required]], + value: [property.value, [Validators.required]] + })); + } + } + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls), {emitEvent: false}); + } + + public override validate() { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + if (!kvList.length && this.required) { + return { + kvMapRequired: true + }; + } + if (!this.kvListFormGroup.valid) { + return { + kvFieldsRequired: true + }; + } + return null; + } + + protected override updateModel() { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + if (this.required && !kvList.length || !this.kvListFormGroup.valid) { + this.propagateChange(null); + } else { + this.propagateChange(kvList); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html index a0ce66a5a7..831630074a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html @@ -16,40 +16,57 @@ -->
-
- {{ keyText | translate }} - {{ valText | translate }} - -
-
-
- - - - {{ keyRequiredText | translate }} - - - - - - {{ valRequiredText | translate }} - - - + @if (description) { +
+ } + @if (keyValsFormArray().controls.length || !emptyText) { +
+ {{ keyText | translate }} + {{ valText | translate }} +
-
+ } +
+ @for (keyValControl of keyValsFormArray().controls; track $index) { +
+ + + @if (keyValControl.get('key').hasError('required')) { + + {{ keyRequiredText | translate }} + + } + + + + @if (keyValControl.get('value').hasError('required')) { + + {{ valRequiredText | translate }} + + } + + +
+ } @empty { + @if (emptyText) { +
{{emptyText | translate}}
+ } + } + @if (hintText) { +
+ }
{{ labelText }}
-
- {{ requiredText }} -
-
- rule-node-config.map-fields-required -
-
- {{ 'rule-node-config.key-val.unique-key-value-pair-error' | translate: - { - valText: valText, - keyText: keyText - } }} -
+ @if (kvListFormGroup.hasError('kvMapRequired')) { +
+ {{ requiredText }} +
+ } + @if (kvListFormGroup.hasError('required')) { +
+ rule-node-config.map-fields-required +
+ } + @if (kvListFormGroup.hasError('uniqueKeyValuePair')) { +
+ {{ 'rule-node-config.key-val.unique-key-value-pair-error' | translate: + { + valText: valText, + keyText: keyText + } }} +
+ }
@@ -40,26 +46,31 @@
-
- - - - - - -
- + @for (keyValControl of keyValsFormArray().controls; track keyValControl) { +
+ + + + + + +
+ +
-
+ } @empty { + @if (emptyText) { +
{{ emptyText }}
+ } + }
@@ -68,5 +79,7 @@ {{ 'rule-node-config.key-val.add-mapping-entry' | translate }}
- + @if (popupHelpLink || hintText) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts index e98f145724..ac8b62487e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts @@ -80,6 +80,8 @@ export class KvMapConfigComponent implements ControlValueAccessor, OnInit, Valid @Input() hintText: string; + @Input() emptyText: string; + @Input() popupHelpLink: string; @Input() @@ -171,7 +173,7 @@ export class KvMapConfigComponent implements ControlValueAccessor, OnInit, Valid }; writeValue(keyValMap: { [key: string]: string }): void { - const keyValuesData = Object.keys(keyValMap).map(key => ({key, value: keyValMap[key]})); + const keyValuesData = Object.keys(keyValMap ?? {}).map(key => ({key, value: keyValMap[key]})); if (this.keyValsFormArray().length === keyValuesData.length) { this.keyValsFormArray().patchValue(keyValuesData, {emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html index c342149422..5ae8457030 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + rule-node-config.functions-field-input - + @if (mathFunctionForm.get('operation').value) { + + } - - - - {{ option.description }} - - - - rule-node-config.no-option-found - + @for (option of filteredOptions | async; track option) { + + + + {{ option.description }} + + + } + @if (!(filteredOptions | async)?.length) { + + rule-node-config.no-option-found + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts index 6f540b6181..8603144e2e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts @@ -21,6 +21,7 @@ import { TranslateService } from '@ngx-translate/core'; import { FunctionData, MathFunction, MathFunctionMap } from '../rule-node-config.models'; import { map, tap } from 'rxjs/operators'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-math-function-autocomplete', @@ -50,6 +51,9 @@ export class MathFunctionAutocompleteComponent implements ControlValueAccessor, @Input() disabled: boolean; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @ViewChild('operationInput', {static: true}) operationInput: ElementRef; mathFunctionForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html index 7ad685d8e2..2ecac54219 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html @@ -15,16 +15,19 @@ limitations under the License. --> - - {{ label }} + + @if (label) { + {{ label }} + } - - {{messageType.name}} - close - + @for (messageType of messageTypes; track messageType) { + + {{messageType.name}} + close + + } - - - - -
-
- rule-node-config.no-message-types-found + @for (messageType of filteredMessageTypes | async; track messageType) { + + + + } + @if ((filteredMessageTypes | async)?.length === 0) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ rule-node-config.no-message-types-found +
+ } @else { + + {{ 'rule-node-config.no-message-type-matching' | translate : + {messageType: truncate.transform(searchText, true, 6, '...')} + }} + + } + + rule-node-config.create-new-message-type +
- - - {{ 'rule-node-config.no-message-type-matching' | translate : - {messageType: truncate.transform(searchText, true, 6, '...')} - }} - - - - rule-node-config.create-new-message-type - -
- + + } help - - {{ 'rule-node-config.select-message-types-required' | translate }} - + matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.message-type' | translate } }}">help + @if (chipList.errorState) { + + {{ 'rule-node-config.select-message-types-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html index 9e7aba378f..14dfd216f5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html @@ -18,9 +18,11 @@
{{ labelText }}
- {{ option.name }} + @for (option of selectOptions; track option) { + {{ option.name }} + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html index 412f8af911..035a739918 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html @@ -16,35 +16,42 @@ -->
- + {{'rule-node-config.output-message-type' | translate}} - - {{msgType.name}} - + @for (msgType of messageTypes; track msgType) { + + {{msgType.name}} + + } - + {{'rule-node-config.message-type-value' | translate}} - - - {{ 'rule-node-config.message-type-value-required' | translate }} - - - {{ 'rule-node-config.message-type-value-max-length' | translate }} - + @if (messageTypeFormGroup.get('messageType').value) { + + } + @if (messageTypeFormGroup.get('messageType').hasError('required')) { + + {{ 'rule-node-config.message-type-value-required' | translate }} + + } + @if (messageTypeFormGroup.get('messageType').hasError('maxlength')) { + + {{ 'rule-node-config.message-type-value-max-length' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts index 5cb9922fcb..9395f59d6a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts @@ -24,7 +24,7 @@ import { Validator, Validators } from '@angular/forms'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { coerceBoolean } from '@shared/public-api'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -61,6 +61,9 @@ export class OutputMessageTypeAutocompleteComponent implements ControlValueAcces @coerceBoolean() disabled: boolean; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Input() @coerceBoolean() set required(value) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html index bab5638bd6..e6fbcdc3df 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html @@ -23,9 +23,11 @@ relation.direction - - {{ directionTypeTranslations.get(type) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts index 83ed4a435f..b0adb57e2a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts @@ -49,7 +49,7 @@ export class RelationsQueryConfigOldComponent extends PageComponent implements C this.requiredValue = coerceBooleanProperty(value); } - directionTypes = Object.keys(EntitySearchDirection); + directionTypes = Object.values(EntitySearchDirection); directionTypeTranslations = entitySearchDirectionTranslations; relationsQueryFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html index 936a9cde9d..01ec5c5e9a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html @@ -19,15 +19,17 @@
rule-node-config.relations-query
- + relation.direction - - {{ directionTypeTranslations.get(type) | translate }} rule-node-config.relations-query-config-direction-suffix - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} rule-node-config.relations-query-config-direction-suffix + + } - + rule-node-config.max-relation-level - - {{ 'rule-node-config.max-relation-level-error' | translate }} - - - {{ 'rule-node-config.max-relation-level-invalid' | translate }} - + @if (relationsQueryFormGroup.get('maxLevel').hasError('min')) { + + {{ 'rule-node-config.max-relation-level-error' | translate }} + + } + @if (relationsQueryFormGroup.get('maxLevel').invalid) { + + {{ 'rule-node-config.max-relation-level-invalid' | translate }} + + }
-
- - {{ 'alias.last-level-relation' | translate }} - -
+ @if (relationsQueryFormGroup.get('maxLevel').value > 1) { +
+ + {{ 'alias.last-level-relation' | translate }} + +
+ }
relation.relation-filters
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html index 9665fff5ce..ac4cc0eafa 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html @@ -17,43 +17,48 @@ -->
+ [popupHelpLink]="popupHelpLink"> -
- - {{ 'rule-node-config.fetch-latest-telemetry-with-timestamp' | translate }} - -
+ class="tb-form-row no-border no-padding"> + + {{ 'rule-node-config.fetch-latest-telemetry-with-timestamp' | translate }} + +
+ }
help + matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.field-name' | translate } }}">help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html index b8d804bfea..dddbcac29e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html @@ -18,12 +18,16 @@
{{ labelText }}
-
- rule-node-config.map-fields-required -
-
- {{ requiredText }} -
+ @if (svListFormGroup.hasError('required')) { +
+ rule-node-config.map-fields-required +
+ } + @if (svListFormGroup.hasError('svMapRequired')) { +
+ {{ requiredText }} +
+ }
@@ -33,30 +37,34 @@
-
- - - - {{option.name}} - - - - - - -
- + @for (keyValControl of keyValsFormArray().controls; track keyValControl) { +
+ + + @for (option of filterSelectOptions(keyValControl); track option) { + + {{option.name}} + + } + + + + + +
+ +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html index e39b97a19d..6011ea79c5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html @@ -17,32 +17,40 @@ -->
- + {{ 'rule-node-config.input-value-key' | translate }} - - {{ 'rule-node-config.input-value-key-required' | translate }} - + @if (calculateDeltaConfigForm.get('inputValueKey').hasError('required') || + calculateDeltaConfigForm.get('inputValueKey').hasError('pattern')) { + + {{ 'rule-node-config.input-value-key-required' | translate }} + + } - + {{ 'rule-node-config.output-value-key' | translate }} - - {{ 'rule-node-config.output-value-key-required' | translate }} - + @if (calculateDeltaConfigForm.get('outputValueKey').hasError('required') || + calculateDeltaConfigForm.get('outputValueKey').hasError('pattern')) { + + {{ 'rule-node-config.output-value-key-required' | translate }} + + }
- + {{ 'rule-node-config.number-of-digits-after-floating-point' | translate }} - - {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} - - - {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} - + @if (calculateDeltaConfigForm.get('round').hasError('min')) { + + {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} + + } + @if (calculateDeltaConfigForm.get('round').hasError('max')) { + + {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} + + }
+ class="tb-form-row same-padding"> {{ 'rule-node-config.use-caching' | translate }} @@ -64,26 +72,30 @@ { periodValueKey: (calculateDeltaConfigForm.get('periodValueKey').valid && calculateDeltaConfigForm.get('addPeriodBetweenMsgs').value) ? calculateDeltaConfigForm.get('periodValueKey').value : 'periodInMs' } }}" - class="tb-form-row no-border no-padding"> + class="tb-form-row no-border no-padding"> {{ 'rule-node-config.add-time-difference-between-readings' | translate: { inputValueKey: calculateDeltaConfigForm.get('inputValueKey').valid ? - calculateDeltaConfigForm.get('inputValueKey').value : 'rule-node-config.input-value-key' | translate } }} + calculateDeltaConfigForm.get('inputValueKey').value : 'rule-node-config.input-value-key' | translate } }}
- - {{ 'rule-node-config.period-value-key' | translate }} - - - {{ 'rule-node-config.period-value-key-required' | translate }} - - + @if (calculateDeltaConfigForm.get('addPeriodBetweenMsgs').value) { + + {{ 'rule-node-config.period-value-key' | translate }} + + @if (calculateDeltaConfigForm.get('periodValueKey').hasError('required')) { + + {{ 'rule-node-config.period-value-key-required' | translate }} + + } + + }
+ class="tb-form-row same-padding"> {{ 'rule-node-config.exclude-zero-deltas' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html index e32b62a92e..e26979cce6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html @@ -20,9 +20,11 @@
- - {{ data.name }} - + @for (data of fetchToData; track data) { + + {{ data.name }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html index ff7939cab4..50af390ff8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html @@ -18,6 +18,7 @@
@@ -26,7 +27,7 @@ tb-help-popup-placement="right" trigger-style="letter-spacing:0.25px; font-size:12px" [tb-help-popup-style]="{maxWidth: '820px'}" - trigger-text="{{ 'rule-node-config.key-val.see-examples' | translate }}">
+ trigger-text="{{ 'rule-node-config.key-val.see-examples' | translate }}">
help @@ -39,144 +40,179 @@ {{ 'rule-node-config.use-metadata-dynamic-interval' | translate }}
-
-
- - {{ 'rule-node-config.interval-start' | translate }} - - - {{ 'rule-node-config.start-interval-value-required' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - - {{ 'rule-node-config.time-unit' | translate }} - - - {{ timeUnitsTranslationMap.get(timeUnit) | translate }} - - - -
-
- - {{ 'rule-node-config.interval-end' | translate }} - - - {{ 'rule-node-config.end-interval-value-required' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - - {{ 'rule-node-config.time-unit' | translate }} - - - {{ timeUnitsTranslationMap.get(timeUnit) | translate }} - - - -
-
- error_outline -
- - {{ 'rule-node-config.fetch-timeseries-from-to' | translate: - { + @if (getTelemetryFromDatabaseConfigForm.get('useMetadataIntervalPatterns').value === false) { +
+
+ + {{ 'rule-node-config.interval-start' | translate }} + + @if (getTelemetryFromDatabaseConfigForm.get('interval.startInterval').hasError('required')) { + + {{ 'rule-node-config.start-interval-value-required' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.startInterval').hasError('min')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.startInterval').hasError('max')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + + + {{ 'rule-node-config.time-unit' | translate }} + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + } + + +
+
+ + {{ 'rule-node-config.interval-end' | translate }} + + @if (getTelemetryFromDatabaseConfigForm.get('interval.endInterval').hasError('required')) { + + {{ 'rule-node-config.end-interval-value-required' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.endInterval').hasError('min')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.endInterval').hasError('max')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + + + {{ 'rule-node-config.time-unit' | translate }} + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + } + + +
+
+ error_outline +
+ @if (getTelemetryFromDatabaseConfigForm.get('interval').valid) { + {{ 'rule-node-config.fetch-timeseries-from-to' | translate: + { startInterval: getTelemetryFromDatabaseConfigForm.get('interval.startInterval').value, endInterval: getTelemetryFromDatabaseConfigForm.get('interval.endInterval').value, startIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get('interval.startIntervalTimeUnit').value.toLowerCase(), endIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get('interval.endIntervalTimeUnit').value.toLowerCase() - } }} - - - {{ "rule-node-config.fetch-timeseries-from-to-invalid" | translate }} - + } }} + } @else { + {{ "rule-node-config.fetch-timeseries-from-to-invalid" | translate }} + } +
-
- + } @else {
- + {{ 'rule-node-config.start-interval' | translate }} - - {{ 'rule-node-config.start-interval-required' | translate }} - + @if (getTelemetryFromDatabaseConfigForm.get('startIntervalPattern').hasError('required') || + getTelemetryFromDatabaseConfigForm.get('startIntervalPattern').hasError('pattern')) { + + {{ 'rule-node-config.start-interval-required' | translate }} + + } - + {{ 'rule-node-config.end-interval' | translate }} - - {{ 'rule-node-config.end-interval-required' | translate }} - + @if (getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').hasError('required') || + getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').hasError('pattern')) { + + {{ 'rule-node-config.end-interval-required' | translate }} + + }
-
+ }
rule-node-config.fetch-strategy
- - {{ data.name }} - + @for (data of headerOptions; track data) { + + {{ data.name }} + + }
{{ deduplicationStrategiesHintTranslations.get(getTelemetryFromDatabaseConfigForm.get('fetchMode').value) | translate }}
-
- - {{ 'aggregation.function' | translate }} - - - {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} - - - -
- - {{ "rule-node-config.order-by-timestamp" | translate }} - - - {{ samplingOrdersTranslate.get(order) | translate }} - + @if (getTelemetryFromDatabaseConfigForm.get('fetchMode').value === fetchMode.ALL) { +
+ + {{ 'aggregation.function' | translate }} + + @for (aggregation of aggregations; track aggregation) { + + {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + + } - - {{ "rule-node-config.limit" | translate }} - - {{ "rule-node-config.limit-hint" | translate }} - - {{ 'rule-node-config.limit-required' | translate }} - - - {{ 'rule-node-config.limit-range' | translate }} - - - {{ 'rule-node-config.limit-range' | translate }} - - + @if (getTelemetryFromDatabaseConfigForm.get('aggregation').value === aggregationTypes.NONE) { +
+ + {{ "rule-node-config.order-by-timestamp" | translate }} + + @for (order of samplingOrders; track order) { + + {{ samplingOrdersTranslate.get(order) | translate }} + + } + + + + {{ "rule-node-config.limit" | translate }} + + {{ "rule-node-config.limit-hint" | translate }} + @if (getTelemetryFromDatabaseConfigForm.get('limit').hasError('required')) { + + {{ 'rule-node-config.limit-required' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('limit').hasError('min')) { + + {{ 'rule-node-config.limit-range' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('limit').hasError('max')) { + + {{ 'rule-node-config.limit-range' | translate }} + + } + +
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html index cdce036bbb..d80674e7fb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html @@ -23,9 +23,11 @@
rule-node-config.data-to-fetch
- - {{ data.name }} - + @for (data of fetchToData; track data) { + + {{ data.name }} + + }
- - {{ data.name }} - + @for (data of fetchToData; track data) { + + {{ data.name }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index 3cf88a2463..9b0a51d7ec 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -29,7 +29,7 @@ labelText="rule-node-config.ai.model" (entityChanged)="onEntityChange($event)" [entityType]="entityType.AI_MODEL" - (createNew)="createModelAi('modelId')" + (createNew)="createModelAi($event, 'modelId')" formControlName="modelId"> @@ -50,28 +50,39 @@ rule-node-config.ai.system-prompt - - {{ 'rule-node-config.ai.system-prompt-max-length' | translate }} - - - {{ 'rule-node-config.ai.system-prompt-blank' | translate }} - + @if (aiConfigForm.get('systemPrompt').hasError('maxlength')) { + + {{ 'rule-node-config.ai.system-prompt-max-length' | translate }} + + } + @if (aiConfigForm.get('systemPrompt').hasError('pattern')) { + + {{ 'rule-node-config.ai.system-prompt-blank' | translate }} + + } rule-node-config.ai.user-prompt - - {{ 'rule-node-config.ai.user-prompt-required' | translate }} - - - {{ 'rule-node-config.ai.user-prompt-max-length' | translate }} - - - {{ 'rule-node-config.ai.user-prompt-blank' | translate }} - + @if (aiConfigForm.get('userPrompt').hasError('required')) { + + {{ 'rule-node-config.ai.user-prompt-required' | translate }} + + } + @if (aiConfigForm.get('userPrompt').hasError('maxlength')) { + + {{ 'rule-node-config.ai.user-prompt-max-length' | translate }} + + } + @if (aiConfigForm.get('userPrompt').hasError('pattern')) { + + {{ 'rule-node-config.ai.user-prompt-blank' | translate }} + + } (AIModelDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - isAdd: true + isAdd: true, + name } }).afterClosed() .subscribe((model) => { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html index 3096b20823..26c7816887 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html @@ -16,29 +16,35 @@ -->
- + rule-node-config.topic - - {{ 'rule-node-config.topic-required' | translate }} - + @if (azureIotHubConfigForm.get('topicPattern').hasError('required')) { + + {{ 'rule-node-config.topic-required' | translate }} + + } rule-node-config.general-pattern-hint - + rule-node-config.hostname - - {{ 'rule-node-config.hostname-required' | translate }} - + @if (azureIotHubConfigForm.get('host').hasError('required')) { + + {{ 'rule-node-config.hostname-required' | translate }} + + } - + rule-node-config.device-id - - {{ 'rule-node-config.device-id-required' | translate }} - + @if (azureIotHubConfigForm.get('clientId').hasError('required')) { + + {{ 'rule-node-config.device-id-required' | translate }} + + } - @@ -50,74 +56,81 @@
- + rule-node-config.credentials-type - - {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }} - + @for (credentialsType of allAzureIotHubCredentialsTypes; track credentialsType) { + + {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }} + + } - - {{ 'rule-node-config.credentials-type-required' | translate }} - + @if (azureIotHubConfigForm.get('credentials.type').hasError('required')) { + + {{ 'rule-node-config.credentials-type-required' | translate }} + + } -
- - - - - rule-node-config.sas-key - - - - {{ 'rule-node-config.sas-key-required' | translate }} - - - - - - - - - - - - - - rule-node-config.private-key-password - - - - +
+ @switch (azureIotHubConfigForm.get('credentials.type').value) { + @case ('anonymous') {} + @case ('sas') { + + rule-node-config.sas-key + + + @if (azureIotHubConfigForm.get('credentials.sasKey').hasError('required')) { + + {{ 'rule-node-config.sas-key-required' | translate }} + + } + + + + } + @case ('cert.PEM') { + + + + + + + + rule-node-config.private-key-password + + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html index 965a0ca8b8..cc02018710 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html @@ -16,82 +16,101 @@ -->
- + rule-node-config.topic-pattern - - {{ 'rule-node-config.topic-pattern-required' | translate }} - + @if (kafkaConfigForm.get('topicPattern').hasError('required')) { + + {{ 'rule-node-config.topic-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint - + rule-node-config.key-pattern rule-node-config.general-pattern-hint
rule-node-config.key-pattern-hint
- + rule-node-config.bootstrap-servers - - {{ 'rule-node-config.bootstrap-servers-required' | translate }} - + @if (kafkaConfigForm.get('bootstrapServers').hasError('required')) { + + {{ 'rule-node-config.bootstrap-servers-required' | translate }} + + } - + rule-node-config.retries - - {{ 'rule-node-config.min-retries-message' | translate }} - + @if (kafkaConfigForm.get('retries').hasError('min')) { + + {{ 'rule-node-config.min-retries-message' | translate }} + + } - + rule-node-config.batch-size-bytes - - {{ 'rule-node-config.min-batch-size-bytes-message' | translate }} - + @if (kafkaConfigForm.get('batchSize').hasError('min')) { + + {{ 'rule-node-config.min-batch-size-bytes-message' | translate }} + + } - + rule-node-config.linger-ms - - {{ 'rule-node-config.min-linger-ms-message' | translate }} - + @if (kafkaConfigForm.get('linger').hasError('min')) { + + {{ 'rule-node-config.min-linger-ms-message' | translate }} + + } - + rule-node-config.buffer-memory-bytes - - {{ 'rule-node-config.min-buffer-memory-bytes-message' | translate }} - + @if (kafkaConfigForm.get('bufferMemory').hasError('min')) { + + {{ 'rule-node-config.min-buffer-memory-bytes-message' | translate }} + + } - + rule-node-config.acks - - {{ ackValue }} - + @for (ackValue of ackValues; track ackValue) { + + {{ ackValue }} + + } - - + [keyText]="'rule-node-config.key' | translate" + [keyRequiredText]="'rule-node-config.key-required' | translate" + [valText]="'rule-node-config.value' | translate" + [valRequiredText]="'rule-node-config.value-required' | translate" + [emptyText]="'rule-node-config.other-properties-empty' | translate"> + {{ 'rule-node-config.add-metadata-key-values-as-kafka-headers' | translate }}
rule-node-config.add-metadata-key-values-as-kafka-headers-hint
- - rule-node-config.charset-encoding - - - {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }} - - - + @if (kafkaConfigForm.get('addMetadataKeyValuesAsKafkaHeaders').value) { + + rule-node-config.charset-encoding + + @for (charset of ToByteStandartCharsetTypesValues; track charset) { + + {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }} + + } + + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html index 1839464bbe..4e80aad77b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html @@ -24,14 +24,16 @@ popupHelpLink="rulenode/node-templatization-doc">
- + {{'rule-node-config.function-name' | translate}} - - {{'rule-node-config.function-name-required' | translate}} - + @if (lambdaConfigForm.get('functionName').hasError('required')) { + + {{'rule-node-config.function-name-required' | translate}} + + } - + {{'rule-node-config.qualifier' | translate}} rule-node-config.qualifier-hint @@ -45,26 +47,32 @@ rule-node-config.aws-credentials
- + rule-node-config.aws-access-key-id - - {{ 'rule-node-config.aws-access-key-id-required' | translate }} - + @if (lambdaConfigForm.get('accessKey').hasError('required')) { + + {{ 'rule-node-config.aws-access-key-id-required' | translate }} + + } - + rule-node-config.aws-secret-access-key - - {{ 'rule-node-config.aws-secret-access-key-required' | translate }} - + @if (lambdaConfigForm.get('secretKey').hasError('required')) { + + {{ 'rule-node-config.aws-secret-access-key-required' | translate }} + + } - + rule-node-config.aws-region - - {{ 'rule-node-config.aws-region-required' | translate }} - + @if (lambdaConfigForm.get('region').hasError('required')) { + + {{ 'rule-node-config.aws-region-required' | translate }} + + }
@@ -76,31 +84,39 @@
- + rule-node-config.connection-timeout - - {{ 'rule-node-config.connection-timeout-required' | translate }} - - - {{ 'rule-node-config.connection-timeout-min' | translate }} - + @if (lambdaConfigForm.get('connectionTimeout').hasError('required')) { + + {{ 'rule-node-config.connection-timeout-required' | translate }} + + } + @if (lambdaConfigForm.get('connectionTimeout').hasError('min')) { + + {{ 'rule-node-config.connection-timeout-min' | translate }} + + } help + matTooltip="{{ 'rule-node-config.connection-timeout-hint' | translate }}">help - + rule-node-config.request-timeout - - {{ 'rule-node-config.request-timeout-required' | translate }} - - - {{ 'rule-node-config.request-timeout-min' | translate }} - + @if (lambdaConfigForm.get('requestTimeout').hasError('required')) { + + {{ 'rule-node-config.request-timeout-required' | translate }} + + } + @if (lambdaConfigForm.get('requestTimeout').hasError('min')) { + + {{ 'rule-node-config.request-timeout-min' | translate }} + + } help + matTooltip="{{ 'rule-node-config.request-timeout-hint' | translate }}">help
- + rule-node-config.topic-pattern - - {{ 'rule-node-config.topic-pattern-required' | translate }} - + @if (mqttConfigForm.get('topicPattern').hasError('required')) { + + {{ 'rule-node-config.topic-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint
- + rule-node-config.host - - {{ 'rule-node-config.host-required' | translate }} - + @if (mqttConfigForm.get('host').hasError('required')) { + + {{ 'rule-node-config.host-required' | translate }} + + } - + rule-node-config.port - - {{ 'rule-node-config.port-required' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - + @if (mqttConfigForm.get('port').hasError('required')) { + + {{ 'rule-node-config.port-required' | translate }} + + } + @if (mqttConfigForm.get('port').hasError('min')) { + + {{ 'rule-node-config.port-range' | translate }} + + } + @if (mqttConfigForm.get('port').hasError('max')) { + + {{ 'rule-node-config.port-range' | translate }} + + } - + rule-node-config.connect-timeout - - {{ 'rule-node-config.connect-timeout-required' | translate }} - - - {{ 'rule-node-config.connect-timeout-range' | translate }} - - - {{ 'rule-node-config.connect-timeout-range' | translate }} - + @if (mqttConfigForm.get('connectTimeoutSec').hasError('required')) { + + {{ 'rule-node-config.connect-timeout-required' | translate }} + + } + @if (mqttConfigForm.get('connectTimeoutSec').hasError('min')) { + + {{ 'rule-node-config.connect-timeout-range' | translate }} + + } + @if (mqttConfigForm.get('connectTimeoutSec').hasError('max')) { + + {{ 'rule-node-config.connect-timeout-range' | translate }} + + }
- + rule-node-config.client-id {{'rule-node-config.client-id-hint' | translate}} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html index ce937fe7d7..8101d2b2a6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html @@ -19,12 +19,14 @@
- + rule-node-config.gcp-project-id - - {{ 'rule-node-config.gcp-project-id-required' | translate }} - + @if (pubSubConfigForm.get('projectId').hasError('required')) { + + {{ 'rule-node-config.gcp-project-id-required' | translate }} + + } - + rule-node-config.pubsub-topic-name - - {{ 'rule-node-config.pubsub-topic-name-required' | translate }} - + @if (pubSubConfigForm.get('topicName').hasError('required')) { + + {{ 'rule-node-config.pubsub-topic-name-required' | translate }} + + }
- - + [keyText]="'rule-node-config.name' | translate" + [keyRequiredText]="'rule-node-config.name-required' | translate" + [valText]="'rule-node-config.value' | translate" + [valRequiredText]="'rule-node-config.value-required' | translate" + [emptyText]="'rule-node-config.message-attributes-empty' | translate"> +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html index ff4c4e71e7..71b53b53f6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html @@ -16,53 +16,63 @@ -->
- + rule-node-config.exchange-name-pattern - + rule-node-config.routing-key-pattern - + rule-node-config.message-properties - - {{ property }} - + @for (property of messageProperties; track property) { + + {{ property }} + + }
- + rule-node-config.host - - {{ 'rule-node-config.host-required' | translate }} - + @if (rabbitMqConfigForm.get('host').hasError('required')) { + + {{ 'rule-node-config.host-required' | translate }} + + } - + rule-node-config.port - - {{ 'rule-node-config.port-required' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - + @if (rabbitMqConfigForm.get('port').hasError('required')) { + + {{ 'rule-node-config.port-required' | translate }} + + } + @if (rabbitMqConfigForm.get('port').hasError('min')) { + + {{ 'rule-node-config.port-range' | translate }} + + } + @if (rabbitMqConfigForm.get('port').hasError('max')) { + + {{ 'rule-node-config.port-range' | translate }} + + }
- + rule-node-config.virtual-host - + rule-node-config.username - + rule-node-config.password @@ -70,27 +80,32 @@ {{ 'rule-node-config.automatic-recovery' | translate }} - + rule-node-config.connection-timeout-ms - - {{ 'rule-node-config.min-connection-timeout-ms-message' | translate }} - + @if (rabbitMqConfigForm.get('connectionTimeout').hasError('min')) { + + {{ 'rule-node-config.min-connection-timeout-ms-message' | translate }} + + } - + rule-node-config.handshake-timeout-ms - - {{ 'rule-node-config.min-handshake-timeout-ms-message' | translate }} - + @if (rabbitMqConfigForm.get('handshakeTimeout').hasError('min')) { + + {{ 'rule-node-config.min-handshake-timeout-ms-message' | translate }} + + } - - + [keyText]="'rule-node-config.key' | translate" + [keyRequiredText]="'rule-node-config.key-required' | translate" + [valText]="'rule-node-config.value' | translate" + [valRequiredText]="'rule-node-config.value-required' | translate" + [emptyText]="'rule-node-config.client-properties-empty' | translate"> +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html index 1a91cba5c7..f5ec1ba156 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html @@ -15,116 +15,176 @@ limitations under the License. --> -
- - rule-node-config.endpoint-url-pattern - - - {{ 'rule-node-config.endpoint-url-pattern-required' | translate }} - - rule-node-config.general-pattern-hint - - +
+
+
rule-node-config.request-url
+ + rule-node-config.url + + @if (restApiCallConfigForm.get('restEndpointUrlPattern').hasError('required')) { + + {{ 'rule-node-config.endpoint-url-pattern-required' | translate }} + + } + rule-node-config.url-hint + + + + {{ 'rule-node-config.query-params' | translate }} +
+
+ + + + +
+
+ rule-node-config.request-method - - {{ requestType }} - + @for (requestType of httpRequestTypes; track requestType) { + + {{ requestType }} + + } - - {{ 'rule-node-config.enable-proxy' | translate }} - - - {{ 'rule-node-config.use-simple-client-http-factory' | translate }} - - - {{ 'rule-node-config.parse-to-plain-text' | translate }} - -
rule-node-config.parse-to-plain-text-hint
- - {{ 'rule-node-config.ignore-request-body' | translate }} - -
- - {{ 'rule-node-config.use-system-proxy-properties' | translate }} - -
-
- - rule-node-config.proxy-scheme - - - {{ proxyScheme }} - - +
+ + + {{ 'rule-node-config.headers' | translate }} + + +
+ + +
+
+
+ +
+ + + {{ 'rule-node-config.advanced-settings' | translate }} + + +
+ + {{ 'rule-node-config.enable-proxy' | translate }} + + @if (restApiCallConfigForm.get('enableProxy').value) { +
+ + {{ 'rule-node-config.use-system-proxy-properties' | translate }} + + @if (!restApiCallConfigForm.get('useSystemProxyProperties').value) { +
+
+ + rule-node-config.proxy-host + + @if (restApiCallConfigForm.get('proxyHost').hasError('required')) { + + {{ 'rule-node-config.proxy-host-required' | translate }} + + } + + + rule-node-config.proxy-port + + @if (restApiCallConfigForm.get('proxyPort').hasError('required')) { + + {{ 'rule-node-config.proxy-port-required' | translate }} + + } + @if (restApiCallConfigForm.get('proxyPort').hasError('min') || restApiCallConfigForm.get('proxyPort').hasError('max')) { + + {{ 'rule-node-config.proxy-port-range' | translate }} + + } + +
+ + rule-node-config.proxy-user + + + + rule-node-config.proxy-password + + + +
+ } +
+ } +
+
+ +
+ {{ 'rule-node-config.parse-to-plain-text' | translate }} +
+
+
+
+ + {{ 'rule-node-config.ignore-request-body' | translate }} + + @if(!restApiCallConfigForm.get('ignoreRequestBody').value) { +
+ + rule-node-config.request-body-template + + + } +
+ + rule-node-config.read-timeout + + rule-node-config.read-timeout-hint + @if (restApiCallConfigForm.get('readTimeoutMs').hasError('max')) { + + {{ 'rule-node-config.int-range' | translate }} + + } - - rule-node-config.proxy-host - - - {{ 'rule-node-config.proxy-host-required' | translate }} - + + rule-node-config.max-parallel-requests-count + + rule-node-config.max-parallel-requests-count-hint + @if (restApiCallConfigForm.get('maxParallelRequestsCount').hasError('max')) { + + {{ 'rule-node-config.int-range' | translate }} + + } - - rule-node-config.proxy-port - - - {{ 'rule-node-config.proxy-port-required' | translate }} - - - {{ 'rule-node-config.proxy-port-range' | translate }} - + + rule-node-config.max-response-size + + rule-node-config.max-response-size-hint + @if (restApiCallConfigForm.get('maxInMemoryBufferSizeInKb').hasError('min') || restApiCallConfigForm.get('maxInMemoryBufferSizeInKb').hasError('max')) { + + {{ 'rule-node-config.memory-buffer-size-range' | translate: { max: MemoryBufferSizeInKbLimit } }} + + } -
- - rule-node-config.proxy-user - - - - rule-node-config.proxy-password - - -
+ +
- - rule-node-config.read-timeout - - rule-node-config.read-timeout-hint - - {{ 'rule-node-config.int-range' | translate }} - - - - rule-node-config.max-parallel-requests-count - - rule-node-config.max-parallel-requests-count-hint - - {{ 'rule-node-config.int-range' | translate }} - - - - rule-node-config.max-response-size - - rule-node-config.max-response-size-hint - - {{ 'rule-node-config.memory-buffer-size-range' | translate: { max: MemoryBufferSizeInKbLimit } }} - - - -
- - -
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss similarity index 67% rename from ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss rename to ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss index b4a09bd8aa..a4ad3fe4f8 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss @@ -13,22 +13,20 @@ * 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; +:host ::ng-deep { + .mat-expansion-panel-content { + .mat-expansion-panel-body { + .tb-kv-map-config { + margin-bottom: 0; + .body { + .mdc-icon-button { + margin-bottom: 22px; + } + } } } - - .tb-card-title{ - padding-top: 0; - padding-bottom: 0; - } + } + textarea.tb-enable-vertical-resize { + resize: vertical; } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts index 4723a0c510..32ea3d57d9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts @@ -18,22 +18,25 @@ import { Component } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; import { HttpRequestType, IntLimit } from '../rule-node-config.models'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-external-node-rest-api-call-config', templateUrl: './rest-api-call-config.component.html', - styleUrls: [], + styleUrls: ['./rest-api-call-config.component.scss'], standalone: false }) export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { restApiCallConfigForm: UntypedFormGroup; - readonly proxySchemes: string[] = ['http', 'https']; readonly httpRequestTypes = Object.keys(HttpRequestType); readonly MemoryBufferSizeInKbLimit = 25000; readonly IntLimit = IntLimit; + isHeadersExpanded = false; + isQueryParamsExpanded = false; + constructor(private fb: UntypedFormBuilder) { super(); } @@ -46,12 +49,11 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { this.restApiCallConfigForm = this.fb.group({ restEndpointUrlPattern: [configuration ? configuration.restEndpointUrlPattern : null, [Validators.required]], requestMethod: [configuration ? configuration.requestMethod : null, [Validators.required]], - useSimpleClientHttpFactory: [configuration ? configuration.useSimpleClientHttpFactory : false, []], parseToPlainText: [configuration ? configuration.parseToPlainText : false, []], ignoreRequestBody: [configuration ? configuration.ignoreRequestBody : false, []], + requestBodyTemplate: [configuration ? configuration.requestBodyTemplate : null, []], enableProxy: [configuration ? configuration.enableProxy : false, []], useSystemProxyProperties: [configuration ? configuration.enableProxy : false, []], - proxyScheme: [configuration ? configuration.proxyHost : null, []], proxyHost: [configuration ? configuration.proxyHost : null, []], proxyPort: [configuration ? configuration.proxyPort : null, []], proxyUser: [configuration ? configuration.proxyUser :null, []], @@ -60,16 +62,27 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { maxParallelRequestsCount: [configuration ? configuration.maxParallelRequestsCount : null, [Validators.min(0), Validators.max(IntLimit)]], headers: [configuration ? configuration.headers : null, []], credentials: [configuration ? configuration.credentials : null, []], - maxInMemoryBufferSizeInKb: [configuration ? configuration.maxInMemoryBufferSizeInKb : null, [Validators.min(1), Validators.max(this.MemoryBufferSizeInKbLimit)]] + maxInMemoryBufferSizeInKb: [configuration ? configuration.maxInMemoryBufferSizeInKb : null, [Validators.min(1), Validators.max(this.MemoryBufferSizeInKbLimit)]], + queryParams: [configuration ? configuration.queryParams : [], []], }); } protected validatorTriggers(): string[] { - return ['useSimpleClientHttpFactory', 'enableProxy', 'useSystemProxyProperties']; + return ['enableProxy', 'useSystemProxyProperties']; + } + + protected prepareInputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + this.isQueryParamsExpanded = configuration && isDefinedAndNotNull(configuration.queryParams) && !!Object.keys(configuration.queryParams).length; + this.isHeadersExpanded = configuration && isDefinedAndNotNull(configuration.headers) && !!Object.keys(configuration.headers).length; + return super.prepareInputConfig(configuration); + } + + protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + configuration.queryParams = isDefinedAndNotNull(configuration.queryParams) ? configuration.queryParams : []; + return super.prepareOutputConfig(configuration); } protected updateValidators(emitEvent: boolean) { - const useSimpleClientHttpFactory: boolean = this.restApiCallConfigForm.get('useSimpleClientHttpFactory').value; const enableProxy: boolean = this.restApiCallConfigForm.get('enableProxy').value; const useSystemProxyProperties: boolean = this.restApiCallConfigForm.get('useSystemProxyProperties').value; @@ -80,12 +93,7 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { } else { this.restApiCallConfigForm.get('proxyHost').setValidators([]); this.restApiCallConfigForm.get('proxyPort').setValidators([]); - - if (useSimpleClientHttpFactory) { - this.restApiCallConfigForm.get('readTimeoutMs').setValidators([]); - } else { - this.restApiCallConfigForm.get('readTimeoutMs').setValidators([Validators.min(0), Validators.max(IntLimit)]); - } + this.restApiCallConfigForm.get('readTimeoutMs').setValidators([Validators.min(0), Validators.max(IntLimit)]); } this.restApiCallConfigForm.get('readTimeoutMs').updateValueAndValidity({emitEvent}); diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html index 98e2c2f686..9f5f9a590f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html @@ -19,98 +19,126 @@ {{ 'rule-node-config.use-system-smtp-settings' | translate }} -
- - rule-node-config.smtp-protocol - - - {{ smtpProtocol.toUpperCase() }} - - - -
- - rule-node-config.smtp-host - - - {{ 'rule-node-config.smtp-host-required' | translate }} - + @if (sendEmailConfigForm.get('useSystemSmtpSettings').value === false) { +
+ + rule-node-config.smtp-protocol + + @for (smtpProtocol of smtpProtocols; track smtpProtocol) { + + {{ smtpProtocol.toUpperCase() }} + + } + - - rule-node-config.smtp-port - - - {{ 'rule-node-config.smtp-port-required' | translate }} - - - {{ 'rule-node-config.smtp-port-range' | translate }} - - - {{ 'rule-node-config.smtp-port-range' | translate }} - - -
- - rule-node-config.timeout-msec - - - {{ 'rule-node-config.timeout-required' | translate }} - - - {{ 'rule-node-config.min-timeout-msec-message' | translate }} - - - - {{ 'rule-node-config.enable-tls' | translate }} - - - rule-node-config.tls-version - - - {{ tlsVersion }} - - - - - {{ 'rule-node-config.enable-proxy' | translate }} - -
- - rule-node-config.proxy-host - - - {{ 'rule-node-config.proxy-host-required' | translate }} - + + rule-node-config.smtp-host + + @if (sendEmailConfigForm.get('smtpHost').hasError('required')) { + + {{ 'rule-node-config.smtp-host-required' | translate }} + + } + + + rule-node-config.smtp-port + + @if (sendEmailConfigForm.get('smtpPort').hasError('required')) { + + {{ 'rule-node-config.smtp-port-required' | translate }} + + } + @if (sendEmailConfigForm.get('smtpPort').hasError('min')) { + + {{ 'rule-node-config.smtp-port-range' | translate }} + + } + @if (sendEmailConfigForm.get('smtpPort').hasError('max')) { + + {{ 'rule-node-config.smtp-port-range' | translate }} + + } - - rule-node-config.proxy-port - - - {{ 'rule-node-config.proxy-port-required' | translate }} +
+ + rule-node-config.timeout-msec + + @if (sendEmailConfigForm.get('timeout').hasError('required')) { + + {{ 'rule-node-config.timeout-required' | translate }} - - {{ 'rule-node-config.proxy-port-range' | translate }} + } + @if (sendEmailConfigForm.get('timeout').hasError('min')) { + + {{ 'rule-node-config.min-timeout-msec-message' | translate }} + } + + + {{ 'rule-node-config.enable-tls' | translate }} + + @if (sendEmailConfigForm.get('enableTls').value === true) { + + rule-node-config.tls-version + + @for (tlsVersion of tlsVersions; track tlsVersion) { + + {{ tlsVersion }} + + } + -
- - rule-node-config.proxy-user - + } + + {{ 'rule-node-config.enable-proxy' | translate }} + + @if (sendEmailConfigForm.get('enableProxy').value) { +
+
+ + rule-node-config.proxy-host + + @if (sendEmailConfigForm.get('proxyHost').hasError('required')) { + + {{ 'rule-node-config.proxy-host-required' | translate }} + + } + + + rule-node-config.proxy-port + + @if (sendEmailConfigForm.get('proxyPort').hasError('required')) { + + {{ 'rule-node-config.proxy-port-required' | translate }} + + } + @if (sendEmailConfigForm.get('proxyPort').hasError('min') || sendEmailConfigForm.get('proxyPort').hasError('max')) { + + {{ 'rule-node-config.proxy-port-range' | translate }} + + } + +
+ + rule-node-config.proxy-user + + + + rule-node-config.proxy-password + + +
+ } + + rule-node-config.username + - - rule-node-config.proxy-password - + + rule-node-config.password + + -
- - rule-node-config.username - - - - rule-node-config.password - - - -
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html index 1f6762c082..d9136b3cb6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html @@ -16,28 +16,33 @@ -->
- + rule-node-config.numbers-to-template - - {{ 'rule-node-config.numbers-to-template-required' | translate }} - + @if (sendSmsConfigForm.get('numbersToTemplate').hasError('required')) { + + {{ 'rule-node-config.numbers-to-template-required' | translate }} + + } - + rule-node-config.sms-message-template - - - {{ 'rule-node-config.sms-message-template-required' | translate }} - + + @if (sendSmsConfigForm.get('smsMessageTemplate').hasError('required')) { + + {{ 'rule-node-config.sms-message-template-required' | translate }} + + } rule-node-config.general-pattern-hint {{ 'rule-node-config.use-system-sms-settings' | translate }} - - + @if (sendSmsConfigForm.get('useSystemSmsSettings').value === false) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html index 2cf15b05e7..31f91ba86a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html @@ -16,31 +16,40 @@ -->
- + rule-node-config.message-template - - {{ 'rule-node-config.message-template-required' | translate }} - + @if (slackConfigForm.get('messageTemplate').hasError('required')) { + + {{ 'rule-node-config.message-template-required' | translate }} + + } rule-node-config.general-pattern-hint {{ 'rule-node-config.use-system-slack-settings' | translate }} - - rule-node-config.slack-api-token - - - {{ 'rule-node-config.slack-api-token-required' | translate }} - - + @if (!slackConfigForm.get('useSystemSettings').value) { + + rule-node-config.slack-api-token + + @if (slackConfigForm.get('botToken').hasError('required')) { + + {{ 'rule-node-config.slack-api-token-required' | translate }} + + } + + } - - {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} - + @for (slackChanelType of slackChanelTypes; track slackChanelType) { + + {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} + + }
- + rule-node-config.topic-arn-pattern - - {{ 'rule-node-config.topic-arn-pattern-required' | translate }} - + @if (snsConfigForm.get('topicArnPattern').hasError('required')) { + + {{ 'rule-node-config.topic-arn-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint - + rule-node-config.aws-access-key-id - - {{ 'rule-node-config.aws-access-key-id-required' | translate }} - + @if (snsConfigForm.get('accessKeyId').hasError('required')) { + + {{ 'rule-node-config.aws-access-key-id-required' | translate }} + + } - + rule-node-config.aws-secret-access-key - - {{ 'rule-node-config.aws-secret-access-key-required' | translate }} - + @if (snsConfigForm.get('secretAccessKey').hasError('required')) { + + {{ 'rule-node-config.aws-secret-access-key-required' | translate }} + + } - + rule-node-config.aws-region - - {{ 'rule-node-config.aws-region-required' | translate }} - + @if (snsConfigForm.get('region').hasError('required')) { + + {{ 'rule-node-config.aws-region-required' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html index 8e3de40601..e803d37921 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html @@ -16,61 +16,78 @@ -->
- + rule-node-config.queue-type - - {{ sqsQueueTypeTranslationsMap.get(type) | translate }} - + @for (type of sqsQueueTypes; track type) { + + {{ sqsQueueTypeTranslationsMap.get(type) | translate }} + + } - + rule-node-config.queue-url-pattern - - {{ 'rule-node-config.queue-url-pattern-required' | translate }} - + @if (sqsConfigForm.get('queueUrlPattern').hasError('required')) { + + {{ 'rule-node-config.queue-url-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint - - rule-node-config.delay-seconds - - - {{ 'rule-node-config.min-delay-seconds-message' | translate }} - - - {{ 'rule-node-config.max-delay-seconds-message' | translate }} - - + @if (sqsConfigForm.get('queueType').value === sqsQueueType.STANDARD) { + + rule-node-config.delay-seconds + + @if (sqsConfigForm.get('delaySeconds').hasError('min')) { + + {{ 'rule-node-config.min-delay-seconds-message' | translate }} + + } + @if (sqsConfigForm.get('delaySeconds').hasError('max')) { + + {{ 'rule-node-config.max-delay-seconds-message' | translate }} + + } + + }
- - - + [keyText]="'rule-node-config.name' | translate" + [keyRequiredText]="'rule-node-config.name-required' | translate" + [valText]="'rule-node-config.value' | translate" + [valRequiredText]="'rule-node-config.value-required' | translate" + [emptyText]="'rule-node-config.message-attributes-empty' | translate"> + + rule-node-config.aws-access-key-id - - {{ 'rule-node-config.aws-access-key-id-required' | translate }} - + @if (sqsConfigForm.get('accessKeyId').hasError('required')) { + + {{ 'rule-node-config.aws-access-key-id-required' | translate }} + + } - + rule-node-config.aws-secret-access-key - - {{ 'rule-node-config.aws-secret-access-key-required' | translate }} - + @if (sqsConfigForm.get('secretAccessKey').hasError('required')) { + + {{ 'rule-node-config.aws-secret-access-key-required' | translate }} + + } - + rule-node-config.aws-region - - {{ 'rule-node-config.aws-region-required' | translate }} - + @if (sqsConfigForm.get('region').hasError('required')) { + + {{ 'rule-node-config.aws-region-required' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts index ee6cfac5da..7e5f93de67 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts @@ -30,7 +30,7 @@ export class SqsConfigComponent extends RuleNodeConfigurationComponent { sqsConfigForm: UntypedFormGroup; sqsQueueType = SqsQueueType; - sqsQueueTypes = Object.keys(SqsQueueType); + sqsQueueTypes = Object.values(SqsQueueType); sqsQueueTypeTranslationsMap = sqsQueueTypeTranslations; constructor(private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html index c4cd8f18ed..ae5a957bf6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html @@ -23,6 +23,7 @@
@@ -30,6 +31,7 @@ matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.field-name' | translate } }}">help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html index 07a1d1068a..6aee9bf101 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html @@ -18,15 +18,18 @@
rule-node-config.relation-search-parameters
- + {{ 'relation.direction' | translate }} - - {{ entitySearchDirectionTranslationsMap.get(direction) | translate }} rule-node-config.relations-query-config-direction-suffix - + @for (direction of entitySearchDirection; track direction) { + + {{ entitySearchDirectionTranslationsMap.get(direction) | translate }} rule-node-config.relations-query-config-direction-suffix + + } @@ -36,20 +39,25 @@ {{ 'rule-node-config.check-relation-to-specific-entity' | translate }}
-
- - - - -
+ @if (checkRelationConfigForm.get('checkForSingleEntity').value) { +
+ + + @if (checkRelationConfigForm.get('entityType').value) { + + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html index ec2a5ba89a..13bc0b15dd 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html @@ -20,37 +20,43 @@
rule-node-config.coordinate-field-names
- + {{ 'rule-node-config.latitude-field-name' | translate }} - - {{ 'rule-node-config.latitude-field-name-required' | translate }} - + @if (geoFilterConfigForm.get('latitudeKeyName').hasError('required')) { + + {{ 'rule-node-config.latitude-field-name-required' | translate }} + + } - + {{ 'rule-node-config.longitude-field-name' | translate }} - - {{ 'rule-node-config.longitude-field-name-required' | translate }} - + @if (geoFilterConfigForm.get('longitudeKeyName').hasError('required')) { + + {{ 'rule-node-config.longitude-field-name-required' | translate }} + + }
-
rule-node-config.coordinate-field-hint
+
rule-node-config.coordinate-field-hint
rule-node-config.geofence-configuration
- + {{ 'rule-node-config.perimeter-type' | translate }} - - {{ perimeterTypeTranslationMap.get(type) | translate }} - + @for (type of perimeterTypes; track type) { + + {{ perimeterTypeTranslationMap.get(type) | translate }} + + }
+ class="tb-form-row no-border no-padding slide-toggle"> {{ 'rule-node-config.fetch-perimeter-info-from-metadata' | translate }}
- - {{ 'rule-node-config.perimeter-key-name' | translate }} - - - {{ 'rule-node-config.perimeter-key-name-required' | translate }} - - {{ 'rule-node-config.perimeter-key-name-hint' | translate }} - -
-
- - {{ 'rule-node-config.circle-center-latitude' | translate }} - - - {{ 'rule-node-config.circle-center-latitude-required' | translate }} + @if (geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { + + {{ 'rule-node-config.perimeter-key-name' | translate }} + + @if (geoFilterConfigForm.get('perimeterKeyName').hasError('required')) { + + {{ 'rule-node-config.perimeter-key-name-required' | translate }} - - - {{ 'rule-node-config.circle-center-longitude' | translate }} - - - {{ 'rule-node-config.circle-center-longitude-required' | translate }} - - + } + {{ 'rule-node-config.perimeter-key-name-hint' | translate }} + + } + @if (geoFilterConfigForm.get('perimeterType').value === perimeterType.CIRCLE && + !geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { +
+
+ + {{ 'rule-node-config.circle-center-latitude' | translate }} + + @if (geoFilterConfigForm.get('centerLatitude').hasError('required')) { + + {{ 'rule-node-config.circle-center-latitude-required' | translate }} + + } + + + {{ 'rule-node-config.circle-center-longitude' | translate }} + + @if (geoFilterConfigForm.get('centerLongitude').hasError('required')) { + + {{ 'rule-node-config.circle-center-longitude-required' | translate }} + + } + +
+
+ + {{ 'rule-node-config.range' | translate }} + + @if (geoFilterConfigForm.get('range').hasError('required')) { + + {{ 'rule-node-config.range-required' | translate }} + + } + + + {{ 'rule-node-config.range-units' | translate }} + + @for (type of rangeUnits; track type) { + + {{ rangeUnitTranslationMap.get(type) | translate }} + + } + + @if (geoFilterConfigForm.get('rangeUnit').hasError('required')) { + + {{ 'rule-node-config.range-units-required' | translate }} + + } + +
-
- - {{ 'rule-node-config.range' | translate }} - - - {{ 'rule-node-config.range-required' | translate }} + } + @if (geoFilterConfigForm.get('perimeterType').value === perimeterType.POLYGON && + !geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { + + {{ 'rule-node-config.polygon-definition' | translate }} + + {{ 'rule-node-config.polygon-definition-hint' | translate }} + @if (geoFilterConfigForm.get('polygonsDefinition').hasError('required')) { + + {{ 'rule-node-config.polygon-definition-required' | translate }} - - - {{ 'rule-node-config.range-units' | translate }} - - - {{ rangeUnitTranslationMap.get(type) | translate }} - - - - {{ 'rule-node-config.range-units-required' | translate }} - - -
-
- - {{ 'rule-node-config.polygon-definition' | translate }} - - {{ 'rule-node-config.polygon-definition-hint' | translate }} - - {{ 'rule-node-config.polygon-definition-required' | translate }} - - + } + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html index a0d90bab4a..b94519d914 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html @@ -17,6 +17,7 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html index 28ff15604e..42b8f371a9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html @@ -16,41 +16,47 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (switchConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (switchConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.html b/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.html index c4d813b757..8b8ff8ffa4 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.html @@ -27,6 +27,7 @@ [excludeEntityIds]="[ruleChainId]" [entityType]="entityType.RULE_CHAIN" [entitySubtype]="ruleChainType" + appearance="outline" formControlName="ruleChainId">
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts b/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts index 3b5b67cfbc..7a2bec7e33 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts @@ -269,7 +269,8 @@ export enum HttpRequestType { GET = 'GET', POST = 'POST', PUT = 'PUT', - DELETE = 'DELETE' + DELETE = 'DELETE', + PATCH = 'PATCH' } export const ToByteStandartCharsetTypes = [ @@ -826,7 +827,7 @@ export enum AttributeScopeResult { SERVER_SCOPE = 'SERVER_SCOPE' } -export const AttributeScopeMap = new Map([ +export const AttributeScopeMap = new Map([ [AttributeScope.SHARED_SCOPE, 'rule-node-config.shared-scope'], [AttributeScope.SERVER_SCOPE, 'rule-node-config.server-scope'], [AttributeScope.CLIENT_SCOPE, 'rule-node-config.client-scope'] diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html index aecafa4164..efcea2eb7b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html @@ -16,51 +16,60 @@ -->
- + rule-node-config.new-originator - - {{ originatorSourceTranslationMap.get(changeOriginatorConfigForm.get('originatorSource').value) | translate }} - + @if (originatorSourceTranslationMap.get(changeOriginatorConfigForm.get('originatorSource').value)) { + + {{ originatorSourceTranslationMap.get(changeOriginatorConfigForm.get('originatorSource').value) | translate }} + + } - - - {{ originatorSourceTranslationMap.get(source) | translate }} - -
- - {{ originatorSourceDescTranslationMap.get(source) | translate }} - -
-
-
-
- - -
- + + {{ originatorSourceTranslationMap.get(source) | translate }} + +
+ + {{ originatorSourceDescTranslationMap.get(source) | translate }} + + + } + + + @if (changeOriginatorConfigForm.get('originatorSource').value === originatorSource.ENTITY) { +
+ + +
+ + + + rule-node-config.entity-name-pattern + + @if (changeOriginatorConfigForm.get('entityNamePattern').hasError('required') || + changeOriginatorConfigForm.get('entityNamePattern').hasError('pattern')) { + + {{ 'rule-node-config.entity-name-pattern-required' | translate }} + + } + +
+
+ } + @if (changeOriginatorConfigForm.get('originatorSource').value === originatorSource.RELATED) { + -
- - rule-node-config.entity-name-pattern - - - {{ 'rule-node-config.entity-name-pattern-required' | translate }} - - -
-
- - -
+ formControlName="relationsQuery"> + + } + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html index 0fc80e6197..a899a344b3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html @@ -23,6 +23,7 @@
- + {{'rule-node-config.interval' | translate}} - - {{'rule-node-config.interval-required' | translate}} - - - {{'rule-node-config.interval-min-error' | translate}} - + @if (deduplicationConfigForm.get('interval').hasError('required')) { + + {{'rule-node-config.interval-required' | translate}} + + } + @if (deduplicationConfigForm.get('interval').hasError('min')) { + + {{'rule-node-config.interval-min-error' | translate}} + + } help + matTooltip="{{ 'rule-node-config.interval-hint' | translate }}">help
rule-node-config.strategy
- - {{ deduplicationStrategiesTranslations.get(strategy) | translate }} - + @for (strategy of deduplicationStrategies; track strategy) { + + {{ deduplicationStrategiesTranslations.get(strategy) | translate }} + + } - - - - - - -
- - -
+ @if (deduplicationConfigForm.get('strategy').value === 'ALL') { + + + } + @if (deduplicationConfigForm.get('strategy').value === 'FIRST') { + + + } + @if (deduplicationConfigForm.get('strategy').value === 'LAST') { + + + } + @if (deduplicationConfigForm.get('strategy').value === deduplicationStrategie.ALL) { +
+ + +
+ }
@@ -61,37 +73,49 @@ rule-node-config.advanced-settings
- + {{'rule-node-config.max-pending-msgs' | translate}} - - {{'rule-node-config.max-pending-msgs-required' | translate}} - - - {{'rule-node-config.max-pending-msgs-max-error' | translate}} - - - {{'rule-node-config.max-pending-msgs-min-error' | translate}} - + @if (deduplicationConfigForm.get('maxPendingMsgs').hasError('required')) { + + {{'rule-node-config.max-pending-msgs-required' | translate}} + + } + @if (deduplicationConfigForm.get('maxPendingMsgs').hasError('max')) { + + {{'rule-node-config.max-pending-msgs-max-error' | translate}} + + } + @if (deduplicationConfigForm.get('maxPendingMsgs').hasError('min')) { + + {{'rule-node-config.max-pending-msgs-min-error' | translate}} + + } help + matTooltip="{{ 'rule-node-config.max-pending-msgs-hint' | translate }}">help - + {{'rule-node-config.max-retries' | translate}} - - {{'rule-node-config.max-retries-required' | translate}} - - - {{'rule-node-config.max-retries-max-error' | translate}} - - - {{'rule-node-config.max-retries-min-error' | translate}} - + @if (deduplicationConfigForm.get('maxRetries').hasError('required')) { + + {{'rule-node-config.max-retries-required' | translate}} + + } + @if (deduplicationConfigForm.get('maxRetries').hasError('max')) { + + {{'rule-node-config.max-retries-max-error' | translate}} + + } + @if (deduplicationConfigForm.get('maxRetries').hasError('min')) { + + {{'rule-node-config.max-retries-min-error' | translate}} + + } help + matTooltip="{{ 'rule-node-config.max-retries-hint' | translate }}">help
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts index 77c95515d3..1efcc0351a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts @@ -31,7 +31,7 @@ export class DeduplicationConfigComponent extends RuleNodeConfigurationComponent deduplicationConfigForm: FormGroup; deduplicationStrategie = FetchMode; - deduplicationStrategies = Object.keys(this.deduplicationStrategie); + deduplicationStrategies = Object.values(this.deduplicationStrategie); deduplicationStrategiesTranslations = deduplicationStrategiesTranslations; constructor(private fb: FormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html index c5d94739d6..4da3fdf45c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html @@ -22,6 +22,7 @@
- + {{ 'rule-node-config.json-path-expression' | translate }} {{ 'rule-node-config.json-path-expression-hint' | translate }} - {{ 'rule-node-config.json-path-expression-required' | translate }} + @if (jsonPathConfigForm.get('jsonPath').hasError('required')) { + {{ 'rule-node-config.json-path-expression-required' | translate }} + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html index 89ce705ea9..8aeab13cd9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html @@ -20,9 +20,11 @@
- - {{ data.name }} - + @for (data of renameIn; track data) { + + {{ data.name }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html index 7b39ba131d..8d73d4c0ff 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html @@ -16,41 +16,47 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + }
@@ -46,94 +48,106 @@
rule-node-config.recipients
+ popupHelpLink="rulenode/to_email_node_fields_templatization">
- + rule-node-config.to-template - - {{ 'rule-node-config.to-template-required' | translate }} - + cdkAutosizeMinRows="1"> + @if (toEmailConfigForm.get('toTemplate').hasError('required')) { + + {{ 'rule-node-config.to-template-required' | translate }} + + } - + rule-node-config.cc-template + cdkAutosizeMinRows="1"> - + rule-node-config.bcc-template + cdkAutosizeMinRows="1">
rule-node-config.message-subject-and-content
+ popupHelpLink="rulenode/to_email_node_fields_templatization">
- + rule-node-config.subject-template - - {{ 'rule-node-config.subject-template-required' | translate }} - + cdkAutosizeMinRows="1"> + @if (toEmailConfigForm.get('subjectTemplate').hasError('required')) { + + {{ 'rule-node-config.subject-template-required' | translate }} + + } - + rule-node-config.mail-body-type - - {{ getBodyTypeName() | translate }} - + @if (toEmailConfigForm.get('mailBodyType').value) { + + {{ getBodyTypeName() | translate }} + + } - - - {{ type.name | translate }} - -
- - {{ type.description | translate }} - -
-
-
- - rule-node-config.body-type-template - - rule-node-config.mail-body-types.after-template-evaluation-hint - - - rule-node-config.body-template - - - {{ 'rule-node-config.body-template-required' | translate }} - - -
-
-
+ @for (type of mailBodyTypes; track type) { + + + {{ type.name | translate }} + +
+ + {{ type.description | translate }} + +
+ } + +
+ @if (toEmailConfigForm.get('mailBodyType').value === 'dynamic') { + + rule-node-config.body-type-template + + rule-node-config.mail-body-types.after-template-evaluation-hint + + } + + rule-node-config.body-template + + @if (toEmailConfigForm.get('bodyTemplate').hasError('required')) { + + {{ 'rule-node-config.body-template-required' | translate }} + + } + + +
+ diff --git a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html index 3beaa93ed1..f48f8b2191 100644 --- a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html @@ -16,26 +16,32 @@ --> - + admin.aws-access-key-id - - {{ 'admin.aws-access-key-id-required' | translate }} - + @if (awsSnsProviderConfigurationFormGroup.get('accessKeyId').hasError('required')) { + + {{ 'admin.aws-access-key-id-required' | translate }} + + } - + admin.aws-secret-access-key - - {{ 'admin.aws-secret-access-key-required' | translate }} - + @if (awsSnsProviderConfigurationFormGroup.get('secretAccessKey').hasError('required')) { + + {{ 'admin.aws-secret-access-key-required' | translate }} + + } - + admin.aws-region - - {{ 'admin.aws-region-required' | translate }} - + @if (awsSnsProviderConfigurationFormGroup.get('region').hasError('required')) { + + {{ 'admin.aws-region-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html index bc1e9430c7..92b986f0c8 100644 --- a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html @@ -17,44 +17,54 @@ -->
- + admin.smpp-provider.smpp-version - - {{smppVersion.value}} - + @for (smppVersion of smppVersions; track smppVersion) { + + {{smppVersion.value}} + + } - + admin.smpp-provider.smpp-host - - {{'admin.smpp-provider.smpp-host-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('host').hasError('required')) { + + {{'admin.smpp-provider.smpp-host-required' | translate}} + + } - + admin.smpp-provider.smpp-port - - {{'admin.smpp-provider.smpp-port-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('port').hasError('required')) { + + {{'admin.smpp-provider.smpp-port-required' | translate}} + + }
- + admin.smpp-provider.system-id - - {{'admin.smpp-provider.system-id-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('systemId').hasError('required')) { + + {{'admin.smpp-provider.system-id-required' | translate}} + + } - + admin.smpp-provider.password - - {{'admin.smpp-provider.password-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('password').hasError('required')) { + + {{'admin.smpp-provider.password-required' | translate}} + + }
@@ -65,19 +75,21 @@ - + admin.smpp-provider.system-type - + admin.smpp-provider.bind-type - - {{bindTypesTranslation.get(bindType) | translate}} - + @for (bindType of bindTypes; track bindType) { + + {{bindTypesTranslation.get(bindType) | translate}} + + } - + admin.smpp-provider.service-type @@ -90,24 +102,28 @@ - + admin.smpp-provider.source-address - + admin.smpp-provider.source-ton - - {{typeOfNumberMap.get(sourceTon).name | translate}} - + @for (sourceTon of typeOfNumber; track sourceTon) { + + {{typeOfNumberMap.get(sourceTon).name | translate}} + + } - + admin.smpp-provider.source-npi - - {{numberingPlanIdentificationMap.get(sourceNpi).name | translate}} - + @for (sourceNpi of numberingPlanIdentification; track sourceNpi) { + + {{numberingPlanIdentificationMap.get(sourceNpi).name | translate}} + + } @@ -119,20 +135,24 @@ - + admin.smpp-provider.destination-ton - - {{typeOfNumberMap.get(destinationTon).name | translate}} - + @for (destinationTon of typeOfNumber; track destinationTon) { + + {{typeOfNumberMap.get(destinationTon).name | translate}} + + } - + admin.smpp-provider.destination-npi - - {{numberingPlanIdentificationMap.get(destinationNpi).name | translate}} - + @for (destinationNpi of numberingPlanIdentification; track destinationNpi) { + + {{numberingPlanIdentificationMap.get(destinationNpi).name | translate}} + + } @@ -144,16 +164,18 @@ - + admin.smpp-provider.address-range - + admin.smpp-provider.coding-scheme - - {{codingSchemesMap.get(codingScheme).name | translate}} - + @for (codingScheme of codingSchemes; track codingScheme) { + + {{codingSchemesMap.get(codingScheme).name | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts index 35111be91b..4a4341364b 100644 --- a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts @@ -68,16 +68,16 @@ export class SmppSmsProviderConfigurationComponent implements ControlValueAcces smppVersions = smppVersions; - bindTypes = Object.keys(BindTypes); + bindTypes = Object.values(BindTypes); bindTypesTranslation = bindTypesTranslationMap; - typeOfNumber = Object.keys(TypeOfNumber); + typeOfNumber = Object.values(TypeOfNumber); typeOfNumberMap = typeOfNumberMap; - numberingPlanIdentification = Object.keys(NumberingPlanIdentification); + numberingPlanIdentification = Object.values(NumberingPlanIdentification); numberingPlanIdentificationMap = numberingPlanIdentificationMap; - codingSchemes = Object.keys(CodingSchemes); + codingSchemes = Object.values(CodingSchemes); codingSchemesMap = codingSchemesMap; private propagateChange = (v: any) => { }; diff --git a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html index 544f8866f0..9d4d540a04 100644 --- a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html @@ -16,35 +16,41 @@ -->
- + admin.sms-provider-type - - {{smsProviderTypeTranslations.get(type) | translate}} - + @for (type of smsProviderTypes; track type) { + + {{smsProviderTypeTranslations.get(type) | translate}} + + } - - {{ 'admin.sms-provider-type-required' | translate }} - + @if (smsProviderConfigurationFormGroup.get('type').hasError('required')) { + + {{ 'admin.sms-provider-type-required' | translate }} + + } -
- - - - - - - - - - - - +
+ @switch (smsProviderConfigurationFormGroup.get('type').value) { + @case (smsProviderType.AWS_SNS) { + + + } + @case (smsProviderType.TWILIO) { + + + } + @case (smsProviderType.SMPP) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts index adb477d8fb..86f07cfaaf 100644 --- a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts @@ -47,7 +47,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export class SmsProviderConfigurationComponent implements ControlValueAccessor, OnInit { smsProviderType = SmsProviderType; - smsProviderTypes = Object.keys(SmsProviderType); + smsProviderTypes = Object.values(SmsProviderType); smsProviderTypeTranslations = smsProviderTypeTranslationMap; smsProviderConfigurationFormGroup: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html index 99d64302ec..b82e951fe2 100644 --- a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html @@ -16,30 +16,38 @@ --> - + admin.number-from - - {{ 'admin.number-from-required' | translate }} - - - {{ 'admin.phone-number-pattern-twilio' | translate }} - + @if (twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('required')) { + + {{ 'admin.number-from-required' | translate }} + + } + @if (twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('pattern')) { + + {{ 'admin.phone-number-pattern-twilio' | translate }} + + } - + admin.twilio-account-sid - - {{ 'admin.twilio-account-sid-required' | translate }} - + @if (twilioSmsProviderConfigurationFormGroup.get('accountSid').hasError('required')) { + + {{ 'admin.twilio-account-sid-required' | translate }} + + } - + admin.twilio-account-token - - {{ 'admin.twilio-account-token-required' | translate }} - + @if (twilioSmsProviderConfigurationFormGroup.get('accountToken').hasError('required')) { + + {{ 'admin.twilio-account-token-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html index 548a7b79c3..00d1f9f782 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html @@ -24,81 +24,92 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
admin.auto-commit-entities
-
- - -
- -
-
-
-
- - -
-
- -
- -
-
- - -
- + + +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+ - + formControlName="entityType" + required + [filterAllowedEntityTypes]="false" + [allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)"> + +
+ + +
+
+
+ @if (entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE) { + + {{ 'version-control.export-credentials' | translate }} + + } + + {{ 'version-control.export-attributes' | translate }} + + + {{ 'version-control.export-relations' | translate }} + + @if (typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)) { + + {{ 'version-control.export-calculated-fields' | translate }} + + }
-
-
- - {{ 'version-control.export-credentials' | translate }} - - - {{ 'version-control.export-attributes' | translate }} - - - {{ 'version-control.export-relations' | translate }} - - - {{ 'version-control.export-calculated-fields' | translate }} -
-
- - -
-
- admin.no-auto-commit-entities-prompt -
+ + +
+ } + @if (!entityTypesFormGroupArray().length) { +
+ admin.no-auto-commit-entities-prompt +
+ }
-
version-control.auto-commit-settings-read-only-hint
+ @if (isReadOnly | async) { +
version-control.auto-commit-settings-read-only-hint
+ }
+
- - version-control.default-sync-strategy - - - {{syncStrategyTranslations.get(strategy) | translate}} - - - - - - - -
- - -
- -
-
-
-
- -
- +} @else { +
+ @if ((versionCreateResult$ | async)?.done || hasError) { +
+ +
+ } @else {
version-control.creating-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html index 96b1927040..a5ced53c03 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html @@ -15,59 +15,63 @@ limitations under the License. --> -
- -

{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}

- -
- - -
- - -
-
- - {{ 'version-control.rollback-on-error' | translate }} - -
-
- - -
-
-
-
+@if (!versionLoadResult$) { +
+ +

{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}

+ +
+ + +
+ + +
+
+ + {{ 'version-control.rollback-on-error' | translate }} + +
+
+ + +
+
+} @else { +
{{ 'version-control.no-entities-restored' | translate }}
-
-
{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}
-
- -
- +
+ @for (entityTypeLoadResult of entityTypeLoadResults; track entityTypeLoadResult.entityType) { +
{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}
+ } + @if ((versionLoadResult$ | async)?.done || hasError) { +
+ +
+ } @else {
version-control.restoring-entities-from-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html index 464dd3cded..4adeb47d69 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html @@ -18,86 +18,90 @@
version-control.entities-to-export
-
- - -
- -
-
{{ entityTypeText(entityTypeFormGroup) }}
-
-
- - -
-
-
- -
- - -
- - version-control.sync-strategy - - - {{ 'version-control.default' | translate }} - - - {{syncStrategyTranslations.get(strategy) | translate}} - - - -
- - {{ 'version-control.export-credentials' | translate }} - - - {{ 'version-control.export-attributes' | translate }} - - - {{ 'version-control.export-relations' | translate }} - - - {{ 'version-control.export-calculated-fields' | translate }} - + @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) { +
+ + +
+ +
+
{{ entityTypeText(entityTypeFormGroup) }}
+
+
+ + +
+
+
+ +
+ + +
+ + version-control.sync-strategy + + + {{ 'version-control.default' | translate }} + + @for (strategy of syncStrategies; track strategy) { + + {{syncStrategyTranslations.get(strategy) | translate}} + + } + + +
+ + {{ 'version-control.export-credentials' | translate }} + + + {{ 'version-control.export-attributes' | translate }} + + + {{ 'version-control.export-relations' | translate }} + + + {{ (entityTypeFormGroup.get('entityType').value === entityTypes.CUSTOMER ? 'version-control.export-alarm-rules' : 'version-control.export-calculated-fields') | translate }} + +
+
+ + {{ 'version-control.all-entities' | translate }} + + @if (!entityTypeFormGroup.get('config').get('allEntities').value) { + + + } +
-
- - {{ 'version-control.all-entities' | translate }} - - - -
-
- -
-
- version-control.no-entities-to-export-prompt -
+ +
+ } @empty { + version-control.no-entities-to-export-prompt + }
-
- -
- -
- - -
-
- - {{ 'version-control.remove-other-entities' | translate }} - - - {{ 'version-control.find-existing-entity-by-name' | translate }} - -
-
- - {{ 'version-control.load-credentials' | translate }} - - - {{ 'version-control.load-attributes' | translate }} - - - {{ 'version-control.load-relations' | translate }} - - - {{ 'version-control.load-calculated-fields' | translate }} - + @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) { +
+ + +
+ +
+
{{ entityTypeText(entityTypeFormGroup) }}
+
+
+ + +
+
+
+ +
+ + +
+
+ + {{ 'version-control.remove-other-entities' | translate }} + + + {{ 'version-control.find-existing-entity-by-name' | translate }} + +
+
+ + {{ 'version-control.load-credentials' | translate }} + + + {{ 'version-control.load-attributes' | translate }} + + + {{ 'version-control.load-relations' | translate }} + + + {{ (entityTypeFormGroup.get('entityType').value === entityTypes.CUSTOMER ? 'version-control.load-alarm-rules' : 'version-control.load-calculated-fields') | translate }} + +
-
- -
-
- version-control.no-entities-to-restore-prompt -
+ +
+ } @empty { + version-control.no-entities-to-restore-prompt + }
- -
-
-
+} @else { + @if ((versionCreateResult$ | async)?.done || resultMessage) { +
{{ resultMessage }}
-
- + } @else {
version-control.creating-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.html index 941a2e42c1..4ae0715981 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.html @@ -18,54 +18,62 @@
- -

{{ 'version-control.diff-entity-with-version' | translate: {versionName} }}

- - - - - {{ 'version-control.differences' | translate : {count: diffCount} }} - - - -
-
-
{{ 'version-control.current' | translate }}
-
-
{{ versionIdContent() }}
-
- + @if (contentReady) { + +

{{ 'version-control.diff-entity-with-version' | translate: {versionName} }}

+ + + + + {{ 'version-control.differences' | translate : {count: diffCount} }} + + + +
+ } + @if (contentReady) { +
+
{{ 'version-control.current' | translate }}
+
+
{{ versionIdContent() }}
+
+ } + @if (!contentReady) { + + }
-
- -
+ @if (contentReady) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index 6057126270..53672bf812 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -15,51 +15,53 @@ limitations under the License. --> -
- -

{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

- -
- - - - -
-
- - {{ 'version-control.load-credentials' | translate }} - - - {{ 'version-control.load-attributes' | translate }} - - - {{ 'version-control.load-relations' | translate }} - - - {{ 'version-control.load-calculated-fields' | translate }} - -
-
- -
- - -
-
-
-
-
+@if (!versionLoadResult$) { + @if (entityDataInfo) { + +

{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

+ +
+ + +
+
+
+ + {{ 'version-control.load-credentials' | translate }} + + + {{ 'version-control.load-attributes' | translate }} + + + {{ 'version-control.load-relations' | translate }} + + + {{ ( externalEntityId?.entityType === EntityType.CUSTOMER ? 'version-control.load-alarm-rules' : 'version-control.load-calculated-fields') | translate }} + +
+
+
+
+ + +
+ } @else { + + } +} @else { + @if ((versionLoadResult$ | async)?.done || errorMessage) { +
-
- + } @else {
version-control.restoring-entity-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts index 7b1eaf1621..06b5f60441 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts @@ -33,6 +33,7 @@ import { delay, share } from 'rxjs/operators'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { Observable, Subscription } from 'rxjs'; import { parseHttpErrorMessage } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-entity-version-restore', @@ -63,6 +64,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn errorMessage: SafeHtml; + EntityType = EntityType; + versionLoadResult$: Observable; private versionLoadResultSubscription: Subscription; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html index e58a93cf30..32a39a713e 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html @@ -32,21 +32,17 @@
-
- - +
+ @if (singleEntityMode) { + + }
+ @if (!singleEntityMode) { + + }
@@ -114,7 +120,9 @@ {{ 'version-control.version-name' | translate }} - {{ entityVersion.name }} +
+ {{ entityVersion.name?.length > 256 ? entityVersion.name.substring(0, 256) + '…' : entityVersion.name }} +
@@ -125,31 +133,37 @@ + [class.min-w-10]="!singleEntityMode" [class.max-w-10]="!singleEntityMode" [class.w-10]="!singleEntityMode">
- - - + @if (singleEntityMode) { + + } + @if (singleEntityMode) { + + } + @if (!singleEntityMode) { + + }
@@ -158,12 +172,12 @@ {{ - singleEntityMode - ? 'version-control.no-entity-versions-text' - : 'version-control.no-versions-text' - }} + singleEntityMode + ? 'version-control.no-entity-versions-text' + : 'version-control.no-versions-text' + }} {{ 'common.loading' | translate }} + class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }}
+ showFirstLastButtons>
diff --git a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html index f6ceab690b..60278cbe9b 100644 --- a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html @@ -19,7 +19,7 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html index ba9d850ab0..be7a8cda56 100644 --- a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html @@ -26,20 +26,26 @@
- - -
+ @if (!hideLoadingBar && isLoading$ | async) { + + + } + @if (hideLoadingBar || (isLoading$ | async) === false) { +
+ }
- + admin.repository-url - - admin.repository-url-required - + @if (repositorySettingsForm.get('repositoryUri').hasError('required')) { + + admin.repository-url-required + + } - + admin.default-branch @@ -53,31 +59,37 @@
admin.authentication-settings - + admin.auth-method - - {{repositoryAuthMethodTranslations.get(method) | translate}} - + @for ( method of repositoryAuthMethods; track method) { + + {{repositoryAuthMethodTranslations.get(method) | translate}} + + }
- + common.username - - {{ 'admin.change-password-access-token' | translate }} - - - admin.password-access-token - - - - + @if (showChangePassword) { + + {{ 'admin.change-password-access-token' | translate }} + + } + @if (changePassword || !showChangePassword) { + + admin.password-access-token + + + + + }
- - {{ 'admin.change-passphrase' | translate }} - - - admin.passphrase - - - + @if (showChangePrivateKeyPassword) { + + {{ 'admin.change-passphrase' | translate }} + + } + @if (changePrivateKeyPassword || !showChangePrivateKeyPassword) { + + admin.passphrase + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html index e02052c2b1..4a71456920 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html @@ -15,13 +15,14 @@ limitations under the License. --> - - - +@if (!(hasRepository$ | async)) { + + +} @else { - + (versionRestored)="versionRestored.emit()"> +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html index 7df9a4893f..8b0ca22543 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
@@ -35,57 +39,66 @@
{{'widget-config.action-source' | translate}}*
- - {{ actionSourceName(actionSourceItem.value) }} - + @for (actionSourceItem of data.actionsData.actionSources | keyvalue; track actionSourceItem) { + + {{ actionSourceName(actionSourceItem.value) }} + + } - - warning - - -
-
-
{{'widget-config.column-index' | translate}}*
- - - - {{ getCellClickColumnInfo($index, column) }} - - - - warning - + @if (widgetActionFormGroup.get('actionSourceId').hasError('required') + && widgetActionFormGroup.get('actionSourceId').touched) { + + warning + + }
+ @if (widgetActionFormGroup.get('actionSourceId').value === 'cellClick') { +
+
{{'widget-config.column-index' | translate}}*
+ + + @for (column of configuredColumns; track column) { + + {{ getCellClickColumnInfo($index, column) | customTranslate }} + + } + + @if (widgetActionFormGroup.get('columnIndex').hasError('required') && + widgetActionFormGroup.get('columnIndex').touched) { + + warning + + } + +
+ }
{{'widget-config.action-name' | translate}}*
- - warning - + class="tb-error"> + warning + + }
@if (widgetActionFormGroup.get('actionSourceId').value === 'headerButton') { @@ -95,9 +108,11 @@
widget-config.header-button.button-type
- - {{ widgetHeaderActionButtonTypeTranslationMap.get(widgetHeaderActionButtonType[button]) | translate }} - + @for (button of widgetHeaderActionButtonTypes; track button) { + + {{ widgetHeaderActionButtonTypeTranslationMap.get(widgetHeaderActionButtonType[button]) | translate }} + + }
@@ -156,35 +171,37 @@
} -
- - - - - {{ 'widget-config.show-hide-action-using-function' | translate }} - - - - - - - -
+ @if (displayShowWidgetActionForm()) { +
+ + + + + {{ 'widget-config.show-hide-action-using-function' | translate }} + + + + + + + +
+ } + formControlName="widgetAction" + [callbacks]="data.callbacks" + [widgetType]="data.widgetType" + [additionalWidgetActionTypes]="data.additionalWidgetActionTypes">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html index 9ea5fc4aca..31b1cdb796 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html @@ -34,7 +34,6 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html index 5bab97a12d..454082ff54 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html @@ -16,14 +16,15 @@ --> - - + @if (!widgetEditMode) { + + + }
widgets.action-button.behavior
@@ -44,7 +45,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="activatedState"> + formControlName="activatedState">
widgets.button-state.disabled-state
@@ -55,7 +56,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html index 34ce199e65..e39d57b64e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.command-button.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html index 3370ef4186..8d05bad66d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.power-button.behavior
@@ -30,7 +30,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.power-button.power-on
@@ -39,7 +39,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.power-button.power-off
@@ -48,7 +48,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -59,7 +59,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -70,21 +70,30 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> - - {{ powerButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> + @for (layout of powerButtonLayouts; track layout) { + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + }
{{ 'widget-config.title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html index 0c9ddd233e..fd4790749c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html @@ -16,14 +16,15 @@ --> - - + @if (!widgetEditMode) { + + + }
widgets.action-button.behavior
@@ -37,7 +38,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
@@ -68,7 +69,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -80,12 +81,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> - - {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> + @for (layout of segmentedButtonLayouts; track layout) { + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + }
@@ -120,9 +123,15 @@ {{ 'widgets.button.label' | translate }}
- - - + @@ -151,9 +160,15 @@ {{ 'widgets.button.label' | translate }}
- - - + @@ -212,16 +227,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
@@ -253,16 +270,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts index 296959a4b4..532d543719 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { Datasource, TargetDevice, } from '@shared/models/widget.models'; +import { Datasource, TargetDevice, widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { ValueType } from '@shared/models/constants'; import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models'; @@ -49,7 +49,7 @@ export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigCompon const datasources: Datasource[] = this.segmentedButtonWidgetConfigForm.get('datasources').value; return getTargetDeviceFromDatasources(datasources); } - + predefinedValues = widgetTitleAutocompleteValues; segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'first'; segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected'; @@ -143,4 +143,52 @@ export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigCompon return this.widgetConfig; } + + protected validatorTriggers(): string[] { + return ['appearance.leftAppearance.showLabel', 'appearance.leftAppearance.showIcon', 'appearance.rightAppearance.showLabel', 'appearance.rightAppearance.showIcon',]; + } + + protected updateValidators(emitEvent: boolean) { + const showLeftLabel: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.showLabel').value; + const showRightLabel: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.showLabel').value; + + const showLeftIcon: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.showIcon').value; + const showRightIcon: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.showIcon').value; + + if (showLeftLabel) { + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.label').enable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.labelFont').enable({emitEvent}); + } else { + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.label').disable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.labelFont').disable({emitEvent}); + } + + if (showRightLabel) { + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.label').enable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.labelFont').enable({emitEvent}); + } else { + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.label').disable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.labelFont').disable({emitEvent}); + } + + if (showLeftIcon) { + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.icon').enable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSize').enable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSizeUnit').enable({emitEvent}); + } else { + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.icon').disable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSize').disable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSizeUnit').disable({emitEvent}); + } + + if (showRightIcon) { + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.icon').enable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSize').enable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSizeUnit').enable({emitEvent}); + } else { + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.icon').disable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSize').disable({emitEvent}); + this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSizeUnit').disable({emitEvent}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html index 7c4b699c60..905e9e3bac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.toggle-button.behavior
@@ -92,9 +92,16 @@ {{ 'widget-config.title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html index 8b29fe82aa..25e9bd78da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html @@ -28,17 +28,23 @@
widgets.aggregated-value-card.arrow
-
-
- - + @if (keysFormArray().controls.length) { +
+ @for (keyControl of keysFormArray().controls; track trackByKey($index, keyControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.aggregated-value-card.no-values' | translate }} + }
- - {{ 'widgets.aggregated-value-card.no-values' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html index 52e1375d8d..f0897ae55c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html @@ -39,9 +39,16 @@ {{ 'widget-config.title' | translate }}
- - - +
- - - + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts index 4be3bc7eae..75c938dd23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { ComparisonResultType, DataKey, Datasource, WidgetConfig, } from '@shared/models/widget.models'; +import { ComparisonResultType, DataKey, Datasource, WidgetConfig, widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { @@ -81,6 +81,8 @@ export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigCo datePreviewFn = this._datePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private cd: ChangeDetectorRef, @@ -102,10 +104,10 @@ export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigCo history: { historyType: HistoryWindowType.INTERVAL, quickInterval: QuickTimeInterval.CURRENT_MONTH_SO_FAR, + interval: 12 * HOUR, }, aggregation: { type: AggregationType.AVG, - interval: 12 * HOUR, limit: 5000 } }; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html index 54b3da30c8..e4daba1f6d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + }
- - - + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts index d57add2d5b..312c8cb01e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts @@ -25,6 +25,7 @@ import { datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { formatValue, isUndefined } from '@core/utils'; @@ -61,6 +62,8 @@ export class LabelValueCardBasicConfigComponent extends BasicWidgetConfigCompone valuePreviewFn = this._valuePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.html index babdecc8ce..23bd465308 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.html @@ -15,101 +15,107 @@ limitations under the License. --> - -
-
- - {{ 'admin.mobile-app.use-system-settings' | translate }} - -
-
-
-
-
widget-config.appearance
-
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgePosition.value | translate }} - - - -
-
- - {{ 'admin.mobile-app.label' | translate }} +@if (mobileAppQrCodeWidgetConfigForm) { + +
+
+ + {{ 'admin.mobile-app.use-system-settings' | translate }} - - -
-
- - {{ 'widgets.liquid-level-card.title' | translate }} - -
- - - - - - - +
+ @if (!mobileAppQrCodeWidgetConfigForm.get('useSystemSettings').value) { +
+
+
widget-config.appearance
+
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + @for (badgePosition of badgePositionTranslationsMap | keyvalue; track badgePosition) { + + {{ badgePosition.value | translate }} + + } + + +
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
+
+ + {{ 'widgets.liquid-level-card.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.liquid-level-card.icon' | translate }} + +
+ + + + + + + + +
+
-
-
- - {{ 'widgets.liquid-level-card.icon' | translate }} - -
- - - - - - - - +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+ +
-
-
-
widget-config.card-appearance
-
-
{{ 'widgets.background.background' | translate }}
- - -
-
-
widget-config.show-card-buttons
- - {{ 'fullscreen.fullscreen' | translate }} - -
-
-
{{ 'widget-config.card-border-radius' | translate }}
- - - -
-
-
{{ 'widget-config.card-padding' | translate }}
- - - -
-
- - -
- + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html index 83490e9a2d..756b7443c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ progressBarLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout"> + @for (layout of progressBarLayouts; track layout) { + + {{ progressBarLayoutTranslationMap.get(layout) | translate }} + + }
@@ -52,9 +56,16 @@ {{ 'widget-config.title' | translate }}
- - - +
-
- - {{ 'widgets.progress-bar.range-ticks' | translate }} - -
- - - - + @if (progressBarWidgetConfigForm.get('layout').value === progressBarLayout.default) { +
+ + {{ 'widgets.progress-bar.range-ticks' | translate }} + +
+ + + + +
-
+ }
widgets.progress-bar.bar
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts index 426c3ee8f7..3e184bae7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts @@ -25,6 +25,7 @@ import { datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -73,6 +74,8 @@ export class ProgressBarBasicConfigComponent extends BasicWidgetConfigComponent valuePreviewFn = this._valuePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html index 07874696f8..9e0f15d1f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } widget-config.appearance
widgets.simple-card.label
- - - +
widgets.simple-card.label-position
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts index 104787dd0f..4e05996186 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts @@ -25,6 +25,7 @@ import { datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -55,6 +56,8 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { simpleCardWidgetConfigForm: UntypedFormGroup; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html index f6b67f72b6..43777d596e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html @@ -44,9 +44,16 @@ {{ 'widget-config.card-title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html index 095b26e11d..90bbca8421 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ valueCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueCardLayouts; track layout) { + + {{ valueCardLayoutTranslationMap.get(layout) | translate }} + + }
@@ -53,9 +57,16 @@ {{ 'widgets.value-card.label' | translate }}
- - - + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts index 04905fd657..2c210b90f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts @@ -25,6 +25,7 @@ import { datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -85,6 +86,8 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent { return layout !== ValueCardLayout.simplified; } + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private cd: ChangeDetectorRef, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html index 5e45c9baa6..2ec1b80253 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html @@ -34,12 +34,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> - - {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueChartCardLayouts; track layout) { + + {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} + + }
@@ -51,9 +53,16 @@ {{ 'widget-config.title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts index 6a940a7a75..99aed7565a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { @@ -41,6 +41,8 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen @ViewChild('barChart') barChartConfigTemplate: TemplateRef; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index f28d0432ca..f927340691 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -49,9 +49,16 @@ {{ 'widget-config.title' | translate }}
- - - +
+ formControlName="grid">
@@ -160,38 +167,41 @@
+ formControlName="barBackgroundSettings" + titleText="widgets.chart.background" + fillNoneTitle="widgets.chart.fill-type-solid"> + stroked + formControlName="noAggregationBarWidthSettings">
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis">
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -207,9 +217,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -270,7 +282,7 @@
+ formControlName="tooltipDateFormat"> @@ -306,7 +318,7 @@
+ formControlName="animation">
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts index 07cd236fb5..d569bcf37b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts @@ -26,6 +26,7 @@ import { legendPositions, legendPositionTranslationMap, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -44,7 +45,10 @@ import { barChartWithLabelsDefaultSettings, BarChartWithLabelsWidgetSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; -import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartType, + updateLatestDataKeys +} from '@home/components/widget/lib/chart/time-series-chart.models'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ @@ -74,6 +78,8 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -164,6 +170,11 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom }); } + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + updateLatestDataKeys([widgetConfig.config.settings.yAxis], this.datasource, this.callbacks); + super.onConfigChanged(widgetConfig); + } + protected prepareOutputConfig(config: any): WidgetConfigComponentData { setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.datasources = config.datasources; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html index 4515802dfa..7bac268c2b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html @@ -22,16 +22,19 @@
datakey.label
datakey.color
-
-
- - + @if (keysFormArray().controls.length) { +
+ @for (keyControl of keysFormArray().controls; track trackByKey($index, keyControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.chart.no-series' | translate }} + }
- - {{ 'widgets.chart.no-series' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts index af411c4bb7..ebd5fb8d69 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { @@ -42,6 +42,8 @@ export class DoughnutBasicConfigComponent extends LatestChartBasicConfigComponen @ViewChild('doughnutChart') doughnutChartConfigTemplate: TemplateRef; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html index ca383c0d88..029a1a8cb3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html @@ -44,9 +44,16 @@ {{ 'widget-config.card-title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html index cded03d843..f03c43286b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html @@ -16,32 +16,34 @@ --> - - + @if (displayTimewindowConfig) { + + + } + [configMode]="basicMode" + hideDatasourceLabel + hideDataKeys + forceSingleDatasource + formControlName="datasources"> + panelTitle="{{ 'widgets.chart.series' | translate }}" + addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" + keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" + removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" + noKeysText="{{ 'widgets.chart.no-series' | translate }}" + requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" + hideUnits + hideDecimals + hideDataKeyUnits + hideDataKeyDecimals + [datasourceType]="datasource?.type" + [deviceId]="datasource?.deviceId" + [entityAliasId]="datasource?.entityAliasId" + formControlName="series">
widget-config.appearance
@@ -50,9 +52,16 @@ {{ 'widget-config.card-title' | translate }}
- - - + {{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -167,9 +178,11 @@
- - {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} - + @for (type of latestChartTooltipValueTypes; track type) { + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} + + } @@ -203,7 +216,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -232,7 +245,7 @@
+ formControlName="actions"> @@ -247,9 +260,11 @@
- - {{ pieChartLabelPositionTranslationMap.get(position) | translate }} - + @for (position of pieChartLabelPositions; track position) { + + {{ pieChartLabelPositionTranslationMap.get(position) | translate }} + + } - - {{ doughnutLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.doughnut.layout' | translate }}" formControlName="layout"> + @for (layout of doughnutLayouts; track layout) { + + {{ doughnutLayoutTranslationMap.get(layout) | translate }} + + }
@@ -339,8 +356,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -378,9 +395,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -427,9 +444,11 @@
{{ 'widgets.radar-chart.shape' | translate }}
- - {{ radarChartShapeTranslations.get(shape) | translate }} - + @for (shape of radarChartShapes; track shape) { + + {{ radarChartShapeTranslations.get(shape) | translate }} + + }
@@ -447,9 +466,11 @@
- - {{ chartLineTypeTranslations.get(type) | translate }} - + @for (type of chartLineTypes; track type) { + + {{ chartLineTypeTranslations.get(type) | translate }} + + } @@ -465,9 +486,11 @@
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + } @@ -483,9 +506,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + }
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts index 6dea17141b..741a25638d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { @@ -41,6 +41,8 @@ export class PieChartBasicConfigComponent extends LatestChartBasicConfigComponen @ViewChild('pieChart') pieChartConfigTemplate: TemplateRef; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts index 578a4957e2..59864ce5e3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { @@ -41,6 +41,8 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo @ViewChild('polarAreaChart') polarAreaChartConfigTemplate: TemplateRef; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts index 52f3094da6..22656ca16a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { @@ -41,6 +41,8 @@ export class RadarChartBasicConfigComponent extends LatestChartBasicConfigCompon @ViewChild('radarChart') radarChartConfigTemplate: TemplateRef; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index c761ccdcc6..9f597fb501 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -33,9 +33,16 @@ {{ 'widget-config.title' | translate }}
- - - +
+ formControlName="grid">
@@ -111,11 +118,11 @@ {{ 'widgets.range-chart.show-range-thresholds' | translate }} + boxButton + hideYAxis + [widgetConfig]="widgetConfig?.config" + title="widgets.range-chart.range-thresholds-settings" + formControlName="rangeThreshold">
@@ -143,9 +150,11 @@ - - {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} - + @for (stepType of lineSeriesStepTypes; track stepType) { + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + }
@@ -158,9 +167,11 @@
widgets.time-series-chart.line-type
- - {{ chartLineTypeTranslations.get(lineType) | translate }} - + @for (lineType of chartLineTypes; track lineType) { + + {{ chartLineTypeTranslations.get(lineType) | translate }} + + }
@@ -187,9 +198,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } widgets.time-series-chart.series.point.point-shape
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
@@ -234,26 +249,29 @@
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis">
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -269,9 +287,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -326,76 +346,76 @@
- - {{ 'tooltip.date' | translate }} - -
- - - - - -
-
-
- -
- {{ 'tooltip.show-date-time-interval' | translate }} -
-
-
-
-
{{ 'tooltip.background-color' | translate }}
+ + {{ 'tooltip.date' | translate }} + +
+ + + + formControlName="tooltipDateColor">
-
-
{{ 'tooltip.background-blur' | translate }}
- - -
px
-
-
- - +
+
+ +
+ {{ 'tooltip.show-date-time-interval' | translate }} +
+
+
+
+
{{ 'tooltip.background-color' | translate }}
+ + +
+
+
{{ 'tooltip.background-blur' | translate }}
+ + +
px
+
+
+ + +
+ + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ +
- - -
-
widget-config.card-appearance
-
-
{{ 'widgets.background.background' | translate }}
- - -
-
-
widget-config.show-card-buttons
- - {{ 'fullscreen.fullscreen' | translate }} - -
-
-
{{ 'widget-config.card-border-radius' | translate }}
- - - -
-
-
{{ 'widget-config.card-padding' | translate }}
- - - -
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + +
- - +
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts index 3b24d1b98e..95dbabfbfc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -26,6 +26,7 @@ import { legendPositions, legendPositionTranslationMap, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -46,7 +47,7 @@ import { } from '@home/components/widget/lib/chart/range-chart-widget.models'; import { lineSeriesStepTypes, - lineSeriesStepTypeTranslations + lineSeriesStepTypeTranslations, updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { chartLabelPositions, @@ -103,6 +104,8 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -286,6 +289,11 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { return this.widgetConfig; } + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + updateLatestDataKeys([widgetConfig.config.settings.yAxis], this.datasource, this.callbacks); + super.onConfigChanged(widgetConfig); + } + protected validatorTriggers(): string[] { return ['showTitle', 'showIcon', 'showRangeThresholds', 'fillArea', 'showLine', 'step', 'showPointLabel', 'enablePointLabelBackground', 'showLegend', 'showTooltip', 'tooltipShowDate']; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 52992518ee..0602e55fc9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -19,11 +19,11 @@ + [configMode]="basicMode" + hideDatasourceLabel + hideDataKeys + forceSingleDatasource + formControlName="datasources">
@@ -36,8 +36,8 @@ {{ 'widgets.time-series-chart.comparison.comparison' | translate }}
- - -
- - {{ 'widgets.time-series-chart.comparison.comparison' | translate }} - -
- - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - - - - - + } + @if (seriesMode === 'comparison') { +
+ + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + + + - + +
-
- - + + }
- - + + } @@ -123,9 +131,16 @@ {{ 'widget-config.title' | translate }}
- - - +
+ formControlName="grid">
@@ -183,10 +198,11 @@ axisType="xAxis">
- - + @if (chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar) { + + + }
@@ -258,9 +274,9 @@
{{ 'tooltip.trigger' | translate }}
{{ 'tooltip.trigger-point' | translate }} + [value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }} {{ 'tooltip.trigger-axis' | translate }} + [value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}
@@ -295,7 +311,7 @@
+ formControlName="tooltipDateFormat"> @@ -315,7 +331,7 @@
- {{ 'tooltip.hide-zero-tooltip-values' | translate }} + {{ 'tooltip.hide-zero-tooltip-values' | translate }}
@@ -370,6 +386,6 @@
+ formControlName="actions"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 008e1fda57..5b1410e5d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -27,6 +27,7 @@ import { legendPositions, legendPositionTranslationMap, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -94,6 +95,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon seriesMode = 'series'; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index d23896289e..b8ea5924a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -16,12 +16,14 @@ -->
- - - {{ 'datakey.timeseries' | translate }} - {{ 'datakey.latest' | translate }} - - + @if (hasAdditionalLatestDataKeys) { + + + {{ 'datakey.timeseries' | translate }} + {{ 'datakey.latest' | translate }} + + + } - - - -
- - - - - {{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }} - - - - {{ timeSeriesChartSeriesTypeIcons.get(type) }} - {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} - - - -
-
- - - - {{ yAxis }} - - - -
-
- - -
-
- - -
-
- - -
widget-config.decimals-suffix
+ @if (!hideDataKeyLabel) { + + -
-
-
-
+ } + @if (timeSeriesChart) { +
+ @if (displayUnitsOrDigits) { + + + @for (yAxis of yAxisIds; track yAxis) { + + {{ yAxis }} + + } + + + } +
+ } + @if (!hideDataKeyColor) { +
+ + +
+ } + @if (!hideUnits && !hideDataKeyUnits && (!singleRow || displayUnitsOrDigits)) { +
+ @if (displayUnitsOrDigits) { + + + } +
+ } + @if (!hideDecimals && !hideDataKeyDecimals && (!singleRow || displayUnitsOrDigits)) { +
+ @if (displayUnitsOrDigits) { + + + @if (singleRow) { +
widget-config.decimals-suffix
+ } +
+ } +
+ } + @if (!singleRow) { +
+
+ @if (modelValue) { + + } +
+
- -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html index d2d8df7139..393ab69ad8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -18,61 +18,83 @@
-
{{ panelTitle }}
+ @if (!hidePanel) { +
{{ panelTitle }}
+ }
-
datakey.source
+ @if (hasAdditionalLatestDataKeys) { +
datakey.source
+ }
datakey.key
datakey.label
-
widgets.time-series-chart.series.type
-
widgets.time-series-chart.axis.y-axis
-
datakey.color
-
widget-config.units-short
-
widget-config.decimals-short
+ @if (timeSeriesChart && showTimeSeriesType) { +
widgets.time-series-chart.series.type
+ } + @if (timeSeriesChart) { +
widgets.time-series-chart.axis.y-axis
+ } + @if (!hideDataKeyColor) { +
datakey.color
+ } + @if (!hideUnits) { +
widget-config.units-short
+ } + @if (!hideDecimals) { +
widget-config.decimals-short
+ }
-
-
- - -
- -
+ @if (keysFormArray().controls.length) { +
+ @for (keyControl of keysFormArray().controls; track trackByKey($index, keyControl)) { +
+ + +
+ +
+
+ }
-
- + } @else { + {{ noKeysText }} + } + @if (errorText) { + + }
- - {{ noKeysText }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html index 72f4b0f1d8..b493696d4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html @@ -19,18 +19,24 @@
widget-config.actions
- - - {{ widgetAction.icon }} - {{ widgetAction.name }} - - + @for (actionSourceId of widgetActionSourceIds; track actionSourceId) { + @for (widgetAction of widgetActionsByActionSourceId(actionSourceId); track widgetAction) { + + {{ widgetAction.icon }} + {{ widgetAction.name }} + + } + } - - + @if (!hasWidgetActions) { + + } + @if (hasWidgetActions) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html index 4bca260f37..8acae32fa7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + }
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.html index 65e4faf42e..fae864ba15 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.html @@ -26,7 +26,7 @@
widget-config.appearance
- +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts index 7928be4ba2..7f23351cf6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { DatasourceType, WidgetConfig, } from '@shared/models/widget.models'; +import { DatasourceType, WidgetConfig, widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { isUndefined } from '@core/utils'; @@ -37,6 +37,8 @@ export class EntityCountBasicConfigComponent extends BasicWidgetConfigComponent entityCountWidgetConfigForm: UntypedFormGroup; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private utils: UtilsService, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.html index be07426222..98d5bfd9ec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + }
-
-
widgets.gauge.progress-bar
-
-
widgets.gauge.progress-bar-color-start
- - -
-
-
widgets.gauge.progress-bar-color-end
- - -
-
-
widgets.gauge.bar-stroke-color
- - + @if (gaugeType === 'linear') { +
+
widgets.gauge.progress-bar
+
+
widgets.gauge.progress-bar-color-start
+ + +
+
+
widgets.gauge.progress-bar-color-end
+ + +
+
+
widgets.gauge.bar-stroke-color
+ + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html index a5dbfac219..c2470210fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - + @if (displayTimewindowConfig) { + + + } - - {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.gauge.simple-gauge-type' | translate }}" + formControlName="gaugeType"> + @for (layout of digitalGaugeLayouts; track layout) { + + {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} + + } -
-
{{ 'widgets.gauge.donut-start-angle' | translate }}
- - - -
+ @if (simpleGaugeWidgetConfigForm.get('gaugeType').value === digitalGaugeType.donut) { +
+
{{ 'widgets.gauge.donut-start-angle' | translate }}
+ + + +
+ }
@@ -76,14 +82,15 @@
widgets.gauge.max-value-short
- - warning - + @if (simpleGaugeWidgetConfigForm.get('maxValue').hasError('maxValue')) { + + warning + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html index 39c516d706..8c8655a0f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ batteryLevelLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout"> + @for (layout of batteryLevelLayouts; track layout) { + + {{ batteryLevelLayoutTranslationMap.get(layout) | translate }} + + } -
-
{{ 'widgets.battery-level.sections-count' | translate }}
- - - -
+ @if (sectionsCountEnabled) { +
+
{{ 'widgets.battery-level.sections-count' | translate }}
+ + + +
+ }
{{ 'widget-config.title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html index d60673f00c..7ecb3a0441 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html @@ -15,332 +15,364 @@ limitations under the License. --> - - - - - -
-
widget-config.appearance
-
- - {{ 'widgets.liquid-level-card.title' | translate }} - -
- - - - - - - -
-
-
- - {{ 'widgets.liquid-level-card.icon' | translate }} - -
- - - - - - - - +@if (levelCardWidgetConfigForm) { + + @if (displayTimewindowConfig) { + + + } + + +
+
widget-config.appearance
+
+ + {{ 'widgets.liquid-level-card.title' | translate }} + +
+ + + + + +
-
-
-
-
widgets.liquid-level-card.shape
- - - {{ DataSourceTypeTranslations.get(type) | translate }} - - +
+ + {{ 'widgets.liquid-level-card.icon' | translate }} + +
+ + + + + + + + +
- +
+
widgets.liquid-level-card.shape
+ + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + +
+ - - {{ ShapesTranslationMap.get(shape) | translate }} - - - -
-
widgets.liquid-level-card.shape-attribute-name
- - -
-
-
widgets.liquid-level-card.shape-by-attribute
-
+ style="width: 100%;" + label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape"> + @for (shape of shapes; track shape) { + + {{ ShapesTranslationMap.get(shape) | translate }} + + } + + @if (levelCardWidgetConfigForm.get('tankSelectionType').value === DataSourceType.attribute) { +
+
widgets.liquid-level-card.shape-attribute-name
+ +
-
- -
- - - {{ LevelCardLayoutTranslationMap.get(layout) | translate }} - - -
-
-
widgets.liquid-level-card.units
-
-
widgets.liquid-level-card.datasource-units
-
- - +
+
widgets.liquid-level-card.shape-by-attribute
+
+
+
+ }
+ + @for (layout of LevelCardLayouts; track layout) { + + {{ LevelCardLayoutTranslationMap.get(layout) | translate }} + + } +
-
-
widgets.liquid-level-card.widget-units
-
- - - - {{ DataSourceTypeTranslations.get(type) | translate }} - - - - - - - - - +
+
widgets.liquid-level-card.units
+
+
widgets.liquid-level-card.datasource-units
+
+ + +
-
-
-
widgets.liquid-level-card.total-volume
-
- - - - {{ DataSourceTypeTranslations.get(type) | translate }} - - - - - - - warning - - - - - - + @if (levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute) { +
+
widgets.liquid-level-card.widget-units
+
+ + + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + + + @if (levelCardWidgetConfigForm.get('widgetUnitsSource').value === DataSourceType.static) { + + + } @else { + + + } +
+
+ } +
+
widgets.liquid-level-card.total-volume
+
+ + + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + + + @if (levelCardWidgetConfigForm.get('volumeSource').value === DataSourceType.static) { + + + @if (levelCardWidgetConfigForm.get('volumeConstant').hasError('required')) { + + warning + + } + + } @else { + + + } +
-
-
-
widgets.liquid-level-card.total-volume-units
-
- - - - {{ DataSourceTypeTranslations.get(type) | translate }} - - - - - - - - - +
+
widgets.liquid-level-card.total-volume-units
+
+ + + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + + + @if (levelCardWidgetConfigForm.get('volumeUnitsSource').value === DataSourceType.static) { + + + } @else { + + + } +
-
-
-
widgets.liquid-level-card.color-and-font
-
-
widgets.liquid-level-card.tank-color
- - -
-
-
widgets.liquid-level-card.liquid-color
- - -
-
-
widgets.liquid-level-card.value
-
- - -
widget-config.decimals-suffix
-
- - - +
+
widgets.liquid-level-card.color-and-font
+
+
widgets.liquid-level-card.tank-color
+
-
-
-
widgets.liquid-level-card.total-volume
-
- - - - +
+
widgets.liquid-level-card.liquid-color
+ +
-
-
-
- - - - - {{ 'widget-config.tooltip' | translate }} - - - - -
- - {{ 'widgets.liquid-level-card.level' | translate }} - + @if (levelCardWidgetConfigForm.get('layout').value !== LevelCardLayout.simple) { +
+
widgets.liquid-level-card.value
- - - - + +
widget-config.decimals-suffix
- + - +
-
- - {{ 'widgets.value-card.date' | translate }} - -
- - + } + @if (levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute) { +
+
widgets.liquid-level-card.total-volume
+
+ - +
-
-
widgets.liquid-level-card.tooltip-background
- - -
-
-
widgets.liquid-level-card.background-blur
- - -
px
-
-
- - -
-
-
widget-config.card-appearance
-
-
{{ 'widgets.background.background' | translate }}
- - + }
-
-
widget-config.show-card-buttons
- - {{ 'fullscreen.fullscreen' | translate }} - -
-
-
{{ 'widget-config.card-border-radius' | translate }}
- - - +
+ + + + + {{ 'widget-config.tooltip' | translate }} + + + + +
+ + {{ 'widgets.liquid-level-card.level' | translate }} + +
+ + + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+
widgets.liquid-level-card.tooltip-background
+ + +
+
+
widgets.liquid-level-card.background-blur
+ + +
px
+
+
+
+
-
-
{{ 'widget-config.card-padding' | translate }}
- - - +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
-
- - - + + + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts index 72820c9564..fb63143c98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts @@ -27,6 +27,7 @@ import { datasourcesHasOnlyComparisonAggregation, DatasourceType, WidgetConfig, + widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -127,6 +128,8 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon datePreviewFn = this._datePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + private keySearchText: string; private latestKeySearchTextResult: Array; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html index 6298e5902e..c34a73d6ac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ signalStrengthLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.signal-strength.layout' | translate }}" formControlName="layout"> + @for (layout of signalStrengthLayouts; track layout) { + + {{ signalStrengthLayoutTranslationMap.get(layout) | translate }} + + }
{{ 'widget-config.title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html index 2d596fd468..c64a4e23d9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.status-widget.behavior
@@ -30,7 +30,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.rpc-state.disabled-state
@@ -41,7 +41,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -52,12 +52,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout"> - - {{ statusWidgetLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout"> + @for (layout of statusWidgetLayouts; track layout) { + + {{ statusWidgetLayoutTranslationMap.get(layout) | translate }} + + }
@@ -69,16 +71,18 @@ {{ 'widgets.status-widget.off' | translate }}
- - - + } + @if (cardStyleMode === 'off') { + - + + }
widget-config.card-appearance
@@ -102,6 +106,6 @@
+ formControlName="actions">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html index fa4401c8af..bfbdc25d9e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html @@ -16,8 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + }
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -86,6 +89,8 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { this.mapWidgetConfigForm = this.fb.group({ mapSettings: [settings, []], + timewindowConfig: [getTimewindowConfig(configData.config), []], + showTitle: [configData.config.showTitle, []], title: [configData.config.title, []], titleFont: [configData.config.titleFont, []], @@ -105,15 +110,10 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { actions: [configData.config.actions || {}, []] }); - if (this.trip) { - this.mapWidgetConfigForm.addControl('timewindowConfig', this.fb.control(getTimewindowConfig(configData.config))) - } } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - if (this.trip) { - setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); - } + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.settings = config.mapSettings || {}; this.widgetConfig.config.showTitle = config.showTitle; @@ -185,4 +185,20 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { config.enableFullscreen = buttons.includes('fullscreen'); } + public get displayTimewindowConfig(): boolean { + if (this.trip) { + return true; + } else { + return this.widget ? MapModelDefinition.hasTimewindow(this.widget) : false; + } + } + + public get onlyHistoryTimewindow(): boolean { + if (this.trip) { + return false; + } else { + return this.widget ? MapModelDefinition.datasourcesHasOnlyComparisonAggregation(this.widget) : false; + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html index 606f092199..1ed4ec86a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.single-switch.behavior
@@ -30,7 +30,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.rpc-state.turn-on
@@ -39,7 +39,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.rpc-state.turn-off
@@ -48,7 +48,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -59,7 +59,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -70,12 +70,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> - - {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> + @for (layout of singleSwitchLayouts; track layout) { + + {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts index cdf8a72fe7..c45fd8bb05 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { TargetDevice, WidgetConfig } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { isUndefined } from '@core/utils'; import { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html index 2eaaacc124..ff5397ea16 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.slider.behavior
@@ -27,7 +27,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.slider.on-value-change
@@ -37,7 +37,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="valueChange"> + formControlName="valueChange">
widgets.rpc-state.disabled-state
@@ -48,7 +48,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -59,12 +59,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> - - {{ sliderLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> + @for (layout of sliderLayouts; track layout) { + + {{ sliderLayoutTranslationMap.get(layout) | translate }} + + }
@@ -76,9 +78,16 @@ {{ 'widget-config.title' | translate }}
- - - +
-
- - {{ 'widgets.slider.value' | translate }} - -
- - - -
widget-config.decimals-suffix
-
- - - - + @if (sliderWidgetConfigForm.get('layout').value !== sliderLayout.simplified) { +
+ + {{ 'widgets.slider.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
-
+ }
{{ 'widgets.slider.range' | translate }}
@@ -206,44 +217,48 @@
-
-
- {{ 'widgets.slider.left-icon' | translate }} -
-
- - - - - - - - -
-
-
-
- {{ 'widgets.slider.right-icon' | translate }} + @if (sliderWidgetConfigForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.left-icon' | translate }} +
+
+ + + + + + + + +
-
- - - - - - - - + } + @if (sliderWidgetConfigForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.right-icon' | translate }} +
+
+ + + + + + + + +
-
+ }
widget-config.card-appearance
@@ -272,6 +287,6 @@
+ formControlName="actions"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts index 1946db01f8..481936aee3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { TargetDevice, WidgetConfig, widgetTitleAutocompleteValues, } from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { formatValue, isUndefined } from '@core/utils'; import { ValueType } from '@shared/models/constants'; @@ -60,6 +60,8 @@ export class SliderBasicConfigComponent extends BasicWidgetConfigComponent { valuePreviewFn = this._valuePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html index d9c654f17b..986af5d80e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.value-stepper.behavior
@@ -27,7 +27,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.value-stepper.left-button-click
@@ -37,7 +37,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="leftButtonClick"> + formControlName="leftButtonClick">
widgets.value-stepper.right-button-click
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="rightButtonClick"> + formControlName="rightButtonClick">
widgets.button-state.disabled-state
@@ -58,7 +58,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -69,12 +69,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> - - {{ valueStepperTypeTranslationMap.get(type) | translate }} - + label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> + @for (type of valueStepperTypes; track type) { + + {{ valueStepperTypeTranslationMap.get(type) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html index 17c33b9f45..fa9780cd06 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
scada.symbol.symbol
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html index 0ac16e2e64..af1cb2d514 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - - - + @if (!windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + } + @if (!windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + }
@@ -64,15 +70,19 @@ hideDataKeyLabel hideDataKeyColor> - - - - + @if (windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + } + @if (windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + }
@@ -83,21 +93,30 @@ breakpoints: { 'lt-md': 2 }}" - label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> - - {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> + @for (layout of windSpeedDirectionLayouts; track layout) { + + {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} + + }
{{ 'widget-config.title' | translate }}
- - - + , protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index a5ecd10817..0a276dcbfd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -16,107 +16,126 @@ -->
- - widget-config.datasource-type - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - datasource.label - - - - - - - - - - - - - + @if (!basicMode) { + + widget-config.datasource-type + + @for (datasourceType of datasourceTypes; track datasourceType) { + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + } + + + } +
+ @switch (datasourceFormGroup.get('type').value) { + @case (datasourceType.function) { + @if (!hideDatasourceLabel) { + + datasource.label + + + } + } + @case (datasourceFormGroup.get('type').value === datasourceType.device || + datasourceFormGroup.get('type').value === datasourceType.entity || + datasourceFormGroup.get('type').value === datasourceType.entityCount || + datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : '') { + @if (datasourceFormGroup.get('type').value === datasourceType.alarmCount && !hideAlarmFilter) { + + } + @if (datasourceFormGroup.get('type').value === datasourceType.device) { + + + } + @if (datasourceFormGroup.get('type').value !== datasourceType.device) { + + + } + @if (!hideDatasourceLabel && [datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)) { + + + + } + } + }
-
- - - - -
- - + @if (!hideDataKeys) { +
+ + + @if (hasAdditionalLatestDataKeys) { + + + } +
+ } + @if ((!basicMode || displayDatasourceFilterForBasicMode) && ![datasourceType.function].includes(datasourceFormGroup.get('type').value)) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html index f1f80b9248..f77c7c3ab4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html @@ -19,23 +19,30 @@
{{ (singleDatasource ? (isAlarmSource ? 'widget-config.alarm-source' : 'widget-config.datasource') : 'widget-config.datasources') | translate }}
-
- {{ 'widget-config.timeseries-key-error' | translate }} -
- - {{ 'device.device' | translate }} - {{ 'entity.entity-alias' | translate }} - + @if (timeseriesKeyError) { +
+ {{ 'widget-config.timeseries-key-error' | translate }} +
+ } + @if (basicMode && !hideDatasourcesMode) { + + {{ 'device.device' | translate }} + {{ 'entity.entity-alias' | translate }} + + }
-
{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}
+ @if (maxDatasources > 1) { +
{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}
+ }
-
- datasource.add-datasource-prompt -
- + @if (datasourcesFormArray.length === 0) { +
+ datasource.add-datasource-prompt +
+ } @else {
- -
-
-
{{$index + 1}}
-
-
-
- - -
- - + @if (!dragDisabled) { + + drag_indicator + + } +
+ }
+
-
-
- + + }
- -
- -
+ } + @if (showAddDatasource) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html index d44c668be8..1d47c35261 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html @@ -23,16 +23,21 @@ {{ 'entity.entity-alias' | translate }}
- - - - + @if (targetDeviceFormGroup.get('type').value === targetDeviceType.device) { + + + } + @if (targetDeviceFormGroup.get('type').value === targetDeviceType.entity) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts index bcabcf9099..8336cf338a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts @@ -33,6 +33,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import { IAliasController } from '@core/api/widget-api.models'; import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-target-device', @@ -73,6 +74,9 @@ export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Vali @Input() disabled: boolean; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + widgetEditMode = this.utils.widgetEditMode; targetDeviceFormGroup: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts index 124df36eb1..29bdc08692 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -23,11 +23,19 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, UntypedFormGroup } from '@angular/forms'; -import { DataKey, DatasourceType, Widget, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; +import { + DataKey, + DatasourceType, + Widget, + widgetTypeCanHaveTimewindow, + WidgetConfigMode, + widgetType +} from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; @@ -107,6 +115,11 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement if (this.isAdd) { this.setupDefaults(widgetConfig); } + if (widgetTypeCanHaveTimewindow(widgetConfig.widgetType) && isUndefinedOrNull(widgetConfig.config.timewindow)) { + widgetConfig.config.timewindow = initModelFromDefaultTimewindow(null, + widgetConfig.widgetType === widgetType.latest, false, this.widgetConfigComponent.timeService, + widgetConfig.widgetType === widgetType.timeseries); + } this.onConfigSet(widgetConfig); this.updateValidators(false); for (const trigger of this.validatorTriggers()) { diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html index 75179512c9..a31ad603f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html @@ -23,12 +23,15 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
+ [currentState]="state" [dashboard]="dashboard" [parentDashboard]="parentDashboard" style="width: 100%; height: 100%;">
- + @if (allowAcknowledgment) { + + } + @if (ctx.settings.allowClear) { + + }
+ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> - - - {{ column.title }} - - - - - - + + {{ column.title }} + + + + @if (column.entityKey.key === 'assignee') { + + @if (alarm.assigneeId && checkAssigneeHasName(alarm.assignee)) { + - - {{ getUserInitials(alarm.assignee) }} - - - {{ getUserDisplayName(alarm.assignee) }} - - - - - {{ alarm.assigneeId ? 'no_accounts' : 'account_circle' }} - - {{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }} + + {{ getUserInitials(alarm.assignee) }} + + + {{ getUserDisplayName(alarm.assignee) }} + - - - - - - - + } @else { + + {{ alarm.assigneeId ? 'no_accounts' : 'account_circle' }} + + {{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }} + + + } + @if (allowAssign) { + + } + + } + + + } - + @if (alarmsDatasource.countCellButtonAction) {
-
+ }
- + @if (alarmsDatasource.countCellButtonAction) {
- - - - -
-
- - - - - - + } + }
-
+ @if (alarm.hasActions) { +
+ + + @for (actionDescriptor of alarm.actionCellButtons; track trackByActionCellDescriptionId($index, actionDescriptor)) { + @if (actionDescriptor.icon) { + + } + } + +
+ } + }
@@ -172,20 +188,24 @@ [class.tb-pointer]="hasRowAction" *matRowDef="let alarm; columns: displayedColumns; let row = index" [style]="rowStyle(alarm, row) | async" - (click)="onRowClick($event, alarm)"> + (click)="onRowClick($event, alarm)">
{{ noDataDisplayMessageText }} + class="no-data-found flex items-center justify-center">{{ noDataDisplayMessageText }} {{ 'common.loading' | translate }} + class="no-data-found flex items-center justify-center" >{{ 'common.loading' | translate }}
- - + @if (displayPagination) { + + } + @if (displayPagination) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 31927e0cb3..06e0cb73ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -37,15 +37,7 @@ import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/wi import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { - deepClone, - hashCode, - isDefined, - isDefinedAndNotNull, - isNotEmptyStr, - isObject, - isUndefined -} from '@core/utils'; +import { deepClone, hashCode, isDefined, isDefinedAndNotNull, isNotEmptyStr, isObject, isUndefined } 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'; @@ -451,7 +443,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (this.subscription.alarmSource) { this.subscription.alarmSource.dataKeys.forEach((alarmDataKey) => { const dataKey: EntityColumn = deepClone(alarmDataKey) as EntityColumn; - const keySettings: TableWidgetDataKeySettings = dataKey.settings; + const keySettings: TableWidgetDataKeySettings = dataKey.settings ?? {}; dataKey.entityKey = dataKeyToEntityKey(alarmDataKey); dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); dataKey.title = getHeaderTitle(dataKey, keySettings, this.utils); @@ -861,6 +853,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, content = this.defaultContent(key, contentInfo, value); } if (isDefined(content)) { + if (typeof content === 'object') { + content = JSON.stringify(content); + } content = this.utils.customTranslation(content, content); switch (typeof content) { case 'string': @@ -974,7 +969,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, (AlarmDetailsDialogComponent, { disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', this.ctx.stateController.dashboardCtrl.dashboardCtx.dashboardCssClass, this.ctx.widgetCssClass], data: { alarmId: alarm.id.id, allowAcknowledgment: this.allowAcknowledgment, 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 index a4e220888b..f77a854a77 100644 --- 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 @@ -26,6 +26,7 @@ import { TbUnit } from '@shared/models/unit.models'; import { ValueFormatProcessor } from '@shared/models/widget-settings.models'; import { UnitService } from '@core/services/unit.service'; import { DataKey } from '@shared/models/widget.models'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; export type AnimationRule = 'linear' | 'quad' | 'quint' | 'cycle' | 'bounce' | 'elastic' | 'dequad' | 'dequint' @@ -201,7 +202,7 @@ export abstract class TbAnalogueGauge 0 ? + this.ctx.$injector.get(CustomTranslatePipe).transform(settings.unitTitle && settings.unitTitle.length > 0 ? settings.unitTitle : dataKey.label) : ''), borders: settings.showBorder !== false, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html index 23e6d9d7dd..067b2b182f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html @@ -26,5 +26,7 @@ [ctx]="ctx" (clicked)="onClick($event)"> - + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html index 37a9bc5ed3..92a0d87f45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html @@ -23,12 +23,14 @@ [class.horizontal-fill]="horizontalFill" [class.vertical-fill]="verticalFill"> + [appearance]="appearance" + [autoScale]="autoScale" + [disabled]="disabled || (loading$ | async)" + [ctx]="ctx" + (clicked)="onClick($event)">
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html index f1aee8b5cc..e95506704d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html @@ -29,6 +29,8 @@ (clicked)="onClick($event)">
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html index cca727e061..1490b9074b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html @@ -21,69 +21,83 @@
- + @if (showValues) { - + }
-
{{ subtitle$ | async }}
+ @if (showSubtitle) { +
{{ subtitle$ | async }}
+ }
- + @if (values[aggregatedValueCardKeyPosition.leftTop]) { - - + } + @if (values[aggregatedValueCardKeyPosition.leftBottom]) { - + }
- + @if (values[aggregatedValueCardKeyPosition.center]) { - + }
- + @if (values[aggregatedValueCardKeyPosition.rightTop]) { - - + } + @if (values[aggregatedValueCardKeyPosition.rightBottom]) { - + }
-
-
-
{{tickMax$ | async}}
-
{{tickMin$ | async}}
-
-
-
-
-
-
-
+ @if (showChart) { +
+
+
{{tickMax$ | async}}
+
{{tickMin$ | async}}
+
+
+
+
+
+
+
+
-
+ } -
+ @if (showDate) { +
+ }
-
- - arrow_upward - arrow_downward -
+ @if (value.showArrow) { +
+ @if (!value.upArrow && !value.downArrow) { + + } + @if (value.upArrow) { + arrow_upward + } + @if (value.downArrow) { + arrow_downward + } +
+ }
{{ value.value }} {{ value.units }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.html new file mode 100644 index 0000000000..11c2f6a54f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.html @@ -0,0 +1,54 @@ + +
+
+ +
+
+ @for (api of apiUsages; track api){ +
+
+
{{ api.label }}
+
+
{{ api.current.value }} / {{ api.maxLimit.value }}
+
+ +
+
+
{{ ('api-usage.status.' + api.status.value) | translate }}
+
+ @if (api.status.value === 'enabled') { + check_circle + } @else { + warning + } +
+
+
+
+ +
+ } +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss new file mode 100644 index 0000000000..fbd2ba3c3f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"; + +$enabled-color: #198038; +$disabled-color: #D12730; +$warning-color: #FAA405; + +.tb-no-notification-svg-color { + color: $tb-primary-color; +} + +.tb-api-usage-panel { + > div:not(.tb-api-usage-overlay) { + z-index: 1; + } + .tb-api-usage-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + .tb-api-usage-content { + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + align-items: center; + .api-items-list { + display: flex; + flex-direction: column; + width: 100%; + .api-item { + &.enabled { + .api-item-statistic-status { + color: $enabled-color; + } + .mat-mdc-progress-bar { + --mat-progress-bar-track-color: #{rgba($enabled-color, 0.06)}; + --mat-progress-bar-active-indicator-color: #{$enabled-color}; + } + } + &.disabled { + .api-item-statistic-status { + color: $disabled-color; + } + .mat-mdc-progress-bar { + --mat-progress-bar-track-color: #{rgba($disabled-color, 0.06)}; + --mat-progress-bar-active-indicator-color: #{$disabled-color}; + } + } + &.warning { + .api-item-statistic-status { + color: $warning-color; + } + .mat-mdc-progress-bar { + --mat-progress-bar-track-color: #{rgba($warning-color, 0.06)}; + --mat-progress-bar-active-indicator-color: #{$warning-color}; + } + } + + &:hover { + background-color: rgba(0, 0, 0, 0.06); + } + &.active { + background-color: rgba($tb-primary-color, 0.06); + .mat-divider { + --mat-divider-color: #{$tb-primary-color}; + } + .api-item-content { + .api-item-title { + font-weight: 500; + color: $tb-primary-color; + } + } + } + + .api-item-content { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 5px 16px; + .api-item-title { + display: flex; + flex: 1; + font-size: 14px; + line-height: 20px; + font-weight: 400; + color: rgba(0, 0, 0, 0.54); + } + .api-item-statistic { + display: flex; + flex: 1; + flex-direction: column; + &-count { + font-size: 14px; + color: rgba(0, 0, 0, 0.54); + } + &-status { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + font-size: 14px; + } + &-progress { + --mat-progress-bar-track-height: 8px; + --mat-progress-bar-active-indicator-height: 8px; + padding: 4px 0; + .mat-mdc-progress-bar { + border-radius: 6px; + } + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts new file mode 100644 index 0000000000..97efac2cc8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -0,0 +1,172 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, Input, OnDestroy, OnInit, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { DatasourceType, widgetType } from '@shared/models/widget.models'; +import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { formattedDataFormDatasourceData } from '@core/utils'; + +import { UtilsService } from '@core/services/utils.service'; +import { + apiUsageDefaultSettings, + ApiUsageWidgetSettings, + getUniqueDataKeys +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; + +@Component({ + selector: 'tb-api-usage-widget', + templateUrl: './api-usage-widget.component.html', + styleUrls: ['api-usage-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ApiUsageWidgetComponent implements OnInit, OnDestroy { + + settings: ApiUsageWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + padding: string; + + apiUsages = []; + currentState = ''; + noDataDisplayMessageText: string; + + private contentResize$: ResizeObserver; + private powers: {key: string, value: number}[] = [ + { key: 'Q', value: 1e15 }, + { key: 'T', value: 1e12 }, + { key: 'B', value: 1e9 }, + { key: 'M', value: 1e6 }, + { key: 'K', value: 1e3 } + ]; + + constructor(private imagePipe: ImagePipe, + private utils: UtilsService, + private sanitizer: DomSanitizer, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.apiUsageWidget = this; + this.settings = {...apiUsageDefaultSettings, ...this.ctx.settings}; + + this.parseApiUsages(); + + const ds = { + type: DatasourceType.entity, + name: '', + entityAliasId: this.settings.dsEntityAliasId, + dataKeys: getUniqueDataKeys(this.settings.apiUsageDataKeys) + } + + const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = { + datasources: [ds], + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + const data = formattedDataFormDatasourceData(subscription.data); + this.apiUsages.forEach(key => { + const progress = (this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0) ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0; + key.progress = isFinite(progress) ? progress : 0; + key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled'; + key.maxLimit.value = this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; + key.current.value = this.isFiniteNumber(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0; + }); + this.cd.detectChanges(); + } + } + }; + this.ctx.subscriptionApi.createSubscription(apiUsageSubscriptionOptions, true).subscribe(); + + const simulated = this.ctx.utilsService.widgetEditMode || this.ctx.isPreview; + + if (!simulated) { + this.currentState = this.ctx.stateController.getStateId(); + this.ctx.stateController.stateId().subscribe((state) => { + this.ctx.updateParamsFromData(true); + this.currentState = state; + this.cd.markForCheck(); + }); + } + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; + } + + private isFiniteNumber(value: any): boolean { + return typeof value === 'number' && isFinite(value); + } + + updateState($event: MouseEvent, stateName: string) { + $event?.preventDefault(); + if (stateName?.length && this.ctx.stateController) { + this.ctx.stateController.updateState(stateName, this.ctx.stateController.getStateParams(), this.ctx.isMobile); + } + } + + parseApiUsages() { + this.settings.apiUsageDataKeys.forEach((key) => { + this.apiUsages.push({ + label: this.utils.customTranslation(key.label, key.label), + state: key.state, + progress: 0, + status: {key: key.status.name, value: 'enabled'}, + maxLimit: {key: key.maxLimit.name, value: '∞'}, + current: {key: key.current.name, value: 0}, + }); + }) + } + + ngOnDestroy() { + if (this.contentResize$) { + this.contentResize$.disconnect(); + } + } + + private toShortNumber(number: any, decimals = 1) { + if (!Number.isFinite(number) || number < 0) { + return '0'; + } + for (const power of this.powers) { + if (number >= power.value) { + const reduced = number / power.value; + const rounded = Number(reduced.toFixed(decimals)); + return `${rounded}${power.key}`; + } + } + return `${Number(number.toFixed(decimals))}`; + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html index cba9dbdd6f..fd56942e6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html @@ -23,8 +23,10 @@
- {{ icon }} -
+ @if (showIcon) { + {{ icon }} + } +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html index 289b54bb56..ea20b3521b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html @@ -23,11 +23,17 @@
-
- {{ icon }} -
-
+ @if (showIcon || showLabel) { +
+ @if (showIcon) { + {{ icon }} + } + @if (showLabel) { +
+ } +
+ }
{{ valueText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html index f486fd0594..e0a38d6eca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html @@ -21,13 +21,14 @@
widgets.notification.notification-types
- - {{ notificationTypesTranslateMap.get(type).name | translate }} - close - + @for (type of selectedNotificationTypes; track type) { + + {{ notificationTypesTranslateMap.get(type).name | translate }} + close + + } - - - + @for (type of filteredNotificationTypesList | async; track type) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html index 0b9acd06fd..9b99295ca8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html @@ -18,24 +18,30 @@
- + @if (layout === progressBarLayout.default) { - + }
-
- - - -
{{ valueText }}
-
+ @if (showTitleValueRow) { +
+ @if (layout === progressBarLayout.simplified) { + + } + @if (showValue) { +
{{ valueText }}
+ } +
+ }
-
-
{{ settings.tickMin }}
-
{{ settings.tickMax }}
-
+ @if (showTicks) { +
+
{{ settings.tickMin }}
+
{{ settings.tickMax }}
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html index 2cc7e023d2..2f9fdb4ca9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html @@ -18,38 +18,44 @@
-
- {{ count$ | async }} -
+ @if (showCounter) { +
+ {{ count$ | async }} +
+ }
- -
-
-
- - - -
-
-
-
- -
- - - -
- notification.no-notifications-yet -
- + @if (loadNotification) { + @if (notifications.length) { +
+
+ @for (notification of notifications; track notification.id.id; let last = $last) { +
+ + + @if (!last) { + + } +
+ } +
+
+ } @else { +
+ + + +
+ notification.no-notifications-yet + } + } @else {
notification.loading-notifications
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts index e68cddacf0..157dc0deec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts @@ -37,7 +37,7 @@ import { unreadNotificationDefaultSettings, UnreadNotificationWidgetSettings } from '@home/components/widget/lib/cards/unread-notification-widget.models'; -import { Notification, NotificationRequest, NotificationType } from '@shared/models/notification.models'; +import { Notification, NotificationType } from '@shared/models/notification.models'; import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models'; import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; import { distinctUntilChanged, map, share, skip, take, tap } from 'rxjs/operators'; @@ -230,10 +230,6 @@ export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy { } } - trackById(index: number, item: NotificationRequest): string { - return item.id.id; - } - private editNotificationTypeFilter($event: Event) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html index 226b5668bf..6000ef6d3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html @@ -21,35 +21,35 @@
- - + @switch (layout) { + @case (valueCardLayout.square) { - - + } + @case (valueCardLayout.vertical) { - - + } + @case (valueCardLayout.centered) {
-
- + } + @case (valueCardLayout.simplified) { - - + } + @case (layout === valueCardLayout.horizontal || + layout === valueCardLayout.horizontal_reversed ? layout : '') {
-
-
+ } + }
@@ -62,13 +62,19 @@
- {{ icon }} + @if (showIcon) { + {{ icon }} + } -
{{ label$ | async }}
+ @if (showLabel) { +
{{ label$ | async }}
+ }
-
+ @if (showDate) { +
+ }
{{ valueText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html index 8d276d1231..4950936073 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html @@ -19,9 +19,11 @@
-
-
{{ valueText }}
-
+ @if (showValue) { +
+
{{ valueText }}
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html index c8a7626cda..adbec193db 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html @@ -28,16 +28,20 @@
-
-
-
-
-
{{ legendKey.label }}
-
+ @if (showLegend) { +
+ @for (legendKey of legendKeys; track legendKey) { +
+
+
+
{{ legendKey.label }}
+
+
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 1f63cd8638..0287b775c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -134,6 +134,12 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft } } + public onLatestDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.latestUpdated(); + } + } + public onLegendKeyEnter(key: DataKey) { this.timeSeriesChart.keyEnter(key); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html index 8e98d5f150..c840dc799e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html @@ -28,17 +28,21 @@
-
-
-
-
-
{{ legendItem.label }}
-
-
{{ legendItem.value }}
+ @if (showLegend) { +
+ @for (legendItem of legendItems; track legendItem) { +
+
+
+
{{ legendItem.label }}
+
+
{{ legendItem.value }}
+
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss index b30534d167..de45bcc5c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss @@ -51,7 +51,7 @@ flex-direction: row-reverse; } .tb-latest-chart-shape { - flex: 1; + flex: 1 1 100%; min-width: 10px; min-height: 10px; display: flex; @@ -95,8 +95,14 @@ &.legend-right, &.legend-left { gap: 24px; .tb-latest-chart-legend { + flex: 0 1 auto; + overflow-y: auto; + max-height: 100%; + flex-wrap: nowrap; + min-width: fit-content; + max-width: 100%; flex-direction: column; - justify-content: center; + justify-content: stretch; align-items: stretch; .tb-latest-chart-legend-item { flex-direction: row; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts index 7dd9c060c7..b6576f8a3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts @@ -182,17 +182,13 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { private onResize() { if (this.legendHorizontal) { - this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', null); this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', null); - this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', null); } const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width; const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height; const size = Math.min(shapeWidth, shapeHeight); if (this.legendHorizontal) { - this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', `${size}px`); this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', `${size}px`); - this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', '1'); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html index 48fe64d4a2..e001d9c56d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -28,14 +28,18 @@
-
-
-
-
-
{{ rangeItem.label }}
-
+ @if (showLegend) { +
+ @for (rangeItem of visibleRangeItems; track rangeItem) { +
+
+
+
{{ rangeItem.label }}
+
+
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 2412d51a83..4ad45c85a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -162,6 +162,12 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn } } + public onLatestDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.latestUpdated(); + } + } + public toggleRangeItem(item: RangeItem) { item.enabled = !item.enabled; this.timeSeriesChart.toggleVisualMapRange(item.index); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index a7d1d6eb6e..7ae4145eb6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -28,83 +28,107 @@
-
- - - - - -
+ @if (showLegend) { +
+ @if (displayLegendValues) { + @if (horizontalLegendPosition) { + + } @else { + + + + + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + } + + + + @for (legendKey of legendKeys; track legendKey) { + + + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + } + + } + +
{{ 'legend.Min' | translate }}{{ 'legend.Max' | translate }}{{ 'legend.Avg' | translate }}{{ 'legend.Total' | translate }}{{ 'legend.Latest' | translate }}
+ + + + + + +
+ } + } @else { + @for (legendKey of legendKeys; track legendKey) { + + } + } +
+ } - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - - - - - - - - - - + + + @for (legendKey of legendKeys; track legendKey) { + + } + - - - - - - - - + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + }
{{ 'legend.Min' | translate }}{{ 'legend.Max' | translate }}{{ 'legend.Avg' | translate }}{{ 'legend.Total' | translate }}{{ 'legend.Latest' | translate }}
+ +
- - - - - - -
- - - - -
@@ -120,9 +144,11 @@ {{ label | translate }} - - + @for (legendKey of legendKeys; track legendKey) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 0574344628..e815eefe11 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -99,6 +99,8 @@ import { TimeSeriesChartTooltipWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; import { TbUnit, TbUnitConverter } from '@shared/models/unit.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; type TimeSeriesChartDataEntry = [number, any, number, number]; @@ -381,8 +383,8 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: number | string; - max?: number | string; + min?: number | string | ValueSourceConfig; + max?: number | string | ValueSourceConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } @@ -867,6 +869,9 @@ export interface TimeSeriesChartAxis { id: string; settings: TimeSeriesChartAxisSettings; option: CartesianAxisOption; + minLatestDataKey?: DataKey; + maxLatestDataKey?: DataKey; + unitConvertor?: (value: number) => number; } export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis { @@ -926,6 +931,29 @@ export const createTimeSeriesYAxis = (units: string, return ticks?.filter(tick => tick.value >= extent[0] && tick.value <= extent[1]); }; } + + let initialMin: number | string | undefined; + if (isDefinedAndNotNull(settings.min)) { + if (typeof settings.min === 'object' && 'type' in settings.min) { + initialMin = undefined; + } else if (typeof settings.min === 'number') { + initialMin = unitConvertor ? unitConvertor(settings.min) : settings.min; + } else if (typeof settings.min === 'string') { + initialMin = settings.min; + } + } + + let initialMax: number | string | undefined; + if (isDefinedAndNotNull(settings.max)) { + if (typeof settings.max === 'object' && 'type' in settings.max) { + initialMax = undefined; + } else if (typeof settings.max === 'number') { + initialMax = unitConvertor ? unitConvertor(settings.max) : settings.max; + } else if (typeof settings.max === 'string') { + initialMax = settings.max; + } + } + return { id: settings.id, decimals, @@ -939,8 +967,8 @@ export const createTimeSeriesYAxis = (units: string, offset: 0, alignTicks: true, scale: true, - min: isDefinedAndNotNull(settings.min) ? unitConvertor(Number(settings.min)) : settings.min, - max: isDefinedAndNotNull(settings.max) ? unitConvertor(Number(settings.max)) : settings.max, + min: initialMin, + max: initialMax, minInterval, splitNumber, interval, @@ -1411,6 +1439,13 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, } } seriesOption.data = item.data; + if (seriesOption.type === 'line') { + const settings: TimeSeriesChartKeySettings = item.dataKey.settings; + const lineSettings = settings.lineSettings; + if (!lineSettings.showPoints) { + seriesOption.showSymbol = item.data.length === 1; + } + } return seriesOption; }; @@ -1462,3 +1497,101 @@ const createSeriesLabelOption = (item: TimeSeriesChartDataItem, show: boolean, } return labelOption; }; + +export const checkLatestDataKeys = (yAxes: TimeSeriesChartYAxes, datasource: Datasource): TimeSeriesChartYAxes => { + const latestKeys = datasource?.latestDataKeys || []; + const result: TimeSeriesChartYAxes = {}; + + for (const [id, axis] of Object.entries(yAxes)) { + axis.min = normalizeAxisLimit(axis.min); + axis.max = normalizeAxisLimit(axis.max); + const minCfg = axis.min; + const maxCfg = axis.max; + + const minValid = !!minCfg && ( + minCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => isYAxisKey(k, minCfg)) + ); + + const maxValid = !!maxCfg && ( + maxCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => isYAxisKey(k, maxCfg)) + ); + + if (minValid && maxValid) { + result[id] = axis; + } + } + + return result; +} + +export const updateLatestDataKeys = (yAxes: TimeSeriesChartYAxisSettings[], datasource: Datasource, dataKeyCallbacks: DataKeysCallbacks)=> { + if (datasource) { + let latestKeys = datasource.latestDataKeys; + if (!latestKeys) { + latestKeys = []; + datasource.latestDataKeys = latestKeys; + } + const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); + const foundYAxisKeys: DataKey[] = []; + + for(const yAxis of yAxes) { + const min = yAxis.min as ValueSourceConfig; + const max = yAxis.max as ValueSourceConfig; + if (min && min.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => isYAxisKey(k, min)); + if (!found) { + const newKey = dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, + null, true, null); + newKey.settings.__yAxisMinKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + if (max && max.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => isYAxisKey(k, max)); + if (!found) { + const newKey = dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, + null, true, null); + newKey.settings.__yAxisMaxKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + } + const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); + for (const key of toRemove) { + const index = latestKeys.indexOf(key); + if (index > -1) { + latestKeys.splice(index, 1); + } + } + } +} + +export const isYAxisKey = (d: DataKey, limit: ValueSourceConfig): boolean => { + return (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType); +} + +export const normalizeAxisLimit = (limit: string | number | ValueSourceConfig): ValueSourceConfig => { + if (!limit) { + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } else if (typeof limit === 'number' || typeof limit === 'string') { + return { + type: ValueSourceType.constant, + value: Number(limit), + entityAlias: null + }; + } + return limit; +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index ca2456e109..d1928d75f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -23,7 +23,7 @@ import { createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, - LineSeriesStepType, + LineSeriesStepType, normalizeAxisLimit, parseThresholdData, TimeSeriesChartAxis, TimeSeriesChartDataItem, @@ -54,13 +54,15 @@ import { getFocusedSeriesIndex, measureAxisNameSize } from '@home/components/widget/lib/chart/echarts-widget.models'; -import { DateFormatProcessor, ValueSourceType } from '@shared/models/widget-settings.models'; +import { DateFormatProcessor, ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; import { formattedDataFormDatasourceData, formatValue, isDefined, isDefinedAndNotNull, isEqual, + isNumber, + isString, mergeDeep } from '@core/utils'; import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; @@ -286,9 +288,36 @@ export class TbTimeSeriesChart { } } } + + for (const yAxis of this.yAxisList) { + const minType = (yAxis.settings.min as ValueSourceConfig).type; + const maxType = (yAxis.settings.max as ValueSourceConfig).type; + if (minType === ValueSourceType.latestKey && yAxis.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } + } + } + + if (maxType === ValueSourceType.latestKey && yAxis.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } + } + } + } } if (this.timeSeriesChart && update) { this.updateSeriesData(); + this.updateAxisLimits(); } } @@ -550,7 +579,10 @@ export class TbTimeSeriesChart { private setupYAxes(): void { const yAxisSettingsList = Object.values(this.settings.yAxes); yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + const axisLimitDatasources: Datasource[] = []; for (const yAxisSettings of yAxisSettingsList) { + yAxisSettings.min = normalizeAxisLimit(yAxisSettings.min); + yAxisSettings.max = normalizeAxisLimit(yAxisSettings.max); const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxisSettings); const units = isNotEmptyTbUnits(axisSettings.units) ? axisSettings.units : this.ctx.units; @@ -563,8 +595,81 @@ export class TbTimeSeriesChart { axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); + if (isDefinedAndNotNull(axisSettings.min)) { + this.processYAxisLimit(axisSettings.min as ValueSourceConfig, 'min', yAxis, axisLimitDatasources, unitConvertor); + } + if (isDefinedAndNotNull(axisSettings.max)) { + this.processYAxisLimit(axisSettings.max as ValueSourceConfig, 'max', yAxis, axisLimitDatasources, unitConvertor); + } this.yAxisList.push(yAxis); } + this.subscribeForAxisLimits(axisLimitDatasources); + } + + private processYAxisLimit( + limit: ValueSourceConfig, + limitType: 'min' | 'max', + yAxis: TimeSeriesChartYAxis, + axisLimitDatasources: Datasource[], + unitConvertor?: (value: number) => number + ): void { + if (limit && typeof limit === 'object' && 'type' in limit) { + if (limit.type === ValueSourceType.latestKey) { + let latestDataKey: DataKey = null; + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + latestDataKey = datasource.latestDataKeys?.find(d => + (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType)); + if (latestDataKey) { + break; + } + } + } + if (latestDataKey) { + if (limitType === 'min') { + yAxis.minLatestDataKey = latestDataKey; + } else { + yAxis.maxLatestDataKey = latestDataKey; + } + } + } else if (limit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(limit.entityAlias); + if (entityAliasId) { + let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); + const entityDataKey: DataKey = { + type: limit.entityKeyType, + name: limit.entityKey, + label: limit.entityKey, + settings: { + yAxisId: yAxis.id, + axisLimit: limitType + } + }; + if (datasource) { + datasource.dataKeys.push(entityDataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: limit.entityAlias, + aliasName: limit.entityAlias, + entityAliasId, + dataKeys: [entityDataKey] + }; + axisLimitDatasources.push(datasource); + } + } + } else if (limit.type === ValueSourceType.constant) { + const value = unitConvertor && isDefinedAndNotNull(limit.value) ? unitConvertor(limit.value) : limit.value; + if (limitType === 'min') { + yAxis.option.min = value; + } else { + yAxis.option.max = value; + } + } + return; + } } private setupVisualMap(): void { @@ -622,6 +727,50 @@ export class TbTimeSeriesChart { } } + private subscribeForAxisLimits(datasources: Datasource[]) { + if (datasources.length) { + const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + let update = false; + if (subscription.data) { + for (const yAxis of this.yAxisList) { + for (const data of subscription.data) { + if (data.dataKey.settings?.yAxisId === yAxis.id) { + const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); + if (data.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (isDefinedAndNotNull(value)) { + if (limitType === 'min') { + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } + } else { + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } + } + } + } + } + } + } + } + if (this.timeSeriesChart && update) { + this.updateAxisLimits(); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); + } + } + private drawChart() { echartsModule.init(); this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal'); @@ -714,6 +863,24 @@ export class TbTimeSeriesChart { } } + private parseAxisLimitData = (data: any, unitConvertor?: (value: number) => number): number => { + let value: number; + if (isDefinedAndNotNull(data)) { + if (isNumber(data)) { + value = data; + } else if (isString(data)) { + value = Number(data); + } + } + if (isDefinedAndNotNull(value) && !isNaN(value)) { + if (unitConvertor) { + return unitConvertor(value); + } + return value; + } + return null; + }; + private updateSeries(): void { this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, this.stackMode, @@ -836,6 +1003,16 @@ export class TbTimeSeriesChart { return changed; } + private updateAxisLimits(): void { + if (this.timeSeriesChart && !this.timeSeriesChart.isDisposed()) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, { + replaceMerge: ['yAxis'] + }); + this.updateAxes(); + } + } + private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { if (!this.stateData) { const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html index 37eb1f807a..a607300147 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html @@ -20,20 +20,32 @@
-
-
-
+ @if (showIconBackground || showIcon) { +
+ @if (showIconBackground) { +
+
+
+ } + @if (showIcon) { + {{ icon }} + }
- {{ icon }} -
+ }
-
{{ label }}
-
+ @if (showLabel) { +
{{ label }}
+ } + @if (layout === countCardLayout.row) { +
+ }
{{ valueText }}
- chevron_right + @if (showChevron) { + chevron_right + }
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 index b3c95cc73b..ec0071243b 100644 --- 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 @@ -16,20 +16,24 @@ -->
- - - + @if (settings.showTemplate) { + + + + } -
- - -
+ @if (!settings.autoConfirm) { +
+ + +
+ }
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 index f5f43a5346..5cdc878b57 100644 --- 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 @@ -19,23 +19,31 @@ [class.short-mode]="ctx.width < 400" [class.labels-hidden]="settings.hideLabels" [class.long-mode]="ctx.width >= 400"> - - widgets.date-range-navigator.localizationMap.Date picker + + @if (!settings.hideLabels) { + widgets.date-range-navigator.localizationMap.Date picker + } {{advancedModel.chosenLabel}} - - widgets.date-range-navigator.localizationMap.Interval + + @if (!settings.hideLabels) { + widgets.date-range-navigator.localizationMap.Interval + } - - {{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}} - - - {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} - + @if (customInterval) { + + {{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}} + + } + @for (date of datesMap | keyvalue: originalOrder; track date) { + + {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} + + } @@ -43,12 +51,16 @@ - - widgets.date-range-navigator.localizationMap.Step size + + @if (!settings.hideLabels) { + widgets.date-range-navigator.localizationMap.Step size + } - - {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} - + @for (date of datesMap | keyvalue: originalOrder; track date) { + + {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} + + } - +@if (addMode || editMode) { +
+ + @if (editMode) { +
+ + +
+ }
-
- - -
-
- +} @else {
- +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html index 1f3756431c..fc2199b679 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html @@ -27,17 +27,19 @@
- - -
- - - - + @if (!editMode && !addMode) { + + } + @if (addMode) { + @switch (mode) { + @case ('docs') { - - + } + @case ('quickLinks') { + [ngModelOptions]="{standalone: true}" + (quickLinkAdded)="linkAdded($event)" + (quickLinkAddCanceled)="addMode = false"> - - - + } + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html index 36f9b78bb0..5247c09ffd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -22,191 +22,213 @@ {{ index + 1 }} - - + @switch (authUser.authority) { + @case (authority.SYS_ADMIN) {
widgets.getting-started.sys-admin.step1.title
- {{ 'tenant.tenants' | translate }} + @if (matStepper.selectedIndex === 0) { + {{ 'tenant.tenants' | translate }} + }
description{{ 'widgets.getting-started.sys-admin.step1.how-to-create-tenant' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step2.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step2.how-to-configure-mail-server' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step3.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step3.how-to-configure-sms-provider' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step4.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step4.how-to-configure-2fa' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step5.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step5.how-to-configure-oauth2' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step6.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} -
-
- - - -
-
widgets.getting-started.tenant-admin.step1.title
- {{ 'device.devices' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step2.title
-
-
-
-
- - Ubuntu - MacOS - Windows - - - -

+ + + +
+
widgets.getting-started.sys-admin.step2.title
+ @if (matStepper.selectedIndex === 1) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step2.how-to-configure-mail-server' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step3.title
+ @if (matStepper.selectedIndex === 2) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step3.how-to-configure-sms-provider' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step4.title
+ @if (matStepper.selectedIndex === 3) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step4.how-to-configure-2fa' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step5.title
+ @if (matStepper.selectedIndex === 4) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step5.how-to-configure-oauth2' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step6.title
+ @if (matStepper.selectedIndex === 5) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} +
+ } + @case (authority.TENANT_ADMIN) { + + +
+
widgets.getting-started.tenant-admin.step1.title
+ @if (matStepper.selectedIndex === 0) { + {{ 'device.devices' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step2.title
+
+
+
+
+ + Ubuntu + MacOS + Windows + + @switch (publishCommandSteps.value) { + @case ('ubuntu') { +

- - -

+ } + @case ('macos') { +

-
- -

-
- -

+ } + @case ('windows') { +

+ } + } +

-
-
- - description{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step3.title
- {{ 'dashboard.dashboards' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step4.title
- {{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step5.title
-
-
-
-

+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step3.title
+ @if (matStepper.selectedIndex === 2) { + {{ 'dashboard.dashboards' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step4.title
+ @if (matStepper.selectedIndex === 3) { + {{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step5.title
+
+
+
+

-
- - description{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step6.title
- {{ 'customer.customers' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }} -
-
-
- -
- -
-
+
+ + description{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }} + + + +
+
widgets.getting-started.tenant-admin.step6.title
+ @if (matStepper.selectedIndex === 5) { + {{ 'customer.customers' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }} +
+ } + } + + @if (allCompleted) { +
+ +
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html index 9fa8ea6b13..beb03851a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html @@ -15,52 +15,69 @@ limitations under the License. --> -
-
-
-
-
- - widgets.version-info.available-version - widgets.version-info.available - + @if (updateMessage?.updateAvailable) { +
+ -
{{ updateMessage?.latestVersion }}
- -
- {{ 'widgets.version-info.upgrade' | translate }} + } @else { +
+
check
+
widgets.version-info.version-is-up-to-date
-
+ }
- -
-
check
-
widgets.version-info.version-is-up-to-date
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html index 025dbf3e33..c47fd94f75 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html @@ -24,21 +24,26 @@
-
-
+ @if (solid) { +
+
+ } @else { + @for (section of batterySections; track trackBySection($index)) { +
+ } + }
-
-
{{ valueText }}
-
+ @if (showValue) { +
+
{{ valueText }}
+
+ }
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html index 9c2803d117..d9cf483025 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html @@ -20,11 +20,15 @@
-
-
-
- {{ error }} + @if (errorsMsg.length) { +
+
+ @for (error of errorsMsg; track error) { +
+ {{ error }} +
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts index f3d02a096a..fad6b59172 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts @@ -511,11 +511,9 @@ export class LiquidLevelWidgetComponent implements OnInit { let content: string; let container: JQuery; const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); - let value = 'N/A'; - + let value: number | string = 'N/A'; if (isNumeric(data)) { - value = this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)) - .toFixed(this.settings.decimals || 0); + value = +this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0); } this.valueColor.update(value); const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont), @@ -529,10 +527,9 @@ export class LiquidLevelWidgetComponent implements OnInit { let volume: number | string; if (this.widgetUnits !== CapacityUnits.percent) { const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to); - volume = this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)) - .toFixed(this.settings.decimals || 0); + volume = +this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0); } else { - volume = this.widgetUnitsConvertor(this.volume).toFixed(this.settings.decimals || 0); + volume = +this.widgetUnitsConvertor(this.volume).toFixed(this.ctx.widgetConfig.decimals || 0); } const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont), diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html index 0dbd05d6bc..43c92c9c73 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html @@ -17,25 +17,35 @@ -->
-
-
- -
+
+ @if (widgetComponent.dashboardWidget.showWidgetTitlePanel || showDate) { +
+ + @if (showDate) { +
+ }
-
-
-
-
-
-
-
widgets.signal-strength.value
-
{{ tooltipValueText }}
-
-
-
widgets.signal-strength.last-update
-
-
+ } +
+
+
+ @if (showTooltip) { +
+
+ @if (showTooltipValue) { +
+
widgets.signal-strength.value
+
{{ tooltipValueText }}
+ } + @if (showTooltipDate) { +
+
widgets.signal-strength.last-update
+
+
+ }
-
+
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html index 0cc88f5404..29e51327db 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html @@ -25,9 +25,15 @@ {{ icon }}
-
{{ label$ | async }}
-
{{ status$ | async }}
+ @if (showLabel) { +
{{ label$ | async }}
+ } + @if (showStatus) { +
{{ status$ | async }}
+ }
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html index e12bb74d9e..42786f7a58 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html @@ -16,43 +16,45 @@ -->
-
-
-
- -
-
- - + @if (attributeUpdateFormGroup) { + + @if (datasourceDetected && !errorMessage) { +
+
+ +
+
+ + +
-
- - -
- {{ errorMessage | translate }} -
-
- + } @else { +
+ {{ errorMessage | translate }} +
+ } + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html index cd50cb6174..4c74e0e668 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html @@ -17,72 +17,120 @@ --> - - - - - - - - + @if (!isRowDirection) { + + + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + } + + } - - - - - + + @if (legendConfig.showMin === true) { + - - - - + } + + } + } + @if (isRowDirection) { + + + @for (legendKey of legendKeys(); track legendKey) { + + } - - - - - - - - + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - + } + + }
{{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}{{ 'legend.latest' | translate }}
{{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}{{ 'legend.latest' | translate }}
- + +
+ + + +
- - - -
{{ 'legend.min' | translate }} + {{ 'legend.min' | translate }}
{{ 'legend.max' | translate }} + } + @if (isRowDirection && legendConfig.showMax === true) { +
{{ 'legend.max' | translate }}
{{ 'legend.avg' | translate }} + } + @if (isRowDirection && legendConfig.showAvg === true) { +
{{ 'legend.avg' | translate }}
{{ 'legend.total' | translate }} + } + @if (isRowDirection && legendConfig.showTotal === true) { +
{{ 'legend.total' | translate }}
{{ 'legend.latest' | translate }} + } + @if (isRowDirection && legendConfig.showLatest === true) { +
{{ 'legend.latest' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html index 485141bb03..afc11a7633 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html @@ -23,16 +23,21 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
entity.entity - - {{ entity.entityParseName }} - + @for (entity of data.entities; track entity) { + + {{ entity.entityParseName }} + + }
widgets.maps.select-entity-hint
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts index 9a03e0c116..ad9735f57f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/providers/image-map.ts @@ -91,7 +91,6 @@ export class ImageMap extends LeafletMap { name: imageUrlAttribute, label: imageUrlAttribute, settings: {}, - _hash: Math.random() } ] } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts index 75fc51eb1e..4b824240ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts @@ -57,6 +57,9 @@ export const createTooltip = (map: TbMap, } } }); + layer.on('mousemove', (e) => { + tooltip.setLatLng(e.latlng); + }); layer.on('mouseout', () => { tooltip.close(); }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts new file mode 100644 index 0000000000..226c3cf3d7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts @@ -0,0 +1,466 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + defaultBasePolylinesDataLayerSettings, + isCutPolygon, + isJSON, + MapDataLayerType, + PolylinesDataLayerSettings, + TbMapDatasource, + TbPolylineCoordinates, + TbPolylineData, + TbPolylineRawCoordinates +} from '@shared/models/widget/maps/map.models'; +import L from 'leaflet'; +import { DataKey, FormattedData } from '@shared/models/widget.models'; +import { ShapeStyleInfo, TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { Observable } from 'rxjs'; +import { isNotEmptyStr, isString } from '@core/utils'; +import { + TbLatestDataLayerItem, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; +import { map } from 'rxjs/operators'; + +class TbPolylineDataLayerItem extends TbLatestDataLayerItem { + + private polylineContainer: L.FeatureGroup; + private polyline: L.Polyline; + private polylineStyleInfo: ShapeStyleInfo; + private editing = false; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: PolylinesDataLayerSettings, + protected dataLayer: TbPolylineDataLayer) { + super(data, dsData, settings, dataLayer); + } + + public isEditing() { + return this.editing; + } + + public updateBubblingMouseEvents() { + this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode(); + } + + public remove() { + super.remove(); + if (this.polylineStyleInfo?.patternId) { + this.dataLayer.getMap().unUseShapePattern(this.polylineStyleInfo.patternId); + } + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { + const polyData = this.dataLayer.extractPolylineCoordinates(data); + const polyConstructor = L.polyline; + this.polyline = polyConstructor(polyData as (TbPolylineRawCoordinates & L.LatLngTuple[]), { + noClip: true, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + + this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => { + this.polylineStyleInfo = styleInfo; + if (this.polyline) { + this.polyline.setStyle(this.polylineStyleInfo.style); + } + }); + + this.polylineContainer = L.featureGroup(); + this.polyline.addTo(this.polylineContainer); + + this.updateLabel(data, dsData); + return this.polylineContainer; + } + + protected unbindLabel() { + this.polylineContainer.unbindTooltip(); + } + + protected bindLabel(content: L.Content): void { + this.polylineContainer.bindTooltip(content, {className: 'tb-polyline-label', permanent: true, direction: 'center'}) + .openTooltip(this.polylineContainer.getBounds().getCenter()); + } + + protected doUpdate(data: FormattedData, dsData: FormattedData[]): void { + this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => { + this.polylineStyleInfo = styleInfo; + this.updatePolylineShape(data); + this.updateTooltip(data, dsData); + this.updateLabel(data, dsData); + + if (!this.editing || !this.dataLayer.getMap().getMap().pm.globalCutModeEnabled()) { + this.polyline.setStyle(this.polylineStyleInfo.style); + } + }); + } + + protected doInvalidateCoordinates(data: FormattedData, _dsData: FormattedData[]): void { + this.updatePolylineShape(data); + } + + protected addItemClass(clazz: string): void { + if ((this.polyline as any)._path) { + L.DomUtil.addClass((this.polyline as any)._path, clazz); + } + } + + protected removeItemClass(clazz: string): void { + if ((this.polyline as any)._path) { + L.DomUtil.removeClass((this.polyline as any)._path, clazz); + } + } + + protected enableDrag(): void { + this.polyline.pm.setOptions({ + snappable: this.dataLayer.isSnappable() + }); + this.polyline.pm.enableLayerDrag(); + this.polyline.on('pm:dragstart', () => { + this.editing = true; + }); + this.polyline.on('pm:drag', () => { + if (this.tooltip?.isOpen()) { + this.tooltip.setLatLng(this.polyline.getBounds().getCenter()); + } + }); + this.polyline.on('pm:dragend', () => { + this.savePolylineCoordinates(); + this.editing = false; + }); + } + + protected disableDrag(): void { + this.polyline.pm.disableLayerDrag(); + this.polyline.off('pm:dragstart'); + this.polyline.off('pm:dragend'); + } + + protected onSelected(): L.TB.ToolbarButtonOptions[] { + const buttons: L.TB.ToolbarButtonOptions[] = []; + if (this.dataLayer.isEditEnabled()) { + this.enablePolylineEditMode(); + buttons.push( + { + id: 'cut', + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.cut'), + iconClass: 'tb-cut', + click: (e, button) => { + const map = this.dataLayer.getMap().getMap(); + if (!map.pm.globalCutModeEnabled()) { + this.disablePolylineRotateMode(); + this.disablePolylineEditMode(); + this.enablePolylineCutMode(button); + } else { + this.disablePolylineCutMode(button); + this.enablePolylineEditMode(); + } + } + }, + { + id: 'rotate', + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.rotate'), + iconClass: 'tb-rotate', + click: (e, button) => { + if (!this.polyline.pm.rotateEnabled()) { + this.disablePolylineCutMode(); + this.disablePolylineEditMode(); + this.enablePolylineRotateMode(button); + } else { + this.disablePolylineRotateMode(button); + this.enablePolylineEditMode(); + } + } + } + ); + } + return buttons; + } + + protected onDeselected(): void { + if (this.dataLayer.isEditEnabled()) { + this.disablePolylineEditMode(); + this.disablePolylineRotateMode(); + } + } + + protected canDeselect(cancel = false): boolean { + const map = this.dataLayer.getMap().getMap(); + if (map.pm.globalCutModeEnabled()) { + if (cancel) { + this.disablePolylineCutMode(); + } + return false; + } else if (this.polyline.pm.rotateEnabled()) { + if (cancel) { + this.disablePolylineRotateMode(); + } + return false; + } else if (this.editing) { + return false; + } + return true; + } + + protected removeDataItemTitle(): string { + return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.polyline.remove-polyline-for', {entityName: this.data.entityName}); + } + + protected removeDataItem(): Observable { + return this.dataLayer.savePolylineCoordinates(this.data, null); + } + + private enablePolylineEditMode() { + this.polyline.on('pm:markerdragstart', () => this.editing = true); + this.polyline.on('pm:markerdragend', () => setTimeout(() => { + this.editing = false; + })); + this.polyline.on('pm:edit', () => this.savePolylineCoordinates()); + this.polyline.pm.enable(); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(false); + } + + private disablePolylineEditMode() { + this.polyline.pm.disable(); + this.polyline.off('pm:markerdragstart'); + this.polyline.off('pm:markerdragend'); + this.polyline.off('pm:edit'); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(true); + } + + private enablePolylineCutMode(cutButton?: L.TB.ToolbarButton) { + this.polylineContainer.closePopup(); + this.editing = true; + this.polyline.options.bubblingMouseEvents = true; + this.polyline.setStyle({ + ...this.polylineStyleInfo.style, dashArray: '5 5', weight: 3, + color: '#3388ff', opacity: 1, fillColor: '#3388ff', fillOpacity: 0.2 + }); + this.addItemClass('tb-cut-mode'); + this.polyline.once('pm:cut', (e) => { + if (e.layer instanceof L.Polyline) { + this.polyline = L.polyline(e.layer.getLatLngs() as L.LatLngExpression[] | L.LatLngExpression[][] , { + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + this.polyline.addTo(this.polylineContainer); + } else if (e.layer instanceof L.LayerGroup) { + const parts: L.LatLngExpression[][] = []; + + if (e.layer && typeof e.layer.getLayers === 'function') { + const segments: L.Polyline[] = e.layer.getLayers() as L.Polyline[]; + segments.forEach(segment => { + parts.push(segment.getLatLngs() as L.LatLngExpression[]); + }); + } else if (e.layer instanceof L.Polyline) { + parts.push(e.layer.getLatLngs() as L.LatLngExpression[]); + } + + if (parts.length > 0) { + this.polyline = L.polyline(parts, { + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + this.polyline.addTo(this.polylineContainer); + } + } + // @ts-ignore + e.layer._pmTempLayer = true; + e.layer.remove(); + this.polylineContainer.removeLayer(this.polyline); + // @ts-ignore + this.polyline._pmTempLayer = false; + this.polyline.addTo(this.polylineContainer); + this.updateSelectedState(); + cutButton?.setActive(false); + this.savePolylineCoordinates(); + }); + const map = this.dataLayer.getMap().getMap(); + map.pm.setLang('en', { + tooltips: { + firstVertex: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-cut-hint'), + finishLine: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.finish-polyline-cut-hint') + } + }, 'en'); + map.pm.enableGlobalCutMode({ + // @ts-ignore + layersToCut: [this.polyline] + }); + // @ts-ignore + L.DomUtil.addClass(map.pm.Draw.Cut._hintMarker.getTooltip()._container, 'tb-place-item-label'); + cutButton?.setActive(true); + map.once('pm:globalcutmodetoggled', (e) => { + if (!e.enabled) { + this.disablePolylineCutMode(cutButton); + this.enablePolylineEditMode(); + } + }); + } + + private disablePolylineCutMode(cutButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode(); + this.polyline.setStyle({...this.polylineStyleInfo.style, dashArray: null}); + this.removeItemClass('tb-cut-mode'); + this.polyline.off('pm:cut'); + const map = this.dataLayer.getMap().getMap(); + map.pm.disableGlobalCutMode(); + cutButton?.setActive(false); + } + + private enablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.polylineContainer.closePopup(); + this.editing = true; + this.polyline.on('pm:rotateend', () => { + this.savePolylineCoordinates(); + }); + this.polyline.pm.enableRotate(); + rotateButton?.setActive(true); + this.polyline.on('pm:rotatedisable', () => { + this.disablePolylineRotateMode(rotateButton); + this.enablePolylineEditMode(); + }); + } + + private disablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polyline.pm.disableRotate(); + this.polyline.off('pm:rotateend'); + this.polyline.off('pm:rotatedisable'); + rotateButton?.setActive(false); + } + + private savePolylineCoordinates() { + let coordinates: TbPolylineCoordinates = this.polyline.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolylineCoordinates; + } + if (this.polyline instanceof L.Rectangle && !isCutPolygon(coordinates)) { + const bounds = this.polyline.getBounds(); + const boundsArray = [bounds.getNorthWest(), bounds.getNorthEast(), bounds.getSouthWest(), bounds.getSouthEast()]; + if (coordinates.every(point => boundsArray.find(boundPoint => boundPoint.equals(point as L.LatLng)) !== undefined)) { + coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; + } + } + this.dataLayer.savePolylineCoordinates(this.data, coordinates).subscribe(); + } + + private updatePolylineShape(data: FormattedData) { + if (this.editing) { + return; + } + + const polyData = this.dataLayer.extractPolylineCoordinates(data) as TbPolylineData; + if (this.polyline instanceof L.Polyline && !(this.polyline instanceof L.Rectangle)) { + this.polyline.setLatLngs(polyData as L.LatLngExpression[] | L.LatLngExpression[][]); + } else if (this.polyline instanceof L.Rectangle && polyData.length === 2) { + const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]); + this.polyline.setBounds(bounds); + } else { + this.polylineContainer.removeLayer(this.polyline); + if (polyData.length === 2 && isCutPolygon(polyData)) { + this.polyline = L.polyline(polyData as L.LatLngExpression[] | L.LatLngExpression[][], { + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode(), + noClip: true + }); + } + + this.polyline.addTo(this.polylineContainer); + this.editModeUpdated(); + } + } +} + +export class TbPolylineDataLayer extends TbShapesDataLayer { + + constructor(protected map: TbMap, + inputSettings: PolylinesDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return 'polylines'; + } + + public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void { + if (layer instanceof L.Polyline) { + let coordinates: TbPolylineCoordinates; + coordinates = layer.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolylineCoordinates; + } + this.savePolylineCoordinates(item.entity, coordinates).subscribe( + (converted) => { + item.entity[this.settings.polylineKey.label] = JSON.stringify(converted); + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a polyline.'); + } + } + + public extractPolylineCoordinates(data: FormattedData): TbPolylineRawCoordinates { + let rawPolyData = data[this.settings.polylineKey.label]; + if (isString(rawPolyData)) { + rawPolyData = JSON.parse(rawPolyData); + } + return this.map.polylineDataToCoordinates(rawPolyData); + } + + public savePolylineCoordinates(data: FormattedData, coordinates: TbPolylineCoordinates): Observable { + const converted = coordinates ? this.map.coordinatesToPolylineData(coordinates) : null; + const polylineData = [ + { + dataKey: this.settings.polylineKey, + value: converted + } + ]; + return this.map.saveItemData(data.$datasource, polylineData, this.settings.edit?.attributeScope).pipe( + map(() => converted) + ); + } + + protected getDataKeys(): DataKey[] { + return [this.settings.polylineKey]; + } + + protected defaultBaseSettings(map: TbMap): Partial { + return defaultBasePolylinesDataLayerSettings(map.type()); + } + + protected doSetup(): Observable { + return super.doSetup(); + } + + protected isValidLayerData(layerData: FormattedData): boolean { + return layerData && ((isNotEmptyStr(layerData[this.settings.polylineKey.label]) && !isJSON(layerData[this.settings.polylineKey.label]) + || Array.isArray(layerData[this.settings.polylineKey.label]))); + } + + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbPolylineDataLayerItem { + return new TbPolylineDataLayerItem(data, dsData, this.settings, this); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index 9db457d9c3..7ae4571c4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -37,10 +37,12 @@ import { isDefinedAndNotNull, objectHashCode, parseTbFunction, safeExecuteTbFunc import { CompiledTbFunction } from '@shared/models/js-function.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; -export type ShapePatternStorage = {[id: string]: { - pattern: L.TB.Pattern; - refCount: number; -}}; +export type ShapePatternStorage = { + [id: string]: { + pattern: L.TB.Pattern; + refCount: number; + } +}; interface ShapePatternInfo { type: ShapeFillType; @@ -311,6 +313,23 @@ export abstract class TbShapesDataLayer, dsData: FormattedData[], fillPatternId: string): Observable { + return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe( + map((patternWithId) => { + const stroke = this.strokeColorProcessor.processColor(data, dsData); + const style: L.PathOptions = { + color: stroke, + weight: this.settings.strokeWeight, + opacity: 1 + }; + return { + patternId: patternWithId.patternId, + style + } + }) + ); + } + public getShapeStyle(data: FormattedData, dsData: FormattedData[], fillPatternId: string): Observable { return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe( map((patternWithId) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index b3e03dfe47..976497e5f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -44,7 +44,7 @@ import { MarkerDataProcessor } from '@home/components/widget/lib/maps/data-layer type TripRouteData = {[time: number]: FormattedData}; -interface PointItem { +export interface PointItem { point: L.CircleMarker; tooltip?: L.Popup; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts index bc1e5f8f89..03c6a365ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts @@ -24,7 +24,11 @@ import { TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, - TbPolygonRawCoordinates + TbPolygonRawCoordinates, + TbPolylineCoordinate, + TbPolylineCoordinates, + TbPolylineRawCoordinate, + TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; @@ -179,5 +183,29 @@ export class TbGeoMap extends TbMap { return circleData; } + public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates { + return (expression).map((el: TbPolylineRawCoordinate) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { + return el; + } else if (Array.isArray(el) && el.length) { + return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate; + } else { + return null; + } + }).filter(el => !!el); + } + public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates { + if (coordinates.length) { + return coordinates.map((point: TbPolylineCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate; + } else { + const convertPoint = latLngPointToBounds(point, this.southWest, this.northEast); + return [convertPoint.lat, convertPoint.lng]; + } + }); + } + return []; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts index 238e6c8ac5..3b8a8f04c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts @@ -23,7 +23,12 @@ import { ImageSourceType, loadImageWithAspect, MapZoomAction, - TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates + TbCircleData, + TbPolygonCoordinate, + TbPolygonCoordinates, + TbPolygonRawCoordinate, + TbPolygonRawCoordinates, TbPolylineCoordinate, TbPolylineCoordinates, TbPolylineRawCoordinate, + TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; @@ -339,4 +344,36 @@ export class TbImageMap extends TbMap { ); } + public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates{ + return expression.map((el: TbPolylineRawCoordinate) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { + const latLng = this.pointToLatLng( + el[0] * this.width, + el[1] * this.height + ); + return [latLng.lat, latLng.lng] as TbPolylineRawCoordinate; + } else if (Array.isArray(el) && el.length) { + return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate; + } + else { + return null; + } + }).filter(el => !!el); + } + + public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates{ + if (coordinates.length) { + return coordinates.map((point: TbPolylineCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate; + } else { + const pos = this.latLngToPoint(point); + return [calculateNewPointCoordinate(pos.x, this.width), calculateNewPointCoordinate(pos.y, this.height)]; + } + }); + } else { + return []; + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index 2953c609ce..c45a3c0ca1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -20,12 +20,15 @@ import { defaultGoogleMapLayerSettings, defaultHereMapLayerSettings, defaultLayerTitle, + defaultOpenFreeMapLayerSettings, defaultOpenStreetMapLayerSettings, defaultTencentMapLayerSettings, GoogleMapLayerSettings, HereMapLayerSettings, MapLayerSettings, MapProvider, + OpenFreeMapLayerSettings, + OpenFreeMapStyleType, OpenStreetMapLayerSettings, ReferenceLayerType, TencentMapLayerSettings @@ -61,6 +64,8 @@ export abstract class TbMapLayer { inputSettings: DeepPartial) { switch (inputSettings.provider) { + case MapProvider.openfreemap: + return new TbOpenFreeMapLayer(ctx, inputSettings); case MapProvider.openstreet: return new TbOpenStreetMapLayer(ctx, inputSettings); case MapProvider.google: @@ -219,6 +224,32 @@ class TbOpenStreetMapLayer extends TbMapLayer { } +const openFreeMapStyleUrl = (style: OpenFreeMapStyleType): string => + `https://tiles.openfreemap.org/styles/${style}`; + +class TbOpenFreeMapLayer extends TbMapLayer { + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial) { + super(ctx, inputSettings); + } + + protected defaultSettings(): OpenFreeMapLayerSettings { + return defaultOpenFreeMapLayerSettings; + } + + protected createLayer(): Observable { + const styleUrl = openFreeMapStyleUrl(this.settings.layerType); + const layer = L.TB.MapLibreGL.mapLibreGLLayer({ + style: styleUrl, + attributionControl: { + customAttribution: 'OpenFreeMap, © OpenMapTiles, Data from OpenStreetMap' + } + }); + return of(layer); + } +} + class TbGoogleMapLayer extends TbMapLayer { static loadedApiKeysGlobal: {[key: string]: boolean} = {}; @@ -318,7 +349,20 @@ class TbCustomMapLayer extends TbMapLayer { } protected createLayer(): Observable { - const layer = L.tileLayer(this.settings.tileUrl); + if (this.settings.vectorTiles) { + const options: any = { + style: this.settings.tileUrl + }; + if (this.settings.customAttribution) { + options.attributionControl = { + customAttribution: this.settings.customAttribution + }; + } + return of(L.TB.MapLibreGL.mapLibreGLLayer(options)); + } + const layer = L.tileLayer(this.settings.tileUrl, this.settings.customAttribution ? { + attribution: this.settings.customAttribution + } : undefined); return of(layer); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 981301969d..8387b1aff7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -169,6 +169,9 @@ div.tb-widget .tb-widget-content.tb-no-interaction { &.tb-draw-circle { mask-image: url('data:image/svg+xml,'); } + &.tb-draw-polyline { + mask-image: url('data:image/svg+xml,'); + } &.tb-close { background: #D12730; mask-image: url('data:image/svg+xml,'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index eb3a284a9f..52080d3a5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -27,7 +27,9 @@ import { TbCircleData, TbMapDatasource, TbPolygonCoordinates, - TbPolygonRawCoordinates + TbPolygonRawCoordinates, + TbPolylineCoordinates, + TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { @@ -80,11 +82,12 @@ import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/tr import { CompiledTbFunction } from '@shared/models/js-function.models'; import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { EntityType } from '@shared/models/entity-type.models'; +import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; +import { TbPolylineDataLayer } from '@home/components/widget/lib/maps/data-layer/polylines-data-layer'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; -import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; -type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; +type TooltipInstancesData = { root: HTMLElement, instances: ITooltipsterInstance[] }; export abstract class TbMap { @@ -124,6 +127,7 @@ export abstract class TbMap { protected addRectangleButton: L.TB.ToolbarButton; protected addPolygonButton: L.TB.ToolbarButton; protected addCircleButton: L.TB.ToolbarButton; + protected addPolylineButton: L.TB.ToolbarButton; protected timeLineComponentRef: ComponentRef; protected timeLineComponent: MapTimelinePanelComponent; @@ -132,6 +136,7 @@ export abstract class TbMap { protected addMarkerDataLayers: TbLatestMapDataLayer[]; protected addPolygonDataLayers: TbLatestMapDataLayer[]; protected addCircleDataLayers: TbLatestMapDataLayer[]; + protected addPolylineDataLayers: TbLatestMapDataLayer[]; protected shapePatternStorage: ShapePatternStorage = {}; @@ -274,6 +279,11 @@ export abstract class TbMap { this.dataLayers.push(...circlesDataLayers); this.latestDataLayers.push(...circlesDataLayers); } + if (this.settings.polylines) { + const polylinesDataLayers = this.settings.polylines.map(settings => new TbPolylineDataLayer(this, settings)); + this.dataLayers.push(...polylinesDataLayers); + this.latestDataLayers.push(...polylinesDataLayers); + } if (this.settings.trips) { const tripsDataLayers = this.settings.trips.map(settings => new TbTripsDataLayer(this, settings)); this.dataLayers.push(...tripsDataLayers); @@ -319,7 +329,7 @@ export abstract class TbMap { let datasources: TbMapDatasource[]; for (const layerType of mapDataLayerTypes) { const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType) - .map(dl => dl.getDataSources()).flat(); + .map(dl => dl.getDataSources()).flat(); if (!datasources) { datasources = typeDatasources; } else { @@ -332,7 +342,7 @@ export abstract class TbMap { const dataLayersSubscriptionOptions: WidgetSubscriptionOptions = { datasources, hasDataPageLink: true, - useDashboardTimewindow: false, + useDashboardTimewindow: this.ctx.widgetConfig.useDashboardTimewindow ?? true, type: widgetType.latest, callbacks: { onDataUpdated: (subscription) => { @@ -344,6 +354,9 @@ export abstract class TbMap { } } }; + if (!dataLayersSubscriptionOptions.useDashboardTimewindow) { + dataLayersSubscriptionOptions.timeWindowConfig = this.ctx.widgetConfig.timewindow; + } this.ctx.subscriptionApi.createSubscription(dataLayersSubscriptionOptions, false).subscribe( (dataLayersSubscription) => { let pageSize = this.settings.mapPageSize; @@ -419,88 +432,100 @@ export abstract class TbMap { private setupEditMode() { - this.editToolbar = L.TB.bottomToolbar({ - mapElement: $(this.mapElement), - closeTitle: this.ctx.translate.instant('action.cancel'), - onClose: () => { - return this.deselectItem(true); - } - }); - - this.map.on('click', () => { - this.deselectItem(); - }); - - if (this.latestDataLayers.some(dl => dl.isEditable())) { - this.map.pm.setGlobalOptions({ snappable: false }); - this.map.pm.applyGlobalOptions(); - } - - const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled()); - const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length; - const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled()); - - if (showDragModeButton || addSupportedDataLayers.length) { - const drawToolbar = L.TB.toolbar({ - position: this.settings.controlsPosition - }).addTo(this.map); - if (showDragModeButton) { - this.dragModeButton = drawToolbar.toolbarButton({ - id: 'dragMode', - title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'), - iconClass: 'tb-drag-mode', - click: (e, button) => { - this.toggleDragMode(e, button); - } - }); - } - this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers'); - if (this.addMarkerDataLayers.length) { - this.addMarkerButton = drawToolbar.toolbarButton({ - id: 'addMarker', - title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), - iconClass: 'tb-place-marker', - click: (e, button) => { - this.placeMarker(e, button); - } - }); - this.addMarkerButton.setDisabled(true); - this.setPlaceMarkerStyle(); - } - this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons'); - if (this.addPolygonDataLayers.length) { - this.addRectangleButton = drawToolbar.toolbarButton({ - id: 'addRectangle', - title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), - iconClass: 'tb-draw-rectangle', - click: (e, button) => { - this.drawRectangle(e, button); - } - }); - this.addRectangleButton.setDisabled(true); - this.addPolygonButton = drawToolbar.toolbarButton({ - id: 'addPolygon', - title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), - iconClass: 'tb-draw-polygon', - click: (e, button) => { - this.drawPolygon(e, button); - } - }); - this.addPolygonButton.setDisabled(true); - } - this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles'); - if (this.addCircleDataLayers.length) { - this.addCircleButton = drawToolbar.toolbarButton({ - id: 'addCircle', - title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), - iconClass: 'tb-draw-circle', - click: (e, button) => { - this.drawCircle(e, button); - } - }); - this.addCircleButton.setDisabled(true); - } - } + this.editToolbar = L.TB.bottomToolbar({ + mapElement: $(this.mapElement), + closeTitle: this.ctx.translate.instant('action.cancel'), + onClose: () => { + return this.deselectItem(true); + } + }); + + this.map.on('click', () => { + this.deselectItem(); + }); + + if (this.latestDataLayers.some(dl => dl.isEditable())) { + this.map.pm.setGlobalOptions({snappable: false}); + this.map.pm.applyGlobalOptions(); + } + + const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled()); + const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length; + const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled()); + + if (showDragModeButton || addSupportedDataLayers.length) { + const drawToolbar = L.TB.toolbar({ + position: this.settings.controlsPosition + }).addTo(this.map); + if (showDragModeButton) { + this.dragModeButton = drawToolbar.toolbarButton({ + id: 'dragMode', + title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'), + iconClass: 'tb-drag-mode', + click: (e, button) => { + this.toggleDragMode(e, button); + } + }); + } + this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers'); + if (this.addMarkerDataLayers.length) { + this.addMarkerButton = drawToolbar.toolbarButton({ + id: 'addMarker', + title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), + iconClass: 'tb-place-marker', + click: (e, button) => { + this.placeMarker(e, button); + } + }); + this.addMarkerButton.setDisabled(true); + this.setPlaceMarkerStyle(); + } + this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons'); + if (this.addPolygonDataLayers.length) { + this.addRectangleButton = drawToolbar.toolbarButton({ + id: 'addRectangle', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), + iconClass: 'tb-draw-rectangle', + click: (e, button) => { + this.drawRectangle(e, button); + } + }); + this.addRectangleButton.setDisabled(true); + this.addPolygonButton = drawToolbar.toolbarButton({ + id: 'addPolygon', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), + iconClass: 'tb-draw-polygon', + click: (e, button) => { + this.drawPolygon(e, button); + } + }); + this.addPolygonButton.setDisabled(true); + } + this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles'); + if (this.addCircleDataLayers.length) { + this.addCircleButton = drawToolbar.toolbarButton({ + id: 'addCircle', + title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), + iconClass: 'tb-draw-circle', + click: (e, button) => { + this.drawCircle(e, button); + } + }); + this.addCircleButton.setDisabled(true); + } + this.addPolylineDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polylines'); + if (this.addPolylineDataLayers.length) { + this.addPolylineButton = drawToolbar.toolbarButton({ + id: 'addPolyline', + title: this.ctx.translate.instant('widgets.maps.data-layer.polyline.draw-polyline'), + iconClass: 'tb-draw-polyline', + click: (e, button) => { + this.drawPolyline(e, button); + } + }); + this.addPolylineButton.setDisabled(true); + } + } } private toggleDragMode(_e: MouseEvent, button: L.TB.ToolbarButton): void { @@ -557,6 +582,13 @@ export abstract class TbMap { })); } + private drawPolyline(e: MouseEvent, button: L.TB.ToolbarButton): void { + this.placeItem(e, button, this.addPolylineDataLayers, (entity) => this.prepareDrawMode('Line', { + startPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}), + finishPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.finish-polyline-hint-with-entity', {entityName: entity.entity.entityDisplayName}), + })); + } + private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[], prepareDrawMode: (entity: UnplacedMapDataItem) => void): void { if (this.isPlacingItem) { @@ -678,6 +710,9 @@ export abstract class TbMap { case MapItemType.circle: this.createCircle(actionData); break; + case MapItemType.polyline: + this.createPolyline(actionData); + break; } } @@ -725,6 +760,17 @@ export abstract class TbMap { })); } + private createPolyline(actionData: PlaceMapItemActionData): void { + this.createItem(actionData, () => this.prepareDrawMode('Line', { + startPolyline: actionData.action.mapItemTooltips.startPolyline + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startPolyline) + : this.ctx.translate.instant(mapItemTooltipsTranslation.startPolyline), + finishPolyline: actionData.action.mapItemTooltips.finishPolyline + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishPolyline) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishPolyline), + })); + } + private createItem(actionData: PlaceMapItemActionData, prepareDrawMode: () => void) { const actionId = 'id' in actionData.action ? actionData.action.id : 'map-button'; if (this.createMapItemActionId === actionId) { @@ -765,7 +811,7 @@ export abstract class TbMap { this.createMapItemActionId = actionId; - const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): {x: number; y: number} | TbPolygonRawCoordinates | TbCircleData => { + const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): { x: number; y: number } | TbPolygonRawCoordinates | TbCircleData => { switch (type) { case MapItemType.marker: if (layer instanceof L.Marker) { @@ -792,6 +838,15 @@ export abstract class TbMap { return this.coordinatesToCircleData(layer.getLatLng(), layer.getRadius()); } return null; + case MapItemType.polyline: + if (layer instanceof L.Polyline) { + let coordinates: any = layer.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0]; + } + return this.coordinatesToPolylineData(coordinates); + } + return null; } } } @@ -813,8 +868,8 @@ export abstract class TbMap { this.editToolbar.close(); } - private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle', tooltipsTranslation: Record) { - this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en'); + private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Line', tooltipsTranslation: Record) { + this.map.pm.setLang('en', {tooltips: tooltipsTranslation}, 'en'); this.map.pm.enableDraw(shape); // @ts-ignore L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label'); @@ -848,46 +903,46 @@ export abstract class TbMap { tooltipData.instances = []; } $(root) - .find('a[role="button"]:not(.leaflet-pm-action)') - .each((_index, element) => { - let title: string; - if (element.title) { - title = element.title; - $(element).removeAttr('title'); - } else if (element.parentElement.title) { - title = element.parentElement.title; - $(element).parent().removeAttr('title'); - } - const tooltip = $(element).tooltipster( - { - content: title, - theme: 'tooltipster-shadow', - delay: 10, - triggerClose: { - click: true, - tap: true, - scroll: true, - mouseleave: true - }, - side, - distance: 2, - trackOrigin: true, - functionBefore: (_instance, helper) => { - if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) { - return false; - } - }, - } - ); - const instance = tooltip.tooltipster('instance'); - tooltipData.instances.push(instance); - instance.on('destroyed', () => { - const index = tooltipData.instances.indexOf(instance); - if (index > -1) { - tooltipData.instances.splice(index, 1); + .find('a[role="button"]:not(.leaflet-pm-action)') + .each((_index, element) => { + let title: string; + if (element.title) { + title = element.title; + $(element).removeAttr('title'); + } else if (element.parentElement.title) { + title = element.parentElement.title; + $(element).parent().removeAttr('title'); } + const tooltip = $(element).tooltipster( + { + content: title, + theme: 'tooltipster-shadow', + delay: 10, + triggerClose: { + click: true, + tap: true, + scroll: true, + mouseleave: true + }, + side, + distance: 2, + trackOrigin: true, + functionBefore: (_instance, helper) => { + if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) { + return false; + } + }, + } + ); + const instance = tooltip.tooltipster('instance'); + tooltipData.instances.push(instance); + instance.on('destroyed', () => { + const index = tooltipData.instances.indexOf(instance); + if (index > -1) { + tooltipData.instances.splice(index, 1); + } + }); }); - }); }); } @@ -899,6 +954,7 @@ export abstract class TbMap { this.updateTripsAnchors(); this.updateBounds(); this.updateEditButtonsStates(); + this.ctx.updateLabelPatterns(); } private updateTrips(subscription: IWidgetSubscription) { @@ -943,6 +999,7 @@ export abstract class TbMap { private updateTripsAppearance() { this.tripDataLayers.forEach(dl => dl.updateAppearance()); } + private updateTripsTime() { this.tripDataLayers.forEach(dl => dl.updateCurrentTime()); } @@ -995,8 +1052,7 @@ export abstract class TbMap { (!this.bounds || !this.bounds.isValid() || (!this.bounds.equals(bounds) || force) && this.settings.fitMapBounds) && !mapBounds.contains(bounds) ) - ) - { + ) { this.bounds = bounds; if (!this.ignoreUpdateBounds && !this.isPlacingItem) { this.fitBounds(bounds); @@ -1023,6 +1079,9 @@ export abstract class TbMap { if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) { this.addCircleButton.setDisabled(true); } + if (this.addPolylineButton && this.addPolylineButton !== this.currentEditButton) { + this.addPolylineButton.setDisabled(true); + } this.customActionsToolbar?.setDisabled(true); } else { if (this.dragModeButton) { @@ -1040,6 +1099,9 @@ export abstract class TbMap { if (this.addCircleButton) { this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } + if (this.addPolylineButton) { + this.addPolylineButton.setDisabled(!this.addPolylineDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } this.customActionsToolbar?.setDisabled(false); } } @@ -1150,7 +1212,7 @@ export abstract class TbMap { $event.preventDefault(); $event.stopPropagation(); } - const { entityId, entityName, entityLabel, entityType } = data.$datasource; + const {entityId, entityName, entityLabel, entityType} = data.$datasource; this.ctx.actionsApi.handleWidgetAction($event, action, { entityType, id: entityId @@ -1219,7 +1281,7 @@ export abstract class TbMap { } } - public saveLocation(data: FormattedData, values: {[key: string]: any}): Observable { + public saveLocation(data: FormattedData, values: { [key: string]: any }): Observable { const datasource = data.$datasource; let dataKeys = datasource.dataKeys; if (datasource.latestDataKeys) { @@ -1329,18 +1391,21 @@ export abstract class TbMap { } } - public abstract locationDataToLatLng(position: {x: number; y: number}): L.LatLng; + public abstract locationDataToLatLng(position: { x: number; y: number }): L.LatLng; - public abstract latLngToLocationData(position: L.LatLng): {x: number; y: number}; + public abstract latLngToLocationData(position: L.LatLng): { x: number; y: number }; public abstract polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates; public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates; + public abstract polylineDataToCoordinates(coordinates: TbPolylineRawCoordinates): TbPolylineRawCoordinates; + + public abstract coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates; + public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData; public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData; - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html index 870583125b..c85272dd90 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html @@ -25,7 +25,9 @@
-
+ @if (settings.showTimestamp) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html index 1da11e665e..d407ba95e0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html @@ -19,9 +19,11 @@ {{ (selectEntityFormGroup.get('entity').value ? 'entity.entity' : 'widgets.maps.data-layer.select-entity') | translate }} - - {{ entity.entity.entityDisplayName }} - + @for (entity of entities; track entity) { + + {{ entity.entity.entityDisplayName }} + + } widgets.maps.data-layer.select-entity-hint diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 4cfc970feb..a5be42db25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -26,21 +26,29 @@ [class.row]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.RIGHT" [class.row-reverse]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.LEFT">
-
- - {{ 'widgets.mobile-app-qr-code.download-on-the-app-store' | translate }} - - - {{ 'widgets.mobile-app-qr-code.get-it-on-google-play' | translate }} - + @if ((mobileAppSettings?.iosEnabled || mobileAppSettings?.androidEnabled) && + mobileAppSettings?.qrCodeConfig.badgeEnabled && showBadgeContainer) { +
+ @if (mobileAppSettings?.iosEnabled) { + + {{ 'widgets.mobile-app-qr-code.download-on-the-app-store' | translate }} + + } + @if (mobileAppSettings?.androidEnabled) { + + {{ 'widgets.mobile-app-qr-code.get-it-on-google-play' | translate }} + + } +
+ } +
+ @if (mobileAppSettings?.qrCodeConfig.qrCodeLabelEnabled) { +
+ {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} +
+ }
-
- {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} -
-
-
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 index c03ac0c603..9f6b755431 100644 --- 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 @@ -19,218 +19,281 @@ [formGroup]="multipleInputFormGroup" tb-toast toastTarget="{{ toastTargetId }}" (ngSubmit)="saveForm()" novalidate autocomplete="off"> -
-
- {{ getGroupTitle(source.datasource) }} - -
- - - {{key.label}} - - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - - {{key.label}} - - - - - - {{ getErrorMessageText(key.settings,'required') }} - - - {{ getErrorMessageText(key.settings,'min') }} - - - {{ getErrorMessageText(key.settings,'max') }} - - - - {{key.label}} - - - - - - - {{ getErrorMessageText(key.settings,'required') }} - - - {{ getErrorMessageText(key.settings,'invalidJSON') | translate }} - - - - {{key.label}} - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - {{ getErrorMessageText(key.settings, 'invalidDate') }} - - - - {{key.label}} - - - {{ getCustomTranslationText(option.label ? option.label : option.value) }} - - - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - -
- {{key.label}} - - - + @if (entityDetected && isAllParametersValid) { +
+ @for (source of sources; track source) { +
+ @if (settings.showGroupTitle) { + {{ getGroupTitle(source.datasource) }} + + } +
+ @for (key of visibleKeys(source); track key) { + @if (key.settings.dataKeyValueType === 'string') { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + + } + @if (['double', 'integer'].includes(key.settings.dataKeyValueType)) { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings,'required') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('min')) { + + {{ getErrorMessageText(key.settings,'min') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('max')) { + + {{ getErrorMessageText(key.settings,'max') }} + + } + + } + @if (key.settings.dataKeyValueType === 'JSON') { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + + @if (multipleInputFormGroup.get(key.formId).hasError('required') && !multipleInputFormGroup.get(key.formId).hasError('invalidJSON')) { + + {{ getErrorMessageText(key.settings,'required') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('invalidJSON')) { + + {{ getErrorMessageText(key.settings,'invalidJSON') | translate }} + + } + + } + @if (['dateTime','date', 'time'].includes(key.settings.dataKeyValueType)) { + + {{key.label}} + + + + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('matDatepickerParse')) { + + {{ getErrorMessageText(key.settings, 'invalidDate') }} + + } + + } + @if (key.settings.dataKeyValueType === 'select') { + + {{key.label}} + + @for (option of key.settings.selectOptions; track option) { + + {{ getCustomTranslationText(option.label ? option.label : option.value) }} + + } + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + + } + @if (key.settings.dataKeyValueType === 'radio') { +
+ {{key.label}} + + @for (option of key.settings.selectOptions; track option) { + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + + {{ getCustomTranslationText(option.label ? option.label : option.value) }} + + + } + +
+ } + @if (key.settings.dataKeyValueType === 'color') { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + + + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + + } + @if (key.settings.dataKeyValueType === 'booleanCheckbox') { + + @if (key.settings.icon || key.settings.customIconUrl) { + + } + {{key.label}} + + } + @if (key.settings.dataKeyValueType === 'booleanSwitch') { + + @if (key.settings.icon || key.settings.customIconUrl) { - - - {{ getCustomTranslationText(option.label ? option.label : option.value) }} - - - + } + {{key.label}} + + } + }
- - {{key.label}} - - - - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - - - - - {{key.label}} - - - - - - {{key.label}} - - -
-
-
-
- - -
-
-
- {{ 'widgets.input-widgets.no-entity-selected' | translate }} + + }
-
- {{ 'widgets.input-widgets.not-allowed-entity' | translate }} + } + @if (entityDetected && isAllParametersValid && settings.showActionButtons) { +
+ + +
+ } + @if ((!entityDetected || !isAllParametersValid) && datasourceDetected) { +
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }} +
+
+ {{ 'widgets.input-widgets.not-allowed-entity' | translate }} +
-
+ } - {{key.settings.icon}} - + @if (!key.settings.customIconUrl) { + {{key.settings.icon}} + } @else { icon - + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html index 2ca0650502..2f1f53968e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html @@ -15,22 +15,28 @@ limitations under the License. --> - - - - - {{section.name}} - - - - - - {{place.icon}} - {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} - - - - - - - +@if (cols) { + + @for (section of showHomeSections$| async; track section) { + + + + {{section.name}} + + + + @for (place of sectionPlaces(section); track place) { + + + {{place.icon}} + {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} + + + } + + + + + } + +} 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 index a70c087a0d..6b1d18fe43 100644 --- 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 @@ -16,50 +16,56 @@ -->
-
-
-
- widgets.input-widgets.no-image - last photo -
- -
-
-
- -
-
- - - -
+ @if (isEntityDetected && dataKeyDetected && isCameraSupport && isProtocolHttps) { +
+
+
+ widgets.input-widgets.no-image + last photo
-
-
- preview photo - -
-
- - -
+ } +
+
+
+ +
+
+ + + +
+
+
+
+ preview photo + +
+
+ + +
+
-
-
- {{ textMessage | translate }} -
+ } + @if (textMessage) { +
+ {{ textMessage | translate }} +
+ }
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 index ae3dff1d97..060a74aebd 100644 --- 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 @@ -19,29 +19,30 @@ import { 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 { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { ImageService } from '@app/core/public-api'; import { AppState } from '@core/core.state'; -import { Overlay } from '@angular/cdk/overlay'; +import { AttributeService } from '@core/http/attribute.service'; 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 { isString } from '@core/utils'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { PageComponent } from '@shared/components/page.component'; 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'; +import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models'; +import { map, Observable, of, switchMap } from 'rxjs'; interface PhotoCameraInputWidgetSettings { widgetTitle: string; + saveToGallery: boolean; + usePublicGalleryLink: boolean; imageQuality: number; imageFormat: string; maxWidth: number; @@ -60,9 +61,7 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On constructor(@Inject(WINDOW) private window: Window, protected store: Store, - private elementRef: ElementRef, - private ngZone: NgZone, - private overlay: Overlay, + private imageService: ImageService, private utils: UtilsService, private attributeService: AttributeService, private sanitizer: DomSanitizer @@ -120,6 +119,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On lastPhoto: SafeUrl; datasourceDetected = false; + private mimeType: string; + private quality: number; + private static hasGetUserMedia(): boolean { return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia); } @@ -161,6 +163,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On this.dataKeyDetected = true; } } + this.mimeType = this.settings.imageFormat || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE; + this.quality = this.settings.imageQuality || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY; + this.detectAvailableDevices(); } @@ -170,8 +175,8 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On 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]); + if (keyData?.length && isString(keyData[0][1])) { + this.lastPhoto = keyData[0][1].startsWith('data:image/') ? this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]) : keyData[0][1]; } } @@ -238,26 +243,34 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On 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; + + const uploadImage$ = this.settings.saveToGallery ? this.saveImageToGallery() : of(this.previewPhoto); + + uploadImage$.pipe( + switchMap((image) => { + const saveData = [{ + key: this.datasource.dataKeys[0].name, + value: image + }]; + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { + return this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData); + } else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { + return this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData); + } + }) + ).subscribe({ + next: () => { + this.isPreviewPhoto = false; + this.updatePhoto = false; + this.closeCamera(); + }, + error: () => { + this.updatePhoto = false; + } }); } @@ -272,12 +285,36 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On 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.previewPhoto = this.canvasElement.toDataURL(this.mimeType, this.quality); this.isPreviewPhoto = true; } + private saveImageToGallery(): Observable { + return new Observable((observer) => { + this.canvasElement.toBlob( + (blob) => { + if (blob) { + observer.next(blob); + observer.complete(); + } else { + observer.error('Failed to create image blob'); + } + }, + this.mimeType, + this.quality + ); + }).pipe( + switchMap((blob) => { + const fileName = this.datasource.dataKeys[0].name; + const file = new File([blob], fileName, { type: this.mimeType }); + return this.imageService.uploadImage(file, fileName); + }), + map((imageInfo) => + this.settings.usePublicGalleryLink ? imageInfo.publicLink : imageInfo.link + ) + ); + } + private inititedVideoStream(deviceId?: string, init = false) { if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) { const videoTrackConstraints = { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html index 01313aa0d0..87495e0477 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html @@ -17,6 +17,10 @@ -->
-
entity.no-data
-
widgets.invalid-qr-code-text
+ @if (!qrCodeText && !invalidQrCodeText) { +
entity.no-data
+ } + @if (invalidQrCodeText) { +
widgets.invalid-qr-code-text
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html index 1b5b3f1992..f98ef7e8c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
@@ -35,18 +38,22 @@ {{ rpcMessageTypeText }}
-
- +
+ widgets.persistent-table.method - - {{'widgets.persistent-table.method-error' | translate}} - - - {{'widgets.persistent-table.white-space-error' | translate}} - + @if (persistentFormGroup.get('method').hasError('required')) { + + {{'widgets.persistent-table.method-error' | translate}} + + } + @if (persistentFormGroup.get('method').hasError('pattern')) { + + {{'widgets.persistent-table.white-space-error' | translate}} + + } - + widgets.persistent-table.retries @@ -71,7 +78,9 @@
-
+ @if (!(isLoading$ | async)) { +
+ }
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + widgets.persistent-table.created-time - + widgets.persistent-table.expiration-time
- + widgets.persistent-table.message-type - + widgets.persistent-table.status - + widgets.persistent-table.method - - widgets.persistent-table.retries - - + @if (persistentFormGroup.get('retries').value) { + + widgets.persistent-table.retries + + + }
@@ -69,7 +73,7 @@ {{ 'widgets.persistent-table.response' | translate }} - + @@ -98,14 +102,15 @@
- + @if (allowDelete) { + + }
- -
-
- - - - - - + }
+ @if (actionCellButtonAction.length) { +
+ + + @for (actionDescriptor of actionCellButtonAction; track actionDescriptor) { + @if (actionDescriptor.icon) { + + } + } + +
+ } + [class.!hidden]="persistentDatasource.dataLoading"> {{ noDataDisplayMessageText }} + class="no-data-found flex items-center justify-center">{{ noDataDisplayMessageText }} {{ 'common.loading' | translate }} -
- - + class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }} +
+ @if (displayPagination) { + + } + @if (displayPagination) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html index faf054957c..83dd2c5b3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html @@ -22,5 +22,7 @@
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts index f538e8cfc8..4836cc3804 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -28,7 +28,7 @@ import { Circle, Effect, Element, G, Gradient, Path, Runner, Svg, Text, Timeline import '@svgdotjs/svg.filter.js'; import tinycolor from 'tinycolor2'; import { WidgetContext } from '@home/models/widget-component.models'; -import { Observable, of } from 'rxjs'; +import { Observable, of, shareReplay } from 'rxjs'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { catchError, map, take } from 'rxjs/operators'; import { MatIconRegistry } from '@angular/material/icon'; @@ -336,6 +336,11 @@ export abstract class PowerButtonShape { protected onPowerSymbolCircle: Path; protected onPowerSymbolLine: Path; + private onIcon$: Observable; + private offIcon$: Observable; + protected onIconOffsetX: number = 0; + protected onIconOffsetY: number = 0; + protected constructor(protected widgetContext: WidgetContext, protected svgShape: Svg, protected iconRegistry: MatIconRegistry, @@ -360,10 +365,14 @@ export abstract class PowerButtonShape { take(1), map((svgElement) => { const element = new Element(svgElement.firstChild); + const iconGroup = this.svgShape.group(); + element.addTo(iconGroup); const box = element.bbox(); const scale = size / box.height; element.scale(scale); - return element; + const scaledBox = iconGroup.bbox(); + iconGroup.translate(-scaledBox.cx, -scaledBox.cy); + return iconGroup; }), catchError(() => of(null) )); @@ -414,8 +423,15 @@ export abstract class PowerButtonShape { public drawOffShape(centerGroup: G, label: boolean, labelWeight?: string, circleStroke?: boolean) { if (this.icons.offButtonIcon.showIcon) { - this.createIconElement(this.icons.offButtonIcon.icon, this.icons.offButtonIcon.iconSize).subscribe(icon => - this.offPowerSymbolIcon = icon.center(cx, cy).addTo(centerGroup)); + this.offIcon$ = this.createIconElement(this.icons.offButtonIcon.icon, this.icons.offButtonIcon.iconSize) + .pipe(shareReplay(1)); + + this.offIcon$.pipe(take(1)).subscribe(icon => { + this.offPowerSymbolIcon = icon; + icon.translate(cx, cy); + centerGroup.add(icon); + }); + } else { if (label) { this.offLabelShape = this.createOffLabel(labelWeight).addTo(centerGroup); @@ -428,10 +444,16 @@ export abstract class PowerButtonShape { public drawOnShape(onCenterGroup?: G, label?: boolean, labelWeight?: string, circleStroke?: boolean, mask?: Circle) { if (this.icons.onButtonIcon.showIcon) { - this.createIconElement(this.icons.onButtonIcon.icon, this.icons.onButtonIcon.iconSize).subscribe(icon => { - this.onPowerSymbolIcon = icon.center(cx, cy); + this.onIcon$ = this.createIconElement(this.icons.onButtonIcon.icon, this.icons.onButtonIcon.iconSize) + .pipe(shareReplay(1)); + + this.onIcon$.subscribe(icon => { + const iconBox = icon.bbox(); + this.onIconOffsetX = iconBox.cx; + this.onIconOffsetY = iconBox.cy; + this.onPowerSymbolIcon = icon.translate(cx, cy); if (isDefinedAndNotNull(onCenterGroup)) { - this.onPowerSymbolIcon.addTo(onCenterGroup); + onCenterGroup.add(this.onPowerSymbolIcon); } if (isDefinedAndNotNull(mask)) { this.createMask(mask, [this.onPowerSymbolIcon]); @@ -462,7 +484,9 @@ export abstract class PowerButtonShape { public onCenterTimeLine(timeline: Timeline, label: boolean) { if (this.icons.onButtonIcon.showIcon) { - this.onPowerSymbolIcon.timeline(timeline); + if (this.onIcon$) { + this.onIcon$.subscribe(icon => icon.timeline(timeline)); + } } else { if (label) { this.onLabelShape.timeline(timeline); @@ -475,7 +499,7 @@ export abstract class PowerButtonShape { public offCenterColor(mainColor: PowerButtonColor, label: boolean) { if (this.icons.offButtonIcon.showIcon) { - this.offPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offIcon$.subscribe(icon => icon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity})) } else { if (label) { this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); @@ -488,7 +512,7 @@ export abstract class PowerButtonShape { public onCenterColor(mainColor: PowerButtonColor, label: boolean) { if (this.icons.onButtonIcon.showIcon) { - this.onPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onIcon$.subscribe((icon)=> icon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity})) } else { if (label) { this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); @@ -501,7 +525,12 @@ export abstract class PowerButtonShape { public buttonAnimation(scale: number, label: boolean) { if (this.icons.onButtonIcon.showIcon) { - powerButtonAnimation(this.onPowerSymbolIcon).transform({scale}); + const translateX = cx - this.onIconOffsetX; + const translateY = cy - this.onIconOffsetY; + powerButtonAnimation(this.onPowerSymbolIcon).transform({ + scale: scale, + translate: [translateX, translateY] + }); } else { if (label) { powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html index b54bc91b6f..421668d0f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html @@ -21,20 +21,32 @@
-
- {{ icon }} -
{{ label$ | async }}
-
+ @if (showIcon || showLabel) { +
+ @if (showIcon) { + {{ icon }} + } + @if (showLabel) { +
{{ label$ | async }}
+ } +
+ }
-
{{ offLabel }}
+ @if (showOffLabel) { +
{{ offLabel }}
+ } -
{{ onLabel }}
+ @if (showOnLabel) { +
{{ onLabel }}
+ }
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html index 60523571fd..afd2bb085c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html @@ -17,18 +17,24 @@ -->
-
- -
-
-
-
{{ valueText }}
+ @if (showWidgetTitlePanel) { +
+
-
-
- {{ leftIcon }} + } +
+ @if (showValue) { +
+
{{ valueText }}
+ } +
+ @if (showLeftRightIcon) { +
+ {{ leftIcon }} +
+ }
-
-
-
{{ tickMinText }}
-
-
-
{{ tickMaxText }}
+ @if (showTicks) { +
+
+
{{ tickMinText }}
+
+
+
{{ tickMaxText }}
+
-
-
-
- {{ rightIcon }} + }
+ @if (showLeftRightIcon) { +
+ {{ rightIcon }} +
+ }
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss index 02d8fa22d5..be6ca5255c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss @@ -106,6 +106,9 @@ $backgroundColorDisabled: var(--tb-slider-background-color-disabled, #D5D7E5); height: 6px; min-height: 6px; min-width: 0; + .mdc-slider__input{ + top: -21px; + } &.mdc-slider--disabled { opacity: 1; } 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 index d681981e01..4eef6a39b0 100644 --- 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 @@ -15,40 +15,45 @@ limitations under the License. --> -
-
- {{ error }} -
-
- {{title}} -
-
+
+ {{ error }} +
+
+ {{title}} +
+
-
- - -
-
- OFF - - ON - + id="switch-container"> +
+ + +
+
+ OFF + + ON + +
+
-
-
- +} @else {
{{title}} - -
+ @if (executingUpdateValue) { + + } + @if (!executingUpdateValue) { +
+ } + [error]="error">
-
+} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html index d70dda0019..c9c24439ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html @@ -31,7 +31,7 @@
{{ valueText }}
+ [style]="valueStyle">{{ valueText }}
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html index 830a62b484..6a0acbdf25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html @@ -17,13 +17,17 @@ -->
- + @if (widgetComponent.dashboardWidget.showWidgetTitlePanel) { - + }
-
scada.no-symbol-selected
+ @if (noScadaSymbol) { +
scada.no-symbol-selected
+ }
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index e81eed3b4a..64183bd00b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -101,34 +101,36 @@ - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + }
@@ -136,9 +138,11 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html index fc89c9840e..82f1de831d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html @@ -29,7 +29,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.power-button.power-on
@@ -38,7 +38,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.power-button.power-off
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -58,7 +58,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -69,12 +69,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> - - {{ powerButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> + @for (layout of powerButtonLayouts; track layout) { + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + }
{{ 'widgets.background.background' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html index 8cef9903f5..e6331aded8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html @@ -29,7 +29,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
@@ -60,7 +60,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -72,12 +72,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> - - {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> + @for (layout of segmentedButtonLayouts; track layout) { + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + }
@@ -112,9 +114,15 @@ {{ 'widgets.button.label' | translate }}
- - - + @@ -143,9 +151,15 @@ {{ 'widgets.button.label' | translate }}
- - - + @@ -204,16 +218,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
@@ -245,16 +261,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts index a87b9d1917..629ddd179e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts @@ -15,7 +15,13 @@ /// import { Component } from '@angular/core'; -import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models'; +import { + TargetDevice, + WidgetSettings, + WidgetSettingsComponent, + widgetTitleAutocompleteValues, + widgetType +} from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -32,6 +38,7 @@ import { WidgetButtonToggleState, widgetButtonToggleStatesTranslations } from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { ValueCardLayout } from '@home/components/widget/lib/cards/value-card-widget.models'; @Component({ selector: 'tb-segmented-button-widget-settings', @@ -68,6 +75,8 @@ export class SegmentedButtonWidgetSettingsComponent extends WidgetSettingsCompon segmentedButtonWidgetSettingsForm: UntypedFormGroup; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); @@ -132,4 +141,52 @@ export class SegmentedButtonWidgetSettingsComponent extends WidgetSettingsCompon }) }); } + + protected validatorTriggers(): string[] { + return ['appearance.leftAppearance.showLabel', 'appearance.leftAppearance.showIcon', 'appearance.rightAppearance.showLabel', 'appearance.rightAppearance.showIcon',]; + } + + protected updateValidators(emitEvent: boolean) { + const showLeftLabel: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.showLabel').value; + const showRightLabel: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.showLabel').value; + + const showLeftIcon: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.showIcon').value; + const showRightIcon: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.showIcon').value; + + if (showLeftLabel) { + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.label').enable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.labelFont').enable({emitEvent}); + } else { + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.label').disable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.labelFont').disable({emitEvent}); + } + + if (showRightLabel) { + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.label').enable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.labelFont').enable({emitEvent}); + } else { + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.label').disable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.labelFont').disable({emitEvent}); + } + + if (showLeftIcon) { + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.icon').enable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSize').enable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSizeUnit').enable({emitEvent}); + } else { + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.icon').disable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSize').disable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSizeUnit').disable({emitEvent}); + } + + if (showRightIcon) { + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.icon').enable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSize').enable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSizeUnit').enable({emitEvent}); + } else { + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.icon').disable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSize').disable({emitEvent}); + this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSizeUnit').disable({emitEvent}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html index 2b579b4750..45dcb56a5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html @@ -22,9 +22,11 @@
{{ 'widgets.aggregated-value-card.position' | translate }}
- - {{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} - + @for (position of aggregatedValueCardKeyPositions; track position) { + + {{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html index 84c2da2255..d2bda36ab2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html @@ -28,9 +28,16 @@ {{ 'widgets.aggregated-value-card.subtitle' | translate }}
- - - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts index e64f47b3b0..3afdc7e6cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component, Injector } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import {WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues} from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -34,6 +34,8 @@ export class AggregatedValueCardWidgetSettingsComponent extends WidgetSettingsCo datePreviewFn = this._datePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private $injector: Injector, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html new file mode 100644 index 0000000000..5a5efa1499 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html @@ -0,0 +1,85 @@ + +
+
+ + + + + +
+
+ + + + + + +
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss new file mode 100644 index 0000000000..8d295765f8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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-form-table-row.tb-api-usage-data-key-row { + + .tb-source-field { + flex: 1 1 25%; + display: flex; + gap: 12px; + .tb-label-field { + flex: 1; + } + } + + .tb-data-key-fields { + display: flex; + gap: 12px; + min-width: 0; + flex: 1 1 50%; + } + + .tb-data-key-field { + flex: 1; + min-width: 0; + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } + + @media #{$mat-lt-lg} { + .tb-source-field { + flex-direction: column; + flex: 1 1 50%; + } + .tb-data-key-fields{ + flex-direction: column; + flex: 1 1 50%; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts new file mode 100644 index 0000000000..3ffff5309f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts @@ -0,0 +1,157 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + DestroyRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + ApiUsageDataKeysSettings, + ApiUsageSettingsContext +} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; +import { Observable, of } from "rxjs"; + +@Component({ + selector: 'tb-api-usage-data-key-row', + templateUrl: './api-usage-data-key-row.component.html', + styleUrls: ['./api-usage-data-key-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageDataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ApiUsageDataKeyRowComponent implements ControlValueAccessor, OnInit { + + DatasourceType = DatasourceType; + DataKeyType = DataKeyType; + + widgetType = widgetType; + + @Input() + disabled: boolean; + + @Input() + dsEntityAliasId: string; + + @Input() + context: ApiUsageSettingsContext; + + @Output() + dataKeyRemoved = new EventEmitter(); + + dataKeyFormGroup: UntypedFormGroup; + + modelValue: ApiUsageDataKeysSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.dataKeyFormGroup = this.fb.group({ + label: [null, [Validators.required]], + state: [null, []], + status: [null, [Validators.required]], + maxLimit: [null, [Validators.required]], + current: [null, [Validators.required]] + }); + this.dataKeyFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => this.updateModel() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataKeyFormGroup.disable({emitEvent: false}); + } else { + this.dataKeyFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: ApiUsageDataKeysSettings): void { + this.modelValue = value; + this.dataKeyFormGroup.patchValue( + { + label: value?.label, + state: value?.state, + status: value?.status, + maxLimit: value?.maxLimit, + current: value?.current + }, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + editKey(keyType: 'status' | 'maxLimit' | 'current') { + const targetDataKey: DataKey = this.dataKeyFormGroup.get(keyType).value; + this.context.editKey(targetDataKey, this.dsEntityAliasId).subscribe( + (updatedDataKey) => { + if (updatedDataKey) { + this.dataKeyFormGroup.get(keyType).patchValue(updatedDataKey); + } + } + ); + } + + private updateValidators() { + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; + this.propagateChange(this.modelValue); + } + + fetchDashboardStates(searchText?: string): Observable> { + return of(this.context.callbacks.fetchDashboardStates(searchText)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts new file mode 100644 index 0000000000..09d1d73743 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts @@ -0,0 +1,118 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; +import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; +import { Observable } from 'rxjs'; +import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { materialColors } from '@shared/models/material.models'; + +export interface ApiUsageSettingsContext { + aliasController: IAliasController; + callbacks: WidgetConfigCallbacks; + widget: Widget; + editKey: (key: DataKey, entityAliasId: string, WidgetType?: widgetType) => Observable; + generateDataKey: (key: DataKey) => DataKey; +} + + +export interface ApiUsageWidgetSettings { + dsEntityAliasId: string; + apiUsageDataKeys: ApiUsageDataKeysSettings[]; + targetDashboardState: string; + background: BackgroundSettings; + padding: string; +} + +export interface ApiUsageDataKeysSettings { + label: string; + state: string; + status: DataKey; + maxLimit: DataKey; + current: DataKey; +} + +const generateDataKey = (label: string, status: string, maxLimit: string, current: string) => { + return { + label, + state: '', + status: { + name: status, + label: status, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + }, + maxLimit: { + name: maxLimit, + label: maxLimit, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + }, + current: { + name: current, + label: current, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + } + } +} + +export const apiUsageDefaultSettings: ApiUsageWidgetSettings = { + dsEntityAliasId: '', + apiUsageDataKeys: [ + generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'), + generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'), + generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'), + generateDataKey('{i18n:api-usage.javascript-function-executions}', 'jsExecutionApiState', 'jsExecutionLimit', 'jsExecutionCount'), + generateDataKey('{i18n:api-usage.tbel-function-executions}', 'tbelExecutionApiState', 'tbelExecutionLimit', 'tbelExecutionCount'), + generateDataKey('{i18n:api-usage.data-points-storage-days}', 'dbApiState', 'storageDataPointsLimit', 'storageDataPointsCount'), + generateDataKey('{i18n:api-usage.alarms-created}', 'alarmApiState', 'createdAlarmsLimit', 'createdAlarmsCount'), + generateDataKey('{i18n:api-usage.emails}', 'emailApiState', 'emailLimit', 'emailCount'), + generateDataKey('{i18n:api-usage.sms}', 'smsApiState', 'smsLimit', 'smsCount'), + ], + targetDashboardState: 'default', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + padding: '0' +}; + +export const getUniqueDataKeys = (data: ApiUsageDataKeysSettings[]): DataKey[] => { + const seenNames = new Set(); + return data + .flatMap(item => [item.status, item.maxLimit, item.current]) + .filter(key => { + if (seenNames.has(key.name)) { + return false; + } + seenNames.add(key.name); + return true; + }); +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html new file mode 100644 index 0000000000..a718b11669 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html @@ -0,0 +1,103 @@ + + +
+
widget-config.datasource
+ + + +
+
+
+
widgets.api-usage.label
+
widgets.api-usage.state-name
+
widgets.api-usage.status
+
widgets.api-usage.limit
+
widgets.api-usage.current-number
+
+
+ @if (dataKeysFormArray().controls?.length) { +
+ @for (dataKeyControl of dataKeysFormArray().controls; track trackByDataKey($index)) { +
+
+ + +
+ +
+
+
+ } +
+ } @else { + {{ 'widgets.api-usage.no-key' | translate }} + } +
+
+ +
+
+ + +
+ +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss new file mode 100644 index 0000000000..6767ec1e7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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-map-data-layers { + .tb-form-table-header-cell { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-x-pos-header { + flex: 1 1 25%; + } + &.tb-y-pos-header { + flex: 1 1 25%; + } + &.tb-key-header { + flex: 1 1 50%; + } + &.tb-actions-header { + width: 80px; + min-width: 80px; + } + @media #{$mat-lt-lg} { + &.tb-source-header { + flex: 1 1 30%; + } + &.tb-x-pos-header, &.tb-y-pos-header { + flex: 1 1 35%; + } + &.tb-key-header { + flex: 1 1 70%; + } + } + @media #{$mat-xs} { + &.tb-x-pos-header, &.tb-y-pos-header { + display: none; + } + &.tb-key-header { + display: none; + } + } + } + + .tb-form-table-body { + tb-api-usage-data-key-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts new file mode 100644 index 0000000000..5bda37fac1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -0,0 +1,213 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { + DataKey, + DataKeyConfigMode, + WidgetSettings, + WidgetSettingsComponent, + widgetType +} from '@shared/models/widget.models'; +import { + AbstractControl, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + ValidatorFn, Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + ApiUsageDataKeysSettings, + apiUsageDefaultSettings, + ApiUsageSettingsContext +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; +import { deepClone } from '@core/utils'; +import { Observable, of } from 'rxjs'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; + +@Component({ + selector: 'tb-api-usage-widget-settings', + templateUrl: './api-usage-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss', 'api-usage-widget-settings.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageWidgetSettingsComponent), + multi: true + } + ], + standalone: false +}) +export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { + + apiUsageWidgetSettingsForm: UntypedFormGroup; + + context: ApiUsageSettingsContext; + + constructor(protected store: Store, + private dialog: MatDialog, + private fb: UntypedFormBuilder) { + super(store); + } + + ngOnInit() { + this.context = { + aliasController: this.aliasController, + callbacks: this.callbacks, + widget: this.widget, + editKey: this.editKey.bind(this), + generateDataKey: this.generateDataKey.bind(this) + }; + } + + protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { + settingsForm.setControl('apiUsageDataKeys', this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), {emitEvent: false}); + } + + dataKeysFormArray(): UntypedFormArray { + return this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray; + } + + trackByDataKey(index: number): any { + return index; + } + + get dragEnabled(): boolean { + return this.dataKeysFormArray().controls.length > 1; + } + + layerDrop(event: CdkDragDrop) { + const layer = this.dataKeysFormArray().at(event.previousIndex); + this.dataKeysFormArray().removeAt(event.previousIndex); + this.dataKeysFormArray().insert(event.currentIndex, layer); + } + + removeDataKey(index: number) { + (this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray).removeAt(index); + } + + addDataKey() { + const dataKey = { + label: '', + state: '', + status: null, + maxLimit: null, + current: null + }; + const dataKeysArray = this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray; + const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]); + dataKeysArray.push(dataKeyControl); + } + + protected settingsForm(): UntypedFormGroup { + return this.apiUsageWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return apiUsageDefaultSettings; + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + dsEntityAliasId: settings?.dsEntityAliasId, + apiUsageDataKeys: settings?.apiUsageDataKeys, + targetDashboardState: settings?.targetDashboardState, + background: settings?.background, + padding: settings.padding + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.apiUsageWidgetSettingsForm = this.fb.group({ + dsEntityAliasId: [settings?.dsEntityAliasId, Validators.required], + apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), + targetDashboardState: [settings?.targetDashboardState], + background: [settings?.background, []], + padding: [settings.padding, []] + }); + } + + private prepareDataKeysFormArray(dataKeys: ApiUsageDataKeysSettings[]): UntypedFormArray { + const dataKeysControls: Array = []; + if (dataKeys) { + dataKeys.forEach((dataLayer) => { + dataKeysControls.push(this.fb.control(dataLayer, [this.apiUsageDataKeyValidator()])); + }); + } + return this.fb.array(dataKeysControls); + } + + protected validatorTriggers(): string[] { + return []; + } + + protected updateValidators() { + } + + apiUsageDataKeyValidator = (): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const value: ApiUsageDataKeysSettings = control.value; + if (!value?.label || !value?.current || !value?.maxLimit || !value?.status) { + return { + dataKey: true + } + } + return null; + }; + }; + + private editKey(key: DataKey, entityAliasId: string, _widgetType = widgetType.latest): Observable { + return this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(key), + dataKeyConfigMode: DataKeyConfigMode.general, + aliasController: this.aliasController, + widgetType: _widgetType, + entityAliasId, + showPostProcessing: true, + callbacks: this.callbacks, + hideDataKeyColor: true, + hideDataKeyDecimals: true, + hideDataKeyUnits: true, + hideDataKeyAggregation: true, + widget: this.widget, + dashboard: null, + dataKeySettingsForm: null, + dataKeySettingsDirective: null + } + }).afterClosed(); + } + + private generateDataKey(key: DataKey): DataKey { + return this.callbacks.generateDataKey(key.name, key.type, null, false, null); + } + + fetchDashboardStates(searchText?: string): Observable> { + return of(this.callbacks.fetchDashboardStates(searchText)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.html index 3a8fb61d99..93db3741f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.html @@ -18,23 +18,27 @@
widgets.dashboard-state.dashboard-state-settings - + - + @if (dashboardStateWidgetSettingsForm.get('stateId').value) { + + } - - - + @for (state of filteredDashboardStates | async; track state) { + + + + } @@ -47,12 +51,13 @@ {{ 'widgets.dashboard-state.autofill-state-layout' | translate }} - + widgets.dashboard-state.default-margin diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html index 97437ba48a..b4520590b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.html @@ -28,9 +28,16 @@ {{ 'widgets.label-card.label' | translate }}
- - - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts index c9177cb78b..5bf0ace732 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import {WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues} from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -35,6 +35,8 @@ export class LabelValueCardWidgetSettingsComponent extends WidgetSettingsCompone valuePreviewFn = this._valuePreviewFn.bind(this); + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html index ae477eeb6d..dc5bd4eab3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html @@ -24,35 +24,39 @@
- + @if (!disabled) { + + }
- + widgets.label-widget.label-pattern - - {{ 'widgets.label-widget.label-pattern-required' | translate }} - + @if (labelWidgetLabelFormGroup.get('pattern').hasError('required')) { + + {{ 'widgets.label-widget.label-pattern-required' | translate }} + + }
widgets.label-widget.label-position -
- +
+ widgets.label-widget.x-pos - + widgets.label-widget.y-pos @@ -60,6 +64,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html index 7dad480482..52e3f39fa3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html @@ -17,27 +17,30 @@ -->
+ label="{{ 'widgets.label-widget.background-image' | translate }}" + formControlName="backgroundImageUrl">
widgets.label-widget.labels
-
- - -
-
-
- widgets.label-widget.no-labels + @for (labelControl of typedSelectOptions; track trackByLabelControl($index, labelControl); let isLast = $last) { +
+ + +
+ }
+ @if (!labelsFormArray().controls.length) { +
+ widgets.label-widget.no-labels +
+ }
-
-
-
widget-config.card-style
-
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
-
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgePosition.value | translate }} - - - + @if (!mobileAppQRCodeWidgetSettingsForm.get('useSystemSettings').value) { +
+
+
widget-config.card-style
+
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + @for (badgePosition of badgePositionTranslationsMap | keyvalue; track badgePosition) { + + {{ badgePosition.value | translate }} + + } + + +
+
+
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
-
-
- - {{ 'admin.mobile-app.label' | translate }} - - - - +
+
+
{{ 'widgets.background.background' | translate }}
+ +
-
-
-
{{ 'widgets.background.background' | translate }}
- - +
{{ 'widget-config.card-padding' | translate }}
+ + +
-
-
{{ 'widget-config.card-padding' | translate }}
- - - -
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html index bd16e4a467..9c222e2379 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout"> - - {{ progressBarLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout"> + @for (layout of progressBarLayouts; track layout) { + + {{ progressBarLayoutTranslationMap.get(layout) | translate }} + + }
@@ -63,20 +65,22 @@
-
- - {{ 'widgets.progress-bar.range-ticks' | translate }} - -
- - - - + @if (progressBarWidgetSettingsForm.get('layout').value === progressBarLayout.default) { +
+ + {{ 'widgets.progress-bar.range-ticks' | translate }} + +
+ + + + +
-
+ }
{{ 'widgets.progress-bar.bar-color' | translate }}
{{ 'widgets.qr-code.use-qr-code-text-function' | translate }} - + widgets.qr-code.qr-code-text-pattern {{ 'widgets.qr-code.qr-code-text-pattern-hint' | translate }} - - {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} - + @if (qrCodeWidgetSettingsForm.get('qrCodeTextPattern').hasError('required')) { + + {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} + + } - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + }
@@ -103,9 +105,11 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + }
@@ -115,6 +119,23 @@ {{ 'widgets.table.use-entity-label-tab-name' | translate }} +
+
widgets.table.sort-by
+
+ + + {{ entityFields.createdTime.name | translate }} + {{ entityFields.name.name | translate }} + + + + + {{ 'common.sort-asc' | translate }} + {{ 'common.sort-desc' | translate }} + + +
+
widgets.table.rows
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index 494963fa53..32d2b1b087 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -20,6 +20,8 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { entityFields } from '@shared/models/entity.models'; @Component({ selector: 'tb-timeseries-table-widget-settings', @@ -29,6 +31,9 @@ import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widge }) export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent { + entityFields = entityFields; + Direction = Direction; + timeseriesTableWidgetSettingsForm: UntypedFormGroup; pageStepSizeValues = []; @@ -59,12 +64,20 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon hideEmptyLines: false, disableStickyHeader: false, useRowStyleFunction: false, - rowStyleFunction: '' + rowStyleFunction: '', + sortOrder: { + property: this.entityFields.createdTime.keyName, + direction: Direction.DESC + } }; } protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize; + settings.sortOrder = { + property: settings.sortOrder?.property || this.entityFields.createdTime.keyName, + direction: settings.sortOrder?.direction || Direction.DESC + }; this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement); return settings; } @@ -94,7 +107,17 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon hideEmptyLines: [settings.hideEmptyLines, []], disableStickyHeader: [settings.disableStickyHeader, []], useRowStyleFunction: [settings.useRowStyleFunction, []], - rowStyleFunction: [settings.rowStyleFunction, [Validators.required]] + rowStyleFunction: [settings.rowStyleFunction, [Validators.required]], + sortOrder: this.fb.group({ + property: [ + settings.sortOrder.property, + Validators.required + ], + direction: [ + settings.sortOrder.direction, + Validators.required + ] + }) }); } @@ -102,6 +125,14 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon return ['useRowStyleFunction', 'displayPagination', 'pageStepCount', 'pageStepIncrement']; } + protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + settings.sortOrder = { + property: settings.sortOrder?.property || this.entityFields.createdTime.keyName, + direction: settings.sortOrder?.direction || Direction.DESC + }; + return settings; + } + protected updateValidators(emitEvent: boolean, trigger: string) { if (trigger === 'pageStepCount' || trigger === 'pageStepIncrement') { this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').reset(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html index 15832b67a3..e605e97c89 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-md': horizontal ? 1 : 2 }}" - label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout"> - - {{ valueCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueCardLayouts; track layout) { + + {{ valueCardLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html index 1f042ab1ff..a03ec60e35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> - - {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueChartCardLayouts; track layout) { + + {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 07ca827630..bda194828e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -24,7 +24,7 @@
+ formControlName="grid">
widgets.bar-chart.bar-appearance
@@ -78,40 +78,43 @@
+ formControlName="barBackgroundSettings" + titleText="widgets.chart.background" + fillNoneTitle="widgets.chart.fill-type-solid"> + stroked + formControlName="noAggregationBarWidthSettings">
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + advanced + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis" + advanced>
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="dataKeyCallbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -127,9 +130,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -190,7 +195,7 @@
+ formControlName="tooltipDateFormat"> @@ -226,7 +231,7 @@
+ formControlName="animation">
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts index 6290572770..55ed3d5035 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts @@ -31,6 +31,7 @@ import { barChartWithLabelsDefaultSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; +import { updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; @Component({ selector: 'tb-bar-chart-with-labels-widget-settings', @@ -123,6 +124,11 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom }); } + protected onSettingsChanged(updated: WidgetSettings) { + updateLatestDataKeys([updated.yAxis], this.datasource, this.dataKeyCallbacks); + super.onSettingsChanged(updated); + } + protected validatorTriggers(): string[] { return ['showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html index 87a84493c2..9bbfc28921 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -202,16 +202,19 @@ widgets.chart.add-threshold
-
-
- - + @if (thresholdsFormArray().length) { +
+ @for (thresholdControl of thresholdsFormArray().controls; track trackByThreshold($index, thresholdControl)) { +
+ + +
+ }
-
+ }
widgets.chart.comparison-settings
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html index b458e20387..a9034b9e12 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html @@ -22,23 +22,29 @@ widgets.chart.radius - - {{ 'widgets.chart.common-pie-settings-range-error' | translate }} - + @if (flotPieWidgetSettingsForm.get('radius').hasError('min') || flotPieWidgetSettingsForm.get('radius').hasError('max')) { + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + + } widgets.chart.inner-radius - - {{ 'widgets.chart.common-pie-settings-range-error' | translate }} - + @if (flotPieWidgetSettingsForm.get('innerRadius').hasError('min') || flotPieWidgetSettingsForm.get('innerRadius').hasError('max')) { + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + + } widgets.chart.tilt - - {{ 'widgets.chart.common-pie-settings-range-error' | translate }} - + @if (flotPieWidgetSettingsForm.get('tilt').hasError('min') || flotPieWidgetSettingsForm.get('tilt').hasError('max')) { + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + + } @@ -56,9 +62,9 @@ + formControlName="color" + icon="format_color_fill" + label="{{ 'widgets.chart.color' | translate }}" openOnInput colorClearButton> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html index e97d45fdd1..ad8b865391 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html @@ -18,13 +18,14 @@
- - - - - - + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html index 0ae52d9993..2079d5e35f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html @@ -24,43 +24,53 @@ {{ 'widgets.chart.enable-selection-mode' | translate }} - - {{ 'widgets.chart.display-smooth-lines' | translate }} - -
-
widgets.chart.line-shadow-size
- - - -
-
-
widgets.chart.default-bar-width
- - - -
-
-
{{ 'widgets.chart.bar-alignment' | translate }}
- - - - {{ 'widgets.chart.bar-alignment-left' | translate }} - - - {{ 'widgets.chart.bar-alignment-right' | translate }} - - - {{ 'widgets.chart.bar-alignment-center' | translate }} - - - -
-
-
widgets.chart.thresholds-line-width
- - - -
+ @if (chartType === 'graph') { + + {{ 'widgets.chart.display-smooth-lines' | translate }} + + } + @if (chartType === 'graph') { +
+
widgets.chart.line-shadow-size
+ + + +
+ } + @if (chartType === 'bar') { +
+
widgets.chart.default-bar-width
+ + + +
+ } + @if (chartType === 'bar') { +
+
{{ 'widgets.chart.bar-alignment' | translate }}
+ + + + {{ 'widgets.chart.bar-alignment-left' | translate }} + + + {{ 'widgets.chart.bar-alignment-right' | translate }} + + + {{ 'widgets.chart.bar-alignment-center' | translate }} + + + +
+ } + @if (chartType === 'graph' || chartType === 'bar') { +
+
widgets.chart.thresholds-line-width
+ + + +
+ }
{{ 'widgets.chart.default-font' | translate }}
@@ -74,25 +84,27 @@
-
-
widget-config.legend
- - - - - {{ 'widget-config.legend' | translate }} - - - - widget-config.advanced-settings - - - - - - - -
+ @if (chartType === 'graph' || chartType === 'bar') { +
+
widget-config.legend
+ + + + + {{ 'widget-config.legend' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+ }
widgets.chart.axis
@@ -267,124 +279,131 @@
-
-
widgets.chart.comparison-settings
- - - - - {{ 'widgets.chart.enable-comparison' | translate }} - - - - widget-config.advanced-settings - - - -
-
{{ 'widgets.chart.time-for-comparison' | translate }}
- - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - -
-
-
widgets.chart.custom-interval-value
- - - -
-
-
widgets.chart.comparison-x-axis-settings
-
-
widgets.chart.axis-title
- - - -
-
- - {{ 'widgets.chart.show-tick-labels' | translate }} + @if (chartType === 'graph' || chartType === 'bar') { +
+
widgets.chart.comparison-settings
+ + + + + {{ 'widgets.chart.enable-comparison' | translate }} -
+ + + widget-config.advanced-settings + + +
-
{{ 'widgets.chart.axis-position' | translate }}
+
{{ 'widgets.chart.time-for-comparison' | translate }}
- - - {{ 'widgets.chart.axis-position-top' | translate }} + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - {{ 'widgets.chart.axis-position-bottom' | translate }} + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
-
- - -
-
-
widgets.chart.custom-legend-settings
- - - - - {{ 'widgets.chart.enable-custom-legend' | translate }} - - - - widget-config.advanced-settings - - - -
-
widgets.chart.label-keys-list
-
-
-
- - -
+
+
widgets.chart.custom-interval-value
+ + + +
+
+
widgets.chart.comparison-x-axis-settings
+
+
widgets.chart.axis-title
+ + +
-
- widgets.chart.no-label-keys +
+ + {{ 'widgets.chart.show-tick-labels' | translate }} +
-
- +
+
{{ 'widgets.chart.axis-position' | translate }}
+ + + + {{ 'widgets.chart.axis-position-top' | translate }} + + + {{ 'widgets.chart.axis-position-bottom' | translate }} + + +
-
- - -
+ + +
+ } + @if (chartType === 'graph' || chartType === 'bar') { +
+
widgets.chart.custom-legend-settings
+ + + + + {{ 'widgets.chart.enable-custom-legend' | translate }} + + + + widget-config.advanced-settings + + + +
+
widgets.chart.label-keys-list
+
+
+ @for (labelDataKeyControl of typedSelectOptions; track trackByLabelDataKey($index, labelDataKeyControl); let isLast = $last) { +
+ + +
+ } +
+ @if (!dataKeysListForLabelsFormArray().controls.length) { +
+ widgets.chart.no-label-keys +
+ } +
+ +
+
+
+
+
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index 65fc17df26..4b3af4a63e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -424,6 +424,10 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit return this.flotSettingsFormGroup.get('dataKeysListForLabels') as UntypedFormArray; } + get typedSelectOptions() { + return this.dataKeysListForLabelsFormArray().controls as (AbstractControl & { new?: boolean })[]; + } + public trackByLabelDataKey(index: number, labelDataKeyControl: AbstractControl): any { return labelDataKeyControl; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html index e9ca4d51ea..9be7ec3da7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html @@ -24,13 +24,15 @@
- + @if (!disabled) { + + }
@@ -41,9 +43,11 @@ widgets.chart.key-name - - {{ 'widgets.chart.key-name-required' | translate }} - + @if (labelDataKeyFormGroup.get('name').hasError('required')) { + + {{ 'widgets.chart.key-name-required' | translate }} + + } widgets.chart.key-type diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html index 8d7ba9a603..fcc637f7b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html @@ -31,9 +31,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -84,9 +86,11 @@
- - {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} - + @for (type of latestChartTooltipValueTypes; track type) { + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} + + } @@ -120,7 +124,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -149,9 +153,11 @@
- - {{ pieChartLabelPositionTranslationMap.get(position) | translate }} - + @for (position of pieChartLabelPositions; track position) { + + {{ pieChartLabelPositionTranslationMap.get(position) | translate }} + + } - - {{ doughnutLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.doughnut.layout' | translate }}" formControlName="layout"> + @for (layout of doughnutLayouts; track layout) { + + {{ doughnutLayoutTranslationMap.get(layout) | translate }} + + }
@@ -256,8 +264,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -303,9 +311,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -360,9 +368,11 @@
{{ 'widgets.radar-chart.shape' | translate }}
- - {{ radarChartShapeTranslations.get(shape) | translate }} - + @for (shape of radarChartShapes; track shape) { + + {{ radarChartShapeTranslations.get(shape) | translate }} + + }
@@ -380,9 +390,11 @@
- - {{ chartLineTypeTranslations.get(type) | translate }} - + @for (type of chartLineTypes; track type) { + + {{ chartLineTypeTranslations.get(type) | translate }} + + } @@ -398,9 +410,11 @@
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + } @@ -416,9 +430,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + }
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html index 5f2c38d251..2ff773c61c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html @@ -24,7 +24,7 @@
+ formControlName="grid">
widgets.range-chart.range-chart-appearance
@@ -45,11 +45,11 @@ {{ 'widgets.range-chart.show-range-thresholds' | translate }} + boxButton + hideYAxis + [widgetConfig]="widgetConfig?.config" + title="widgets.range-chart.range-thresholds-settings" + formControlName="rangeThreshold">
@@ -77,9 +77,11 @@ - - {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} - + @for (stepType of lineSeriesStepTypes; track stepType) { + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + }
@@ -92,9 +94,11 @@
widgets.time-series-chart.line-type
- - {{ chartLineTypeTranslations.get(lineType) | translate }} - + @for (lineType of chartLineTypes; track lineType) { + + {{ chartLineTypeTranslations.get(lineType) | translate }} + + }
@@ -121,9 +125,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } widgets.time-series-chart.series.point.point-shape
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
@@ -168,26 +176,29 @@
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis">
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="dataKeyCallbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -203,9 +214,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -266,7 +279,7 @@
+ formControlName="tooltipDateFormat"> @@ -302,7 +315,7 @@
+ formControlName="animation">
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts index 703c621d62..053b6858bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -30,7 +30,7 @@ import { rangeChartDefaultSettings } from '@home/components/widget/lib/chart/ran import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; import { lineSeriesStepTypes, - lineSeriesStepTypeTranslations + lineSeriesStepTypeTranslations, updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { chartLabelPositions, @@ -269,6 +269,11 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { } } + protected onSettingsChanged(updated: WidgetSettings) { + updateLatestDataKeys([updated.yAxis], this.datasource, this.dataKeyCallbacks); + super.onSettingsChanged(updated); + } + private _pointLabelPreviewFn(): string { const units = getSourceTbUnitSymbol(this.widgetConfig.config.units); const decimals: number = this.widgetConfig.config.decimals; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html index fcc2faf05f..d8706aa2d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -34,67 +34,74 @@
widgets.time-series-chart.axis.y-axis
- - {{ yAxis }} - + @for (yAxis of yAxisIds; track yAxis) { + + {{ yAxis }} + + }
-
-
widgets.time-series-chart.series.series-type
- - {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} - -
+ @if (chartType === TimeSeriesChartType.default) { +
+
widgets.time-series-chart.series.series-type
+ + @for (type of timeSeriesChartSeriesTypes; track type) { + {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} + } + +
+ } @else { +
{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}
+ } + formControlName="barSettings">
widgets.chart.tooltip-settings
+ formControlName="tooltipValueFormatter" + [globalVariables]="functionScopeVariables" + [functionArgs]="['value', 'latestData']" + functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" + helpId="widget/lib/flot/tooltip_value_format_fn">
-
-
widgets.time-series-chart.comparison.settings
- - - - - {{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }} - - - - -
-
widgets.time-series-chart.comparison.comparison-values-label
- - - -
-
-
{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}
- - -
-
-
-
+ @if (comparisonEnabled) { +
+
widgets.time-series-chart.comparison.settings
+ + + + + {{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }} + + + + +
+
widgets.time-series-chart.comparison.comparison-values-label
+ + + +
+
+
{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}
+ + +
+
+
+
+ } - -
{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html index 9a562cec25..b607679be2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html @@ -16,56 +16,124 @@ --> -
-
widgets.time-series-chart.series.line.line
-
- - {{ 'widgets.time-series-chart.series.line.show-line' | translate }} - + @if (chartType !== TimeSeriesChartType.point) { +
+
widgets.time-series-chart.series.line.line
+ @if (chartType !== TimeSeriesChartType.state) { +
+ + {{ 'widgets.time-series-chart.series.line.show-line' | translate }} + +
+ } +
+ + {{ 'widgets.time-series-chart.series.line.step-line' | translate }} + + + + @for (stepType of lineSeriesStepTypes; track stepType) { + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + } + + +
+ @if (chartType !== TimeSeriesChartType.state) { +
+ + {{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} + +
+ } +
+
widgets.time-series-chart.line-type
+ + + @for (lineType of chartLineTypes; track lineType) { + + {{ chartLineTypeTranslations.get(lineType) | translate }} + + } + + +
+
+
widgets.time-series-chart.line-width
+ + + +
-
- - {{ 'widgets.time-series-chart.series.line.step-line' | translate }} + } + @if (chartType !== TimeSeriesChartType.point) { +
+
widgets.time-series-chart.series.point.points
+
+ + {{ 'widgets.time-series-chart.series.point.show-points' | translate }} + +
+ +
+ } @else { +
+ +
+ {{ 'widgets.time-series-chart.series.point.point-label' | translate }} +
- - - - {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} - - - +
+ + + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } + + + + + + +
-
- - {{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} +
+ + {{ 'widgets.time-series-chart.series.point.point-label-background' | translate }} + +
-
widgets.time-series-chart.line-type
+
widgets.time-series-chart.series.point.point-shape
- - - {{ chartLineTypeTranslations.get(lineType) | translate }} - + + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
-
widgets.time-series-chart.line-width
+
widgets.time-series-chart.series.point.point-size
- +
-
-
-
widgets.time-series-chart.series.point.points
-
- - {{ 'widgets.time-series-chart.series.point.show-points' | translate }} - -
- -
+ } @@ -80,9 +148,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } widgets.time-series-chart.series.point.point-shape
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index f9123bb723..b49d8ad902 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -56,24 +56,28 @@
- - + + } + formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="dataKeyCallbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + [yAxisIds]="yAxisIds" + [supportsUnitConversion]="widgetConfig.typeParameters.supportsUnitConversion">
widgets.time-series-chart.chart-style
@@ -83,7 +87,7 @@
+ formControlName="grid">
@@ -95,15 +99,16 @@
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + advanced + axisType="xAxis">
- - + @if (chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar) { + + + }
@@ -175,9 +180,9 @@
{{ 'tooltip.trigger' | translate }}
{{ 'tooltip.trigger-point' | translate }} + [value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }} {{ 'tooltip.trigger-axis' | translate }} + [value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}
@@ -207,11 +212,11 @@
+ formControlName="tooltipValueFormatter" + [globalVariables]="functionScopeVariables" + [functionArgs]="['value', 'latestData']" + functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" + helpId="widget/lib/flot/tooltip_value_format_fn">
@@ -219,7 +224,7 @@
+ formControlName="tooltipDateFormat"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html index 229fb79a62..b6ddd18d6d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html @@ -17,31 +17,36 @@ -->
- - + @if (!fullscreen) { + + } + @if (fullscreen) { + + }
-
- - -
- + @if (!fullscreen) { +
+ + +
+ } @else {
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html index 6d36ee7e24..683b97c588 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html @@ -19,29 +19,31 @@
-
- - - - {{ 'widget.resource-is-extension' | translate }} - - -
+ @for (resource of action.customResources; track resource; let i = $index) { +
+ + + + {{ 'widget.resource-is-extension' | translate }} + + +
+ }
- - - - + @if (hasCustomFunction) { + + + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html index 0e009341ee..e04426ce1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html @@ -22,40 +22,43 @@
{{ 'widgets.value-action.action' | translate }}
- - {{ getValueActionTranslationsMap.get(action) | translate }} - + @for (action of getValueActions; track action) { + + {{ getValueActionTranslationsMap.get(action) | translate }} + + }
- - + @switch (getValueSettingsFormGroup.get('action').value) { + @case (getValueAction.DO_NOTHING) {
widgets.value-action.value
+ formControlName="defaultValue">
-
- + } + @case (getValueAction.EXECUTE_RPC) {
{{ 'widgets.value-action.method' | translate }}*
- - warning - + @if (getValueSettingsFormGroup.get('executeRpc').get('method').hasError('required') + && getValueSettingsFormGroup.get('executeRpc').get('method').touched) { + + warning + + }
-
- + } + @case (getValueAction.GET_ATTRIBUTE) {
{{ 'widgets.value-action.attribute-scope' | translate }}
@@ -64,9 +67,11 @@ {{ 'attribute.scope-any' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + }
@@ -85,8 +90,8 @@
- - + } + @case (getValueAction.GET_TIME_SERIES) {
{{ 'widgets.value-action.time-series-key' | translate }}*
@@ -102,15 +107,17 @@
-
- + } + @case (getValueAction.GET_ALARM_STATUS) {
alarm.alarm-severity
- - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + }
@@ -124,101 +131,111 @@
-
- -
-
-
widgets.value-action.action-result-converter
- - {{ 'widgets.value-action.converter-none' | translate }} - {{ 'widgets.value-action.converter-function' | translate }} - -
- - -
-
{{ 'widgets.value-action.state-when-result-is' | translate:{state: stateLabel} }}
- +
+
widgets.value-action.action-result-converter
+ + {{ 'widgets.value-action.converter-none' | translate }} + {{ 'widgets.value-action.converter-function' | translate }} + +
+ @if (getValueSettingsFormGroup.get('dataToValue').get('type').value === dataToValueType.FUNCTION) { + + + } + @if (valueType === ValueType.BOOLEAN) { +
+
{{ 'widgets.value-action.state-when-result-is' | translate:{state: stateLabel} }}
+ - + layout="column" + formControlName="compareToValue"> + +
+ }
-
-
- - - - - - widget-config.advanced-settings - - - -
-
{{ 'widgets.value-action.request-timeout-ms' | translate }}
- - - + + + + + + widget-config.advanced-settings + + + +
+
{{ 'widgets.value-action.request-timeout-ms' | translate }}
+ + + @if (getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && + getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid) { + - warning - - -
-
- - - - - {{ 'widgets.value-action.request-persistent' | translate }} - - - - -
-
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
- - - + warning + + } + +
+
+ + + + + {{ 'widgets.value-action.request-persistent' | translate }} + + + + +
+
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
+ + + @if (getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && + getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid) { + - warning - - -
-
-
-
-
-
-
+ class="tb-error"> + warning +
+ } +
+
+
+
+
+ + +
+ }
} + @case (MapItemType.polyline) { +
+
widget-action.map-item-tooltip.start-draw-polyline
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-polyline
+ + + +
+ } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html index 1110455dc9..026eef2bf4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html @@ -20,132 +20,93 @@
{{ 'widget-action.mobile.action-type' | translate }}*
- - {{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }} - + @for (actionType of mobileActionTypes; track actionType) { + + {{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }} + + } - - warning - + @if (mobileActionFormGroup.get('type').hasError('required') + && mobileActionFormGroup.get('type').touched) { + + warning + + }
- - - - - - - - - - - - - - - - - - - - - - + + @if (mobileActionFormGroup.get('type').value === mobileActionType.takePhoto || + mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery || + mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot) { +
+ + {{ 'widget-action.mobile.save-to-gallery' | translate }} + +
+ } + @if (mobileActionFormGroup.get('type').value === mobileActionType.deviceProvision) { +
+
{{ 'widget-action.mobile.provision-type' | translate }}*
+ + + @for (type of provisionTypes; track type) { + + {{ provisionTypeTranslationMap.get(type) | translate }} + + } + + +
+ } + + @for (config of actionConfig; track config.formControlName) { +
+ + + + {{ config.title | translate }} + + + + +
+ }
- - + + @if(mobileActionFormGroup.get('type').value) { + @for (config of commonActionConfig; track config.formControlName) { +
+ + + + {{ config.title | translate }} + + + + +
+ } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts index 469bbf95c7..3e6a55ead6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts @@ -24,6 +24,9 @@ import { } from '@angular/forms'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { + ActionConfig, + ProvisionType, + provisionTypeTranslationMap, WidgetActionType, WidgetMobileActionDescriptor, WidgetMobileActionType, @@ -35,6 +38,7 @@ import { getDefaultGetPhoneNumberFunction, getDefaultHandleEmptyResultFunction, getDefaultHandleErrorFunction, + getDefaultHandleNonMobileFallBackFunction, getDefaultProcessImageFunction, getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, @@ -69,6 +73,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit functionScopeVariables: string[]; + actionConfig: ActionConfig[]; + commonActionConfig: ActionConfig[]; + + provisionTypes = Object.values(ProvisionType); + provisionTypeTranslationMap = provisionTypeTranslationMap; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -100,8 +110,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit this.mobileActionFormGroup = this.fb.group({ type: [null, Validators.required], handleEmptyResultFunction: [null], - handleErrorFunction: [null] + handleErrorFunction: [null], + handleNonMobileFallbackFunction: [null] }); + this.getCommonActionConfigs(); this.mobileActionFormGroup.get('type').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((type: WidgetMobileActionType) => { @@ -110,6 +122,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit action = {...action, ...this.mobileActionTypeFormGroup.value}; } this.updateMobileActionType(type, action); + this.getActionConfigs(); }); this.mobileActionFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) @@ -134,10 +147,14 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit } writeValue(value: WidgetMobileActionDescriptor | null): void { - this.mobileActionFormGroup.patchValue({type: value?.type, - handleEmptyResultFunction: value?.handleEmptyResultFunction, - handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false}); + this.mobileActionFormGroup.patchValue({ + type: value?.type, + handleEmptyResultFunction: value?.handleEmptyResultFunction, + handleErrorFunction: value?.handleErrorFunction, + handleNonMobileFallbackFunction: value?.handleNonMobileFallbackFunction + }, {emitEvent: false}); this.updateMobileActionType(value?.type, value); + this.getActionConfigs(); } private updateModel() { @@ -165,6 +182,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit handleErrorFunction = getDefaultHandleErrorFunction(type); this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false}); } + let handleNonMobileFallbackFunction = action?.handleNonMobileFallbackFunction; + const defaultHandleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction(); + if (defaultHandleNonMobileFallbackFunction !== handleNonMobileFallbackFunction) { + handleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction(); + this.mobileActionFormGroup.patchValue({handleNonMobileFallbackFunction}, {emitEvent: false}); + } } this.mobileActionTypeFormGroup = this.fb.group({}); if (type) { @@ -184,6 +207,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit 'processImageFunction', this.fb.control(processImageFunction, []) ); + this.mobileActionTypeFormGroup.addControl( + 'saveToGallery', + this.fb.control(action?.saveToGallery || false, []) + ); break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: @@ -268,6 +295,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit 'handleProvisionSuccessFunction', this.fb.control(handleProvisionSuccessFunction, [Validators.required]) ); + this.mobileActionTypeFormGroup.addControl( + 'provisionType', + this.fb.control(action?.provisionType || ProvisionType.auto, []) + ); } } this.mobileActionTypeFormGroup.valueChanges.pipe( @@ -277,5 +308,108 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit }); } + getActionConfigs() { + const type = this.mobileActionFormGroup.get('type').value; + this.actionConfig = []; + switch (type) { + case this.mobileActionType.deviceProvision: + this.actionConfig.push({ + title: 'widget-action.mobile.handle-provision-success-function', + formControlName: 'handleProvisionSuccessFunction', + functionName: 'handleProvisionSuccess', + functionArgs: ['deviceName', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'] + }); + break; + case this.mobileActionType.mapDirection: + case this.mobileActionType.mapLocation: + this.actionConfig.push({ + title: 'widget-action.mobile.get-location-function', + formControlName: 'getLocationFunction', + functionName: 'getLocation', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_get_location_fn' + }); + this.actionConfig.push({ + title: 'widget-action.mobile.process-launch-result-function', + formControlName: 'processLaunchResultFunction', + functionName: 'processLaunchResult', + functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_launch_result_fn' + }); + break; + case this.mobileActionType.makePhoneCall: + this.actionConfig.push({ + title: 'widget-action.mobile.get-phone-number-function', + formControlName: 'getPhoneNumberFunction', + functionName: 'getPhoneNumber', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_get_phone_number_fn' + }); + this.actionConfig.push({ + title: 'widget-action.mobile.process-launch-result-function', + formControlName: 'processLaunchResultFunction', + functionName: 'processLaunchResult', + functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_launch_result_fn' + }); + break; + case this.mobileActionType.takePhoto: + case this.mobileActionType.takePictureFromGallery: + case this.mobileActionType.takeScreenshot: + this.actionConfig.push({ + title: 'widget-action.mobile.process-image-function', + formControlName: 'processImageFunction', + functionName: 'processImage', + functionArgs: ['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_image_fn' + }); + break; + case this.mobileActionType.scanQrCode: + this.actionConfig.push({ + title: 'widget-action.mobile.process-qr-code-function', + formControlName: 'processQrCodeFunction', + functionName: 'processQrCode', + functionArgs: ['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_qr_code_fn' + }); + break; + case this.mobileActionType.getLocation: + this.actionConfig.push({ + title: 'widget-action.mobile.process-location-function', + formControlName: 'processLocationFunction', + functionName: 'processLocation', + functionArgs: ['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_location_fn' + }); + break; + } + } + + getCommonActionConfigs() { + this.commonActionConfig = [ + { + title: 'widget-action.mobile.handle-empty-result-function', + formControlName: 'handleEmptyResultFunction', + functionName: 'handleEmptyResult', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_handle_empty_result_fn' + }, + { + title: 'widget-action.mobile.handle-error-function', + formControlName: 'handleErrorFunction', + functionName: 'handleError', + functionArgs: ['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_handle_error_fn' + }, + { + title: 'widget-action.mobile.handle-non-mobile-fallback-function', + formControlName: 'handleNonMobileFallbackFunction', + functionName: 'handleNonMobileFallback', + functionArgs: ['$event', 'widgetContext'], + helpId: 'widget/action/mobile_handle_non_mobile_fallback_fn' + } + ]; + } + protected readonly WidgetActionType = WidgetActionType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts index b991a83576..c48407cd1e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts @@ -172,6 +172,14 @@ const handleErrorFunctionTemplate: TbFunction = ' }, 100);\n' + '}\n'; +const handleNonMobileFallbackFunctionTemplate: TbFunction = + '// Optional function body to handle non-mobile fallback \n' + + 'showFallbackToast();\n' + + '\n' + + 'function showFallbackToast(title, error) {\n' + + ' widgetContext.showWarnToast(\'This action is only available in the mobile application.\');\n' + + '}\n'; + const getLocationFunctionTemplate: TbFunction = '// Function body that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.\n' + '// Usually location can be obtained from entity attributes/telemetry. \n\n' + @@ -326,3 +334,5 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF } return handleErrorFunctionTemplate.replace('--TITLE--', title); }; + +export const getDefaultHandleNonMobileFallBackFunction = () => handleNonMobileFallbackFunctionTemplate; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw index cb8c23faee..05e06542a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw @@ -79,7 +79,7 @@ function AddEntityDialogController(instance) { const mapType = widgetContext.mapInstance.type(); attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); - } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { + } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line' ) { attributes.push({key: 'perimeter', value: additionalParams.coordinates}); } else if (mapItemType === 'Circle') { attributes.push({key: 'circle', value: additionalParams.coordinates}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html index 001d2817ab..3b30491f8c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html @@ -22,39 +22,44 @@
{{ 'widgets.value-action.action' | translate }}
- - {{ setValueActionTranslationsMap.get(action) | translate }} - + @for (action of setValueActions; track action) { + + {{ setValueActionTranslationsMap.get(action) | translate }} + + }
- - + @switch (setValueSettingsFormGroup.get('action').value) { + @case (setValueAction.EXECUTE_RPC) {
{{ 'widgets.value-action.method' | translate }}*
- - warning - + @if (setValueSettingsFormGroup.get('executeRpc').get('method').hasError('required') + && setValueSettingsFormGroup.get('executeRpc').get('method').touched) { + + warning + + }
-
- + } + @case (setValueAction.SET_ATTRIBUTE) {
{{ 'widgets.value-action.attribute-scope' | translate }}
- - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + }
@@ -73,8 +78,8 @@
- - + } + @case (setValueAction.ADD_TIME_SERIES) {
{{ 'widgets.value-action.time-series-key' | translate }}*
@@ -90,99 +95,113 @@
-
- + } + }
{{ (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC ? - 'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}
+ 'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}
- {{ 'widgets.value-action.converter-value' | translate }} - {{ 'widgets.value-action.converter-constant' | translate }} + @if (valueType !== ValueType.BOOLEAN) { + {{ 'widgets.value-action.converter-value' | translate }} + } + @if (valueType === ValueType.BOOLEAN) { + {{ 'widgets.value-action.converter-constant' | translate }} + } {{ 'widgets.value-action.converter-function' | translate }} - {{ 'widgets.value-action.converter-none' | translate }} + @if (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC) { + {{ 'widgets.value-action.converter-none' | translate }} + }
- - - - + formControlName="constantValue"> + + } + @if (setValueSettingsFormGroup.get('valueToData').get('type').value === valueToDataType.FUNCTION) { + + + }
-
- - - - - - widget-config.advanced-settings - - - -
-
{{ 'widgets.value-action.request-timeout-ms' | translate }}
- - - + + + + + + widget-config.advanced-settings + + + +
+
{{ 'widgets.value-action.request-timeout-ms' | translate }}
+ + + @if (setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && + setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid) { + - warning - - -
-
- - - - - {{ 'widgets.value-action.request-persistent' | translate }} - - - - -
-
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
- - - + warning + + } + +
+
+ + + + + {{ 'widgets.value-action.request-persistent' | translate }} + + + + +
+
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
+ + + @if (setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && + setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid) { + - warning - - -
-
-
-
-
-
-
+ class="tb-error"> + warning +
+ } +
+
+
+
+
+ + +
+ }
- - warning - - - - - - - -
- - -
-
{{ 'widget-action.URL' | translate }}
- - - - warning - - -
-
- -
- - {{ 'widget-action.open-right-layout' | translate }} - -
-
- -
- - {{ 'widget-action.open-new-browser-tab' | translate }} - -
-
- -
- - {{ 'widget-action.set-entity-from-widget' | translate }} - -
-
-
{{ 'alias.state-entity-parameter-name' | translate }}
- - - -
-
- -
-
{{ 'widget-action.state-display-type' | translate }}
- - - - {{ stateDisplayTypeName(displayType) }} - - - -
- - -
-
{{ 'widget-action.dialog-title' | translate }}
- - - -
-
- - {{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} - + } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? + widgetActionFormGroup.get('type').value : '') { +
+
+ {{ 'widget-action.target-dashboard-state' | translate }} + @if (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState) { + * + }
-
-
{{ 'widget-action.dialog-width' | translate }}
- - + + + @if (actionTypeFormGroup.get('targetDashboardStateId').value) { + + } + @if (actionTypeFormGroup.get('targetDashboardStateId').hasError('required') + && actionTypeFormGroup.get('targetDashboardStateId').touched) { warning - -
-
-
{{ 'widget-action.dialog-height' | translate }}
- - + } + + @for (state of filteredDashboardStates | async; track state) { + + + + } + + +
+ } + @case (widgetActionType.openURL) { +
+
{{ 'widget-action.URL' | translate }}
+ + + @if (actionTypeFormGroup.get('url').invalid && actionTypeFormGroup.get('url').touched) { warning - -
- - -
-
{{ 'widget-action.popover-preferred-placement' | translate }}
- - - - {{ popoverPlacementName(placement) }} - - - -
+ } + +
+ } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ? + widgetActionFormGroup.get('type').value : '') { +
+ + {{ 'widget-action.open-right-layout' | translate }} + +
+ } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboard || + widgetActionFormGroup.get('type').value === widgetActionType.openURL ? + widgetActionFormGroup.get('type').value : '') { +
+ + {{ 'widget-action.open-new-browser-tab' | translate }} + +
+ } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? + widgetActionFormGroup.get('type').value : '') { + @if (widgetType !== WidgetType.static) {
- - {{ 'widget-action.popover-hide-on-click-outside' | translate }} + + {{ 'widget-action.set-entity-from-widget' | translate }}
+ } + @if (actionTypeFormGroup.get('setEntityId').value) {
- - {{ 'widget-action.popover-hide-dashboard-toolbar' | translate }} - -
-
-
{{ 'widget-action.popover-width' | translate }}
- - -
-
-
{{ 'widget-action.popover-height' | translate }}
- - +
{{ 'alias.state-entity-parameter-name' | translate }}
+ + +
- - - - - - - - +
{{ 'widget-action.state-display-type' | translate }}
+ + + @for (displayType of allStateDisplayTypes; track displayType) { + + {{ stateDisplayTypeName(displayType) }} + + } + + +
+ + @switch (actionTypeFormGroup.get('stateDisplayType').value) { + @case ('separateDialog') { +
+
{{ 'widget-action.dialog-title' | translate }}
+ + + +
+
+ + {{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} + +
+
+
{{ 'widget-action.dialog-width' | translate }}
+ + + @if (stateDisplayTypeFormGroup.get('dialogWidth').invalid && stateDisplayTypeFormGroup.get('dialogWidth').touched) { + + warning + + } + +
+
+
{{ 'widget-action.dialog-height' | translate }}
+ + + @if (stateDisplayTypeFormGroup.get('dialogHeight').invalid && stateDisplayTypeFormGroup.get('dialogHeight').touched) { + + warning + + } + +
+ } + @case ('popover') { +
+
{{ 'widget-action.popover-preferred-placement' | translate }}
+ + + @for (placement of allPopoverPlacements; track placement) { + + {{ popoverPlacementName(placement) }} + + } + + +
+
+ + {{ 'widget-action.popover-hide-on-click-outside' | translate }} + +
+
+ + {{ 'widget-action.popover-hide-dashboard-toolbar' | translate }} + +
+
+
{{ 'widget-action.popover-width' | translate }}
+ + +
+
+
{{ 'widget-action.popover-height' | translate }}
+ + +
+ + } + @case ('normal') { + } + } +
+ } + @case (widgetActionType.custom) { + -
- -
-
{{ 'widget-action.map-item-type' | translate }}
- - - @for(type of mapItemTypes; track type) { - {{ mapItemTypeTranslationMap.get(type) | translate }} - } - - -
- - -
- - - - - - - - + > + } + @case (widgetActionType.placeMapItem) { +
+
{{ 'widget-action.map-item-type' | translate }}
+ + + @for(type of mapItemTypes; track type) { + {{ mapItemTypeTranslationMap.get(type) | translate }} + } + + +
+ + + } + @case (widgetActionFormGroup.get('type').value === widgetActionType.customPretty + || widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem + ? widgetActionFormGroup.get('type').value : '') { + + + } + @case (widgetActionType.mobileAction) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html index a1d2ff2243..03548a16f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html @@ -45,12 +45,14 @@ formControlName="color">
- + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html index 4acda6d6b4..255003caa4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html @@ -21,7 +21,9 @@ class="mat-block" [appearance]="inlineField ? 'outline' : appearance" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> - {{ 'entity.entity-alias' | translate }} + @if (showLabel && !inlineField) { + {{ 'entity.entity-alias' | translate }} + }
- - - - warning - + @if (selectEntityAliasFormGroup.get('entityAlias').value) { + + } + @if (selectEntityAliasFormGroup.get('entityAlias').value?.id) { + + } + @if (inlineField && !modelValue && tbRequired + && selectEntityAliasFormGroup.get('entityAlias').touched) { + + warning + + }
- + @if (!inlineField && !selectEntityAliasFormGroup.get('entityAlias').value && !disabled) { + + } - - - - -
-
- entity.no-aliases-found -
- + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + } + @if (!(filteredEntityAliases | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ entity.no-aliases-found +
+ } @else { + + {{ 'entity.no-alias-matching' | translate : {alias: (searchText | truncate: true: 6: '...')} }} + + } - {{ 'entity.no-alias-matching' | translate : {alias: (searchText | truncate: true: 6: '...')} }} + entity.create-new-alias - - - entity.create-new-alias - -
-
+
+
+ }
- - {{ 'entity.alias-required' | translate }} - + @if (!inlineField && !modelValue && tbRequired) { + + {{ 'entity.alias-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts index 0b2cacd6da..561541981f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts @@ -65,6 +65,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, callbacks: EntityAliasSelectCallbacks; @Input() + @coerceBoolean() showLabel: boolean; @ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html index 16cb7a8e19..9964e6b375 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html @@ -18,17 +18,19 @@
date.time-granularity-formats
-
-
{{ formatTimeUnitTranslations.get(unit) | translate }}
-
- - -
-
- -
{{ previewText[unit] }}
+ @for (unit of formatTimeUnits; track unit) { +
+
{{ formatTimeUnitTranslations.get(unit) | translate }}
+
+ + +
+
+ +
{{ previewText[unit] }}
+
-
+ }
- + @if (copyFromStates?.length) { + + }
- + @for (state of copyFromStates; track state) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html index 4c4544467c..f1bd3f92d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html @@ -27,14 +27,16 @@ [activated]="state === widgetButtonState.activated" [disabled]="state === widgetButtonState.disabled"> - + @if (modelValue) { + + }
- + @if (copyFromStates?.length) { + + }
- + @for (state of copyFromStates; track state) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html index f750ffbd19..ba2c744211 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html @@ -26,14 +26,16 @@ [hovered]="state === widgetButtonToggleState.hovered" [disabled]="state === widgetButtonToggleState.disabled"> - + @if (modelValue) { + + }
- - {{ easing }} - + @for (easing of chartAnimationEasings; track easing) { + + {{ easing }} + + }
@@ -69,9 +71,11 @@
widgets.chart.animation.update-animation-easing
- - {{ easing }} - + @for (easing of chartAnimationEasings; track easing) { + + {{ easing }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.html index dbfaa2a8fc..6ccc5d6834 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.html @@ -33,13 +33,15 @@
-
-
widgets.chart.bar.bar-width
- - -
%
-
-
+ @if (!series) { +
+
widgets.chart.bar.bar-width
+ + +
%
+
+
+ }
@@ -49,9 +51,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + }
{{ titleText | translate }}
- {{ chartFillTypeTranslationMap.get(type) | translate }} + @for (type of chartFillTypes; track type) { + {{ chartFillTypeTranslationMap.get(type) | translate }} + }
- + @if (fillSettingsFormGroup.get('type').value === ChartFillType.opacity) {
widgets.chart.opacity
@@ -31,23 +33,25 @@ step="0.1" placeholder="{{ 'widget-config.set' | translate }}"/>
-
- -
-
widgets.chart.gradient-stops
-
-
widgets.chart.gradient-start
- - - -
widgets.chart.gradient-end
- - - + } + @if (fillSettingsFormGroup.get('type').value === ChartFillType.gradient) { + +
+
widgets.chart.gradient-stops
+
+
widgets.chart.gradient-start
+ + + +
widgets.chart.gradient-end
+ + + +
-
- + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html index 380ffcd604..e500157c64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html @@ -19,6 +19,9 @@
{{ panelTitle }}
widgets.time-series-chart.axis.position
- - {{ timeSeriesAxisPositionTranslations.get(position) | translate }} - + @for (position of axisPositions; track position) { + + {{ timeSeriesAxisPositionTranslations.get(position) | translate }} + + }
-
-
widget-config.units-short
- - -
-
-
widget-config.decimals-short
- - - -
-
- - -
+ @if (axisType === 'yAxis' && !hideUnits) { +
+
widget-config.units-short
+ + +
+ } + @if (axisType === 'yAxis' && !hideDecimals) { +
+
widget-config.decimals-short
+ + + +
+ } + @if (axisType === 'yAxis' && advanced) { +
+ + +
+ }
widgets.time-series-chart.axis.tick-labels
- - + @if (axisType === 'xAxis') { + + + }
-
- - -
+ @if (axisType === 'yAxis' && advanced) { +
+ + +
+ }
@@ -139,42 +151,62 @@ formControlName="splitLinesColor">
-
-
- {{ 'widgets.time-series-chart.axis.ticks-interval' | translate }} + @if (axisType === 'yAxis') { +
+
+ {{ 'widgets.time-series-chart.axis.ticks-interval' | translate }} +
+ + +
- - - -
-
-
- {{ 'widgets.time-series-chart.axis.split-number' | translate }} + } + @if (axisType === 'yAxis') { +
+
+ {{ 'widgets.time-series-chart.axis.split-number' | translate }} +
+ + +
- - - -
+ } + @if (axisType === 'yAxis') { +
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
+
+ } -
-
-
widgets.chart.chart-axis.scale
-
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - - -
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 1d7a4a2c37..2f23666574 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -17,13 +17,15 @@ import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, + NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup, + ValidationErrors, Validator, Validators } from '@angular/forms'; import { - AxisPosition, defaultXAxisTicksFormat, + AxisPosition, defaultXAxisTicksFormat, normalizeAxisLimit, timeSeriesAxisPositionTranslations, TimeSeriesChartAxisSettings, TimeSeriesChartXAxisSettings, TimeSeriesChartYAxisSettings @@ -32,6 +34,9 @@ import { merge } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { Datasource } from '@app/shared/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; @Component({ selector: 'tb-time-series-chart-axis-settings', @@ -42,11 +47,16 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), + multi: true } ], standalone: false }) -export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { +export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor, Validator { @Input() @coerceBoolean() @@ -62,6 +72,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu defaultXAxisTicksFormat = defaultXAxisTicksFormat; + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; @@ -119,8 +138,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu this.axisSettingsFormGroup.addControl('ticksGenerator', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('interval', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('splitNumber', this.fb.control(null, [Validators.min(1)])); - this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); - this.axisSettingsFormGroup.addControl('max', this.fb.control(null, [])); + this.axisSettingsFormGroup.addControl('min', this.fb.control(normalizeAxisLimit(null), [])); + this.axisSettingsFormGroup.addControl('max', this.fb.control(normalizeAxisLimit(null), [])); } else if (this.axisType === 'xAxis') { this.axisSettingsFormGroup.addControl('ticksFormat', this.fb.control(null, [])); } @@ -148,6 +167,12 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu registerOnTouched(_fn: any): void { } + validate(): ValidationErrors | null { + return this.axisSettingsFormGroup.valid ? null : { + axisSettings: false + }; + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html index bde8d623d4..8027f4d689 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html @@ -24,31 +24,36 @@ - - {{ timeSeriesStateSourceTypeTranslations.get(type) | translate }} - + @for (type of timeSeriesStateSourceTypes; track type) { + + {{ timeSeriesStateSourceTypeTranslations.get(type) | translate }} + + }
- - - + } + @if (stateFormGroup.get('sourceType').value === TimeSeriesChartStateSourceType.range) { + - - - + + } + @if (stateFormGroup.get('sourceType').value === TimeSeriesChartStateSourceType.range) { + - - + + + }
-
-
- - - + @if (statesFormArray().controls.length) { +
+ @for (stateControl of statesFormArray().controls; track trackByState($index, stateControl)) { +
+ + + @if (!$last) { + + } +
+ }
-
+ } @else { + {{ 'widgets.time-series-chart.state.no-states' | translate }} + }
- - {{ 'widgets.time-series-chart.state.no-states' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html index b603a8ddaa..30ec805710 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html @@ -19,69 +19,81 @@
- - {{ timeSeriesThresholdTypeTranslations.get(type) | translate }} - + @for (type of timeSeriesThresholdTypes; track type) { + + {{ timeSeriesThresholdTypeTranslations.get(type) | translate }} + + } - - + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.entity) { + + + }
- - - - warning - - - - - - -
-
- - - - {{ yAxis }} - - - + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.constant) { + + + @if (thresholdFormGroup.get('value').hasError('required') + && thresholdFormGroup.get('value').touched) { + + warning + + } + + } + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.latestKey) { + + + } + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.entity) { + + + }
+ @if (!hideYAxis) { +
+ + + @for (yAxis of yAxisIds; track yAxis) { + + {{ yAxis }} + + } + + +
+ }
+ [widgetConfig]="widgetConfig" + [yAxisIds]="yAxisIds" + [hideYAxis]="hideYAxis" + icon="settings" + [formControl]="thresholdSettingsFormControl"> - +@if (boxButton) { + +} @else { - +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html index 448a31fec0..c2dd3ffab4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html @@ -21,24 +21,34 @@
widgets.time-series-chart.threshold.source
widgets.time-series-chart.threshold.key-value
-
widgets.time-series-chart.axis.y-axis
+ @if (!hideYAxis) { +
widgets.time-series-chart.axis.y-axis
+ }
widgets.color.color
widget-config.units-short
widget-config.decimals-short
-
-
- - - + @if (thresholdsFormArray().controls.length) { +
+ @for (thresholdControl of thresholdsFormArray().controls; track trackByThreshold($index, thresholdControl)) { +
+ + + @if (!$last) { + + } +
+ }
-
+ } @else { + {{ 'widgets.time-series-chart.threshold.no-thresholds' | translate }} + }
- - {{ 'widgets.time-series-chart.threshold.no-thresholds' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html index 0272dd60d2..86c54762c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html @@ -33,27 +33,29 @@ cdkDropList cdkDropListOrientation="vertical" [cdkDropListDisabled]="!dragEnabled" (cdkDropListDropped)="axisDrop($event)"> -
- - -
- + @for (axisControl of axesFormArray().controls; track trackByAxis($index, axisControl)) { +
+ + +
+ +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index de7b19deba..662214a1df 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -36,17 +36,23 @@ import { Validator } from '@angular/forms'; import { + checkLatestDataKeys, defaultTimeSeriesChartYAxisSettings, getNextTimeSeriesYAxisId, - TimeSeriesChartYAxes, TimeSeriesChartYAxisId, + TimeSeriesChartYAxes, + TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, timeSeriesChartYAxisValid, - timeSeriesChartYAxisValidator + timeSeriesChartYAxisValidator, + updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { mergeDeep } from '@core/utils'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; +import { Datasource } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -69,6 +75,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }) export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator { + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; @@ -114,6 +129,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, for (const axis of axes) { yAxes[axis.id] = axis; } + updateLatestDataKeys(Object.values(yAxes), this.datasource, this.dataKeyCallbacks); this.propagateChange(yAxes); } ); @@ -136,7 +152,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, } writeValue(value: TimeSeriesChartYAxes | undefined): void { - const yAxes: TimeSeriesChartYAxes = value || {}; + const yAxes: TimeSeriesChartYAxes = checkLatestDataKeys(value || {}, this.datasource); if (!yAxes.default) { yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); @@ -195,4 +211,5 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, }); return this.fb.array(axesControls); } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html index 7194fd36a9..ceb34f83c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -25,19 +25,21 @@ - - {{ timeSeriesAxisPositionTranslations.get(position) | translate }} - + @for (position of axisPositions; track position) { + + {{ timeSeriesAxisPositionTranslations.get(position) | translate }} + + } -
+
- +
-
+
- +
@@ -59,14 +61,16 @@ settings
- + @if (modelValue.id !== 'default') { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 42602d902f..93e91124d5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -29,7 +29,7 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { - AxisPosition, + AxisPosition, normalizeAxisLimit, timeSeriesAxisPositionTranslations, TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; @@ -42,6 +42,8 @@ import { import { deepClone } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { TimeSeriesChartYAxesPanelComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -86,6 +88,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O constructor(private fb: UntypedFormBuilder, private translate: TranslateService, private popoverService: TbPopoverService, + private timeSeriesChartYAxesPanel: TimeSeriesChartYAxesPanelComponent, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private cd: ChangeDetectorRef, @@ -98,8 +101,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O position: [null, []], units: [null, []], decimals: [null, []], - min: [null, []], - max: [null, []], + min: this.createLimitFormGroup(), + max: this.createLimitFormGroup(), show: [null, []] }); this.axisFormGroup.valueChanges.pipe( @@ -133,17 +136,19 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O writeValue(value: TimeSeriesChartYAxisSettings): void { this.modelValue = value; - this.axisFormGroup.patchValue( - { - label: value.label, - position: value.position, - units: value.units, - decimals: value.decimals, - min: value.min, - max: value.max, - show: value.show, - }, {emitEvent: false} - ); + const min = normalizeAxisLimit(value.min); + const max = normalizeAxisLimit(value.max); + + this.axisFormGroup.patchValue({ + label: value.label, + position: value.position, + units: value.units, + decimals: value.decimals, + min, + max, + show: value.show, + }, { emitEvent: false }); + this.updateValidators(); this.cd.markForCheck(); } @@ -166,7 +171,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O axisType: 'yAxis', panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), axisSettings: deepClone(this.modelValue), - advanced: this.advanced + advanced: this.advanced, + aliasController: this.timeSeriesChartYAxesPanel.aliasController, + dataKeyCallbacks: this.timeSeriesChartYAxesPanel.dataKeyCallbacks, + datasource: this.timeSeriesChartYAxesPanel.datasource }, isModal: true }); @@ -191,6 +199,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } } + checkIsConstantLimit(limit: 'min' | 'max') { + return this.axisFormGroup.get(`${limit}.type`)?.value === ValueSourceType.constant; + } + private updateValidators() { const show: boolean = this.axisFormGroup.get('show').value; if (show) { @@ -204,10 +216,21 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.axisFormGroup.get('units').disable({emitEvent: false}); this.axisFormGroup.get('decimals').disable({emitEvent: false}); } + if(!this.checkIsConstantLimit('min')){ + this.axisFormGroup.get('min').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('min').enable({emitEvent: false}); + } + if(!this.checkIsConstantLimit('max')){ + this.axisFormGroup.get('max').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('max').enable({emitEvent: false}); + } + } private updateModel() { - const value = this.axisFormGroup.value; + const value = this.axisFormGroup.getRawValue(); this.modelValue.label = value.label; this.modelValue.position = value.position; this.modelValue.units = value.units; @@ -217,4 +240,16 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.modelValue.show = value.show; this.propagateChange(this.modelValue); } + + private createLimitFormGroup() { + return this.fb.group({ + type: [ValueSourceType.constant, []], + value: [null, []], + latestKey: [null, []], + latestKeyType: [null, []], + entityAlias: [null, []], + entityKey: [null, []], + entityKeyType: [null, []] + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html index eb3f84eb0e..58e1a108f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html @@ -19,12 +19,14 @@
widgets.time-series-chart.no-aggregation-bar-width-strategy
- - {{ timeSeriesChartNoAggregationBarWidthStrategyTranslations.get(strategy) | translate }} - + @for (strategy of timeSeriesChartNoAggregationBarWidthStrategies; track strategy) { + + {{ timeSeriesChartNoAggregationBarWidthStrategyTranslations.get(strategy) | translate }} + + }
- + @if (barWidthSettingsFormGroup.get('strategy').value === TimeSeriesChartNoAggregationBarWidthStrategy.group) { - - + } + @if (barWidthSettingsFormGroup.get('strategy').value === TimeSeriesChartNoAggregationBarWidthStrategy.separate) { - + }
@@ -51,8 +53,8 @@ {{ (formGroup.get('relative').value ? - 'widgets.time-series-chart.bar-width-relative' : - 'widgets.time-series-chart.bar-width-absolute') | translate }} + 'widgets.time-series-chart.bar-width-relative' : + 'widgets.time-series-chart.bar-width-absolute') | translate }} {{ 'widgets.time-series-chart.bar-width-relative' | translate }} @@ -62,14 +64,18 @@ - - - % - - - - ms - + @if (formGroup.get('relative').value) { + + + % + + } + @if (!formGroup.get('relative').value) { + + + ms + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html index 83aee39785..d92497ed52 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html @@ -16,94 +16,96 @@ --> -
-
widgets.color.settings-type
- - - {{ 'widgets.color.basic-mode' | translate }} - - - {{ 'widgets.color.advanced-mode' | translate}} - - -
- - + @if (advancedMode) {
-
{{ panelTitle }}
+
widgets.color.settings-type
+ + + {{ 'widgets.color.basic-mode' | translate }} + + + {{ 'widgets.color.advanced-mode' | translate}} + +
-
- -
- -
-
-
-
-
widgets.color.from
- - - + } @else { +
+ @if (panelTitle) { +
{{ panelTitle }}
+ } +
+ } -
widgets.color.to
- - - - - +
+ @switch (colorRangeListFormGroup.get('advancedMode').value) { + @case (false) { +
+ @for (rangeFormGroup of rangeListFormGroups; track trackByRange($index, rangeFormGroup); let isLast = $last) { +
+
+
+
widgets.color.from
+ + + +
widgets.color.to
+ + + + + +
+ +
+
- -
- + }
-
- - - -
-
- - - + } + @case (true) { +
+ @for (advancedRangeControl of advancedRangeControls; track trackByAdvancedRange($index, advancedRangeControl); let isLast = $last) { +
+ + + +
+ }
-
- - - + } + }
- - - -
- -
-
-
- - - + } + + +
+ @for (comp of settingsComponents; track comp) { + + } +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html index cf7d3d051f..2c69cf1fe6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html @@ -19,10 +19,12 @@
widgets.color.color-settings
- - {{ colorTypeTranslationsMap.get(type) | translate }} - + @for (type of colorTypes; track type) { + + {{ colorTypeTranslationsMap.get(type) | translate }} + + }
@@ -60,19 +62,23 @@
- + @if (settingsComponents?.length) { + + }
- + @for (comp of settingsComponents; track comp) { + + }
@@ -94,7 +100,7 @@
-
+
- mdi:function-variant + @if (modelValue?.type === colorType.function) { + mdi:function-variant + } @else { +
+
+
+ } - -
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts index 9a9ba27ce0..e4744b8918 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts @@ -165,7 +165,7 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD renderer: this.renderer, componentType: ColorSettingsPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: 'left', + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], context: { colorSettings: this.modelValue, settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this), diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html index 40db32d978..993bea21a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html @@ -21,12 +21,14 @@ breakpoints: { 'lt-md': 1 }}" - label="{{ 'widgets.count.layout' | translate }}" formControlName="layout"> - - {{ countCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.count.layout' | translate }}" formControlName="layout"> + @for (layout of countCardLayouts; track layout) { + + {{ countCardLayoutTranslationMap.get(layout) | translate }} + + }
@@ -38,9 +40,22 @@ {{ 'widgets.count.label' | translate }}
- - - + @if (!predefinedValues) { + + + + }@else { + + } @@ -63,7 +78,7 @@ [backgroundColor]="countWidgetConfigForm.get('iconBackgroundColor').enabled ? countWidgetConfigForm.get('iconBackgroundColor').value?.color : ''" - formControlName="icon"> + formControlName="icon"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts index 79689174c0..b78da31004 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts @@ -62,6 +62,9 @@ export class CountWidgetSettingsComponent extends PageComponent implements OnIni @Input() alarmElseEntity: boolean; + @Input() + predefinedValues: string[]; + private propagateChange = null; countCardLayouts = countCardLayouts; @@ -70,7 +73,6 @@ export class CountWidgetSettingsComponent extends PageComponent implements OnIni countCardLayoutImageMap: Map; countWidgetConfigForm: UntypedFormGroup; - constructor(protected store: Store, private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html index 1881fa7336..008f63544d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html @@ -19,16 +19,17 @@ - - warning - + class="tb-error"> + warning + + } + [width]="flex ? '100%' : ''" formControlName="unit">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html index 3ca5657b2a..61d1d54684 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html @@ -17,8 +17,12 @@ --> - - - {{ cssUnit }} + @if (allowEmpty) { + + + } + @for (cssUnit of cssUnitsList; track cssUnit) { + {{ cssUnit }} + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html index 710d44774c..fbdb34748a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html @@ -17,15 +17,20 @@ --> - {{ dateFormatDisplayValue(dateFormat) }} + @for (dateFormat of dateFormatList; track dateFormat) { + {{ dateFormatDisplayValue(dateFormat) }} + } - - + @if (dateFormatFormControl.value?.custom) { + + } + @if (dateFormatFormControl.value?.auto && !dateFormatFormControl.value?.custom) { + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html index c7ece2becf..84aa13bccb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html @@ -23,52 +23,59 @@ -
-
-
-
{{$index + 1}}
-
- - -
- - -
+ @if (propertiesFormArray().controls.length) { +
+ @for (propertyControl of propertiesFormArray().controls; track trackByProperty($index, propertyControl)) { +
+
+
{{$index + 1}}
+
+ + +
+ @if (!disabled) { + + } + @if (!disabled) { + + } +
+
+ }
-
-
- -
+ } @else { + {{ 'dynamic-form.property.no-items' | translate }} + } + @if (!disabled || !propertiesFormArray().controls.length) { +
+ +
+ }
- - {{ 'dynamic-form.property.no-items' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html index 885fa20ee4..062f300d6b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html @@ -22,72 +22,83 @@
dynamic-form.property.name
dynamic-form.property.type
- - - -
-
-
-
- - -
+ @if (!disabled && importExport) { + + } + @if (!disabled && importExport) { -
+ } + @if (!disabled) { + + }
- -
-
- + @if (propertiesFormArray().controls.length) { +
+ @for (propertyControl of propertiesFormArray().controls; track trackByProperty($index, propertyControl)) { +
+ + +
+ @if (!disabled) { + + } +
+
+ } +
+ } @else { + {{ 'dynamic-form.property.no-properties' | translate }} + } + @if (errorText) { + + }
+ @if (!disabled) { +
+ +
+ }
- - {{ 'dynamic-form.property.no-properties' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html index 334ace9886..9a26856883 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html @@ -46,42 +46,50 @@
dynamic-form.property.type
- - {{ formPropertyTypeTranslations.get(type) | translate }} - + @for (type of formPropertyTypes; track type) { + + {{ formPropertyTypeTranslations.get(type) | translate }} + + }
-
-
dynamic-form.property.array-item
-
-
dynamic-form.property.item-type
- - - - {{ formPropertyTypeTranslations.get(type) | translate }} - - - -
-
-
dynamic-form.property.item-name
- - - + @if (isArray) { +
+
dynamic-form.property.array-item
+
+
dynamic-form.property.item-type
+ + + @for (type of arrayItemFormPropertyTypes; track type) { + + {{ formPropertyTypeTranslations.get(type) | translate }} + + } + + +
+
+
dynamic-form.property.item-name
+ + + +
+ +
+ } + @if (!isArray) { -
- - - - -
- - {{ 'dynamic-form.property.value-required' | translate }} - -
+ } + @if (![FormPropertyType.fieldset, FormPropertyType.htmlSection, FormPropertyType.array].includes(propertyFormGroup.get('type').value)) { +
+ + {{ 'dynamic-form.property.value-required' | translate }} + +
+ }
@@ -90,7 +98,7 @@ - + @if (isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)) {
dynamic-form.property.sub-label
-
-
-
dynamic-form.property.input-field-suffix
- - - -
-
-
dynamic-form.property.disable-on-property
- - - {{ 'dynamic-form.property.disable-on-property-none' | translate }} - - {{ prop }} - - - -
+ } + @if ([FormPropertyType.number, FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)) { +
+
dynamic-form.property.input-field-suffix
+ + + +
+ } + @if (propertyFormGroup.get('type').value !== FormPropertyType.htmlSection) { +
+
dynamic-form.property.disable-on-property
+ + + {{ 'dynamic-form.property.disable-on-property-none' | translate }} + @for (prop of booleanPropertyIds; track prop) { + + {{ prop }} + + } + + +
+ }
- - - - + @if (isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)) { + + + } + @if (isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value) && FormPropertyType.switch !== propertyFormGroup.get('type').value) { + + + }
@@ -164,154 +182,174 @@ (click)="cancel()"> {{ (disabled ? 'action.close' : 'action.cancel') | translate }} - + @if (!disabled) { + + }
-
-
dynamic-form.property.textarea-rows
- - - -
-
-
dynamic-form.property.number-settings
-
-
dynamic-form.property.min
- - - - -
dynamic-form.property.max
- - - - -
dynamic-form.property.step
- - +
dynamic-form.property.textarea-rows
+ +
-
-
- - - - {{ 'dynamic-form.property.properties' | translate }} - - - - - - - -
-
- - - - {{ 'dynamic-form.property.html-section-settings' | translate }} - - - -
- - -
- - -
-
-
- + } + @if (propertyItemType === FormPropertyType.number) { +
+
dynamic-form.property.number-settings
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + + +
dynamic-form.property.step
+ + + +
+
+ } + @if (propertyItemType === FormPropertyType.fieldset) {
- {{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }} + {{ 'dynamic-form.property.properties' | translate }} -
-
dynamic-form.property.buttons-direction
- - {{ 'dynamic-form.property.direction-column' | translate }} - {{ 'dynamic-form.property.direction-row' | translate }} - -
-
- - {{ 'dynamic-form.property.enable-multiple-select' | translate }} - -
-
- - {{ 'dynamic-form.property.allow-empty-select-option' | translate }} - + + + + +
+ } + @if (propertyItemType === FormPropertyType.htmlSection) { +
+ + + + {{ 'dynamic-form.property.html-section-settings' | translate }} + + + +
+ +
-
-
dynamic-form.property.selected-options-limit
-
-
dynamic-form.property.min
- - - - -
dynamic-form.property.max
- - - + + + + +
+ } + @if ([FormPropertyType.select, FormPropertyType.radios].includes(propertyItemType)) { +
+ + + + {{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }} + + + + @if (propertyItemType === FormPropertyType.radios) { +
+
dynamic-form.property.buttons-direction
+ + {{ 'dynamic-form.property.direction-column' | translate }} + {{ 'dynamic-form.property.direction-row' | translate }} +
-
+ } + @if (propertyItemType === FormPropertyType.select) { +
+ + {{ 'dynamic-form.property.enable-multiple-select' | translate }} + +
+ } + @if (propertyItemType === FormPropertyType.select && !propertyFormGroup.get('multiple').value) { +
+ + {{ 'dynamic-form.property.allow-empty-select-option' | translate }} + +
+ } + @if (propertyItemType === FormPropertyType.select && propertyFormGroup.get('multiple').value) { +
+
dynamic-form.property.selected-options-limit
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + +
+
+ }
- -
-
dynamic-form.property.help-id
- - - -
- + } + @if ([FormPropertyType.javascript, FormPropertyType.markdown].includes(propertyItemType)) { +
+
dynamic-form.property.help-id
+ + + +
+ } + @if (propertyItemType === FormPropertyType.datetime) {
dynamic-form.property.datetime-type
{{ 'dynamic-form.property.datetime-type-date' | translate }} + value="date">{{ 'dynamic-form.property.datetime-type-date' | translate }} {{ 'dynamic-form.property.datetime-type-time' | translate }} + value="time">{{ 'dynamic-form.property.datetime-type-time' | translate }} {{ 'dynamic-form.property.datetime-type-datetime' | translate }} + value="datetime">{{ 'dynamic-form.property.datetime-type-datetime' | translate }}
@@ -319,165 +357,221 @@ {{ 'dynamic-form.property.enable-clear-button' | translate }}
-
-
- - dynamic-form.property.default-value - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
dynamic-form.property.default-value
- - -
{{ item.label | customTranslate }}
-
-
-
-
- - {{ 'dynamic-form.property.support-unit-conversion' | translate }} - -
-
-
dynamic-form.property.default-value
- - - - - - - - - - - - - - - - - - - - - {{ item.label | customTranslate }} - - - - - {{ item.label | customTranslate }} - - - - - - - - -
+ } + @if (propertyItemType === FormPropertyType.textarea) { +
+ + dynamic-form.property.default-value + + +
+ } + @if (propertyItemType === FormPropertyType.javascript) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.json) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.html) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.css) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.markdown) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.image && !isArray) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.radios) { +
+
dynamic-form.property.default-value
+ + @for (item of propertyFormGroup.get('items').value; track item) { + +
{{ item.label | customTranslate }}
+
+ } +
+
+ } + @if (propertyItemType === FormPropertyType.units) { +
+ + {{ 'dynamic-form.property.support-unit-conversion' | translate }} + +
+ } + @if (isPropertyTypeAllowedForRow(propertyItemType)) { +
+
dynamic-form.property.default-value
+ @if ([FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)) { + + + + } + @if (propertyItemType === FormPropertyType.color) { + + + } + @if (propertyItemType === FormPropertyType.color_settings) { + + + } + @if (propertyItemType === FormPropertyType.number) { + + + + } + @if (propertyItemType === FormPropertyType.font) { + + + } + @if (propertyItemType === FormPropertyType.units) { + + } + @if (propertyItemType === FormPropertyType.switch) { + + } + @if (propertyItemType === FormPropertyType.icon) { + + + } + @if (propertyItemType === FormPropertyType.select) { + + @if (!propertyFormGroup.get('multiple').value) { + + @if (propertyFormGroup.get('allowEmptyOption').value) { + + } + @for (item of propertyFormGroup.get('items').value; track item) { + + {{ item.label | customTranslate }} + + } + + } + @if (propertyFormGroup.get('multiple').value) { + + @for (item of propertyFormGroup.get('items').value; track item) { + + {{ item.label | customTranslate }} + + } + + } + + } + @if (propertyItemType === FormPropertyType.datetime) { + @if (propertyFormGroup.get('dateTimeType').value === 'date') { + + } + @if (propertyFormGroup.get('dateTimeType').value === 'time') { + + } + @if (propertyFormGroup.get('dateTimeType').value === 'datetime') { + + } + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html index eae448d2a5..1d14969e74 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html @@ -24,9 +24,11 @@ - - {{ formPropertyTypeTranslations.get(type) | translate }} - + @for (type of formPropertyTypes; track type) { + + {{ formPropertyTypeTranslations.get(type) | translate }} + + }
@@ -38,13 +40,15 @@ matTooltipPosition="above"> settings - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html index a15c256745..bcff604829 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html @@ -25,13 +25,15 @@
- + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html index cfb179fe4d..f0096cff5c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html @@ -22,41 +22,49 @@
dynamic-form.property.label
-
-
- - -
- -
+ @if (selectItemsFormArray().controls.length) { +
+ @for (selectItemControl of selectItemsFormArray().controls; track trackBySelectItem($index, selectItemControl)) { +
+ + +
+ @if (!disabled) { + + } +
+
+ }
-
- -
-
- + } @else { + {{ 'dynamic-form.property.no-options' | translate }} + } + @if (errorText) { + + }
+ @if (!disabled) { +
+ +
+ }
- - {{ 'dynamic-form.property.no-options' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html index cb1cb71750..c8014dabef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html @@ -18,66 +18,78 @@
-
{{ title }}
+ @if (title) { +
{{ title }}
+ } - - -
- - - -
{{ propertyGroup.title | customTranslate }}
-
-
- - - -
-
- + @for (propertyGroup of propertyGroups; track propertyGroup) { + @if (propertyGroup.visible) { + @if (propertyGroup.title) { +
+ + + +
{{ propertyGroup.title | customTranslate }}
+
+
+ + + +
+
+ } + @if (!propertyGroup.title) { -
-
-
+ } + } + }
- - - - - - - - - - - - - -
- + @if (propertyContainer.visible) { + @if (propertyContainer.type === FormPropertyContainerType.field) { + + + } + @if (propertyContainer.type === FormPropertyContainerType.row) { + + + } + @if (propertyContainer.type === FormPropertyContainerType.fieldset) { + - -
- - -
- -
-
+ title="{{ propertyContainer.label | customTranslate }}" + stroked + [properties]="propertyContainer.properties"> + + } + @if (propertyContainer.type === FormPropertyContainerType.array) { + @if (propertyContainer.arrayItemProperty.type === FormPropertyType.image) { +
+ + +
+ } + @if (propertyContainer.arrayItemProperty.type !== FormPropertyType.image) { + + + } + } + @if (propertyContainer.type === FormPropertyContainerType.htmlSection) { +
+ } + }
-
+ }
@@ -89,185 +101,247 @@
- -
- {{ propertyRow.label | customTranslate }} + @if (propertyRow.switch && propertyRow.switch.visible) { + +
+ {{ propertyRow.label | customTranslate }} +
+
+ } + @if (!propertyRow.switch) { +
+
+ {{ propertyRow.label | customTranslate }} +
- -
-
- {{ propertyRow.label | customTranslate }} + } + @if (propertyRow.properties.length) { +
+ @for (property of propertyRow.properties; track property) { + @if (property.visible) { + @if (property.subLabel) { +
{{ property.subLabel | customTranslate }}
+ } + @if (isInputFieldPropertyType(property.type)) { + + + } + @if (property.type === FormPropertyType.color) { + + + } + @if (property.type === FormPropertyType.color_settings) { + + + } + @if (property.type === FormPropertyType.font) { + + + } + @if (property.type === FormPropertyType.units) { + + } + @if (property.type === FormPropertyType.icon) { + + + } + @if (property.divider) { + + } + } + }
-
-
- - -
{{ property.subLabel | customTranslate }}
- - - - - - - - - - - - - - -
-
-
+ }
- - {{ property.name | customTranslate }} - - - - {{ property.name | customTranslate }} - -
{{ property.fieldSuffix | customTranslate }}
-
- - {{ property.name | customTranslate }} - -
{{ property.fieldSuffix | customTranslate }}
-
- - {{ property.name | customTranslate }} - + @if (singleField) { + {{ property.name | customTranslate }} + } + + + } + @if ([FormPropertyType.text, FormPropertyType.password].includes(property.type)) { + + @if (singleField) { + {{ property.name | customTranslate }} + } + + @if (property.fieldSuffix) { +
{{ property.fieldSuffix | customTranslate }}
+ } +
+ } + @if (property.type === FormPropertyType.number) { + + @if (singleField) { + {{ property.name | customTranslate }} + } + + @if (property.fieldSuffix) { +
{{ property.fieldSuffix | customTranslate }}
+ } +
+ } + @if (property.type === FormPropertyType.select) { + + @if (singleField) { + {{ property.name | customTranslate }} + } + + @if (!property.multiple && property.allowEmptyOption) { + + } + @for (item of property.items; track item) { + + {{ item.label | customTranslate }} + + } + + + } + @if (property.type === FormPropertyType.datetime) { + + + } + @if (property.type === FormPropertyType.javascript) { +
+ + +
+ } + @if (property.type === FormPropertyType.json) { +
+ + +
+ } + @if (property.type === FormPropertyType.html) { +
+ - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
{{ property.name | customTranslate }}
- - -
{{ item.label | customTranslate }}
-
-
-
+
+
+ } + @if (property.type === FormPropertyType.css) { +
+ + +
+ } + @if (property.type === FormPropertyType.markdown) { +
+ + +
+ } + @if (property.type === FormPropertyType.image) { +
+ + +
+ } + @if (property.type === FormPropertyType.radios) { +
+
{{ property.name | customTranslate }}
+ + @for (item of property.items; track item) { + +
{{ item.label | customTranslate }}
+
+ } +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html index 329a6aaf97..c11e528911 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html @@ -24,26 +24,31 @@ [formControl]="entityAliasFormControl" (focusin)="onEntityAliasFocus()" [matAutocomplete]="entityAliasAutocomplete"> - - warning - - + @if (required && entityAliasFormControl.hasError('required') + && entityAliasFormControl.touched) { + + warning + + } + @if (entityAliasFormControl.value && !entityAliasFormControl.disabled) { + + } - - - + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html index 9ca50c55f0..326832d34e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html @@ -21,7 +21,9 @@ class="mat-block" [appearance]="inlineField ? 'outline' : appearance" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> - {{ 'filter.filter' | translate }} + @if (showLabel && !inlineField) { + {{ 'filter.filter' | translate }} + }
- - - warning - + @if (selectFilterFormGroup.get('filter').value && !disabled) { + + } + @if (inlineField && !modelValue && tbRequired + && selectFilterFormGroup.get('filter').touched) { + + warning + + }
- + @if (!inlineField && !selectFilterFormGroup.get('filter').value && !disabled) { + + } - - - - -
-
- filter.no-filters-found -
- + @for (filter of filteredFilters | async; track filter) { + + + + } + @if (!(filteredFilters | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ filter.no-filters-found +
+ } @else { + + {{ 'filter.no-filter-matching' | translate : {filter: (searchText | truncate: true: 6: '...')} }} + + } - {{ 'filter.no-filter-matching' | translate : {filter: (searchText | truncate: true: 6: '...')} }} + filter.create-new-filter - - - filter.create-new-filter - -
-
+
+
+ }
- - {{ 'filter.filter-required' | translate }} - + @if (!inlineField && !modelValue && tbRequired) { + + {{ 'filter.filter-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html index f0bf2aa5cf..b795dfe25b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html @@ -19,13 +19,17 @@
widgets.widget-font.font-settings
widgets.widget-font.size
-
- - - - -
-
widgets.widget-font.auto
+ @if (!autoScale) { +
+ + + + +
+ } + @if (autoScale) { +
widgets.widget-font.auto
+ }
widgets.widget-font.font-family
@@ -33,19 +37,23 @@ - + @if (fontFormGroup.get('family').value) { + + } - - - + @for (family of filteredFontFamilies | async; track family) { + + + + }
@@ -55,9 +63,11 @@ - - {{ fontWeightTranslationsMap.has(weight) ? (fontWeightTranslationsMap.get(weight) | translate) : weight }} - + @for (weight of fontWeightsList; track weight) { + + {{ fontWeightTranslationsMap.has(weight) ? (fontWeightTranslationsMap.get(weight) | translate) : weight }} + + }
@@ -67,33 +77,45 @@ - - {{ fontStyleTranslationsMap.get(style) | translate }} - + @for (style of fontStylesList; track style) { + + {{ fontStyleTranslationsMap.get(style) | translate }} + + }
-
-
widgets.widget-font.line-height
- - - -
widgets.widget-font.auto
-
+ @if (!disabledLineHeight) { +
+
widgets.widget-font.line-height
+ @if (!autoScale) { + + + + } + @if (autoScale) { +
widgets.widget-font.auto
+ } +
+ }
widgets.widget-font.preview
- - + @if (clearButton) { + + } + @if (clearButton) { + + } + +
+
+ } +
+
+ } + +
+
+
widgets.color.gradient-end
+
+
+
widgets.color.end-value
+ + + +
+ + formControlName="end"> -
- - +
-
- - -
-
-
widgets.color.gradient-end
-
-
-
widgets.color.end-value
- - - + } + @case (true) { +
+
+
+ widgets.color.gradient-start + widgets.color.gradient-start-min
- +
+ + + formControlName="color"> - -
-
-
- - -
-
-
- widgets.color.gradient-start - widgets.color.gradient-start-min -
-
- - - - +
-
- -
-
-
-
-
-
- widgets.color.gradient-color - widgets.color.gradient-color-min -
-
- - - - + @if (advancedGradientListFormGroups.length) { +
+
+ @for (gradientFormGroup of advancedGradientListFormGroups; track trackByGradient($index, gradientFormGroup); let isLast = $last) { +
+
+
+
+ widgets.color.gradient-color + widgets.color.gradient-color-min +
+
+ + + + +
+
+ + +
-
- - + }
-
-
- - -
-
-
- widgets.color.gradient-end - widgets.color.gradient-end-min -
-
- - - - + } + +
+
+
+ widgets.color.gradient-end + widgets.color.gradient-end-min +
+
+ + + + +
-
- + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts index c908c86eea..fa97267843 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts @@ -118,9 +118,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro this.gradientFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => this.updateModel()); - this.gradientFormGroup.get('advancedMode').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0)); } ngOnDestroy() { @@ -237,7 +234,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro this.gradientListFormArray.removeAt(index); } this.gradientFormGroup.markAsDirty(); - setTimeout(() => {this.popover?.updatePosition();}, 0); } gradientDrop(event: CdkDragDrop, advanced = false) { @@ -256,7 +252,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)')); } this.gradientFormGroup.markAsDirty(); - setTimeout(() => {this.popover?.updatePosition();}, 0); } updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html index b8cbb8ba82..93a1ed6adc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html @@ -28,15 +28,17 @@ - -
-
-
{{ option.name }}
-
- + @for (option of options; track option) { + +
+
+
{{ option.name }}
+
+ +
-
- + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html index fa8a152d31..f89fde663a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html @@ -16,32 +16,43 @@ --> -
- - {{ 'widgets.status-widget.label' | translate }} - -
- - - - - + @if (layout !== StatusWidgetLayout.icon) { +
+ + {{ 'widgets.status-widget.label' | translate }} + +
+ + + +
-
-
- - {{ 'widgets.status-widget.status' | translate }} - -
- - - - - + } + @if (layout !== StatusWidgetLayout.icon) { +
+ + {{ 'widgets.status-widget.status' | translate }} + +
+ + + + + +
-
+ }
{{ 'widgets.status-widget.icon' | translate }} @@ -67,13 +78,17 @@ formControlName="primaryColor">
- -
-
widgets.status-widget.secondary
- - -
+ @if (layout !== StatusWidgetLayout.icon) { + + } + @if (layout !== StatusWidgetLayout.icon) { +
+
widgets.status-widget.secondary
+ + +
+ }
widgets.status-widget.background
@@ -93,13 +108,17 @@ formControlName="primaryColorDisabled">
- -
-
widgets.status-widget.secondary
- - -
+ @if (layout !== StatusWidgetLayout.icon) { + + } + @if (layout !== StatusWidgetLayout.icon) { +
+
widgets.status-widget.secondary
+ + +
+ }
widgets.status-widget.background
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts index ed80795ca0..b3caf03e89 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts @@ -22,6 +22,7 @@ import { StatusWidgetStateSettings } from '@home/components/widget/lib/indicator/status-widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { widgetTitleAutocompleteValues } from '@app/shared/public-api'; @Component({ selector: 'tb-status-widget-state-settings', @@ -52,6 +53,8 @@ export class StatusWidgetStateSettingsComponent implements OnInit, OnChanges, Co public stateSettingsFormGroup: UntypedFormGroup; + predefinedValues = widgetTitleAutocompleteValues; + constructor(private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html index 6635982677..802adb5211 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html @@ -19,39 +19,45 @@

{{ 'datakey.configuration' | translate }}

- - + @if (hasAdvanced) { + + + }
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
+ [dataKeyConfigMode]="dataKeyConfigMode" + [dataKeySettingsForm]="data.dataKeySettingsForm" + [dataKeySettingsDirective]="data.dataKeySettingsDirective" + [deviceId]="data.deviceId" + [entityAliasId]="data.entityAliasId" + [dashboard]="data.dashboard" + [aliasController]="data.aliasController" + [widget]="data.widget" + [widgetType]="data.widgetType" + [showPostProcessing]="data.showPostProcessing" + [callbacks]="data.callbacks" + [hideDataKeyName]="data.hideDataKeyName" + [hideDataKeyLabel]="data.hideDataKeyLabel" + [hideDataKeyColor]="data.hideDataKeyColor" + [hideDataKeyUnits]="data.hideDataKeyUnits" + [hideDataKeyDecimals]="data.hideDataKeyDecimals" + [hideDataKeyAggregation]="data.hideDataKeyAggregation" + [supportsUnitConversion]="data.supportsUnitConversion" + formControlName="dataKey">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts index 131a332f04..ba5725de08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts @@ -56,6 +56,7 @@ export interface DataKeyConfigDialogData { hideDataKeyColor?: boolean; hideDataKeyUnits?: boolean; hideDataKeyDecimals?: boolean; + hideDataKeyAggregation?: boolean; supportsUnitConversion?: boolean } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html index 52eb5aabb3..c8bec5a971 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html @@ -15,185 +15,218 @@ limitations under the License. --> -
-
-
-
datakey.general
- - {{ 'entity.key' | translate }} - - - - - - - - - - datakey.label - - - -
-
widget-config.units-short
- - -
-
-
widget-config.decimals-short
- - - -
-
-
-
{{ 'datakey.color' | translate }}
- - -
-
- datakey.data-generation-func -
- - -
- -
-
-
{{ 'datakey.aggregation' | translate }}
- - - - {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + close + + } + + @for (key of filteredKeys | async; track key) { + + - - -
-
- {{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }} -
- {{ 'datakey.aggregation-type-hint-common' | translate }} -
-
-
-
-
-
-
datakey.delta-calculation
-
- - - - - {{ 'datakey.enable-delta-calculation' | translate }} - - {{ 'datakey.enable-delta-calculation-hint' | translate }} - - - - + } + + + } + @if (!hideDataKeyLabel) { + + datakey.label + + + } + @if (modelValue.type !== dataKeyTypes.alarm) { + @if (!hideDataKeyUnits) {
-
{{ 'widgets.chart.time-for-comparison' | translate }}
- - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - +
widget-config.units-short
+ +
-
-
{{ 'widgets.chart.custom-interval-value' | translate }}
- - + } + @if (!hideDataKeyDecimals) { +
+
widget-config.decimals-short
+ +
-
-
{{ 'datakey.delta-calculation-result' | translate }}
+ } + } + @if (!hideDataKeyColor) { +
+
{{ 'datakey.color' | translate }}
+ + +
+ } + @if (modelValue.type === dataKeyTypes.function) { +
+ datakey.data-generation-func +
+ + +
+ } + @if (!hideDataKeyAggregation && widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries) { +
+
+
{{ 'datakey.aggregation' | translate }}
- - - {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} - + + @for (aggregation of aggregations; track aggregation) { + + {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + + }
- - - +
+ {{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }} + @if (dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE) { +
+ {{ 'datakey.aggregation-type-hint-common' | translate }} +
+ } +
+
+ }
-
-
- - - - - {{ 'datakey.use-data-post-processing-func' | translate }} - - - - - - - - - + @if (dataKeyFormGroup.get('aggregationType').value && dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE) { +
+
datakey.delta-calculation
+
+ + + + + {{ 'datakey.enable-delta-calculation' | translate }} + + {{ 'datakey.enable-delta-calculation-hint' | translate }} + + + + @if (dataKeyFormGroup.get('comparisonEnabled').value) { +
+
{{ 'widgets.chart.time-for-comparison' | translate }}
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + +
+ @if (dataKeyFormGroup.get('timeForComparison').value === 'customInterval') { +
+
{{ 'widgets.chart.custom-interval-value' | translate }}
+ + + +
+ } +
+
{{ 'datakey.delta-calculation-result' | translate }}
+ + + @for (comparisonResultType of comparisonResults; track comparisonResultType) { + + {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} + + } + + +
+ } +
+
+
+
+ } + @if ((modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute || modelValue.type === dataKeyTypes.count) && showPostProcessing) { +
+ + + + + {{ 'datakey.use-data-post-processing-func' | translate }} + + + + + @if (dataKeyFormGroup.get('usePostProcessing').value) { + + + } + @if (dataKeyFormGroup.get('usePostProcessing').value) { + + } + + +
+ }
-
- +} @else {
-
+} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts index 4311a06ddd..87e25f2614 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts @@ -154,6 +154,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @coerceBoolean() hideDataKeyDecimals = false; + @Input() + @coerceBoolean() + hideDataKeyAggregation = false; + @Input() @coerceBoolean() supportsUnitConversion = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html index 545c944ec7..f1e494ff21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html @@ -16,26 +16,32 @@ --> -
-
{{ 'legend.direction' | translate }}
- - - - {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} - - - -
+ @if (!hideDirection) { +
+
{{ 'legend.direction' | translate }}
+ + + @for (direction of legendDirections; track direction) { + + {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} + + } + + +
+ }
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} - + {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html index b79c6ec5a0..7989ee86e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html @@ -19,34 +19,39 @@
- - {{ datasourceTypesTranslations.get(type) | translate }} - + @for (type of datasourceTypes; track type) { + + {{ datasourceTypesTranslations.get(type) | translate }} + + } - - - - - - - + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.function) { + + + + } + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.device) { + + + } + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.entity) { + + + }
widgets.maps.data-layer.data-keys
-
-
- - + @if (dataSourcesFormArray().controls.length) { +
+ @for (dataSourceControl of dataSourcesFormArray().controls; track trackByDataSource($index, dataSourceControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.maps.data-layer.no-datasources' | translate }} + }
- - {{ 'widgets.maps.data-layer.no-datasources' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html index 360461b1ce..b4a8555f97 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html @@ -21,10 +21,11 @@ [disabled]="disabled" #matButton (click)="openColorSettingsPopup($event, matButton)"> - mdi:function-variant + @if (modelValue?.type === DataLayerColorType.function) { + mdi:function-variant + } @else { +
+
+
+ } - -
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html index 4429957983..3941753d57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -33,30 +33,36 @@ - - - - - + @if (patternSettingsFormGroup.get('type').value === DataLayerPatternType.pattern) { + + + } + @if (patternSettingsFormGroup.get('type').value === DataLayerPatternType.function) { + + + } + @if (patternType === 'tooltip') {
widgets.maps.data-layer.tooltip-trigger
- - {{ dataLayerTooltipTriggerTranslationMap.get(trigger) | translate }} - + @for (trigger of dataLayerTooltipTriggers; track trigger) { + + {{ dataLayerTooltipTriggerTranslationMap.get(trigger) | translate }} + + }
@@ -65,30 +71,32 @@ {{ 'widgets.maps.data-layer.auto-close-tooltips' | translate }}
-
-
widgets.maps.data-layer.tooltip-offset
-
-
-
widgets.maps.data-layer.tooltip-offset-horizontal
- - - -
-
-
widgets.maps.data-layer.tooltip-offset-vertical
- - - + @if (hasTooltipOffset) { +
+
widgets.maps.data-layer.tooltip-offset
+
+
+
widgets.maps.data-layer.tooltip-offset-horizontal
+ + + +
+
+
widgets.maps.data-layer.tooltip-offset-vertical
+ + + +
-
+ } - + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html index 0d83a4e423..9d9f4377e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html @@ -25,11 +25,13 @@ {{ 'widgets.maps.image.image-source-entity-key' | translate }}
- - - + @if (ImageSourceType.image === imageMapSourceFormGroup.get('sourceType').value) { + + + } + @if (ImageSourceType.entityKey === imageMapSourceFormGroup.get('sourceType').value) {
widgets.maps.image.source-entity-alias
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html index 421d518a90..6cab6208f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html @@ -18,14 +18,15 @@
- - warning - + @if (mapActionButton.hasError('invalidButtonConfig')) { + + warning + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 8fb0194d59..8e484c8e3d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -30,139 +30,172 @@
{{ 'widget-config.datasource' | translate }}
- {{ datasourceTypesTranslations.get(type) | translate }} + @for (type of datasourceTypes; track type) { + {{ datasourceTypesTranslations.get(type) | translate }} + }
- - datasource.label - - - - - - - - + @if (dataLayerFormGroup.get('dsType').value === DatasourceType.function) { + + datasource.label + + + } + @if (dataLayerFormGroup.get('dsType').value === DatasourceType.device) { + + + } + @if (dataLayerFormGroup.get('dsType').value === DatasourceType.entity) { + + + } + @if ([DatasourceType.device, DatasourceType.entity].includes(dataLayerFormGroup.get('dsType').value)) { + + + }
- - - - widgets.maps.data-layer.more-datasources - - - - - - - + @if (dataLayerFormGroup.get('dsType').value !== DatasourceType.function) { + + + + widgets.maps.data-layer.more-datasources + + + + + + + + }
{{ 'datakey.keys' | translate }}
-
+ @if (['trips', 'markers'].includes(dataLayerType)) { +
+ + + + +
+ } + @if (dataLayerType === 'polygons') { + (keyEdit)="editKey('polygonKey')" + formControlName="polygonKey"> + } + @if (dataLayerType === 'circles') { + (keyEdit)="editKey('circleKey')" + formControlName="circleKey"> -
- - - - + } + @if (dataLayerType === 'polylines') { + + + }
{{ 'widget-config.appearance' | translate }}
- + @if (['trips', 'markers'].includes(dataLayerType)) {
@@ -210,28 +243,34 @@ -
-
widgets.maps.data-layer.marker.shape
- -
-
-
widgets.maps.data-layer.marker.icon
- -
-
-
widgets.maps.data-layer.marker.image
- -
+ @if (dataLayerFormGroup.get('markerType').value === MarkerType.shape) { +
+
widgets.maps.data-layer.marker.shape
+ +
+ } + @if (dataLayerFormGroup.get('markerType').value === MarkerType.icon) { +
+
widgets.maps.data-layer.marker.icon
+ +
+ } + @if (dataLayerFormGroup.get('markerType').value === MarkerType.image) { +
+
widgets.maps.data-layer.marker.image
+ +
+ }
widgets.maps.data-layer.marker.marker-offset
@@ -247,18 +286,20 @@
-
- - {{ 'widgets.maps.data-layer.marker.rotate-marker' | translate }} - -
-
widgets.maps.data-layer.marker.offset-angle
- - -
deg
-
+ @if (dataLayerType === 'trips') { +
+ + {{ 'widgets.maps.data-layer.marker.rotate-marker' | translate }} + +
+
widgets.maps.data-layer.marker.offset-angle
+ + +
deg
+
+
-
+ } @if (dataLayerType === 'trips') { @@ -266,19 +307,21 @@
-
-
widgets.maps.data-layer.marker.position-conversion
- - -
- - + @if (mapType === MapType.image) { +
+
widgets.maps.data-layer.marker.position-conversion
+ + +
+ } + } + @if (dataLayerType === 'trips') {
@@ -302,7 +345,7 @@ [dsType]="dataLayerFormGroup.get('dsType').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" - helpId="widget/lib/map/path_color_fn" formControlName="pathStrokeColor"> + helpId="widget/lib/map/path_color_fn" formControlName="pathStrokeColor">
@@ -321,9 +364,11 @@
- - {{ pathDecoratorSymbolTranslationMap.get(symbol) | translate }} - + @for (symbol of pathDecoratorSymbols; track symbol) { + + {{ pathDecoratorSymbolTranslationMap.get(symbol) | translate }} + + }
@@ -393,7 +438,7 @@ [dsType]="dataLayerFormGroup.get('dsType').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" - helpId="widget/lib/map/path_point_color_fn" formControlName="pointColor"> + helpId="widget/lib/map/path_point_color_fn" formControlName="pointColor">
-
- + } + @if (['polygons', 'circles'].includes(dataLayerType)) {
{{ 'widgets.maps.data-layer.shape.fill' | translate }}
@@ -418,31 +463,39 @@ {{ 'widgets.maps.data-layer.shape.fill-type-image' | translate }}
-
-
widgets.maps.data-layer.shape.color
- +
widgets.maps.data-layer.shape.color
+ -
-
-
widgets.maps.data-layer.shape.stripe-pattern
- - -
-
-
widgets.maps.data-layer.shape.image
- -
+
+ } + @if (dataLayerFormGroup.get('fillType').value === ShapeFillType.stripe) { +
+
widgets.maps.data-layer.shape.stripe-pattern
+ + +
+ } + @if (dataLayerFormGroup.get('fillType').value === ShapeFillType.image) { +
+
widgets.maps.data-layer.shape.image
+ +
+ }
+ } + @if (['polygons', 'circles', 'polylines'].includes(dataLayerType)) {
widgets.maps.data-layer.shape.stroke
@@ -455,10 +508,12 @@ [dsType]="dataLayerFormGroup.get('dsType').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" - helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="strokeColor"> + helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : + (dataLayerType === 'circles' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/polyline_stroke_color_fn') }}" + formControlName="strokeColor">
- + } @if (dataLayerType !== 'trips') { } @@ -476,39 +531,46 @@ formControlName="groups">
-
-
{{ dataLayerEditTitle | translate }}
-
-
widgets.maps.data-layer.edit-instruments
- - - {{ dataLayerEditActionTranslationMap.get(action) | translate }} - - -
-
-
widgets.maps.data-layer.persist-location-attribute-scope
- - - {{ telemetryTypeTranslationsShort.get(AttributeScope.SERVER_SCOPE) | translate }} - - - {{ telemetryTypeTranslationsShort.get(AttributeScope.SHARED_SCOPE) | translate }} - - -
-
- - - {{ 'widgets.maps.data-layer.enable-snapping' | translate }} - - + @if (dataLayerType !== 'trips') { +
+
{{ dataLayerEditTitle | translate }}
+
+
widgets.maps.data-layer.edit-instruments
+ + @for (action of dataLayerEditActions; track action) { + + {{ dataLayerEditActionTranslationMap.get(action) | translate }} + + } + +
+ @if (showEditAttributeScope) { +
+
widgets.maps.data-layer.persist-location-attribute-scope
+ + + {{ telemetryTypeTranslationsShort.get(AttributeScope.SERVER_SCOPE) | translate }} + + + {{ telemetryTypeTranslationsShort.get(AttributeScope.SHARED_SCOPE) | translate }} + + +
+ } +
+ + + {{ 'widgets.maps.data-layer.enable-snapping' | translate }} + + +
-
- - + } + @if (dataLayerType === 'markers') { + + + }
+ @if (['trips', 'markers'].includes(dataLayerType)) { + - - - -
- - - - - - - - + [widgetType]="dataLayerType === 'trips' ? widgetType.timeseries : widgetType.latest" + [dataKeyType]="context.functionsOnly ? DataKeyType.function : null" + [dataKeyTypes]="dataLayerType === 'trips' ? [DataKeyType.timeseries] : [DataKeyType.attribute, DataKeyType.timeseries]" + [callbacks]="context.callbacks" + [generateKey]="context.generateDataKey" + (keyEdit)="editKey('xKey')" + formControlName="xKey"> + + } + @if (['trips', 'markers'].includes(dataLayerType)) { + + + } + @if (dataLayerType === 'polygons') { + + + } + @if (dataLayerType === 'circles') { + + + } + @if (dataLayerType === 'polylines') { + + + } +
-
+ @if (dataLayersFormArray().controls.length) { +
+ @for (dataLayerControl of dataLayersFormArray().controls; track trackByDataLayer($index, dataLayerControl)) { +
+ + +
+ +
+
+ }
-
+ } @else { + {{ noDataLayersText | translate }} + }
- - {{ noDataLayersText | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts index 63fe3dc31a..b8e1b73115 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts @@ -110,6 +110,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle'; this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles'; break; + case 'polylines': + this.addDataLayerText = 'widgets.maps.data-layer.polyline.add-polylines'; + this.noDataLayersText = 'widgets.maps.data-layer.polyline.no-polylines'; + break; } this.dataLayersFormGroup = this.fb.group({ dataLayers: [this.fb.array([]), []] diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html index fcec47bc2b..2a2b6c27a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html @@ -19,29 +19,33 @@
- - {{ datasourceTypesTranslations.get(type) | translate }} - + @for (type of datasourceTypes; track type) { + + {{ datasourceTypesTranslations.get(type) | translate }} + + } - - - - + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.device) { + + + } + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.entity) { + + + }
widgets.maps.data-layer.filter
-
-
- - + @if (dataSourcesFormArray().controls.length) { +
+ @for (dataSourceControl of dataSourcesFormArray().controls; track trackByDataSource($index, dataSourceControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.maps.data-layer.no-datasources' | translate }} + }
- - {{ 'widgets.maps.data-layer.no-datasources' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html index d59fe2d283..69d1abeab2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html @@ -21,47 +21,79 @@ - - {{ mapProviderTranslationMap.get(provider) | translate }} - + @for (provider of mapProviders; track provider) { + + {{ mapProviderTranslationMap.get(provider) | translate }} + + } - - - - {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ googleMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ hereLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ tencentLayerTranslationMap.get(layerType) | translate }} - - - - - - + @if (MapProvider.openfreemap === layerFormGroup.get('provider').value) { + + + @for (styleType of openFreeMapStyleTypes; track styleType) { + + {{ openFreeMapStyleTranslationMap.get(styleType) | translate }} + + } + + + } + @if (MapProvider.openstreet === layerFormGroup.get('provider').value) { + + + @for (layerType of openStreetLayerTypes; track layerType) { + + {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.google === layerFormGroup.get('provider').value) { + + + @for (layerType of googleMapLayerTypes; track layerType) { + + {{ googleMapLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.here === layerFormGroup.get('provider').value) { + + + @for (layerType of hereLayerTypes; track layerType) { + + {{ hereLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.tencent === layerFormGroup.get('provider').value) { + + + @for (layerType of tencentLayerTypes; track layerType) { + + {{ tencentLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.custom === layerFormGroup.get('provider').value) { + + + + }
- - {{ mapProviderTranslationMap.get(provider) | translate }} - + @for (provider of mapProviders; track provider) { + + {{ mapProviderTranslationMap.get(provider) | translate }} + + }
-
-
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? 'widgets.maps.layer.provider.custom.tile-url' : 'widgets.maps.layer.layer') | translate }}
- - - - {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ googleMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ hereLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ tencentLayerTranslationMap.get(layerType) | translate }} - - - - - - -
-
-
widgets.maps.layer.credentials.credentials
+ @if (MapProvider.custom === layerFormGroup.get('provider').value) {
-
widgets.maps.layer.credentials.api-key
- - - + +
+ {{ 'widgets.maps.layer.provider.custom.vector-tiles' | translate }} +
+
+ } +
+
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? (layerFormGroup.get('vectorTiles').value ? 'widgets.maps.layer.provider.custom.style-url' : 'widgets.maps.layer.provider.custom.tile-url') : 'widgets.maps.layer.layer') | translate }}
+ @if (MapProvider.openfreemap === layerFormGroup.get('provider').value) { + + + @for (styleType of openFreeMapStyleTypes; track styleType) { + + {{ openFreeMapStyleTranslationMap.get(styleType) | translate }} + + } + + + } + @if (MapProvider.openstreet === layerFormGroup.get('provider').value) { + + + @for (layerType of openStreetLayerTypes; track layerType) { + + {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.google === layerFormGroup.get('provider').value) { + + + @for (layerType of googleMapLayerTypes; track layerType) { + + {{ googleMapLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.here === layerFormGroup.get('provider').value) { + + + @for (layerType of hereLayerTypes; track layerType) { + + {{ hereLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.tencent === layerFormGroup.get('provider').value) { + + + @for (layerType of tencentLayerTypes; track layerType) { + + {{ tencentLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.custom === layerFormGroup.get('provider').value) { + + + + }
+ @if ([MapProvider.google, MapProvider.here].includes(layerFormGroup.get('provider').value)) { +
+
widgets.maps.layer.credentials.credentials
+
+
widgets.maps.layer.credentials.api-key
+ + + +
+
+ }
widgets.maps.layer.reference.reference-layer
{{ 'widgets.maps.layer.reference.no-layer' | translate }} - - {{ referenceLayerTypeTranslationMap.get(layer) | translate }} - + @for (layer of referenceLayerTypes; track layer) { + + {{ referenceLayerTypeTranslationMap.get(layer) | translate }} + + }
+ @if (MapProvider.custom === layerFormGroup.get('provider').value) { +
+
widgets.maps.layer.provider.custom.attribution
+ + + +
+ }
-
-
- -
- -
+ @if (layersFormArray().controls.length) { +
+ @for (layerControl of layersFormArray().controls; track trackByLayer($index, layerControl)) { +
+ +
+ +
+
+ }
-
+ } @else { + {{ 'widgets.maps.layer.no-layers' | translate }} + }
- - {{ 'widgets.maps.layer.no-layers' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index 567a3b7dd7..943d166a0b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -26,13 +26,17 @@ {{ 'widgets.maps.map-type.image' | translate }}
- - - - + @if (MapType.geoMap === mapSettingsFormGroup.get('mapType').value) { + + + } + @if (MapType.image === mapSettingsFormGroup.get('mapType').value) { + + + }
@@ -41,40 +45,50 @@
- {{ 'widgets.maps.overlays.trips' | translate }} + @if (trip) { + {{ 'widgets.maps.overlays.trips' | translate }} + } {{ 'widgets.maps.overlays.markers' | translate }} {{ 'widgets.maps.overlays.polygons' | translate }} {{ 'widgets.maps.overlays.circles' | translate }} + {{ 'widgets.maps.overlays.polylines' | translate }}
- + @if (trip) { + + } + [mapType]="mapSettingsFormGroup.get('mapType').value"> + [mapType]="mapSettingsFormGroup.get('mapType').value"> + [mapType]="mapSettingsFormGroup.get('mapType').value"> +
{{ 'widgets.maps.data-layer.additional-datasources' | translate }}
+ [context]="context">
@@ -85,35 +99,45 @@
widgets.maps.control.position
- - {{ mapControlsPositionTranslationMap.get(position) | translate }} - + @for (position of mapControlPositions; track position) { + + {{ mapControlsPositionTranslationMap.get(position) | translate }} + + }
widgets.maps.control.zoom-actions
- - {{ mapZoomActionTranslationMap.get(action) | translate }} - + @for (action of mapZoomActions; track action) { + + {{ mapZoomActionTranslationMap.get(action) | translate }} + + }
widgets.maps.control.scale
- - {{ mapScaleTranslationMap.get(scale) | translate }} - + @for (scale of mapScales; track scale) { + + {{ mapScaleTranslationMap.get(scale) | translate }} + + }
-
- - {{ 'widgets.maps.control.switch-to-drag-mode-using-button' | translate }} - -
- + @if (showDragButtonModeButtonSettings) { +
+ + {{ 'widgets.maps.control.switch-to-drag-mode-using-button' | translate }} + +
+ } + @if (trip) { + + }
@@ -121,7 +145,7 @@
{{ 'widgets.maps.common.common-map-settings' | translate }}
- + @if (MapType.geoMap === mapSettingsFormGroup.get('mapType').value) {
{{ 'widgets.maps.common.fit-map-bounds' | translate }} @@ -141,7 +165,7 @@
-
+ }
widgets.maps.common.entities-limit
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts index ee1b7d5b78..2dd6f701a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts @@ -24,6 +24,7 @@ export interface MapSettingsContext { aliasController: IAliasController; callbacks: WidgetConfigCallbacks; widget: Widget; - editKey: (key: DataKey, deviceId: string, entityAliasId: string, WidgetType?: widgetType) => Observable; + editKey: (key: DataKey, deviceId: string, entityAliasId: string, WidgetType?: widgetType, + hideDataKeyAggregation?: boolean) => Observable; generateDataKey: (key: DataKey) => DataKey; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 0e33f9d83e..51b1fa271b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -146,6 +146,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid markers: [null, []], polygons: [null, []], circles: [null, []], + polylines: [null, []], additionalDataSources: [null, []], controlsPosition: [null, []], zoomActions: [null, []], @@ -181,7 +182,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid }); merge(this.mapSettingsFormGroup.get('markers').valueChanges, this.mapSettingsFormGroup.get('polygons').valueChanges, - this.mapSettingsFormGroup.get('circles').valueChanges + this.mapSettingsFormGroup.get('circles').valueChanges, + this.mapSettingsFormGroup.get('polylines').valueChanges ).pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { @@ -282,6 +284,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value; dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); } + if (!dragModeButtonSettingsEnabled) { + const polylines: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polylines').value; + dragModeButtonSettingsEnabled = polylines.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); + } if (!dragModeButtonSettingsEnabled) { const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value; dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); @@ -299,7 +305,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid this.propagateChange(this.modelValue); } - private editKey(key: DataKey, deviceId: string, entityAliasId: string, _widgetType = widgetType.latest): Observable { + private editKey(key: DataKey, deviceId: string, entityAliasId: string, _widgetType = widgetType.latest, + hideDataKeyAggregation = true): Observable { return this.dialog.open(DataKeyConfigDialogComponent, { disableClose: true, @@ -316,6 +323,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid hideDataKeyColor: true, hideDataKeyDecimals: true, hideDataKeyUnits: true, + hideDataKeyAggregation: hideDataKeyAggregation, widget: this.widget, dashboard: null, dataKeySettingsForm: null, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html index 1ca905dbe7..2c3d443378 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html @@ -19,49 +19,57 @@
widgets.maps.data-layer.tooltip-tag-actions
- -
-
-
- - {{ action.name }} - + @for (action of actionsFormGroup.get('actions').value; track action) { + +
+
+
+ + {{ action.name }} + +
+ @if (!disabled) { + + } + @if (!disabled) { + + }
- - -
- + + } - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html index 150789ffbd..c6b8d4eefd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html @@ -18,10 +18,10 @@
widgets.maps.data-layer.marker.marker-icon
+ [popover]="popover" (iconSelected)="selectIcon($event)">
widgets.maps.data-layer.marker.marker-appearance
- + @for (containerInfo of iconContainers; track containerInfo) { @if (containerInfo.iconContainer === iconContainer) { } - + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html index 22d7a09f48..2a3d3fd812 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html @@ -21,8 +21,9 @@ [disabled]="disabled" #matButton (click)="openImageSettingsPopup($event, matButton)"> - mdi:function-variant + @if (modelValue?.type === MarkerImageType.function) { + mdi:function-variant + } @else { + + } - - - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html index 0c76ad3fe5..5961577162 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html @@ -26,13 +26,17 @@ [disabled]="disabled" #matButton (click)="openShapePopup($event, matButton)"> - -
-
+ @if (markerType === MarkerType.shape) { + + } + @if (markerType === MarkerType.icon) { +
+
+ } + helpId="widget/lib/map/color_fn" formControlName="color">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html index e9611bf955..2e94accae9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html @@ -18,21 +18,25 @@
widgets.maps.data-layer.marker.marker-shapes
- - - - + @for (shapeInfo of shapes; track shapeInfo) { + @if (shapeInfo.shape === shape) { + + } + @if (shapeInfo.shape !== shape) { + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html index f890575008..e5157f09ea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html @@ -21,9 +21,10 @@ [disabled]="disabled" #matButton (click)="openImageSettingsPopup($event, matButton)"> - mdi:function-variant + @if (modelValue?.type === ShapeFillImageType.function) { + mdi:function-variant + } @else { + + } - - - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html index 966cef8c3f..36b6ed86fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html @@ -38,9 +38,11 @@
widgets.maps.timeline.speed-options
- - {{ speed }} - + @for (speed of [1,5,10,15,25]; track speed) { + + {{ speed }} + + }
@@ -50,7 +52,7 @@ + formControlName="timestampFormat">
-
-
scada.behavior.behavior
- -
- - - -
{{ behaviorGroup.title | customTranslate }}
-
-
- - - -
-
- - - -
-
+ @if (behaviorGroups?.length) { +
+
scada.behavior.behavior
+ @for (behaviorGroup of behaviorGroups; track behaviorGroup) { + @if (behaviorGroup.title) { +
+ + + +
{{ behaviorGroup.title | customTranslate }}
+
+
+ + + +
+
+ } + @if (!behaviorGroup.title) { + + } + } +
+ } @@ -44,43 +48,49 @@
- + @for (behavior of behaviors; track behavior) { - + }
{{ behavior.name | customTranslate }}
- - - - - - + @if (behavior.type === ScadaSymbolBehaviorType.value) { + + + } + @if (behavior.type === ScadaSymbolBehaviorType.action) { + + + } + @if (behavior.type === ScadaSymbolBehaviorType.widgetAction) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html index c24ff79466..a2e07002a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html @@ -19,59 +19,67 @@
- - {{ valueSourceDataKeyTypeTranslation.get(type) | translate }} - + @for (type of valueSourceDataKeyTypes; track type) { + + {{ valueSourceDataKeyTypeTranslation.get(type) | translate }} + + } - - + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.entity) { + + + }
- - - - warning - - - - - - + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.constant) { + + + @if (valueSourceFormGroup.get('value').hasError('required') + && valueSourceFormGroup.get('value').touched) { + + warning + + } + + } + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.latestKey) { + + + } + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.entity) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index b088489cc9..8721b060f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -40,16 +40,20 @@ #entityAliasInput formControlName="entityAlias" [matAutocomplete]="entityAliasAutocomplete"> - + @if (valueSourceFormGroup.get('entityAlias').value && !valueSourceFormGroup.get('entityAlias').disabled) { + + } - - - + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + }
@@ -57,16 +61,20 @@ #keyInput formControlName="attribute" [matAutocomplete]="keyAutocomplete"> - + @if (valueSourceFormGroup.get('attribute').value && !valueSourceFormGroup.get('attribute').disabled) { + + } - - - + @for (key of filteredKeys | async; track key) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html index edba87c0db..4e1e5e460c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html @@ -15,19 +15,19 @@ limitations under the License. --> -
+
- + widgets.widget-font.font-family - + {{ sizeTitle }}
- + widgets.widget-font.font-style @@ -41,7 +41,7 @@ - + widgets.widget-font.font-weight @@ -71,16 +71,19 @@
- - + @if (hasShadowColor) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 9004928078..2a9d05b57b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -267,6 +267,7 @@ import { import { ShapeFillStripeSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; +import { AxisScaleRowComponent } from './axis-scale-row.component'; @NgModule({ declarations: [ @@ -372,7 +373,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], imports: [ CommonModule, @@ -453,7 +455,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], providers: [ ColorSettingsComponentService, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html index 65f2044a7b..219ef4485c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html @@ -17,9 +17,13 @@ -->
-
{{definedDirectiveError}}
- - + @if (definedDirectiveError) { +
{{definedDirectiveError}}
+ } + @if (useDynamicForm()) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html index 5882b18996..764651379d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html @@ -15,43 +15,52 @@ limitations under the License. --> - - {{ (keyType === dataKeyType.attribute - ? attributeLabel + [formGroup]="deviceKeyFormGroup"> + @if (!inlineField) { + {{ (keyType === dataKeyType.attribute + ? attributeLabel : timeseriesLabel) | translate }} + } - - - warning - + @if (deviceKeyFormGroup.get('key').value && !deviceKeyFormGroup.get('key').disabled) { + + } + @if (inlineField && requiredText && deviceKeyFormGroup.get('key').hasError('required') + && deviceKeyFormGroup.get('key').touched) { + + warning + + } - - - + @for (key of filteredKeys | async; track key) { + + + + } - - {{ requiredText | translate }} - + @if (!inlineField && requiredText && deviceKeyFormGroup.get('key').hasError('required')) { + + {{ requiredText | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts index f02de5fa8d..d1c215a668 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts @@ -47,6 +47,7 @@ import { EntityFilter, singleEntityFilterFromDeviceId } from '@shared/models/que import { AliasFilterType } from '@shared/models/alias.models'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-device-key-autocomplete', @@ -97,6 +98,9 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI @coerceBoolean() inlineField: boolean; + @Input() + appearance: MatFormFieldAppearance; + dataKeyType = DataKeyType; private modelValue: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html index b4f79384ce..aa5b32162c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html @@ -18,12 +18,13 @@
widgets.rpc.common-settings - + widgets.rpc.led-title @@ -38,7 +39,7 @@ {{ 'widgets.rpc.perform-rpc-status-check' | translate }} - + widgets.rpc.retrieve-led-status-value-method @@ -53,13 +54,14 @@ [class.!hidden]="ledIndicatorWidgetSettingsForm.get('performCheckStatus').value" [aliasController]="aliasController" [targetDevice]="targetDevice" + appearance="outline" [keyType]="ledIndicatorWidgetSettingsForm.get('retrieveValueMethod').value === 'attribute' ? dataKeyType.attribute : dataKeyType.timeseries" attributeLabel="widgets.rpc.led-status-value-attribute" timeseriesLabel="widgets.rpc.led-status-value-timeseries" required formControlName="valueAttribute"> - + widgets.rpc.check-status-method @@ -73,7 +75,7 @@
widgets.rpc.rpc-settings - + widgets.rpc.request-timeout @@ -92,7 +94,7 @@ - + widgets.rpc.persistent-polling-interval diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html index 76929a5394..4765dabde3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html @@ -55,34 +55,36 @@ - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + }
@@ -90,14 +92,16 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + }
- + widgets.table.default-sort-order @@ -105,18 +109,20 @@
widgets.persistent-table.columns-settings - + widgets.persistent-table.display-columns - - {{ displayColumnFromValue(column)?.name }} - cancel - + @for (column of persistentTableWidgetSettingsForm.get('displayColumns').value; track column) { + + {{ displayColumnFromValue(column)?.name }} + cancel + + } - - - - -
-
- widgets.persistent-table.no-columns-found -
- + @for (column of filteredDisplayColumns | async; track column) { + + + + } + @if ((filteredDisplayColumns | async)?.length === 0) { + +
+ @if (!textIsNotEmpty(columnSearchText)) { +
+ widgets.persistent-table.no-columns-found +
+ } @else { {{ translate.get('widgets.persistent-table.no-columns-matching', {column: truncate.transform(columnSearchText, true, 6, '...')}) | async }} - -
-
+ } +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.html index 15dd118737..9e6b354ab0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.html @@ -18,7 +18,7 @@
widgets.rpc.common-settings - + widgets.rpc.switch-title diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html index c6a9618641..9a988b763f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html @@ -26,12 +26,14 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html index 608dd81aed..00eef97a77 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html @@ -18,7 +18,7 @@
widgets.rpc.rpc-settings - + widgets.rpc.request-timeout diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html index 15fcefcf9a..9fd436701a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html @@ -18,7 +18,7 @@
widgets.rpc.rpc-settings - + widgets.rpc.request-timeout @@ -37,7 +37,7 @@ - + widgets.rpc.persistent-polling-interval diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html index 72bc8d8219..2a22fdd74b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html @@ -18,11 +18,11 @@
widgets.rpc.common-settings - + widgets.rpc.widget-title - + widgets.rpc.button-label @@ -32,7 +32,7 @@ {{ 'widgets.rpc.is-one-way-command' | translate }} - + widgets.rpc.rpc-method @@ -42,7 +42,7 @@ validateOnChange formControlName="methodParams"> - + widgets.rpc.request-timeout @@ -64,7 +64,7 @@ - + widgets.rpc.persistent-polling-interval diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html index 776b773428..e34b9ae2a0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html @@ -29,7 +29,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.rpc-state.turn-on
@@ -38,7 +38,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.rpc-state.turn-off
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -58,7 +58,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -69,12 +69,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> - - {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> + @for (layout of singleSwitchLayouts; track layout) { + + {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} + + }
@@ -86,9 +88,16 @@ {{ 'widgets.single-switch.label' | translate }}
- - - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts index d2c022bdf4..73124033e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models'; +import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues, widgetType } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -52,6 +52,8 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent singleSwitchWidgetSettingsForm: UntypedFormGroup; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html index 476e2e07ce..4527111874 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html @@ -18,10 +18,14 @@
widgets.rpc.common-settings - - widgets.rpc.slide-toggle-label - - +
widgets.rpc.label-position diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts index 3c9bf7fff8..d8d95bdddc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -32,6 +32,8 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent slideToggleWidgetSettingsForm: UntypedFormGroup; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html index 5d1d987a76..01edd30eb3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html @@ -26,7 +26,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.slider.on-value-change
@@ -36,7 +36,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="valueChange"> + formControlName="valueChange">
widgets.rpc-state.disabled-state
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -58,12 +58,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> - - {{ sliderLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> + @for (layout of sliderLayouts; track layout) { + + {{ sliderLayoutTranslationMap.get(layout) | translate }} + + }
@@ -84,25 +86,27 @@
widgets.slider.slider
-
- - {{ 'widgets.slider.value' | translate }} - -
- - - -
widget-config.decimals-suffix
-
- - - - + @if (sliderWidgetSettingsForm.get('layout').value !== sliderLayout.simplified) { +
+ + {{ 'widgets.slider.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
-
+ }
{{ 'widgets.slider.range' | translate }}
@@ -180,43 +184,47 @@
-
-
- {{ 'widgets.slider.left-icon' | translate }} -
-
- - - - - - - - -
-
-
-
- {{ 'widgets.slider.right-icon' | translate }} + @if (sliderWidgetSettingsForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.left-icon' | translate }} +
+
+ + + + + + + + +
-
- - - - - - - - + } + @if (sliderWidgetSettingsForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.right-icon' | translate }} +
+
+ + + + + + + + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html index 145459382d..19ab4f24f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html @@ -18,7 +18,7 @@
widgets.rpc.common-settings - + widgets.rpc.switch-title diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html index 3536341211..7ab592ef24 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html @@ -23,7 +23,7 @@
widgets.rpc.retrieve-value-settings - + widgets.rpc.retrieve-value-method @@ -45,11 +45,12 @@ switchRpcSettingsFormGroup.get('retrieveValueMethod').value !== 'timeseries'" [aliasController]="aliasController" [targetDevice]="targetDevice" + appearance="outline" [keyType]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' ? dataKeyType.attribute : dataKeyType.timeseries" required formControlName="valueKey"> - + widgets.rpc.get-value-method @@ -63,7 +64,7 @@
widgets.rpc.update-value-settings - + widgets.rpc.set-value-method @@ -78,7 +79,7 @@
widgets.rpc.rpc-settings - + widgets.rpc.request-timeout @@ -97,7 +98,7 @@ - + widgets.rpc.persistent-polling-interval diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html index ae4e74e6d8..e881157fa9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html @@ -18,15 +18,20 @@
widgets.rpc.common-settings - - widgets.rpc.widget-title - - - + + widgets.rpc.button-label - + widgets.rpc.device-attribute-scope diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts index 4871e3db37..a76016b1e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -33,6 +33,8 @@ export class UpdateDeviceAttributeWidgetSettingsComponent extends WidgetSettings contentTypes = ContentType; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html index bb81b8d5c2..0f30cc1434 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html @@ -26,7 +26,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.value-stepper.left-button-click
@@ -36,7 +36,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="leftButtonClick"> + formControlName="leftButtonClick">
widgets.value-stepper.right-button-click
@@ -46,7 +46,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="rightButtonClick"> + formControlName="rightButtonClick">
widgets.button-state.disabled-state
@@ -57,7 +57,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -67,12 +67,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> - - {{ valueStepperTypeTranslationMap.get(type) | translate }} - + label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> + @for (type of valueStepperTypes; track type) { + + {{ valueStepperTypeTranslationMap.get(type) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html index 3921e3655b..417ca8d436 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html @@ -41,7 +41,7 @@ {{ 'widgets.date-range-navigator.picker-show-template' | translate }} - + widgets.date-range-navigator.first-day-of-week @@ -64,7 +64,7 @@ - + widgets.date-range-navigator.initial-interval @@ -108,7 +108,7 @@ - + widgets.date-range-navigator.initial-step-size diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html index 92c39ec75f..8950de48f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html @@ -20,9 +20,15 @@
widgets.table.table-header
{{ 'widgets.table.entities-table-title' | translate }}
- - - +
@@ -98,34 +104,36 @@ - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + }
@@ -133,9 +141,11 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts index 3b31cf5645..68a560a956 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -32,6 +32,8 @@ export class EntitiesTableWidgetSettingsComponent extends WidgetSettingsComponen entitiesTableWidgetSettingsForm: UntypedFormGroup; pageStepSizeValues = []; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.html index ffd81460fb..147b7defb1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.html @@ -18,6 +18,6 @@
widgets.entity-count.entity-count-card-style
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts index 6b2ef15a65..8307aaff1d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -30,6 +30,8 @@ import { countDefaultSettings } from '@home/components/widget/lib/count/count-wi export class EntityCountWidgetSettingsComponent extends WidgetSettingsComponent { entityCountWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html index bc377800fd..f51d3784b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html @@ -16,10 +16,15 @@ -->
- - widgets.gateway.gateway-title - - + {{ 'widgets.gateway.read-only' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts index 404a731df9..9b91652207 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -29,6 +29,8 @@ import { AppState } from '@core/core.state'; export class GatewayConfigSingleDeviceWidgetSettingsComponent extends WidgetSettingsComponent { gatewayConfigSingleDeviceWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html index 0e32d98266..3b35789e65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html @@ -18,26 +18,32 @@
widgets.gateway.general-settings - - widgets.gateway.widget-title - - - + + widgets.gateway.default-archive-file-name - + widgets.gateway.device-type-for-new-gateway
widgets.gateway.messages-settings - + widgets.gateway.save-config-success-message - + widgets.gateway.device-name-exists-message diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts index 6992b416ab..0eef25e44d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -29,6 +29,8 @@ import { AppState } from '@core/core.state'; export class GatewayConfigWidgetSettingsComponent extends WidgetSettingsComponent { gatewayConfigWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html index 65311f6e94..41e6a93d65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html @@ -16,20 +16,27 @@ -->
- - widgets.gateway.events-title - - - + + widgets.gateway.events-filter - - {{eventFilter}} - cancel - + @for (eventFilter of gatewayEventsWidgetSettingsForm.get('eventsReg').value; track eventFilter) { + + {{eventFilter}} + cancel + + } , private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html index a82a9a0b65..aa94fe22f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html @@ -16,14 +16,14 @@ --> - - + @switch (gaugeType) { + @case ('radial') { - - + } + @case ('linear') { - - + } + }
widgets.gauge.ticks-settings
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html index 29d9a65d17..3decf4f45f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html @@ -23,21 +23,25 @@ breakpoints: { 'lt-md': 2 }}" - label="{{ 'widgets.gauge.simple-gauge-type' | translate }}" - formControlName="gaugeType"> - - {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.gauge.simple-gauge-type' | translate }}" + formControlName="gaugeType"> + @for (layout of digitalGaugeLayouts; track layout) { + + {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} + + } -
-
{{ 'widgets.gauge.donut-start-angle' | translate }}
- - - -
+ @if (digitalGaugeWidgetSettingsForm.get('gaugeType').value === digitalGaugeType.donut) { +
+
{{ 'widgets.gauge.donut-start-angle' | translate }}
+ + + +
+ }
@@ -65,14 +69,15 @@
widgets.gauge.max-value-short
- - warning - + @if (digitalGaugeWidgetSettingsForm.get('maxValue').hasError('maxValue')) { + + warning + + }
@@ -227,8 +232,8 @@
widgets.gauge.tick-values
-
+ @for (tickValueControl of tickValuesFormArray().controls; track trackByTickValue($index, tickValueControl); let isLast = $last) { +
drag_indicator -
-
-
- widgets.gauge.no-tick-values +
+ }
+ @if (!tickValuesFormArray().controls.length) { +
+ widgets.gauge.no-tick-values +
+ }
+ @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html index 9a00b3bb4c..c995a68414 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html @@ -21,6 +21,7 @@ @@ -28,19 +29,22 @@ widgets.gpio.gpio-switches
-
- - -
+ @for (gpioItemControl of typedSelectOptions; track trackByGpioItem($index, gpioItemControl); let isLast = $last) { +
+ + +
+ }
-
+ @if (!gpioListFormArray().controls.length) { +
widgets.gpio.no-gpio-switches -
+ class="tb-prompt flex items-center justify-center">widgets.gpio.no-gpio-switches +
+ }
-
-
-
+ @if (hasColor) { +
+
+
+ }
- + @if (!disabled) { + + }
@@ -46,32 +50,34 @@
- + widgets.gpio.pin - + widgets.gpio.label
- + widgets.gpio.row - + widgets.gpio.column
- - + @if (hasColor) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html index 1b7ff3d4e5..f002322bfe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html @@ -28,20 +28,23 @@ widgets.gpio.gpio-leds
-
- - -
+ @for (gpioItemControl of typedSelectOptions; track trackByGpioItem($index, gpioItemControl); let isLast = $last) { +
+ + +
+ }
-
+ @if (!gpioListFormArray().controls.length) { +
widgets.gpio.no-gpio-leds -
+ class="tb-prompt flex items-center justify-center">widgets.gpio.no-gpio-leds +
+ }
+ @if (!disabled) { + + }
@@ -41,11 +45,11 @@
- + widgets.input-widgets.option-value - + widgets.input-widgets.option-label diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html index f11b16200f..53a508a038 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html @@ -18,11 +18,11 @@
widgets.input-widgets.general-settings - + widgets.input-widgets.widget-title - + widgets.input-widgets.claim-button-label @@ -46,11 +46,11 @@
- + widgets.input-widgets.device-name-label - + widgets.input-widgets.secret-key-label @@ -60,23 +60,23 @@
widgets.input-widgets.messages-settings - + widgets.input-widgets.claim-device-success-message - + widgets.input-widgets.claim-device-not-found-message - + widgets.input-widgets.claim-device-failed-message - + widgets.input-widgets.claim-device-name-required-message - + widgets.input-widgets.claim-device-secret-key-required-message diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html index f621fdbc97..96b7d8419a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html @@ -15,19 +15,39 @@ limitations under the License. --> -
-
- widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - -
-
- widgets.input-widgets.image-settings -
- - widgets.input-widgets.image-format +
+
+
widgets.input-widgets.general-settings
+
+
widgets.input-widgets.widget-title
+ +
+
+
+
widgets.input-widgets.save-image
+ + {{ 'widgets.input-widgets.save-to-gallery' | translate }} + + @if (photoCameraInputWidgetSettingsForm.get('saveToGallery').value) { + + {{ 'widgets.input-widgets.public-image' | translate }} + + } +
+
+
widgets.input-widgets.image-settings
+
+
widgets.input-widgets.image-format
+ {{ 'widgets.input-widgets.image-format-jpeg' | translate }} @@ -40,20 +60,33 @@ - - widgets.input-widgets.image-quality - - -
-
- - widgets.input-widgets.max-image-width - - - - widgets.input-widgets.max-image-height - - -
-
+
+ + @if (photoCameraInputWidgetSettingsForm.get('imageFormat').value !== 'image/png') { +
+
widgets.input-widgets.image-quality
+ + + % + +
+ } + +
+
Size
+
+
widgets.input-widgets.max-image-width
+ + + px + + +
widgets.input-widgets.max-image-height
+ + + px + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts index 8350ababa2..60245faff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts @@ -15,10 +15,10 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; @Component({ selector: 'tb-photo-camera-input-widget-settings', @@ -30,6 +30,8 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo photoCameraInputWidgetSettingsForm: UntypedFormGroup; + predefinedValues = widgetTitleAutocompleteValues; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); @@ -43,6 +45,8 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo return { widgetTitle: '', + saveToGallery: false, + usePublicGalleryLink: true, imageFormat: 'image/png', imageQuality: 0.92, maxWidth: 640, @@ -58,11 +62,28 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo widgetTitle: [settings.widgetTitle, []], // Image settings - + saveToGallery: [settings.saveToGallery], + usePublicGalleryLink: [settings.usePublicGalleryLink], imageFormat: [settings.imageFormat, []], - imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(1)]], + imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(100)]], maxWidth: [settings.maxWidth, [Validators.min(1)]], maxHeight: [settings.maxHeight, [Validators.min(1)]] }); } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + ...settings, + saveToGallery: settings.saveToGallery ?? false, + usePublicGalleryLink: settings.usePublicGalleryLink ?? false, + imageQuality: settings.imageQuality * 100 + } + } + + protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + return { + ...settings, + imageQuality: settings.imageQuality / 100 + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html index d2d1b32eff..f23e000981 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html @@ -18,18 +18,25 @@
widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - +
{{ 'widgets.input-widgets.show-label' | translate }} - - widgets.input-widgets.label - - + @if (hasLabelValue) { + + widgets.input-widgets.label + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts index 56dbc014c3..f6d89abfd0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts @@ -29,6 +29,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { widgetTitleAutocompleteValues } from '@app/shared/public-api'; export interface UpdateAttributeGeneralSettings { widgetTitle: string; @@ -78,6 +79,8 @@ export class UpdateAttributeGeneralSettingsComponent extends PageComponent imple @Input() hasLabelValue = true; + + predefinedValues = widgetTitleAutocompleteValues; private modelValue: UpdateAttributeGeneralSettings; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html index 9825593f07..056209f77a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html @@ -18,10 +18,15 @@
widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - + {{ 'widgets.input-widgets.show-result-message' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts index 8e9e739543..b1c9065214 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -29,6 +29,8 @@ import { AppState } from '@core/core.state'; export class UpdateBooleanAttributeWidgetSettingsComponent extends WidgetSettingsComponent { updateBooleanAttributeWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html index 7b1080fd24..195e95bb97 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html @@ -18,10 +18,16 @@
widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - + {{ 'widgets.input-widgets.show-result-message' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts index 506fe0b69e..53f65e5f19 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -29,6 +29,8 @@ import { AppState } from '@core/core.state'; export class UpdateImageAttributeWidgetSettingsComponent extends WidgetSettingsComponent { updateImageAttributeWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html index 507ec97a5f..2294fac186 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html @@ -18,10 +18,15 @@
widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - +
{{ 'widgets.input-widgets.show-label' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts index 2154c40f3a..3236b9a4b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -29,6 +29,8 @@ import { AppState } from '@core/core.state'; export class UpdateJsonAttributeWidgetSettingsComponent extends WidgetSettingsComponent { updateJsonAttributeWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html index 5867aef0aa..0c50acd514 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html @@ -18,19 +18,25 @@
widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - + {{ 'widgets.input-widgets.show-result-message' | translate }}
- + widgets.input-widgets.latitude-key-name - + widgets.input-widgets.longitude-key-name @@ -48,16 +54,16 @@ {{ 'widgets.input-widgets.show-label' | translate }}
- + widgets.input-widgets.latitude-label - + widgets.input-widgets.longitude-label
- + widgets.input-widgets.input-fields-alignment @@ -76,7 +82,7 @@ {{ 'widgets.input-widgets.longitude-field-required' | translate }}
- + widgets.input-widgets.required-error-message diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts index 18ecb43711..043b5e4373 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -29,6 +29,8 @@ import { AppState } from '@core/core.state'; export class UpdateLocationAttributeWidgetSettingsComponent extends WidgetSettingsComponent { updateLocationAttributeWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html index 6419028708..faadbd6581 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html @@ -23,7 +23,7 @@ widgets.input-widgets.general-settings
- + widgets.input-widgets.datakey-type @@ -37,7 +37,7 @@ - + widgets.input-widgets.datakey-value-type @@ -83,7 +83,7 @@ {{ 'widgets.input-widgets.value-is-required' | translate }}
- + widgets.input-widgets.ability-to-edit-attribute @@ -97,42 +97,44 @@ - + widgets.input-widgets.disable-on-datakey-name
-
- - widgets.input-widgets.field-appearance - - - {{ 'widgets.input-widgets.appearance-fill' | translate }} - - - {{ 'widgets.input-widgets.appearance-outline' | translate }} - - - - - widgets.input-widgets.subscript-sizing - - - {{ 'widgets.input-widgets.subscript-sizing-fixed' | translate }} - - - {{ 'widgets.input-widgets.subscript-sizing-dynamic' | translate }} - - - -
+ @if (!['booleanSwitch', 'booleanCheckbox', 'radio'].includes(updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value)) { +
+ + widgets.input-widgets.field-appearance + + + {{ 'widgets.input-widgets.appearance-fill' | translate }} + + + {{ 'widgets.input-widgets.appearance-outline' | translate }} + + + + + widgets.input-widgets.subscript-sizing + + + {{ 'widgets.input-widgets.subscript-sizing-fixed' | translate }} + + + {{ 'widgets.input-widgets.subscript-sizing-dynamic' | translate }} + + + +
+ }
widgets.input-widgets.slide-toggle-settings - + widgets.input-widgets.slide-toggle-label-position @@ -152,20 +154,22 @@
-
- - -
-
-
- - {{ (updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value === 'select' ? 'widgets.input-widgets.no-select-options' : 'widgets.input-widgets.no-radio-options') | translate }} - + @for (selectOptionControl of typedSelectOptions; track trackBySelectOption($index, selectOptionControl); let isLast = $last) { +
+ + +
+ }
+ @if (!selectOptionsFormArray().controls.length) { +
+ + {{ (updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value === 'select' ? 'widgets.input-widgets.no-select-options' : 'widgets.input-widgets.no-radio-options') | translate }} + +
+ }
@@ -212,15 +217,15 @@ updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value !== 'double')" class="fields-group"> widgets.input-widgets.numeric-field-settings
- + widgets.input-widgets.step-interval - + widgets.input-widgets.min-value - + widgets.input-widgets.max-value @@ -228,29 +233,30 @@
widgets.input-widgets.error-messages - + widgets.input-widgets.required-error-message
- + class="flex flex-row gap-2"> + widgets.input-widgets.min-value-error-message - + widgets.input-widgets.max-value-error-message
+ updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value !== 'time'" class="mat-block flex-1" + appearance="outline"> widgets.input-widgets.invalid-date-error-message - + widgets.input-widgets.invalid-JSON-error-message @@ -258,16 +264,16 @@
widgets.input-widgets.dialog-editor-settings - + widgets.input-widgets.title
- + widgets.input-widgets.save-button-label - + widgets.input-widgets.cancel-button-label @@ -275,7 +281,7 @@
+ class="fields-group"> widgets.input-widgets.icon-settings {{ 'widgets.input-widgets.use-custom-icon' | translate }} @@ -283,11 +289,12 @@ + formControlName="icon" + appearance="outline"> + label="{{ 'widgets.input-widgets.input-cell-icon' | translate }}" + formControlName="customIcon">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts index c007c51ef8..0212ee4279 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -256,6 +256,10 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings return this.updateMultipleAttributesKeySettingsForm.get('selectOptions') as UntypedFormArray; } + get typedSelectOptions() { + return this.selectOptionsFormArray().controls as (AbstractControl & { new?: boolean })[]; + } + public trackBySelectOption(index: number, selectOptionControl: AbstractControl): any { return selectOptionControl; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html index eedd0ec063..4c0d9bf4f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html @@ -18,10 +18,16 @@
widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - + {{ 'widgets.input-widgets.show-result-message' | translate }} @@ -46,11 +52,11 @@ {{ 'widgets.input-widgets.update-all-values' | translate }}
- + widgets.input-widgets.save-button-label - + widgets.input-widgets.reset-button-label @@ -65,7 +71,7 @@ {{ 'widgets.input-widgets.show-group-title' | translate }} - + widgets.input-widgets.group-title @@ -74,7 +80,7 @@
widgets.input-widgets.layout
- + widgets.input-widgets.fields-alignment @@ -85,17 +91,17 @@ - + widgets.input-widgets.fields-in-row
- + widgets.input-widgets.row-gap - + widgets.input-widgets.column-gap diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts index 5fbfad3486..0a92babdfb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -29,6 +29,8 @@ import { AppState } from '@core/core.state'; export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSettingsComponent { updateMultipleAttributesWidgetSettingsForm: UntypedFormGroup; + + predefinedValues = widgetTitleAutocompleteValues; constructor(protected store: Store, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html index f13de79d27..1a8fb6a328 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html @@ -88,9 +88,11 @@ widgets.maps.show-tooltip-action - - {{showTooltipActionTranslations.get(action) | translate}} - + @for (action of showTooltipActions; track action) { + + {{showTooltipActionTranslations.get(action) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html index 247a92f7ba..b0d8c742f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html @@ -23,17 +23,21 @@ (focusin)="onFocus()" formControlName="key" [matAutocomplete]="keyAutocomplete"> - + @if (keyFormGroup.get('key').value && !keyFormGroup.get('key').disabled) { + + } - - - + @for (key of filteredKeys | async; track key) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html index 7ffaff50c4..49b0e479c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html @@ -23,9 +23,11 @@ widgets.maps.default-map-type - - {{googleMapTypeTranslations.get(type) | translate}} - + @for (type of googleMapTypes; track type) { + + {{googleMapTypeTranslations.get(type) | translate}} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html index 056be11115..6a052ac907 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html @@ -19,9 +19,11 @@ widgets.maps.map-layer - - {{hereMapProviderTranslations.get(provider) | translate}} - + @for (provider of hereMapProviders; track provider) { + + {{hereMapProviderTranslations.get(provider) | translate}} + + }
@@ -30,22 +32,23 @@ {{ 'widgets.maps.here-use-new-version-api-3' | translate }} -
- - widgets.maps.here-app-id - - - - widgets.maps.here-app-code - - -
- + @if (!providerSettingsFormGroup.get('credentials.useV3').value) { +
+ + widgets.maps.here-app-id + + + + widgets.maps.here-app-code + + +
+ } @else { widgets.maps.here-api-key -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html index 6182b2b1fa..628d4853e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html @@ -17,7 +17,7 @@ -->
+ formControlName="mapImageUrl">
widgets.maps.image-map-background-from-entity-attribute @@ -30,18 +30,22 @@ (focusin)="onEntityAliasFocus()" formControlName="imageEntityAlias" [matAutocomplete]="entityAliasAutocomplete"> - + @if (providerSettingsFormGroup.get('imageEntityAlias').value && !providerSettingsFormGroup.get('imageEntityAlias').disabled) { + + } - - - + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + } @@ -52,18 +56,22 @@ (focusin)="onKeyFocus()" formControlName="imageUrlAttribute" [matAutocomplete]="keyAutocomplete"> - + @if (providerSettingsFormGroup.get('imageUrlAttribute').value && !providerSettingsFormGroup.get('imageUrlAttribute').disabled) { + + } - - - + @for (key of filteredKeys | async; track key) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html index b2d334a621..8566956dc0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html @@ -21,9 +21,11 @@ widgets.maps.map-provider - - {{mapProviderTranslations.get(provider) | translate}} - + @for (provider of mapProviders; track provider) { + + {{mapProviderTranslations.get(provider) | translate}} + + } - - - - + @if (!routeMap) { + + + } + @if (routeMap) { + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html index b786ea700e..609cfea4b8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html @@ -90,9 +90,11 @@ widgets.maps.show-tooltip-action - - {{showTooltipActionTranslations.get(action) | translate}} - + @for (action of showTooltipActions; track action) { + + {{showTooltipActionTranslations.get(action) | translate}} + + } @@ -171,8 +173,8 @@ + label="{{ 'widgets.maps.custom-marker-image' | translate }}" + formControlName="markerImage"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html index a08823395d..8fb0b3c705 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html @@ -19,9 +19,11 @@ widgets.maps.openstreet-provider - - {{openStreetMapProviderTranslations.get(provider) | translate}} - + @for (provider of mapProviders; track provider) { + + {{openStreetMapProviderTranslations.get(provider) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html index 3d82204a26..03bb018528 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html @@ -88,9 +88,11 @@ widgets.maps.show-tooltip-action - - {{showTooltipActionTranslations.get(action) | translate}} - + @for (action of showTooltipActions; track action) { + + {{showTooltipActionTranslations.get(action) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html index 88c36ab010..ccde76edad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html @@ -23,9 +23,11 @@ widgets.maps.default-map-type - - {{tencentMapTypeTranslations.get(type) | translate}} - + @for (type of tencentMapTypes; track type) { + + {{tencentMapTypeTranslations.get(type) | translate}} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html index 6034cbfc5c..57e4cfb873 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html @@ -73,9 +73,11 @@ widgets.maps.decorator-symbol - - {{polylineDecoratorSymbolTranslations.get(symbol) | translate}} - + @for (symbol of polylineDecoratorSymbols; track symbol) { + + {{polylineDecoratorSymbolTranslations.get(symbol) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html index e5176cd05b..e7dab2597f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html @@ -16,16 +16,17 @@ -->
- + widgets.navigation.title - + widgets.navigation.navigation-path diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html index d8545078c1..18284f79b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html @@ -24,14 +24,16 @@ {{ 'widgets.navigation.filter-type-exclude' | translate }}
- + widgets.navigation.items - - {{ filterItem }} - cancel - + @for (filterItem of navigationCardsWidgetSettingsForm.get('filter').value; track filterItem) { + + {{ filterItem }} + cancel + + } - - - + @for (filterItem of filteredFilterItems | async; track filterItem) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html index fa9c964654..230db9e857 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-md': 2 }}" - label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> - - {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> + @for (layout of windSpeedDirectionLayouts; track layout) { + + {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} + + }
{{ (hasCenterValue ? 'widgets.wind-speed-direction.center-value' : 'widgets.wind-speed-direction.wind-direction') | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index af8694d24c..3d8c564b67 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -375,6 +375,12 @@ import { ValueStepperWidgetSettingsComponent } from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component'; import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings/map/map-widget-settings.component'; +import { + ApiUsageWidgetSettingsComponent +} from "@home/components/widget/lib/settings/cards/api-usage-widget-settings.component"; +import { + ApiUsageDataKeyRowComponent +} from "@home/components/widget/lib/settings/cards/api-usage-data-key-row.component"; @NgModule({ declarations: [ @@ -508,7 +514,9 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings LabelValueCardWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent, - MapWidgetSettingsComponent + MapWidgetSettingsComponent, + ApiUsageWidgetSettingsComponent, + ApiUsageDataKeyRowComponent ], imports: [ CommonModule, @@ -647,7 +655,8 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings LabelValueCardWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent, - MapWidgetSettingsComponent + MapWidgetSettingsComponent, + ApiUsageWidgetSettingsComponent ] }) export class WidgetSettingsModule { 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 index 34995f2396..d2b191adfd 100644 --- 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 @@ -433,7 +433,7 @@ export function noDataMessage(noDataDisplayMessage: string, defaultMessage: stri return translate.instant(defaultMessage); } -export function constructTableCssString(widgetConfig: WidgetConfig): string { +export function constructTableCssString(widgetConfig: WidgetConfig, isTabsEnabled = false): 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)'; @@ -452,111 +452,48 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString(); const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); - const cssString = - '.mat-mdc-input-element::placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element::-moz-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element::-webkit-input-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element:-ms-input-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - 'mat-toolbar.mat-mdc-table-toolbar {\n' + - 'color: ' + mdDark + ';\n' + - '}\n' + - 'mat-toolbar.mat-mdc-table-toolbar:not([color="primary"]) button.mat-mdc-icon-button mat-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-tab .mdc-tab__text-label {\n' + - 'color: ' + mdDark + ';\n' + - '}\n' + - '.mat-mdc-tab-header-pagination-chevron {\n' + - 'border-color: ' + mdDark + ';\n' + - '}\n' + - '.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron {\n' + - 'border-color: ' + mdDarkDisabled2 + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-header-row {\n' + - 'background-color: ' + origBackgroundColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-header-cell {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell, .mat-mdc-table .mat-mdc-header-cell {\n' + - 'border-bottom-color: ' + mdDarkDivider + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell .mat-mdc-checkbox ' + - '.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+ - '~.mdc-checkbox__background, ' + - '.mat-table .mat-header-cell .mat-mdc-checkbox ' + - '.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+ - '~.mdc-checkbox__background {\n' + - 'border-color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'transition: background-color .2s;\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.tb-current-entity {\n' + - 'background-color: ' + currentEntityColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.tb-current-entity .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + currentEntityStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) {\n' + - 'background-color: ' + hoverColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + hoverStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) {\n' + - 'background-color: ' + selectedColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + selectedStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky, .mat-mdc-table .mat-mdc-header-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + origBackgroundColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row {\n' + - 'color: ' + mdDark + ';\n' + - 'background-color: rgba(0, 0, 0, 0);\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button mat-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] mat-icon {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button tb-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] tb-icon {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-divider {\n' + - 'border-top-color: ' + mdDarkDivider + ';\n' + - '}\n' + - '.mat-mdc-paginator {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-paginator button.mat-mdc-icon-button {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-paginator button.mat-mdc-icon-button[disabled][disabled] {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-mdc-paginator .mat-mdc-select-value {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}'; - return cssString; -} - -export function getHeaderTitle(dataKey: DataKey, keySettings: TableWidgetDataKeySettings, utils: UtilsService) { - if (isDefined(keySettings.customTitle) && isNotEmptyStr(keySettings.customTitle)) { + let cssString = ` { + --mat-toolbar-container-text-color: ${mdDark}; + --mat-table-header-headline-color: ${mdDarkSecondary}; + --mat-table-row-item-label-text-color: ${mdDark}; + --mat-icon-color: ${mdDarkSecondary}; + --mat-icon-button-disabled-icon-color: ${mdDarkDisabled}; + --mat-divider-color: ${mdDarkDivider}; + --mat-paginator-container-text-color: ${mdDarkSecondary}; + --mat-icon-button-icon-color: ${mdDarkSecondary}; + --mat-paginator-enabled-icon-color: ${mdDarkSecondary}; + --mat-paginator-disabled-icon-color: ${mdDarkDisabled}; + --mat-select-enabled-trigger-text-color: ${mdDarkSecondary}; + --mat-select-disabled-trigger-text-color: ${mdDarkDisabled}; + --mat-table-row-item-outline-color: ${mdDarkDivider}; + --mat-checkbox-unselected-focus-icon-color: ${mdDarkSecondary}; + + --tb-orig-background-color: ${origBackgroundColor}; + --tb-current-entity-color: ${currentEntityColor}; + --tb-current-entity-sticky-color: ${currentEntityStickyColor}; + --tb-hover-color: ${hoverColor}; + --tb-hover-sticky-color: ${hoverStickyColor}; + --tb-selected-color: ${selectedColor}; + --tb-selected-sticky-color: ${selectedStickyColor}; + } + `; + if (isTabsEnabled) { + cssString += ` + .mat-mdc-tab-group { + --mat-tab-inactive-label-text-color: ${mdDarkSecondary}; + --mat-tab-pagination-icon-color: ${mdDark}; + --mat-tab-pagination-disabled-icon-color: ${mdDarkDisabled2}; + --mat-tab-inactive-ripple-color: ${mdDarkSecondary}; + --mat-tab-inactive-focus-label-text-color: ${mdDarkSecondary}; + --mat-tab-inactive-hover-label-text-color: ${mdDarkSecondary}; + } + ` + } + return cssString ; +} + +export function getHeaderTitle(dataKey: DataKey, keySettings: TableWidgetDataKeySettings | undefined, utils: UtilsService) { + if (isNotEmptyStr(keySettings?.customTitle)) { return utils.customTranslation(keySettings.customTitle, keySettings.customTitle); } return dataKey.label; 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 index 87ed15ed6c..91c6dbb317 100644 --- 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 @@ -44,7 +44,53 @@ padding: 0 5px; } } + + .mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron { + border-color: var(--mat-tab-pagination-disabled-icon-color); + } + } + + .mat-mdc-input-element::placeholder { + color: var(--mat-table-header-headline-color); } + + .mat-mdc-table { + .mat-mdc-header-row { + background-color: var(--tb-orig-background-color); + } + + .mat-mdc-cell, .mat-mdc-header-cell { + &.mat-mdc-table-sticky { + background-color:var(--tb-orig-background-color); + } + } + + .mat-mdc-row { + background-color: rgba(0,0,0,0); + &.tb-current-entity { + background-color: var(--tb-current-entity-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-current-entity-sticky-color); + } + } + &:hover:not(.tb-current-entity){ + background-color: var(--tb-hover-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-hover-sticky-color); + } + } + &.mat-row-select.mat-selected:not(.tb-current-entity){ + background-color: var(--tb-selected-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-selected-sticky-color); + } + } + &, .mat-mdc-cell.mat-mdc-table-sticky { + transition: background-color .2s; + background-color:var(--tb-orig-background-color); + } + } + } } :host-context(.tb-has-timewindow) { 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 index c6b4f3d8ed..a744ac54a2 100644 --- 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 @@ -41,91 +41,108 @@ class="flex-1" mat-stretch-tabs="false" [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> - - -
- - - Timestamp - - - - - {{ h.dataKey.label }} - - - - - - -
-
-
-
+ @for (source of sources; track trackBySourcesIndex(index, source); let index = $index) { + + @if (isActiveTab(index)) { +
+
+ @if (showTimestamp) { + + {{ 'widgets.table.timestamp-column-name' | translate }} + + - - - -
- - - - -
-
- - - - + } + } +
+ @if (row.hasActions) { +
+ - - -
-
-
- - - -
- {{ noDataDisplayMessageText }} -
- - -
-
+ + @for (actionDescriptor of row.actionCellButtons; track trackByActionCellDescriptionId($index, actionDescriptor)) { + @if (actionDescriptor.icon) { + + } + } + +
+ } + } + + + + + + {{ noDataDisplayMessageText }} + + @if (displayPagination) { + + } + @if (displayPagination) { + + } + } + + } 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 index a7fcfee6d3..4ce5b9e130 100644 --- 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 @@ -106,12 +106,14 @@ import { ComponentPortal } from '@angular/cdk/portal'; import { FormBuilder } from '@angular/forms'; import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; import { DateFormatSettings, ValueFormatProcessor } from '@shared/models/widget-settings.models'; +import { entityFields } from '@shared/models/entity.models'; export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { showTimestamp: boolean; showMilliseconds: boolean; hideEmptyLines: boolean; dateFormat: DateFormatSettings; + sortOrder: SortOrder; } interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings { @@ -152,6 +154,7 @@ interface TimeseriesTableSource { timeseriesDatasource: TimeseriesDatasource; header: TimeseriesHeader[]; rowDataTemplate: {[key: string]: any}; + displayName: string; } @Component({ @@ -376,8 +379,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.noDataDisplayMessageText = noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widget.no-data-found', this.utils, this.translate); - - let cssString = constructTableCssString(this.widgetConfig); + let cssString = constructTableCssString(this.widgetConfig, true); const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; cssString += '.tb-table-widget mat-toolbar.mat-mdc-table-toolbar:not([color=primary]) {\n' + @@ -393,11 +395,38 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.updateDatasources(); } - public getTabLabel(source: TimeseriesTableSource){ - if (this.useEntityLabel) { - return source.datasource.entityLabel || source.datasource.entityName; - } else { - return source.datasource.entityName; + private getTabLabel(source: Datasource):string { + const value = this.useEntityLabel + ? (source.entityLabel || source.entityName) + : source.entityName; + + return this.utils.customTranslation(value); + } + + private sortDatasources(source: TimeseriesTableSource[]) { + const property = this.settings?.sortOrder?.property; + const direction = this.settings?.sortOrder?.direction; + const isAsc = direction === Direction.ASC; + + if (property === entityFields.name.keyName) { + const collator = new Intl.Collator(undefined, { + sensitivity: "variant", + numeric: true, + ignorePunctuation: false + }); + + source.sort((a, b) => { + const valueA = a.displayName || ''; + const valueB = b.displayName || ''; + + return isAsc + ? collator.compare(valueA, valueB) + : collator.compare(valueB, valueA); + }); + } else if (property === entityFields.createdTime.keyName) { + if (isAsc) { + source.reverse(); + } } } @@ -425,6 +454,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI source.pageLink = new PageLink(pageSize, 0, null, sortOrder); source.rowDataTemplate = {}; source.rowDataTemplate.Timestamp = null; + source.displayName = this.getTabLabel(datasource); if (this.showTimestamp) { source.displayedColumns.push('0'); } @@ -444,6 +474,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } } if (this.sources.length) { + this.sortDatasources(this.sources); this.sources.forEach((source, index) => { this.prepareDisplayedColumn(index); source.displayedColumns = this.displayedColumns[index].filter(value => value.display).map(value => value.def); @@ -514,7 +545,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI let title = ''; const header = this.sources[index].header.find(column => column.index.toString() === value); if (value === '0') { - title = 'Timestamp'; + title = this.translate.instant('widgets.table.timestamp-column-name'); } else if (value === 'actions') { title = 'Actions'; } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html index a41bf2ed82..a5a7bb955b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html @@ -16,32 +16,40 @@ -->
-
-
-
-
- -
-
-
-
+ @if (settings.showLabel) { +
+ } +
+
+
+ @if (settings.showTooltip) { + + } +
+
+ @for (mainTooltip of mainTooltips; track mainTooltip) { +
+ }
- +
+ @if (historicalData) { + + }
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 index bcd74e37f1..dc0161d0e3 100644 --- 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 @@ -94,6 +94,7 @@ import { SelectMapEntityPanelComponent } from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; +import { ApiUsageWidgetComponent } from "@home/components/widget/lib/cards/api-usage-widget.component"; @NgModule({ declarations: [ @@ -151,7 +152,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane ScadaSymbolWidgetComponent, SelectMapEntityPanelComponent, MapTimelinePanelComponent, - MapWidgetComponent + MapWidgetComponent, + ApiUsageWidgetComponent ], imports: [ CommonModule, @@ -214,7 +216,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane UnreadNotificationWidgetComponent, NotificationTypeFilterPanelComponent, ScadaSymbolWidgetComponent, - MapWidgetComponent + MapWidgetComponent, + ApiUsageWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: 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 index a792f50147..b7faecb1e4 100644 --- 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 @@ -16,208 +16,240 @@ -->
-
- - -
- - -
-
-
-
- -
-
- + @if (!hideHeader) { +
+ @if (!hideToggleHeader && widgetConfigMode === widgetConfigModes.advanced) { + + + } +
+ + +
-
-
-
widget-config.card-title
- - {{ 'widget-config.display-title' | translate }} - -
-
widget-config.title
-
+ } + @if (widgetConfigMode === widgetConfigModes.advanced) { +
+ @if (displayData) { +
+ +
+ } +
+ +
+
+
+
widget-config.card-title
+ + {{ 'widget-config.display-title' | translate }} + +
+
widget-config.title
+
+ @if (widgetType === widgetTypes.static || widgetType === widgetTypes.alarm) { + + + + } @else { + + } + + + + +
+
+
+
widget-config.title-tooltip
- + - - +
+
+ + {{ 'widget-config.display-icon' | translate }} + +
+ + + + + + + +
+
+ + + + widget-config.advanced-title-style + + + + + + +
+
+
widget-config.card-style
+
+
{{ 'widget-config.text-color' | translate }}
+ formControlName="color">
-
-
-
widget-config.title-tooltip
- - - -
-
- - {{ 'widget-config.display-icon' | translate }} - -
- - - - - +
+
{{ 'widget-config.background-color' | translate }}
+ formControlName="backgroundColor">
-
- - - - widget-config.advanced-title-style - - - - - - -
-
-
widget-config.card-style
-
-
{{ 'widget-config.text-color' | translate }}
- - -
-
-
{{ 'widget-config.background-color' | translate }}
- - -
-
-
{{ 'widget-config.padding' | translate }}
- - - -
-
-
{{ 'widget-config.margin' | translate }}
- - - -
-
-
{{ 'widget-config.border-radius' | translate }}
- - - -
- - {{ 'widget-config.drop-shadow' | translate }} - - - - - widget-config.advanced-widget-style - - - - - - - -
-
-
widget-config.card-buttons
- - {{ 'widget-config.enable-fullscreen' | translate }} - -
-
-
-
- - -
-
-
-
-
widget-config.resize-options
-
- - {{ 'widget-config.resizable' | translate }} +
+
{{ 'widget-config.padding' | translate }}
+ + + +
+
+
{{ 'widget-config.margin' | translate }}
+ + + +
+
+
{{ 'widget-config.border-radius' | translate }}
+ + + +
+ + {{ 'widget-config.drop-shadow' | translate }} + + + + widget-config.advanced-widget-style + + + + + + +
-
- - {{ 'widget-config.preserve-aspect-ratio' | translate }} +
+
widget-config.card-buttons
+ + {{ 'widget-config.enable-fullscreen' | translate }}
-
-
{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}
-
- - {{ 'widget-config.mobile-hide' | translate }} - -
-
- - {{ 'widget-config.desktop-hide' | translate }} - -
-
-
widget-config.order
- - - -
-
-
widget-config.height
- - - +
+
+ +
+
+ @if (!showLayoutConfig || isDefaultBreakpoint) { +
+
widget-config.resize-options
+
+ + {{ 'widget-config.resizable' | translate }} + +
+
+ + {{ 'widget-config.preserve-aspect-ratio' | translate }} + +
+
+ } + @if (showLayoutConfig) { +
+
{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}
+ @if (isDefaultBreakpoint) { +
+ + {{ 'widget-config.mobile-hide' | translate }} + +
+ } + @if (isDefaultBreakpoint) { +
+ + {{ 'widget-config.desktop-hide' | translate }} + +
+ } +
+
widget-config.order
+ + + +
+
+
widget-config.height
+ + + +
+
+ } +
-
- + } @else {
-
- -
{{basicModeDirectiveError}}
-
- + @if (hasBasicModeDirective) { +
+ + @if (basicModeDirectiveError) { +
{{basicModeDirectiveError}}
+ } +
+ } @else {
@@ -227,98 +259,122 @@
-
+ }
-
+ } - - - - -
-
-
alarm.filter
- + @if (displayTimewindowConfig) { + + + + + } + @if (widgetType === widgetTypes.alarm) { +
+
+
alarm.filter
+ +
+
- -
-
- - -
- - - - -
-
widget-config.alarm-source
- - -
-
-
widget-config.limits
-
-
widget-config.data-page-size
- - - + } + @if (widgetType !== widgetTypes.rpc && + widgetType !== widgetTypes.alarm && + modelValue?.isDataEnabled) { +
+ +
-
+ } + @if (widgetType === widgetTypes.rpc && + modelValue?.isDataEnabled) { + + + + + } + @if (widgetType === widgetTypes.alarm && modelValue?.isDataEnabled) { +
+
widget-config.alarm-source
+ + +
+ } + @if (displayLimits) { +
+
widget-config.limits
+
+
widget-config.data-page-size
+ + + +
+
+ } - - - - -
-
widget-config.data-settings
-
-
widget-config.units-by-default
- - -
-
-
widget-config.decimals-by-default
- - - + @if (!displayData && displayTimewindowConfig) { + + + + + } + @if (displayAppearanceDataSettings) { +
+
widget-config.data-settings
+ @if (displayUnitsConfig) { +
+
widget-config.units-by-default
+ + +
+ } + @if (displayUnitsConfig) { +
+
widget-config.decimals-by-default
+ + + +
+ } + @if (displayNoDataDisplayMessageConfig) { +
+
widget-config.no-data-display-message
+ + + +
+ }
-
-
widget-config.no-data-display-message
- - - + } + @if (displayAdvancedAppearance) { +
+ +
-
-
- - -
+ }
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 index a8fa966bd7..57b579d8e4 100644 --- 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 @@ -39,7 +39,10 @@ import { targetDeviceValid, Widget, WidgetConfigMode, - widgetType + widgetTitleAutocompleteValues, + widgetType, + widgetTypeCanHaveTimewindow, + widgetTypeHasTimewindow } from '@shared/models/widget.models'; import { AsyncValidator, @@ -53,7 +56,7 @@ import { Validators } from '@angular/forms'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { deepClone, genNextLabel, isDefined, isObject } from '@app/core/utils'; +import { deepClone, genNextLabel, isDefined, isDefinedAndNotNull, isObject } from '@app/core/utils'; import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models'; import { IAliasController } from '@core/api/widget-api.models'; import { EntityAlias } from '@shared/models/alias.models'; @@ -84,6 +87,9 @@ import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/co import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { WidgetService } from '@core/http/widget.service'; +import { TimeService } from '@core/services/time.service'; +import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; +import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; import Timeout = NodeJS.Timeout; @Component({ @@ -176,6 +182,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe headerOptions: ToggleHeaderOption[] = []; selectedOption: string; + predefinedValues = widgetTitleAutocompleteValues; public dataSettings: UntypedFormGroup; public targetDeviceSettings: UntypedFormGroup; @@ -202,6 +209,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe constructor(protected store: Store, private utils: UtilsService, private entityService: EntityService, + public timeService: TimeService, private dialog: MatDialog, public translate: TranslateService, private fb: UntypedFormBuilder, @@ -367,16 +375,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.advancedSettings = this.fb.group({}); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { + if (widgetTypeCanHaveTimewindow(this.widgetType)) { this.dataSettings.addControl('timewindowConfig', this.fb.control({ useDashboardTimewindow: true, displayTimewindow: true, timewindow: null, timewindowStyle: null })); - if (this.widgetType === widgetType.alarm) { - this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); - } + } + if (this.widgetType === widgetType.alarm) { + this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); } if (this.modelValue.isDataEnabled) { if (this.widgetType !== widgetType.rpc && @@ -530,14 +538,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe }, {emitEvent: false} ); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { + if (widgetTypeCanHaveTimewindow(this.widgetType)) { const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? config.useDashboardTimewindow : true; this.dataSettings.get('timewindowConfig').patchValue({ useDashboardTimewindow, displayTimewindow: isDefined(config.displayTimewindow) ? config.displayTimewindow : true, - timewindow: config.timewindow, + timewindow: isDefinedAndNotNull(config.timewindow) + ? config.timewindow + : initModelFromDefaultTimewindow(null, this.widgetType === widgetType.latest, this.onlyHistoryTimewindow(), + this.timeService, this.widgetType === widgetType.timeseries), timewindowStyle: config.timewindowStyle }, {emitEvent: false}); } @@ -726,11 +737,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } public get displayTimewindowConfig(): boolean { - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { + if (widgetTypeHasTimewindow(this.widgetType)) { return true; } else if (this.widgetType === widgetType.latest) { - const datasources = this.dataSettings.get('datasources').value; - return datasourcesHasAggregation(datasources); + const widgetDefinition = findWidgetModelDefinition(this.widget); + if (widgetDefinition) { + return widgetDefinition.datasourcesHasAggregation(this.widget); + } else { + const datasources = this.dataSettings.get('datasources').value; + return datasourcesHasAggregation(datasources); + } } } @@ -753,8 +769,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe public onlyHistoryTimewindow(): boolean { if (this.widgetType === widgetType.latest) { - const datasources = this.dataSettings.get('datasources').value; - return datasourcesHasOnlyComparisonAggregation(datasources); + const widgetDefinition = findWidgetModelDefinition(this.widget); + if (widgetDefinition) { + return widgetDefinition.datasourcesHasOnlyComparisonAggregation(this.widget); + } else { + const datasources = this.dataSettings.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } } else { return false; } @@ -763,7 +784,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe public generateDataKey(chip: any, type: DataKeyType, dataKeySettingsForm: FormProperty[], isLatestDataKey: boolean, dataKeySettingsFunction: DataKeySettingsFunction): DataKey { if (isObject(chip)) { - (chip as DataKey)._hash = Math.random(); return chip; } else { let label: string = chip; @@ -781,7 +801,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe label, color: this.genNextColor(), settings: {}, - _hash: Math.random() }; if (type === DataKeyType.function) { result.name = 'f(x)'; @@ -920,7 +939,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe if (this.modelValue) { const configuredColumns = new Array(); if (this.modelValue.config?.datasources[0]?.dataKeys?.length) { - configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys)); + const { + displayEntityLabel, + displayEntityName, + displayEntityType, + entityNameColumnTitle, + entityLabelColumnTitle + } = this.modelValue.config.settings; + const displayEntitiesArray = []; + if (displayEntityName) { + const displayName = entityNameColumnTitle ? entityNameColumnTitle : 'entityName'; + displayEntitiesArray.push({name: displayName, label: displayName}); + } + if (displayEntityLabel) { + const displayLabel = entityLabelColumnTitle ? entityLabelColumnTitle : 'entityLabel'; + displayEntitiesArray.push({name: displayLabel, label: displayLabel}); + } + if (displayEntityType) { + displayEntitiesArray.push({name: 'entityType', label: 'entityType'}); + } + configuredColumns.push(...displayEntitiesArray, ...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys)); } if (this.modelValue.config?.alarmSource?.dataKeys?.length) { configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.alarmSource.dataKeys)); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index e182371f2b..69cf1de06a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -28,15 +28,17 @@ [class.tb-edit]="isEdit || isEditingWidget" [class.tb-hover]="hovered" [style]="widget.style"> -
- - - - - - -
+ @if (!!widgetComponent.widgetContext?.inited) { +
+ @if (!widgetComponent.widgetContext?.embedTitlePanel) { + + } + @if (!widgetComponent.widgetContext?.embedActionsPanel) { + + } +
+ }
-
-
- {{widget.titleIcon}} -
- {{widget.title$ | async}} -
- -
- - -
+ @if (widget.showWidgetTitlePanel) { +
+ @if (widget.showTitle) { +
+ @if (widget.showTitleIcon) { + {{widget.titleIcon}} + } +
+ {{widget.title$ | async}} +
+ +
+ } + @if (widget.hasTimewindow) { + + + } +
+ }
-
-
- @for (action of widget.customHeaderActions; track action.name; let last = $last) { - + @if (widget.showWidgetActions) { +
+
+ @for (action of widget.customHeaderActions; track action.name; let last = $last) { + + } +
+ @for (action of widget.widgetActions; track action) { + } +
- - - -
+ } -@switch (action.buttonType) { - @case (widgetHeaderActionButtonType.miniFab) { - - } - @case (widgetHeaderActionButtonType.basic) { - - } - @case (widgetHeaderActionButtonType.raised) { - - } - @case (widgetHeaderActionButtonType.stroked) { - - } - @case (widgetHeaderActionButtonType.flat) { - - } - @default { - + @switch (action.buttonType) { + @case (widgetHeaderActionButtonType.miniFab) { + + } + @case (widgetHeaderActionButtonType.basic) { + + } + @case (widgetHeaderActionButtonType.raised) { + + } + @case (widgetHeaderActionButtonType.stroked) { + + } + @case (widgetHeaderActionButtonType.flat) { + + } + @default { + + } } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 1fedcbec13..4bdceedf7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -152,8 +152,8 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O this.widget.widgetContext.containerChangeDetector = this.cd; const cssString = this.widget.widget.config.widgetCss; if (isNotEmptyStr(cssString)) { - this.cssClass = - this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString); + this.cssClass = this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString, true); + this.widget.widgetContext.widgetCssClass = this.cssClass; } $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent)); $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); @@ -221,7 +221,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O widgetActionAbsolute(widgetComponent: WidgetComponent, absolute = false) { return absolute ? true : !(this.widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && - (this.widget.showTitle||this.widget.hasAggregation)) && !widgetComponent.widgetContext?.embedActionsPanel; + (this.widget.showTitle||this.widget.hasTimewindow)) && !widgetComponent.widgetContext?.embedActionsPanel; } onClicked(event: MouseEvent): void { @@ -397,37 +397,39 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O @Component({ template: `
-
- {{ 'widget.reference' | translate }} - -
+ @if (container.widget.isReference) { +
+ {{ 'widget.reference' | translate }} + +
+ }
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 index b0dbb95937..c66578afac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -21,13 +21,17 @@
-
+@if (widgetErrorData) { +
-
- {{ noDataDisplayMessageText }} -
+} +@if (displayNoData) { +
+ {{ noDataDisplayMessageText }} +
+}
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 index cb96a493a1..b04fad8caf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -38,6 +38,7 @@ import { } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { + MobileImageResult, Widget, WidgetAction, WidgetActionDescriptor, @@ -126,6 +127,7 @@ import { IModulesMap } from '@modules/common/modules-map.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; import { HttpClient } from '@angular/common/http'; +import { addDiagnosticChain } from '@angular/compiler-cli/src/ngtsc/diagnostics'; @Component({ selector: 'tb-widget', @@ -1224,12 +1226,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, switch (type) { case WidgetMobileActionType.takePictureFromGallery: case WidgetMobileActionType.takePhoto: + case WidgetMobileActionType.takeScreenshot: + argsObservable = of([mobileAction.saveToGallery]); + break; case WidgetMobileActionType.scanQrCode: case WidgetMobileActionType.getLocation: - case WidgetMobileActionType.takeScreenshot: - case WidgetMobileActionType.deviceProvision: argsObservable = of([]); break; + case WidgetMobileActionType.deviceProvision: + argsObservable = of([mobileAction.provisionType]); + break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: argsObservable = compileTbFunction(this.http, mobileAction.getLocationFunction, '$event', 'widgetContext', 'entityId', @@ -1299,6 +1305,10 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, case WidgetMobileActionType.takePhoto: case WidgetMobileActionType.takeScreenshot: const imageUrl = actionResult.imageUrl; + if (!additionalParams) { + additionalParams = {}; + } + additionalParams.imageInfo = actionResult.imageInfo; if (isNotEmptyTbFunction(mobileAction.processImageFunction)) { compileTbFunction(this.http, mobileAction.processImageFunction, 'imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel').subscribe( @@ -1423,6 +1433,23 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, ); } } + } else if (!this.mobileService.isMobileApp()) { + if (isNotEmptyTbFunction(mobileAction.handleNonMobileFallbackFunction)) { + compileTbFunction(this.http, mobileAction.handleNonMobileFallbackFunction, '$event', 'widgetContext',).subscribe( + { + next: (compiled) => { + try { + compiled.execute($event, this.widgetContext); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } + } + ); + } } } ); 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 index ef80df1e9a..01a83e5d11 100644 --- 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 @@ -25,8 +25,11 @@ close - - +@if (isLoading$ | async) { + +} @else { +
+}
- {{ 'device.wizard.device-details' | translate}} -
- + {{ 'device.wizard.device-details' | translate }} +
+ device.name - - {{ 'device.name-required' | translate }} - - - {{ 'device.name-max-length' | translate }} - + @if (deviceWizardFormGroup.get('name').hasError('required')) { + {{ 'device.name-required' | translate }} + } + @if (deviceWizardFormGroup.get('name').hasError('maxlength')) { + {{ 'device.name-max-length' | translate }} + } - + device.label - - {{ 'device.label-max-length' | translate }} - + @if (deviceWizardFormGroup.get('label').hasError('maxlength')) { + {{ 'device.label-max-length' | translate }} + } @@ -68,20 +72,22 @@ {{ 'device.is-gateway' | translate }} - - {{ 'device.overwrite-activity-time' | translate }} - + @if (deviceWizardFormGroup.get('gateway').value) { + + {{ 'device.overwrite-activity-time' | translate }} + + }
- + device.description - +
@@ -100,15 +106,18 @@
- + @if (selectedIndex > 0) { + + } - + @if (showNext) { + + }
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 index 9f4c873f35..19883b75ff 100644 --- 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 @@ -74,6 +74,7 @@ max-height: 100%; overflow-y: auto; scrollbar-gutter: stable; + padding-top: 4px; } } } 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 index 06f99fc498..c397f8b1a0 100644 --- 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 @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
{{ assignToCustomerText | translate }} @@ -35,7 +38,7 @@ formControlName="entityIds" required [entityType]="entityType" - > + >
diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html index 6a4be4aeb4..4aed4a41b8 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html @@ -25,15 +25,19 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
{{ assignToEdgeText | translate }} 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 index 4af3aa07a1..efa6ef3c1c 100644 --- 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 @@ -25,14 +25,18 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
{{ assignToCustomerText | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html similarity index 79% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html rename to ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html index b2c1a9ece9..0b337f0bd9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ 'calculated-fields.debugging' | translate}}

+

{{ data.title | translate}}

+ [class.!hidden]="displaySearchMode()" + class="mat-toolbar-tools flex-1">
@@ -67,18 +66,22 @@ mat-icon-button (click)="openSearch()"> search - + @if (fullscreenEnabled) { + + } - - + @if (!hideLoadingBar && (isLoading$ | async)) { + + + }
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 index 3d3823d12a..70ea95a05f 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.html @@ -16,6 +16,8 @@ --> - {{section.icon}} + @if (section.icon !== null) { + {{section.icon}} + } {{section.customTranslate ? (section.name | customTranslate) : (section.name | translate)}} 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 index 31753751af..ce475363df 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -16,13 +16,17 @@ --> - {{section.icon}} + @if (section.icon !== null) { + {{section.icon}} + } {{section.customTranslate ? (section.name | customTranslate) : (section.name | translate)}} + [class.tb-toggled]="section.opened">
    -
  • - -
  • + @for (page of section.pages; track trackBySectionPages($index, page)) { +
  • + +
  • + }
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 index 0921a59f14..34304319ae 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.html +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -16,8 +16,16 @@ -->
    -
  • - - -
  • + @for (section of menuSections$ | async; track trackByMenuSection($index, section)) { +
  • + @switch (section.type === 'link') { + @case (true) { + + } + @case (false) { + + } + } +
  • + }
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 index 1820eb0edd..081c4c5eab 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -15,14 +15,7 @@ /// import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2'; -import { - datasourcesHasAggregation, - datasourcesHasOnlyComparisonAggregation, - FormattedData, - Widget, - WidgetPosition, - widgetType -} from '@app/shared/models/widget.models'; +import { FormattedData, 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'; @@ -43,6 +36,11 @@ import { UtilsService } from '@core/services/utils.service'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { ComponentStyle, iconStyle, textStyle } from '@shared/models/widget-settings.models'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; +import { + widgetDatasourcesHasAggregation, + widgetDatasourcesHasOnlyComparisonAggregation, + widgetHasTimewindow +} from '@shared/models/widget/widget-model.definition'; export interface WidgetsData { widgets: Array; @@ -616,16 +614,13 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true; this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true; - let canHaveTimewindow = false; + const canHaveTimewindow = widgetHasTimewindow(this.widget); let onlyQuickInterval = false; let onlyHistoryTimewindow = false; - if (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) { - canHaveTimewindow = true; - } else if (this.widget.type === widgetType.latest) { - canHaveTimewindow = datasourcesHasAggregation(this.widget.config.datasources); - onlyQuickInterval = canHaveTimewindow; + if (this.widget.type === widgetType.latest) { + onlyQuickInterval = widgetDatasourcesHasAggregation(this.widget); if (canHaveTimewindow) { - onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(this.widget.config.datasources); + onlyHistoryTimewindow = widgetDatasourcesHasOnlyComparisonAggregation(this.widget); } } 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 index eaeade89ae..cc3112442f 100644 --- a/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts +++ b/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts @@ -115,9 +115,11 @@ export class AttributeDatasource implements DataSource { } isAllSelected(): Observable { - const numSelected = this.selection.selected.length; return this.attributesSubject.pipe( - map((attributes) => numSelected === attributes.length) + map((attributes) => { + const numSelected = this.selection.selected.length; + return attributes.length > 0 && numSelected === attributes.length + }) ); } 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 index 913e754fb7..f870150e32 100644 --- 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 @@ -51,6 +51,9 @@ export type HeaderCellStyleFunction> = (key: string) = export type CellStyleFunction> = (entity: T, key: string) => object; export type CopyCellContent> = (entity: T, key: string, length: number) => object; +export type EntityColumnsType = Array> & EntityLinkTableColumn> & EntityChipsEntityTableColumn>>>; +export type EntityColumnType = Partial> & EntityLinkTableColumn> & EntityChipsEntityTableColumn>>; + export enum CellActionDescriptorType { 'DEFAULT', 'COPY_BUTTON'} export interface CellActionDescriptor> { @@ -175,7 +178,7 @@ export class EntityTableConfig, P extends PageLink = P selectionEnabled = true; searchEnabled = true; addEnabled = true; - addAsTextButton = false; + addAsTextButton = true; entitiesDeleteEnabled = true; detailsPanelEnabled = true; hideDetailsTabsOnEdit = true; diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 224e2dc3f4..e913e2bd71 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -20,18 +20,23 @@ import { SafeHtml } from '@angular/platform-browser'; import { PageLink } from '@shared/models/page/page-link'; import { Timewindow } from '@shared/models/time/time.models'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; -import { ElementRef, EventEmitter, Renderer2, ViewContainerRef } from '@angular/core'; +import { ElementRef, EventEmitter, ViewContainerRef } from '@angular/core'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { - CellActionDescriptor, EntityActionTableColumn, EntityColumn, EntityTableColumn, + CellActionDescriptor, + EntityActionTableColumn, + EntityColumn, + EntityColumnsType, + EntityTableColumn, EntityTableConfig, GroupActionDescriptor, HeaderActionDescriptor } from '@home/models/entity/entities-table-config.models'; import { ActivatedRoute } from '@angular/router'; +import type { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; export type EntitiesTableAction = 'add'; @@ -42,7 +47,7 @@ export interface IEntitiesTableComponent { groupActionDescriptors: Array>>; cellActionDescriptors: Array>>; actionColumns: Array>>; - entityColumns: Array>>; + entityColumns: EntityColumnsType; displayedColumns: string[]; headerCellStyleCache: Array; cellContentCache: Array; @@ -64,6 +69,7 @@ export interface IEntitiesTableComponent { paginator: MatPaginator; sort: MatSort; route: ActivatedRoute; + entityDetailsPanel: EntityDetailsPanelComponent; viewContainerRef: ViewContainerRef; addEnabled(): boolean; 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 index 1840bcce9d..a9d57a0d41 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -38,6 +38,7 @@ import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; import { IAliasController, IStateController, + IWidgetHttpUtils, IWidgetSubscription, IWidgetUtils, RpcApi, @@ -58,6 +59,13 @@ import { ViewContainerRef } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { + createDefaultHttpOptions, + defaultHttpOptions, + defaultHttpOptionsFromConfig, + defaultHttpOptionsFromParams, + defaultHttpUploadOptions +} from '@core/http/http-utils'; import { RafService } from '@core/services/raf.service'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { TenantId } from '@shared/models/id/tenant-id'; @@ -68,7 +76,8 @@ import { formatValue, getEntityDetailsPageURL, hasDatasourceLabelsVariables, - isDefined + isDefined, + isDefinedAndNotNull } from '@core/utils'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -147,6 +156,7 @@ export interface WidgetAction extends IWidgetAction { export interface IDashboardWidget { updateWidgetParams(): void; + updateParamsFromData(detectChanges?: boolean): void; } export class WidgetContext { @@ -293,6 +303,14 @@ export class WidgetContext { getEntityDetailsPageURL }; + httpUtils: IWidgetHttpUtils = { + defaultHttpOptions, + defaultHttpOptionsFromConfig, + defaultHttpOptionsFromParams, + defaultHttpUploadOptions, + createDefaultHttpOptions + }; + $widgetElement: JQuery; $container: JQuery; $containerParent: JQuery; @@ -307,6 +325,8 @@ export class WidgetContext { widgetNamespace?: string; subscriptionApi?: WidgetSubscriptionApi; + widgetCssClass?: string; + actionsApi?: WidgetActionsApi; activeEntityInfo?: SubscriptionEntityInfo; @@ -478,6 +498,10 @@ export class WidgetContext { } } + updateParamsFromData(detectChanges = false) { + this.dashboardWidget.updateParamsFromData(detectChanges); + } + updateAliases(aliasIds?: Array) { this.aliasController.updateAliases(aliasIds); } @@ -548,7 +572,9 @@ export class LabelVariablePattern { const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); label = createLabelFromSubscriptionEntityInfo(entityInfo, label); } else { - const datasource = this.ctx.defaultSubscription?.firstDatasource; + const datasource = isDefinedAndNotNull(this.ctx.defaultSubscription) + ? this.ctx.defaultSubscription.firstDatasource ?? undefined + : (this.ctx as any).mapInstance?.getData()[0]; label = createLabelFromDatasource(datasource, label); } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html index c18d59a5c8..9948f20cf7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html @@ -15,9 +15,10 @@ limitations under the License. --> - - - +@if (!(hasRepository$ | async)) { + + +} @else { - +} 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 index 4b122194e8..0142c83a85 100644 --- 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 @@ -23,19 +23,24 @@
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + admin.base-url - - {{ 'admin.base-url-required' | translate }} - + @if (generalSettings.get('baseUrl').hasError('required')) { + + {{ 'admin.base-url-required' | translate }} + + }
@@ -63,9 +68,12 @@
admin.device-connectivity.device-connectivity
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
@@ -78,38 +86,42 @@
- + @if (protocol === 'http') { - - + } + @if (protocol === 'mqtt') { - - + } + @if (protocol === 'coap') { - + }
{{ 'admin.device-connectivity.' + protocol | translate }}
- + admin.device-connectivity.host - + admin.device-connectivity.port - - {{ 'admin.device-connectivity.port-pattern' | translate }} - - - {{ 'admin.device-connectivity.port-range' | translate }} - + @if (deviceConnectivitySettingsForm.get(protocol + '.port').hasError('pattern')) { + + {{ 'admin.device-connectivity.port-pattern' | translate }} + + } + @if (deviceConnectivitySettingsForm.get(protocol + '.port').hasError('min') || + deviceConnectivitySettingsForm.get(protocol + '.port').hasError('max')) { + + {{ 'admin.device-connectivity.port-range' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html index 9ebfd0369b..25c9adfea0 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html @@ -22,9 +22,13 @@ admin.home-settings - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
@@ -32,6 +36,7 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- + admin.mail-from - - {{ 'admin.mail-from-required' | translate }} - + @if (mailSettings.get('mailFrom').hasError('required')) { + + {{ 'admin.mail-from-required' | translate }} + + } - + admin.oauth2.smtp-provider - - {{ templates.get(provider)?.name || 'Custom' }} - + @for (provider of templateProvider; track provider) { + + {{ templates.get(provider)?.name || 'Custom' }} + + } @@ -54,99 +62,123 @@ - + admin.smtp-protocol - - {{protocol.toUpperCase()}} - + @for (protocol of smtpProtocols; track protocol) { + + {{protocol.toUpperCase()}} + + }
- + admin.smtp-host - - {{ 'admin.smtp-host-required' | translate }} - + @if (mailSettings.get('smtpHost').hasError('required')) { + + {{ 'admin.smtp-host-required' | translate }} + + } - + admin.smtp-port {{smtpPortInput.value?.length || 0}}/5 - - {{ 'admin.smtp-port-required' | translate }} - - - {{ 'admin.smtp-port-invalid' | translate }} - + @if (mailSettings.get('smtpPort').hasError('required')) { + + {{ 'admin.smtp-port-required' | translate }} + + } + @if (mailSettings.get('smtpPort').hasError('pattern') || mailSettings.get('smtpPort').hasError('maxlength')) { + + {{ 'admin.smtp-port-invalid' | translate }} + + }
- + admin.timeout-msec {{timeoutInput.value?.length || 0}}/6 - - {{ 'admin.timeout-required' | translate }} - - - {{ 'admin.timeout-invalid' | translate }} - + @if (mailSettings.get('timeout').hasError('required')) { + + {{ 'admin.timeout-required' | translate }} + + } + @if (mailSettings.get('timeout').hasError('pattern') || mailSettings.get('timeout').hasError('maxlength')) { + + {{ 'admin.timeout-invalid' | translate }} + + } {{ 'admin.enable-tls' | translate }} - - admin.tls-version - - - {{ tlsVersion }} - - - + @if (mailSettings.get('enableTls').value) { + + admin.tls-version + + @for (tlsVersion of tlsVersions; track tlsVersion) { + + {{ 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 - - - + @if (mailSettings.get('enableProxy').value) { +
+
+ + admin.proxy-host + + @if (mailSettings.get('proxyHost').hasError('required')) { + + {{ 'admin.proxy-host-required' | translate }} + + } + + + admin.proxy-port + + @if (mailSettings.get('proxyPort').hasError('required')) { + + {{ 'admin.proxy-port-required' | translate }} + + } + @if (mailSettings.get('proxyPort').hasError('pattern') + || mailSettings.get('proxyPort').hasError('min') + || mailSettings.get('proxyPort').hasError('max')) { + + {{ 'admin.proxy-port-range' | translate }} + + } + +
+
+ + admin.proxy-user + + + + admin.proxy-password + + + +
-
+ }
admin.oauth2.authentication - + common.username @@ -156,161 +188,197 @@ {{ 'admin.oauth2.basic' | translate }} {{ 'admin.oauth2.oauth2' | translate }} -
-
-
+ @if (helpLink && mailSettings.get('enableOauth2').value) { +
+
+
+ }
-
- - {{ 'admin.change-password' | translate }} - - - common.password - - - -
-
-
- - admin.oauth2.client-id - - - {{ 'admin.oauth2.client-id-required' | translate }} - - - {{ 'admin.oauth2.client-id-max-length' | translate }} - - - - - admin.oauth2.client-secret - - - {{ 'admin.oauth2.client-secret-required' | translate }} - - - {{ 'admin.oauth2.client-secret-max-length' | translate }} - - -
- - - admin.oauth2.microsoft-tenant-id - - - {{ 'admin.oauth2.microsoft-tenant-id-required' | translate }} - - - - - - tenant-profile.advanced-settings - - - -
- - admin.oauth2.authorization-uri - - - - {{ 'admin.oauth2.access-token-uri-required' | translate }} + @if (!mailSettings.get('enableOauth2').value) { +
+ @if (showChangePassword) { + + {{ 'admin.change-password' | translate }} + + } + @if (mailSettings.get('changePassword').value || !showChangePassword) { + + common.password + + + + } +
+ } + @if (mailSettings.get('enableOauth2').value) { +
+
+ + admin.oauth2.client-id + + @if (mailSettings.get('clientId').hasError('required')) { + + {{ 'admin.oauth2.client-id-required' | translate }} - - {{ 'admin.oauth2.uri-pattern-error' | translate }} + } + @if (mailSettings.get('clientId').hasError('maxlen gth')) { + + {{ 'admin.oauth2.client-id-max-length' | translate }} - - - - admin.oauth2.token-uri - - - - {{ 'admin.oauth2.access-token-uri-required' | translate }} + } + + + admin.oauth2.client-secret + + @if (mailSettings.get('clientSecret').hasError('required')) { + + {{ 'admin.oauth2.client-secret-required' | translate }} - - {{ 'admin.oauth2.uri-pattern-error' | translate }} + } + @if (mailSettings.get('clientSecret').hasError('maxlength')) { + + {{ 'admin.oauth2.client-secret-max-length' | translate }} - -
- - - - - -
- admin.oauth2.redirect-uri -
-
-
- - admin.oauth2.protocol - - - {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} - - + } + +
+ @if (mailSettings.get('providerId').value === mailServerOauth2Provider.OFFICE_365) { + + admin.oauth2.microsoft-tenant-id + + @if (mailSettings.get('providerTenantId').hasError('required')) { + + {{ 'admin.oauth2.microsoft-tenant-id-required' | translate }} + + } + + } + + + + tenant-profile.advanced-settings + + + +
+ + admin.oauth2.authorization-uri + + + @if (mailSettings.get('authUri').hasError('required')) { + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + } + @if (mailSettings.get('authUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + } - - admin.domain-name - - - {{ 'admin.error-verification-url' | translate }} - - - {{ 'admin.domain-name-max-length' | translate }} + + admin.oauth2.token-uri + + + @if (mailSettings.get('tokenUri').hasError('required')) { + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + } + @if (mailSettings.get('tokenUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + } + +
+ + +
+
+
+ admin.oauth2.redirect-uri +
+
+
+ + admin.oauth2.protocol + + @for (protocol of protocols; track protocol) { + + {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} + + } + + + + admin.domain-name + + @if (domainForm.get('name').hasError('pattern')) { + + {{ 'admin.error-verification-url' | translate }} + + } + @if (domainForm.get('name').hasError('maxlength')) { + + {{ 'admin.domain-name-max-length' | translate }} + + } + +
+ @if (domainForm.hasError('unique')) { + + {{ 'admin.domain-name-unique' | translate }} + } +
+
+ + admin.oauth2.redirect-uri-template + + +
- - {{ 'admin.domain-name-unique' | translate }} - -
- -
- - admin.oauth2.redirect-uri-template - - - -
+
+
+ } + @if (mailSettings.get('enableOauth2').value) { +
+
+ admin.oauth2.access-token-status + + {{ accessTokenStatus }} +
-
-
-
-
- admin.oauth2.access-token-status - - {{ accessTokenStatus }} - -
- -
+ +
+ }
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html index 0fc66255e4..3e02fae438 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html @@ -17,57 +17,73 @@ -->
- + admin.oauth2.title - - {{ 'admin.oauth2.client-title-required' | translate }} - - - {{ 'admin.oauth2.client-title-max-length' | translate }} - + @if (entityForm.get('title').hasError('required')) { + + {{ 'admin.oauth2.client-title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'admin.oauth2.client-title-max-length' | translate }} + + }
- + admin.oauth2.provider - - {{ provider }} - + @for (provider of templateProvider; track provider) { + + {{ provider }} + + }
- + admin.oauth2.allowed-platforms - - {{ platformTypeTranslations.get(platform) | translate }} - + @for (platform of platformTypes; track platform) { + + {{ platformTypeTranslations.get(platform) | translate }} + + }
- + admin.oauth2.client-id - - {{ 'admin.oauth2.client-id-required' | translate }} - - - {{ 'admin.oauth2.client-id-max-length' | translate }} - + @if (entityForm.get('clientId').hasError('required')) { + + {{ 'admin.oauth2.client-id-required' | translate }} + + } + @if (entityForm.get('clientId').hasError('maxlength')) { + + {{ 'admin.oauth2.client-id-max-length' | translate }} + + } - + admin.oauth2.client-secret - - {{ 'admin.oauth2.client-secret-required' | translate }} - - - {{ 'admin.oauth2.client-secret-max-length' | translate }} - + @if (entityForm.get('clientSecret').hasError('required')) { + + {{ 'admin.oauth2.client-secret-required' | translate }} + + } + @if (entityForm.get('clientSecret').hasError('maxlength')) { + + {{ 'admin.oauth2.client-secret-max-length' | translate }} + + }
@@ -85,233 +101,291 @@ {{ 'admin.oauth2.mapper' | 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.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.user-name-attribute-name - - - {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} - - -
-
-
- - admin.oauth2.type - - - {{ mapperType }} - - - -
-
-
- - admin.oauth2.email-attribute-key - - - {{ 'admin.oauth2.email-attribute-key-required' | translate }} + @if (generalSettingsMode) { +
+
+ + admin.oauth2.access-token-uri + + @if (entityForm.get('accessTokenUri').hasError('required')) { + + {{ 'admin.oauth2.access-token-uri-required' | translate }} - - {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} + } + @if (entityForm.get('accessTokenUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} - -
-
- - admin.oauth2.first-name-attribute-key - - - {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} + } + + + admin.oauth2.authorization-uri + + @if (entityForm.get('authorizationUri').hasError('required')) { + + {{ 'admin.oauth2.authorization-uri-required' | translate }} - - - admin.oauth2.last-name-attribute-key - - - {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} + } + @if (entityForm.get('authorizationUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} - -
-
- - admin.oauth2.tenant-name-strategy - - - {{ tenantNameStrategy }} - - - - - admin.oauth2.tenant-name-pattern - - - {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + } + +
+
+ + admin.oauth2.jwk-set-uri + + @if (entityForm.get('jwkSetUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} - - {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} + } + + + admin.oauth2.user-info-uri + + @if (entityForm.get('userInfoUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + } + +
+
+ + admin.oauth2.client-authentication-method + + @for (clientAuthenticationMethod of clientAuthenticationMethods; track clientAuthenticationMethod) { + + {{ clientAuthenticationMethod | uppercase }} + + } + + +
+ @if (isCustomProvider()) { +
+ + admin.oauth2.login-button-label + + @if (entityForm.get('loginButtonLabel').hasError('required')) { + + {{ 'admin.oauth2.login-button-label-required' | translate }} + + } +
+
admin.oauth2.login-button-icon
+ + +
-
- - admin.oauth2.customer-name-pattern - - - {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} - - + } +
+
+ + {{ 'admin.oauth2.allow-user-creation' | translate }} +
-
- - admin.oauth2.default-dashboard-name - - - {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} - - - - {{ 'admin.oauth2.always-fullscreen' | translate}} +
+ + {{ 'admin.oauth2.activate-user' | translate }}
-
-
- - admin.oauth2.url - - - {{ 'admin.oauth2.url-required' | translate }} - - - {{ 'admin.oauth2.url-pattern' | translate }} - - - {{ 'admin.oauth2.url-max-length' | translate }} - - -
-
- - common.username - - - {{ 'admin.oauth2.username-max-length' | translate }} - - - - common.password - - - {{ 'admin.oauth2.password-max-length' | translate }} + + +
+ } + @if (!generalSettingsMode) { +
+
+ + admin.oauth2.user-name-attribute-name + + @if (entityForm.get('userNameAttributeName').hasError('required')) { + + {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} + } + +
+
+
+ + admin.oauth2.type + + @for (mapperType of mapperTypes; track mapperType) { + + {{ mapperType }} + + } +
- - {{ 'admin.oauth2.send-token' | translate}} - + @if (entityForm.get('mapperConfig.type').value !== mapperType.CUSTOM && entityForm.get('mapperConfig.basic')) { +
+ @if (entityForm.get('mapperConfig.type').value !== mapperType.GITHUB) { +
+ + admin.oauth2.email-attribute-key + + @if (entityForm.get('mapperConfig.basic.emailAttributeKey').hasError('required')) { + + {{ 'admin.oauth2.email-attribute-key-required' | translate }} + + } + @if (entityForm.get('mapperConfig.basic.emailAttributeKey').hasError('maxlength')) { + + {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} + + } + +
+ } +
+ + admin.oauth2.first-name-attribute-key + + @if (entityForm.get('mapperConfig.basic.firstNameAttributeKey').hasError('maxlength')) { + + {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} + + } + + + admin.oauth2.last-name-attribute-key + + @if (entityForm.get('mapperConfig.basic.lastNameAttributeKey').hasError('maxlength')) { + + {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} + + } + +
+
+ + admin.oauth2.tenant-name-strategy + + @for (tenantNameStrategy of tenantNameStrategies; track tenantNameStrategy) { + + {{ tenantNameStrategy }} + + } + + + + admin.oauth2.tenant-name-pattern + + @if (entityForm.get('mapperConfig.basic.tenantNamePattern').hasError('required')) { + + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + + } + @if (entityForm.get('mapperConfig.basic.tenantNamePattern').hasError('maxlength')) { + + {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} + + } + +
+
+ + admin.oauth2.customer-name-pattern + + @if (entityForm.get('mapperConfig.basic.customerNamePattern').hasError('maxlength')) { + + {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} + + } + +
+
+ + admin.oauth2.default-dashboard-name + + @if (entityForm.get('mapperConfig.basic.defaultDashboardName').hasError('maxlength')) { + + {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} + + } + + + {{ 'admin.oauth2.always-fullscreen' | translate}} + +
+
+ } + @if (entityForm.get('mapperConfig.type').value === mapperType.CUSTOM && entityForm.get('mapperConfig.custom')) { +
+
+ + admin.oauth2.url + + @if (entityForm.get('mapperConfig.custom.url').hasError('required')) { + + {{ 'admin.oauth2.url-required' | translate }} + + } + @if (entityForm.get('mapperConfig.custom.url').hasError('pattern')) { + + {{ 'admin.oauth2.url-pattern' | translate }} + + } + @if (entityForm.get('mapperConfig.custom.url').hasError('maxlength')) { + + {{ 'admin.oauth2.url-max-length' | translate }} + + } + +
+
+ + common.username + + @if (entityForm.get('mapperConfig.custom.username').hasError('maxlength')) { + + {{ 'admin.oauth2.username-max-length' | translate }} + + } + + + common.password + + @if (entityForm.get('mapperConfig.custom.password').hasError('maxlength')) { + + {{ 'admin.oauth2.password-max-length' | translate }} + + } + +
+ + {{ 'admin.oauth2.send-token' | translate}} + +
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts index e882f5c35c..f5027e9eba 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts @@ -85,8 +85,8 @@ export class DomainTableConfigResolver { this.config.loadEntity = id => this.domainService.getDomainInfoById(id.id); this.config.saveEntity = (domain, originalDomain) => { const clientsIds = domain.oauth2ClientInfos as Array || []; - const shouldUpdateClients = domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), - originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort()); + const shouldUpdateClients = domain.id && !isEqual(domain.oauth2ClientInfos, + originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info)); delete domain.oauth2ClientInfos; return this.domainService.saveDomain(domain, domain.id ? null : clientsIds).pipe( diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html index 0816cb6f5a..dc0726e68d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html @@ -17,22 +17,28 @@ -->
- + admin.oauth2.domain-name - - {{ 'admin.oauth2.domain-name-required' | translate }} - - - {{ 'admin.domain-name-max-length' | translate }} - - - {{ 'admin.error-verification-url' | translate }} - + @if (entityForm.get('name').hasError('required')) { + + {{ 'admin.oauth2.domain-name-required' | translate }} + + } + @if (entityForm.get('name').hasError('maxlength')) { + + {{ 'admin.domain-name-max-length' | translate }} + + } + @if (entityForm.get('name').hasError('pattern')) { + + {{ 'admin.error-verification-url' | translate }} + + }
- + admin.oauth2.redirect-url-template
-
- + admin.jwt.expiration-time - - {{ 'admin.jwt.expiration-time-required' | translate }} - - - {{ 'admin.jwt.expiration-time-max' | translate }} - - - {{ 'admin.jwt.expiration-time-min' | translate }} - + @if (jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('required')) { + + {{ 'admin.jwt.expiration-time-required' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('max')) { + + {{ 'admin.jwt.expiration-time-max' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('min')) { + + {{ 'admin.jwt.expiration-time-min' | translate }} + + } - + admin.jwt.refresh-expiration-time - - {{ 'admin.jwt.refresh-expiration-time-required' | translate }} - - - {{ 'admin.jwt.refresh-expiration-time-max' | translate }} - - - {{ 'admin.jwt.refresh-expiration-time-min' | translate }} - - - {{ 'admin.jwt.refresh-expiration-time-less-token' | translate }} - + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('required')) { + + {{ 'admin.jwt.refresh-expiration-time-required' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('max')) { + + {{ 'admin.jwt.refresh-expiration-time-max' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('min')) { + + {{ 'admin.jwt.refresh-expiration-time-min' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('lessToken')) { + + {{ 'admin.jwt.refresh-expiration-time-less-token' | 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 index 406cd966e0..93eff35f8c 100644 --- 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 @@ -22,14 +22,18 @@ margin: 0; } - .fields-group { - padding: 8px 16px 0; - margin: 10px 0; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; + .mat-mdc-card.settings-card { + fieldset { + .fields-group { + padding: 8px 16px 0; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; - legend { - color: rgba(0, 0, 0, .7); + legend { + color: rgba(0, 0, 0, .7); + } + } } } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html index c36923dc84..96975f3f70 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html @@ -25,25 +25,33 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + admin.sms-message - - {{ 'admin.sms-message-required' | translate }} - - - {{ 'admin.sms-message-max-length' | translate }} - + @if (sendTestSmsFormGroup.get('message').hasError('required')) { + + {{ 'admin.sms-message-required' | translate }} + + } + @if (sendTestSmsFormGroup.get('message').hasError('maxLength')) { + + {{ 'admin.sms-message-max-length' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html index 2c38ad4ab8..a83292d2ea 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html @@ -15,35 +15,41 @@ limitations under the License. --> - - - - admin.sms-provider-settings - - -
-
- - -
- - - - -
- - -
- -
-
+@if (isSysAdmin()) { + + + + admin.sms-provider-settings + + +
+
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ } + +
+ + +
+ + +
+
+
+
+}
@@ -53,32 +59,40 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ } - + admin.slack-api-token - - - admin.mobile-settings - - - - - - - - + @if (isSysAdmin()) { + + + admin.mobile-settings + + + + + } + @if (isSysAdmin()) { + + + + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index d276377526..46495abd5e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -24,173 +24,257 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
-
-
- admin.2fa.available-providers - - - - - - {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} - - - - - - - - - admin.2fa.issuer-name - - - {{ "admin.2fa.issuer-name-required" | translate }} - - - -
- - admin.2fa.verification-message-template - - - {{ "admin.2fa.verification-message-template-required" | translate }} - - - {{ "admin.2fa.verification-message-template-pattern" | translate }} - - - - - admin.2fa.verification-code-lifetime - - - {{ "admin.2fa.verification-code-lifetime-required" | translate }} - - - {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} - - -
-
- - admin.2fa.verification-code-lifetime - - - {{ "admin.2fa.verification-code-lifetime-required" | translate }} - - - {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} - - -
-
- - admin.2fa.number-of-codes - - - {{ "admin.2fa.number-of-codes-required" | translate }} - - - {{ "admin.2fa.number-of-codes-pattern" | translate }} - - -
-
-
-
- -
-
-
- admin.2fa.verification-limitations -
- - admin.2fa.total-allowed-time-for-verification - - - {{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }} - - - {{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }} - - - - admin.2fa.retry-verification-code-period - - - {{ 'admin.2fa.retry-verification-code-period-required' | translate }} - - - {{ 'admin.2fa.retry-verification-code-period-pattern' | translate }} - - - - admin.2fa.max-verification-failures-before-user-lockout - - - {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} - - -
- - +
+
+ - - + + + {{ 'admin.2fa.force-2fa' | translate }} - {{ 'admin.2fa.verification-code-check-rate-limit' | translate }} -
- - admin.2fa.number-of-checking-attempts - - - {{ 'admin.2fa.number-of-checking-attempts-required' | translate }} - - - {{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }} - - - - admin.2fa.within-time - - - {{ 'admin.2fa.within-time-required' | translate }} - - - {{ 'admin.2fa.within-time-pattern' | translate }} - +
+ + admin.2fa.enforce-for + + @for (type of notificationTargetConfigTypes; track type) { + + {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} + + } + -
+ @if (twoFaFormGroup.get('enforcedUsersFilter.type').value === notificationTargetConfigType.TENANT_ADMINISTRATORS) { +
+
+ + {{ 'tenant.tenant' | translate }} + {{ 'tenant-profile.tenant-profile' | translate }} + +
+ @if (twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value) { + + + } @else { + + + } +
+ } +
-
+ + +
+
admin.2fa.available-providers
+ @if (!atListOneProvider) { + + } + @for (provider of providersForm.controls; track trackByElement(i, provider); let i = $index) { +
+ + + + + {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} + + + + + @switch (provider.get('providerType').value) { + @case (twoFactorAuthProviderType.TOTP) { + + admin.2fa.issuer-name + + @if (provider.get('issuerName').hasError('required') || + provider.get('issuerName').hasError('pattern')) { + + {{ "admin.2fa.issuer-name-required" | translate }} + + } + + } + @case (twoFactorAuthProviderType.SMS) { +
+ + admin.2fa.verification-message-template + + @if (provider.get('smsVerificationMessageTemplate').hasError('required')) { + + {{ "admin.2fa.verification-message-template-required" | translate }} + + } + @if (provider.get('smsVerificationMessageTemplate').hasError('pattern')) { + + {{ "admin.2fa.verification-message-template-pattern" | translate }} + + } + + + +
+ } + @case (twoFactorAuthProviderType.EMAIL) { +
+ + +
+ } + @case (twoFactorAuthProviderType.BACKUP_CODE) { +
+ + admin.2fa.number-of-codes + + @if (provider.get('codesQuantity').hasError('required')) { + + {{ "admin.2fa.number-of-codes-required" | translate }} + + } + @if (provider.get('codesQuantity').hasError('min') || + provider.get('codesQuantity').hasError('pattern')) { + + {{ "admin.2fa.number-of-codes-pattern" | translate }} + + } + +
+ } + } +
+
+
+ } +
+
+
admin.2fa.verification-limitations
+
+
+ + + + + + admin.2fa.max-verification-failures-before-user-lockout + + @if (twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('pattern') + || twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('min') + || twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')) { + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} + + } + +
+
+ + + + + {{ 'admin.2fa.verification-code-check-rate-limit' | translate }} + + + + +
+ + admin.2fa.number-of-checking-attempts + + @if (twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')) { + + {{ 'admin.2fa.number-of-checking-attempts-required' | translate }} + + } + @if (twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern') + || twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')) { + + {{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }} + + } + + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss index feb312c0f2..9107f5947d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss @@ -71,13 +71,6 @@ } :host ::ng-deep { - - .mat-mdc-form-field { - .mat-mdc-form-field-infix { - width: 100%; - } - } - .mat-expansion-panel { .mat-expansion-panel-content { font-size: 16px; diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index a70996291d..f27976f07d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { Component, DestroyRef, OnInit, QueryList, ViewChildren } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { Store } from '@ngrx/store'; @@ -28,10 +28,11 @@ import { TwoFactorAuthSettings, TwoFactorAuthSettingsForm } from '@shared/models/two-factor-auth.models'; -import { isNotEmptyStr } from '@core/utils'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { isDefined, isNotEmptyStr } from '@core/utils'; import { MatExpansionPanel } from '@angular/material/expansion'; +import { NotificationTargetConfigType, NotificationTargetConfigTypeInfoMap } from '@shared/models/notification.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'tb-2fa-settings', @@ -39,22 +40,29 @@ import { MatExpansionPanel } from '@angular/material/expansion'; styleUrls: ['./settings-card.scss', './two-factor-auth-settings.component.scss'], standalone: false }) -export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { +export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { - private readonly destroy$ = new Subject(); private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]; twoFaFormGroup: UntypedFormGroup; twoFactorAuthProviderType = TwoFactorAuthProviderType; twoFactorAuthProvidersData = twoFactorAuthProvidersData; + notificationTargetConfigType = NotificationTargetConfigType; + notificationTargetConfigTypes: NotificationTargetConfigType[] = this.allowNotificationTargetConfigTypes(); + notificationTargetConfigTypeInfoMap = NotificationTargetConfigTypeInfoMap; + + filterByTenants: boolean; + entityType = EntityType; + showMainLoadingBar = false; @ViewChildren(MatExpansionPanel) expansionPanel: QueryList; constructor(protected store: Store, private twoFaService: TwoFactorAuthenticationService, - private fb: UntypedFormBuilder) { + private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { super(store); } @@ -62,15 +70,11 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI this.build2faSettingsForm(); this.twoFaService.getTwoFaSettings().subscribe((setting) => { this.setAuthConfigFormValue(setting); + this.twoFaFormGroup.markAsUntouched(); + this.twoFaFormGroup.markAsPristine(); }); } - ngOnDestroy() { - super.ngOnDestroy(); - this.destroy$.next(); - this.destroy$.complete(); - } - confirmForm(): UntypedFormGroup { return this.twoFaFormGroup; } @@ -81,7 +85,15 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI this.joinRateLimit(setting, 'verificationCodeCheckRateLimit'); const providers = setting.providers.filter(provider => provider.enable); providers.forEach(provider => delete provider.enable); - const config = Object.assign(setting, {providers}); + const enforcedUsersFilter = this.twoFaFormGroup.get('enforcedUsersFilter').value; + if (enforcedUsersFilter.filterByTenants) { + enforcedUsersFilter.tenantProfilesIds = null; + } else { + enforcedUsersFilter.tenantsIds = null; + } + delete enforcedUsersFilter.filterByTenants; + const config = Object.assign(setting, {providers}, {enforcedUsersFilter}); + this.filterByTenants = this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value; this.twoFaService.saveTwoFaSettings(config).subscribe( (settings) => { this.setAuthConfigFormValue(settings); @@ -118,6 +130,13 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI private build2faSettingsForm(): void { this.twoFaFormGroup = this.fb.group({ + enforceTwoFa: [false], + enforcedUsersFilter: this.fb.group({ + type: [NotificationTargetConfigType.ALL_USERS], + filterByTenants: [true], + tenantsIds: [], + tenantProfilesIds: [] + }), maxVerificationFailuresBeforeUserLockout: [30, [ Validators.pattern(/^\d*$/), Validators.min(0), @@ -131,14 +150,14 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI verificationCodeCheckRateLimitEnable: [false], verificationCodeCheckRateLimitNumber: [{value: 3, disabled: true}, this.posIntValidation], verificationCodeCheckRateLimitTime: [{value: 900, disabled: true}, this.posIntValidation], - minVerificationCodeSendPeriod: ['30', [Validators.required, Validators.min(5), Validators.pattern(/^\d*$/)]], + minVerificationCodeSendPeriod: [30, [Validators.required, Validators.min(5), Validators.pattern(/^\d*$/)]], providers: this.fb.array([]) }); Object.values(TwoFactorAuthProviderType).forEach(provider => { this.buildProvidersSettingsForm(provider); }); this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe( - takeUntil(this.destroy$) + takeUntilDestroyed(this.destroyRef) ).subscribe(value => { if (value) { this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false}); @@ -149,7 +168,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI } }); this.providersForm.valueChanges.pipe( - takeUntil(this.destroy$) + takeUntilDestroyed(this.destroyRef) ).subscribe((value: TwoFactorAuthProviderConfigForm[]) => { const activeProvider = value.filter(provider => provider.enable); const indexBackupCode = Object.values(TwoFactorAuthProviderType).indexOf(TwoFactorAuthProviderType.BACKUP_CODE); @@ -162,6 +181,22 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI this.providersForm.at(indexBackupCode).get('enable').enable( {emitEvent: false}); } }); + this.twoFaFormGroup.get('enforceTwoFa').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => { + if (value) { + this.twoFaFormGroup.get('enforcedUsersFilter').enable({emitEvent: false}); + } else { + this.twoFaFormGroup.get('enforcedUsersFilter').disable({emitEvent: false}); + } + }); + } + + get atListOneProvider():boolean { + if (this.twoFaFormGroup.get('enforceTwoFa').value) { + return this.providersForm.value.some(value => value.enable); + } + return true; } private setAuthConfigFormValue(settings: TwoFactorAuthSettings) { @@ -173,19 +208,24 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI verificationCodeCheckRateLimitTime: checkRateLimitTime || 900, providers: [] }); + if (settings?.enforceTwoFa) { + this.getByIndexPanel(0).open(); + } if (checkRateLimitNumber > 0) { - this.getByIndexPanel(this.providersForm.length).open(); + this.getByIndexPanel(this.providersForm.length+1).open(); } Object.values(TwoFactorAuthProviderType).forEach((provider, index) => { const findIndex = allowProvidersConfig.indexOf(provider); if (findIndex > -1) { processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true})); - this.getByIndexPanel(index).open(); + this.getByIndexPanel(index+1).open(); } else { processFormValue.providers.push({enable: false}); } }); this.twoFaFormGroup.patchValue(processFormValue); + this.filterByTenants = isDefined(this.filterByTenants) ? this.filterByTenants : !Array.isArray(settings?.enforcedUsersFilter?.tenantProfilesIds); + this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').patchValue(this.filterByTenants, {onlySelf: true}); } private buildProvidersSettingsForm(provider: TwoFactorAuthProviderType) { @@ -213,7 +253,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI } const newProviders = this.fb.group(formControlConfig); newProviders.get('enable').valueChanges.pipe( - takeUntil(this.destroy$) + takeUntilDestroyed(this.destroyRef) ).subscribe(value => { if (value) { newProviders.enable({emitEvent: false}); @@ -246,4 +286,12 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI delete processFormValue[`${property}Number`]; delete processFormValue[`${property}Time`]; } + + private allowNotificationTargetConfigTypes(): NotificationTargetConfigType[] { + return [ + NotificationTargetConfigType.ALL_USERS, + NotificationTargetConfigType.TENANT_ADMINISTRATORS, + NotificationTargetConfigType.SYSTEM_ADMINISTRATORS + ]; + } } diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts index 2a73488d7e..7c78bd7c02 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts @@ -47,7 +47,6 @@ export class AiModelsTableConfigResolver { ) { this.config.selectionEnabled = true; this.config.entityType = EntityType.AI_MODEL; - this.config.addAsTextButton = true; this.config.rowPointer = true; this.config.detailsPanelEnabled = false; this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL); diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts index 4e220dd53c..75dd645b40 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts @@ -14,39 +14,130 @@ /// limitations under the License. /// -import { Injectable, NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { DestroyRef, inject, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; -import { Observable } from 'rxjs'; -import { OAuth2Service } from '@core/http/oauth2.service'; import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component'; import { AlarmsMode } from '@shared/models/alarm.models'; import { MenuId } from '@core/services/menu.models'; +import { RouterTabsComponent } from "@home/components/router-tabs.component"; +import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component"; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DatePipe } from '@angular/common'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { UtilsService } from '@core/services/utils.service'; +import { AlarmRulesTableConfig } from '@home/components/alarm-rules/alarm-rules-table-config'; -@Injectable() -export class OAuth2LoginProcessingUrlResolver { - - constructor(private oauth2Service: OAuth2Service) { - } - - resolve(): Observable { - return this.oauth2Service.getLoginProcessingUrl(); - } -} +export const AlarmRulesTableConfigResolver: ResolveFn = + (_route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot, + alarmRulesService = inject(AlarmRulesService), + translate = inject(TranslateService), + dialog = inject(MatDialog), + store = inject(Store), + datePipe = inject(DatePipe), + destroyRef = inject(DestroyRef), + importExportService = inject(ImportExportService), + entityDebugSettingsService = inject(EntityDebugSettingsService), + utilsService = inject(UtilsService), + router = inject(Router), + ) => { + return new AlarmRulesTableConfig( + alarmRulesService, + translate, + dialog, + datePipe, + null, + store, + destroyRef, + null, + null, + null, + importExportService, + entityDebugSettingsService, + utilsService, + router, + true, + ); + }; const routes: Routes = [ { path: 'alarms', - component: AlarmTableComponent, + component: RouterTabsComponent, data: { auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], - title: 'alarm.alarms', breadcrumb: { - menuId: MenuId.alarms + menuId: MenuId.alarms_center + } + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + redirectTo: '/alarms/alarms' + } + }, + { + path: 'alarms', + component: AlarmTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'alarm.alarms', + breadcrumb: { + menuId: MenuId.alarms + }, + isPage: true, + alarmsMode: AlarmsMode.ALL + } }, - isPage: true, - alarmsMode: AlarmsMode.ALL - } + { + path: 'alarm-rules', + data: { + breadcrumb: { + menuId: MenuId.alarm_rules + }, + }, + children: [ + { + path: '', + component: AlarmRulesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'alarm-rule.alarm-rules', + isPage: true, + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'tune' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN], + title: 'entity.type-calculated-fields', + }, + resolve: { + entitiesTableConfig: AlarmRulesTableConfigResolver + } + } + ] + } + ] } ]; diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.html b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.html new file mode 100644 index 0000000000..9b38e2bc59 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.html @@ -0,0 +1,30 @@ + +@if (entity) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts new file mode 100644 index 0000000000..c26624bebc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import type { + AlarmRulesTableConfig, + AlarmRuleTableEntity +} from '@home/components/alarm-rules/alarm-rules-table-config'; +import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; + +@Component({ + selector: 'tb-alarm-rules-tabs', + templateUrl: './alarm-rules-tabs.component.html', + styleUrls: [], + standalone: false +}) +export class AlarmRulesTabsComponent extends EntityTabsComponent { + + readonly DebugEventType = DebugEventType; + readonly EventType = EventType; + + constructor() { + super(); + } + + get debugActionDisabled(): boolean { + return !debugCfActionEnabled(this.entity); + }; + + onDebugEventSelected(event: CalculatedFieldEventBody) { + (this.entitiesTableConfig as AlarmRulesTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments)) + .subscribe((_expression) => { }); + }; +} diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts index 9438e743d1..4e77b8f5ff 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts @@ -20,9 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { AlarmRoutingModule } from '@home/pages/alarm/alarm-routing.module'; +import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.component'; @NgModule({ - declarations: [], + declarations: [ + AlarmRulesTabsComponent + ], imports: [ CommonModule, SharedModule, diff --git a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html index 3218330929..c14adbf8be 100644 --- a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html @@ -15,17 +15,29 @@ limitations under the License. --> - - - - - - - - - +@if (entity && !isEdit) { + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + } + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + + } +} diff --git a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts index 2204850bc5..e7c96b68a8 100644 --- a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts +++ b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts @@ -29,7 +29,7 @@ import { AssetProfileRoutingModule } from './asset-profile-routing.module'; CommonModule, SharedModule, HomeComponentsModule, - AssetProfileRoutingModule + AssetProfileRoutingModule, ] }) export class AssetProfileModule { } 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 index 69b1aa3abe..ce680334da 100644 --- 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 @@ -16,6 +16,7 @@ --> - - - - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + + + + } + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + + + + + } +} 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 index 10ef434755..a75f20dfca 100644 --- 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 @@ -19,6 +19,7 @@ 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'; +import { EntityId } from "@shared/models/id/entity-id"; @Component({ selector: 'tb-asset-tabs', @@ -28,6 +29,8 @@ import { AssetInfo } from '@app/shared/models/asset.models'; }) export class AssetTabsComponent extends EntityTabsComponent { + ownerId: EntityId; + constructor(protected store: Store) { super(store); } @@ -36,4 +39,9 @@ export class AssetTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + protected setEntity(entity: AssetInfo) { + this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId; + super.setEntity(entity); + } + } 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 index 6230fcae0d..837342ec57 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -64,7 +64,7 @@
- asset.assignedToCustomer @@ -75,42 +75,45 @@ {{ 'asset.asset-public' | translate }}
-
- +
+ asset.name - - {{ 'asset.name-required' | translate }} - - - {{ 'asset.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'asset.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'asset.name-max-length' | translate }} + } - + asset.label - - {{ 'asset.label-max-length' | translate }} - + @if (entityForm.get('label').hasError('maxlength')) { + {{ 'asset.label-max-length' | translate }} + } - - + @if (isAdd) { + + + }
- + asset.description - +
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 index d6dcf2b0b4..ec85d828ac 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.module.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.module.ts @@ -35,7 +35,7 @@ import { AssetTabsComponent } from '@home/pages/asset/asset-tabs.component'; SharedModule, HomeComponentsModule, HomeDialogsModule, - AssetRoutingModule + AssetRoutingModule, ] }) export class AssetModule { } 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 index 2a4cd70c90..b9943eb41f 100644 --- 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 @@ -25,7 +25,7 @@ export const auditLogsRoutes: Routes = [ path: 'auditLogs', component: AuditLogTableComponent, data: { - auth: [Authority.TENANT_ADMIN], + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'audit-log.audit-logs', breadcrumb: { menuId: MenuId.audit_log 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 index 8670592897..502014d798 100644 --- 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 @@ -20,8 +20,7 @@ import { SharedModule } from '@shared/shared.module'; import { AuditLogRoutingModule } from '@modules/home/pages/audit-log/audit-log-routing.module'; @NgModule({ - declarations: [ - ], + declarations: [], imports: [ CommonModule, SharedModule, diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-field-page.module.ts b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-field-page.module.ts new file mode 100644 index 0000000000..c9db9860fa --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-field-page.module.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { CalculatedFieldsRoutingModule } from '@home/pages/calculated-fields/calculated-fields-routing.module'; +import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module'; +import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component'; + +@NgModule({ + declarations: [ + CalculatedFieldsTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + CalculatedFieldsRoutingModule, + ] +}) +export class CalculatedFieldPageModule { } \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts new file mode 100644 index 0000000000..20fe941cb1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts @@ -0,0 +1,113 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DestroyRef, inject, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DatePipe } from '@angular/common'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { UtilsService } from '@core/services/utils.service'; + +export const CalculatedFieldsTableConfigResolver: ResolveFn = + (_route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot, + calculatedFieldsService = inject(CalculatedFieldsService), + translate = inject(TranslateService), + dialog = inject(MatDialog), + store = inject(Store), + datePipe = inject(DatePipe), + destroyRef = inject(DestroyRef), + importExportService = inject(ImportExportService), + entityDebugSettingsService = inject(EntityDebugSettingsService), + utilsService = inject(UtilsService), + router = inject(Router), + ) => { + return new CalculatedFieldsTableConfig( + calculatedFieldsService, + translate, + dialog, + datePipe, + null, + store, + destroyRef, + null, + null, + null, + importExportService, + entityDebugSettingsService, + utilsService, + router, + true, + ); + }; + +const routes: Routes = [ + { + path: 'calculatedFields', + data: { + breadcrumb: { + menuId: MenuId.calculated_fields + } + }, + children: [ + { + path: '', + component: CalculatedFieldsTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'entity.type-calculated-fields', + isPage: true, + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'mdi:function-variant' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN], + title: 'entity.type-calculated-fields', + }, + resolve: { + entitiesTableConfig: CalculatedFieldsTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [] +}) +export class CalculatedFieldsRoutingModule { } \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.html b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.html new file mode 100644 index 0000000000..9b38e2bc59 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.html @@ -0,0 +1,30 @@ + +@if (entity) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts new file mode 100644 index 0000000000..8e441a5088 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import type { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; + +@Component({ + selector: 'tb-calculated-fields-tabs', + templateUrl: './calculated-fields-tabs.component.html', + styleUrls: [], + standalone: false +}) +export class CalculatedFieldsTabsComponent extends EntityTabsComponent { + + readonly DebugEventType = DebugEventType; + readonly EventType = EventType; + + constructor() { + super(); + } + + get debugActionDisabled(): boolean { + return !debugCfActionEnabled(this.entity); + }; + + onDebugEventSelected(event: CalculatedFieldEventBody) { + (this.entitiesTableConfig as CalculatedFieldsTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments), false) + .subscribe((expression) => { + (this.entitiesTableConfig as CalculatedFieldsTableConfig).getTable(); + const entityDetailsPanel = this.entitiesTableConfig.getTable().entityDetailsPanel; + entityDetailsPanel.onToggleEditMode(true); + entityDetailsPanel.selectedTab = 0; + setTimeout(() => { + entityDetailsPanel.detailsForm.get('configuration').setValue({...this.entity.configuration, expression}); + entityDetailsPanel.detailsForm.get('configuration').markAsDirty(); + }); + }); + }; +} 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 index 8f9dc70b06..d6f3379ead 100644 --- 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 @@ -15,43 +15,50 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + } + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + + + + + + } +} 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 index b96e574cef..cb771237d1 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html @@ -46,13 +46,14 @@ [class.!hidden]="isEdit"> {{'customer.manage-dashboards' | translate }} - + @if (edgesSupportEnabled()) { + + } + + + @if (isLoading$ | async) { + + + } @else { +
+ } +
+ + +
+ +
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts new file mode 100644 index 0000000000..336043907f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -0,0 +1,90 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { DashboardService } from '@core/http/dashboard.service'; +import { Dashboard } from '@app/shared/models/dashboard.models'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface DashboardInfoDialogData { + dashboard: Dashboard; +} + +@Component({ + selector: 'tb-import-dashboard-file-dialog', + templateUrl: './import-dashboard-file-dialog.component.html', + styleUrls: [], + standalone: false +}) +export class ImportDashboardFileDialogComponent extends DialogComponent implements OnInit { + + private dashboard: Dashboard; + currentFileName: string = ''; + uploadFileFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData, + private dashboardService: DashboardService, + protected dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.dashboard = data.dashboard; + } + + ngOnInit(): void { + this.uploadFileFormGroup = this.fb.group({ + file: [null] + }); + } + + cancel(): void { + this.dialogRef.close(); + } + + save() { + const fileControl = this.uploadFileFormGroup.get('file'); + if (!fileControl || !fileControl.value) { + return; + } + + const dashboardContent = { + ...fileControl.value, + description: this.dashboard.configuration.description + }; + this.dashboard.configuration = dashboardContent; + + this.dashboardService.saveDashboard(this.dashboard).subscribe(() => { + this.dialogRef.close(true); + }) + } + + loadDataFromJsonContent(content: string): any { + try { + const importData = JSON.parse(content); + return importData ? importData['configuration'] : importData; + } catch (err) { + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); + return null; + } + } +} 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 index 43f0f3dc6b..e21f996584 100644 --- 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 @@ -25,13 +25,14 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- - +
{{ publicLink }}
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
{{ labelText | translate }} 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 index 27433a3f2f..76349148fd 100644 --- 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 @@ -15,71 +15,113 @@ limitations under the License. --> - -
- - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - - -
- - -
-
-
- - - - -
-
- -
-
-
- -
-
- - -
-
-
- -
-
- - +@if (entity) { + + @if (transportType.isActive) { +
+ + device-profile.transport-type + + @for (type of deviceTransportTypes; track type) { + + {{deviceTransportTypeTranslations.get(type) | translate}} + + } + + @if (detailsForm.get('transportType').value) { + + {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}} + + } + @if (detailsForm.get('transportType').hasError('required')) { + + {{ 'device-profile.transport-type-required' | translate }} + + } + +
+ + +
+
+ } +
+ @if (authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + + + + } + + @if (hasOldRules || authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + +
+ @if (hasOldRules && !isEdit) { +
+ + {{ 'alarm-rule.alarm-rules-actual' | translate }} + {{ 'alarm-rule.alarm-rules-old' | translate }} + +
+ } + @if (alarmRulesOldVersion || isEdit) { + @if (alarmRules.isActive) { +
+
+ +
+
+ } + } @else { +
+ + +
+ } +
+
+ } + + + @if (deviceProvisioning.isActive) { +
+
+ + +
+
+ } +
+ @if (!isEdit) { + + + + } + @if (authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + + + + + } +} +@if (false) { + +
+
+ + +
-
- - - - - - - + +} 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 index 1a762ac881..0560f88174 100644 --- 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 @@ -42,6 +42,9 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent, private destroyRef: DestroyRef) { super(store); @@ -58,6 +61,8 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent
-
- - - - +
+ @switch (type) { + @case (deviceProfileType.DEFAULT) { + + + } + }
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 index d5690a9cdc..cb9b8eed57 100644 --- 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 @@ -17,27 +17,31 @@ -->
- - - -
device.device-configuration
-
-
- - -
- - - -
device.transport-configuration
-
-
- - -
+ @if (displayDeviceConfiguration) { + + + +
device.device-configuration
+
+
+ + +
+ } + @if (displayTransportConfiguration) { + + + +
device.transport-configuration
+
+
+ + +
+ }
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 index b18974839e..41f2007ce9 100644 --- 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 @@ -16,36 +16,38 @@ -->
-
- - - - - - - - - - - - - - - - - - - - +
+ @switch (transportType) { + @case (deviceTransportType.DEFAULT) { + + + } + @case (deviceTransportType.MQTT) { + + + } + @case (deviceTransportType.LWM2M) { + + + } + @case (deviceTransportType.COAP) { + + + } + @case (deviceTransportType.SNMP) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html index 631c28ad98..ec05b5ae77 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html @@ -17,117 +17,149 @@ -->
- + device-profile.snmp.host - - {{ 'device-profile.snmp.host-required' | translate }} - + @if (snmpDeviceTransportConfigurationFormGroup.get('host').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('host').hasError('pattern')) { + + {{ 'device-profile.snmp.host-required' | translate }} + + } - + device-profile.snmp.port - - {{ 'device-profile.snmp.port-required' | translate }} - - - {{ 'device-profile.snmp.port-format' | translate }} - + @if (snmpDeviceTransportConfigurationFormGroup.get('port').hasError('required')) { + + {{ 'device-profile.snmp.port-required' | translate }} + + } + @if (snmpDeviceTransportConfigurationFormGroup.get('port').hasError('pattern') || + snmpDeviceTransportConfigurationFormGroup.get('port').hasError('min')) { + + {{ 'device-profile.snmp.port-format' | translate }} + + }
- + device-profile.snmp.protocol-version - - {{ snmpDeviceProtocolVersion | lowercase }} - + @for (snmpDeviceProtocolVersion of snmpDeviceProtocolVersions; track snmpDeviceProtocolVersion) { + + {{ snmpDeviceProtocolVersion | lowercase }} + + } - - {{ 'device-profile.snmp.protocol-version-required' | translate }} - - -
- - device-profile.snmp.community - - - {{ 'device-profile.snmp.community-required' | translate }} + @if (snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').hasError('required')) { + + {{ 'device-profile.snmp.protocol-version-required' | translate }} - -
-
-
- - device-profile.snmp.user-name - - - {{ 'device-profile.snmp.user-name-required' | translate }} - - - - device-profile.snmp.security-name - - - {{ 'device-profile.snmp.security-name-required' | translate }} - - -
-
- - device-profile.snmp.authentication-protocol - - - {{ snmpAuthenticationProtocolTranslation.get(snmpAuthenticationProtocol) }} - - - - {{ 'device-profile.snmp.authentication-protocol-required' | translate }} - - - - device-profile.snmp.authentication-passphrase - - - {{ 'device-profile.snmp.authentication-passphrase-required' | translate }} - - -
-
- - device-profile.snmp.privacy-protocol - - - {{ snmpPrivacyProtocolTranslation.get(snmpPrivacyProtocol) }} - - - - {{ 'device-profile.snmp.privacy-protocol-required' | translate }} - - - - device-profile.snmp.privacy-passphrase - - - {{ 'device-profile.snmp.privacy-passphrase-required' | translate }} - - -
-
- - device-profile.snmp.context-name - - - - device-profile.snmp.engine-id - + } + + @if (!isV3protocolVersion()) { +
+ + device-profile.snmp.community + + @if (snmpDeviceTransportConfigurationFormGroup.get('community').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('community').hasError('pattern')) { + + {{ 'device-profile.snmp.community-required' | translate }} + + } -
-
+
+ } + @if (isV3protocolVersion()) { +
+
+ + device-profile.snmp.user-name + + @if (snmpDeviceTransportConfigurationFormGroup.get('username').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('username').hasError('pattern')) { + + {{ 'device-profile.snmp.user-name-required' | translate }} + + } + + + device-profile.snmp.security-name + + @if (snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('pattern')) { + + {{ 'device-profile.snmp.security-name-required' | translate }} + + } + +
+
+ + device-profile.snmp.authentication-protocol + + @for (snmpAuthenticationProtocol of snmpAuthenticationProtocols; track snmpAuthenticationProtocol) { + + {{ snmpAuthenticationProtocolTranslation.get(snmpAuthenticationProtocol) }} + + } + + @if (snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').hasError('required')) { + + {{ 'device-profile.snmp.authentication-protocol-required' | translate }} + + } + + + device-profile.snmp.authentication-passphrase + + @if (snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('pattern')) { + + {{ 'device-profile.snmp.authentication-passphrase-required' | translate }} + + } + +
+
+ + device-profile.snmp.privacy-protocol + + @for (snmpPrivacyProtocol of snmpPrivacyProtocols; track snmpPrivacyProtocol) { + + {{ snmpPrivacyProtocolTranslation.get(snmpPrivacyProtocol) }} + + } + + @if (snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').hasError('required')) { + + {{ 'device-profile.snmp.privacy-protocol-required' | translate }} + + } + + + device-profile.snmp.privacy-passphrase + + @if (snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('pattern')) { + + {{ 'device-profile.snmp.privacy-passphrase-required' | translate }} + + } + +
+
+ + device-profile.snmp.context-name + + + + device-profile.snmp.engine-id + + +
+
+ } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html index fd30184330..20ef481b4a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html @@ -18,7 +18,7 @@

{{ dialogTitle }}

- +
-
- - - {{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }} - - -
- - -
device.connectivity.use-following-instructions
- - - - - Windows - - -
-
-
device.connectivity.install-necessary-client-tools
-
device.connectivity.install-curl-windows
-
- - -
-
-
- - - - MacOS - - -
-
-
device.connectivity.install-necessary-client-tools
-
device.connectivity.install-curl-macos
-
- - -
-
-
- - - - Linux - - -
-
-
device.connectivity.install-necessary-client-tools
- -
- - -
-
-
-
-
- -
- - -
- + @if (loadedCommand) { +
+ + @if (allowTransportType.has(BasicTransportType.HTTP)) { + + {{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.MQTT)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.COAP)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.SNMP)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.LWM2M)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }} + + } + +
+ @switch (transportProtocol.value) { + @case (BasicTransportType.HTTP) {
device.connectivity.use-following-instructions
- + @@ -146,18 +79,13 @@
device.connectivity.install-necessary-client-tools
-
- - -
+
device.connectivity.install-curl-windows
+ *ngTemplateOutlet="executeCommand; context: {cmd: { + noSecLabel: 'HTTP', noSec: commands.http.http, + secLabel: 'HTTPs', sec: commands.http.https, + doc: {text: '', href: ''}}}">
@@ -171,14 +99,13 @@
device.connectivity.install-necessary-client-tools
- +
device.connectivity.install-curl-macos
+ *ngTemplateOutlet="executeCommand; context: {cmd: { + noSecLabel: 'HTTP', noSec: commands.http.http, + secLabel: 'HTTPs', sec: commands.http.https, + doc: {text: '', href: ''}}}">
@@ -193,146 +120,235 @@
device.connectivity.install-necessary-client-tools
+ [data]='createMarkDownCommand("sudo apt-get install curl")'>
+ *ngTemplateOutlet="executeCommand; context: {cmd: { + noSecLabel: 'HTTP', noSec: commands.http.http, + secLabel: 'HTTPs', sec: commands.http.https, + doc: {text: '', href: ''}}}">
- - - - Docker - - -
- + } + @case (DeviceTransportType.MQTT) { + @if (commands.mqtt.sparkplug) { +
+ + +
+ } @else { +
device.connectivity.use-following-instructions
+ + + + + Windows + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ @if (!!commands.mqtt.docker) { + + + + Docker + + +
+ - -
+
+
+
+
+ } + + } + } + @case (DeviceTransportType.COAP) { +
device.connectivity.use-following-instructions
+ + + + + Linux - - - - - -
device.connectivity.use-following-instructions
- - - - - Linux - - -
-
-
device.connectivity.install-necessary-client-tools
-
+ +
+ @if (commands.coap.coaps !== 'Check documentation') { +
+
device.connectivity.install-necessary-client-tools
+
- -
-
- + +
+
+ } + - -
- - - - - - Docker - - -
- +
+
+
+ @if (!!commands.coap.docker) { + + + + Docker + + +
+ - -
-
-
- - - -
- +
+
+ + } + + } + @case (DeviceTransportType.SNMP) { +
+ - -
- - -
- +
+ } + @case (DeviceTransportType.LWM2M) { +
+ - -
-
- -
-
-
-
device.state
-
- {{ (status ? 'device.active' : 'device.inactive') | translate }} -
+ +
+ } + }
-
attribute.latest-telemetry
-
-
-
device.time
-
attribute.key
-
attribute.value
+
+
+
device.state
+
+ {{ (status ? 'device.active' : 'device.inactive') | translate }} +
-
-
-
{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}
-
{{ telemetry.key }}
-
{{ telemetry.value }}
+
attribute.latest-telemetry
+
+
+
device.time
+
attribute.key
+
attribute.value
+ @if (latestTelemetry.length) { +
+ @for (telemetry of latestTelemetry; track telemetry) { +
+
{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}
+
{{ telemetry.key }}
+
{{ telemetry.value }}
+
+ } +
+ } @else { +
+
+
attribute.no-latest-telemetry
+
+ }
+
+ } @else { +
+ + + {{ 'device.connectivity.loading-check-connectivity-command' | translate }} +
-
+ }
{{ 'action.dont-show-again' | translate}} + (click)="close()">{{ 'action.close' | translate }}
- -
- - - {{ 'device.connectivity.loading-check-connectivity-command' | translate }} - -
-
- -
-
-
attribute.no-latest-telemetry
-
-
@@ -344,24 +360,25 @@ {{ cmd.secLabel }}
- - + @switch (protocolType.value) { + @case ('noSec') { - - -
- -
- + [data]=createMarkDownCommand(cmd.noSec)> + } + @case ('sec') { + @if (cmd.sec !== 'Check documentation') { +
+ +
+ } @else { -
-
-
+ } + } + }
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 index b0f26e1ccf..5f679a5389 100644 --- 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 @@ -24,25 +24,28 @@ close - - +@if ((isLoading$ | async)) { + + +}
-
-
- - -
-
- + @if (!loadingCredentials) { +
+
+ + +
+
+ } @else {
{{ 'device.loading-device-credentials' | translate }}
-
+ }
- + {{ 'action.save' | translate }} + + }
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 index 9278d20f50..007976282e 100644 --- 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 @@ -15,47 +15,48 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + + + + } + + + + + + + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + } +} 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 index d1729e5fb4..96c23db9d2 100644 --- 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 @@ -19,6 +19,7 @@ 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'; +import { EntityId } from "@shared/models/id/entity-id"; @Component({ selector: 'tb-device-tabs', @@ -28,6 +29,8 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone }) export class DeviceTabsComponent extends EntityTabsComponent { + ownerId: EntityId; + constructor(protected store: Store) { super(store); } @@ -36,4 +39,9 @@ export class DeviceTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + protected setEntity(entity: DeviceInfo) { + this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId; + super.setEntity(entity); + } + } 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 index 07606331cf..3da91d1e44 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -83,7 +83,7 @@
- device.assignedToCustomer @@ -94,37 +94,39 @@ {{ 'device.device-public' | translate }}
-
- +
+ device.name - - {{ 'device.name-required' | translate }} - - - {{ 'device.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'device.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'device.name-max-length' | translate }} + } - + device.label - - {{ 'device.label-max-length' | translate }} - + @if (entityForm.get('label').hasError('maxlength')) { + {{ 'device.label-max-length' | translate }} + } {{ 'device.is-gateway' | translate }} - - {{ 'device.overwrite-activity-time' | translate }} - + @if (entityForm.get('additionalInfo.gateway').value) { + + {{ 'device.overwrite-activity-time' | translate }} + + } - + device.description - +
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html index ed523cf98d..9376ff1b3e 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html @@ -25,64 +25,65 @@
-
-
- - - - - Docker - - -
-
- + @if (loadedInstructions) { +
+
+ + + + + Docker + + +
+
+ +
-
- - - - - - Ubuntu - - -
-
- + + + + + + Ubuntu + + +
+
+ +
-
- - - - - - CentOS/RHEL - - -
-
- + + + + + + CentOS/RHEL + + +
+
+ +
-
- - - + + + +
+
+ } @else { +
+ + + {{ 'edge.loading-edge-instructions' | translate }} +
-
+ }
{{ 'action.dont-show-again' | translate}} + (click)="close()">{{ 'action.close' | translate }}
- -
- - - {{ 'edge.loading-edge-instructions' | translate }} - -
-
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html index b912757027..3f298aff3d 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html @@ -15,42 +15,56 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 83f95c71e2..25fe5e2c0c 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -114,30 +114,27 @@
-
- - - - - - -
+ @if (!upgradeAvailable) { + + } @else { + + }
- edge.assignedToCustomer @@ -148,57 +145,60 @@ {{ 'edge.edge-public' | translate }}
-
- +
+ edge.name - - {{ 'edge.name-required' | translate }} - - - {{ 'edge.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'edge.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'edge.name-max-length' | translate }} + }
- - edge.edge-key - - -
- - edge.edge-secret - - -
-
- +
+ edge.label - - {{ 'edge.label-max-length' | translate }} - + @if (entityForm.get('label').hasError('maxlength')) { + {{ 'edge.label-max-length' | translate }} + }
- + edge.description - +
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 index d2ca9dcfe9..c62c5ce8d4 100644 --- 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 @@ -16,6 +16,7 @@ --> - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} 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 index 15b0b3da8b..06c1470ff6 100644 --- 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 @@ -64,7 +64,7 @@
- entity-view.assignedToCustomer @@ -75,29 +75,30 @@ {{ 'entity-view.entity-view-public' | translate }}
-
- +
+ entity-view.name - - {{ 'entity-view.name-required' | translate }} - - - {{ 'entity-view.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'entity-view.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'entity-view.name-max-length' | translate }} + } + [entityType]="entityType.ENTITY_VIEW">
- + + formControlName="entityId" + appearance="outline">
@@ -109,26 +110,26 @@
entity-view.attributes-propagation-hint
- + label="entity-view.client-attributes" + [dataKeyType]="dataKeyType.attribute" + appearance="outline"> - + label="entity-view.shared-attributes" + [dataKeyType]="dataKeyType.attribute" + appearance="outline"> - + label="entity-view.server-attributes" + [dataKeyType]="dataKeyType.attribute" + appearance="outline"> @@ -138,12 +139,12 @@
entity-view.timeseries-data-hint
- + label="entity-view.timeseries" + [dataKeyType]="dataKeyType.timeseries" + appearance="outline">
@@ -152,16 +153,18 @@ dateText="{{ 'entity-view.start-ts' | translate }}" [maxDate]="maxStartTimeMs | async" formControlName="startTimeMs" + appearance="outline" >
- + entity-view.description - +
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 index d3269fae53..848430c0cf 100644 --- 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 @@ -15,25 +15,30 @@ limitations under the License. --> - - +@if (homeDashboard) { + +} @else { - - - - {{section.name}} - - - - - - {{place.icon}} - {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} - - - - - - + @for (section of homeSections$| async; track section) { + + + + {{section.name}} + + + + @for (place of section.places; track place) { + + + {{place.icon}} + {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} + + + } + + + + + } - +} 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 index 84412fabf4..a4b7df8739 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -27,6 +27,7 @@ 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 { CalculatedFieldPageModule } from '@home/pages/calculated-fields/calculated-field-page.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'; @@ -69,6 +70,7 @@ import { AiModelModule } from '@home/pages/ai-model/ai-model.module'; EdgeModule, EntityViewModule, CustomerModule, + CalculatedFieldPageModule, RuleChainModule, WidgetLibraryModule, DashboardModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html index d984ac99c9..79bd13c8b1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html @@ -26,9 +26,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts index 2ee6465d33..7a9f72b7aa 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts @@ -29,6 +29,7 @@ import { MobileAppService } from '@core/http/mobile-app.service'; export interface MobileAppDialogData { platformType: PlatformType; + name?: string } @Component({ @@ -56,6 +57,9 @@ export class MobileAppDialogComponent extends DialogComponent { this.mobileAppComponent.entityForm.markAsDirty(); + if (this.data.name) { + this.mobileAppComponent.entityForm.get('title').patchValue(this.data.name, {emitEvent: false}); + } this.mobileAppComponent.entityForm.patchValue({platformType: this.data.platformType}); this.mobileAppComponent.entityForm.get('platformType').disable({emitEvent: false}); this.mobileAppComponent.isEdit = true; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts index 13698ac4a5..274edf5933 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -57,7 +57,6 @@ export class MobileAppTableConfigResolver { ) { this.config.selectionEnabled = false; this.config.entityType = EntityType.MOBILE_APP; - this.config.addAsTextButton = true; this.config.entitiesDeleteEnabled = false; this.config.rowPointer = true; this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html index bf8bbacc83..0e88ff03a6 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -28,30 +28,40 @@ icon="content_copy"> - - {{ 'mobile.mobile-package-required' | translate }} - - - {{ 'mobile.mobile-package-max-length' | translate }} - - - {{ 'mobile.mobile-package-pattern' | translate }} - + @if (entityForm.get('pkgName').hasError('required')) { + + {{ 'mobile.mobile-package-required' | translate }} + + } + @if (entityForm.get('pkgName').hasError('maxlength')) { + + {{ 'mobile.mobile-package-max-length' | translate }} + + } + @if (entityForm.get('pkgName').hasError('pattern')) { + + {{ 'mobile.mobile-package-pattern' | translate }} + + }
mobile.mobile-package-title - - {{ 'mobile.mobile-package-title-max-length' | translate }} - + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'mobile.mobile-package-title-max-length' | translate }} + + } mobile.platform-type - - {{ platformTypeTranslations.get(platformType) | translate }} - + @for (platformType of platformTypes; track platformType) { + + {{ platformTypeTranslations.get(platformType) | translate }} + + } @@ -59,14 +69,15 @@
- + @if (isEdit) { + + }
- - {{ 'mobile.application-secret-required' | translate }} - + @if (entityForm.get('appSecret').hasError('required')) { + + {{ 'mobile.application-secret-required' | translate }} + + }
mobile.status - - {{ mobileAppStatusTranslations.get(mobileAppStatus) | translate }} - + @for (mobileAppStatus of mobileAppStatuses; track mobileAppStatus) { + + {{ mobileAppStatusTranslations.get(mobileAppStatus) | translate }} + + }
@@ -96,9 +111,11 @@ mobile.min-version - - {{ 'mobile.invalid-version-pattern' | translate }} - + @if (entityForm.get('versionInfo.minVersion').hasError('pattern')) { + + {{ 'mobile.invalid-version-pattern' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html index d9aa1ada3d..d02a692247 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html @@ -23,9 +23,12 @@ close - - -
+@if (isLoading$ | async) { + + +} @else { +
+}
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html index 95b34f5866..e422703215 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html @@ -30,41 +30,46 @@ mobile.page-name - - warning - - - warning - - - warning - + @if (customMobilePageForm.get('label').hasError('required') && customMobilePageForm.get('label').touched) { + + warning + + } + @if (customMobilePageForm.get('label').hasError('pattern') && customMobilePageForm.get('label').touched) { + + warning + + } + @if (customMobilePageForm.get('label').hasError('maxlength') && customMobilePageForm.get('label').touched) { + + warning + + }
mobile.page-type - - {{ mobilePageTypeTranslations.get(pageType) | translate }} - + @for (pageType of mobilePagesTypes; track pageType) { + + {{ mobilePageTypeTranslations.get(pageType) | translate }} + + } - + @if (customMobilePageForm.get('type').value === MobilePageType.DASHBOARD) { - - + } + @if (customMobilePageForm.get('type').value === MobilePageType.WEB_VIEW) { mobile.url - - {{ 'mobile.invalid-url-format' | translate }} - + @if (customMobilePageForm.get('url').hasError('pattern')) { + + {{ 'mobile.invalid-url-format' | translate }} + + } - - + } + @if (customMobilePageForm.get('type').value === MobilePageType.CUSTOM) { mobile.path - - {{ 'mobile.invalid-path-format' | translate }} - + @if (customMobilePageForm.get('path').hasError('pattern')) { + + {{ 'mobile.invalid-path-format' | translate }} + + } - + } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html index 14f54ae664..b33aa51ef1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html @@ -18,16 +18,18 @@
mobile.edit-page
- + @if (!disabled) { + + }
@@ -43,22 +45,24 @@ mobile.page-name - - warning - - - warning - + @if (mobilePageFormGroup.get('label').hasError('pattern') && mobilePageFormGroup.get('label').touched) { + + warning + + } + @if (mobilePageFormGroup.get('label').hasError('maxlength') && mobilePageFormGroup.get('label').touched) { + + warning + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html index 46b3132100..90008ab846 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html @@ -22,38 +22,38 @@ {{ 'mobile.show-hidden-pages' | translate }}
- - - - + @if (!readonly) { + + + + + }
-
-
-
- {{ 'mobile.custom-page' | translate }} + @for (menuItemControl of visibleMobilePagesControls(); track trackByMenuItem($index, menuItemControl);) { +
+
+ @if (isCustom(menuItemControl)) { +
+ {{ 'mobile.custom-page' | translate }} +
+ } + @if (dragEnabled) { + + } + +
- - - + @if (!$last && !readonly) { + + }
- -
+ }
- + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html index 9824311079..4c5bcf74ed 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html @@ -34,58 +34,67 @@
- - warning - - - warning - - - warning - - - - + @if (mobilePageRowForm.get('label').hasError('required') && mobilePageRowForm.get('label').touched) { + + warning + + } + @if (mobilePageRowForm.get('label').hasError('pattern') && mobilePageRowForm.get('label').touched) { + + warning + + } + @if (mobilePageRowForm.get('label').hasError('maxlength') && mobilePageRowForm.get('label').touched) { + + warning + + } + @if (isCustomMenuItem) { + + + + }
- - - + @if (!disabled) { + + @if (isDefaultMenuItem) { + + } + @if (isCustomMenuItem) { + + } + } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 3845e417b0..738dd372b7 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -25,9 +25,11 @@ close - - -
+@if (isLoading$ | async) { + +} @else { +
+}
@@ -41,15 +43,21 @@ mobile.title - - {{ 'mobile.title-required' | translate }} - - - {{ 'mobile.title-cannot-contain-only-spaces' | translate }} - - - {{ 'mobile.title-max-length' | translate }} - + @if (bundlesForms.get('title').hasError('required')) { + + {{ 'mobile.title-required' | translate }} + + } + @if (bundlesForms.get('title').hasError('pattern')) { + + {{ 'mobile.title-cannot-contain-only-spaces' | translate }} + + } + @if (bundlesForms.get('title').hasError('maxlength')) { + + {{ 'mobile.title-max-length' | translate }} + + } mobile.description - + @@ -106,8 +114,10 @@
- + @if (selectedIndex > 0) { + + } - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index 1a2e2d7b72..5a4cc4599a 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -22,9 +22,12 @@
- - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
@@ -35,17 +38,20 @@ {{ 'admin.mobile-app.custom' | translate }}
-
-
{{ 'mobile.bundle' | translate }}
- - -
+ @if (!mobileAppSettingsForm.get('useDefaultApp').value) { +
+
{{ 'mobile.bundle' | translate }}
+ + +
+ }
{{ 'admin.mobile-app.android' | translate }} @@ -65,48 +71,57 @@ {{ 'admin.mobile-app.disabled' | translate }}
-
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgePosition.value | translate }} - - - + @if (mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { +
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + @for (badgePosition of badgePositionTranslationsMap | keyvalue; track badgePosition) { + + {{ badgePosition.value | translate }} + + } + + +
-
-
-
- - {{ 'admin.mobile-app.label' | translate }} - - - - +
+ + {{ 'admin.mobile-app.label' | translate }} + + + + @if (!mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').untouched && + mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('required') || + mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('maxlength')) { + - warning - - + class="tb-error"> + warning + + } + +
-
-
-
admin.mobile-app.preview
- - -
+ } + @if (mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { +
+
admin.mobile-app.preview
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html index 02e706264d..9d4a353683 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html @@ -26,136 +26,155 @@ close - - -
+ @if (isLoading$ | async) { + + } @else { +
+ }
- + notification.name - - {{ 'notification.name-required' | translate }} - + @if (targetNotificationForm.get('name').hasError('required')) { + {{ 'notification.name-required' | translate }} + }
- -
-
{{ notificationTargetTypeTranslationMap.get(notificationTargetType) | translate }}
-
-
-
-
- - notification.recipient-type.user-filters - - - {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} - - - {{ notificationTargetConfigTypeInfoMap.get(targetNotificationForm.get('configuration.usersFilter.type').value).hint | translate }} - - - -
-
- - {{ 'tenant.tenant' | translate }} - {{ 'tenant-profile.tenant-profile' | translate }} - -
- - - - - - - - -
-
- - - - - - - - -
-
-
- - - - {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} + @for (notificationTargetType of notificationTargetTypes; track notificationTargetType) { + +
+
{{ notificationTargetTypeTranslationMap.get(notificationTargetType) | translate }}
+
-
- - -
-
-
- - {{ "notification.use-old-api" | translate }} - - - open_in_new - + } + + @if (targetNotificationForm.get('configuration.type').value === notificationTargetType.PLATFORM_USERS) { +
+ + notification.recipient-type.user-filters + + @for (type of notificationTargetConfigTypes; track type) { + + {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} + + } + + {{ notificationTargetConfigTypeInfoMap.get(targetNotificationForm.get('configuration.usersFilter.type').value).hint | translate }} + + @switch (targetNotificationForm.get('configuration.usersFilter.type').value) { + @case (notificationTargetConfigType.TENANT_ADMINISTRATORS) { + @if (isSysAdmin()) { +
+
+ + {{ 'tenant.tenant' | translate }} + {{ 'tenant-profile.tenant-profile' | translate }} + +
+ @if (targetNotificationForm.get('configuration.usersFilter.filterByTenants').value) { + + + } @else { + + + } +
+ } + } + @case (notificationTargetConfigType.USER_LIST) { + + + } + @case (notificationTargetConfigType.CUSTOMER_USERS) { + + + } + } +
+ } + @if (targetNotificationForm.get('configuration.type').value === notificationTargetType.SLACK) { +
+ + + @for (slackChanelType of slackChanelTypes; track slackChanelType) { + + {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} + + } + + + +
+ } + @if (targetNotificationForm.get('configuration.type').value === notificationTargetType.MICROSOFT_TEAMS) { +
+
+ + {{ "notification.use-old-api" | translate }} + + + open_in_new + +
+ + {{ (targetNotificationForm.get('configuration.useOldApi').value ? + 'notification.webhook-url' : 'notification.workflow-url') | translate }} + + @if (targetNotificationForm.get('configuration.webhookUrl').hasError('required')) { + {{ (targetNotificationForm.get('configuration.useOldApi').value ? 'notification.webhook-url-required' : 'notification.workflow-url-required') | translate }} + } + + + notification.channel-name + + @if (targetNotificationForm.get('configuration.channelName').hasError('required')) { + {{ 'notification.channel-name-required' | translate }} + } +
- - {{ (targetNotificationForm.get('configuration.useOldApi').value ? - 'notification.webhook-url' : 'notification.workflow-url') | translate }} - - - {{ (targetNotificationForm.get('configuration.useOldApi').value ? 'notification.webhook-url-required' : 'notification.workflow-url-required') | translate }} - - - - notification.channel-name - - - {{ 'notification.channel-name-required' | translate }} - - -
- + } + notification.description - +
diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts index c2fc08fb7c..8e729e94ef 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts @@ -48,7 +48,6 @@ export class RecipientTableConfigResolver { this.config.entityType = EntityType.NOTIFICATION_TARGET; this.config.detailsPanelEnabled = false; - this.config.addAsTextButton = true; this.config.rowPointer = true; this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_TARGET); diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html index 987e87fb0d..77bbf0e4a0 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html @@ -17,25 +17,28 @@ -->
-
notification.first-recipient
- + @if (systemEscalation) { +
notification.first-recipient
+ } @else {
notification.after + [min]="60000" + [max]="604800000" + disabledAdvanced=""> notification.notify
-
+ } -
-
- notification.no-rule -
-
- -
+ @for (escalationControl of escalationsFormArray.controls; track $index; let isLast = $last) { +
+ + + @if ($index === 0) { + + } + @if (!($index === 0) && !disabled) { + + } +
+ } + @if (!escalationsFormArray.controls.length) { +
+ notification.no-rule +
+ } + @if (!disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html index cc6e24e952..cb2643d893 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -25,9 +25,11 @@ close - - -
+@if (isLoading$ | async) { + +} @else { +
+}
@@ -37,70 +39,76 @@ {{ 'notification.basic-settings' | translate }} - + notification.rule-name - - {{ 'notification.rule-name-required' | translate }} - + @if (ruleNotificationForm.get('name').hasError('required')) { + {{ 'notification.rule-name-required' | translate }} + } {{ 'notification.rule-enable' | translate }} - + notification.trigger.trigger - - {{ triggerTypeTranslationMap.get(trigger) | translate }} - + @for (trigger of triggerTypes; track trigger) { + + {{ triggerTypeTranslationMap.get(trigger) | translate }} + + } - - {{ 'notification.trigger.trigger-required' | translate }} - + @if (ruleNotificationForm.get('triggerType').hasError('required')) { + {{ 'notification.trigger.trigger-required' | translate }} + } -
- - - -
- + @if (ruleNotificationForm.get('triggerType').value !== triggerType.ALARM) { +
+ + + +
+ } @else {
notification.escalation-chain
notification.recipients
-
+ }
notification.stop-escalation-alarm-status-become
- + alarm.alarm-status-list - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + }
@@ -108,559 +116,608 @@ - - {{ 'notification.alarm-trigger-settings' | translate }} -
-
-
- notification.filter - - - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - + @if (ruleNotificationForm.get('triggerType').value === triggerType.ALARM) { + + {{ 'notification.alarm-trigger-settings' | translate }} + +
+
+ notification.filter + + + + alarm.alarm-severity-list + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + } + + +
+ + notification.notify-on + + @for (alarmAction of alarmActions; track alarmAction) { + + {{ alarmActionTranslationMap.get(alarmAction) | translate }} + + } + @if (alarmTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } -
- - notification.notify-on - - - {{ alarmActionTranslationMap.get(alarmAction) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
-
-
-
- - notification.description - - -
-
-
- - - {{ 'notification.device-activity-trigger-settings' | translate }} -
-
-
- - {{ 'device.device' | translate }} - {{ 'device-profile.device-profile' | translate }} - -
- - - - - - - - - - notification.notify-on - - - {{ deviceEventTranslationMap.get(deviceEvent) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
-
-
-
- - notification.description - - -
-
-
- - - {{ 'notification.entity-action-trigger-settings' | translate }} -
-
- notification.filter - - -
- notification.status - {{ 'notification.created' | translate }} - {{ 'notification.updated' | translate }} - {{ 'notification.deleted' | translate }}
-
-
-
-
- - notification.description - - -
-
-
- - - {{ 'notification.alarm-comment-trigger-settings' | translate }} -
-
-
- notification.filter - - - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - - - - - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - + +
+
+ + notification.description + -
-
- - {{ 'notification.notify-only-user-comments' | translate }} - - - {{ 'notification.notify-on-comment-update' | translate }} -
-
- -
-
- - notification.description - - -
-
-
- - {{ 'notification.alarm-assignment-trigger-settings' | translate }} -
-
-
- notification.filter - - + + + } - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - + @if (ruleNotificationForm.get('triggerType').value === triggerType.DEVICE_ACTIVITY) { + + {{ 'notification.device-activity-trigger-settings' | translate }} +
+
+
+ + {{ 'device.device' | translate }} + {{ 'device-profile.device-profile' | translate }} + +
+ @if (deviceInactivityTemplateForm.get('triggerConfig.filterByDevice').value) { + + + } @else { + + + } + + notification.notify-on + + @for (deviceEvent of deviceEvents; track deviceEvent) { + + {{ deviceEventTranslationMap.get(deviceEvent) | translate }} + + } + @if (deviceInactivityTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } - - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - +
+
+
+
+ + notification.description + +
+
+
+ } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.ENTITY_ACTION) { + + {{ 'notification.entity-action-trigger-settings' | translate }} +
+
+ notification.filter + + +
+ notification.status + {{ 'notification.created' | translate }} + {{ 'notification.updated' | translate }} + {{ 'notification.deleted' | translate }} +
- - notification.notify-on - - - {{ alarmAssignmentActionTranslationMap.get(alarmAssignmentAction) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
- -
-
- - notification.description - - -
-
-
- - {{ 'notification.rule-engine-events-trigger-settings' | translate }} -
-
-
- notification.rule-engine-filter - - - - rulechain.rulechain-events - - - {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} - - + +
+
+ + notification.description + - - {{ 'notification.only-rule-chain-lifecycle-failures' | translate }} - -
-
- notification.rule-node-filter - - {{ 'notification.track-rule-node-events' | translate }} - -
- - rulenode.rule-node-events - - - {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} - +
+ + + } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.ALARM_COMMENT) { + + {{ 'notification.alarm-comment-trigger-settings' | translate }} +
+
+
+ notification.filter + + + + alarm.alarm-severity-list + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + } - - {{ 'notification.only-rule-node-lifecycle-failures' | translate }} + + alarm.alarm-status-list + + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + } + + +
+
+ + {{ 'notification.notify-only-user-comments' | translate }} + + + {{ 'notification.notify-on-comment-update' | translate }}
-
-
- -
-
- - notification.description - - -
-
-
+
+ +
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.edge-trigger-settings' | translate }} -
-
-
- notification.filter - - - + @if (ruleNotificationForm.get('triggerType').value === triggerType.ALARM_ASSIGNMENT) { + + {{ 'notification.alarm-assignment-trigger-settings' | translate }} + +
+
+ notification.filter + + + + alarm.alarm-severity-list + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + } + + + + alarm.alarm-status-list + + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + } + + +
+ notification.notify-on - - - {{ edgeConnectionEventTranslationMap.get(edgeEvent) | translate }} - + + @for (alarmAssignmentAction of alarmAssignmentActions; track alarmAssignmentAction) { + + {{ alarmAssignmentActionTranslationMap.get(alarmAssignmentAction) | translate }} + + } + @if (alarmAssignmentTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } -
-
-
-
-
- - notification.description - - -
-
-
+ + +
+
+ + notification.description + + +
+
+ + } - - {{ 'notification.edge-trigger-settings' | translate }} -
-
-
- notification.filter - - -
-
-
-
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT) { + + {{ 'notification.rule-engine-events-trigger-settings' | translate }} +
+
+
+ notification.rule-engine-filter + + + + rulechain.rulechain-events + + @for (componentLifecycleEvent of componentLifecycleEvents; track componentLifecycleEvent) { + + {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} + + } + + + + {{ 'notification.only-rule-chain-lifecycle-failures' | translate }} + +
+
+ notification.rule-node-filter + + {{ 'notification.track-rule-node-events' | translate }} + + @if (ruleEngineEventsTemplateForm.get('triggerConfig.trackRuleNodeEvents').value) { +
+ + rulenode.rule-node-events + + @for (componentLifecycleEvent of componentLifecycleEvents; track componentLifecycleEvent) { + + {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} + + } + + + + {{ 'notification.only-rule-node-lifecycle-failures' | translate }} + +
+ } +
+
+
+
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.entities-limit-trigger-settings' | translate }} -
-
- notification.filter - - -
- notification.threshold -
- - - - - - % + @if (ruleNotificationForm.get('triggerType').value === triggerType.EDGE_CONNECTION) { + + {{ 'notification.edge-trigger-settings' | translate }} + +
+
+ notification.filter + + + + notification.notify-on + + @for (edgeEvent of edgeConnectionEvents; track edgeEvent) { + + {{ edgeConnectionEventTranslationMap.get(edgeEvent) | translate }} + + } + +
+
+ +
+
+ + notification.description + + +
+
+
+ } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.EDGE_COMMUNICATION_FAILURE) { + + {{ 'notification.edge-trigger-settings' | translate }} +
+
+
+ notification.filter + + +
+
+
+
+
+ + notification.description + + +
+
+
+ } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.ENTITIES_LIMIT) { + + {{ 'notification.entities-limit-trigger-settings' | translate }} +
+
+ notification.filter + + +
+ notification.threshold +
+ + + + + + % + +
-
-
- -
-
- - notification.description - - -
-
-
+ + +
+
+ + notification.description + + +
+
+ + } - - {{ 'notification.api-usage-trigger-settings' | translate }} -
-
- - api-usage.api-features - - - {{ apiFeatureTranslationMap.get(apiFeature) | translate }} - - - {{ 'notification.api-feature-hint' | translate }} - - - notification.notify-on - - - {{ apiUsageStateValueTranslationMap.get(apiUsageStateValue) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
-
-
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.API_USAGE_LIMIT) { + + {{ 'notification.api-usage-trigger-settings' | translate }} +
+
+ + api-usage.api-features + + @for (apiFeature of apiFeatures; track apiFeature) { + + {{ apiFeatureTranslationMap.get(apiFeature) | translate }} + + } + + {{ 'notification.api-feature-hint' | translate }} + + + notification.notify-on + + @for (apiUsageStateValue of apiUsageStateValues; track apiUsageStateValue) { + + {{ apiUsageStateValueTranslationMap.get(apiUsageStateValue) | translate }} + + } + + @if (apiUsageLimitTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } + +
+
+
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.new-platform-version-trigger-settings' | translate }} -
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.NEW_PLATFORM_VERSION) { + + {{ 'notification.new-platform-version-trigger-settings' | translate }} +
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.rate-limits-trigger-settings' | translate }} -
-
- - -
-
-
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.RATE_LIMITS) { + + {{ 'notification.rate-limits-trigger-settings' | translate }} +
+
+ + +
+
+
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.task-processing-failure-trigger-settings' | translate }} -
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.TASK_PROCESSING_FAILURE) { + + {{ 'notification.task-processing-failure-trigger-settings' | translate }} +
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.resources-shortage-trigger-settings' | translate }} -
-
- notification.filter -
- notification.cpu-threshold -
- - - - - - % - + @if (ruleNotificationForm.get('triggerType').value === triggerType.RESOURCES_SHORTAGE) { + + {{ 'notification.resources-shortage-trigger-settings' | translate }} + +
+ notification.filter +
+ notification.cpu-threshold +
+ + + + + + % + +
-
-
- notification.ram-threshold -
- - - - - - % - +
+ notification.ram-threshold +
+ + + + + + % + +
-
-
- notification.storage-threshold -
- - - - - - % - +
+ notification.storage-threshold +
+ + + + + + % + +
-
-
-
-
-
- - notification.description - - -
-
-
+ + +
+
+ + notification.description + + +
+
+ + } +
- + @if (selectedIndex > 0) { + + } - - +@if (isLoading$ | async) { + + +}
{{ 'notification.use-template' | translate }}
-
- - - -
- - -
-
-
-
- -
notification.at-least-one-should-be-selected
+
+ @if (notificationRequestForm.get('useTemplate').value) { + + + + } @else { + +
+
+
+
+ +
notification.at-least-one-should-be-selected
+
+ @if (showRefresh) { + + }
- -
-
- - - - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - - - chevron_right - - - -
- - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - -
-
-
+
+ @for (deliveryMethods of notificationDeliveryMethods; track deliveryMethods) { + @if (isInteractDeliveryMethod(deliveryMethods)) { + + + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + + @if (isInteractDeliveryMethod(deliveryMethods)) { + + chevron_right + + } + + } @else { +
+ + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + +
+ } + } +
-
-
+ } +
- - - + @if (notificationRequestForm.get('targets').value?.length > 1) { +
+ @for (detail of (preview.recipientsCountByTarget | keyvalue); track detail.key) { +
+ {{ detail.value }}{{ detail.key }} +
+ } +
+ } + + + @for (recipientTitle of preview.recipientsPreview; track recipientTitle) { + + {{ recipientTitle }} + + } + + + @if (preview.processedTemplates.WEB?.enabled) { +
+
+ mdi:bell-badge +
notification.delivery-method.web-preview
+
+
+ +
+
+ } + @if (preview.processedTemplates.MOBILE_APP?.enabled) { +
+
+ mdi:cellphone-text +
notification.delivery-method.mobile-app-preview
+
+
+
{{ preview.processedTemplates.MOBILE_APP.subject }}
+
{{ preview.processedTemplates.MOBILE_APP.body }}
+
+
+ } + @if (preview.processedTemplates.SMS?.enabled) { +
+
+ mdi:message-processing +
notification.delivery-method.sms-preview
+
+
+ {{ preview.processedTemplates.SMS.body }} +
+
+ } + @if (preview.processedTemplates.EMAIL?.enabled) { +
+
+ mdi:email +
notification.delivery-method.email-preview
+
+
+
{{ preview.processedTemplates.EMAIL.subject }}
+ + +
+
+ } + @if (preview.processedTemplates.SLACK?.enabled) { +
+
+ mdi:slack +
notification.delivery-method.slack-preview
+
+
+ {{ preview.processedTemplates.SLACK.body }} +
+
+ } + @if (preview.processedTemplates.MICROSOFT_TEAMS?.enabled) { +
+
+ mdi:microsoft-teams +
notification.delivery-method.microsoft-teams-preview
+
+
+
{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}
+ {{ preview.processedTemplates.MICROSOFT_TEAMS.body }} + @if (preview.processedTemplates.MICROSOFT_TEAMS.button?.enabled) { + + } +
+
+ } + + }
- + @if (selectedIndex > 0) { + + } + (click)="nextStep()">{{ nextStepLabel() | translate }}
diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss index 51108c1c5e..774f4eb672 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -218,6 +218,7 @@ width: 100%;; overflow-y: auto; scrollbar-gutter: stable; + padding-top: 4px; @media #{$mat-gt-sm} { min-width: 500px; } diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html index 856cf091a6..d4dda48d4c 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html @@ -29,13 +29,15 @@ -
-
- + @for (deliveryMethod of deliveryMethods; track deliveryMethod) { +
+
+ +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html index 608a11539e..73d0209640 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html @@ -25,13 +25,17 @@
+ (click)="resetSettings()">{{ 'notification.settings.reset-all' | translate }}
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -46,25 +50,31 @@ notification.settings.type -
-
- - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - + @for (deliveryMethods of notificationDeliveryMethods; track deliveryMethods) { +
+
+ + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + +
-
+ }
-
- - - -
+ @for (settingsControl of notificationSettingsFormArray.controls; track settingsControl; let i = $index) { +
+ + + @if (!$last) { + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html index 2caff5e475..63ada301e1 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html @@ -29,62 +29,63 @@
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : + @if (!hideButtonText) { + + notification.button-text + + @if (actionButtonConfigForm.get('text').hasError('required')) { + {{ 'notification.button-text-required' | translate }} + } + @if (actionButtonConfigForm.get('text').hasError('maxlength')) { + {{ 'notification.button-text-max-length' | translate : {length: actionButtonConfigForm.get('text').getError('maxlength').requiredLength} - }} - - + }} + } + + }
-
- +
+ notification.action-type - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - + @for (actionButtonLinkType of actionButtonLinkTypes; track actionButtonLinkType) { + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + } - - notification.link - - - {{ 'notification.link-required' | translate }} - - - {{ 'notification.link-max-length' | translate : + @if (actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK) { + + notification.link + + @if (actionButtonConfigForm.get('link').hasError('required')) { + {{ 'notification.link-required' | translate }} + } + @if (actionButtonConfigForm.get('link').hasError('maxlength')) { + {{ 'notification.link-max-length' | translate : {length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength} - }} - - - + }} + } + + } @else { - + - + }
- - {{ 'notification.set-entity-from-notification' | translate }} - + @if (actionButtonConfigForm.get('linkType').value === actionButtonLinkType.DASHBOARD) { + + {{ 'notification.set-entity-from-notification' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html index 9bbe1eaa5c..cb7ed554cb 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html @@ -18,7 +18,7 @@
notification.customize-messages
- {{ 'notification.input-fields-support-templatization' | translate}} + {{ 'notification.input-fields-support-templatization' | translate }}
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).name | translate }} - - - - - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - {{'notification.subject-max-length' | translate : - {length: templateConfigurationForm.get('WEB.subject').getError('maxlength').requiredLength} - }} - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: templateConfigurationForm.get('WEB.body').getError('maxlength').requiredLength} - }} - - -
-
- - {{ 'icon.icon' | translate }} - -
- - - - + @if (templateConfigurationForm.get('WEB.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).name | translate }} + + + + + notification.subject + + @if (templateConfigurationForm.get('WEB.subject').hasError('required')) { + {{ 'notification.subject-required' | translate }} + } + @if (templateConfigurationForm.get('WEB.subject').hasError('maxlength')) { + {{ 'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('WEB.subject').getError('maxlength').requiredLength} + }} + } + + + notification.message + + @if (templateConfigurationForm.get('WEB.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + @if (templateConfigurationForm.get('WEB.body').hasError('maxlength')) { + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('WEB.body').getError('maxlength').requiredLength} + }} + } + +
+
+ + {{ 'icon.icon' | translate }} + +
+ + + +
-
- - -
-
-
-
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).name | translate }} - - - - - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - {{ 'notification.subject-max-length' | translate : - {length: templateConfigurationForm.get('MOBILE_APP.subject').getError('maxlength').requiredLength} - }} - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: templateConfigurationForm.get('MOBILE_APP.body').getError('maxlength').requiredLength} - }} - - -
-
- - {{ 'icon.icon' | translate }} - -
- - - - + + +
+ + +
+ } + @if (templateConfigurationForm.get('MOBILE_APP.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).name | translate }} + + + + + notification.subject + + @if (templateConfigurationForm.get('MOBILE_APP.subject').hasError('required')) { + {{ 'notification.subject-required' | translate }} + } + @if (templateConfigurationForm.get('MOBILE_APP.subject').hasError('maxlength')) { + {{ 'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('MOBILE_APP.subject').getError('maxlength').requiredLength} + }} + } + + + notification.message + + @if (templateConfigurationForm.get('MOBILE_APP.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + @if (templateConfigurationForm.get('MOBILE_APP.body').hasError('maxlength')) { + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('MOBILE_APP.body').getError('maxlength').requiredLength} + }} + } + +
+
+ + {{ 'icon.icon' | translate }} + +
+ + + +
+ +
- - - -
-
-
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).name | translate }} - - - - - notification.message - - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: templateConfigurationForm.get('SMS.body').getError('maxlength').requiredLength} - }} - - - - -
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).name | translate }} - - - - - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - {{'notification.subject-max-length' | translate : - {length: templateConfigurationForm.get('EMAIL.subject').getError('maxlength').requiredLength} - }} - - - notification.message - - - - {{ 'notification.message-required' | translate }} - - - -
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).name | translate }} - - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - - - -
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).name | translate }} - - - - - notification.subject - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - -
-
-
notification.theme-color
- + + +
+ } + @if (templateConfigurationForm.get('SMS.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).name | translate }} + + + + + notification.message + + + @if (templateConfigurationForm.get('SMS.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + @if (templateConfigurationForm.get('SMS.body').hasError('maxlength')) { + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('SMS.body').getError('maxlength').requiredLength} + }} + } + + + +
+ } + @if (templateConfigurationForm.get('EMAIL.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).name | translate }} + + + + + notification.subject + + @if (templateConfigurationForm.get('EMAIL.subject').hasError('required')) { + {{ 'notification.subject-required' | translate }} + } + @if (templateConfigurationForm.get('EMAIL.subject').hasError('maxlength')) { + {{ 'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('EMAIL.subject').getError('maxlength').requiredLength} + }} + } + + notification.message + + + @if ((interacted || templateConfigurationForm.get('EMAIL.body').touched) && templateConfigurationForm.get('EMAIL.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + + +
+ } + @if (templateConfigurationForm.get('SLACK.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).name | translate }} + + + + + notification.message + + @if (templateConfigurationForm.get('SLACK.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + + + +
+ } + @if (templateConfigurationForm.get('MICROSOFT_TEAMS.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).name | translate }} + + + + + notification.subject + + + + notification.message + + @if (templateConfigurationForm.get('MICROSOFT_TEAMS.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + +
+
+
notification.theme-color
+ +
+ +
- - - -
-
-
+
+ + + } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 7298fb6ed9..dbe49a6238 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -25,8 +25,11 @@ close - - +@if (isLoading$ | async) { + +} @else { +
+}
{{ 'notification.setup' | translate }}
- + notification.name - - {{ 'notification.name-required' | translate }} - - - - notification.type - - - {{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }} - - + @if (templateNotificationForm.get('name').hasError('required')) { + {{ 'notification.name-required' | translate }} + } + @if (!hideSelectType) { + + notification.type + + @for (notificationType of notificationTypes; track notificationType) { + + {{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }} + + } + + + }
notification.at-least-one-should-be-selected
-
- - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - -
+ @for (deliveryMethods of notificationDeliveryMethods; track deliveryMethods) { +
+ + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + +
+ }
@@ -86,8 +93,10 @@
- + @if (selectedIndex > 0) { + + }
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- + user.email - - {{ 'user.email-required' | translate }} - - - {{ 'user.invalid-email-format' | translate }} - + @if (profile.get('email').hasError('required')) { + + {{ 'user.email-required' | translate }} + + } + @if (profile.get('email').hasError('email')) { + + {{ 'user.invalid-email-format' | translate }} + + } - + user.first-name - + user.last-name - + language.language - - - {{ lang ? ('language.locales.' + lang | translate) : ''}} - + + {{ 'language.auto' | translate }} + @for(lang of languageList; track lang) { + {{ 'language.locales.' + lang | translate }} + } - + unit.unit-system @@ -76,20 +87,23 @@ } -
- - - {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} - -
+ @if (!isSysAdmin()) { +
+ + + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} + +
+ }
+ +
+ + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss similarity index 70% rename from ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss rename to ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss index ed5e4c4a64..fa37306f7b 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss @@ -13,17 +13,18 @@ * 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; - } - } - } + display: grid; + width: 650px; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; } + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + padding: 8px; + } +} \ No newline at end of file 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 index 8156c1690d..41e72a14cf 100644 --- 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 @@ -26,8 +26,10 @@ close - - + @if (isLoading$ | async) { + + + }
close - - + @if (isLoading$ | async) { + + + }
close - - + @if (isLoading$ | async) { + + + }
- + rulechain.name - - {{ 'rulechain.name-required' | translate }} - - - {{ 'rulechain.name-max-length' | translate }} - + @if (createNestedRuleChainFormGroup.get('name').hasError('required')) { + + {{ 'rulechain.name-required' | translate }} + + } + @if (createNestedRuleChainFormGroup.get('name').hasError('maxlength')) { + + {{ 'rulechain.name-max-length' | translate }} + + }
- + rulechain.description 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 index 65aabc17aa..f6e244a81b 100644 --- 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 @@ -15,16 +15,19 @@ limitations under the License. --> - + rulenode.link-labels - - {{label.name}} - close - + @for (label of labels; track label) { + + {{label.name}} + @if (!disabled) { + close + } + + } - - - - -
-
- rulenode.no-link-labels-found + @for (label of filteredLabels | async; track label) { + + + + } + @if ((filteredLabels | async)?.length === 0) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ rulenode.no-link-labels-found +
+ } @else { + + {{ translate.get('rulenode.no-link-label-matching', + {label: truncate.transform(searchText, true, 6, '...')}) | async }} + + } + @if (allowCustom) { + + rulenode.create-new-link-label + + }
- - - {{ 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/rule-node-config.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html index 25d2fd315d..f1108b17ea 100644 --- 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 @@ -17,12 +17,16 @@ -->
-
{{definedDirectiveError}}
- - + @if (definedDirectiveError) { +
{{definedDirectiveError}}
+ } + @if (!useDefinedDirective()) { + + + }
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 index b605a469cd..a83c1f8b17 100644 --- 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 @@ -15,42 +15,48 @@ limitations under the License. --> -
- -
+@if (!isAdd && ruleNode.component?.configurationDescriptor?.nodeDefinition?.ruleChainNode) { +
+ +
+}
- + rulenode.name - - {{ 'rulenode.name-required' | translate }} - - - {{ 'rulenode.name-max-length' | translate }} - + @if (ruleNodeFormGroup.get('name').hasError('required') || ruleNodeFormGroup.get('name').hasError('pattern')) { + + {{ 'rulenode.name-required' | translate }} + + } + @if (ruleNodeFormGroup.get('name').hasError('maxlength')) { + + {{ 'rulenode.name-max-length' | translate }} + + }
- + [entityType]="entityType.RULE_NODE"/> + @if (isSingleton()) { + + }
- - + @if (isAddQueue()) { + + + }
- + rulenode.rule-node-description - +
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 index bf231b7126..b5cbfaf5a8 100644 --- 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 @@ -37,6 +37,7 @@ import { ComponentClusteringMode } from '@shared/models/component-descriptor.mod import { coerceBoolean } from '@shared/decorators/coercion'; import { ServiceType } from '@shared/models/queue.models'; import { takeUntil } from 'rxjs/operators'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; @Component({ selector: 'tb-rule-node', @@ -65,6 +66,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O @coerceBoolean() isAdd = false; + @Input() + additionalActionConfig: AdditionalDebugActionConfig; + @Output() initRuleNode = new EventEmitter(); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.html new file mode 100644 index 0000000000..015c2a38d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.html @@ -0,0 +1,59 @@ + + + + +
+
{{ 'rulechain.note-background-color' | translate }}
+ + +
+
+ + + rule-node-config.advanced-settings + +
+
{{ 'rulechain.note-border' | translate }}
+
+ + + px + + + +
+
+
+ + {{ 'rulechain.note-apply-default-markdown-style' | translate }} + +
+ + +
+
+ diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.scss new file mode 100644 index 0000000000..6d89be41ad --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.scss @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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-markdown-editor { + .markdown-content.tb-edit-mode { + &:not(.tb-fullscreen) { + padding-bottom: 0; + } + } + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.ts new file mode 100644 index 0000000000..a5a6bb1622 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, DestroyRef, inject, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatExpansionPanel } from '@angular/material/expansion'; +import { FormBuilder, Validators } from '@angular/forms'; +import { + FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE as DEFAULT_APPLY_MARKDOWN_STYLE, + FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR as DEFAULT_BACKGROUND_COLOR, + FC_RULE_NOTE_DEFAULT_BORDER_WIDTH as DEFAULT_BORDER_WIDTH, + FcRuleNote +} from '@shared/models/rule-node.models'; +import { isNotEmptyStr } from '@core/utils'; +import tinycolor from 'tinycolor2'; + +@Component({ + selector: 'tb-rule-note-editor', + templateUrl: './rule-note-editor.component.html', + styleUrls: ['./rule-note-editor.component.scss'], + standalone: false +}) +export class RuleNoteEditorComponent implements OnChanges, OnInit { + + @Input() + note: FcRuleNote; + + @ViewChild('advancedPanel') advancedPanel: MatExpansionPanel; + + private fb = inject(FormBuilder); + private destroyRef = inject(DestroyRef); + + noteForm = this.fb.group({ + content: ['', Validators.maxLength(65536)], + backgroundColor: [DEFAULT_BACKGROUND_COLOR], + borderColor: [tinycolor(DEFAULT_BACKGROUND_COLOR).darken(20).toString()], + borderWidth: [DEFAULT_BORDER_WIDTH, [Validators.min(0)]], + applyDefaultMarkdownStyle: [DEFAULT_APPLY_MARKDOWN_STYLE], + markdownCss: ['', Validators.maxLength(65536)] + }); + + ngOnInit(): void { + this.noteForm.get('backgroundColor').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(color => { + const borderControl = this.noteForm.get('borderColor'); + if (borderControl.pristine) { + borderControl.setValue(this.borderColorFrom(color), { emitEvent: false }); + } + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.note) { + this.updatedForm(); + } + } + + private borderColorFrom(bg: string): string { + const color = tinycolor(bg || DEFAULT_BACKGROUND_COLOR); + return color.isDark() ? color.lighten(20).toString() : color.darken(20).toString(); + } + + private updatedForm(): void { + const bg = this.note?.backgroundColor || DEFAULT_BACKGROUND_COLOR; + this.noteForm.setValue({ + content: this.note?.content || '', + backgroundColor: bg, + borderColor: this.note?.borderColor || this.borderColorFrom(bg), + borderWidth: this.note?.borderWidth ?? DEFAULT_BORDER_WIDTH, + applyDefaultMarkdownStyle: this.note?.applyDefaultMarkdownStyle ?? DEFAULT_APPLY_MARKDOWN_STYLE, + markdownCss: this.note?.markdownCss || '' + }); + const shouldExpand = this.hasNonDefaultAdvancedSettings(bg); + setTimeout(() => { + if (shouldExpand) { + this.advancedPanel?.open(); + } else { + this.advancedPanel?.close(); + } + this.noteForm.markAsPristine(); + }, 0); + } + + private hasNonDefaultAdvancedSettings(bg: string): boolean { + const note = this.note; + if (!note) return false; + return (note.borderColor != null && note.borderColor !== this.borderColorFrom(bg)) + || (note.borderWidth != null && note.borderWidth !== DEFAULT_BORDER_WIDTH) + || note.applyDefaultMarkdownStyle === false + || isNotEmptyStr(note.markdownCss); + } +} 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 index 7f204aac2b..e506e8d87c 100644 --- 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 @@ -28,14 +28,15 @@ opened position="start"> - - + @if (!isImport) { + + + }
@@ -59,37 +60,40 @@
- - - - {{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).icon }} -
{{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).name }}
-
-
- - -
+ @for (ruleNodeType of ruleNodeTypesLibraryArray; track ruleNodeType) { + + + + {{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).icon }} +
{{ ruleNodeTypeDescriptorsMap.get(ruleNodeType).name }}
+
+
+ + +
+ }
- -
-
-
- - - - - - - - - - -
-
-
{{editingRuleNode.component.name}}
-
 
-
-
 
-
+
+
+
+ + + + + + @if (editingRuleNode.ruleNodeId) { + + + + + } + +
+
+
{{editingRuleNode.component.name}}
+
 
+
+
 
+
+
-
- - - - -
-
-
- - -
+ + + + } + @if (editingRuleNodeLink) { + +
+
+
+ + +
+ } + @if (editingNote) { + + + + + } - + @if (!isImport) { + + + } -
+ @for (menuItem of contextInfo.menuItems; track menuItem) { +
+ @if (menuItem.divider) { + + } + @if (!menuItem.divider) { + + } +
+ }
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 index 4152ca767a..b38fe007e8 100644 --- 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 @@ -54,6 +54,15 @@ z-index: 2; } + button.mdc-fab.add-note-button { + position: absolute; + top: 10px; + right: 110px; + opacity: .85; + margin: 0 6px; + z-index: 2; + } + .tb-library-node-btn { width: 20px; height: 90px; @@ -188,6 +197,20 @@ margin: -3px; border: solid 3px #f00; } + &.fc-edit { + margin: -2px; + border: solid 2px rgba(0, 0, 0, 0.35); + } + } + } + + .fc-note { + border-radius: 8px; + &.fc-selected:not(.fc-edit) { + box-shadow: 0 0 0 3px #f00; + } + &.fc-selected.fc-edit { + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.35); } } 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 index c6dc3eae9c..7ac3a00b80 100644 --- 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 @@ -51,7 +51,6 @@ 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, @@ -63,9 +62,12 @@ import { } from '@shared/models/rule-chain.models'; import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart'; import { + FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE, + FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR, FcRuleEdge, FcRuleNode, FcRuleNodeType, + FcRuleNote, getRuleNodeHelpLink, LinkLabel, outputNodeClazz, @@ -81,14 +83,15 @@ import { RuleChainService } from '@core/http/rule-chain.service'; import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject, throwError } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { ISearchableComponent } from '../../models/searchable-component.models'; -import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { deepClone, guid, isDefinedAndNotNull } from '@core/utils'; import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; import { RuleNodeLinkComponent } from './rule-node-link.component'; +import { RuleNoteEditorComponent } from './rule-note-editor.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 { DebugEventType, DebugRuleNodeEventBody, EventType } from '@shared/models/event.models'; +import { DebugEventType, DebugRuleNodeEventBody } from '@shared/models/event.models'; import { MatMiniFabButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; @@ -97,8 +100,10 @@ import { MatDrawer } from '@angular/material/sidenav'; import { HttpStatusCode } from '@angular/common/http'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { EntityDebugSettings } from '@shared/models/entity.models'; -import Timeout = NodeJS.Timeout; import { DomSanitizer } from '@angular/platform-browser'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { EventsDialogComponent } from '@home/dialogs/events-dialog.component'; +import Timeout = NodeJS.Timeout; @Component({ selector: 'tb-rulechain-page', @@ -130,8 +135,6 @@ export class RuleChainPageComponent extends PageComponent @ViewChild('drawer') drawer: MatDrawer; - eventTypes = EventType; - debugEventTypes = DebugEventType; ruleChainMenuPosition = { x: '0px', y: '0px' }; @@ -161,11 +164,16 @@ export class RuleChainPageComponent extends PageComponent @ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent; @ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent; + @ViewChild('tbRuleNote') ruleNoteComponent: RuleNoteEditorComponent; editingRuleNodeLink: FcRuleEdge = null; isEditingRuleNodeLink = false; editingRuleNodeLinkIndex = -1; + editingNote: FcRuleNote = null; + isEditingNote = false; + editingNoteIndex = -1; + hotKeys: Hotkey[] = []; enableHotKeys = true; @@ -178,22 +186,23 @@ export class RuleChainPageComponent extends PageComponent ruleChainModel: FcRuleNodeModel = { nodes: [], - edges: [] + edges: [], + notes: [] }; selectedObjects = []; editCallbacks: UserCallbacks = { - edgeDoubleClick: (event, edge) => { + edgeDoubleClick: (_event, edge) => { this.openLinkDetails(edge); }, - edgeEdit: (event, edge) => { + edgeEdit: (_event, edge) => { this.openLinkDetails(edge); }, nodeCallbacks: { - doubleClick: (event, node: FcRuleNode) => { + doubleClick: (_event, node: FcRuleNode) => { this.openNodeDetails(node); }, - nodeEdit: (event, node: FcRuleNode) => { + nodeEdit: (_event, node: FcRuleNode) => { this.openNodeDetails(node); }, mouseEnter: this.displayNodeDescriptionTooltip.bind(this), @@ -202,7 +211,7 @@ export class RuleChainPageComponent extends PageComponent }, isValidEdge: (source, destination) => source.type === FlowchartConstants.rightConnectorType && destination.type === FlowchartConstants.leftConnectorType, - createEdge: (event, edge: FcRuleEdge) => { + createEdge: (_event, edge: FcRuleEdge) => { const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; if (sourceNode.component.type === RuleNodeType.INPUT) { const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + '')); @@ -236,8 +245,19 @@ export class RuleChainPageComponent extends PageComponent } } }, - dropNode: (event, node: FcRuleNode) => { + dropNode: (_event, node: FcRuleNode) => { this.addRuleNode(node); + }, + noteCallbacks: { + noteEdit: (_event, note) => { + this.openNoteDetails(note); + }, + doubleClick: (_event, note) => { + this.openNoteDetails(note); + } + }, + noteRemoved: (_note) => { + this.isDirty = true; } }; @@ -265,11 +285,9 @@ export class RuleChainPageComponent extends PageComponent private tooltipTimeout: Timeout; - constructor(protected store: Store, - private route: ActivatedRoute, + constructor(private route: ActivatedRoute, private router: Router, private ruleChainService: RuleChainService, - private authService: AuthService, private translate: TranslateService, private itembuffer: ItemBufferService, private popoverService: TbPopoverService, @@ -280,7 +298,7 @@ export class RuleChainPageComponent extends PageComponent public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder) { - super(store); + super(); this.route.data.pipe( takeUntil(this.destroy$) ).subscribe( @@ -361,12 +379,15 @@ export class RuleChainPageComponent extends PageComponent this.selectedObjects = []; this.ruleChainModel.nodes = []; this.ruleChainModel.edges = []; + this.ruleChainModel.notes = []; this.ruleNodeTypesModel = {}; if (this.ruleChainCanvas) { this.ruleChainCanvas.adjustCanvasSize(true); } this.isEditingRuleNode = false; this.isEditingRuleNodeLink = false; + this.isEditingNote = false; + this.editingNote = null; this.updateRuleNodesHighlight(); } @@ -387,7 +408,7 @@ export class RuleChainPageComponent extends PageComponent new Hotkey(['ctrl+c', 'meta+c'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); - this.copyRuleNodes(); + this.copyRuleChainObjects(); return false; } return true; @@ -398,8 +419,8 @@ export class RuleChainPageComponent extends PageComponent new Hotkey(['ctrl+v', 'meta+v'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); - if (this.itembuffer.hasRuleNodes()) { - this.pasteRuleNodes(); + if (this.itembuffer.hasRuleChainObjects()) { + this.pasteRuleChainObjects(); } return false; } @@ -463,6 +484,17 @@ export class RuleChainPageComponent extends PageComponent }, ['INPUT', 'SELECT', 'TEXTAREA'], this.translate.instant('rulenode.create-nested-rulechain')) ); + this.hotKeys.push( + new Hotkey('alt+n', (event: KeyboardEvent) => { + if (this.enableHotKeys) { + event.preventDefault(); + this.addNote(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('rulechain.add-note')) + ); } } @@ -521,7 +553,7 @@ export class RuleChainPageComponent extends PageComponent }); if (this.expansionPanels) { for (let i = 0; i < ruleNodeTypesLibrary.length; i++) { - const panel = this.expansionPanels.find((item, index) => index === i); + const panel = this.expansionPanels.find((_item, index) => index === i); if (panel) { const type = ruleNodeTypesLibrary[i]; if (!this.ruleNodeTypesModel[type].model.nodes.length) { @@ -655,6 +687,7 @@ export class RuleChainPageComponent extends PageComponent } }); } + this.ruleChainModel.notes = deepClone(this.ruleChainMetaData.notes) || []; if (this.ruleChainCanvas) { this.ruleChainCanvas.adjustCanvasSize(true); } @@ -686,10 +719,12 @@ export class RuleChainPageComponent extends PageComponent } private prepareContextMenu(item: FcItemInfo): RuleChainMenuContextInfo { - if (this.objectsSelected() || (!item.node && !item.edge)) { + if (this.objectsSelected() || (!item.node && !item.edge && !item.note)) { return this.prepareRuleChainContextMenu(); } else if (item.node) { return this.prepareRuleNodeContextMenu(item.node); + } else if (item.note) { + return this.prepareNoteContextMenu(item.note); } else if (item.edge) { return this.prepareEdgeContextMenu(item.edge); } @@ -703,11 +738,12 @@ export class RuleChainPageComponent extends PageComponent subtitle: this.translate.instant('rulechain.rulechain'), menuItems: [] }; - if (this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length) { + if (this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length || + this.ruleChainCanvas.modelService.notes.getSelectedNotes().length) { contextInfo.menuItems.push( { action: () => { - this.copyRuleNodes(); + this.copyRuleChainObjects(); }, enabled: true, value: 'rulenode.copy-selected', @@ -719,14 +755,25 @@ export class RuleChainPageComponent extends PageComponent contextInfo.menuItems.push( { action: ($event) => { - this.pasteRuleNodes($event); + this.pasteRuleChainObjects($event); }, - enabled: this.itembuffer.hasRuleNodes(), + enabled: this.itembuffer.hasRuleChainObjects(), value: 'action.paste', icon: 'content_paste', shortcut: 'M-V' } ); + contextInfo.menuItems.push( + { + action: ($event) => { + this.addNote($event); + }, + enabled: true, + value: 'rulechain.add-note', + icon: 'sticky_note_2', + shortcut: 'A-N' + } + ); contextInfo.menuItems.push( { divider: true @@ -905,7 +952,7 @@ export class RuleChainPageComponent extends PageComponent toIndexSet.add(toIndex); } }); - const noInputNodes = selectedNodes.filter((node, index) => !toIndexSet.has(index)); + const noInputNodes = selectedNodes.filter((_node, index) => !toIndexSet.has(index)); return noInputNodes.filter((node: FcRuleNode) => node.component.configurationDescriptor.nodeDefinition.inEnabled).length <= 1; } return false; @@ -923,11 +970,13 @@ export class RuleChainPageComponent extends PageComponent } }).afterClosed().subscribe((ruleChain) => { if (ruleChain) { + const selectedNotes: FcRuleNote[] = this.ruleChainCanvas.modelService.notes.getSelectedNotes(); this.ruleChainCanvas.modelService.deselectAll(); const ruleChainMetaData: RuleChainMetaData = { ruleChainId: ruleChain.id, nodes: [], - connections: [] + connections: [], + notes: deepClone(selectedNotes) }; let outputEdges: FcRuleEdge[] = []; let minX: number = null; @@ -991,7 +1040,7 @@ export class RuleChainPageComponent extends PageComponent } } }); - const noInputNodes = selectedNodes.filter((node, index) => !toIndexSet.has(index)); + const noInputNodes = selectedNodes.filter((_node, index) => !toIndexSet.has(index)); const possibleInputNodes = noInputNodes.filter((node: FcRuleNode) => node.component.configurationDescriptor.nodeDefinition.inEnabled); let inputEdges: FcRuleEdge[] = []; @@ -1037,6 +1086,10 @@ export class RuleChainPageComponent extends PageComponent node.additionalInfo.layoutX -= deltaX; node.additionalInfo.layoutY -= deltaY; }); + ruleChainMetaData.notes?.forEach((note) => { + note.x -= deltaX; + note.y -= deltaY; + }); this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe(() => { const component = this.ruleChainService.getRuleNodeComponentByClazz(this.ruleChainType, ruleChainNodeClazz); const descriptor = ruleNodeTypeDescriptors.get(component.type); @@ -1088,6 +1141,9 @@ export class RuleChainPageComponent extends PageComponent selectedNodes.forEach((node) => { this.ruleChainCanvas.modelService.nodes.delete(node); }); + selectedNotes.forEach((note) => { + this.ruleChainCanvas.modelService.notes.delete(note); + }); this.onModelChanged(); this.updateRuleNodesHighlight(); }); @@ -1144,12 +1200,15 @@ export class RuleChainPageComponent extends PageComponent } private copyNode(node: FcRuleNode) { - this.itembuffer.copyRuleNodes([node], []); + this.itembuffer.copyRuleChainObjects([node], [], []); } - private copyRuleNodes() { + private copyRuleChainObjects() { const nodes: FcRuleNode[] = this.ruleChainCanvas.modelService.nodes.getSelectedNodes(); const edges: FcRuleEdge[] = this.ruleChainCanvas.modelService.edges.getSelectedEdges(); + const notes: FcRuleNote[] = (this.ruleChainModel.notes || []).filter( + note => this.ruleChainCanvas.modelService.notes.isSelected(note) + ); const connections: RuleNodeConnection[] = []; edges.forEach((edge) => { const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); @@ -1168,10 +1227,10 @@ export class RuleChainPageComponent extends PageComponent connections.push(connection); } }); - this.itembuffer.copyRuleNodes(nodes, connections); + this.itembuffer.copyRuleChainObjects(nodes, connections, notes); } - private pasteRuleNodes(event?: MouseEvent) { + private pasteRuleChainObjects(event?: MouseEvent) { const canvas = $(this.ruleChainCanvas.modelService.canvasHtmlElement); let x: number; let y: number; @@ -1186,11 +1245,11 @@ export class RuleChainPageComponent extends PageComponent x = scrollLeft + scrollParent.width() / 2; y = scrollTop + scrollParent.height() / 2; } - const ruleNodes = this.itembuffer.pasteRuleNodes(x, y); - if (ruleNodes) { + const ruleChainObjects = this.itembuffer.pasteRuleChainObjects(x, y); + if (ruleChainObjects) { this.ruleChainCanvas.modelService.deselectAll(); const nodes: FcRuleNode[] = []; - ruleNodes.nodes.forEach((node) => { + ruleChainObjects.nodes.forEach((node) => { node.id = 'rule-chain-node-' + this.nextNodeID++; const component = node.component; if (component.configurationDescriptor.nodeDefinition.inEnabled) { @@ -1213,7 +1272,7 @@ export class RuleChainPageComponent extends PageComponent this.ruleChainModel.nodes.push(node); this.ruleChainCanvas.modelService.nodes.select(node); }); - ruleNodes.connections.forEach((connection) => { + ruleChainObjects.connections.forEach((connection) => { const sourceNode = nodes[connection.fromIndex]; const destNode = nodes[connection.toIndex]; if ( (connection.isInputSource || sourceNode) && destNode ) { @@ -1249,15 +1308,142 @@ export class RuleChainPageComponent extends PageComponent } } }); + if (ruleChainObjects.notes?.length) { + if (!this.ruleChainModel.notes) { + this.ruleChainModel.notes = []; + } + ruleChainObjects.notes.forEach((note) => { + note.id = guid(); + this.ruleChainModel.notes.push(note); + this.ruleChainCanvas.modelService.notes.select(note); + }); + } this.updateRuleNodesHighlight(); this.validate(); this.onModelChanged(); } } + addNote(event?: MouseEvent): void { + this.dialog.open>(AddNoteDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + id: '', x: 0, y: 0, width: 200, height: 120, + content: '', + backgroundColor: FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR, + applyDefaultMarkdownStyle: FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE, + markdownCss: '' + } + }).afterClosed().subscribe((noteData) => { + if (noteData !== undefined) { + 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(); + x = scrollParent.scrollLeft() + scrollParent.width() / 2; + y = scrollParent.scrollTop() + scrollParent.height() / 2; + } + const note: FcRuleNote = { + id: guid(), + x, + y, + width: 200, + height: 120, + ...noteData + }; + if (!this.ruleChainModel.notes) { + this.ruleChainModel.notes = []; + } + this.ruleChainModel.notes.push(note); + this.isDirty = true; + } + }); + } + + private prepareNoteContextMenu(note: FcRuleNote): RuleChainMenuContextInfo { + const contextInfo: RuleChainMenuContextInfo = { + headerClass: 'tb-rulechain-header', + icon: 'sticky_note_2', + title: this.translate.instant('rulechain.note'), + menuItems: [] + }; + if (!note.readonly) { + contextInfo.menuItems.push( + { + action: () => { + this.openNoteDetails(note); + }, + enabled: true, + value: 'action.edit', + icon: 'edit' + } + ); + contextInfo.menuItems.push( + { + action: () => { + this.itembuffer.copyRuleChainObjects([], [], [note]); + }, + enabled: true, + value: 'action.copy', + icon: 'content_copy' + } + ); + contextInfo.menuItems.push( + { + action: () => { + this.ruleChainCanvas.modelService.notes.delete(note); + }, + enabled: true, + value: 'action.delete', + icon: 'clear', + shortcut: 'Del' + } + ); + } + return contextInfo; + } + + private openNoteDetails(note: FcRuleNote): void { + this.enableHotKeys = false; + this.updateErrorTooltips(true); + this.isEditingRuleNode = false; + this.editingRuleNode = null; + this.isEditingRuleNodeLink = false; + this.editingRuleNodeLink = null; + this.isEditingNote = true; + this.editingNoteIndex = this.ruleChainModel.notes.indexOf(note); + this.editingNote = deepClone(note); + } + + saveNote(): void { + this.ruleNoteComponent.noteForm.markAsPristine(); + Object.assign(this.editingNote, this.ruleNoteComponent.noteForm.value); + this.ruleChainModel.notes[this.editingNoteIndex] = this.editingNote; + this.editingNote = deepClone(this.editingNote); + this.onModelChanged(); + } + + onRevertNoteEdit(): void { + this.ruleNoteComponent.noteForm.markAsPristine(); + const note = this.ruleChainModel.notes[this.editingNoteIndex]; + this.editingNote = deepClone(note); + } + + onEditNoteClosed(): void { + this.editingNote = null; + this.isEditingNote = false; + } + onDetailsDrawerClosed() { this.onEditRuleNodeClosed(); this.onEditRuleNodeLinkClosed(); + this.onEditNoteClosed(); this.enableHotKeys = true; this.updateErrorTooltips(false); } @@ -1367,7 +1553,7 @@ export class RuleChainPageComponent extends PageComponent details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description); } } - + name = this.sanitizer.sanitize(SecurityContext.HTML, name); desc = this.sanitizer.sanitize(SecurityContext.HTML, desc); @@ -1417,7 +1603,8 @@ export class RuleChainPageComponent extends PageComponent objectsSelected(): boolean { return this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length > 0 || - this.ruleChainCanvas.modelService.edges.getSelectedEdges().length > 0; + this.ruleChainCanvas.modelService.edges.getSelectedEdges().length > 0 || + this.ruleChainCanvas.modelService.notes.getSelectedNotes().length > 0; } deleteSelected() { @@ -1475,6 +1662,7 @@ export class RuleChainPageComponent extends PageComponent ruleChainId: this.ruleChain.id, nodes: [], connections: [], + notes: deepClone(this.ruleChainModel.notes) || [], version: ruleChain.version }; const nodes: FcRuleNode[] = []; @@ -1533,6 +1721,9 @@ export class RuleChainPageComponent extends PageComponent .subscribe((savedRuleChainMetaData) => { this.ruleChain.version = savedRuleChainMetaData.version; this.ruleChainMetaData = savedRuleChainMetaData; + if (!this.ruleChainMetaData.notes) { + this.ruleChainMetaData.notes = deepClone(this.ruleChainModel.notes) || []; + } if (this.isImport) { this.isDirtyValue = false; this.isImport = false; @@ -1657,6 +1848,39 @@ export class RuleChainPageComponent extends PageComponent } } + get additionalActionConfig (): AdditionalDebugActionConfig { + return { + title: this.translate.instant('action.see-debug-events'), + action: this.switchToEventsTab.bind(this) + } + } + + private switchToEventsTab() { + if(!this.ruleNodeComponent.ruleNodeFormGroup.dirty) { + this.selectedRuleNodeTabIndex = 1; + } else { + this.openDebugEventsDialog(); + } + } + + private openDebugEventsDialog(): void { + this.dialog.open(EventsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + title: 'rulenode.events', + debugEventTypes: [DebugEventType.DEBUG_RULE_NODE], + defaultEventType: DebugEventType.DEBUG_RULE_NODE, + tenantId: this.ruleChain.tenantId.id, + entityId: this.editingRuleNode.ruleNodeId, + functionTestButtonLabel: this.ruleNodeTestButtonLabel, + onDebugEventSelected: this.onDebugEventSelected.bind(this) + } + }) + .afterClosed() + .subscribe(); + } + private updateNodeErrorTooltip(node: FcRuleNode) { if (node.error) { const element = $('#' + node.id); @@ -1761,7 +1985,7 @@ export interface AddRuleNodeLinkDialogData { standalone: false }) export class AddRuleNodeLinkDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { + implements ErrorStateMatcher { ruleNodeLinkFormGroup: UntypedFormGroup; @@ -1791,9 +2015,6 @@ export class AddRuleNodeLinkDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { + implements ErrorStateMatcher { @ViewChild('tbRuleNode', {static: true}) ruleNodeDetailsComponent: RuleNodeDetailsComponent; @@ -1848,9 +2069,6 @@ export class AddRuleNodeDialogComponent extends DialogComponent> { + + @ViewChild('tbRuleNote') ruleNoteComponent: RuleNoteEditorComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: FcRuleNote, + public dialogRef: MatDialogRef>) { + super(store, router, dialogRef); + } + + cancel(): void { + this.dialogRef.close(undefined); + } + + save(): void { + if (this.ruleNoteComponent.noteForm.valid) { + this.dialogRef.close(this.ruleNoteComponent.noteForm.value); + } + } +} 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 index 84fdecafcb..b796f16db4 100644 --- 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 @@ -40,6 +40,6 @@ export interface RuleChainMenuContextInfo { icon: string; iconUrl?: string; title: string; - subtitle: string; + subtitle?: string; menuItems: RuleChainMenuItem[]; } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts index 388b23d56c..473d022b08 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts @@ -27,23 +27,27 @@ import { AddRuleNodeDialogComponent, AddRuleNodeLinkDialogComponent, CreateNestedRuleChainDialogComponent, + AddNoteDialogComponent, RuleChainPageComponent } from '@home/pages/rulechain/rulechain-page.component'; import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; import { RuleNodeLinkComponent } from '@home/pages/rulechain/rule-node-link.component'; +import { RuleNoteEditorComponent } from '@home/pages/rulechain/rule-note-editor.component'; @NgModule({ declarations: [ RuleChainPageComponent, RuleNodeDetailsComponent, + RuleNoteEditorComponent, LinkLabelsComponent, RuleNodeLinkComponent, RuleNodeConfigComponent, AddRuleNodeLinkDialogComponent, AddRuleNodeDialogComponent, - CreateNestedRuleChainDialogComponent + CreateNestedRuleChainDialogComponent, + AddNoteDialogComponent ], imports: [ CommonModule, 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 index cd93367fc0..ecf2433b31 100644 --- 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 @@ -15,46 +15,60 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} 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 index 403cbbb953..fbc7e9f286 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html @@ -77,24 +77,24 @@
-
- +
+ rulechain.name - - {{ 'rulechain.name-required' | translate }} - - - {{ 'rulechain.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'rulechain.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'rulechain.name-max-length' | translate }} + } {{ 'rulechain.debug-mode' | translate }}
- + rulechain.description - +
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 index c1f2e94a40..641a820de2 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts @@ -22,13 +22,15 @@ import { RuleChainRoutingModule } from '@modules/home/pages/rulechain/rulechain- import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component'; import { RuleNodeComponent } from '@home/pages/rulechain/rulenode.component'; -import { FC_NODE_COMPONENT_CONFIG } from 'ngx-flowchart'; +import { RuleNoteComponent } from '@home/pages/rulechain/rulenote.component'; +import { FC_NODE_COMPONENT_CONFIG, FC_NOTE_COMPONENT_CONFIG } from 'ngx-flowchart'; @NgModule({ declarations: [ RuleChainComponent, RuleChainTabsComponent, RuleNodeComponent, + RuleNoteComponent, ], providers: [ { @@ -37,6 +39,12 @@ import { FC_NODE_COMPONENT_CONFIG } from 'ngx-flowchart'; nodeComponentType: RuleNodeComponent } }, + { + provide: FC_NOTE_COMPONENT_CONFIG, + useValue: { + noteComponentType: RuleNoteComponent + } + }, ], imports: [ CommonModule, 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 index e406168acb..c125d1ab1b 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html @@ -16,52 +16,66 @@ -->
+ (dblclick)="userNodeCallbacks.doubleClick($event, node)" + (mousedown)="userNodeCallbacks.mouseDown($event, node)" + (mouseenter)="userNodeCallbacks.mouseEnter($event, node)" + (mouseleave)="userNodeCallbacks.mouseLeave($event, node)">
- {{node.icon}} - + @if (!iconUrl) { + {{node.icon}} + } + @if (iconUrl) { + + }
{{ node.component.name }} - {{ node.name }} + @if (node.name) { + {{ node.name }} + }
-
-
-
+ @for (connector of modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType); track connector) { +
+
+
+ }
-
-
-
+ @for (connector of modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType); track connector) { +
+
+
+ }
-
-
- -
-
- × -
-
-
-
- -
-
+ @if (modelservice.isEditable() && !node.readonly) { +
+
+ +
+
+ × +
+
+ } + @if (node.component?.configurationDescriptor?.nodeDefinition?.ruleChainNode && modelservice.isEditable()) { +
+
+ +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.html new file mode 100644 index 0000000000..e99ab9f965 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.html @@ -0,0 +1,44 @@ + +@if (modelservice.isEditable() && !note.readonly && edit) { +
+ +
+
+ × +
+} +
+ @if (note?.content) { + + + } +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.scss new file mode 100644 index 0000000000..18de130565 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.scss @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + width: 100%; + height: 100%; + box-sizing: border-box; + + .fc-nodeedit, + .fc-nodedelete { + display: block; + position: absolute; + border: solid 2px #eee; + border-radius: 50%; + font-weight: 600; + line-height: 20px; + height: 20px; + padding-top: 2px; + width: 22px; + background: #494949; + color: #fff; + text-align: center; + vertical-align: bottom; + cursor: pointer; + z-index: 10; + } + + .fc-nodeedit { + top: -24px; + right: 16px; + font-size: 15px; + } + + .fc-nodedelete { + top: -24px; + right: -13px; + font-size: 18px; + } + + .tb-rule-note-bg { + position: relative; + width: 100%; + height: 100%; + background-color: #FFF9C4; + border: 1px solid #E6D600; + border-radius: 8px; + box-sizing: border-box; + padding: 8px; + overflow: auto; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.ts new file mode 100644 index 0000000000..5624f4d271 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { FcNoteComponent } from 'ngx-flowchart'; +import cssjs from '@core/css/css'; +import { hashCode, isNotEmptyStr } from '@core/utils'; +import { + FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE, + FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR, + FcRuleNote +} from '@shared/models/rule-node.models'; + +@Component({ + selector: 'tb-rule-note', + templateUrl: './rulenote.component.html', + styleUrls: ['./rulenote.component.scss'], + standalone: false +}) +export class RuleNoteComponent extends FcNoteComponent implements OnInit, OnChanges { + + readonly defaultBackgroundColor = FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR; + + private static readonly HEADING_STYLE_OVERRIDE = '.tb-markdown-view h1 { font-size: 3rem; }'; + private static readonly PADDING_STYLE_OVERRIDE = '.tb-markdown-view p, .tb-markdown-view h1, .tb-markdown-view h2, .tb-markdown-view h3, .tb-markdown-view h4, .tb-markdown-view h5, .tb-markdown-view h6 {padding-left: 0 !important;}'; + + applyDefault = FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE; + additionalStyles: string[]; + noteClass: string; + note: FcRuleNote; + + ngOnInit(): void { + super.ngOnInit(); + this.applyDefault = this.note?.applyDefaultMarkdownStyle ?? FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE; + this.processCss(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.note) { + this.applyDefault = this.note?.applyDefaultMarkdownStyle ?? FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE; + this.processCss(); + } + } + + private processCss(): void { + let cssString = this.note?.markdownCss; + const styles: string[] = []; + if (this.applyDefault) { + styles.push(RuleNoteComponent.HEADING_STYLE_OVERRIDE); + styles.push(RuleNoteComponent.PADDING_STYLE_OVERRIDE); + } + if (isNotEmptyStr(cssString)) { + const cssParser = new cssjs(); + this.noteClass = 'rule-note-' + hashCode(cssString); + cssParser.cssPreviewNamespace = this.noteClass; + cssParser.testMode = false; + const cssObjects = cssParser.applyNamespacing(cssString); + cssString = cssParser.getCSSForEditor(cssObjects); + styles.push(cssString); + } else { + this.noteClass = undefined; + } + this.additionalStyles = styles.length ? styles : undefined; + } + + noteEdit(event: MouseEvent): void { + event.stopPropagation(); + this.userNoteCallbacks?.noteEdit?.(event, this.note); + } + + noteDelete(event: MouseEvent): void { + event.stopPropagation(); + this.modelservice.notes.delete(this.note); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html index 3275e3e898..81eef74152 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html @@ -46,31 +46,39 @@
scada.behavior.type
- - {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} - + @for (type of scadaSymbolBehaviorTypes; track type) { + + {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} + + }
-
-
scada.behavior.value-type
- - - -
- - {{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} -
-
- - - {{ valueTypesMap.get(valueType).name | translate }} - -
-
-
- - + @if ([ScadaSymbolBehaviorType.value, ScadaSymbolBehaviorType.action].includes(behaviorFormGroup.get('type').value)) { +
+
scada.behavior.value-type
+ + + @if (behaviorFormGroup.get('valueType').value) { + +
+ + {{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} +
+
+ } + @for (valueType of valueTypes; track valueType) { + + + {{ valueTypesMap.get(valueType).name | translate }} + + } +
+
+
+ } + @if (behaviorFormGroup.get('type').value === ScadaSymbolBehaviorType.value) { + @if (behaviorFormGroup.get('valueType').value === ValueType.BOOLEAN) {
scada.behavior.true-label
@@ -89,7 +97,7 @@
-
+ }
scada.behavior.default-settings
-
- + } + @if (behaviorFormGroup.get('type').value === ScadaSymbolBehaviorType.action) {
scada.behavior.default-settings
-
- + } + @if (behaviorFormGroup.get('type').value === ScadaSymbolBehaviorType.widgetAction) {
scada.behavior.default-settings
-
+ }
- + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html index 195d2de0fd..819bb8de99 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html @@ -24,9 +24,11 @@ - - {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} - + @for (type of scadaSymbolBehaviorTypes; track type) { + + {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} + + }
@@ -38,13 +40,15 @@ matTooltipPosition="above"> settings - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html index c65ea45ebb..f85a37dee9 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html @@ -23,43 +23,51 @@
scada.behavior.type
-
-
- - -
- -
+ @if (behaviorsFormArray().controls.length) { +
+ @for (behaviorControl of behaviorsFormArray().controls; track trackByBehavior($index, behaviorControl)) { +
+ + +
+ @if (!disabled) { + + } +
+
+ }
-
- -
-
- + } @else { + {{ 'scada.behavior.no-behaviors' | translate }} + } + @if (errorText) { + + }
+ @if (!disabled) { +
+ +
+ } - - {{ 'scada.behavior.no-behaviors' | translate }} - diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html index a7fbbfce2e..e7782cebb9 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html @@ -19,22 +19,22 @@ diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html index 643e161e37..b3a64d6c0e 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html @@ -26,23 +26,27 @@
- + @if (!disabled || tagFormGroup.get('stateRenderFunction').value) { + + }
- + @if (!disabled || tagFormGroup.get('clickAction').value) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html index 225596a062..38d1532b0f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html @@ -22,18 +22,23 @@
scada.state-render-function
scada.tag.on-click-action
-
-
- - - + @if (tagsFormArray().controls.length) { +
+ @for (tagControl of tagsFormArray().controls; track trackByTag($index, tagControl)) { +
+ + + @if (!$last) { + + } +
+ }
-
+ } @else { + {{ 'scada.tag.no-tags' | translate }} + }
- - {{ 'scada.tag.no-tags' | translate }} - diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html index 3d981e3611..30dbe4f734 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -37,8 +37,10 @@
scada.description
+ rows="1">
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html index bba6f4319e..9dfcd7bd24 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html @@ -43,14 +43,16 @@ remove
-
- -
+ @if (displayShowHidden) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index 60baf0e63b..f4065f8f93 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -184,30 +184,36 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem @Component({ template: `
- {{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }}: - {{ symbolElement?.tag }} - - - -
`, + {{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }}: + {{ symbolElement?.tag }} + @if (!symbolElement?.readonly) { + + } + @if (displayTagSettings) { + + } + @if (!symbolElement?.readonly) { + + } +
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], encapsulation: ViewEncapsulation.None, standalone: false @@ -360,37 +366,49 @@ class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent imp @Component({ template: `
-
scada.state-render-function
- - -
scada.tag.on-click-action
- - -
`, + @if (!symbolElement?.readonly || hasStateRenderFunction) { +
scada.state-render-function
+ } + @if (hasStateRenderFunction) { + + } + @if (!hasStateRenderFunction && !symbolElement?.readonly) { + + } + @if (!symbolElement?.readonly || hasClickAction) { +
scada.tag.on-click-action
+ } + @if (hasClickAction) { + + } + @if (!hasClickAction && !symbolElement?.readonly) { + + } +
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], encapsulation: ViewEncapsulation.None, standalone: false diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index 5b8abc406e..06d8ff153b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -37,46 +37,48 @@ {{ 'scada.create-widget' | translate }} -
-
- - - - -
-
-
-
-
scada.symbol.symbol
- - + @if (previewMode) { +
+
+ + + + +
+
+
+
+
scada.symbol.symbol
+ + +
+ +
- -
-
+ }
visibility {{ 'scada.preview' | translate }} -
- - -
+ @if (!readonly) { +
+ + +
+ }
- - - + @if (!previewMode) { + + } + @if (previewMode) { + + + }
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html index 3822fea0e1..bb0eace707 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html @@ -24,15 +24,20 @@ close - - -
+@if (isLoading$ | async) { + + +} @else { +
+}

security.2fa.dialog.backup-code-description

-
- {{ code }} -
+ @for (code of backupCode?.codes; track code) { +
+ {{ code }} +
+ }
- - -
+@if (isLoading$ | async) { + + +} @else { +
+}
@@ -37,16 +40,20 @@

security.2fa.dialog.email-step-description

- + - - {{ 'user.email-required' | translate }} - - - {{ 'user.invalid-email-format' | translate }} - + @if (emailConfigForm.get('email').hasError('required')) { + + {{ 'user.email-required' | translate }} + + } + @if (emailConfigForm.get('email').hasError('email')) { + + {{ 'user.invalid-email-format' | translate }} + + } - - -
+@if (isLoading$ | async) { + + +} @else { +
+}
@@ -39,6 +42,7 @@
@@ -58,15 +62,17 @@ {{ 'security.2fa.dialog.verification-step-description' | translate : {address: smsConfigForm.get('phone').value} }}

- + - - {{ 'security.2fa.dialog.verification-code-invalid' | translate }} - + @if (smsVerificationForm.get('verificationCode').invalid) { + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + }
- - -
+@if (isLoading$ | async) { + + +} @else { +
+}
@@ -52,16 +55,31 @@

security.2fa.dialog.scan-qr-code

+

login.enter-key-manually

+
+ {{ totpAuthURLSecret }} + + +

security.2fa.dialog.enter-verification-code

- + - - {{ 'security.2fa.dialog.verification-code-invalid' | translate }} - + @if (totpConfigForm.get('verificationCode').invalid) { + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts index e110b1ebaf..fe394393e7 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts @@ -43,6 +43,7 @@ export class TotpAuthDialogComponent extends DialogComponent; @@ -56,6 +57,7 @@ export class TotpAuthDialogComponent extends DialogComponent { this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig; this.totpAuthURL = this.authAccountConfig.authUrl; + this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret'); this.authAccountConfig.useByDefault = true; import('qrcode').then((QRCode) => { unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.html b/ui-ngx/src/app/modules/home/pages/security/security.component.html index f2bdd657a8..548160ec32 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.html @@ -30,51 +30,77 @@
+ +
+ + api-key.api-keys + + +
+

profile.change-password

- + profile.current-password - - {{ 'security.password-requirement.incorrect-password-try-again' | translate }} - + @if (changePassword.get('currentPassword').hasError('differencePassword')) { + + {{ 'security.password-requirement.incorrect-password-try-again' | translate }} + + } - + login.new-password - - {{ 'security.password-requirement.password-not-meet-requirements' | translate }} - - - {{ changePassword.get('newPassword').getError('alreadyUsed') }} - - - {{ 'security.password-requirement.password-should-difference' | translate }} - - - {{ 'security.password-requirement.password-should-not-contain-spaces' | translate }} - + @if (changePassword.get('newPassword').errors + && !changePassword.get('newPassword').hasError('alreadyUsed') + && !changePassword.get('newPassword').hasError('hasWhitespaces') + && !changePassword.get('newPassword').hasError('passwordSameAsOld')) { + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + } + @if (changePassword.get('newPassword').hasError('alreadyUsed')) { + + {{ changePassword.get('newPassword').getError('alreadyUsed') }} + + } + @if (changePassword.get('newPassword').hasError('passwordSameAsOld')) { + + {{ 'security.password-requirement.password-should-difference' | translate }} + + } + @if (changePassword.get('newPassword').hasError('hasWhitespaces')) { + + {{ 'security.password-requirement.password-should-not-contain-spaces' | translate }} + + }
- + login.new-password-again - - {{ 'security.password-requirement.new-passwords-not-match' | translate }} - + @if (changePassword.get('newPassword2').hasError('passwordsNotMatch')) { + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + }
@@ -83,47 +109,61 @@
-
-

security.password-requirement.password-requirements

-

security.password-requirement.at-least

-

- - {{ changePassword.get('newPassword').hasError('notUpperCase') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.uppercase-letter' | translate : {count: passwordPolicy.minimumUppercaseLetters} }} -

-

- - {{ changePassword.get('newPassword').hasError('notLowerCase') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.lowercase-letter' | translate : {count: passwordPolicy.minimumLowercaseLetters} }} -

-

- - {{ changePassword.get('newPassword').hasError('notNumeric') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.digit' | translate : {count: passwordPolicy.minimumDigits} }} -

-

- - {{ changePassword.get('newPassword').hasError('notSpecial') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.special-character' | translate : {count: passwordPolicy.minimumSpecialCharacters} }} -

-

- - {{ changePassword.get('newPassword').hasError('minLength') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }} -

-
-

security.password-requirement.at-most

-

- - {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.maximumLength} }} -

+ @if (passwordPolicy) { +
+

security.password-requirement.password-requirements

+

security.password-requirement.at-least

+ @if (passwordPolicy.minimumUppercaseLetters > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notUpperCase') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.uppercase-letter' | translate : {count: passwordPolicy.minimumUppercaseLetters} }} +

+ } + @if (passwordPolicy.minimumLowercaseLetters > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notLowerCase') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.lowercase-letter' | translate : {count: passwordPolicy.minimumLowercaseLetters} }} +

+ } + @if (passwordPolicy.minimumDigits > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notNumeric') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.digit' | translate : {count: passwordPolicy.minimumDigits} }} +

+ } + @if (passwordPolicy.minimumSpecialCharacters > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notSpecial') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.special-character' | translate : {count: passwordPolicy.minimumSpecialCharacters} }} +

+ } + @if (passwordPolicy.minimumLength > 0) { +

+ + {{ changePassword.get('newPassword').hasError('minLength') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }} +

+ } + @if (passwordPolicy.maximumLength > 0) { +
+

security.password-requirement.at-most

+

+ + {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.maximumLength} }} +

+
+ }
-
+ }
- - - admin.2fa.2fa - - -
security.2fa.2fa-description
-
-

security.2fa.authenticate-with

-
- -
-

{{ providersData.get(provider).name | translate }}

-
-
- {{ providersData.get(provider).description | translate }} + @if (allowTwoFactorProviders.length) { + + + admin.2fa.2fa + + +
security.2fa.2fa-description
+
+

security.2fa.authenticate-with

+ + @for (provider of allowTwoFactorProviders; track trackByProvider($index, provider)) { +
+

{{ providersData.get(provider).name | translate }}

+
+ @if (!twoFactorAuth.get(provider).value) { +
+ {{ providersData.get(provider).description | translate }} +
+ } @else { +
+ {{ providersData.get(provider).activatedHint | translate: providerDataInfo(provider) }} +
+ } + +
- -
- {{ providersData.get(provider).activatedHint | translate: providerDataInfo(provider) }} -
-
- - + @if (twoFactorAuth.get(provider).value && provider !== twoFactorAuthProviderType.BACKUP_CODE && !activeSingleProvider) { + + security.2fa.main-2fa-method + + } + @if (twoFactorAuth.get(provider).value && provider === twoFactorAuthProviderType.BACKUP_CODE) { + + }
- - security.2fa.main-2fa-method - - -
- - - - + @if (!$last) { + + } + } + + + }
diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts index 6b66cc7eb6..60a8c54e0d 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.ts @@ -21,9 +21,9 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, + FormGroupDirective, UntypedFormBuilder, - UntypedFormGroup, FormGroupDirective, - NgForm, + UntypedFormGroup, ValidationErrors, ValidatorFn, Validators @@ -48,10 +48,15 @@ import { import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map'; import { takeUntil, tap } from 'rxjs/operators'; import { Observable, of, Subject } from 'rxjs'; -import { isDefinedAndNotNull, isEqual } from '@core/utils'; +import { isDefinedAndNotNull } from '@core/utils'; import { AuthService } from '@core/auth/auth.service'; import { UserPasswordPolicy } from '@shared/models/settings.models'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { + ApiKeysTableDialogComponent, + ApiKeysTableDialogData +} from '@home/components/api-key/api-keys-table-dialog.component'; +import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; @Component({ selector: 'tb-security', @@ -165,7 +170,12 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro this.changePassword = this.fb.group({ currentPassword: [''], newPassword: ['', Validators.required], - newPassword2: ['', this.samePasswordValidation(false, 'newPassword')] + newPassword2: [''] + }, { + validators: [ + this.passwordNotSameAsOld(), + passwordsMatchValidator('newPassword', 'newPassword2'), + ] }); } @@ -173,64 +183,36 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro this.authService.getUserPasswordPolicy().subscribe(policy => { this.passwordPolicy = policy; this.changePassword.get('newPassword').setValidators([ - this.passwordStrengthValidator(), - this.samePasswordValidation(true, 'currentPassword'), + passwordStrengthValidator(this.passwordPolicy), Validators.required ]); this.changePassword.get('newPassword').updateValueAndValidity({emitEvent: false}); }); } - private passwordStrengthValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const value: string = control.value; - const errors: any = {}; - - if (this.passwordPolicy.minimumUppercaseLetters > 0 && - !new RegExp(`(?:.*?[A-Z]){${this.passwordPolicy.minimumUppercaseLetters}}`).test(value)) { - errors.notUpperCase = true; - } - - if (this.passwordPolicy.minimumLowercaseLetters > 0 && - !new RegExp(`(?:.*?[a-z]){${this.passwordPolicy.minimumLowercaseLetters}}`).test(value)) { - errors.notLowerCase = true; - } - - if (this.passwordPolicy.minimumDigits > 0 - && !new RegExp(`(?:.*?\\d){${this.passwordPolicy.minimumDigits}}`).test(value)) { - errors.notNumeric = true; - } - - if (this.passwordPolicy.minimumSpecialCharacters > 0 && - !new RegExp(`(?:.*?[\\W_]){${this.passwordPolicy.minimumSpecialCharacters}}`).test(value)) { - errors.notSpecial = true; - } + passwordNotSameAsOld(): ValidatorFn { + return (group: AbstractControl): ValidationErrors | null => { + const currentPassControl = group.get('currentPassword'); + const newPassControl = group.get('newPassword'); - if (!this.passwordPolicy.allowWhitespaces && /\s/.test(value)) { - errors.hasWhitespaces = true; - } - if (this.passwordPolicy.minimumLength > 0 && value.length < this.passwordPolicy.minimumLength) { - errors.minLength = true; - } + const current = currentPassControl?.value ?? ''; + const newPass = newPassControl?.value ?? ''; - if (!value.length || this.passwordPolicy.maximumLength > 0 && value.length > this.passwordPolicy.maximumLength) { - errors.maxLength = true; - } - - return isEqual(errors, {}) ? null : errors; - }; - } - - private samePasswordValidation(isSame: boolean, key: string): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const value: string = control.value; - const keyValue = control.parent?.value[key]; - - if (isSame) { - return value === keyValue ? {samePassword: true} : null; + if (current && newPass && current === newPass) { + newPassControl?.setErrors({ + ...newPassControl.errors, + passwordSameAsOld: true + }); + return { passwordSameAsOld: true }; + } else { + const currentErrors = newPassControl?.errors; + if (currentErrors?.passwordSameAsOld) { + const { passwordSameAsOld, ...rest } = currentErrors; + newPassControl.setErrors(Object.keys(rest).length ? rest : null); + } + return null; } - return value !== keyValue ? {differencePassword: true} : null; }; } @@ -352,22 +334,24 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro onChangePassword(form: FormGroupDirective): void { if (this.changePassword.valid) { this.authService.changePassword(this.changePassword.get('currentPassword').value, - this.changePassword.get('newPassword').value, {ignoreErrors: true}).subscribe(() => { - this.discardChanges(form); - }, - (error) => { - if (error.status === 400 && error.error.message === 'Current password doesn\'t match!') { - this.changePassword.get('currentPassword').setErrors({differencePassword: true}); - } else if (error.status === 400 && error.error.message.startsWith('Password must')) { - this.loadPasswordPolicy(); - } else if (error.status === 400 && error.error.message.startsWith('Password was already used')) { - this.changePassword.get('newPassword').setErrors({alreadyUsed: error.error.message}); - } else { - this.store.dispatch(new ActionNotificationShow({ - message: error.error.message, - type: 'error', - target: 'changePassword' - })); + this.changePassword.get('newPassword').value, {ignoreErrors: true}).subscribe({ + next: () => { + this.discardChanges(form); + }, + error: (error) => { + if (error.status === 400 && error.error.message === 'Current password doesn\'t match!') { + this.changePassword.get('currentPassword').setErrors({ differencePassword: true }); + } else if (error.status === 400 && error.error.message.startsWith('Password must')) { + this.loadPasswordPolicy(); + } else if (error.status === 400 && error.error.message.startsWith('Password was already used')) { + this.changePassword.get('newPassword').setErrors({ alreadyUsed: error.error.message }); + } else { + this.store.dispatch(new ActionNotificationShow({ + message: error.error.message, + type: 'error', + target: 'changePassword' + })); + } } }); } else { @@ -385,4 +369,15 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro newPassword2: '' }); } + + openApiKeysTable() { + this.dialog.open( + ApiKeysTableDialogComponent, { + disableClose: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + userId: this.user.id, + } + }).afterClosed().subscribe(); + } } 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 index 3e7a150552..d8e4ab8bda 100644 --- 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 @@ -15,24 +15,26 @@ limitations under the License. --> - - - - - - - - - - - +@if (entity) { + + + + + + + + + @if (!isEdit) { + + + + } +} 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 index 2f2ee3e924..c2485dc7f7 100644 --- 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 @@ -15,33 +15,27 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - +@if (entity) { + + + + + + + + + + + + + + +} 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 index 83d4e47d85..8e7b639c38 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html @@ -48,17 +48,22 @@
- + tenant.title - - {{ 'tenant.title-required' | translate }} - - - {{ 'tenant.title-max-length' | translate }} - + @if (entityForm.get('title').hasError('required')) { + + {{ 'tenant.title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'tenant.title-max-length' | translate }} + + }
- + @if (entity?.id) { +
+
+ + + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} + +
+
+ } + + tenant.description - + -
-
- - - {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} - -
-
-
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 index 1ac1ff6cf2..c8e1b97f5f 100644 --- 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 @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
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 index 636b56116d..60ae3ad1c2 100644 --- 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 @@ -26,17 +26,22 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + user.activation-method - - {{ activationMethodTranslations.get(activationMethodEnum[activationMethod]) | translate }} - + @for (activationMethod of activationMethods; track activationMethod) { + + {{ activationMethodTranslations.get(activationMethodEnum[activationMethod]) | translate }} + + }
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 index a58931e2f3..c8b7aabdac 100644 --- 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 @@ -85,6 +85,12 @@ export class AddUserDialogComponent extends DialogComponent { 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 index 84a8c8dd19..1046a3799e 100644 --- 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 @@ -15,28 +15,42 @@ limitations under the License. --> - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity) { + + + +} 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 index 1c3f13fcc2..789d2244b6 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.html @@ -46,13 +46,14 @@ [class.!hidden]="isEdit || isUserActivated()"> {{'user.resend-activation' | translate }} - + @if (loginAsUserEnabled$ | async) { + + } - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
{{ dialogTitle }} - + widget.title - - {{ 'widget.title-required' | translate }} - + @if (saveWidgetTypeAsFormGroup.get('title').hasError('required')) { + + {{ 'widget.title-required' | translate }} + + } 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 index ba70e617eb..60ecf4ea6d 100644 --- 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 @@ -26,20 +26,25 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + @for (type of allWidgetTypes; track 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 index c9c3f634ae..685f0f3336 100644 --- 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 @@ -27,9 +27,11 @@ - - {{ widgetTypesDataMap.get(widgetTypes[type]).name | translate }} - + @for (type of allWidgetTypes; track type) { + + {{ widgetTypesDataMap.get(widgetTypes[type]).name | translate }} + + } @@ -48,15 +50,17 @@ undo action.undo - + @if (!isReadOnly) { + + } - + @if (!isReadOnly) { + + } -
+ @for (resource of widget.resources; track resource; let i = $index) { +
+ + + + {{ 'widget.resource-is-extension' | translate }} + + +
+ }
@@ -315,10 +328,12 @@
-
- -
+ @if (!iframeWidgetEditModeInited) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html index e365b185e8..f6bb65468b 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html @@ -17,7 +17,9 @@ --> - {{ label }} + @if (label) { + {{ label }} + } - + @if (selectWidgetTypeFormGroup.get('widgetType').value && !disabled) { + + } - -
- {{ widgetType.name }} -
-
-
{{ 'widget.deprecated' | translate }}
+ @for (widgetType of filteredWidgetTypes | async; track widgetType) { + +
+ {{ widgetType.name }} +
+
+ @if (widgetType.deprecated) { +
{{ 'widget.deprecated' | translate }}
+ } +
-
- - -
-
- {{ 'widget.no-widgets-text' | translate }} + + } + @if (!(filteredWidgetTypes | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'widget.no-widgets-text' | translate }} +
+ } @else { + + {{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }} + + }
- - - {{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }} - - -
- + + } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html index 77957a8008..ec789ed6d2 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html @@ -15,9 +15,11 @@ limitations under the License. --> - - - +@if (isTenantWidgetType() && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html index 871b9b37ef..b3f4d8024f 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html @@ -44,27 +44,27 @@
- + widget.title - - {{ 'widget.title-required' | translate }} - - - {{ 'widget.title-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + + {{ 'widget.title-required' | translate }} + + } + @if (entityForm.get('name').hasError('maxlength')) { + + {{ 'widget.title-max-length' | translate }} + + } - - widget.description - - {{descriptionInput.value?.length || 0}}/1024 -
@@ -75,6 +75,11 @@ {{ 'widget.deprecated' | translate }}
+ + widget.description + + {{descriptionInput.value?.length || 0}}/1024 +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html index c5c8ed259e..c3a965b752 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- - - +@if (isTenantWidgetsBundle() && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html index 39308311ab..e56c3370ae 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html @@ -27,13 +27,14 @@ {{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }} - + @if (!isReadOnly) { + + }
- - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
-
-
- {{ widget.name }} -
-
{{ widget.name }}
-
{{ 'widget.deprecated' | translate }}
+ @for (widget of widgets; track trackByWidget($index, widget); let isLast = $last) { +
+
+ {{ widget.name }} +
+
{{ widget.name }}
+ @if (widget.deprecated) { +
{{ 'widget.deprecated' | translate }}
+ } +
+ @if (!editMode) { + + } + @if (!editMode) { + + } + @if (editMode && !addMode) { + + } + @if (editMode) { + + }
- - - - -
+ }
-
- add -
-
-
+ @if (editMode && !addMode && widgets?.length) { +
add -
widget.add-existing-widget
-
-
-
-
widget.no-widgets
-
-
- - -
- + } + @if (editMode && !addMode && !widgets?.length) { +
+
+ add +
widget.add-existing-widget
+
-
+ } + @if (isReadOnly && !widgets?.length) { +
+
+
widget.no-widgets
+
+ } + @if (addMode) { +
+ + +
+ +
+
+ } - - - + @if (editMode && (widgets?.length || isDirty)) { + + } + @if (editMode && (widgets?.length || isDirty)) { + + } + @if (!isReadOnly && !editMode) { + + } 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 index 6546118554..7f607c1730 100644 --- 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 @@ -15,61 +15,67 @@ limitations under the License. --> -
- - - - -
+@if (!standalone) { +
+ + + + +
+}
- + widgets-bundle.title - - {{ 'widgets-bundle.title-required' | translate }} - - - {{ 'widgets-bundle.title-max-length' | translate }} - + @if (entityForm.get('title').hasError('required')) { + + {{ 'widgets-bundle.title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'widgets-bundle.title-max-length' | translate }} + + } - - widgets-bundle.description - - {{descriptionInput.value?.length || 0}}/1024 - {{ 'widgets-bundle.scada' | translate }} - + widgets-bundle.order + + widgets-bundle.description + + {{descriptionInput.value?.length || 0}}/1024 +
diff --git a/ui-ngx/src/app/modules/login/login-routing.module.ts b/ui-ngx/src/app/modules/login/login-routing.module.ts index 18dca78ba2..4f11cc1e92 100644 --- a/ui-ngx/src/app/modules/login/login-routing.module.ts +++ b/ui-ngx/src/app/modules/login/login-routing.module.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { inject, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; import { LoginComponent } from './pages/login/login.component'; import { AuthGuard } from '@core/guards/auth.guard'; @@ -25,6 +25,22 @@ import { CreatePasswordComponent } from '@modules/login/pages/login/create-passw import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { Authority } from '@shared/models/authority.enum'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; +import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component'; +import { of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { AuthService } from '@core/auth/auth.service'; +import { UserPasswordPolicy } from '@shared/models/settings.models'; + +const passwordPolicyResolver: ResolveFn = (route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router = inject(Router), + authService = inject(AuthService)) => { + return authService.getUserPasswordPolicy({ignoreErrors: true}).pipe( + catchError(() => { + return of({} as UserPasswordPolicy); + }) + ); +}; const routes: Routes = [ { @@ -52,7 +68,10 @@ const routes: Routes = [ title: 'login.reset-password', module: 'public' }, - canActivate: [AuthGuard] + canActivate: [AuthGuard], + resolve: { + passwordPolicy: passwordPolicyResolver + } }, { path: 'login/resetExpiredPassword', @@ -62,7 +81,10 @@ const routes: Routes = [ module: 'public', expiredPassword: true }, - canActivate: [AuthGuard] + canActivate: [AuthGuard], + resolve: { + passwordPolicy: passwordPolicyResolver + } }, { path: 'login/createPassword', @@ -71,7 +93,10 @@ const routes: Routes = [ title: 'login.create-password', module: 'public' }, - canActivate: [AuthGuard] + canActivate: [AuthGuard], + resolve: { + passwordPolicy: passwordPolicyResolver + } }, { path: 'login/mfa', @@ -83,6 +108,16 @@ const routes: Routes = [ }, canActivate: [AuthGuard] }, + { + path: 'login/force-mfa', + component: ForceTwoFactorAuthLoginComponent, + data: { + title: 'login.two-factor-authentication', + auth: [Authority.MFA_CONFIGURATION_TOKEN], + module: 'public' + }, + canActivate: [AuthGuard] + }, { path: 'activationLinkExpired', component: LinkExpiredComponent, diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts index 45d3c04c41..3cb9dbae0c 100644 --- a/ui-ngx/src/app/modules/login/login.module.ts +++ b/ui-ngx/src/app/modules/login/login.module.ts @@ -25,6 +25,7 @@ import { ResetPasswordComponent } from '@modules/login/pages/login/reset-passwor import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; +import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component'; @NgModule({ declarations: [ @@ -33,7 +34,8 @@ import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.co ResetPasswordComponent, CreatePasswordComponent, TwoFactorAuthLoginComponent, - LinkExpiredComponent + LinkExpiredComponent, + ForceTwoFactorAuthLoginComponent, ], imports: [ CommonModule, 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 index 06a1e838af..a73d8b3a25 100644 --- 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 @@ -15,45 +15,65 @@ limitations under the License. --> -
- - - - login.create-password - +
+ + + login.create-password - - - - +
+ @if (isLoading || (isLoading$ | async)) { + + + } + @if (!isLoading && !(isLoading$ | async)) { + + } +
-
-
- - - common.password - - lock - - - - login.password-again - - lock - - -
- - -
-
-
+ + common.password + + lock + + @if (createPassword.get('newPassword').errors) { + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + } + + + login.password-again + + lock + + @if (createPassword.get('newPassword2').hasError('passwordsNotMatch')) { + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + } + +
+ + +
+ + 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 index cfc57d3bb6..c72f1751a2 100644 --- 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 @@ -14,62 +14,59 @@ /// limitations under the License. /// -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component } 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 { UntypedFormBuilder } from '@angular/forms'; -import { ActionNotificationShow } from '@core/notification/notification.actions'; -import { TranslateService } from '@ngx-translate/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { UserPasswordPolicy } from '@shared/models/settings.models'; +import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; +import { finalize } from 'rxjs/operators'; +import { PageComponent } from '@shared/components/page.component'; @Component({ selector: 'tb-create-password', templateUrl: './create-password.component.html', - styleUrls: ['./create-password.component.scss'], + styleUrls: ['./password.component.scss'], standalone: false }) -export class CreatePasswordComponent extends PageComponent implements OnInit, OnDestroy { +export class CreatePasswordComponent extends PageComponent { - activateToken = ''; - sub: Subscription; + passwordPolicy: UserPasswordPolicy; + createPassword: FormGroup; - createPassword = this.fb.group({ - password: [''], - password2: [''] - }); + isLoading = false; - constructor(protected store: Store, - private route: ActivatedRoute, + private activateToken: string; + + constructor(private route: ActivatedRoute, private authService: AuthService, - private translate: TranslateService, - public fb: UntypedFormBuilder) { - super(store); - } + private fb: FormBuilder) { + super(); + this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; + this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; - ngOnInit() { - this.sub = this.route - .queryParams - .subscribe(params => { - this.activateToken = params.activateToken || ''; - }); + this.buildCreatePasswordForm(); } - ngOnDestroy(): void { - super.ngOnDestroy(); - this.sub.unsubscribe(); + private buildCreatePasswordForm() { + this.createPassword = this.fb.group({ + newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]], + newPassword2: [''] + }, { + validators: [ + passwordsMatchValidator('newPassword', 'newPassword2'), + ] + }); } 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' })); + if (this.createPassword.invalid) { + this.createPassword.markAllAsTouched(); } else { - this.authService.activate( - this.activateToken, - this.createPassword.get('password').value, true).subscribe(); + this.isLoading = true; + this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true).pipe( + finalize(() => {this.isLoading = false;}) + ).subscribe(() => {}); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html new file mode 100644 index 0000000000..7126e6b3a2 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -0,0 +1,322 @@ + + + + + + + + + diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss new file mode 100644 index 0000000000..46b613b1f1 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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; + width: 100%; + height: 100%; + + .tb-two-factor-auth-login-content { + background-color: #eee; + --mat-card-elevated-container-elevation: none; + + .progress-bar { + z-index: 10; + margin-bottom: -4px; + width: 450px; + } + + .tb-two-factor-auth-login-card { + max-height: 100vh; + overflow: auto; + padding: 48px 48px 48px 16px; + + @media #{$mat-xs} { + height: 100%; + } + + @media #{$mat-gt-xs} { + width: 450px !important; + } + + .mat-mdc-card-title { + font: 400 28px / 36px Roboto, "Helvetica Neue", sans-serif; + } + + .mat-mdc-card-header { + padding: 0; + } + + .mat-mdc-card-content { + margin-top: 34px; + margin-left: 40px; + padding: 0; + } + + .mat-body { + letter-spacing: 0.25px; + line-height: 16px; + } + + .backup-code { + p { + text-align: justify; + } + + .container { + border: 1px solid; + border-radius: 4px; + gap: 16px; + display: grid; + grid-template-columns: 1fr 1fr; + justify-items: center; + padding: 16px 0; + margin-bottom: 16px; + + .code { + letter-spacing: 0.25px; + font-family: Roboto Mono, "Helvetica Neue", monospace; + } + } + + .action-buttons { + margin-bottom: 40px; + } + } + } + } + + ::ng-deep { + .tb-two-factor-auth-login-content { + .tb-two-factor-auth-login-card { + button.mat-mdc-icon-button { + .mat-icon { + color: rgba(255, 255, 255, 0.8); + } + } + } + .mat-mdc-form-field .mat-mdc-form-field-hint-wrapper { + color: rgba(255, 255, 255, 0.8); + } + } + + button.provider, button.navigation { + text-align: start; + font-weight: 400; + color: rgba(255, 255, 255, 0.8); + &:not([disabled][disabled]) { + border-color: rgba(255, 255, 255, .8); + } + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts new file mode 100644 index 0000000000..364ce7ffac --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts @@ -0,0 +1,318 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, OnDestroy, OnInit, signal, ViewChild } 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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { + AccountTwoFaSettings, + BackupCodeTwoFactorAuthAccountConfig, + TotpTwoFactorAuthAccountConfig, + TwoFactorAuthAccountConfig, + twoFactorAuthProvidersEnterCodeCardTranslate, + twoFactorAuthProvidersLoginData, + twoFactorAuthProvidersSuccessCardTranslate, + TwoFactorAuthProviderType +} from '@shared/models/two-factor-auth.models'; +import { phoneNumberPattern } from '@shared/models/settings.models'; +import { deepClone, isDefinedAndNotNull, unwrapModule } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import printTemplate from '@home/pages/security/authentication-dialog/backup-code-print-template.raw'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { mergeMap, tap } from 'rxjs/operators'; +import { ActionNotificationShow } from "@core/notification/notification.actions"; + +enum ForceTwoFAState { + SETUP = 'setup', + AUTHENTICATOR_APP = 'authenticatorApp', + SMS = 'sms', + EMAIL = 'email', + BACKUP_CODE = 'backupCode', +} + +enum ProvidersState { + INPUT = 'INPUT', + ENTER_CODE = 'ENTER_CODE', + SUCCESS = 'SUCCESS', +} + +enum BackupCodeState { + CODE = 'CODE', + SUCCESS = 'SUCCESS', +} + +@Component({ + selector: 'tb-force-two-factor-auth-login', + templateUrl: './force-two-factor-auth-login.component.html', + styleUrls: ['./force-two-factor-auth-login.component.scss'], + standalone: false +}) +export class ForceTwoFactorAuthLoginComponent extends PageComponent implements OnInit, OnDestroy { + + TwoFactorAuthProviderType = TwoFactorAuthProviderType; + providersData = twoFactorAuthProvidersLoginData; + allowProviders: TwoFactorAuthProviderType[] = []; + config: AccountTwoFaSettings; + + twoFactorAuthProvidersEnterCodeCardTranslate = twoFactorAuthProvidersEnterCodeCardTranslate; + twoFactorAuthProvidersSuccessCardTranslate = twoFactorAuthProvidersSuccessCardTranslate; + + ForceTwoFAState = ForceTwoFAState; + ProvidersState = ProvidersState; + BackupCodeState = BackupCodeState + + state = signal(ForceTwoFAState.SETUP); + appState = signal(ProvidersState.INPUT); + smsState = signal(ProvidersState.INPUT); + emailState = signal(ProvidersState.INPUT); + backupCodeState = signal(BackupCodeState.CODE); + + totpAuthURL: string; + totpAuthURLSecret: string; + backupCode: BackupCodeTwoFactorAuthAccountConfig; + + configForm: UntypedFormGroup; + smsConfigForm: UntypedFormGroup; + emailConfigForm: UntypedFormGroup; + + private providersInfo: TwoFactorAuthProviderType[]; + private authAccountConfig: TwoFactorAuthAccountConfig; + private useByDefault: boolean = true; + + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + + constructor(protected store: Store, + private authService: AuthService, + private twoFaService: TwoFactorAuthenticationService, + private importExportService: ImportExportService, + public dialog: MatDialog, + public dialogService: DialogService, + private fb: UntypedFormBuilder) { + super(store); + } + + ngOnInit() { + this.providersInfo = this.authService.forceTwoFactorAuthProviders; + this.allowedProviders(); + this.configForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + + this.smsConfigForm = this.fb.group({ + phone: ['', [Validators.required, Validators.pattern(phoneNumberPattern)]] + }); + + this.emailConfigForm = this.fb.group({ + email: [getCurrentAuthUser(this.store).sub, [Validators.required, Validators.email]] + }); + + this.twoFaService.getAccountTwoFaSettings().subscribe(accountConfig => { + if (accountConfig) { + this.config = accountConfig; + this.useByDefault = false; + } + }); + } + + goBackByType(type: TwoFactorAuthProviderType) { + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.appState.set(ProvidersState.INPUT); + this.updateQRCode(); + break; + case TwoFactorAuthProviderType.SMS: + this.smsState.set(ProvidersState.INPUT); + break; + case TwoFactorAuthProviderType.EMAIL: + this.emailState.set(ProvidersState.INPUT); + break; + } + } + + get isAnyProviderAvailable() { + return this.config?.configs ? Object.keys(this.config?.configs)?.length < this.allowProviders?.length : true; + } + + private allowedProviders() { + if (isDefinedAndNotNull(this.config)) { + this.allowProviders = this.providersInfo; + } else { + this.allowProviders = this.providersInfo.filter(provider => provider !== TwoFactorAuthProviderType.BACKUP_CODE); + } + } + + updateState(type: TwoFactorAuthProviderType) { + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.state.set(ForceTwoFAState.AUTHENTICATOR_APP); + this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => { + this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig; + this.totpAuthURL = this.authAccountConfig.authUrl; + this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret'); + this.authAccountConfig.useByDefault = this.useByDefault; + this.useByDefault = false; + this.updateQRCode(); + }); + break; + case TwoFactorAuthProviderType.SMS: + this.state.set(ForceTwoFAState.SMS); + break; + case TwoFactorAuthProviderType.EMAIL: + this.state.set(ForceTwoFAState.EMAIL); + break; + case TwoFactorAuthProviderType.BACKUP_CODE: + this.state.set(ForceTwoFAState.BACKUP_CODE); + this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.BACKUP_CODE).pipe( + tap((data: BackupCodeTwoFactorAuthAccountConfig) => this.backupCode = data), + mergeMap(data => this.twoFaService.verifyAndSaveTwoFaAccountConfig(data, null, {ignoreLoading: true})) + ).subscribe((config) => { + this.config = config; + }); + break; + } + } + + sendSmsCode() { + if (this.smsConfigForm.valid) { + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.SMS, + useByDefault: this.useByDefault, + phoneNumber: this.smsConfigForm.get('phone').value as string + }; + this.useByDefault = false; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.smsState.set(ProvidersState.ENTER_CODE)); + } + } + + sendEmailCode() { + if (this.emailConfigForm.valid) { + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.EMAIL, + useByDefault: this.useByDefault, + email: this.emailConfigForm.get('email').value as string + }; + this.useByDefault = false; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.emailState.set(ProvidersState.ENTER_CODE)); + } + } + + tryAnotherWay(type: TwoFactorAuthProviderType) { + this.state.set(ForceTwoFAState.SETUP); + this.configForm.reset(); + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.appState.set(ProvidersState.INPUT); + break; + case TwoFactorAuthProviderType.SMS: + this.smsState.set(ProvidersState.INPUT); + this.smsConfigForm.reset(); + break; + case TwoFactorAuthProviderType.EMAIL: + this.emailState.set(ProvidersState.INPUT) + this.emailConfigForm.get('email').reset(getCurrentAuthUser(this.store).sub); + break; + } + } + + saveConfig(type: TwoFactorAuthProviderType) { + if (this.configForm.valid) { + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.configForm.get('verificationCode').value).subscribe({ + next: (config) => { + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.appState.set(ProvidersState.SUCCESS); + break; + case TwoFactorAuthProviderType.SMS: + this.smsState.set(ProvidersState.SUCCESS); + break; + case TwoFactorAuthProviderType.EMAIL: + this.emailState.set(ProvidersState.SUCCESS); + break; + } + this.config = config; + this.authAccountConfig = null; + this.allowedProviders(); + }, + error: error => { + if (error.status === 400) { + this.configForm.get('verificationCode').setErrors({incorrectCode: true}); + } else if (error.status === 429) { + this.configForm.get('verificationCode').setErrors({tooManyRequest: true}); + } else { + this.store.dispatch(new ActionNotificationShow({ + message: error.error.message, + type: 'error', + verticalPosition: 'top', + horizontalPosition: 'left' + })); + } + } + }) + } + } + + private updateQRCode() { + import('qrcode').then((QRCode) => { + unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); + this.canvasRef.nativeElement.style.width = 'auto'; + this.canvasRef.nativeElement.style.height = 'auto'; + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + cancelLogin() { + this.authService.logout(); + } + + downloadFile() { + this.importExportService.exportText(this.backupCode.codes, 'backup-codes'); + } + + printCode() { + const codeTemplate = deepClone(this.backupCode.codes) + .map(code => `
${code}
`).join(''); + const printPage = printTemplate.replace('${codesBlock}', codeTemplate); + const newWindow = window.open('', 'Print backup code'); + + newWindow.document.open(); + newWindow.document.write(printPage); + + setTimeout(() => { + newWindow.print(); + + newWindow.document.close(); + + setTimeout(() => { + newWindow.close(); + }, 10); + }, 0); + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html index 36484d0fdb..15eb084094 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html @@ -23,16 +23,11 @@ {{ title | translate }}
- - -
{{ message | translate }}
- diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss index e8db9286b4..7ec064c7ad 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss @@ -20,6 +20,7 @@ flex: 1 1 0; .tb-expired-link-content { background-color: #eee; + --mat-card-elevated-container-elevation: none; .tb-expired-link-card { letter-spacing: 0.15px; line-height: 24px; diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts index 9b927b7c7a..1e4f2ec68d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts @@ -15,10 +15,7 @@ /// import { Component } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'tb-link-expired', @@ -26,23 +23,16 @@ import { ActivatedRoute, Router } from '@angular/router'; styleUrls: ['./link-expired.component.scss'], standalone: false }) -export class LinkExpiredComponent extends PageComponent { +export class LinkExpiredComponent { isPasswordLinkExpired: boolean; title: string; message: string; - constructor(protected store: Store, - private route: ActivatedRoute, - private router: Router) { - super(store); + constructor(private route: ActivatedRoute) { this.isPasswordLinkExpired = this.route.snapshot.data.passwordLinkExpired; this.title = this.isPasswordLinkExpired ? 'login.reset-password-link-expired' : 'login.activation-link-expired'; this.message = this.isPasswordLinkExpired ? 'login.reset-password-link-expired-message' : 'login.activation-link-expired-message'; } - - navigateToLoginPage() { - this.router.navigateByUrl('login'); - } } 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 index 8c8c32b2e7..14f968d11e 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -17,55 +17,73 @@ -->
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 index 0f584d610d..a671f14fb9 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -23,45 +23,49 @@ margin-top: 36px; margin-bottom: 76px; background-color: #eee; + --mat-card-elevated-container-elevation: none; .tb-login-form { @media #{$mat-gt-xs} { - width: 550px !important; + width: 480px !important; } .forgot-password { - padding: 0 0.5em 1em; - .tb-reset-password { - padding: 0 6px; - } + --mat-button-text-horizontal-padding: 0; + --mat-button-text-container-height: 20px; } .tb-action-button{ - padding: 20px 0 16px; + margin: 20px 0 16px; } } - .oauth-container{ - padding: 0; - - .container-divider { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - width: 100%; + .container-divider { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + margin-bottom: 16px; - .line { - flex: 1; - } + .mat-divider-horizontal{ + --mat-divider-color: var(--mat-form-field-outlined-outline-color); + } - .mat-divider-horizontal{ - position: relative; - } + .text { + padding-right: 8px; + padding-left: 8px; + font-size: 16px; + line-height: 24px; + } + } - .text { - padding-right: 10px; - padding-left: 10px; - } + .oauth-container{ + .title { + font-size: 20px; + line-height: 24px; + letter-spacing: .1px; + text-align: center; + margin-bottom: 4px; } .material-icons{ @@ -71,15 +75,33 @@ a.login-with-button { color: rgba(black, 0.87); - background-color: map-get($tb-dark-mat-indigo, 50); + background-color: #ffffff; + } + + .login-button-container { + a.login-with-button { + max-width: 170px; + min-width: 60px; + flex-grow: 1; + flex-basis: 130px; + + .tb-mat-20 { + margin-right: 12px; + } + } - &:hover { - border-bottom: 0; + &:has(> :nth-child(1):last-child) { + a.login-with-button { + max-width: 100%; + } } - .icon { - height: 20px; - width: 20px; + &:has(> :nth-child(2):last-child), + &:has(> :nth-child(4):last-child) { + a.login-with-button { + max-width: 100%; + flex-basis: 180px; + } } } } 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 index 5f83872261..a10e213be5 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -16,15 +16,14 @@ 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 { UntypedFormBuilder, Validators } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; import { Router } from '@angular/router'; import { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models'; import { validateEmail } from '@app/core/utils'; +import { PageComponent } from '@shared/components/page.component'; +import { finalize } from 'rxjs/operators'; @Component({ selector: 'tb-login', @@ -35,6 +34,7 @@ import { validateEmail } from '@app/core/utils'; export class LoginComponent extends PageComponent implements OnInit { passwordViolation = false; + isLoading = false; loginFormGroup = this.fb.group({ username: ['', [Validators.required, validateEmail]], @@ -42,11 +42,10 @@ export class LoginComponent extends PageComponent implements OnInit { }); oauth2Clients: Array = null; - constructor(protected store: Store, - private authService: AuthService, + constructor(private authService: AuthService, public fb: UntypedFormBuilder, private router: Router) { - super(store); + super(); } ngOnInit() { @@ -55,9 +54,11 @@ export class LoginComponent extends PageComponent implements OnInit { login(): void { if (this.loginFormGroup.valid) { - this.authService.login(this.loginFormGroup.value).subscribe( - () => {}, - (error: HttpErrorResponse) => { + this.isLoading = true; + this.authService.login(this.loginFormGroup.value).pipe( + finalize(() => {this.isLoading = false;}) + ).subscribe({ + error: (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}`); @@ -66,12 +67,9 @@ export class LoginComponent extends PageComponent implements OnInit { } } } - ); - } else { - Object.keys(this.loginFormGroup.controls).forEach(field => { - const control = this.loginFormGroup.get(field); - control.markAsTouched({onlySelf: true}); }); + } else { + this.loginFormGroup.markAllAsTouched(); } } diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/password.component.scss similarity index 88% rename from ui-ngx/src/app/modules/login/pages/login/create-password.component.scss rename to ui-ngx/src/app/modules/login/pages/login/password.component.scss index f9a5220daa..c4f9e393ec 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/password.component.scss @@ -18,9 +18,10 @@ :host { display: flex; flex: 1 1 0; - .tb-create-password-content { + .tb-password-content { background-color: #eee; - .tb-create-password-card { + --mat-card-elevated-container-elevation: none; + .tb-password-card { @media #{$mat-gt-xs} { width: 450px !important; } 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 index 0314975b36..e609f82bb5 100644 --- 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 @@ -15,41 +15,41 @@ limitations under the License. --> -
- - - - login.request-password-reset - + + + login.request-password-reset - - - - +
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { + + } +
-
-
- - - login.email - - email - - {{ 'user.invalid-email-format' | translate }} - - -
- - -
-
-
+ + login.email + + email + @if (requestPasswordRequest.get('email').invalid) { + + {{ 'user.invalid-email-format' | translate }} + + } + +
+ + +
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 index f59b4b3632..716b761d3d 100644 --- 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 @@ -27,10 +27,10 @@ import { validateEmail } from '@app/core/utils'; @Component({ selector: 'tb-reset-password-request', templateUrl: './reset-password-request.component.html', - styleUrls: ['./reset-password-request.component.scss'], + styleUrls: ['./password.component.scss'], standalone: false }) -export class ResetPasswordRequestComponent extends PageComponent implements OnInit { +export class ResetPasswordRequestComponent extends PageComponent { clicked: boolean = false; @@ -45,9 +45,6 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn super(store); } - ngOnInit() { - } - disableInputs() { this.requestPasswordRequest.disable(); this.clicked = true; 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 index 39c35ed46c..30f6a8afdc 100644 --- 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 @@ -15,48 +15,70 @@ limitations under the License. --> -
- - - - login.password-reset - - -
login.expired-password-reset-message
-
+
+ + + login.password-reset + @if (isExpiredPassword) { + + login.expired-password-reset-message + + } - - - - +
+ @if (isLoading) { + + + } + @if (!isLoading) { + + } +
-
-
- - - login.new-password - - lock - - - - login.new-password-again - - lock - - -
- - -
-
-
+ + login.new-password + + lock + + @if (resetPassword.get('newPassword').errors) { + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + } + + + login.new-password-again + + lock + + @if (resetPassword.get('newPassword2').hasError('passwordsNotMatch')) { + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + } + +
+ + +
+ + 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 index ce90ec829f..fb6a679ab0 100644 --- 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 @@ -14,68 +14,61 @@ /// limitations under the License. /// -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component } 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 { UntypedFormBuilder } from '@angular/forms'; -import { ActionNotificationShow } from '@core/notification/notification.actions'; -import { TranslateService } from '@ngx-translate/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { UserPasswordPolicy } from '@shared/models/settings.models'; +import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; +import { finalize } from 'rxjs/operators'; @Component({ selector: 'tb-reset-password', templateUrl: './reset-password.component.html', - styleUrls: ['./reset-password.component.scss'], + styleUrls: ['./password.component.scss'], standalone: false }) -export class ResetPasswordComponent extends PageComponent implements OnInit, OnDestroy { +export class ResetPasswordComponent { isExpiredPassword: boolean; + isLoading = false; - resetToken = ''; - sub: Subscription; + resetPassword: FormGroup; + passwordPolicy: UserPasswordPolicy; - resetPassword = this.fb.group({ - newPassword: [''], - newPassword2: [''] - }); + private resetToken: string; - constructor(protected store: Store, - private route: ActivatedRoute, + constructor(private route: ActivatedRoute, private router: Router, private authService: AuthService, - private translate: TranslateService, - public fb: UntypedFormBuilder) { - super(store); - } + private fb: FormBuilder) { + + this.resetToken = this.route.snapshot.queryParams['resetToken'] || ''; + this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; + this.isExpiredPassword = this.route.snapshot.data['expiredPassword'] ?? false; - ngOnInit() { - this.isExpiredPassword = this.route.snapshot.data.expiredPassword; - this.sub = this.route - .queryParams - .subscribe(params => { - this.resetToken = params.resetToken || ''; - }); + this.buildResetPasswordForm(); } - ngOnDestroy(): void { - super.ngOnDestroy(); - this.sub.unsubscribe(); + private buildResetPasswordForm() { + this.resetPassword = this.fb.group({ + newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]], + newPassword2: [''] + }, { + validators: [ + passwordsMatchValidator('newPassword', 'newPassword2'), + ] + }); } 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' })); + if (this.resetPassword.invalid) { + this.resetPassword.markAllAsTouched(); } else { - this.authService.resetPassword( - this.resetToken, - this.resetPassword.get('newPassword').value).subscribe( - () => this.router.navigateByUrl('login') - ); + this.isLoading = true; + this.authService.resetPassword(this.resetToken, this.resetPassword.get('newPassword').value).pipe( + finalize(() => {this.isLoading = false}) + ).subscribe(() => this.router.navigateByUrl('login')); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html index 0e73783a5a..eb894f322e 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html @@ -27,70 +27,85 @@
-
-

login.select-way-to-verify

- - - -
-
-
-
-

{{ providerDescription }}

-
- - - - {{ 'security.2fa.dialog.verification-code-invalid' | translate }} - - - {{ 'security.2fa.dialog.verification-code-incorrect' | translate }} - - - {{ 'security.2fa.dialog.verification-code-many-request' | translate }} - - -
- - - -
-
-
- {{ 'login.resend-code-wait' | translate : {time: countDownTime} }} -
- + } +
+ } + @if (selectedProvider) { + +
+
+

{{ providerDescription }}

+
+ + + @if (verificationForm.get('verificationCode').getError('required') || + verificationForm.get('verificationCode').getError('minlength') || + verificationForm.get('verificationCode').getError('maxlength') || + verificationForm.get('verificationCode').getError('pattern')) { + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + } + @if (verificationForm.get('verificationCode').getError('incorrectCode')) { + + {{ 'security.2fa.dialog.verification-code-incorrect' | translate }} + + } + @if (verificationForm.get('verificationCode').getError('tooManyRequest')) { + + {{ 'security.2fa.dialog.verification-code-many-request' | translate }} + + } +
- + +
+ @if (showResendAction) { +
+ @if (countDownTime) { +
+ {{ 'login.resend-code-wait' | translate : {time: countDownTime} }} +
+ } + +
+ } + @if (allowProviders.length > 1) { + + } +
-
-
-
+
+ + }
diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss index 78d99773a4..1f5faee280 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss @@ -21,6 +21,7 @@ .tb-two-factor-auth-login-content { background-color: #eee; + --mat-card-elevated-container-elevation: none; .tb-two-factor-auth-login-card { padding: 48px 48px 48px 16px; @@ -72,9 +73,19 @@ } ::ng-deep { + .tb-two-factor-auth-login-content { + .tb-two-factor-auth-login-card { + button.mat-mdc-icon-button { + .mat-icon { + color: rgba(255, 255, 255, 0.8); + } + } + } + } button.provider { text-align: start; font-weight: 400; + color: rgba(255, 255, 255, 0.8); &:not([disabled][disabled]) { border-color: rgba(255, 255, 255, .8); } diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.html b/ui-ngx/src/app/shared/components/button/copy-button.component.html index d664b4d097..c72cfdcda3 100644 --- a/ui-ngx/src/app/shared/components/button/copy-button.component.html +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.html @@ -23,10 +23,11 @@ [matTooltip]="matTooltipText" [matTooltipPosition]="matTooltipPosition" (click)="copy($event)"> - - {{ icon || mdiIcon }} - - + @if (!copied) { + + {{ icon || mdiIcon }} + + } @else { done - + } diff --git a/ui-ngx/src/app/shared/components/button/public-api.ts b/ui-ngx/src/app/shared/components/button/public-api.ts new file mode 100644 index 0000000000..e4a852a9b8 --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/public-api.ts @@ -0,0 +1,20 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT 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 './copy-button.component'; +export * from './toggle-password.component'; +export * from './widget-button-toggle.component'; +export * from './widget-button.component'; diff --git a/ui-ngx/src/app/shared/components/button/toggle-password.component.html b/ui-ngx/src/app/shared/components/button/toggle-password.component.html index 07e91101ab..e284af02c4 100644 --- a/ui-ngx/src/app/shared/components/button/toggle-password.component.html +++ b/ui-ngx/src/app/shared/components/button/toggle-password.component.html @@ -15,6 +15,8 @@ limitations under the License. --> - +@if (!hideToggle) { + +} diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html index fb0c8bfab9..3684b29e5b 100644 --- a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html @@ -27,16 +27,28 @@ [class.tb-disabled-state]="disabled" [class.tb-disabled]="disabled"> -
- {{ appearance.leftAppearance.icon }} - {{ leftLabel$ | async }} -
+ @if (appearance.leftAppearance.showIcon || appearance.leftAppearance.showLabel) { +
+ @if (appearance.leftAppearance.showIcon) { + {{ appearance.leftAppearance.icon }} + } + @if (appearance.leftAppearance.showLabel) { + {{ leftLabel$ | async }} + } +
+ }
-
- {{ appearance.rightAppearance.icon }} - {{ rightLabel$ | async }} -
+ @if (appearance.rightAppearance.showIcon || appearance.rightAppearance.showLabel) { +
+ @if (appearance.rightAppearance.showIcon) { + {{ appearance.rightAppearance.icon }} + } + @if (appearance.rightAppearance.showLabel) { + {{ rightLabel$ | async }} + } +
+ }
diff --git a/ui-ngx/src/app/shared/components/button/widget-button.component.html b/ui-ngx/src/app/shared/components/button/widget-button.component.html index 2ff5d5e07f..16cf5fd557 100644 --- a/ui-ngx/src/app/shared/components/button/widget-button.component.html +++ b/ui-ngx/src/app/shared/components/button/widget-button.component.html @@ -33,8 +33,14 @@ [class.tb-disabled-state]="disabled" [style.border-radius]="computedBorderRadius" [style.pointer-events]="disableEvents ? 'none' : ''"> -
- {{ appearance.icon }} - {{ label$ | async }} -
+ @if (appearance.showIcon || appearance.showLabel) { +
+ @if (appearance.showIcon) { + {{ appearance.icon }} + } + @if (appearance.showLabel) { + {{ label$ | async }} + } +
+ } diff --git a/ui-ngx/src/app/shared/components/cheatsheet.component.ts b/ui-ngx/src/app/shared/components/cheatsheet.component.ts index 00fe6e5790..41fa0ce39f 100644 --- a/ui-ngx/src/app/shared/components/cheatsheet.component.ts +++ b/ui-ngx/src/app/shared/components/cheatsheet.component.ts @@ -115,17 +115,23 @@ import Mousetrap from 'mousetrap'; } } `], template: ``, +

{{ title }}

+ @if (helpVisible) { + + @for (hotkey of hotkeysList; track hotkey) { + + + + + } +
+ @for (key of hotkey.formatted; track key) { + {{ key }} + } + {{ hotkey.description }}
+ } +
×
+ `, standalone: false }) export class TbCheatSheetComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index 92c360e8c7..95c16c2662 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -15,26 +15,37 @@ limitations under the License. --> - - - {{icon}} - {{label}} - -
-
-
- - - - {{ requiredText }} - -
- +@if (!asBoxInput) { + + @if (icon || label) { + + @if (icon) { + {{icon}} + } + @if (label) { + {{label}} + } + + } +
+
+
+ + @if (colorClearButton && !disabled) { + + } + @if (colorFormGroup.get('color').hasError('required')) { + + {{ requiredText }} + + } +
+} @else { -
+} diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index dd5dd847a3..77e5668dfc 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -42,6 +42,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { TbPopoverService } from '@shared/components/popover.service'; import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-color-input', @@ -83,6 +84,9 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro @coerceBoolean() noBorder = false; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; diff --git a/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss new file mode 100644 index 0000000000..bae2e433aa --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + .color-input-container { + display: flex; + gap: 4px; + align-items: center; + } + .color-input { + max-width: 72px; + margin-bottom: 4px; + } +} + + diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html index 0c58364c17..088c48bd15 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html @@ -18,22 +18,26 @@
- + @if (colorClearButton) { + + }
- + @if (colorCancelButton) { + + }
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index ed161a5b9f..799bcdf00c 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -83,7 +83,7 @@ height: 56px; display: flex; align-items: center; - gap: 20px; + gap: 8px; .presentation-select { font-size: 14px; @@ -104,7 +104,7 @@ display: flex; flex-direction: row; flex-wrap: wrap; - justify-content: space-between; + justify-content: center; gap: 8px; @media #{$mat-xs} { flex-direction: column; diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html index fd64bff16a..c69d680b59 100644 --- a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html @@ -30,7 +30,7 @@ - + %
diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss index 2f7a404c0d..73a6aff9f6 100644 --- a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss @@ -19,11 +19,11 @@ gap: 8px; } .hex-input { - max-width: 190px; + max-width: 220px; } .alpha-input { - min-width: 60px; - max-width: 60px; + min-width: 72px; + max-width: 72px; } ::ng-deep { diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html new file mode 100644 index 0000000000..cf8e25eb51 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html @@ -0,0 +1,54 @@ + +
+
+ + + + @if (labelVisible) { + H + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + S + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + L + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + A + } +
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts new file mode 100644 index 0000000000..e1cc27e8fa --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { Color } from '@iplab/ngx-color-picker'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +type Channel = 'H' | 'S' | 'L'; + +@Component({ + selector: 'tb-hsla-input', + templateUrl: './hsla-input.component.html', + styleUrl: './color-input.base.scss', + standalone: false +}) +export class HslaInputComponent { + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + @Input() + @coerceBoolean() + public labelVisible = false; + + @Input() + public suffixValue = '%'; + + public get value(): ReturnType { + return this.color.getHsla(); + } + + public get alphaValue(): number { + return this.color ? Math.round(this.color.getHsla().getAlpha() * 100) : 0; + } + + public onAlphaInputChange(inputValue: number): void { + if (!this.color) return; + const hsla = this.color.getHsla(); + const alpha = +inputValue / 100; + if (hsla.alpha !== alpha) { + const newColor = new Color().setHsla(hsla.getHue(), hsla.getSaturation(), hsla.getLightness(), alpha); + this.colorChange.emit(newColor); + } + } + + public onInputChange(newValue: number, channel: Channel): void { + if (!this.color) return; + const hsla = this.value; + const hue = channel === 'H' ? +newValue : hsla.getHue(); + const saturation = channel === 'S' ? +newValue : hsla.getSaturation(); + const lightness = channel === 'L' ? +newValue : hsla.getLightness(); + if (hue === hsla.getHue() && saturation === hsla.getSaturation() && lightness === hsla.getLightness()) return; + const newColor = new Color().setHsla(hue, saturation, lightness, hsla.getAlpha()); + this.colorChange.emit(newColor); + } +} diff --git a/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts new file mode 100644 index 0000000000..55604df28e --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, EventEmitter, HostBinding, HostListener, Input, numberAttribute, Output } from '@angular/core'; + +@Directive({ + selector: '[inputChange]', + standalone: false +}) +export class InputChangeDirective { + + @Input({transform: numberAttribute}) + @HostBinding('attr.min') + min = 0; + + @Input({transform: numberAttribute}) + @HostBinding('attr.max') + max = 255; + + @Output() + public inputChange = new EventEmitter(); + + @HostListener('input', ['$event']) + public inputChanges(event: any): void { + const element = event.target as HTMLInputElement || event.srcElement as HTMLInputElement; + const value = element.value; + + const numeric = parseFloat(value); + if (!isNaN(numeric) && numeric >= this.min && numeric <= this.max) { + this.inputChange.emit(numeric); + } + } +} diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html new file mode 100644 index 0000000000..3cb856f147 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html @@ -0,0 +1,52 @@ + +
+
+ + + + @if (labelVisible) { + R + } +
+
+ + + + @if (labelVisible) { + G + } +
+
+ + + + @if (labelVisible) { + B + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + A + } +
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts new file mode 100644 index 0000000000..0aea5af264 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { Color } from '@iplab/ngx-color-picker'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +type Channel = 'R' | 'G' | 'B'; + +@Component({ + selector: 'tb-rgba-input', + templateUrl: './rgba-input.component.html', + styleUrl: './color-input.base.scss', + standalone: false +}) +export class RgbaInputComponent { + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + @Input() + @coerceBoolean() + public labelVisible = false; + + @Input() + public suffixValue = '%'; + + public get value(): ReturnType { + return this.color.getRgba(); + } + + public get alphaValue(): string { + return this.color ? Math.round(this.color.getRgba().getAlpha() * 100).toString() : ''; + } + + public onAlphaInputChange(inputValue: number): void { + if (!this.color) return; + const color = this.color.getRgba(); + const alpha = +inputValue / 100; + if (color.getAlpha() !== alpha) { + const newColor = new Color().setRgba(color.getRed(), color.getGreen(), color.getBlue(), alpha).toRgbaString(); + this.colorChange.emit(new Color(newColor)); + } + } + + onInputChange(newValue: number, channel: Channel) { + if (!this.color) return; + const rgba = this.value; + const red = channel === 'R' ? newValue : rgba.getRed(); + const green = channel === 'G' ? newValue : rgba.getGreen(); + const blue = channel === 'B' ? newValue : rgba.getBlue(); + if (red === rgba.getRed() && green === rgba.getGreen() && blue === rgba.getBlue()) return; + this.colorChange.emit(new Color().setRgba(red, green, blue, rgba.alpha)); + } +} diff --git a/ui-ngx/src/app/shared/components/contact.component.html b/ui-ngx/src/app/shared/components/contact.component.html index 2ae047d443..283ba88061 100644 --- a/ui-ngx/src/app/shared/components/contact.component.html +++ b/ui-ngx/src/app/shared/components/contact.component.html @@ -17,54 +17,56 @@ -->
- + contact.city - - {{ 'contact.city-max-length' | translate }} - + @if (parentForm.get('city').hasError('maxlength')) { + {{ 'contact.city-max-length' | translate }} + } - + contact.state - - {{ 'contact.state-max-length' | translate }} - + @if (parentForm.get('state').hasError('maxlength')) { + {{ 'contact.state-max-length' | translate }} + } - + contact.postal-code - - {{ 'contact.postal-code-invalid' | translate }} - + @if (parentForm.get('zip').hasError('pattern')) { + {{ 'contact.postal-code-invalid' | translate }} + }
- + contact.address - + contact.address2 - + contact.email - - {{ 'user.invalid-email-format' | translate }} - + @if (parentForm.get('email').hasError('email')) { + {{ 'user.invalid-email-format' | translate }} + }
diff --git a/ui-ngx/src/app/shared/components/country-autocomplete.component.html b/ui-ngx/src/app/shared/components/country-autocomplete.component.html index befb160df6..1e3ffecac5 100644 --- a/ui-ngx/src/app/shared/components/country-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/country-autocomplete.component.html @@ -29,38 +29,51 @@ (focusin)="onFocus()" (blur)="checkInputAndAutoSelect()" [matAutocomplete]="countryAutocomplete"> - + @if (countryFormGroup.get('country').value && !disabled) { + + } - - {{country.flag}} - - - -
-
- contact.no-country-found -
- - + @for (country of filteredCountries | async; track country) { + + {{country.flag}} + + + } + @if (!(filteredCountries | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ contact.no-country-found +
+ } @else { + {{ 'contact.no-country-matching' | translate: - {country: (searchText | truncate: true: 6: '...')} }} - - -
-
+ {country: (searchText | truncate: true: 6: '...')} }} +
+ } +
+
+ }
- {{ autocompleteHint }} - - {{ requiredText }} - - - {{ objectRequiredText }} - + @if (autocompleteHint) { + {{ autocompleteHint }} + } + @if (countryFormGroup.get('country').hasError('required')) { + + {{ requiredText }} + + } + @if (countryFormGroup.get('country').hasError('objectRequired')) { + + {{ objectRequiredText }} + + } diff --git a/ui-ngx/src/app/shared/components/css.component.html b/ui-ngx/src/app/shared/components/css.component.html index 9729b97e06..b0050c974f 100644 --- a/ui-ngx/src/app/shared/components/css.component.html +++ b/ui-ngx/src/app/shared/components/css.component.html @@ -21,9 +21,11 @@
- + @if (!disabled) { + + }
- {{ label }} + class="mat-block" + [floatLabel]="floatLabel" + [appearance]="inlineField ? 'outline' : appearance" + [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> + @if (!inlineField && label) { + {{ label }} + } - - {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) | customTranslate }} - - - - warning - + @if (useDashboardLink && selectDashboardFormGroup.get('dashboard').value && disabled) { + + {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) | customTranslate }} + + } + @if (selectDashboardFormGroup.get('dashboard').value && !disabled) { + + } + @if (inlineField && requiredText && selectDashboardFormGroup.get('dashboard').hasError('required') + && selectDashboardFormGroup.get('dashboard').touched) { + + warning + + } - - - - -
-
- {{ 'dashboard.no-dashboards-text' | translate }} + @for (dashboard of filteredDashboards | async; track dashboard) { + + + + } + @if (!(filteredDashboards | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'dashboard.no-dashboards-text' | translate }} +
+ } @else { + + {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }} + + }
- - - {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }} - - -
- + + } - - - - - - + @if (!inlineField) { + + + + } + @if (!inlineField) { + + + + } 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 index b26352b817..2342210dba 100644 --- a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html @@ -20,9 +20,11 @@ {{'dashboard.select-dashboard' | translate}} - - {{dashboard.title}} - + @for (dashboard of dashboards$ | async; track dashboard) { + + {{dashboard.title}} + + }
diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.html b/ui-ngx/src/app/shared/components/dashboard-select.component.html index f3b278cd59..b617afe9d1 100644 --- a/ui-ngx/src/app/shared/components/dashboard-select.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.html @@ -20,9 +20,11 @@ [disabled]="disabled" [(ngModel)]="dashboardId" (ngModelChange)="dashboardIdChanged()"> - - {{dashboard.title}} - + @for (dashboard of dashboards$ | async; track dashboard) { + + {{dashboard.title}} + + }
+ @if (selectDashboardStateFormGroup.get('dashboardStateId').value) { + + } - - - + @for (state of filteredStatesDashboard$ | async; track state) { + + + + } - - {{ 'widget-action.target-dashboard-state-required' | translate }} - + @if (selectDashboardStateFormGroup.get('dashboardStateId').hasError('required')) { + + {{ 'widget-action.target-dashboard-state-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts index 49c8835986..2d5404244c 100644 --- a/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.ts @@ -24,7 +24,7 @@ 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 { FloatLabelType } from '@angular/material/form-field'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; @Component({ @@ -51,6 +51,9 @@ export class DashboardStateAutocompleteComponent implements ControlValueAccessor @Input() label: string = this.translate.instant('widget-action.target-dashboard-state'); + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Input() placeholder: string; 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 index 0778e2ecd6..d2e07083f6 100644 --- a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html @@ -16,12 +16,11 @@ -->

{{data.title}}

- +@if (data.textMode) {
-
- +} @else {
-
+}
diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts new file mode 100644 index 0000000000..50d847609f --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts @@ -0,0 +1,61 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Overlay, OverlayContainer, OverlayModule } from '@angular/cdk/overlay'; +import { inject, Injector, NgModule } from '@angular/core'; +import { DEFAULT_DIALOG_CONFIG, Dialog, DialogConfig, DialogModule } from '@angular/cdk/dialog'; +import { MatDialogModule } from '@angular/material/dialog'; +import { DynamicDialog, DynamicMatDialog } from './dynamic-dialog'; +import { DynamicOverlay } from './dynamic-overlay'; +import { DynamicOverlayContainer, PARENT_OVERLAY_CONTAINER } from './dynamic-overlay-container'; + +export const DYNAMIC_MAT_DIALOG_PROVIDERS = [ + { + provide: DynamicMatDialog, + useFactory: () => { + const parentInjector = inject(Injector); + const parentOverlayContainer = parentInjector.get(OverlayContainer); + + const customInjector = Injector.create({ + providers: [ + { provide: PARENT_OVERLAY_CONTAINER, useValue: parentOverlayContainer }, + DynamicOverlayContainer, + { provide: OverlayContainer, useExisting: DynamicOverlayContainer }, + DynamicOverlay, + { provide: Overlay, useExisting: DynamicOverlay }, + DynamicDialog, + { provide: Dialog, useExisting: DynamicDialog }, + DynamicMatDialog, + { provide: DEFAULT_DIALOG_CONFIG, useValue: new DialogConfig() } + ], + parent: parentInjector + }); + + return customInjector.get(DynamicMatDialog); + } + } +]; + +@NgModule({ + imports: [ + OverlayModule, + DialogModule, + MatDialogModule + ], + providers: DYNAMIC_MAT_DIALOG_PROVIDERS +}) +export class DynamicMatDialogModule { +} diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts new file mode 100644 index 0000000000..df707ad49d --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, TemplateRef } from '@angular/core'; +import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; +import { DynamicOverlay } from './dynamic-overlay'; +import { ComponentType } from '@angular/cdk/overlay'; +import { Dialog } from '@angular/cdk/dialog'; + +export interface DynamicMatDialogConfig extends MatDialogConfig { + containerElement?: HTMLElement; +} + +@Injectable() +export class DynamicMatDialog extends MatDialog { + + private _customOverlay = inject(DynamicOverlay); + + public override open(component: ComponentType | TemplateRef, config?: DynamicMatDialogConfig): MatDialogRef { + if (config?.containerElement) { + config.containerElement.style.transform = 'translateZ(0)'; + this._customOverlay.setContainerElement(config.containerElement); + } + try { + return super.open(component, config); + } finally { + if (config?.containerElement) { + this._customOverlay.setContainerElement(null); + } + } + } +} + +@Injectable() +export class DynamicDialog extends Dialog { +} diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts new file mode 100644 index 0000000000..99a84f5a6a --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { OverlayContainer } from "@angular/cdk/overlay"; +import { inject, Injectable, InjectionToken } from "@angular/core"; + +export const PARENT_OVERLAY_CONTAINER = new InjectionToken('PARENT_OVERLAY_CONTAINER'); + +@Injectable() +export class DynamicOverlayContainer extends OverlayContainer { + + private _globalContainer = inject(PARENT_OVERLAY_CONTAINER); + private _customElement: HTMLElement | null = null; + + public override getContainerElement(): HTMLElement { + return this._customElement || this._globalContainer.getContainerElement(); + } + + setContainerElement(element: HTMLElement | null): void { + this._customElement = element; + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts new file mode 100644 index 0000000000..4d5a6836c9 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts @@ -0,0 +1,29 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Overlay } from '@angular/cdk/overlay'; +import { inject, Injectable } from '@angular/core'; +import { DynamicOverlayContainer } from './dynamic-overlay-container'; + +@Injectable() +export class DynamicOverlay extends Overlay { + + private _dynamicOverlayContainer = inject(DynamicOverlayContainer); + + public setContainerElement(containerElement: HTMLElement): void { + this._dynamicOverlayContainer.setContainerElement(containerElement); + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts index fe173be672..b50670e693 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts @@ -18,7 +18,6 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { SharedModule } from '@shared/shared.module'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { CommonModule } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityInfoData, VersionedEntity } from '@shared/models/entity.models'; import { EntityId } from '@shared/models/id/entity-id'; @@ -34,9 +33,8 @@ interface EntityConflictDialogData { templateUrl: 'entity-conflict-dialog.component.html', styleUrls: ['./entity-conflict-dialog.component.scss'], imports: [ - CommonModule, - SharedModule, - ] + SharedModule +] }) export class EntityConflictDialogComponent { diff --git a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html new file mode 100644 index 0000000000..6c78f822ac --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html @@ -0,0 +1,40 @@ + +
+ +
+
{{ 'entity.limit-reached' | translate }}
+
+
+
+
+ + +
+
+ {{ 'entity.request-sysadmin-text' | translate }} + {{ 'entity.login-here' | translate }} + {{ 'entity.to-increase-limit' | translate }} +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss new file mode 100644 index 0000000000..c888b0ac2c --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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'; + +.entity-limit-exceeded-dialog { + max-width: 100%; + height: 100%; + + @media #{$mat-gt-xs} { + max-width: 500px; + max-height: 80vh; + } + + position: relative; + padding: 40px 40px 24px 40px; + display: flex; + flex-direction: column; + gap: 20px; + + .close-btn { + position: absolute; + top: 8px; + right: 8px; + color: rgba(0, 0, 0, 0.54); + } + + .entity-limit-exceeded-bg { + position: relative; + width: 100%; + height: 140px; + &:before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: $tb-primary-color; + mask-image: url(/assets/entity-limit-reached.svg); + -webkit-mask-image: url(/assets/entity-limit-reached.svg); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-size: contain; + mask-position: center; + -webkit-mask-position: center; + } + } + + .entity-limit-exceeded-title { + font-size: 24px; + font-style: normal; + font-weight: 500; + line-height: 32px; + color: rgba(0, 0, 0, 0.87); + text-align: center; + } + + .entity-limit-exceeded-container { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: center; + .entity-limit-exceeded-text { + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + color: rgba(0, 0, 0, 0.76); + text-align: center; + } + } + + .request-entity-limit-increase-sysadmin-prompt { + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + color: rgba(0, 0, 0, 0.54); + } + +} diff --git a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts new file mode 100644 index 0000000000..0ed168bef2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Component, Inject, ViewEncapsulation } 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 { TranslateService } from '@ngx-translate/core'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthService } from '@core/auth/auth.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { NotificationService } from '@core/http/notification.service'; + +export interface EntityLimitExceededDialogData { + entityType: EntityType; + limit: number; +} + +// @dynamic +@Component({ + selector: 'tb-entity-limit-exceeded-dialog', + templateUrl: './entity-limit-exceeded-dialog.component.html', + styleUrls: ['./entity-limit-exceeded-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class EntityLimitExceededDialogComponent extends DialogComponent { + + limitReachedText: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EntityLimitExceededDialogData, + public dialogRef: MatDialogRef, + private authService: AuthService, + private dialogs: DialogService, + private translate: TranslateService, + private notificationService: NotificationService) { + super(store, router, dialogRef); + + let entitiesText: string; + if (data.limit > 1) { + entitiesText = data.limit + ' ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).typePlural) as string).toLowerCase(); + } else { + entitiesText = '1 ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase(); + } + this.limitReachedText = this.translate.instant('entity.limit-reached-text', { entities: entitiesText, entity: (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase() }); + } + + cancel(): void { + this.dialogRef.close(); + } + + requestLimitIncrease($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.notificationService.sendEntitiesLimitIncreaseRequest(this.data.entityType).subscribe( + () => { + this.dialogRef.close(); + this.dialogs.alert( + this.translate.instant('entity.increase-limit-request-sent-title'), + this.translate.instant('entity.increase-limit-request-sent-text'), + this.translate.instant('action.close') + ); + } + ); + } + + loginAsSysAdmin($event: Event) { + if ($event) { + $event.preventDefault(); + $event.stopPropagation(); + } + this.authService.redirectUrl = `/tenants/${getCurrentAuthUser(this.store).tenantId}`; + this.authService.logout(); + } + +} diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html index 379a7266db..73c19ea831 100644 --- a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html @@ -20,10 +20,12 @@
{{ message }}
dialog.error-message-title
{{ errorMessage }}
- - {{ 'dialog.error-details-title' | translate }} - - + @if (errorDetails) { + + {{ 'dialog.error-details-title' | translate }} + + + }
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 index 16ef833ec3..0878443cfa 100644 --- 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 @@ -34,9 +34,10 @@
-
+
@@ -60,7 +61,8 @@
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 index 5ef8d69e7a..6624bd5e5d 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -18,12 +18,13 @@ - {{ label | translate }} + @if (!inlineField) { + {{ label | translate }} + } - - {{ displayEntityFn(selectEntityFormGroup.get('entity').value) }} - - - - - warning - + @if (showEntityLink) { + + {{ displayEntityFn(selectEntityFormGroup.get('entity').value) }} + + } + @if (selectEntityFormGroup.get('entity').value && !disabled) { + + } + @if (allowCreateNew && !selectEntityFormGroup.get('entity').value && !disabled) { + + } + @if (inlineField && selectEntityFormGroup.get('entity').hasError('required') + && selectEntityFormGroup.get('entity').touched) { + + warning + + } - - - - -
-
- {{ notFoundEntities | translate }} -
- - + @for (entity of filteredEntities | async; track entity) { + + + + } + @if (!(filteredEntities | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ notFoundEntities | translate }} +
+ } @else { + {{ noEntitiesMatchingText | translate: {entity: searchText} }} - - -
-
+
+ @if (allowCreateNew) { + + entity.create-new-key + + } + } +
+
+ }
- - {{ requiredErrorText | translate }} - + @if (!inlineField && selectEntityFormGroup.get('entity').hasError('required')) { + + {{ requiredErrorText | translate }} + + }
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 index e045768712..7e7e381d0f 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -22,7 +22,7 @@ import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/opera import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; -import { BaseData } from '@shared/models/base-data'; +import { BaseData, getEntityDisplayName } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -139,11 +139,15 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @coerceArray() additionalClasses: Array; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + @Output() entityChanged = new EventEmitter>(); @Output() - createNew = new EventEmitter(); + createNew = new EventEmitter(); @ViewChild('entityInput', {static: true}) entityInput: ElementRef; @@ -298,6 +302,18 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.entityRequiredText = 'ai-models.model-required'; this.notFoundEntities = 'ai-models.no-model-text'; break; + case EntityType.DEVICE_PROFILE: + this.entityText = 'device-profile.device-profile'; + this.noEntitiesMatchingText = 'device-profile.no-device-profiles-matching'; + this.entityRequiredText = 'device-profile.device-profile-required'; + this.notFoundEntities = 'device-profile.no-device-profiles-text'; + break; + case EntityType.ASSET_PROFILE: + this.entityText = 'asset-profile.asset-profile'; + this.noEntitiesMatchingText = 'asset-profile.no-asset-profiles-matching'; + this.entityRequiredText = 'asset-profile.asset-profile-required'; + this.notFoundEntities = 'asset-profile.no-asset-profiles-text'; + break; case AliasEntityType.CURRENT_CUSTOMER: this.entityText = 'customer.default-customer'; this.noEntitiesMatchingText = 'customer.no-customers-matching'; @@ -396,7 +412,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } displayEntityFn(entity?: BaseData): string | undefined { - return entity ? entity.name : undefined; + return entity ? (this.useEntityDisplayName ? getEntityDisplayName(entity) : entity.name) : undefined; } private fetchEntities(searchText?: string): Observable>> { @@ -452,12 +468,16 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit return entityType; } - createNewEntity($event: Event) { + createNewEntity($event: Event, searchText?: string) { $event.stopPropagation(); - this.createNew.emit(); + this.createNew.emit(searchText); } get showEntityLink(): boolean { return this.selectEntityFormGroup.get('entity').value && this.disabled && this.entityURL !== ''; } + + markAsTouched(): void { + this.selectEntityFormGroup.get('entity').markAsTouched(); + } } 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 index f143fd227c..1b4e58c21e 100644 --- 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 @@ -23,34 +23,43 @@ (keydown)="gatewayNameEnter($event)" (keypress)="gatewayNameEnter($event)" [matAutocomplete]="gatewayAutocomplete" - > - + > + @if (selectDeviceGatewayFormGroup.get('gateway').value && !isStateForm) { + + } - - - - -
-
- gateway.no-gateway-found -
- - - gateway.no-gateway-matching + @for (gateway of filteredGateways | async; track gateway) { + + + + } + @if (!(filteredGateways | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ gateway.no-gateway-found +
+ } @else { + + gateway.no-gateway-matching + + } + + gateway.create-new-gateway - - - gateway.create-new-gateway - -
-
+
+
+ }
- - {{ 'gateway.gateway-name-required' | translate }} - + @if (selectDeviceGatewayFormGroup.get('gateway').hasError('required')) { + + {{ 'gateway.gateway-name-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html index a2ee72b97b..c6f55849f9 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html @@ -16,23 +16,23 @@ --> - - @if (keyControl.value) { + @if (keyControl.value && keyControl.enabled) { - } @else if (keyControl.hasError('required') && keyControl.touched) { + } @else if (keyControl.hasError('required') && keyControl.touched && keyControl.enabled) { warning @@ -43,7 +43,7 @@ @for (key of filteredKeys$ | async; track key) { } @empty { - @if (!keyControl.value) { + @if (!keyControl.value && enableAutocomplete) { {{ 'entity.no-keys-found' | translate }} } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts index 92451e56e9..ab8f5f241c 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { Component, effect, ElementRef, forwardRef, input, OnChanges, SimpleChanges, ViewChild, } from '@angular/core'; +import { + Component, + effect, + ElementRef, + forwardRef, + Input, + input, + OnChanges, + SimpleChanges, + ViewChild, +} from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -32,6 +42,7 @@ import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry. import { EntitiesKeysByQuery } from '@shared/models/entity.models'; import { EntityFilter } from '@shared/models/query/query.models'; import { isEqual } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-entity-key-autocomplete', @@ -54,6 +65,10 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val @ViewChild('keyInput', {static: true}) keyInput: ElementRef; + @Input() placeholder = this.translate.instant('action.set'); + @Input() requiredText = this.translate.instant('common.hint.key-required'); + @Input() enableAutocomplete = true; + entityFilter = input.required(); dataKeyType = input.required(); keyScopeType = input(); @@ -68,12 +83,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val keys$ = this.keyInputSubject.asObservable() .pipe( switchMap(() => { + if (!this.enableAutocomplete) { + return of([] as string[]); + } return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({ pageLink: { page: 0, pageSize: 100 }, entityFilter: this.entityFilter(), - }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType()); + }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true}); }), map(result => { + if (Array.isArray(result)) { + return result; + } this.cachedResult = result; switch (this.dataKeyType()) { case DataKeyType.attribute: @@ -97,6 +118,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val constructor( private fb: FormBuilder, private entityService: EntityService, + private translate: TranslateService, ) { this.keyControl.valueChanges .pipe(takeUntilDestroyed()) @@ -118,7 +140,10 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val changes.dataKeyType.currentValue !== changes.dataKeyType.previousValue; if (filterChanged || keyScopeChanged || keyTypeChanged) { - this.keyControl.setValue('', {emitEvent: false}); + this.cachedResult = null; + if (!this.keyControl.disabled) { + this.keyControl.setValue('', {emitEvent: false}); + } } } @@ -137,10 +162,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val registerOnTouched(_): void {} validate(): ValidationErrors | null { - return this.keyControl.valid ? null : { keyControl: false }; + return this.keyControl.valid || this.keyControl.disabled ? null : { keyControl: false }; } writeValue(value: string): void { this.keyControl.patchValue(value, {emitEvent: false}); } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.keyControl.disable({emitEvent: false}); + } else { + this.keyControl.enable({emitEvent: false}); + } + } } 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 index f48bf48804..8ecebe7d0d 100644 --- 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 @@ -15,15 +15,21 @@ limitations under the License. --> - - - - {{key}} - close - + + @if (label) { + {{ label | translate }} +} + + @for (key of modelValue; track key) { + + {{key}} + @if (!disabled) { + close + } + + } - - - + @for (key of filteredKeys | async; track key) { + + + + } 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 index a034af3825..5aa5cf3eb5 100644 --- 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 @@ -29,6 +29,7 @@ import { MatChipInputEvent, MatChipGrid } from '@angular/material/chips'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { isEqual } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-entity-keys-list', @@ -62,9 +63,15 @@ export class EntityKeysListComponent implements ControlValueAccessor, OnInit, Af @Input() keysText: string; + @Input() + label: string; + @Input() dataKeyType: DataKeyType; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; 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 index ea9248f17a..034f118b4d 100644 --- 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 @@ -16,27 +16,32 @@ -->
- - - - + @if (displayEntityTypeSelect) { + + + } + @if (modelValue.entityType) { + + + }
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 index 25852cf5a2..1bf532af2e 100644 --- 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 @@ -21,6 +21,7 @@ import { EntityService } from '@core/http/entity.service'; import { EntityId } from '@shared/models/id/entity-id'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { isDefinedAndNotNull } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; interface EntityListSelectModel { entityType: EntityType | AliasEntityType; @@ -69,6 +70,12 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit { @Input() additionEntityTypes: {[key in string]: string} = {}; + @Input({transform: booleanAttribute}) + useEntityDisplayName = false; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + displayEntityTypeSelect: boolean; private defaultEntityType: EntityType | AliasEntityType = 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 index 9cc0969a5f..8781908d82 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -21,16 +21,21 @@ [class.tb-chips]="inlineField" [class.flex]="inlineField" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> - {{ labelText }} + @if (!inlineField && labelText) { + {{ labelText }} + } - - {{entity.name}} - close - + @for (entity of entities; track entity) { + + {{ displayEntityFn(entity) }} + @if (!disabled) { + close + } + + } - + @if (allowCreateNew && !disabled) { + + } - - - - -
-
- {{ 'entity.no-entities-text' | translate }} -
- - - {{ 'entity.no-entities-matching' | translate: {entity: searchText} }} - - @if (allowCreateNew) { + @for (entity of filteredEntities | async; track entity) { + + + + } + @if (!(filteredEntities | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'entity.no-entities-text' | translate }} +
+ } @else { - entity.create-new-key + {{ 'entity.no-entities-matching' | translate: {entity: searchText} }} + @if (allowCreateNew) { + + entity.create-new-key + + } } - -
-
+
+
+ }
- - {{ hint }} - - - {{ requiredText }} - + @if (!inlineField && hint) { + + {{ hint }} + + } + @if (!inlineField && entityListFormGroup.get('entities').hasError('required')) { + + {{ requiredText }} + + }
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 index 1baca6b2f3..af0b0a654f 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -39,7 +39,7 @@ import { Observable } from 'rxjs'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; -import { BaseData } from '@shared/models/base-data'; +import { BaseData, getEntityDisplayName } 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'; @@ -126,6 +126,10 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan @coerceBoolean() allowCreateNew: boolean; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + @Output() createNew = new EventEmitter(); @@ -280,7 +284,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan } public displayEntityFn(entity?: BaseData): string | undefined { - return entity ? entity.name : undefined; + return entity ? (this.useEntityDisplayName ? getEntityDisplayName(entity) : entity.name) : undefined; } private fetchEntities(searchText?: string): Observable>> { 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 index 7d57e575ef..e76db03f77 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -16,23 +16,30 @@ -->
- - - - + @if (displayEntityTypeSelect) { + + + } + @if (modelValue.entityType && !entityTypeNullUUID.has(modelValue.entityType)) { + + + }
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 index 3f5eeefca4..0b7271966e 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -26,6 +26,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatFormFieldAppearance } from '@angular/material/form-field'; +import { BaseData } from '@shared/models/base-data'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; @Component({ selector: 'tb-entity-select', @@ -38,7 +40,9 @@ import { MatFormFieldAppearance } from '@angular/material/form-field'; }], standalone: false }) -export class EntitySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { +export class EntitySelectComponent implements ControlValueAccessor, OnInit { + + @ViewChild('entityAutocompleteComponent') entityAutocompleteComponent!: EntityAutocompleteComponent; entitySelectFormGroup: UntypedFormGroup; @@ -63,16 +67,30 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte @Input() appearance: MatFormFieldAppearance = 'fill'; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + + @Input() + filterAllowedEntityTypes = true; + + @Input() + defaultEntityType: AliasEntityType | EntityType; + + @Input() + entityTypeLabel: string; + + @Output() + entityChanged = new EventEmitter>(); + displayEntityTypeSelect: boolean; AliasEntityType = AliasEntityType; - entityTypeNullUUID: Set = new Set([ + entityTypeNullUUID = new Set([ AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER ]); - private readonly defaultEntityType: EntityType | AliasEntityType = null; - private propagateChange = (v: any) => { }; constructor(private store: Store, @@ -83,15 +101,18 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes); + + let defaultEntityType: EntityType | AliasEntityType = null + if (entityTypes.length === 1) { this.displayEntityTypeSelect = false; - this.defaultEntityType = entityTypes[0]; + defaultEntityType = entityTypes[0]; } else { this.displayEntityTypeSelect = true; } this.entitySelectFormGroup = this.fb.group({ - entityType: [this.defaultEntityType], + entityType: [defaultEntityType], entityId: [null] }); } @@ -123,9 +144,19 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte if (additionNullUIIDEntityTypes.length > 0) { additionNullUIIDEntityTypes.forEach((entityType) => this.entityTypeNullUUID.add(entityType)); } - } - ngAfterViewInit(): void { + if (this.filterAllowedEntityTypes === false) { + if (this.allowedEntityTypes?.length === 1) { + this.displayEntityTypeSelect = false; + this.entitySelectFormGroup.get('entityType').setValue(this.allowedEntityTypes[0]) + } else { + this.displayEntityTypeSelect = true; + } + } + + if (this.defaultEntityType) { + this.entitySelectFormGroup.get('entityType').setValue(this.defaultEntityType); + } } setDisabledState(isDisabled: boolean): void { @@ -173,4 +204,12 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte } } } + + changeEntity(entity: BaseData): void { + this.entityChanged.emit(entity); + } + + entityAutocompleteMarkAsTouched() { + this.entityAutocompleteComponent.markAsTouched(); + } } 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 index 942ef0632b..117742515b 100644 --- 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 @@ -23,24 +23,32 @@ (focusin)="onFocus()" [required]="required" [matAutocomplete]="subTypeAutocomplete"> - + @if (subTypeFormGroup.get('subType').value && !disabled) { + + } - - - + @for (subType of filteredSubTypes | async; track subType) { + + + + } - - {{ entitySubtypeRequiredText | translate }} - - - {{ entitySubtypeMaxLength | translate }} - + @if (subTypeFormGroup.get('subType').hasError('required')) { + + {{ entitySubtypeRequiredText | translate }} + + } + @if (subTypeFormGroup.get('subType').hasError('maxlength')) { + + {{ entitySubtypeMaxLength | translate }} + + }
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 index b6320bacf7..261ecea281 100644 --- 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 @@ -18,16 +18,21 @@ - {{ label }} + @if (label) { + {{ label }} + } - - {{customTranslate(entitySubtype)}} - close - + @for (entitySubtype of entitySubtypeList; track entitySubtype) { + + {{customTranslate(entitySubtype)}} + @if (!disabled) { + close + } + + } - - - + @for (entitySubtype of filteredEntitySubtypeList | async; track entitySubtype) { + + + + }
- - {{ subtypeListEmptyText | translate }} - + @if (entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')) { + + {{ subtypeListEmptyText | translate }} + + }
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 index afe60778f8..fb855de0b4 100644 --- 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 @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Observable, ReplaySubject, Subscription, throwError } from 'rxjs'; import { debounceTime, map, mergeMap, share } from 'rxjs/operators'; @@ -31,6 +31,8 @@ import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { UtilsService } from '@core/services/utils.service'; import { EntityService } from '@core/http/entity.service'; +import { CalculatedFieldType } from "@shared/models/calculated-field.models"; +import { CalculatedFieldsService } from "@core/http/calculated-fields.service"; @Component({ selector: 'tb-entity-subtype-list', @@ -45,7 +47,7 @@ import { EntityService } from '@core/http/entity.service'; ], standalone: false }) -export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { +export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, OnDestroy { entitySubtypeListFormGroup: FormGroup; @@ -78,6 +80,9 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, @Input() entityType: EntityType; + @Input() + calculatedFieldType: CalculatedFieldType; + @Input() emptyInputPlaceholder: string; @@ -115,15 +120,17 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, private dirty = false; - private propagateChange = (v: any) => { }; + private propagateChange = (_v: any) => { }; private hasPageDataEntitySubTypes = new Set([ - EntityType.ALARM + EntityType.ALARM, + EntityType.CALCULATED_FIELD ]); constructor(private broadcast: BroadcastService, public translate: TranslateService, private alarmService: AlarmService, + private calculatedFieldsService: CalculatedFieldsService, private utils: UtilsService, private fb: FormBuilder, private entityService: EntityService) { @@ -195,6 +202,13 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, this.noSubtypesMathingText = 'alarm.no-alarm-types-matching'; this.subtypeListEmptyText = 'alarm.alarm-type-list-empty'; break; + case EntityType.CALCULATED_FIELD: + this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-rule-type') + : this.translate.instant('alarm-rule.any-type'); + this.secondaryPlaceholder = '+' + this.translate.instant('alarm-rule.alarm-type'); + this.noSubtypesMathingText = 'alarm-rule.no-alarm-rule-types-matching'; + this.subtypeListEmptyText = 'alarm-rule.alarm-rule-type-list-empty'; + break; } if (this.emptyInputPlaceholder) { @@ -212,9 +226,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, ); } - ngAfterViewInit(): void { - } - ngOnDestroy(): void { if (this.broadcastSubscription) { this.broadcastSubscription.unsubscribe(); @@ -286,7 +297,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, } selected(event: MatAutocompleteSelectedEvent): void { - this.add(event.option.viewValue); + this.add(event.option.value); this.clear(''); } @@ -316,15 +327,22 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, if (this.hasPageDataEntitySubTypes.has(this.entityType)) { const pageLink = new PageLink(25, 0, searchText); let subTypesPagesObservable: Observable>; + let subTypesCfPagesObservable: Observable>; switch (this.entityType) { case EntityType.ALARM: subTypesPagesObservable = this.alarmService.getAlarmTypes(pageLink, {ignoreLoading: true}); break; + case EntityType.CALCULATED_FIELD: + subTypesCfPagesObservable = this.calculatedFieldsService.getCalculatedFieldNames(pageLink, CalculatedFieldType.ALARM, {ignoreLoading: true}); } if (subTypesPagesObservable) { this.entitySubtypes = subTypesPagesObservable.pipe( map(subTypesPage => subTypesPage.data.map(subType => subType.type)), ); + } else if (subTypesCfPagesObservable) { + this.entitySubtypes = subTypesCfPagesObservable.pipe( + map(subTypesPage => subTypesPage.data) + ); } else { return throwError(null); } 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 index f9fa081c4d..1f62aa4f7b 100644 --- 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 @@ -16,13 +16,19 @@ --> - {{ entitySubtypeTitle | translate }} + @if (showLabel) { + {{ entitySubtypeTitle | translate }} + } - - {{ displaySubTypeFn(subType) }} - + @for (subType of subTypesOptions | async; track subType) { + + {{ displaySubTypeFn(subType) }} + + } - - {{ entitySubtypeRequiredText | translate }} - + @if (subTypeFormGroup.get('subType').hasError('required')) { + + {{ entitySubtypeRequiredText | translate }} + + } 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 index 15b4c6ae5e..f929e34199 100644 --- 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 @@ -20,15 +20,20 @@ class="mat-block" [class]="additionalClasses" [floatLabel]="floatLabel" subscriptSizing="{{ subscriptSizing }}"> - {{ label }} + @if (label) { + {{ label }} + } - - {{entityType.name}} - close - + @for (entityType of entityTypeList; track entityType) { + + {{entityType.name}} + @if (!disabled) { + close + } + + } - - - - - + @for (entityType of filteredEntityTypeList | async; track entityType) { + + + + } + @if (!(filteredEntityTypeList | async)?.length) { + + {{ translate.get('entity.no-entity-types-matching', {entityType: searchText}) | async }} - - + + + }
- - {{ 'entity.entity-type-list-required' | translate }} - + @if (entityTypeListFormGroup.get('entityTypeList').hasError('required')) { + + {{ 'entity.entity-type-list-required' | translate }} + + }
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 index b3f4fa5300..190eff28e1 100644 --- 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 @@ -18,16 +18,22 @@ - {{ label }} + @if (showLabel && !inlineField) { + {{ label }} + } - - {{ displayEntityTypeFn(type) }} - + @for (type of entityTypes; track type) { + + {{ displayEntityTypeFn(type) }} + + } @if (!inlineField && subscriptSizing === 'dynamic') { } - - {{ 'entity.type-required' | translate }} - + @if (!inlineField && entityTypeFormGroup.get('entityType').hasError('required')) { + + {{ 'entity.type-required' | translate }} + + } 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 index ea24c373d6..99d3a83c64 100644 --- 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 @@ -53,8 +53,16 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @coerceBoolean() showLabel: boolean; + private labelValue = this.translate.instant('entity.type'); + + get label(): string { + return this.labelValue; + } + @Input() - label = this.translate.instant('entity.type'); + set label(value: string) { + this.labelValue = value ?? this.translate.instant('entity.type'); + } @Input() @coerceBoolean() @@ -165,13 +173,17 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, } } - displayEntityTypeFn(entityType?: EntityType | AliasEntityType | null): string | undefined { - if (this.additionEntityTypes[entityType]) { - return this.additionEntityTypes[entityType]; + displayEntityTypeFn(entityType?: EntityType | AliasEntityType | string | null): string | undefined { + if (this.additionEntityTypes[entityType as EntityType]) { + return this.additionEntityTypes[entityType as EntityType]; } else if (entityType) { return this.translate.instant(entityTypeTranslations.get(entityType as EntityType).type); } else { return ''; } } + + markAsTouched(): void { + this.entityTypeFormGroup.get('entityType').markAsTouched(); + } } diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index 86e1d4c7e5..44b5442fac 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -16,11 +16,13 @@ -->
- + @if (label) { + + }
@@ -43,7 +45,7 @@ {{ dropLabel }}
- -
{{ noFileText }}
-
{{ fileName }}
-
dashboard.maximum-upload-file-size
+ @if (!fileName && required && requiredAsError) { + + } + @if (!fileName && !requiredAsError) { +
{{ noFileText }}
+ } + @if (fileName) { +
{{ fileName }}
+ } + @if (maxSizeByte && !disabled) { +
dashboard.maximum-upload-file-size
+ }
diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index 51b3fec787..3a309a97ac 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -181,17 +181,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, if (readers.length) { Promise.all(readers).then((files) => { - files = files.filter(file => file.fileContent != null || file.files != null); - if (files.length === 1) { - this.fileContent = files[0].fileContent; - this.fileName = files[0].fileName; - this.files = files[0].files; - this.mediaType = files[0].mediaType; + const validResults = files.filter(file => file.fileContent != null || file.files != null); + + if (validResults.length === 1) { + this.fileContent = validResults[0].fileContent; + this.fileName = validResults[0].fileName; + this.files = validResults[0].files; + this.mediaType = validResults[0].mediaType; this.updateModel(); - } else if (files.length > 1) { - this.fileContent = files.map(content => content.fileContent); - this.fileName = files.map(content => content.fileName); - this.files = files.map(content => content.files); + } else if (validResults.length > 1) { + this.fileContent = validResults.map(content => content.fileContent); + this.fileName = validResults.map(content => content.fileName); + this.files = validResults.map(content => content.files); this.updateModel(); } }); @@ -210,11 +211,10 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, fileContent: null, fileName: file.name, files: file.file, - mediaType: file.file?.type || null + mediaType: file.file.type || null }); return; } - const reader = new FileReader(); reader.onload = () => { let fileName = null; 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 index 1ea0f1d6da..d2d408e9ff 100644 --- a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html +++ b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html @@ -26,14 +26,16 @@ {{ footerFabButtons.fabTogglerIcon }}
- + @for (btn of buttons; track btn) { + + }
diff --git a/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html b/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html index d83594e0b7..ef57d199dc 100644 --- a/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html +++ b/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html @@ -18,28 +18,31 @@ -
-
- - - - - - + @if (itemsRow) { +
+ @for (item of itemsRow; track trackByItem(col, item); let col = $index) { +
+ @if (item === 'loadingCell') { + + } + @if (isObject(item)) { + + } +
+ }
-
+ } - +@if (dataSource.isEmpty) { - +} - + @if (dataSource.initialDataLoading) { - - - - + } @else { + + }
diff --git a/ui-ngx/src/app/shared/components/help-popup.component.html b/ui-ngx/src/app/shared/components/help-popup.component.html index bec404b063..65edb22cc3 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.html +++ b/ui-ngx/src/app/shared/components/help-popup.component.html @@ -15,44 +15,54 @@ limitations under the License. --> -
-
- -
-
-
-
- +
+
+} +@if (textMode) { +
+
+ -
-
+ + @if (triggerSafeHtml) { + + } + @if ((!popoverVisible || popoverReady)) { + open_in_new + } + @if (popoverVisible && !popoverReady) { + + } + + +
+
+} diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html index ae84c5633f..f2f0983975 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html @@ -16,7 +16,8 @@ --> -{{ hintIcon }} +@if (tooltipText) { + {{ hintIcon }} +} diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss index 906091e102..c11dc6447a 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss @@ -17,7 +17,6 @@ display: flex; flex-direction: row; align-items: center; - gap: 4px; } :host { diff --git a/ui-ngx/src/app/shared/components/html.component.html b/ui-ngx/src/app/shared/components/html.component.html index f17272bf71..6520340374 100644 --- a/ui-ngx/src/app/shared/components/html.component.html +++ b/ui-ngx/src/app/shared/components/html.component.html @@ -21,9 +21,11 @@
- + @if (!disabled) { + + }
; @@ -114,6 +120,8 @@ export class TbIconComponent implements AfterContentInit, AfterViewChecked, OnDe constructor(private contentObserver: ContentObserver, private renderer: Renderer2, private _iconRegistry: MatIconRegistry, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, private readonly _errorHandler: ErrorHandler) { } @@ -152,16 +160,29 @@ export class TbIconComponent implements AfterContentInit, AfterViewChecked, OnDe private _updateIcon() { const useSvgIcon = isSvgIcon(this.icon); + const useImageIcon = isTbImage(this.icon); if (this._useSvgIcon !== useSvgIcon) { this._useSvgIcon = useSvgIcon; if (!this._useSvgIcon) { this._updateSvgIcon(undefined); } else { this._updateFontIcon(undefined); + this._updateImageIcon(undefined); + } + } + if (this._useImageIcon !== useImageIcon) { + this._useImageIcon = useImageIcon; + if (!this._useImageIcon) { + this._updateImageIcon(undefined); + } else { + this._updateFontIcon(undefined); + this._updateSvgIcon(undefined); } } if (this._useSvgIcon) { this._updateSvgIcon(this.icon); + } else if (this._useImageIcon) { + this._updateImageIcon(this.icon); } else { this._updateFontIcon(this.icon); } @@ -282,4 +303,49 @@ export class TbIconComponent implements AfterContentInit, AfterViewChecked, OnDe } } + private _updateImageIcon(rawName: string | undefined) { + if (rawName) { + this._clearImageIcon(); + this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( + imageUrl => { + const urlStr = imageUrl as string; + const isSvg = urlStr?.startsWith('data:image/svg+xml') || urlStr?.endsWith('.svg'); + if (isSvg) { + const safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urlStr); + this._iconRegistry + .getSvgIconFromUrl(safeUrl) + .pipe(take(1)) + .subscribe({ + next: (svg) => { + this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement); + this._imageElement = svg; + }, + error: () => this._setImageElement(urlStr) + }); + } else { + this._setImageElement(urlStr); + } + } + ); + } else { + this._clearImageIcon(); + } + } + + private _setImageElement(urlStr: string) { + const imgElement = this.renderer.createElement('img'); + this.renderer.addClass(imgElement, 'mat-icon'); + this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); + this.renderer.setAttribute(imgElement, 'src', urlStr); + this.renderer.insertBefore(this._elementRef.nativeElement, imgElement, this._iconNameContent.nativeElement); + this._imageElement = imgElement; + } + + private _clearImageIcon() { + const elem: HTMLElement = this._elementRef.nativeElement; + if (this._imageElement !== null) { + this.renderer.removeChild(elem, this._imageElement); + this._imageElement = null; + } + } } diff --git a/ui-ngx/src/app/shared/components/image-input.component.html b/ui-ngx/src/app/shared/components/image-input.component.html index 99fae6649c..6171d0d8be 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.html +++ b/ui-ngx/src/app/shared/components/image-input.component.html @@ -16,55 +16,74 @@ -->
- + @if (label) { + + }
-
-
-
{{ 'dashboard.empty-image' | translate }}
- -
- -
-
-
- cloud_upload -
- image-input.drag-and-drop -
- image-input.or - - -
+ @if (showPreview && (safeImageUrl || disabled)) { +
+
+ @if (!safeImageUrl && disabled) { +
{{ 'dashboard.empty-image' | translate }}
+ } @else { + + }
-
-
- -
+ close + + } +
+ } + @if (!disabled) { +
+
+ cloud_upload +
+ image-input.drag-and-drop +
+ image-input.or + + +
+
+
+
+ } + @if (!showPreview && safeImageUrl && showClearButton && !disabled) { + + } +
-
-
{{ fileName }}
-
dashboard.maximum-upload-file-size
-
+ @if ((showFileName && fileName) || (maxSizeByte && !disabled)) { +
+ @if (showFileName && fileName) { +
{{ fileName }}
+ } + @if (maxSizeByte && !disabled) { +
dashboard.maximum-upload-file-size
+ } +
+ }
diff --git a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html index cf6a054af4..c5a1a749ef 100644 --- a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html @@ -24,36 +24,45 @@ close - - -
+@if (isLoading$ | async) { + + +} @else { +
+}
-
- - - -
- -
{{ 'image.embed-to-html' | translate }}
-
-
-
image.embed-to-html
-
-
- -
- -
-
-
+ @if (!readonly || image.public) { +
+ + + + @if (!readonly) { +
+ +
{{ 'image.embed-to-html' | translate }}
+
+
+ } + @if (readonly) { +
image.embed-to-html
+ } +
+
+ +
+ +
+
+
+ }
image.embed-to-angular-template
+ [data]="embedToAngularTemplateCode()">
diff --git a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html index 2ebe68d897..be6344ffae 100644 --- a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html +++ b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html @@ -16,79 +16,95 @@ -->
- + @if (label) { + + }
- + @if (linkType !== ImageLinkType.none && imageUrl) { + + } @else { +
{{ (disabled ? 'image.no-image' : 'image.no-image-selected') | translate }}
+ }
-
-
-
-
- -
- {{ imageResource.title }} + @if (linkType !== ImageLinkType.none && (linkType !== ImageLinkType.base64 || !disabled)) { +
+ @if (linkType === ImageLinkType.base64) { +
-
-
{{ imageResource.descriptor.width }}x{{ imageResource.descriptor.height }}
- -
{{ imageResource.descriptor.size | fileSize }}
+ } + @if (linkType === ImageLinkType.resource) { +
+ @if (!loadingImageResource) { +
+ {{ imageResource.title }} +
+
+
{{ imageResource.descriptor.width }}x{{ imageResource.descriptor.height }}
+ +
{{ imageResource.descriptor.size | fileSize }}
+
+ } @else { + + }
- -
-
- - +
+ } + @if (!disabled && linkType !== ImageLinkType.external) { + -
+ }
- -
-
- - -
+ } + @if (linkType === ImageLinkType.none && !disabled) { +
+ + +
+ }
- -
{{ (disabled ? 'image.no-image' : 'image.no-image-selected') | translate }}
-
- - - diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-dialog.component.html index 0eefce39cf..0fe1a4be59 100644 --- a/ui-ngx/src/app/shared/components/image/image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.html @@ -25,36 +25,45 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- + image.name - - {{ 'image.name-required' | translate }} - - - + @if (imageFormGroup.get('title').hasError('required')) { + + {{ 'image.name-required' | translate }} + + } + @if (!readonly && imageFormGroup.dirty) { + + } + @if (!readonly && !imageFormGroup.invalid && imageFormGroup.dirty) { + + }
@@ -82,14 +91,16 @@ code
- + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html index f442556b18..8e4bd5a294 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.html +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html @@ -24,7 +24,9 @@
{{ (isScada ? 'scada.symbols' : 'image.gallery') | translate }}
-
+ @if (isScada) { +
+ }
@@ -44,10 +46,12 @@
- {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + @if (!isSysAdmin) { + {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + }
@@ -74,10 +78,10 @@ mdi:file-import
- {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + @if (!isSysAdmin) { + {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + }
@@ -132,200 +138,208 @@
-
-
- - - - - - - - - - - - - - -
- {{ image.title }} -
-
-
- - {{ 'image.name' | translate }} - - {{ image.title }} - - - - {{ 'image.created-time' | translate }} - - {{ image.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} - - - - {{ 'image.resolution' | translate }} - - {{ image.descriptor.width }}x{{ image.descriptor.height }} - - - - {{ 'image.size' | translate }} - - {{ image.descriptor.size | fileSize }} - - - - {{ 'image.system' | translate }} - - {{isSystem(image) ? 'check_box' : 'check_box_outline_blank'}} - - - - - - -
- - - - - -
-
- - -
+ + + + + + + + + + + + + +
+ {{ image.title }} +
+
+
+ + {{ 'image.name' | translate }} + + {{ image.title }} + + + + {{ 'image.created-time' | translate }} + + {{ image.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + {{ 'image.resolution' | translate }} + + {{ image.descriptor.width }}x{{ image.descriptor.height }} + + + + {{ 'image.size' | translate }} + + {{ image.descriptor.size | fileSize }} + + + + {{ 'image.system' | translate }} + + {{isSystem(image) ? 'check_box' : 'check_box_outline_blank'}} + + + + + + +
+ - - - + } + - - -
-
-
- - - - - - - - -
- - - - {{ 'common.loading' | translate }} +
+
+ + + + + @if (!isScada) { + + } + + + +
+ + + + + + + + + + + + @if ((dataSource.isEmpty() | async) && !dataSource.dataLoading) { + + } + {{ 'common.loading' | translate }} +
+ +
- - -
-
- - -
+ } + @if (mode === 'grid') { +
+ + +
+ }
- - {{ 'common.loading' | translate }} - + + {{ 'common.loading' | translate }} +
@@ -337,49 +351,57 @@
-
- - - - -
+ @if (!selectionMode) { +
+ + + @if (!isScada) { + + } + @if (deleteEnabled(item)) { + + } +
+ }
- - + @if (!selectionMode) { + + } + @if (selectionMode) { + + }
{{ item.title }} @@ -389,7 +411,9 @@
{{ item.title }}
-
sys
+ @if (isSystem(item)) { +
sys
+ }
{{ item.descriptor.width }}x{{ item.descriptor.height }}
diff --git a/ui-ngx/src/app/shared/components/image/image-references.component.html b/ui-ngx/src/app/shared/components/image/image-references.component.html index 32ce614dc2..421dac3caf 100644 --- a/ui-ngx/src/app/shared/components/image/image-references.component.html +++ b/ui-ngx/src/app/shared/components/image/image-references.component.html @@ -15,40 +15,45 @@ limitations under the License. --> - +@if (simpleList) { - - - +} @else { + @if (contentReady) {
    - -
  • -
    {{ 'image.system-entities' | translate }}
    - -
  • - + @for (entry of referencedEntitiesEntries; track entry) { + @if (isSystem(entry[0])) { +
  • +
    {{ 'image.system-entities' | translate }}
    + +
  • + } @else {
  • {{ 'tenant.tenant' | translate }} {{ entry[1].tenantName }} {{ 'image.entities' | translate }}
  • -
    -
    + } + }
-
-
+ } @else { + + } +} - - - - + @for (referencedEntity of entriesList; track referencedEntity) { + + + + + }
{{ referencedEntity.typeName }} - {{ referencedEntity.entity.name }} - {{ referencedEntity.entity.name }} -
{{ referencedEntity.typeName }} + @if (showDetailsLink) { + {{ referencedEntity.entity.name }} + } + @if (!showDetailsLink) { + {{ referencedEntity.entity.name }} + } +
- - - diff --git a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html index 3108e6a67c..9844484987 100644 --- a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html +++ b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html @@ -16,95 +16,114 @@ -->
- + @if (label) { + + }
-
- {{ 'image-input.images' | translate }} [{{ $index }}] -
-
- drag_indicator -
-
- -
-
- + @for (imageUrl of imageUrls; track imageUrl) { +
+ {{ 'image-input.images' | translate }} [{{ $index }}] +
+ @if (!disabled) { +
+ drag_indicator +
+ } +
+ +
+ @if (!disabled) { +
+ +
+ }
-
-
{{ 'image-input.no-images' | translate }}
+ } + @if (!imageUrls.length) { +
{{ 'image-input.no-images' | translate }}
+ }
-
-
- -
-
-
- + } + @if (linkType === ImageLinkType.none) { +
+ -
-
+ }
-
- - -
-
+ }
- -
{{ 'image.no-image-selected' | translate }}
-
diff --git a/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html index abaa800acf..99068df58e 100644 --- a/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html +++ b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html @@ -16,50 +16,62 @@ -->
- + @if (label) { + + }
- + @if (linkType !== ScadaSymbolLinkType.none && scadaSymbolUrl) { + + } @else { +
{{ (disabled ? 'scada.no-symbol' : 'scada.no-symbol-selected') | translate }}
+ }
-
-
- -
- {{ imageResource.title | customTranslate }} + @if (linkType !== ScadaSymbolLinkType.none && !disabled) { +
+ @if (linkType === ScadaSymbolLinkType.resource) { +
+ @if (!loadingImageResource) { +
+ {{ imageResource.title | customTranslate }} +
+ } @else { + + }
- + } + @if (linkType === ScadaSymbolLinkType.content) { +
+
+ {{ scadaSymbolMetadata.title | customTranslate }} +
+
+ } + @if (linkType === ScadaSymbolLinkType.resource && !disabled) { + + }
-
-
- {{ scadaSymbolMetadata.title | customTranslate }} -
+ } + @if (linkType === ScadaSymbolLinkType.none && !disabled) { +
+
- -
-
- -
+ }
- -
{{ (disabled ? 'scada.no-symbol' : 'scada.no-symbol-selected') | translate }}
-
- - - diff --git a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html index 61da3149b9..3e7b85268a 100644 --- a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+ @if (isLoading$ | async) { + + + } @else { +
+ }
- - image.name - - - {{ 'image.name-required' | translate }} - - + @if (uploadImage && uploadImageFormGroup.get('file').value) { + + image.name + + @if (uploadImageFormGroup.get('title').hasError('required')) { + + {{ 'image.name-required' | translate }} + + } + + }
diff --git a/ui-ngx/src/app/shared/components/js-func-module-row.component.html b/ui-ngx/src/app/shared/components/js-func-module-row.component.html index 2b60f9071c..5e6463ddd5 100644 --- a/ui-ngx/src/app/shared/components/js-func-module-row.component.html +++ b/ui-ngx/src/app/shared/components/js-func-module-row.component.html @@ -18,14 +18,15 @@
- - warning - + @if (moduleRowFormGroup.get('alias').invalid && moduleRowFormGroup.get('alias').touched) { + + warning + + }
+ help-icon-tooltip="{{ 'js-func.show-module-info' | translate }}">
+ help-icon-tooltip="{{ 'js-func.show-module-source-code' | translate }}">
-
-
- - + @if (modulesFormArray().controls.length) { +
+ @for (moduleControl of modulesFormArray().controls; track trackByModule($index, moduleControl)) { +
+ + +
+ }
-
- + } @else { + {{ 'js-func.no-modules' | translate }} + } + @if (modulesFormGroup.hasError('moduleAliasNotUnique')) { + + }
- - {{ 'js-func.no-modules' | translate }} - diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index e5d663152d..6cd69d6301 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -19,29 +19,37 @@ tb-fullscreen [fullscreen]="fullscreen">
- + @if (!hideLabel) { + + } -
-
- -
-
- + @if (!disabled && withModules) { +
+
+ +
+
+ } + @if (!disabled) { + + } -
+ @if (helpId) { +
+ }
-
- -
+ @if (!hideBrackets) { +
+ +
+ }
diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 67bdbf732c..665c78d17a 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -18,28 +18,34 @@
-
- - - - -
-
- -
-
-
+ } + @if (!readonly && !disabled) { + + } +
+
+ +
+
+
+ }
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 index 3c18ae38d6..13d2c5fa8a 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.html +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.html @@ -24,14 +24,18 @@ 'tb-readonly': readonly, 'tb-error': !objectValid}">{{ label }} - - + @if (!readonly && !disabled) { + + } + @if (!readonly && !disabled) { + + } -
+ @for (keyValControl of keyValsFormArray().controls; track keyValControl) { +
+ + + + + + + +
+ } {{noDataText ? noDataText : 'key-val.no-data'}} + class="no-data-found flex items-center justify-center" translate>{{noDataText ? noDataText : 'key-val.no-data'}}
+ mat-button (click)="toggleEditMode()">{{ 'markdown.edit' | translate }} -
+ mat-button (click)="toggleEditMode()">{{ 'markdown.preview' | translate }} + @if (helpId) { +
+ }
-
- -
+ @if (readonly || disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.ts b/ui-ngx/src/app/shared/components/markdown-editor.component.ts index 2c44104caf..72d9c9064b 100644 --- a/ui-ngx/src/app/shared/components/markdown-editor.component.ts +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.ts @@ -55,6 +55,8 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On @Input() helpId: string; + @Input() helpPopupStyle: { [klass: string]: any } = {}; + @Input() @coerceBoolean() required: boolean; diff --git a/ui-ngx/src/app/shared/components/markdown-sanitize.helper.ts b/ui-ngx/src/app/shared/components/markdown-sanitize.helper.ts new file mode 100644 index 0000000000..942cc257f3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown-sanitize.helper.ts @@ -0,0 +1,258 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +/** Block keywords: @keyword (...) { body } */ +const BLOCK_KEYWORDS = [ + 'if', 'else if', 'else', 'for', 'empty', 'switch', 'case', + 'default', 'defer', 'placeholder', 'loading', 'error', +]; + +/** Inline keywords: @keyword identifier = expr; */ +const INLINE_KEYWORDS = ['let']; + +/** Chain keywords that can follow a closing '}' */ +const CHAIN_KEYWORDS = [ + 'else if', 'else', 'empty', 'error', 'placeholder', 'loading', +]; + +/** Validation: inline keyword → regex that must match after keyword */ +const INLINE_VALIDATORS: Record = { + let: /^\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*=/, +}; + +/** HTML entities decoded in preserved blocks (order matters: & must be last) */ +const HTML_ENTITIES: [RegExp, string][] = [ + [/'/g, "'"], + [/"/g, '"'], + [/</g, '<'], + [/>/g, '>'], + [/&/g, '&'], +]; + +/** Matches any known @keyword at position 0 */ +const KEYWORD_RE = new RegExp( + `@(${[...BLOCK_KEYWORDS, ...INLINE_KEYWORDS] + .sort((a, b) => b.length - a.length).join('|')})(?=[\\s(;{])`, +); + +/** Matches a chain keyword after '}' */ +const CHAIN_RE = new RegExp( + `^\\s*@(${CHAIN_KEYWORDS + .sort((a, b) => b.length - a.length).join('|')})(?=[\\s(;{])`, +); + +// ─── Public API ────────────────────────────────────────────────────── + +/** + * Sanitize a markdown-compiled HTML template for safe Angular compilation. + * + * Escapes `{`, `}`, `@` that are not part of Angular control flow, + * preserves recognized control flow blocks (@if, @for, @switch, @let, @defer …), + * decodes HTML entities inside them, and protects `
` code blocks.
+ */
+export function sanitizeTemplate(template: string): string {
+  // Phase 1 — protect zones that must not be processed
+  const protectedZones: string[] = [];
+  template = extractProtectedZones(template, protectedZones);
+
+  // Phase 2 — extract Angular control flow blocks
+  const blocks: string[] = [];
+  template = extractControlFlowBlocks(template, blocks);
+
+  // Phase 3 — decode HTML entities inside control flow blocks
+  decodeHtmlEntities(blocks);
+
+  // Phase 4 — escape Angular-sensitive chars in remaining text
+  template = escapeAngularChars(template);
+
+  // Phase 5 — restore control flow blocks (as-is)
+  template = restoreBlocks(template, '__NG_BLOCK_', blocks);
+
+  // Phase 6 — restore protected zones (with escaping)
+  template = restoreBlocks(template, '__PROTECTED_', protectedZones, true);
+
+  return template;
+}
+
+// ─── Phase helpers ───────────────────────────────────────────────────
+
+function extractProtectedZones(template: string, zones: string[]): string {
+  return template.replace(//g, match => {
+    zones.push(match);
+    return `__PROTECTED_${zones.length - 1}__`;
+  });
+}
+
+function decodeHtmlEntities(blocks: string[]): void {
+  for (let i = 0; i < blocks.length; i++) {
+    for (const [pattern, replacement] of HTML_ENTITIES) {
+      blocks[i] = blocks[i].replace(pattern, replacement);
+    }
+  }
+}
+
+function escapeAngularChars(text: string): string {
+  return text.replace(/{/g, '{').replace(/}/g, '}').replace(/@/g, '@');
+}
+
+function restoreBlocks(template: string, prefix: string, blocks: string[], escape = false): string {
+  let restored = '';
+  let searchStart = 0;
+  for (let i = 0; i < blocks.length; i++) {
+    const token = `${prefix}${i}__`;
+    const tokenIndex = template.indexOf(token, searchStart);
+    if (tokenIndex === -1) continue;
+    const content = escape ? escapeAngularChars(blocks[i]) : blocks[i];
+    restored += template.slice(searchStart, tokenIndex) + content;
+    searchStart = tokenIndex + token.length;
+  }
+  restored += template.slice(searchStart);
+  return restored;
+}
+
+// ─── Control flow extraction ─────────────────────────────────────────
+
+function extractControlFlowBlocks(template: string, blocks: string[]): string {
+  let result = '';
+  let i = 0;
+  while (i < template.length) {
+    if (template[i] !== '@') {
+      result += template[i++];
+      continue;
+    }
+    const extracted = tryExtractInline(template, i)
+                   ?? tryExtractBlock(template, i);
+    if (extracted) {
+      blocks.push(extracted.content);
+      result += `__NG_BLOCK_${blocks.length - 1}__`;
+      i = extracted.end;
+    } else {
+      result += template[i++];
+    }
+  }
+  return result;
+}
+
+/** Try to extract an inline statement: @let identifier = expr; */
+function tryExtractInline(template: string, pos: number): { content: string; end: number } | null {
+  const match = matchKeywordAt(template, pos);
+  if (!match || !INLINE_KEYWORDS.includes(match.keyword)) {
+    return null;
+  }
+  const validator = INLINE_VALIDATORS[match.keyword];
+  if (validator && !validator.test(template.substring(pos + match.raw.length))) {
+    return null;
+  }
+  const semicolonIdx = findStatementSemicolon(template, pos);
+  if (semicolonIdx === -1) {
+    return null;
+  }
+  return { content: template.substring(pos, semicolonIdx + 1), end: semicolonIdx + 1 };
+}
+
+/** Try to extract a block statement: @keyword (...) { body } with optional chains */
+function tryExtractBlock(template: string, start: number): { content: string; end: number } | null {
+  let i = start;
+  const len = template.length;
+
+  while (i < len) {
+    const match = matchKeywordAt(template, i);
+    if (!match) {
+      return null;
+    }
+    i += match.raw.length;
+
+    // optional parenthesized params
+    i = skipWhitespace(template, i);
+    if (i < len && template[i] === '(') {
+      const parenEnd = findMatchingPair(template, i, '(', ')');
+      if (parenEnd === -1) return null;
+      i = skipWhitespace(template, parenEnd + 1);
+    }
+
+    // required { body }
+    if (i >= len || template[i] !== '{') return null;
+    const braceEnd = findMatchingPair(template, i, '{', '}');
+    if (braceEnd === -1) return null;
+    i = braceEnd + 1;
+
+    // check for chain keyword (@else, @empty, etc.)
+    const chainMatch = template.substring(i).match(CHAIN_RE);
+    if (chainMatch) {
+      i += chainMatch[0].indexOf('@');
+      continue;
+    }
+
+    return { content: template.substring(start, i), end: i };
+  }
+  return null;
+}
+
+// ─── Low-level helpers ───────────────────────────────────────────────
+
+function matchKeywordAt(template: string, pos: number): { keyword: string; raw: string } | null {
+  const remaining = template.substring(pos);
+  const m = KEYWORD_RE.exec(remaining);
+  return m && m.index === 0 ? { keyword: m[1], raw: m[0] } : null;
+}
+
+function skipWhitespace(str: string, pos: number): number {
+  while (pos < str.length && (str[pos] === ' ' || str[pos] === '\n' || str[pos] === '\r' || str[pos] === '\t')) {
+    pos++;
+  }
+  return pos;
+}
+
+function findMatchingPair(template: string, start: number, open: string, close: string): number {
+  let depth = 0;
+  return scanOutsideStrings(template, start, pos => {
+    if (template[pos] === open) depth++;
+    else if (template[pos] === close) {
+      if (--depth === 0) return pos;
+    }
+  });
+}
+
+/** Find the real statement-terminating ; (skipping ; inside HTML entities and entity-encoded strings) */
+function findStatementSemicolon(template: string, start: number): number {
+  return scanOutsideStrings(template, start, pos => {
+    if (template[pos] === ';') {
+      const before = template.substring(Math.max(start, pos - 12), pos);
+      if (!/&[a-zA-Z0-9#]+$/.test(before)) return pos;
+    }
+  });
+}
+
+/** Scan template skipping entity-encoded strings ("...", '...'), call handler for each char outside strings */
+function scanOutsideStrings(template: string, start: number, onChar: (pos: number) => number): number {
+  const ENTITY_QUOTES = ['"', '''];
+  let inString: string | null = null;
+  let i = start;
+  while (i < template.length) {
+    if (!inString) {
+      const quote = ENTITY_QUOTES.find(q => template.startsWith(q, i));
+      if (quote) { inString = quote; i += quote.length; continue; }
+    } else if (template.startsWith(inString, i)) {
+      i += inString.length; inString = null; continue;
+    }
+    if (!inString) {
+      const result = onChar(i);
+      if (result !== undefined) return result;
+    }
+    i++;
+  }
+  return -1;
+}
diff --git a/ui-ngx/src/app/shared/components/markdown.component.html b/ui-ngx/src/app/shared/components/markdown.component.html
index 62709f4f90..203900dd96 100644
--- a/ui-ngx/src/app/shared/components/markdown.component.html
+++ b/ui-ngx/src/app/shared/components/markdown.component.html
@@ -17,10 +17,12 @@
 -->
 
 
-
- {{error}} -
+ {{error}} +
+}
diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index f797960f6d..e7d9df1eb3 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -38,6 +38,7 @@ import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { guid, isDefinedAndNotNull } from '@core/utils'; import { Observable, of, ReplaySubject } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { sanitizeTemplate } from './markdown-sanitize.helper'; let defaultMarkdownStyle: string; @@ -133,7 +134,7 @@ export class TbMarkdownComponent implements OnChanges { const preHtml = preElements.item(i).outerHTML.replace('ngnonbindable=""', 'ngNonBindable'); template = template.replace(matches[i][0], preHtml); } - template = this.sanitize(template); + template = sanitizeTemplate(template); this.markdownContainer.clear(); let styles: string[] = []; let readyObservable: Observable; @@ -267,10 +268,6 @@ export class TbMarkdownComponent implements OnChanges { } } - private sanitize(template: string): string { - return template.replace(/{/g, '{').replace(/}/g, '}').replace(/@/g, '@'); - } - private destroyMarkdownInstanceResources() { if (this.tbMarkdownInstanceComponentType) { this.dynamicComponentFactoryService.destroyDynamicComponent(this.tbMarkdownInstanceComponentType); 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 index 59566607be..55e76946d6 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -15,20 +15,23 @@ limitations under the License. --> -
- {{materialIconFormGroup.get('icon').value}} - - {{ label }} - - - -
- +@if (!asBoxInput) { +
+ {{materialIconFormGroup.get('icon').value}} + + {{ label }} + + @if (iconClearButton) { + + } + +
+} @else { -
+} 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 index e232209fea..ec040bd488 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -36,6 +36,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { MaterialIconsComponent } from '@shared/components/material-icons.component'; import { MatButton } from '@angular/material/button'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-material-icon-select', @@ -72,6 +73,13 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @coerceBoolean() iconClearButton = false; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -170,7 +178,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.viewContainerRef, MaterialIconsComponent, 'left', true, null, { selectedIcon: this.materialIconFormGroup.get('icon').value, - iconClearButton: this.iconClearButton + iconClearButton: this.iconClearButton, + allowedCustomIcon: this.allowedCustomIcon, }, {}, {}, {}, true); diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index d9e87f11fd..bb9e3fca58 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -16,63 +16,100 @@ -->
-
icon.icons
- - search - - - - -
- - - - -
-
- -
-
-
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
-
-
-
- + } + @if (icon.name !== selectedIcon) { + + } + } +
+ + @if (notFound) { +
+
+
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
+
+ } + @if (iconClearButton || !showAllSubject.value) { +
+ @if (iconClearButton) { + - - -
+ {{ 'action.clear' | translate }} + + } + + @if (!showAllSubject.value) { + + } +
+ } + } @else { + + +
+ + +
+ }
diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index 203772c6bd..101f7756aa 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -37,6 +37,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { isTbImage } from '@shared/models/resource.models'; @Component({ selector: 'tb-material-icons', @@ -62,6 +63,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { @coerceBoolean() showTitle = true; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + @Input() popover: TbPopoverComponent; @@ -72,6 +77,8 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { showAllSubject = new BehaviorSubject(false); searchIconControl: UntypedFormControl; + isCustomIcon = false; + iconsRowHeight = 48; iconsPanelHeight: string; @@ -123,14 +130,15 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { map((data) => data.iconRows), share() ); + this.isCustomIcon = isTbImage(this.selectedIcon) } clearSearch() { this.searchIconControl.patchValue('', {emitEvent: true}); } - selectIcon(icon: MaterialIcon) { - this.iconSelected.emit(icon.name); + selectIcon(icon: string) { + this.iconSelected.emit(icon); } clearIcon() { 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 index b073d93a4e..c63cbf82fb 100644 --- a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ 'rulenode.message-type' | translate }} - + @if (messageTypeFormGroup.get('messageType').value && !disabled) { + + } - - - + @for (messageType of filteredMessageTypes | async; track messageType) { + + + + } - - {{ 'rulenode.message-type-required' | translate }} - + @if (messageTypeFormGroup.get('messageType').hasError('required')) { + + {{ '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 index f9ca55f587..b9719a6863 100644 --- a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts @@ -24,6 +24,7 @@ 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'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-message-type-autocomplete', @@ -51,6 +52,9 @@ export class MessageTypeAutocompleteComponent implements ControlValueAccessor, O this.requiredValue = coerceBooleanProperty(value); } + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @Input() disabled: boolean; diff --git a/ui-ngx/src/app/shared/components/multiple-image-input.component.html b/ui-ngx/src/app/shared/components/multiple-image-input.component.html index a906430a7a..da82cf54db 100644 --- a/ui-ngx/src/app/shared/components/multiple-image-input.component.html +++ b/ui-ngx/src/app/shared/components/multiple-image-input.component.html @@ -16,7 +16,9 @@ -->
- + @if (label) { + + }
@@ -24,46 +26,58 @@ [dndDisableIf]="disabled" (dndDrop)="imageDrop($event)" [class.no-images]="!safeImageUrls.length">
-
- {{ 'image-input.images' | translate }} [{{ $index }}] -
-
- drag_indicator -
-
- -
-
- + @for (safeImageUrl of safeImageUrls; track safeImageUrl) { +
+ {{ 'image-input.images' | translate }} [{{ $index }}] +
+ @if (!disabled) { +
+ drag_indicator +
+ } +
+ +
+ @if (!disabled) { +
+ +
+ }
-
-
{{ 'image-input.no-images' | translate }}
+ } + @if (!safeImageUrls.length) { +
{{ 'image-input.no-images' | translate }}
+ }
-
-
- cloud_upload - image-input.drop-images-or - - + @if (!disabled) { +
+
+ cloud_upload + image-input.drop-images-or + + +
-
+ }
-
dashboard.maximum-upload-file-size
+ @if (maxSizeByte && !disabled) { +
dashboard.maximum-upload-file-size
+ }
diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.html b/ui-ngx/src/app/shared/components/notification/notification.component.html index db04d8df98..5a7a35eb2a 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.html +++ b/ui-ngx/src/app/shared/components/notification/notification.component.html @@ -17,35 +17,44 @@ -->
-
- - {{ notification.additionalConfig.icon.icon }} - -
- - - {{ notificationTypeIcons.get(notification.type) }} - - + @if (showIcon) { +
+ + {{ notification.additionalConfig.icon.icon }} + +
+ } @else { + @if (notificationTypeIcons.get(notification.type)) { + + {{ notificationTypeIcons.get(notification.type) }} + + } + }
- + @if (showButton) { + + }
{{ notification.createdTime ?? currentDate | dateAgo }} - -
- - {{alarmSeverityTranslations.get(notification.info.alarmSeverity) | translate}} - -
+ @if (!hideMarkAsReadButton) { + + } + @if (notification.type === notificationType.ALARM && !notification.info.cleared) { +
+ + {{alarmSeverityTranslations.get(notification.info.alarmSeverity) | translate}} + +
+ }
diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.ts b/ui-ngx/src/app/shared/components/notification/notification.component.ts index 941552ab5e..d4f58f6989 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.ts +++ b/ui-ngx/src/app/shared/components/notification/notification.component.ts @@ -17,7 +17,6 @@ import { Component, EventEmitter, Input, OnInit, Output, SecurityContext } from '@angular/core'; import { ActionButtonLinkType, - AlarmSeverityNotificationColors, Notification, NotificationStatus, NotificationType, @@ -25,7 +24,11 @@ import { } from '@shared/models/notification.models'; import { UtilsService } from '@core/services/utils.service'; import { Router } from '@angular/router'; -import { alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { + alarmSeverityBackgroundColors, + alarmSeverityColors, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; import tinycolor from 'tinycolor2'; import { StateObject } from '@core/api/widget-api.models'; import { objToBase64URI } from '@core/utils'; @@ -136,13 +139,13 @@ export class NotificationComponent implements OnInit { } } - alarmColorSeverity(alpha: number) { - return tinycolor(AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity)).setAlpha(alpha).toRgbString(); + alarmColorSeverityBackground() { + return alarmSeverityBackgroundColors.get(this.notification.info.alarmSeverity); } notificationColor(): string { if (this.notification.type === NotificationType.ALARM && !this.notification.info.cleared) { - return AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity); + return alarmSeverityColors.get(this.notification.info.alarmSeverity); } return 'transparent'; } @@ -156,9 +159,11 @@ export class NotificationComponent implements OnInit { notificationIconColor(): object { if (this.notification.type === NotificationType.ALARM) { - return {color: AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity)}; + return {color: alarmSeverityColors.get(this.notification.info.alarmSeverity)}; } else if (this.notification.type === NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT) { return {color: '#D12730'}; + } else if (this.notification.type === NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST) { + return {color: '#305680'}; } return null; } diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html index 29eaa77881..26ff960db8 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + notification.template - - - + @if (selectTemplateFormGroup.get('templateName').value && !disabled) { + + } + @if (selectTemplateFormGroup.get('templateName').value?.id && !disabled && allowEdit) { + + } + @if (allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled) { + + } - - - - - {{ notificationDeliveryMethodInfoMap.get(method.key).name | translate }} - - - - -
-
- notification.no-notification-templates + @for (template of (filteredTemplate | async); track template) { + + + + @for (method of template.configuration.deliveryMethodsTemplates | keyvalue; track method) { + + {{ getNotificationDeliveryMethodInfoMap(method.key).name | translate }} + + } + + + } + @if (!(filteredTemplate | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ notification.no-notification-templates +
+ } @else { + {{ 'notification.no-template-matching' | translate : {template: truncate.transform(searchText, true, 6,'...')} }} + @if (allowCreate) { + + notification.create-new-template + + } + }
- - {{ 'notification.no-template-matching' | translate : {template: truncate.transform(searchText, true, 6, '...')} }} - - notification.create-new-template - - -
- + + } - - {{ 'notification.template-required' | translate }} - + @if (selectTemplateFormGroup.get('templateName').hasError('required')) { + + {{ 'notification.template-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts index 61b5b1c973..477f93d208 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts @@ -28,6 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { emptyPageData } from '@shared/models/page/page-data'; import { + NotificationDeliveryMethod, NotificationDeliveryMethodInfoMap, NotificationTemplate, NotificationType @@ -41,6 +42,7 @@ import { import { MatDialog } from '@angular/material/dialog'; import { MatButton } from '@angular/material/button'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-template-autocomplete', @@ -71,6 +73,8 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn @coerceBoolean() allowEdit = false; + @Input() + appearance: MatFormFieldAppearance = 'fill'; @Input() disabled: boolean; @@ -217,7 +221,7 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn button._elementRef.nativeElement.blur(); this.createTemplateByName($event); } - + createTemplateByName($event: Event, name?: string) { $event?.stopPropagation(); this.openNotificationTemplateDialog({ @@ -265,4 +269,8 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn this.updateView(null); this.dirty = true; } + + getNotificationDeliveryMethodInfoMap(key: string) { + return this.notificationDeliveryMethodInfoMap.get(key as NotificationDeliveryMethod); + } } diff --git a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html index 6ccefe94ea..7ff7f7ba2c 100644 --- a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ placeholderText | translate }} - - {{ displayPackageFn(otaPackageFormGroup.get('packageId').value) }} - - + @if (showDetailsPageLink && usePackageLink && otaPackageFormGroup.get('packageId').value && disabled) { + + {{ displayPackageFn(otaPackageFormGroup.get('packageId').value) }} + + } + @if (otaPackageFormGroup.get('packageId').value && !disabled) { + + } - - - - -
-
- {{ notFoundPackage | translate }} -
- - + @for (package of filteredPackages | async; track package) { + + + + } + @if (!(filteredPackages | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ notFoundPackage | translate }} +
+ } @else { + {{ translate.get(notMatchingPackage, - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} - - -
-
+ {entity: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ } +
+
+ }
- - {{ requiredErrorText | translate }} - - {{ hintText | translate }} + @if (otaPackageFormGroup.get('packageId').hasError('required')) { + + {{ requiredErrorText | translate }} + + } + @if (!disabled) { + {{ hintText | translate }} + }
diff --git a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts index 339550e18a..a7208d4507 100644 --- a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts @@ -35,6 +35,7 @@ import { getEntityDetailsPageURL, isDefinedAndNotNull } from '@core/utils'; import { AuthUser } from '@shared/models/user.model'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-ota-package-autocomplete', @@ -93,6 +94,9 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On @Input() showDetailsPageLink = false; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { diff --git a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html new file mode 100644 index 0000000000..534850034a --- /dev/null +++ b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html @@ -0,0 +1,35 @@ + + +
+ @for (rule of passwordErrorRules; track $index) { + @if (!rule.policyProp || passwordPolicy[rule.policyProp] > 0) { +

+ + {{ checkForError(rule.key) ? 'mdi:close' : 'mdi:check' }} + + {{ rule.translation | translate : passwordPolicy }} +

+ } + } +
+
+
diff --git a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss new file mode 100644 index 0000000000..a0bd527632 --- /dev/null +++ b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.password-checklist-card { + background-color: #0000009E; + backdrop-filter: blur(8px); + color: white; + padding: 12px 16px; + border-radius: 8px; + position: relative; + min-width: 220px; + display: flex; + gap: 8px; + flex-direction: column; + + & > tb-icon { + color: white; + } + + & > p { + margin: 0; + } + + & > .tooltip-arrow { + position: absolute; + bottom: -6px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid #002b36; + } +} diff --git a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts new file mode 100644 index 0000000000..83fceaa374 --- /dev/null +++ b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, ViewEncapsulation } from '@angular/core'; +import { CdkOverlayOrigin, ConnectionPositionPair } from '@angular/cdk/overlay'; +import { passwordErrorRules } from '@shared/models/password.models'; +import { AbstractControl } from '@angular/forms'; +import { UserPasswordPolicy } from '@shared/models/settings.models'; +import { POSITION_MAP } from '@shared/models/overlay.models'; + +@Component({ + selector: 'tb-password-requirements-tooltip', + templateUrl: './password-requirements-tooltip.component.html', + styleUrl: './password-requirements-tooltip.component.scss', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class PasswordRequirementsTooltipComponent { + @Input() passwordControl: AbstractControl; + @Input() passwordPolicy: UserPasswordPolicy; + @Input() trigger: CdkOverlayOrigin; + + passwordErrorRules = passwordErrorRules; + isTooltipOpen = false; + + overlayPositions: ConnectionPositionPair[] = [ + {...POSITION_MAP.top, offsetY: -20} + ]; + + checkForError(errorName: string): boolean { + return this.passwordControl?.hasError(errorName) ?? false; + } + + onFocus(): void { + this.isTooltipOpen = true; + } + + onBlur(): void { + this.isTooltipOpen = false; + } +} diff --git a/ui-ngx/src/app/shared/components/phone-input.component.html b/ui-ngx/src/app/shared/components/phone-input.component.html index 69ba1c6b64..2590b5d913 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.html +++ b/ui-ngx/src/app/shared/components/phone-input.component.html @@ -17,16 +17,24 @@ -->
-
- {{ flagIcon }} - - - - {{country.flag}} - {{' ' + country.name + ' +' + country.dialCode }} - - -
+ @if (showFlagSelect) { +
+ @if (!isLoad) { + {{ flagIcon }} + } + @if (isLoad) { + + } + + @for (country of allCountries; track country) { + + {{country.flag}} + {{' ' + country.name + ' +' + country.dialCode }} + + } + +
+ } {{ label }} - - - {{ 'phone-input.phone-input-required' | translate }} - - - {{ 'phone-input.phone-input-validation' | translate }} - + + @if (phoneFormGroup.get('phoneNumber').hasError('required')) { + + {{ requiredErrorText }} + + } + @if (phoneFormGroup.get('phoneNumber').hasError('invalidPhoneNumber')) { + + {{ validationErrorText }} + + }
diff --git a/ui-ngx/src/app/shared/components/phone-input.component.ts b/ui-ngx/src/app/shared/components/phone-input.component.ts index 7f75741273..680d382c38 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.ts +++ b/ui-ngx/src/app/shared/components/phone-input.component.ts @@ -78,6 +78,15 @@ export class PhoneInputComponent implements OnInit, ControlValueAccessor, Valida @Input() label = this.translate.instant('phone-input.phone-input-label'); + @Input() + hint = 'phone-input.phone-input-hint'; + + @Input() + requiredErrorText = this.translate.instant('phone-input.phone-input-required'); + + @Input() + validationErrorText = this.translate.instant('phone-input.phone-input-validation'); + get showFlagSelect(): boolean { return this.enableFlagsSelect && !this.isLegacy; } diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index 7b74c90bfd..f971d94ec3 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -327,9 +327,9 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { (overlayOutsideClick)="onClickOutside($event)" (detach)="hide()" (positionChange)="onPositionChange($event)" - > + >
+ (@popoverMotion.done)="animationDone()">
+ >
@@ -366,7 +370,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
- `, + `, standalone: false }) export class TbPopoverComponent implements OnDestroy, OnInit { diff --git a/ui-ngx/src/app/shared/components/protobuf-content.component.html b/ui-ngx/src/app/shared/components/protobuf-content.component.html index 167b68d2aa..ae485347d1 100644 --- a/ui-ngx/src/app/shared/components/protobuf-content.component.html +++ b/ui-ngx/src/app/shared/components/protobuf-content.component.html @@ -21,10 +21,12 @@
- + @if (!readonly && !disabled) { + + }
+ class="mat-block autocomplete-queue" [subscriptSizing]="subscriptSizing" [appearance]="appearance"> {{ 'queue.queue-name' | translate }} - + @if (selectQueueFormGroup.get('queueName').value && !disabled) { + + } - - - {{getDescription(queue)}} - - -
-
- queue.no-queues-found -
- - + @for (queue of filteredQueues | async; track queue) { + + + {{getDescription(queue)}} + + } + @if (!(filteredQueues | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ queue.no-queues-found +
+ } @else { + {{ translate.get('queue.no-queues-matching', - {queue: truncate.transform(searchText, true, 6, '...')}) | async }} - - -
-
+ {queue: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ } +
+
+ }
- {{ autocompleteHint | translate }} - - {{ 'queue.queue-required' | translate }} - + @if (autocompleteHint) { + {{ autocompleteHint | translate }} + } + @if (selectQueueFormGroup.get('queueName').hasError('required')) { + + {{ 'queue.queue-required' | translate }} + + }
diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts index f687681ee3..bc06d55941 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts @@ -31,7 +31,7 @@ import { QueueService } from '@core/http/queue.service'; import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { emptyPageData } from '@shared/models/page/page-data'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; @Component({ selector: 'tb-queue-autocomplete', @@ -77,6 +77,9 @@ export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit @Input() disabled: boolean; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @ViewChild('queueInput', {static: true}) queueInput: ElementRef; filteredQueues: Observable>>; 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 index 8e4542bad2..397c58dc78 100644 --- 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 @@ -17,7 +17,9 @@ --> - {{ 'relation.relation-type' | translate }} + @if (showLabel) { + {{ 'relation.relation-type' | translate }} + } - + @if (relationTypeFormGroup.get('relationType').value && !disabled) { + + } - - - + @for (relationType of filteredRelationTypes | async; track relationType) { + + + + } - - {{ 'relation.relation-type-required' | translate }} - - - {{ 'relation.relation-type-max-length' | translate }} - + @if (relationTypeFormGroup.get('relationType').hasError('required')) { + + {{ 'relation.relation-type-required' | translate }} + + } + @if (relationTypeFormGroup.get('relationType').hasError('maxlength')) { + + {{ 'relation.relation-type-max-length' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html b/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html index 5f3160168e..f461ad16cb 100644 --- a/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html @@ -29,36 +29,44 @@ [required]="required" [matAutocomplete]="entityAutocomplete" [matAutocompleteDisabled]="!allowAutocomplete"> - + @if (resourceFormGroup.get('resource').value && !disabled) { + + } - - - - - -
-
- {{ 'js-func.no-js-module-text' | translate }} -
- - - {{ 'js-func.no-js-module-matching' | translate: {module: searchText | truncate: true: 6: '...'} }} - - -
-
-
- - - {{ searchText }} + @for (resource of filteredResources$ | async; track resource) { + + - + } + @if (subType !== ResourceSubType.EXTENSION) { + @if (!(filteredResources$ | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'js-func.no-js-module-text' | translate }} +
+ } @else { + + {{ 'js-func.no-js-module-matching' | translate: {module: searchText | truncate: true: 6: '...'} }} + + } +
+
+ } + } @else { + @if (!(filteredResources$ | async)?.length) { + + {{ searchText }} + + } + }
diff --git a/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html index 5b357b3ed0..253a92e50a 100644 --- a/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html @@ -18,7 +18,7 @@

{{configuration.title}}

- + @if (data.multiple) {
@@ -35,20 +35,21 @@ - - - - {{ image.title }} - - + @if (displayPreview) { + + + + {{ image.title }} + + + } - + @if (dataSource.selection.isEmpty()) { {{ 'resource.title' | translate }} - - + } @else { {{ translate.get(configuration.selectedText, {count: dataSource.selection.selected.length}) | async }} - + } {{ resource.title }} @@ -60,23 +61,22 @@ + (click)="toggleShowReferences($event, resource, showReferencesButton)">{{ 'image.references' | translate }} + *matRowDef="let resource; columns: configuration.columns;">
-
+ } @else { + +
+ }
+ cdkFocusInitial>{{ (data.multiple ? 'action.delete-selected' : 'action.delete-anyway') | translate}}
- - -
-
diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html index beb6f09081..62584f5336 100644 --- a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html @@ -25,19 +25,25 @@ - - check - - - -
- - {{ 'rulechain.no-rulechains-matching' | translate : {entity: searchText} }} - -
-
+ @for (ruleChain of filteredRuleChains | async; track ruleChain) { + + @if (ruleChainId === ruleChain.id.id) { + check + } + + + } + @if (!(filteredRuleChains | async)?.length) { + +
+ + {{ 'rulechain.no-rulechains-matching' | translate : {entity: searchText} }} + +
+
+ }
diff --git a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html index fe67e82143..a0d3c4023a 100644 --- a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ labelText }} - + @if (conversationSlackFormGroup.get('conversation').value && !disabled) { + + } - - - - -
-
- notification.not-found-slack-recipient + @for (template of slackConversation$ | async; track template) { + + + + } + @if (!(slackConversation$ | async)?.length) { + +
+ @if (!textIsNotEmpty(slackSearchText)) { +
+ notification.not-found-slack-recipient +
+ } @else { + Not slack recipient matches {{ truncate.transform(slackSearchText, true, 6, '...') }} + }
- - Not slack recipient matches {{ truncate.transform(slackSearchText, true, 6, '...') }} - -
- + + } - - {{ requiredText }} - + @if (conversationSlackFormGroup.get('conversation').hasError('required')) { + + {{ requiredText }} + + } diff --git a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts index 0087afeda5..5a6ac46d1c 100644 --- a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts @@ -27,6 +27,7 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { SlackChanelType, SlackConversation } from '@shared/models/notification.models'; import { NotificationService } from '@core/http/notification.service'; import { isEqual } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-slack-conversation-autocomplete', @@ -67,6 +68,9 @@ export class SlackConversationAutocompleteComponent implements ControlValueAcces @Input() token: string; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + @ViewChild('slackInput', {static: true}) slackInput: ElementRef; slackConversation$: Observable>; diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.html b/ui-ngx/src/app/shared/components/snack-bar-component.html index 2197cebdcc..0bd630d154 100644 --- a/ui-ngx/src/app/shared/components/snack-bar-component.html +++ b/ui-ngx/src/app/shared/components/snack-bar-component.html @@ -24,11 +24,10 @@ [class.warn-toast]="notification.type === 'warn'" [class.success-toast]="notification.type === 'success'" [class.info-toast]="notification.type === 'info'"> - + @if (!notification.modern) {
-
- + } @else {
- + }
diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-autocomplete.component.html index 8ce80a82e9..2471b5caf3 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.html @@ -17,35 +17,45 @@ --> - {{label}} + @if (label) { + {{label}} + } - - - warning - - - {{errorText}} - + @if (selectionFormControl.value && !disabled) { + + } + @if (selectionFormControl.errors && selectionFormControl.touched && !showInlineError) { + + warning + + } + @if (selectionFormControl.errors && showInlineError) { + + {{ getErrorMessage }} + + } - - - + @for (option of filteredOptions$ | async; track option) { + + + + } diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.scss b/ui-ngx/src/app/shared/components/string-autocomplete.component.scss index 6d219a04e2..267502ab13 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.scss +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.scss @@ -20,23 +20,9 @@ max-width: 100%; box-sizing: border-box; - .tb-autocomplete.tb-option-input-autocomplete { - .mat-mdc-option { - border-bottom: none; - - .mdc-list-item__primary-text { - flex: 1; - display: flex; - flex-direction: row; - gap: 8px; - - .tb-option { - font-size: 14px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - } - } + &.tb-inline-field { + .mdc-icon-button { + margin-right: 8px; } } } diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.ts b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts index 4257b0bbc2..0399eddc88 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts @@ -14,27 +14,25 @@ /// limitations under the License. /// -import { - Component, - Input, - forwardRef, - OnInit, - ViewChild, - ElementRef -} from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, - NG_VALUE_ACCESSOR, + FormBuilder, FormControl, - Validators, - FormBuilder + NG_VALUE_ACCESSOR, + ValidatorFn, + Validators } from '@angular/forms'; import { Observable, of } from 'rxjs'; -import { tap, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { coerceBoolean } from '@shared/decorators/coercion'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +export interface ErrorMessageConfig { + [errorKey: string]: string; +} + @Component({ selector: 'tb-string-autocomplete', templateUrl: './string-autocomplete.component.html', @@ -86,6 +84,12 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit @Input() errorText: string; + @Input() + controlValidators: ValidatorFn[] = []; + + @Input() + errorMessages: ErrorMessageConfig; + @Input() @coerceBoolean() showInlineError = false; @@ -107,7 +111,17 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit } ngOnInit() { - this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []); + const validators = [Validators.pattern(/.*\S.*/)]; + if (this.controlValidators?.length) { + validators.push(...this.controlValidators); + const parentHasRequired = this.controlValidators.some(v => v === Validators.required); + if (this.required && !parentHasRequired) { + validators.push(Validators.required); + } + } else if (this.required) { + validators.push(Validators.required); + } + this.selectionFormControl = this.fb.control('', validators); this.filteredOptions$ = this.selectionFormControl.valueChanges .pipe( tap(value => this.updateView(value)), @@ -153,7 +167,7 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit updateView(value: string) { this.searchText = value ? value : ''; if (this.modelValue !== value) { - this.modelValue = value; + this.modelValue = value?.trim(); this.propagateChange(this.modelValue); } } @@ -181,4 +195,18 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit this.nameInput.nativeElement.focus(); }, 0); } + + get getErrorMessage(): string { + if (!this.selectionFormControl.errors) { + return ''; + } + if (this.errorMessages) { + for (const errorKey in this.selectionFormControl.errors) { + if (this.errorMessages[errorKey]) { + return this.errorMessages[errorKey]; + } + } + } + return this.errorText; + } } diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 7a0489345f..dd8f280aab 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -20,16 +20,22 @@ [floatLabel]="floatLabel" [appearance]="appearance" [subscriptSizing]="subscriptSizing"> - {{ label }} + @if (label) { + {{ label }} + } - - {{ item.name }} - close - + @for (item of itemList; track item) { + + {{ item.name }} + @if (!disabled) { + close + } + + } @for (value of filteredValues | async; track value.value) { - } @empty { + } @empty { @if (!allowUserValue) { {{ 'common.not-found' | translate }} } @@ -67,7 +73,9 @@
- - {{ requiredText }} - + @if (stringItemsForm.get('items').hasError('required')) { + + {{ requiredText }} + + } diff --git a/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.html new file mode 100644 index 0000000000..1f10bc4974 --- /dev/null +++ b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.html @@ -0,0 +1,65 @@ + + + @if (label) { + {{label}} + } +

+  
+  @if (predefinedValuesButton) {
+    
+  }
+  @if (selectionFormControl.hasError('required') && !showInlineError) {
+    
+      warning
+    
+  }
+  @if (selectionFormControl.hasError('required') && showInlineError) {
+    
+      {{errorText}}
+    
+  }
+
+ +
+ @for(option of filteredOptions; track option; let i = $index) { + + + + } +
+
diff --git a/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.scss b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.scss new file mode 100644 index 0000000000..acd26041ef --- /dev/null +++ b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.scss @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "constants"; + +:host { + .highlighted-text { + position: absolute; + inset: 0; + margin: 0; + z-index: 1; + pointer-events: none; + padding-top: var(--mat-form-field-filled-with-label-container-padding-top); + padding-bottom: var(--mat-form-field-filled-with-label-container-padding-bottom); + font: inherit; + letter-spacing: inherit; + text-decoration: inherit; + text-transform: inherit; + overflow: hidden; + color: var(--mat-form-field-filled-input-text-color, var(--mat-app-on-surface)); + + &.disabled { + color: var(--mat-form-field-filled-disabled-input-text-color); + } + } + + .input-field-highlight { + position: relative; + background: transparent; + z-index: 2; + color: transparent; + width: 100%; + } +} + +:host ::ng-deep { + + .mdc-text-field--outlined .mat-mdc-form-field-infix { + .highlighted-text { + padding-top: var(--mat-form-field-container-vertical-padding); + padding-bottom: var(--mat-form-field-container-vertical-padding); + } + } + + .highlighted-text { + .highlight { + color: $tb-secondary-color; + text-shadow: 0 0 0.5px var($tb-secondary-color, var(--mat-app-on-surface)); + } + + &.disabled { + .highlight { + color: $tb-primary-color-light; + text-shadow: 0 0 0.5px var($tb-primary-color-light, var(--mat-form-field-filled-disabled-input-text-color)); + } + } + } +} + +::ng-deep { + div.tb-sting-pattern-autocomplete-panel { + width: 100%; + max-height: 256px; + visibility: visible; + transform-origin: center top; + overflow: auto; + padding: 8px 0; + box-sizing: border-box; + position: static; + border-radius: var(--mat-autocomplete-container-shape, var(--mat-app-corner-extra-small)); + box-shadow: var(--mat-autocomplete-container-elevation-shadow); + background-color: var(--mat-autocomplete-background-color, var(--mat-app-surface-container)) + } + + .cdk-overlay-pane:not(.mat-mdc-autocomplete-panel-above) & { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .mat-mdc-autocomplete-panel-above & { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + transform-origin: center bottom; + } + +} diff --git a/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.ts b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.ts new file mode 100644 index 0000000000..60f001f188 --- /dev/null +++ b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.ts @@ -0,0 +1,435 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + QueryList, + SimpleChanges, + TemplateRef, + ViewChild, + ViewChildren, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { fromEvent, merge } from 'rxjs'; +import { debounceTime, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + ConnectedPosition, + FlexibleConnectedPositionStrategy, + Overlay, + OverlayRef, + PositionStrategy +} from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; + +@Component({ + selector: 'tb-string-pattern-autocomplete', + templateUrl: './string-pattern-autocomplete.component.html', + styleUrls: ['./string-pattern-autocomplete.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StringPatternAutocompleteComponent), + multi: true + } + ], + standalone: false +}) +export class StringPatternAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { + + @ViewChild('inputRef', {static: true}) inputRef: ElementRef; + @ViewChild('highlightTextRef', {static: true}) highlightTextRef: ElementRef; + @ViewChild('autocompleteTemplate', {static: true}) autocompleteTemplate: TemplateRef; + @ViewChildren('optionItem', { read: ElementRef }) optionItems!: QueryList>; + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + required = false; + + @Input({required: true}) + predefinedValues: Array; + + @Input() + placeholderText: string = this.translate.instant('widget-config.set'); + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + additionalClass: string | string[] | Record; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + label: string; + + @Input() + tooltipClass = 'tb-error-tooltip'; + + @Input() + errorText: string; + + @Input() + @coerceBoolean() + showInlineError = false; + + @Input() + @coerceBoolean() + predefinedValuesButton = false; + + @Input() + patternSymbol = '$'; + + @Input() + brackets: 'curly' | 'square'; + + selectionFormControl: FormControl; + filteredOptions: Array; + + searchText = ''; + highlightedHtml = ''; + activeOptionIndex = -1; + + private modelValue: string | null; + private overlayRef!: OverlayRef; + + private predefinedValuesButtonMode = false; + + private propagateChange = (_val: any) => { + }; + + constructor(private fb: FormBuilder, + private overlay: Overlay, + private translate: TranslateService, + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []); + merge( + fromEvent(this.inputRef.nativeElement, 'selectionchange'), + this.selectionFormControl.valueChanges.pipe(tap(value => this.updateView(value))) + ).pipe( + debounceTime(50), + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.onSelectionChange(); + this.cd.markForCheck(); + }); + + fromEvent(this.inputRef.nativeElement, 'keydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(event => this.handleKeydown(event)); + } + + ngOnChanges(changes: SimpleChanges) { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'predefinedValues' || propName === 'patternSymbol') { + this.highlightedHtml = this.getHighlightHtml(this.modelValue); + } + if (propName === 'required') { + if (change.currentValue) { + this.selectionFormControl.addValidators(Validators.required); + } else { + this.selectionFormControl.removeValidators(Validators.required); + } + this.selectionFormControl.updateValueAndValidity({emitEvent: false}) + } + } + } + } + + writeValue(option?: string): void { + this.searchText = ''; + this.modelValue = option ?? null; + this.highlightedHtml = this.getHighlightHtml(this.modelValue); + this.selectionFormControl.patchValue(this.modelValue, {emitEvent: false}); + } + + onFocus() { + this.onSelectionChange(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectionFormControl.disable({emitEvent: false}); + this.closeAutocomplete(); + } else { + this.selectionFormControl.enable({emitEvent: false}); + } + } + + onInputScroll(event: Event) { + const scrollLeft = (event.target as HTMLInputElement).scrollLeft; + const scrollTop = (event.target as HTMLInputElement).scrollTop; + if (this.highlightTextRef && this.highlightTextRef.nativeElement) { + this.highlightTextRef.nativeElement.scrollLeft = scrollLeft; + this.highlightTextRef.nativeElement.scrollTop = scrollTop; + } + } + + optionSelected(value: string) { + const position = this.inputRef.nativeElement.selectionStart; + const triggerIndex = this.predefinedValuesButtonMode ? position : this.modelValue.lastIndexOf(this.patternSymbol, position - 1); + if (triggerIndex === -1) { + return; + } + let prepareValue: string; + switch (this.brackets) { + case 'curly': + prepareValue = `{${value}}`; + break; + case 'square': + prepareValue = `[${value}]`; + break; + default: + prepareValue = value; + } + const base = this.modelValue ?? ''; + const newText = `${base.substring(0, triggerIndex)}${this.patternSymbol}${prepareValue}${base.substring(position)}`; + this.selectionFormControl.patchValue(newText); + this.searchText = ''; + setTimeout(() => { + this.inputRef.nativeElement.setSelectionRange(triggerIndex + prepareValue.length + 1, triggerIndex + prepareValue.length + 1); + this.inputRef.nativeElement.focus(); + }); + this.closeAutocomplete(); + } + + openValues($event: MouseEvent) { + $event.stopPropagation(); + const selectionStart = this.inputRef.nativeElement.selectionStart; + this.filteredOptions = [...this.predefinedValues]; + this.predefinedValuesButtonMode = true; + this.openAutocomplete(selectionStart); + } + + private updateView(value: string) { + if (this.modelValue !== value) { + this.modelValue = value; + this.highlightedHtml = this.getHighlightHtml(this.modelValue); + this.propagateChange(this.modelValue); + } + } + + private onSelectionChange() { + const selectionStart = this.inputRef.nativeElement.selectionStart; + const selectionEnd = this.inputRef.nativeElement.selectionEnd; + if (selectionStart === selectionEnd && selectionStart > 0 && !this.disabled) { + this.filteredOptions = this.getFilteredOptions(this.modelValue, selectionStart); + this.activeOptionIndex = 0; + } else { + this.filteredOptions = []; + this.activeOptionIndex = -1; + } + if (this.filteredOptions.length) { + this.openAutocomplete(selectionStart); + } else { + this.closeAutocomplete() + } + } + + private getHighlightHtml(text: string): string { + if (!text) { + return ''; + } + let regex: RegExp; + let simpleGroup = false; + text = text.replace(//g, '>'); + switch (this.brackets) { + case 'curly': + regex = new RegExp(`([${this.patternSymbol}](\\{([^}]*)\\}))`, 'g'); + break; + case 'square': + regex = new RegExp(`([${this.patternSymbol}](\\[([^]]*)\\]))`, 'g'); + break; + default: + regex = new RegExp(`([${this.patternSymbol}](\\w+))`, 'g'); + simpleGroup = true + break + } + return text.replace(regex, (_match: string, p1: string, p2: string, p3: string) => { + const value = simpleGroup ? p2 : p3; + if (this.predefinedValues.includes(value)) { + return `${p1}` + } + return p1; + }); + } + + private getFilteredOptions(text: string, index = text.length): Array { + const triggerIndex = text.lastIndexOf(this.patternSymbol, index - 1); + if (triggerIndex === -1) { + return []; + } + + let currentWordEndIndex = text.indexOf(' ', index); + + if (currentWordEndIndex === -1) { + currentWordEndIndex = text.length; + } + + let startOffset = 1; + switch (this.brackets) { + case 'curly': + if (text[triggerIndex + startOffset] === '{') { + startOffset++; + } + break; + case 'square': + if (text[triggerIndex + startOffset] === '[') { + startOffset++; + } + break; + } + + this.searchText = text.substring(triggerIndex + startOffset, currentWordEndIndex).toLowerCase(); + if (this.searchText.includes(' ')) { + return []; + } + + const result = this.predefinedValues.filter(value => value.toLowerCase().startsWith(this.searchText)); + if (result.length === 1 && result[0].toLowerCase() === this.searchText) { + return []; + } + return result; + } + + private openAutocomplete(cursorIndex?: number): void { + const patternIndex = this.predefinedValuesButtonMode ? cursorIndex : this.modelValue.lastIndexOf(this.patternSymbol, cursorIndex - 1); + if (!this.overlayRef) { + this.overlayRef = this.overlay.create({ + positionStrategy: this.getOverlayPosition(patternIndex), + scrollStrategy: this.overlay.scrollStrategies.reposition(), + panelClass: 'tb-select-overlay', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true + }); + this.overlayRef.backdropClick().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.closeAutocomplete()); + } + if (!this.overlayRef.hasAttached()) { + const templatePortal = new TemplatePortal(this.autocompleteTemplate, this.viewContainerRef); + this.overlayRef.attach(templatePortal); + this.overlayRef.updatePositionStrategy(this.getOverlayPosition(patternIndex)) + } + } + + private closeAutocomplete(): void { + this.predefinedValuesButtonMode = false; + if (this.overlayRef && this.overlayRef.hasAttached()) { + this.overlayRef.detach(); + } + } + + private getOverlayPosition(patternIndex: number): PositionStrategy { + const strategy = this.overlay + .position() + .flexibleConnectedTo(this.inputRef) + .withFlexibleDimensions(false) + .withPush(false); + + this.setStrategyPositions(strategy, patternIndex); + return strategy; + } + + private setStrategyPositions(positionStrategy: FlexibleConnectedPositionStrategy, patternIndex: number) { + let offsetX = patternIndex > 0 ? patternIndex * 8 : 0; + const inputBounds = this.inputRef.nativeElement.getBoundingClientRect(); + if (offsetX + 180 > inputBounds.width) { + offsetX = inputBounds.width - 180; + } + const belowPositions: ConnectedPosition[] = [ + {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetX}, + {originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetX}, + ]; + + const panelClass = 'mat-mdc-autocomplete-panel-above'; + const abovePositions: ConnectedPosition[] = [ + {originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', panelClass, offsetX}, + {originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', panelClass, offsetX}, + ]; + + const positions = [...belowPositions, ...abovePositions]; + positionStrategy.withPositions(positions); + } + + private setActiveOption(index: number): void { + if (this.filteredOptions.length === 0) { + this.activeOptionIndex = -1; + return; + } + + this.activeOptionIndex = Math.max(-1, Math.min(index, this.filteredOptions.length - 1)); + this.cd.markForCheck(); + + if (this.activeOptionIndex >= 0 && this.optionItems.get(this.activeOptionIndex)) { + this.optionItems.get(this.activeOptionIndex).nativeElement.scrollIntoView({ block: 'nearest' }) + } + } + + private handleKeydown(event: KeyboardEvent): void { + if (!this.overlayRef?.hasAttached()) { + return; + } + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + this.setActiveOption(this.activeOptionIndex + 1); + break; + case 'ArrowUp': + event.preventDefault(); + this.setActiveOption(this.activeOptionIndex - 1); + break; + case 'Enter': + event.preventDefault(); + if (this.activeOptionIndex >= 0 && this.activeOptionIndex < this.filteredOptions.length) { + this.optionSelected(this.filteredOptions[this.activeOptionIndex]); + } + break; + case 'Escape': + this.closeAutocomplete(); + break; + } + } +} diff --git a/ui-ngx/src/app/shared/components/svg-xml.component.html b/ui-ngx/src/app/shared/components/svg-xml.component.html index 55ba188547..a720568c47 100644 --- a/ui-ngx/src/app/shared/components/svg-xml.component.html +++ b/ui-ngx/src/app/shared/components/svg-xml.component.html @@ -19,7 +19,9 @@ tb-fullscreen [fullscreen]="fullscreen">
- + @if (!noLabel) { + + }
-
- + + subscriptSizing="dynamic"> @if (labelText && !inlineField) { {{ labelText }} } - +
@if (inlineField) { - warning - } @else { - - + } + } + @if ((subscriptSizing === 'fixed') && !inlineField) { + {{ hintText }} + } + @if (hasError && !inlineField) { + {{ hasError }} }
- @if (!inlineField) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts b/ui-ngx/src/app/shared/components/time-unit-input.component.ts similarity index 52% rename from ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts rename to ui-ngx/src/app/shared/components/time-unit-input.component.ts index e863d957b6..93293698b3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -23,9 +23,10 @@ import { NG_VALUE_ACCESSOR, ValidationErrors, Validator, + ValidatorFn, Validators } from '@angular/forms'; -import { TimeUnit, timeUnitTranslations } from '../rule-node-config.models'; +import { TimeUnit, timeUnitTranslations } from '@home/components/rule-node/rule-node-config.models'; import { isDefinedAndNotNull, isNumeric } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion'; @@ -51,11 +52,14 @@ interface TimeUnitInputModel { }], standalone: false }) -export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit { +export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { @Input() labelText: string; + @Input() + hintText: string; + @Input() @coerceBoolean() required: boolean; @@ -77,6 +81,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() maxErrorText: string; + @Input() + @coerceNumber() + stepMultipleOf: number; + + @Input() + stepMultipleOfErrorText: string; + @Input() subscriptSizing: SubscriptSizing = 'fixed'; @@ -87,6 +98,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @coerceBoolean() inlineField: boolean; + @Input() + @coerceBoolean() + sameWidthInputs: boolean = false; + + @Input() + containerClass: string | string[] | Record = "flex gap-4"; + timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; timeUnitTranslations = timeUnitTranslations; @@ -103,6 +121,8 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, [TimeUnit.SECONDS, SECOND/SECOND], ]); + minValueValidator = 0; + private modelValue: number; private propagateChange: (value: any) => void = () => {}; @@ -112,39 +132,16 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } ngOnInit() { - if (this.maxTime) { - const maxTimeMs = this.maxTime * SECOND; - if (maxTimeMs < MINUTE) { - this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (maxTimeMs < HOUR) { - this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (maxTimeMs < DAY) { - this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS); - } + if (isDefinedAndNotNull(this.maxTime)) { + this.updatedAllowTimeUnitInterval(this.maxTime); } - if(this.required || this.maxTime) { - const timeControl = this.timeInputForm.get('time'); - const validators = [Validators.pattern(/^\d*$/)]; - if (this.required) { - validators.push(Validators.required); - } - if (this.maxTime) { - validators.push((control: AbstractControl) => - Validators.max(Math.floor(this.maxTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)))(control) - ); - } - if (isDefinedAndNotNull(this.minTime)) { - validators.push(Validators.min(this.minTime)); - } - timeControl.setValidators(validators); - timeControl.updateValueAndValidity({ emitEvent: false }); - } + this.refreshTimeValidators(); this.timeInputForm.get('timeUnit').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { - this.timeInputForm.get('time').updateValueAndValidity({onlySelf: true}); + this.refreshTimeValidators(); this.timeInputForm.get('time').markAsTouched({onlySelf: true}); }); @@ -155,6 +152,10 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, }); } + get minValue(): number { + return Math.ceil(this.minTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)); + } + get hasError(): string { if (this.timeInputForm.get('time').hasError('required') && this.requiredText) { return this.requiredText; @@ -162,6 +163,25 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, return this.minErrorText; } else if (this.timeInputForm.get('time').hasError('max') && this.maxErrorText) { return this.maxErrorText; + } else if (this.timeInputForm.get('time').hasError('stepMultipleOf') && this.stepMultipleOfErrorText) { + return this.stepMultipleOfErrorText; + } + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'maxTime') { + if (isDefinedAndNotNull(this.maxTime)) { + this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; + this.updatedAllowTimeUnitInterval(this.maxTime); + } + } + if (['minTime', 'maxTime', 'required', 'stepMultipleOf'].includes(propName)) { + this.refreshTimeValidators(); + } + } } } @@ -177,7 +197,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, this.timeInputForm.disable({emitEvent: false}); } else { this.timeInputForm.enable({emitEvent: false}); - if(this.timeInputForm.invalid) { + if(!this.timeInputForm.valid) { setTimeout(() => this.updatedModel(this.timeInputForm.value, true)) } } @@ -186,7 +206,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, writeValue(sec: number) { if (sec !== this.modelValue) { if (isDefinedAndNotNull(sec) && isNumeric(sec) && Number(sec) !== 0) { - this.timeInputForm.patchValue(this.parseTime(sec), {emitEvent: false}); + this.timeInputForm.patchValue(this.parseTime(sec), {emitEvent: true}); this.modelValue = sec; } else { this.timeInputForm.patchValue({ @@ -199,7 +219,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } validate(): ValidationErrors | null { - return this.timeInputForm.valid ? null : { + return this.timeInputForm.disabled || this.timeInputForm.valid ? null : { timeInput: false }; } @@ -224,4 +244,80 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } } + private createStepMultipleOfValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const time = control.value; + if (!isDefinedAndNotNull(time) || !isNumeric(time)) { + return null; + } + const numericTime = Number(time); + if (numericTime === 0) { + return null; + } + + const timeUnit = control.parent?.get('timeUnit')?.value as TimeUnit; + if (!timeUnit) { + return null; + } + + const unitInSec = this.timeIntervalsInSec.get(timeUnit); + const totalTimeInSec = numericTime * unitInSec; + const multipleOfVal = this.stepMultipleOf; + + let isValid: boolean; + if (totalTimeInSec < multipleOfVal) { + isValid = (multipleOfVal % totalTimeInSec === 0); + } else { + isValid = (totalTimeInSec % multipleOfVal === 0); + } + return isValid ? null : { stepMultipleOf: true }; + }; + } + + private updatedAllowTimeUnitInterval(maxTime: number) { + const maxTimeMs = maxTime * SECOND; + this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; + if (maxTimeMs < MINUTE) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); + } else if (maxTimeMs < HOUR) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); + } else if (maxTimeMs < DAY) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS); + } + } + + private refreshTimeValidators() { + const timeControl = this.timeInputForm.get('time'); + if (!timeControl) return; + + const currentUnit = this.timeInputForm.get('timeUnit')?.value as TimeUnit; + if (!currentUnit) return; + + const secondsInUnit = this.timeIntervalsInSec.get(currentUnit) ?? 1; + const validators: ValidatorFn[] = [Validators.pattern(/^\d*$/)]; + + if (this.required) { + validators.push(Validators.required); + } + + if (isDefinedAndNotNull(this.minTime)) { + this.minValueValidator = Math.ceil(this.minTime / secondsInUnit); + validators.push(Validators.min(this.minValueValidator)); + } else { + this.minValueValidator = 0; + validators.push(Validators.min(this.minValueValidator)); + } + + if (isDefinedAndNotNull(this.maxTime)) { + const maxInCurrentUnit = Math.floor(this.maxTime / secondsInUnit); + validators.push(Validators.max(maxInCurrentUnit)); + } + + if (isDefinedAndNotNull(this.stepMultipleOf) && this.stepMultipleOf > 0) { + validators.push(this.createStepMultipleOfValidator()); + } + + timeControl.setValidators(validators); + timeControl.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + } } diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html index 8738fd2506..3341556e75 100644 --- a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-options-config-panel.component.html @@ -24,9 +24,11 @@
- - {{ aggregationTypesTranslations.get(aggregationTypes[type]) | translate }} - + @for (type of allAggregationTypes; track type) { + + {{ aggregationTypesTranslations.get(aggregationTypes[type]) | translate }} + + }
diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html index a63ee7540b..bd2a4aee7d 100644 --- a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html +++ b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html @@ -18,11 +18,15 @@ - {{ label }} + @if (displayLabel) { + {{ label }} + } - - {{ displayAggregationTypeFn(type) }} - + @for (type of aggregationTypes; track type) { + + {{ displayAggregationTypeFn(type) }} + + } diff --git a/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html index 75e53e4404..f3c474d2a4 100644 --- a/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html +++ b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html @@ -20,12 +20,16 @@ [subscriptSizing]="subscriptSizing" [appearance]="appearance"> - - {{ 'timewindow.all' | translate }} - - - {{ interval.name | translate:interval.translateParams }} - + @if (timeintervalFormGroup.get('aggIntervals').value?.length === allIntervals.length) { + + {{ 'timewindow.all' | translate }} + + } + @for (interval of allIntervals; track interval) { + + {{ interval.name | translate:interval.translateParams }} + + } -- - - {{ interval.name | translate:interval.translateParams }} - + @for (interval of selectedIntervals; track interval) { + + {{ interval.name | translate:interval.translateParams }} + + } diff --git a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss index be3170428c..37b0e5b46e 100644 --- a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss +++ b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss @@ -29,6 +29,9 @@ .mdc-slider { height: 40px; + .mdc-slider__input { + top: 0; + } ::ng-deep { .mdc-slider__thumb { left: -20px; diff --git a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts index ae1e773b54..7594e1f4a3 100644 --- a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts +++ b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts @@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { TimeService } from '@core/services/time.service'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { isDefined } from '@core/utils'; @Component({ selector: 'tb-datapoints-limit', @@ -70,7 +71,11 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator @Input() disabled: boolean; - private propagateChange = (v: any) => { }; + private propagateChangeValue: any; + + private propagateChange = (v: any) => { + this.propagateChangeValue = v; + }; private destroy$ = new Subject(); @@ -80,6 +85,9 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator registerOnChange(fn: any): void { this.propagateChange = fn; + if (isDefined(this.propagateChangeValue)) { + this.propagateChange(this.propagateChangeValue); + } } registerOnTouched(fn: any): void { @@ -116,19 +124,20 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator } } - private checkLimit(limit?: number): number { - if (!limit || limit < this.minDatapointsLimit()) { - return this.minDatapointsLimit(); + writeValue(value: number | null): void { + this.modelValue = value; + let limit = this.modelValue; + if (!limit) { + limit = Math.ceil(this.maxDatapointsLimit() / 2); + } else if (limit < this.minDatapointsLimit()) { + limit = this.minDatapointsLimit(); } else if (limit > this.maxDatapointsLimit()) { - return this.maxDatapointsLimit(); + limit = this.maxDatapointsLimit(); } - return limit; - } - writeValue(value: number | null): void { - this.modelValue = this.checkLimit(value); + this.updateView(limit); this.datapointsLimitFormGroup.patchValue( - { limit: this.modelValue }, {emitEvent: false} + { limit: limit }, {emitEvent: false} ); } diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.html b/ui-ngx/src/app/shared/components/time/datetime.component.html index 8ef3ee3c21..25861aa4d4 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.html +++ b/ui-ngx/src/app/shared/components/time/datetime.component.html @@ -21,14 +21,18 @@ [class.tb-time-field]="!fieldClass && type === 'time'" [class.with-clear]="allowClear" [subscriptSizing]="subscriptSizing" [appearance]="appearance" [floatLabel]="floatLabel"> - {{ dateText }} + @if (showLabel) { + {{ dateText }} + }
- + @if (allowClear && date && !disabled) { + + }
diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.ts b/ui-ngx/src/app/shared/components/time/datetime.component.ts index 6d89185702..8306b14872 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.ts +++ b/ui-ngx/src/app/shared/components/time/datetime.component.ts @@ -40,7 +40,7 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor { return this.requiredValue; } @Input() - set required(value: boolean) { + set required(value: unknown) { this.requiredValue = coerceBooleanProperty(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 index c7c983c371..152f4ca083 100644 --- 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 @@ -16,39 +16,49 @@ -->
-
- - -
- - -
- {{ currentTime | date:'medium'}} - {{ "widget.no-data-found" | translate}} -
-
- - - - - - {{speedValue}} - +
+ + +
+ + +
+ @if (currentTime) { + {{ currentTime | date:'medium'}} + } + @if (!currentTime) { + {{ "widget.no-data-found" | translate}} + } +
+ + + @if (!playing) { + + } + @if (playing) { + + } + + @for (speedValue of speeds; track speedValue) { + {{speedValue}} + } + +
diff --git a/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html index 8953db42b1..646c63df94 100644 --- a/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html @@ -32,20 +32,20 @@ {{ "timewindow.interval" | translate }}
- + @if (aggregation) {
{{"timewindow.allowed-agg-intervals" | translate }}
{{"timewindow.default-agg-interval" | translate }}
-
+ }
- + @for (interval of intervalsFormArray.controls; track trackByElement(i, interval); let i = $index) {
{{ interval.get('name').value }}
- + @if (aggregation) {
-
+ }
-
+ }
diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html index 66bbb01b14..0b95467753 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html @@ -17,11 +17,15 @@ -->
- timewindow.interval + @if (displayLabel) { + timewindow.interval + } - - {{ timeIntervalTranslationMap.get(interval) | translate}} - + @for (interval of intervals; track interval) { + + {{ timeIntervalTranslationMap.get(interval) | translate}} + + } diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.html b/ui-ngx/src/app/shared/components/time/timeinterval.component.html index cc9793155c..b0955afcc3 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.html +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.html @@ -19,11 +19,15 @@
- {{ predefinedName }} + @if (predefinedName) { + {{ predefinedName }} + } - - {{ interval.name | translate:interval.translateParams }} - + @for (interval of intervals; track interval) { + + {{ interval.name | translate:interval.translateParams }} + + } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html index 446d55d7b2..272fa6239c 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html @@ -19,7 +19,7 @@

{{ 'timewindow.timewindow-settings' | translate }}

-
+
-
-
{{ 'timewindow.timewindow' | translate }}
-
- - {{ 'timewindow.hide-timewindow-section' | translate }} - -
- - - - - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) { +
+
{{ 'timewindow.timewindow' | translate }}
- - {{ 'timewindow.disable-custom-interval' | translate }} + + {{ 'timewindow.hide-timewindow-section' | translate }}
-
- -
- {{ 'timewindow.hide' | translate }} -
-
- + + } + @if (timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL) { +
+ + {{ 'timewindow.disable-custom-interval' | translate }} + +
+
+ @if (!quickIntervalOnly) { + +
+ {{ 'timewindow.hide' | translate }} +
+
+ } + - - -
- - -
- -
- {{ 'timewindow.hide' | translate }} + @if (!(timewindowForm.get('realtime.hideLastInterval').value || + timewindowForm.get('realtime.hideInterval').value)) { + + } +
-
- + @if (!quickIntervalOnly) { + +
+ {{ 'timewindow.hide' | translate }} +
+
+ } + - - -
-
- -
-
{{ 'timewindow.timewindow' | translate }}
-
- - {{ 'timewindow.hide-timewindow-section' | translate }} - -
- - - + edit + + } + +
+ } + + } - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY) { +
+
{{ 'timewindow.timewindow' | translate }}
- - {{ 'timewindow.disable-custom-interval' | translate }} + + {{ 'timewindow.hide-timewindow-section' | translate }}
-
- -
- {{ 'timewindow.hide' | translate }} -
-
- + + @if (timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL) { +
+ + {{ 'timewindow.disable-custom-interval' | translate }} + +
+
+ +
+ {{ 'timewindow.hide' | translate }} +
+
+ - - -
- - -
- -
- {{ 'timewindow.hide' | translate }} + @if (!(timewindowForm.get('history.hideLastInterval').value || + timewindowForm.get('history.hideInterval').value)) { + + } +
-
- + +
+ {{ 'timewindow.hide' | translate }} +
+
+ - -
- -
- -
- {{ 'timewindow.hide' | translate }} -
-
- +
+ } + @if (timewindowForm.get('history.historyType').value === historyTypes.INTERVAL) { +
+ +
+ {{ 'timewindow.hide' | translate }} +
+
+ - - -
-
+ @if (!(timewindowForm.get('history.hideQuickInterval').value || + timewindowForm.get('history.hideInterval').value)) { + + } + +
+ } + + } - + @if (aggregation) {
{{ 'aggregation.aggregation' | translate }}
@@ -198,37 +215,38 @@ - + @if (!timewindowForm.get('hideAggregation').value) { + + }
- -
-
{{ 'aggregation.limit' | translate }}
-
- -
- {{ 'timewindow.hide' | translate }} -
-
- - - - -
-
- + @if (timewindowForm.get('aggregation.type').value === aggregationTypes.NONE) { +
+
{{ 'aggregation.limit' | translate }}
+
+ +
+ {{ 'timewindow.hide' | translate }} +
+
+ + + + +
+
+ }
{{ 'aggregation.group-interval' | translate }}
- - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) {
{{ 'timewindow.disable-custom-interval' | translate }} @@ -250,8 +268,8 @@
-
- + } + @if (timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY) {
{{ 'timewindow.disable-custom-interval' | translate }} @@ -273,8 +291,7 @@
-
- + }
@@ -283,7 +300,7 @@
-
+ }
{{ 'timezone.timezone' | translate }}
@@ -301,6 +318,16 @@
+ @if (showSaveAsDefault) { +
+
timewindow.save-current-settings-as-default
+
+ + {{ 'timewindow.hide-option-from-end-users' | translate }} + +
+
+ }
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 87f27bbe4d..8d6116bef7 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -36,7 +36,15 @@ 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'; -import { deepClone, isDefined, isDefinedAndNotNull, isObject, mergeDeep } from '@core/utils'; +import { + deepClean, + deepClone, + deleteFalseProperties, + isDefined, + isDefinedAndNotNull, + isEmpty, + mergeDeepIgnoreArray +} from '@core/utils'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -54,6 +62,7 @@ import { export interface TimewindowConfigDialogData { quickIntervalOnly: boolean; aggregation: boolean; + showSaveAsDefault: boolean; timewindow: Timewindow; } @@ -69,6 +78,8 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On aggregation = false; + showSaveAsDefault = false; + timewindowForm: FormGroup; historyTypes = HistoryWindowType; @@ -133,6 +144,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On super(store); this.quickIntervalOnly = data.quickIntervalOnly; this.aggregation = data.aggregation; + this.showSaveAsDefault = data.showSaveAsDefault; this.timewindow = data.timewindow; if (!this.quickIntervalOnly) { @@ -153,73 +165,75 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On this.timewindowForm = this.fb.group({ selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME], realtime: this.fb.group({ - realtimeType: [ isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ], - timewindowMs: [ isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null ], - interval: [ isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null ], - quickInterval: [ isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null ], - disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomInterval) - ? this.timewindow.realtime?.disableCustomInterval : false ], - disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomGroupInterval) - ? this.timewindow.realtime?.disableCustomGroupInterval : false ], - hideInterval: [ isDefinedAndNotNull(this.timewindow.realtime.hideInterval) - ? this.timewindow.realtime.hideInterval : false ], + realtimeType: [ this.quickIntervalOnly + ? RealtimeWindowType.INTERVAL + : (isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL) ], + timewindowMs: [ isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null ], + interval: [ isDefined(realtime?.interval) ? realtime.interval : null ], + quickInterval: [ isDefined(realtime?.quickInterval) ? realtime.quickInterval : null ], + disableCustomInterval: [ isDefinedAndNotNull(realtime?.disableCustomInterval) + ? realtime.disableCustomInterval : false ], + disableCustomGroupInterval: [ isDefinedAndNotNull(realtime?.disableCustomGroupInterval) + ? realtime.disableCustomGroupInterval : false ], + hideInterval: [ isDefinedAndNotNull(realtime?.hideInterval) + ? realtime.hideInterval : false ], hideLastInterval: [{ - value: isDefinedAndNotNull(this.timewindow.realtime.hideLastInterval) - ? this.timewindow.realtime.hideLastInterval : false, - disabled: this.timewindow.realtime.hideInterval + value: isDefinedAndNotNull(realtime?.hideLastInterval) + ? realtime.hideLastInterval : false, + disabled: realtime?.hideInterval }], hideQuickInterval: [{ - value: isDefinedAndNotNull(this.timewindow.realtime.hideQuickInterval) - ? this.timewindow.realtime.hideQuickInterval : false, - disabled: this.timewindow.realtime.hideInterval + value: isDefinedAndNotNull(realtime?.hideQuickInterval) + ? realtime.hideQuickInterval : false, + disabled: realtime?.hideInterval }], advancedParams: this.fb.group({ - allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedLastIntervals) - ? this.timewindow.realtime.advancedParams.allowedLastIntervals : null ], - allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedQuickIntervals) - ? this.timewindow.realtime.advancedParams.allowedQuickIntervals : null ], - lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.lastAggIntervalsConfig) - ? this.timewindow.realtime.advancedParams.lastAggIntervalsConfig : null ], - quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.quickAggIntervalsConfig) - ? this.timewindow.realtime.advancedParams.quickAggIntervalsConfig : null ] + allowedLastIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedLastIntervals) + ? realtime.advancedParams.allowedLastIntervals : null ], + allowedQuickIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedQuickIntervals) + ? realtime.advancedParams.allowedQuickIntervals : null ], + lastAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.lastAggIntervalsConfig) + ? realtime.advancedParams.lastAggIntervalsConfig : null ], + quickAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.quickAggIntervalsConfig) + ? realtime.advancedParams.quickAggIntervalsConfig : null ] }) }), history: this.fb.group({ - historyType: [ isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL ], - timewindowMs: [ isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null ], - interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null ], - fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null ], - quickInterval: [ isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null ], - disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomInterval) - ? this.timewindow.history?.disableCustomInterval : false ], - disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomGroupInterval) - ? this.timewindow.history?.disableCustomGroupInterval : false ], - hideInterval: [ isDefinedAndNotNull(this.timewindow.history.hideInterval) - ? this.timewindow.history.hideInterval : false ], + historyType: [ isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL ], + timewindowMs: [ isDefined(history?.timewindowMs) ? history.timewindowMs : null ], + interval: [ isDefined(history?.interval) ? history.interval : null ], + fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? history.fixedTimewindow : null ], + quickInterval: [ isDefined(history?.quickInterval) ? history.quickInterval : null ], + disableCustomInterval: [ isDefinedAndNotNull(history?.disableCustomInterval) + ? history.disableCustomInterval : false ], + disableCustomGroupInterval: [ isDefinedAndNotNull(history?.disableCustomGroupInterval) + ? history.disableCustomGroupInterval : false ], + hideInterval: [ isDefinedAndNotNull(history?.hideInterval) + ? history.hideInterval : false ], hideLastInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideLastInterval) - ? this.timewindow.history.hideLastInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideLastInterval) + ? history.hideLastInterval : false, + disabled: history?.hideInterval }], hideQuickInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideQuickInterval) - ? this.timewindow.history.hideQuickInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideQuickInterval) + ? history.hideQuickInterval : false, + disabled: history?.hideInterval }], hideFixedInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideFixedInterval) - ? this.timewindow.history.hideFixedInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideFixedInterval) + ? history.hideFixedInterval : false, + disabled: history?.hideInterval }], advancedParams: this.fb.group({ - allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedLastIntervals) - ? this.timewindow.history.advancedParams.allowedLastIntervals : null ], - allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedQuickIntervals) - ? this.timewindow.history.advancedParams.allowedQuickIntervals : null ], - lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.lastAggIntervalsConfig) - ? this.timewindow.history.advancedParams.lastAggIntervalsConfig : null ], - quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.quickAggIntervalsConfig) - ? this.timewindow.history.advancedParams.quickAggIntervalsConfig : null ] + allowedLastIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedLastIntervals) + ? history.advancedParams.allowedLastIntervals : null ], + allowedQuickIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedQuickIntervals) + ? history.advancedParams.allowedQuickIntervals : null ], + lastAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.lastAggIntervalsConfig) + ? history.advancedParams.lastAggIntervalsConfig : null ], + quickAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.quickAggIntervalsConfig) + ? history.advancedParams.quickAggIntervalsConfig : null ] }) }), aggregation: this.fb.group({ @@ -234,7 +248,9 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On hideAggInterval: [ isDefinedAndNotNull(this.timewindow.hideAggInterval) ? this.timewindow.hideAggInterval : false ], hideTimezone: [ isDefinedAndNotNull(this.timewindow.hideTimezone) - ? this.timewindow.hideTimezone : false ] + ? this.timewindow.hideTimezone : false ], + hideSaveAsDefault: [ isDefinedAndNotNull(this.timewindow.hideSaveAsDefault) + ? this.timewindow.hideSaveAsDefault : false ], }); this.updateValidators(this.timewindowForm.get('aggregation.type').value); @@ -416,16 +432,11 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On realtimeDisableCustomInterval, historyDisableCustomInterval, timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams, this.realtimeTimewindowOptions, this.historyTimewindowOptions); - this.timewindowForm.patchValue({ - hideAggregation: timewindowFormValue.hideAggregation, - hideAggInterval: timewindowFormValue.hideAggInterval, - hideTimezone: timewindowFormValue.hideTimezone - }); } update() { const timewindowFormValue = this.timewindowForm.getRawValue(); - this.timewindow = mergeDeep(this.timewindow, timewindowFormValue); + this.timewindow = mergeDeepIgnoreArray(this.timewindow, timewindowFormValue); const realtimeConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval && (timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideLastInterval)); @@ -436,82 +447,50 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On const historyConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval && (timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideQuickInterval)); - if (realtimeConfigurableLastIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedLastIntervals?.length) { - this.timewindow.realtime.advancedParams.allowedLastIntervals = timewindowFormValue.realtime.advancedParams.allowedLastIntervals; - } else { + if (!realtimeConfigurableLastIntervalsAvailable) { delete this.timewindow.realtime.advancedParams.allowedLastIntervals; } - if (realtimeConfigurableQuickIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedQuickIntervals?.length) { - this.timewindow.realtime.advancedParams.allowedQuickIntervals = timewindowFormValue.realtime.advancedParams.allowedQuickIntervals; - } else { + if (!realtimeConfigurableQuickIntervalsAvailable) { delete this.timewindow.realtime.advancedParams.allowedQuickIntervals; } - if (realtimeConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig) && - Object.keys(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig).length) { + if (realtimeConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig)) { this.timewindow.realtime.advancedParams.lastAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig; } else { delete this.timewindow.realtime.advancedParams.lastAggIntervalsConfig; } - if (realtimeConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig) && - Object.keys(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig).length) { + if (realtimeConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig)) { this.timewindow.realtime.advancedParams.quickAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig; } else { delete this.timewindow.realtime.advancedParams.quickAggIntervalsConfig; } - if (historyConfigurableLastIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedLastIntervals?.length) { - this.timewindow.history.advancedParams.allowedLastIntervals = timewindowFormValue.history.advancedParams.allowedLastIntervals; - } else { + if (!historyConfigurableLastIntervalsAvailable) { delete this.timewindow.history.advancedParams.allowedLastIntervals; } - if (historyConfigurableQuickIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedQuickIntervals?.length) { - this.timewindow.history.advancedParams.allowedQuickIntervals = timewindowFormValue.history.advancedParams.allowedQuickIntervals; - } else { + if (!historyConfigurableQuickIntervalsAvailable) { delete this.timewindow.history.advancedParams.allowedQuickIntervals; } - if (historyConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig) && - Object.keys(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig).length) { + if (historyConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig)) { this.timewindow.history.advancedParams.lastAggIntervalsConfig = timewindowFormValue.history.advancedParams.lastAggIntervalsConfig; } else { delete this.timewindow.history.advancedParams.lastAggIntervalsConfig; } - if (historyConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig) && - Object.keys(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig).length) { + if (historyConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig)) { this.timewindow.history.advancedParams.quickAggIntervalsConfig = timewindowFormValue.history.advancedParams.quickAggIntervalsConfig; } else { delete this.timewindow.history.advancedParams.quickAggIntervalsConfig; } - if (!Object.keys(this.timewindow.realtime.advancedParams).length) { - delete this.timewindow.realtime.advancedParams; - } - if (!Object.keys(this.timewindow.history.advancedParams).length) { - delete this.timewindow.history.advancedParams; - } - - if (timewindowFormValue.allowedAggTypes?.length && !timewindowFormValue.hideAggregation) { - this.timewindow.allowedAggTypes = timewindowFormValue.allowedAggTypes; - } else { + if (timewindowFormValue.hideAggregation) { delete this.timewindow.allowedAggTypes; } - if (!this.timewindow.realtime.disableCustomInterval) { - delete this.timewindow.realtime.disableCustomInterval; - } - if (!this.timewindow.realtime.disableCustomGroupInterval) { - delete this.timewindow.realtime.disableCustomGroupInterval; - } - if (!this.timewindow.history.disableCustomInterval) { - delete this.timewindow.history.disableCustomInterval; - } - if (!this.timewindow.history.disableCustomGroupInterval) { - delete this.timewindow.history.disableCustomGroupInterval; - } - if (!this.aggregation) { delete this.timewindow.aggregation; } - this.dialogRef.close(this.timewindow); + + deleteFalseProperties(this.timewindow); + this.dialogRef.close(deepClean(this.timewindow)); } cancel() { 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 index 031846299f..322da81efb 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -16,193 +16,228 @@ -->
-
- - - -
+ @if (!historyOnly) { +
+ + + @if (isEdit) { + + } +
+ }
- -
-
-
-
{{ 'timewindow.timewindow' | translate }}
- - - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) { + @if (realtimeIntervalSelectionAvailable) { +
+
+
+
{{ 'timewindow.timewindow' | translate }}
+ @if (realtimeTypeSelectionAvailable) { + + + + + } +
+
- - - -
- -
- + @if (timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL) { + - - - + } + @if (timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL) { + - -
-
-
- - -
-
-
-
{{ 'timewindow.timewindow' | translate }}
- - - - + + }
- +
+ } @else { +
+
{{ 'timezone.timezone' | translate }}
-
- -
- + } + } + + @if (timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY) { + @if (historyIntervalSelectionAvailable) { +
+
+
+
{{ 'timewindow.timewindow' | translate }}
+ @if (historyTypeSelectionAvailable) { + + + + + } +
+ + +
+ @if (historyIntervalSelectionAvailable && + timewindowForm.get('history.historyType').value !== historyTypes.FOR_ALL_TIME) { +
+ @if (timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL) { + - - - + } + @if (timewindowForm.get('history.historyType').value === historyTypes.FIXED) { + - - - + } + @if (timewindowForm.get('history.historyType').value === historyTypes.INTERVAL) { + - -
-
- - - - -
-
{{ 'aggregation.aggregation' | translate }}
- - + + } +
+ } - -
-
{{ 'aggregation.limit' | translate }}
- - + } @else { +
+
{{ 'timezone.timezone' | translate }}
+ +
- + } + } -
- - -
{{ 'aggregation.group-interval' | translate }}
-
-
- - -
{{ 'aggregation.group-interval' | translate }}
-
-
-
- + @if (aggregationOptionsAvailable) { + + @if (isEdit || !timewindow.hideAggregation) { +
+
{{ 'aggregation.aggregation' | translate }}
+ + +
+ } + @if (timewindowForm.get('aggregation.type').value === aggregationTypes.NONE && (isEdit || !timewindow.hideAggInterval)) { +
+
{{ 'aggregation.limit' | translate }}
+ + +
+ } +
+ @if (timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE) { +
+ @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) { + + +
{{ 'aggregation.group-interval' | translate }}
+
+
+ } + @if (timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY) { + + +
{{ 'aggregation.group-interval' | translate }}
+
+
+ } +
+ } + } + @if (saveAsDefaultAvailable) { + + {{ 'timewindow.save-current-settings-as-default' | translate }} + + }
- -
-
{{ 'timezone.timezone' | translate }}
- - -
-
- - + @if (timezone && (isEdit || !timewindow.hideTimezone)) { + + + } - -
- - -
+ @if (panelMode) { + + } + @if (panelMode) { +
+ + +
+ } 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 index 75bc80bd8f..b278b59a85 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss @@ -39,5 +39,9 @@ &-settings-btn { color: rgba(0, 0, 0, 0.54); } + + &-checkbox { + --mat-checkbox-state-layer-size: 24px; + } } } 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 index 61ac7c5e7e..32f4c46596 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -27,12 +27,14 @@ import { } from '@angular/core'; import { AggregationType, + clearTimewindowConfig, currentHistoryTimewindow, currentRealtimeTimewindow, historyAllowedAggIntervals, HistoryWindowType, historyWindowTypeTranslations, Interval, + MINUTE, QuickTimeInterval, realtimeAllowedAggIntervals, RealtimeWindowType, @@ -45,7 +47,7 @@ import { import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { FormControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { TimeService } from '@core/services/time.service'; import { deepClone, isDefined } from '@core/utils'; import { OverlayRef } from '@angular/cdk/overlay'; @@ -68,6 +70,7 @@ export interface TimewindowPanelData { timezone: boolean; isEdit: boolean; panelMode: boolean; + showSaveAsDefault?: boolean; } export const TIMEWINDOW_PANEL_DATA = new InjectionToken('TimewindowPanelData'); @@ -110,6 +113,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O aggregationTypes = AggregationType; result: Timewindow; + saveTimewindow: boolean; + saveTimewindowControl: FormControl; timewindowTypeOptions: ToggleHeaderOption[] = [{ name: this.translate.instant('timewindow.history'), @@ -125,6 +130,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O historyTypeSelectionAvailable: boolean; historyIntervalSelectionAvailable: boolean; aggregationOptionsAvailable: boolean; + saveAsDefaultAvailable: boolean; realtimeDisableCustomInterval: boolean; realtimeDisableCustomGroupInterval: boolean; @@ -168,14 +174,14 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O }); } - if ((this.isEdit || !this.timewindow.realtime.hideLastInterval) && !this.quickIntervalOnly) { + if ((this.isEdit || !this.timewindow.realtime?.hideLastInterval) && !this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.LAST_INTERVAL)), value: this.realtimeTypes.LAST_INTERVAL }); } - if (this.isEdit || !this.timewindow.realtime.hideQuickInterval || this.quickIntervalOnly) { + if (this.isEdit || !this.timewindow.realtime?.hideQuickInterval || this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.INTERVAL)), value: this.realtimeTypes.INTERVAL @@ -189,21 +195,21 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O }); } - if (this.isEdit || !this.timewindow.history.hideLastInterval) { + if (this.isEdit || !this.timewindow.history?.hideLastInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.LAST_INTERVAL)), value: this.historyTypes.LAST_INTERVAL }); } - if (this.isEdit || !this.timewindow.history.hideFixedInterval) { + if (this.isEdit || !this.timewindow.history?.hideFixedInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FIXED)), value: this.historyTypes.FIXED }); } - if (this.isEdit || !this.timewindow.history.hideQuickInterval) { + if (this.isEdit || !this.timewindow.history?.hideQuickInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.INTERVAL)), value: this.historyTypes.INTERVAL @@ -212,13 +218,15 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.realtimeTypeSelectionAvailable = this.realtimeTimewindowOptions.length > 1; this.historyTypeSelectionAvailable = this.historyTimewindowOptions.length > 1; - this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime.hideInterval || - (this.timewindow.realtime.hideLastInterval && this.timewindow.realtime.hideQuickInterval)); - this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history.hideInterval || - (this.timewindow.history.hideLastInterval && this.timewindow.history.hideQuickInterval && this.timewindow.history.hideFixedInterval)); + this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime?.hideInterval || + (this.timewindow.realtime?.hideLastInterval && this.timewindow.realtime?.hideQuickInterval)); + this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history?.hideInterval || + (this.timewindow.history?.hideLastInterval && this.timewindow.history?.hideQuickInterval && this.timewindow.history?.hideFixedInterval)); this.aggregationOptionsAvailable = this.aggregation && (this.isEdit || !(this.timewindow.hideAggregation && this.timewindow.hideAggInterval)); + + this.saveAsDefaultAvailable = this.data.showSaveAsDefault && (!this.timewindow.hideSaveAsDefault || this.isEdit); } ngOnInit(): void { @@ -231,28 +239,28 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O const aggregation = this.timewindow.aggregation; if (!this.isEdit) { - if (realtime.hideLastInterval && !realtime.hideQuickInterval) { + if (realtime?.hideLastInterval && !realtime?.hideQuickInterval) { realtime.realtimeType = RealtimeWindowType.INTERVAL; } - if (realtime.hideQuickInterval && !realtime.hideLastInterval) { + if (realtime?.hideQuickInterval && !realtime?.hideLastInterval) { realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL; } - if (history.hideLastInterval) { + if (history?.hideLastInterval) { if (!history.hideFixedInterval) { history.historyType = HistoryWindowType.FIXED; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } - if (history.hideFixedInterval) { + if (history?.hideFixedInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } - if (history.hideQuickInterval) { + if (history?.hideQuickInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideFixedInterval) { @@ -261,34 +269,40 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } } + if (this.saveAsDefaultAvailable) { + this.saveTimewindowControl = this.fb.control({value: this.isEdit, disabled: this.isEdit}); + } + this.timewindowForm = this.fb.group({ selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME], realtime: this.fb.group({ realtimeType: [{ - value: isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, - disabled: realtime.hideInterval + value: this.quickIntervalOnly + ? RealtimeWindowType.INTERVAL + : (isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL), + disabled: realtime?.hideInterval }], timewindowMs: [{ - value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null, - disabled: realtime.hideInterval || realtime.hideLastInterval + value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : MINUTE, + disabled: realtime?.hideInterval || realtime?.hideLastInterval }], interval: [{ value:isDefined(realtime?.interval) ? realtime.interval : null, disabled: hideAggInterval }], quickInterval: [{ - value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : null, - disabled: realtime.hideInterval || realtime.hideQuickInterval + value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : QuickTimeInterval.CURRENT_DAY, + disabled: realtime?.hideInterval || realtime?.hideQuickInterval }] }), history: this.fb.group({ historyType: [{ value: isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL, - disabled: history.hideInterval + disabled: history?.hideInterval }], timewindowMs: [{ - value: isDefined(history?.timewindowMs) ? history.timewindowMs : null, - disabled: history.hideInterval || history.hideLastInterval + value: isDefined(history?.timewindowMs) ? history.timewindowMs : MINUTE, + disabled: history?.hideInterval || history?.hideLastInterval }], interval: [{ value:isDefined(history?.interval) ? history.interval : null, @@ -297,11 +311,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O fixedTimewindow: [{ value: isDefined(history?.fixedTimewindow) && this.timewindow.selectedTab === TimewindowType.HISTORY && history.historyType === HistoryWindowType.FIXED ? history.fixedTimewindow : null, - disabled: history.hideInterval || history.hideFixedInterval + disabled: history?.hideInterval || history?.hideFixedInterval }], quickInterval: [{ - value: isDefined(history?.quickInterval) ? history.quickInterval : null, - disabled: history.hideInterval || history.hideQuickInterval + value: isDefined(history?.quickInterval) ? history.quickInterval : QuickTimeInterval.CURRENT_DAY, + disabled: history?.hideInterval || history?.hideQuickInterval }] }), aggregation: this.fb.group({ @@ -380,8 +394,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.timewindowForm.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { - this.prepareTimewindowConfig(); - this.changeTimewindow.emit(this.timewindow); + this.changeTimewindow.emit(this.prepareTimewindowConfig()); }); } } @@ -408,27 +421,30 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } update() { - this.prepareTimewindowConfig(); - this.result = this.timewindow; + this.result = this.prepareTimewindowConfig(); + this.saveTimewindow = this.saveAsDefaultAvailable && this.saveTimewindowControl.enabled && this.saveTimewindowControl.value; this.overlayRef?.dispose(); } - private prepareTimewindowConfig() { + private prepareTimewindowConfig(clearConfig = true): Timewindow { const timewindowFormValue = this.timewindowForm.getRawValue(); this.timewindow.selectedTab = timewindowFormValue.selectedTab; - this.timewindow.realtime = {...this.timewindow.realtime, ...{ - realtimeType: timewindowFormValue.realtime.realtimeType, - timewindowMs: timewindowFormValue.realtime.timewindowMs, - quickInterval: timewindowFormValue.realtime.quickInterval, - interval: timewindowFormValue.realtime.interval - }}; - this.timewindow.history = {...this.timewindow.history, ...{ - historyType: timewindowFormValue.history.historyType, - timewindowMs: timewindowFormValue.history.timewindowMs, - interval: timewindowFormValue.history.interval, - fixedTimewindow: timewindowFormValue.history.fixedTimewindow, - quickInterval: timewindowFormValue.history.quickInterval, - }}; + if (this.timewindow.selectedTab === TimewindowType.REALTIME) { + this.timewindow.realtime = {...this.timewindow.realtime, ...{ + realtimeType: timewindowFormValue.realtime.realtimeType, + timewindowMs: timewindowFormValue.realtime.timewindowMs, + quickInterval: timewindowFormValue.realtime.quickInterval, + interval: timewindowFormValue.realtime.interval, + }}; + } else { + this.timewindow.history = {...this.timewindow.history, ...{ + historyType: timewindowFormValue.history.historyType, + timewindowMs: timewindowFormValue.history.timewindowMs, + fixedTimewindow: timewindowFormValue.history.fixedTimewindow, + quickInterval: timewindowFormValue.history.quickInterval, + interval: timewindowFormValue.history.interval, + }}; + } if (this.aggregation) { this.timewindow.aggregation = { @@ -439,6 +455,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O if (this.timezone) { this.timewindow.timezone = timewindowFormValue.timezone; } + + if (clearConfig) { + return clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); + } else { + return deepClone(this.timewindow); + } } private updateTimewindowForm() { @@ -548,7 +570,6 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } openTimewindowConfig() { - this.prepareTimewindowConfig(); this.dialog.open( TimewindowConfigDialogComponent, { autoFocus: false, @@ -557,7 +578,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O data: { quickIntervalOnly: this.quickIntervalOnly, aggregation: this.aggregation, - timewindow: deepClone(this.timewindow) + timewindow: this.prepareTimewindowConfig(false), + showSaveAsDefault: this.data.showSaveAsDefault } }).afterClosed() .subscribe((res) => { @@ -570,12 +592,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } private updateTimewindowAdvancedParams() { - this.realtimeDisableCustomInterval = this.timewindow.realtime.disableCustomInterval; - this.realtimeDisableCustomGroupInterval = this.timewindow.realtime.disableCustomGroupInterval; - this.historyDisableCustomInterval = this.timewindow.history.disableCustomInterval; - this.historyDisableCustomGroupInterval = this.timewindow.history.disableCustomGroupInterval; + this.realtimeDisableCustomInterval = this.timewindow.realtime?.disableCustomInterval; + this.realtimeDisableCustomGroupInterval = this.timewindow.realtime?.disableCustomGroupInterval; + this.historyDisableCustomInterval = this.timewindow.history?.disableCustomInterval; + this.historyDisableCustomGroupInterval = this.timewindow.history?.disableCustomGroupInterval; - if (this.timewindow.realtime.advancedParams) { + if (this.timewindow.realtime?.advancedParams) { this.realtimeAdvancedParams = this.timewindow.realtime.advancedParams; this.realtimeAllowedLastIntervals = this.timewindow.realtime.advancedParams.allowedLastIntervals; this.realtimeAllowedQuickIntervals = this.timewindow.realtime.advancedParams.allowedQuickIntervals; @@ -584,7 +606,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.realtimeAllowedLastIntervals = null; this.realtimeAllowedQuickIntervals = null; } - if (this.timewindow.history.advancedParams) { + if (this.timewindow.history?.advancedParams) { this.historyAdvancedParams = this.timewindow.history.advancedParams; this.historyAllowedLastIntervals = this.timewindow.history.advancedParams.allowedLastIntervals; this.historyAllowedQuickIntervals = this.timewindow.history.advancedParams.allowedQuickIntervals; diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.html b/ui-ngx/src/app/shared/components/time/timewindow.component.html index 55c2f58120..5a2d5016a0 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.html @@ -15,44 +15,56 @@ limitations under the License. --> - - - -
- {{ computedTimewindowStyle.icon }} -
- {{innerValue?.displayValue}} | {{innerValue?.displayTimezoneAbbr}} -
- {{ computedTimewindowStyle.icon }} -
+@if (asButton && panelMode && !strokedButton && !flatButton) { + +} +@if (asButton && panelMode && strokedButton) { + +} +@if (asButton && panelMode && flatButton) { + +} +@if (!asButton && panelMode) { +
+ @if (computedTimewindowStyle.showIcon && computedTimewindowStyle.iconPosition === 'left') { + {{ computedTimewindowStyle.icon }} + } +
+ {{innerValue?.displayValue}} | {{innerValue?.displayTimezoneAbbr}} +
+ @if (computedTimewindowStyle.showIcon && computedTimewindowStyle.iconPosition === 'right') { + {{ computedTimewindowStyle.icon }} + } +
+} diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index e071492a81..3d98ab0022 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -19,12 +19,14 @@ import { Component, DestroyRef, ElementRef, + EventEmitter, forwardRef, HostBinding, Injector, Input, OnChanges, OnInit, + Output, SimpleChanges, StaticProvider, ViewChild, @@ -190,6 +192,13 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan @coerceBoolean() panelMode = true; + @Input() + @coerceBoolean() + showSaveAsDefault = false; + + @Output() + saveAsDefault = new EventEmitter(); + innerValue: Timewindow; timewindowDisabled: boolean; @@ -262,6 +271,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan timezone: this.timezone, isEdit: this.isEdit, panelMode: this.panelMode, + showSaveAsDefault: this.showSaveAsDefault, } as TimewindowPanelData }, { @@ -281,7 +291,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan this.innerValue = componentRef.instance.result; this.timewindowDisabled = this.isTimewindowDisabled(); this.updateDisplayValue(); - this.notifyChanged(); + this.notifyChanged(this.showSaveAsDefault && componentRef.instance.saveTimewindow); } }); this.cd.detectChanges(); @@ -300,7 +310,8 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan private onHistoryOnlyChanged(): boolean { if (this.historyOnlyValue && this.innerValue && this.innerValue.selectedTab !== TimewindowType.HISTORY) { - this.innerValue.selectedTab = TimewindowType.HISTORY; + this.innerValue = initModelFromDefaultTimewindow(this.innerValue, this.quickIntervalOnly, this.historyOnly, + this.timeService, this.aggregation); this.updateDisplayValue(); return true; } @@ -320,7 +331,8 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan } writeValue(obj: Timewindow): void { - this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService); + this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService, + this.aggregation); this.timewindowDisabled = this.isTimewindowDisabled(); if (this.onHistoryOnlyChanged()) { setTimeout(() => { @@ -334,8 +346,11 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan } } - notifyChanged() { + notifyChanged(notifySaveAsDefault = false) { this.propagateChange(cloneSelectedTimewindow(this.innerValue)); + if (notifySaveAsDefault) { + this.saveAsDefault.emit(this.innerValue); + } } displayValue(): string { @@ -402,6 +417,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan timezone: this.timezone, isEdit: this.isEdit, panelMode: this.panelMode, + showSaveAsDefault: this.showSaveAsDefault, } const injector = Injector.create({ providers: [{ provide: TIMEWINDOW_PANEL_DATA, useValue: panelData }], @@ -413,7 +429,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan ).subscribe(value => { this.innerValue = value; this.timewindowDisabled = this.isTimewindowDisabled(); - this.notifyChanged(); + this.notifyChanged(this.showSaveAsDefault && componentRef.instance.saveTimewindow); }) } } 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 index 73343b71e3..5e343ce946 100644 --- a/ui-ngx/src/app/shared/components/time/timezone-select.component.html +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.html @@ -17,35 +17,45 @@ --> - timezone.timezone + @if (displayLabel) { + timezone.timezone + } - + @if (selectTimezoneFormGroup.get('timezone').value && !disabled) { + + } - - - - - + @for (timezone of filteredTimezones | async; track timezone) { + + + + } + @if (!(filteredTimezones | async)?.length) { + + {{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }} - - + + + } - - {{ 'timezone.timezone-required' | translate }} - + @if (selectTimezoneFormGroup.get('timezone').hasError('required')) { + + {{ 'timezone.timezone-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.html b/ui-ngx/src/app/shared/components/time/timezone.component.html index 1ca6c746b8..3f37bcdec9 100644 --- a/ui-ngx/src/app/shared/components/time/timezone.component.html +++ b/ui-ngx/src/app/shared/components/time/timezone.component.html @@ -15,40 +15,48 @@ limitations under the License. --> - - - -
-
- {{displayValue()}} -
-
+@if (asButton && !strokedButton && !flatButton) { + +} +@if (asButton && strokedButton) { + +} +@if (asButton && flatButton) { + +} +@if (!asButton) { +
+
+ {{displayValue()}} +
+
+} diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index bddaa896b6..e9063b3be2 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -26,33 +26,48 @@ [class.tb-mat-24]="isMdLg"> chevron_left -
- - - {{ option.name }} - - warning - - - -
+@if ((useSelect$ | async) === false) { +
+ + @for (option of options; track trackByHeaderOption($index, option)) { + + {{ option.name }} + @if ((option.error$ | async) && value !== option.value) { + + warning + + } + + } + +
+} @else { + + + @for (option of options; track option) { + {{ option.name }} + } + + +} - - - - {{ option.name }} - - - diff --git a/ui-ngx/src/app/shared/components/unit-input.component.html b/ui-ngx/src/app/shared/components/unit-input.component.html index 66d92c83d1..c2cd671072 100644 --- a/ui-ngx/src/app/shared/components/unit-input.component.html +++ b/ui-ngx/src/app/shared/components/unit-input.component.html @@ -23,27 +23,33 @@ (focusin)="onFocus()" [matAutocomplete]="unitsAutocomplete" [matAutocompleteDisabled]="supportsUnitConversion"> - - - warning - - - mdi:swap-vertical-circle-outline - + @if (unitsFormControl.value && !disabled && unitsFormControl.valid && !isUnitMapping) { + + } + @if (unitsFormControl.hasError('required')) { + + warning + + } + @if (supportsUnitConversion && isUnitMapping) { + + mdi:swap-vertical-circle-outline + + } } - } + }
diff --git a/ui-ngx/src/app/shared/components/unit-settings-panel.component.html b/ui-ngx/src/app/shared/components/unit-settings-panel.component.html index 41875971f8..c27f56dd3f 100644 --- a/ui-ngx/src/app/shared/components/unit-settings-panel.component.html +++ b/ui-ngx/src/app/shared/components/unit-settings-panel.component.html @@ -22,9 +22,9 @@
unit.unit-settings.source-unit
+ translate>unit.unit-settings.source-unit
+ [measure]="measure" [tagFilter]="tagFilter">
@@ -36,7 +36,7 @@
unit.unit-settings.target-metric-unit
+ translate>unit.unit-settings.target-metric-unit
unit.unit-settings.target-imperial-unit + translate>unit.unit-settings.target-imperial-unit unit.unit-settings.target-hybrid-unit + translate>unit.unit-settings.target-hybrid-unit
- + @if (!disabled) { + + } - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html index 30b67fd38c..91454db185 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.html +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -16,26 +16,34 @@ -->
-
diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html index a2f9426aa3..1ee5ac89b3 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.html +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -17,85 +17,107 @@ -->
- - - -
- - {{ valueTypes.get(valueType).name | translate }} -
-
- - - {{ valueTypes.get(valueTypeEnum[valueType]).name | translate }} - -
-
- - - - warning - - - - - + + +
+ + {{ valueTypes.get(valueType).name | translate }} +
+
+ @for (valueType of valueTypeKeys; track valueType) { + + + {{ valueTypes.get(valueTypeEnum[valueType]).name | translate }} + + } +
+
+ } + @if (valueType === valueTypeEnum.STRING) { + + + @if (value.hasError('required') && value.control.touched) { + + warning + + } + + } + @if (valueType === valueTypeEnum.INTEGER) { + + + @if (value.control.invalid && value.control.touched) { + - warning - - - - - - warning - - - - - {{ trueLabel }} - {{ falseLabel }} - - - {{ modelValue ? trueLabel : falseLabel }} - - -
- - - + warning + + } + + } + @if (valueType === valueTypeEnum.DOUBLE) { + + + @if (value.hasError('required') && value.control.touched) { + + warning + + } + + } + @if (valueType === valueTypeEnum.BOOLEAN) { + @if (!shortBooleanField) { + + {{ trueLabel }} + {{ falseLabel }} + + } + @if (shortBooleanField) { + + {{ modelValue ? trueLabel : falseLabel }} + + } + } + @if (valueType === valueTypeEnum.JSON) { +
+ + + @if (value.control.invalid && value.control.touched) { + - warning - - - -
+ class="tb-error"> + warning + + } + + @if (!hideJsonEdit) { + + } +
+ }
diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html index 18b28a90ee..40ddca3dce 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html @@ -15,7 +15,10 @@ limitations under the License. --> - + {{ 'version-control.branch' | translate }} - + @if (branchFormGroup.get('branch').value && !disabled) { + + } - - check - - - {{ 'version-control.default' | translate }} - + @for (branch of filteredBranches | async; track branch) { + + @if (selectionMode && branch.name === modelValue) { + check + } + @if (selectionMode && branch.name !== modelValue) { + + } + + @if (branch.default) { + {{ 'version-control.default' | translate }} + } + + } - - {{ 'version-control.branch-required' | translate }} - + @if (branchFormGroup.get('branch').hasError('required')) { + + {{ 'version-control.branch-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts index be2745cda6..ece8c1b228 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts @@ -58,6 +58,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit @Input() subscriptSizing: SubscriptSizing = 'fixed'; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { @@ -94,9 +97,6 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit @Input() emptyPlaceholder: string; - @Input() - appearance: MatFormFieldAppearance = 'fill'; - @ViewChild('branchAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('branchInput', { read: MatAutocompleteTrigger, static: true }) autoCompleteTrigger: MatAutocompleteTrigger; @ViewChild('branchInput', {static: true}) branchInput: ElementRef; diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html index 8332d0a268..270284dc11 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html +++ b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html @@ -23,7 +23,9 @@ (focus)="toggleFocus()" (blur)="toggleFocus()" [placeholder]=placeholder> - + @if (searchText) { + + } 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 index 04355f2bc1..d383c7ba71 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - +
{{widgetsBundle?.title}} - widgets-bundle.system + @if (isSystem(widgetsBundle)) { + widgets-bundle.system + }
- - widgets-bundle.create-new - - - - - -
- {{widgetsBundle.title}} - widgets-bundle.system -
-
+ @if (createNew) { + + widgets-bundle.create-new + + } @else { + @if (!required) { + + } + } + @for (widgetsBundle of widgetsBundles$ | async; track widgetsBundle) { + +
+ {{widgetsBundle.title}} + @if (isSystem(widgetsBundle)) { + widgets-bundle.system + } +
+
+ }
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 index aa4d8328ba..e8bd78262b 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts @@ -40,6 +40,7 @@ import { WidgetsBundleDialogComponent, WidgetsBundleDialogData } from '@home/pages/widget/widgets-bundle-dialog.component'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-widgets-bundle-select', @@ -79,6 +80,9 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni @coerceBoolean() createNew: boolean; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + widgetsBundles$: Observable>; widgetsBundles: Array; diff --git a/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html b/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html index ef27998db3..7bd900f1cf 100644 --- a/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html @@ -24,8 +24,10 @@ close - - +@if ((isLoading$ | async) && !ignoreLoading) { + + +}
{{ prompt | translate }} diff --git a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html index eec9ee4e39..bf0f0401c8 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html @@ -27,8 +27,10 @@ close - - + @if (isLoading$ | async) { + + + }
@@ -47,23 +49,25 @@
+ (click)="cancel()">{{ 'action.cancel' | translate }} + (click)="nextStep(2)">{{ 'action.continue' | translate }}
-
+ {{ 'import.stepper-text.configuration' | translate }}
- + import.csv-delimiter - - {{ delimiter.value }} - + @for (delimiter of delimiters; track delimiter) { + + {{ delimiter.value }} + + }
@@ -79,15 +83,15 @@
+ (click)="previousStep()">{{ 'action.back' | translate }}
+ (click)="cancel()">{{ 'action.cancel' | translate }} + (click)="nextStep(3)">{{ 'action.continue' | translate }}
@@ -99,15 +103,15 @@
+ (click)="previousStep()">{{ 'action.back' | translate }}
+ (click)="cancel()">{{ 'action.cancel' | translate }} + (click)="nextStep(4)">{{ 'action.continue' | translate }}
@@ -119,32 +123,39 @@ {{ 'action.done' | translate }}
-

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

-

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

-

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

- - - -
import.details
-
-
- -
-
+ @if (statistical?.created) { +

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

+ } + @if (statistical?.updated) { +

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

+ } + @if (statistical?.errors) { +

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

+ } + @if (statistical?.errorsList?.length) { + + + +
import.details
+
+
+ +
+
+ }
+ (click)="nextStep(6)">{{ 'action.ok' | translate }}
diff --git a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts index ae93a23125..9cea16b3e0 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts +++ b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts @@ -39,7 +39,7 @@ import { import { ImportExportService } from '@shared/import-export/import-export.service'; import { TableColumnsAssignmentComponent } from '@shared/import-export/table-columns-assignment.component'; import { Ace } from 'ace-builds'; -import { getAce } from '@shared/models/ace/ace.models'; +import { getAce, updateEditorSize } from '@shared/models/ace/ace.models'; export interface ImportDialogCsvData { entityType: EntityType; @@ -284,21 +284,9 @@ export class ImportDialogCsvComponent extends DialogComponent 0) { - const lines = content.split('\n'); - newHeight = 16 * lines.length + 24; - } - const minHeight = Math.min(200, newHeight); - this.renderer.setStyle(editorElement, 'minHeight', minHeight.toString() + 'px'); - this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); - editor.resize(); - } - } diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.html b/ui-ngx/src/app/shared/import-export/import-dialog.component.html index af672e4281..ec6ed1ff6b 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.html @@ -25,32 +25,40 @@ close - - + @if (isLoading$ | async) { + + + }
- - {{ importFileLabel | translate }} - {{ importContentLabel | translate }} - - - - - + @if (enableImportFromContent) { + + {{ importFileLabel | translate }} + {{ importContentLabel | translate }} + + } + @if (importFormGroup.get('importType').value === 'file') { + + + } + @if (importFormGroup.get('importType').value === 'content') { + + + }
diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index f3a6a89bce..6593e8c9bf 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -183,8 +183,8 @@ export class ImportExportService { }); } - public openCalculatedFieldImportDialog(): Observable { - return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe( + public openCalculatedFieldImportDialog(importTitle = 'calculated-fields.import', importFileLabel = 'calculated-fields.file'): Observable { + return this.openImportDialog(importTitle, importFileLabel).pipe( catchError(() => of(null)), ); } diff --git a/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html index 46be8d8c57..7f51f98303 100644 --- a/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html +++ b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html @@ -32,25 +32,33 @@ {{ 'import.column-type.column-type' | translate }} - - {{ columnTypesTranslations.get(type.value) | translate }} - - - - {{ columnTypesTranslations.get(credential.value) | translate }} + @for (type of columnTypes; track type) { + + {{ columnTypesTranslations.get(type.value) | translate }} - + } + @if (entityType === entityTypeDevice) { + + @for (credential of columnDeviceCredentials; track credential) { + + {{ columnTypesTranslations.get(credential.value) | translate }} + + } + + } {{ 'import.column-key' | translate }} - - - + @if (isColumnTypeDiffers(column.type)) { + + + + } diff --git a/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.scss b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.scss index 778d3c4486..f8d56f1c49 100644 --- a/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.scss +++ b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.scss @@ -32,4 +32,10 @@ flex: 0 0 120px; min-width: 120px; } + mat-row { + mat-cell { + padding-top: 4px; + padding-bottom: 4px; + } + } } diff --git a/ui-ngx/src/app/shared/models/ace/ace.models.ts b/ui-ngx/src/app/shared/models/ace/ace.models.ts index 2ba42e362c..c14a803293 100644 --- a/ui-ngx/src/app/shared/models/ace/ace.models.ts +++ b/ui-ngx/src/app/shared/models/ace/ace.models.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs/internal/Observable'; import { forkJoin, from, of } from 'rxjs'; import { map, mergeMap, tap } from 'rxjs/operators'; import { unwrapModule } from '@core/utils'; +import { Renderer2 } from '@angular/core'; let aceDependenciesLoaded = false; let aceModule: any; @@ -98,6 +99,46 @@ export function getAceDiff(): Observable { } } +export function updateEditorSize(editorElement: any, content: string, editor: Ace.Editor, renderer: Renderer2, options?: { + showGutter?: boolean, + ignoreHeight?: boolean, + ignoreWidth?: boolean, + setMinHeight?: boolean +}): void { + let newHeight = 200; + let newWidth = 600; + if (content && content.length > 0) { + if (editor.renderer.lineHeight <= 0) { + editor.renderer.updateFull(true); + } + const lines = content.split('\n'); + newHeight = editor.renderer.lineHeight * 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); + }); + if (options?.showGutter) { + maxLineLength += lines.length.toString().length; + } + newWidth = 10 * maxLineLength + 16; + if (options?.showGutter) { + newWidth += 32; + } + } + if (!options.ignoreHeight) { + renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); + } + if (options.setMinHeight) { + renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); + } + if (!options.ignoreWidth) { + renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); + } + editor.resize(); +} + export class Range implements Ace.Range { public start: Ace.Point; diff --git a/ui-ngx/src/app/shared/models/alarm-rule.models.ts b/ui-ngx/src/app/shared/models/alarm-rule.models.ts new file mode 100644 index 0000000000..e2d7b8b154 --- /dev/null +++ b/ui-ngx/src/app/shared/models/alarm-rule.models.ts @@ -0,0 +1,310 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { CustomTimeSchedulerItem } from "@shared/models/device.models"; +import { DashboardId } from "@shared/models/id/dashboard-id"; +import { TimeUnit } from "@shared/models/time/time.models"; +import { ComplexOperation, EntityKeyValueType, FilterPredicateType } from "@shared/models/query/query.models"; +import { EntityType } from "@shared/models/entity-type.models"; +import { Observable } from "rxjs"; +import { + CalculatedField, + CalculatedFieldArgument, + CalculatedFieldEventArguments +} from "@shared/models/calculated-field.models"; + +export const alarmRuleEntityTypeList = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + +export enum AlarmRuleScheduleType { + ANY_TIME = 'ANY_TIME', + SPECIFIC_TIME = 'SPECIFIC_TIME', + CUSTOM = 'CUSTOM' +} + +export const AlarmRuleScheduleTypeTranslationMap = new Map( + [ + [AlarmRuleScheduleType.ANY_TIME, 'alarm-rule.schedule.any-time'], + [AlarmRuleScheduleType.SPECIFIC_TIME, 'alarm-rule.schedule.specific-time'], + [AlarmRuleScheduleType.CUSTOM, 'alarm-rule.schedule.custom'] + ] +); + +export enum AlarmRuleConditionType { + SIMPLE = 'SIMPLE', + DURATION = 'DURATION', + REPEATING = 'REPEATING' +} + +export const AlarmRuleConditionTypeTranslationMap = new Map( + [ + [AlarmRuleConditionType.SIMPLE, 'alarm-rule.conditions.simple'], + [AlarmRuleConditionType.DURATION, 'alarm-rule.conditions.duration'], + [AlarmRuleConditionType.REPEATING, 'alarm-rule.conditions.repeating'] + ] +); + +export enum AlarmRuleExpressionType { + SIMPLE = 'SIMPLE', + TBEL = 'TBEL', +} + +export const FilterPredicateTypeTranslationMap = new Map( + [ + [FilterPredicateType.STRING, 'alarm-rule.filter-predicate-type.string'], + [FilterPredicateType.NUMERIC, 'alarm-rule.filter-predicate-type.numeric'], + [FilterPredicateType.BOOLEAN, 'alarm-rule.filter-predicate-type.boolean'], + [FilterPredicateType.COMPLEX, 'alarm-rule.filter-predicate-type.complex'] + ] +); + +export interface AlarmRule { + condition: AlarmRuleCondition; + alarmDetails?: string; + dashboardId?: DashboardId; +} + +export interface AlarmRuleCondition { + type: AlarmRuleConditionType; + expression: AlarmRuleExpression; + schedule?: AlarmRuleSchedule; + unit?: TimeUnit; + value?: AlarmRuleValue; + count?: AlarmRuleValue; +} + +export interface AlarmRuleExpression { + type: AlarmRuleExpressionType; + expression?: string; + filters?: Array; + operation?: ComplexOperation; +} + +export interface AlarmRuleSchedule { + staticValue?: { + type?: AlarmRuleScheduleType; + timezone?: string; + daysOfWeek?: number[]; + startsOn?: number; + endsOn?: number; + items?: CustomTimeSchedulerItem[]; + }; + dynamicValueArgument?: string; +} + +export interface AlarmRuleFilter { + argument: string; + valueType: EntityKeyValueType; + operation: ComplexOperation; + predicates: AlarmRuleFilterPredicate[]; +} + +export interface AlarmRulePredicateInfo { + keyFilterPredicate: AlarmRuleFilterPredicate; +} + +export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate | + NumericAlarmRuleFilterPredicate | + BooleanAlarmRuleFilterPredicate | + ComplexAlarmRuleFilterPredicate | + NoDataAlarmRuleFilterPredicate; + +export interface AlarmRuleValue { + dynamicValueArgument?: string; + staticValue?: T +} + +export interface StringAlarmRuleFilterPredicate { + type: AlarmRuleFilterPredicateType.STRING; + operation: AlarmRuleStringOperation; + value: AlarmRuleValue; + ignoreCase: boolean; +} + +export interface NumericAlarmRuleFilterPredicate { + type: AlarmRuleFilterPredicateType.NUMERIC; + operation: AlarmRuleNumericOperation; + value: AlarmRuleValue; +} + +export interface BooleanAlarmRuleFilterPredicate { + type: AlarmRuleFilterPredicateType.BOOLEAN; + operation: AlarmRuleBooleanOperation; + value: AlarmRuleValue; +} + +export interface NoDataAlarmRuleFilterPredicate { + type: AlarmRuleFilterPredicateType.NO_DATA; + unit: TimeUnit, + operation: AlarmRuleStringOperation.NO_DATA | AlarmRuleNumericOperation.NO_DATA | AlarmRuleBooleanOperation.NO_DATA; + duration: AlarmRuleValue; +} + +export interface BaseComplexFilterPredicate { + type: AlarmRuleFilterPredicateType.COMPLEX; + operation: ComplexOperation; + predicates: Array; +} + +export type ComplexAlarmRuleFilterPredicate = BaseComplexFilterPredicate; + +export interface AlarmRuleFilterConfig { + name?: Array; + entityType?: EntityType; + entities?: Array; +} + +export const filterOperationTranslationMap = new Map( + [ + [ComplexOperation.AND, 'alarm-rule.filter-operation.and'], + [ComplexOperation.OR, 'alarm-rule.filter-operation.or'], + ] +); + +export enum AlarmRuleFilterPredicateType { + STRING = 'STRING', + NUMERIC = 'NUMERIC', + BOOLEAN = 'BOOLEAN', + COMPLEX = 'COMPLEX', + NO_DATA = 'NO_DATA' +} + +export enum AlarmRuleStringOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + NO_DATA = 'NO_DATA', + STARTS_WITH = 'STARTS_WITH', + ENDS_WITH = 'ENDS_WITH', + CONTAINS = 'CONTAINS', + NOT_CONTAINS = 'NOT_CONTAINS', + IN = 'IN', + NOT_IN = 'NOT_IN', +} + +export const alarmRuleStringOperationTranslationMap = new Map( + [ + [AlarmRuleStringOperation.EQUAL, 'filter.operation.equal'], + [AlarmRuleStringOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [AlarmRuleStringOperation.STARTS_WITH, 'filter.operation.starts-with'], + [AlarmRuleStringOperation.ENDS_WITH, 'filter.operation.ends-with'], + [AlarmRuleStringOperation.CONTAINS, 'filter.operation.contains'], + [AlarmRuleStringOperation.NOT_CONTAINS, 'filter.operation.not-contains'], + [AlarmRuleStringOperation.IN, 'filter.operation.in'], + [AlarmRuleStringOperation.NOT_IN, 'filter.operation.not-in'], + [AlarmRuleStringOperation.NO_DATA, 'alarm-rule.missing-for'] + ] +); + +export enum AlarmRuleNumericOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + NO_DATA = 'NO_DATA', + GREATER = 'GREATER', + LESS = 'LESS', + GREATER_OR_EQUAL = 'GREATER_OR_EQUAL', + LESS_OR_EQUAL = 'LESS_OR_EQUAL' +} + +export const alarmRuleNumericOperationTranslationMap = new Map( + [ + [AlarmRuleNumericOperation.EQUAL, 'filter.operation.equal'], + [AlarmRuleNumericOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [AlarmRuleNumericOperation.GREATER, 'filter.operation.greater'], + [AlarmRuleNumericOperation.LESS, 'filter.operation.less'], + [AlarmRuleNumericOperation.GREATER_OR_EQUAL, 'filter.operation.greater-or-equal'], + [AlarmRuleNumericOperation.LESS_OR_EQUAL, 'filter.operation.less-or-equal'], + [AlarmRuleNumericOperation.NO_DATA, 'alarm-rule.missing-for'] + ] +); + +export enum AlarmRuleBooleanOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + NO_DATA = 'NO_DATA', +} + +export const alarmRuleBooleanOperationTranslationMap = new Map( + [ + [AlarmRuleBooleanOperation.EQUAL, 'filter.operation.equal'], + [AlarmRuleBooleanOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [AlarmRuleBooleanOperation.NO_DATA, 'alarm-rule.missing-for'] + ] +); + +export const alarmRuleDefaultScript = + '// Triggers when temperature is above 20 degrees\n' + + 'return temperature > 20;' + +export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit?: boolean, expression?: string) => Observable; + +export function checkPredicates(predicates: any[], validSet: Set): boolean { + for (const predicate of predicates) { + if (!predicate) continue; + if (predicate?.value?.dynamicValueArgument) { + if (!validSet.has(predicate.value.dynamicValueArgument)) { + return false; + } + } + if (predicate.type === 'COMPLEX' && Array.isArray(predicate.predicates)) { + if (!checkPredicates(predicate.predicates, validSet)) { + return false; + } + } + } + return true; +} + +export function areFilterAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); + const filter = obj || []; + if (filter.argument && !validSet.has(filter.argument)) { + return false; + } + if (Array.isArray(filter.predicates)) { + if (!checkPredicates(filter.predicates, validSet)) { + return false; + } + } + return true; +} + +export function areFiltersAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); + const filters = obj || []; + for (const filter of filters) { + if (filter.argument && !validSet.has(filter.argument)) { + return false; + } + } + for (const filter of filters) { + if (Array.isArray(filter.predicates)) { + if (!checkPredicates(filter.predicates, validSet)) { + return false; + } + } + } + return true; +} + +export function isPredicateArgumentsValid(predicates: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); + if (Array.isArray(predicates)) { + if (!checkPredicates(predicates, validSet)) { + return false; + } + } + return true; +} diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 937f617a5c..41e8940d55 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -88,11 +88,21 @@ export const alarmSearchStatusTranslations = new Map( export const alarmSeverityColors = new Map( [ - [AlarmSeverity.CRITICAL, 'red'], - [AlarmSeverity.MAJOR, 'orange'], - [AlarmSeverity.MINOR, '#ffca3d'], - [AlarmSeverity.WARNING, '#abab00'], - [AlarmSeverity.INDETERMINATE, 'green'] + [AlarmSeverity.CRITICAL, 'var(--tb-alarm-severity-critical, rgb(209, 39, 48))'], + [AlarmSeverity.MAJOR, 'var(--tb-alarm-severity-major, rgb(246, 103, 22))'], + [AlarmSeverity.MINOR, 'var(--tb-alarm-severity-minor, rgb(250, 164, 5))'], + [AlarmSeverity.WARNING, 'var(--tb-alarm-severity-warning, rgb(242, 218, 5))'], + [AlarmSeverity.INDETERMINATE, 'var(--tb-alarm-severity-indeterminate, rgba(0, 0, 0, 0.38))'] + ] +); + +export const alarmSeverityBackgroundColors = new Map( + [ + [AlarmSeverity.CRITICAL, `var(--tb-alarm-severity-critical-bg, rgba(209, 39, 48, 0.06))`], + [AlarmSeverity.MAJOR, 'var(--tb-alarm-severity-major-bg, rgba(246, 103, 22, 0.06))'], + [AlarmSeverity.MINOR, 'var(--tb-alarm-severity-minor-bg, rgba(250, 164, 5, 0.06))'], + [AlarmSeverity.WARNING, 'var(--tb-alarm-severity-warning-bg, rgba(242, 218, 5, 0.06))'], + [AlarmSeverity.INDETERMINATE, 'var(--tb-alarm-severity-indeterminate-bg, rgba(0, 0, 0, 0.06))'] ] ); @@ -120,12 +130,27 @@ export enum AlarmCommentType { OTHER = 'OTHER' } +export enum AlarmMessage { + ACKED_BY_USER = "alarm.system-comments.acked-by-user", + CLEARED_BY_USER = "alarm.system-comments.cleared-by-user", + ASSIGNED_TO_USER = "alarm.system-comments.assigned-to-user", + UNASSIGNED_BY_USER = "alarm.system-comments.unassigned-to-user", + UNASSIGNED_FROM_DELETED_USER = "alarm.system-comments.unassigned-from-deleted-user", + COMMENT_DELETED = "alarm.system-comments.comment-deleted", + SEVERITY_CHANGED = "alarm.system-comments.severity-changed", +} + export interface AlarmComment extends BaseData { alarmId: AlarmId; userId?: UserId; type: AlarmCommentType; comment: { text: string; + subtype?: keyof typeof AlarmMessage; + userName?: string; + assigneeName?: string; + oldSeverity?: AlarmSeverity; + newSeverity?: AlarmSeverity; edited?: boolean; editedOn?: number; }; @@ -140,6 +165,7 @@ export interface AlarmCommentInfo extends AlarmComment { export interface AlarmInfo extends Alarm { originatorName: string; originatorLabel: string; + originatorDisplayName?: string; assignee: AlarmAssignee; } @@ -172,6 +198,7 @@ export const simulatedAlarm: AlarmInfo = { clearTs: 0, assignTs: 0, originatorName: 'Simulated', + originatorDisplayName: 'Simulated', originatorLabel: 'Simulated', assignee: { firstName: '', @@ -242,6 +269,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { value: 'originatorName', name: 'alarm.originator' }, + originatorDisplayName: { + keyName: 'originatorDisplayName', + value: 'originatorDisplayName', + name: 'alarm.originator' + }, originatorLabel: { keyName: 'originatorLabel', value: 'originatorLabel', @@ -271,6 +303,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { keyName: 'assignee', value: 'assignee', name: 'alarm.assignee' + }, + details: { + keyName: 'details', + value: 'details', + name: 'alarm.details' } }; diff --git a/ui-ngx/src/app/shared/models/api-key.models.ts b/ui-ngx/src/app/shared/models/api-key.models.ts new file mode 100644 index 0000000000..6d18a905c5 --- /dev/null +++ b/ui-ngx/src/app/shared/models/api-key.models.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { HasTenantId } from '@shared/models/entity.models'; +import { ApiKeyId } from '@shared/models/id/api-key-id'; +import { UserId } from '@shared/models/id/user-id'; + +export const userInfoCommand = (baseUrl: string, apiKey: string): string => `curl -X GET "${baseUrl}/api/auth/user" -H "Content-Type: application/json" -H "X-Authorization: ApiKey ${apiKey}"` + +export interface ApiKeyInfo extends BaseData, HasTenantId { + enabled: boolean; + expirationTime: number; + description: string; + expired: boolean; + userId: UserId; +} + +export interface ApiKey extends ApiKeyInfo { + value: string; +} diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index e8cc99507d..59bd97bde6 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -20,6 +20,7 @@ import { CustomerId } from './id/customer-id'; import { EntityId } from './id/entity-id'; import { UserId } from './id/user-id'; import { TenantId } from './id/tenant-id'; +import { isArraysEqualIgnoreUndefined } from "@core/utils"; export enum AuditLogMode { TENANT, @@ -132,3 +133,11 @@ export interface AuditLog extends BaseData { actionStatus: ActionStatus; actionFailureDetails: string; } + +export interface AuditLogFilter { + actionTypes: string[]; +} + +export const auditLogFilterEquals = (filter1?: AuditLogFilter, filter2?: AuditLogFilter): boolean => { + return isArraysEqualIgnoreUndefined(filter1.actionTypes, filter2.actionTypes); +}; diff --git a/ui-ngx/src/app/shared/models/authority.enum.ts b/ui-ngx/src/app/shared/models/authority.enum.ts index b790e2cd56..8efaad725c 100644 --- a/ui-ngx/src/app/shared/models/authority.enum.ts +++ b/ui-ngx/src/app/shared/models/authority.enum.ts @@ -20,5 +20,6 @@ export enum Authority { CUSTOMER_USER = 'CUSTOMER_USER', REFRESH_TOKEN = 'REFRESH_TOKEN', ANONYMOUS = 'ANONYMOUS', - PRE_VERIFICATION_TOKEN = 'PRE_VERIFICATION_TOKEN' + PRE_VERIFICATION_TOKEN = 'PRE_VERIFICATION_TOKEN', + MFA_CONFIGURATION_TOKEN = 'MFA_CONFIGURATION_TOKEN' } diff --git a/ui-ngx/src/app/shared/models/base-data.ts b/ui-ngx/src/app/shared/models/base-data.ts index 125fcfb814..d4716881d9 100644 --- a/ui-ngx/src/app/shared/models/base-data.ts +++ b/ui-ngx/src/app/shared/models/base-data.ts @@ -16,7 +16,9 @@ import { EntityId } from '@shared/models/id/entity-id'; import { HasUUID } from '@shared/models/id/has-uuid'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; +import { User } from '@shared/models/user.model'; export declare type HasId = EntityId | HasUUID; @@ -49,3 +51,12 @@ export function hasIdEquals(id1: HasId, id2: HasId): boolean { return id1 === id2; } } + +export function getEntityDisplayName(entity: BaseData): string { + if (entity?.id?.entityType === EntityType.USER) { + const user = entity as User; + const userName = (user?.firstName ?? '') + " " + (user?.lastName ?? ''); + return isNotEmptyStr(userName) ? userName.trim() : entity?.name; + } + return isNotEmptyStr(entity?.label) ? entity.label : entity?.name; +} diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 7e3a79efd7..552d2a3a95 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -14,11 +14,7 @@ /// limitations under the License. /// -import { - HasEntityDebugSettings, - HasTenantId, - HasVersion -} from '@shared/models/entity.models'; +import { HasEntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models'; import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { CalculatedFieldId } from '@shared/models/id/calculated-field-id'; import { EntityId } from '@shared/models/id/entity-id'; @@ -33,45 +29,269 @@ import { dotOperatorHighlightRule, endGroupHighlightRule } from '@shared/models/ace/ace.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { AlarmSeverity } from "@shared/models/alarm.models"; + +export const FORBIDDEN_NAMES = ['ctx', 'e', 'pi']; -export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { - configuration: CalculatedFieldConfiguration; - type: CalculatedFieldType; +interface BaseCalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { entityId: EntityId; + additionalInfo?: any; +} + +export interface CalculatedFieldSimple extends BaseCalculatedField { + type: CalculatedFieldType.SIMPLE; + configuration: CalculatedFieldSimpleConfiguration; +} + +export interface CalculatedFieldScript extends BaseCalculatedField { + type: CalculatedFieldType.SCRIPT; + configuration: CalculatedFieldScriptConfiguration; +} + +export interface CalculatedFieldGeofencing extends BaseCalculatedField { + type: CalculatedFieldType.GEOFENCING; + configuration: CalculatedFieldGeofencingConfiguration; +} + +export interface CalculatedFieldPropagation extends BaseCalculatedField { + type: CalculatedFieldType.PROPAGATION; + configuration: CalculatedFieldPropagationConfiguration; } +export interface CalculatedFieldAlarmRule extends BaseCalculatedField { + type: CalculatedFieldType.ALARM; + configuration: CalculatedFieldAlarmRuleConfiguration; +} + +export interface CalculatedFieldRelatedEntityAggregation extends BaseCalculatedField { + type: CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + configuration: CalculatedFieldRelatedAggregationConfiguration; +} + + +export type CalculatedField = + | CalculatedFieldSimple + | CalculatedFieldScript + | CalculatedFieldGeofencing + | CalculatedFieldPropagation + | CalculatedFieldRelatedEntityAggregation + | CalculatedFieldAlarmRule; + +export type WithCalculatedFieldInfo = T & {entityName: string}; +export type CalculatedFieldInfo = WithCalculatedFieldInfo; +export type CalculatedFieldAlarmRuleInfo = WithCalculatedFieldInfo; + export enum CalculatedFieldType { SIMPLE = 'SIMPLE', SCRIPT = 'SCRIPT', + GEOFENCING = 'GEOFENCING', + PROPAGATION = 'PROPAGATION', + RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION', + ENTITY_AGGREGATION = 'ENTITY_AGGREGATION', + ALARM = 'ALARM', +} + +export interface CalculatedFieldTypeTranslate { + name: string; + hint?: string; } -export const CalculatedFieldTypeTranslations = new Map( +export const CalculatedFieldTypeTranslations = new Map( [ - [CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'], - [CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'], + [CalculatedFieldType.SIMPLE, { + name: 'calculated-fields.type.simple', + hint: 'calculated-fields.type.simple-hint', + }], + [CalculatedFieldType.SCRIPT, { + name: 'calculated-fields.type.script', + hint: 'calculated-fields.type.script-hint', + }], + [CalculatedFieldType.GEOFENCING, { + name: 'calculated-fields.type.geofencing', + hint: 'calculated-fields.type.geofencing-hint', + }], + [CalculatedFieldType.PROPAGATION, { + name: 'calculated-fields.type.propagation', + hint: 'calculated-fields.type.propagation-hint', + }], + [CalculatedFieldType.RELATED_ENTITIES_AGGREGATION, { + name: 'calculated-fields.type.related-entities-aggregation', + hint: 'calculated-fields.type.related-entities-aggregation-hint' + }], + [CalculatedFieldType.ENTITY_AGGREGATION, { + name: 'calculated-fields.type.time-series-data-aggregation', + hint: 'calculated-fields.type.time-series-data-aggregation-hint', + }], ] ) -export interface CalculatedFieldConfiguration { - type: CalculatedFieldType; +export const calculatedFieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) + +export type CalculatedFieldConfiguration = + | CalculatedFieldSimpleConfiguration + | CalculatedFieldScriptConfiguration + | CalculatedFieldGeofencingConfiguration + | CalculatedFieldPropagationConfiguration + | CalculatedFieldRelatedAggregationConfiguration + | CalculatedFieldEntityAggregationConfiguration + | CalculatedFieldAlarmRuleConfiguration; + +export interface CalculatedFieldSimpleConfiguration { + type: CalculatedFieldType.SIMPLE; + expression: string; + arguments: Record; + useLatestTs: boolean; + output: CalculatedFieldSimpleOutput; +} + +export interface CalculatedFieldScriptConfiguration { + type: CalculatedFieldType.SCRIPT; expression: string; arguments: Record; output: CalculatedFieldOutput; } -export interface CalculatedFieldOutput { - type: OutputType; +export interface CalculatedFieldGeofencingConfiguration { + type: CalculatedFieldType.GEOFENCING; + zoneGroups: Record; + scheduledUpdateEnabled: boolean; + scheduledUpdateInterval?: number; + output: CalculatedFieldOutput; +} + +export interface CalculatedFieldRelatedAggregationConfiguration { + type: CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + relation: RelationPathLevel; + arguments: Record; + metrics: Record; + deduplicationIntervalInSec: number; + scheduledUpdateInterval?: number; + useLatestTs: boolean; + output: CalculatedFieldOutput & { decimalsByDefault?: number; }; +} + +export interface CalculatedFieldEntityAggregationConfiguration { + type: CalculatedFieldType.ENTITY_AGGREGATION; + arguments: Record; + metrics: Record; + interval: AggInterval; + watermark?: WatermarkConfig; + produceIntermediateResult?: boolean; + output: CalculatedFieldOutput & { decimalsByDefault?: number; }; +} + +export interface WatermarkConfig { + duration: number; +} + +interface BasePropagationConfiguration { + type: CalculatedFieldType.PROPAGATION; + relation: RelationPathLevel; + arguments: Record; + output: CalculatedFieldOutput; +} + +export interface CalculatedFieldAlarmRuleConfiguration { + type: CalculatedFieldType.ALARM; + arguments: Record; + createRules: Record; + clearRule?: AlarmRule; + propagate: boolean; + propagateToOwner: boolean; + propagateToTenant: boolean; + propagateRelationTypes?: Array; +} + +export interface PropagationWithNoExpression extends BasePropagationConfiguration { + applyExpressionToResolvedArguments: false; +} + +export interface PropagationWithExpression extends BasePropagationConfiguration { + applyExpressionToResolvedArguments: true; + expression: string; +} + +export type CalculatedFieldPropagationConfiguration = + | PropagationWithNoExpression + | PropagationWithExpression; + +export type CalculatedFieldOutput = + | CalculatedFieldOutputAttribute + | CalculatedFieldOutputTimeSeries; + +export interface CalculatedFieldOutputAttribute { + type: OutputType.Attribute, + scope: AttributeScope; + strategy: AttributeOutputStrategy; +} + +export interface CalculatedFieldOutputTimeSeries { + type: OutputType.Timeseries; + strategy: TimeSeriesOutputStrategy; +} + +export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { name: string; - scope?: AttributeScope; decimalsByDefault?: number; } +export type AttributeOutputStrategy = + | AttributeImmediateOutputStrategy + | AttributeRuleChainOutputStrategy; + +export interface AttributeImmediateOutputStrategy { + type: OutputStrategyType.IMMEDIATE; + updateAttributesOnlyOnValueChange: boolean; + sendAttributesUpdatedNotification: boolean; + saveAttribute: boolean; + sendWsUpdate: boolean; + processCfs: boolean; +} + +export interface AttributeRuleChainOutputStrategy { + type: OutputStrategyType.RULE_CHAIN; +} + +export type TimeSeriesOutputStrategy = + | TimeSeriesRuleChainOutputStrategy + | TimeSeriesImmediateOutputStrategy; + +export interface TimeSeriesRuleChainOutputStrategy { + type: OutputStrategyType.IMMEDIATE; + ttl: number; + saveTimeSeries: boolean; + saveLatest: boolean; + sendWsUpdate: boolean; + processCfs: boolean; +} + +export interface TimeSeriesImmediateOutputStrategy { + type: OutputStrategyType.RULE_CHAIN; +} + +export enum OutputStrategyType { + IMMEDIATE = 'IMMEDIATE', + RULE_CHAIN = 'RULE_CHAIN' +} + +export const OutputStrategyTypeTranslations = new Map( + [ + [OutputStrategyType.IMMEDIATE, 'calculated-fields.output-strategy.process-right-away'], + [OutputStrategyType.RULE_CHAIN, 'calculated-fields.output-strategy.process-rule-chains'], + ] +) + export enum ArgumentEntityType { Current = 'CURRENT', Device = 'DEVICE', Asset = 'ASSET', Customer = 'CUSTOMER', Tenant = 'TENANT', + Owner = 'CURRENT_OWNER', + RelationQuery = 'RELATION_PATH_QUERY', } export const ArgumentEntityTypeTranslations = new Map( @@ -81,6 +301,43 @@ export const ArgumentEntityTypeTranslations = new Map( + [ + [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, 'calculated-fields.report-transition-event-and-presence'], + [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_ONLY, 'calculated-fields.report-transition-event-only'], + [GeofencingReportStrategy.REPORT_PRESENCE_STATUS_ONLY, 'calculated-fields.report-presence-status-only'] + ] +) + +export const GeofencingDirectionTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-from'], + [EntitySearchDirection.TO, 'calculated-fields.direction-to'], + ] +) + +export const GeofencingDirectionLevelTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-down'], + [EntitySearchDirection.TO, 'calculated-fields.direction-up'], + ] +) + +export const PropagationDirectionTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-down-child'], + [EntitySearchDirection.TO, 'calculated-fields.direction-up-parent'], ] ) @@ -127,10 +384,115 @@ export interface CalculatedFieldArgument { refEntityKey: RefEntityKey; defaultValue?: string; refEntityId?: RefEntityId; + refDynamicSourceConfiguration?: RefDynamicSourceConfiguration; limit?: number; timeWindow?: number; } +export interface RefDynamicSourceConfiguration { + type: ArgumentEntityType.Owner; +} + +export enum AggFunction { + AVG='AVG', + MIN='MIN', + MAX='MAX', + SUM='SUM', + COUNT='COUNT', + COUNT_UNIQUE='COUNT_UNIQUE' +} + +export const AggFunctionTranslations = new Map([ + [AggFunction.AVG, 'calculated-fields.metrics.aggregation-type.avg'], + [AggFunction.MIN, 'calculated-fields.metrics.aggregation-type.min'], + [AggFunction.MAX, 'calculated-fields.metrics.aggregation-type.max'], + [AggFunction.SUM, 'calculated-fields.metrics.aggregation-type.sum'], + [AggFunction.COUNT, 'calculated-fields.metrics.aggregation-type.count'], + [AggFunction.COUNT_UNIQUE, 'calculated-fields.metrics.aggregation-type.count-unique'], +]) + +export enum AggIntervalType { + HOUR = 'HOUR', + DAY = 'DAY', + WEEK = 'WEEK', + WEEK_SUN_SAT = 'WEEK_SUN_SAT', + MONTH = 'MONTH', + QUARTER = 'QUARTER', + YEAR = 'YEAR', + CUSTOM = 'CUSTOM' +} + +export const AggIntervalTypeTranslations = new Map( + [ + [AggIntervalType.HOUR, 'calculated-fields.aggregate-period.hour'], + [AggIntervalType.DAY, 'calculated-fields.aggregate-period.day'], + [AggIntervalType.WEEK, 'calculated-fields.aggregate-period.week'], + [AggIntervalType.WEEK_SUN_SAT, 'calculated-fields.aggregate-period.week-sun-sat'], + [AggIntervalType.MONTH, 'calculated-fields.aggregate-period.month'], + [AggIntervalType.QUARTER, 'calculated-fields.aggregate-period.quarter'], + [AggIntervalType.YEAR, 'calculated-fields.aggregate-period.year'], + [AggIntervalType.CUSTOM, 'calculated-fields.aggregate-period.custom'] + ] +); + +export interface AggInterval { + type: AggIntervalType; + tz: string; + offsetSec?: number + durationSec?: number +} + +export interface CalculatedFieldAggMetric { + function: AggFunction; + filter?: string; + input: AggKeyInput | AggFunctionInput; + defaultValue?: number; +} + +export interface CalculatedFieldAggMetricValue extends CalculatedFieldAggMetric { + name: string; +} + +export enum AggInputType { + key = 'key', + function = 'function' +} + +export const AggInputTypeTranslations = new Map([ + [AggInputType.key, 'calculated-fields.metrics.value-source-type.key'], + [AggInputType.function, 'calculated-fields.metrics.value-source-type.function'], +]) + +export interface AggKeyInput { + type: AggInputType.key; + key: string; +} + +export interface AggFunctionInput { + type: AggInputType.function; + function: string; +} + +export interface CalculatedFieldGeofencing { + perimeterKeyName: string; + reportStrategy: GeofencingReportStrategy; + refEntityId?: RefEntityId; + refDynamicSourceConfiguration: RefDynamicSourceGeofencingConfiguration; + createRelationsWithMatchedZones: boolean; + relationType: string; + direction: EntitySearchDirection; +} + +export interface RefDynamicSourceGeofencingConfiguration { + type: ArgumentEntityType.RelationQuery | ArgumentEntityType.Owner; + levels?: Array; +} + +export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing { + name: string; + entityName?: string; +} + export interface RefEntityKey { key: string; type: ArgumentType; @@ -147,7 +509,7 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument { entityName?: string; } -export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record, closeAllOnSave?: boolean) => Observable; +export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record, closeAllOnSave?: boolean, expression?: string) => Observable; export interface CalculatedFieldTestScriptInputParams { arguments: CalculatedFieldEventArguments; @@ -166,7 +528,7 @@ export const ArgumentEntityTypeParamsMap =new Map { - switch (entityId.entityType) { + switch (entityId?.entityType) { case EntityType.ASSET_PROFILE: return { assetTypes: [entityName], @@ -185,11 +547,22 @@ export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entity } } +export const debugCfActionEnabled = (cf: CalculatedField) => { + return (cf.type === CalculatedFieldType.SCRIPT || + (cf.type === CalculatedFieldType.PROPAGATION && cf.configuration.applyExpressionToResolvedArguments) + ); +} + export interface CalculatedFieldArgumentValueBase { argumentName: string; type: ArgumentType; } +export interface RelationPathLevel { + direction: EntitySearchDirection; + relationType: string; +} + export interface CalculatedFieldAttributeArgumentValue extends CalculatedFieldArgumentValueBase { ts: number; value: ValueType; @@ -211,6 +584,25 @@ export type CalculatedFieldArgumentEventValue = CalculatedF export type CalculatedFieldEventArguments = Record>; +export const calculatedFieldsEntityTypeList = [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + +export const defaultCalculatedFieldOutput: CalculatedFieldOutputTimeSeries = { + type: OutputType.Timeseries, + strategy: { + type: OutputStrategyType.IMMEDIATE, + ttl: 0, + saveTimeSeries: true, + saveLatest: true, + sendWsUpdate: true, + processCfs: true + } +} + +export const defaultSimpleCalculatedFieldOutput: CalculatedFieldSimpleOutput = { + name: '', + ...defaultCalculatedFieldOutput +} + export const CalculatedFieldCtxLatestTelemetryArgumentAutocomplete = { meta: 'object', type: '{ ts: number; value: any; }', @@ -654,3 +1046,54 @@ export const calculatedFieldDefaultScript = 'return {\n' + ' "temperatureC": (temperatureF - 32) / 1.8\n' + '};' + +export function notEmptyObjectValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) { + return {emptyObject: true}; + } + return null; + }; +} + +export function forbiddenNamesValidator(forbiddenNames: string[]): ValidatorFn { + const forbiddenNameSet = new Set(forbiddenNames); + + return (control: FormControl) => { + const trimmedValue = (control.value || '').trim(); + return forbiddenNameSet.has(trimmedValue) ? { forbiddenName: true } : null; + }; +} + +export function uniqueNameValidator(existingNames: string[]): ValidatorFn { + const namesSet = new Set((existingNames || []).map(name => name.toLowerCase())); + + return (control: FormControl) => { + const newName = (control.value || '').trim().toLowerCase(); + + if (!newName) { + return null; + } + + return namesSet.has(newName) ? { duplicateName: true } : null; + }; +} + +export interface CalculatedFieldsQuery { + types: Array; + entityType?: EntityType; + entities?: Array; + name?: Array; +} + +export const calculatedFieldMetricFilterDefaultScript = + '// Sample filter script to include only active and unoccupied parking spaces\n' + + '// Goal: Count only parking spaces that are active and currently free\n\n' + + 'return active == true && occupied == false;'; + +export const calculatedFieldMetricMapDefaultScript = + '// Sample map script to convert temperature from Fahrenheit to Celsius\n' + + '// Goal: Apply conversion per entity before aggregation (e.g., for average temperature)\n\n' + + 'var temperatureC = (temperature - 32) / 1.8;\n' + + 'return toFixed(temperatureC, 2);'; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 429bfe217a..afc68fdfcd 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -31,6 +31,7 @@ export const Constants = { itemNotFound: 32, tooManyRequests: 33, tooManyUpdates: 34, + entitiesLimitExceeded: 41, passwordViolation: 45 }, entryPoints: { @@ -52,6 +53,7 @@ export const serverErrorCodesTranslations = new Map([ [Constants.serverErrorCode.itemNotFound, 'server-error.item-not-found'], [Constants.serverErrorCode.tooManyRequests, 'server-error.too-many-requests'], [Constants.serverErrorCode.tooManyUpdates, 'server-error.too-many-updates'], + [Constants.serverErrorCode.entitiesLimitExceeded, 'server-error.entities-limit-exceeded'], ]); export const httpStatusMessageMap = new Map([ @@ -190,6 +192,7 @@ export const HelpLinks = { entitiesImport: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/bulk-provisioning`, rulechains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/rule-chains`, lwm2mResourceLibrary: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/lwm2m-api`, + jsExtension: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/ui/advanced-development`, dashboards: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/dashboards`, otaUpdates: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ota-updates`, widgetTypes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library/#widget-types`, @@ -220,8 +223,10 @@ export const HelpLinks = { mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`, aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`, + apiKeys: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/api-keys/`, timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`, - trendzSettings: `${helpBaseUrl}/docs/trendz/` + trendzSettings: `${helpBaseUrl}/docs/trendz/`, + alarmRules: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/alarm-rules/`, } }; /* eslint-enable max-len */ diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 1f797da162..5bd00f2549 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -186,8 +186,8 @@ export interface DashboardConfiguration { settings?: DashboardSettings; widgets?: {[id: string]: Widget } | Widget[]; states?: {[id: string]: DashboardState }; - entityAliases?: EntityAliases; - filters?: Filters; + entityAliases: EntityAliases; + filters: Filters; [key: string]: any; } diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 8e29b5b2b4..3cf1199980 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -22,7 +22,7 @@ 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, HasTenantId, HasVersion } from '@shared/models/entity.models'; +import { EntityInfoData, HasTenantId, HasVersion, SaveEntityParams } from '@shared/models/entity.models'; import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; import _moment from 'moment'; @@ -739,6 +739,10 @@ export interface DeviceInfoFilter { active?: boolean; } +export interface SaveDeviceParams extends SaveEntityParams { + accessToken?: string; +} + export class DeviceInfoQuery { pageLink: PageLink; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 8f65a189d7..4ee4674c75 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -52,6 +52,7 @@ export enum EntityType { MOBILE_APP = 'MOBILE_APP', CALCULATED_FIELD = 'CALCULATED_FIELD', AI_MODEL = 'AI_MODEL', + API_KEY = 'API_KEY', } export enum AliasEntityType { @@ -489,7 +490,8 @@ export const entityTypeTranslations = new Map([ [NotificationType.ALARM_ASSIGNMENT, 'assignment_turned_in'], [NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'settings_ethernet'], [NotificationType.ENTITIES_LIMIT, 'data_thresholding'], + [NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, 'mdi:file-cog'], [NotificationType.API_USAGE_LIMIT, 'insert_chart'], [NotificationType.TASK_PROCESSING_FAILURE, 'warning'], [NotificationType.RESOURCES_SHORTAGE, 'warning'] ]); -export const AlarmSeverityNotificationColors = new Map( - [ - [AlarmSeverity.CRITICAL, '#D12730'], - [AlarmSeverity.MAJOR, '#FEAC0C'], - [AlarmSeverity.MINOR, '#F2DA05'], - [AlarmSeverity.WARNING, '#F66716'], - [AlarmSeverity.INDETERMINATE, '#00000061'] - ] -); - export enum ActionButtonLinkType { LINK = 'LINK', DASHBOARD = 'DASHBOARD' @@ -623,6 +615,12 @@ export const NotificationTemplateTypeTranslateMap = new Map{ + return (group: AbstractControl): ValidationErrors | null => { + const newPassControl = group.get(firstControlName); + const confirmControl = group.get(secondControlName); + + if (!newPassControl || !confirmControl) { + return null; + } + + const newPass = newPassControl.value ?? ''; + const confirm = confirmControl.value ?? ''; + + if ((newPass || confirm) && confirm !== newPass) { + confirmControl.setErrors({ passwordsNotMatch: true }); + return { passwordsNotMatch: true }; + } else { + const currentErrors = confirmControl?.errors; + if (currentErrors?.['passwordsNotMatch']) { + const { passwordsNotMatch, ...rest } = currentErrors; + confirmControl?.setErrors(Object.keys(rest).length ? rest : null); + } + return null; + } + }; +} + +export const passwordStrengthValidator = (passwordPolicy: UserPasswordPolicy): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const value: string = control.value; + const errors: any = {}; + + if (passwordPolicy.minimumUppercaseLetters > 0 && + !new RegExp(`(?:.*?[A-Z]){${passwordPolicy.minimumUppercaseLetters}}`).test(value)) { + errors.notUpperCase = true; + } + + if (passwordPolicy.minimumLowercaseLetters > 0 && + !new RegExp(`(?:.*?[a-z]){${passwordPolicy.minimumLowercaseLetters}}`).test(value)) { + errors.notLowerCase = true; + } + + if (passwordPolicy.minimumDigits > 0 + && !new RegExp(`(?:.*?\\d){${passwordPolicy.minimumDigits}}`).test(value)) { + errors.notNumeric = true; + } + + if (passwordPolicy.minimumSpecialCharacters > 0 && + !new RegExp(`(?:.*?[\\W_]){${passwordPolicy.minimumSpecialCharacters}}`).test(value)) { + errors.notSpecial = true; + } + + if (!passwordPolicy.allowWhitespaces && /\s/.test(value)) { + errors.hasWhitespaces = true; + } + + if (passwordPolicy.minimumLength > 0 && value.length < passwordPolicy.minimumLength) { + errors.minLength = true; + } + + if (!value.length || passwordPolicy.maximumLength > 0 && value.length > passwordPolicy.maximumLength) { + errors.maxLength = true; + } + + return isEqual(errors, {}) ? null : errors; + }; +} diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index b3f21b0abe..7b35639aaf 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -71,3 +71,4 @@ export * from './query/query.models'; export * from './regex.constants'; export * from './trendz-settings.models'; export * from './ai-model.models'; +export * from './password.models'; diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 7f04f07664..cd35c410dc 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -401,20 +401,21 @@ export interface FilterInfo { filter: string; editable: boolean; keyFilters: Array; + keyFiltersOperation?: ComplexOperation; } export interface FiltersInfo { datasourceFilters: {[datasourceIndex: number]: FilterInfo}; } -export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array): string { +export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array, + operation: ComplexOperation = ComplexOperation.AND): string { const filtersText = keyFilters.map(keyFilter => - keyFilterToText(translate, datePipe, keyFilter, - keyFilters.length > 1 ? ComplexOperation.AND : undefined)); + keyFilterToText(translate, datePipe, keyFilter, operation)); let result: string; if (filtersText.length > 1) { - const andText = translate.instant('filter.operation.and'); - result = filtersText.join(' ' + andText + ' '); + const opText = translate.instant(complexOperationTranslationMap.get(operation)); + result = filtersText.join(' ' + opText + ' '); } else { result = filtersText[0]; } @@ -802,6 +803,7 @@ export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter = export interface EntityCountQuery { entityFilter: EntityFilter; keyFilters?: Array; + keyFiltersOperation?: ComplexOperation; } export interface AbstractDataQuery extends EntityCountQuery { @@ -940,13 +942,15 @@ export const getFilterId = (filters: Filters, filterInfo: FilterInfo): string => const newFilterName = createFilterName(filters, filterInfo.filter); newFilterId = guid(); filters[newFilterId] = {id: newFilterId, filter: newFilterName, - keyFilters: filterInfo.keyFilters, editable: filterInfo.editable}; + keyFilters: filterInfo.keyFilters, editable: filterInfo.editable, + keyFiltersOperation: filterInfo.keyFiltersOperation}; } return newFilterId; } const isFilterEqual = (filter1: FilterInfo, filter2: FilterInfo): boolean => { - return isEqual(filter1.keyFilters, filter2.keyFilters); + return isEqual(filter1.keyFilters, filter2.keyFilters) && + filter1.keyFiltersOperation === filter2.keyFiltersOperation; } const createFilterName = (filters: Filters, filter: string): string => { diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index eacb68de35..71934a777f 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -89,7 +89,7 @@ export interface TbResourceInfo extends Omit, 'name' | export type ResourceInfo = TbResourceInfo; export interface Resource extends ResourceInfo { - data?: string; + data?: any; name?: string; } @@ -190,6 +190,7 @@ export const isImageResourceUrl = (url: string): boolean => url && IMAGES_URL_RE export const isJSResourceUrl = (url: string): boolean => url && RESOURCES_URL_REGEXP.test(url); export const isJSResource = (url: string): boolean => url?.startsWith(TB_RESOURCE_PREFIX); +export const isTbImage = (url: string): boolean => url?.startsWith(TB_IMAGE_PREFIX); export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => { const res = url.match(IMAGES_URL_REGEXP); diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index cf859e859f..feb4952236 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -18,7 +18,7 @@ import { BaseData, ExportableEntity } 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 { FcRuleNote, RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models'; import { ComponentClusteringMode, ComponentType } from '@shared/models/component-descriptor.models'; import { HasTenantId, HasVersion } from '@shared/models/entity.models'; @@ -39,6 +39,7 @@ export interface RuleChainMetaData extends HasVersion { firstNodeIndex?: number; nodes: Array; connections: Array; + notes?: Array; } export interface RuleChainImport { diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 30e1648f7e..1789e5c5d8 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -18,7 +18,7 @@ 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'; +import { FcEdge, FcNode, FcNote } from 'ngx-flowchart'; import { Observable } from 'rxjs'; import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, OnInit } from '@angular/core'; @@ -27,6 +27,7 @@ import { RuleChainType } from '@shared/models/rule-chain.models'; import { DebugRuleNodeEventBody } from '@shared/models/event.models'; import { EntityTestScriptResult, HasEntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { strings } from '@angular-devkit/core'; export interface RuleNodeConfiguration { [key: string]: any; @@ -358,6 +359,19 @@ export interface FcRuleEdge extends FcEdge { labels?: string[]; } +export const FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR = '#FFF9C4'; +export const FC_RULE_NOTE_DEFAULT_BORDER_WIDTH = 1; +export const FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE = true; + +export interface FcRuleNote extends FcNote { + content?: string; + backgroundColor?: string; + borderColor?: string; + borderWidth?: number; + applyDefaultMarkdownStyle?: boolean; + markdownCss?: string; +} + export enum ScriptLanguage { JS = 'JS', TBEL = 'TBEL' diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 323bd7687e..262ecfc62a 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -19,6 +19,11 @@ import { TenantId } from './id/tenant-id'; import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { QueueInfo } from '@shared/models/queue.models'; +import { FormControl } from '@angular/forms'; + +export type FormControlsFrom = { + [K in keyof T]-?: FormControl; +}; export enum TenantProfileType { DEFAULT = 'DEFAULT' @@ -101,6 +106,15 @@ export interface DefaultTenantProfileConfiguration { maxCalculatedFieldsPerEntity: number; maxArgumentsPerCF: number; + maxRelationLevelPerCfArgument: number; + minAllowedDeduplicationIntervalInSecForCF: number; + minAllowedAggregationIntervalInSecForCF: number; + maxRelatedEntitiesToReturnPerCfArgument: number; + minAllowedScheduledUpdateIntervalInSecForCF: number; + intermediateAggregationIntervalInSecForCF: number; + cfReevaluationCheckInterval: number; + alarmsReevaluationInterval: number; + maxDataPointsPerRollingArg: number; maxStateSizeInKBytes: number; maxSingleValueArgumentSizeInKBytes: number; @@ -165,6 +179,14 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxCalculatedFieldsPerEntity: 5, maxArgumentsPerCF: 10, maxDataPointsPerRollingArg: 1000, + maxRelationLevelPerCfArgument: 2, + minAllowedDeduplicationIntervalInSecForCF: 10, + minAllowedAggregationIntervalInSecForCF: 60, + maxRelatedEntitiesToReturnPerCfArgument: 100, + minAllowedScheduledUpdateIntervalInSecForCF: 10, + intermediateAggregationIntervalInSecForCF: 300, + cfReevaluationCheckInterval: 60, + alarmsReevaluationInterval: 60, maxStateSizeInKBytes: 32, maxSingleValueArgumentSizeInKBytes: 2, calculatedFieldDebugEventsRateLimit: '' diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 5fa8207eee..13baa42cb2 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -15,7 +15,7 @@ /// import { TimeService } from '@core/services/time.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils'; +import { deepClean, deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils'; import moment_ from 'moment'; import * as momentTz from 'moment-timezone'; import { IntervalType } from '@shared/models/telemetry/telemetry.models'; @@ -169,6 +169,7 @@ export interface Timewindow { history?: HistoryWindow; aggregation?: Aggregation; timezone?: string; + hideSaveAsDefault?: boolean; } export interface SubscriptionAggregation extends Aggregation { @@ -279,40 +280,29 @@ export const historyInterval = (timewindowMs: number): Timewindow => ({ } }); -export const defaultTimewindow = (timeService: TimeService): Timewindow => { +export const defaultTimewindow = (timeService: TimeService, isDashboard = false): Timewindow => { const currentTime = moment().valueOf(); return { - displayValue: '', - hideAggregation: false, - hideAggInterval: false, - hideTimezone: false, selectedTab: TimewindowType.REALTIME, realtime: { realtimeType: RealtimeWindowType.LAST_INTERVAL, - interval: SECOND, - timewindowMs: MINUTE, + interval: MINUTE, + timewindowMs: HOUR, quickInterval: QuickTimeInterval.CURRENT_DAY, - hideInterval: false, - hideLastInterval: false, - hideQuickInterval: false }, history: { historyType: HistoryWindowType.LAST_INTERVAL, - interval: SECOND, - timewindowMs: MINUTE, + interval: MINUTE, + timewindowMs: HOUR, fixedTimewindow: { startTimeMs: currentTime - DAY, endTimeMs: currentTime }, quickInterval: QuickTimeInterval.CURRENT_DAY, - hideInterval: false, - hideLastInterval: false, - hideFixedInterval: false, - hideQuickInterval: false }, aggregation: { - type: AggregationType.AVG, - limit: Math.floor(timeService.getMaxDatapointsLimit() / 2) + type: isDashboard ? AggregationType.NONE : AggregationType.AVG , + limit: isDashboard ? timeService.getMaxDatapointsLimit() : Math.floor(timeService.getMaxDatapointsLimit() / 2) } }; }; @@ -326,40 +316,51 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => { }; export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, - historyOnly: boolean, timeService: TimeService): Timewindow => { - const model = defaultTimewindow(timeService); + historyOnly: boolean, timeService: TimeService, hasAggregation: boolean, + isDashboard = false): Timewindow => { + const model = defaultTimewindow(timeService, isDashboard); if (value) { if (value.allowedAggTypes?.length) { model.allowedAggTypes = value.allowedAggTypes; } - model.hideAggregation = value.hideAggregation; - model.hideAggInterval = value.hideAggInterval; - model.hideTimezone = value.hideTimezone; + if (value.hideAggregation) { + model.hideAggregation = value.hideAggregation; + } + if (value.hideAggInterval) { + model.hideAggInterval = value.hideAggInterval; + } + if (value.hideTimezone) { + model.hideTimezone = value.hideTimezone; + } + if (value.hideSaveAsDefault) { + model.hideSaveAsDefault = value.hideSaveAsDefault; + } + model.selectedTab = getTimewindowType(value); // for backward compatibility - if (isDefinedAndNotNull((value as any).hideInterval)) { + if ((value as any).hideInterval) { model.realtime.hideInterval = (value as any).hideInterval; model.history.hideInterval = (value as any).hideInterval; delete (value as any).hideInterval; } - if (isDefinedAndNotNull((value as any).hideLastInterval)) { + if ((value as any).hideLastInterval) { model.realtime.hideLastInterval = (value as any).hideLastInterval; delete (value as any).hideLastInterval; } - if (isDefinedAndNotNull((value as any).hideQuickInterval)) { + if ((value as any).hideQuickInterval) { model.realtime.hideQuickInterval = (value as any).hideQuickInterval; delete (value as any).hideQuickInterval; } if (isDefined(value.realtime)) { - if (isDefinedAndNotNull(value.realtime.hideInterval)) { + if (value.realtime.hideInterval) { model.realtime.hideInterval = value.realtime.hideInterval; } - if (isDefinedAndNotNull(value.realtime.hideLastInterval)) { + if (value.realtime.hideLastInterval) { model.realtime.hideLastInterval = value.realtime.hideLastInterval; } - if (isDefinedAndNotNull(value.realtime.hideQuickInterval)) { + if (value.realtime.hideQuickInterval) { model.realtime.hideQuickInterval = value.realtime.hideQuickInterval; } if (value.realtime.disableCustomInterval) { @@ -393,16 +394,16 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO } } if (isDefined(value.history)) { - if (isDefinedAndNotNull(value.history.hideInterval)) { + if (value.history.hideInterval) { model.history.hideInterval = value.history.hideInterval; } - if (isDefinedAndNotNull(value.history.hideLastInterval)) { + if (value.history.hideLastInterval) { model.history.hideLastInterval = value.history.hideLastInterval; } - if (isDefinedAndNotNull(value.history.hideFixedInterval)) { + if (value.history.hideFixedInterval) { model.history.hideFixedInterval = value.history.hideFixedInterval; } - if (isDefinedAndNotNull(value.history.hideQuickInterval)) { + if (value.history.hideQuickInterval) { model.history.hideQuickInterval = value.history.hideQuickInterval; } if (value.history.disableCustomInterval) { @@ -451,7 +452,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO } model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); } - model.timezone = value.timezone; + if (value.timezone) { + model.timezone = value.timezone; + } } if (quickIntervalOnly) { model.realtime.realtimeType = RealtimeWindowType.INTERVAL; @@ -459,7 +462,7 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO if (historyOnly) { model.selectedTab = TimewindowType.HISTORY; } - return model; + return clearTimewindowConfig(model, quickIntervalOnly, historyOnly, hasAggregation); }; export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number, @@ -1108,19 +1111,97 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => { if (timewindow.allowedAggTypes?.length) { cloned.allowedAggTypes = timewindow.allowedAggTypes; } - cloned.hideAggregation = timewindow.hideAggregation || false; - cloned.hideAggInterval = timewindow.hideAggInterval || false; - cloned.hideTimezone = timewindow.hideTimezone || false; + if (timewindow.hideAggregation) { + cloned.hideAggregation = timewindow.hideAggregation; + } + if (timewindow.hideAggInterval) { + cloned.hideAggInterval = timewindow.hideAggInterval; + } + if (timewindow.hideTimezone) { + cloned.hideTimezone = timewindow.hideTimezone; + } + if (timewindow.hideSaveAsDefault) { + cloned.hideSaveAsDefault = timewindow.hideSaveAsDefault; + } if (isDefined(timewindow.selectedTab)) { cloned.selectedTab = timewindow.selectedTab; } - cloned.realtime = deepClone(timewindow.realtime); - cloned.history = deepClone(timewindow.history); - cloned.aggregation = deepClone(timewindow.aggregation); - cloned.timezone = timewindow.timezone; + if (isDefined(timewindow.realtime)) { + cloned.realtime = deepClone(timewindow.realtime); + } + if (isDefined(timewindow.history)) { + cloned.history = deepClone(timewindow.history); + } + if (isDefined(timewindow.aggregation)) { + cloned.aggregation = deepClone(timewindow.aggregation); + } + if (timewindow.timezone) { + cloned.timezone = timewindow.timezone; + } return cloned; }; +export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean, + historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => { + const noneAggregation = hasAggregation && timewindow.aggregation?.type === AggregationType.NONE; + if (timewindow.selectedTab === TimewindowType.REALTIME) { + if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) { + delete timewindow.realtime.timewindowMs; + } else { + delete timewindow.realtime.quickInterval; + } + + delete timewindow.history?.historyType; + delete timewindow.history?.timewindowMs; + delete timewindow.history?.fixedTimewindow; + delete timewindow.history?.quickInterval; + + delete timewindow.history?.interval; + if (!hasAggregation || noneAggregation) { + delete timewindow.realtime.interval; + } + } else { + if (timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { + delete timewindow.history.fixedTimewindow; + delete timewindow.history.quickInterval; + } else if (timewindow.history.historyType === HistoryWindowType.FIXED) { + delete timewindow.history.timewindowMs; + delete timewindow.history.quickInterval; + } else if (timewindow.history.historyType === HistoryWindowType.INTERVAL) { + delete timewindow.history.timewindowMs; + delete timewindow.history.fixedTimewindow; + } else { + delete timewindow.history.timewindowMs; + delete timewindow.history.fixedTimewindow; + delete timewindow.history.quickInterval; + } + + delete timewindow.realtime?.realtimeType; + delete timewindow.realtime?.timewindowMs; + delete timewindow.realtime?.quickInterval; + + delete timewindow.realtime?.interval; + if (!hasAggregation || noneAggregation) { + delete timewindow.history.interval; + } + } + + if (!hasAggregation) { + delete timewindow.aggregation; + } else if (!noneAggregation) { + delete timewindow.aggregation.limit; + } + + if (historyOnly) { + delete timewindow.realtime; + } + + if (!hasTimezone) { + delete timewindow.timezone; + } + return deepClean(timewindow); +}; + export interface TimeInterval { name: string; translateParams: {[key: string]: any}; @@ -1198,6 +1279,16 @@ export const defaultTimeIntervals = new Array( translateParams: {hours: 5}, value: 5 * HOUR }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 6}, + value: 6 * HOUR + }, + { + name: 'timeinterval.hours-interval', + translateParams: {hours: 8}, + value: 8 * HOUR + }, { name: 'timeinterval.hours-interval', translateParams: {hours: 10}, diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts index 98e801e632..7b74d282ba 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -14,7 +14,11 @@ /// limitations under the License. /// +import { UsersFilter } from '@shared/models/notification.models'; + export interface TwoFactorAuthSettings { + enforceTwoFa: boolean; + enforcedUsersFilter: UsersFilter; maxVerificationFailuresBeforeUserLockout: number; providers: Array; totalAllowedTimeForVerification: number; @@ -24,12 +28,18 @@ export interface TwoFactorAuthSettings { } export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{ + enforceTwoFa: boolean; + enforcedUsersFilter: UsersFilterWithFilterByTenant; providers: Array; verificationCodeCheckRateLimitEnable: boolean; verificationCodeCheckRateLimitNumber: number; verificationCodeCheckRateLimitTime: number; } +export interface UsersFilterWithFilterByTenant extends UsersFilter{ + filterByTenants?: boolean; +} + export type TwoFactorAuthProviderConfig = Partial; @@ -183,3 +193,61 @@ export const twoFactorAuthProvidersLoginData = new Map>( + [ + [ + TwoFactorAuthProviderType.TOTP, { + name: 'login.enable-authenticator-app', + description: 'login.enable-authenticator-app-description' + } + ], + [ + TwoFactorAuthProviderType.SMS, { + name: 'login.enable-authenticator-sms', + description: 'login.enable-authenticator-sms-description' + } + ], + [ + TwoFactorAuthProviderType.EMAIL, { + name: 'login.enable-authenticator-email', + description: 'login.enable-authenticator-email-description' + } + ], + [ + TwoFactorAuthProviderType.BACKUP_CODE, { + name: 'security.2fa.provider.backup_code', + description: 'login.backup-code-auth-description' + } + ] + ] +); + +export const twoFactorAuthProvidersSuccessCardTranslate = new Map>( + [ + [ + TwoFactorAuthProviderType.TOTP, { + name: 'login.authenticator-app-success', + description: 'login.authenticator-app-success-description' + } + ], + [ + TwoFactorAuthProviderType.SMS, { + name: 'login.authenticator-sms-success', + description: 'login.authenticator-sms-success-description' + } + ], + [ + TwoFactorAuthProviderType.EMAIL, { + name: 'login.authenticator-email-success', + description: 'login.authenticator-email-success-description' + } + ], + [ + TwoFactorAuthProviderType.BACKUP_CODE, { + name: 'login.authenticator-backup-code-success', + description: 'login.authenticator-backup-code-success-description' + } + ] + ] +); diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index d3a27d1442..be4300fea2 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -265,4 +265,4 @@ export interface EntityDataInfo { hasCalculatedFields: boolean; } -export const typesWithCalculatedFields = new Set([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE]); +export const typesWithCalculatedFields = new Set([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE, EntityType.CUSTOMER]); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 43ef0d7df6..982f492eba 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -24,6 +24,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { AlarmFilter, AlarmFilterConfig, + ComplexOperation, EntityDataPageLink, EntityFilter, KeyFilter @@ -33,7 +34,7 @@ import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, Inject, OnInit, Type } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { AbstractControl, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormGroup, ValidatorFn } from '@angular/forms'; import { Observable } from 'rxjs'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; @@ -51,6 +52,7 @@ import { TbFunction } from '@shared/models/js-function.models'; import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TbUnit } from '@shared/models/unit.models'; +import { ImageResourceInfo } from '@shared/models/resource.models'; export enum widgetType { timeseries = 'timeseries', @@ -71,6 +73,8 @@ export interface WidgetTypeData { template: WidgetTypeTemplate; } +export const widgetTitleAutocompleteValues = ['entityName', 'entityLabel']; + export const widgetTypesData = new Map( [ [ @@ -398,7 +402,6 @@ export interface DataKey extends KeyInfo { inLegend?: boolean; isAdditional?: boolean; origDataKeyIndex?: number; - _hash?: number; } export type CellClickColumnInfo = Pick; @@ -448,6 +451,7 @@ export interface Datasource { origDatasourceIndex?: number; pageLink?: EntityDataPageLink; keyFilters?: Array; + keyFiltersOperation?: ComplexOperation; entityFilter?: EntityFilter; alarmFilterConfig?: AlarmFilterConfig; alarmFilter?: AlarmFilter; @@ -489,6 +493,14 @@ export const targetDeviceValid = (targetDevice?: TargetDevice): boolean => ((targetDevice.type === TargetDeviceType.device && !!targetDevice.deviceId) || (targetDevice.type === TargetDeviceType.entity && !!targetDevice.entityAliasId)); +export const widgetTypeHasTimewindow = (type: widgetType): boolean => { + return type === widgetType.timeseries || type === widgetType.alarm; +} + +export const widgetTypeCanHaveTimewindow = (type: widgetType): boolean => { + return widgetTypeHasTimewindow(type) || type === widgetType.latest; +} + export const datasourcesHasAggregation = (datasources?: Array): boolean => { if (datasources) { const foundDatasource = datasources.find(datasource => { @@ -622,11 +634,36 @@ export enum WidgetMobileActionType { deviceProvision = 'deviceProvision', } +export interface ActionConfig { + title: string, + formControlName: string, + functionName: string, + functionArgs: string[], + helpId?: string +} + +export enum ProvisionType { + auto = 'auto', + wiFi = 'wiFi', + ble = 'ble', + softAp = 'softAp' +} + +export const provisionTypeTranslationMap = new Map( + [ + [ ProvisionType.auto, 'widget-action.mobile.auto' ], + [ ProvisionType.wiFi, 'widget-action.mobile.wi-fi' ], + [ ProvisionType.ble, 'widget-action.mobile.ble' ], + [ ProvisionType.softAp, 'widget-action.mobile.soft-ap' ], + ] +); + export enum MapItemType { marker = 'marker', polygon = 'polygon', rectangle = 'rectangle', - circle = 'circle' + circle = 'circle', + polyline = 'polyline' } export const widgetActionTypes = Object.keys(WidgetActionType) @@ -666,6 +703,7 @@ export const mapItemTypeTranslationMap = new Map( [ MapItemType.polygon, 'widget-action.map-item.polygon' ], [ MapItemType.rectangle, 'widget-action.map-item.rectangle' ], [ MapItemType.circle, 'widget-action.map-item.circle' ], + [ MapItemType.polyline, 'widget-action.map-item.polyline' ] ] ) @@ -675,6 +713,7 @@ export interface MobileLaunchResult { export interface MobileImageResult { imageUrl: string; + imageInfo?: ImageResourceInfo; } export interface MobileQrCodeResult { @@ -706,10 +745,12 @@ export interface WidgetMobileActionResult { export interface ProvisionSuccessDescriptor { handleProvisionSuccessFunction: TbFunction; + provisionType?: string; } export interface ProcessImageDescriptor { processImageFunction: TbFunction; + saveToGallery?: boolean; } export interface ProcessLaunchResultDescriptor { @@ -743,6 +784,7 @@ export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescript type: WidgetMobileActionType; handleErrorFunction?: TbFunction; handleEmptyResultFunction?: TbFunction; + handleNonMobileFallbackFunction?: TbFunction; } export interface CustomActionDescriptor { @@ -789,6 +831,8 @@ export interface MapItemTooltips { finishRect?: string; startCircle?: string; finishCircle?: string; + startPolyline?: string; + finishPolyline?: string; } export const mapItemTooltipsTranslation: Required = Object.freeze({ @@ -799,7 +843,9 @@ export const mapItemTooltipsTranslation: Required = Object.free startRect: 'widgets.maps.data-layer.polygon.rectangle-place-first-point-hint', finishRect: 'widgets.maps.data-layer.polygon.finish-rectangle-hint', startCircle: 'widgets.maps.data-layer.circle.place-circle-center-hint', - finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint' + finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint', + startPolyline: 'widgets.maps.data-layer.polyline.polyline-place-first-point-hint', + finishPolyline: 'widgets.maps.data-layer.polyline.finish-polyline-hint' }) export interface WidgetActionDescriptor extends WidgetAction { @@ -1108,5 +1154,4 @@ export abstract class WidgetSettingsComponent extends PageComponent implements protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { } - } diff --git a/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts b/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts new file mode 100644 index 0000000000..fbbcbd7105 --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts @@ -0,0 +1,94 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; +import { FilterInfo, Filters } from '@shared/models/query/query.models'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models'; +import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; +import { + ApiUsageWidgetSettings, + getUniqueDataKeys +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; + +interface AliasFilterPair { + alias?: EntityAliasInfo; + filter?: FilterInfo; +} + +interface ApiUsageDatasourcesInfo { + ds?: AliasFilterPair; +} + +export const ApiUsageModelDefinition: WidgetModelDefinition = { + testWidget(widget: Widget): boolean { + if (widget?.config?.settings) { + const settings = widget.config.settings; + if (settings.apiUsageDataKeys && Array.isArray(settings.apiUsageDataKeys)) { + return true; + } + } + return false; + }, + prepareExportInfo(dashboard: Dashboard, widget: Widget): ApiUsageDatasourcesInfo { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + const info: ApiUsageDatasourcesInfo = {}; + if (settings.dsEntityAliasId) { + info.ds = prepareExportDataSourcesInfo(dashboard, settings.dsEntityAliasId); + } + return info; + }, + updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: ApiUsageDatasourcesInfo): void { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + if (info?.ds?.alias) { + settings.dsEntityAliasId = getEntityAliasId(entityAliases, info.ds.alias); + } + }, + datasources(widget: Widget): Datasource[] { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + const datasources: Datasource[] = []; + if (settings.apiUsageDataKeys?.length && settings.dsEntityAliasId) { + datasources.push({ + type: DatasourceType.entity, + name: '', + entityAliasId: settings.dsEntityAliasId, + dataKeys: getUniqueDataKeys(settings.apiUsageDataKeys) + }); + } + return datasources; + }, + hasTimewindow(): boolean { + return false; + }, + datasourcesHasAggregation(): boolean { + return false; + }, + datasourcesHasOnlyComparisonAggregation(): boolean { + return false; + } +}; + +const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: string): AliasFilterPair => { + const aliasAndFilter: AliasFilterPair = {}; + const entityAlias = dashboard.configuration.entityAliases[settings]; + if (entityAlias) { + aliasAndFilter.alias = { + alias: entityAlias.alias, + filter: entityAlias.filter + }; + } + return aliasAndFilter; +} diff --git a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts index 4505b14771..38cc0b52f4 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts @@ -17,10 +17,18 @@ import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; import { Dashboard } from '@shared/models/dashboard.models'; -import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models'; import { + Datasource, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, + DatasourceType, + Widget +} from '@shared/models/widget.models'; +import { + additionalMapDataSourcesToDatasources, BaseMapSettings, MapDataLayerSettings, + MapDataLayerType, MapDataSourceSettings, mapDataSourceSettingsToDatasource, MapType @@ -121,9 +129,26 @@ export const MapModelDefinition: WidgetModelDefinition = { datasources.push(...getMapDataLayersDatasources(settings.circles)); } if (settings.additionalDataSources?.length) { - datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources)); + datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); } return datasources; + }, + hasTimewindow(widget: Widget): boolean { + const settings: BaseMapSettings = widget.config.settings as BaseMapSettings; + if (settings.trips?.length) { + return true; + } else { + const datasources: Datasource[] = getMapLatestDataLayersDatasources(settings); + return datasourcesHasAggregation(datasources); + } + }, + datasourcesHasAggregation(widget: Widget): boolean { + const datasources: Datasource[] = getMapLatestDataLayersDatasources(widget.config.settings as BaseMapSettings); + return datasourcesHasAggregation(datasources); + }, + datasourcesHasOnlyComparisonAggregation(widget: Widget): boolean { + const datasources: Datasource[] = getMapLatestDataLayersDatasources(widget.config.settings as BaseMapSettings); + return datasourcesHasOnlyComparisonAggregation(datasources); } }; @@ -211,15 +236,38 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource } } -const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => { +const getMapDataLayersDatasources = (settings: MapDataLayerSettings[], + includeDataKeys = false, dataLayerType?: MapDataLayerType): Datasource[] => { const datasources: Datasource[] = []; settings.forEach((dsSettings) => { - datasources.push(mapDataSourceSettingsToDatasource(dsSettings)); - if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) { - (dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => { - datasources.push(mapDataSourceSettingsToDatasource(ds)); + const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings, null, includeDataKeys, dataLayerType); + datasources.push(datasource); + if (dsSettings.additionalDataSources?.length) { + dsSettings.additionalDataSources.forEach((ds) => { + const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds); + if (includeDataKeys) { + additionalDatasource.dataKeys.push(...datasource.dataKeys); + } + datasources.push(additionalDatasource); }); } }); return datasources; }; + +const getMapLatestDataLayersDatasources = (settings: BaseMapSettings): Datasource[] => { + const datasources: Datasource[] = []; + if (settings.markers?.length) { + datasources.push(...getMapDataLayersDatasources(settings.markers, true, 'markers')); + } + if (settings.polygons?.length) { + datasources.push(...getMapDataLayersDatasources(settings.polygons, true, 'polygons')); + } + if (settings.circles?.length) { + datasources.push(...getMapDataLayersDatasources(settings.circles, true, 'circles')); + } + if (settings.additionalDataSources?.length) { + datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); + } + return datasources; +}; diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 6db3b1481b..69f6fd02f1 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -24,6 +24,7 @@ import { } from '@shared/models/widget.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { + deepClone, guid, hashCode, isDefinedAndNotNull, @@ -61,19 +62,47 @@ export interface TbMapDatasource extends Datasource { mapDataIds: string[]; } -export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings, id = guid()): TbMapDatasource => { +export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings | MapDataLayerSettings, + id = guid(), + includeDataKeys = false, dataLayerType?: MapDataLayerType): TbMapDatasource => { + const dataKeys = includeDataKeys ? mapDataLayerDatasourceDataKeys((settings as MapDataLayerSettings), dataLayerType) : []; return { type: settings.dsType, name: settings.dsLabel, deviceId: settings.dsDeviceId, entityAliasId: settings.dsEntityAliasId, filterId: settings.dsFilterId, - dataKeys: [], + dataKeys: dataKeys, latestDataKeys: [], mapDataIds: [id] }; }; +const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, + dataLayerType: MapDataLayerType): DataKey[] => { + const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; + switch (dataLayerType) { + case 'trips': + const tripsSettings = settings as TripsDataLayerSettings; + dataKeys.push(tripsSettings.xKey, tripsSettings.yKey); + break; + case 'markers': + const markersSettings = settings as MarkersDataLayerSettings; + dataKeys.push(markersSettings.xKey, markersSettings.yKey); + break; + case 'polygons': + dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey); + break; + case 'circles': + dataKeys.push((settings as CirclesDataLayerSettings).circleKey); + break; + case 'polylines': + dataKeys.push((settings as PolylinesDataLayerSettings).polylineKey); + break; + } + return dataKeys; +}; + export enum DataLayerPatternType { pattern = 'pattern', @@ -133,6 +162,7 @@ export interface DataLayerEditSettings { snappable: boolean; } + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataSources?: MapDataSourceSettings[]; additionalDataKeys?: DataKey[]; @@ -140,7 +170,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings { tooltip: DataLayerTooltipSettings; click: WidgetAction; groups?: string[]; - edit: DataLayerEditSettings; + edit: DataLayerEditSettings; } export const defaultBaseDataLayerSettings = (mapType: MapType): Partial => ({ @@ -156,7 +186,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details' - : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', offsetX: 0, offsetY: -1 }, @@ -170,9 +200,9 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial { if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) { @@ -196,7 +226,7 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData case 'markers': const markersDataLayer = dataLayer as MarkersDataLayerSettings; if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name || - !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { + !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { return false; } break; @@ -206,6 +236,12 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData return false; } break; + case 'polylines': + const polylinesDataLayer = dataLayer as PolylinesDataLayerSettings; + if (!polylinesDataLayer.polylineKey?.type || !polylinesDataLayer.polylineKey?.name) { + return false; + } + break; case 'circles': const circlesDataLayer = dataLayer as CirclesDataLayerSettings; if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) { @@ -274,6 +310,7 @@ export interface MarkerIconSettings extends BaseMarkerShapeSettings { iconContainer?: MarkerIconContainer; icon: string; } + export interface MarkerClusteringSettings { enable: boolean; zoomOnClick: boolean; @@ -574,8 +611,11 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial< color: '#3388ff', }, strokeWeight: 3 -} as Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) export interface CirclesDataLayerSettings extends ShapeDataLayerSettings { circleKey: DataKey; @@ -625,8 +665,45 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) + +export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings { + polylineKey: DataKey; +} + +export const defaultPolylinesDataLayerSettings = (mapType: MapType, functionsOnly = false): PolylinesDataLayerSettings => mergeDeep({ + dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, + dsLabel: functionsOnly ? 'First polyline' : '', + polylineKey: { + name: functionsOnly ? 'f(x)' : 'perimeter', + label: 'perimeter', + type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + settings: {}, + color: materialColors[0].value + } +} as PolylinesDataLayerSettings, defaultBasePolylinesDataLayerSettings(mapType) as PolylinesDataLayerSettings); + +export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ + fillType: ShapeFillType.color, + fillColor: { + type: DataLayerColorType.constant, + color: 'rgba(51,136,255,0.2)', + }, + strokeColor: { + type: DataLayerColorType.constant, + color: '#3388ff', + }, + strokeWeight: 3 + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) + export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { switch (dataLayerType) { @@ -638,6 +715,8 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map return defaultPolygonsDataLayerSettings(mapType, functionsOnly); case 'circles': return defaultCirclesDataLayerSettings(mapType, functionsOnly); + case 'polylines': + return defaultPolylinesDataLayerSettings(mapType, functionsOnly); } }; @@ -651,6 +730,8 @@ export const defaultBaseMapDataLayerSettings = ( return defaultBasePolygonsDataLayerSettings(mapType) as T; case 'circles': return defaultBaseCirclesDataLayerSettings(mapType) as T; + case 'polylines': + return defaultBasePolylinesDataLayerSettings(mapType) as T; } } @@ -658,10 +739,11 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings { dataKeys: DataKey[]; } -export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => { +export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[], + includeDataKeys = true): TbMapDatasource[] => { return additionalMapDataSources.map(addDs => { const res = mapDataSourceSettingsToDatasource(addDs); - res.dataKeys = addDs.dataKeys; + res.dataKeys = includeDataKeys ? addDs.dataKeys : []; return res; }); }; @@ -700,13 +782,13 @@ export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSource }; export const additionalMapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const dataSource: AdditionalMapDataSourceSettings = control.value; - if (!additionalMapDataSourceValid(dataSource)) { - return { - dataSource: true - }; - } - return null; + const dataSource: AdditionalMapDataSourceSettings = control.value; + if (!additionalMapDataSourceValid(dataSource)) { + return { + dataSource: true + }; + } + return null; }; export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => { @@ -788,6 +870,7 @@ export interface BaseMapSettings { markers: MarkersDataLayerSettings[]; polygons: PolygonsDataLayerSettings[]; circles: CirclesDataLayerSettings[]; + polylines: PolylinesDataLayerSettings[]; additionalDataSources: AdditionalMapDataSourceSettings[]; controlsPosition: MapControlsPosition; zoomActions: MapZoomAction[]; @@ -812,6 +895,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { markers: [], polygons: [], circles: [], + polylines: [], additionalDataSources: [], controlsPosition: MapControlsPosition.topleft, zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], @@ -845,6 +929,7 @@ export const defaultMapActionButtonSettings: MapActionButtonSettings = { } export enum MapProvider { + openfreemap = 'openfreemap', openstreet = 'openstreet', google = 'google', here = 'here', @@ -856,6 +941,7 @@ export const mapProviders = Object.keys(MapProvider) as MapProvider[]; export const mapProviderTranslationMap = new Map( [ + [MapProvider.openfreemap, 'widgets.maps.layer.provider.openfreemap.title'], [MapProvider.openstreet, 'widgets.maps.layer.provider.openstreet.title'], [MapProvider.google, 'widgets.maps.layer.provider.google.title'], [MapProvider.here, 'widgets.maps.layer.provider.here.title'], @@ -891,6 +977,9 @@ export const mapLayerValid = (layer: MapLayerSettings): boolean => { return false; } switch (layer.provider) { + case MapProvider.openfreemap: + const openFreeMapLayer = layer as OpenFreeMapLayerSettings; + return !!openFreeMapLayer.layerType; case MapProvider.openstreet: const openStreetLayer = layer as OpenStreetMapLayerSettings; return !!openStreetLayer.layerType; @@ -924,6 +1013,9 @@ export const defaultLayerTitle = (layer: MapLayerSettings): string => { return null; } switch (layer.provider) { + case MapProvider.openfreemap: + const ofmLayer = layer as OpenFreeMapLayerSettings; + return openFreeMapStyleTranslationMap.get(ofmLayer.layerType); case MapProvider.openstreet: const openStreetLayer = layer as OpenStreetMapLayerSettings; return openStreetMapLayerTranslationMap.get(openStreetLayer.layerType); @@ -975,6 +1067,32 @@ export const defaultOpenStreetMapLayerSettings: OpenStreetMapLayerSettings = { layerType: OpenStreetLayerType.openStreetMapnik } +export enum OpenFreeMapStyleType { + bright = 'bright', + positron = 'positron', + liberty = 'liberty' +} + +export const openFreeMapStyleTypes = Object.values(OpenFreeMapStyleType) as OpenFreeMapStyleType[]; + +export const openFreeMapStyleTranslationMap = new Map( + [ + [OpenFreeMapStyleType.liberty, 'widgets.maps.layer.provider.openfreemap.liberty'], + [OpenFreeMapStyleType.bright, 'widgets.maps.layer.provider.openfreemap.bright'], + [OpenFreeMapStyleType.positron, 'widgets.maps.layer.provider.openfreemap.positron'] + ] +); + +export interface OpenFreeMapLayerSettings extends MapLayerSettings { + provider: MapProvider.openfreemap; + layerType: OpenFreeMapStyleType; +} + +export const defaultOpenFreeMapLayerSettings: OpenFreeMapLayerSettings = { + provider: MapProvider.openfreemap, + layerType: OpenFreeMapStyleType.bright +} + export enum GoogleLayerType { roadmap = 'roadmap', satellite = 'satellite', @@ -1064,6 +1182,8 @@ export const defaultTencentMapLayerSettings: TencentMapLayerSettings = { export interface CustomMapLayerSettings extends MapLayerSettings { provider: MapProvider.custom; tileUrl: string; + vectorTiles?: boolean; + customAttribution?: string; } export const defaultCustomMapLayerSettings: CustomMapLayerSettings = { @@ -1073,6 +1193,8 @@ export const defaultCustomMapLayerSettings: CustomMapLayerSettings = { export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings => { switch (provider) { + case MapProvider.openfreemap: + return defaultOpenFreeMapLayerSettings; case MapProvider.openstreet: return defaultOpenStreetMapLayerSettings; case MapProvider.google: @@ -1089,9 +1211,9 @@ export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings export const defaultMapLayers: MapLayerSettings[] = [ { label: '{i18n:widgets.maps.layer.roadmap}', - provider: MapProvider.openstreet, - layerType: OpenStreetLayerType.openStreetMapnik, - } as OpenStreetMapLayerSettings, + provider: MapProvider.openfreemap, + layerType: OpenFreeMapStyleType.bright, + } as OpenFreeMapLayerSettings, { label: '{i18n:widgets.maps.layer.satellite}', provider: MapProvider.openstreet, @@ -1218,6 +1340,12 @@ export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][] export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; export type TbPolygonCoordinates = TbPolygonCoordinate[]; +export type TbPolylineRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][]; +export type TbPolylineRawCoordinates = TbPolylineRawCoordinate[]; +export type TbPolylineData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; +export type TbPolylineCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; +export type TbPolylineCoordinates = TbPolylineCoordinate[]; + export interface TbCircleData { latitude: number; longitude: number; diff --git a/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts index d042987223..04c44874a1 100644 --- a/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts @@ -14,23 +14,60 @@ /// limitations under the License. /// -import { Datasource, Widget } from '@shared/models/widget.models'; +import { + Datasource, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, + Widget, + widgetType, + widgetTypeHasTimewindow +} from '@shared/models/widget.models'; import { Dashboard } from '@shared/models/dashboard.models'; import { EntityAliases } from '@shared/models/alias.models'; import { Filters } from '@shared/models/query/query.models'; import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition'; +import { ApiUsageModelDefinition } from '@shared/models/widget/home-widgets/api-usage-model.definition'; export interface WidgetModelDefinition { testWidget(widget: Widget): boolean; prepareExportInfo(dashboard: Dashboard, widget: Widget): T; updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void; datasources(widget: Widget): Datasource[]; + hasTimewindow(widget: Widget): boolean; + datasourcesHasAggregation(widget: Widget): boolean; + datasourcesHasOnlyComparisonAggregation(widget: Widget): boolean; } const widgetModelRegistry: WidgetModelDefinition[] = [ - MapModelDefinition + MapModelDefinition, + ApiUsageModelDefinition ]; export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => { return widgetModelRegistry.find(def => def.testWidget(widget)); } + +export const widgetHasTimewindow = (widget: Widget): boolean => { + const widgetDefinition = findWidgetModelDefinition(widget); + if (widgetDefinition) { + return widgetDefinition.hasTimewindow(widget); + } + return widgetTypeHasTimewindow(widget.type) + || (widget.type === widgetType.latest && datasourcesHasAggregation(widget.config.datasources)); +}; + +export const widgetDatasourcesHasAggregation = (widget: Widget): boolean => { + const widgetDefinition = findWidgetModelDefinition(widget); + if (widgetDefinition) { + return widgetDefinition.datasourcesHasAggregation(widget); + } + return widget.type === widgetType.latest && datasourcesHasAggregation(widget.config.datasources); +}; + +export const widgetDatasourcesHasOnlyComparisonAggregation = (widget: Widget): boolean => { + const widgetDefinition = findWidgetModelDefinition(widget); + if (widgetDefinition) { + return widgetDefinition.datasourcesHasOnlyComparisonAggregation(widget); + } + return widget.type === widgetType.latest && datasourcesHasOnlyComparisonAggregation(widget.config.datasources); +}; diff --git a/ui-ngx/src/app/shared/pipe/date-expiration.pipe.ts b/ui-ngx/src/app/shared/pipe/date-expiration.pipe.ts new file mode 100644 index 0000000000..faaafc92e4 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/date-expiration.pipe.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES 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 { DatePipe } from '@angular/common'; +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { isDefined } from '@core/utils'; + +@Pipe({ + name: 'dateExpiration', + standalone: false +}) +export class DateExpirationPipe implements PipeTransform { + + constructor(private millisecondsToTimeString: MillisecondsToTimeStringPipe, private datePipe: DatePipe) { + } + + transform(expirationMs: number, arg?: any): string { + const displayDate = isDefined(arg?.displayDate) ? arg.displayDate : true; + const dateFormat = isDefined(arg?.dateFormat) ? arg.dateFormat : ' (dd/MM/yyyy)'; + const shortFormat = isDefined(arg?.shortFormat) ? arg.shortFormat : true; + const onlyFirstDigit = isDefined(arg?.onlyFirstDigit) ? arg.onlyFirstDigit : true; + let time = this.millisecondsToTimeString.transform(expirationMs, shortFormat, onlyFirstDigit); + if (displayDate) { + const exactDate = this.datePipe.transform(expirationMs + Date.now(), dateFormat); + time += exactDate; + } + return time; + } +} diff --git a/ui-ngx/src/app/shared/pipe/short-number.pipe.ts b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts index 0fda04844f..fedf958067 100644 --- a/ui-ngx/src/app/shared/pipe/short-number.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts @@ -16,20 +16,26 @@ import { Pipe, PipeTransform } from '@angular/core'; +export interface ShortNumberArgs { + long?: boolean; + roundDown?: boolean; +} + @Pipe({ - name: 'shortNumber', - standalone: false + name: 'shortNumber', + standalone: false }) export class ShortNumberPipe implements PipeTransform { - transform(number: number, args?: any): any { - if (isNaN(number)) return 0; - if (number === null) return 0; - if (number === 0) return 0; + transform(number: number, args?: ShortNumberArgs): string { + if (!Number.isFinite(number) || number === 0) { + return '0'; + } let abs = Math.abs(number); const rounder = Math.pow(10, 1); const isNegative = number < 0; - const isLong = args && args.long; + const isLong = args?.long; + const roundDown = args?.roundDown; let key = ''; const powers = [ @@ -42,7 +48,7 @@ export class ShortNumberPipe implements PipeTransform { for (let i = 0; i < powers.length; i++) { let reduced = abs / powers[i].value; - reduced = Math.round(reduced * rounder) / rounder; + reduced = (roundDown ? Math.floor : Math.round)(reduced * rounder) / rounder; if (reduced >= 1) { abs = reduced; key = isLong ? powers[i].longKey : powers[i].key; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index c3791c1332..cefc1c8824 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -220,15 +220,28 @@ import { CountryData } from '@shared/models/country.models'; import { SvgXmlComponent } from '@shared/components/svg-xml.component'; import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component'; import { AggregationTypeSelectComponent } from '@shared/components/time/aggregation/aggregation-type-select.component'; -import { AggregationOptionsConfigPanelComponent } from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; +import { + AggregationOptionsConfigPanelComponent +} from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; import { IntervalOptionsConfigPanelComponent } from '@shared/components/time/interval-options-config-panel.component'; -import { GroupingIntervalOptionsComponent } from '@shared/components/time/aggregation/grouping-interval-options.component'; +import { + GroupingIntervalOptionsComponent +} from '@shared/components/time/aggregation/grouping-interval-options.component'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.component'; import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; +import { PasswordRequirementsTooltipComponent } from '@shared/components/password-requirements-tooltip.component'; +import { StringPatternAutocompleteComponent } from '@shared/components/string-pattern-autocomplete.component'; +import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component'; +import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe'; +import { EntityLimitExceededDialogComponent } from '@shared/components/dialog/entity-limit-exceeded-dialog.component'; +import { DynamicMatDialogModule } from '@shared/components/dialog/dynamic/dynamic-dialog.module'; import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle'; +import { RgbaInputComponent } from '@shared/components/color-picker/rgba-input.component'; +import { HslaInputComponent } from '@shared/components/color-picker/hsla-input.component'; +import { InputChangeDirective } from '@shared/components/color-picker/input-change.directive'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -237,6 +250,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @NgModule({ providers: [ DatePipe, + SelectableColumnsPipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, HighlightPipe, @@ -373,6 +387,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TodoDialogComponent, ColorPickerDialogComponent, MaterialIconsDialogComponent, + EntityLimitExceededDialogComponent, ColorInputComponent, MaterialIconSelectComponent, NodeScriptTestDialogComponent, @@ -400,6 +415,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ShortNumberPipe, SelectableColumnsPipe, KeyboardShortcutPipe, + DateExpirationPipe, TbJsonToStringDirective, JsonObjectEditDialogComponent, HistorySelectorComponent, @@ -451,6 +467,12 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + PasswordRequirementsTooltipComponent, + TimeUnitInputComponent, + StringPatternAutocompleteComponent, + RgbaInputComponent, + HslaInputComponent, + InputChangeDirective ], imports: [ CommonModule, @@ -513,7 +535,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) useFactory: MarkedOptionsFactory, deps: [MarkedOptionsService] } - }) + }), + DynamicMatDialogModule ], exports: [ FooterComponent, @@ -638,6 +661,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TodoDialogComponent, ColorPickerDialogComponent, MaterialIconsDialogComponent, + EntityLimitExceededDialogComponent, ColorInputComponent, MaterialIconSelectComponent, NodeScriptTestDialogComponent, @@ -665,6 +689,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) SafePipe, ShortNumberPipe, SelectableColumnsPipe, + DateExpirationPipe, RouterModule, TranslateModule, JsonObjectEditDialogComponent, @@ -715,6 +740,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + PasswordRequirementsTooltipComponent, + TimeUnitInputComponent, + StringPatternAutocompleteComponent, + DynamicMatDialogModule ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 9738e9ac0f..b5e26f6ec8 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -25,8 +25,7 @@ "useCellStyleFunction": false, "useCellContentFunction": true, "cellContentFunction": "return JSON.parse(value).ruleChainName;" - }, - "_hash": 0.9954481282345906 + } }, { "name": "ruleEngineException", @@ -37,8 +36,7 @@ "useCellStyleFunction": false, "useCellContentFunction": true, "cellContentFunction": "return JSON.parse(value).ruleNodeName;" - }, - "_hash": 0.18580357036589978 + } }, { "name": "ruleEngineException", @@ -49,8 +47,7 @@ "useCellStyleFunction": false, "useCellContentFunction": true, "cellContentFunction": "return JSON.parse(value).message;" - }, - "_hash": 0.7255162989552142 + } } ], "alarmFilterConfig": { @@ -64,22 +61,19 @@ "type": "entityField", "label": "Queue name", "color": "#ffc107", - "settings": {}, - "_hash": 0.6889245277142959 + "settings": {} }, { "name": "serviceId", "type": "entityField", "label": "Service Id", "color": "#607d8b", - "settings": {}, - "_hash": 0.7972226575450785 + "settings": {} } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, "selectedTab": 0, @@ -99,10 +93,14 @@ "settings": { "showTimestamp": true, "displayPagination": true, - "defaultPageSize": 10 + "defaultPageSize": 10, + "sortOrder": { + "property": "name", + "direction": "ASC" + } }, "title": "{i18n:api-usage.exceptions}", - "dropShadow": true, + "dropShadow": false, "enableFullscreen": true, "titleStyle": { "fontSize": "16px", @@ -121,395 +119,1772 @@ "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "", - "configMode": "basic" + "configMode": "basic", + "borderRadius": "12px", + "titleColor": "rgba(0, 0, 0, 0.54)", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": "query_builder", + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + } }, "id": "a669cf86-e715-efa4-dd9a-b839abf499e9", "typeFullFqn": "system.cards.timeseries_table" }, - "aab68ab5-8e40-8694-c55c-8eb1c89b88fb": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, + "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", "dataKeys": [ { - "name": "transportMsgLimit", + "name": "successfulMsgs", "type": "timeseries", - "label": "limit", + "label": "{i18n:api-usage.successful}", "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "transportMsgCount", + "name": "failedMsgs", "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, + "label": "{i18n:api-usage.permanent-failures}", + "color": "#ef5350", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;\n", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "transportDataPointsLimit", + "name": "tmpFailed", "type": "timeseries", - "label": "pointsLimit", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.22082255831864894, + "label": "{i18n:api-usage.processing-failures}", + "color": "#ffc107", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#ffc107", + "settings": {} }, { - "name": "transportDataPointsCount", - "type": "timeseries", - "label": "pointsCount", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.6340356364819146, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "title", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.6894070537030252, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.transport}\";" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.430957831457494, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.662147926074595, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.messages}';" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "pointsUnit", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.44620898738917947, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.data-points}';" + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#607d8b", + "settings": {} } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "displayValue": "", + "hideAggregation": false, + "hideAggInterval": false, "selectedTab": 0, "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 3600000, + "interval": 1000 }, "aggregation": { - "type": "AVG", - "limit": 25000 + "type": "NONE", + "limit": 10000 } }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "#FFFFFF", "color": "rgba(0, 0, 0, 0.87)", "padding": "0px", "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\nconst [apiUsageBar2, apiUsagePercent2, apiUsageValue2] = calculateBarValues(data[0].pointsCount, data[0].pointsLimit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `${data[0].title}` +\n '
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].pointsUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent2}
` +\n '
' +\n `
${apiUsageValue2}
` +\n '
' +\n '
' + \n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "Transport", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "transport_details", - "icon": "insert_chart", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "transport", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "a60e09be-1bea-dfc3-6abb-f87e73256899" - } - ] - } - }, - "row": 0, - "col": 0, - "id": "aab68ab5-8e40-8694-c55c-8eb1c89b88fb" - }, - "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "ruleEngineExecutionLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "ruleEngineExecutionCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.3551317421302518, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.rule-engine}\";" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" }, - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.5100381746798048, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.executions}\";" - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
${title}
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "Rule Engine execution", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": false, + "relativeWidth": 2, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.54)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipHideZeroValues": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 300, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "12px", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)" + }, + "title": "{i18n:api-usage.queue-stats}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + }, + "titleColor": "rgba(0, 0, 0, 0.54)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "fa938580-33db-f1b3-fafc-bc3e3784ad57" + }, + "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", + "dataKeys": [ + { + "name": "timeoutMsgs", + "type": "timeseries", + "label": "{i18n:api-usage.permanent-timeouts}", + "color": "#4caf50", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "tmpTimeout", + "type": "timeseries", + "label": "{i18n:api-usage.processing-timeouts}", + "color": "#9c27b0", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#f44336", + "settings": {} + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#ffc107", + "settings": {} + } + ] + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "selectedTab": 0, + "realtime": { + "timewindowMs": 3600000, + "interval": 1000 + }, + "aggregation": { + "type": "NONE", + "limit": 10000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": false, + "relativeWidth": 2, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.54)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipHideZeroValues": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 300, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "12px", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)" + }, + "title": "{i18n:api-usage.processing-failures-and-timeouts}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + }, + "titleColor": "rgba(0, 0, 0, 0.54)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "2ee89893-4e38-5331-95b7-3fd4f310c5a7" + }, + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-messages}", + "color": "#2196f3", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": false, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 50000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": null, + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.54)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px", + "tooltipStackedShowTotal": false + }, + "title": "{i18n:api-usage.transport-messages-hourly-activity}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + }, + "titleColor": "rgba(0, 0, 0, 0.54)", + "titleTooltip": "", + "widgetStyle": null, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "85240e8c-7af7-90a9-ad0a-726013c479a6" + }, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportDataPointsCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4caf50", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 50000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": null, + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.54)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "showLegend": false, - "useDashboardTimewindow": true, + "title": "{i18n:api-usage.transport-data-point-hourly-activity}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, "displayTimewindow": true, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + }, + "titleColor": "rgba(0, 0, 0, 0.54)", + "titleTooltip": "", + "widgetStyle": {}, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "rule_engine_execution_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_execution", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "3c30248f-0cd8-fb97-a917-bc1e09984a79" - }, - { - "name": "rule_engine_statistics_details", - "icon": "show_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "04e4565a-9e24-23df-f376-f2ec70a8165f" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a" + "id": "d0a10a8f-8f48-f9d6-8306-d12af9b49690" }, - "d70d26d4-e22d-4ca9-9ea7-f9c87c093321": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -519,130 +1894,75 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "jsApiState", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'ENABLED';", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionLimit", - "type": "timeseries", - "label": "jsLimit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionCount", - "type": "timeseries", - "label": "jsCount", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.7673280949238444, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.scripts}\";", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "jsUnit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.7926918686567068, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.javascript}\";", - "aggregationType": "NONE" - }, - { - "name": "tbelExecutionApiState", - "type": "timeseries", - "label": "tbelApiState", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.2002981454581909, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'ENABLED';" - }, - { - "name": "tbelExecutionLimit", - "type": "timeseries", - "label": "tbelLimit", - "color": "#ffeb3b", - "settings": {}, - "_hash": 0.5039854873031677, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;" - }, - { - "name": "tbelExecutionCount", - "type": "timeseries", - "label": "tbelCount", - "color": "#e91e63", - "settings": {}, - "_hash": 0.9506731992087107, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;" - }, - { - "name": "tbelExecutionApiState", + "name": "ruleEngineExecutionCountHourly", "type": "timeseries", - "label": "tbelUnit", - "color": "#ffeb3b", - "settings": {}, - "_hash": 0.3673530683177082, - "aggregationType": "NONE", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#ab00ff", + "settings": { + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.tbel}\";" + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null } ], "alarmFilterConfig": { @@ -650,250 +1970,326 @@ "ACTIVE" ] } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 50000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.54)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [jsUsageBar, jsUsagePercent, jsUsageValue] = calculateBarValues(data[0].jsCount, data[0].jsLimit);\nconst [tbelUsageBar, tbelUsagePercent, tbelUsageValue] = calculateBarValues(data[0].tbelCount, data[0].tbelLimit);\n\nconst jsApiState = data[0].jsApiState;\nconst tbelApiState = data[0].tbelApiState;\nlet currentState;\nif (jsApiState === 'DISABLED' || tbelApiState === 'DISABLED') {\n currentState = 'DISABLED';\n} else if (jsApiState === 'WARNING' || tbelApiState === 'WARNING') {\n currentState = 'WARNING';\n} else {\n currentState = 'ENABLED';\n}\nconst cardClass = currentState.toLowerCase()\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `${data[0].title}` +\n '
' +\n `
${currentState}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].jsUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${jsUsagePercent}
` +\n '
' +\n `
${jsUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].tbelUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${tbelUsagePercent}
` +\n '
' +\n `
${tbelUsageValue}
` +\n '
' +\n '
' + \n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "JavaScript functions", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", + "title": "{i18n:api-usage.rule-engine-hourly-activity}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", "actions": { - "elementClick": [ + "headerButton": [ { - "name": "script_functions_details", - "icon": "insert_chart", + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.54)", + "customButtonStyle": {}, "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "openDashboardState", - "targetDashboardStateId": "script_functions", + "targetDashboardStateId": "rule_engine_statistics", "setEntityId": false, "stateEntityParamName": null, "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "d4961bea-84de-e1af-e50f-666b98d34cd5" + "id": "f9f08190-9ed9-d802-5b7a-c57ff84b5648" } ] - } - }, - "row": 0, - "col": 0, - "id": "d70d26d4-e22d-4ca9-9ea7-f9c87c093321" - }, - "4d3ea95c-3188-9872-1817-2f989c7729e0": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "storageDataPointsLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "storageDataPointsCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#ffc107", - "settings": {}, - "_hash": 0.8737107059960671, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.6301889725474652, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.telemetry}\";" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.0027742924142306613, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.data-points-storage-days}\";" - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
${title}
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "Telemetry persistence", "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, "displayTimewindow": true, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + }, + "titleColor": "rgba(0, 0, 0, 0.54)", + "titleTooltip": "", + "widgetStyle": {}, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "telemetry_persistence_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "6248831c-5b3f-8879-8548-afcf43f10610" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "4d3ea95c-3188-9872-1817-2f989c7729e0" + "id": "4544080d-9b6f-b592-9cd4-0e0335d33857" }, - "2d0d6ff6-cd59-51d4-b916-38e22cdd0702": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -903,72 +2299,75 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "createdAlarmsCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "alarmApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#ffc107", - "settings": {}, - "_hash": 0.8737107059960671, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "alarmApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.43439375716502227, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.alarm}\";" - }, - { - "name": "alarmApiState", + "name": "storageDataPointsCountHourly", "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.9964061963495883, + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039ee", + "settings": { + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.alarms-created}\";" + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null } ], "alarmFilterConfig": { @@ -976,90 +2375,310 @@ "ACTIVE" ] } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 50000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.54)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
${title}
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "title": "{i18n:api-usage.data-points-storage-days-hourly-activity}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [] }, - "title": "Alarm created", "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + }, + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "email_messages_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "alarms_created", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "946ba769-84ac-1507-6baa-94701de8967b" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "2d0d6ff6-cd59-51d4-b916-38e22cdd0702" + "id": "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2" }, - "120573cc-e246-eb49-7d80-68e5d3b3c0cc": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -1069,128 +2688,39 @@ "filterId": null, "dataKeys": [ { - "name": "emailApiState", + "name": "transportMsgCountHourly", "type": "timeseries", - "label": "apiState", + "label": "{i18n:api-usage.transport-messages}", "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "emailLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "emailCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "smsApiState", - "type": "timeseries", - "label": "apiStatePoint", - "color": "#e91e63", - "settings": {}, - "_hash": 0.2969682764607864, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "smsLimit", - "type": "timeseries", - "label": "pointsLimit", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.22082255831864894, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "smsCount", - "type": "timeseries", - "label": "pointsCount", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.6340356364819146, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "title", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.6894070537030252, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.notifications}\";", - "aggregationType": "NONE" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "unit", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.0005447336528170421, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.email}';" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "pointsUnit", - "color": "#e91e63", - "settings": {}, - "_hash": 0.12117146988088967, - "aggregationType": "NONE", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.sms}';" + "usePostProcessing": null, + "postFuncBody": null } ], "alarmFilterConfig": { @@ -1201,83 +2731,310 @@ } ], "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, "history": { "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, + "timewindowMs": 2592000000, + "interval": 86400000, "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 }, "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "AVG", + "type": "SUM", "limit": 25000 - } + }, + "timezone": null }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "#FFFFFF", "color": "rgba(0, 0, 0, 0.87)", "padding": "0px", "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\nconst [apiUsageBar2, apiUsagePercent2, apiUsageValue2] = calculateBarValues(data[0].pointsCount, data[0].pointsLimit);\n\nconst apiState = data[0].apiState;\nconst apiStatePoint = data[0].apiStatePoint;\nlet currentState;\nif (apiState === 'DISABLED' || apiStatePoint === 'DISABLED') {\n currentState = 'DISABLED';\n} else if (apiState === 'WARNING' || apiStatePoint === 'WARNING') {\n currentState = 'WARNING';\n} else {\n currentState = 'ENABLED';\n}\n\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `${data[0].title}` +\n '
' +\n `
${currentState}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].pointsUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent2}
` +\n '
' +\n `
${apiUsageValue2}
` +\n '
' +\n '
' + \n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.54)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-msg-daily-activity}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" }, - "title": "Notifications (Email/SMS)", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "transport_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "notifications", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "46b7cefe-e1f2-67c1-4055-3a214520f869" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "120573cc-e246-eb49-7d80-68e5d3b3c0cc" + "id": "fb155957-1af4-233e-e2fb-09e648e75d6e" }, - "63f99d90-23ab-f8c2-3290-1e693ded5a2e": { + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1319,75 +3076,43 @@ }, "type": "bar" }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": false, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "transportDataPointsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.46849996721308895, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, "postFuncBody": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -1460,7 +3185,8 @@ "weight": "400", "lineHeight": "1" }, - "tickLabelColor": null, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -1471,9 +3197,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1490,7 +3216,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -1499,7 +3225,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -1516,7 +3243,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -1548,41 +3277,97 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-hourly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.transport-msg-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "transport", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "6ef12f6a-0266-25cf-6ca5-5dcb772252c6" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -1607,14 +3392,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "63f99d90-23ab-f8c2-3290-1e693ded5a2e" + "id": "4817e33b-87be-5be3-eaca-ca68a2eb4e0c" }, - "a2b7e906-2d8a-41a8-99a6-409531bfa743": { + "79056202-c92b-1dae-ce49-318ec52e2d3b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1628,70 +3413,35 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCountHourly", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, @@ -1708,22 +3458,23 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_YEAR", - "interval": 7200000 + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -1797,6 +3548,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -1807,9 +3559,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1826,7 +3578,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -1835,7 +3587,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -1852,7 +3605,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -1884,51 +3639,97 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-hourly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.transport-data-points-daily-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-statistics}", - "icon": "show_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "f9f08190-9ed9-d802-5b7a-c57ff84b5648" - }, - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_execution", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "1aec196b-44ba-ddf4-c4dc-c3f60c1eb6fc" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -1953,14 +3754,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a2b7e906-2d8a-41a8-99a6-409531bfa743" + "id": "79056202-c92b-1dae-ce49-318ec52e2d3b" }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1974,11 +3775,12 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2001,6 +3803,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2027,6 +3831,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -2035,81 +3841,13 @@ "end": 0 } } - } - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "tbelExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.6818645685001823, "aggregationType": null, "units": null, "decimals": null, @@ -2126,22 +3864,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", - "interval": 3600000 + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -2215,6 +3964,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2225,9 +3975,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2244,7 +3994,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -2253,11 +4003,109 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", @@ -2265,82 +4113,39 @@ "weight": "500", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false - }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-hourly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.transport-data-points-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "script_functions", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "4687d3f6-8800-a3b6-26e5-0d33f3b828a9" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -2365,14 +4170,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "ca996b66-ab7e-f977-152c-98e4ebf2a901" + "id": "966ffee7-ba0d-8e54-f903-e8d015ca8cd2" }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2386,11 +4191,12 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "ruleEngineExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2413,6 +4219,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2439,6 +4247,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -2447,15 +4257,19 @@ "end": 0 } } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.0661644137210089, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null + "postFuncBody": null } ], "alarmFilterConfig": { @@ -2466,22 +4280,23 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "interval": 300000 + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -2555,6 +4370,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2565,9 +4381,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2584,7 +4400,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -2593,7 +4409,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2610,7 +4427,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2642,24 +4461,100 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-hourly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.rule-engine-daily-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", "actions": { "headerButton": [ { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.54)", + "customButtonStyle": {}, + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", - "setEntityId": false, + "targetDashboardStateId": "rule_engine_statistics", + "setEntityId": true, "stateEntityParamName": null, "openRightLayout": false, - "id": "16707efb-e572-bd02-c219-55fc1b0f672a" + "openInSeparateDialog": false, + "openInPopover": false, + "id": "2592147a-3f62-987a-78c0-cdb775fb4233" } ] }, @@ -2669,14 +4564,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -2701,14 +4596,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34" + "id": "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc" }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2722,11 +4617,12 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsCountHourly", + "name": "ruleEngineExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2749,6 +4645,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2775,6 +4673,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -2783,15 +4683,19 @@ "end": 0 } } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.0661644137210089, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null + "postFuncBody": null } ], "alarmFilterConfig": { @@ -2802,22 +4706,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -2891,6 +4806,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2901,9 +4817,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2920,7 +4836,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -2929,7 +4845,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2946,7 +4863,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2956,51 +4875,122 @@ "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.alarms-created-hourly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.rule-engine-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", "actions": { "headerButton": [ { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.54)", + "customButtonStyle": {}, + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", "type": "openDashboardState", - "targetDashboardStateId": "alarms_created", - "setEntityId": false, + "targetDashboardStateId": "rule_engine_statistics", + "setEntityId": true, "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, "openRightLayout": false, - "id": "371882f9-ea23-3abc-fca8-9449c5dfdd6b" + "openInSeparateDialog": false, + "openInPopover": false, + "id": "b6ba96cf-48b8-f40f-f010-10b95e7dc819" } ] }, @@ -3010,14 +5000,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -3042,14 +5032,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a" + "id": "43a2b982-6c02-d9bd-71ee-34e8e6cf8893" }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3063,142 +5053,35 @@ "filterId": null, "dataKeys": [ { - "name": "emailCountHourly", + "name": "jsExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", - "color": "#4caf50", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.1348755140779876, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "smsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", - "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, @@ -3215,24 +5098,16 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "interval": 300000 + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { "type": "NONE", "limit": 50000 - }, - "timezone": null + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -3304,6 +5179,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3314,9 +5190,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -3333,7 +5209,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -3342,7 +5218,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -3359,7 +5236,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -3391,46 +5270,97 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } - }, - "title": "{i18n:api-usage.notifications-hourly-activity}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "notifications", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "49aefac0-ec5e-d6f3-f39c-8744759f4b19" - } - ] + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, + "title": "{i18n:api-usage.javascript-function-executions-hourly-activity}", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -3455,14 +5385,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe" + "id": "76fe83c9-c30f-00a5-6299-40c759ca6705" }, - "0b091dc3-eec3-847e-d0ad-fdf12d474e7a": { + "a43598d1-7bfd-f329-ee61-c343f34f069f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3476,46 +5406,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "transportDataPointsCountHourly", + "name": "jsExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3538,22 +5432,25 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.46849996721308895, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -3644,6 +5541,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3654,8 +5552,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -3673,7 +5571,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -3682,7 +5580,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -3699,7 +5598,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -3731,10 +5632,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-daily-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.javascript-function-executions-daily-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -3745,14 +5715,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -3777,14 +5747,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "0b091dc3-eec3-847e-d0ad-fdf12d474e7a" + "id": "a43598d1-7bfd-f329-ee61-c343f34f069f" }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3798,47 +5768,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCount", - "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "transportDataPointsCount", + "name": "jsExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3861,9 +5794,9 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.46849996721308895, "units": null, "decimals": null, "funcBody": null, @@ -3871,35 +5804,37 @@ "postFuncBody": null, "aggregationType": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 2592000000, "timewindowMs": 31536000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 1000 + "limit": 25000 }, "timezone": null }, @@ -3973,6 +5908,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3985,7 +5921,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4002,7 +5938,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -4011,59 +5947,131 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", "style": "normal", - "weight": "500", + "weight": "400", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-daily-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.javascript-function-executions-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -4074,14 +6082,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -4106,14 +6114,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "536d7104-49f8-fde6-5827-61b8419f15ec" + "id": "3ebd62a8-dcb7-c96b-8571-e61084248f5b" }, - "c77e417c-ad9d-8e23-3ea1-c75edd653bc0": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4127,10 +6135,10 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCountHourly", + "name": "tbelExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4153,41 +6161,35 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729900300, - "endTimeMs": 1709816300300 - }, - "quickInterval": "CURRENT_DAY" + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null + "type": "NONE", + "limit": 50000 + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4259,6 +6261,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4269,8 +6272,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4288,7 +6291,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -4297,7 +6300,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4314,7 +6318,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4346,10 +6352,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-daily-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.tbel-function-executions-hourly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -4360,14 +6435,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -4392,14 +6467,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c77e417c-ad9d-8e23-3ea1-c75edd653bc0" + "id": "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4" }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4413,10 +6488,10 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCount", + "name": "tbelExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4439,32 +6514,44 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4536,6 +6623,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4548,7 +6636,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4565,7 +6653,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -4574,7 +6662,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4591,7 +6680,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4623,10 +6714,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-monthly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.tbel-function-executions-daily-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -4637,14 +6797,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -4669,14 +6829,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "870904d2-d2e1-a1b9-ce56-b03fd47259b5" + "id": "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2" }, - "c66e5060-57fd-11e7-6616-65b82c294ac2": { + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4690,10 +6850,10 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "name": "tbelExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4716,48 +6876,49 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tbelExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.5212969314724616, - "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", - "limit": 1000 - } + "type": "NONE", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4829,6 +6990,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4839,8 +7001,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4858,7 +7020,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -4867,7 +7029,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4884,7 +7047,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4916,10 +7081,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-daily-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.tbel-function-executions-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -4930,14 +7164,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -4962,14 +7196,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c66e5060-57fd-11e7-6616-65b82c294ac2" + "id": "efc8d4e9-dee2-b677-c378-c1a666543bf4" }, - "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806": { + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4983,10 +7217,10 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCount", + "name": "storageDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039EE", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5009,48 +7243,44 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tbelExecutionCount", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.49748239768082403, - "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5122,6 +7352,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5134,12 +7365,12 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { - "relative": false, + "relative": true, "relativeWidth": 2, - "absoluteWidth": 900000000 + "absoluteWidth": 1000 } }, "showLegend": true, @@ -5151,7 +7382,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -5160,7 +7391,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5177,7 +7409,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5209,10 +7443,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-monthly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.data-points-storage-days-daily-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -5223,14 +7526,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -5255,14 +7558,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806" + "id": "1249d3e2-6b3a-4e4a-65e9-6ed22959871e" }, - "7f4100d2-41be-4954-d353-1d45000dbbbb": { + "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5276,10 +7579,10 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "storageDataPointsCount", "type": "timeseries", "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "color": "#1039EE", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5302,32 +7605,54 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", - "limit": 1000 - } + "type": "NONE", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5399,6 +7724,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5409,8 +7735,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5428,7 +7754,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -5437,7 +7763,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5454,7 +7781,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5486,10 +7815,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-daily-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.data-points-storage-days-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -5500,14 +7898,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -5532,14 +7930,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "7f4100d2-41be-4954-d353-1d45000dbbbb" + "id": "c2f2da29-741d-54f6-5f1d-6f6ae616ea02" }, - "226ef8c9-8488-3664-21ac-0b6217336202": { + "8e07dbe5-aa7a-19c1-c470-5f055df948a7": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5553,10 +7951,10 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCount", + "name": "createdAlarmsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "label": "{i18n:api-usage.alarms-created}", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5579,31 +7977,34 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { "type": "NONE", - "limit": 1000 + "limit": 50000 } }, "showTitle": true, @@ -5676,6 +8077,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5688,7 +8090,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -5705,7 +8107,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -5714,7 +8116,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5731,7 +8134,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5763,10 +8168,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-monthly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.alarms-created-hourly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -5777,14 +8251,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -5809,14 +8283,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "226ef8c9-8488-3664-21ac-0b6217336202" + "id": "8e07dbe5-aa7a-19c1-c470-5f055df948a7" }, - "bef6c27b-9fe7-ee92-40d9-9696c501a1f9": { + "e0fe9887-d61c-7813-05a7-f60811e5c5bf": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5833,7 +8307,7 @@ "name": "createdAlarmsCountHourly", "type": "timeseries", "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5843,7 +8317,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "pointShapeFormatter": "", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -5856,32 +8330,44 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5953,6 +8439,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5963,8 +8450,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5982,7 +8469,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -5991,7 +8478,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6008,7 +8496,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6040,10 +8530,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, "title": "{i18n:api-usage.alarms-created-daily-activity}", - "dropShadow": true, + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -6054,14 +8613,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -6086,14 +8645,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "bef6c27b-9fe7-ee92-40d9-9696c501a1f9" + "id": "e0fe9887-d61c-7813-05a7-f60811e5c5bf" }, - "52305cf8-2258-5745-a0e7-41a171594bb3": { + "99a40c35-c232-16c5-c42f-3cc80ddb9243": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6110,7 +8669,7 @@ "name": "createdAlarmsCount", "type": "timeseries", "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6120,7 +8679,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "pointShapeFormatter": "", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -6133,32 +8692,54 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 2592000000, "timewindowMs": 31536000000, - "interval": 1000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6230,6 +8811,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6242,7 +8824,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -6259,7 +8841,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -6268,7 +8850,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6285,7 +8868,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6317,10 +8902,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, "title": "{i18n:api-usage.alarms-created-monthly-activity}", - "dropShadow": true, + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -6331,14 +8985,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -6363,14 +9017,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "52305cf8-2258-5745-a0e7-41a171594bb3" + "id": "99a40c35-c232-16c5-c42f-3cc80ddb9243" }, - "36fdf999-ca22-9a4c-269d-3f004d792792": { + "407f7630-406e-9c24-cb3d-b1cbdd190f15": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6387,7 +9041,7 @@ "name": "emailCountHourly", "type": "timeseries", "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6410,31 +9064,34 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 1000 + "type": "NONE", + "limit": 50000 } }, "showTitle": true, @@ -6507,6 +9164,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6517,8 +9175,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6536,7 +9194,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -6545,7 +9203,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6562,7 +9221,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6594,10 +9255,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.email-messages-daily-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.emails-hourly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -6608,14 +9338,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -6640,14 +9370,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "36fdf999-ca22-9a4c-269d-3f004d792792" + "id": "407f7630-406e-9c24-cb3d-b1cbdd190f15" }, - "9a191755-499d-535e-86c5-061102729c02": { + "b12fb875-89fe-af4c-b344-bf4178de419f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6661,10 +9391,10 @@ "filterId": null, "dataKeys": [ { - "name": "smsCountHourly", + "name": "emailCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", + "label": "{i18n:api-usage.email-messages}", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6687,32 +9417,44 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6784,6 +9526,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6794,8 +9537,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6813,7 +9556,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -6822,7 +9565,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6839,7 +9583,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6871,10 +9617,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.sms-messages-daily-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.emails-daily-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -6885,14 +9700,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -6917,14 +9732,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "9a191755-499d-535e-86c5-061102729c02" + "id": "b12fb875-89fe-af4c-b344-bf4178de419f" }, - "4b266318-8357-33ef-ca5a-74cbf90e014f": { + "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6941,7 +9756,7 @@ "name": "emailCount", "type": "timeseries", "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6964,32 +9779,49 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 2592000000, "timewindowMs": 31536000000, - "interval": 1000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7061,6 +9893,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7073,7 +9906,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7090,7 +9923,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -7099,7 +9932,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7116,7 +9950,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7148,10 +9984,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.email-messages-monthly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.emails-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -7162,14 +10067,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -7194,14 +10099,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "4b266318-8357-33ef-ca5a-74cbf90e014f" + "id": "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f" }, - "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8": { + "5648a56e-5a33-3018-92bd-d8e3dbe8aeee": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7215,10 +10120,10 @@ "filterId": null, "dataKeys": [ { - "name": "smsCount", + "name": "smsCountHourly", "type": "timeseries", "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -7241,31 +10146,34 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], - "timewindow": { - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { "type": "NONE", - "limit": 1000 + "limit": 50000 } }, "showTitle": true, @@ -7338,6 +10246,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7350,7 +10259,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7367,7 +10276,7 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", @@ -7376,7 +10285,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7393,7 +10303,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7425,10 +10337,79 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.sms-messages-monthly-activity}", - "dropShadow": true, + "title": "{i18n:api-usage.sms-messages-hourly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -7439,14 +10420,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -7471,14 +10452,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8" + "id": "5648a56e-5a33-3018-92bd-d8e3dbe8aeee" }, - "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "ab5518c1-34d6-7e17-04b4-6520496d5fe1": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7487,266 +10468,75 @@ "datasources": [ { "type": "entity", - "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, "dataKeys": [ { - "name": "successfulMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.successful}", - "color": "#4caf50", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.15490750967648736, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "failedMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.permanent-failures}", - "color": "#ef5350", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.4186621166514697, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tmpFailed", + "name": "smsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.processing-failures}", - "color": "#ffc107", + "label": "{i18n:api-usage.sms-messages}", + "color": "#F36021", "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.49891007198715376, - "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ], "alarmFilterConfig": { "statusList": [ "ACTIVE" ] - }, - "latestDataKeys": [ - { - "name": "queueName", - "type": "entityField", - "label": "Queue name", - "color": "#ffc107", - "settings": {}, - "_hash": 0.7021721434431745 - }, - { - "name": "serviceId", - "type": "entityField", - "label": "Service Id", - "color": "#607d8b", - "settings": {}, - "_hash": 0.5924381120750077 - } - ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 0, - "realtime": { - "timewindowMs": 3600000, - "interval": 1000 + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 10000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7818,6 +10608,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7828,8 +10619,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -7847,16 +10638,17 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", "sortDataKeys": false, - "showMin": true, - "showMax": true, + "showMin": false, + "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7873,7 +10665,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7885,13 +10679,12 @@ }, "tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateInterval": true, - "tooltipHideZeroValues": true, "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundBlur": 4, "animation": { "animation": true, "animationThreshold": 2000, - "animationDuration": 300, + "animationDuration": 1000, "animationEasing": "cubicOut", "animationDelay": 0, "animationDurationUpdate": 300, @@ -7907,10 +10700,78 @@ "blur": 3 } }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.queue-stats}", - "dropShadow": true, + "title": "{i18n:api-usage.sms-messages-daily-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -7921,241 +10782,128 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", - "titleTooltip": "", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "0px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "fa938580-33db-f1b3-fafc-bc3e3784ad57" - }, - "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", - "dataKeys": [ - { - "name": "timeoutMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.permanent-timeouts}", - "color": "#4caf50", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.565222981550328, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, + "lineHeight": "20px" + }, + "titleColor": "rgba(0, 0, 0, 0.54)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "12px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "ab5518c1-34d6-7e17-04b4-6520496d5fe1" + }, + "2e7326ac-98d3-e68c-b7cf-948118a3f140": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ { - "name": "tmpTimeout", + "name": "smsCount", "type": "timeseries", - "label": "{i18n:api-usage.processing-timeouts}", - "color": "#9c27b0", + "label": "{i18n:api-usage.sms-messages}", + "color": "#F36021", "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.2679547062508352, - "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - }, - "latestDataKeys": [ - { - "name": "queueName", - "type": "entityField", - "label": "Queue name", - "color": "#f44336", - "settings": {}, - "_hash": 0.7066844328378095 - }, - { - "name": "serviceId", - "type": "entityField", - "label": "Service Id", - "color": "#ffc107", - "settings": {}, - "_hash": 0.1371570237026627 + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 0, + "hideTimezone": false, + "selectedTab": 1, "realtime": { - "timewindowMs": 3600000, - "interval": 1000 + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 10000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -8227,6 +10975,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -8237,8 +10986,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -8256,20 +11005,118 @@ "weight": "400", "lineHeight": "16px" }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendLabelColor": "rgba(0, 0, 0, 0.54)", "legendConfig": { "direction": "column", "position": "bottom", "sortDataKeys": false, - "showMin": true, - "showMax": true, + "showMin": false, + "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", @@ -8277,49 +11124,21 @@ "weight": "500", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false - }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipHideZeroValues": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 300, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.processing-failures-and-timeouts}", - "dropShadow": true, + "title": "{i18n:api-usage.sms-messages-monthly-activity}", + "dropShadow": false, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", @@ -8330,14 +11149,14 @@ "useDashboardTimewindow": false, "displayTimewindow": true, "titleFont": { - "size": 16, + "size": 14, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal", - "lineHeight": "24px" + "lineHeight": "20px" }, - "titleColor": "rgba(0, 0, 0, 0.87)", + "titleColor": "rgba(0, 0, 0, 0.54)", "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", @@ -8362,12 +11181,319 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "12px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "2ee89893-4e38-5331-95b7-3fd4f310c5a7" + "id": "2e7326ac-98d3-e68c-b7cf-948118a3f140" + }, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "typeFullFqn": "system.api_usage", + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entity", + "name": "", + "dataKeys": [] + } + ], + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0", + "settings": { + "dsEntityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "apiUsageDataKeys": [ + { + "label": "{i18n:api-usage.transport-messages}", + "state": "transport_messages", + "status": { + "name": "transportApiState", + "label": "transportApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "transportMsgLimit", + "label": "transportMsgLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "transportMsgCount", + "label": "transportMsgCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.transport-data-points}", + "state": "transport_data_points", + "status": { + "name": "transportApiState", + "label": "transportApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "transportDataPointsLimit", + "label": "transportDataPointsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "transportDataPointsCount", + "label": "transportDataPointsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.rule-engine-executions}", + "state": "rule_engine_executions", + "status": { + "name": "ruleEngineApiState", + "label": "ruleEngineApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "ruleEngineExecutionLimit", + "label": "ruleEngineExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "ruleEngineExecutionCount", + "label": "ruleEngineExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.javascript-function-executions}", + "state": "javascript_function_executions", + "status": { + "name": "jsExecutionApiState", + "label": "jsExecutionApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "jsExecutionLimit", + "label": "jsExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "jsExecutionCount", + "label": "jsExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.tbel-function-executions}", + "state": "tbel_function_executions", + "status": { + "name": "tbelExecutionApiState", + "label": "tbelExecutionApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "tbelExecutionLimit", + "label": "tbelExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "tbelExecutionCount", + "label": "tbelExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.data-points-storage-days}", + "state": "data_points_storage_days", + "status": { + "name": "dbApiState", + "label": "dbApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "storageDataPointsLimit", + "label": "storageDataPointsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "storageDataPointsCount", + "label": "storageDataPointsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.alarms-created}", + "state": "alarms_created", + "status": { + "name": "alarmApiState", + "label": "alarmApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "createdAlarmsLimit", + "label": "createdAlarmsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "createdAlarmsCount", + "label": "createdAlarmsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.emails}", + "state": "emails", + "status": { + "name": "emailApiState", + "label": "emailApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "emailLimit", + "label": "emailLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "emailCount", + "label": "emailCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.sms}", + "state": "sms", + "status": { + "name": "smsApiState", + "label": "smsApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "smsLimit", + "label": "smsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "smsCount", + "label": "smsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + } + ], + "targetDashboardState": "default", + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "0" + }, + "title": "{i18n:api-usage.api-usage}", + "decimals": null, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": ".tb-widget-header {\n height: 48px;\n align-items: center !important;\n padding: 5px 10px 0 10px;\n}\n\n@media screen and (min-width: 960px) {\n .tb-widget {\n margin-right: 0px !important;\n }\n}", + "titleStyle": {}, + "pageSize": 1024, + "noDataDisplayMessage": "", + "actions": { + "headerButton": [ + { + "name": "{i18n:widgets.api-usage.go-to-main-state}", + "buttonType": "stroked", + "showIcon": false, + "icon": "undo", + "buttonColor": "#305680", + "buttonBorderColor": "#0000001F", + "customButtonStyle": { + "padding": "0 16px" + }, + "useShowWidgetActionFunction": true, + "showWidgetActionFunction": "return widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;", + "type": "custom", + "customFunction": "const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "1ea1cca6-47d1-3539-d051-9535129fb12b" + } + ] + }, + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": null, + "weight": "500", + "style": null, + "lineHeight": "20px" + }, + "borderRadius": "12px", + "titleColor": "rgba(0, 0, 0, 0.54)", + "margin": "12px" + }, + "row": 0, + "col": 0, + "id": "07e3a570-c961-b72d-3371-5b29f3617b73" } }, "states": { @@ -8377,346 +11503,843 @@ "layouts": { "main": { "widgets": { - "aab68ab5-8e40-8694-c55c-8eb1c89b88fb": { - "sizeX": 4, - "sizeY": 2, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 - }, - "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a": { - "sizeX": 4, - "sizeY": 2, + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 12, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 100, + "outerMargin": false, + "layoutType": "divider", + "minColumns": 12, + "viewFormat": "grid", + "rowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "sizeX": 7, + "sizeY": 5, "row": 0, - "col": 4 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "d70d26d4-e22d-4ca9-9ea7-f9c87c093321": { - "sizeX": 4, - "sizeY": 2, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "sizeX": 7, + "sizeY": 5, "row": 0, - "col": 8 + "col": 7, + "resizable": true, + "mobileHeight": 6 + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "sizeX": 7, + "sizeY": 5, + "row": 5, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "sizeX": 7, + "sizeY": 5, + "row": 5, + "col": 7, + "resizable": true, + "mobileHeight": 6 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false + } + } + } + }, + "rule_engine_statistics": { + "name": "{i18n:api-usage.rule-engine-statistics}", + "root": false, + "layouts": { + "main": { + "widgets": { + "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "sizeX": 24, + "sizeY": 5, + "row": 7, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "4d3ea95c-3188-9872-1817-2f989c7729e0": { - "sizeX": 4, - "sizeY": 2, + "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "sizeX": 12, + "sizeY": 7, "row": 0, - "col": 12 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "2d0d6ff6-cd59-51d4-b916-38e22cdd0702": { - "sizeX": 4, - "sizeY": 2, + "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "sizeX": 12, + "sizeY": 7, "row": 0, - "col": 16 - }, - "120573cc-e246-eb49-7d80-68e5d3b3c0cc": { - "sizeX": 4, - "sizeY": 2, + "col": 12, + "resizable": true, + "mobileHeight": 6 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "outerMargin": true, + "layoutType": "default", + "minColumns": 24, + "viewFormat": "grid", + "rowHeight": 70 + } + } + } + }, + "transport_messages": { + "name": "{i18n:api-usage.transport-messages}", + "root": false, + "layouts": { + "main": { + "widgets": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, - "col": 20 - }, - "63f99d90-23ab-f8c2-3290-1e693ded5a2e": { - "sizeX": 8, - "sizeY": 4, - "row": 2, - "col": 0 - }, - "a2b7e906-2d8a-41a8-99a6-409531bfa743": { - "sizeX": 8, + "col": 0, + "resizable": true, + "mobileHeight": 4 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "sizeX": 6, "sizeY": 4, - "row": 2, - "col": 8 + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { - "sizeX": 8, + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { + "sizeX": 6, "sizeY": 4, - "row": 2, - "col": 16 + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { - "sizeX": 8, + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "sizeX": 12, "sizeY": 4, - "row": 6, + "resizable": true, + "row": 0, "col": 0 - }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { - "sizeX": 8, - "sizeY": 4, - "row": 6, - "col": 8 - }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { - "sizeX": 8, - "sizeY": 4, - "row": 6, - "col": 16 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 100, - "outerMargin": true + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false } } } }, - "transport": { - "name": "{i18n:api-usage.transport}", + "transport_data_points": { + "name": "{i18n:api-usage.transport-data-points}", "root": false, "layouts": { "main": { "widgets": { - "0b091dc3-eec3-847e-d0ad-fdf12d474e7a": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "79056202-c92b-1dae-ce49-318ec52e2d3b": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + }, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, "col": 0 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "rule_engine_execution": { + "rule_engine_executions": { "name": "{i18n:api-usage.rule-engine-executions}", "root": false, "layouts": { "main": { "widgets": { - "c77e417c-ad9d-8e23-3ea1-c75edd653bc0": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, "col": 0 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "telemetry_persistence": { - "name": "{i18n:api-usage.telemetry-persistence}", + "javascript_function_executions": { + "name": "{i18n:api-usage.javascript-function-executions}", "root": false, "layouts": { "main": { "widgets": { - "7f4100d2-41be-4954-d353-1d45000dbbbb": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "226ef8c9-8488-3664-21ac-0b6217336202": { - "sizeX": 24, - "sizeY": 6, - "row": 6, - "col": 0 + "a43598d1-7bfd-f329-ee61-c343f34f069f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "rule_engine_statistics": { - "name": "{i18n:api-usage.rule-engine-statistics}", + "tbel_function_executions": { + "name": "{i18n:api-usage.tbel-function-executions}", "root": false, "layouts": { "main": { "widgets": { - "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 5, - "row": 7, + "sizeY": 39, + "row": 0, "col": 0 - }, - "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { "sizeX": 12, - "sizeY": 7, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false + } + } + } + }, + "data_points_storage_days": { + "name": "{i18n:api-usage.data-points-storage-days}", + "root": false, + "layouts": { + "main": { + "widgets": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { "sizeX": 12, - "sizeY": 7, + "sizeY": 4, + "resizable": true, "row": 0, - "col": 12 + "col": 0 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "notifications": { - "name": "{i18n:api-usage.notifications-email-sms}", + "emails": { + "name": "{i18n:api-usage.emails}", "root": false, "layouts": { "main": { "widgets": { - "36fdf999-ca22-9a4c-269d-3f004d792792": { - "sizeX": 12, - "sizeY": 6, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 - }, - "9a191755-499d-535e-86c5-061102729c02": { + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "407f7630-406e-9c24-cb3d-b1cbdd190f15": { "sizeX": 12, - "sizeY": 6, + "sizeY": 4, "row": 0, - "col": 12 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "4b266318-8357-33ef-ca5a-74cbf90e014f": { - "sizeX": 12, - "sizeY": 6, - "row": 6, - "col": 0 + "b12fb875-89fe-af4c-b344-bf4178de419f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8": { - "sizeX": 12, - "sizeY": 6, - "row": 6, - "col": 12 + "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "alarms_created": { - "name": "{i18n:api-usage.alarms-created}", + "sms": { + "name": "{i18n:api-usage.sms}", "root": false, "layouts": { "main": { "widgets": { - "bef6c27b-9fe7-ee92-40d9-9696c501a1f9": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "5648a56e-5a33-3018-92bd-d8e3dbe8aeee": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "52305cf8-2258-5745-a0e7-41a171594bb3": { - "sizeX": 24, - "sizeY": 6, - "row": 6, - "col": 0 + "ab5518c1-34d6-7e17-04b4-6520496d5fe1": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "2e7326ac-98d3-e68c-b7cf-948118a3f140": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "script_functions": { - "name": "{i18n:api-usage.scripts}", + "alarms_created": { + "name": "{i18n:api-usage.alarms-created}", "root": false, "layouts": { "main": { "widgets": { - "c66e5060-57fd-11e7-6616-65b82c294ac2": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "8e07dbe5-aa7a-19c1-c470-5f055df948a7": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806": { - "sizeX": 24, - "sizeY": 6, - "row": 6, - "col": 0 + "e0fe9887-d61c-7813-05a7-f60811e5c5bf": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "99a40c35-c232-16c5-c42f-3cc80ddb9243": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 12, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } @@ -8743,24 +12366,15 @@ }, "filters": {}, "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "interval": 3600000 + "timewindowMs": 86400000 }, "aggregation": { "type": "NONE", "limit": 50000 - }, - "timezone": null + } }, "settings": { "stateControllerId": "entity", @@ -8776,7 +12390,7 @@ "dashboardLogoUrl": null, "hideToolbar": false, "showUpdateDashboardImage": false, - "dashboardCss": ".card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n\n.card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 12px 12px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-mdc-button-base {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mdc-button__label {\n pointer-events: none;\n}\n\n.action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n.card .unit {\n color: #666666;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-mdc-button-base {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-mdc-button-base {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-mdc-button-base {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} " + "dashboardCss": ".tb-widget-actions {\n --mat-icon-color: rgba(0, 0, 0, 0.54);\n}" } }, "name": "Api Usage" diff --git a/ui-ngx/src/assets/dashboard/customer_user_home_page.json b/ui-ngx/src/assets/dashboard/customer_user_home_page.json index 17d7262f23..f3c24e1298 100644 --- a/ui-ngx/src/assets/dashboard/customer_user_home_page.json +++ b/ui-ngx/src/assets/dashboard/customer_user_home_page.json @@ -12,11 +12,6 @@ "sizeY": 3, "config": { "datasources": [], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, "showTitle": false, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -108,7 +103,6 @@ "label": "totalDevices", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -130,7 +124,6 @@ "label": "activeDevices", "color": "#4caf50", "settings": {}, - "_hash": 0.1262449138010293, "aggregationType": null, "units": null, "decimals": null, @@ -152,7 +145,6 @@ "label": "inactiveDevices", "color": "#f44336", "settings": {}, - "_hash": 0.39119172615806797, "aggregationType": null, "units": null, "decimals": null, @@ -163,30 +155,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -210,7 +178,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -238,7 +205,6 @@ "label": "totalAlarms", "color": "#ffc107", "settings": {}, - "_hash": 0.8743321483507485, "aggregationType": null, "units": null, "decimals": null, @@ -265,7 +231,6 @@ "label": "assignedToMeAlarms", "color": "#ffc107", "settings": {}, - "_hash": 0.6392390736755882, "aggregationType": null, "units": null, "decimals": null, @@ -293,7 +258,6 @@ "label": "criticalAlarms", "color": "#ffc107", "settings": {}, - "_hash": 0.8329478098552843, "aggregationType": null, "units": null, "decimals": null, @@ -312,37 +276,13 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "", + "markdownTextPattern": "", "applyDefaultMarkdownStyle": false, "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n" }, @@ -359,7 +299,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -429,7 +368,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 20, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -521,33 +461,14 @@ } }, "timewindow": { - "displayValue": "", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168326072, - "endTimeMs": 1680254726072 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 60000 }, "aggregation": { - "type": "AVG", - "limit": 25000 + "type": "AVG" } }, "settings": { @@ -567,6 +488,5 @@ "dashboardCss": ".tb-widget-container > .tb-widget {\n border: 1px solid rgba(0, 0, 0, 0.05);\n box-shadow: 0px 5px 16px rgba(0, 0, 0, 0.04);\n border-radius: 12px;\n}\n\n.tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 16px !important;\n}\n\n.tb-card-title {\n display: grid;\n}\n\n.tb-home-widget-title {\n font-style: normal;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tb-home-widget-link {\n position: relative;\n border-bottom: none;\n}\n\n.tb-home-widget-link:hover {\n border-bottom: none;\n}\n\n.tb-home-widget-link:focus {\n border-bottom: none;\n}\n\n.tb-home-widget-link::after {\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px; \n}\n\n.tb-home-widget-link:hover::after {\n color: inherit;\n}\n\n.tb-home-widget-info-icon {\n color: rgba(0, 0, 0, 0.12);\n font-size: 16px;\n width: 16px;\n height: 16px;\n line-height: 15px;\n vertical-align: middle;\n}\n\n.tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n padding: 0;\n}\n\n.tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n cursor: pointer;\n user-select: none;\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .tb-widget-container > .tb-widget {\n border-radius: 4px;\n }\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 2px !important;\n }\n .tb-hide-md {\n display: none;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 8px !important;\n }\n .tb-hide-lg {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-hide-md-lg {\n display: none;\n }\n\n .tb-home-widget-title {\n font-size: 12px;\n line-height: 16px;\n }\n \n .tb-widget-container > .tb-widget .tb-widget-title {\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 12px;\n line-height: 16px;\n min-height: 24px;\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 {\n width: 24px;\n height: 24px;\n line-height: 24px;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 .mat-icon {\n width: 18px;\n height: 18px;\n font-size: 18px;\n }\n \n .tb-widget-container > .tb-widget tb-legend {\n padding-bottom: 0 !important;\n }\n \n .tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n font-size: 11px;\n line-height: 16px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (max-width: 959px), screen and (min-width: 1820px) {\n .tb-hide-not-md-lg {\n display: none;\n }\n}\n" } }, - "externalId": null, "name": "Customer User Home Page" -} +} \ No newline at end of file diff --git a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json index 739402e72f..408c9d2eb6 100644 --- a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json @@ -12,30 +12,6 @@ "sizeY": 3.5, "config": { "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -59,7 +35,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -87,7 +62,6 @@ "label": "clusterMode", "color": "#2196f3", "settings": {}, - "_hash": 0.7272123990942316, "aggregationType": "NONE", "units": null, "decimals": null, @@ -98,30 +72,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#ffffff", "color": "rgba(0, 0, 0, 0.87)", @@ -148,7 +98,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -164,30 +113,6 @@ "sizeY": 3.5, "config": { "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -211,7 +136,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -227,30 +151,6 @@ "sizeY": 3.5, "config": { "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -273,7 +173,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -289,30 +188,6 @@ "sizeY": 3.5, "config": { "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -335,7 +210,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -351,11 +225,6 @@ "sizeY": 3, "config": { "datasources": [], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, "showTitle": false, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -394,16 +263,14 @@ "type": "timeseries", "label": "cpuUsage", "color": "#2196f3", - "settings": {}, - "_hash": 0.659021918423117 + "settings": {} }, { "name": "cpuCount", "type": "timeseries", "label": "cpuCount", "color": "#4caf50", - "settings": {}, - "_hash": 0.5332753234169949 + "settings": {} }, { "name": "cpuUsage", @@ -411,7 +278,6 @@ "label": "status", "color": "#f44336", "settings": {}, - "_hash": 0.025084892962804473, "aggregationType": "NONE", "units": null, "decimals": null, @@ -425,7 +291,6 @@ "label": "statusTooltip", "color": "#ffc107", "settings": {}, - "_hash": 0.8542003006820891, "aggregationType": "NONE", "units": null, "decimals": null, @@ -436,30 +301,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -483,7 +324,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -511,7 +351,6 @@ "label": "memoryUsage", "color": "#2196f3", "settings": {}, - "_hash": 0.659021918423117, "aggregationType": "NONE", "units": null, "decimals": null, @@ -525,7 +364,6 @@ "label": "totalMemory", "color": "#4caf50", "settings": {}, - "_hash": 0.5332753234169949, "aggregationType": "NONE", "units": null, "decimals": null, @@ -539,7 +377,6 @@ "label": "status", "color": "#f44336", "settings": {}, - "_hash": 0.025084892962804473, "aggregationType": "NONE", "units": null, "decimals": null, @@ -553,7 +390,6 @@ "label": "statusTooltip", "color": "#ffc107", "settings": {}, - "_hash": 0.8542003006820891, "aggregationType": "NONE", "units": null, "decimals": null, @@ -564,30 +400,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -611,7 +423,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -639,7 +450,6 @@ "label": "discUsage", "color": "#2196f3", "settings": {}, - "_hash": 0.659021918423117, "aggregationType": "NONE", "units": null, "decimals": null, @@ -653,7 +463,6 @@ "label": "totalDiscSpace", "color": "#4caf50", "settings": {}, - "_hash": 0.5332753234169949, "aggregationType": "NONE", "units": null, "decimals": null, @@ -667,7 +476,6 @@ "label": "status", "color": "#f44336", "settings": {}, - "_hash": 0.025084892962804473, "aggregationType": "NONE", "units": null, "decimals": null, @@ -681,7 +489,6 @@ "label": "statusTooltip", "color": "#ffc107", "settings": {}, - "_hash": 0.8542003006820891, "aggregationType": "NONE", "units": null, "decimals": null, @@ -692,30 +499,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -739,7 +522,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -766,16 +548,14 @@ "type": "timeseries", "label": "cpuUsage", "color": "#2196f3", - "settings": {}, - "_hash": 0.659021918423117 + "settings": {} }, { "name": "cpuCount", "type": "timeseries", "label": "cpuCount", "color": "#4caf50", - "settings": {}, - "_hash": 0.5332753234169949 + "settings": {} }, { "name": "cpuUsage", @@ -783,7 +563,6 @@ "label": "cpuStatus", "color": "#f44336", "settings": {}, - "_hash": 0.025084892962804473, "aggregationType": "NONE", "units": null, "decimals": null, @@ -797,7 +576,6 @@ "label": "cpuStatusTooltip", "color": "#ffc107", "settings": {}, - "_hash": 0.8542003006820891, "aggregationType": "NONE", "units": null, "decimals": null, @@ -810,8 +588,7 @@ "type": "timeseries", "label": "memoryUsage", "color": "#607d8b", - "settings": {}, - "_hash": 0.9761283269385537 + "settings": {} }, { "name": "totalMemory", @@ -819,7 +596,6 @@ "label": "totalMemory", "color": "#9c27b0", "settings": {}, - "_hash": 0.6363130990513808, "aggregationType": "NONE", "units": null, "decimals": null, @@ -833,7 +609,6 @@ "label": "memoryStatus", "color": "#8bc34a", "settings": {}, - "_hash": 0.15388812616419556, "aggregationType": "NONE", "units": null, "decimals": null, @@ -847,7 +622,6 @@ "label": "memoryStatusTooltip", "color": "#3f51b5", "settings": {}, - "_hash": 0.8086245517967001, "aggregationType": "NONE", "units": null, "decimals": null, @@ -860,8 +634,7 @@ "type": "timeseries", "label": "discUsage", "color": "#e91e63", - "settings": {}, - "_hash": 0.4278686534236851 + "settings": {} }, { "name": "totalDiscSpace", @@ -869,7 +642,6 @@ "label": "totalDiscSpace", "color": "#ffeb3b", "settings": {}, - "_hash": 0.543030429255416, "aggregationType": "NONE", "units": null, "decimals": null, @@ -883,7 +655,6 @@ "label": "discStatus", "color": "#03a9f4", "settings": {}, - "_hash": 0.29138586081227835, "aggregationType": "NONE", "units": null, "decimals": null, @@ -897,7 +668,6 @@ "label": "discStatusTooltip", "color": "#ff9800", "settings": {}, - "_hash": 0.08995267596665912, "aggregationType": "NONE", "units": null, "decimals": null, @@ -908,30 +678,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -955,7 +701,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -971,30 +716,6 @@ "sizeY": 3.5, "config": { "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "rgba(0,0,0,0)", "color": "rgba(0, 0, 0, 0.87)", @@ -1021,7 +742,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1049,7 +769,6 @@ "label": "tenantsCount", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -1060,30 +779,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1107,7 +802,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1135,7 +829,6 @@ "label": "tenantProfilesCount", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -1146,30 +839,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1193,7 +862,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1221,7 +889,6 @@ "label": "devicesCount", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -1232,30 +899,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1279,7 +922,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1307,7 +949,6 @@ "label": "assetsCount", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -1318,30 +959,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1365,7 +982,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1393,7 +1009,6 @@ "label": "usersCount", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -1404,30 +1019,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1451,7 +1042,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1479,7 +1069,6 @@ "label": "customersCount", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -1490,30 +1079,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1537,7 +1102,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1565,7 +1129,6 @@ "label": "tenantsCount", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -1587,7 +1150,6 @@ "label": "tenantProfilesCount", "color": "#4caf50", "settings": {}, - "_hash": 0.04291827117688696, "aggregationType": null, "units": null, "decimals": null, @@ -1609,7 +1171,6 @@ "label": "devicesCount", "color": "#f44336", "settings": {}, - "_hash": 0.45105711028955886, "aggregationType": null, "units": null, "decimals": null, @@ -1631,7 +1192,6 @@ "label": "assetsCount", "color": "#ffc107", "settings": {}, - "_hash": 0.17028494648519876, "aggregationType": null, "units": null, "decimals": null, @@ -1653,7 +1213,6 @@ "label": "usersCount", "color": "#607d8b", "settings": {}, - "_hash": 0.0362458342595664, "aggregationType": null, "units": null, "decimals": null, @@ -1675,7 +1234,6 @@ "label": "customersCount", "color": "#9c27b0", "settings": {}, - "_hash": 0.7596585662918276, "aggregationType": null, "units": null, "decimals": null, @@ -1686,30 +1244,6 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1733,7 +1267,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1749,30 +1282,6 @@ "sizeY": 3.5, "config": { "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "rgba(0,0,0,0)", "color": "rgba(0, 0, 0, 0.87)", @@ -1799,7 +1308,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -1885,7 +1393,6 @@ } } }, - "_hash": 0.39181957822569946, "decimals": 0 } ], @@ -2212,7 +1719,6 @@ } } }, - "_hash": 0.39181957822569946, "decimals": null, "aggregationType": null, "units": null, @@ -2285,7 +1791,6 @@ } } }, - "_hash": 0.9392996197028385, "aggregationType": null, "units": null, "decimals": null, @@ -2358,7 +1863,6 @@ } } }, - "_hash": 0.027335636460212864, "aggregationType": null, "units": null, "decimals": null, @@ -2626,7 +2130,8 @@ "enableFullscreen": false, "widgetStyle": {}, "widgetCss": "", - "noDataDisplayMessage": "" + "noDataDisplayMessage": "", + "datasources": [] }, "row": 0, "col": 0, @@ -2669,7 +2174,8 @@ "showTitleIcon": false, "titleTooltip": "", "titleStyle": null, - "borderRadius": "12px" + "borderRadius": "12px", + "datasources": [] }, "row": 0, "col": 0, @@ -2709,7 +2215,8 @@ "fontWeight": 400 }, "widgetCss": "", - "noDataDisplayMessage": "" + "noDataDisplayMessage": "", + "datasources": [] }, "row": 0, "col": 0, @@ -2795,7 +2302,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 20, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -2822,7 +2330,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": true, "mobileRowHeight": 70, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -2857,7 +2366,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": true, "mobileRowHeight": 70, - "outerMargin": false + "outerMargin": false, + "layoutType": "default" } } } @@ -2896,7 +2406,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": true, "mobileRowHeight": 70, - "outerMargin": false + "outerMargin": false, + "layoutType": "default" } } } @@ -2923,7 +2434,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": true, "mobileRowHeight": 70, - "outerMargin": false + "outerMargin": false, + "layoutType": "default" } } } @@ -2980,7 +2492,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "layoutType": "default" } } } @@ -3007,7 +2520,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": true, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "layoutType": "default" } } } @@ -3041,7 +2555,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 20 + "mobileRowHeight": 20, + "layoutType": "default" } } } @@ -3113,33 +2628,14 @@ }, "filters": {}, "timewindow": { - "displayValue": "", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168326072, - "endTimeMs": 1680254726072 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 60000 }, "aggregation": { - "type": "AVG", - "limit": 25000 + "type": "AVG" } }, "settings": { @@ -3160,4 +2656,4 @@ } }, "name": "System Administrator Home Page" -} +} \ No newline at end of file diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index 6e3b698200..9fb2d25cf8 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -12,30 +12,6 @@ "sizeY": 3.5, "config": { "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -59,7 +35,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -75,11 +50,6 @@ "sizeY": 3, "config": { "datasources": [], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, "showTitle": false, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -196,7 +166,6 @@ "label": "totalDevices", "color": "#2196f3", "settings": {}, - "_hash": 0.8491768696709192, "aggregationType": null, "units": null, "decimals": null, @@ -218,7 +187,6 @@ "label": "activeDevices", "color": "#4caf50", "settings": {}, - "_hash": 0.1262449138010293, "aggregationType": null, "units": null, "decimals": null, @@ -240,7 +208,6 @@ "label": "inactiveDevices", "color": "#f44336", "settings": {}, - "_hash": 0.39119172615806797, "aggregationType": null, "units": null, "decimals": null, @@ -251,37 +218,13 @@ ] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "", + "markdownTextPattern": "", "applyDefaultMarkdownStyle": false, "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n" }, @@ -298,7 +241,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -326,7 +268,6 @@ "label": "totalAlarms", "color": "#ffc107", "settings": {}, - "_hash": 0.8743321483507485, "aggregationType": null, "units": null, "decimals": null, @@ -353,7 +294,6 @@ "label": "assignedToMeAlarms", "color": "#ffc107", "settings": {}, - "_hash": 0.6392390736755882, "aggregationType": null, "units": null, "decimals": null, @@ -381,7 +321,6 @@ "label": "criticalAlarms", "color": "#ffc107", "settings": {}, - "_hash": 0.8329478098552843, "aggregationType": null, "units": null, "decimals": null, @@ -400,37 +339,13 @@ } } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "", + "markdownTextPattern": "", "applyDefaultMarkdownStyle": false, "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n" }, @@ -447,7 +362,6 @@ "fontWeight": 400 }, "showLegend": false, - "useDashboardTimewindow": true, "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "" @@ -533,7 +447,6 @@ } } }, - "_hash": 0.39181957822569946, "decimals": 0 } ], @@ -859,7 +772,6 @@ } } }, - "_hash": 0.39181957822569946, "decimals": 0, "aggregationType": null, "funcBody": null, @@ -1146,7 +1058,8 @@ "fontWeight": 400 }, "widgetCss": "", - "noDataDisplayMessage": "" + "noDataDisplayMessage": "", + "datasources": [] }, "row": 0, "col": 0, @@ -1169,7 +1082,8 @@ "enableFullscreen": false, "widgetStyle": {}, "widgetCss": "", - "noDataDisplayMessage": "" + "noDataDisplayMessage": "", + "datasources": [] }, "row": 0, "col": 0, @@ -1212,7 +1126,8 @@ "showTitleIcon": false, "titleTooltip": "", "titleStyle": null, - "borderRadius": "12px" + "borderRadius": "12px", + "datasources": [] }, "row": 0, "col": 0, @@ -1298,7 +1213,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 20, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1325,7 +1241,8 @@ "backgroundImageUrl": null, "mobileAutoFillHeight": true, "mobileRowHeight": 70, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1352,7 +1269,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": true, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "layoutType": "default" } } } @@ -1386,7 +1304,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 20 + "mobileRowHeight": 20, + "layoutType": "default" } } } @@ -1478,33 +1397,14 @@ } }, "timewindow": { - "displayValue": "", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168326072, - "endTimeMs": 1680254726072 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 60000 }, "aggregation": { - "type": "AVG", - "limit": 25000 + "type": "AVG" } }, "settings": { @@ -1525,4 +1425,4 @@ } }, "name": "Tenant Administrator Home Page" -} +} \ No newline at end of file diff --git a/ui-ngx/src/assets/entity-limit-reached.svg b/ui-ngx/src/assets/entity-limit-reached.svg new file mode 100644 index 0000000000..55b53f8808 --- /dev/null +++ b/ui-ngx/src/assets/entity-limit-reached.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md new file mode 100644 index 0000000000..bc332f1ea5 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md @@ -0,0 +1,127 @@ +#### Active-all-the-time schedule format + +For a schedule that is always active, the argument can be an empty JSON object or a JSON object in the following format: + +```javascript +{ + "type": "ANY_TIME" +} +``` + +#### Specific time schedule format + +The argument value for a specific time schedule must be a JSON object in the following format (the `type` field is optional): + +```javascript +{ + "type": "SPECIFIC_TIME", + "daysOfWeek": [ + 2, + 4 + ], + "endsOn": 0, + "startsOn": 0, + "timezone": "Europe/Kiev" +} +``` + +
    +
  • +timezone: the name of the timezone. +
  • +
  • +daysOfWeek: days of the week (Monday = 1, Tuesday = 2, ..., Sunday = 7) when the schedule should be active. +
  • +
  • +startsOn: time of day in milliseconds from the start of the day (00:00) when the schedule becomes active on the selected days. +
  • +
  • +endsOn: time of day in milliseconds from the start of the day (00:00) when the schedule stops being active on the selected days. +If this value is not provided or equals 0, it defaults to the full day (24 hours in milliseconds). +
  • +
+ +If both startsOn and endsOn are 0, the schedule is active for the entire day. + +#### Custom time schedule format + +The argument value for a specific time schedule must be a JSON object in the following format (the `type` field is optional): + +```javascript +{ + "type": "CUSTOM", + "timezone": "Europe/Kiev", + "items": [ + { + "dayOfWeek": 1, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 2, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 3, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 4, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 5, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 6, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 7, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + } + ] +} +``` + +
    +
  • +timezone: the name of the timezone. +
  • +
  • +items: a list of day-specific schedule entries. +
  • +
+ +Each item represents one day of the week and contains: +
    +
  • +dayOfWeek: the day number (Monday = 1, Tuesday = 2, ..., Sunday = 7) +
  • +
  • +enabled: a boolean value that defines whether this day is active in the schedule. +
  • +
  • +startsOn: time of day in milliseconds from the start of the day (00:00) when the schedule becomes active for that day. +
  • +
  • +endsOn: time of day in milliseconds from the start of the day (00:00) when the schedule stops being active for that day. +If this value is not provided or equals 0, it defaults to the full day (24 hours in milliseconds). +
  • +
+ +If both startsOn and endsOn are 0, the schedule is active for the entire day. diff --git a/ui-ngx/src/assets/help/en_US/alarm-rule/expression_fn.md b/ui-ngx/src/assets/help/en_US/alarm-rule/expression_fn.md new file mode 100644 index 0000000000..0670384bd8 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/alarm-rule/expression_fn.md @@ -0,0 +1,47 @@ +## Alarm rule condition TBEL script function + +The **expression()** function is a user-defined script that enables custom condition expressions using [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. +It receives arguments configured in the alarm rule setup, along with an additional `ctx` object that stores `latestTs` and provides access to all arguments. + +### Function signature + +```javascript +function expression(ctx, arg1, arg2, ...): boolean +``` + +### Supported arguments + +There are two types of arguments supported in the alarm rule configuration: attributes and latest telemetry. + +These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. + +### Usage + +**Example: Convert temperature from Fahrenheit to Celsius and raise the alarm if the Celsius value is greater than 36** + +```javascript +var temperatureC = (temperatureF - 32) / 1.8; +return temperatureC > 36; +``` + +Alternatively, use `ctx` to access the argument as an object: + +```json +{ + "temperatureF": { + "ts": 1740644636669, + "value": 98.7 + } +} +``` + +You may notice that the object includes both the `value` of an argument and its timestamp as `ts`. +The `ctx` object also includes the property `latestTs`, which represents the latest timestamp of the arguments telemetry in milliseconds. + +Let's modify the expression that converts Fahrenheit to Celsius to also check if the temperature's timestamp is exactly at the start of an hour: + +```javascript +var temperatureC = (ctx.args.temperatureF.value - 32) / 1.8; +var temperatureTs = ctx.args.temperatureF.ts; +return temperatureC > 36 && ((temperatureTs / 1000) % 3600) == 0; +``` diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md index 1d45dea3c8..6129dac351 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md @@ -11,12 +11,16 @@ function calculate(ctx, arg1, arg2, ...): object | object[] ### Supported Arguments +Arguments are passed to the function by **name** defined in the calculated field configuration. + There are three types of arguments supported in the calculated field configuration: #### Attribute and Latest Telemetry Arguments These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. +#### Direct argument access via **``** + **Example: Convert Temperature from Fahrenheit to Celsius** ```javascript @@ -26,7 +30,9 @@ return { } ``` -Alternatively, using `ctx` to access the argument as an object: +#### Accessing argument via **`ctx.args.`** + +In addition to direct access, arguments can be accessed via the `ctx.args.` object, which includes both the `value` of an argument and its timestamp as `ts`: ```json { @@ -37,7 +43,6 @@ Alternatively, using `ctx` to access the argument as an object: } ``` -You may notice that the object includes both the `value` of an argument and its timestamp as `ts`. Let's modify the function that converts Fahrenheit to Celsius to also return the timestamp information: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md new file mode 100644 index 0000000000..22316fe418 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md @@ -0,0 +1,70 @@ +## Calculated Field TBEL Filter Function + +The **filter()** function is a [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) script used in aggregation metrics of a calculated field. + +It receives arguments configured in the calculated field setup, along with an additional `ctx` object that stores `latestTs` and provides access to all arguments. + +It allows you to include or exclude related entities from aggregation based on their telemetry or attribute values. + +The filter is evaluated per related entity before the aggregation function is applied. + +### Function Signature + +```javascript +function filter(ctx, arg1, arg2, ...): boolean +``` + +### Supported Arguments + +Arguments are passed to the function by **name** defined in the calculated field configuration. + +There are two types of arguments supported in the calculated field configuration: **Attribute and Latest Telemetry Arguments** + +These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. + +#### Direct argument access via **``** + +**Example: Count free parking spaces** + +**Goal**: Include only parking spaces that are active and not occupied. + +```javascript +return active == true && occupied == false; +``` + +Only entities that satisfy this condition will be included in the aggregation. + +#### Accessing argument via **`ctx.args.`** + +In addition to direct access, arguments can be accessed via the `ctx.args.` object, which includes both the `value` of an argument and its timestamp as `ts`: + +```json +{ + "consumption": { + "ts": 1740644656669, + "value": 542.6 + } +} +``` + +The `ctx.latestTs` property represents the latest timestamp across all related entities and their arguments participating in the aggregation. + +**Example: Calculate the total consumption across multiple related pumps** + +**Scenario**: Each pump reports consumption telemetry approximately every 10 minutes, but reporting times may vary due to network delays (up to ~30 seconds). +To avoid counting outdated values, only recently updated telemetry should be included. + +**Goal**: Include only pumps whose consumption value was reported within 1 minute of the latest timestamp. + +```javascript +var ONE_MINUTE = 60 * 1000; +return (ctx.latestTs - ctx.args.consumption.ts) <= ONE_MINUTE; +``` + +### Function return format + +The function **must** return a boolean: +- `true` → include entity in aggregation +- `false` → exclude entity from aggregation + +Any other return type is considered invalid. diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/map_expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/map_expression_fn.md new file mode 100644 index 0000000000..41ccf3540b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/map_expression_fn.md @@ -0,0 +1,79 @@ +## Calculated Field TBEL Map Function + +The **map()** function is a [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) script used in aggregation metrics of a calculated field. + +It determines the value applied by each related entity to the aggregation. + +The function receives arguments configured in the calculated field setup, along with an additional `ctx` object that stores `latestTs` and provides access to all arguments. + +The function is evaluated **per related entity**, after filtering is applied. + +### Function Signature + +```javascript +function map(ctx, arg1, arg2, ...): number | boolean | string +``` + +### Supported Arguments + +Arguments are passed to the function by **name** defined in the calculated field configuration. + +There are two types of arguments supported in the calculated field configuration: **Attribute and Latest Telemetry Arguments** + +These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. + +#### Direct argument access via **``** + +**Example: Calculate average temperature across sensors** + +**Scenario**: Multiple related sensors report temperature in Fahrenheit. You want to aggregate temperature values, but the aggregation must be performed in Celsius. + +**Goal**: Convert temperature to Celsius before aggregation + +```javascript +var temperatureC = (temperature - 32) / 1.8; +return toFixed(temperatureC, 2); +``` + +Instead of creating a separate calculated field for conversion, the transformation is performed per entity, before aggregation. + +#### Accessing argument via **`ctx.args.`** + +In addition to direct access, arguments can be accessed via the `ctx.args.` object, which includes both the `value` of an argument and its timestamp as `ts`: + +```json +{ + "temperature": { + "ts": 1740644656761, + "value": 33.6 + } +} +``` + +The `ctx.latestTs` property represents the latest timestamp across all related entities and their arguments participating in the aggregation. + +**Example: Calculate average temperature** + +**Scenario**: Each sensor reports temperature approximately every 10 minutes, but reporting times may vary due to network delays (up to ~30 seconds). +If a temperature value is outdated, you want the entity to remain part of the aggregation, but contribute a default value instead of a stale one. + +**Goal**: Return a default value when the temperature was not reported within 1 minute of the latest aggregation timestamp. + +```javascript +var ONE_MINUTE = 60 * 1000; +if ((ctx.latestTs - ctx.args.temperature.ts) > ONE_MINUTE) { + return 0; +}; +return temperature; +``` + +### Function return format + +The returned value is passed directly to the aggregation engine: + +| Aggregation function | Expected return value | +|--------------------------------|-----------------------| +| Count, Count unique | any value | +| Sum, Average, Minimum, Maximum | number | + +Returning a value of an incompatible type may result in the entity being ignored or the aggregation producing incorrect results. diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm.md b/ui-ngx/src/assets/help/en_US/notification/alarm.md index 00ba021e1b..e0e0dc3e8c 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm.md @@ -16,11 +16,13 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); * `recipientEmail` - email of the recipient; * `recipientFirstName` - first name of the recipient; * `recipientLastName` - last name of the recipient; +* `details.` - any key field from the alarm's details. Fox example, if details are `{"data": "Temperature is 25"}`, use `${details.data}` to access "Temperature is 25"; Parameter names must be wrapped using `${...}`. For example: `${action}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md index aa80b13b35..53b2ade36d 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md @@ -15,6 +15,7 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `assigneeTitle` - title of the assignee; * `assigneeEmail` - email of the assignee; diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md index 150e76babc..41ada1fba2 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md @@ -15,6 +15,7 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `comment` - text of the comment; * `action` - one of: 'added', 'updated'; diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md index 712f2b45e7..0df4732ab4 100644 --- a/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md +++ b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md @@ -12,6 +12,9 @@ Available template parameters: * `edgeId` - the edge id as uuid string; * `edgeName` - the name of the edge; * `failureMsg` - the string representation of the failure, occurred on the Edge; +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_connection.md b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md index 37f0ec7573..4c97f6ccde 100644 --- a/ui-ngx/src/assets/help/en_US/notification/edge_connection.md +++ b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md @@ -12,6 +12,9 @@ Available template parameters: * `edgeId` - the edge id as uuid string; * `edgeName` - the name of the edge; * `eventType` - the string representation of the connectivity status: connected or disconnected; +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md b/ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md new file mode 100644 index 0000000000..388f0f532e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md @@ -0,0 +1,46 @@ +#### Entity count limit increase request notification templatization + +
+
+ +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `entityType` - one of: 'Device', 'Asset', 'Customer', 'User', 'Dashboard', 'Rule chain', 'Edge'; +* `userEmail` - email of the user who sends the request; +* `increaseLimitActionLabel` - label of the button used to open Limits Management page, for ex: 'Set new limit'; +* `increaseLimitLink` - link to the Limits Management page; +* `baseUrl` - used to construct the full URL for the Limits Management page in email notifications; + +Parameter names must be wrapped using `${...}`. For example: `${userEmail}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${userEmail:upperCase}` +* `lowerCase`, for example - `${userEmail:lowerCase}` +* `capitalize`, for example - `${userEmail:capitalize}` + +
+ +##### Examples + +Let's assume the notification about the increasing limit of the maximum allowed devices for the tenant. +The following template: + +```text +${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit. +{:copy-code} +``` + +will be transformed to: + +```text +johndoe@company.com has reached the maximum number of devices allowed and is requesting an increase to the device limit. +``` + +
+ +
+
diff --git a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md index 6ea03a514c..2c0634a5f0 100644 --- a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md +++ b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md @@ -13,6 +13,9 @@ Available template parameters: * `usage` - the current usage value of the resource; * `serviceId` - the service id (convenient in cluster setup); * `serviceType` - the service type (convenient in cluster setup); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${resource}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md index 0bcf107298..da46dc11bd 100644 --- a/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md +++ b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md @@ -16,6 +16,9 @@ Available template parameters: * `entityType` - the type of the entity to which the task is related; * `entityId` - the id of the entity to which the task is related; * `attempt` - the number of attempts processing the task +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${entityType}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/rulechain/note_content.md b/ui-ngx/src/assets/help/en_US/rulechain/note_content.md new file mode 100644 index 0000000000..d21167be7f --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulechain/note_content.md @@ -0,0 +1,195 @@ +#### Markdown/HTML note content + +
+
+ +Notes support **Markdown** and **HTML** markup for rich text formatting directly on the rule chain canvas. + +
+
+ +#### Headings + +Use `#` to create headings. The number of `#` characters defines the level: + +```markdown +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +{:copy-code} +``` + +
+
+ +#### Text formatting + +```markdown +**Bold text** +*Italic text* +~~Strikethrough~~ +**_Bold and italic_** +{:copy-code} +``` + +**Bold text** +*Italic text* +~~Strikethrough~~ +**_Bold and italic_** + +
+
+ +#### Lists + +Unordered list using `-` or `*`: + +```markdown +- Step 1: Receive telemetry +- Step 2: Filter by threshold + - Value > 100 → alarm branch + - Value ≤ 100 → success branch +- Step 3: Save to time series +{:copy-code} +``` + +- Step 1: Receive telemetry +- Step 2: Filter by threshold + - Value > 100 → alarm branch + - Value ≤ 100 → success branch +- Step 3: Save to time series + +Ordered list using numbers: + +```markdown +1. Validate message type +2. Enrich with device attributes +3. Route to appropriate handler +{:copy-code} +``` + +1. Validate message type +2. Enrich with device attributes +3. Route to appropriate handler + +
+
+ +#### Blockquotes + +Use `>` for notes, warnings, or highlighted information: + +```markdown +> **Note:** This node requires a valid device alias to be configured upstream. + +> **Warning:** Debug mode increases load — disable in production. +{:copy-code} +``` + +> **Note:** This node requires a valid device alias to be configured upstream. + +> **Warning:** Debug mode increases load — disable in production. + +
+
+ +#### Inline code and code blocks + +Use backticks for inline code and triple backticks for blocks: + +```markdown +The message type must be `POST_TELEMETRY_REQUEST`. +{:copy-code} +``` + +The message type must be `POST_TELEMETRY_REQUEST`. + +````markdown +```json +{ + "temperature": 42.5, + "humidity": 68 +} +``` +{:copy-code} +```` + +```json +{ + "temperature": 42.5, + "humidity": 68 +} +``` + +
+
+ +#### Tables + +```markdown +| Branch | Condition | Next node | +|----------|------------------|--------------------| +| True | temperature > 80 | Create Alarm | +| False | temperature ≤ 80 | Save Timeseries | +{:copy-code} +``` + +| Branch | Condition | Next node | +|----------|------------------|--------------------| +| True | temperature > 80 | Create Alarm | +| False | temperature ≤ 80 | Save Timeseries | + +
+
+ +#### Horizontal rule + +Use `---` to visually separate sections: + +```markdown +### Input + +Device telemetry message. + +--- + +### Output + +Enriched message with customer attributes. +{:copy-code} +``` + +
+
+ +#### HTML elements + +When **Apply default markdown style** is enabled, you can also use HTML tags: + +```html +🔴 Critical path
+🟢 Happy path +{:copy-code} +``` + +
+
+ +#### Tips for rule chain notes + +Use notes to annotate the canvas with context that helps your team: + +```markdown +## 🌡️ Temperature monitoring flow + +Processes telemetry from HVAC sensors. + +| Threshold | Severity | +|-----------|----------| +| > 85 °C | CRITICAL | +| > 70 °C | WARNING | + +> **Owner:** Platform team · **Updated:** 2026-02 +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md index bc8823c777..3bb410f10b 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md @@ -79,7 +79,7 @@ function AddEntityDialogController(instance) { const mapType = widgetContext.mapInstance.type(); attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); - } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { + } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line') { attributes.push({key: 'perimeter', value: additionalParams.coordinates}); } else if (mapItemType === 'Circle') { attributes.push({key: 'circle', value: additionalParams.coordinates}); diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md index 131a674099..b192042d2f 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md @@ -26,8 +26,9 @@ A JavaScript function triggered after a map item is placed. Optionally uses an H
  • coordinates: Coordinates - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
    • Marker: {x: number; y: number}, where x represents latitude, and y represents longitude.
    • -
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • -
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • +
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polyline: TbPolylineRawCoordinates contains an array of points defining the polyline.
    Note: The coordinates will be automatically converted according to the selected map type.
  • diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md index be95f136ad..9af015d033 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md @@ -5,7 +5,7 @@ *function (data, dsData): string* -A JavaScript function used to compute color of the trip path point. +A JavaScript function used to compute color of the path point. **Parameters:** @@ -15,7 +15,7 @@ A JavaScript function used to compute color of the trip path point. **Returns:** -Should return string value presenting color of the trip path point. +Should return string value presenting color of the path point. In case no data is returned, color value from **Color** settings field will be used. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md index 4ce996626a..e9e202ef85 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md @@ -5,7 +5,7 @@ *function (data, dsData): string* -A JavaScript function used to compute text or HTML code to be displayed in the trip path point tooltip. +A JavaScript function used to compute text or HTML code to be displayed in the path point tooltip. **Parameters:** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md new file mode 100644 index 0000000000..b2eafef8cb --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md @@ -0,0 +1,22 @@ +#### Polyline label function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code of the polyline label. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML of the polyline label. + +
    + +{% include widget/lib/map/label_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md new file mode 100644 index 0000000000..6b49763393 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md @@ -0,0 +1,24 @@ +#### Polyline stroke color function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute stroke color of the polyline. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting stroke color of the polyline. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md new file mode 100644 index 0000000000..0de9b8ee7a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md @@ -0,0 +1,22 @@ +#### Polyline tooltip function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code to be displayed in the polyline tooltip. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the polyline tooltip. + +
    + +{% include widget/lib/map/tooltip_fn_examples %} diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index f5bebc2798..e171d4ed85 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -3882,7 +3882,6 @@ "two-factor-authentication": "المصادقة الثنائية العاملة", "passwords-mismatch-error": "يجب أن تكون كلمات المرور المدخلة متطابقة!", "password-again": "إعادة كلمة المرور", - "sign-in": "الرجاء تسجيل الدخول", "username": "اسم المستخدم (البريد الإلكتروني)", "remember-me": "تذكرني", "forgot-password": "نسيت كلمة المرور؟", diff --git a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json index 8633400394..5278a2c6a9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json @@ -3233,7 +3233,6 @@ "create-password": "Crear contrasenya", "passwords-mismatch-error": "¡Les contraseñas introduïdes han de ser iguals!", "password-again": "Repetiu la contrasenya de nou", - "sign-in": "Per favor, inicieu sessió", "username": "Nom d'usuari (correu electrònic)", "remember-me": "Recordar-me", "forgot-password": "Ha oblidat la contrasenya?", diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 041e3efe56..d065dc2ffd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -2151,7 +2151,6 @@ "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?", diff --git a/ui-ngx/src/assets/locale/locale.constant-da_DK.json b/ui-ngx/src/assets/locale/locale.constant-da_DK.json index 0992ccd6ff..fc39de2abc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-da_DK.json +++ b/ui-ngx/src/assets/locale/locale.constant-da_DK.json @@ -78,6 +78,7 @@ "show-more": "Vis mere", "dont-show-again": "Vis ikke igen", "see-documentation": "Se dokumentation", + "see-debug-events": "Se debug-hændelser", "clear": "Ryd", "upload": "Upload", "delete-anyway": "Slet alligevel", @@ -485,6 +486,7 @@ "2fa": { "2fa": "To-faktor autentificering", "available-providers": "Tilgængelige udbydere", + "available-providers-required": "Mindst én 2FA-udbyder skal være konfigureret.", "issuer-name": "Udsteders navn", "issuer-name-required": "Udsteders navn er påkrævet.", "max-verification-failures-before-user-lockout": "Maks. antal verificeringsfejl før kontolåsning", @@ -513,7 +515,9 @@ "verification-message-template-required": "Skabelon for verificeringsbesked er påkrævet.", "within-time": "Inden for tid (sek)", "within-time-pattern": "Tiden skal være et positivt heltal.", - "within-time-required": "Tid er påkrævet." + "within-time-required": "Tid er påkrævet.", + "force-2fa": "Gennemtving tofaktorgodkendelse", + "enforce-for": "Gennemtving for" }, "jwt": { "security-settings": "JWT sikkerhedsindstillinger", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarm", + "alarm-list": "Alarmliste", "alarms": "Alarmer", "all-alarms": "Alle alarmer", "select-alarm": "Vælg alarm", @@ -649,7 +654,16 @@ "alarm-type": "Alarmtype", "enter-alarm-type": "Indtast alarmtype", "no-alarm-types-matching": "Ingen alarmtyper matcher '{{entitySubtype}}'.", - "alarm-type-list-empty": "Ingen alarmtyper valgt." + "alarm-type-list-empty": "Ingen alarmtyper valgt.", + "system-comments": { + "acked-by-user": "Alarmen blev kvitteret af bruger {{userName}}", + "cleared-by-user": "Alarmen blev ryddet af bruger {{userName}}", + "assigned-to-user": "Alarmen blev tildelt af bruger {{userName}} til bruger {{assigneeName}}", + "unassigned-to-user": "Alarmens tildeling blev fjernet af bruger {{userName}}", + "unassigned-from-deleted-user": "Alarmens tildeling blev fjernet, fordi bruger {{userName}} - blev slettet", + "comment-deleted": "Bruger {{userName}} slettede sin kommentar", + "severity-changed": "Alarmens alvorlighed blev opdateret fra {{oldSeverity}} til {{newSeverity}}" + } }, "alarm-activity": { "add": "Tilføj en kommentar...", @@ -754,6 +768,7 @@ "name-max-length": "Navn skal være mindre end 256 tegn", "label-max-length": "Etiket skal være mindre end 256 tegn", "description": "Beskrivelse", + "description-required": "Beskrivelse er påkrævet.", "type": "Type", "type-required": "Type er påkrævet.", "details": "Detaljer", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Månedlig aktivitet for oprettede alarmer", "data-points": "Datapunkter", "data-points-storage-days": "Opbevaringsdage for datapunkter", + "data-points-storage-days-hourly-activity": "Datalagringsdage for datapunkter – timeaktivitet", + "data-points-storage-days-daily-activity": "Datalagringsdage for datapunkter – dagsaktivitet", + "data-points-storage-days-monthly-activity": "Datalagringsdage for datapunkter – månedsaktivitet", "device-api": "Enheds-API", "email": "E-mail", "email-messages": "E-mail-beskeder", @@ -900,6 +918,7 @@ "rule-node": "Rule Node", "sms": "SMS", "sms-messages": "SMS-beskeder", + "sms-messages-hourly-activity": "SMS-beskeder – timeaktivitet", "sms-messages-daily-activity": "Daglig aktivitet for SMS-beskeder", "sms-messages-monthly-activity": "Månedlig aktivitet for SMS-beskeder", "successful": "${entityName} succesfuld", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Timebaseret aktivitet for telemetri-persistens", "telemetry-persistence-monthly-activity": "Månedlig aktivitet for telemetri-persistens", "transport": "Transport", + "transport-msg-hourly-activity": "Transportbeskeder – timeaktivitet", + "transport-msg-daily-activity": "Transportbeskeder – dagsaktivitet", + "transport-msg-monthly-activity": "Transportbeskeder – månedsaktivitet", "transport-daily-activity": "Transport – dagsaktivitet", "transport-data-points": "Transportdatapunkter", + "transport-data-points-hourly-activity": "Transportdatapunkter – timeaktivitet", + "transport-data-points-daily-activity": "Transportdatapunkter – dagsaktivitet", + "transport-data-points-monthly-activity": "Transportdatapunkter – månedsaktivitet", "view-details": "Vis detaljer", "view-statistics": "Vis statistik", - "transport-messages": "Transportbeskeder" + "transport-messages": "Transportbeskeder", + "transport-messages-hourly-activity": "Transportbeskeder – timeaktivitet", + "transport-data-point-hourly-activity": "Transportdatapunkt – timeaktivitet", + "javascript-function-executions": "JavaScript-funktionsudførsler", + "javascript-function-executions-hourly-activity": "JavaScript-funktionsudførsler – timeaktivitet", + "javascript-function-executions-daily-activity": "JavaScript-funktionsudførsler – dagsaktivitet", + "javascript-function-executions-monthly-activity": "JavaScript-funktionsudførsler – månedsaktivitet", + "tbel-function-executions": "TBEL-funktionsudførsler", + "tbel-function-executions-hourly-activity": "TBEL-funktionsudførsler – timeaktivitet", + "tbel-function-executions-daily-activity": "TBEL-funktionsudførsler – dagsaktivitet", + "tbel-function-executions-monthly-activity": "TBEL-funktionsudførsler – månedsaktivitet", + "created-reports": "Oprettede rapporter", + "created-reports-hourly-activity": "Oprettede rapporter – timeaktivitet", + "created-reports-daily-activity": "Oprettede rapporter – dagsaktivitet", + "created-reports-monthly-activity": "Oprettede rapporter – månedsaktivitet", + "emails": "Emails", + "emails-hourly-activity": "Emails – timeaktivitet", + "emails-daily-activity": "Emails – dagsaktivitet", + "emails-monthly-activity": "Emails – månedsaktivitet", + "status": { + "enabled": "Aktiveret", + "disabled": "Deaktiveret", + "warning": "Advarsel" + } }, "api-limit": { "cassandra-write-queries-core": "REST API Cassandra-skriveforespørgsler", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Edge-oplinkbeskeder", "edge-uplink-messages-per-edge": "Edge-oplinkbeskeder pr. edge" }, + "api-key": { + "api-key": "API-nøgle", + "api-keys": "API-nøgler", + "delete-api-key-title": "Er du sikker på, at du vil slette API-nøglen '{{name}}'?", + "delete-api-key-text": "Vær forsigtig, efter bekræftelsen kan nøglen ikke gendannes.", + "delete-api-keys-title": "Er du sikker på, at du vil slette { count, plural, =1 {1 API-nøgle} other {# API-nøgler} }?", + "delete-api-keys-text": "Vær forsigtig, efter bekræftelsen kan alle valgte nøgler ikke gendannes.", + "expiration-date": "Udløbsdato", + "date": "Dato", + "description": "Beskrivelse", + "disable": "Deaktiver", + "edit-description": "Rediger beskrivelse", + "enable": "Aktivér API-nøgle ", + "expiration-time": "Udløbstid", + "expiration-time-never": "Aldrig", + "expiration-time-custom": "Tilpasset", + "generate": "Generér", + "generate-title": "Generér API-nøgle", + "generate-text": "Bemærk: API-nøglen arver tilladelserne fra den bruger, den oprettes til.", + "generated-api-key-title": "API-nøgle genereret. Lad os tjekke forbindelsen!", + "generated-api-key-copy": "Sørg for at kopiere og gemme din API-nøgle nu, da du ikke kan se den igen.", + "generated-api-key-command": "Brug følgende instruktioner til at tjekke forbindelsen. Som resultat bør du modtage oplysninger om den aktuelle bruger:", + "generated-api-key-insecure-url": "Hvis du udfører kommandoer over en usikker HTTP-forbindelse, sendes din API-nøgle ukrypteret, hvilket gør den sårbar over for opsnapning.", + "list": "{ count, plural, =1 {Én API-nøgle} other {Liste over # API-nøgler} }", + "manage": "Administrér", + "manage-api-keys": "Administrér API-nøgler", + "no-found": "Ingen API-nøgler fundet", + "selected-api-keys": "{ count, plural, =1 {1 API-nøgle} other {# API-nøgler} } valgt", + "search": "Søg efter API-nøgler", + "status": "Status", + "status-active": "Aktiv", + "status-inactive": "Inaktiv", + "status-expired": "Udløbet" + }, "audit-log": { "audit": "Revision", "audit-logs": "Revisionslogge", @@ -991,7 +1073,11 @@ "type-provision-failure": "Klargøring af device mislykkedes", "type-timeseries-updated": "Telemetri opdateret", "type-timeseries-deleted": "Telemetri slettet", - "type-sms-sent": "SMS sendt" + "type-sms-sent": "SMS sendt", + "any-type": "Enhver type", + "audit-log-filter-title": "Auditlogfilter", + "filter-title": "Filter", + "filter-types": "Auditlogtyper" }, "debug-settings": { "label": "Fejlfindingskonfiguration", @@ -1009,15 +1095,29 @@ "expression": "Udtryk", "no-found": "Ingen beregnede felter fundet", "list": "{ count, plural, =1 {Et beregnet felt} other {Liste over # beregnede felter} }", + "add": "Tilføj beregnet felt", "selected-fields": "{ count, plural, =1 {1 beregnet felt} other {# beregnede felter} } valgt", "type": { "simple": "Simpel", - "script": "Script" + "simple-hint": "Simpel aritmetisk beregning baseret på inputargumenter.", + "script": "Script", + "script-hint": "Beregning over definerede argumenter ved brug af et TBEL-script.", + "geofencing": "Geofencing", + "geofencing-hint": "Evaluering af entitetens GPS-position og overgange i forhold til konfigurerede geofencing-zonegrupper.", + "propagation": "Propagering", + "propagation-hint": "Propagering af data til overordnede eller underordnede entiteter baseret på relationsretning og -type.", + "related-entities-aggregation": "Aggregering af relaterede entiteter", + "related-entities-aggregation-hint": "Aggregering af seneste data fra relaterede entiteter.", + "time-series-data-aggregation": "Aggregering af tidsseriedata", + "time-series-data-aggregation-hint": "Aggregering af historiske data fra en aktuel entitet." }, + "preview": "Forhåndsvisning", "arguments": "Argumenter", "decimals-by-default": "Decimaler som standard", "debugging": "Fejlfinding af beregnet felt", + "calculated-field-details": "Detaljer for beregnet felt", "argument-name": "Argumentnavn", + "name": "Navn", "datasource": "Datakilde", "add-argument": "Tilføj argument", "test-script-function": "Test scriptfunktion", @@ -1029,6 +1129,8 @@ "argument-asset": "Aktiv", "argument-customer": "Kunde", "argument-tenant": "Aktuel lejer", + "argument-owner": "Nuværende ejer", + "argument-relation-query": "Relaterede entiteter", "argument-type": "Argumenttype", "attribute": "Attribut", "copy-argument-name": "Kopiér argumentnavn", @@ -1042,12 +1144,14 @@ "shared-attributes": "Delte attributter", "attribute-key": "Attributnøgle", "default-value": "Standardværdi", + "default-value-required": "Standardværdi er påkrævet.", "limit": "Maks. værdier", "time-window": "Tidsvindue", "customer-name": "Kundenavn", "asset-name": "Aktivnavn", "timeseries": "Tidsserie", "output": "Output", + "output-hint": "Definerer, hvordan output behandles.", "create": "Opret nyt beregnet felt", "file": "Beregnet felt-fil", "invalid-file-error": "Ugyldigt filformat. Sørg for, at filen er en gyldig JSON-fil.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Vær forsigtig, efter bekræftelse vil alle valgte beregnede felter blive fjernet og alle relaterede data ikke kunne gendannes.", "test-with-this-message": "Test med denne meddelelse", "use-latest-timestamp": "Brug seneste tidsstempel", + "entity-coordinates": "Entitetkoordinater", + "latitude-time-series-key": "Breddegradstidsserienøgle", + "latitude-time-series-key-required": "Breddegradstidsserienøgle er påkrævet.", + "longitude-time-series-key": "Længdegradstidsserienøgle", + "longitude-time-series-key-required": "Længdegradstidsserienøgle er påkrævet.", + "geofencing-zone-groups": "Geofencingzonegrupper", + "geofencing-zone-groups-settings": "Geofencingzonegruppeindstillinger", + "target-zone": "Målzone", + "perimeter-key": "Perimeternøgle", + "report-strategy": "Rapporteringsstrategi", + "no-zone-configured": "Mindst én zone er påkrævet.", + "no-zone-configured-required": "Mindst én zonegruppe skal være konfigureret.", + "add-zone-group": "Tilføj zonegruppe", + "report-transition-event-only": "Kun overgangshændelser", + "report-presence-status-only": "Kun tilstedeværelsesstatus", + "report-transition-event-and-presence": "Tilstedeværelsesstatus og overgangshændelser", + "perimeter-attribute-key": "Perimeterattributnøgle", + "perimeter-attribute-key-required": "Perimeterattributnøgle er påkrævet.", + "perimeter-attribute-key-pattern": "Perimeterattributnøgle er ugyldig.", + "entity-zone-relationship": "Sti fra Entitet til zoner", + "direction": "Relationsretning", + "direction-from": "Fra entitet til zone", + "direction-to": "Fra zone til entitet", + "relation-type": "Relationstype", + "create-relation-with-matched-zones": "Opret relationer for kildeentitet med matchende zoner", + "relation-level": "Relationsniveau", + "fetch-last-available-level": "Hent kun senest tilgængelige niveau", + "zone-group-refresh-interval": "Opdateringsinterval for zonegrupper", + "copy-zone-group-name": "Kopiér zonegruppenavn", + "open-details-page": "Åbn entitetens detaljeside", + "level": "Niveau", + "direction-level": "Retning", + "direction-up": "Op", + "direction-up-parent": "Op til overordnet", + "direction-down": "Ned", + "direction-down-child": "Ned til underordnet", + "add-level": "Tilføj niveau", + "delete-level": "Slet niveau", + "no-level": "Intet niveau konfigureret", + "levels-required": "Mindst ét niveau skal være konfigureret.", + "max-allowed-levels-error": "Relationsniveau overskrider det maksimalt tilladte.", + "propagation-path-related-entities": "Propageringssti til relaterede entiteter", + "propagate-type": { + "arguments-only": "Kun argumenter", + "expression-result": "Beregningsresultat" + }, + "script": "Script", + "data-propagate": "Data, der skal propageres", + "output-key": "Outputnøgle", + "copy-output-key": "Kopiér outputnøgle", + "aggregation-path-related-entities": "Aggregeringssti til relaterede entiteter", + "deduplication-interval": "Deduplikeringsinterval", + "deduplication-interval-min": "Deduplikeringsinterval skal være mindst {{ sec }} sekunder.", + "deduplication-interval-hint": "Minimumstid mellem telemetriaggregeringer.", + "deduplication-interval-required": "Deduplikeringsinterval er påkrævet.", + "calculated-field-filter-title": "Filter for beregnet felt", + "filter-title": "Filter", + "calculated-field-types": "Typer af beregnede felter", + "events": "Hændelser", + "any-type": "Enhver type", + "metrics": { + "metrics": "Metrikker", + "metrics-empty": "Mindst én metrik skal være konfigureret.", + "metric-name": "Metriknavn", + "metric-name-required": "Metriknavn er påkrævet.", + "metric-name-pattern": "Metriknavn er ugyldigt.", + "metric-name-duplicate": "Der findes allerede en metrik med dette navn.", + "metric-name-max-length": "Metriknavn skal være under 256 tegn.", + "metric-name-forbidden": "Metriknavn er reserveret og kan ikke bruges.", + "copy-metric-name": "Kopiér metriknavn", + "argument-name": "Argumentnavn", + "aggregation": "Aggregering", + "aggregation-type": { + "avg": "Gennemsnit", + "min": "Minimum", + "max": "Maksimum", + "sum": "Sum", + "count": "Antal", + "count-unique": "Antal unikke" + }, + "filtered": "Filtreret", + "value-source": "Værdikilde", + "value-source-hint": "Definerer, hvordan værdien til aggregering hentes.", + "value-source-type": { + "key": "Nøgle", + "function": "Funktion" + }, + "no-metrics-configured": "Mindst én metrik er påkrævet.", + "add-metric": "Tilføj metrik", + "max-metrics": "Maksimalt antal metrikker er nået.", + "metric-settings": "Metrikindstillinger", + "filter": "Filter", + "filter-hint": "Aktiverer filtrering af entiteter under aggregering. Filterfunktionen skal returnere en boolesk værdi og kan bruge alle konfigurerede argumenter." + }, + "output-strategy": { + "strategy": "Strategi", + "process-right-away": "Behandl med det samme", + "process-rule-chains": "Behandl via Regelkæder", + "save-time-series": "Gem i tidsserie", + "save-database": "Gem i database", + "save-latest-values": "Gem i seneste værdier", + "send-web-sockets": "Send til WebSockets", + "save-calculated-fields": "Send til beregnede felter", + "update-attribute-only-on-value-change": "Opdater attribut kun ved værdiskift", + "send-attributes-updated-notification": "Send notifikation om opdaterede attributter", + "ttl": "Tilpasset TTL", + "ttl-required": "TTL er påkrævet", + "ttl-min": "Kun 0 som minimum-TTL er tilladt", + "processing-parameters": "Behandlingsparametre", + "hint": { + "strategy": "Styrer, om resultatet behandles med det samme eller sendes til en regelkæde til yderligere behandling.", + "processing-options": "Behandlingsmuligheder", + "update-attribute-only-on-value-change": "Opdaterer attribut ved hver indgående besked, uanset om værdien har ændret sig. Dette øger API-forbrug og reducerer ydeevnen.", + "update-attribute-only-on-value-change-enabled": "Opdaterer attribut kun, når værdien ændrer sig. Hvis værdien er uændret, opdateres tidsstempler ikke, og der sendes ikke notifikationer.", + "send-attributes-updated-notification": "Sender en Attributter opdateret-hændelse til standardregel kæden.", + "save-time-series": "Gemmer tidsseriedata i tabellen ts_kv i databasen.", + "save-database": "Gemmer attributdata i databasen.", + "save-latest-values": "Opdaterer tidsseriedata i tabellen ts_kv_latest i databasen, hvis den nye værdi er nyere.", + "send-web-sockets-attribute": "Notificerer WebSocket-abonnementer om opdateringer af attributdata.", + "send-web-sockets-time-series": "Notificerer WebSocket-abonnementer om opdateringer af tidsseriedata.", + "save-calculated-fields-attribute": "Notificerer beregnede felter om opdateringer af attributdata.", + "save-calculated-fields-time-series": "Notificerer beregnede felter om opdateringer af tidsseriedata.", + "ttl": "Definerer opbevaringsperioden for tidsseriedata. Hvis deaktiveret, bruges Lejerprofil-TTL." + } + }, + "aggregate-interval-type": "Aggregeringsintervaltype", + "aggregate-interval-value": "Aggregeringsintervalværdi", + "aggregate-interval-value-required": "Aggregeringsintervalværdi er påkrævet.", + "aggregate-interval-value-min": "Aggregeringsintervalværdi skal være mindst { sec, plural, =0 {0 sekund} =1 {1 sekund} other {# sekunder} }.", + "aggregate-interval-value-step-multiple-of": "Aggregeringsintervalværdi skal være en divisor eller et multiplum af 1 dag.", + "aggregate-period": { + "hour": "Time", + "day": "Dag", + "week": "Uge (man - søn)", + "week-sun-sat": "Uge (søn - lør)", + "month": "Måned", + "quarter": "Kvartal", + "year": "År", + "custom": "Tilpasset" + }, + "aggregate-period-hint-offset": "Dit aggregeringsinterval vil være: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Dit aggregeringsinterval vil være: {{ interval }} og så videre.", + "entity-aggregation": { + "argument-hint": "Data vil blive hentet fra den aktuelle entitet.", + "argument-title-hint": "Definerer de inputargumenter, der bruges til aggregering.", + "argument-setting-hint": "Seneste telemetri er den eneste tilgængelige argumenttype for dette beregnede felt.", + "aggregation-interval": "Aggregeringsinterval", + "aggregation-interval-hint": "Definerer, hvor ofte aggregering udføres. Eksempel: hver 1 time aggregerer data kl. 00:00, 01:00, 02:00 osv. Aggregeringsresultater lagres med tidsstemplet, der svarer til starten af aggregeringsintervallet.", + "apply-offset": "Anvend forskydning på aggregeringsinterval", + "apply-offset-hint": "Definerer, hvor meget starten af hver aggregeringsperiode skal forskydes (f.eks. +10 minutter - 00:10, 01:10).", + "offset-value": "Forskydningsværdi", + "offset-value-required": "Forskydningsværdi er påkrævet.", + "offset-value-min": "Forskydningsværdi skal være et positivt heltal.", + "offset-value-max": "Forskydningsværdi skal være mindre end aggregeringsintervalværdien.", + "wait-delay": "Anvend ventetimeout for forsinket telemetri", + "wait-delay-hint": "Definerer, hvor længe der skal ventes på forsinket telemetri, efter intervallet slutter. Hvis sådan telemetri ankommer, genberegnes resultatet for det interval.", + "duration": "Varighed", + "duration-required": "Varighed er påkrævet.", + "duration-min": "Varighed skal være mindst 1 minut.", + "duration-hint": "Hvor længe der skal ventes på forsinkede data, efter intervallet slutter.", + "produce-intermediate-result": "Generér mellemliggende resultat", + "produce-intermediate-result-hint": "Beregner metrikker i det aktuelle interval for at generere et mellemliggende resultat. Opdateringer sker ikke oftere end én gang hver {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Beregnet felt af typen simpel må ikke indeholde nøgler med tidsserieglidningstype.", + "arguments-propagate-arguments-with-rolling": "Typen 'Time series rolling' er inkompatibel med propagering af 'Kun argumenter'.", + "arguments-propagate-argument-entity-type": "Entitetstypen er inkompatibel med propagering af 'Kun argumenter'.", + "arguments-propagate-argument-must-current-entity": "Mindst ét argument skal konfigureres med kildenhedstypen 'Aktuel entitet'.", "arguments-empty": "Argumenter må ikke være tomme.", "expression-required": "Udtryk er påkrævet.", "expression-invalid": "Udtryk er ugyldigt", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Argument med dette navn findes allerede.", "argument-name-max-length": "Argumentnavn skal være under 256 tegn.", "argument-name-forbidden": "Argumentnavn er reserveret og kan ikke anvendes.", + "output-key-required": "Outputnøgle er påkrævet.", + "output-key-pattern": "Outputnøgle er ugyldig.", + "output-key-duplicate": "Der findes allerede en nøgle med dette navn.", + "output-key-max-length": "Outputnøgle skal være under 256 tegn.", + "output-key-forbidden": "Outputnøgle er reserveret og kan ikke bruges.", + "entity-type-required": "Entitetstype er påkrævet", + "name-required": "Navn er påkrævet.", + "name-pattern": "Navn er ugyldigt.", + "name-duplicate": "Der findes allerede et navn med dette navn.", + "name-max-length": "Navn skal være under 256 tegn.", + "name-forbidden": "Navn er reserveret og kan ikke bruges.", "argument-type-required": "Argumenttype er påkrævet.", "max-args": "Maksimalt antal argumenter er nået.", "decimals-range": "Standard decimaler skal være et tal mellem 0 og 15.", "expression": "Standardudtryk demonstrerer, hvordan man omregner temperatur fra Fahrenheit til Celsius.", "arguments-entity-not-found": "Målentitet for argument ikke fundet.", - "use-latest-timestamp": "Hvis aktiveret, vil den beregnede værdi blive gemt med det nyeste tidsstempel fra argumenternes telemetri i stedet for serverens tid." + "use-latest-timestamp": "Hvis aktiveret, vil den beregnede værdi blive gemt med det nyeste tidsstempel fra argumenternes telemetri i stedet for serverens tid.", + "entity-coordinates": "Angiv de tidsserienøgler, der leverer entitetens GPS-koordinater (breddegrad og længdegrad).", + "geofencing-zone-groups": "Definér én eller flere geofencingzonegrupper, der skal kontrolleres (f.eks. 'allowedZones', 'restrictedZones'). Hver gruppe skal have et unikt navn, som bruges som præfiks for telemetrinøgler for beregnet felt-output.", + "perimeter-attribute-key": "Angiv attributnøglen, der indeholder definitionen af geofencingzonens perimeter. Perimeteren hentes altid fra serversideattributter for zoneentiteten.", + "report-strategy": "Tilstedeværelsesstatus rapporterer, om entiteten aktuelt er INDE i eller UDE af zonegruppen. Overgangshændelser rapporterer, hvornår entiteten GIK IND I eller FORLOD zonegruppen.", + "create-relation-with-matched-zones": "Opret og vedligehold automatisk relationer mellem entiteten og de zoner, den aktuelt er inde i. Relationer fjernes, når entiteten forlader en zone, og oprettes, når den går ind i en ny.", + "relation-type-required": "Relationstype er påkrævet.", + "relation-level-required": "Relationsniveau er påkrævet.", + "relation-level-min": "Minimumsværdi for relationsniveau er 1.", + "relation-level-max": "Maksimumværdi for relationsniveau er {{max}}.", + "geofencing-empty": "Mindst én zonegruppe skal være konfigureret.", + "geofencing-entity-not-found": "Geofencingmålsentitet blev ikke fundet.", + "max-geofencing-zone": "Maksimalt antal geofencingzoner er nået.", + "zone-group-refresh-interval": "Definerer, hvor ofte zonegrupper, der er konfigureret via relaterede entiteter, opdateres.", + "zone-group-refresh-interval-required": "Opdateringsinterval for zonegrupper er påkrævet.", + "zone-group-refresh-interval-min": "Opdateringsinterval for zonegrupper skal være mindst {{ min }} sekunder.", + "propagation-path-related-entities": "Definerer en direkte sti på ét enkelt niveau til en relateret entitet baseret på den valgte retning og relationstype.", + "data-propagate": "Definerer de data, der skal propageres fra argumenterne konfigureret nedenfor. 'Kun argumenter' bruger de hentede data direkte, mens 'Beregningsresultat' beregner en ny værdi ud fra disse data.", + "aggregation-path-related-entities": "Definerer en aggregeringssti på ét enkelt niveau via direkte relationer med overordnede eller underordnede entiteter, baseret på retning og relationstype. Kun relationer mellem enheds-, asset-, kunde- og lejerentiteter understøttes.", + "arguments-aggregation": "Definerer de inputargumenter, der bruges til filtrering og aggregering.", + "setting-arguments-aggregation": "Data vil blive hentet fra relaterede entiteter, der er konfigureret i aggregeringsstien.", + "metrics": "Definerer metrikker, der aggregeres baseret på konfigurerede argumenter.", + "entity-aggregation-metrics": "Definerer metrikker, der aggregeres baseret på konfigurerede argumenter over de angivne tidsintervaller.", + "import-invalid-calculated-field-type": "Kan ikke importere beregnet felt: Ugyldig struktur for beregnet felt.", + "simple-expression-title": "Aritmetisk udtryk, der definerer, hvordan den beregnede værdi beregnes.", + "script-title": "TBEL-script, der definerer beregningslogikken og outputværdierne.", + "simple-arguments": "Aritmetisk udtryk, der definerer, hvordan den beregnede værdi beregnes.", + "script-arguments": "Definerer de inputargumenter, der er tilgængelige for scriptet." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Alarmregler", + "alarm-rule": "Alarmregel", + "alarm-rules": "Alarmregler", + "alarm-rules-old": "Gammel", + "alarm-rules-actual": "Aktuel", + "severities": "Alvorlighedsgrader", + "cleared": "Rydningsbetingelse", + "delete-title": "Er du sikker på, at du vil slette alarmreglen '{{title}}'?", + "delete-text": "Vær forsigtig, efter bekræftelsen kan alarmreglen og alle relaterede data ikke gendannes.", + "delete-multiple-title": "Er du sikker på, at du vil slette { count, plural, =1 {1 alarmregel} other {# alarmregler} }?", + "delete-multiple-text": "Vær forsigtig, efter bekræftelsen vil alle valgte alarmregler blive fjernet, og alle relaterede data kan ikke gendannes.", + "create": "Opret ny alarmregel", + "add": "Tilføj alarmregel", + "copy": "Kopiér alarmregelkonfiguration", + "details": "Detaljer for alarmregel", + "no-found": "Ingen alarmregler fundet", + "list": "{ count, plural, =1 {Én alarmregel} other {Liste over # alarmregler} }", + "selected-fields": "{ count, plural, =1 {1 alarmregel} other {# alarmregler} } valgt", + "import": "Importér alarmregel", + "file": "Alarmregelfil", + "export": "Eksportér alarmregel", + "export-failed-error": "Kan ikke eksportere alarmregel: {{error}}", + "entity-type": "Entitetstype", + "entity-type-required": "Entitetstype er påkrævet.", + "alarm-type": "Alarmtype", + "alarm-type-hint": "Unik identifikator (f.eks. HighTempAlarm) inden for alarmoprinderens omfang (Enhed, Asset osv.) for at undgå konflikter.", + "alarm-type-required": "Alarmtype er påkrævet.", + "alarm-type-pattern": "Alarmtype er ugyldig.", + "alarm-type-max-length": "Alarmtype skal være under 256 tegn.", + "clear-alarm": "Ryd alarm", + "value-argument": "Argument", + "value-argument-required": "Argument er påkrævet.", + "static-settings": "Statiske indstillinger", + "configuration": "Konfiguration", + "static-schedule": "Statisk", + "dynamic-schedule": "Dynamisk", + "operation-and": "OG", + "operation-or": "ELLER", + "condition-during": "I løbet af {{during}}", + "condition-during-dynamic": "I løbet af \"{{ attribute }}\"", + "condition-repeat-times": "Gentages { count, plural, =1 {1 gang} other {# gange} }", + "condition-repeat-times-dynamic": "Gentages \"{{ attribute }}\" gange", + "filter-preview": "Forhåndsvisning af filter", + "condition-settings": "Betingelsesindstillinger", + "static": "Statisk", + "dynamic": "Dynamisk", + "argument-filters": "Argumentfiltre", + "argument-name": "Argumentnavn", + "value-type": "Værditype", + "general": "Generelt", + "filters": "Filtre", + "date-time-hint": "Argumentet skal være i epoch-millisekunder. Eksempel: 1698839340000 svarer til 2023-11-01 12:49:00 UTC.", + "operation": "Operation", + "value-source": "Værdikilde", + "value": "Værdi", + "ignore-case": "Ignorer store/små bogstaver", + "condition": "Betingelse", + "script": "Script", + "add-filter": "Tilføj argumentfilter", + "edit-filter": "Argumentfilter", + "remove-filter": "Fjern argumentfilter", + "no-filter": "Mindst ét filter er påkrævet.", + "conditions": { + "simple": "Simpel", + "duration": "Varighed", + "repeating": "Gentagende" + }, + "schedule-title": "Tidsplan", + "edit-schedule": "Rediger alarmtidsplan", + "schedule-type": "Tidsplanlægger-type", + "schedule-type-required": "Tidsplanlægger-type er påkrævet.", + "schedule": { + "any-time": "Aktiv hele tiden", + "specific-time": "Aktiv på et bestemt tidspunkt", + "custom": "Tilpasset" + }, + "schedule-day": { + "monday": "Mandag", + "tuesday": "Tirsdag", + "wednesday": "Onsdag", + "thursday": "Torsdag", + "friday": "Fredag", + "saturday": "Lørdag", + "sunday": "Søndag" + }, + "schedule-days": "Dage", + "schedule-time": "Tid", + "schedule-time-from": "Fra", + "schedule-time-to": "Til", + "schedule-days-of-week-required": "Mindst én ugedag skal vælges.", + "tbel": "TBEL", + "expression-type": { + "simple": "Simpel", + "script": "Script" + }, + "operation-type": { + "and": "Og", + "or": "Eller" + }, + "filter-predicate-type": { + "string": "Streng", + "numeric": "Numerisk", + "boolean": "Boolesk", + "complex": "Kompleks" + }, + "alarm-rule-additional-info": "Yderligere info", + "edit-alarm-rule-additional-info": "Rediger yderligere info", + "alarm-rule-additional-info-placeholder": "Angiv venligst dine kommentarer og justeringer her for at vise dem i Alarmdetaljer under Yderligere info", + "alarm-rule-additional-info-hint": "Tip: brug ${Argumentnavn} til at erstatte værdier for de argumenter, der bruges i alarmregelbetingelsen.", + "alarm-rule-additional-info-icon-hint": "Brug Argumentnavn til at erstatte værdier for de argumenter, der bruges i alarmregelbetingelsen.", + "alarm-rule-mobile-dashboard": "Mobilt Dashboard", + "alarm-rule-mobile-dashboard-hint": "Bruges af mobilapplikationen som et Dashboard til alarmdetaljer.", + "alarm-rule-no-mobile-dashboard": "Intet Dashboard valgt", + "alarm-rule-condition": "Alarmregelbetingelse", + "enter-alarm-rule-condition-prompt": "Tilføj betingelse", + "enter-alarm-rule-clear-condition-prompt": "Tilføj rydningsbetingelse", + "edit-alarm-rule-condition": "Alarmbetingelse", + "condition-type": "Betingelsestype", + "condition-type-hint": "\"Varighed\"- og \"Gentagende\"-mulighederne er ikke tilgængelige, når operationen \"Mangler i\" bruges i filtret.", + "select-alarm-severity": "Vælg alarmens alvorlighedsgrad", + "add-create-alarm-rule-prompt": "Mindst én udløserbetingelse er påkrævet.", + "add-create-alarm-rule": "Tilføj udløserbetingelse", + "add-clear-alarm-rule": "Tilføj rydningsbetingelse", + "condition-duration": "Betingelsesvarighed", + "condition-duration-value": "Varighedsværdi", + "condition-duration-time-unit": "Tidsenhed", + "condition-duration-value-range": "Varighedsværdi skal være i intervallet fra 1 til 2147483647.", + "condition-duration-value-pattern": "Varighedsværdi skal være heltal.", + "condition-duration-value-required": "Varighedsværdi er påkrævet.", + "condition-duration-time-unit-required": "Tidsenhed er påkrævet.", + "condition-repeating-value": "Antal hændelser", + "condition-repeating-value-hint": "Opdatering af ethvert alarmregelargument vil blive talt som en hændelse", + "condition-repeating-value-range": "Antal hændelser skal være i intervallet fra 1 til 2147483647.", + "condition-repeating-value-pattern": "Antal hændelser skal være heltal.", + "condition-repeating-value-required": "Antal hændelser er påkrævet.", + "create-conditions": "Udløserbetingelser", + "clear-condition": "Rydningsbetingelse", + "no-clear-alarm-rule": "Ingen rydningsbetingelse konfigureret.", + "advanced-settings": "Avancerede indstillinger", + "propagate-alarm": "Propagér alarm til relaterede entiteter", + "alarm-rule-relation-types-list": "Relationstyper", + "alarm-rule-relation-types-list-hint": "Definerer relationstyper til filtrering af de relaterede entiteter. Hvis ikke angivet, vil alarmen blive propageret til alle relaterede entiteter.", + "propagate-alarm-to-owner": "Propagér alarm til entitetens ejer (Kunde eller Lejer)", + "propagate-alarm-to-tenant": "Propagér alarm til Lejer", + "alarm-rule-filter-title": "Alarmregelfilter", + "filter-title": "Filter", + "debugging": "Fejlfinding af alarmregel", + "any-type": "Enhver type", + "enter-alarm-rule-type": "Angiv alarmtype", + "no-alarm-rule-types-matching": "Der blev ikke fundet nogen alarmtyper, der matcher '{{entitySubtype}}'.", + "alarm-rule-type-list-empty": "Ingen alarmtyper valgt.", + "alarm-rule-type-list": "Liste over alarmtyper", + "alarm-rule-entity-list": "Entitetsliste", + "missing-for": "mangler i", + "time-unit": "Enhed", + "mode": "Tilstand", + "type": "Type", + "value-required": "Værdi er påkrævet.", + "min-value": "Værdien skal være 1 eller højere.", + "argument-in-use": "Argumentet bruges som generelt argument.", + "import-invalid-alarm-rule-type": "Kan ikke importere alarmregel: Ugyldig struktur for alarmregel.", + "no-filter-preview": "Intet filter angivet", + "filter-operation": { + "and": "Og", + "or": "Eller" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Dokumentation", "time-left": "{{time}} tilbage", "output": "Output", + "sort-asc": "Stigende", + "sort-desc": "Faldende", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Dashboardrækkefølge i mobilapplikation", "mobile-hide": "Skjul dashboard i mobilapplikation", "update-image": "Opdatér dashboardbillede", + "update-new-version": "Upload ny version", + "upload-file-to-update": "Upload fil for at opdatere", "take-screenshot": "Tag skærmbillede", "select-widget-title": "Vælg widget", "select-widget-value": "{{title}}: vælg widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrap-klient", "bootstrap-server": "Bootstrap-server", "lwm2m-server": "LwM2M-server", + "client-reboot": "Trigger for registreringsopdatering", + "bootstrap-reboot": "Trigger for bootstrap-anmodning", "client-publicKey-or-id": "Klientens offentlige nøgle eller ID", "client-publicKey-or-id-required": "Klientens offentlige nøgle eller ID er påkrævet.", "client-publicKey-or-id-tooltip-psk": "PSK-identifikator er en vilkårlig identifikator op til 128 bytes som beskrevet i standarden [RFC7925].\nDen SKAL konverteres til en streng og kodes med UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "Kort server-ID er påkrævet.", "short-id-range": "Kort server-ID skal være mellem {{ min }} og {{ max }}.", "short-id-pattern": "Kort server-ID skal være et positivt heltal.", + "short-id-pattern-bs": "Kort server-ID må kun være null", "lifetime": "Klientens registreringslevetid", "lifetime-required": "Registreringslevetid er påkrævet.", "lifetime-pattern": "Registreringslevetid skal være et positivt heltal.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Alle ressourcer observeres med en enkelt sammensat Observe-anmodning (mere effektivt, mindre fleksibelt)", "composite-by-object": "Sammensat efter objekt", "composite-by-object-description": "Ressourcer grupperes efter objekttype og observeres via separate sammensatte Observe-anmodninger (balanceret tilgang)" - } + }, + "init-attr-tel-as-obs-strategy": "Initialisér attributter og telemetri ved hjælp af Observe-strategien", + "init-attr-tel-as-obs-strategy-hint": "Hvis false - initialiseres attributter og telemetri ved at læse deres værdier én ad gangen.\\nHvis true - initialiseres attributter og telemetri ved at abonnere på deres værdier ved hjælp af Observe-strategien." }, "snmp": { "add-communication-config": "Tilføj kommunikationskonfiguration", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Regelnoder", "list-of-rulenodes": "{ count, plural, =1 {Én regelnode} other {Liste over # regelnoder} }", "rulenode-name-starts-with": "Regelnoder, hvis navne starter med '{{prefix}}'", + "type-api-key": "API-nøgle", + "type-api-keys": "API-nøgler", "type-current-customer": "Aktuel kunde", "type-current-tenant": "Aktuel lejer", "type-current-user": "Aktuel bruger", @@ -2656,6 +3143,7 @@ "details": "Enhedsdetaljer", "no-entities-prompt": "Ingen enheder fundet", "no-data": "Ingen data at vise", + "show-all-columns": "Vis alle", "columns-to-display": "Kolonner at vise", "type-api-usage-state": "API-brugsstatus", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Én mobilapplikation} other {Liste over # mobilapplikationer} }", "type-mobile-app-bundle": "Mobilpakke", "type-mobile-app-bundles": "Mobilpakker", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Én mobilpakke} other {Liste over # mobilpakker} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Én mobilpakke} other {Liste over # mobilpakker} }", + "limit-reached": "Grænse nået", + "limit-reached-text": "Du har nået grænsen på {{ entities }}. For at tilføje flere skal du bede din systemadministrator om at øge din {{ entity }}-grænse.", + "request-limit-increase": "Anmod om øgning af grænse", + "request-sysadmin-text": "Er du systemadministratoren?", + "login-here": "Log ind her", + "to-increase-limit": "for at øge grænsen.", + "increase-limit-request-sent-title": "Vi har sendt en automatisk anmodning til din systemadministrator om at øge grænsen", + "increase-limit-request-sent-text": "Giv dem venligst lidt tid til at gennemgå anmodningen og opdatere indstillingerne. Du skal muligvis genindlæse denne side for at se ændringerne." }, "entity-field": { "created-time": "Oprettelsestidspunkt", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Nulstillingslink er sendt", "email": "Email", "invalid-email-format": "Ugyldigt emailformat.", + "sign-in-with": "Log ind med {{name}}", + "sign-in-to-your-account": "Log ind på din konto", "or": "eller", "error": "Loginfejl", "verify-your-identity": "Bekræft din identitet", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Aktiveringslinket er udløbet", "activation-link-expired-message": "Linket til at aktivere din profil er udløbet. Du kan vende tilbage til login-siden for at modtage en ny email.", "reset-password-link-expired": "Nulstillingslinket for adgangskode er udløbet", - "reset-password-link-expired-message": "Linket til at nulstille din adgangskode er udløbet. Du kan vende tilbage til login-siden for at modtage en ny email." + "reset-password-link-expired-message": "Linket til at nulstille din adgangskode er udløbet. Du kan vende tilbage til login-siden for at modtage en ny email.", + "two-fa": "Tofaktorgodkendelse", + "two-fa-required": "Tofaktorgodkendelse er påkrævet", + "set-up-verification-method": "Opsæt en verifikationsmetode for at fortsætte", + "set-up-verification-method-login": "Opsæt en verifikationsmetode eller log ind", + "enable-authenticator-app": "Aktivér godkendelsesapp", + "enable-authenticator-app-description": "Indtast venligst sikkerhedskoden fra din godkendelsesapp", + "enable-authenticator-sms": "Aktivér SMS-godkendelse", + "enable-authenticator-sms-description": "Indtast den 6-cifrede kode, vi lige har sendt til ", + "enable-authenticator-email": "Aktivér email-godkendelse", + "enable-authenticator-email-description": "En sikkerhedskode er blevet sendt til din emailadresse på ", + "enter-key-manually": "eller indtast denne 32-cifrede nøgle manuelt:", + "continue": "Fortsæt", + "confirm": "Bekræft", + "authenticator-app-success": "Godkendelsesapp blev aktiveret", + "authenticator-app-success-description": "Næste gang du logger ind, skal du angive en tofaktorgodkendelseskode", + "authenticator-sms-success": "SMS-godkendelse blev aktiveret", + "authenticator-sms-success-description": "Næste gang du logger ind, bliver du bedt om at indtaste sikkerhedskoden, som sendes til telefonnummeret", + "authenticator-email-success": "Email-godkendelse blev aktiveret", + "authenticator-email-success-description": "Næste gang du logger ind, bliver du bedt om at indtaste sikkerhedskoden, som sendes til din emailadresse", + "authenticator-backup-code-success": "Backupkode blev aktiveret", + "authenticator-backup-code-success-description": "Næste gang du logger ind, bliver du bedt om at indtaste sikkerhedskoden eller bruge en af backupkoderne.", + "add-verification-method": "Tilføj verifikationsmetode", + "get-backup-code": "Hent backupkode", + "copy-key": "Kopiér nøgle", + "send-code": "Send kode", + "email-label": "Email", + "email-description": "Indtast en email, der skal bruges som din godkendelse.", + "sms-description": "Indtast et telefonnummer, der skal bruges som din godkendelse.", + "backup-code-description": "Print koderne, så du har dem ved hånden, når du skal bruge dem til at logge ind på din konto. Du kan bruge hver backupkode én gang.", + "backup-code-warn": "Når du forlader denne side, kan disse koder ikke vises igen. Opbevar dem sikkert ved hjælp af mulighederne nedenfor.", + "download-txt": "Download (txt)", + "print": "Udskriv", + "verification-code": "6-cifret kode", + "verification-code-invalid": "Ugyldigt format for verifikationskode", + "verification-code-incorrect": "Verifikationskoden er forkert", + "verification-code-many-request": "For mange anmodninger om at kontrollere verifikationskode", + "scan-qr-code": "Scan denne QR-kode med din verifikationsapp", + "phone-input": { + "phone-input-label": "Telefonnummer", + "phone-input-required": "Telefonnummer er påkrævet", + "phone-input-validation": "Telefonnummer er ugyldigt eller ikke muligt", + "phone-input-pattern": "Ugyldigt telefonnummer. Skal være i E.164-format, f.eks. {{phoneNumber}}", + "phone-input-hint": "Telefonnummer i E.164-format, f.eks. {{phoneNumber}}" + } }, "mobile": { "add-application": "Tilføj applikation", @@ -4175,6 +4717,7 @@ "api-usage-limit": "API-brugsgrænse", "device-activity": "Enhedsaktivitet", "entities-limit": "Enhedsgrænse", + "entities-limit-increase-request": "Anmodning om øgning af entitetsgrænse", "entity-action": "Enhedshandling", "general": "Generelt", "rule-engine-lifecycle-event": "Livscyklushændelse for Rule Engine", @@ -4392,6 +4935,12 @@ "at-least": "Mindst:", "character": "{ count, plural, =1 {1 tegn} other {# tegn} }", "digit": "{ count, plural, =1 {1 ciffer} other {# cifre} }", + "password-tooltip-min-length": "Mindst {{minimumLength}} tegn langt", + "password-tooltip-max-length": "Højst {{maximumLength}} tegn langt", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} stort bogstav", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} lille bogstav", + "password-tooltip-digit": "{{minimumDigits}} tal", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} specialtegn", "incorrect-password-try-again": "Forkert adgangskode. Prøv igen", "lowercase-letter": "{ count, plural, =1 {1 lille bogstav} other {# små bogstaver} }", "new-passwords-not-match": "De nye adgangskoder matcher ikke", @@ -4450,7 +4999,8 @@ "additional-info": "Yderligere info (JSON)", "invalid-additional-info": "Kunne ikke fortolke JSON for yderligere info.", "no-relations-text": "Ingen relationer fundet", - "not": "Ikke" + "not": "Ikke", + "copy-type": "Kopiér type" }, "resource": { "add": "Tilføj ressource", @@ -5400,7 +5950,8 @@ }, "time-series": "Tidsserier", "latest": "Seneste værdier", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Beregnede felter og alarmregler" }, "save-attribute": { "processing-settings": "Behandlingsindstillinger", @@ -5595,7 +6146,8 @@ "bad-request-params": "Ugyldige forespørgselsparametre", "item-not-found": "Element ikke fundet", "too-many-requests": "For mange forespørgsler", - "too-many-updates": "For mange opdateringer" + "too-many-updates": "For mange opdateringer", + "entities-limit-exceeded": "Entitetsgrænse overskredet" }, "tenant": { "tenant": "Lejer", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Maksimalt antal argumenter pr. beregnet felt", "max-arguments-per-cf-range": "Maksimalt antal argumenter pr. beregnet felt kan ikke være negativt", "max-arguments-per-cf-required": "Maksimalt antal argumenter pr. beregnet felt er påkrævet", + "max-related-level-per-argument": "Maksimalt relationsniveau pr. argumentet 'Relaterede entiteter'", + "max-related-level-per-argument-range": "Maks. antal for relationsniveau pr. argumentet 'Relaterede entiteter' kan ikke være mindre end '1'", + "max-related-level-per-argument-required": "Maks. antal for relationsniveau pr. argumentet 'Relaterede entiteter' er påkrævet", + "min-allowed-scheduled-update-interval": "Min. tilladt opdateringsinterval for argumenter af typen 'Relaterede entiteter' (sekunder)", + "min-allowed-scheduled-update-interval-range": "Min. tilladt opdateringsinterval kan ikke være negativt", + "min-allowed-deduplication-interval": "Min. tilladt deduplikeringsinterval (sekunder)", + "min-allowed-deduplication-interval-range": "Min. tilladt deduplikeringsintervalværdi kan ikke være negativ", + "min-allowed-deduplication-interval-required": "Min. tilladt deduplikeringsinterval er påkrævet", + "intermediate-aggregation-interval": "Mellemliggende aggregeringsinterval (sekunder)", + "intermediate-aggregation-interval-range": "Mellemliggende aggregeringsintervalværdi kan ikke være mindre end '1'", + "intermediate-aggregation-interval-required": "Mellemliggende aggregeringsinterval er påkrævet", + "reevaluation-check-interval": "Kontrolinterval for revurdering (sekunder)", + "reevaluation-check-interval-range": "Kontrolinterval for revurdering kan ikke være mindre end '1'", + "reevaluation-check-interval-required": "Kontrolinterval for revurdering er påkrævet", + "alarms-reevaluation-interval": "Revurderingsinterval for alarmer (sekunder)", + "alarms-reevaluation-interval-range": "Revurderingsinterval for alarmer kan ikke være mindre end '1'", + "alarms-reevaluation-interval-required": "Revurderingsinterval for alarmer er påkrævet", + "min-allowed-aggregation-interval": "Min. tilladt aggregeringsinterval (sekunder)", + "min-allowed-aggregation-interval-range": "Min. tilladt aggregeringsintervalværdi kan ikke være negativ", + "min-allowed-aggregation-interval-required": "Min. tilladt aggregeringsinterval er påkrævet", + "min-allowed-scheduled-update-interval-required": "Min. tilladt opdateringsintervals minimumsværdi er påkrævet", "max-state-size": "Maksimal tilstandsstørrelse i KB", "max-state-size-range": "Maksimal tilstandsstørrelse i KB kan ikke være negativ", "max-state-size-required": "Maksimal tilstandsstørrelse i KB er påkrævet", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Maksimalt antal abonnementer pr. almindelig bruger", "ws-limit-max-subscriptions-per-public-user": "Maksimalt antal abonnementer pr. offentlig bruger", "ws-limit-updates-per-session": "WS-opdateringer pr. session", + "relation-search-entity-limit": "Entitetsgrænse for relationssøgning", + "relation-search-entity-limit-hint": "Begrænser antallet af entiteter, der opløses på det sidste niveau i relationsstien. Gælder for argumenter af typen 'Relaterede entiteter' og propageringsfelter.", + "relation-search-entity-limit-required": "Entitetsgrænse for relationssøgning er påkrævet", + "relation-search-entity-limit-range": "Entitetsgrænse for relationssøgning kan ikke være mindre end '1'", "rate-limits": { "add-limit": "Tilføj begrænsning", "and-also-less-than": "og også mindre end", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Standard grupperingsinterval", "edit-intervals-list-hint": "Liste over tilgængelige intervalmuligheder kan specificeres.", "edit-grouping-intervals-list-hint": "Det er muligt at konfigurere listen over grupperingsintervaller og standard grupperingsinterval.", - "all": "Alle" + "all": "Alle", + "save-current-settings-as-default": "Gem aktuelle indstillinger som standardtidsvindue", + "hide-option-from-end-users": "Skjul indstillingen for slutbrugere" }, "tooltip": { "trigger": "Udløser", @@ -6648,6 +7227,7 @@ "export-attributes": "Eksporter attributter", "export-credentials": "Eksporter legitimationsoplysninger", "export-calculated-fields": "Eksportér beregnede felter \nog alarmregler", + "export-alarm-rules": "Eksportér alarmregler", "entity-versions": "Entitetsversioner", "versions": "Versioner", "created-time": "Oprettelsestidspunkt", @@ -6665,6 +7245,7 @@ "load-attributes": "Indlæs attributter", "load-credentials": "Indlæs legitimationsoplysninger", "load-calculated-fields": "Indlæs beregnede felter og alarmregler", + "load-alarm-rules": "Indlæs alarmregler", "compare-with-current": "Sammenlign med nuværende", "diff-entity-with-version": "Forskel fra entitetsversion '{{versionName}}'", "previous-difference": "Forrige forskel", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Scan QR-kode", "make-phone-call": "Foretag telefonopkald", "get-location": "Hent telefonplacering", - "take-screenshot": "Tag skærmbillede" + "take-screenshot": "Tag skærmbillede", + "handle-provision-success-function": "Håndtér funktion ved vellykket provisionering", + "get-location-function": "Hent placeringsfunktion", + "process-launch-result-function": "Behandl funktion for startresultat", + "get-phone-number-function": "Hent telefonnummerfunktion", + "process-image-function": "Behandl billedfunktion", + "process-qr-code-function": "Behandl QR-kodefunktion", + "process-location-function": "Behandl placeringsfunktion", + "handle-empty-result-function": "Håndtér funktion for tomt resultat", + "handle-error-function": "Håndtér fejlfunktion", + "handle-non-mobile-fallback-function": "Håndtér fallbackfunktion for ikke-mobil", + "save-to-gallery": "Gem i 'Billedgalleri'", + "provision-type": "Provisioneringstype", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Brugerdefineret handlingsfunktion", "custom-pretty-function": "Brugerdefineret handling (med HTML-skabelon) funktion", @@ -6883,7 +7480,8 @@ "marker": "Markør", "polygon": "Polygon", "rectangle": "Rektangel", - "circle": "Cirkel" + "circle": "Cirkel", + "polyline": "Polylinje" }, "place-map-item": "Placér kortelement", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Fortsæt med at tegne polygon", "finish-draw-polygon": "Afslut tegning af polygon", "start-draw-circle": "Start med at tegne cirkel", - "finish-draw-circle": "Afslut tegning af cirkel" + "finish-draw-circle": "Afslut tegning af cirkel", + "start-draw-polyline": "Start tegning af polylinje", + "finish-draw-polyline": "Afslut tegning af polylinje" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Opdater animationsforsinkelse" }, "chart-axis": { + "limit": "Grænse", + "source": "Kilde", + "key-value": "Nøgle / Værdi", + "value-required": "Værdi er påkrævet.", + "entity-key-required": "Entitetsnøgle er påkrævet.", + "key-required": "Nøgle er påkrævet.", + "scale-limits": "Skalagrænser", + "scale-appearance": "Skalaudseende", "scale": "Skala", "scale-min": "min", "scale-max": "maks", @@ -8005,7 +8613,10 @@ "add-radio-option": "Tilføj radiomulighed", "radio-label-position": "Etiketposition", "radio-label-position-before": "Før", - "radio-label-position-after": "Efter" + "radio-label-position-after": "Efter", + "save-image": "Gem billede", + "save-to-gallery": "Gem automatisk optagne billeder i Billedgalleriet", + "public-image": "Gør billedet tilgængeligt for enhver uautoriseret bruger" }, "invalid-qr-code-text": "Ugyldig inputtekst til QR-kode. Input skal være af typen streng", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Rejser", "markers": "Markører", "polygons": "Polygoner", - "circles": "Cirkler" + "circles": "Cirkler", + "polylines": "Polylinjer" }, "data-layer": { "source": "Kilde", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Cirkel for '{{entityName}}': klik for at afslutte og gemme cirkel", "finish-circle-hint": "Cirkel: klik for at afslutte tegning" }, + "polyline": { + "polyline-key": "Polylinjenøgle", + "polyline-key-required": "Polylinjenøgle er påkrævet", + "no-polylines": "Ingen polylinjer konfigureret", + "add-polylines": "Tilføj polylinje", + "polyline-configuration": "Polylinjekonfiguration", + "remove-polyline": "Fjern polylinje", + "edit": "Rediger polylinje", + "cut": "Beskær polylinjeområde", + "rotate": "Rotér polylinje", + "remove-polyline-for": "Fjern polylinje for '{{entityName}}'", + "draw-polyline": "Tegn polylinje", + "polyline-place-first-point-hint-with-entity": "Polylinje for '{{entityName}}': klik for at placere første punkt", + "polyline-place-first-point-hint": "Polylinje: klik for at placere første punkt", + "finish-polyline-hint-with-entity": "Polylinje for '{{entityName}}': klik for at afslutte tegningen", + "finish-polyline-hint": "Polylinje: klik for at afslutte tegningen", + "polyline-place-first-point-cut-hint": "Klik for at placere første punkt", + "finish-polyline-cut-hint": "Klik på den første markør for at afslutte og gemme" + }, "select-entity": "Vælg enhed", "select-entity-hint": "Tip: klik på kortet efter valg for at angive position" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Vis tomt mellemrum i stedet for skjult cellehandling", "dont-reserve-space-hidden-action": "Reserver ikke plads til skjulte handlingsknapper", "display-timestamp": "Tidsstempel", + "timestamp-column-name": "Tidsstempel", "display-pagination": "Vis sidetal", "default-page-size": "Standard sidestørrelse", "page-step-settings": "Indstillinger for sidetrin", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Mindst én alarmkolonne skal angives", "table-tabs": "Tabeller i faner", "show-cell-actions-menu-mobile": "Vis cellehandlingsmenu i mobiltilstand", - "disable-sorting": "Deaktiver sortering" + "disable-sorting": "Deaktiver sortering", + "sort-by": "Sortér faner efter", + "sort-timestamp-option": "Oprettelsestid" }, "latest-chart": { "total": "Total", @@ -9491,11 +10125,28 @@ "content": "

    Ved at oprette dashboards til slutbrugere kan en kundebruger kun se sine egne enheder, mens data fra andre kunder skjules.

    Følg dokumentationen for at lære, hvordan du gør det:

    " } } + }, + "api-usage": { + "api-usage": "API-forbrug", + "label": "Etiket", + "state-name": "Tilstandsnavn", + "status": "Status", + "status-required": "Status er påkrævet.", + "limit": "Maks. grænse", + "limit-required": "Maks. grænse er påkrævet.", + "current-number": "Aktuelt antal", + "current-number-required": "Aktuelt antal er påkrævet.", + "add-key": "Tilføj nøgle", + "no-key": "Ingen nøgle", + "delete-key": "Slet nøgle", + "target-dashboard-state": "Mål-dashboardtilstand", + "go-to-main-state": "Gå til standardvisning" } }, "icon": { "icon": "Ikon", "icons": "Ikoner", + "custom": "Tilpasset", "select-icon": "Vælg ikon", "material-icons": "Materialeikoner", "show-all": "Vis alle ikoner", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "af" }, "language": { + "auto": "Auto", "language": "Sprog" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index d218d2b96b..2dfdd9645d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -78,6 +78,7 @@ "show-more": "Mehr anzeigen", "dont-show-again": "Nicht erneut anzeigen", "see-documentation": "Dokumentation ansehen", + "see-debug-events": "Debug-Ereignisse ansehen", "clear": "Löschen", "upload": "Hochladen", "delete-anyway": "Trotzdem löschen", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Zwei-Faktor-Authentifizierung", "available-providers": "Verfügbare Anbieter", + "available-providers-required": "Mindestens ein 2FA-Anbieter muss konfiguriert werden.", "issuer-name": "Ausstellername", "issuer-name-required": "Ausstellername ist erforderlich.", "max-verification-failures-before-user-lockout": "Maximale Verifizierungsfehler vor Kontosperrung", @@ -513,7 +515,9 @@ "verification-message-template-required": "Nachrichtenvorlage ist erforderlich.", "within-time": "Innerhalb von (Sek.)", "within-time-pattern": "Zeit muss eine positive Ganzzahl sein.", - "within-time-required": "Zeit ist erforderlich." + "within-time-required": "Zeit ist erforderlich.", + "force-2fa": "Zwei-Faktor-Authentifizierung erzwingen", + "enforce-for": "Erzwingen für" }, "jwt": { "security-settings": "JWT-Sicherheitseinstellungen", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarm", + "alarm-list": "Alarmliste", "alarms": "Alarme", "all-alarms": "Alle Alarme", "select-alarm": "Alarm auswählen", @@ -649,7 +654,16 @@ "alarm-type": "Alarmtyp", "enter-alarm-type": "Alarmtyp eingeben", "no-alarm-types-matching": "Keine Alarmtypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", - "alarm-type-list-empty": "Keine Alarmtypen ausgewählt." + "alarm-type-list-empty": "Keine Alarmtypen ausgewählt.", + "system-comments": { + "acked-by-user": "Alarm wurde von Benutzer {{userName}} bestätigt", + "cleared-by-user": "Alarm wurde von Benutzer {{userName}} gelöscht", + "assigned-to-user": "Alarm wurde von Benutzer {{userName}} Benutzer {{assigneeName}} zugewiesen", + "unassigned-to-user": "Zuweisung des Alarms wurde von Benutzer {{userName}} aufgehoben", + "unassigned-from-deleted-user": "Zuweisung des Alarms wurde aufgehoben, weil Benutzer {{userName}} gelöscht wurde", + "comment-deleted": "Benutzer {{userName}} hat seinen Kommentar gelöscht", + "severity-changed": "Alarmschweregrad wurde von {{oldSeverity}} auf {{newSeverity}} aktualisiert" + } }, "alarm-activity": { "add": "Kommentar hinzufügen...", @@ -754,6 +768,7 @@ "name-max-length": "Name sollte weniger als 256 Zeichen haben", "label-max-length": "Label sollte weniger als 256 Zeichen haben", "description": "Beschreibung", + "description-required": "Beschreibung ist erforderlich.", "type": "Typ", "type-required": "Typ ist erforderlich.", "details": "Details", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Monatliche Aktivität der Alarm-Erstellung", "data-points": "Datenpunkte", "data-points-storage-days": "Speicherzeitraum der Datenpunkte (in Tagen)", + "data-points-storage-days-hourly-activity": "Stichpunkt-Speichertage stündliche Aktivität", + "data-points-storage-days-daily-activity": "Stichpunkt-Speichertage tägliche Aktivität", + "data-points-storage-days-monthly-activity": "Stichpunkt-Speichertage monatliche Aktivität", "device-api": "Geräte-API", "email": "E-Mail", "email-messages": "E-Mail-Nachrichten", @@ -900,6 +918,7 @@ "rule-node": "Regelknoten", "sms": "SMS", "sms-messages": "SMS-Nachrichten", + "sms-messages-hourly-activity": "SMS-Nachrichten stündliche Aktivität", "sms-messages-daily-activity": "Tägliche Aktivität der SMS-Nachrichten", "sms-messages-monthly-activity": "Monatliche Aktivität der SMS-Nachrichten", "successful": "${entityName} Erfolgreich", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Stündliche Aktivität der Telemetriepersistenz", "telemetry-persistence-monthly-activity": "Monatliche Aktivität der Telemetriepersistenz", "transport": "Transport", + "transport-msg-hourly-activity": "Transportnachrichten stündliche Aktivität", + "transport-msg-daily-activity": "Transportnachrichten tägliche Aktivität", + "transport-msg-monthly-activity": "Transportnachrichten monatliche Aktivität", "transport-daily-activity": "Tägliche Transportaktivität", "transport-data-points": "Transportierte Datenpunkte", + "transport-data-points-hourly-activity": "Transportdatenpunkte stündliche Aktivität", + "transport-data-points-daily-activity": "Transportdatenpunkte tägliche Aktivität", + "transport-data-points-monthly-activity": "Transportdatenpunkte monatliche Aktivität", "view-details": "Details anzeigen", "view-statistics": "Statistiken anzeigen", - "transport-messages": "Transportnachrichten" + "transport-messages": "Transportnachrichten", + "transport-messages-hourly-activity": "Transportnachrichten stündliche Aktivität", + "transport-data-point-hourly-activity": "Transportdatenpunkt stündliche Aktivität", + "javascript-function-executions": "Ausführungen von JavaScript-Funktionen", + "javascript-function-executions-hourly-activity": "Ausführungen von JavaScript-Funktionen stündliche Aktivität", + "javascript-function-executions-daily-activity": "Ausführungen von JavaScript-Funktionen tägliche Aktivität", + "javascript-function-executions-monthly-activity": "Ausführungen von JavaScript-Funktionen monatliche Aktivität", + "tbel-function-executions": "Ausführungen von TBEL-Funktionen", + "tbel-function-executions-hourly-activity": "Ausführungen von TBEL-Funktionen stündliche Aktivität", + "tbel-function-executions-daily-activity": "Ausführungen von TBEL-Funktionen tägliche Aktivität", + "tbel-function-executions-monthly-activity": "Ausführungen von TBEL-Funktionen monatliche Aktivität", + "created-reports": "Erstellte Berichte", + "created-reports-hourly-activity": "Erstellte Berichte stündliche Aktivität", + "created-reports-daily-activity": "Erstellte Berichte tägliche Aktivität", + "created-reports-monthly-activity": "Erstellte Berichte monatliche Aktivität", + "emails": "E-Mails", + "emails-hourly-activity": "E-Mails stündliche Aktivität", + "emails-daily-activity": "E-Mails tägliche Aktivität", + "emails-monthly-activity": "E-Mails monatliche Aktivität", + "status": { + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "warning": "Warnung" + } }, "api-limit": { "cassandra-write-queries-core": "REST-API Cassandra-Schreibabfragen", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Edge-Uplink-Nachrichten", "edge-uplink-messages-per-edge": "Edge-Uplink-Nachrichten pro Edge" }, + "api-key": { + "api-key": "API-Schlüssel", + "api-keys": "API-Schlüssel", + "delete-api-key-title": "Möchten Sie den API-Schlüssel '{{name}}' wirklich löschen?", + "delete-api-key-text": "Achtung, nach der Bestätigung kann der Schlüssel nicht wiederhergestellt werden.", + "delete-api-keys-title": "Möchten Sie { count, plural, =1 {1 API-Schlüssel} other {# API-Schlüssel} } wirklich löschen?", + "delete-api-keys-text": "Achtung, nach der Bestätigung können alle ausgewählten Schlüssel nicht wiederhergestellt werden.", + "expiration-date": "Ablaufdatum", + "date": "Datum", + "description": "Beschreibung", + "disable": "Deaktivieren", + "edit-description": "Beschreibung bearbeiten", + "enable": "API-Schlüssel aktivieren ", + "expiration-time": "Ablaufzeit", + "expiration-time-never": "Nie", + "expiration-time-custom": "Benutzerdefiniert", + "generate": "Generieren", + "generate-title": "API-Schlüssel generieren", + "generate-text": "Hinweis: Der API-Schlüssel übernimmt die Berechtigungen des Benutzers, für den er erstellt wird.", + "generated-api-key-title": "API-Schlüssel generiert. Lassen Sie uns die Verbindung prüfen!", + "generated-api-key-copy": "Stellen Sie sicher, dass Sie Ihren API-Schlüssel jetzt kopieren und speichern, da Sie ihn nicht erneut anzeigen können.", + "generated-api-key-command": "Verwenden Sie die folgenden Anweisungen, um die Verbindung zu prüfen. Als Ergebnis sollten Sie die aktuellen Benutzerinformationen erhalten:", + "generated-api-key-insecure-url": "Das Ausführen von Befehlen über eine unsichere HTTP-Verbindung sendet Ihren API-Schlüssel unverschlüsselt und macht ihn anfällig für Abfangen.", + "list": "{ count, plural, =1 {Ein API-Schlüssel} other {Liste von # API-Schlüsseln} }", + "manage": "Verwalten", + "manage-api-keys": "API-Schlüssel verwalten", + "no-found": "Keine API-Schlüssel gefunden", + "selected-api-keys": "{ count, plural, =1 {1 API-Schlüssel} other {# API-Schlüssel} } ausgewählt", + "search": "API-Schlüssel suchen", + "status": "Status", + "status-active": "Aktiv", + "status-inactive": "Inaktiv", + "status-expired": "Abgelaufen" + }, "audit-log": { "audit": "Audit", "audit-logs": "Audit-Protokolle", @@ -991,7 +1073,11 @@ "type-provision-failure": "Gerätebereitstellung fehlgeschlagen", "type-timeseries-updated": "Telemetrie aktualisiert", "type-timeseries-deleted": "Telemetrie gelöscht", - "type-sms-sent": "SMS gesendet" + "type-sms-sent": "SMS gesendet", + "any-type": "Beliebiger Typ", + "audit-log-filter-title": "Audit-Protokoll-Filter", + "filter-title": "Filter", + "filter-types": "Audit-Protokoll-Typen" }, "debug-settings": { "label": "Debug-Konfiguration", @@ -1009,15 +1095,29 @@ "expression": "Ausdruck", "no-found": "Keine berechneten Felder gefunden", "list": "{ count, plural, =1 {Ein berechnetes Feld} other {Liste von # berechneten Feldern} }", + "add": "Berechnetes Feld hinzufügen", "selected-fields": "{ count, plural, =1 {1 berechnetes Feld} other {# berechnete Felder} } ausgewählt", "type": { "simple": "Einfach", - "script": "Skript" + "simple-hint": "Einfache arithmetische Berechnung basierend auf Eingabeargumenten.", + "script": "Skript", + "script-hint": "Berechnung über definierte Argumente mithilfe eines TBEL-Skripts.", + "geofencing": "Geofencing", + "geofencing-hint": "Auswertung der GPS-Position der Entität und von Übergängen gegenüber konfigurierten Geofencing-Zonengruppen.", + "propagation": "Weitergabe", + "propagation-hint": "Weitergabe von Daten an übergeordnete oder untergeordnete Entitäten basierend auf Beziehungsrichtung und -typ.", + "related-entities-aggregation": "Aggregation verwandter Entitäten", + "related-entities-aggregation-hint": "Aggregation der neuesten Daten aus verwandten Entitäten.", + "time-series-data-aggregation": "Aggregation von Zeitreihendaten", + "time-series-data-aggregation-hint": "Aggregation historischer Daten aus einer aktuellen Entität." }, + "preview": "Vorschau", "arguments": "Argumente", "decimals-by-default": "Standard-Dezimalstellen", "debugging": "Berechnetes Feld Debugging", + "calculated-field-details": "Details zum berechneten Feld", "argument-name": "Argumentname", + "name": "Name", "datasource": "Datenquelle", "add-argument": "Argument hinzufügen", "test-script-function": "Skriptfunktion testen", @@ -1029,6 +1129,8 @@ "argument-asset": "Asset", "argument-customer": "Kunde", "argument-tenant": "Aktueller Mandant", + "argument-owner": "Aktueller Eigentümer", + "argument-relation-query": "Verwandte Entitäten", "argument-type": "Argumenttyp", "attribute": "Attribut", "copy-argument-name": "Argumentnamen kopieren", @@ -1042,12 +1144,14 @@ "shared-attributes": "Geteilte Attribute", "attribute-key": "Attribut-Schlüssel", "default-value": "Standardwert", + "default-value-required": "Standardwert ist erforderlich.", "limit": "Maximale Werte", "time-window": "Zeitfenster", "customer-name": "Kundenname", "asset-name": "Asset-Name", "timeseries": "Zeitreihe", "output": "Ausgabe", + "output-hint": "Legt fest, wie die Ausgabe verarbeitet wird.", "create": "Neues berechnetes Feld erstellen", "file": "Berechnetes Feld-Datei", "invalid-file-error": "Ungültiges Dateiformat. Bitte stellen Sie sicher, dass die Datei eine gültige JSON-Datei ist.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Vorsicht, nach der Bestätigung werden alle ausgewählten berechneten Felder entfernt und alle zugehörigen Daten unwiederbringlich gelöscht.", "test-with-this-message": "Mit dieser Nachricht testen", "use-latest-timestamp": "Letzten Zeitstempel verwenden", + "entity-coordinates": "Entitätskoordinaten", + "latitude-time-series-key": "Breitengrad-Zeitreihenschlüssel", + "latitude-time-series-key-required": "Breitengrad-Zeitreihenschlüssel ist erforderlich.", + "longitude-time-series-key": "Längengrad-Zeitreihenschlüssel", + "longitude-time-series-key-required": "Längengrad-Zeitreihenschlüssel ist erforderlich.", + "geofencing-zone-groups": "Geofencing-Zonengruppen", + "geofencing-zone-groups-settings": "Einstellungen der Geofencing-Zonengruppe", + "target-zone": "Zielzone", + "perimeter-key": "Umfangsschlüssel", + "report-strategy": "Meldestrategie", + "no-zone-configured": "Mindestens eine Zone ist erforderlich.", + "no-zone-configured-required": "Mindestens eine Zonengruppe muss konfiguriert werden.", + "add-zone-group": "Zonengruppe hinzufügen", + "report-transition-event-only": "Nur Übergangsereignisse", + "report-presence-status-only": "Nur Anwesenheitsstatus", + "report-transition-event-and-presence": "Anwesenheitsstatus und Übergangsereignisse", + "perimeter-attribute-key": "Umfangsattributschlüssel", + "perimeter-attribute-key-required": "Umfangsattributschlüssel ist erforderlich.", + "perimeter-attribute-key-pattern": "Umfangsattributschlüssel ist ungültig.", + "entity-zone-relationship": "Pfad von der Entität zu den Zonen", + "direction": "Beziehungsrichtung", + "direction-from": "Von der Entität zur Zone", + "direction-to": "Von der Zone zur Entität", + "relation-type": "Beziehungstyp", + "create-relation-with-matched-zones": "Beziehungen für die Quellentität mit übereinstimmenden Zonen erstellen", + "relation-level": "Beziehungsebene", + "fetch-last-available-level": "Nur letzte verfügbare Ebene abrufen", + "zone-group-refresh-interval": "Aktualisierungsintervall der Zonengruppen", + "copy-zone-group-name": "Name der Zonengruppe kopieren", + "open-details-page": "Detailseite der Entität öffnen", + "level": "Ebene", + "direction-level": "Richtung", + "direction-up": "Nach oben", + "direction-up-parent": "Nach oben zum übergeordneten Element", + "direction-down": "Nach unten", + "direction-down-child": "Nach unten zum untergeordneten Element", + "add-level": "Ebene hinzufügen", + "delete-level": "Ebene löschen", + "no-level": "Keine Ebene konfiguriert", + "levels-required": "Mindestens eine Ebene muss konfiguriert werden.", + "max-allowed-levels-error": "Beziehungsebene überschreitet das maximal Zulässige.", + "propagation-path-related-entities": "Weitergabepfad zu verwandten Entitäten", + "propagate-type": { + "arguments-only": "Nur Argumente", + "expression-result": "Berechnungsergebnis" + }, + "script": "Skript", + "data-propagate": "Weiterzugebende Daten", + "output-key": "Ausgabeschlüssel", + "copy-output-key": "Ausgabeschlüssel kopieren", + "aggregation-path-related-entities": "Aggregationspfad zu verwandten Entitäten", + "deduplication-interval": "Deduplizierungsintervall", + "deduplication-interval-min": "Deduplizierungsintervall muss mindestens {{ sec }} Sekunden betragen.", + "deduplication-interval-hint": "Mindestzeit zwischen Telemetrieaggregationen.", + "deduplication-interval-required": "Deduplizierungsintervall ist erforderlich.", + "calculated-field-filter-title": "Filter für berechnete Felder", + "filter-title": "Filter", + "calculated-field-types": "Typen berechneter Felder", + "events": "Ereignisse", + "any-type": "Beliebiger Typ", + "metrics": { + "metrics": "Metriken", + "metrics-empty": "Mindestens eine Metrik muss konfiguriert werden.", + "metric-name": "Metrikname", + "metric-name-required": "Metrikname ist erforderlich.", + "metric-name-pattern": "Metrikname ist ungültig.", + "metric-name-duplicate": "Eine Metrik mit diesem Namen existiert bereits.", + "metric-name-max-length": "Der Metrikname sollte kürzer als 256 Zeichen sein.", + "metric-name-forbidden": "Der Metrikname ist reserviert und kann nicht verwendet werden.", + "copy-metric-name": "Metrikname kopieren", + "argument-name": "Argumentname", + "aggregation": "Aggregation", + "aggregation-type": { + "avg": "Durchschnitt", + "min": "Minimum", + "max": "Maximum", + "sum": "Summe", + "count": "Anzahl", + "count-unique": "Eindeutige Anzahl" + }, + "filtered": "Gefiltert", + "value-source": "Wertquelle", + "value-source-hint": "Legt fest, wie der Wert für die Aggregation ermittelt wird.", + "value-source-type": { + "key": "Schlüssel", + "function": "Funktion" + }, + "no-metrics-configured": "Mindestens eine Metrik ist erforderlich.", + "add-metric": "Metrik hinzufügen", + "max-metrics": "Maximale Anzahl an Metriken erreicht.", + "metric-settings": "Metrikeinstellungen", + "filter": "Filter", + "filter-hint": "Aktiviert die Filterung von Entitäten während der Aggregation. Die Filterfunktion muss einen booleschen Wert zurückgeben und kann alle konfigurierten Argumente verwenden." + }, + "output-strategy": { + "strategy": "Strategie", + "process-right-away": "Sofort verarbeiten", + "process-rule-chains": "Über Regelketten verarbeiten", + "save-time-series": "In Zeitreihe speichern", + "save-database": "In Datenbank speichern", + "save-latest-values": "In neueste Werte speichern", + "send-web-sockets": "An WebSockets senden", + "save-calculated-fields": "An Berechnete Felder senden", + "update-attribute-only-on-value-change": "Attribut nur bei Wertänderung aktualisieren", + "send-attributes-updated-notification": "Benachrichtigung über aktualisierte Attribute senden", + "ttl": "Benutzerdefinierte TTL", + "ttl-required": "TTL ist erforderlich", + "ttl-min": "Als minimales TTL ist nur 0 zulässig", + "processing-parameters": "Verarbeitungsparameter", + "hint": { + "strategy": "Steuert, ob das Ergebnis sofort verarbeitet oder zur zusätzlichen Verarbeitung an eine Regelkette gesendet wird.", + "processing-options": "Verarbeitungsoptionen", + "update-attribute-only-on-value-change": "Aktualisiert das Attribut bei jeder eingehenden Nachricht, unabhängig davon, ob sich der Wert geändert hat. Dies erhöht die API-Nutzung und verringert die Performance.", + "update-attribute-only-on-value-change-enabled": "Aktualisiert das Attribut nur, wenn sich der Wert ändert. Wenn der Wert unverändert ist, werden Zeitstempel nicht aktualisiert und Benachrichtigungen nicht gesendet.", + "send-attributes-updated-notification": "Sendet ein Ereignis „Attribute aktualisiert“ an die Standard-Regelkette.", + "save-time-series": "Speichert Zeitreihendaten in der Tabelle ts_kv in der Datenbank.", + "save-database": "Speichert Attributdaten in der Datenbank.", + "save-latest-values": "Aktualisiert Zeitreihendaten in der Tabelle ts_kv_latest in der Datenbank, wenn der neue Wert aktueller ist.", + "send-web-sockets-attribute": "Benachrichtigt WebSocket-Abonnements über Aktualisierungen der Attributdaten.", + "send-web-sockets-time-series": "Benachrichtigt WebSocket-Abonnements über Aktualisierungen der Zeitreihendaten.", + "save-calculated-fields-attribute": "Benachrichtigt Berechnete Felder über Aktualisierungen der Attributdaten.", + "save-calculated-fields-time-series": "Benachrichtigt Berechnete Felder über Aktualisierungen der Zeitreihendaten.", + "ttl": "Definiert die Aufbewahrungsdauer für Zeitreihendaten. Wenn deaktiviert, wird die TTL des Mandantenprofils verwendet." + } + }, + "aggregate-interval-type": "Aggregationsintervalltyp", + "aggregate-interval-value": "Aggregationsintervallwert", + "aggregate-interval-value-required": "Aggregationsintervallwert ist erforderlich.", + "aggregate-interval-value-min": "Aggregationsintervallwert muss mindestens { sec, plural, =0 {0 Sekunde} =1 {1 Sekunde} other {# Sekunden} } betragen.", + "aggregate-interval-value-step-multiple-of": "Aggregationsintervallwert muss ein Teiler oder ein Vielfaches von 1 Tag sein.", + "aggregate-period": { + "hour": "Stunde", + "day": "Tag", + "week": "Woche (Mo - So)", + "week-sun-sat": "Woche (So - Sa)", + "month": "Monat", + "quarter": "Quartal", + "year": "Jahr", + "custom": "Benutzerdefiniert" + }, + "aggregate-period-hint-offset": "Ihr Aggregationsintervall wird sein: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Ihr Aggregationsintervall wird sein: {{ interval }} und so weiter.", + "entity-aggregation": { + "argument-hint": "Daten werden von der aktuellen Entität abgerufen.", + "argument-title-hint": "Legt die für die Aggregation verwendeten Eingabeargumente fest.", + "argument-setting-hint": "Neueste Telemetrie ist der einzige verfügbare Argumenttyp für dieses berechnete Feld.", + "aggregation-interval": "Aggregationsintervall", + "aggregation-interval-hint": "Legt fest, wie oft die Aggregation durchgeführt wird. Beispiel: Alle 1 Stunde werden Daten um 00:00, 01:00, 02:00 usw. aggregiert. Aggregationsergebnisse werden mit dem Zeitstempel gespeichert, der dem Beginn des Aggregationsintervalls entspricht.", + "apply-offset": "Offset auf Aggregationsintervall anwenden", + "apply-offset-hint": "Legt fest, um wie viel der Beginn jeder Aggregationsperiode verschoben wird (z. B. +10 Minuten – 00:10, 01:10).", + "offset-value": "Offset-Wert", + "offset-value-required": "Offset-Wert ist erforderlich.", + "offset-value-min": "Offset-Wert muss eine positive ganze Zahl sein.", + "offset-value-max": "Offset-Wert sollte kleiner als der Aggregationsintervallwert sein.", + "wait-delay": "Await-Timeout für verzögerte Telemetrie anwenden", + "wait-delay-hint": "Legt fest, wie lange nach dem Ende des Intervalls auf verzögerte Telemetrie gewartet wird. Wenn solche Telemetrie eintrifft, wird das Ergebnis für dieses Intervall neu berechnet.", + "duration": "Dauer", + "duration-required": "Dauer ist erforderlich.", + "duration-min": "Dauer sollte mindestens 1 Minute betragen.", + "duration-hint": "Wie lange nach dem Ende des Intervalls auf verzögerte Daten gewartet wird.", + "produce-intermediate-result": "Zwischenergebnis erzeugen", + "produce-intermediate-result-hint": "Berechnet Metriken während des aktuellen Intervalls, um ein Zwischenergebnis zu erzeugen. Aktualisierungen erfolgen nicht häufiger als einmal alle {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Ein berechnetes Feld vom Typ „Einfach“ sollte keine Schlüssel mit dem Zeitreihen-Rolling-Typ enthalten.", + "arguments-propagate-arguments-with-rolling": "Der Typ „Zeitreihen-Rolling“ ist nicht kompatibel mit der Weitergabe „Nur Argumente“.", + "arguments-propagate-argument-entity-type": "Der Entitätstyp ist nicht kompatibel mit der Weitergabe „Nur Argumente“.", + "arguments-propagate-argument-must-current-entity": "Mindestens ein Argument muss mit dem Quellentitätstyp „Aktuelle Entität“ konfiguriert werden.", "arguments-empty": "Mindestens ein Argument sollte angegeben werden.", "expression-required": "Ausdruck ist erforderlich.", "expression-invalid": "Ausdruck ist ungültig", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Ein Argument mit diesem Namen existiert bereits.", "argument-name-max-length": "Argumentname sollte weniger als 256 Zeichen enthalten.", "argument-name-forbidden": "Argumentname ist reserviert und darf nicht verwendet werden.", + "output-key-required": "Ausgabeschlüssel ist erforderlich.", + "output-key-pattern": "Ausgabeschlüssel ist ungültig.", + "output-key-duplicate": "Ein Schlüssel mit diesem Namen existiert bereits.", + "output-key-max-length": "Ausgabeschlüssel sollte kürzer als 256 Zeichen sein.", + "output-key-forbidden": "Ausgabeschlüssel ist reserviert und kann nicht verwendet werden.", + "entity-type-required": "Entitätstyp ist erforderlich", + "name-required": "Name ist erforderlich.", + "name-pattern": "Name ist ungültig.", + "name-duplicate": "Ein Name mit diesem Namen existiert bereits.", + "name-max-length": "Name sollte kürzer als 256 Zeichen sein.", + "name-forbidden": "Name ist reserviert und kann nicht verwendet werden.", "argument-type-required": "Argumenttyp ist erforderlich.", "max-args": "Maximale Anzahl an Argumenten erreicht.", "decimals-range": "Standard-Dezimalstellen sollten eine Zahl zwischen 0 und 15 sein.", "expression": "Standardausdruck demonstriert, wie eine Temperatur von Fahrenheit in Celsius umgewandelt wird.", "arguments-entity-not-found": "Zielentität des Arguments nicht gefunden.", - "use-latest-timestamp": "Wenn aktiviert, wird der berechnete Wert mit dem neuesten Zeitstempel aus der Telemetrie der Argumente gespeichert, anstatt mit der Serverzeit." + "use-latest-timestamp": "Wenn aktiviert, wird der berechnete Wert mit dem neuesten Zeitstempel aus der Telemetrie der Argumente gespeichert, anstatt mit der Serverzeit.", + "entity-coordinates": "Geben Sie die Zeitreihenschlüssel an, die die GPS-Koordinaten der Entität (Breitengrad und Längengrad) bereitstellen.", + "geofencing-zone-groups": "Definieren Sie eine oder mehrere Geofencing-Zonengruppen zur Prüfung (z. B. 'allowedZones', 'restrictedZones'). Jede Gruppe muss einen eindeutigen Namen haben, der als Präfix für Telemetrieschlüssel der Ausgabe des berechneten Feldes verwendet wird.", + "perimeter-attribute-key": "Legen Sie den Attributschlüssel fest, der die Definition des Geofencing-Zonenumfangs enthält. Der Umfang wird immer aus serverseitigen Attributen der Zonenentität übernommen.", + "report-strategy": "Der Anwesenheitsstatus meldet, ob sich die Entität aktuell INNERHALB oder AUSSERHALB der Zonengruppe befindet. Übergangsereignisse melden, wann die Entität die Zonengruppe BETRETEN oder VERLASSEN hat.", + "create-relation-with-matched-zones": "Erstellt und pflegt automatisch Beziehungen zwischen der Entität und den Zonen, in denen sie sich aktuell befindet. Beziehungen werden entfernt, wenn die Entität eine Zone verlässt, und erstellt, wenn sie eine neue betritt.", + "relation-type-required": "Beziehungstyp ist erforderlich.", + "relation-level-required": "Beziehungsebene ist erforderlich.", + "relation-level-min": "Der minimale Wert der Beziehungsebene ist 1.", + "relation-level-max": "Der maximale Wert der Beziehungsebene ist {{max}}.", + "geofencing-empty": "Mindestens eine Zonengruppe muss konfiguriert werden.", + "geofencing-entity-not-found": "Geofencing-Zielentität nicht gefunden.", + "max-geofencing-zone": "Maximale Anzahl an Geofencing-Zonen erreicht.", + "zone-group-refresh-interval": "Legt fest, wie oft Zonengruppen, die über verwandte Entitäten konfiguriert sind, aktualisiert werden.", + "zone-group-refresh-interval-required": "Aktualisierungsintervall der Zonengruppen ist erforderlich.", + "zone-group-refresh-interval-min": "Aktualisierungsintervall der Zonengruppe sollte mindestens {{ min }} Sekunden betragen.", + "propagation-path-related-entities": "Definiert einen direkten, einstufigen Pfad zu einer verwandten Entität basierend auf der ausgewählten Richtung und dem Beziehungstyp. Es werden nur Beziehungen zwischen Geräte-, Asset-, Kunden- und Mandantenentitäten unterstützt. Maximale Anzahl der über den Beziehungspfad aufgelösten Entitäten ist {{ max }}.", + "data-propagate": "Legt die Daten fest, die aus den unten konfigurierten Argumenten weitergegeben werden. 'Nur Argumente' verwendet die abgerufenen Daten direkt, während 'Berechnungsergebnis' aus diesen Daten einen neuen Wert berechnet.", + "aggregation-path-related-entities": "Definiert einen einstufigen Aggregationspfad über direkte Beziehungen zu übergeordneten oder untergeordneten Entitäten, basierend auf Richtung und Beziehungstyp. Es werden nur Beziehungen zwischen Geräte-, Asset-, Kunden- und Mandantenentitäten unterstützt. Maximale Anzahl der über den Beziehungspfad aufgelösten Entitäten ist {{ max }}.", + "arguments-aggregation": "Legt die Eingabeargumente fest, die für Filterung und Aggregation verwendet werden.", + "setting-arguments-aggregation": "Daten werden von verwandten Entitäten abgerufen, die im Aggregationspfad konfiguriert sind.", + "metrics": "Legt Metriken fest, die basierend auf konfigurierten Argumenten aggregiert werden.", + "entity-aggregation-metrics": "Legt Metriken fest, die basierend auf konfigurierten Argumenten über die angegebenen Zeitintervalle aggregiert werden.", + "import-invalid-calculated-field-type": "Berechnetes Feld kann nicht importiert werden: Ungültige Struktur des berechneten Feldes.", + "simple-expression-title": "Arithmetischer Ausdruck, der definiert, wie der berechnete Wert ermittelt wird.", + "script-title": "TBEL-Skript, das die Berechnungslogik und Ausgabewerte definiert.", + "simple-arguments": "Arithmetischer Ausdruck, der definiert, wie der berechnete Wert ermittelt wird.", + "script-arguments": "Legt die Eingabeargumente fest, die dem Skript zur Verfügung stehen." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Alarmregeln", + "alarm-rule": "Alarmregel", + "alarm-rules": "Alarmregeln", + "alarm-rules-old": "Alt", + "alarm-rules-actual": "Aktuell", + "severities": "Schweregrade", + "cleared": "Löschbedingung", + "delete-title": "Möchten Sie die Alarmregel '{{title}}' wirklich löschen?", + "delete-text": "Achtung, nach der Bestätigung können die Alarmregel und alle zugehörigen Daten nicht wiederhergestellt werden.", + "delete-multiple-title": "Möchten Sie { count, plural, =1 {1 Alarmregel} other {# Alarmregeln} } wirklich löschen?", + "delete-multiple-text": "Achtung, nach der Bestätigung werden alle ausgewählten Alarmregeln entfernt und alle zugehörigen Daten können nicht wiederhergestellt werden.", + "create": "Neue Alarmregel erstellen", + "add": "Alarmregel hinzufügen", + "copy": "Alarmregelkonfiguration kopieren", + "details": "Details zur Alarmregel", + "no-found": "Keine Alarmregeln gefunden", + "list": "{ count, plural, =1 {Eine Alarmregel} other {Liste von # Alarmregeln} }", + "selected-fields": "{ count, plural, =1 {1 Alarmregel} other {# Alarmregeln} } ausgewählt", + "import": "Alarmregel importieren", + "file": "Alarmregeldatei", + "export": "Alarmregel exportieren", + "export-failed-error": "Alarmregel kann nicht exportiert werden: {{error}}", + "entity-type": "Entitätstyp", + "entity-type-required": "Entitätstyp ist erforderlich.", + "alarm-type": "Alarmtyp", + "alarm-type-hint": "Eindeutiger Bezeichner (z. B. HighTempAlarm) innerhalb des Gültigkeitsbereichs des Alarmerstellers (Gerät, Asset usw.), um Konflikte zu vermeiden.", + "alarm-type-required": "Alarmtyp ist erforderlich.", + "alarm-type-pattern": "Alarmtyp ist ungültig.", + "alarm-type-max-length": "Alarmtyp sollte kürzer als 256 Zeichen sein.", + "clear-alarm": "Alarm löschen", + "value-argument": "Argument", + "value-argument-required": "Argument ist erforderlich.", + "static-settings": "Statische Einstellungen", + "configuration": "Konfiguration", + "static-schedule": "Statisch", + "dynamic-schedule": "Dynamisch", + "operation-and": "UND", + "operation-or": "ODER", + "condition-during": "Während {{during}}", + "condition-during-dynamic": "Während \"{{ attribute }}\"", + "condition-repeat-times": "Wiederholt sich { count, plural, =1 {1 Mal} other {# Mal} }", + "condition-repeat-times-dynamic": "Wiederholt sich \"{{ attribute }}\"-mal", + "filter-preview": "Filtervorschau", + "condition-settings": "Bedingungseinstellungen", + "static": "Statisch", + "dynamic": "Dynamisch", + "argument-filters": "Argumentfilter", + "argument-name": "Argumentname", + "value-type": "Werttyp", + "general": "Allgemein", + "filters": "Filter", + "date-time-hint": "Das Argument muss in Epoch-Millisekunden angegeben werden. Beispiel: 1698839340000 entspricht 2023-11-01 12:49:00 UTC.", + "operation": "Operation", + "value-source": "Wertquelle", + "value": "Wert", + "ignore-case": "Groß-/Kleinschreibung ignorieren", + "condition": "Bedingung", + "script": "Skript", + "add-filter": "Argumentfilter hinzufügen", + "edit-filter": "Argumentfilter", + "remove-filter": "Argumentfilter entfernen", + "no-filter": "Mindestens ein Filter ist erforderlich.", + "conditions": { + "simple": "Einfach", + "duration": "Dauer", + "repeating": "Wiederholend" + }, + "schedule-title": "Zeitplan", + "edit-schedule": "Alarmzeitplan bearbeiten", + "schedule-type": "Zeitplanertyp", + "schedule-type-required": "Zeitplanertyp ist erforderlich.", + "schedule": { + "any-time": "Immer aktiv", + "specific-time": "Zu einer bestimmten Zeit aktiv", + "custom": "Benutzerdefiniert" + }, + "schedule-day": { + "monday": "Montag", + "tuesday": "Dienstag", + "wednesday": "Mittwoch", + "thursday": "Donnerstag", + "friday": "Freitag", + "saturday": "Samstag", + "sunday": "Sonntag" + }, + "schedule-days": "Tage", + "schedule-time": "Zeit", + "schedule-time-from": "Von", + "schedule-time-to": "Bis", + "schedule-days-of-week-required": "Mindestens ein Wochentag muss ausgewählt werden.", + "tbel": "TBEL", + "expression-type": { + "simple": "Einfach", + "script": "Skript" + }, + "operation-type": { + "and": "Und", + "or": "Oder" + }, + "filter-predicate-type": { + "string": "String", + "numeric": "Numerisch", + "boolean": "Boolesch", + "complex": "Komplex" + }, + "alarm-rule-additional-info": "Zusätzliche Informationen", + "edit-alarm-rule-additional-info": "Zusätzliche Informationen bearbeiten", + "alarm-rule-additional-info-placeholder": "Bitte geben Sie hier Ihre Kommentare und Anpassungen ein, um sie in den Alarmdetails unter „Zusätzliche Informationen“ anzuzeigen", + "alarm-rule-additional-info-hint": "Hinweis: Verwenden Sie ${Argumentname}, um Werte der Argumente zu ersetzen, die in der Alarmregelbedingung verwendet werden.", + "alarm-rule-additional-info-icon-hint": "Verwenden Sie den Argumentnamen, um Werte der Argumente zu ersetzen, die in der Alarmregelbedingung verwendet werden.", + "alarm-rule-mobile-dashboard": "Mobiles Dashboard", + "alarm-rule-mobile-dashboard-hint": "Wird von der mobilen Anwendung als Dashboard für Alarmdetails verwendet.", + "alarm-rule-no-mobile-dashboard": "Kein Dashboard ausgewählt", + "alarm-rule-condition": "Alarmregelbedingung", + "enter-alarm-rule-condition-prompt": "Bedingung hinzufügen", + "enter-alarm-rule-clear-condition-prompt": "Löschbedingung hinzufügen", + "edit-alarm-rule-condition": "Alarmbedingung", + "condition-type": "Bedingungstyp", + "condition-type-hint": "Die Optionen „Dauer“ und „Wiederholend“ sind nicht verfügbar, wenn im Filter die Operation „Missing for“ verwendet wird.", + "select-alarm-severity": "Alarmschweregrad auswählen", + "add-create-alarm-rule-prompt": "Mindestens eine Auslösebedingung ist erforderlich.", + "add-create-alarm-rule": "Auslösebedingung hinzufügen", + "add-clear-alarm-rule": "Löschbedingung hinzufügen", + "condition-duration": "Bedingungsdauer", + "condition-duration-value": "Dauerwert", + "condition-duration-time-unit": "Zeiteinheit", + "condition-duration-value-range": "Der Dauerwert muss im Bereich von 1 bis 2147483647 liegen.", + "condition-duration-value-pattern": "Der Dauerwert muss eine ganze Zahl sein.", + "condition-duration-value-required": "Dauerwert ist erforderlich.", + "condition-duration-time-unit-required": "Zeiteinheit ist erforderlich.", + "condition-repeating-value": "Anzahl der Ereignisse", + "condition-repeating-value-hint": "Die Aktualisierung eines beliebigen Alarmregelarguments wird als Ereignis gezählt", + "condition-repeating-value-range": "Die Anzahl der Ereignisse muss im Bereich von 1 bis 2147483647 liegen.", + "condition-repeating-value-pattern": "Die Anzahl der Ereignisse muss eine ganze Zahl sein.", + "condition-repeating-value-required": "Anzahl der Ereignisse ist erforderlich.", + "create-conditions": "Auslösebedingungen", + "clear-condition": "Löschbedingung", + "no-clear-alarm-rule": "Keine Löschbedingung konfiguriert.", + "advanced-settings": "Erweiterte Einstellungen", + "propagate-alarm": "Alarm an verwandte Entitäten weitergeben", + "alarm-rule-relation-types-list": "Beziehungstypen", + "alarm-rule-relation-types-list-hint": "Legt Beziehungstypen fest, um die verwandten Entitäten zu filtern. Wenn nicht festgelegt, wird der Alarm an alle verwandten Entitäten weitergegeben.", + "propagate-alarm-to-owner": "Alarm an den Entitätseigentümer weitergeben (Kunde oder Mandant)", + "propagate-alarm-to-tenant": "Alarm an den Mandanten weitergeben", + "alarm-rule-filter-title": "Alarmregel-Filter", + "filter-title": "Filter", + "debugging": "Alarmregel-Debugging", + "any-type": "Beliebiger Typ", + "enter-alarm-rule-type": "Alarmtyp eingeben", + "no-alarm-rule-types-matching": "Keine Alarmtypen gefunden, die zu '{{entitySubtype}}' passen.", + "alarm-rule-type-list-empty": "Keine Alarmtypen ausgewählt.", + "alarm-rule-type-list": "Liste der Alarmtypen", + "alarm-rule-entity-list": "Entitätsliste", + "missing-for": "fehlt seit", + "time-unit": "Einheit", + "mode": "Modus", + "type": "Typ", + "value-required": "Wert ist erforderlich.", + "min-value": "Wert muss 1 oder größer sein.", + "argument-in-use": "Argument wird als allgemeines Argument verwendet.", + "import-invalid-alarm-rule-type": "Alarmregel kann nicht importiert werden: Ungültige Alarmregelstruktur.", + "no-filter-preview": "Kein Filter angegeben", + "filter-operation": { + "and": "Und", + "or": "Oder" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Dokumentation", "time-left": "{{time}} verbleibend", "output": "Ausgabe", + "sort-asc": "Aufsteigend", + "sort-desc": "Absteigend", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Reihenfolge des Dashboards in mobiler App", "mobile-hide": "Dashboard in mobiler App ausblenden", "update-image": "Dashboard-Bild aktualisieren", + "update-new-version": "Neue Version hochladen", + "upload-file-to-update": "Datei zum Aktualisieren hochladen", "take-screenshot": "Screenshot erstellen", "select-widget-title": "Widget auswählen", "select-widget-value": "{{title}}: Widget auswählen", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrap-Client", "bootstrap-server": "Bootstrap-Server", "lwm2m-server": "LwM2M-Server", + "client-reboot": "Registrierungsupdate-Trigger", + "bootstrap-reboot": "Bootstrap-Anforderungs-Trigger", "client-publicKey-or-id": "Öffentlicher Client-Schlüssel oder ID", "client-publicKey-or-id-required": "Öffentlicher Client-Schlüssel oder ID ist erforderlich.", "client-publicKey-or-id-tooltip-psk": "Die PSK-Kennung ist eine beliebige PSK-Kennung bis zu 128 Byte, wie im Standard [RFC7925] beschrieben.\nDie PSK-Kennung MUSS zuerst in eine Zeichenkette umgewandelt und dann mittels UTF-8 in Oktetten codiert werden.", @@ -2225,6 +2707,7 @@ "short-id-required": "Kurze Server-ID ist erforderlich.", "short-id-range": "Kurze Server-ID sollte im Bereich von {{ min }} bis {{ max }} liegen.", "short-id-pattern": "Kurze Server-ID muss eine positive Ganzzahl sein.", + "short-id-pattern-bs": "Kurze Server-ID darf nur null sein", "lifetime": "Lebensdauer der Client-Registrierung", "lifetime-required": "Registrierungslebensdauer des Clients ist erforderlich.", "lifetime-pattern": "Registrierungslebensdauer muss eine positive Ganzzahl sein.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Alle Ressourcen werden mit einer einzigen zusammengefassten Beobachtungsanfrage überwacht (effizienter, weniger flexibel)", "composite-by-object": "Nach Objekten zusammengefasst", "composite-by-object-description": "Ressourcen werden nach Objekttyp gruppiert und mit separaten zusammengefassten Beobachtungsanfragen überwacht (ausgewogener Ansatz)" - } + }, + "init-attr-tel-as-obs-strategy": "Attribute und Telemetrie mithilfe der Observe-Strategie initialisieren", + "init-attr-tel-as-obs-strategy-hint": "Wenn false – werden Attribute und Telemetrie initialisiert, indem ihre Werte einzeln gelesen werden.\\nWenn true – werden Attribute und Telemetrie initialisiert, indem ihre Werte über die Observe-Strategie abonniert werden." }, "snmp": { "add-communication-config": "Kommunikationskonfiguration hinzufügen", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Regelknoten", "list-of-rulenodes": "{ count, plural, =1 {Ein Regelknoten} other {Liste von # Regelknoten} }", "rulenode-name-starts-with": "Regelknoten, deren Name mit '{{prefix}}' beginnt", + "type-api-key": "API-Schlüssel", + "type-api-keys": "API-Schlüssel", "type-current-customer": "Aktueller Kunde", "type-current-tenant": "Aktueller Tenant", "type-current-user": "Aktueller Benutzer", @@ -2656,6 +3143,7 @@ "details": "Entitätsdetails", "no-entities-prompt": "Keine Entitäten gefunden", "no-data": "Keine Daten zur Anzeige vorhanden", + "show-all-columns": "Alle anzeigen", "columns-to-display": "Anzuzeigende Spalten", "type-api-usage-state": "API-Nutzungsstatus", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Eine mobile Anwendung} other {Liste von # mobilen Anwendungen} }", "type-mobile-app-bundle": "Mobile-Bundle", "type-mobile-app-bundles": "Mobile-Bundles", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Ein Mobile-Bundle} other {Liste von # Mobile-Bundles} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Ein Mobile-Bundle} other {Liste von # Mobile-Bundles} }", + "limit-reached": "Limit erreicht", + "limit-reached-text": "Sie haben das Limit von {{ entities }} erreicht. Um weitere hinzuzufügen, bitten Sie bitte Ihren Systemadministrator, Ihr {{ entity }}-Limit zu erhöhen.", + "request-limit-increase": "Limiterhöhung anfordern", + "request-sysadmin-text": "Sind Sie der Systemadministrator?", + "login-here": "Hier anmelden", + "to-increase-limit": "um das Limit zu erhöhen.", + "increase-limit-request-sent-title": "Wir haben eine automatische Anfrage an Ihren Systemadministrator gesendet, um das Limit zu erhöhen", + "increase-limit-request-sent-text": "Bitte geben Sie ihm etwas Zeit, die Anfrage zu prüfen und die Einstellungen zu aktualisieren. Möglicherweise müssen Sie diese Seite aktualisieren, um die Änderungen zu sehen." }, "entity-field": { "created-time": "Erstellungszeit", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Link zum Zurücksetzen wurde gesendet", "email": "E-Mail", "invalid-email-format": "Ungültiges E-Mail-Format.", + "sign-in-with": "Anmelden mit {{name}}", + "sign-in-to-your-account": "Bei Ihrem Konto anmelden", "or": "oder", "error": "Anmeldefehler", "verify-your-identity": "Identität bestätigen", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Aktivierungslink ist abgelaufen", "activation-link-expired-message": "Der Link zur Aktivierung Ihres Profils ist abgelaufen. Sie können zur Anmeldeseite zurückkehren, um eine neue E-Mail zu erhalten.", "reset-password-link-expired": "Link zum Zurücksetzen des Passworts ist abgelaufen", - "reset-password-link-expired-message": "Der Link zum Zurücksetzen Ihres Passworts ist abgelaufen. Sie können zur Anmeldeseite zurückkehren, um eine neue E-Mail zu erhalten." + "reset-password-link-expired-message": "Der Link zum Zurücksetzen Ihres Passworts ist abgelaufen. Sie können zur Anmeldeseite zurückkehren, um eine neue E-Mail zu erhalten.", + "two-fa": "Zwei-Faktor-Authentifizierung", + "two-fa-required": "Zwei-Faktor-Authentifizierung ist erforderlich", + "set-up-verification-method": "Richten Sie eine Verifizierungsmethode ein, um fortzufahren", + "set-up-verification-method-login": "Richten Sie eine Verifizierungsmethode ein oder melden Sie sich an", + "enable-authenticator-app": "Authenticator-App aktivieren", + "enable-authenticator-app-description": "Bitte geben Sie den Sicherheitscode aus Ihrer Authenticator-App ein", + "enable-authenticator-sms": "SMS-Authenticator aktivieren", + "enable-authenticator-sms-description": "Geben Sie den 6-stelligen Code ein, den wir gerade gesendet haben an ", + "enable-authenticator-email": "E-Mail-Authenticator aktivieren", + "enable-authenticator-email-description": "Ein Sicherheitscode wurde an Ihre E-Mail-Adresse gesendet an ", + "enter-key-manually": "oder geben Sie diesen 32-stelligen Schlüssel manuell ein:", + "continue": "Weiter", + "confirm": "Bestätigen", + "authenticator-app-success": "Authenticator-App erfolgreich aktiviert", + "authenticator-app-success-description": "Beim nächsten Anmelden müssen Sie einen Code zur Zwei-Faktor-Authentifizierung angeben", + "authenticator-sms-success": "SMS-Authenticator erfolgreich aktiviert", + "authenticator-sms-success-description": "Beim nächsten Anmelden werden Sie aufgefordert, den Sicherheitscode einzugeben, der an die Telefonnummer gesendet wird", + "authenticator-email-success": "E-Mail-Authenticator erfolgreich aktiviert", + "authenticator-email-success-description": "Beim nächsten Anmelden werden Sie aufgefordert, den Sicherheitscode einzugeben, der an Ihre E-Mail-Adresse gesendet wird", + "authenticator-backup-code-success": "Backup-Code erfolgreich aktiviert", + "authenticator-backup-code-success-description": "Beim nächsten Anmelden werden Sie aufgefordert, den Sicherheitscode einzugeben oder einen der Backup-Codes zu verwenden.", + "add-verification-method": "Verifizierungsmethode hinzufügen", + "get-backup-code": "Backup-Code abrufen", + "copy-key": "Schlüssel kopieren", + "send-code": "Code senden", + "email-label": "E-Mail", + "email-description": "Geben Sie eine E-Mail-Adresse ein, die Sie als Authenticator verwenden möchten.", + "sms-description": "Geben Sie eine Telefonnummer ein, die Sie als Authenticator verwenden möchten.", + "backup-code-description": "Drucken Sie die Codes aus, damit Sie sie griffbereit haben, wenn Sie sie zum Anmelden bei Ihrem Konto benötigen. Sie können jeden Backup-Code einmal verwenden.", + "backup-code-warn": "Sobald Sie diese Seite verlassen, können diese Codes nicht erneut angezeigt werden. Bewahren Sie sie sicher auf, indem Sie die folgenden Optionen verwenden.", + "download-txt": "Herunterladen (txt)", + "print": "Drucken", + "verification-code": "6-stelliger Code", + "verification-code-invalid": "Ungültiges Format des Verifizierungscodes", + "verification-code-incorrect": "Verifizierungscode ist falsch", + "verification-code-many-request": "Zu viele Anfragen zur Prüfung des Verifizierungscodes", + "scan-qr-code": "Scannen Sie diesen QR-Code mit Ihrer Verifizierungs-App", + "phone-input": { + "phone-input-label": "Telefonnummer", + "phone-input-required": "Telefonnummer ist erforderlich", + "phone-input-validation": "Telefonnummer ist ungültig oder nicht möglich", + "phone-input-pattern": "Ungültige Telefonnummer. Muss im E.164-Format sein, z. B. {{phoneNumber}}", + "phone-input-hint": "Telefonnummer im E.164-Format, z. B. {{phoneNumber}}" + } }, "mobile": { "add-application": "Anwendung hinzufügen", @@ -4175,6 +4717,7 @@ "api-usage-limit": "API-Nutzungslimit", "device-activity": "Geräteaktivität", "entities-limit": "Entitätenlimit", + "entities-limit-increase-request": "Anfrage zur Erhöhung des Entitätenlimits", "entity-action": "Entitätsaktion", "general": "Allgemein", "rule-engine-lifecycle-event": "Lebenszyklusereignis der Rule Engine", @@ -4392,6 +4935,12 @@ "at-least": "Mindestens:", "character": "{ count, plural, =1 {1 Zeichen} other {# Zeichen} }", "digit": "{ count, plural, =1 {1 Ziffer} other {# Ziffern} }", + "password-tooltip-min-length": "Mindestens {{minimumLength}} Zeichen lang", + "password-tooltip-max-length": "Höchstens {{maximumLength}} Zeichen lang", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} Großbuchstabe", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} Kleinbuchstabe", + "password-tooltip-digit": "{{minimumDigits}} Zahl", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} Sonderzeichen", "incorrect-password-try-again": "Falsches Passwort. Bitte versuchen Sie es erneut", "lowercase-letter": "{ count, plural, =1 {1 Kleinbuchstabe} other {# Kleinbuchstaben} }", "new-passwords-not-match": "Die neuen Passwörter stimmen nicht überein", @@ -4450,7 +4999,8 @@ "additional-info": "Zusätzliche Informationen (JSON)", "invalid-additional-info": "Zusätzliche Info-JSON kann nicht geparst werden.", "no-relations-text": "Keine Beziehungen gefunden", - "not": "Nicht" + "not": "Nicht", + "copy-type": "Typ kopieren" }, "resource": { "add": "Ressource hinzufügen", @@ -5400,7 +5950,8 @@ }, "time-series": "Zeitreihe", "latest": "Neueste Werte", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Berechnete Felder und Alarmregeln" }, "save-attribute": { "processing-settings": "Verarbeitungseinstellungen", @@ -5595,7 +6146,8 @@ "bad-request-params": "Ungültige Anfrageparameter", "item-not-found": "Element nicht gefunden", "too-many-requests": "Zu viele Anfragen", - "too-many-updates": "Zu viele Aktualisierungen" + "too-many-updates": "Zu viele Aktualisierungen", + "entities-limit-exceeded": "Entitätenlimit überschritten" }, "tenant": { "tenant": "Mieter", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Maximale Argumente pro berechnetem Feld", "max-arguments-per-cf-range": "Maximale Argumente pro berechnetem Feld dürfen nicht negativ sein", "max-arguments-per-cf-required": "Maximale Argumente pro berechnetem Feld sind erforderlich", + "max-related-level-per-argument": "Maximale Beziehungsebene pro Argument „Verwandte Entitäten“", + "max-related-level-per-argument-range": "Die maximale Beziehungsebene pro Argument „Verwandte Entitäten“ darf nicht kleiner als „1“ sein", + "max-related-level-per-argument-required": "Die maximale Beziehungsebene pro Argument „Verwandte Entitäten“ ist erforderlich", + "min-allowed-scheduled-update-interval": "Minimal zulässiges Aktualisierungsintervall für Argumente „Verwandte Entitäten“ (Sekunden)", + "min-allowed-scheduled-update-interval-range": "Der Mindestwert für das minimal zulässige Aktualisierungsintervall darf nicht negativ sein", + "min-allowed-deduplication-interval": "Minimal zulässiges Deduplizierungsintervall (Sekunden)", + "min-allowed-deduplication-interval-range": "Der Wert des minimal zulässigen Deduplizierungsintervalls darf nicht negativ sein", + "min-allowed-deduplication-interval-required": "Minimal zulässiges Deduplizierungsintervall ist erforderlich", + "intermediate-aggregation-interval": "Intermediäres Aggregationsintervall (Sekunden)", + "intermediate-aggregation-interval-range": "Der Wert des intermediären Aggregationsintervalls darf nicht kleiner als „1“ sein", + "intermediate-aggregation-interval-required": "Intermediäres Aggregationsintervall ist erforderlich", + "reevaluation-check-interval": "Intervall für Neubewertungsprüfung (Sekunden)", + "reevaluation-check-interval-range": "Der Wert des Intervalls für die Neubewertungsprüfung darf nicht kleiner als „1“ sein", + "reevaluation-check-interval-required": "Intervall für Neubewertungsprüfung ist erforderlich", + "alarms-reevaluation-interval": "Intervall für Alarmneubewertung (Sekunden)", + "alarms-reevaluation-interval-range": "Der Wert des Intervalls für Alarmneubewertung darf nicht kleiner als „1“ sein", + "alarms-reevaluation-interval-required": "Intervall für Alarmneubewertung ist erforderlich", + "min-allowed-aggregation-interval": "Minimal zulässiges Aggregationsintervall (Sekunden)", + "min-allowed-aggregation-interval-range": "Der Wert des minimal zulässigen Aggregationsintervalls darf nicht negativ sein", + "min-allowed-aggregation-interval-required": "Minimal zulässiges Aggregationsintervall ist erforderlich", + "min-allowed-scheduled-update-interval-required": "Der Mindestwert für das minimal zulässige Aktualisierungsintervall ist erforderlich", "max-state-size": "Maximale Zustandsgröße in KB", "max-state-size-range": "Maximale Zustandsgröße darf nicht negativ sein", "max-state-size-required": "Maximale Zustandsgröße ist erforderlich", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Maximale Abonnements pro normalem Benutzer", "ws-limit-max-subscriptions-per-public-user": "Maximale Abonnements pro öffentlichem Benutzer", "ws-limit-updates-per-session": "WebSocket-Aktualisierungen pro Sitzung", + "relation-search-entity-limit": "Entitätslimit für Beziehungssuche", + "relation-search-entity-limit-hint": "Begrenzt die Anzahl der Entitäten, die auf der letzten Ebene des Beziehungspfads aufgelöst werden. Gilt für Argumente „Verwandte Entitäten“ und Weitergabefelder.", + "relation-search-entity-limit-required": "Entitätslimit für Beziehungssuche ist erforderlich", + "relation-search-entity-limit-range": "Entitätslimit für Beziehungssuche darf nicht kleiner als „1“ sein", "rate-limits": { "add-limit": "Limit hinzufügen", "and-also-less-than": "und außerdem kleiner als", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Standard-Gruppierungsintervall", "edit-intervals-list-hint": "Liste verfügbarer Intervalloptionen kann angegeben werden.", "edit-grouping-intervals-list-hint": "Gruppierungsintervallliste und Standardintervall konfigurierbar.", - "all": "Alle" + "all": "Alle", + "save-current-settings-as-default": "Aktuelle Einstellungen als Standard-Zeitfenster speichern", + "hide-option-from-end-users": "Option vor Endbenutzern ausblenden" }, "tooltip": { "trigger": "Auslöser", @@ -6648,6 +7227,7 @@ "export-attributes": "Attribute exportieren", "export-credentials": "Zugangsdaten exportieren", "export-calculated-fields": "Berechnete Felder \nund Alarmregeln exportieren", + "export-alarm-rules": "Alarmregeln exportieren", "entity-versions": "Entitätsversionen", "versions": "Versionen", "created-time": "Erstellungszeit", @@ -6665,6 +7245,7 @@ "load-attributes": "Attribute laden", "load-credentials": "Zugangsdaten laden", "load-calculated-fields": "Berechnete Felder laden", + "load-alarm-rules": "Alarmregeln laden", "compare-with-current": "Mit aktueller vergleichen", "diff-entity-with-version": "Unterschiede mit Entitätsversion '{{versionName}}'", "previous-difference": "Vorheriger Unterschied", @@ -6874,7 +7455,23 @@ "scan-qr-code": "QR-Code scannen", "make-phone-call": "Anruf tätigen", "get-location": "Standort des Telefons abrufen", - "take-screenshot": "Screenshot erstellen" + "take-screenshot": "Screenshot erstellen", + "handle-provision-success-function": "Funktion zum Verarbeiten eines erfolgreichen Provisionings", + "get-location-function": "Funktion zum Abrufen des Standorts", + "process-launch-result-function": "Funktion zum Verarbeiten des Start-Ergebnisses", + "get-phone-number-function": "Funktion zum Abrufen der Telefonnummer", + "process-image-function": "Funktion zum Verarbeiten des Bildes", + "process-qr-code-function": "Funktion zum Verarbeiten des QR-Codes", + "process-location-function": "Funktion zum Verarbeiten des Standorts", + "handle-empty-result-function": "Funktion zum Verarbeiten eines leeren Ergebnisses", + "handle-error-function": "Funktion zum Verarbeiten eines Fehlers", + "handle-non-mobile-fallback-function": "Fallback-Funktion für Nicht-Mobile", + "save-to-gallery": "In 'Bildergalerie' speichern", + "provision-type": "Provisioning-Typ", + "auto": "Auto", + "wi-fi": "WLAN", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Benutzerdefinierte Aktionsfunktion", "custom-pretty-function": "Benutzerdefinierte Aktion (mit HTML-Vorlage) Funktion", @@ -6883,7 +7480,8 @@ "marker": "Markierung", "polygon": "Polygon", "rectangle": "Rechteck", - "circle": "Kreis" + "circle": "Kreis", + "polyline": "Polylinie" }, "place-map-item": "Kartenelement platzieren", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Polygon weiterzeichnen", "finish-draw-polygon": "Polygonzeichnen beenden", "start-draw-circle": "Kreis zeichnen starten", - "finish-draw-circle": "Kreiszeichnen beenden" + "finish-draw-circle": "Kreiszeichnen beenden", + "start-draw-polyline": "Polylinie zeichnen starten", + "finish-draw-polyline": "Polylinie zeichnen abschließen" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Verzögerung der Aktualisierungsanimation" }, "chart-axis": { + "limit": "Limit", + "source": "Quelle", + "key-value": "Schlüssel / Wert", + "value-required": "Wert ist erforderlich.", + "entity-key-required": "Entitätsschlüssel ist erforderlich.", + "key-required": "Schlüssel ist erforderlich.", + "scale-limits": "Skalengrenzen", + "scale-appearance": "Skalendesign", "scale": "Skala", "scale-min": "min", "scale-max": "max", @@ -8005,7 +8613,10 @@ "add-radio-option": "Optionsfeldoption hinzufügen", "radio-label-position": "Labelposition", "radio-label-position-before": "Vorher", - "radio-label-position-after": "Nachher" + "radio-label-position-after": "Nachher", + "save-image": "Bild speichern", + "save-to-gallery": "Aufgenommene Bilder automatisch in der Bildergalerie speichern", + "public-image": "Macht das Bild für jeden nicht autorisierten Benutzer verfügbar" }, "invalid-qr-code-text": "Ungültiger Eingabetext für QR-Code. Eingabe sollte vom Typ String sein", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Reisen", "markers": "Marker", "polygons": "Polygone", - "circles": "Kreise" + "circles": "Kreise", + "polylines": "Polylinien" }, "data-layer": { "source": "Quelle", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Kreis für '{{entityName}}': Klicken, um Kreis zu beenden und zu speichern", "finish-circle-hint": "Kreis: Klicken, um Zeichnung zu beenden" }, + "polyline": { + "polyline-key": "Polylinienschlüssel", + "polyline-key-required": "Polylinienschlüssel ist erforderlich", + "no-polylines": "Keine Polylinien konfiguriert", + "add-polylines": "Polylinie hinzufügen", + "polyline-configuration": "Polylinienkonfiguration", + "remove-polyline": "Polylinie entfernen", + "edit": "Polylinie bearbeiten", + "cut": "Polylinienbereich ausschneiden", + "rotate": "Polylinie drehen", + "remove-polyline-for": "Polylinie für '{{entityName}}' entfernen", + "draw-polyline": "Polylinie zeichnen", + "polyline-place-first-point-hint-with-entity": "Polylinie für '{{entityName}}': Klicken, um den ersten Punkt zu setzen", + "polyline-place-first-point-hint": "Polylinie: Klicken, um den ersten Punkt zu setzen", + "finish-polyline-hint-with-entity": "Polylinie für '{{entityName}}': Klicken, um das Zeichnen abzuschließen", + "finish-polyline-hint": "Polylinie: Klicken, um das Zeichnen abzuschließen", + "polyline-place-first-point-cut-hint": "Klicken, um den ersten Punkt zu setzen", + "finish-polyline-cut-hint": "Klicken Sie auf den ersten Marker, um abzuschließen und zu speichern" + }, "select-entity": "Entität auswählen", "select-entity-hint": "Hinweis: Nach der Auswahl auf die Karte klicken, um die Position festzulegen" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Leeren Platz anstelle der versteckten Zellaktion anzeigen", "dont-reserve-space-hidden-action": "Keinen Platz für versteckte Aktionsschaltflächen reservieren", "display-timestamp": "Zeitstempel anzeigen", + "timestamp-column-name": "Zeitstempel", "display-pagination": "Seitennummerierung anzeigen", "default-page-size": "Standard-Seitengröße", "page-step-settings": "Seitenschritteinstellungen", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Mindestens eine Alarmspalte muss angegeben werden", "table-tabs": "Tabellen-Tabs", "show-cell-actions-menu-mobile": "Zellenaktions-Dropdownmenü im mobilen Modus anzeigen", - "disable-sorting": "Sortierung deaktivieren" + "disable-sorting": "Sortierung deaktivieren", + "sort-by": "Tabs sortieren nach", + "sort-timestamp-option": "Erstellungszeit" }, "latest-chart": { "total": "Gesamt", @@ -9491,11 +10125,28 @@ "content": "

    Durch die Erstellung von Dashboards für Endbenutzer kann ein Kundenbenutzer nur seine eigenen Geräte sehen, während Daten anderer Kunden verborgen bleiben.

    Folgen Sie der Dokumentation, um zu erfahren, wie es geht:

    " } } + }, + "api-usage": { + "api-usage": "API-Nutzung", + "label": "Label", + "state-name": "Statusname", + "status": "Status", + "status-required": "Status ist erforderlich.", + "limit": "Maximales Limit", + "limit-required": "Maximales Limit ist erforderlich.", + "current-number": "Aktuelle Anzahl", + "current-number-required": "Aktuelle Anzahl ist erforderlich.", + "add-key": "Schlüssel hinzufügen", + "no-key": "Kein Schlüssel", + "delete-key": "Schlüssel löschen", + "target-dashboard-state": "Ziel-Dashboardstatus", + "go-to-main-state": "Zur Standardansicht wechseln" } }, "icon": { "icon": "Symbol", "icons": "Symbole", + "custom": "Benutzerdefiniert", "select-icon": "Symbol auswählen", "material-icons": "Materialsymbole", "show-all": "Alle Symbole anzeigen", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "von" }, "language": { + "auto": "Auto", "language": "Sprache" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index a12db1a589..ebd4d4df2d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -78,6 +78,7 @@ "show-more": "Εμφάνιση περισσότερων", "dont-show-again": "Να μην εμφανιστεί ξανά", "see-documentation": "Δείτε την τεκμηρίωση", + "see-debug-events": "Προβολή συμβάντων αποσφαλμάτωσης", "clear": "Καθαρισμός", "upload": "Μεταφόρτωση", "delete-anyway": "Διαγραφή ούτως ή άλλως", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Έλεγχος ταυτότητας δύο παραγόντων", "available-providers": "Διαθέσιμοι πάροχοι", + "available-providers-required": "Πρέπει να έχει ρυθμιστεί τουλάχιστον ένας πάροχος 2FA.", "issuer-name": "Όνομα εκδότη", "issuer-name-required": "Το όνομα εκδότη είναι υποχρεωτικό.", "max-verification-failures-before-user-lockout": "Μέγιστος αριθμός αποτυχημένων ελέγχων πριν από κλείδωμα χρήστη", @@ -513,7 +515,9 @@ "verification-message-template-required": "Το πρότυπο μηνύματος επαλήθευσης είναι υποχρεωτικό.", "within-time": "Εντός χρόνου (δευτ.)", "within-time-pattern": "Ο χρόνος πρέπει να είναι θετικός ακέραιος.", - "within-time-required": "Ο χρόνος είναι υποχρεωτικός." + "within-time-required": "Ο χρόνος είναι υποχρεωτικός.", + "force-2fa": "Επιβολή ελέγχου ταυτότητας δύο παραγόντων", + "enforce-for": "Επιβολή για" }, "jwt": { "security-settings": "Ρυθμίσεις ασφαλείας JWT", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Συναγερμός", + "alarm-list": "Λίστα συναγερμών", "alarms": "Συναγερμοί", "all-alarms": "Όλοι οι συναγερμοί", "select-alarm": "Επιλογή συναγερμού", @@ -649,7 +654,16 @@ "alarm-type": "Τύπος συναγερμού", "enter-alarm-type": "Εισαγωγή τύπου συναγερμού", "no-alarm-types-matching": "Δεν βρέθηκαν τύποι συναγερμού που να ταιριάζουν με '{{entitySubtype}}'.", - "alarm-type-list-empty": "Δεν έχουν επιλεγεί τύποι συναγερμού." + "alarm-type-list-empty": "Δεν έχουν επιλεγεί τύποι συναγερμού.", + "system-comments": { + "acked-by-user": "Ο συναγερμός επιβεβαιώθηκε από τον χρήστη {{userName}}", + "cleared-by-user": "Ο συναγερμός εκκαθαρίστηκε από τον χρήστη {{userName}}", + "assigned-to-user": "Ο συναγερμός ανατέθηκε από τον χρήστη {{userName}} στον χρήστη {{assigneeName}}", + "unassigned-to-user": "Η ανάθεση του συναγερμού καταργήθηκε από τον χρήστη {{userName}}", + "unassigned-from-deleted-user": "Η ανάθεση του συναγερμού καταργήθηκε επειδή ο χρήστης {{userName}} - διαγράφηκε", + "comment-deleted": "Ο χρήστης {{userName}} διέγραψε το σχόλιό του", + "severity-changed": "Η σοβαρότητα του συναγερμού ενημερώθηκε από {{oldSeverity}} σε {{newSeverity}}" + } }, "alarm-activity": { "add": "Προσθήκη σχολίου...", @@ -754,6 +768,7 @@ "name-max-length": "Το όνομα πρέπει να είναι μικρότερο από 256 χαρακτήρες", "label-max-length": "Η ετικέτα πρέπει να είναι μικρότερη από 256 χαρακτήρες", "description": "Περιγραφή", + "description-required": "Απαιτείται περιγραφή.", "type": "Τύπος", "type-required": "Ο τύπος είναι υποχρεωτικός.", "details": "Λεπτομέρειες", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Μηνιαία δραστηριότητα δημιουργίας συναγερμών", "data-points": "Σημεία δεδομένων", "data-points-storage-days": "Ημέρες αποθήκευσης σημείων δεδομένων", + "data-points-storage-days-hourly-activity": "Ωριαία δραστηριότητα ημερών αποθήκευσης σημείων δεδομένων", + "data-points-storage-days-daily-activity": "Ημερήσια δραστηριότητα ημερών αποθήκευσης σημείων δεδομένων", + "data-points-storage-days-monthly-activity": "Μηνιαία δραστηριότητα ημερών αποθήκευσης σημείων δεδομένων", "device-api": "API Συσκευής", "email": "Email", "email-messages": "Μηνύματα email", @@ -900,6 +918,7 @@ "rule-node": "Κόμβος κανόνα", "sms": "SMS", "sms-messages": "Μηνύματα SMS", + "sms-messages-hourly-activity": "Ωριαία δραστηριότητα μηνυμάτων SMS", "sms-messages-daily-activity": "Ημερήσια δραστηριότητα SMS", "sms-messages-monthly-activity": "Μηνιαία δραστηριότητα SMS", "successful": "Επιτυχίες ${entityName}", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Ωριαία δραστηριότητα διατήρησης τηλεμετρίας", "telemetry-persistence-monthly-activity": "Μηνιαία δραστηριότητα διατήρησης τηλεμετρίας", "transport": "Μεταφορά", + "transport-msg-hourly-activity": "Ωριαία δραστηριότητα μηνυμάτων transport", + "transport-msg-daily-activity": "Ημερήσια δραστηριότητα μηνυμάτων transport", + "transport-msg-monthly-activity": "Μηνιαία δραστηριότητα μηνυμάτων transport", "transport-daily-activity": "Ημερήσια δραστηριότητα μεταφοράς", "transport-data-points": "Σημεία δεδομένων μεταφοράς", + "transport-data-points-hourly-activity": "Ωριαία δραστηριότητα σημείων δεδομένων transport", + "transport-data-points-daily-activity": "Ημερήσια δραστηριότητα σημείων δεδομένων transport", + "transport-data-points-monthly-activity": "Μηνιαία δραστηριότητα σημείων δεδομένων transport", "view-details": "Προβολή λεπτομερειών", "view-statistics": "Προβολή στατιστικών", - "transport-messages": "Μηνύματα transport" + "transport-messages": "Μηνύματα transport", + "transport-messages-hourly-activity": "Ωριαία δραστηριότητα μηνυμάτων transport", + "transport-data-point-hourly-activity": "Ωριαία δραστηριότητα σημείου δεδομένων transport", + "javascript-function-executions": "Εκτελέσεις συναρτήσεων JavaScript", + "javascript-function-executions-hourly-activity": "Ωριαία δραστηριότητα εκτελέσεων συναρτήσεων JavaScript", + "javascript-function-executions-daily-activity": "Ημερήσια δραστηριότητα εκτελέσεων συναρτήσεων JavaScript", + "javascript-function-executions-monthly-activity": "Μηνιαία δραστηριότητα εκτελέσεων συναρτήσεων JavaScript", + "tbel-function-executions": "Εκτελέσεις συναρτήσεων TBEL", + "tbel-function-executions-hourly-activity": "Ωριαία δραστηριότητα εκτελέσεων συναρτήσεων TBEL", + "tbel-function-executions-daily-activity": "Ημερήσια δραστηριότητα εκτελέσεων συναρτήσεων TBEL", + "tbel-function-executions-monthly-activity": "Μηνιαία δραστηριότητα εκτελέσεων συναρτήσεων TBEL", + "created-reports": "Δημιουργημένες αναφορές", + "created-reports-hourly-activity": "Ωριαία δραστηριότητα δημιουργημένων αναφορών", + "created-reports-daily-activity": "Ημερήσια δραστηριότητα δημιουργημένων αναφορών", + "created-reports-monthly-activity": "Μηνιαία δραστηριότητα δημιουργημένων αναφορών", + "emails": "emails", + "emails-hourly-activity": "Ωριαία δραστηριότητα emails", + "emails-daily-activity": "Ημερήσια δραστηριότητα emails", + "emails-monthly-activity": "Μηνιαία δραστηριότητα emails", + "status": { + "enabled": "Ενεργό", + "disabled": "Ανενεργό", + "warning": "Προειδοποίηση" + } }, "api-limit": { "cassandra-write-queries-core": "Ερωτήματα εγγραφής Cassandra από REST API", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Μηνύματα uplink από Edge", "edge-uplink-messages-per-edge": "Μηνύματα uplink από Edge ανά μονάδα Edge" }, + "api-key": { + "api-key": "Κλειδί API", + "api-keys": "Κλειδιά API", + "delete-api-key-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το κλειδί API '{{name}}';", + "delete-api-key-text": "Προσοχή: μετά την επιβεβαίωση, το κλειδί δεν θα μπορεί να ανακτηθεί.", + "delete-api-keys-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, =1 {1 κλειδί API} other {# κλειδιά API} };", + "delete-api-keys-text": "Προσοχή: μετά την επιβεβαίωση, όλα τα επιλεγμένα κλειδιά δεν θα μπορούν να ανακτηθούν.", + "expiration-date": "Ημερομηνία λήξης", + "date": "Ημερομηνία", + "description": "Περιγραφή", + "disable": "Απενεργοποίηση", + "edit-description": "Επεξεργασία περιγραφής", + "enable": "Ενεργοποίηση κλειδιού API ", + "expiration-time": "Χρόνος λήξης", + "expiration-time-never": "Ποτέ", + "expiration-time-custom": "Προσαρμοσμένο", + "generate": "Δημιουργία", + "generate-title": "Δημιουργία κλειδιού API", + "generate-text": "Σημείωση: το κλειδί API κληρονομεί τα δικαιώματα του χρήστη για τον οποίο δημιουργείται.", + "generated-api-key-title": "Το κλειδί API δημιουργήθηκε. Ας ελέγξουμε τη συνδεσιμότητα!", + "generated-api-key-copy": "Βεβαιωθείτε ότι αντιγράψατε και αποθηκεύσατε τώρα το κλειδί API σας, καθώς δεν θα μπορείτε να το δείτε ξανά.", + "generated-api-key-command": "Χρησιμοποιήστε τις ακόλουθες οδηγίες για να ελέγξετε τη συνδεσιμότητα. Ως αποτέλεσμα, θα πρέπει να λάβετε τις τρέχουσες πληροφορίες χρήστη:", + "generated-api-key-insecure-url": "Η εκτέλεση εντολών μέσω μη ασφαλούς σύνδεσης HTTP θα αποστείλει το κλειδί API σας χωρίς κρυπτογράφηση, καθιστώντας το ευάλωτο σε υποκλοπή.", + "list": "{ count, plural, =1 {Ένα κλειδί API} other {Λίστα # κλειδιών API} }", + "manage": "Διαχείριση", + "manage-api-keys": "Διαχείριση κλειδιών API", + "no-found": "Δεν βρέθηκαν κλειδιά API", + "selected-api-keys": "{ count, plural, =1 {1 κλειδί API επιλεγμένο} other {# κλειδιά API επιλεγμένα} }", + "search": "Αναζήτηση κλειδιών API", + "status": "Κατάσταση", + "status-active": "Ενεργό", + "status-inactive": "Ανενεργό", + "status-expired": "Ληγμένο" + }, "audit-log": { "audit": "Έλεγχος", "audit-logs": "Αρχεία καταγραφής ελέγχου", @@ -991,7 +1073,11 @@ "type-provision-failure": "Η εγκατάσταση της συσκευής απέτυχε", "type-timeseries-updated": "Ενημερώθηκε τηλεμετρία", "type-timeseries-deleted": "Διαγράφηκε τηλεμετρία", - "type-sms-sent": "SMS στάλθηκε" + "type-sms-sent": "SMS στάλθηκε", + "any-type": "Οποιοσδήποτε τύπος", + "audit-log-filter-title": "Φίλτρο καταγραφής ελέγχου", + "filter-title": "Φίλτρο", + "filter-types": "Τύποι καταγραφής ελέγχου" }, "debug-settings": { "label": "Ρυθμίσεις εντοπισμού σφαλμάτων", @@ -1009,15 +1095,29 @@ "expression": "Έκφραση", "no-found": "Δεν βρέθηκαν υπολογιζόμενα πεδία", "list": "{ count, plural, =1 {Ένα υπολογιζόμενο πεδίο} other {Λίστα με # υπολογιζόμενα πεδία} }", + "add": "Προσθήκη υπολογιζόμενου πεδίου", "selected-fields": "{ count, plural, =1 {1 υπολογιζόμενο πεδίο} other {# υπολογιζόμενα πεδία} } επιλεγμένα", "type": { "simple": "Απλό", - "script": "Σενάριο" + "simple-hint": "Απλός αριθμητικός υπολογισμός με βάση τα ορίσματα εισόδου.", + "script": "Σενάριο", + "script-hint": "Υπολογισμός πάνω σε καθορισμένα ορίσματα με χρήση σεναρίου TBEL.", + "geofencing": "Γεωπερίφραξη", + "geofencing-hint": "Αξιολόγηση της θέσης GPS της οντότητας και των μεταβάσεων σε σχέση με τις ρυθμισμένες ομάδες ζωνών γεωπερίφραξης.", + "propagation": "Διάδοση", + "propagation-hint": "Διάδοση δεδομένων σε γονικές ή θυγατρικές οντότητες με βάση την κατεύθυνση και τον τύπο της σχέσης.", + "related-entities-aggregation": "Συγκέντρωση σχετικών οντοτήτων", + "related-entities-aggregation-hint": "Συγκέντρωση των πιο πρόσφατων δεδομένων από σχετικές οντότητες.", + "time-series-data-aggregation": "Συγκέντρωση δεδομένων χρονοσειράς", + "time-series-data-aggregation-hint": "Συγκέντρωση ιστορικών δεδομένων από την τρέχουσα οντότητα." }, + "preview": "Προεπισκόπηση", "arguments": "Ορίσματα", "decimals-by-default": "Αριθμοί μετά την υποδιαστολή (προεπιλογή)", "debugging": "Εντοπισμός σφαλμάτων υπολογιζόμενου πεδίου", + "calculated-field-details": "Λεπτομέρειες υπολογιζόμενου πεδίου", "argument-name": "Όνομα ορίσματος", + "name": "Όνομα", "datasource": "Πηγή δεδομένων", "add-argument": "Προσθήκη ορίσματος", "test-script-function": "Δοκιμή συνάρτησης script", @@ -1029,6 +1129,8 @@ "argument-asset": "Περιουσιακό στοιχείο", "argument-customer": "Πελάτης", "argument-tenant": "Τρέχων ενοικιαστής", + "argument-owner": "Τρέχων ιδιοκτήτης", + "argument-relation-query": "Σχετικές οντότητες", "argument-type": "Τύπος ορίσματος", "attribute": "Χαρακτηριστικό", "copy-argument-name": "Αντιγραφή ονόματος ορίσματος", @@ -1042,12 +1144,14 @@ "shared-attributes": "Κοινόχρηστα χαρακτηριστικά", "attribute-key": "Κλειδί χαρακτηριστικού", "default-value": "Προεπιλεγμένη τιμή", + "default-value-required": "Απαιτείται προεπιλεγμένη τιμή.", "limit": "Μέγιστες τιμές", "time-window": "Παράθυρο χρόνου", "customer-name": "Όνομα πελάτη", "asset-name": "Όνομα περιουσιακού στοιχείου", "timeseries": "Χρονοσειρά", "output": "Έξοδος", + "output-hint": "Καθορίζει τον τρόπο επεξεργασίας της εξόδου.", "create": "Δημιουργία νέου υπολογιζόμενου πεδίου", "file": "Αρχείο υπολογιζόμενου πεδίου", "invalid-file-error": "Μη έγκυρη μορφή αρχείου. Βεβαιωθείτε ότι είναι έγκυρο αρχείο JSON.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Προσοχή, μετά την επιβεβαίωση όλα τα επιλεγμένα υπολογιζόμενα πεδία θα διαγραφούν και όλα τα σχετικά δεδομένα δεν θα είναι ανακτήσιμα.", "test-with-this-message": "Δοκιμή με αυτό το μήνυμα", "use-latest-timestamp": "Χρήση πιο πρόσφατης χρονικής σήμανσης", + "entity-coordinates": "Συντεταγμένες οντότητας", + "latitude-time-series-key": "Κλειδί χρονοσειράς γεωγραφικού πλάτους", + "latitude-time-series-key-required": "Απαιτείται κλειδί χρονοσειράς γεωγραφικού πλάτους.", + "longitude-time-series-key": "Κλειδί χρονοσειράς γεωγραφικού μήκους", + "longitude-time-series-key-required": "Απαιτείται κλειδί χρονοσειράς γεωγραφικού μήκους.", + "geofencing-zone-groups": "Ομάδες ζωνών γεωπερίφραξης", + "geofencing-zone-groups-settings": "Ρυθμίσεις ομάδας ζωνών γεωπερίφραξης", + "target-zone": "Ζώνη στόχος", + "perimeter-key": "Κλειδί περιμέτρου", + "report-strategy": "Στρατηγική αναφοράς", + "no-zone-configured": "Απαιτείται τουλάχιστον μία ζώνη.", + "no-zone-configured-required": "Πρέπει να έχει ρυθμιστεί τουλάχιστον μία ομάδα ζωνών.", + "add-zone-group": "Προσθήκη ομάδας ζωνών", + "report-transition-event-only": "Μόνο συμβάντα μετάβασης", + "report-presence-status-only": "Μόνο κατάσταση παρουσίας", + "report-transition-event-and-presence": "Κατάσταση παρουσίας και συμβάντα μετάβασης", + "perimeter-attribute-key": "Κλειδί χαρακτηριστικού περιμέτρου", + "perimeter-attribute-key-required": "Απαιτείται κλειδί χαρακτηριστικού περιμέτρου.", + "perimeter-attribute-key-pattern": "Το κλειδί χαρακτηριστικού περιμέτρου δεν είναι έγκυρο.", + "entity-zone-relationship": "Διαδρομή από την οντότητα στις ζώνες", + "direction": "Κατεύθυνση σχέσης", + "direction-from": "Από την οντότητα στη ζώνη", + "direction-to": "Από τη ζώνη στην οντότητα", + "relation-type": "Τύπος σχέσης", + "create-relation-with-matched-zones": "Δημιουργία σχέσεων για την οντότητα προέλευσης με τις ζώνες που αντιστοιχούν", + "relation-level": "Επίπεδο σχέσης", + "fetch-last-available-level": "Λήψη μόνο του τελευταίου διαθέσιμου επιπέδου", + "zone-group-refresh-interval": "Διάστημα ανανέωσης ομάδων ζωνών", + "copy-zone-group-name": "Αντιγραφή ονόματος ομάδας ζωνών", + "open-details-page": "Άνοιγμα σελίδας λεπτομερειών οντότητας", + "level": "Επίπεδο", + "direction-level": "Κατεύθυνση", + "direction-up": "Προς τα πάνω", + "direction-up-parent": "Προς τα πάνω έως τη γονική", + "direction-down": "Προς τα κάτω", + "direction-down-child": "Προς τα κάτω έως τη θυγατρική", + "add-level": "Προσθήκη επιπέδου", + "delete-level": "Διαγραφή επιπέδου", + "no-level": "Δεν έχει ρυθμιστεί επίπεδο", + "levels-required": "Πρέπει να έχει ρυθμιστεί τουλάχιστον ένα επίπεδο.", + "max-allowed-levels-error": "Το επίπεδο σχέσης υπερβαίνει το μέγιστο επιτρεπόμενο.", + "propagation-path-related-entities": "Διαδρομή διάδοσης προς σχετικές οντότητες", + "propagate-type": { + "arguments-only": "Μόνο ορίσματα", + "expression-result": "Αποτέλεσμα υπολογισμού" + }, + "script": "Σενάριο", + "data-propagate": "Δεδομένα προς διάδοση", + "output-key": "Κλειδί εξόδου", + "copy-output-key": "Αντιγραφή κλειδιού εξόδου", + "aggregation-path-related-entities": "Διαδρομή συγκέντρωσης προς σχετικές οντότητες", + "deduplication-interval": "Διάστημα αποδιπλοεγγραφής", + "deduplication-interval-min": "Το διάστημα αποδιπλοεγγραφής πρέπει να είναι τουλάχιστον {{ sec }} δευτερόλεπτα.", + "deduplication-interval-hint": "Ελάχιστος χρόνος μεταξύ συγκεντρώσεων τηλεμετρίας.", + "deduplication-interval-required": "Απαιτείται διάστημα αποδιπλοεγγραφής.", + "calculated-field-filter-title": "Φίλτρο υπολογιζόμενου πεδίου", + "filter-title": "Φίλτρο", + "calculated-field-types": "Τύποι υπολογιζόμενου πεδίου", + "events": "Συμβάντα", + "any-type": "Οποιοσδήποτε τύπος", + "metrics": { + "metrics": "Μετρικές", + "metrics-empty": "Πρέπει να έχει ρυθμιστεί τουλάχιστον μία μετρική.", + "metric-name": "Όνομα μετρικής", + "metric-name-required": "Απαιτείται όνομα μετρικής.", + "metric-name-pattern": "Το όνομα μετρικής δεν είναι έγκυρο.", + "metric-name-duplicate": "Υπάρχει ήδη μετρική με τέτοιο όνομα.", + "metric-name-max-length": "Το όνομα μετρικής πρέπει να είναι μικρότερο από 256 χαρακτήρες.", + "metric-name-forbidden": "Το όνομα μετρικής είναι δεσμευμένο και δεν μπορεί να χρησιμοποιηθεί.", + "copy-metric-name": "Αντιγραφή ονόματος μετρικής", + "argument-name": "Όνομα ορίσματος", + "aggregation": "Συγκέντρωση", + "aggregation-type": { + "avg": "Μέσος όρος", + "min": "Ελάχιστο", + "max": "Μέγιστο", + "sum": "Άθροισμα", + "count": "Πλήθος", + "count-unique": "Πλήθος μοναδικών" + }, + "filtered": "Φιλτραρισμένο", + "value-source": "Πηγή τιμής", + "value-source-hint": "Καθορίζει πώς λαμβάνεται η τιμή για τη συγκέντρωση.", + "value-source-type": { + "key": "Κλειδί", + "function": "Συνάρτηση" + }, + "no-metrics-configured": "Απαιτείται τουλάχιστον μία μετρική.", + "add-metric": "Προσθήκη μετρικής", + "max-metrics": "Έχει επιτευχθεί ο μέγιστος αριθμός μετρικών.", + "metric-settings": "Ρυθμίσεις μετρικής", + "filter": "Φίλτρο", + "filter-hint": "Ενεργοποιεί το φιλτράρισμα οντοτήτων κατά τη συγκέντρωση. Η συνάρτηση φίλτρου πρέπει να επιστρέφει μια λογική τιμή (boolean) και μπορεί να χρησιμοποιεί όλα τα ρυθμισμένα ορίσματα." + }, + "output-strategy": { + "strategy": "Στρατηγική", + "process-right-away": "Άμεση επεξεργασία", + "process-rule-chains": "Επεξεργασία μέσω αλυσίδων κανόνων", + "save-time-series": "Αποθήκευση σε χρονοσειρά", + "save-database": "Αποθήκευση στη βάση δεδομένων", + "save-latest-values": "Αποθήκευση στις πιο πρόσφατες τιμές", + "send-web-sockets": "Αποστολή σε WebSockets", + "save-calculated-fields": "Αποστολή σε υπολογιζόμενα πεδία", + "update-attribute-only-on-value-change": "Ενημέρωση χαρακτηριστικού μόνο όταν αλλάζει η τιμή", + "send-attributes-updated-notification": "Αποστολή ειδοποίησης ενημέρωσης χαρακτηριστικών", + "ttl": "Προσαρμοσμένο TTL", + "ttl-required": "Απαιτείται TTL", + "ttl-min": "Επιτρέπεται μόνο ελάχιστο TTL 0", + "processing-parameters": "Παράμετροι επεξεργασίας", + "hint": { + "strategy": "Ελέγχει αν το αποτέλεσμα επεξεργάζεται άμεσα ή αποστέλλεται σε αλυσίδα κανόνων για πρόσθετη επεξεργασία.", + "processing-options": "Επιλογές επεξεργασίας", + "update-attribute-only-on-value-change": "Ενημερώνει το χαρακτηριστικό σε κάθε εισερχόμενο μήνυμα, ανεξάρτητα από το αν η τιμή έχει αλλάξει. Αυτό αυξάνει τη χρήση API και μειώνει την απόδοση.", + "update-attribute-only-on-value-change-enabled": "Ενημερώνει το χαρακτηριστικό μόνο όταν αλλάζει η τιμή. Αν η τιμή δεν αλλάξει, δεν ενημερώνονται οι χρονοσφραγίδες και δεν αποστέλλονται ειδοποιήσεις.", + "send-attributes-updated-notification": "Αποστέλλει συμβάν ενημέρωσης χαρακτηριστικών (Attributes Updated) στην προεπιλεγμένη αλυσίδα κανόνων.", + "save-time-series": "Αποθηκεύει δεδομένα χρονοσειράς στον πίνακα ts_kv στη βάση δεδομένων.", + "save-database": "Αποθηκεύει δεδομένα χαρακτηριστικών στη βάση δεδομένων.", + "save-latest-values": "Ενημερώνει τα δεδομένα χρονοσειράς στον πίνακα ts_kv_latest στη βάση δεδομένων, αν η νέα τιμή είναι πιο πρόσφατη.", + "send-web-sockets-attribute": "Ειδοποιεί τις συνδρομές WebSocket για ενημερώσεις στα δεδομένα χαρακτηριστικών.", + "send-web-sockets-time-series": "Ειδοποιεί τις συνδρομές WebSocket για ενημερώσεις στα δεδομένα χρονοσειράς.", + "save-calculated-fields-attribute": "Ειδοποιεί τα υπολογιζόμενα πεδία για ενημερώσεις στα δεδομένα χαρακτηριστικών.", + "save-calculated-fields-time-series": "Ειδοποιεί τα υπολογιζόμενα πεδία για ενημερώσεις στα δεδομένα χρονοσειράς.", + "ttl": "Καθορίζει την περίοδο διατήρησης για τα δεδομένα χρονοσειράς. Αν είναι απενεργοποιημένο, χρησιμοποιείται το TTL του προφίλ ενοικιαστή." + } + }, + "aggregate-interval-type": "Τύπος διαστήματος συγκέντρωσης", + "aggregate-interval-value": "Τιμή διαστήματος συγκέντρωσης", + "aggregate-interval-value-required": "Απαιτείται τιμή διαστήματος συγκέντρωσης.", + "aggregate-interval-value-min": "Η τιμή διαστήματος συγκέντρωσης πρέπει να είναι τουλάχιστον { sec, plural, =0 {0 δευτερόλεπτα} =1 {1 δευτερόλεπτο} other {# δευτερόλεπτα} }.", + "aggregate-interval-value-step-multiple-of": "Η τιμή διαστήματος συγκέντρωσης πρέπει να είναι διαιρέτης ή πολλαπλάσιο της 1 ημέρας.", + "aggregate-period": { + "hour": "Ώρα", + "day": "Ημέρα", + "week": "Εβδομάδα (Δευ - Κυρ)", + "week-sun-sat": "Εβδομάδα (Κυρ - Σαβ)", + "month": "Μήνας", + "quarter": "Τρίμηνο", + "year": "Έτος", + "custom": "Προσαρμοσμένο" + }, + "aggregate-period-hint-offset": "Το διάστημα συγκέντρωσής σας θα είναι: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Το διάστημα συγκέντρωσής σας θα είναι: {{ interval }} κ.ο.κ.", + "entity-aggregation": { + "argument-hint": "Τα δεδομένα θα ληφθούν από την τρέχουσα οντότητα.", + "argument-title-hint": "Καθορίζει τα ορίσματα εισόδου που χρησιμοποιούνται για τη συγκέντρωση.", + "argument-setting-hint": "Η πιο πρόσφατη τηλεμετρία είναι ο μόνος διαθέσιμος τύπος ορίσματος για αυτό το υπολογιζόμενο πεδίο.", + "aggregation-interval": "Διάστημα συγκέντρωσης", + "aggregation-interval-hint": "Καθορίζει πόσο συχνά εκτελείται η συγκέντρωση. Παράδειγμα: κάθε 1 ώρα συγκεντρώνει δεδομένα στις 00:00, 01:00, 02:00 κ.λπ. Τα αποτελέσματα συγκέντρωσης αποθηκεύονται με τη χρονοσφραγίδα που αντιστοιχεί στην αρχή του διαστήματος συγκέντρωσης.", + "apply-offset": "Εφαρμογή μετατόπισης στο διάστημα συγκέντρωσης", + "apply-offset-hint": "Καθορίζει πόσο θα μετατοπιστεί η αρχή κάθε περιόδου συγκέντρωσης (π.χ. +10 λεπτά - 00:10, 01:10).", + "offset-value": "Τιμή μετατόπισης", + "offset-value-required": "Απαιτείται τιμή μετατόπισης.", + "offset-value-min": "Η τιμή μετατόπισης πρέπει να είναι θετικός ακέραιος.", + "offset-value-max": "Η τιμή μετατόπισης πρέπει να είναι μικρότερη από την τιμή διαστήματος συγκέντρωσης.", + "wait-delay": "Εφαρμογή χρονικού ορίου αναμονής για καθυστερημένη τηλεμετρία", + "wait-delay-hint": "Καθορίζει πόσο θα περιμένετε για καθυστερημένη τηλεμετρία μετά τη λήξη του διαστήματος. Αν φτάσει τέτοια τηλεμετρία, το αποτέλεσμα για εκείνο το διάστημα θα επανυπολογιστεί.", + "duration": "Διάρκεια", + "duration-required": "Απαιτείται διάρκεια.", + "duration-min": "Η διάρκεια πρέπει να είναι τουλάχιστον 1 λεπτό.", + "duration-hint": "Πόσο θα περιμένετε για καθυστερημένα δεδομένα μετά τη λήξη του διαστήματος.", + "produce-intermediate-result": "Παραγωγή ενδιάμεσου αποτελέσματος", + "produce-intermediate-result-hint": "Υπολογίζει μετρικές κατά τη διάρκεια του τρέχοντος διαστήματος για την παραγωγή ενδιάμεσου αποτελέσματος. Οι ενημερώσεις δεν πραγματοποιούνται συχνότερα από μία φορά κάθε {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Το υπολογιζόμενο πεδίο τύπου «Απλό» δεν πρέπει να περιέχει κλειδιά με τύπο «Κυλιόμενη χρονοσειρά».", + "arguments-propagate-arguments-with-rolling": "Ο τύπος «Κυλιόμενη χρονοσειρά» δεν είναι συμβατός με τη διάδοση «Μόνο ορίσματα».", + "arguments-propagate-argument-entity-type": "Ο τύπος οντότητας δεν είναι συμβατός με τη διάδοση «Μόνο ορίσματα».", + "arguments-propagate-argument-must-current-entity": "Πρέπει να έχει ρυθμιστεί τουλάχιστον ένα όρισμα με τον τύπο οντότητας προέλευσης «Τρέχουσα οντότητα».", "arguments-empty": "Πρέπει να καθοριστεί τουλάχιστον ένα όρισμα.", "expression-required": "Απαιτείται έκφραση.", "expression-invalid": "Η έκφραση δεν είναι έγκυρη.", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Υπάρχει ήδη όρισμα με αυτό το όνομα.", "argument-name-max-length": "Το όνομα του ορίσματος πρέπει να είναι μικρότερο από 256 χαρακτήρες.", "argument-name-forbidden": "Το όνομα του ορίσματος είναι δεσμευμένο και δεν μπορεί να χρησιμοποιηθεί.", + "output-key-required": "Απαιτείται κλειδί εξόδου.", + "output-key-pattern": "Το κλειδί εξόδου δεν είναι έγκυρο.", + "output-key-duplicate": "Υπάρχει ήδη κλειδί με τέτοιο όνομα.", + "output-key-max-length": "Το κλειδί εξόδου πρέπει να είναι μικρότερο από 256 χαρακτήρες.", + "output-key-forbidden": "Το κλειδί εξόδου είναι δεσμευμένο και δεν μπορεί να χρησιμοποιηθεί.", + "entity-type-required": "Απαιτείται τύπος οντότητας.", + "name-required": "Απαιτείται όνομα.", + "name-pattern": "Το όνομα δεν είναι έγκυρο.", + "name-duplicate": "Υπάρχει ήδη όνομα με αυτήν την τιμή.", + "name-max-length": "Το όνομα πρέπει να είναι μικρότερο από 256 χαρακτήρες.", + "name-forbidden": "Το όνομα είναι δεσμευμένο και δεν μπορεί να χρησιμοποιηθεί.", "argument-type-required": "Απαιτείται τύπος ορίσματος.", "max-args": "Έχει επιτευχθεί το μέγιστο πλήθος ορισμάτων.", "decimals-range": "Ο αριθμός δεκαδικών πρέπει να είναι από 0 έως 15.", "expression": "Η προεπιλεγμένη έκφραση δείχνει πώς να μετατρέψετε τη θερμοκρασία από Φαρενάιτ σε Κελσίου.", "arguments-entity-not-found": "Η οντότητα στόχος του ορίσματος δεν βρέθηκε.", - "use-latest-timestamp": "Αν είναι ενεργοποιημένο, η τιμή θα αποθηκευτεί με τη πιο πρόσφατη χρονική σήμανση από τα τηλεμετρικά δεδομένα των ορισμάτων, αντί για την ώρα του διακομιστή." + "use-latest-timestamp": "Αν είναι ενεργοποιημένο, η τιμή θα αποθηκευτεί με τη πιο πρόσφατη χρονική σήμανση από τα τηλεμετρικά δεδομένα των ορισμάτων, αντί για την ώρα του διακομιστή.", + "entity-coordinates": "Καθορίστε τα κλειδιά χρονοσειράς που παρέχουν τις συντεταγμένες GPS της οντότητας (γεωγραφικό πλάτος και γεωγραφικό μήκος).", + "geofencing-zone-groups": "Ορίστε μία ή περισσότερες ομάδες ζωνών γεωπερίφραξης για έλεγχο (π.χ. 'allowedZones', 'restrictedZones'). Κάθε ομάδα πρέπει να έχει μοναδικό όνομα, το οποίο χρησιμοποιείται ως πρόθεμα για τα κλειδιά τηλεμετρίας εξόδου του υπολογιζόμενου πεδίου.", + "perimeter-attribute-key": "Ορίστε το κλειδί χαρακτηριστικού που περιέχει τον ορισμό της περιμέτρου της ζώνης γεωπερίφραξης. Η περίμετρος λαμβάνεται πάντα από χαρακτηριστικά πλευράς διακομιστή της οντότητας ζώνης.", + "report-strategy": "Οι αναφορές κατάστασης παρουσίας υποδεικνύουν αν η οντότητα βρίσκεται αυτή τη στιγμή ΕΝΤΟΣ ή ΕΚΤΟΣ της ομάδας ζωνών. Τα συμβάντα μετάβασης αναφέρουν πότε η οντότητα ΕΙΣΗΛΘΕ ή ΕΞΗΛΘΕ από την ομάδα ζωνών.", + "create-relation-with-matched-zones": "Αυτόματη δημιουργία και διατήρηση σχέσεων μεταξύ της οντότητας και των ζωνών στις οποίες βρίσκεται αυτή τη στιγμή. Οι σχέσεις αφαιρούνται όταν η οντότητα εξέρχεται από μια ζώνη και δημιουργούνται όταν εισέρχεται σε μια νέα.", + "relation-type-required": "Απαιτείται τύπος σχέσης.", + "relation-level-required": "Απαιτείται επίπεδο σχέσης.", + "relation-level-min": "Η ελάχιστη τιμή επιπέδου σχέσης είναι 1.", + "relation-level-max": "Η μέγιστη τιμή επιπέδου σχέσης είναι {{max}}.", + "geofencing-empty": "Πρέπει να έχει ρυθμιστεί τουλάχιστον μία ομάδα ζωνών.", + "geofencing-entity-not-found": "Δεν βρέθηκε η οντότητα στόχος γεωπερίφραξης.", + "max-geofencing-zone": "Έχει επιτευχθεί ο μέγιστος αριθμός ζωνών γεωπερίφραξης.", + "zone-group-refresh-interval": "Καθορίζει πόσο συχνά ανανεώνονται οι ομάδες ζωνών που έχουν ρυθμιστεί μέσω σχετικών οντοτήτων.", + "zone-group-refresh-interval-required": "Απαιτείται διάστημα ανανέωσης ομάδων ζωνών.", + "zone-group-refresh-interval-min": "Το διάστημα ανανέωσης ομάδων ζωνών πρέπει να είναι τουλάχιστον {{ min }} δευτερόλεπτα.", + "propagation-path-related-entities": "Καθορίζει μια άμεση διαδρομή ενός επιπέδου προς σχετική οντότητα με βάση την επιλεγμένη κατεύθυνση και τον τύπο σχέσης. Υποστηρίζονται μόνο σχέσεις μεταξύ οντοτήτων συσκευής, περιουσιακού στοιχείου, πελάτη και ενοικιαστή. Το μέγιστο πλήθος οντοτήτων που επιλύονται μέσω της διαδρομής σχέσεων είναι {{ max }}.", + "data-propagate": "Καθορίζει τα δεδομένα που θα διαδοθούν από τα ορίσματα που ρυθμίζονται παρακάτω. Το 'Arguments only' χρησιμοποιεί απευθείας τα ανακτημένα δεδομένα, ενώ το 'Expression result' υπολογίζει μια νέα τιμή από αυτά τα δεδομένα.", + "aggregation-path-related-entities": "Καθορίζει μια διαδρομή συγκέντρωσης ενός επιπέδου μέσω άμεσων σχέσεων με γονικές ή θυγατρικές οντότητες, με βάση την κατεύθυνση και τον τύπο σχέσης. Υποστηρίζονται μόνο σχέσεις μεταξύ οντοτήτων συσκευής, περιουσιακού στοιχείου, πελάτη και ενοικιαστή. Το μέγιστο πλήθος οντοτήτων που επιλύονται μέσω της διαδρομής σχέσεων είναι {{ max }}.", + "arguments-aggregation": "Καθορίζει τα ορίσματα εισόδου που χρησιμοποιούνται για φιλτράρισμα και συγκέντρωση.", + "setting-arguments-aggregation": "Τα δεδομένα θα ληφθούν από σχετικές οντότητες που έχουν ρυθμιστεί στη διαδρομή συγκέντρωσης.", + "metrics": "Καθορίζει τις μετρικές που συγκεντρώνονται με βάση τα ρυθμισμένα ορίσματα.", + "entity-aggregation-metrics": "Καθορίζει τις μετρικές που συγκεντρώνονται με βάση τα ρυθμισμένα ορίσματα στα καθορισμένα χρονικά διαστήματα.", + "import-invalid-calculated-field-type": "Δεν είναι δυνατή η εισαγωγή υπολογιζόμενου πεδίου: μη έγκυρη δομή υπολογιζόμενου πεδίου.", + "simple-expression-title": "Αριθμητική έκφραση που καθορίζει πώς υπολογίζεται η υπολογιζόμενη τιμή.", + "script-title": "Σενάριο TBEL που καθορίζει τη λογική υπολογισμού και τις τιμές εξόδου.", + "simple-arguments": "Αριθμητική έκφραση που καθορίζει πώς υπολογίζεται η υπολογιζόμενη τιμή.", + "script-arguments": "Καθορίζει τα ορίσματα εισόδου που είναι διαθέσιμα στο σενάριο." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Κανόνες συναγερμού", + "alarm-rule": "Κανόνας συναγερμού", + "alarm-rules": "Κανόνες συναγερμού", + "alarm-rules-old": "Παλαιό", + "alarm-rules-actual": "Τρέχον", + "severities": "Επίπεδα σοβαρότητας", + "cleared": "Συνθήκη εκκαθάρισης", + "delete-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον κανόνα συναγερμού '{{title}}';", + "delete-text": "Προσοχή: μετά την επιβεβαίωση, ο κανόνας συναγερμού και όλα τα σχετικά δεδομένα δεν θα μπορούν να ανακτηθούν.", + "delete-multiple-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, =1 {1 κανόνα συναγερμού} other {# κανόνες συναγερμού} };", + "delete-multiple-text": "Προσοχή: μετά την επιβεβαίωση, όλοι οι επιλεγμένοι κανόνες συναγερμού θα αφαιρεθούν και όλα τα σχετικά δεδομένα δεν θα μπορούν να ανακτηθούν.", + "create": "Δημιουργία νέου κανόνα συναγερμού", + "add": "Προσθήκη κανόνα συναγερμού", + "copy": "Αντιγραφή διαμόρφωσης κανόνα συναγερμού", + "details": "Λεπτομέρειες κανόνα συναγερμού", + "no-found": "Δεν βρέθηκαν κανόνες συναγερμού", + "list": "{ count, plural, =1 {Ένας κανόνας συναγερμού} other {Λίστα # κανόνων συναγερμού} }", + "selected-fields": "{ count, plural, =1 {Επιλεγμένος 1 κανόνας συναγερμού} other {Επιλεγμένοι # κανόνες συναγερμού} }", + "import": "Εισαγωγή κανόνα συναγερμού", + "file": "Αρχείο κανόνα συναγερμού", + "export": "Εξαγωγή κανόνα συναγερμού", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή του κανόνα συναγερμού: {{error}}", + "entity-type": "Τύπος οντότητας", + "entity-type-required": "Απαιτείται τύπος οντότητας.", + "alarm-type": "Τύπος συναγερμού", + "alarm-type-hint": "Μοναδικό αναγνωριστικό (π.χ. HighTempAlarm) στο εύρος του δημιουργού του συναγερμού (Συσκευή, Περιουσιακό στοιχείο κ.λπ.) για την αποφυγή συγκρούσεων.", + "alarm-type-required": "Απαιτείται τύπος συναγερμού.", + "alarm-type-pattern": "Ο τύπος συναγερμού δεν είναι έγκυρος.", + "alarm-type-max-length": "Ο τύπος συναγερμού πρέπει να είναι μικρότερος από 256 χαρακτήρες.", + "clear-alarm": "Εκκαθάριση συναγερμού", + "value-argument": "Όρισμα", + "value-argument-required": "Απαιτείται όρισμα.", + "static-settings": "Στατικές ρυθμίσεις", + "configuration": "Διαμόρφωση", + "static-schedule": "Στατικό", + "dynamic-schedule": "Δυναμικό", + "operation-and": "ΚΑΙ", + "operation-or": "Ή", + "condition-during": "Κατά τη διάρκεια {{during}}", + "condition-during-dynamic": "Κατά τη διάρκεια \"{{ attribute }}\"", + "condition-repeat-times": "Επαναλαμβάνεται { count, plural, =1 {1 φορά} other {# φορές} }", + "condition-repeat-times-dynamic": "Επαναλαμβάνεται \"{{ attribute }}\" φορές", + "filter-preview": "Προεπισκόπηση φίλτρου", + "condition-settings": "Ρυθμίσεις συνθήκης", + "static": "Στατικό", + "dynamic": "Δυναμικό", + "argument-filters": "Φίλτρα ορισμάτων", + "argument-name": "Όνομα ορίσματος", + "value-type": "Τύπος τιμής", + "general": "Γενικά", + "filters": "Φίλτρα", + "date-time-hint": "Το όρισμα πρέπει να είναι σε epoch milliseconds. Παράδειγμα: το 1698839340000 ισοδυναμεί με 2023-11-01 12:49:00 UTC.", + "operation": "Τελεστής", + "value-source": "Πηγή τιμής", + "value": "Τιμή", + "ignore-case": "Παράβλεψη πεζών-κεφαλαίων", + "condition": "Συνθήκη", + "script": "Σενάριο", + "add-filter": "Προσθήκη φίλτρου ορίσματος", + "edit-filter": "Φίλτρο ορίσματος", + "remove-filter": "Αφαίρεση φίλτρου ορίσματος", + "no-filter": "Απαιτείται τουλάχιστον ένα φίλτρο.", + "conditions": { + "simple": "Απλό", + "duration": "Διάρκεια", + "repeating": "Επαναλαμβανόμενο" + }, + "schedule-title": "Χρονοπρόγραμμα", + "edit-schedule": "Επεξεργασία χρονοπρογράμματος συναγερμού", + "schedule-type": "Τύπος χρονοπρογραμματιστή", + "schedule-type-required": "Απαιτείται τύπος χρονοπρογραμματιστή.", + "schedule": { + "any-time": "Ενεργό συνεχώς", + "specific-time": "Ενεργό σε συγκεκριμένη ώρα", + "custom": "Προσαρμοσμένο" + }, + "schedule-day": { + "monday": "Δευτέρα", + "tuesday": "Τρίτη", + "wednesday": "Τετάρτη", + "thursday": "Πέμπτη", + "friday": "Παρασκευή", + "saturday": "Σάββατο", + "sunday": "Κυριακή" + }, + "schedule-days": "Ημέρες", + "schedule-time": "Ώρα", + "schedule-time-from": "Από", + "schedule-time-to": "Έως", + "schedule-days-of-week-required": "Πρέπει να επιλεγεί τουλάχιστον μία ημέρα της εβδομάδας.", + "tbel": "TBEL", + "expression-type": { + "simple": "Απλό", + "script": "Σενάριο" + }, + "operation-type": { + "and": "Και", + "or": "Ή" + }, + "filter-predicate-type": { + "string": "Συμβολοσειρά", + "numeric": "Αριθμητικό", + "boolean": "Λογικό", + "complex": "Σύνθετο" + }, + "alarm-rule-additional-info": "Πρόσθετες πληροφορίες", + "edit-alarm-rule-additional-info": "Επεξεργασία πρόσθετων πληροφοριών", + "alarm-rule-additional-info-placeholder": "Παρακαλούμε καταχωρίστε εδώ τα σχόλια και τις προσαρμογές σας, ώστε να εμφανίζονται στις λεπτομέρειες συναγερμού, στην ενότητα «Πρόσθετες πληροφορίες»", + "alarm-rule-additional-info-hint": "Υπόδειξη: χρησιμοποιήστε ${Argument name} για να αντικαθιστάτε τιμές των ορισμάτων που χρησιμοποιούνται στη συνθήκη κανόνα συναγερμού.", + "alarm-rule-additional-info-icon-hint": "Χρησιμοποιήστε το Argument name για να αντικαθιστάτε τιμές των ορισμάτων που χρησιμοποιούνται στη συνθήκη κανόνα συναγερμού.", + "alarm-rule-mobile-dashboard": "Πίνακας ελέγχου για κινητά", + "alarm-rule-mobile-dashboard-hint": "Χρησιμοποιείται από την εφαρμογή για κινητά ως πίνακας ελέγχου λεπτομερειών συναγερμού.", + "alarm-rule-no-mobile-dashboard": "Δεν έχει επιλεγεί πίνακας ελέγχου", + "alarm-rule-condition": "Συνθήκη κανόνα συναγερμού", + "enter-alarm-rule-condition-prompt": "Προσθήκη συνθήκης", + "enter-alarm-rule-clear-condition-prompt": "Προσθήκη συνθήκης εκκαθάρισης", + "edit-alarm-rule-condition": "Συνθήκη συναγερμού", + "condition-type": "Τύπος συνθήκης", + "condition-type-hint": "Οι επιλογές \"Διάρκεια\" και \"Επαναλαμβανόμενο\" δεν είναι διαθέσιμες όταν χρησιμοποιείται στο φίλτρο ο τελεστής \"Missing for\".", + "select-alarm-severity": "Επιλογή σοβαρότητας συναγερμού", + "add-create-alarm-rule-prompt": "Απαιτείται τουλάχιστον μία συνθήκη ενεργοποίησης.", + "add-create-alarm-rule": "Προσθήκη συνθήκης ενεργοποίησης", + "add-clear-alarm-rule": "Προσθήκη συνθήκης εκκαθάρισης", + "condition-duration": "Διάρκεια συνθήκης", + "condition-duration-value": "Τιμή διάρκειας", + "condition-duration-time-unit": "Μονάδα χρόνου", + "condition-duration-value-range": "Η τιμή διάρκειας πρέπει να βρίσκεται στο εύρος από 1 έως 2147483647.", + "condition-duration-value-pattern": "Η τιμή διάρκειας πρέπει να είναι ακέραιοι αριθμοί.", + "condition-duration-value-required": "Απαιτείται τιμή διάρκειας.", + "condition-duration-time-unit-required": "Απαιτείται μονάδα χρόνου.", + "condition-repeating-value": "Πλήθος συμβάντων", + "condition-repeating-value-hint": "Η ενημέρωση οποιουδήποτε ορίσματος κανόνα συναγερμού θα μετράται ως συμβάν", + "condition-repeating-value-range": "Το πλήθος συμβάντων πρέπει να βρίσκεται στο εύρος από 1 έως 2147483647.", + "condition-repeating-value-pattern": "Το πλήθος συμβάντων πρέπει να είναι ακέραιοι αριθμοί.", + "condition-repeating-value-required": "Απαιτείται πλήθος συμβάντων.", + "create-conditions": "Συνθήκες ενεργοποίησης", + "clear-condition": "Συνθήκη εκκαθάρισης", + "no-clear-alarm-rule": "Δεν έχει ρυθμιστεί συνθήκη εκκαθάρισης.", + "advanced-settings": "Ρυθμίσεις για προχωρημένους", + "propagate-alarm": "Διάδοση συναγερμού σε σχετικές οντότητες", + "alarm-rule-relation-types-list": "Τύποι σχέσεων", + "alarm-rule-relation-types-list-hint": "Καθορίζει τους τύπους σχέσεων για φιλτράρισμα των σχετικών οντοτήτων. Αν δεν οριστεί, ο συναγερμός θα διαδοθεί σε όλες τις σχετικές οντότητες.", + "propagate-alarm-to-owner": "Διάδοση συναγερμού στον κάτοχο οντότητας (Πελάτης ή Ενοικιαστής)", + "propagate-alarm-to-tenant": "Διάδοση συναγερμού στον ενοικιαστή", + "alarm-rule-filter-title": "Φίλτρο κανόνα συναγερμού", + "filter-title": "Φίλτρο", + "debugging": "Αποσφαλμάτωση κανόνα συναγερμού", + "any-type": "Οποιοσδήποτε τύπος", + "enter-alarm-rule-type": "Εισαγωγή τύπου συναγερμού", + "no-alarm-rule-types-matching": "Δεν βρέθηκαν τύποι συναγερμού που να αντιστοιχούν στο '{{entitySubtype}}'.", + "alarm-rule-type-list-empty": "Δεν έχουν επιλεγεί τύποι συναγερμού.", + "alarm-rule-type-list": "Λίστα τύπων συναγερμού", + "alarm-rule-entity-list": "Λίστα οντοτήτων", + "missing-for": "λείπει για", + "time-unit": "Μονάδα", + "mode": "Λειτουργία", + "type": "Τύπος", + "value-required": "Απαιτείται τιμή.", + "min-value": "Η τιμή πρέπει να είναι 1 ή μεγαλύτερη.", + "argument-in-use": "Το όρισμα χρησιμοποιείται ως γενικό όρισμα.", + "import-invalid-alarm-rule-type": "Δεν είναι δυνατή η εισαγωγή κανόνα συναγερμού: μη έγκυρη δομή κανόνα συναγερμού.", + "no-filter-preview": "Δεν έχει καθοριστεί φίλτρο", + "filter-operation": { + "and": "Και", + "or": "Ή" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Τεκμηρίωση", "time-left": "Απομένει {{time}}", "output": "Έξοδος", + "sort-asc": "Αύξουσα", + "sort-desc": "Φθίνουσα", "suffix": { "s": "δ", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Σειρά πίνακα ελέγχου στην εφαρμογή κινητού", "mobile-hide": "Απόκρυψη πίνακα ελέγχου στην εφαρμογή κινητού", "update-image": "Ενημέρωση εικόνας πίνακα ελέγχου", + "update-new-version": "Μεταφόρτωση νέας έκδοσης", + "upload-file-to-update": "Μεταφόρτωση αρχείου για ενημέρωση", "take-screenshot": "Λήψη στιγμιότυπου", "select-widget-title": "Επιλογή widget", "select-widget-value": "{{title}}: επιλογή widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrap πελάτη", "bootstrap-server": "Διακομιστής Bootstrap", "lwm2m-server": "Διακομιστής LwM2M", + "client-reboot": "Έναυσμα ενημέρωσης καταχώρισης", + "bootstrap-reboot": "Έναυσμα αίτησης bootstrap", "client-publicKey-or-id": "Δημόσιο κλειδί ή Id πελάτη", "client-publicKey-or-id-required": "Απαιτείται Δημόσιο κλειδί ή Id πελάτη.", "client-publicKey-or-id-tooltip-psk": "Το αναγνωριστικό PSK είναι ένα αυθαίρετο αναγνωριστικό PSK έως 128 byte, όπως περιγράφεται στο πρότυπο [RFC7925].\nΤο αναγνωριστικό PSK ΠΡΕΠΕΙ να μετατραπεί πρώτα σε συμβολοσειρά χαρακτήρων και στη συνέχεια να κωδικοποιηθεί σε οκτάδες χρησιμοποιώντας UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "Το σύντομο αναγνωριστικό διακομιστή είναι υποχρεωτικό.", "short-id-range": "Το σύντομο αναγνωριστικό διακομιστή πρέπει να είναι εντός του εύρους από {{ min }} έως {{ max }}.", "short-id-pattern": "Το σύντομο αναγνωριστικό διακομιστή πρέπει να είναι θετικός ακέραιος αριθμός.", + "short-id-pattern-bs": "Το σύντομο αναγνωριστικό διακομιστή πρέπει να είναι μόνο null", "lifetime": "Διάρκεια ζωής καταχώρισης πελάτη", "lifetime-required": "Η διάρκεια εγγραφής πελάτη είναι υποχρεωτική.", "lifetime-pattern": "Η διάρκεια εγγραφής πελάτη πρέπει να είναι θετικός ακέραιος αριθμός.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Όλοι οι πόροι παρακολουθούνται με ένα ενιαίο αίτημα Composite Observe (πιο αποδοτικό, λιγότερο ευέλικτο)", "composite-by-object": "Συνδυασμένο ανά αντικείμενο", "composite-by-object-description": "Οι πόροι ομαδοποιούνται ανά τύπο αντικειμένου και παρακολουθούνται με ξεχωριστά αιτήματα Composite Observe (ισορροπημένη προσέγγιση)" - } + }, + "init-attr-tel-as-obs-strategy": "Αρχικοποίηση χαρακτηριστικών και τηλεμετρίας με χρήση στρατηγικής Observe", + "init-attr-tel-as-obs-strategy-hint": "Αν είναι false - τα χαρακτηριστικά και η τηλεμετρία αρχικοποιούνται με ανάγνωση των τιμών τους μία προς μία.\\nΑν είναι true - τα χαρακτηριστικά και η τηλεμετρία αρχικοποιούνται με εγγραφή στις τιμές τους χρησιμοποιώντας τη στρατηγική Observe." }, "snmp": { "add-communication-config": "Προσθήκη ρύθμισης επικοινωνίας", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Κόμβοι κανόνα", "list-of-rulenodes": "{ count, plural, =1 {Ένας κόμβος κανόνα} other {Λίστα με # κόμβους κανόνα} }", "rulenode-name-starts-with": "Κόμβοι κανόνα με ονόματα που ξεκινούν με '{{prefix}}'", + "type-api-key": "Κλειδί API", + "type-api-keys": "Κλειδιά API", "type-current-customer": "Τρέχων πελάτης", "type-current-tenant": "Τρέχων ενοικιαστής", "type-current-user": "Τρέχων χρήστης", @@ -2656,6 +3143,7 @@ "details": "Λεπτομέρειες οντότητας", "no-entities-prompt": "Δεν βρέθηκαν οντότητες", "no-data": "Δεν υπάρχουν δεδομένα για εμφάνιση", + "show-all-columns": "Εμφάνιση όλων", "columns-to-display": "Στήλες προς εμφάνιση", "type-api-usage-state": "Κατάσταση Χρήσης API", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Μία εφαρμογή κινητού} other {Λίστα με # εφαρμογές κινητού} }", "type-mobile-app-bundle": "Πακέτο εφαρμογής κινητού", "type-mobile-app-bundles": "Πακέτα εφαρμογής κινητού", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Ένα πακέτο κινητού} other {Λίστα με # πακέτα κινητού} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Ένα πακέτο κινητού} other {Λίστα με # πακέτα κινητού} }", + "limit-reached": "Το όριο επιτεύχθηκε", + "limit-reached-text": "Έχετε φτάσει το όριο των {{ entities }}. Για να προσθέσετε περισσότερα, παρακαλούμε ζητήστε από τον διαχειριστή συστήματος να αυξήσει το όριό σας για {{ entity }}.", + "request-limit-increase": "Αίτημα αύξησης ορίου", + "request-sysadmin-text": "Είστε ο διαχειριστής συστήματος;", + "login-here": "Σύνδεση εδώ", + "to-increase-limit": "για αύξηση του ορίου.", + "increase-limit-request-sent-title": "Στείλαμε ένα αυτοματοποιημένο αίτημα στον διαχειριστή συστήματος για αύξηση του ορίου", + "increase-limit-request-sent-text": "Παρακαλούμε δώστε τους λίγο χρόνο για να εξετάσουν το αίτημα και να ενημερώσουν τις ρυθμίσεις. Ενδέχεται να χρειαστεί να ανανεώσετε αυτή τη σελίδα για να δείτε τις αλλαγές." }, "entity-field": { "created-time": "Χρόνος δημιουργίας", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Ο σύνδεσμος επαναφοράς έχει σταλεί", "email": "Email", "invalid-email-format": "Μη έγκυρη μορφή email.", + "sign-in-with": "Σύνδεση με {{name}}", + "sign-in-to-your-account": "Σύνδεση στον λογαριασμό σας", "or": "ή", "error": "Σφάλμα σύνδεσης", "verify-your-identity": "Επαληθεύστε την ταυτότητά σας", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Ο σύνδεσμος ενεργοποίησης έχει λήξει", "activation-link-expired-message": "Ο σύνδεσμος ενεργοποίησης του προφίλ σας έχει λήξει. Μπορείτε να επιστρέψετε στη σελίδα σύνδεσης για να λάβετε νέο email.", "reset-password-link-expired": "Ο σύνδεσμος επαναφοράς κωδικού έχει λήξει", - "reset-password-link-expired-message": "Ο σύνδεσμος για την επαναφορά του κωδικού έχει λήξει. Μπορείτε να επιστρέψετε στη σελίδα σύνδεσης για να λάβετε νέο email." + "reset-password-link-expired-message": "Ο σύνδεσμος για την επαναφορά του κωδικού έχει λήξει. Μπορείτε να επιστρέψετε στη σελίδα σύνδεσης για να λάβετε νέο email.", + "two-fa": "Έλεγχος ταυτότητας δύο παραγόντων", + "two-fa-required": "Απαιτείται έλεγχος ταυτότητας δύο παραγόντων", + "set-up-verification-method": "Ρύθμιση μεθόδου επαλήθευσης για συνέχεια", + "set-up-verification-method-login": "Ρύθμιση μεθόδου επαλήθευσης ή σύνδεση", + "enable-authenticator-app": "Ενεργοποίηση εφαρμογής ελέγχου ταυτότητας", + "enable-authenticator-app-description": "Παρακαλούμε εισαγάγετε τον κωδικό ασφαλείας από την εφαρμογή ελέγχου ταυτότητας", + "enable-authenticator-sms": "Ενεργοποίηση ελέγχου ταυτότητας SMS", + "enable-authenticator-sms-description": "Εισαγάγετε τον 6ψήφιο κωδικό που μόλις στείλαμε στο ", + "enable-authenticator-email": "Ενεργοποίηση ελέγχου ταυτότητας Email", + "enable-authenticator-email-description": "Ένας κωδικός ασφαλείας στάλθηκε στη διεύθυνση Email σας στο ", + "enter-key-manually": "ή εισαγάγετε χειροκίνητα αυτό το κλειδί 32 ψηφίων:", + "continue": "Συνέχεια", + "confirm": "Επιβεβαίωση", + "authenticator-app-success": "Η εφαρμογή ελέγχου ταυτότητας ενεργοποιήθηκε με επιτυχία", + "authenticator-app-success-description": "Την επόμενη φορά που θα συνδεθείτε, θα χρειαστεί να παρέχετε κωδικό ελέγχου ταυτότητας δύο παραγόντων", + "authenticator-sms-success": "Ο έλεγχος ταυτότητας SMS ενεργοποιήθηκε με επιτυχία", + "authenticator-sms-success-description": "Την επόμενη φορά που θα συνδεθείτε, θα σας ζητηθεί να εισαγάγετε τον κωδικό ασφαλείας που θα σταλεί στον αριθμό τηλεφώνου σας", + "authenticator-email-success": "Ο έλεγχος ταυτότητας Email ενεργοποιήθηκε με επιτυχία", + "authenticator-email-success-description": "Την επόμενη φορά που θα συνδεθείτε, θα σας ζητηθεί να εισαγάγετε τον κωδικό ασφαλείας που θα σταλεί στη διεύθυνση Email σας", + "authenticator-backup-code-success": "Ο εφεδρικός κωδικός ενεργοποιήθηκε με επιτυχία", + "authenticator-backup-code-success-description": "Την επόμενη φορά που θα συνδεθείτε, θα σας ζητηθεί να εισαγάγετε τον κωδικό ασφαλείας ή να χρησιμοποιήσετε έναν από τους εφεδρικούς κωδικούς.", + "add-verification-method": "Προσθήκη μεθόδου επαλήθευσης", + "get-backup-code": "Λήψη εφεδρικού κωδικού", + "copy-key": "Αντιγραφή κλειδιού", + "send-code": "Αποστολή κωδικού", + "email-label": "Email", + "email-description": "Εισαγάγετε ένα Email για χρήση ως έλεγχος ταυτότητας.", + "sms-description": "Εισαγάγετε έναν αριθμό τηλεφώνου για χρήση ως έλεγχος ταυτότητας.", + "backup-code-description": "Εκτυπώστε τους κωδικούς ώστε να τους έχετε διαθέσιμους όταν χρειαστεί να τους χρησιμοποιήσετε για σύνδεση στον λογαριασμό σας. Μπορείτε να χρησιμοποιήσετε κάθε εφεδρικό κωδικό μία φορά.", + "backup-code-warn": "Μόλις φύγετε από αυτή τη σελίδα, αυτοί οι κωδικοί δεν μπορούν να εμφανιστούν ξανά. Αποθηκεύστε τους με ασφάλεια χρησιμοποιώντας τις παρακάτω επιλογές.", + "download-txt": "Λήψη (txt)", + "print": "Εκτύπωση", + "verification-code": "6ψήφιος κωδικός", + "verification-code-invalid": "Μη έγκυρη μορφή κωδικού επαλήθευσης", + "verification-code-incorrect": "Ο κωδικός επαλήθευσης είναι λανθασμένος", + "verification-code-many-request": "Πάρα πολλά αιτήματα για έλεγχο κωδικού επαλήθευσης", + "scan-qr-code": "Σαρώστε αυτόν τον κωδικό QR με την εφαρμογή επαλήθευσής σας", + "phone-input": { + "phone-input-label": "Αριθμός τηλεφώνου", + "phone-input-required": "Απαιτείται αριθμός τηλεφώνου", + "phone-input-validation": "Ο αριθμός τηλεφώνου δεν είναι έγκυρος ή δεν είναι δυνατός", + "phone-input-pattern": "Μη έγκυρος αριθμός τηλεφώνου. Πρέπει να είναι σε μορφή E.164, π.χ. {{phoneNumber}}", + "phone-input-hint": "Αριθμός τηλεφώνου σε μορφή E.164, π.χ. {{phoneNumber}}" + } }, "mobile": { "add-application": "Προσθήκη εφαρμογής", @@ -4175,6 +4717,7 @@ "api-usage-limit": "Όριο χρήσης API", "device-activity": "Δραστηριότητα συσκευής", "entities-limit": "Όριο οντοτήτων", + "entities-limit-increase-request": "Αίτημα αύξησης ορίου οντοτήτων", "entity-action": "Ενέργεια οντότητας", "general": "Γενικό", "rule-engine-lifecycle-event": "Γεγονός κύκλου ζωής μηχανής κανόνων", @@ -4392,6 +4935,12 @@ "at-least": "Τουλάχιστον:", "character": "{ count, plural, =1 {1 χαρακτήρας} other {# χαρακτήρες} }", "digit": "{ count, plural, =1 {1 ψηφίο} other {# ψηφία} }", + "password-tooltip-min-length": "Τουλάχιστον {{minimumLength}} χαρακτήρες", + "password-tooltip-max-length": "Το πολύ {{maximumLength}} χαρακτήρες", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} κεφαλαίος χαρακτήρας", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} πεζός χαρακτήρας", + "password-tooltip-digit": "{{minimumDigits}} αριθμός", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} ειδικός χαρακτήρας", "incorrect-password-try-again": "Λανθασμένος κωδικός. Προσπαθήστε ξανά", "lowercase-letter": "{ count, plural, =1 {1 πεζό γράμμα} other {# πεζά γράμματα} }", "new-passwords-not-match": "Οι νέοι κωδικοί δεν ταιριάζουν", @@ -4450,7 +4999,8 @@ "additional-info": "Επιπλέον πληροφορίες (JSON)", "invalid-additional-info": "Δεν ήταν δυνατή η ανάλυση του JSON των επιπλέον πληροφοριών.", "no-relations-text": "Δεν βρέθηκαν συσχετίσεις", - "not": "Όχι" + "not": "Όχι", + "copy-type": "Αντιγραφή τύπου" }, "resource": { "add": "Προσθήκη πόρου", @@ -5400,7 +5950,8 @@ }, "time-series": "Χρονοσειρές", "latest": "Τελευταίες τιμές", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Υπολογιζόμενα πεδία και κανόνες συναγερμού" }, "save-attribute": { "processing-settings": "Ρυθμίσεις επεξεργασίας", @@ -5595,7 +6146,8 @@ "bad-request-params": "Μη έγκυρες παράμετροι αιτήματος", "item-not-found": "Το στοιχείο δεν βρέθηκε", "too-many-requests": "Πάρα πολλά αιτήματα", - "too-many-updates": "Πάρα πολλές ενημερώσεις" + "too-many-updates": "Πάρα πολλές ενημερώσεις", + "entities-limit-exceeded": "Υπέρβαση ορίου οντοτήτων" }, "tenant": { "tenant": "Ενοικιαστής", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Μέγιστος αριθμός παραμέτρων ανά υπολογισμένο πεδίο", "max-arguments-per-cf-range": "Ο μέγιστος αριθμός παραμέτρων δεν μπορεί να είναι αρνητικός", "max-arguments-per-cf-required": "Απαιτείται μέγιστος αριθμός παραμέτρων ανά υπολογισμένο πεδίο", + "max-related-level-per-argument": "Μέγιστο επίπεδο σχέσης ανά όρισμα «Σχετικές οντότητες»", + "max-related-level-per-argument-range": "Το μέγιστο επίπεδο σχέσης ανά όρισμα «Σχετικές οντότητες» δεν μπορεί να είναι μικρότερο από «1»", + "max-related-level-per-argument-required": "Απαιτείται το μέγιστο επίπεδο σχέσης ανά όρισμα «Σχετικές οντότητες»", + "min-allowed-scheduled-update-interval": "Ελάχιστο επιτρεπόμενο διάστημα ενημέρωσης για ορίσματα «Σχετικές οντότητες» (δευτερόλεπτα)", + "min-allowed-scheduled-update-interval-range": "Η ελάχιστη τιμή του ελάχιστου επιτρεπόμενου διαστήματος ενημέρωσης δεν μπορεί να είναι αρνητική", + "min-allowed-deduplication-interval": "Ελάχιστο επιτρεπόμενο διάστημα αποδιπλοεγγραφής (δευτερόλεπτα)", + "min-allowed-deduplication-interval-range": "Η τιμή του ελάχιστου επιτρεπόμενου διαστήματος αποδιπλοεγγραφής δεν μπορεί να είναι αρνητική", + "min-allowed-deduplication-interval-required": "Απαιτείται ελάχιστο επιτρεπόμενο διάστημα αποδιπλοεγγραφής", + "intermediate-aggregation-interval": "Διάστημα ενδιάμεσης συγκέντρωσης (δευτερόλεπτα)", + "intermediate-aggregation-interval-range": "Η τιμή του διαστήματος ενδιάμεσης συγκέντρωσης δεν μπορεί να είναι μικρότερη από «1»", + "intermediate-aggregation-interval-required": "Απαιτείται διάστημα ενδιάμεσης συγκέντρωσης", + "reevaluation-check-interval": "Διάστημα ελέγχου επαναξιολόγησης (δευτερόλεπτα)", + "reevaluation-check-interval-range": "Η τιμή του διαστήματος ελέγχου επαναξιολόγησης δεν μπορεί να είναι μικρότερη από «1»", + "reevaluation-check-interval-required": "Απαιτείται διάστημα ελέγχου επαναξιολόγησης", + "alarms-reevaluation-interval": "Διάστημα επαναξιολόγησης συναγερμών (δευτερόλεπτα)", + "alarms-reevaluation-interval-range": "Η τιμή του διαστήματος επαναξιολόγησης συναγερμών δεν μπορεί να είναι μικρότερη από «1»", + "alarms-reevaluation-interval-required": "Απαιτείται διάστημα επαναξιολόγησης συναγερμών", + "min-allowed-aggregation-interval": "Ελάχιστο επιτρεπόμενο διάστημα συγκέντρωσης (δευτερόλεπτα)", + "min-allowed-aggregation-interval-range": "Η τιμή του ελάχιστου επιτρεπόμενου διαστήματος συγκέντρωσης δεν μπορεί να είναι αρνητική", + "min-allowed-aggregation-interval-required": "Απαιτείται ελάχιστο επιτρεπόμενο διάστημα συγκέντρωσης", + "min-allowed-scheduled-update-interval-required": "Απαιτείται η ελάχιστη τιμή του ελάχιστου επιτρεπόμενου διαστήματος ενημέρωσης", "max-state-size": "Μέγιστο μέγεθος κατάστασης σε KB", "max-state-size-range": "Το μέγιστο μέγεθος κατάστασης δεν μπορεί να είναι αρνητικό", "max-state-size-required": "Απαιτείται μέγιστο μέγεθος κατάστασης", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Μέγιστος αριθμός εγγραφών ανά χρήστη", "ws-limit-max-subscriptions-per-public-user": "Μέγιστος αριθμός εγγραφών ανά δημόσιο χρήστη", "ws-limit-updates-per-session": "WS ενημερώσεις ανά συνεδρία", + "relation-search-entity-limit": "Όριο οντοτήτων αναζήτησης σχέσεων", + "relation-search-entity-limit-hint": "Περιορίζει τον αριθμό των οντοτήτων που επιλύονται στο τελευταίο επίπεδο της διαδρομής σχέσεων. Εφαρμόζεται στα ορίσματα «Σχετικές οντότητες» και στα πεδία διάδοσης.", + "relation-search-entity-limit-required": "Απαιτείται όριο οντοτήτων αναζήτησης σχέσεων", + "relation-search-entity-limit-range": "Το όριο οντοτήτων αναζήτησης σχέσεων δεν μπορεί να είναι μικρότερο από '1'", "rate-limits": { "add-limit": "Προσθήκη ορίου", "and-also-less-than": "και επίσης μικρότερο από", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Προεπιλεγμένο διάστημα ομαδοποίησης", "edit-intervals-list-hint": "Μπορεί να καθοριστεί λίστα διαθέσιμων διαστημάτων.", "edit-grouping-intervals-list-hint": "Δυνατότητα διαμόρφωσης λίστας και προεπιλογής διαστημάτων ομαδοποίησης.", - "all": "Όλα" + "all": "Όλα", + "save-current-settings-as-default": "Αποθήκευση τρεχουσών ρυθμίσεων ως προεπιλεγμένου χρονικού παραθύρου", + "hide-option-from-end-users": "Απόκρυψη επιλογής από τους τελικούς χρήστες" }, "tooltip": { "trigger": "Ενεργοποιητής", @@ -6648,6 +7227,7 @@ "export-attributes": "Εξαγωγή ιδιοτήτων", "export-credentials": "Εξαγωγή διαπιστευτηρίων", "export-calculated-fields": "Εξαγωγή υπολογιζόμενων πεδίων \nκαι κανόνων συναγερμού", + "export-alarm-rules": "Εξαγωγή κανόνων συναγερμού", "entity-versions": "Εκδόσεις οντοτήτων", "versions": "Εκδόσεις", "created-time": "Ώρα δημιουργίας", @@ -6665,6 +7245,7 @@ "load-attributes": "Φόρτωση ιδιοτήτων", "load-credentials": "Φόρτωση διαπιστευτηρίων", "load-calculated-fields": "Φόρτωση υπολογιζόμενων πεδίων", + "load-alarm-rules": "Φόρτωση κανόνων συναγερμού", "compare-with-current": "Σύγκριση με την τρέχουσα", "diff-entity-with-version": "Διαφορά με την έκδοση οντότητας '{{versionName}}'", "previous-difference": "Προηγούμενη διαφορά", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Σάρωση κωδικού QR", "make-phone-call": "Πραγματοποίηση τηλεφωνικής κλήσης", "get-location": "Λήψη τοποθεσίας τηλεφώνου", - "take-screenshot": "Λήψη στιγμιότυπου οθόνης" + "take-screenshot": "Λήψη στιγμιότυπου οθόνης", + "handle-provision-success-function": "Συνάρτηση χειρισμού επιτυχίας παροχής", + "get-location-function": "Συνάρτηση λήψης τοποθεσίας", + "process-launch-result-function": "Συνάρτηση επεξεργασίας αποτελέσματος εκκίνησης", + "get-phone-number-function": "Συνάρτηση λήψης αριθμού τηλεφώνου", + "process-image-function": "Συνάρτηση επεξεργασίας εικόνας", + "process-qr-code-function": "Συνάρτηση επεξεργασίας κωδικού QR", + "process-location-function": "Συνάρτηση επεξεργασίας τοποθεσίας", + "handle-empty-result-function": "Συνάρτηση χειρισμού κενού αποτελέσματος", + "handle-error-function": "Συνάρτηση χειρισμού σφάλματος", + "handle-non-mobile-fallback-function": "Συνάρτηση εναλλακτικής διαχείρισης για μη κινητά", + "save-to-gallery": "Αποθήκευση στη 'Συλλογή εικόνων'", + "provision-type": "Τύπος παροχής", + "auto": "Αυτόματο", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Συνάρτηση προσαρμοσμένης ενέργειας", "custom-pretty-function": "Συνάρτηση προσαρμοσμένης ενέργειας (με πρότυπο HTML)", @@ -6883,7 +7480,8 @@ "marker": "Δείκτης", "polygon": "Πολύγωνο", "rectangle": "Ορθογώνιο", - "circle": "Κύκλος" + "circle": "Κύκλος", + "polyline": "Πολυγραμμή" }, "place-map-item": "Τοποθέτηση στοιχείου χάρτη", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Συνέχεια σχεδίασης πολυγώνου", "finish-draw-polygon": "Ολοκλήρωση σχεδίασης πολυγώνου", "start-draw-circle": "Έναρξη σχεδίασης κύκλου", - "finish-draw-circle": "Ολοκλήρωση σχεδίασης κύκλου" + "finish-draw-circle": "Ολοκλήρωση σχεδίασης κύκλου", + "start-draw-polyline": "Έναρξη σχεδίασης πολυγραμμής", + "finish-draw-polyline": "Ολοκλήρωση σχεδίασης πολυγραμμής" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Καθυστέρηση κίνησης κατά την ενημέρωση" }, "chart-axis": { + "limit": "Όριο", + "source": "Πηγή", + "key-value": "Κλειδί / Τιμή", + "value-required": "Απαιτείται τιμή.", + "entity-key-required": "Απαιτείται κλειδί οντότητας.", + "key-required": "Απαιτείται κλειδί.", + "scale-limits": "Όρια κλίμακας", + "scale-appearance": "Εμφάνιση κλίμακας", "scale": "Κλίμακα", "scale-min": "ελάχ.", "scale-max": "μέγ.", @@ -8005,7 +8613,10 @@ "add-radio-option": "Προσθήκη επιλογής κουμπιού", "radio-label-position": "Θέση ετικέτας", "radio-label-position-before": "Πριν", - "radio-label-position-after": "Μετά" + "radio-label-position-after": "Μετά", + "save-image": "Αποθήκευση εικόνας", + "save-to-gallery": "Αυτόματη αποθήκευση των ληφθεισών εικόνων στη συλλογή εικόνων", + "public-image": "Κάνει την εικόνα διαθέσιμη σε οποιονδήποτε μη εξουσιοδοτημένο χρήστη" }, "invalid-qr-code-text": "Μη έγκυρο κείμενο εισόδου για τον QR κώδικα. Η είσοδος πρέπει να είναι τύπου string", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Διαδρομές", "markers": "Δείκτες", "polygons": "Πολύγωνα", - "circles": "Κύκλοι" + "circles": "Κύκλοι", + "polylines": "Πολυγραμμές" }, "data-layer": { "source": "Πηγή", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Κύκλος για '{{entityName}}': κάντε κλικ για ολοκλήρωση και αποθήκευση", "finish-circle-hint": "Κύκλος: κάντε κλικ για ολοκλήρωση σχεδίασης" }, + "polyline": { + "polyline-key": "Κλειδί πολυγραμμής", + "polyline-key-required": "Απαιτείται κλειδί πολυγραμμής", + "no-polylines": "Δεν έχουν ρυθμιστεί πολυγραμμές", + "add-polylines": "Προσθήκη πολυγραμμής", + "polyline-configuration": "Διαμόρφωση πολυγραμμής", + "remove-polyline": "Αφαίρεση πολυγραμμής", + "edit": "Επεξεργασία πολυγραμμής", + "cut": "Αποκοπή περιοχής πολυγραμμής", + "rotate": "Περιστροφή πολυγραμμής", + "remove-polyline-for": "Αφαίρεση πολυγραμμής για '{{entityName}}'", + "draw-polyline": "Σχεδίαση πολυγραμμής", + "polyline-place-first-point-hint-with-entity": "Πολυγραμμή για '{{entityName}}': κάντε κλικ για να τοποθετήσετε το πρώτο σημείο", + "polyline-place-first-point-hint": "Πολυγραμμή: κάντε κλικ για να τοποθετήσετε το πρώτο σημείο", + "finish-polyline-hint-with-entity": "Πολυγραμμή για '{{entityName}}': κάντε κλικ για να ολοκληρώσετε τη σχεδίαση", + "finish-polyline-hint": "Πολυγραμμή: κάντε κλικ για να ολοκληρώσετε τη σχεδίαση", + "polyline-place-first-point-cut-hint": "Κάντε κλικ για να τοποθετήσετε το πρώτο σημείο", + "finish-polyline-cut-hint": "Κάντε κλικ στον πρώτο δείκτη για να ολοκληρώσετε και να αποθηκεύσετε" + }, "select-entity": "Επιλογή οντότητας", "select-entity-hint": "Υπόδειξη: μετά την επιλογή, κάντε κλικ στον χάρτη για να ορίσετε θέση" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Εμφάνιση κενού χώρου αντί κουμπιού σε κρυφό κελί", "dont-reserve-space-hidden-action": "Χωρίς κράτηση χώρου για κρυφές ενέργειες", "display-timestamp": "Χρονική σήμανση", + "timestamp-column-name": "Χρονοσφραγίδα", "display-pagination": "Εμφάνιση σελιδοποίησης", "default-page-size": "Προεπιλεγμένο μέγεθος σελίδας", "page-step-settings": "Ρυθμίσεις βήματος σελίδας", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Πρέπει να καθοριστεί τουλάχιστον μία στήλη συναγερμών", "table-tabs": "Καρτέλες πίνακα", "show-cell-actions-menu-mobile": "Εμφάνιση μενού ενεργειών κελιού σε κινητά", - "disable-sorting": "Απενεργοποίηση ταξινόμησης" + "disable-sorting": "Απενεργοποίηση ταξινόμησης", + "sort-by": "Ταξινόμηση καρτελών κατά", + "sort-timestamp-option": "Χρόνος δημιουργίας" }, "latest-chart": { "total": "Σύνολο", @@ -9491,11 +10125,28 @@ "content": "

    Δημιουργώντας πίνακες ελέγχου τελικού χρήστη, ένας πελάτης μπορεί να δει μόνο τις δικές του συσκευές. Τα δεδομένα άλλων πελατών θα είναι κρυφά.

    Ακολουθήστε την τεκμηρίωση για το πώς να το κάνετε:

    " } } + }, + "api-usage": { + "api-usage": "Χρήση API", + "label": "Ετικέτα", + "state-name": "Όνομα κατάστασης", + "status": "Κατάσταση", + "status-required": "Απαιτείται κατάσταση.", + "limit": "Μέγιστο όριο", + "limit-required": "Απαιτείται μέγιστο όριο.", + "current-number": "Τρέχων αριθμός", + "current-number-required": "Απαιτείται τρέχων αριθμός.", + "add-key": "Προσθήκη κλειδιού", + "no-key": "Χωρίς κλειδί", + "delete-key": "Διαγραφή κλειδιού", + "target-dashboard-state": "Κατάσταση πίνακα ελέγχου στόχος", + "go-to-main-state": "Μετάβαση στην προεπιλεγμένη προβολή" } }, "icon": { "icon": "Εικονίδιο", "icons": "Εικονίδια", + "custom": "Προσαρμοσμένο", "select-icon": "Επιλέξτε εικονίδιο", "material-icons": "Material εικονίδια", "show-all": "Εμφάνιση όλων των εικονιδίων", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "από" }, "language": { + "auto": "Auto", "language": "Γλώσσα" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index ea11fff5a0..3c7863210c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -78,6 +78,7 @@ "show-more": "Show more", "dont-show-again": "Do not show again", "see-documentation": "See documentation", + "see-debug-events": "See debug events", "clear": "Clear", "upload": "Upload", "delete-anyway": "Delete anyway", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Two-factor authentication", "available-providers": "Available providers", + "available-providers-required": "At least one 2FA provider should be configured.", "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", @@ -496,24 +498,26 @@ "number-of-codes-pattern": "Number of codes must be a positive integer.", "number-of-codes-required": "Number of codes is required.", "provider": "Provider", - "retry-verification-code-period": "Retry verification code period (sec)", + "retry-verification-code-period": "Retry verification code period", "retry-verification-code-period-pattern": "Minimal period time is 5 sec", "retry-verification-code-period-required": "Retry verification code period is required.", - "total-allowed-time-for-verification": "Total allowed time for verification (sec)", + "total-allowed-time-for-verification": "Total allowed time for verification", "total-allowed-time-for-verification-pattern": "Minimal total allowed time is 60 sec", "total-allowed-time-for-verification-required": "Total allowed time is required.", "use-system-two-factor-auth-settings": "Use system two factor auth settings", "verification-code-check-rate-limit": "Verification code check rate limit", - "verification-code-lifetime": "Verification code lifetime (sec)", + "verification-code-lifetime": "Verification code lifetime", "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", "verification-code-lifetime-required": "Verification code lifetime is required.", "verification-message-template": "Verification message template", "verification-limitations": "Verification limitations", "verification-message-template-pattern": "Verification message need to contains pattern: ${code}", "verification-message-template-required": "Verification message template is required.", - "within-time": "Within time (sec)", + "within-time": "Within time", "within-time-pattern": "Time must be a positive integer.", - "within-time-required": "Time is required." + "within-time-required": "Time is required.", + "force-2fa": "Enforce two-factor authentication", + "enforce-for": "Enforce for" }, "jwt": { "security-settings": "JWT security settings", @@ -555,6 +559,7 @@ }, "alarm": { "alarm": "Alarm", + "alarm-list": "Alarm list", "alarms": "Alarms", "all-alarms": "All alarms", "select-alarm": "Select alarm", @@ -634,7 +639,7 @@ "delete-alarms-text": "Are you sure you want to delete { count, plural, =1 {1 alarm} other {# alarms} }?", "selected-alarms-are-cleared": "Selected alarms are already cleared", "alarm-status-filter": "Alarm Status Filter", - "alarm-filter-title": "Alarm Filter", + "alarm-filter-title": "Alarm filter", "assigned": "Assigned", "filter-title": "Filter", "max-count-load": "Maximum number of alarms to load (0 - unlimited)", @@ -655,7 +660,16 @@ "alarm-type": "Alarm type", "enter-alarm-type": "Enter alarm type", "no-alarm-types-matching": "No alarm types matching '{{entitySubtype}}' were found.", - "alarm-type-list-empty": "No alarm types selected." + "alarm-type-list-empty": "No alarm types selected.", + "system-comments": { + "acked-by-user": "Alarm was acknowledged by user {{userName}}", + "cleared-by-user": "Alarm was cleared by user {{userName}}", + "assigned-to-user": "Alarm was assigned by user {{userName}} to user {{assigneeName}}", + "unassigned-to-user": "Alarm was unassigned by user {{userName}}", + "unassigned-from-deleted-user": "Alarm was unassigned because user {{userName}} - was deleted", + "comment-deleted": "User {{userName}} deleted his comment", + "severity-changed": "Alarm severity was updated from {{oldSeverity}} to {{newSeverity}}" + } }, "alarm-activity": { "add": "Add a comment...", @@ -760,6 +774,7 @@ "name-max-length": "Name should be less than 256", "label-max-length": "Label should be less than 256", "description": "Description", + "description-required": "Description is required.", "type": "Type", "type-required": "Type is required.", "details": "Details", @@ -793,7 +808,7 @@ "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", + "asset-required": "Asset is required.", "name-starts-with": "Asset name expression", "help-text": "Use '%' according to need: '%asset_name_contains%', '%asset_name_ends', 'asset_starts_with%'.", "import": "Import assets", @@ -865,15 +880,18 @@ "api-features": "API features", "api-usage": "API usage", "alarm": "Alarm", - "alarms-created": "Alarms created", + "alarms-created": "Created alarms", "queue-stats": "Queue Stats", "processing-failures-and-timeouts": "Processing Failures and Timeouts", "exceptions": "Exceptions", - "alarms-created-daily-activity": "Alarms created daily activity", - "alarms-created-hourly-activity": "Alarms created hourly activity", - "alarms-created-monthly-activity": "Alarms created monthly activity", + "alarms-created-daily-activity": "Created alarms daily activity", + "alarms-created-hourly-activity": "Created alarms hourly activity", + "alarms-created-monthly-activity": "Created alarms monthly activity", "data-points": "Data points", "data-points-storage-days": "Data points storage days", + "data-points-storage-days-hourly-activity": "Data points storage days hourly activity", + "data-points-storage-days-daily-activity": "Data points storage days daily activity", + "data-points-storage-days-monthly-activity": "Data points storage days monthly activity", "device-api": "Device API", "email": "Email", "email-messages": "Email messages", @@ -899,14 +917,15 @@ "processing-timeouts": "${entityName} Processing Timeouts", "rule-chain": "Rule Chain", "rule-engine": "Rule Engine", - "rule-engine-daily-activity": "Rule Engine daily activity", "rule-engine-executions": "Rule Engine executions", "rule-engine-hourly-activity": "Rule Engine hourly activity", + "rule-engine-daily-activity": "Rule Engine daily activity", "rule-engine-monthly-activity": "Rule Engine monthly activity", "rule-engine-statistics": "Rule Engine Statistics", "rule-node": "Rule Node", "sms": "SMS", "sms-messages": "SMS messages", + "sms-messages-hourly-activity": "SMS messages hourly activity", "sms-messages-daily-activity": "SMS messages daily activity", "sms-messages-monthly-activity": "SMS messages monthly activity", "successful": "${entityName} Successful", @@ -916,13 +935,40 @@ "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", "transport": "Transport", + "transport-msg-hourly-activity": "Transport messages hourly activity", + "transport-msg-daily-activity": "Transport messages daily activity", + "transport-msg-monthly-activity": "Transport messages monthly activity", "transport-daily-activity": "Transport daily activity", "transport-data-points": "Transport data points", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "Transport messages", - "transport-monthly-activity": "Transport monthly activity", + "transport-data-points-hourly-activity": "Transport data points hourly activity", + "transport-data-points-daily-activity": "Transport data points daily activity", + "transport-data-points-monthly-activity": "Transport data points monthly activity", "view-details": "View details", - "view-statistics": "View statistics" + "view-statistics": "View statistics", + "transport-messages": "Transport messages", + "transport-messages-hourly-activity": "Transport messages hourly activity", + "transport-data-point-hourly-activity": "Transport data point hourly activity", + "javascript-function-executions": "JavaScript function executions", + "javascript-function-executions-hourly-activity": "JavaScript function executions hourly activity", + "javascript-function-executions-daily-activity": "JavaScript function executions daily activity", + "javascript-function-executions-monthly-activity": "JavaScript function executions monthly activity", + "tbel-function-executions": "TBEL function executions", + "tbel-function-executions-hourly-activity": "TBEL function executions hourly activity", + "tbel-function-executions-daily-activity": "TBEL function executions daily activity", + "tbel-function-executions-monthly-activity": "TBEL function executions monthly activity", + "created-reports": "Created reports", + "created-reports-hourly-activity": "Created reports hourly activity", + "created-reports-daily-activity": "Created reports daily activity", + "created-reports-monthly-activity": "Created reports monthly activity", + "emails": "Emails", + "emails-hourly-activity": "Emails hourly activity", + "emails-daily-activity": "Emails daily activity", + "emails-monthly-activity": "Emails monthly activity", + "status": { + "enabled": "Enabled", + "disabled": "Disabled", + "warning": "Warning" + } }, "api-limit": { "cassandra-write-queries-core": "Rest API Cassandra write queries", @@ -947,6 +993,40 @@ "edge-uplink-messages": "Edge uplink messages", "edge-uplink-messages-per-edge": "Edge uplink messages per edge" }, + "api-key": { + "api-key": "API key", + "api-keys": "API keys", + "delete-api-key-title": "Are you sure you want to delete the API key '{{name}}'?", + "delete-api-key-text": "Be careful, after the confirmation key will become unrecoverable.", + "delete-api-keys-title": "Are you sure you want to delete { count, plural, =1 {1 API key} other {# API keys} }?", + "delete-api-keys-text": "Be careful, after the confirmation all selected keys will become unrecoverable.", + "expiration-date": "Expiration date", + "date": "date", + "description": "Description", + "disable": "Disable", + "edit-description": "Edit description", + "enable": "Enable API key ", + "expiration-time": "Expiration time", + "expiration-time-never": "Never", + "expiration-time-custom": "Custom", + "generate": "Generate", + "generate-title": "Generate API key", + "generate-text": "Note: The API key inherits the permissions of the user it is created for.", + "generated-api-key-title": "API key generated. Let’s check connectivity!", + "generated-api-key-copy": "Make sure to copy and save your API key now as you will not be able to see it again.", + "generated-api-key-command": "Use the following instructions to check connectivity. As a result, you should receive the current user information:", + "generated-api-key-insecure-url": "Executing commands over an insecure HTTP connection will send your API key unencrypted, making it vulnerable to interception.", + "list": "{ count, plural, =1 {One API key} other {List of # API keys} }", + "manage": "Manage", + "manage-api-keys": "Manage API keys", + "no-found": "No API keys found", + "selected-api-keys": "{ count, plural, =1 {1 API key} other {# API keys} } selected", + "search": "Search API keys", + "status": "Status", + "status-active": "Active", + "status-inactive": "Inactive", + "status-expired": "Expired" + }, "audit-log": { "audit": "Audit", "audit-logs": "Audit logs", @@ -1000,10 +1080,14 @@ "type-provision-failure": "Device provisioning was failed", "type-timeseries-updated": "Telemetry updated", "type-timeseries-deleted": "Telemetry deleted", - "type-sms-sent": "SMS sent" + "type-sms-sent": "SMS sent", + "any-type":"Any type", + "audit-log-filter-title": "Audit log filter", + "filter-title": "Filter", + "filter-types": "Audit log types" }, "debug-settings": { - "label": "Debug Configuration", + "label": "Debug configuration", "on-failure": "Failures only (24/7)", "all-messages": "All messages ({{time}})", "failures": "Failures", @@ -1018,19 +1102,33 @@ "expression": "Expression", "no-found": "No calculated fields found", "list": "{ count, plural, =1 {One calculated field} other {List of # calculated fields} }", + "add": "Add calculated field", "selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected", "type": { "simple": "Simple", - "script": "Script" + "simple-hint": "Simple arithmetic calculation based on input arguments.", + "script": "Script", + "script-hint": "Calculation over defined arguments using a TBEL script.", + "geofencing" : "Geofencing", + "geofencing-hint" : "Evaluation of entity GPS position and transitions against configured geofencing zone groups.", + "propagation": "Propagation", + "propagation-hint": "Propagation of data to parent or child entities based on relation direction and type.", + "related-entities-aggregation": "Related entities aggregation", + "related-entities-aggregation-hint": "Aggregation of latest data from related entities.", + "time-series-data-aggregation": "Time series data aggregation", + "time-series-data-aggregation-hint": "Aggregation of historical data from a current entity." }, + "preview": "Preview", "arguments": "Arguments", "decimals-by-default": "Decimals by default", "debugging": "Calculated field debugging", + "calculated-field-details": "Calculated field details", "argument-name": "Argument name", + "name": "Name", "datasource": "Datasource", "add-argument": "Add argument", "test-script-function": "Test script function", - "no-arguments": "No arguments configured", + "no-arguments": "At least one argument is required.", "argument-settings": "Argument settings", "argument-current": "Current entity", "argument-current-tenant": "Current tenant", @@ -1038,8 +1136,9 @@ "argument-asset": "Asset", "argument-customer": "Customer", "argument-tenant": "Current tenant", + "argument-owner": "Current owner", + "argument-relation-query": "Related entities", "argument-type": "Argument type", - "see-debug-events": "See debug events", "attribute": "Attribute", "copy-argument-name": "Copy argument name", "timeseries-key": "Time series key", @@ -1052,12 +1151,14 @@ "shared-attributes": "Shared attributes", "attribute-key": "Attribute key", "default-value": "Default value", + "default-value-required": "Default value is required.", "limit": "Max values", "time-window": "Time window", "customer-name": "Customer name", "asset-name": "Asset name", "timeseries": "Time series", "output": "Output", + "output-hint": "Defines how output is processed.", "create": "Create new calculated field", "file": "Calculated field file", "invalid-file-error": "Invalid file format. Please make sure the file is a valid JSON file.", @@ -1071,9 +1172,175 @@ "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", "test-with-this-message": "Test with this message", "use-latest-timestamp": "Use latest timestamp", + "entity-coordinates": "Entity coordinates", + "latitude-time-series-key": "Latitude time series key", + "latitude-time-series-key-required": "Latitude time series key is required.", + "longitude-time-series-key": "Longitude time series key", + "longitude-time-series-key-required": "Longitude time series key is required.", + "geofencing-zone-groups": "Geofencing zone groups", + "geofencing-zone-groups-settings": "Geofencing zone group settings", + "target-zone": "Target zone", + "perimeter-key": "Perimeter key", + "report-strategy": "Report strategy", + "no-zone-configured": "At least one zone is required.", + "no-zone-configured-required": "At least one zone group must be configured.", + "add-zone-group": "Add zone group", + "report-transition-event-only": "Transition events only", + "report-presence-status-only": "Presence status only", + "report-transition-event-and-presence": "Presence status and transition events", + "perimeter-attribute-key": "Perimeter attribute key", + "perimeter-attribute-key-required": "Perimeter attribute key is required.", + "perimeter-attribute-key-pattern": "Perimeter attribute key is invalid.", + "entity-zone-relationship": "Path from Entity to Zones", + "direction": "Relation direction", + "direction-from": "From entity to zone", + "direction-to": "From zone to entity", + "relation-type": "Relation type", + "create-relation-with-matched-zones": "Create relations for source entity with matched zones", + "relation-level": "Relation level", + "fetch-last-available-level": "Fetch last available level only", + "zone-group-refresh-interval": "Zone groups refresh interval", + "copy-zone-group-name": "Copy zone group name", + "open-details-page": "Open entity details page", + "level": "Level", + "direction-level": "Direction", + "direction-up": "Up", + "direction-up-parent": "Up to parent", + "direction-down": "Down", + "direction-down-child": "Down to child", + "add-level": "Add level", + "delete-level": "Delete level", + "no-level": "No level configured", + "levels-required": "At least one level must be configured.", + "max-allowed-levels-error": "Relation level exceeds the maximum allowed.", + "propagation-path-related-entities": "Propagation path to related entities", + "propagate-type": { + "arguments-only": "Arguments only", + "expression-result": "Calculation result" + }, + "script": "Script", + "data-propagate": "Data to propagate", + "output-key": "Output key", + "copy-output-key": "Copy output key", + "aggregation-path-related-entities": "Aggregation path to related entities", + "deduplication-interval": "Deduplication interval", + "deduplication-interval-min": "Deduplication interval should be at least {{ sec }} seconds.", + "deduplication-interval-hint": "Minimum time between telemetry aggregations.", + "deduplication-interval-required": "Deduplication interval is required.", + "calculated-field-filter-title": "Calculated field filter", + "filter-title": "Filter", + "calculated-field-types": "Calculated field types", + "events": "Events", + "any-type": "Any type", + "metrics": { + "metrics": "Metrics", + "metrics-empty": "At least one metric must be configured.", + "metric-name": "Metric name", + "metric-name-required": "Metric name is required.", + "metric-name-pattern": "Metric name is invalid.", + "metric-name-duplicate": "Metric with such name already exists.", + "metric-name-max-length": "Metric name should be less than 256 characters.", + "metric-name-forbidden": "Metric name is reserved and cannot be used.", + "copy-metric-name": "Copy metric name", + "argument-name": "Argument name", + "aggregation": "Aggregation", + "aggregation-type": { + "avg": "Average", + "min": "Minimum", + "max": "Maximum", + "sum": "Sum", + "count": "Count", + "count-unique": "Count unique" + }, + "filtered": "Filtered", + "value-source": "Value source", + "value-source-hint": "Defines how the value for aggregation is obtained.", + "value-source-type": { + "key": "Key", + "function": "Function" + }, + "no-metrics-configured": "At least one metric is required.", + "add-metric": "Add metric", + "max-metrics": "Maximum number of metrics reached.", + "metric-settings": "Metric settings", + "filter": "Filter", + "filter-hint": "Enables filtering of entities during aggregation. The filter function must return a boolean value and can use all configured arguments." + }, + "output-strategy": { + "strategy": "Strategy", + "process-right-away": "Process right away", + "process-rule-chains": "Process via Rule Chains", + "save-time-series": "Save to time series", + "save-database": "Save to database", + "save-latest-values": "Save to latest values", + "send-web-sockets": "Send to WebSockets", + "save-calculated-fields": "Send to Calculated fields", + "update-attribute-only-on-value-change": "Update attribute only on value change", + "send-attributes-updated-notification": "Send attributes updated notification", + "ttl": "Custom TTL", + "ttl-required": "TTL is required", + "ttl-min": "Only 0 minimum TTL is allowed", + "processing-parameters": "Processing parameters", + "hint": { + "strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing.", + "processing-options": "Processing options", + "update-attribute-only-on-value-change": "Updates attribute on every incoming message, regardless of whether the value has changed. This increases API usage and reduces performance.", + "update-attribute-only-on-value-change-enabled": "Updates attribute only when the value changes. If the value is unchanged, timestamps are not updated and notifications are not sent.", + "send-attributes-updated-notification": "Sends an Attributes Updated event to the default rule chain.", + "save-time-series": "Saves time series data to the ts_kv table in the database.", + "save-database": "Saves attribute data to the database.", + "save-latest-values": "Updates time series data in the ts_kv_latest table in the database if the new value is more recent.", + "send-web-sockets-attribute": "Notifies WebSocket subscriptions about updates to the attribute data.", + "send-web-sockets-time-series": "Notifies WebSocket subscriptions about updates to the time series data.", + "save-calculated-fields-attribute": "Notifies calculated fields about updates to the attribute data.", + "save-calculated-fields-time-series": "Notifies calculated fields about updates to the time series data.", + "ttl": "Defines the retention period for time series data. If disabled, the Tenant Profile TTL is used." + } + }, + "aggregate-interval-type": "Aggregate interval type", + "aggregate-interval-value": "Aggregate interval value", + "aggregate-interval-value-required": "Aggregate interval value is required.", + "aggregate-interval-value-min": "Aggregate interval value should be at least { sec, plural, =0 {0 second} =1 {1 second} other {# seconds} }.", + "aggregate-interval-value-step-multiple-of": "Aggregate interval value must be a divisor or multiple of 1 day.", + "aggregate-period": { + "hour": "Hour", + "day": "Day", + "week": "Week (Mon - Sun)", + "week-sun-sat": "Week (Sun - Sat)", + "month": "Month", + "quarter": "Quarter", + "year": "Year", + "custom": "Custom" + }, + "aggregate-period-hint-offset": "Your aggregation interval will be: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Your aggregation interval will be: {{ interval }} and so on.", + "entity-aggregation": { + "argument-hint": "Data will be fetched from current entity.", + "argument-title-hint": "Defines the input arguments used for aggregation.", + "argument-setting-hint": "Latest telemetry is the only available argument type for this calculated field.", + "aggregation-interval": "Aggregation interval", + "aggregation-interval-hint": "Defines how often aggregation is performed. Example: every 1 hour aggregates data at 00:00, 01:00, 02:00, etc. Aggregation results are stored with the timestamp corresponding to the start of the aggregation interval.", + "apply-offset": "Apply offset to aggregation interval", + "apply-offset-hint": "Defines how much to shift the start of each aggregation period (e.g., +10 minutes - 00:10, 01:10).", + "offset-value": "Offset value", + "offset-value-required": "Offset value is required.", + "offset-value-min": "Offset value must be a positive integer.", + "offset-value-max": "Offset value should be less than the aggregate interval value.", + "wait-delay": "Apply await timeout for delayed telemetry", + "wait-delay-hint": "Defines how long to wait for delayed telemetry after the interval ends. If such telemetry arrives, the result for that interval will be recalculated.", + "duration": "Duration", + "duration-required": "Duration is required.", + "duration-min": "Duration should be at least 1 minute.", + "duration-hint": "How long to wait for delayed data after the interval ends.", + "produce-intermediate-result": "Produce intermediate result", + "produce-intermediate-result-hint": "Calculates metrics during the current interval to produce an intermediate result. Updates occur no more often than once every {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", - "arguments-empty": "Arguments should not be empty.", + "arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.", + "arguments-propagate-argument-entity-type": "Entity type is incompatible with 'Arguments only' propagation.", + "arguments-propagate-argument-must-current-entity": "At least one argument must be configured with the 'Current entity' source entity type.", + "arguments-empty": "At least one argument should be specified.", "expression-required": "Expression is required.", "expression-invalid": "Expression is invalid", "expression-max-length": "Expression length should be less than 255 characters.", @@ -1082,12 +1349,218 @@ "argument-name-duplicate": "Argument with such name already exists.", "argument-name-max-length": "Argument name should be less than 256 characters.", "argument-name-forbidden": "Argument name is reserved and cannot be used.", + "output-key-required": "Output key is required.", + "output-key-pattern": "Output key is invalid.", + "output-key-duplicate": "Key with such name already exists.", + "output-key-max-length": "Output key should be less than 256 characters.", + "output-key-forbidden": "Output key is reserved and cannot be used.", + "entity-type-required": "Entity type is required", + "name-required": "Name is required.", + "name-pattern": "Name is invalid.", + "name-duplicate": "Name with such name already exists.", + "name-max-length": "Name should be less than 256 characters.", + "name-forbidden": "Name is reserved and cannot be used.", "argument-type-required": "Argument type is required.", "max-args": "Maximum number of arguments reached.", "decimals-range": "Decimals by default should be a number between 0 and 15.", "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius.", "arguments-entity-not-found": "Argument target entity not found.", - "use-latest-timestamp": "If enabled, the calculated value will be persisted using the most recent timestamp from the arguments telemetry, instead of the server time." + "use-latest-timestamp": "If enabled, the calculated value will be persisted using the most recent timestamp from the arguments telemetry, instead of the server time.", + "entity-coordinates": "Specify the time series keys that provide entity GPS coordinates (latitude and longitude).", + "geofencing-zone-groups": "Define one or more geofencing zones groups to check (e.g. 'allowedZones', 'restrictedZones'). Each group must have a unique name, which is used as a prefix for calculated field output telemetry keys.", + "perimeter-attribute-key": "Set the attribute key that contains the geofencing zone perimeter definition. The perimeter is always taken from server-side attributes of the zone entity.", + "report-strategy": "Presence status reports whether the entity is currently INSIDE or OUTSIDE the zone group. Transition events report when the entity ENTERED or LEFT the zone group.", + "create-relation-with-matched-zones": "Automatically create and maintain relations between the entity and the zones it is currently inside. Relations are removed when the entity leaves a zone and created when it enters a new one.", + "relation-type-required": "Relation type is required.", + "relation-level-required": "Relation level is required.", + "relation-level-min": "Minimum relation level value is 1.", + "relation-level-max": "Maximum relation level value is {{max}}.", + "geofencing-empty": "At least one zone group must be configured.", + "geofencing-entity-not-found": "Geofencing target entity not found.", + "max-geofencing-zone": "Maximum number of geofencing zones reached.", + "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.", + "zone-group-refresh-interval-required": "Zone groups refresh interval is required.", + "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} seconds.", + "propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type. Only relations between device, asset, customer, and tenant entities are supported. Maximum entities resolved by relation path is {{ max }}.", + "data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.", + "aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities, based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported. Maximum entities resolved by relation path is {{ max }}.", + "arguments-aggregation": "Defines the input arguments used for filtering and aggregation.", + "setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.", + "metrics": "Defines metrics aggregated based on configured arguments.", + "entity-aggregation-metrics": "Defines metrics aggregated based on configured arguments over the specified time intervals.", + "import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field structure.", + "simple-expression-title": "Arithmetic expression that defines how the calculated value is computed.", + "script-title": "TBEL script that defines the calculation logic and output values.", + "simple-arguments": "Arithmetic expression that defines how the calculated value is computed.", + "script-arguments": "Defines the input arguments available to the script." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Alarm rules", + "alarm-rule": "Alarm rule", + "alarm-rules": "Alarm rules", + "alarm-rules-old": "Old", + "alarm-rules-actual": "Actual", + "severities": "Severities", + "cleared": "Clear condition", + "delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?", + "delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.", + "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?", + "delete-multiple-text": "Be careful, after the confirmation all selected alarm rules will be removed and all related data will become unrecoverable.", + "create": "Create new alarm rule", + "add": "Add alarm rule", + "copy": "Copy alarm rule configuration", + "details": "Alarm rule details", + "no-found": "No alarm rules found", + "list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }", + "selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected", + "import": "Import alarm rule", + "file": "Alarm rule file", + "export": "Export alarm rule", + "export-failed-error": "Unable to export alarm rule: {{error}}", + "entity-type": "Entity type", + "entity-type-required": "Entity type is required.", + "alarm-type": "Alarm type", + "alarm-type-hint": "Unique identifier (e.g., HighTempAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.", + "alarm-type-required": "Alarm type is required.", + "alarm-type-pattern": "Alarm type is invalid.", + "alarm-type-max-length": "Alarm type should be less than 256 characters.", + "clear-alarm": "Clear alarm", + "value-argument": "Argument", + "value-argument-required": "Argument is required.", + "static-settings": "Static settings", + "configuration": "Configuration", + "static-schedule": "Static", + "dynamic-schedule": "Dynamic", + "operation-and": "AND", + "operation-or": "OR", + "condition-during": "During {{during}}", + "condition-during-dynamic": "During \"{{ attribute }}\"", + "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", + "condition-repeat-times-dynamic": "Repeats \"{{ attribute }}\" times", + "filter-preview": "Filter preview", + "condition-settings": "Condition settings", + "static": "Static", + "dynamic": "Dynamic", + "argument-filters": "Argument filters", + "argument-name": "Argument name", + "value-type": "Value type", + "general": "General", + "filters": "Filters", + "date-time-hint": "The argument must be in epoch milliseconds. Example: 1698839340000 equals 2023-11-01 12:49:00 UTC.", + "operation": "Operation", + "value-source": "Value source", + "value": "Value", + "ignore-case": "Ignore case", + "condition": "Condition", + "script": "Script", + "add-filter": "Add argument filter", + "edit-filter": "Argument filter", + "remove-filter": "Remove argument filter", + "no-filter": "At least one filter is required.", + "conditions": { + "simple": "Simple", + "duration": "Duration", + "repeating": "Repeating" + }, + "schedule-title": "Schedule", + "edit-schedule": "Edit alarm schedule", + "schedule-type": "Scheduler type", + "schedule-type-required": "Scheduler type is required.", + "schedule": { + "any-time": "Active all the time", + "specific-time": "Active at a specific time", + "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.", + "tbel": "TBEL", + "expression-type": { + "simple": "Simple", + "script": "Script" + }, + "operation-type": { + "and": "And", + "or": "Or" + }, + "filter-predicate-type": { + "string": "String", + "numeric": "Numeric", + "boolean": "Boolean", + "complex": "Complex" + }, + "alarm-rule-additional-info": "Additional info", + "edit-alarm-rule-additional-info": "Edit additional info", + "alarm-rule-additional-info-placeholder": "Please provide your comments and adjustments here to display them within Alarm details under Additional info", + "alarm-rule-additional-info-hint": "Hint: use ${Argument name} to substitute values of the arguments that are used in alarm rule condition.", + "alarm-rule-additional-info-icon-hint": "Use Argument name to substitute values of the arguments that are used in alarm rule condition.", + "alarm-rule-mobile-dashboard": "Mobile dashboard", + "alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard.", + "alarm-rule-no-mobile-dashboard": "No dashboard selected", + "alarm-rule-condition": "Alarm rule condition", + "enter-alarm-rule-condition-prompt": "Add condition", + "enter-alarm-rule-clear-condition-prompt": "Add clearing condition", + "edit-alarm-rule-condition": "Alarm condition", + "condition-type": "Condition type", + "condition-type-hint": "\"Duration\" and \"Repeating\" options are not available when the \"Missing for\" operation is used in the filter.", + "select-alarm-severity": "Select alarm severity", + "add-create-alarm-rule-prompt": "At least one trigger condition is required.", + "add-create-alarm-rule": "Add trigger condition", + "add-clear-alarm-rule": "Add clear condition", + "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.", + "condition-repeating-value": "Count of events", + "condition-repeating-value-hint": "Update of any alarm rule argument will be counted as event", + "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.", + "create-conditions": "Trigger conditions", + "clear-condition": "Clear condition", + "no-clear-alarm-rule": "No clear condition configured.", + "advanced-settings": "Advanced settings", + "propagate-alarm": "Propagate alarm to related entities", + "alarm-rule-relation-types-list": "Relation types", + "alarm-rule-relation-types-list-hint": "Defines relation types to filter the related entities. If not set, the alarm will be propagated to all related entities.", + "propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)", + "propagate-alarm-to-tenant": "Propagate alarm to Tenant", + "alarm-rule-filter-title": "Alarm rule filter", + "filter-title": "Filter", + "debugging": "Alarm rule debugging", + "any-type": "Any type", + "enter-alarm-rule-type": "Enter alarm type", + "no-alarm-rule-types-matching": "No alarm types matching '{{entitySubtype}}' were found.", + "alarm-rule-type-list-empty": "No alarm types selected.", + "alarm-rule-type-list": "Alarm type list", + "alarm-rule-entity-list": "Entity list", + "missing-for": "missing for", + "time-unit": "Unit", + "mode": "Mode", + "type": "Type", + "value-required": "Value is required.", + "min-value": "Value must be 1 or greater.", + "argument-in-use": "Argument is used as general argument.", + "import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule structure.", + "no-filter-preview": "No filter specified", + "filter-operation": { + "and": "And", + "or": "Or" } }, "ai-models": { @@ -1231,6 +1704,8 @@ "documentation": "Documentation", "time-left": "{{time}} left", "output": "Output", + "sort-asc": "Ascending", + "sort-desc": "Descending", "suffix": { "s": "s", "ms": "ms" @@ -1300,7 +1775,7 @@ "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", + "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", @@ -1367,6 +1842,8 @@ "mobile-order": "Dashboard order in mobile application", "mobile-hide": "Hide dashboard in mobile application", "update-image": "Update dashboard image", + "update-new-version": "Upload new version", + "upload-file-to-update": "Upload file to update", "take-screenshot": "Take screenshot", "select-widget-title": "Select widget", "select-widget-value": "{{title}}: select widget", @@ -1737,6 +2214,8 @@ "bootstrap-tab": "Bootstrap Client", "bootstrap-server": "Bootstrap Server", "lwm2m-server": "LwM2M Server", + "client-reboot": "Registration Update Trigger", + "bootstrap-reboot": "Bootstrap-Request Trigger", "client-publicKey-or-id": "Client Public Key or Id", "client-publicKey-or-id-required": "Client Public Key or Id is required.", "client-publicKey-or-id-tooltip-psk": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", @@ -1784,8 +2263,7 @@ "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", "overwrite-activity-time": "Overwrite activity time for connected device", - "device-filter": "Device filter", - "device-filter-title": "Device Filter", + "device-filter-title": "Device filter", "filter-title": "Filter", "device-state": "Device state", "state": "State", @@ -1924,7 +2402,7 @@ "search": "Search asset profiles", "selected-asset-profiles": "{ count, plural, =1 {1 asset profile} other {# asset profiles} } selected", "no-asset-profiles-matching": "No asset profile matching '{{entity}}' were found.", - "asset-profile-required": "Asset profile is required", + "asset-profile-required": "Asset profile is required.", "idCopiedMessage": "Asset profile Id has been copied to clipboard", "set-default": "Make asset profile default", "delete": "Delete asset profile", @@ -1969,7 +2447,7 @@ "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", + "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", @@ -2238,6 +2716,7 @@ "short-id-required": "Short server ID is required.", "short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.", "short-id-pattern": "Short server ID must be a positive integer.", + "short-id-pattern-bs": "Short server ID must be only null", "lifetime": "Client registration lifetime", "lifetime-required": "Client registration lifetime is required.", "lifetime-pattern": "Client registration lifetime must be a positive integer.", @@ -2332,7 +2811,9 @@ "composite-all-description": "All resources are observed with a single Composite Observe request (more efficient, less flexible)", "composite-by-object": "Composite by objects", "composite-by-object-description": "Resources are grouped by object type and observed using separate Composite Observe requests (balanced approach)" - } + }, + "init-attr-tel-as-obs-strategy": "Initialize attributes and telemetry using Observe strategy", + "init-attr-tel-as-obs-strategy-hint": "If false - attributes and telemetry are initialized by reading their values one by one.\\nIf true - attributes and telemetry are initialized by subscribing to their values using the Observe strategy." }, "snmp": { "add-communication-config": "Add communication config", @@ -2648,6 +3129,8 @@ "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-api-key": "API key", + "type-api-keys": "API keys", "type-current-customer": "Current Customer", "type-current-tenant": "Current Tenant", "type-current-user": "Current User", @@ -2669,6 +3152,7 @@ "details": "Entity details", "no-entities-prompt": "No entities found", "no-data": "No data to display", + "show-all-columns":"Show All", "columns-to-display": "Columns to Display", "type-api-usage-state": "API Usage State", "type-edge": "Edge", @@ -2714,7 +3198,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }", "type-mobile-app-bundle": "Mobile bundle", "type-mobile-app-bundles": "Mobile bundles", - "list-of-mobile-app-bundles": "{ count, plural, =1 {One mobile bundle} other {List of # mobile bundles} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {One mobile bundle} other {List of # mobile bundles} }", + "limit-reached": "Limit reached", + "limit-reached-text": "You have reached the limit of {{ entities }}. To add more, please ask your System Administrator to increase your {{ entity }} limit.", + "request-limit-increase": "Request limit increase", + "request-sysadmin-text": "Are you the System Administrator?", + "login-here": "Login here", + "to-increase-limit": "to increase limit.", + "increase-limit-request-sent-title": "We have sent an automated request to your System Administrator to increase the limit", + "increase-limit-request-sent-text": "Please allow some time for them to review the request and update the settings. You may need to refresh this page to see the changes." }, "entity-field": { "created-time": "Created time", @@ -3118,6 +3610,7 @@ "order-priority": "Display order", "key-filter": "Key filter", "key-filters": "Key filters", + "or-conditions-disabled": "OR conditions between key filters are disabled by the system administrator.", "key-name": "Key name", "key-name-required": "Key name is required.", "key-type": { @@ -3786,24 +4279,25 @@ }, "login": { "login": "Login", - "request-password-reset": "Request Password Reset", - "reset-password": "Reset Password", - "create-password": "Create Password", + "request-password-reset": "Request password reset", + "reset-password": "Reset password", + "create-password": "Create password", "two-factor-authentication": "Two factor authentication", "passwords-mismatch-error": "Entered passwords must be same!", "password-again": "Password again", - "sign-in": "Please sign in", + "sign-in": "Sign in", "username": "Username (email)", "remember-me": "Remember me", - "forgot-password": "Forgot Password?", + "forgot-password": "Forgot your password?", "password-reset": "Password reset", - "expired-password-reset-message": "Your credentials has been expired! Please create new password.", + "expired-password-reset-message": "Your password has expired! \nPlease enter a new password.", "new-password": "New password", "new-password-again": "Confirm new password", "password-link-sent-message": "Reset link has been sent", "email": "Email", "invalid-email-format": "Invalid email format.", - "login-with": "Login with {{name}}", + "sign-in-with": "Sign in with {{name}}", + "sign-in-to-your-account": "Sign in to your account", "or": "or", "error": "Login error", "verify-your-identity": "Verify your identity", @@ -3822,7 +4316,51 @@ "activation-link-expired": "Activation link has expired", "activation-link-expired-message": "The link to activate your profile has expired. You can return to the login page to receive a new email.", "reset-password-link-expired": "Password reset link has expired", - "reset-password-link-expired-message": "The link to reset your password has expired. You can return to the login page to receive a new email." + "reset-password-link-expired-message": "The link to reset your password has expired. You can return to the login page to receive a new email.", + "two-fa": "Two-factor authentication", + "two-fa-required": "Two-factor authentication is required", + "set-up-verification-method": "Set up a verification method to continue", + "set-up-verification-method-login": "Set up a verification method or login", + "enable-authenticator-app": "Enable authenticator app", + "enable-authenticator-app-description": "Please enter the security code from your authenticator app", + "enable-authenticator-sms": "Enable SMS authenticator", + "enable-authenticator-sms-description": "Enter a 6-digit code we just sent to ", + "enable-authenticator-email": "Enable email authenticator", + "enable-authenticator-email-description": "A security code has been sent to your email address at ", + "enter-key-manually": "or enter this 32-digits key manually:", + "continue": "Continue", + "confirm": "Confirm", + "authenticator-app-success": "Authenticator app successfully enabled", + "authenticator-app-success-description": "The next time you log in, you will need to provide a two-factor authentication code", + "authenticator-sms-success": "SMS authenticator successfully enabled", + "authenticator-sms-success-description": "The next time you log in, you will be prompted to enter the security code that will be sent to the phone number", + "authenticator-email-success": "Email authenticator successfully enabled", + "authenticator-email-success-description": "The next time you log in, you will be prompted to enter the security code that will be sent to your email address", + "authenticator-backup-code-success": "Backup code successfully enabled", + "authenticator-backup-code-success-description": "The next time you log in, you will be prompted to enter the security code or use one of backup code.", + "add-verification-method": "Add verification method", + "get-backup-code": "Get backup code", + "copy-key": "Copy key", + "send-code": "Send code", + "email-label": "Email", + "email-description": "Enter an email to use as your authenticator.", + "sms-description": "Enter a phone number to use as your authenticator.", + "backup-code-description": "Print out the codes so you have them handy when you need to use them to log in to your account. You can use each backup code once.", + "backup-code-warn": "Once you leave this page, these codes cannot be shown again. Store them safely using the options below.", + "download-txt": "Download (txt)", + "print": "Print", + "verification-code": "6-digit code", + "verification-code-invalid": "Invalid verification code format", + "verification-code-incorrect": "Verification code is incorrect", + "verification-code-many-request": "Too many requests to check verification code", + "scan-qr-code": "Scan this QR code with your verification app", + "phone-input": { + "phone-input-label": "Phone number", + "phone-input-required": "Phone number is required", + "phone-input-validation": "Phone number is invalid or not possible", + "phone-input-pattern": "Invalid phone number. Should be in E.164 format, ex. {{phoneNumber}}", + "phone-input-hint": "Phone Number in E.164 format, ex. {{phoneNumber}}" + } }, "markdown": { "edit": "Edit", @@ -4196,6 +4734,7 @@ "api-usage-limit": "API usage limit", "device-activity": "Device activity", "entities-limit": "Entities limit", + "entities-limit-increase-request": "Entities limit increase request", "entity-action": "Entity action", "general": "General", "rule-engine-lifecycle-event": "Rule engine lifecycle event", @@ -4390,7 +4929,7 @@ "verification-code": "6-digit code", "verification-code-invalid": "Invalid verification code format", "verification-code-incorrect": "Verification code is incorrect", - "verification-code-many-request": "Too many requests check verification code", + "verification-code-many-request": "Too many requests to check verification code", "verification-step-description": "Enter a 6-digit code we just sent to {{address}}", "verification-step-label": "Verification" }, @@ -4413,6 +4952,12 @@ "at-least": "At least:", "character": "{ count, plural, =1 {1 character} other {# characters} }", "digit": "{ count, plural, =1 {1 digit} other {# digits} }", + "password-tooltip-min-length": "At least {{minimumLength}} characters long", + "password-tooltip-max-length": "At most {{maximumLength}} characters long", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} uppercase character", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} lowercase character", + "password-tooltip-digit": "{{minimumDigits}} number", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} special character", "incorrect-password-try-again": "Incorrect password. Try again", "lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }", "new-passwords-not-match": "New password didn't match", @@ -4471,7 +5016,8 @@ "additional-info": "Additional info (JSON)", "invalid-additional-info": "Unable to parse additional info json.", "no-relations-text": "No relations found", - "not": "Not" + "not": "Not", + "copy-type": "Copy type" }, "resource": { "add": "Add resource", @@ -4620,7 +5166,17 @@ "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", - "unassign-rulechains": "Unassign rulechains" + "unassign-rulechains": "Unassign rulechains", + "add-note": "Add note", + "paste-note": "Paste note", + "note": "Note", + "edit-note": "Edit note", + "note-content": "Markdown/HTML content", + "note-details": "Note details", + "note-background-color": "Background color", + "note-border": "Border", + "note-apply-default-markdown-style": "Apply default markdown style", + "note-custom-css": "Note content CSS" }, "rulenode": { "rule-node-events": "Rule node events", @@ -4965,10 +5521,11 @@ "endpoint-url-pattern": "Endpoint URL pattern", "endpoint-url-pattern-required": "Endpoint URL pattern is required", "request-method": "Request method", - "use-simple-client-http-factory": "Use simple client HTTP factory", "ignore-request-body": "Without request body", + "request-body-template": "Request body template", + "request-body-template-hint": "Use ${metadataKey} for metadata, $[messageKey] for message values. If empty, the message payload is used as the request body. For non-JSON payloads, enable 'Parse to plain text'.", "parse-to-plain-text": "Parse to plain text", - "parse-to-plain-text-hint": "If selected, request body message payload will be transformed from JSON string to plain text, e.g. msg = \"Hello,\\t\"world\"\" will be parsed to Hello, \"world\"", + "parse-to-plain-text-hint": "If selected, the request body (message payload or body template result) will be sent as a plain text string instead of being parsed as JSON, e.g. msg = \"Hello,\\t\"world\"\" will be parsed to Hello, \"world\"", "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", @@ -4990,6 +5547,7 @@ "bootstrap-servers": "Bootstrap servers", "bootstrap-servers-required": "Bootstrap servers value is required", "other-properties": "Other properties", + "other-properties-empty": "No other properties configured", "key": "Key", "key-required": "Key is required", "retries": "Automatically retry times if fails", @@ -5027,6 +5585,7 @@ "handshake-timeout-ms": "Handshake timeout (ms)", "min-handshake-timeout-ms-message": "Only 0 ms minimum value is allowed.", "client-properties": "Client properties", + "client-properties-empty": "No client properties configured", "queue-url-pattern": "Queue URL pattern", "queue-url-pattern-required": "Queue URL pattern is required", "delay-seconds": "Delay (seconds)", @@ -5044,6 +5603,7 @@ "pubsub-topic-name": "Topic name", "pubsub-topic-name-required": "Topic name is required", "message-attributes": "Message attributes", + "message-attributes-empty": "No message attributes configured", "message-attributes-hint": "Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields", "connect-timeout": "Connection timeout (sec)", "connect-timeout-required": "Connection timeout is required.", @@ -5121,7 +5681,6 @@ "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", "numbers-to-template": "Phone Numbers To Template", "numbers-to-template-required": "Phone Numbers To Template is required", "numbers-to-template-hint": "Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body", @@ -5405,6 +5964,17 @@ "tell-failure-aws-lambda-hint": "Rule node forces failure of message processing if AWS Lambda function execution raises exception.", "basic-mode": "Basic", "advanced-mode": "Advanced", + "url-hint": "Enter the request URL. Ensure special characters are percent-encoded.", + "query-params": "Query parameters", + "query-params-hint": "Additional query parameters to include in the request URL. Both names and values support templatization and are strictly (RFC 3986) encoded after template resolution.", + "query-params-empty": "No query parameters configured", + "query-params-name": "Name", + "query-params-value": "Value", + "query-params-name-required": "Name is required", + "query-params-value-required": "Value is required", + "headers-empty": "No headers configured", + "request-url": "Request URL", + "url": "URL", "save-time-series": { "processing-settings": "Processing settings", "processing-settings-hint": "Define how incoming messages are processed. Basic processing settings allow you to select preconfigured strategies, while Advanced settings allow you to select individual processing strategies for each action.", @@ -5422,7 +5992,7 @@ "time-series": "Time series", "latest": "Latest values", "web-sockets": "WebSockets", - "calculated-fields": "Calculated fields" + "calculated-fields-and-alarm-rules": "Calculated fields and alarm rules" }, "save-attribute": { "processing-settings": "Processing settings", @@ -5617,7 +6187,8 @@ "bad-request-params": "Bad request params", "item-not-found": "Item not found", "too-many-requests": "Too many requests", - "too-many-updates": "Too many updates" + "too-many-updates": "Too many updates", + "entities-limit-exceeded": "Entities limit exceeded" }, "tenant": { "tenant": "Tenant", @@ -5755,6 +6326,27 @@ "max-arguments-per-cf": "Arguments per calculated field max number", "max-arguments-per-cf-range": "Arguments per calculated field max number can't be negative", "max-arguments-per-cf-required": "Arguments per calculated field max number is required", + "max-related-level-per-argument": "Maximum relation level per 'Related entities' argument", + "max-related-level-per-argument-range": "Relation level per 'Related entities' argument max number can't be less than '1'", + "max-related-level-per-argument-required": "Relation level per 'Related entities' argument max number is required", + "min-allowed-scheduled-update-interval": "Min allowed update interval for 'Related entities' arguments (seconds)", + "min-allowed-scheduled-update-interval-range": "Min allowed update interval min number can't be negative", + "min-allowed-deduplication-interval": "Min allowed deduplication interval (seconds)", + "min-allowed-deduplication-interval-range": "Min allowed deduplication interval value can't be negative", + "min-allowed-deduplication-interval-required": "Min allowed deduplication interval is required", + "intermediate-aggregation-interval": "Intermediate aggregation interval (seconds)", + "intermediate-aggregation-interval-range": "Intermediate aggregation interval value can't be less than '1'", + "intermediate-aggregation-interval-required": "Intermediate aggregation interval is required", + "reevaluation-check-interval": "Reevaluation check interval (seconds)", + "reevaluation-check-interval-range": "Reevaluation check interval value can't be less than '1'", + "reevaluation-check-interval-required": "Reevaluation check interval is required", + "alarms-reevaluation-interval": "Alarms reevaluation interval (seconds)", + "alarms-reevaluation-interval-range": "Alarms reevaluation interval value can't be less than '1'", + "alarms-reevaluation-interval-required": "Alarms reevaluation interval is required", + "min-allowed-aggregation-interval": "Min allowed aggregation interval (seconds)", + "min-allowed-aggregation-interval-range": "Min allowed aggregation interval value can't be negative", + "min-allowed-aggregation-interval-required": "Min allowed aggregation interval is required", + "min-allowed-scheduled-update-interval-required": "Min allowed update interval min number is required", "max-state-size": "State maximum size in KB", "max-state-size-range": "State maximum size in KB can't be negative", "max-state-size-required": "State maximum size in KB is required", @@ -5830,6 +6422,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number", "ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number", "ws-limit-updates-per-session": "WS updates per session", + "relation-search-entity-limit": "Relation search entity limit", + "relation-search-entity-limit-hint": "Limits the number of entities resolved at the last level of the relation path. Applies to 'Related entities' arguments and Propagation fields.", + "relation-search-entity-limit-required": "Relation search entity limit", + "relation-search-entity-limit-range": "Relation search entity limit can't be less than '1'", "rate-limits": { "add-limit": "Add limit", "and-also-less-than": "and also less than", @@ -6015,7 +6611,9 @@ "default-agg-interval": "Default grouping interval", "edit-intervals-list-hint": "List of available interval options can be specified.", "edit-grouping-intervals-list-hint": "It is possible to configure the grouping intervals list and default grouping interval.", - "all": "All" + "all": "All", + "save-current-settings-as-default": "Save current settings as default time window", + "hide-option-from-end-users": "Hide option from end-users" }, "tooltip": { "trigger": "Trigger", @@ -6669,7 +7267,8 @@ "export-relations": "Export relations", "export-attributes": "Export attributes", "export-credentials": "Export credentials", - "export-calculated-fields": "Export calculated fields", + "export-calculated-fields": "Export calculated fields \nand alarm rules", + "export-alarm-rules": "Export alarm rules", "entity-versions": "Entity versions", "versions": "Versions", "created-time": "Created time", @@ -6686,7 +7285,8 @@ "load-relations": "Load relations", "load-attributes": "Load attributes", "load-credentials": "Load credentials", - "load-calculated-fields": "Load calculated fields", + "load-calculated-fields": "Load calculated fields and alarm rules", + "load-alarm-rules": "Load alarm rules", "compare-with-current": "Compare with current", "diff-entity-with-version": "Diff with entity version '{{versionName}}'", "previous-difference": "Previous Difference", @@ -6896,7 +7496,23 @@ "scan-qr-code": "Scan QR Code", "make-phone-call": "Make phone call", "get-location": "Get phone location", - "take-screenshot": "Take screenshot" + "take-screenshot": "Take screenshot", + "handle-provision-success-function": "Handle provision success function", + "get-location-function": "Get location function", + "process-launch-result-function": "Process launch result function", + "get-phone-number-function": "Get phone number function", + "process-image-function": "Process image function", + "process-qr-code-function": "Process QR code function", + "process-location-function": "Process location function", + "handle-empty-result-function": "Handle empty result function", + "handle-error-function": "Handle error function", + "handle-non-mobile-fallback-function": "Handle Non-Mobile fallback function", + "save-to-gallery": "Save to 'Image gallery'", + "provision-type": "Provision type", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Custom action function", "custom-pretty-function": "Custom action (with HTML template) function", @@ -6905,7 +7521,8 @@ "marker": "Marker", "polygon": "Polygon", "rectangle": "Rectangle", - "circle": "Circle" + "circle": "Circle", + "polyline": "Polyline" }, "place-map-item": "Place map item", "map-item-tooltip": { @@ -6917,7 +7534,9 @@ "continue-draw-polygon": "Continue draw polygon", "finish-draw-polygon": "Finish draw polygon", "start-draw-circle": "Start draw circle", - "finish-draw-circle": "Finish draw circle" + "finish-draw-circle": "Finish draw circle", + "start-draw-polyline": "Start draw polyline", + "finish-draw-polyline": "Finish draw polyline" } }, "widgets-bundle": { @@ -7477,6 +8096,14 @@ "update-animation-delay": "Update animation delay" }, "chart-axis": { + "limit":"Limit", + "source": "Source", + "key-value": "Key / Value", + "value-required": "Value is required.", + "entity-key-required": "Entity key is required.", + "key-required": "Key is required.", + "scale-limits": "Scale limits", + "scale-appearance": "Scale appearance", "scale": "Scale", "scale-min": "min", "scale-max": "max", @@ -7933,14 +8560,14 @@ "attribute-scope-server": "Server attribute", "attribute-scope-shared": "Shared attribute", "value-required": "Value required", - "image-settings": "Image settings", + "image-settings": "Image output settings", "image-format": "Image format", "image-format-jpeg": "JPEG", "image-format-png": "PNG", "image-format-webp": "WEBP", - "image-quality": "Image quality that use lossy compression such as jpeg and webp", - "max-image-width": "Maximum image width", - "max-image-height": "Maximum image height", + "image-quality": "Image quality", + "max-image-width": "Max width", + "max-image-height": "Max height", "action-buttons": "Action buttons", "show-action-buttons": "Show action buttons", "update-all-values": "Update all values, not only modified", @@ -8021,7 +8648,10 @@ "add-radio-option": "Add radio option", "radio-label-position": "Label position", "radio-label-position-before": "Before", - "radio-label-position-after": "After" + "radio-label-position-after": "After", + "save-image": "Save image", + "save-to-gallery": "Automatically store captured images in Image Gallery", + "public-image": "Makes image avaliable for any unauthorized user" }, "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", "qr-code": { @@ -8302,7 +8932,18 @@ }, "custom": { "title": "Custom", - "tile-url": "Tile URL" + "tile-url": "Tile URL", + "style-url": "Style URL", + "vector-tiles": "Vector tiles", + "vector-tiles-hint": "Enable when using a style URL from providers like OpenFreeMap or MapTiler.", + "attribution": "Copyright text", + "attribution-hint": "Displayed at the bottom of the map. Required by most tile providers." + }, + "openfreemap": { + "title": "OpenFreeMap", + "liberty": "Liberty", + "bright": "Bright", + "positron": "Positron" } }, "credentials": { @@ -8316,7 +8957,8 @@ "trips": "Trips", "markers": "Markers", "polygons": "Polygons", - "circles": "Circles" + "circles": "Circles", + "polylines": "Polylines" }, "data-layer": { "source": "Source", @@ -8513,6 +9155,25 @@ "finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle", "finish-circle-hint": "Circle: click to finish drawing" }, + "polyline": { + "polyline-key": "Polyline key", + "polyline-key-required": "Polyline key required", + "no-polylines": "No polylines configured", + "add-polylines": "Add polyline", + "polyline-configuration": "Polyline configuration", + "remove-polyline": "Remove polyline", + "edit": "Edit polyline", + "cut": "Cut polyline area", + "rotate": "Rotate polyline", + "remove-polyline-for": "Remove polyline for '{{entityName}}'", + "draw-polyline": "Draw polyline", + "polyline-place-first-point-hint-with-entity": "Polyline for '{{entityName}}': click to place first point", + "polyline-place-first-point-hint": "Polyline: click to place first point", + "finish-polyline-hint-with-entity": "Polyline for '{{entityName}}': click to finish drawing", + "finish-polyline-hint": "Polyline: click to finish drawing", + "polyline-place-first-point-cut-hint": "Click to place first point", + "finish-polyline-cut-hint": "Click first marker to finish and save" + }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position" }, @@ -8954,6 +9615,7 @@ "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", "display-timestamp": "Timestamp", + "timestamp-column-name":"Timestamp", "display-pagination": "Display pagination", "default-page-size": "Default page size", "page-step-settings": "Page step settings", @@ -9015,7 +9677,9 @@ "alarm-column-error": "At least one alarm column should be specified", "table-tabs": "Table tabs", "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode", - "disable-sorting": "Disable sorting" + "disable-sorting": "Disable sorting", + "sort-by": "Sort tabs by", + "sort-timestamp-option": "Created time" }, "latest-chart": { "total": "Total", @@ -9511,6 +10175,22 @@ "how-to-create-customer-and-assign-dashboard": "How to create Customer and assign Dashboard" } } + }, + "api-usage": { + "api-usage": "API usage", + "label": "Label", + "state-name": "State name", + "status": "Status", + "status-required": "Status is required.", + "limit": "Max limit", + "limit-required": "Max limit is required.", + "current-number": "Current number", + "current-number-required": "Current number is required.", + "add-key": "Add key", + "no-key": "No key", + "delete-key": "Delete key", + "target-dashboard-state": "Target dashboard state", + "go-to-main-state": "Go to default view" } }, "color": { @@ -9519,6 +10199,7 @@ "icon": { "icon": "Icon", "icons": "Icons", + "custom": "Custom", "select-icon": "Select icon", "material-icons": "Material icons", "show-all": "Show all icons", @@ -9559,6 +10240,7 @@ "items-per-page-separator": "of" }, "language": { + "auto": "Auto", "language": "Language", "locales": { "ar_AE": "العربية (الإمارات العربية المتحدة)", @@ -9571,6 +10253,7 @@ "es_ES": "español (España)", "fa_IR": "فارسی (ایران)", "fr_FR": "français (France)", + "hi_IN": "हिन्दी (भारत)", "it_IT": "italiano (Italia)", "ja_JP": "日本語 (日本)", "ka_GE": "ქართული (საქართველო)", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index cca6b2a6ff..c919107272 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -78,6 +78,7 @@ "show-more": "Mostrar más", "dont-show-again": "No mostrar de nuevo", "see-documentation": "Ver documentación", + "see-debug-events": "Ver eventos de depuración", "clear": "Limpiar", "upload": "Subir", "delete-anyway": "Eliminar de todos modos", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Autenticación de dos factores", "available-providers": "Proveedores disponibles", + "available-providers-required": "Debe configurarse al menos un proveedor de 2FA.", "issuer-name": "Nombre del emisor", "issuer-name-required": "Se requiere el nombre del emisor.", "max-verification-failures-before-user-lockout": "Máximo de fallos de verificación antes de bloquear al usuario", @@ -513,7 +515,9 @@ "verification-message-template-required": "Se requiere la plantilla del mensaje de verificación.", "within-time": "Dentro del tiempo (seg)", "within-time-pattern": "El tiempo debe ser un número entero positivo.", - "within-time-required": "Se requiere el tiempo." + "within-time-required": "Se requiere el tiempo.", + "force-2fa": "Aplicar autenticación de dos factores", + "enforce-for": "Aplicar para" }, "jwt": { "security-settings": "Configuración de seguridad JWT", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarma", + "alarm-list": "Lista de alarmas", "alarms": "Alarmas", "all-alarms": "Todas las alarmas", "select-alarm": "Seleccionar alarma", @@ -649,7 +654,16 @@ "alarm-type": "Tipo de alarma", "enter-alarm-type": "Introducir tipo de alarma", "no-alarm-types-matching": "No se encontraron tipos de alarma que coincidan con '{{entitySubtype}}'.", - "alarm-type-list-empty": "No se han seleccionado tipos de alarma." + "alarm-type-list-empty": "No se han seleccionado tipos de alarma.", + "system-comments": { + "acked-by-user": "La alarma fue reconocida por el usuario {{userName}}", + "cleared-by-user": "La alarma fue borrada por el usuario {{userName}}", + "assigned-to-user": "La alarma fue asignada por el usuario {{userName}} al usuario {{assigneeName}}", + "unassigned-to-user": "La alarma fue desasignada por el usuario {{userName}}", + "unassigned-from-deleted-user": "La alarma fue desasignada porque el usuario {{userName}} fue eliminado", + "comment-deleted": "El usuario {{userName}} eliminó su comentario", + "severity-changed": "La gravedad de la alarma se actualizó de {{oldSeverity}} a {{newSeverity}}" + } }, "alarm-activity": { "add": "Agregar un comentario...", @@ -754,6 +768,7 @@ "name-max-length": "El nombre debe tener menos de 256 caracteres", "label-max-length": "La etiqueta debe tener menos de 256 caracteres", "description": "Descripción", + "description-required": "La descripción es obligatoria.", "type": "Tipo", "type-required": "Se requiere el tipo.", "details": "Detalles", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Actividad mensual de alarmas creadas", "data-points": "Puntos de datos", "data-points-storage-days": "Días de almacenamiento de puntos de datos", + "data-points-storage-days-hourly-activity": "Actividad horaria de días de almacenamiento de puntos de datos", + "data-points-storage-days-daily-activity": "Actividad diaria de días de almacenamiento de puntos de datos", + "data-points-storage-days-monthly-activity": "Actividad mensual de días de almacenamiento de puntos de datos", "device-api": "API de dispositivo", "email": "Correo electrónico", "email-messages": "Mensajes de correo", @@ -900,6 +918,7 @@ "rule-node": "Nodo de regla", "sms": "SMS", "sms-messages": "Mensajes SMS", + "sms-messages-hourly-activity": "Actividad horaria de mensajes SMS", "sms-messages-daily-activity": "Actividad diaria de mensajes SMS", "sms-messages-monthly-activity": "Actividad mensual de mensajes SMS", "successful": "${entityName} exitoso", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Actividad por hora de persistencia de telemetría", "telemetry-persistence-monthly-activity": "Actividad mensual de persistencia de telemetría", "transport": "Transporte", + "transport-msg-hourly-activity": "Actividad horaria de mensajes de transporte", + "transport-msg-daily-activity": "Actividad diaria de mensajes de transporte", + "transport-msg-monthly-activity": "Actividad mensual de mensajes de transporte", "transport-daily-activity": "Actividad diaria de transporte", "transport-data-points": "Puntos de datos de transporte", + "transport-data-points-hourly-activity": "Actividad horaria de puntos de datos de transporte", + "transport-data-points-daily-activity": "Actividad diaria de puntos de datos de transporte", + "transport-data-points-monthly-activity": "Actividad mensual de puntos de datos de transporte", "view-details": "Ver detalles", "view-statistics": "Ver estadísticas", - "transport-messages": "Mensajes de transporte" + "transport-messages": "Mensajes de transporte", + "transport-messages-hourly-activity": "Actividad horaria de mensajes de transporte", + "transport-data-point-hourly-activity": "Actividad horaria de puntos de datos de transporte", + "javascript-function-executions": "Ejecuciones de funciones JavaScript", + "javascript-function-executions-hourly-activity": "Actividad horaria de ejecuciones de funciones JavaScript", + "javascript-function-executions-daily-activity": "Actividad diaria de ejecuciones de funciones JavaScript", + "javascript-function-executions-monthly-activity": "Actividad mensual de ejecuciones de funciones JavaScript", + "tbel-function-executions": "Ejecuciones de funciones TBEL", + "tbel-function-executions-hourly-activity": "Actividad horaria de ejecuciones de funciones TBEL", + "tbel-function-executions-daily-activity": "Actividad diaria de ejecuciones de funciones TBEL", + "tbel-function-executions-monthly-activity": "Actividad mensual de ejecuciones de funciones TBEL", + "created-reports": "Reportes creados", + "created-reports-hourly-activity": "Actividad horaria de reportes creados", + "created-reports-daily-activity": "Actividad diaria de reportes creados", + "created-reports-monthly-activity": "Actividad mensual de reportes creados", + "emails": "Emails", + "emails-hourly-activity": "Actividad horaria de Emails", + "emails-daily-activity": "Actividad diaria de Emails", + "emails-monthly-activity": "Actividad mensual de Emails", + "status": { + "enabled": "Activado", + "disabled": "Desactivado", + "warning": "Advertencia" + } }, "api-limit": { "cassandra-write-queries-core": "Consultas de escritura en Cassandra desde la API REST", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Mensajes uplink de edge", "edge-uplink-messages-per-edge": "Mensajes uplink de edge por instancia de edge" }, + "api-key": { + "api-key": "Clave de API", + "api-keys": "Claves de API", + "delete-api-key-title": "¿Estás seguro de que quieres eliminar la clave de API '{{name}}'?", + "delete-api-key-text": "Ten cuidado, tras la confirmación la clave será irrecuperable.", + "delete-api-keys-title": "¿Estás seguro de que quieres eliminar { count, plural, =1 {1 clave de API} other {# claves de API} }?", + "delete-api-keys-text": "Ten cuidado, tras la confirmación todas las claves seleccionadas serán irrecuperables.", + "expiration-date": "Fecha de caducidad", + "date": "fecha", + "description": "Descripción", + "disable": "Desactivar", + "edit-description": "Editar descripción", + "enable": "Activar clave de API ", + "expiration-time": "Hora de caducidad", + "expiration-time-never": "Nunca", + "expiration-time-custom": "Personalizado", + "generate": "Generar", + "generate-title": "Generar clave de API", + "generate-text": "Nota: la clave de API hereda los permisos del usuario para el que se crea.", + "generated-api-key-title": "Clave de API generada. ¡Vamos a comprobar la conectividad!", + "generated-api-key-copy": "Asegúrate de copiar y guardar tu clave de API ahora, ya que no podrás verla de nuevo.", + "generated-api-key-command": "Usa las siguientes instrucciones para comprobar la conectividad. Como resultado, deberías recibir la información del usuario actual:", + "generated-api-key-insecure-url": "Ejecutar comandos a través de una conexión HTTP insegura enviará tu clave de API sin cifrar, haciéndola vulnerable a la interceptación.", + "list": "{ count, plural, =1 {Una clave de API} other {Lista de # claves de API} }", + "manage": "Gestionar", + "manage-api-keys": "Gestionar claves de API", + "no-found": "No se han encontrado claves de API", + "selected-api-keys": "{ count, plural, =1 {1 clave de API} other {# claves de API} } seleccionadas", + "search": "Buscar claves de API", + "status": "Estado", + "status-active": "Activa", + "status-inactive": "Inactiva", + "status-expired": "Caducada" + }, "audit-log": { "audit": "Auditoría", "audit-logs": "Registros de auditoría", @@ -991,7 +1073,11 @@ "type-provision-failure": "El aprovisionamiento del dispositivo falló", "type-timeseries-updated": "Telemetría actualizada", "type-timeseries-deleted": "Telemetría eliminada", - "type-sms-sent": "SMS enviado" + "type-sms-sent": "SMS enviado", + "any-type": "Cualquier tipo", + "audit-log-filter-title": "Filtro de registro de auditoría", + "filter-title": "Filtrar", + "filter-types": "Tipos de registro de auditoría" }, "debug-settings": { "label": "Configuración de depuración", @@ -1009,15 +1095,29 @@ "expression": "Expresión", "no-found": "No se encontraron campos calculados", "list": "{ count, plural, =1 {Un campo calculado} other {Lista de # campos calculados} }", + "add": "Añadir campo calculado", "selected-fields": "{ count, plural, =1 {1 campo calculado} other {# campos calculados} } seleccionado(s)", "type": { "simple": "Simple", - "script": "Script" + "simple-hint": "Cálculo aritmético simple basado en argumentos de entrada.", + "script": "Script", + "script-hint": "Cálculo sobre los argumentos definidos mediante un script TBEL.", + "geofencing": "Geovallado", + "geofencing-hint": "Evaluación de la posición GPS de la entidad y de las transiciones respecto a los grupos de zonas de geovallado configurados.", + "propagation": "Propagación", + "propagation-hint": "Propagación de datos a entidades padre o hijas según la dirección y el tipo de relación.", + "related-entities-aggregation": "Agregación de entidades relacionadas", + "related-entities-aggregation-hint": "Agregación de los datos más recientes de entidades relacionadas.", + "time-series-data-aggregation": "Agregación de datos de series temporales", + "time-series-data-aggregation-hint": "Agregación de datos históricos de una entidad actual." }, + "preview": "Vista previa", "arguments": "Argumentos", "decimals-by-default": "Decimales por defecto", "debugging": "Depuración de campos calculados", + "calculated-field-details": "Detalles del campo calculado", "argument-name": "Nombre del argumento", + "name": "Nombre", "datasource": "Origen de datos", "add-argument": "Agregar argumento", "test-script-function": "Probar función de script", @@ -1029,6 +1129,8 @@ "argument-asset": "Activo", "argument-customer": "Customer", "argument-tenant": "Tenant actual", + "argument-owner": "Propietario actual", + "argument-relation-query": "Entidades relacionadas", "argument-type": "Tipo de argumento", "attribute": "Atributo", "copy-argument-name": "Copiar nombre del argumento", @@ -1042,12 +1144,14 @@ "shared-attributes": "Atributos compartidos", "attribute-key": "Clave del atributo", "default-value": "Valor por defecto", + "default-value-required": "El valor predeterminado es obligatorio.", "limit": "Máx. valores", "time-window": "Ventana de tiempo", "customer-name": "Nombre del customer", "asset-name": "Nombre del activo", "timeseries": "Serie temporal", "output": "Salida", + "output-hint": "Define cómo se procesa la salida.", "create": "Crear nuevo campo calculado", "file": "Archivo de campo calculado", "invalid-file-error": "Formato de archivo inválido. Asegúrate de que el archivo sea un JSON válido.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Ten cuidado, después de la confirmación todos los campos calculados seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", "test-with-this-message": "Probar con este mensaje", "use-latest-timestamp": "Usar la última marca de tiempo", + "entity-coordinates": "Coordenadas de la entidad", + "latitude-time-series-key": "Clave de serie temporal de latitud", + "latitude-time-series-key-required": "La clave de serie temporal de latitud es obligatoria.", + "longitude-time-series-key": "Clave de serie temporal de longitud", + "longitude-time-series-key-required": "La clave de serie temporal de longitud es obligatoria.", + "geofencing-zone-groups": "Grupos de zonas de geovallado", + "geofencing-zone-groups-settings": "Configuración de grupos de zonas de geovallado", + "target-zone": "Zona objetivo", + "perimeter-key": "Clave de perímetro", + "report-strategy": "Estrategia de informe", + "no-zone-configured": "Se requiere al menos una zona.", + "no-zone-configured-required": "Debe configurarse al menos un grupo de zonas.", + "add-zone-group": "Añadir grupo de zonas", + "report-transition-event-only": "Solo eventos de transición", + "report-presence-status-only": "Solo estado de presencia", + "report-transition-event-and-presence": "Estado de presencia y eventos de transición", + "perimeter-attribute-key": "Clave de atributo de perímetro", + "perimeter-attribute-key-required": "La clave de atributo de perímetro es obligatoria.", + "perimeter-attribute-key-pattern": "La clave de atributo de perímetro no es válida.", + "entity-zone-relationship": "Ruta desde la entidad hasta las zonas", + "direction": "Dirección de la relación", + "direction-from": "De la entidad a la zona", + "direction-to": "De la zona a la entidad", + "relation-type": "Tipo de relación", + "create-relation-with-matched-zones": "Crear relaciones para la entidad de origen con las zonas coincidentes", + "relation-level": "Nivel de relación", + "fetch-last-available-level": "Obtener solo el último nivel disponible", + "zone-group-refresh-interval": "Intervalo de actualización de grupos de zonas", + "copy-zone-group-name": "Copiar nombre del grupo de zonas", + "open-details-page": "Abrir página de detalles de la entidad", + "level": "Nivel", + "direction-level": "Dirección", + "direction-up": "Arriba", + "direction-up-parent": "Arriba hasta el padre", + "direction-down": "Abajo", + "direction-down-child": "Abajo hasta el hijo", + "add-level": "Añadir nivel", + "delete-level": "Eliminar nivel", + "no-level": "No hay ningún nivel configurado", + "levels-required": "Debe configurarse al menos un nivel.", + "max-allowed-levels-error": "El nivel de relación supera el máximo permitido.", + "propagation-path-related-entities": "Ruta de propagación a entidades relacionadas", + "propagate-type": { + "arguments-only": "Solo argumentos", + "expression-result": "Resultado del cálculo" + }, + "script": "Script", + "data-propagate": "Datos a propagar", + "output-key": "Clave de salida", + "copy-output-key": "Copiar clave de salida", + "aggregation-path-related-entities": "Ruta de agregación a entidades relacionadas", + "deduplication-interval": "Intervalo de deduplicación", + "deduplication-interval-min": "El intervalo de deduplicación debe ser de al menos {{ sec }} segundos.", + "deduplication-interval-hint": "Tiempo mínimo entre agregaciones de telemetría.", + "deduplication-interval-required": "El intervalo de deduplicación es obligatorio.", + "calculated-field-filter-title": "Filtro de campo calculado", + "filter-title": "Filtrar", + "calculated-field-types": "Tipos de campo calculado", + "events": "Eventos", + "any-type": "Cualquier tipo", + "metrics": { + "metrics": "Métricas", + "metrics-empty": "Debe configurarse al menos una métrica.", + "metric-name": "Nombre de métrica", + "metric-name-required": "El nombre de la métrica es obligatorio.", + "metric-name-pattern": "El nombre de la métrica no es válido.", + "metric-name-duplicate": "Ya existe una métrica con ese nombre.", + "metric-name-max-length": "El nombre de la métrica debe tener menos de 256 caracteres.", + "metric-name-forbidden": "El nombre de la métrica está reservado y no se puede usar.", + "copy-metric-name": "Copiar nombre de la métrica", + "argument-name": "Nombre del argumento", + "aggregation": "Agregación", + "aggregation-type": { + "avg": "Media", + "min": "Mínimo", + "max": "Máximo", + "sum": "Suma", + "count": "Recuento", + "count-unique": "Recuento de únicos" + }, + "filtered": "Filtrado", + "value-source": "Origen del valor", + "value-source-hint": "Define cómo se obtiene el valor para la agregación.", + "value-source-type": { + "key": "Clave", + "function": "Función" + }, + "no-metrics-configured": "Se requiere al menos una métrica.", + "add-metric": "Añadir métrica", + "max-metrics": "Se ha alcanzado el número máximo de métricas.", + "metric-settings": "Configuración de la métrica", + "filter": "Filtro", + "filter-hint": "Permite filtrar entidades durante la agregación. La función de filtro debe devolver un valor booleano y puede usar todos los argumentos configurados." + }, + "output-strategy": { + "strategy": "Estrategia", + "process-right-away": "Procesar de inmediato", + "process-rule-chains": "Procesar mediante cadenas de reglas", + "save-time-series": "Guardar en series temporales", + "save-database": "Guardar en base de datos", + "save-latest-values": "Guardar en valores más recientes", + "send-web-sockets": "Enviar a WebSockets", + "save-calculated-fields": "Enviar a campos calculados", + "update-attribute-only-on-value-change": "Actualizar atributo solo cuando cambie el valor", + "send-attributes-updated-notification": "Enviar notificación de atributos actualizados", + "ttl": "TTL personalizado", + "ttl-required": "El TTL es obligatorio", + "ttl-min": "Solo se permite un TTL mínimo de 0", + "processing-parameters": "Parámetros de procesamiento", + "hint": { + "strategy": "Controla si el resultado se procesa inmediatamente o se envía a una cadena de reglas para un procesamiento adicional.", + "processing-options": "Opciones de procesamiento", + "update-attribute-only-on-value-change": "Actualiza el atributo en cada mensaje entrante, independientemente de si el valor ha cambiado. Esto incrementa el uso de la API y reduce el rendimiento.", + "update-attribute-only-on-value-change-enabled": "Actualiza el atributo solo cuando cambia el valor. Si el valor no cambia, no se actualizan las marcas de tiempo y no se envían notificaciones.", + "send-attributes-updated-notification": "Envía un evento Attributes Updated a la cadena de reglas predeterminada.", + "save-time-series": "Guarda datos de series temporales en la tabla ts_kv de la base de datos.", + "save-database": "Guarda datos de atributos en la base de datos.", + "save-latest-values": "Actualiza los datos de series temporales en la tabla ts_kv_latest de la base de datos si el nuevo valor es más reciente.", + "send-web-sockets-attribute": "Notifica a las suscripciones de WebSocket sobre las actualizaciones de los datos de atributos.", + "send-web-sockets-time-series": "Notifica a las suscripciones de WebSocket sobre las actualizaciones de los datos de series temporales.", + "save-calculated-fields-attribute": "Notifica a los campos calculados sobre las actualizaciones de los datos de atributos.", + "save-calculated-fields-time-series": "Notifica a los campos calculados sobre las actualizaciones de los datos de series temporales.", + "ttl": "Define el periodo de retención de los datos de series temporales. Si está desactivado, se usa el TTL del perfil de Tenant." + } + }, + "aggregate-interval-type": "Tipo de intervalo de agregación", + "aggregate-interval-value": "Valor del intervalo de agregación", + "aggregate-interval-value-required": "El valor del intervalo de agregación es obligatorio.", + "aggregate-interval-value-min": "El valor del intervalo de agregación debe ser de al menos { sec, plural, =0 {0 segundo} =1 {1 segundo} other {# segundos} }.", + "aggregate-interval-value-step-multiple-of": "El valor del intervalo de agregación debe ser un divisor o un múltiplo de 1 día.", + "aggregate-period": { + "hour": "Hora", + "day": "Día", + "week": "Semana (lun - dom)", + "week-sun-sat": "Semana (dom - sáb)", + "month": "Mes", + "quarter": "Trimestre", + "year": "Año", + "custom": "Personalizado" + }, + "aggregate-period-hint-offset": "Tu intervalo de agregación será: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Tu intervalo de agregación será: {{ interval }} y así sucesivamente.", + "entity-aggregation": { + "argument-hint": "Los datos se obtendrán de la entidad actual.", + "argument-title-hint": "Define los argumentos de entrada utilizados para la agregación.", + "argument-setting-hint": "La telemetría más reciente es el único tipo de argumento disponible para este campo calculado.", + "aggregation-interval": "Intervalo de agregación", + "aggregation-interval-hint": "Define la frecuencia con la que se realiza la agregación. Ejemplo: cada 1 hora agrega datos a las 00:00, 01:00, 02:00, etc. Los resultados de la agregación se almacenan con la marca de tiempo correspondiente al inicio del intervalo de agregación.", + "apply-offset": "Aplicar desfase al intervalo de agregación", + "apply-offset-hint": "Define cuánto desplazar el inicio de cada periodo de agregación (p. ej., +10 minutos - 00:10, 01:10).", + "offset-value": "Valor de desfase", + "offset-value-required": "El valor de desfase es obligatorio.", + "offset-value-min": "El valor de desfase debe ser un entero positivo.", + "offset-value-max": "El valor de desfase debe ser inferior al valor del intervalo de agregación.", + "wait-delay": "Aplicar tiempo de espera para telemetría retrasada", + "wait-delay-hint": "Define cuánto tiempo esperar la telemetría retrasada después de que termine el intervalo. Si llega esa telemetría, se recalculará el resultado para ese intervalo.", + "duration": "Duración", + "duration-required": "La duración es obligatoria.", + "duration-min": "La duración debe ser de al menos 1 minuto.", + "duration-hint": "Cuánto tiempo esperar los datos retrasados después de que termine el intervalo.", + "produce-intermediate-result": "Generar resultado intermedio", + "produce-intermediate-result-hint": "Calcula métricas durante el intervalo actual para generar un resultado intermedio. Las actualizaciones no se producen con más frecuencia que una vez cada {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Un campo calculado de tipo simple no debe contener claves con el tipo de serie temporal móvil.", + "arguments-propagate-arguments-with-rolling": "El tipo 'Serie temporal móvil' es incompatible con la propagación 'Solo argumentos'.", + "arguments-propagate-argument-entity-type": "El tipo de entidad es incompatible con la propagación 'Solo argumentos'.", + "arguments-propagate-argument-must-current-entity": "Debe configurarse al menos un argumento con el tipo de entidad de origen 'Entidad actual'.", "arguments-empty": "Los argumentos no deben estar vacíos.", "expression-required": "Se requiere una expresión.", "expression-invalid": "La expresión no es válida.", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Ya existe un argumento con ese nombre.", "argument-name-max-length": "El nombre del argumento debe tener menos de 256 caracteres.", "argument-name-forbidden": "El nombre del argumento está reservado y no puede utilizarse.", + "output-key-required": "La clave de salida es obligatoria.", + "output-key-pattern": "La clave de salida no es válida.", + "output-key-duplicate": "Ya existe una clave con ese nombre.", + "output-key-max-length": "La clave de salida debe tener menos de 256 caracteres.", + "output-key-forbidden": "La clave de salida está reservada y no se puede usar.", + "entity-type-required": "El tipo de entidad es obligatorio", + "name-required": "El nombre es obligatorio.", + "name-pattern": "El nombre no es válido.", + "name-duplicate": "Ya existe un campo calculado con ese nombre.", + "name-max-length": "El nombre debe tener menos de 256 caracteres.", + "name-forbidden": "El nombre está reservado y no se puede usar.", "argument-type-required": "Se requiere el tipo de argumento.", "max-args": "Se ha alcanzado el número máximo de argumentos.", "decimals-range": "Los decimales por defecto deben ser un número entre 0 y 15.", "expression": "La expresión por defecto muestra cómo transformar una temperatura de Fahrenheit a Celsius.", "arguments-entity-not-found": "No se encontró la entidad de destino del argumento.", - "use-latest-timestamp": "Si está activado, el valor calculado se guardará usando la marca de tiempo más reciente de la telemetría de los argumentos, en lugar de la hora del servidor." + "use-latest-timestamp": "Si está activado, el valor calculado se guardará usando la marca de tiempo más reciente de la telemetría de los argumentos, en lugar de la hora del servidor.", + "entity-coordinates": "Especifica las claves de serie temporal que proporcionan las coordenadas GPS de la entidad (latitud y longitud).", + "geofencing-zone-groups": "Define uno o varios grupos de zonas de geovallado para comprobar (p. ej., 'allowedZones', 'restrictedZones'). Cada grupo debe tener un nombre único, que se usa como prefijo para las claves de telemetría de salida del campo calculado.", + "perimeter-attribute-key": "Establece la clave de atributo que contiene la definición del perímetro de la zona de geovallado. El perímetro siempre se toma de los atributos del lado del servidor de la entidad de zona.", + "report-strategy": "El estado de presencia indica si la entidad está actualmente DENTRO o FUERA del grupo de zonas. Los eventos de transición indican cuándo la entidad ENTRÓ o SALIÓ del grupo de zonas.", + "create-relation-with-matched-zones": "Crea y mantiene automáticamente relaciones entre la entidad y las zonas en las que se encuentra actualmente. Las relaciones se eliminan cuando la entidad sale de una zona y se crean cuando entra en una nueva.", + "relation-type-required": "El tipo de relación es obligatorio.", + "relation-level-required": "El nivel de relación es obligatorio.", + "relation-level-min": "El valor mínimo del nivel de relación es 1.", + "relation-level-max": "El valor máximo del nivel de relación es {{max}}.", + "geofencing-empty": "Debe configurarse al menos un grupo de zonas.", + "geofencing-entity-not-found": "No se ha encontrado la entidad objetivo de geovallado.", + "max-geofencing-zone": "Se ha alcanzado el número máximo de zonas de geovallado.", + "zone-group-refresh-interval": "Define con qué frecuencia se actualizan los grupos de zonas configurados mediante entidades relacionadas.", + "zone-group-refresh-interval-required": "El intervalo de actualización de grupos de zonas es obligatorio.", + "zone-group-refresh-interval-min": "El intervalo de actualización del grupo de zonas debe ser de al menos {{ min }} segundos.", + "propagation-path-related-entities": "Define una ruta directa de un solo nivel a una entidad relacionada según la dirección seleccionada y el tipo de relación. Solo se admiten relaciones entre entidades de dispositivo, activo, cliente y tenant. El máximo de entidades resueltas por la ruta de relación es {{ max }}.", + "data-propagate": "Define los datos que se propagarán desde los argumentos configurados a continuación. 'Solo argumentos' usa los datos recuperados directamente, mientras que 'Resultado del cálculo' calcula un nuevo valor a partir de esos datos.", + "aggregation-path-related-entities": "Define una ruta de agregación de un solo nivel mediante relaciones directas con entidades padre o hijas, según la dirección y el tipo de relación. Solo se admiten relaciones entre entidades de dispositivo, activo, cliente y tenant. El máximo de entidades resueltas por la ruta de relación es {{ max }}.", + "arguments-aggregation": "Define los argumentos de entrada utilizados para el filtrado y la agregación.", + "setting-arguments-aggregation": "Los datos se obtendrán de las entidades relacionadas configuradas en la ruta de agregación.", + "metrics": "Define las métricas agregadas en función de los argumentos configurados.", + "entity-aggregation-metrics": "Define las métricas agregadas en función de los argumentos configurados a lo largo de los intervalos de tiempo especificados.", + "import-invalid-calculated-field-type": "No se puede importar el campo calculado: la estructura del campo calculado no es válida.", + "simple-expression-title": "Expresión aritmética que define cómo se calcula el valor calculado.", + "script-title": "Script TBEL que define la lógica de cálculo y los valores de salida.", + "simple-arguments": "Expresión aritmética que define cómo se calcula el valor calculado.", + "script-arguments": "Define los argumentos de entrada disponibles para el script." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Reglas de alarma", + "alarm-rule": "Regla de alarma", + "alarm-rules": "Reglas de alarma", + "alarm-rules-old": "Antiguas", + "alarm-rules-actual": "Actuales", + "severities": "Gravedades", + "cleared": "Condición de borrado", + "delete-title": "¿Estás seguro de que quieres eliminar la regla de alarma '{{title}}'?", + "delete-text": "Ten cuidado, tras la confirmación la regla de alarma y todos los datos relacionados serán irrecuperables.", + "delete-multiple-title": "¿Estás seguro de que quieres eliminar { count, plural, =1 {1 regla de alarma} other {# reglas de alarma} }?", + "delete-multiple-text": "Ten cuidado, tras la confirmación se eliminarán todas las reglas de alarma seleccionadas y todos los datos relacionados serán irrecuperables.", + "create": "Crear nueva regla de alarma", + "add": "Añadir regla de alarma", + "copy": "Copiar configuración de la regla de alarma", + "details": "Detalles de la regla de alarma", + "no-found": "No se han encontrado reglas de alarma", + "list": "{ count, plural, =1 {Una regla de alarma} other {Lista de # reglas de alarma} }", + "selected-fields": "{ count, plural, =1 {1 regla de alarma} other {# reglas de alarma} } seleccionadas", + "import": "Importar regla de alarma", + "file": "Archivo de regla de alarma", + "export": "Exportar regla de alarma", + "export-failed-error": "No se puede exportar la regla de alarma: {{error}}", + "entity-type": "Tipo de entidad", + "entity-type-required": "El tipo de entidad es obligatorio.", + "alarm-type": "Tipo de alarma", + "alarm-type-hint": "Identificador único (p. ej., HighTempAlarm) dentro del ámbito del originador de la alarma (Dispositivo, Activo, etc.) para evitar conflictos.", + "alarm-type-required": "El tipo de alarma es obligatorio.", + "alarm-type-pattern": "El tipo de alarma no es válido.", + "alarm-type-max-length": "El tipo de alarma debe tener menos de 256 caracteres.", + "clear-alarm": "Borrar alarma", + "value-argument": "Argumento", + "value-argument-required": "El argumento es obligatorio.", + "static-settings": "Configuración estática", + "configuration": "Configuración", + "static-schedule": "Estática", + "dynamic-schedule": "Dinámica", + "operation-and": "Y", + "operation-or": "O", + "condition-during": "Durante {{during}}", + "condition-during-dynamic": "Durante \"{{ attribute }}\"", + "condition-repeat-times": "Se repite { count, plural, =1 {1 vez} other {# veces} }", + "condition-repeat-times-dynamic": "Se repite \"{{ attribute }}\" veces", + "filter-preview": "Vista previa del filtro", + "condition-settings": "Configuración de la condición", + "static": "Estática", + "dynamic": "Dinámica", + "argument-filters": "Filtros de argumentos", + "argument-name": "Nombre del argumento", + "value-type": "Tipo de valor", + "general": "General", + "filters": "Filtros", + "date-time-hint": "El argumento debe estar en milisegundos desde epoch. Ejemplo: 1698839340000 equivale a 2023-11-01 12:49:00 UTC.", + "operation": "Operación", + "value-source": "Origen del valor", + "value": "Valor", + "ignore-case": "Ignorar mayúsculas y minúsculas", + "condition": "Condición", + "script": "Script", + "add-filter": "Añadir filtro de argumentos", + "edit-filter": "Filtro de argumentos", + "remove-filter": "Eliminar filtro de argumentos", + "no-filter": "Se requiere al menos un filtro.", + "conditions": { + "simple": "Simple", + "duration": "Duración", + "repeating": "Repetición" + }, + "schedule-title": "Programación", + "edit-schedule": "Editar programación de alarma", + "schedule-type": "Tipo de programador", + "schedule-type-required": "El tipo de programador es obligatorio.", + "schedule": { + "any-time": "Activa todo el tiempo", + "specific-time": "Activa en un horario específico", + "custom": "Personalizado" + }, + "schedule-day": { + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday": "Sábado", + "sunday": "Domingo" + }, + "schedule-days": "Días", + "schedule-time": "Hora", + "schedule-time-from": "Desde", + "schedule-time-to": "Hasta", + "schedule-days-of-week-required": "Debe seleccionarse al menos un día de la semana.", + "tbel": "TBEL", + "expression-type": { + "simple": "Simple", + "script": "Script" + }, + "operation-type": { + "and": "Y", + "or": "O" + }, + "filter-predicate-type": { + "string": "Cadena", + "numeric": "Numérico", + "boolean": "Booleano", + "complex": "Complejo" + }, + "alarm-rule-additional-info": "Información adicional", + "edit-alarm-rule-additional-info": "Editar información adicional", + "alarm-rule-additional-info-placeholder": "Indica aquí tus comentarios y ajustes para mostrarlos en Detalles de la alarma, en Información adicional", + "alarm-rule-additional-info-hint": "Sugerencia: usa ${Nombre del argumento} para sustituir los valores de los argumentos que se usan en la condición de la regla de alarma.", + "alarm-rule-additional-info-icon-hint": "Usa el nombre del argumento para sustituir los valores de los argumentos que se usan en la condición de la regla de alarma.", + "alarm-rule-mobile-dashboard": "Tablero móvil", + "alarm-rule-mobile-dashboard-hint": "Lo usa la aplicación móvil como tablero de detalles de la alarma.", + "alarm-rule-no-mobile-dashboard": "No se ha seleccionado ningún tablero", + "alarm-rule-condition": "Condición de la regla de alarma", + "enter-alarm-rule-condition-prompt": "Añadir condición", + "enter-alarm-rule-clear-condition-prompt": "Añadir condición de borrado", + "edit-alarm-rule-condition": "Condición de alarma", + "condition-type": "Tipo de condición", + "condition-type-hint": "Las opciones \"Duración\" y \"Repetición\" no están disponibles cuando se usa la operación \"Missing for\" en el filtro.", + "select-alarm-severity": "Seleccionar gravedad de la alarma", + "add-create-alarm-rule-prompt": "Se requiere al menos una condición de activación.", + "add-create-alarm-rule": "Añadir condición de activación", + "add-clear-alarm-rule": "Añadir condición de borrado", + "condition-duration": "Duración de la condición", + "condition-duration-value": "Valor de duración", + "condition-duration-time-unit": "Unidad de tiempo", + "condition-duration-value-range": "El valor de duración debe estar en un rango de 1 a 2147483647.", + "condition-duration-value-pattern": "El valor de duración debe ser un número entero.", + "condition-duration-value-required": "El valor de duración es obligatorio.", + "condition-duration-time-unit-required": "La unidad de tiempo es obligatoria.", + "condition-repeating-value": "Número de eventos", + "condition-repeating-value-hint": "La actualización de cualquier argumento de la regla de alarma se contará como un evento", + "condition-repeating-value-range": "El número de eventos debe estar en un rango de 1 a 2147483647.", + "condition-repeating-value-pattern": "El número de eventos debe ser un número entero.", + "condition-repeating-value-required": "El número de eventos es obligatorio.", + "create-conditions": "Condiciones de activación", + "clear-condition": "Condición de borrado", + "no-clear-alarm-rule": "No hay ninguna condición de borrado configurada.", + "advanced-settings": "Configuración avanzada", + "propagate-alarm": "Propagar alarma a entidades relacionadas", + "alarm-rule-relation-types-list": "Tipos de relación", + "alarm-rule-relation-types-list-hint": "Define los tipos de relación para filtrar las entidades relacionadas. Si no se establece, la alarma se propagará a todas las entidades relacionadas.", + "propagate-alarm-to-owner": "Propagar alarma al propietario de la entidad (Cliente o Tenant)", + "propagate-alarm-to-tenant": "Propagar alarma al Tenant", + "alarm-rule-filter-title": "Filtro de regla de alarma", + "filter-title": "Filtrar", + "debugging": "Depuración de regla de alarma", + "any-type": "Cualquier tipo", + "enter-alarm-rule-type": "Introducir tipo de alarma", + "no-alarm-rule-types-matching": "No se han encontrado tipos de alarma que coincidan con '{{entitySubtype}}'.", + "alarm-rule-type-list-empty": "No se ha seleccionado ningún tipo de alarma.", + "alarm-rule-type-list": "Lista de tipos de alarma", + "alarm-rule-entity-list": "Lista de entidades", + "missing-for": "sin datos durante", + "time-unit": "Unidad", + "mode": "Modo", + "type": "Tipo", + "value-required": "El valor es obligatorio.", + "min-value": "El valor debe ser 1 o mayor.", + "argument-in-use": "El argumento se usa como argumento general.", + "import-invalid-alarm-rule-type": "No se puede importar la regla de alarma: la estructura de la regla de alarma no es válida.", + "no-filter-preview": "No se ha especificado ningún filtro", + "filter-operation": { + "and": "Y", + "or": "O" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Documentación", "time-left": "{{time}} restante", "output": "Salida", + "sort-asc": "Ascendente", + "sort-desc": "Descendente", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Orden del tablero en la aplicación móvil", "mobile-hide": "Ocultar tablero en la aplicación móvil", "update-image": "Actualizar imagen del tablero", + "update-new-version": "Subir nueva versión", + "upload-file-to-update": "Subir archivo para actualizar", "take-screenshot": "Tomar captura de pantalla", "select-widget-title": "Seleccionar widget", "select-widget-value": "{{title}}: seleccionar widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Cliente Bootstrap", "bootstrap-server": "Servidor Bootstrap", "lwm2m-server": "Servidor LwM2M", + "client-reboot": "Disparador de actualización de registro", + "bootstrap-reboot": "Disparador de solicitud de arranque", "client-publicKey-or-id": "Clave pública o ID del cliente", "client-publicKey-or-id-required": "Se requiere la clave pública o el ID del cliente.", "client-publicKey-or-id-tooltip-psk": "El identificador PSK es arbitrario hasta 128 bytes, conforme a la norma [RFC7925].\nDebe convertirse a cadena y luego a octetos en UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "Se requiere el ID corto del servidor.", "short-id-range": "El ID corto del servidor debe estar en un rango de {{ min }} a {{ max }}.", "short-id-pattern": "El ID corto del servidor debe ser un número entero positivo.", + "short-id-pattern-bs": "El ID corto del servidor solo debe ser null", "lifetime": "Tiempo de vida del registro del cliente", "lifetime-required": "Se requiere el tiempo de vida del registro del cliente.", "lifetime-pattern": "El tiempo de vida del registro debe ser un número entero positivo.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Todos los recursos se observan con una única solicitud Composite Observe (más eficiente, menos flexible)", "composite-by-object": "Compuesta por objetos", "composite-by-object-description": "Los recursos se agrupan por tipo de objeto y se observan usando solicitudes Composite Observe separadas (enfoque equilibrado)" - } + }, + "init-attr-tel-as-obs-strategy": "Inicializar atributos y telemetría usando la estrategia Observe", + "init-attr-tel-as-obs-strategy-hint": "Si es false: los atributos y la telemetría se inicializan leyendo sus valores uno a uno.\\nSi es true: los atributos y la telemetría se inicializan suscribiéndose a sus valores usando la estrategia Observe." }, "snmp": { "add-communication-config": "Agregar configuración de comunicación", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Nodos de regla", "list-of-rulenodes": "{ count, plural, =1 {Un nodo de regla} other {Lista de # nodos de regla} }", "rulenode-name-starts-with": "Nodos de regla cuyos nombres comienzan con '{{prefix}}'", + "type-api-key": "Clave de API", + "type-api-keys": "Claves de API", "type-current-customer": "Customer actual", "type-current-tenant": "Tenant actual", "type-current-user": "Usuario actual", @@ -2656,6 +3143,7 @@ "details": "Detalles de la entidad", "no-entities-prompt": "No se encontraron entidades", "no-data": "No hay datos para mostrar", + "show-all-columns": "Mostrar todo", "columns-to-display": "Columnas para mostrar", "type-api-usage-state": "Estado de uso de API", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Una aplicación móvil} other {Lista de # aplicaciones móviles} }", "type-mobile-app-bundle": "Paquete móvil", "type-mobile-app-bundles": "Paquetes móviles", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Un paquete móvil} other {Lista de # paquetes móviles} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Un paquete móvil} other {Lista de # paquetes móviles} }", + "limit-reached": "Límite alcanzado", + "limit-reached-text": "Has alcanzado el límite de {{ entities }}. Para añadir más, pide a tu administrador del sistema que aumente tu límite de {{ entity }}.", + "request-limit-increase": "Solicitar aumento del límite", + "request-sysadmin-text": "¿Eres el administrador del sistema?", + "login-here": "Iniciar sesión aquí", + "to-increase-limit": "para aumentar el límite.", + "increase-limit-request-sent-title": "Hemos enviado una solicitud automática a tu administrador del sistema para aumentar el límite", + "increase-limit-request-sent-text": "Deja algo de tiempo para que revisen la solicitud y actualicen la configuración. Puede que tengas que actualizar esta página para ver los cambios." }, "entity-field": { "created-time": "Hora de creación", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Enlace de restablecimiento enviado", "email": "Correo electrónico", "invalid-email-format": "Formato de correo electrónico inválido.", + "sign-in-with": "Iniciar sesión con {{name}}", + "sign-in-to-your-account": "Iniciar sesión en tu cuenta", "or": "o", "error": "Error de inicio de sesión", "verify-your-identity": "Verifica tu identidad", @@ -3807,7 +4305,51 @@ "activation-link-expired": "El enlace de activación ha expirado", "activation-link-expired-message": "El enlace para activar tu perfil ha expirado. Puedes regresar a la página de inicio de sesión para recibir un nuevo correo.", "reset-password-link-expired": "El enlace para restablecer la contraseña ha expirado", - "reset-password-link-expired-message": "El enlace para restablecer tu contraseña ha expirado. Puedes regresar a la página de inicio de sesión para recibir un nuevo correo." + "reset-password-link-expired-message": "El enlace para restablecer tu contraseña ha expirado. Puedes regresar a la página de inicio de sesión para recibir un nuevo correo.", + "two-fa": "Autenticación de dos factores", + "two-fa-required": "La autenticación de dos factores es obligatoria", + "set-up-verification-method": "Configura un método de verificación para continuar", + "set-up-verification-method-login": "Configura un método de verificación o inicia sesión", + "enable-authenticator-app": "Activar aplicación de autenticación", + "enable-authenticator-app-description": "Introduce el código de seguridad de tu aplicación de autenticación", + "enable-authenticator-sms": "Activar autenticación por SMS", + "enable-authenticator-sms-description": "Introduce el código de 6 dígitos que acabamos de enviar a ", + "enable-authenticator-email": "Activar autenticación por Email", + "enable-authenticator-email-description": "Se ha enviado un código de seguridad a tu dirección de email en ", + "enter-key-manually": "o introduce manualmente esta clave de 32 dígitos:", + "continue": "Continuar", + "confirm": "Confirmar", + "authenticator-app-success": "Aplicación de autenticación activada correctamente", + "authenticator-app-success-description": "La próxima vez que inicies sesión, tendrás que proporcionar un código de autenticación de dos factores", + "authenticator-sms-success": "Autenticación por SMS activada correctamente", + "authenticator-sms-success-description": "La próxima vez que inicies sesión, se te pedirá que introduzcas el código de seguridad que se enviará al número de teléfono", + "authenticator-email-success": "Autenticación por Email activada correctamente", + "authenticator-email-success-description": "La próxima vez que inicies sesión, se te pedirá que introduzcas el código de seguridad que se enviará a tu dirección de email", + "authenticator-backup-code-success": "Código de respaldo activado correctamente", + "authenticator-backup-code-success-description": "La próxima vez que inicies sesión, se te pedirá que introduzcas el código de seguridad o que uses uno de los códigos de respaldo.", + "add-verification-method": "Añadir método de verificación", + "get-backup-code": "Obtener código de respaldo", + "copy-key": "Copiar clave", + "send-code": "Enviar código", + "email-label": "Email", + "email-description": "Introduce un email para usarlo como autenticador.", + "sms-description": "Introduce un número de teléfono para usarlo como autenticador.", + "backup-code-description": "Imprime los códigos para tenerlos a mano cuando necesites usarlos para iniciar sesión en tu cuenta. Puedes usar cada código de respaldo una sola vez.", + "backup-code-warn": "Una vez que salgas de esta página, estos códigos no podrán mostrarse de nuevo. Guárdalos de forma segura usando las opciones de abajo.", + "download-txt": "Descargar (txt)", + "print": "Imprimir", + "verification-code": "Código de 6 dígitos", + "verification-code-invalid": "Formato de código de verificación no válido", + "verification-code-incorrect": "El código de verificación es incorrecto", + "verification-code-many-request": "Demasiadas solicitudes para comprobar el código de verificación", + "scan-qr-code": "Escanea este código QR con tu aplicación de verificación", + "phone-input": { + "phone-input-label": "Número de teléfono", + "phone-input-required": "El número de teléfono es obligatorio", + "phone-input-validation": "El número de teléfono no es válido o no es posible", + "phone-input-pattern": "Número de teléfono no válido. Debe estar en formato E.164, p. ej. {{phoneNumber}}", + "phone-input-hint": "Número de teléfono en formato E.164, p. ej. {{phoneNumber}}" + } }, "mobile": { "add-application": "Agregar aplicación", @@ -4175,6 +4717,7 @@ "api-usage-limit": "Límite de uso de API", "device-activity": "Actividad del dispositivo", "entities-limit": "Límite de entidades", + "entities-limit-increase-request": "Solicitud de aumento del límite de entidades", "entity-action": "Acción de entidad", "general": "General", "rule-engine-lifecycle-event": "Evento del ciclo de vida del motor de reglas", @@ -4392,6 +4935,12 @@ "at-least": "Al menos:", "character": "{ count, plural, =1 {1 carácter} other {# caracteres} }", "digit": "{ count, plural, =1 {1 dígito} other {# dígitos} }", + "password-tooltip-min-length": "Al menos {{minimumLength}} caracteres", + "password-tooltip-max-length": "Como máximo {{maximumLength}} caracteres", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} carácter en mayúscula", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} carácter en minúscula", + "password-tooltip-digit": "{{minimumDigits}} número", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} carácter especial", "incorrect-password-try-again": "Contraseña incorrecta. Inténtalo de nuevo", "lowercase-letter": "{ count, plural, =1 {1 letra minúscula} other {# letras minúsculas} }", "new-passwords-not-match": "La nueva contraseña no coincide", @@ -4450,7 +4999,8 @@ "additional-info": "Información adicional (JSON)", "invalid-additional-info": "No se pudo analizar el JSON de información adicional.", "no-relations-text": "No se encontraron relaciones", - "not": "No" + "not": "No", + "copy-type": "Copiar tipo" }, "resource": { "add": "Agregar recurso", @@ -5400,7 +5950,8 @@ }, "time-series": "Serie temporal", "latest": "Últimos valores", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Campos calculados y reglas de alarma" }, "save-attribute": { "processing-settings": "Configuraciones de procesamiento", @@ -5595,7 +6146,8 @@ "bad-request-params": "Parámetros de solicitud incorrectos", "item-not-found": "Elemento no encontrado", "too-many-requests": "Demasiadas solicitudes", - "too-many-updates": "Demasiadas actualizaciones" + "too-many-updates": "Demasiadas actualizaciones", + "entities-limit-exceeded": "Se ha superado el límite de entidades" }, "tenant": { "tenant": "Tenant", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Número máximo de argumentos por campo calculado", "max-arguments-per-cf-range": "El número máximo de argumentos por campo calculado no puede ser negativo", "max-arguments-per-cf-required": "Se requiere el número máximo de argumentos por campo calculado", + "max-related-level-per-argument": "Nivel máximo de relación por argumento de 'Entidades relacionadas'", + "max-related-level-per-argument-range": "El número máximo del nivel de relación por argumento de 'Entidades relacionadas' no puede ser inferior a '1'", + "max-related-level-per-argument-required": "El número máximo del nivel de relación por argumento de 'Entidades relacionadas' es obligatorio", + "min-allowed-scheduled-update-interval": "Intervalo mínimo de actualización permitido para argumentos de 'Entidades relacionadas' (segundos)", + "min-allowed-scheduled-update-interval-range": "El número mínimo del intervalo mínimo de actualización permitido no puede ser negativo", + "min-allowed-deduplication-interval": "Intervalo mínimo de deduplicación permitido (segundos)", + "min-allowed-deduplication-interval-range": "El valor del intervalo mínimo de deduplicación permitido no puede ser negativo", + "min-allowed-deduplication-interval-required": "El intervalo mínimo de deduplicación permitido es obligatorio", + "intermediate-aggregation-interval": "Intervalo de agregación intermedia (segundos)", + "intermediate-aggregation-interval-range": "El valor del intervalo de agregación intermedia no puede ser inferior a '1'", + "intermediate-aggregation-interval-required": "El intervalo de agregación intermedia es obligatorio", + "reevaluation-check-interval": "Intervalo de comprobación de reevaluación (segundos)", + "reevaluation-check-interval-range": "El valor del intervalo de comprobación de reevaluación no puede ser inferior a '1'", + "reevaluation-check-interval-required": "El intervalo de comprobación de reevaluación es obligatorio", + "alarms-reevaluation-interval": "Intervalo de reevaluación de alarmas (segundos)", + "alarms-reevaluation-interval-range": "El valor del intervalo de reevaluación de alarmas no puede ser inferior a '1'", + "alarms-reevaluation-interval-required": "El intervalo de reevaluación de alarmas es obligatorio", + "min-allowed-aggregation-interval": "Intervalo mínimo de agregación permitido (segundos)", + "min-allowed-aggregation-interval-range": "El valor del intervalo mínimo de agregación permitido no puede ser negativo", + "min-allowed-aggregation-interval-required": "El intervalo mínimo de agregación permitido es obligatorio", + "min-allowed-scheduled-update-interval-required": "El número mínimo del intervalo mínimo de actualización permitido es obligatorio", "max-state-size": "Tamaño máximo del estado en KB", "max-state-size-range": "El tamaño máximo del estado en KB no puede ser negativo", "max-state-size-required": "Se requiere el tamaño máximo del estado en KB", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Número máximo de suscripciones por usuario regular", "ws-limit-max-subscriptions-per-public-user": "Número máximo de suscripciones por usuario público", "ws-limit-updates-per-session": "Actualizaciones WS por sesión", + "relation-search-entity-limit": "Límite de entidades en la búsqueda de relaciones", + "relation-search-entity-limit-hint": "Limita el número de entidades resueltas en el último nivel de la ruta de relación. Se aplica a los argumentos de 'Entidades relacionadas' y a los campos de propagación.", + "relation-search-entity-limit-required": "El límite de entidades en la búsqueda de relaciones es obligatorio", + "relation-search-entity-limit-range": "El límite de entidades en la búsqueda de relaciones no puede ser inferior a '1'", "rate-limits": { "add-limit": "Agregar límite", "and-also-less-than": "y también menor que", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Intervalo de agrupación predeterminado", "edit-intervals-list-hint": "Se puede especificar una lista de opciones de intervalo disponibles.", "edit-grouping-intervals-list-hint": "Es posible configurar la lista de intervalos de agrupación y el intervalo de agrupación predeterminado.", - "all": "Todos" + "all": "Todos", + "save-current-settings-as-default": "Guardar la configuración actual como ventana de tiempo predeterminada", + "hide-option-from-end-users": "Ocultar opción a los usuarios finales" }, "tooltip": { "trigger": "Disparador", @@ -6648,6 +7227,7 @@ "export-attributes": "Exportar atributos", "export-credentials": "Exportar credenciales", "export-calculated-fields": "Exportar campos calculados \ny reglas de alarma", + "export-alarm-rules": "Exportar reglas de alarma", "entity-versions": "Versiones de entidad", "versions": "Versiones", "created-time": "Fecha de creación", @@ -6665,6 +7245,7 @@ "load-attributes": "Cargar atributos", "load-credentials": "Cargar credenciales", "load-calculated-fields": "Cargar campos calculados", + "load-alarm-rules": "Cargar reglas de alarma", "compare-with-current": "Comparar con actual", "diff-entity-with-version": "Diferencias con versión de entidad '{{versionName}}'", "previous-difference": "Diferencia anterior", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Escanear código QR", "make-phone-call": "Hacer llamada telefónica", "get-location": "Obtener ubicación del teléfono", - "take-screenshot": "Tomar captura de pantalla" + "take-screenshot": "Tomar captura de pantalla", + "handle-provision-success-function": "Gestionar función de aprovisionamiento correcto", + "get-location-function": "Obtener función de ubicación", + "process-launch-result-function": "Procesar función de resultado de inicio", + "get-phone-number-function": "Obtener función de número de teléfono", + "process-image-function": "Procesar función de imagen", + "process-qr-code-function": "Procesar función de código QR", + "process-location-function": "Procesar función de ubicación", + "handle-empty-result-function": "Gestionar función de resultado vacío", + "handle-error-function": "Gestionar función de error", + "handle-non-mobile-fallback-function": "Gestionar función de alternativa para no móvil", + "save-to-gallery": "Guardar en la 'Galería de imágenes'", + "provision-type": "Tipo de aprovisionamiento", + "auto": "Automático", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Función de acción personalizada", "custom-pretty-function": "Función de acción personalizada (con plantilla HTML)", @@ -6883,7 +7480,8 @@ "marker": "Marcador", "polygon": "Polígono", "rectangle": "Rectángulo", - "circle": "Círculo" + "circle": "Círculo", + "polyline": "Polilínea" }, "place-map-item": "Colocar elemento en el mapa", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Continuar dibujo del polígono", "finish-draw-polygon": "Finalizar dibujo del polígono", "start-draw-circle": "Iniciar dibujo del círculo", - "finish-draw-circle": "Finalizar dibujo del círculo" + "finish-draw-circle": "Finalizar dibujo del círculo", + "start-draw-polyline": "Iniciar dibujo de polilínea", + "finish-draw-polyline": "Finalizar dibujo de polilínea" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Retardo de la animación de actualización" }, "chart-axis": { + "limit": "Límite", + "source": "Fuente", + "key-value": "Clave / Valor", + "value-required": "El valor es obligatorio.", + "entity-key-required": "La clave de entidad es obligatoria.", + "key-required": "La clave es obligatoria.", + "scale-limits": "Límites de escala", + "scale-appearance": "Apariencia de escala", "scale": "Escala", "scale-min": "mín", "scale-max": "máx", @@ -8005,7 +8613,10 @@ "add-radio-option": "Agregar opción de radio", "radio-label-position": "Posición de la etiqueta", "radio-label-position-before": "Antes", - "radio-label-position-after": "Después" + "radio-label-position-after": "Después", + "save-image": "Guardar imagen", + "save-to-gallery": "Guardar automáticamente las imágenes capturadas en la Galería de imágenes", + "public-image": "Hace la imagen disponible para cualquier usuario no autorizado" }, "invalid-qr-code-text": "Texto de entrada no válido para el código QR. La entrada debe ser de tipo cadena", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Viajes", "markers": "Marcadores", "polygons": "Polígonos", - "circles": "Círculos" + "circles": "Círculos", + "polylines": "Polilíneas" }, "data-layer": { "source": "Fuente", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Círculo para '{{entityName}}': haz clic para finalizar y guardar el círculo", "finish-circle-hint": "Círculo: haz clic para finalizar el dibujo" }, + "polyline": { + "polyline-key": "Clave de polilínea", + "polyline-key-required": "Clave de polilínea obligatoria", + "no-polylines": "No hay polilíneas configuradas", + "add-polylines": "Añadir polilínea", + "polyline-configuration": "Configuración de polilínea", + "remove-polyline": "Eliminar polilínea", + "edit": "Editar polilínea", + "cut": "Cortar área de polilínea", + "rotate": "Rotar polilínea", + "remove-polyline-for": "Eliminar polilínea para '{{entityName}}'", + "draw-polyline": "Dibujar polilínea", + "polyline-place-first-point-hint-with-entity": "Polilínea para '{{entityName}}': haz clic para colocar el primer punto", + "polyline-place-first-point-hint": "Polilínea: haz clic para colocar el primer punto", + "finish-polyline-hint-with-entity": "Polilínea para '{{entityName}}': haz clic para terminar de dibujar", + "finish-polyline-hint": "Polilínea: haz clic para terminar de dibujar", + "polyline-place-first-point-cut-hint": "Haz clic para colocar el primer punto", + "finish-polyline-cut-hint": "Haz clic en el primer marcador para finalizar y guardar" + }, "select-entity": "Seleccionar entidad", "select-entity-hint": "Sugerencia: después de la selección haz clic en el mapa para establecer la posición" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Mostrar espacio vacío en lugar del botón de acción oculto", "dont-reserve-space-hidden-action": "No reservar espacio para botones de acción ocultos", "display-timestamp": "Marca de tiempo", + "timestamp-column-name": "Marca de tiempo", "display-pagination": "Mostrar paginación", "default-page-size": "Tamaño de página predeterminado", "page-step-settings": "Configuraciones de pasos de página", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Debe especificarse al menos una columna de alarma", "table-tabs": "Pestañas de la tabla", "show-cell-actions-menu-mobile": "Mostrar menú desplegable de acciones de celda en modo móvil", - "disable-sorting": "Deshabilitar ordenamiento" + "disable-sorting": "Deshabilitar ordenamiento", + "sort-by": "Ordenar pestañas por", + "sort-timestamp-option": "Hora de creación" }, "latest-chart": { "total": "Total", @@ -9491,11 +10125,28 @@ "content": "

    Al crear tableros para el usuario final, un usuario customer solo podrá ver sus propios dispositivos y los datos de otro customer estarán ocultos.

    Sigue la documentación para saber cómo hacerlo:

    " } } + }, + "api-usage": { + "api-usage": "Uso de la API", + "label": "Etiqueta", + "state-name": "Nombre del estado", + "status": "Estado", + "status-required": "El estado es obligatorio.", + "limit": "Límite máximo", + "limit-required": "El límite máximo es obligatorio.", + "current-number": "Número actual", + "current-number-required": "El número actual es obligatorio.", + "add-key": "Añadir clave", + "no-key": "Sin clave", + "delete-key": "Eliminar clave", + "target-dashboard-state": "Estado del tablero objetivo", + "go-to-main-state": "Ir a la vista predeterminada" } }, "icon": { "icon": "Icono", "icons": "Iconos", + "custom": "Personalizado", "select-icon": "Seleccionar icono", "material-icons": "Iconos de Material", "show-all": "Mostrar todos los iconos", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "de" }, "language": { + "auto": "Auto", "language": "Idioma" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json index 4ab3e8a059..dd15a3d7d0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json @@ -1131,7 +1131,6 @@ "create-password": "ايجاد رمز عبور", "passwords-mismatch-error": "!رمزهاي عبور وارد شده بايد مشابه باشند", "password-again": "رمز عبور دوباره", - "sign-in": "لطفا وارد شويد", "username": "(نام کاربري (پست الکترونيک", "remember-me": "مرا به خاطر داشته باش", "forgot-password": "رمز عبور را فراموش کرده ايد؟", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index 6406930aab..15fa9b5586 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -78,6 +78,7 @@ "show-more": "Afficher plus", "dont-show-again": "Ne plus afficher", "see-documentation": "Voir la documentation", + "see-debug-events": "Voir les événements de débogage", "clear": "Effacer", "upload": "Téléverser", "delete-anyway": "Supprimer quand même", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Authentification à deux facteurs", "available-providers": "Fournisseurs disponibles", + "available-providers-required": "Au moins un fournisseur 2FA doit être configuré.", "issuer-name": "Nom de l'émetteur", "issuer-name-required": "Le nom de l'émetteur est requis.", "max-verification-failures-before-user-lockout": "Nombre maximal d'échecs de vérification avant verrouillage de l'utilisateur", @@ -513,7 +515,9 @@ "verification-message-template-required": "Le modèle de message de vérification est requis.", "within-time": "Délai (sec)", "within-time-pattern": "Le délai doit être un entier positif.", - "within-time-required": "Le délai est requis." + "within-time-required": "Le délai est requis.", + "force-2fa": "Appliquer l’authentification à deux facteurs", + "enforce-for": "Appliquer à" }, "jwt": { "security-settings": "Paramètres de sécurité JWT", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarme", + "alarm-list": "Liste des alarmes", "alarms": "Alarmes", "all-alarms": "Toutes les alarmes", "select-alarm": "Sélectionner une alarme", @@ -649,7 +654,16 @@ "alarm-type": "Type d'alarme", "enter-alarm-type": "Entrer le type d'alarme", "no-alarm-types-matching": "Aucun type d'alarme correspondant à '{{entitySubtype}}' n'a été trouvé.", - "alarm-type-list-empty": "Aucun type d'alarme sélectionné." + "alarm-type-list-empty": "Aucun type d'alarme sélectionné.", + "system-comments": { + "acked-by-user": "L’alarme a été acquittée par l’utilisateur {{userName}}", + "cleared-by-user": "L’alarme a été effacée par l’utilisateur {{userName}}", + "assigned-to-user": "L’alarme a été assignée par l’utilisateur {{userName}} à l’utilisateur {{assigneeName}}", + "unassigned-to-user": "L’alarme a été désassignée par l’utilisateur {{userName}}", + "unassigned-from-deleted-user": "L’alarme a été désassignée car l’utilisateur {{userName}} a été supprimé", + "comment-deleted": "L’utilisateur {{userName}} a supprimé son commentaire", + "severity-changed": "La sévérité de l’alarme a été mise à jour de {{oldSeverity}} à {{newSeverity}}" + } }, "alarm-activity": { "add": "Ajouter un commentaire...", @@ -754,6 +768,7 @@ "name-max-length": "Le nom doit contenir moins de 256 caractères", "label-max-length": "L'étiquette doit contenir moins de 256 caractères", "description": "Description", + "description-required": "La description est requise.", "type": "Type", "type-required": "Le type est requis.", "details": "Détails", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Activité mensuelle des alarmes créées", "data-points": "Points de données", "data-points-storage-days": "Durée de conservation des points de données (jours)", + "data-points-storage-days-hourly-activity": "Activité horaire des jours de stockage des points de données", + "data-points-storage-days-daily-activity": "Activité quotidienne des jours de stockage des points de données", + "data-points-storage-days-monthly-activity": "Activité mensuelle des jours de stockage des points de données", "device-api": "API des appareils", "email": "E-mail", "email-messages": "Messages e-mail", @@ -900,6 +918,7 @@ "rule-node": "Nœud de règle", "sms": "SMS", "sms-messages": "Messages SMS", + "sms-messages-hourly-activity": "Activité horaire des messages SMS", "sms-messages-daily-activity": "Activité quotidienne des SMS", "sms-messages-monthly-activity": "Activité mensuelle des SMS", "successful": "${entityName} Réussis", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Activité horaire de persistance", "telemetry-persistence-monthly-activity": "Activité mensuelle de persistance", "transport": "Transport", + "transport-msg-hourly-activity": "Activité horaire des messages de transport", + "transport-msg-daily-activity": "Activité quotidienne des messages de transport", + "transport-msg-monthly-activity": "Activité mensuelle des messages de transport", "transport-daily-activity": "Activité quotidienne de transport", "transport-data-points": "Points de données de transport", + "transport-data-points-hourly-activity": "Activité horaire des points de données de transport", + "transport-data-points-daily-activity": "Activité quotidienne des points de données de transport", + "transport-data-points-monthly-activity": "Activité mensuelle des points de données de transport", "view-details": "Voir les détails", "view-statistics": "Voir les statistiques", - "transport-messages": "Messages de transport" + "transport-messages": "Messages de transport", + "transport-messages-hourly-activity": "Activité horaire des messages de transport", + "transport-data-point-hourly-activity": "Activité horaire du point de données de transport", + "javascript-function-executions": "Exécutions de fonctions JavaScript", + "javascript-function-executions-hourly-activity": "Activité horaire des exécutions de fonctions JavaScript", + "javascript-function-executions-daily-activity": "Activité quotidienne des exécutions de fonctions JavaScript", + "javascript-function-executions-monthly-activity": "Activité mensuelle des exécutions de fonctions JavaScript", + "tbel-function-executions": "Exécutions de fonctions TBEL", + "tbel-function-executions-hourly-activity": "Activité horaire des exécutions de fonctions TBEL", + "tbel-function-executions-daily-activity": "Activité quotidienne des exécutions de fonctions TBEL", + "tbel-function-executions-monthly-activity": "Activité mensuelle des exécutions de fonctions TBEL", + "created-reports": "Rapports créés", + "created-reports-hourly-activity": "Activité horaire des rapports créés", + "created-reports-daily-activity": "Activité quotidienne des rapports créés", + "created-reports-monthly-activity": "Activité mensuelle des rapports créés", + "emails": "Emails", + "emails-hourly-activity": "Activité horaire des Emails", + "emails-daily-activity": "Activité quotidienne des Emails", + "emails-monthly-activity": "Activité mensuelle des Emails", + "status": { + "enabled": "Activé", + "disabled": "Désactivé", + "warning": "Avertissement" + } }, "api-limit": { "cassandra-write-queries-core": "Requêtes d’écriture Cassandra via l’API REST", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Messages montants Edge", "edge-uplink-messages-per-edge": "Messages montants Edge par instance" }, + "api-key": { + "api-key": "Clé API", + "api-keys": "Clés API", + "delete-api-key-title": "Êtes-vous sûr de vouloir supprimer la clé API '{{name}}' ?", + "delete-api-key-text": "Attention : après confirmation, la clé deviendra irrécupérable.", + "delete-api-keys-title": "Êtes-vous sûr de vouloir supprimer { count, plural, =1 {1 clé API} other {# clés API} } ?", + "delete-api-keys-text": "Attention : après confirmation, toutes les clés sélectionnées deviendront irrécupérables.", + "expiration-date": "Date d’expiration", + "date": "date", + "description": "Description", + "disable": "Désactiver", + "edit-description": "Modifier la description", + "enable": "Activer la clé API ", + "expiration-time": "Heure d’expiration", + "expiration-time-never": "Jamais", + "expiration-time-custom": "Personnalisé", + "generate": "Générer", + "generate-title": "Générer une clé API", + "generate-text": "Remarque : la clé API hérite des autorisations de l’utilisateur pour lequel elle est créée.", + "generated-api-key-title": "Clé API générée. Vérifions la connectivité !", + "generated-api-key-copy": "Assurez-vous de copier et d’enregistrer votre clé API maintenant, car vous ne pourrez plus l’afficher.", + "generated-api-key-command": "Utilisez les instructions suivantes pour vérifier la connectivité. En résultat, vous devriez recevoir les informations de l’utilisateur actuel :", + "generated-api-key-insecure-url": "L’exécution de commandes via une connexion HTTP non sécurisée enverra votre clé API sans chiffrement, la rendant vulnérable à l’interception.", + "list": "{ count, plural, =1 {Une clé API} other {Liste de # clés API} }", + "manage": "Gérer", + "manage-api-keys": "Gérer les clés API", + "no-found": "Aucune clé API trouvée", + "selected-api-keys": "{ count, plural, =1 {1 clé API} other {# clés API} } sélectionné(s)", + "search": "Rechercher des clés API", + "status": "Statut", + "status-active": "Actif", + "status-inactive": "Inactif", + "status-expired": "Expiré" + }, "audit-log": { "audit": "Audit", "audit-logs": "Journaux d'audit", @@ -991,7 +1073,11 @@ "type-provision-failure": "Le provisionnement de l'appareil a échoué", "type-timeseries-updated": "Télémétrie mise à jour", "type-timeseries-deleted": "Télémétrie supprimée", - "type-sms-sent": "SMS envoyé" + "type-sms-sent": "SMS envoyé", + "any-type": "Tout type", + "audit-log-filter-title": "Filtre du journal d’audit", + "filter-title": "Filtre", + "filter-types": "Types de journal d’audit" }, "debug-settings": { "label": "Configuration de débogage", @@ -1009,15 +1095,29 @@ "expression": "Expression", "no-found": "Aucun champ calculé trouvé", "list": "{ count, plural, =1 {Un champ calculé} other {Liste de # champs calculés} }", + "add": "Ajouter un champ calculé", "selected-fields": "{ count, plural, =1 {1 champ calculé} other {# champs calculés} } sélectionné(s)", "type": { "simple": "Simple", - "script": "Script" + "simple-hint": "Calcul arithmétique simple basé sur les arguments d’entrée.", + "script": "Script", + "script-hint": "Calcul sur des arguments définis à l’aide d’un script TBEL.", + "geofencing": "Géorepérage", + "geofencing-hint": "Évaluation de la position GPS de l’entité et des transitions par rapport aux groupes de zones de géorepérage configurés.", + "propagation": "Propagation", + "propagation-hint": "Propagation des données vers les entités parentes ou enfants en fonction de la direction et du type de relation.", + "related-entities-aggregation": "Agrégation des entités associées", + "related-entities-aggregation-hint": "Agrégation des dernières données des entités associées.", + "time-series-data-aggregation": "Agrégation des données de séries temporelles", + "time-series-data-aggregation-hint": "Agrégation des données historiques d’une entité actuelle." }, + "preview": "Aperçu", "arguments": "Arguments", "decimals-by-default": "Décimales par défaut", "debugging": "Débogage du champ calculé", + "calculated-field-details": "Détails du champ calculé", "argument-name": "Nom de l'argument", + "name": "Nom", "datasource": "Source de données", "add-argument": "Ajouter un argument", "test-script-function": "Tester la fonction script", @@ -1029,6 +1129,8 @@ "argument-asset": "Actif", "argument-customer": "Client", "argument-tenant": "Tenant actuel", + "argument-owner": "Propriétaire actuel", + "argument-relation-query": "Entités associées", "argument-type": "Type d'argument", "attribute": "Attribut", "copy-argument-name": "Copier le nom de l'argument", @@ -1042,12 +1144,14 @@ "shared-attributes": "Attributs partagés", "attribute-key": "Clé d'attribut", "default-value": "Valeur par défaut", + "default-value-required": "La valeur par défaut est requise.", "limit": "Valeurs max.", "time-window": "Fenêtre temporelle", "customer-name": "Nom du client", "asset-name": "Nom de l’actif", "timeseries": "Séries temporelles", "output": "Sortie", + "output-hint": "Définit la façon dont la sortie est traitée.", "create": "Créer un nouveau champ calculé", "file": "Fichier de champ calculé", "invalid-file-error": "Format de fichier invalide. Veuillez vous assurer que le fichier est un JSON valide.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Attention, après confirmation, tous les champs calculés sélectionnés seront supprimés et les données associées seront irrécupérables.", "test-with-this-message": "Tester avec ce message", "use-latest-timestamp": "Utiliser le dernier horodatage", + "entity-coordinates": "Coordonnées de l’entité", + "latitude-time-series-key": "Clé de série temporelle de latitude", + "latitude-time-series-key-required": "La clé de série temporelle de latitude est requise.", + "longitude-time-series-key": "Clé de série temporelle de longitude", + "longitude-time-series-key-required": "La clé de série temporelle de longitude est requise.", + "geofencing-zone-groups": "Groupes de zones de géorepérage", + "geofencing-zone-groups-settings": "Paramètres des groupes de zones de géorepérage", + "target-zone": "Zone cible", + "perimeter-key": "Clé de périmètre", + "report-strategy": "Stratégie de rapport", + "no-zone-configured": "Au moins une zone est requise.", + "no-zone-configured-required": "Au moins un groupe de zones doit être configuré.", + "add-zone-group": "Ajouter un groupe de zones", + "report-transition-event-only": "Événements de transition uniquement", + "report-presence-status-only": "Statut de présence uniquement", + "report-transition-event-and-presence": "Statut de présence et événements de transition", + "perimeter-attribute-key": "Clé d’attribut de périmètre", + "perimeter-attribute-key-required": "La clé d’attribut de périmètre est requise.", + "perimeter-attribute-key-pattern": "La clé d’attribut de périmètre n’est pas valide.", + "entity-zone-relationship": "Chemin de l’entité vers les zones", + "direction": "Direction de la relation", + "direction-from": "De l’entité vers la zone", + "direction-to": "De la zone vers l’entité", + "relation-type": "Type de relation", + "create-relation-with-matched-zones": "Créer des relations pour l’entité source avec les zones correspondantes", + "relation-level": "Niveau de relation", + "fetch-last-available-level": "Récupérer uniquement le dernier niveau disponible", + "zone-group-refresh-interval": "Intervalle d’actualisation des groupes de zones", + "copy-zone-group-name": "Copier le nom du groupe de zones", + "open-details-page": "Ouvrir la page des détails de l’entité", + "level": "Niveau", + "direction-level": "Direction", + "direction-up": "Vers le haut", + "direction-up-parent": "Vers le haut jusqu’au parent", + "direction-down": "Vers le bas", + "direction-down-child": "Vers le bas jusqu’à l’enfant", + "add-level": "Ajouter un niveau", + "delete-level": "Supprimer le niveau", + "no-level": "Aucun niveau configuré", + "levels-required": "Au moins un niveau doit être configuré.", + "max-allowed-levels-error": "Le niveau de relation dépasse le maximum autorisé.", + "propagation-path-related-entities": "Chemin de propagation vers les entités associées", + "propagate-type": { + "arguments-only": "Arguments uniquement", + "expression-result": "Résultat du calcul" + }, + "script": "Script", + "data-propagate": "Données à propager", + "output-key": "Clé de sortie", + "copy-output-key": "Copier la clé de sortie", + "aggregation-path-related-entities": "Chemin d’agrégation vers les entités associées", + "deduplication-interval": "Intervalle de déduplication", + "deduplication-interval-min": "L’intervalle de déduplication doit être d’au moins {{ sec }} secondes.", + "deduplication-interval-hint": "Temps minimum entre les agrégations de télémétrie.", + "deduplication-interval-required": "L’intervalle de déduplication est requis.", + "calculated-field-filter-title": "Filtre des champs calculés", + "filter-title": "Filtre", + "calculated-field-types": "Types de champs calculés", + "events": "Événements", + "any-type": "Tout type", + "metrics": { + "metrics": "Métriques", + "metrics-empty": "Au moins une métrique doit être configurée.", + "metric-name": "Nom de la métrique", + "metric-name-required": "Le nom de la métrique est requis.", + "metric-name-pattern": "Le nom de la métrique n’est pas valide.", + "metric-name-duplicate": "Une métrique portant ce nom existe déjà.", + "metric-name-max-length": "Le nom de la métrique doit comporter moins de 256 caractères.", + "metric-name-forbidden": "Le nom de la métrique est réservé et ne peut pas être utilisé.", + "copy-metric-name": "Copier le nom de la métrique", + "argument-name": "Nom de l’argument", + "aggregation": "Agrégation", + "aggregation-type": { + "avg": "Moyenne", + "min": "Minimum", + "max": "Maximum", + "sum": "Somme", + "count": "Nombre", + "count-unique": "Nombre d’éléments uniques" + }, + "filtered": "Filtré", + "value-source": "Source de la valeur", + "value-source-hint": "Définit comment la valeur à agréger est obtenue.", + "value-source-type": { + "key": "Clé", + "function": "Fonction" + }, + "no-metrics-configured": "Au moins une métrique est requise.", + "add-metric": "Ajouter une métrique", + "max-metrics": "Nombre maximum de métriques atteint.", + "metric-settings": "Paramètres de la métrique", + "filter": "Filtre", + "filter-hint": "Permet de filtrer les entités lors de l’agrégation. La fonction de filtre doit renvoyer une valeur booléenne et peut utiliser tous les arguments configurés." + }, + "output-strategy": { + "strategy": "Stratégie", + "process-right-away": "Traiter immédiatement", + "process-rule-chains": "Traiter via les chaînes de règles", + "save-time-series": "Enregistrer dans les séries temporelles", + "save-database": "Enregistrer dans la base de données", + "save-latest-values": "Enregistrer dans les dernières valeurs", + "send-web-sockets": "Envoyer vers WebSockets", + "save-calculated-fields": "Envoyer vers les champs calculés", + "update-attribute-only-on-value-change": "Mettre à jour l’attribut uniquement en cas de changement de valeur", + "send-attributes-updated-notification": "Envoyer une notification de mise à jour des attributs", + "ttl": "TTL personnalisé", + "ttl-required": "Le TTL est requis", + "ttl-min": "Seul un TTL minimum de 0 est autorisé", + "processing-parameters": "Paramètres de traitement", + "hint": { + "strategy": "Contrôle si le résultat est traité immédiatement ou envoyé à une chaîne de règles pour un traitement supplémentaire.", + "processing-options": "Options de traitement", + "update-attribute-only-on-value-change": "Met à jour l’attribut à chaque message entrant, que la valeur ait changé ou non. Cela augmente l’utilisation de l’API et réduit les performances.", + "update-attribute-only-on-value-change-enabled": "Met à jour l’attribut uniquement lorsque la valeur change. Si la valeur est inchangée, les horodatages ne sont pas mis à jour et les notifications ne sont pas envoyées.", + "send-attributes-updated-notification": "Envoie un événement « Attributes Updated » à la chaîne de règles par défaut.", + "save-time-series": "Enregistre les données de séries temporelles dans la table ts_kv de la base de données.", + "save-database": "Enregistre les données d’attribut dans la base de données.", + "save-latest-values": "Met à jour les données de séries temporelles dans la table ts_kv_latest de la base de données si la nouvelle valeur est plus récente.", + "send-web-sockets-attribute": "Notifie les abonnements WebSocket des mises à jour des données d’attribut.", + "send-web-sockets-time-series": "Notifie les abonnements WebSocket des mises à jour des données de séries temporelles.", + "save-calculated-fields-attribute": "Notifie les champs calculés des mises à jour des données d’attribut.", + "save-calculated-fields-time-series": "Notifie les champs calculés des mises à jour des données de séries temporelles.", + "ttl": "Définit la durée de conservation des données de séries temporelles. Si désactivé, le TTL du profil de locataire est utilisé." + } + }, + "aggregate-interval-type": "Type d’intervalle d’agrégation", + "aggregate-interval-value": "Valeur de l’intervalle d’agrégation", + "aggregate-interval-value-required": "La valeur de l’intervalle d’agrégation est requise.", + "aggregate-interval-value-min": "La valeur de l’intervalle d’agrégation doit être d’au moins { sec, plural, =0 {0 seconde} =1 {1 seconde} other {# secondes} }.", + "aggregate-interval-value-step-multiple-of": "La valeur de l’intervalle d’agrégation doit être un diviseur ou un multiple d’un jour.", + "aggregate-period": { + "hour": "Heure", + "day": "Jour", + "week": "Semaine (lun. - dim.)", + "week-sun-sat": "Semaine (dim. - sam.)", + "month": "Mois", + "quarter": "Trimestre", + "year": "Année", + "custom": "Personnalisé" + }, + "aggregate-period-hint-offset": "Votre intervalle d’agrégation sera : {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Votre intervalle d’agrégation sera : {{ interval }} et ainsi de suite.", + "entity-aggregation": { + "argument-hint": "Les données seront récupérées à partir de l’entité actuelle.", + "argument-title-hint": "Définit les arguments d’entrée utilisés pour l’agrégation.", + "argument-setting-hint": "La dernière télémétrie est le seul type d’argument disponible pour ce champ calculé.", + "aggregation-interval": "Intervalle d’agrégation", + "aggregation-interval-hint": "Définit la fréquence d’exécution de l’agrégation. Exemple : toutes les 1 heures, les données sont agrégées à 00:00, 01:00, 02:00, etc. Les résultats de l’agrégation sont stockés avec l’horodatage correspondant au début de l’intervalle d’agrégation.", + "apply-offset": "Appliquer un décalage à l’intervalle d’agrégation", + "apply-offset-hint": "Définit de combien décaler le début de chaque période d’agrégation (p. ex., +10 minutes – 00:10, 01:10).", + "offset-value": "Valeur du décalage", + "offset-value-required": "La valeur du décalage est requise.", + "offset-value-min": "La valeur du décalage doit être un entier positif.", + "offset-value-max": "La valeur du décalage doit être inférieure à la valeur de l’intervalle d’agrégation.", + "wait-delay": "Appliquer un délai d’attente pour la télémétrie retardée", + "wait-delay-hint": "Définit combien de temps attendre la télémétrie retardée après la fin de l’intervalle. Si une telle télémétrie arrive, le résultat de cet intervalle sera recalculé.", + "duration": "Durée", + "duration-required": "La durée est requise.", + "duration-min": "La durée doit être d’au moins 1 minute.", + "duration-hint": "Durée d’attente des données retardées après la fin de l’intervalle.", + "produce-intermediate-result": "Produire un résultat intermédiaire", + "produce-intermediate-result-hint": "Calcule les métriques pendant l’intervalle actuel afin de produire un résultat intermédiaire. Les mises à jour n’ont lieu pas plus d’une fois toutes les {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Un champ calculé de type simple ne doit pas contenir de clés avec le type « Série temporelle glissante ».", + "arguments-propagate-arguments-with-rolling": "Le type « Série temporelle glissante » est incompatible avec la propagation « Arguments uniquement ».", + "arguments-propagate-argument-entity-type": "Le type d’entité est incompatible avec la propagation « Arguments uniquement ».", + "arguments-propagate-argument-must-current-entity": "Au moins un argument doit être configuré avec le type d’entité source « Entité actuelle ».", "arguments-empty": "Au moins un argument doit être spécifié.", "expression-required": "L'expression est requise.", "expression-invalid": "L'expression est invalide.", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Un argument avec ce nom existe déjà.", "argument-name-max-length": "Le nom de l'argument doit contenir moins de 256 caractères.", "argument-name-forbidden": "Ce nom d'argument est réservé et ne peut pas être utilisé.", + "output-key-required": "La clé de sortie est requise.", + "output-key-pattern": "La clé de sortie n’est pas valide.", + "output-key-duplicate": "Une clé portant ce nom existe déjà.", + "output-key-max-length": "La clé de sortie doit comporter moins de 256 caractères.", + "output-key-forbidden": "La clé de sortie est réservée et ne peut pas être utilisée.", + "entity-type-required": "Le type d’entité est requis.", + "name-required": "Le nom est requis.", + "name-pattern": "Le nom n’est pas valide.", + "name-duplicate": "Un nom identique existe déjà.", + "name-max-length": "Le nom doit comporter moins de 256 caractères.", + "name-forbidden": "Le nom est réservé et ne peut pas être utilisé.", "argument-type-required": "Le type d'argument est requis.", "max-args": "Nombre maximal d'arguments atteint.", "decimals-range": "Le nombre de décimales par défaut doit être compris entre 0 et 15.", "expression": "L'expression par défaut montre comment convertir une température de Fahrenheit en Celsius.", "arguments-entity-not-found": "L'entité cible de l'argument est introuvable.", - "use-latest-timestamp": "Si activé, la valeur calculée sera enregistrée avec l’horodatage le plus récent parmi la télémétrie des arguments, au lieu de celui du serveur." + "use-latest-timestamp": "Si activé, la valeur calculée sera enregistrée avec l’horodatage le plus récent parmi la télémétrie des arguments, au lieu de celui du serveur.", + "entity-coordinates": "Spécifiez les clés de série temporelle qui fournissent les coordonnées GPS de l’entité (latitude et longitude).", + "geofencing-zone-groups": "Définissez un ou plusieurs groupes de zones de géorepérage à vérifier (p. ex. « allowedZones », « restrictedZones »). Chaque groupe doit avoir un nom unique, utilisé comme préfixe pour les clés de télémétrie de sortie du champ calculé.", + "perimeter-attribute-key": "Définissez la clé d’attribut qui contient la définition du périmètre de la zone de géorepérage. Le périmètre est toujours récupéré à partir des attributs côté serveur de l’entité de zone.", + "report-strategy": "Le statut de présence indique si l’entité est actuellement À L’INTÉRIEUR ou À L’EXTÉRIEUR du groupe de zones. Les événements de transition indiquent quand l’entité est ENTRÉE dans le groupe de zones ou l’a QUITTÉ.", + "create-relation-with-matched-zones": "Créer et maintenir automatiquement des relations entre l’entité et les zones dans lesquelles elle se trouve actuellement. Les relations sont supprimées lorsque l’entité quitte une zone et créées lorsqu’elle entre dans une nouvelle zone.", + "relation-type-required": "Le type de relation est requis.", + "relation-level-required": "Le niveau de relation est requis.", + "relation-level-min": "La valeur minimale du niveau de relation est 1.", + "relation-level-max": "La valeur maximale du niveau de relation est {{max}}.", + "geofencing-empty": "Au moins un groupe de zones doit être configuré.", + "geofencing-entity-not-found": "L’entité cible de géorepérage est introuvable.", + "max-geofencing-zone": "Nombre maximum de zones de géorepérage atteint.", + "zone-group-refresh-interval": "Définit la fréquence d’actualisation des groupes de zones configurés via des entités associées.", + "zone-group-refresh-interval-required": "L’intervalle d’actualisation des groupes de zones est requis.", + "zone-group-refresh-interval-min": "L’intervalle d’actualisation des groupes de zones doit être d’au moins {{ min }} secondes.", + "propagation-path-related-entities": "Définit un chemin direct à un seul niveau vers une entité associée, selon la direction sélectionnée et le type de relation. Seules les relations entre les entités d’appareil, d’actif, de client et de locataire sont prises en charge. Le nombre maximal d’entités résolues par le chemin de relation est de {{ max }}.", + "data-propagate": "Définit les données à propager à partir des arguments configurés ci-dessous. « Arguments uniquement » utilise directement les données récupérées, tandis que « Résultat de l’expression » calcule une nouvelle valeur à partir de ces données.", + "aggregation-path-related-entities": "Définit un chemin d’agrégation à un seul niveau via des relations directes avec des entités parentes ou enfants, selon la direction et le type de relation. Seules les relations entre les entités d’appareil, d’actif, de client et de locataire sont prises en charge. Le nombre maximal d’entités résolues par le chemin de relation est de {{ max }}.", + "arguments-aggregation": "Définit les arguments d’entrée utilisés pour le filtrage et l’agrégation.", + "setting-arguments-aggregation": "Les données seront récupérées à partir des entités associées configurées dans le chemin d’agrégation.", + "metrics": "Définit les métriques agrégées en fonction des arguments configurés.", + "entity-aggregation-metrics": "Définit les métriques agrégées en fonction des arguments configurés sur les intervalles de temps spécifiés.", + "import-invalid-calculated-field-type": "Impossible d’importer le champ calculé : structure de champ calculé non valide.", + "simple-expression-title": "Expression arithmétique qui définit la manière dont la valeur calculée est déterminée.", + "script-title": "Script TBEL qui définit la logique de calcul et les valeurs de sortie.", + "simple-arguments": "Expression arithmétique qui définit la manière dont la valeur calculée est déterminée.", + "script-arguments": "Définit les arguments d’entrée disponibles pour le script." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Règles d’alarme", + "alarm-rule": "Règle d’alarme", + "alarm-rules": "Règles d’alarme", + "alarm-rules-old": "Anciennes", + "alarm-rules-actual": "Actuelles", + "severities": "Sévérités", + "cleared": "Condition d’effacement", + "delete-title": "Êtes-vous sûr de vouloir supprimer la règle d’alarme « {{title}} » ?", + "delete-text": "Attention : après confirmation, la règle d’alarme et toutes les données associées deviendront irrécupérables.", + "delete-multiple-title": "Êtes-vous sûr de vouloir supprimer { count, plural, =1 {1 règle d’alarme} other {# règles d’alarme} } ?", + "delete-multiple-text": "Attention : après confirmation, toutes les règles d’alarme sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.", + "create": "Créer une nouvelle règle d’alarme", + "add": "Ajouter une règle d’alarme", + "copy": "Copier la configuration de la règle d’alarme", + "details": "Détails de la règle d’alarme", + "no-found": "Aucune règle d’alarme trouvée", + "list": "{ count, plural, =1 {Une règle d’alarme} other {Liste de # règles d’alarme} }", + "selected-fields": "{ count, plural, =1 {1 règle d’alarme} other {# règles d’alarme} } sélectionné(s)", + "import": "Importer une règle d’alarme", + "file": "Fichier de règle d’alarme", + "export": "Exporter la règle d’alarme", + "export-failed-error": "Impossible d’exporter la règle d’alarme : {{error}}", + "entity-type": "Type d’entité", + "entity-type-required": "Le type d’entité est requis.", + "alarm-type": "Type d’alarme", + "alarm-type-hint": "Identifiant unique (p. ex. HighTempAlarm) dans le périmètre de l’émetteur de l’alarme (Appareil, Actif, etc.) afin d’éviter les conflits.", + "alarm-type-required": "Le type d’alarme est requis.", + "alarm-type-pattern": "Le type d’alarme n’est pas valide.", + "alarm-type-max-length": "Le type d’alarme doit comporter moins de 256 caractères.", + "clear-alarm": "Effacer l’alarme", + "value-argument": "Argument", + "value-argument-required": "L’argument est requis.", + "static-settings": "Paramètres statiques", + "configuration": "Configuration", + "static-schedule": "Statique", + "dynamic-schedule": "Dynamique", + "operation-and": "ET", + "operation-or": "OU", + "condition-during": "Pendant {{during}}", + "condition-during-dynamic": "Pendant « {{ attribute }} »", + "condition-repeat-times": "Répète { count, plural, =1 {1 fois} other {# fois} }", + "condition-repeat-times-dynamic": "Répète « {{ attribute }} » fois", + "filter-preview": "Aperçu du filtre", + "condition-settings": "Paramètres de condition", + "static": "Statique", + "dynamic": "Dynamique", + "argument-filters": "Filtres d’arguments", + "argument-name": "Nom de l’argument", + "value-type": "Type de valeur", + "general": "Général", + "filters": "Filtres", + "date-time-hint": "L’argument doit être en millisecondes epoch. Exemple : 1698839340000 correspond à 2023-11-01 12:49:00 UTC.", + "operation": "Opération", + "value-source": "Source de la valeur", + "value": "Valeur", + "ignore-case": "Ignorer la casse", + "condition": "Condition", + "script": "Script", + "add-filter": "Ajouter un filtre d’argument", + "edit-filter": "Filtre d’argument", + "remove-filter": "Supprimer le filtre d’argument", + "no-filter": "Au moins un filtre est requis.", + "conditions": { + "simple": "Simple", + "duration": "Durée", + "repeating": "Répétition" + }, + "schedule-title": "Planification", + "edit-schedule": "Modifier la planification de l’alarme", + "schedule-type": "Type de planificateur", + "schedule-type-required": "Le type de planificateur est requis.", + "schedule": { + "any-time": "Active en permanence", + "specific-time": "Active à une heure précise", + "custom": "Personnalisé" + }, + "schedule-day": { + "monday": "Lundi", + "tuesday": "Mardi", + "wednesday": "Mercredi", + "thursday": "Jeudi", + "friday": "Vendredi", + "saturday": "Samedi", + "sunday": "Dimanche" + }, + "schedule-days": "Jours", + "schedule-time": "Heure", + "schedule-time-from": "De", + "schedule-time-to": "À", + "schedule-days-of-week-required": "Au moins un jour de la semaine doit être sélectionné.", + "tbel": "TBEL", + "expression-type": { + "simple": "Simple", + "script": "Script" + }, + "operation-type": { + "and": "ET", + "or": "OU" + }, + "filter-predicate-type": { + "string": "Chaîne", + "numeric": "Numérique", + "boolean": "Booléen", + "complex": "Complexe" + }, + "alarm-rule-additional-info": "Informations supplémentaires", + "edit-alarm-rule-additional-info": "Modifier les informations supplémentaires", + "alarm-rule-additional-info-placeholder": "Veuillez fournir ici vos commentaires et ajustements afin de les afficher dans les détails de l’alarme, sous « Informations supplémentaires »", + "alarm-rule-additional-info-hint": "Astuce : utiliser ${Nom de l’argument} pour substituer les valeurs des arguments utilisés dans la condition de la règle d’alarme.", + "alarm-rule-additional-info-icon-hint": "Utiliser le nom de l’argument pour substituer les valeurs des arguments utilisés dans la condition de la règle d’alarme.", + "alarm-rule-mobile-dashboard": "Tableau de bord mobile", + "alarm-rule-mobile-dashboard-hint": "Utilisé par l’application mobile comme tableau de bord des détails de l’alarme.", + "alarm-rule-no-mobile-dashboard": "Aucun tableau de bord sélectionné", + "alarm-rule-condition": "Condition de la règle d’alarme", + "enter-alarm-rule-condition-prompt": "Ajouter une condition", + "enter-alarm-rule-clear-condition-prompt": "Ajouter une condition d’effacement", + "edit-alarm-rule-condition": "Condition d’alarme", + "condition-type": "Type de condition", + "condition-type-hint": "Les options \"Durée\" et \"Répétition\" ne sont pas disponibles lorsque l’opération \"Manquant pendant\" est utilisée dans le filtre.", + "select-alarm-severity": "Sélectionner la sévérité de l’alarme", + "add-create-alarm-rule-prompt": "Au moins une condition de déclenchement est requise.", + "add-create-alarm-rule": "Ajouter une condition de déclenchement", + "add-clear-alarm-rule": "Ajouter une condition d’effacement", + "condition-duration": "Durée de la condition", + "condition-duration-value": "Valeur de la durée", + "condition-duration-time-unit": "Unité de temps", + "condition-duration-value-range": "La valeur de la durée doit être comprise entre 1 et 2147483647.", + "condition-duration-value-pattern": "La valeur de la durée doit être un entier.", + "condition-duration-value-required": "La valeur de la durée est requise.", + "condition-duration-time-unit-required": "L’unité de temps est requise.", + "condition-repeating-value": "Nombre d’événements", + "condition-repeating-value-hint": "La mise à jour de n’importe quel argument de la règle d’alarme sera comptabilisée comme un événement", + "condition-repeating-value-range": "Le nombre d’événements doit être compris entre 1 et 2147483647.", + "condition-repeating-value-pattern": "Le nombre d’événements doit être un entier.", + "condition-repeating-value-required": "Le nombre d’événements est requis.", + "create-conditions": "Conditions de déclenchement", + "clear-condition": "Condition d’effacement", + "no-clear-alarm-rule": "Aucune condition d’effacement configurée.", + "advanced-settings": "Paramètres avancés", + "propagate-alarm": "Propager l’alarme vers les entités associées", + "alarm-rule-relation-types-list": "Types de relation", + "alarm-rule-relation-types-list-hint": "Définit les types de relation pour filtrer les entités associées. Si non défini, l’alarme sera propagée à toutes les entités associées.", + "propagate-alarm-to-owner": "Propager l’alarme vers le propriétaire de l’entité (Client ou Locataire)", + "propagate-alarm-to-tenant": "Propager l’alarme vers le Locataire", + "alarm-rule-filter-title": "Filtre de règle d’alarme", + "filter-title": "Filtre", + "debugging": "Débogage des règles d’alarme", + "any-type": "Tout type", + "enter-alarm-rule-type": "Saisir le type d’alarme", + "no-alarm-rule-types-matching": "Aucun type d’alarme correspondant à « {{entitySubtype}} » n’a été trouvé.", + "alarm-rule-type-list-empty": "Aucun type d’alarme sélectionné.", + "alarm-rule-type-list": "Liste des types d’alarme", + "alarm-rule-entity-list": "Liste des entités", + "missing-for": "manquant pendant", + "time-unit": "Unité", + "mode": "Mode", + "type": "Type", + "value-required": "La valeur est requise.", + "min-value": "La valeur doit être supérieure ou égale à 1.", + "argument-in-use": "L’argument est utilisé comme argument général.", + "import-invalid-alarm-rule-type": "Impossible d’importer la règle d’alarme : structure de règle d’alarme non valide.", + "no-filter-preview": "Aucun filtre spécifié", + "filter-operation": { + "and": "ET", + "or": "OU" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Documentation", "time-left": "{{time}} restantes", "output": "Sortie", + "sort-asc": "Croissant", + "sort-desc": "Décroissant", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Ordre du tableau de bord dans l'application mobile", "mobile-hide": "Masquer le tableau de bord dans l'application mobile", "update-image": "Mettre à jour l'image du tableau de bord", + "update-new-version": "Téléverser une nouvelle version", + "upload-file-to-update": "Téléverser le fichier pour mettre à jour", "take-screenshot": "Prendre une capture d’écran", "select-widget-title": "Sélectionner un widget", "select-widget-value": "{{title}} : sélectionner un widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Client Bootstrap", "bootstrap-server": "Serveur Bootstrap", "lwm2m-server": "Serveur LwM2M", + "client-reboot": "Déclencheur de mise à jour d’enregistrement", + "bootstrap-reboot": "Déclencheur de requête de bootstrap", "client-publicKey-or-id": "Clé publique ou ID du client", "client-publicKey-or-id-required": "La clé publique ou l'ID du client est requis.", "client-publicKey-or-id-tooltip-psk": "L'identifiant PSK est un identifiant PSK arbitraire jusqu'à 128 octets, comme décrit dans la norme [RFC7925].\nL'identifiant PSK DOIT d'abord être converti en chaîne de caractères, puis encodé en octets en utilisant UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "L’ID court du serveur est requis.", "short-id-range": "L’ID court du serveur doit être compris entre {{ min }} et {{ max }}.", "short-id-pattern": "L’ID court doit être un entier positif.", + "short-id-pattern-bs": "L’ID court du serveur doit être uniquement null", "lifetime": "Durée d’enregistrement du client", "lifetime-required": "La durée d’enregistrement du client est requise.", "lifetime-pattern": "La durée d’enregistrement doit être un entier positif.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Toutes les ressources sont observées avec une seule requête Observe Composite (plus efficace, moins flexible)", "composite-by-object": "Composite par objets", "composite-by-object-description": "Les ressources sont regroupées par type d’objet et observées via des requêtes Observe Composite distinctes (approche équilibrée)" - } + }, + "init-attr-tel-as-obs-strategy": "Initialiser les attributs et la télémétrie en utilisant la stratégie Observe", + "init-attr-tel-as-obs-strategy-hint": "Si false - les attributs et la télémétrie sont initialisés en lisant leurs valeurs une par une.\\nSi true - les attributs et la télémétrie sont initialisés en s’abonnant à leurs valeurs via la stratégie Observe." }, "snmp": { "add-communication-config": "Ajouter une configuration de communication", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Nœuds de règle", "list-of-rulenodes": "{ count, plural, =1 {Un nœud de règle} other {Liste de # nœuds de règle} }", "rulenode-name-starts-with": "Nœuds de règle dont le nom commence par '{{prefix}}'", + "type-api-key": "Clé API", + "type-api-keys": "Clés API", "type-current-customer": "Client actuel", "type-current-tenant": "Locataire actuel", "type-current-user": "Utilisateur actuel", @@ -2656,6 +3143,7 @@ "details": "Détails de l'entité", "no-entities-prompt": "Aucune entité trouvée", "no-data": "Aucune donnée à afficher", + "show-all-columns": "Tout afficher", "columns-to-display": "Colonnes à afficher", "type-api-usage-state": "État d'utilisation de l'API", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Une application mobile} other {Liste de # applications mobiles} }", "type-mobile-app-bundle": "Pack mobile", "type-mobile-app-bundles": "Packs mobiles", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Un bundle mobile} other {Liste de # bundles mobiles} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Un bundle mobile} other {Liste de # bundles mobiles} }", + "limit-reached": "Limite atteinte", + "limit-reached-text": "Vous avez atteint la limite de {{ entities }}. Pour en ajouter davantage, veuillez demander à votre administrateur système d’augmenter votre limite de {{ entity }}.", + "request-limit-increase": "Demander une augmentation de limite", + "request-sysadmin-text": "Êtes-vous l’administrateur système ?", + "login-here": "Se connecter ici", + "to-increase-limit": "pour augmenter la limite.", + "increase-limit-request-sent-title": "Nous avons envoyé une demande automatisée à votre administrateur système pour augmenter la limite", + "increase-limit-request-sent-text": "Veuillez leur laisser un peu de temps pour examiner la demande et mettre à jour les paramètres. Vous devrez peut-être actualiser cette page pour voir les modifications." }, "entity-field": { "created-time": "Date de création", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Le lien de réinitialisation a été envoyé", "email": "Email", "invalid-email-format": "Format d'email invalide.", + "sign-in-with": "Se connecter avec {{name}}", + "sign-in-to-your-account": "Se connecter à votre compte", "or": "ou", "error": "Erreur de connexion", "verify-your-identity": "Vérifiez votre identité", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Le lien d'activation a expiré", "activation-link-expired-message": "Le lien d’activation de votre profil a expiré. Vous pouvez revenir à la page de connexion pour recevoir un nouvel email.", "reset-password-link-expired": "Le lien de réinitialisation du mot de passe a expiré", - "reset-password-link-expired-message": "Le lien de réinitialisation du mot de passe a expiré. Vous pouvez revenir à la page de connexion pour recevoir un nouvel email." + "reset-password-link-expired-message": "Le lien de réinitialisation du mot de passe a expiré. Vous pouvez revenir à la page de connexion pour recevoir un nouvel email.", + "two-fa": "Authentification à deux facteurs", + "two-fa-required": "L’authentification à deux facteurs est requise", + "set-up-verification-method": "Configurer une méthode de vérification pour continuer", + "set-up-verification-method-login": "Configurer une méthode de vérification ou se connecter", + "enable-authenticator-app": "Activer l’application d’authentification", + "enable-authenticator-app-description": "Veuillez saisir le code de sécurité de votre application d’authentification", + "enable-authenticator-sms": "Activer l’authentificateur SMS", + "enable-authenticator-sms-description": "Saisissez le code à 6 chiffres que nous venons d’envoyer à ", + "enable-authenticator-email": "Activer l’authentificateur Email", + "enable-authenticator-email-description": "Un code de sécurité a été envoyé à votre adresse email à ", + "enter-key-manually": "ou saisissez manuellement cette clé à 32 chiffres :", + "continue": "Continuer", + "confirm": "Confirmer", + "authenticator-app-success": "Application d’authentification activée avec succès", + "authenticator-app-success-description": "La prochaine fois que vous vous connecterez, vous devrez fournir un code d’authentification à deux facteurs", + "authenticator-sms-success": "Authentificateur SMS activé avec succès", + "authenticator-sms-success-description": "La prochaine fois que vous vous connecterez, vous serez invité à saisir le code de sécurité qui sera envoyé au numéro de téléphone", + "authenticator-email-success": "Authentificateur Email activé avec succès", + "authenticator-email-success-description": "La prochaine fois que vous vous connecterez, vous serez invité à saisir le code de sécurité qui sera envoyé à votre adresse email", + "authenticator-backup-code-success": "Code de secours activé avec succès", + "authenticator-backup-code-success-description": "La prochaine fois que vous vous connecterez, vous serez invité à saisir le code de sécurité ou à utiliser l’un des codes de secours.", + "add-verification-method": "Ajouter une méthode de vérification", + "get-backup-code": "Obtenir un code de secours", + "copy-key": "Copier la clé", + "send-code": "Envoyer le code", + "email-label": "Email", + "email-description": "Saisissez un email à utiliser comme authentificateur.", + "sms-description": "Saisissez un numéro de téléphone à utiliser comme authentificateur.", + "backup-code-description": "Imprimez les codes afin de les avoir à portée de main lorsque vous en aurez besoin pour vous connecter à votre compte. Vous pouvez utiliser chaque code de secours une seule fois.", + "backup-code-warn": "Une fois que vous quittez cette page, ces codes ne peuvent plus être affichés. Stockez-les en toute sécurité à l’aide des options ci-dessous.", + "download-txt": "Télécharger (txt)", + "print": "Imprimer", + "verification-code": "Code à 6 chiffres", + "verification-code-invalid": "Format du code de vérification non valide", + "verification-code-incorrect": "Le code de vérification est incorrect", + "verification-code-many-request": "Trop de demandes pour vérifier le code de vérification", + "scan-qr-code": "Scannez ce code QR avec votre application de vérification", + "phone-input": { + "phone-input-label": "Numéro de téléphone", + "phone-input-required": "Le numéro de téléphone est requis", + "phone-input-validation": "Le numéro de téléphone n’est pas valide ou n’est pas possible", + "phone-input-pattern": "Numéro de téléphone non valide. Doit être au format E.164, ex. {{phoneNumber}}", + "phone-input-hint": "Numéro de téléphone au format E.164, ex. {{phoneNumber}}" + } }, "mobile": { "add-application": "Ajouter une application", @@ -4175,6 +4717,7 @@ "api-usage-limit": "Limite d'utilisation de l'API", "device-activity": "Activité de l'appareil", "entities-limit": "Limite d'entités", + "entities-limit-increase-request": "Demande d’augmentation de la limite d’entités", "entity-action": "Action sur l'entité", "general": "Général", "rule-engine-lifecycle-event": "Événement du cycle de vie du moteur de règles", @@ -4392,6 +4935,12 @@ "at-least": "Au moins :", "character": "{ count, plural, =1 {1 caractère} other {# caractères} }", "digit": "{ count, plural, =1 {1 chiffre} other {# chiffres} }", + "password-tooltip-min-length": "Au moins {{minimumLength}} caractères", + "password-tooltip-max-length": "Au plus {{maximumLength}} caractères", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} caractère(s) en majuscule", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} caractère(s) en minuscule", + "password-tooltip-digit": "{{minimumDigits}} chiffre(s)", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} caractère(s) spécial(s)", "incorrect-password-try-again": "Mot de passe incorrect. Veuillez réessayer", "lowercase-letter": "{ count, plural, =1 {1 lettre minuscule} other {# lettres minuscules} }", "new-passwords-not-match": "Les nouveaux mots de passe ne correspondent pas", @@ -4450,7 +4999,8 @@ "additional-info": "Informations supplémentaires (JSON)", "invalid-additional-info": "Impossible d’analyser le JSON des informations supplémentaires.", "no-relations-text": "Aucune relation trouvée", - "not": "Non" + "not": "Non", + "copy-type": "Copier le type" }, "resource": { "add": "Ajouter une ressource", @@ -5400,7 +5950,8 @@ }, "time-series": "Séries temporelles", "latest": "Dernières valeurs", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Champs calculés et règles d'alarme" }, "save-attribute": { "processing-settings": "Paramètres de traitement", @@ -5595,7 +6146,8 @@ "bad-request-params": "Paramètres de requête incorrects", "item-not-found": "Élément introuvable", "too-many-requests": "Trop de requêtes", - "too-many-updates": "Trop de mises à jour" + "too-many-updates": "Trop de mises à jour", + "entities-limit-exceeded": "Limite d’entités dépassée" }, "tenant": { "tenant": "Locataire", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Nombre maximal d’arguments par champ calculé", "max-arguments-per-cf-range": "Le nombre maximal d’arguments ne peut pas être négatif", "max-arguments-per-cf-required": "Le nombre maximal d’arguments est requis", + "max-related-level-per-argument": "Niveau de relation maximal par argument « Entités associées »", + "max-related-level-per-argument-range": "Le nombre maximal du niveau de relation par argument « Entités associées » ne peut pas être inférieur à « 1 »", + "max-related-level-per-argument-required": "Le nombre maximal du niveau de relation par argument « Entités associées » est requis", + "min-allowed-scheduled-update-interval": "Intervalle minimal de mise à jour autorisé pour les arguments « Entités associées » (secondes)", + "min-allowed-scheduled-update-interval-range": "La valeur minimale de l’intervalle de mise à jour autorisé ne peut pas être négative", + "min-allowed-deduplication-interval": "Intervalle minimal de déduplication autorisé (secondes)", + "min-allowed-deduplication-interval-range": "La valeur minimale de l’intervalle de déduplication autorisé ne peut pas être négative", + "min-allowed-deduplication-interval-required": "L’intervalle minimal de déduplication autorisé est requis", + "intermediate-aggregation-interval": "Intervalle d’agrégation intermédiaire (secondes)", + "intermediate-aggregation-interval-range": "La valeur de l’intervalle d’agrégation intermédiaire ne peut pas être inférieure à « 1 »", + "intermediate-aggregation-interval-required": "L’intervalle d’agrégation intermédiaire est requis", + "reevaluation-check-interval": "Intervalle de vérification de la réévaluation (secondes)", + "reevaluation-check-interval-range": "La valeur de l’intervalle de vérification de la réévaluation ne peut pas être inférieure à « 1 »", + "reevaluation-check-interval-required": "L’intervalle de vérification de la réévaluation est requis", + "alarms-reevaluation-interval": "Intervalle de réévaluation des alarmes (secondes)", + "alarms-reevaluation-interval-range": "La valeur de l’intervalle de réévaluation des alarmes ne peut pas être inférieure à « 1 »", + "alarms-reevaluation-interval-required": "L’intervalle de réévaluation des alarmes est requis", + "min-allowed-aggregation-interval": "Intervalle minimal d’agrégation autorisé (secondes)", + "min-allowed-aggregation-interval-range": "La valeur minimale de l’intervalle d’agrégation autorisé ne peut pas être négative", + "min-allowed-aggregation-interval-required": "L’intervalle minimal d’agrégation autorisé est requis", + "min-allowed-scheduled-update-interval-required": "La valeur minimale de l’intervalle de mise à jour autorisé est requise", "max-state-size": "Taille maximale de l'état (en Ko)", "max-state-size-range": "La taille maximale ne peut pas être négative", "max-state-size-required": "La taille maximale de l'état est requise", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Nombre maximal d’abonnements par utilisateur régulier", "ws-limit-max-subscriptions-per-public-user": "Nombre maximal d’abonnements par utilisateur public", "ws-limit-updates-per-session": "Mises à jour WS par session", + "relation-search-entity-limit": "Limite de recherche d’entités par relation", + "relation-search-entity-limit-hint": "Limite le nombre d’entités résolues au dernier niveau du chemin de relation. S’applique aux arguments « Entités associées » et aux champs de propagation.", + "relation-search-entity-limit-required": "La limite de recherche d’entités par relation est requise.", + "relation-search-entity-limit-range": "La limite de recherche d’entités par relation ne peut pas être inférieure à « 1 »", "rate-limits": { "add-limit": "Ajouter une limite", "and-also-less-than": "et aussi inférieur à", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Intervalle de regroupement par défaut", "edit-intervals-list-hint": "Il est possible de spécifier la liste des options d’intervalle disponibles.", "edit-grouping-intervals-list-hint": "Il est possible de configurer la liste des intervalles de regroupement et l’intervalle par défaut.", - "all": "Tous" + "all": "Tous", + "save-current-settings-as-default": "Enregistrer les paramètres actuels comme fenêtre temporelle par défaut", + "hide-option-from-end-users": "Masquer l’option aux utilisateurs finaux" }, "tooltip": { "trigger": "Déclencheur", @@ -6648,6 +7227,7 @@ "export-attributes": "Exporter les attributs", "export-credentials": "Exporter les identifiants", "export-calculated-fields": "Exporter les champs calculés \net les règles d’alarme", + "export-alarm-rules": "Exporter les règles d’alarme", "entity-versions": "Versions des entités", "versions": "Versions", "created-time": "Date de création", @@ -6665,6 +7245,7 @@ "load-attributes": "Charger les attributs", "load-credentials": "Charger les identifiants", "load-calculated-fields": "Charger les champs calculés", + "load-alarm-rules": "Charger les règles d’alarme", "compare-with-current": "Comparer avec l'actuel", "diff-entity-with-version": "Comparer avec la version d'entité '{{versionName}}'", "previous-difference": "Différence précédente", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Scanner le code QR", "make-phone-call": "Passer un appel téléphonique", "get-location": "Obtenir la localisation du téléphone", - "take-screenshot": "Faire une capture d'écran" + "take-screenshot": "Faire une capture d'écran", + "handle-provision-success-function": "Gérer la fonction de succès du provisionnement", + "get-location-function": "Obtenir la fonction de localisation", + "process-launch-result-function": "Traiter la fonction de résultat de lancement", + "get-phone-number-function": "Obtenir la fonction de numéro de téléphone", + "process-image-function": "Traiter la fonction d’image", + "process-qr-code-function": "Traiter la fonction de code QR", + "process-location-function": "Traiter la fonction de localisation", + "handle-empty-result-function": "Gérer la fonction de résultat vide", + "handle-error-function": "Gérer la fonction d’erreur", + "handle-non-mobile-fallback-function": "Gérer la fonction de repli non mobile", + "save-to-gallery": "Enregistrer dans la 'Galerie d'images'", + "provision-type": "Type de provisionnement", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Fonction d'action personnalisée", "custom-pretty-function": "Fonction d'action personnalisée (avec modèle HTML)", @@ -6883,7 +7480,8 @@ "marker": "Marqueur", "polygon": "Polygone", "rectangle": "Rectangle", - "circle": "Cercle" + "circle": "Cercle", + "polyline": "Polyligne" }, "place-map-item": "Placer un élément cartographique", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Continuer le dessin du polygone", "finish-draw-polygon": "Terminer le dessin du polygone", "start-draw-circle": "Commencer à dessiner un cercle", - "finish-draw-circle": "Terminer le dessin du cercle" + "finish-draw-circle": "Terminer le dessin du cercle", + "start-draw-polyline": "Commencer à dessiner une polyligne", + "finish-draw-polyline": "Terminer le dessin de la polyligne" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Délai de l’animation de mise à jour" }, "chart-axis": { + "limit": "Limite", + "source": "Source", + "key-value": "Clé / valeur", + "value-required": "La valeur est requise.", + "entity-key-required": "La clé d’entité est requise.", + "key-required": "La clé est requise.", + "scale-limits": "Limites d’échelle", + "scale-appearance": "Apparence de l’échelle", "scale": "Échelle", "scale-min": "min", "scale-max": "max", @@ -8005,7 +8613,10 @@ "add-radio-option": "Ajouter une option radio", "radio-label-position": "Position de l’étiquette", "radio-label-position-before": "Avant", - "radio-label-position-after": "Après" + "radio-label-position-after": "Après", + "save-image": "Enregistrer l’image", + "save-to-gallery": "Enregistrer automatiquement les images capturées dans la galerie d’images", + "public-image": "Rendre l’image disponible pour tout utilisateur non autorisé" }, "invalid-qr-code-text": "Texte saisi invalide pour le code QR. L'entrée doit être une chaîne de caractères", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Trajets", "markers": "Marqueurs", "polygons": "Polygones", - "circles": "Cercles" + "circles": "Cercles", + "polylines": "Polylignes" }, "data-layer": { "source": "Source", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Cercle pour '{{entityName}}' : cliquez pour terminer et enregistrer le cercle", "finish-circle-hint": "Cercle : cliquez pour terminer le dessin" }, + "polyline": { + "polyline-key": "Clé de polyligne", + "polyline-key-required": "La clé de polyligne est requise", + "no-polylines": "Aucune polyligne configurée", + "add-polylines": "Ajouter une polyligne", + "polyline-configuration": "Configuration de la polyligne", + "remove-polyline": "Supprimer la polyligne", + "edit": "Modifier la polyligne", + "cut": "Couper la zone de polyligne", + "rotate": "Faire pivoter la polyligne", + "remove-polyline-for": "Supprimer la polyligne pour « {{entityName}} »", + "draw-polyline": "Dessiner une polyligne", + "polyline-place-first-point-hint-with-entity": "Polyligne pour « {{entityName}} » : cliquer pour placer le premier point", + "polyline-place-first-point-hint": "Polyligne : cliquer pour placer le premier point", + "finish-polyline-hint-with-entity": "Polyligne pour « {{entityName}} » : cliquer pour terminer le dessin", + "finish-polyline-hint": "Polyligne : cliquer pour terminer le dessin", + "polyline-place-first-point-cut-hint": "Cliquer pour placer le premier point", + "finish-polyline-cut-hint": "Cliquer sur le premier marqueur pour terminer et enregistrer" + }, "select-entity": "Sélectionner une entité", "select-entity-hint": "Astuce : après la sélection, cliquer sur la carte pour définir la position" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Afficher un espace vide à la place du bouton d'action masqué", "dont-reserve-space-hidden-action": "Ne pas réserver d'espace pour les boutons d'action masqués", "display-timestamp": "Horodatage", + "timestamp-column-name": "Horodatage", "display-pagination": "Afficher la pagination", "default-page-size": "Taille de page par défaut", "page-step-settings": "Paramètres de pas de page", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Au moins une colonne d'alarme doit être spécifiée", "table-tabs": "Onglets du tableau", "show-cell-actions-menu-mobile": "Afficher le menu déroulant d'actions des cellules en mode mobile", - "disable-sorting": "Désactiver le tri" + "disable-sorting": "Désactiver le tri", + "sort-by": "Trier les onglets par", + "sort-timestamp-option": "Date de création" }, "latest-chart": { "total": "Total", @@ -9491,11 +10125,28 @@ "content": "

    En créant des tableaux de bord pour les utilisateurs finaux, un utilisateur client ne peut voir que ses propres appareils. Les données d'autres clients lui seront invisibles.

    Suivez la documentation pour savoir comment procéder :

    " } } + }, + "api-usage": { + "api-usage": "Utilisation de l’API", + "label": "Libellé", + "state-name": "Nom de l’état", + "status": "Statut", + "status-required": "Le statut est requis.", + "limit": "Limite max", + "limit-required": "La limite max est requise.", + "current-number": "Nombre actuel", + "current-number-required": "Le nombre actuel est requis.", + "add-key": "Ajouter une clé", + "no-key": "Aucune clé", + "delete-key": "Supprimer la clé", + "target-dashboard-state": "État cible du tableau de bord", + "go-to-main-state": "Aller à la vue par défaut" } }, "icon": { "icon": "Icône", "icons": "Icônes", + "custom": "Personnalisé", "select-icon": "Sélectionner une icône", "material-icons": "Icônes Material", "show-all": "Afficher toutes les icônes", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "sur" }, "language": { + "auto": "Auto", "language": "Langue" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-hi_IN.json b/ui-ngx/src/assets/locale/locale.constant-hi_IN.json new file mode 100644 index 0000000000..36b77be8c6 --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-hi_IN.json @@ -0,0 +1,10193 @@ +{ + "access": { + "unauthorized": "अनधिकृत", + "unauthorized-access": "अनधिकृत अभिगम", + "unauthorized-access-text": "इस संसाधन तक पहुँच के लिए आपको साइन इन करना आवश्यक है!", + "access-forbidden": "अभिगम निषिद्ध", + "access-forbidden-text": "आपके पास इस स्थान तक पहुँच अधिकार नहीं हैं!
    यदि आप अभी भी इस स्थान तक पहुँच प्राप्त करना चाहते हैं, तो किसी दूसरे उपयोगकर्ता से साइन इन करने का प्रयास करें।", + "refresh-token-expired": "सत्र समाप्त हो गया है", + "refresh-token-failed": "सत्र को रीफ़्रेश नहीं किया जा सका", + "permission-denied": "अनुमति अस्वीकृत", + "permission-denied-text": "आपके पास यह कार्य करने की अनुमति नहीं है!" + }, + "account": { + "account": "खाता", + "notification-settings": "सूचना सेटिंग्स" + }, + "action": { + "activate": "सक्रिय करें", + "suspend": "निलंबित करें", + "save": "सहेजें", + "saveAs": "इस रूप में सहेजें", + "move": "स्थानांतरित करें", + "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": "परिवर्तनों को अस्वीकार करें", + "decline": "अस्वीकार करें", + "close": "बंद करें", + "back": "वापस", + "run": "चलाएँ", + "sign-in": "साइन इन करें!", + "edit": "संपादित करें", + "view": "देखें", + "create": "बनाएँ", + "drag": "खींचें", + "refresh": "ताज़ा करें", + "undo": "पूर्ववत करें", + "copy": "कॉपी करें", + "paste": "पेस्ट करें", + "copy-reference": "संदर्भ कॉपी करें", + "paste-reference": "संदर्भ पेस्ट करें", + "import": "आयात करें", + "export": "निर्यात करें", + "share-via": "{{provider}} के माध्यम से साझा करें", + "select": "चयन करें", + "continue": "जारी रखें", + "discard-changes": "परिवर्तनों को त्यागें", + "download": "डाउनलोड करें", + "next": "अगला", + "next-with-label": "अगला: {{label}}", + "read-more": "और पढ़ें", + "hide": "छुपाएँ", + "test": "परीक्षण करें", + "done": "हो गया", + "print": "प्रिंट करें", + "restore": "पुनर्स्थापित करें", + "confirm": "पुष्टि करें", + "more": "अधिक", + "less": "कम", + "skip": "छोड़ें", + "send": "भेजें", + "reset": "रीसेट करें", + "show-more": "और दिखाएँ", + "dont-show-again": "दोबारा न दिखाएँ", + "see-documentation": "दस्तावेज़ देखें", + "see-debug-events": "डिबग घटनाएँ देखें", + "clear": "साफ़ करें", + "upload": "अपलोड करें", + "delete-anyway": "फिर भी हटाएँ", + "delete-selected": "चयनित हटाएँ", + "set": "सेट करें" + }, + "aggregation": { + "aggregation": "संकलन", + "function": "डेटा संकलन फ़ंक्शन", + "limit": "अधिकतम मान", + "group-interval": "समूहीकरण अंतराल", + "min": "न्यूनतम", + "max": "अधिकतम", + "avg": "औसत", + "sum": "योग", + "count": "गणना", + "none": "कोई नहीं" + }, + "admin": { + "settings": "सेटिंग्स", + "general": "सामान्य", + "general-settings": "सामान्य सेटिंग्स", + "home-settings": "होम सेटिंग्स", + "home": "होम", + "outgoing-mail": "मेल सर्वर", + "outgoing-mail-settings": "आउटगोइंग मेल सर्वर सेटिंग्स", + "system-settings": "सिस्टम सेटिंग्स", + "test-mail-sent": "परीक्षण मेल सफलतापूर्वक भेजा गया!", + "base-url": "बेस URL", + "base-url-required": "बेस URL आवश्यक है।", + "prohibit-different-url": "क्लाइंट रिक्वेस्ट हेडर से होस्टनेम का उपयोग निषिद्ध करें", + "prohibit-different-url-hint": "यह सेटिंग प्रोडक्शन वातावरण के लिए सक्षम होनी चाहिए। इसे अक्षम करने पर सुरक्षा समस्याएँ हो सकती हैं", + "device-connectivity": { + "device-connectivity": "डिवाइस कनेक्टिविटी", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPs", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "यदि होस्ट या पोर्ट फ़ील्ड खाली हैं, तो डिफ़ॉल्ट प्रोटोकॉल मान का उपयोग किया जाएगा।", + "host": "होस्ट", + "port": "पोर्ट", + "port-pattern": "पोर्ट एक धनात्मक पूर्णांक होना चाहिए।", + "port-range": "पोर्ट 1 से 65535 की सीमा में होना चाहिए।" + }, + "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 संस्करण", + "enable-proxy": "प्रॉक्सी सक्षम करें", + "proxy-host": "प्रॉक्सी होस्ट", + "proxy-host-required": "प्रॉक्सी होस्ट आवश्यक है।", + "proxy-port": "प्रॉक्सी पोर्ट", + "proxy-port-required": "प्रॉक्सी पोर्ट आवश्यक है।", + "proxy-port-range": "प्रॉक्सी पोर्ट 1 से 65535 की सीमा में होना चाहिए।", + "proxy-user": "प्रॉक्सी उपयोगकर्ता", + "proxy-password": "प्रॉक्सी पासवर्ड", + "change-password": "पासवर्ड बदलें", + "send-test-mail": "टेस्ट मेल भेजें", + "sms-provider": "SMS प्रदाता", + "sms-provider-settings": "SMS प्रदाता सेटिंग्स", + "sms-provider-type": "SMS प्रदाता प्रकार", + "sms-provider-type-required": "SMS प्रदाता प्रकार आवश्यक है।", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "AWS Access Key ID", + "aws-access-key-id-required": "AWS Access Key ID आवश्यक है", + "aws-secret-access-key": "AWS Secret Access Key", + "aws-secret-access-key-required": "AWS Secret Access Key आवश्यक है", + "aws-region": "AWS Region", + "aws-region-required": "AWS Region आवश्यक है", + "number-from": "स्रोत फोन नंबर", + "number-from-required": "स्रोत फोन नंबर आवश्यक है।", + "number-to": "गंतव्य फोन नंबर", + "number-to-required": "गंतव्य फोन नंबर आवश्यक है।", + "phone-number-hint": "फ़ोन नंबर E.164 फ़ॉर्मेट में, उदाहरण: +19995550123", + "phone-number-hint-twilio": "फ़ोन नंबर E.164 फ़ॉर्मेट/फोन नंबर के SID/मैसेजिंग सर्विस SID में, उदाहरण: +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "अमान्य फोन नंबर। E.164 फ़ॉर्मेट में होना चाहिए, उदाहरण: +19995550123.", + "phone-number-pattern-twilio": "अमान्य फोन नंबर। E.164 फ़ॉर्मेट/फोन नंबर के SID/मैसेजिंग सर्विस SID में होना चाहिए, उदाहरण: +19995550123/PNXXX/MGXXX.", + "sms-message": "SMS संदेश", + "sms-message-required": "SMS संदेश आवश्यक है।", + "sms-message-max-length": "SMS संदेश 1600 अक्षरों से अधिक लंबा नहीं हो सकता", + "twilio-account-sid": "Twilio Account SID", + "twilio-account-sid-required": "Twilio Account SID आवश्यक है", + "twilio-account-token": "Twilio Account Token", + "twilio-account-token-required": "Twilio Account Token आवश्यक है", + "send-test-sms": "परीक्षण SMS भेजें", + "test-sms-sent": "परीक्षण SMS सफलतापूर्वक भेजा गया!", + "security-settings": "सुरक्षा सेटिंग्स", + "password-policy": "पासवर्ड नीति", + "minimum-password-length": "न्यूनतम पासवर्ड लंबाई", + "minimum-password-length-required": "न्यूनतम पासवर्ड लंबाई आवश्यक है", + "minimum-password-length-range": "न्यूनतम पासवर्ड लंबाई 6 से 50 की सीमा में होनी चाहिए", + "maximum-password-length": "अधिकतम पासवर्ड लंबाई", + "maximum-password-length-min": "अधिकतम पासवर्ड लंबाई कम-से-कम 6 होनी चाहिए", + "maximum-password-length-less-min": "अधिकतम पासवर्ड लंबाई न्यूनतम लंबाई से अधिक होनी चाहिए", + "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": "पासवर्ड पुन: उपयोग आवृत्ति (दिनों में) नकारात्मक नहीं हो सकती", + "allow-whitespace": "व्हाइटस्पेस की अनुमति दें", + "force-reset-password-if-no-valid": "अमान्य होने पर पासवर्ड रीसेट करने के लिए बाध्य करें", + "force-reset-password-if-no-valid-hint": "कृपया इस फ़ीचर को सक्षम करते समय सावधान रहें: यह अमान्य पासवर्ड वाले उपयोगकर्ताओं को ईमेल के माध्यम से अपना पासवर्ड रीसेट करने की आवश्यकता होगी।", + "general-policy": "सामान्य नीति", + "max-failed-login-attempts": "खाता लॉक होने से पहले असफल लॉगिन प्रयासों की अधिकतम संख्या", + "minimum-max-failed-login-attempts-range": "असफल लॉगिन प्रयासों की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "user-lockout-notification-email": "उपयोगकर्ता खाता लॉक होने की स्थिति में, ईमेल पर सूचना भेजें", + "user-activation-token-ttl": "उपयोगकर्ता सक्रियण लिंक TTL (घंटों में)", + "user-activation-token-ttl-range": "उपयोगकर्ता सक्रियण लिंक TTL 1 से 24 घंटे की सीमा में होना चाहिए", + "password-reset-token-ttl": "पासवर्ड रीसेट लिंक TTL (घंटों में)", + "password-reset-token-ttl-range": "पासवर्ड रीसेट लिंक TTL 1 से 24 घंटे की सीमा में होना चाहिए", + "mobile-secret-key-length": "मोबाइल सीक्रेट कुंजी की लंबाई", + "mobile-secret-key-length-range": "मोबाइल सीक्रेट कुंजी की लंबाई धनात्मक होनी चाहिए", + "domain-name": "डोमेन नाम", + "domain-name-unique": "डोमेन नाम और प्रोटोकॉल अद्वितीय होने चाहिए।", + "domain-name-max-length": "डोमेन नाम 256 अक्षरों से कम होना चाहिए", + "error-verification-url": "डोमेन नाम में '/' और ':' चिन्ह नहीं होने चाहिए। उदाहरण: thingsboard.io", + "connection-settings": "कनेक्शन सेटिंग्स", + "oauth2": { + "access-token-uri": "ऐक्सेस टोकन URI", + "access-token-uri-required": "ऐक्सेस टोकन URI आवश्यक है।", + "activate-user": "उपयोगकर्ता को सक्रिय करें", + "add-domain": "डोमेन जोड़ें", + "delete-domain": "डोमेन हटाएँ", + "add-provider": "प्रोवाइडर जोड़ें", + "delete-provider": "प्रोवाइडर हटाएँ", + "allow-user-creation": "उपयोगकर्ता बनाने की अनुमति दें", + "always-fullscreen": "हमेशा फुलस्क्रीन", + "authorization-uri": "ऑथराइजेशन URI", + "authorization-uri-required": "ऑथराइजेशन URI आवश्यक है।", + "add-client": "OAuth 2.0 क्लाइंट जोड़ें", + "client-details": "OAuth 2.0 क्लाइंट विवरण", + "client": "OAuth 2.0 क्लाइंट", + "clients": "OAuth 2.0 क्लाइंट्स", + "no-oauth2-clients": "कोई OAuth 2.0 क्लाइंट नहीं मिला", + "search-oauth2-clients": "OAuth 2.0 क्लाइंट्स खोजें", + "delete-client-title": "क्या आप वाकई OAuth 2.0 क्लाइंट '{{clientName}}' हटाना चाहते हैं?", + "delete-client-text": "सावधान रहें, पुष्टि के बाद क्लाइंट और उससे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "delete-mobile-app-title": "क्या आप वाकई मोबाइल एप्लिकेशन '{{applicationName}}' हटाना चाहते हैं?", + "delete-mobile-app-text": "सावधान रहें, पुष्टि के बाद मोबाइल एप्लिकेशन और उससे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "title": "शीर्षक", + "client-title-required": "शीर्षक आवश्यक है", + "client-title-max-length": "शीर्षक 100 अक्षरों से कम होना चाहिए", + "advanced-settings": "उन्नत सेटिंग्स", + "domain-details": "डोमेन विवरण", + "no-domains": "कोई डोमेन नहीं मिला", + "search-domains": "डोमेन खोजें", + "mobile-app-details": "मोबाइल एप्लिकेशन विवरण", + "add-mobile-app": "मोबाइल एप्लिकेशन जोड़ें", + "no-mobile-apps": "कोई मोबाइल एप्लिकेशन नहीं मिला", + "search-mobile-apps": "मोबाइल एप्लिकेशन खोजें", + "send-token": "टोकन भेजें", + "create-new": "नया बनाएँ", + "client-authentication-method": "क्लाइंट प्रमाणीकरण विधि", + "client-id": "क्लाइंट ID", + "client-id-required": "क्लाइंट ID आवश्यक है।", + "client-id-max-length": "क्लाइंट ID 256 अक्षरों से कम होना चाहिए", + "client-secret": "क्लाइंट सीक्रेट", + "client-secret-required": "क्लाइंट सीक्रेट आवश्यक है।", + "client-secret-max-length": "क्लाइंट सीक्रेट 2049 अक्षरों से कम होना चाहिए", + "custom-setting": "कस्टम सेटिंग्स", + "customer-name-pattern": "ग्राहक नाम पैटर्न", + "customer-name-pattern-max-length": "ग्राहक नाम पैटर्न 256 अक्षरों से कम होना चाहिए", + "default-dashboard-name": "डिफ़ॉल्ट डैशबोर्ड नाम", + "default-dashboard-name-max-length": "डिफ़ॉल्ट डैशबोर्ड नाम 256 अक्षरों से कम होना चाहिए", + "delete-domain-text": "सावधान रहें, पुष्टि के बाद डोमेन और सभी प्रदाता डेटा उपलब्ध नहीं रहेगा।", + "delete-domain-title": "क्या आप वाकई डोमेन '{{domainName}}' हटाना चाहते हैं?", + "delete-registration-text": "सावधान रहें, पुष्टि के बाद प्रदाता का डेटा उपलब्ध नहीं रहेगा।", + "delete-registration-title": "क्या आप वाकई प्रदाता '{{name}}' हटाना चाहते हैं?", + "email-attribute-key": "ईमेल विशेषता कुंजी", + "email-attribute-key-required": "ईमेल विशेषता कुंजी आवश्यक है।", + "email-attribute-key-max-length": "ईमेल विशेषता कुंजी 32 अक्षरों से कम होना चाहिए", + "first-name-attribute-key": "पहले नाम की विशेषता कुंजी", + "first-name-attribute-key-max-length": "पहले नाम की विशेषता कुंजी 32 अक्षरों से कम होना चाहिए", + "general": "सामान्य", + "jwk-set-uri": "JSON वेब की (JWK) URI", + "last-name-attribute-key": "अंतिम नाम की विशेषता कुंजी", + "last-name-attribute-key-max-length": "अंतिम नाम की विशेषता कुंजी 32 अक्षरों से कम होना चाहिए", + "login-button-icon": "लॉगिन बटन आइकन", + "login-button-label": "प्रदाता लेबल", + "login-button-label-placeholder": " $(Provider label) से लॉगिन करें", + "login-button-label-required": "लेबल आवश्यक है।", + "login-provider": "लॉगिन प्रदाता", + "mapper": "मैपर", + "new-domain": "नया डोमेन", + "oauth2": "OAuth 2.0", + "password-max-length": "पासवर्ड 256 अक्षरों से कम होना चाहिए", + "redirect-uri-template": "रीडायरेक्ट URI टेम्पलेट", + "copy-redirect-uri": "रीडायरेक्ट URI कॉपी करें", + "registration-id": "पंजीकरण ID", + "registration-id-required": "पंजीकरण ID आवश्यक है।", + "registration-id-unique": "पंजीकरण ID सिस्टम के लिए अद्वितीय होना चाहिए।", + "scope": "स्कोप", + "scope-required": "स्कोप आवश्यक है।", + "tenant-name-pattern": "टेनेंट नाम पैटर्न", + "tenant-name-pattern-required": "टेनेंट नाम पैटर्न आवश्यक है।", + "tenant-name-pattern-max-length": "टेनेंट नाम पैटर्न 256 अक्षरों से कम होना चाहिए", + "tenant-name-strategy": "टेनेंट नाम रणनीति", + "type": "मैपर प्रकार", + "uri-pattern-error": "अमान्य URI फ़ॉर्मेट।", + "url": "URL", + "url-pattern": "अमान्य URL फ़ॉर्मेट।", + "url-required": "URL आवश्यक है।", + "url-max-length": "URL 256 अक्षरों से कम होना चाहिए", + "user-info-uri": "उपयोगकर्ता जानकारी URI", + "user-info-uri-required": "उपयोगकर्ता जानकारी URI आवश्यक है।", + "username-max-length": "उपयोगकर्ता नाम 256 अक्षरों से कम होना चाहिए", + "user-name-attribute-name": "उपयोगकर्ता नाम विशेषता कुंजी", + "user-name-attribute-name-required": "उपयोगकर्ता नाम विशेषता कुंजी आवश्यक है", + "protocol": "प्रोटोकॉल", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "OAuth 2.0 सेटिंग्स सक्षम करें", + "disable": "OAuth 2.0 सेटिंग्स अक्षम करें", + "edge": "Edge पर प्रसार करें", + "edge-enable": "Edge पर प्रसार सक्षम करें", + "edge-disable": "Edge पर प्रसार अक्षम करें", + "domains": "डोमेन", + "mobile-apps": "मोबाइल एप्लिकेशन", + "mobile-package": "एप्लिकेशन पैकेज", + "mobile-package-placeholder": "उदा.: my.example.app", + "mobile-package-hint": "Android के लिए: आपका अपना अद्वितीय Application ID. iOS के लिए: Product bundle identifier.", + "mobile-package-unique": "एप्लिकेशन पैकेज अद्वितीय होना चाहिए।", + "mobile-package-required": "एप्लिकेशन पैकेज आवश्यक है।", + "mobile-package-max-length": "एप्लिकेशन पैकेज 256 अक्षरों से कम होना चाहिए", + "mobile-package-spaces": "एप्लिकेशन पैकेज में रिक्त स्थान नहीं होने चाहिए", + "mobile-app-secret": "एप्लिकेशन सीक्रेट", + "mobile-app-secret-hint": "Base64 एन्कोडेड स्ट्रिंग जो कम से कम 512 बिट डेटा का प्रतिनिधित्व करती हो।", + "mobile-app-secret-required": "एप्लिकेशन सीक्रेट आवश्यक है।", + "mobile-app-secret-min-length": "एप्लिकेशन सीक्रेट में कम से कम 512 बिट डेटा होना आवश्यक है।", + "mobile-app-secret-base64": "एप्लिकेशन सीक्रेट base64 फ़ॉर्मेट में होना चाहिए।", + "invalid-mobile-app-secret": "एप्लिकेशन सीक्रेट में केवल अल्फ़ान्यूमेरिक वर्ण होने चाहिए और इसकी लंबाई 16 से 2048 अक्षरों के बीच होनी चाहिए।", + "copy-mobile-app-secret": "एप्लिकेशन सीक्रेट कॉपी करें", + "delete-mobile-app": "एप्लिकेशन जानकारी हटाएँ", + "providers": "प्रदाता", + "platform-web": "वेब", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "सभी प्लेटफ़ॉर्म", + "smtp-provider": "SMTP प्रदाता", + "allowed-platforms": "अनुमत प्लेटफ़ॉर्म", + "authentication": "प्रमाणीकरण", + "basic": "बेसिक", + "provider": "प्रदाता", + "redirect-url": "रीडायरेक्ट URI", + "domain-name": "डोमेन नाम", + "domain-name-required": "डोमेन नाम आवश्यक है", + "redirect-url-template": "रीडायरेक्ट URI टेम्पलेट", + "microsoft-tenant-id": "डायरेक्टरी (टेनेंट) ID", + "microsoft-tenant-id-required": "डायरेक्टरी (टेनेंट) ID आवश्यक है", + "token-uri": "टोकन URI", + "token-uri-required": "टोकन URI आवश्यक है", + "redirect-uri": "रीडायरेक्ट URI", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "कस्टम", + "generate-access-token": "एक्सेस टोकन जनरेट करें", + "update-access-token": "एक्सेस टोकन अपडेट करें", + "access-token-status": "एक्सेस टोकन स्थिति:", + "token-status-generated": "जनरेट किया गया", + "token-status-not-generated": "जनरेट नहीं किया गया" + }, + "smpp-provider": { + "smpp-version": "SMPP संस्करण", + "smpp-host": "SMPP होस्ट", + "smpp-host-required": "SMPP होस्ट आवश्यक है", + "smpp-port": "SMPP पोर्ट", + "smpp-port-required": "SMPP पोर्ट आवश्यक है", + "system-id": "सिस्टम ID", + "system-id-required": "सिस्टम ID आवश्यक है", + "password": "पासवर्ड", + "password-required": "पासवर्ड आवश्यक है", + "type-settings": "टाइप सेटिंग्स", + "source-settings": "सोर्स सेटिंग्स", + "destination-settings": "डेस्टिनेशन सेटिंग्स", + "additional-settings": "अतिरिक्त सेटिंग्स", + "system-type": "सिस्टम प्रकार", + "bind-type": "बाइंड प्रकार", + "service-type": "सर्विस प्रकार", + "source-address": "सोर्स पता", + "source-ton": "सोर्स TON", + "source-npi": "सोर्स NPI", + "destination-ton": "डेस्टिनेशन TON (Type of Number)", + "destination-npi": "डेस्टिनेशन NPI (Numbering Plan Identification)", + "address-range": "पते की रेंज", + "coding-scheme": "कोडिंग स्कीम", + "bind-type-tx": "ट्रांसमीटर", + "bind-type-rx": "रिसीवर", + "bind-type-trx": "ट्रांससीवर", + "ton-unknown": "अज्ञात", + "ton-international": "अंतरराष्ट्रीय", + "ton-national": "राष्ट्रीय", + "ton-network-specific": "नेटवर्क विशिष्ट", + "ton-subscriber-number": "सब्सक्राइबर नंबर", + "ton-alphanumeric": "अल्फ़ान्यूमेरिक", + "ton-abbreviated": "संक्षिप्त", + "npi-unknown": "0 - अज्ञात", + "npi-isdn": "1 - ISDN/टेलीफोन नंबरिंग प्लान (E163/E164)", + "npi-data-numbering-plan": "3 - डेटा नंबरिंग प्लान (X.121)", + "npi-telex-numbering-plan": "4 - टेलेक्स नंबरिंग प्लान (F.69)", + "npi-land-mobile": "6 - लैंड मोबाइल (E.212)", + "npi-national-numbering-plan": "8 - राष्ट्रीय नंबरिंग प्लान", + "npi-private-numbering-plan": "9 - निजी नंबरिंग प्लान", + "npi-ermes-numbering-plan": "10 - ERMES नंबरिंग प्लान (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - इंटरनेट (IP)", + "npi-wap-client-id": "18 - WAP क्लाइंट Id (WAP Forum द्वारा परिभाषित किया जाना है)", + "scheme-smsc": "0 - SMSC डिफ़ॉल्ट अल्फ़ाबेट (short और long code के लिए ASCII और toll-free के लिए GSM)", + "scheme-ia5": "1 - IA5 (short और long code के लिए ASCII, toll-free के लिए Latin 9 (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - Octet Unspecified (8-bit binary)", + "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - Octet Unspecified (8-bit binary)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Cyrillic (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Latin/Hebrew (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - पिक्टोग्राम एन्कोडिंग", + "scheme-music-codes": "10 - म्यूजिक कोड्स (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - एक्सटेंडेड Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - कोरियन ग्राफ़िक कैरेक्टर सेट (KS C 5601/KS X 1001)" + }, + "queue-select-name": "क्यू नाम चुनें", + "queue-name": "नाम", + "queue-name-required": "क्यू नाम आवश्यक है!", + "queues": "क्यूज़", + "queue-partitions": "पार्टिशन", + "queue-submit-strategy": "सबमिट स्ट्रेटेजी", + "queue-processing-strategy": "प्रोसेसिंग स्ट्रेटेजी", + "queue-configuration": "क्यू कॉन्फ़िगरेशन", + "repository-settings": "रिपॉज़िटरी सेटिंग्स", + "repository": "रिपॉज़िटरी", + "repository-url": "रिपॉज़िटरी URL", + "repository-url-required": "रिपॉज़िटरी URL आवश्यक है।", + "default-branch": "डिफ़ॉल्ट ब्रांच नाम", + "repository-read-only": "केवल पढ़ने के लिए", + "show-merge-commits": "मर्ज कमिट दिखाएँ", + "authentication-settings": "प्रमाणीकरण सेटिंग्स", + "auth-method": "प्रमाणीकरण विधि", + "auth-method-username-password": "पासवर्ड / एक्सेस टोकन", + "auth-method-username-password-hint": "GitHub उपयोगकर्ताओं को रिपॉज़िटरी पर लिखने की अनुमति वाले एक्सेस tokens का उपयोग करना आवश्यक है।", + "auth-method-private-key": "प्राइवेट की", + "password-access-token": "पासवर्ड / एक्सेस टोकन", + "change-password-access-token": "पासवर्ड / एक्सेस टोकन बदलें", + "private-key": "प्राइवेट की", + "drop-private-key-file-or": "प्राइवेट की फ़ाइल को ड्रैग और ड्रॉप करें या", + "passphrase": "पासफ़्रेज़", + "enter-passphrase": "पासफ़्रेज़ दर्ज करें", + "change-passphrase": "पासफ़्रेज़ बदलें", + "check-access": "एक्सेस जाँचें", + "check-repository-access-success": "रिपॉज़िटरी एक्सेस सफलतापूर्वक सत्यापित हो गया!", + "delete-repository-settings-title": "क्या आप वाकई रिपॉज़िटरी सेटिंग्स हटाना चाहते हैं?", + "delete-repository-settings-text": "सावधान रहें, पुष्टि के बाद रिपॉज़िटरी सेटिंग्स हटा दी जाएँगी और वर्ज़न कंट्रोल फ़ीचर उपलब्ध नहीं रहेगा।", + "auto-commit-settings": "ऑटो-कमिट सेटिंग्स", + "auto-commit": "ऑटो-कमिट", + "auto-commit-entities": "ऑटो-कमिट एंटिटीज़", + "no-auto-commit-entities-prompt": "ऑटो-कमिट के लिए कोई एंटिटी कॉन्फ़िगर नहीं की गई है", + "delete-auto-commit-settings-title": "क्या आप वाकई ऑटो-कमिट सेटिंग्स हटाना चाहते हैं?", + "delete-auto-commit-settings-text": "सावधान रहें, पुष्टि के बाद ऑटो-कमिट सेटिंग्स हटा दी जाएँगी और सभी एंटिटीज़ के लिए ऑटो-कमिट अक्षम हो जाएगा।", + "mobile-app": { + "mobile-app": "मोबाइल ऐप", + "mobile-app-qr-code-widget-settings": "मोबाइल ऐप QR कोड विजेट सेटिंग्स", + "applications": "एप्लिकेशन", + "default": "डिफ़ॉल्ट", + "custom": "कस्टम", + "android": "Android", + "ios": "iOS", + "appearance": "दिखावट", + "appearance-on-home-page": "होम पेज पर दिखावट", + "enabled": "सक्रिय", + "disabled": "निष्क्रिय", + "badges": "बैज", + "label": "लेबल", + "label-required": "लेबल आवश्यक है", + "label-max-length": "लेबल 50 अक्षरों से कम या बराबर होना चाहिए", + "right": "दायाँ", + "left": "बायाँ", + "set": "सेट करें", + "preview": "पूर्वावलोकन", + "connect-mobile-app": "मोबाइल ऐप कनेक्ट करें", + "use-system-settings": "सिस्टम सेटिंग्स का उपयोग करें" + }, + "2fa": { + "2fa": "दो-कारक प्रमाणीकरण", + "available-providers": "उपलब्ध प्रदाता", + "available-providers-required": "कम से कम एक 2FA प्रदाता को कॉन्फ़िगर किया जाना चाहिए।", + "issuer-name": "जारीकर्ता नाम", + "issuer-name-required": "जारीकर्ता नाम आवश्यक है।", + "max-verification-failures-before-user-lockout": "उपयोगकर्ता लॉक होने से पहले अधिकतम सत्यापन विफलताएँ", + "max-verification-failures-before-user-lockout-pattern": "अधिकतम सत्यापन विफलताएँ धनात्मक पूर्णांक होनी चाहिए।", + "number-of-checking-attempts": "जाँच प्रयासों की संख्या", + "number-of-checking-attempts-pattern": "जाँच प्रयासों की संख्या धनात्मक पूर्णांक होनी चाहिए।", + "number-of-checking-attempts-required": "जाँच प्रयासों की संख्या आवश्यक है।", + "number-of-codes": "कोड की संख्या", + "number-of-codes-pattern": "कोड की संख्या धनात्मक पूर्णांक होनी चाहिए।", + "number-of-codes-required": "कोड की संख्या आवश्यक है।", + "provider": "प्रदाता", + "retry-verification-code-period": "सत्यापन कोड पुनः प्रयास अवधि", + "retry-verification-code-period-pattern": "न्यूनतम अवधि 5 सेकंड है।", + "retry-verification-code-period-required": "सत्यापन कोड पुनः प्रयास अवधि आवश्यक है।", + "total-allowed-time-for-verification": "सत्यापन के लिए कुल अनुमत समय", + "total-allowed-time-for-verification-pattern": "न्यूनतम कुल अनुमत समय 60 सेकंड है।", + "total-allowed-time-for-verification-required": "कुल अनुमत समय आवश्यक है।", + "use-system-two-factor-auth-settings": "सिस्टम दो-कारक प्रमाणीकरण सेटिंग्स का उपयोग करें", + "verification-code-check-rate-limit": "सत्यापन कोड जाँच दर सीमा", + "verification-code-lifetime": "सत्यापन कोड जीवनकाल", + "verification-code-lifetime-pattern": "सत्यापन कोड जीवनकाल धनात्मक पूर्णांक होना चाहिए।", + "verification-code-lifetime-required": "सत्यापन कोड जीवनकाल आवश्यक है।", + "verification-message-template": "सत्यापन संदेश टेम्पलेट", + "verification-limitations": "सत्यापन सीमाएँ", + "verification-message-template-pattern": "सत्यापन संदेश में यह पैटर्न होना चाहिए: ${code}", + "verification-message-template-required": "सत्यापन संदेश टेम्पलेट आवश्यक है।", + "within-time": "समय के भीतर", + "within-time-pattern": "समय धनात्मक पूर्णांक होना चाहिए।", + "within-time-required": "समय आवश्यक है।", + "force-2fa": "दो-कारक प्रमाणीकरण लागू करें", + "enforce-for": "के लिए लागू करें" + }, + "jwt": { + "security-settings": "JWT सुरक्षा सेटिंग्स", + "issuer-name": "जारीकर्ता नाम", + "issuer-name-required": "जारीकर्ता नाम आवश्यक है।", + "signings-key": "साइनिंग की", + "signings-key-hint": "Base64 एन्कोडेड स्ट्रिंग जो कम से कम 512 बिट डेटा का प्रतिनिधित्व करती हो।", + "signings-key-required": "साइनिंग की आवश्यक है।", + "signings-key-min-length": "साइनिंग की में कम से कम 512 बिट डेटा होना आवश्यक है।", + "signings-key-base64": "साइनिंग की base64 फ़ॉर्मेट में होनी चाहिए।", + "expiration-time": "टोकन समाप्ति समय (सेकंड में)", + "expiration-time-required": "टोकन समाप्ति समय आवश्यक है।", + "expiration-time-max": "अधिकतम अनुमत समय 2147483647 सेकंड (68 वर्ष) है।", + "expiration-time-min": "न्यूनतम समय 60 सेकंड (1 मिनट) है।", + "refresh-expiration-time": "रिफ़्रेश टोकन समाप्ति समय (सेकंड में)", + "refresh-expiration-time-required": "रिफ़्रेश टोकन समाप्ति समय आवश्यक है।", + "refresh-expiration-time-max": "अधिकतम अनुमत समय 2147483647 सेकंड (68 वर्ष) है।", + "refresh-expiration-time-min": "न्यूनतम समय 900 सेकंड (15 मिनट) है।", + "refresh-expiration-time-less-token": "रिफ़्रेश टोकन का समय टोकन समय से अधिक होना चाहिए।", + "generate-key": "की जनरेट करें", + "info-header": "सभी उपयोगकर्ताओं को दोबारा लॉगिन करना होगा", + "info-message": "JWT साइनिंग की बदलने से जारी किए गए सभी टोकन अमान्य हो जाएँगे। सभी उपयोगकर्ताओं को दोबारा लॉगिन करना पड़ेगा। इसका असर उन स्क्रिप्ट्स पर भी पड़ेगा जो Rest API/Websockets का उपयोग करती हैं।" + }, + "resources": "संसाधन", + "notifications": "सूचनाएँ", + "notifications-settings": "सूचना सेटिंग्स", + "slack-api-token": "Slack API टोकन", + "slack": "Slack", + "slack-settings": "Slack सेटिंग्स", + "mobile-settings": "मोबाइल सेटिंग्स", + "firebase-service-account-file": "Firebase सर्विस अकाउंट क्रेडेंशियल्स JSON फ़ाइल", + "select-firebase-service-account-file": "अपनी Firebase सर्विस अकाउंट क्रेडेंशियल्स फ़ाइल को ड्रैग और ड्रॉप करें या " + }, + "alarm": { + "alarm": "अलार्म", + "alarm-list": "अलार्म सूची", + "alarms": "अलार्म", + "all-alarms": "सभी अलार्म", + "select-alarm": "अलार्म चुनें", + "no-alarms-matching": "'{{entity}}' से मेल खाता कोई अलार्म नहीं मिला।", + "alarm-required": "अलार्म आवश्यक है", + "alarm-filter": "अलार्म फ़िल्टर", + "filter": "फ़िल्टर", + "alarm-status": "अलार्म स्थिति", + "alarm-status-list": "अलार्म स्थिति सूची", + "any-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": "विवरण", + "originator-label": "स्रोत लेबल", + "assign": "असाइन करें", + "assignments": "असाइनमेंट", + "assignee": "असाइन प्राप्तकर्ता", + "assignee-id": "असाइन प्राप्तकर्ता ID", + "assignee-first-name": "असाइन प्राप्तकर्ता पहला नाम", + "assignee-last-name": "असाइन प्राप्तकर्ता अंतिम नाम", + "assignee-email": "असाइन प्राप्तकर्ता ईमेल", + "unassigned": "असाइन नहीं", + "user-deleted": "उपयोगकर्ता हटाया गया", + "assignee-not-set": "सभी", + "status": "स्थिति", + "alarm-details": "अलार्म विवरण", + "start-time": "आरंभ समय", + "assign-time": "असाइन समय", + "end-time": "समाप्ति समय", + "ack-time": "स्वीकार किए जाने का समय", + "clear-time": "क्लियर किए जाने का समय", + "duration": "अवधि", + "alarm-severity": "अलार्म गंभीरता", + "alarm-severity-list": "अलार्म गंभीरता सूची", + "any-severity": "कोई भी गंभीरता", + "severity-critical": "क्रिटिकल", + "severity-major": "मेजर", + "severity-minor": "माइनर", + "severity-warning": "चेतावनी", + "severity-indeterminate": "अनिर्धारित", + "acknowledge": "स्वीकार करें", + "clear": "क्लियर करें", + "delete": "हटाएँ", + "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": "क्या आप वाकई अलार्म स्वीकार करना चाहते हैं?", + "selected-alarms-are-acknowledged": "चयनित अलार्म पहले से ही स्वीकार किए जा चुके हैं", + "clear-alarms-title": "{ count, plural, =1 {1 अलार्म} other {# अलार्म} } क्लियर करें", + "clear-alarms-text": "क्या आप वाकई { count, plural, =1 {1 अलार्म} other {# अलार्म} } क्लियर करना चाहते हैं?", + "clear-alarm-title": "अलार्म क्लियर करें", + "clear-alarm-text": "क्या आप वाकई अलार्म क्लियर करना चाहते हैं?", + "delete-alarms-title": "{ count, plural, =1 {1 अलार्म} other {# अलार्म} } हटाएँ", + "delete-alarms-text": "क्या आप वाकई { count, plural, =1 {1 अलार्म} other {# अलार्म} } हटाना चाहते हैं?", + "selected-alarms-are-cleared": "चयनित अलार्म पहले से ही क्लियर किए जा चुके हैं", + "alarm-status-filter": "अलार्म स्टेटस फ़िल्टर", + "alarm-filter-title": "अलार्म फ़िल्टर", + "assigned": "असाइन किया गया", + "filter-title": "फ़िल्टर", + "max-count-load": "लोड करने के लिए अलार्म की अधिकतम संख्या (0 - असीमित)", + "max-count-load-required": "लोड करने के लिए अलार्म की अधिकतम संख्या आवश्यक है।", + "max-count-load-error-min": "न्यूनतम मान 0 है।", + "fetch-size": "फ़ेच साइज़", + "fetch-size-required": "फ़ेच साइज़ आवश्यक है।", + "fetch-size-error-min": "न्यूनतम मान 10 है।", + "alarm-types": "अलार्म प्रकार", + "alarm-type-list": "अलार्म प्रकार सूची", + "any-type": "कोई भी प्रकार", + "assigned-to-current-user": "वर्तमान उपयोगकर्ता को असाइन किया गया", + "assigned-to-me": "मुझे असाइन किया गया", + "search-propagated-alarms": "प्रसारित अलार्म खोजें", + "comments": "अलार्म टिप्पणियाँ", + "show-more": "और दिखाएँ", + "additional-info": "अतिरिक्त जानकारी", + "alarm-type": "अलार्म प्रकार", + "enter-alarm-type": "अलार्म प्रकार दर्ज करें", + "no-alarm-types-matching": "'{{entitySubtype}}' से मेल खाता कोई अलार्म प्रकार नहीं मिला।", + "alarm-type-list-empty": "कोई अलार्म प्रकार चुना नहीं गया है।", + "system-comments": { + "acked-by-user": "अलार्म को उपयोगकर्ता {{userName}} द्वारा स्वीकार किया गया", + "cleared-by-user": "अलार्म को उपयोगकर्ता {{userName}} द्वारा क्लियर किया गया", + "assigned-to-user": "अलार्म को उपयोगकर्ता {{userName}} द्वारा उपयोगकर्ता {{assigneeName}} को असाइन किया गया", + "unassigned-to-user": "अलार्म को उपयोगकर्ता {{userName}} द्वारा असाइन हटाया गया", + "unassigned-from-deleted-user": "अलार्म को असाइन हटाया गया क्योंकि उपयोगकर्ता {{userName}} को हटा दिया गया था", + "comment-deleted": "उपयोगकर्ता {{userName}} ने अपनी टिप्पणी हटा दी", + "severity-changed": "अलार्म की गंभीरता को {{oldSeverity}} से {{newSeverity}} में अपडेट किया गया" + } + }, + "alarm-activity": { + "add": "टिप्पणी जोड़ें...", + "alarm-comment": "अलार्म टिप्पणी", + "comments": "टिप्पणियाँ", + "delete-alarm-comment": "क्या आप यह टिप्पणी हटाना चाहते हैं?", + "refresh": "रिफ़्रेश करें", + "oldest-first": "सबसे पुरानी पहले", + "newest-first": "सबसे नई पहले", + "activity": "गतिविधि", + "export": "CSV में एक्सपोर्ट करें", + "author": "लेखक", + "created-date": "निर्माण तिथि", + "edited-date": "संपादन तिथि", + "text": "पाठ", + "system": "सिस्टम" + }, + "alias": { + "add": "उपनाम जोड़ें", + "edit": "उपनाम संपादित करें", + "name": "उपनाम नाम", + "name-required": "उपनाम नाम आवश्यक है", + "duplicate-alias": "इसी नाम वाला उपनाम पहले से मौजूद है।", + "filter-type-single-entity": "एकल एंटिटी", + "filter-type-entity-list": "एंटिटी सूची", + "filter-type-entity-name": "एंटिटी नाम", + "filter-type-entity-type": "एंटिटी प्रकार", + "filter-type-state-entity": "डैशबोर्ड स्टेट से एंटिटी", + "filter-type-state-entity-description": "डैशबोर्ड स्टेट पैरामीटर से ली गई एंटिटी", + "filter-type-asset-type": "एसेट प्रकार", + "filter-type-asset-type-description": "प्रकार '{{assetTypes}}' के एसेट", + "filter-type-asset-type-and-name-description": "प्रकार '{{assetTypes}}' के एसेट जिनका नाम '{{prefix}}' से शुरू होता है", + "filter-type-device-type": "डिवाइस प्रकार", + "filter-type-device-type-description": "प्रकार '{{deviceTypes}}' के डिवाइस", + "filter-type-device-type-and-name-description": "प्रकार '{{deviceTypes}}' के डिवाइस जिनका नाम '{{prefix}}' से शुरू होता है", + "filter-type-entity-view-type": "एंटिटी व्यू प्रकार", + "filter-type-entity-view-type-description": "प्रकार '{{entityViewTypes}}' के एंटिटी व्यू", + "filter-type-entity-view-type-and-name-description": "प्रकार '{{entityViewTypes}}' के एंटिटी व्यू जिनका नाम '{{prefix}}' से शुरू होता है", + "filter-type-edge-type": "Edge प्रकार", + "filter-type-edge-type-description": "प्रकार '{{edgeTypes}}' के Edge", + "filter-type-edge-type-and-name-description": "प्रकार '{{edgeTypes}}' के Edge जिनका नाम '{{prefix}}' से शुरू होता है", + "filter-type-relations-query": "रिलेशन क्वेरी", + "filter-type-relations-query-description": "{{entities}} जिनके पास {{relationType}} रिलेशन {{direction}} {{rootEntity}} है", + "filter-type-edge-search-query": "Edge सर्च क्वेरी", + "filter-type-edge-search-query-description": "टाइप {{edgeTypes}} वाले Edge जिनके पास {{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}} है", + "filter-type-apiUsageState": "API उपयोग स्थिति", + "entity-filter": "एंटिटी फ़िल्टर", + "resolve-multiple": "कई एंटिटीज़ के रूप में रिज़ॉल्व करें", + "resolve-multiple-hint": "सभी फ़िल्टर की गई एंटिटीज़ से डेटा को एक साथ दिखाने के लिए सक्षम करें। \nअक्षम होने पर, विजेट केवल चयनित एंटिटी से डेटा दिखाता है।", + "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": "डिफ़ॉल्ट रूप से", + "query-options": "क्वेरी विकल्प", + "max-relation-level": "अधिकतम रिलेशन स्तर", + "unlimited-level": "असीमित स्तर", + "state-entity": "डैशबोर्ड स्टेट एंटिटी", + "all-entities": "सभी एंटिटीज़", + "any-relation": "कोई भी" + }, + "asset": { + "asset": "एसेट", + "assets": "एसेट", + "management": "एसेट प्रबंधन", + "view-assets": "एसेट देखें", + "add": "एसेट जोड़ें", + "asset-type-max-length": "एसेट प्रकार 256 अक्षरों से कम होना चाहिए", + "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": "नाम आवश्यक है।", + "name-max-length": "नाम 256 अक्षरों से कम होना चाहिए", + "label-max-length": "लेबल 256 अक्षरों से कम होना चाहिए", + "description": "विवरण", + "description-required": "विवरण आवश्यक है।", + "type": "प्रकार", + "type-required": "प्रकार आवश्यक है।", + "details": "विवरण", + "events": "घटनाएँ", + "add-asset-text": "नया एसेट जोड़ें", + "asset-details": "एसेट विवरण", + "assign-assets": "एसेट असाइन करें", + "assign-assets-text": "कस्टमर को { count, plural, =1 {1 एसेट} other {# एसेट} } असाइन करें", + "assign-asset-to-edge-title": "एसेट को Edge पर असाइन करें", + "assign-asset-to-edge-text": "कृपया Edge को असाइन करने के लिए एसेट चुनें", + "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": "एसेट नाम अभिव्यक्ति", + "help-text": "आवश्यकतानुसार '%' का उपयोग करें: '%asset_name_contains%', '%asset_name_ends', 'asset_starts_with%'.", + "search": "एसेट खोजें", + "import": "एसेट आयात करें", + "asset-file": "एसेट फ़ाइल", + "label": "लेबल", + "assign-asset-to-edge": "एसेट को Edge पर असाइन करें", + "unassign-asset-from-edge": "एसेट अनअसाइन करें", + "unassign-asset-from-edge-title": "क्या आप वाकई एसेट '{{assetName}}' को अनअसाइन करना चाहते हैं?", + "unassign-asset-from-edge-text": "पुष्टि के बाद एसेट अनअसाइन हो जाएगा और Edge द्वारा एक्सेस नहीं किया जा सकेगा।", + "unassign-assets-from-edge-title": "क्या आप वाकई { count, plural, =1 {1 एसेट} other {# एसेट} } को अनअसाइन करना चाहते हैं?", + "unassign-assets-from-edge-text": "पुष्टि के बाद सभी चयनित एसेट अनअसाइन हो जाएँगे और Edge द्वारा एक्सेस नहीं किए जा सकेंगे।", + "selected-assets": "{ count, plural, =1 {1 एसेट} other {# एसेट} } चयनित" + }, + "attribute": { + "attributes": "विशेषताएँ", + "latest-telemetry": "नवीनतम टेलीमेट्री", + "no-latest-telemetry": "कोई नवीनतम टेलीमेट्री नहीं", + "attributes-scope": "एंटिटी विशेषताओं का स्कोप", + "scope-telemetry": "टेलीमेट्री", + "scope-latest-telemetry": "नवीनतम टेलीमेट्री", + "scope-client": "क्लाइंट विशेषताएँ", + "scope-server": "सर्वर विशेषताएँ", + "scope-shared": "साझा विशेषताएँ", + "scope-client-short": "क्लाइंट", + "scope-server-short": "सर्वर", + "scope-shared-short": "साझा", + "scope-latest-short": "नवीनतम", + "scope-any": "कोई भी", + "add": "विशेषता जोड़ें", + "key": "कुंजी", + "key-max-length": "कुंजी 256 अक्षरों से कम होनी चाहिए", + "last-update-time": "अंतिम अपडेट समय", + "key-required": "विशेषता कुंजी आवश्यक है।", + "value": "मान", + "value-required": "विशेषता मान आवश्यक है।", + "telemetry-key-required": "टेलीमेट्री कुंजी आवश्यक है", + "telemetry-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 {# टेलीमेट्री यूनिट} } चयनित", + "no-attributes-text": "कोई विशेषताएँ नहीं मिलीं", + "no-telemetry-text": "कोई टेलीमेट्री नहीं मिली", + "copy-key": "कुंजी कॉपी करें", + "add-telemetry": "टेलीमेट्री जोड़ें", + "copy-value": "मान कॉपी करें", + "delete-timeseries": { + "start-time": "आरंभ समय", + "ends-on": "समाप्ति तिथि", + "strategy": "रणनीति", + "delete-strategy": "डिलीट रणनीति", + "all-data": "सभी डेटा हटाएँ", + "all-data-except-latest-value": "नवीनतम मान को छोड़कर सभी डेटा हटाएँ", + "latest-value": "नवीनतम मान हटाएँ", + "all-data-for-time-period": "निर्दिष्ट समय अवधि के लिए सभी डेटा हटाएँ", + "rewrite-latest-value": "नवीनतम मान पुनः लिखें" + } + }, + "api-usage": { + "api-features": "API सुविधाएँ", + "api-usage": "API उपयोग", + "alarm": "अलार्म", + "alarms-created": "बनाए गए अलार्म", + "queue-stats": "क्यू आँकड़े", + "processing-failures-and-timeouts": "प्रोसेसिंग विफलताएँ और टाइमआउट", + "exceptions": "अपवाद", + "alarms-created-daily-activity": "बनाए गए अलार्म की दैनिक गतिविधि", + "alarms-created-hourly-activity": "बनाए गए अलार्म की प्रति घंटा गतिविधि", + "alarms-created-monthly-activity": "बनाए गए अलार्म की मासिक गतिविधि", + "data-points": "डेटा पॉइंट", + "data-points-storage-days": "डेटा पॉइंट भंडारण (दिनों में)", + "data-points-storage-days-hourly-activity": "डेटा पॉइंट्स भंडारण दिनों की प्रति घंटा सक्रियता", + "data-points-storage-days-daily-activity": "डेटा पॉइंट्स भंडारण दिनों की दैनिक सक्रियता", + "data-points-storage-days-monthly-activity": "डेटा पॉइंट्स भंडारण दिनों की मासिक सक्रियता", + "device-api": "डिवाइस API", + "email": "ईमेल", + "email-messages": "ईमेल संदेश", + "email-messages-daily-activity": "ईमेल संदेशों की दैनिक गतिविधि", + "email-messages-monthly-activity": "ईमेल संदेशों की मासिक गतिविधि", + "executions": "निष्पादन", + "scripts": "स्क्रिप्ट", + "scripts-hourly-activity": "स्क्रिप्ट की प्रति घंटा गतिविधि", + "scripts-daily-activity": "स्क्रिप्ट की दैनिक गतिविधि", + "scripts-monthly-activity": "स्क्रिप्ट की मासिक गतिविधि", + "javascript": "JavaScript", + "javascript-executions": "JavaScript निष्पादन", + "tbel": "TBEL", + "tbel-executions": "TBEL निष्पादन", + "latest-error": "नवीनतम त्रुटि", + "messages": "संदेश", + "notifications": "सूचनाएँ", + "notifications-email-sms": "सूचनाएँ (Email/SMS)", + "notifications-hourly-activity": "सूचनाओं की प्रति घंटा गतिविधि", + "permanent-failures": "${entityName} स्थायी विफलताएँ", + "permanent-timeouts": "${entityName} स्थायी टाइमआउट", + "processing-failures": "${entityName} प्रोसेसिंग विफलताएँ", + "processing-timeouts": "${entityName} प्रोसेसिंग टाइमआउट", + "rule-chain": "रूल चेन", + "rule-engine": "रूल इंजन", + "rule-engine-executions": "रूल इंजन निष्पादन", + "rule-engine-hourly-activity": "रूल इंजन की प्रति घंटा सक्रियता", + "rule-engine-daily-activity": "रूल इंजन की दैनिक सक्रियता", + "rule-engine-monthly-activity": "रूल इंजन की मासिक सक्रियता", + "rule-engine-statistics": "रूल इंजन सांख्यिकी", + "rule-node": "रूल नोड", + "sms": "एसएमएस", + "sms-messages": "एसएमएस संदेश", + "sms-messages-hourly-activity": "एसएमएस संदेशों की प्रति घंटा सक्रियता", + "sms-messages-daily-activity": "एसएमएस संदेशों की दैनिक सक्रियता", + "sms-messages-monthly-activity": "एसएमएस संदेशों की मासिक सक्रियता", + "successful": "${entityName} सफल", + "telemetry": "टेलीमेट्री", + "telemetry-persistence": "टेलीमेट्री परसिस्टेंस", + "telemetry-persistence-daily-activity": "टेलीमेट्री परसिस्टेंस दैनिक गतिविधि", + "telemetry-persistence-hourly-activity": "टेलीमेट्री परसिस्टेंस प्रति घंटा गतिविधि", + "telemetry-persistence-monthly-activity": "टेलीमेट्री परसिस्टेंस मासिक गतिविधि", + "transport": "ट्रांसपोर्ट", + "transport-msg-hourly-activity": "ट्रांसपोर्ट संदेशों की प्रति घंटा सक्रियता", + "transport-msg-daily-activity": "ट्रांसपोर्ट संदेशों की दैनिक सक्रियता", + "transport-msg-monthly-activity": "ट्रांसपोर्ट संदेशों की मासिक सक्रियता", + "transport-daily-activity": "ट्रांसपोर्ट की दैनिक सक्रियता", + "transport-data-points": "ट्रांसपोर्ट डेटा पॉइंट्स", + "transport-data-points-hourly-activity": "ट्रांसपोर्ट डेटा पॉइंट्स की प्रति घंटा सक्रियता", + "transport-data-points-daily-activity": "ट्रांसपोर्ट डेटा पॉइंट्स की दैनिक सक्रियता", + "transport-data-points-monthly-activity": "ट्रांसपोर्ट डेटा पॉइंट्स की मासिक सक्रियता", + "view-details": "विवरण देखें", + "view-statistics": "सांख्यिकी देखें", + "transport-messages": "ट्रांसपोर्ट संदेश", + "transport-messages-hourly-activity": "ट्रांसपोर्ट संदेशों की प्रति घंटा सक्रियता", + "transport-data-point-hourly-activity": "ट्रांसपोर्ट डेटा पॉइंट की प्रति घंटा सक्रियता", + "javascript-function-executions": "JavaScript फ़ंक्शन निष्पादन", + "javascript-function-executions-hourly-activity": "JavaScript फ़ंक्शन निष्पादन की प्रति घंटा सक्रियता", + "javascript-function-executions-daily-activity": "JavaScript फ़ंक्शन निष्पादन की दैनिक सक्रियता", + "javascript-function-executions-monthly-activity": "JavaScript फ़ंक्शन निष्पादन की मासिक सक्रियता", + "tbel-function-executions": "TBEL फ़ंक्शन निष्पादन", + "tbel-function-executions-hourly-activity": "TBEL फ़ंक्शन निष्पादन की प्रति घंटा सक्रियता", + "tbel-function-executions-daily-activity": "TBEL फ़ंक्शन निष्पादन की दैनिक सक्रियता", + "tbel-function-executions-monthly-activity": "TBEL फ़ंक्शन निष्पादन की मासिक सक्रियता", + "created-reports": "बनाए गए रिपोर्ट्स", + "created-reports-hourly-activity": "बनाए गए रिपोर्ट्स की प्रति घंटा सक्रियता", + "created-reports-daily-activity": "बनाए गए रिपोर्ट्स की दैनिक सक्रियता", + "created-reports-monthly-activity": "बनाए गए रिपोर्ट्स की मासिक सक्रियता", + "emails": "ईमेल", + "emails-hourly-activity": "ईमेल की प्रति घंटा सक्रियता", + "emails-daily-activity": "ईमेल की दैनिक सक्रियता", + "emails-monthly-activity": "ईमेल की मासिक सक्रियता", + "status": { + "enabled": "सक्षम", + "disabled": "निष्क्रिय", + "warning": "चेतावनी" + } + }, + "api-limit": { + "cassandra-write-queries-core": "Rest API Cassandra लिखने की क्वेरीज़", + "cassandra-read-queries-core": "Rest API और WS टेलीमेट्री Cassandra पढ़ने की क्वेरीज़", + "cassandra-write-queries-rule-engine": "Rule Engine टेलीमेट्री Cassandra लिखने की क्वेरीज़", + "cassandra-read-queries-rule-engine": "Rule Engine टेलीमेट्री Cassandra पढ़ने की क्वेरीज़", + "cassandra-write-queries-monolith": "Monolith टेलीमेट्री Cassandra लिखने की क्वेरीज़", + "cassandra-read-queries-monolith": "Monolith टेलीमेट्री Cassandra पढ़ने की क्वेरीज़", + "entity-version-creation": "एंटिटी वर्ज़न निर्माण", + "entity-version-load": "एंटिटी वर्ज़न लोड", + "notification-requests": "सूचना अनुरोध", + "notification-requests-per-rule": "प्रति Rule के सूचना अनुरोध", + "rest-api-requests": "REST API अनुरोध", + "rest-api-requests-per-customer": "प्रति कस्टमर REST API अनुरोध", + "transport-messages": "ट्रांसपोर्ट संदेश", + "transport-messages-per-device": "प्रति डिवाइस ट्रांसपोर्ट संदेश", + "transport-messages-per-gateway": "प्रति Gateway ट्रांसपोर्ट संदेश", + "transport-messages-per-gateway-device": "प्रति Gateway डिवाइस ट्रांसपोर्ट संदेश", + "ws-updates-per-session": "प्रति सत्र WS अपडेट", + "edge-events": "Edge ईवेंट", + "edge-events-per-edge": "प्रति Edge ईवेंट", + "edge-uplink-messages": "Edge uplink संदेश", + "edge-uplink-messages-per-edge": "प्रति Edge uplink संदेश" + }, + "api-key": { + "api-key": "API कुंजी", + "api-keys": "API कुंजियाँ", + "delete-api-key-title": "क्या आप वाकई API कुंजी '{{name}}' को हटाना चाहते हैं?", + "delete-api-key-text": "सावधान रहें, पुष्टि के बाद कुंजी पुनः प्राप्त नहीं की जा सकेगी।", + "delete-api-keys-title": "क्या आप वाकई { count, plural, =1 {1 API कुंजी} other {# API कुंजियाँ} } हटाना चाहते हैं?", + "delete-api-keys-text": "सावधान रहें, पुष्टि के बाद सभी चयनित कुंजियाँ पुनः प्राप्त नहीं की जा सकेंगी।", + "expiration-date": "समाप्ति तिथि", + "date": "तिथि", + "description": "विवरण", + "disable": "निष्क्रिय करें", + "edit-description": "विवरण संपादित करें", + "enable": "API कुंजी सक्षम करें", + "expiration-time": "समाप्ति समय", + "expiration-time-never": "कभी नहीं", + "expiration-time-custom": "कस्टम", + "generate": "जनरेट करें", + "generate-title": "API कुंजी जनरेट करें", + "generate-text": "ध्यान दें: API कुंजी उस उपयोगकर्ता की अनुमति प्राप्त करती है जिसके लिए यह बनाई गई है।", + "generated-api-key-title": "API कुंजी जनरेट की गई। चलिए कनेक्टिविटी जांचते हैं!", + "generated-api-key-copy": "सुनिश्चित करें कि आप अपनी API कुंजी को अब कॉपी और सेव कर लें क्योंकि आप इसे फिर से नहीं देख पाएंगे।", + "generated-api-key-command": "कनेक्टिविटी जांचने के लिए निम्नलिखित निर्देशों का पालन करें। इसके परिणामस्वरूप, आपको वर्तमान उपयोगकर्ता जानकारी प्राप्त होनी चाहिए:", + "generated-api-key-insecure-url": "असुरक्षित HTTP कनेक्शन पर कमांड निष्पादित करने से आपकी API कुंजी बिना एन्क्रिप्ट के भेजी जाएगी, जिससे यह इंटरसेप्शन के लिए संवेदनशील हो जाएगी।", + "list": "{ count, plural, =1 {एक API कुंजी} other {# API कुंजियाँ} }", + "manage": "प्रबंधित करें", + "manage-api-keys": "API कुंजियों का प्रबंधन करें", + "no-found": "कोई API कुंजी नहीं मिली", + "selected-api-keys": "{ count, plural, =1 {1 API कुंजी} other {# API कुंजियाँ} } चयनित", + "search": "API कुंजियाँ खोजें", + "status": "स्थिति", + "status-active": "सक्रिय", + "status-inactive": "निष्क्रिय", + "status-expired": "समाप्त" + }, + "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-assigned-to-edge": "Edge को असाइन किया गया", + "type-unassigned-from-edge": "Edge से अनअसाइन किया गया", + "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-alarm-delete": "अलार्म हटाया गया", + "type-alarm-assign": "अलार्म असाइन किया गया", + "type-alarm-unassign": "अलार्म अनअसाइन किया गया", + "type-added-comment": "टिप्पणी जोड़ी गई", + "type-updated-comment": "टिप्पणी अपडेट की गई", + "type-deleted-comment": "टिप्पणी हटाई गई", + "type-login": "लॉगिन", + "type-logout": "लॉगआउट", + "type-lockout": "लॉकआउट", + "status-success": "सफलता", + "status-failure": "विफलता", + "audit-log-details": "ऑडिट लॉग विवरण", + "no-audit-logs-prompt": "कोई लॉग नहीं मिला", + "action-data": "कार्रवाई डेटा", + "failure-details": "विफलता विवरण", + "search": "ऑडिट लॉग खोजें", + "clear-search": "खोज साफ़ करें", + "type-assigned-from-tenant": "टेनेंट से असाइन किया गया", + "type-assigned-to-tenant": "टेनेंट को असाइन किया गया", + "type-provision-success": "डिवाइस प्रोविज़न किया गया", + "type-provision-failure": "डिवाइस प्रोविज़निंग विफल रही", + "type-timeseries-updated": "टेलीमेट्री अपडेट की गई", + "type-timeseries-deleted": "टेलीमेट्री हटाई गई", + "type-sms-sent": "SMS भेजा गया", + "any-type": "कोई भी प्रकार", + "audit-log-filter-title": "ऑडिट लॉग फ़िल्टर", + "filter-title": "फ़िल्टर", + "filter-types": "ऑडिट लॉग प्रकार" + }, + "debug-settings": { + "label": "डिबग कॉन्फ़िगरेशन", + "on-failure": "केवल विफलताएँ (24/7)", + "all-messages": "सभी संदेश ({{time}})", + "failures": "विफलताएँ", + "entity": "एंटिटी", + "hint": { + "main-limited": "प्रति {{time}} अधिकतम {{msg}} {{entity}} डिबग संदेश रिकॉर्ड किए जाएँगे।", + "on-failure": "केवल त्रुटि संदेश लॉग करें।", + "all-messages": "सभी डिबग संदेश लॉग करें।" + } + }, + "calculated-fields": { + "expression": "व्यंजक", + "no-found": "कोई कैलक्युलेटेड फ़ील्ड नहीं मिला", + "list": "{ count, plural, =1 {एक कैलक्युलेटेड फ़ील्ड} other {# कैलक्युलेटेड फ़ील्ड्स की सूची} }", + "add": "कैलक्युलेटेड फ़ील्ड जोड़ें", + "selected-fields": "{ count, plural, =1 {1 कैलक्युलेटेड फ़ील्ड} other {# कैलक्युलेटेड फ़ील्ड्स} } चुना गया", + "type": { + "simple": "साधारण", + "simple-hint": "इनपुट तर्कों के आधार पर सरल अंकगणितीय गणना।", + "script": "स्क्रिप्ट", + "script-hint": "निर्धारित तर्कों पर TBEL स्क्रिप्ट का उपयोग करके गणना।", + "geofencing": "जियोफेंसिंग", + "geofencing-hint": "निर्धारित जियोफेंसिंग क्षेत्र समूहों के खिलाफ एंटिटी GPS स्थिति और संक्रमणों का मूल्यांकन।", + "propagation": "प्रसार", + "propagation-hint": "रिलेशन दिशा और प्रकार के आधार पर डेटा का पैरेंट या चाइल्ड एंटिटीज़ में प्रसार।", + "related-entities-aggregation": "संबंधित एंटिटीज़ का संकलन", + "related-entities-aggregation-hint": "संबंधित एंटिटीज़ से नवीनतम डेटा का संकलन।", + "time-series-data-aggregation": "समय श्रृंखला डेटा संकलन", + "time-series-data-aggregation-hint": "वर्तमान एंटिटी से ऐतिहासिक डेटा का संकलन।" + }, + "preview": "पूर्वावलोकन", + "arguments": "आर्गुमेंट्स", + "decimals-by-default": "डिफ़ॉल्ट दशमलव", + "debugging": "कैलक्युलेटेड फ़ील्ड डिबगिंग", + "calculated-field-details": "कैलक्युलेटेड फ़ील्ड विवरण", + "argument-name": "आर्गुमेंट नाम", + "name": "नाम", + "datasource": "डेटा स्रोत", + "add-argument": "आर्गुमेंट जोड़ें", + "test-script-function": "स्क्रिप्ट फ़ंक्शन टेस्ट करें", + "no-arguments": "कोई आर्गुमेंट कॉन्फ़िगर नहीं है", + "argument-settings": "आर्गुमेंट सेटिंग्स", + "argument-current": "करेंट एंटिटी", + "argument-current-tenant": "करेंट टेनेंट", + "argument-device": "डिवाइस", + "argument-asset": "एसेट", + "argument-customer": "कस्टमर", + "argument-tenant": "करेंट टेनेंट", + "argument-owner": "करेंट ओनर", + "argument-relation-query": "संबंधित एंटिटीज़", + "argument-type": "आर्गुमेंट प्रकार", + "attribute": "विशेषता", + "copy-argument-name": "आर्गुमेंट नाम कॉपी करें", + "timeseries-key": "टाइम सीरीज़ कुंजी", + "device-name": "डिवाइस नाम", + "latest-telemetry": "नवीनतम टेलीमेट्री", + "rolling": "टाइम सीरीज़ रोलिंग", + "attribute-scope": "विशेषता स्कोप", + "server-attributes": "सर्वर विशेषताएँ", + "client-attributes": "क्लाइंट विशेषताएँ", + "shared-attributes": "शेयर्ड विशेषताएँ", + "attribute-key": "विशेषता कुंजी", + "default-value": "डिफ़ॉल्ट मान", + "default-value-required": "डिफ़ॉल्ट मान आवश्यक है।", + "limit": "अधिकतम मान", + "time-window": "टाइम विंडो", + "customer-name": "कस्टमर नाम", + "asset-name": "एसेट नाम", + "timeseries": "टाइम सीरीज़", + "output": "आउटपुट", + "output-hint": "परिभाषित करता है कि आउटपुट को कैसे संसाधित किया जाता है।", + "create": "नई कैलक्युलेटेड फ़ील्ड बनाएँ", + "file": "कैलक्युलेटेड फ़ील्ड फ़ाइल", + "invalid-file-error": "अमान्य फ़ाइल फ़ॉर्मेट। कृपया सुनिश्चित करें कि फ़ाइल एक मान्य JSON फ़ाइल है।", + "import": "कैलक्युलेटेड फ़ील्ड इम्पोर्ट करें", + "export": "कैलक्युलेटेड फ़ील्ड एक्सपोर्ट करें", + "export-failed-error": "कैलक्युलेटेड फ़ील्ड एक्सपोर्ट नहीं हो सका: {{error}}", + "output-type": "आउटपुट प्रकार", + "delete-title": "क्या आप वाकई कैलक्युलेटेड फ़ील्ड '{{title}}' को हटाना चाहते हैं?", + "delete-text": "सावधान रहें, पुष्टि के बाद कैलक्युलेटेड फ़ील्ड और उससे संबंधित सभी डेटा हमेशा के लिए मिटा दिए जाएँगे।", + "delete-multiple-title": "क्या आप वाकई { count, plural, =1 {1 कैलक्युलेटेड फ़ील्ड} other {# कैलक्युलेटेड फ़ील्ड्स} } को हटाना चाहते हैं?", + "delete-multiple-text": "सावधान रहें, पुष्टि के बाद सभी चुनी हुई कैलक्युलेटेड फ़ील्ड्स हटा दी जाएँगी और उनसे संबंधित सारा डेटा हमेशा के लिए मिटा दिया जाएगा।", + "test-with-this-message": "इस संदेश के साथ परीक्षण करें", + "use-latest-timestamp": "नवीनतम टाइमस्टैम्प का उपयोग करें", + "entity-coordinates": "एंटिटी निर्देशांक", + "latitude-time-series-key": "अक्षांश समय श्रृंखला कुंजी", + "latitude-time-series-key-required": "अक्षांश समय श्रृंखला कुंजी आवश्यक है।", + "longitude-time-series-key": "देशांतर समय श्रृंखला कुंजी", + "longitude-time-series-key-required": "देशांतर समय श्रृंखला कुंजी आवश्यक है।", + "geofencing-zone-groups": "जियोफेंसिंग क्षेत्र समूह", + "geofencing-zone-groups-settings": "जियोफेंसिंग क्षेत्र समूह सेटिंग्स", + "target-zone": "लक्ष्य क्षेत्र", + "perimeter-key": "परिधि कुंजी", + "report-strategy": "रिपोर्ट रणनीति", + "no-zone-configured": "कम से कम एक क्षेत्र आवश्यक है।", + "no-zone-configured-required": "कम से कम एक क्षेत्र समूह कॉन्फ़िगर किया जाना चाहिए।", + "add-zone-group": "क्षेत्र समूह जोड़ें", + "report-transition-event-only": "केवल संक्रमण घटनाएँ", + "report-presence-status-only": "केवल उपस्थिति स्थिति", + "report-transition-event-and-presence": "उपस्थिति स्थिति और संक्रमण घटनाएँ", + "perimeter-attribute-key": "परिधि विशेषता कुंजी", + "perimeter-attribute-key-required": "परिधि विशेषता कुंजी आवश्यक है।", + "perimeter-attribute-key-pattern": "परिधि विशेषता कुंजी अमान्य है।", + "entity-zone-relationship": "एंटिटी से क्षेत्रों तक पथ", + "direction": "रिलेशन दिशा", + "direction-from": "एंटिटी से क्षेत्र तक", + "direction-to": "क्षेत्र से एंटिटी तक", + "relation-type": "रिलेशन प्रकार", + "create-relation-with-matched-zones": "मैच किए गए क्षेत्रों के साथ स्रोत एंटिटी के लिए रिलेशन बनाएं", + "relation-level": "रिलेशन स्तर", + "fetch-last-available-level": "केवल अंतिम उपलब्ध स्तर प्राप्त करें", + "zone-group-refresh-interval": "क्षेत्र समूह रिफ्रेश अंतराल", + "copy-zone-group-name": "क्षेत्र समूह का नाम कॉपी करें", + "open-details-page": "एंटिटी विवरण पृष्ठ खोलें", + "level": "स्तर", + "direction-level": "दिशा", + "direction-up": "ऊपर", + "direction-up-parent": "माता-पिता तक ऊपर", + "direction-down": "नीचे", + "direction-down-child": "बच्चे तक नीचे", + "add-level": "स्तर जोड़ें", + "delete-level": "स्तर हटाएं", + "no-level": "कोई स्तर कॉन्फ़िगर नहीं किया गया", + "levels-required": "कम से कम एक स्तर कॉन्फ़िगर किया जाना चाहिए।", + "max-allowed-levels-error": "रिलेशन स्तर अधिकतम अनुमत सीमा से अधिक है।", + "propagation-path-related-entities": "संबंधित एंटिटीज़ तक प्रसार पथ", + "propagate-type": { + "arguments-only": "केवल तर्क", + "expression-result": "गणना परिणाम" + }, + "script": "स्क्रिप्ट", + "data-propagate": "प्रसारित करने के लिए डेटा", + "output-key": "आउटपुट कुंजी", + "copy-output-key": "आउटपुट कुंजी कॉपी करें", + "aggregation-path-related-entities": "संबंधित एंटिटीज़ तक संकलन पथ", + "deduplication-interval": "डेडुप्लिकेशन अंतराल", + "deduplication-interval-min": "डेडुप्लिकेशन अंतराल कम से कम {{ sec }} सेकंड होना चाहिए।", + "deduplication-interval-hint": "टेलीमेट्री संकलनों के बीच न्यूनतम समय।", + "deduplication-interval-required": "डेडुप्लिकेशन अंतराल आवश्यक है।", + "calculated-field-filter-title": "कैलक्युलेटेड फ़ील्ड फ़िल्टर", + "filter-title": "फ़िल्टर", + "calculated-field-types": "कैलक्युलेटेड फ़ील्ड प्रकार", + "events": "घटनाएँ", + "any-type": "कोई भी प्रकार", + "metrics": { + "metrics": "मेट्रिक्स", + "metrics-empty": "कम से कम एक मेट्रिक को कॉन्फ़िगर किया जाना चाहिए।", + "metric-name": "मेट्रिक नाम", + "metric-name-required": "मेट्रिक नाम आवश्यक है।", + "metric-name-pattern": "मेट्रिक नाम अमान्य है।", + "metric-name-duplicate": "इस नाम वाला मेट्रिक पहले से मौजूद है।", + "metric-name-max-length": "मेट्रिक नाम 256 अक्षरों से कम होना चाहिए।", + "metric-name-forbidden": "मेट्रिक नाम आरक्षित है और इसे उपयोग नहीं किया जा सकता।", + "copy-metric-name": "मेट्रिक नाम कॉपी करें", + "argument-name": "तर्क नाम", + "aggregation": "संकलन", + "aggregation-type": { + "avg": "औसत", + "min": "न्यूनतम", + "max": "अधिकतम", + "sum": "योग", + "count": "गणना", + "count-unique": "विशिष्ट गणना" + }, + "filtered": "फ़िल्टर किया गया", + "value-source": "मान स्रोत", + "value-source-hint": "परिभाषित करता है कि संकलन के लिए मान कैसे प्राप्त किया जाता है।", + "value-source-type": { + "key": "कुंजी", + "function": "फ़ंक्शन" + }, + "no-metrics-configured": "कम से कम एक मेट्रिक आवश्यक है।", + "add-metric": "मेट्रिक जोड़ें", + "max-metrics": "अधिकतम मेट्रिक्स संख्या पहुँच गई।", + "metric-settings": "मेट्रिक सेटिंग्स", + "filter": "फ़िल्टर", + "filter-hint": "संकलन के दौरान एंटिटीज़ को फ़िल्टर करने के लिए फ़िल्टरिंग सक्षम करता है। फ़िल्टर फ़ंक्शन को एक बूलियन मान लौटाना चाहिए और यह सभी कॉन्फ़िगर किए गए तर्कों का उपयोग कर सकता है।" + }, + "output-strategy": { + "strategy": "रणनीति", + "process-right-away": "तत्काल प्रक्रिया करें", + "process-rule-chains": "रूल चेन के माध्यम से प्रक्रिया करें", + "save-time-series": "समय श्रृंखला में सहेजें", + "save-database": "डेटाबेस में सहेजें", + "save-latest-values": "नवीनतम मानों में सहेजें", + "send-web-sockets": "WebSockets पर भेजें", + "save-calculated-fields": "कैलक्युलेटेड फ़ील्ड्स में भेजें", + "update-attribute-only-on-value-change": "मान में परिवर्तन होने पर ही गुण अपडेट करें", + "send-attributes-updated-notification": "गुण अपडेट सूचना भेजें", + "ttl": "कस्टम TTL", + "ttl-required": "TTL आवश्यक है", + "ttl-min": "केवल 0 न्यूनतम TTL अनुमत है", + "processing-parameters": "प्रसंस्करण पैरामीटर", + "hint": { + "strategy": "नियंत्रित करता है कि परिणाम तुरंत संसाधित किया जाए या अतिरिक्त प्रसंस्करण के लिए रूल चेन को भेजा जाए।", + "processing-options": "प्रसंस्करण विकल्प", + "update-attribute-only-on-value-change": "प्रत्येक आने वाले संदेश पर गुण को अपडेट करता है, चाहे मान में परिवर्तन हुआ हो या नहीं। यह API उपयोग बढ़ाता है और प्रदर्शन को कम करता है।", + "update-attribute-only-on-value-change-enabled": "गुण को केवल तब अपडेट करें जब मान में परिवर्तन हो। यदि मान अपरिवर्तित रहता है, तो समय-चिह्न अपडेट नहीं होते और सूचनाएँ नहीं भेजी जातीं।", + "send-attributes-updated-notification": "गुण अपडेट इवेंट को डिफ़ॉल्ट रूल चेन को भेजता है।", + "save-time-series": "समय श्रृंखला डेटा को डेटाबेस में ts_kv तालिका में सहेजता है।", + "save-database": "गुण डेटा को डेटाबेस में सहेजता है।", + "save-latest-values": "यदि नया मान नवीनतम है तो समय श्रृंखला डेटा को डेटाबेस में ts_kv_latest तालिका में अपडेट करता है।", + "send-web-sockets-attribute": "WebSocket सब्सक्रिप्शंस को गुण डेटा के अपडेट के बारे में सूचित करता है।", + "send-web-sockets-time-series": "WebSocket सब्सक्रिप्शंस को समय श्रृंखला डेटा के अपडेट के बारे में सूचित करता है।", + "save-calculated-fields-attribute": "कैलक्युलेटेड फ़ील्ड्स को गुण डेटा के अपडेट के बारे में सूचित करता है।", + "save-calculated-fields-time-series": "कैलक्युलेटेड फ़ील्ड्स को समय श्रृंखला डेटा के अपडेट के बारे में सूचित करता है।", + "ttl": "समय श्रृंखला डेटा के लिए प्रतिधारण अवधि को परिभाषित करता है। यदि अक्षम है, तो टेनेन्ट प्रोफ़ाइल TTL का उपयोग किया जाता है।" + } + }, + "aggregate-interval-type": "संकलन अंतराल प्रकार", + "aggregate-interval-value": "संकलन अंतराल मान", + "aggregate-interval-value-required": "संकलन अंतराल मान आवश्यक है।", + "aggregate-interval-value-min": "संकलन अंतराल मान कम से कम { sec, plural, =0 {0 सेकंड} =1 {1 सेकंड} other {# सेकंड} } होना चाहिए।", + "aggregate-interval-value-step-multiple-of": "संकलन अंतराल मान 1 दिन का भाजक या गुणज होना चाहिए।", + "aggregate-period": { + "hour": "घंटा", + "day": "दिन", + "week": "सप्ताह (सोम - रविवार)", + "week-sun-sat": "सप्ताह (रविवार - शनिवार)", + "month": "महीना", + "quarter": "तिमाही", + "year": "वर्ष", + "custom": "कस्टम" + }, + "aggregate-period-hint-offset": "आपका संकलन अंतराल होगा: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "आपका संकलन अंतराल होगा: {{ interval }} और इसी तरह।", + "entity-aggregation": { + "argument-hint": "डेटा वर्तमान एंटिटी से फ़ेच किया जाएगा।", + "argument-title-hint": "संकलन के लिए उपयोग होने वाले इनपुट तर्कों को परिभाषित करता है।", + "argument-setting-hint": "इस कैल्क्युलेटेड फ़ील्ड के लिए 'नवीनतम टेलीमेट्री' ही एकमात्र उपलब्ध तर्क प्रकार है।", + "aggregation-interval": "संकलन अंतराल", + "aggregation-interval-hint": "यह परिभाषित करता है कि संकलन कितनी बार किया जाता है। उदाहरण: हर 1 घंटे पर डेटा 00:00, 01:00, 02:00, आदि पर संकलित होता है। संकलन परिणाम उस टाइमस्टैम्प के साथ स्टोर किए जाते हैं जो संकलन अंतराल की शुरुआत के अनुरूप होता है।", + "apply-offset": "संकलन अंतराल पर ऑफ़सेट लागू करें", + "apply-offset-hint": "यह परिभाषित करता है कि प्रत्येक संकलन अवधि की शुरुआत को कितना शिफ़्ट करना है (उदा., +10 मिनट - 00:10, 01:10)।", + "offset-value": "ऑफ़सेट मान", + "offset-value-required": "ऑफ़सेट मान आवश्यक है।", + "offset-value-min": "ऑफ़सेट मान एक धनात्मक पूर्णांक होना चाहिए।", + "offset-value-max": "ऑफ़सेट मान संकलन अंतराल मान से कम होना चाहिए।", + "wait-delay": "देरी से आने वाली टेलीमेट्री के लिए await टाइमआउट लागू करें", + "wait-delay-hint": "यह परिभाषित करता है कि अंतराल समाप्त होने के बाद देरी से आने वाली टेलीमेट्री के लिए कितनी देर प्रतीक्षा करनी है। यदि ऐसी टेलीमेट्री आती है, तो उस अंतराल के परिणाम की पुनर्गणना की जाएगी।", + "duration": "अवधि", + "duration-required": "अवधि आवश्यक है।", + "duration-min": "अवधि कम से कम 1 मिनट होनी चाहिए।", + "duration-hint": "अंतराल समाप्त होने के बाद देरी से आने वाले डेटा के लिए कितनी देर प्रतीक्षा करनी है।", + "produce-intermediate-result": "मध्यवर्ती परिणाम तैयार करें", + "produce-intermediate-result-hint": "वर्तमान अंतराल के दौरान मेट्रिक्स की गणना करके मध्यवर्ती परिणाम तैयार करता है। अपडेट {{ time }} में एक बार से अधिक बार नहीं होंगे।" + }, + "hint": { + "arguments-simple-with-rolling": "'Simple' प्रकार के कैल्क्युलेटेड फ़ील्ड में 'Time series rolling' प्रकार वाली कुंजियाँ नहीं होनी चाहिए।", + "arguments-propagate-arguments-with-rolling": "'Time series rolling' प्रकार 'Arguments only' प्रोपेगेशन के साथ संगत नहीं है।", + "arguments-propagate-argument-entity-type": "एंटिटी टाइप 'Arguments only' प्रोपेगेशन के साथ संगत नहीं है।", + "arguments-propagate-argument-must-current-entity": "कम से कम एक तर्क को 'Current entity' सोर्स एंटिटी टाइप के साथ कॉन्फ़िगर किया जाना चाहिए।", + "arguments-empty": "कम से कम एक तर्क निर्दिष्ट किया जाना चाहिए।", + "expression-required": "एक्सप्रेशन आवश्यक है।", + "expression-invalid": "एक्सप्रेशन अमान्य है।", + "expression-max-length": "एक्सप्रेशन की लंबाई 255 अक्षरों से कम होनी चाहिए।", + "argument-name-required": "आर्गुमेंट नाम आवश्यक है।", + "argument-name-pattern": "आर्गुमेंट नाम अमान्य है।", + "argument-name-duplicate": "ऐसे नाम वाला आर्गुमेंट पहले से मौजूद है।", + "argument-name-max-length": "आर्गुमेंट नाम 256 अक्षरों से कम होना चाहिए।", + "argument-name-forbidden": "यह आर्गुमेंट नाम आरक्षित है और इसका उपयोग नहीं किया जा सकता।", + "output-key-required": "आउटपुट कुंजी आवश्यक है।", + "output-key-pattern": "आउटपुट कुंजी अमान्य है।", + "output-key-duplicate": "इस नाम की कुंजी पहले से मौजूद है।", + "output-key-max-length": "आउटपुट कुंजी 256 अक्षरों से कम होनी चाहिए।", + "output-key-forbidden": "आउटपुट कुंजी आरक्षित है और इसका उपयोग नहीं किया जा सकता।", + "entity-type-required": "एंटिटी प्रकार आवश्यक है।", + "name-required": "नाम आवश्यक है।", + "name-pattern": "नाम अमान्य है।", + "name-duplicate": "इस नाम वाला नाम पहले से मौजूद है।", + "name-max-length": "नाम 256 अक्षरों से कम होना चाहिए।", + "name-forbidden": "नाम आरक्षित है और इसका उपयोग नहीं किया जा सकता।", + "argument-type-required": "आर्गुमेंट टाइप आवश्यक है।", + "max-args": "आर्गुमेंट्स की अधिकतम संख्या पूरी हो चुकी है।", + "decimals-range": "डिफ़ॉल्ट दशमलव 0 से 15 के बीच की संख्या होनी चाहिए।", + "expression": "डिफ़ॉल्ट एक्सप्रेशन दिखाता है कि तापमान को फ़ारेनहाइट से सेल्सियस में कैसे बदला जाए।", + "arguments-entity-not-found": "आर्गुमेंट की लक्षित एंटिटी नहीं मिली।", + "use-latest-timestamp": "अगर सक्षम किया गया, तो कैलक्युलेटेड मान को सर्वर समय की बजाय आर्गुमेंट्स की टेलीमेट्री के नवीनतम टाइमस्टैम्प के साथ सेव किया जाएगा।", + "entity-coordinates": "एंटिटी GPS निर्देशांक (अक्षांश और देशांतर) प्रदान करने वाली समय श्रृंखला कुंजियों को निर्दिष्ट करें।", + "geofencing-zone-groups": "एक या अधिक जियोफेंसिंग जोन समूहों को परिभाषित करें जिन्हें चेक किया जाना चाहिए (उदाहरण: 'allowedZones', 'restrictedZones')। प्रत्येक समूह का एक अद्वितीय नाम होना चाहिए, जिसका उपयोग कैल्क्युलेटेड फ़ील्ड आउटपुट टेलीमेट्री कुंजियों के लिए प्रीफिक्स के रूप में किया जाता है।", + "perimeter-attribute-key": "वह गुण कुंजी सेट करें जो जियोफेंसिंग जोन की परिधि परिभाषा को शामिल करती है। परिधि हमेशा जोन एंटिटी के सर्वर-साइड गुणों से ली जाती है।", + "report-strategy": "उपस्थिति स्थिति रिपोर्ट करती है कि एंटिटी वर्तमान में जोन समूह के अंदर या बाहर है। संक्रमण घटनाएँ रिपोर्ट करती हैं जब एंटिटी जोन समूह में प्रवेश करती है या छोड़ देती है।", + "create-relation-with-matched-zones": "स्वचालित रूप से एंटिटी और उन क्षेत्रों के बीच संबंध बनाएं जिनमें वह वर्तमान में स्थित है। संबंध तब हटाए जाते हैं जब एंटिटी एक क्षेत्र छोड़ देती है और नए क्षेत्र में प्रवेश करते समय बनाए जाते हैं।", + "relation-type-required": "रिलेशन प्रकार आवश्यक है।", + "relation-level-required": "रिलेशन स्तर आवश्यक है।", + "relation-level-min": "न्यूनतम रिलेशन स्तर मान 1 है।", + "relation-level-max": "अधिकतम रिलेशन स्तर मान {{max}} है।", + "geofencing-empty": "कम से कम एक जोन समूह को कॉन्फ़िगर किया जाना चाहिए।", + "geofencing-entity-not-found": "जियोफेंसिंग लक्ष्य एंटिटी नहीं मिली।", + "max-geofencing-zone": "जियोफेंसिंग क्षेत्रों की अधिकतम संख्या प्राप्त हो गई है।", + "zone-group-refresh-interval": "परिभाषित करता है कि संबंधित एंटिटीज़ के माध्यम से कॉन्फ़िगर किए गए जोन समूहों को कितनी बार ताजगी की आवश्यकता है।", + "zone-group-refresh-interval-required": "जोन समूह रिफ्रेश अंतराल आवश्यक है।", + "zone-group-refresh-interval-min": "जोन समूह रिफ्रेश अंतराल कम से कम {{ min }} सेकंड होना चाहिए।", + "propagation-path-related-entities": "चयनित दिशा और रिलेशन प्रकार के आधार पर संबंधित एंटिटी तक एक सीधा, एकल-स्तरीय पथ परिभाषित करता है। केवल डिवाइस, एसेट, कस्टमर और टेनेन्ट एंटिटी के बीच के रिलेशन को समर्थन दिया जाता है। रिलेशन पथ द्वारा अधिकतम एंटिटीज़ {{ max }} हैं।", + "data-propagate": "परिभाषित करता है कि किस डेटा को नीचे कॉन्फ़िगर किए गए तर्कों से प्रसारित किया जाएगा। 'Arguments only' डेटा को सीधे प्राप्त करता है, जबकि 'Expression result' उस डेटा से नया मान गणना करता है।", + "aggregation-path-related-entities": "दिशा और रिलेशन प्रकार के आधार पर पैरेंट या चाइल्ड एंटिटीज़ के साथ सीधे रिलेशन के माध्यम से एकल-स्तरीय संकलन पथ परिभाषित करता है। केवल डिवाइस, एसेट, कस्टमर और टेनेन्ट एंटिटी के बीच के रिलेशन को समर्थन दिया जाता है। रिलेशन पथ द्वारा अधिकतम एंटिटीज़ {{ max }} हैं।", + "arguments-aggregation": "फ़िल्टरिंग और संकलन के लिए उपयोग किए गए इनपुट तर्कों को परिभाषित करता है।", + "setting-arguments-aggregation": "डेटा संबंधित एंटिटीज़ से प्राप्त किया जाएगा जो संकलन पथ में कॉन्फ़िगर की गई हैं।", + "metrics": "कॉन्फ़िगर किए गए तर्कों के आधार पर संकलित मेट्रिक्स को परिभाषित करता है।", + "entity-aggregation-metrics": "निर्धारित समय अंतरालों पर कॉन्फ़िगर किए गए तर्कों के आधार पर संकलित मेट्रिक्स को परिभाषित करता है।", + "import-invalid-calculated-field-type": "कैलक्युलेटेड फ़ील्ड आयात करने में असमर्थ: अमान्य कैलक्युलेटेड फ़ील्ड संरचना।", + "simple-expression-title": "अंकगणितीय अभिव्यक्ति जो परिभाषित करती है कि कैल्क्युलेटेड मान कैसे गणना किया जाता है।", + "script-title": "TBEL स्क्रिप्ट जो गणना लॉजिक और आउटपुट मानों को परिभाषित करती है।", + "simple-arguments": "अंकगणितीय अभिव्यक्ति जो परिभाषित करती है कि कैल्क्युलेटेड मान कैसे गणना किया जाता है।", + "script-arguments": "स्क्रिप्ट के लिए उपलब्ध इनपुट तर्कों को परिभाषित करता है।" + } + }, + "alarm-rule": { + "alarm-rules-tab": "अलार्म नियम", + "alarm-rule": "अलार्म नियम", + "alarm-rules": "अलार्म नियम", + "alarm-rules-old": "पुराना", + "alarm-rules-actual": "वर्तमान", + "severities": "गंभीरता", + "cleared": "क्लियर स्थिति", + "delete-title": "क्या आप वाकई अलार्म नियम '{{title}}' को हटाना चाहते हैं?", + "delete-text": "सावधान रहें, पुष्टि के बाद अलार्म नियम और सभी संबंधित डेटा पुनः प्राप्त नहीं किए जा सकेंगे।", + "delete-multiple-title": "क्या आप वाकई { count, plural, =1 {1 अलार्म नियम} other {# अलार्म नियम} } हटाना चाहते हैं?", + "delete-multiple-text": "सावधान रहें, पुष्टि के बाद सभी चयनित अलार्म नियम हटा दिए जाएंगे और सभी संबंधित डेटा पुनः प्राप्त नहीं किए जा सकेंगे।", + "create": "नया अलार्म नियम बनाएं", + "add": "अलार्म नियम जोड़ें", + "copy": "अलार्म नियम कॉन्फ़िगरेशन कॉपी करें", + "details": "अलार्म नियम विवरण", + "no-found": "कोई अलार्म नियम नहीं मिला", + "list": "{ count, plural, =1 {एक अलार्म नियम} other {# अलार्म नियम} }", + "selected-fields": "{ count, plural, =1 {1 अलार्म नियम} other {# अलार्म नियम} } चयनित", + "import": "अलार्म नियम आयात करें", + "file": "अलार्म नियम फ़ाइल", + "export": "अलार्म नियम निर्यात करें", + "export-failed-error": "अलार्म नियम निर्यात करने में असमर्थ: {{error}}", + "entity-type": "एंटिटी प्रकार", + "entity-type-required": "एंटिटी प्रकार आवश्यक है।", + "alarm-type": "अलार्म प्रकार", + "alarm-type-hint": "अलार्म उत्पत्ति (डिवाइस, एसेट, आदि) के दायरे में अद्वितीय पहचानकर्ता (जैसे, HighTempAlarm) ताकि संघर्षों से बचा जा सके।", + "alarm-type-required": "अलार्म प्रकार आवश्यक है।", + "alarm-type-pattern": "अलार्म प्रकार अमान्य है।", + "alarm-type-max-length": "अलार्म प्रकार 256 अक्षरों से कम होना चाहिए।", + "clear-alarm": "अलार्म को क्लियर करें", + "value-argument": "तर्क", + "value-argument-required": "तर्क आवश्यक है।", + "static-settings": "स्थैतिक सेटिंग्स", + "configuration": "कॉन्फ़िगरेशन", + "static-schedule": "स्थैतिक", + "dynamic-schedule": "डायनेमिक", + "operation-and": "AND", + "operation-or": "OR", + "condition-during": "{{during}} के दौरान", + "condition-during-dynamic": "\"{{attribute}}\" के दौरान", + "condition-repeat-times": "{ count, plural, =1 {1 बार} other {# बार} } दोहराता है", + "condition-repeat-times-dynamic": "\"{{attribute}}\" बार दोहराता है", + "filter-preview": "फ़िल्टर पूर्वावलोकन", + "condition-settings": "स्थिति सेटिंग्स", + "static": "स्थैतिक", + "dynamic": "डायनेमिक", + "argument-filters": "तर्क फ़िल्टर", + "argument-name": "तर्क नाम", + "value-type": "मान प्रकार", + "general": "सामान्य", + "filters": "फ़िल्टर", + "date-time-hint": "तर्क को एपोक मिलिसेकंड में होना चाहिए। उदाहरण: 1698839340000 = 2023-11-01 12:49:00 UTC।", + "operation": "ऑपरेशन", + "value-source": "मान स्रोत", + "value": "मान", + "ignore-case": "अक्षरमाला का ध्यान न दें", + "condition": "स्थिति", + "script": "स्क्रिप्ट", + "add-filter": "तर्क फ़िल्टर जोड़ें", + "edit-filter": "तर्क फ़िल्टर संपादित करें", + "remove-filter": "तर्क फ़िल्टर हटाएँ", + "no-filter": "कम से कम एक फ़िल्टर आवश्यक है।", + "conditions": { + "simple": "साधारण", + "duration": "अवधि", + "repeating": "दोहराना" + }, + "schedule-title": "अनुसूची", + "edit-schedule": "अलार्म अनुसूची संपादित करें", + "schedule-type": "अनुसूचक प्रकार", + "schedule-type-required": "अनुसूचक प्रकार आवश्यक है।", + "schedule": { + "any-time": "हमेशा सक्रिय", + "specific-time": "विशिष्ट समय पर सक्रिय", + "custom": "कस्टम" + }, + "schedule-day": { + "monday": "सोमवार", + "tuesday": "मंगलवार", + "wednesday": "बुधवार", + "thursday": "गुरुवार", + "friday": "शुक्रवार", + "saturday": "शनिवार", + "sunday": "रविवार" + }, + "schedule-days": "दिन", + "schedule-time": "समय", + "schedule-time-from": "से", + "schedule-time-to": "तक", + "schedule-days-of-week-required": "कम से कम एक दिन को सप्ताह के लिए चुना जाना चाहिए।", + "tbel": "TBEL", + "expression-type": { + "simple": "साधारण", + "script": "स्क्रिप्ट" + }, + "operation-type": { + "and": "और", + "or": "या" + }, + "filter-predicate-type": { + "string": "स्ट्रिंग", + "numeric": "संख्यात्मक", + "boolean": "बूलियन", + "complex": "जटिल" + }, + "alarm-rule-additional-info": "अतिरिक्त जानकारी", + "edit-alarm-rule-additional-info": "अतिरिक्त जानकारी संपादित करें", + "alarm-rule-additional-info-placeholder": "कृपया अपनी टिप्पणियाँ और समायोजन यहां प्रदान करें ताकि उन्हें अलार्म विवरण में अतिरिक्त जानकारी के तहत दिखाया जा सके।", + "alarm-rule-additional-info-hint": "संकेत: अलार्म नियम स्थिति में उपयोग किए गए तर्कों के मानों को प्रतिस्थापित करने के लिए ${तर्क नाम} का उपयोग करें।", + "alarm-rule-additional-info-icon-hint": "अलार्म नियम स्थिति में उपयोग किए गए तर्कों के मानों को प्रतिस्थापित करने के लिए तर्क नाम का उपयोग करें।", + "alarm-rule-mobile-dashboard": "मोबाइल डैशबोर्ड", + "alarm-rule-mobile-dashboard-hint": "मोबाइल एप्लिकेशन द्वारा अलार्म विवरण डैशबोर्ड के रूप में उपयोग किया जाता है।", + "alarm-rule-no-mobile-dashboard": "कोई डैशबोर्ड चयनित नहीं है", + "alarm-rule-condition": "अलार्म नियम स्थिति", + "enter-alarm-rule-condition-prompt": "स्थिति जोड़ें", + "enter-alarm-rule-clear-condition-prompt": "साफ़ करने की स्थिति जोड़ें", + "edit-alarm-rule-condition": "अलार्म स्थिति", + "condition-type": "स्थिति प्रकार", + "condition-type-hint": "\"अवधि\" और \"दोहराना\" विकल्प तब उपलब्ध नहीं होते जब \"नहीं मिल रहा है\" ऑपरेशन फ़िल्टर में उपयोग किया जाता है।", + "select-alarm-severity": "अलार्म गंभीरता चुनें", + "add-create-alarm-rule-prompt": "कम से कम एक ट्रिगर स्थिति आवश्यक है।", + "add-create-alarm-rule": "ट्रिगर स्थिति जोड़ें", + "add-clear-alarm-rule": "साफ़ करने की स्थिति जोड़ें", + "condition-duration": "स्थिति अवधि", + "condition-duration-value": "अवधि मान", + "condition-duration-time-unit": "समय इकाई", + "condition-duration-value-range": "अवधि मान को 1 से 2147483647 के बीच होना चाहिए।", + "condition-duration-value-pattern": "अवधि मान पूर्णांक होना चाहिए।", + "condition-duration-value-required": "अवधि मान आवश्यक है।", + "condition-duration-time-unit-required": "समय इकाई आवश्यक है।", + "condition-repeating-value": "घटनाओं की गिनती", + "condition-repeating-value-hint": "अलार्म नियम तर्क के किसी भी अपडेट को घटना के रूप में गिना जाएगा", + "condition-repeating-value-range": "घटनाओं की गिनती 1 से 2147483647 के बीच होनी चाहिए।", + "condition-repeating-value-pattern": "घटनाओं की गिनती पूर्णांक होनी चाहिए।", + "condition-repeating-value-required": "घटनाओं की गिनती आवश्यक है।", + "create-conditions": "ट्रिगर स्थितियाँ", + "clear-condition": "साफ़ करने की स्थिति", + "no-clear-alarm-rule": "कोई साफ़ करने की स्थिति कॉन्फ़िगर नहीं की गई है।", + "advanced-settings": "उन्नत सेटिंग्स", + "propagate-alarm": "अलार्म को संबंधित एंटिटीज़ में प्रसारित करें", + "alarm-rule-relation-types-list": "रिलेशन प्रकार", + "alarm-rule-relation-types-list-hint": "संबंधित एंटिटीज़ को फ़िल्टर करने के लिए रिलेशन प्रकारों को परिभाषित करता है। यदि सेट नहीं किया गया हो, तो अलार्म सभी संबंधित एंटिटीज़ में प्रसारित किया जाएगा।", + "propagate-alarm-to-owner": "अलार्म को एंटिटी मालिक (कस्टमर या टेनेन्ट) को प्रसारित करें", + "propagate-alarm-to-tenant": "अलार्म को टेनेन्ट को प्रसारित करें", + "alarm-rule-filter-title": "अलार्म नियम फ़िल्टर", + "filter-title": "फ़िल्टर", + "debugging": "अलार्म नियम डिबगिंग", + "any-type": "कोई भी प्रकार", + "enter-alarm-rule-type": "अलार्म प्रकार दर्ज करें", + "no-alarm-rule-types-matching": "कोई अलार्म प्रकार '{{entitySubtype}}' से मेल नहीं खाता पाया गया।", + "alarm-rule-type-list-empty": "कोई अलार्म प्रकार चयनित नहीं है।", + "alarm-rule-type-list": "अलार्म प्रकार सूची", + "alarm-rule-entity-list": "एंटिटी सूची", + "missing-for": "नहीं मिल रहा है", + "time-unit": "इकाई", + "mode": "मोड", + "type": "प्रकार", + "value-required": "मान आवश्यक है।", + "min-value": "मान 1 या उससे अधिक होना चाहिए।", + "argument-in-use": "तर्क को सामान्य तर्क के रूप में उपयोग किया जा रहा है।", + "import-invalid-alarm-rule-type": "अलार्म नियम आयात करने में असमर्थ: अमान्य अलार्म नियम संरचना।", + "no-filter-preview": "कोई फ़िल्टर निर्दिष्ट नहीं किया गया", + "filter-operation": { + "and": "और", + "or": "या" + } + }, + "ai-models": { + "ai-models": "AI मॉडल्स", + "ai-model": "AI मॉडल", + "model": "मॉडल", + "name": "नाम", + "ai-provider": "AI प्रदाता", + "no-found": "कोई AI मॉडल नहीं मिला", + "list": "{ count, plural, =1 {एक मॉडल} other {# मॉडलों की सूची} }", + "selected-fields": "{ count, plural, =1 {1 मॉडल} other {# मॉडल्स} } चयनित", + "add": "मॉडल जोड़ें", + "delete-model-title": "क्या आप वाकई मॉडल '{{modelName}}' हटाना चाहते हैं?", + "delete-model-text": "सावधान रहें, पुष्टि के बाद मॉडल और उससे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "delete-models-title": "क्या आप वाकई { count, plural, =1 {1 मॉडल} other {# मॉडल्स} } हटाना चाहते हैं?", + "delete-models-text": "सावधान रहें, पुष्टि के बाद सभी चयनित मॉडल्स हटा दिए जाएँगे और उनसे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "GitHub Models", + "ollama": "Ollama" + }, + "name-required": "नाम आवश्यक है।", + "name-max-length": "नाम 255 अक्षरों या उससे कम का होना चाहिए।", + "provider": "प्रदाता", + "api-key": "API कुंजी", + "api-key-required": "API कुंजी आवश्यक है।", + "api-key-open-ai-required": "आधिकारिक OpenAI API का उपयोग करते समय API कुंजी आवश्यक है।", + "project-id": "प्रोजेक्ट ID", + "project-id-required": "प्रोजेक्ट ID आवश्यक है।", + "location": "स्थान", + "location-required": "स्थान आवश्यक है।", + "service-account-key-file": "सर्विस अकाउंट की फ़ाइल", + "service-account-key-file-required": "सर्विस अकाउंट की फ़ाइल आवश्यक है।", + "no-file": "कोई फ़ाइल चयनित नहीं है।", + "drop-file": "फ़ाइल यहाँ छोड़ें या फ़ाइल चुनने के लिए क्लिक करें।", + "personal-access-token": "पर्सनल ऐक्सेस टोकन", + "personal-access-token-required": "पर्सनल ऐکسेस टोकन आवश्यक है।", + "configuration": "कॉन्फ़िगरेशन", + "model-id": "मॉडल ID", + "model-id-required": "मॉडल ID आवश्यक है।", + "deployment-name": "डिप्लॉयमेंट नाम", + "deployment-name-required": "डिप्लॉयमेंट नाम आवश्यक है।", + "set": "सेट करें", + "region": "रीजन", + "region-required": "रीजन आवश्यक है।", + "access-key-id": "ऐक्सेस की ID", + "access-key-id-required": "ऐक्सेस की ID आवश्यक है।", + "secret-access-key": "सीक्रेट ऐक्सेस की", + "secret-access-key-required": "सीक्रेट ऐक्सेस की आवश्यक है।", + "temperature": "टेम्परेचर", + "temperature-hint": "मॉडल के आउटपुट में रैंडमनेस के स्तर को समायोजित करता है। अधिक मान रैंडमनेस बढ़ाते हैं, जबकि कम मान उसे घटाते हैं।", + "temperature-min": "0 या उससे अधिक होना चाहिए।", + "top-p": "टॉप P", + "top-p-hint": "मॉडल के चुनने के लिए सबसे संभावित टोकन्स का एक पूल बनाता है। अधिक मान बड़ा और अधिक विविध पूल बनाते हैं, जबकि कम मान छोटा पूल बनाते हैं।", + "top-p-min-max": "0 से बड़ा और अधिकतम 1 होना चाहिए।", + "top-k": "टॉप K", + "top-k-hint": "मॉडल के विकल्पों को \"K\" सबसे संभावित टोकन्स के एक निश्चित सेट तक सीमित करता है।", + "top-k-min": "0 या उससे अधिक होना चाहिए।", + "presence-penalty": "प्रेज़ेन्स पेनल्टी", + "presence-penalty-hint": "यदि कोई टोकन पहले से टेक्स्ट में आ चुका है, तो उसकी संभावना पर एक स्थिर पेनल्टी लागू करता है।", + "frequency-penalty": "फ़्रीक्वेंसी पेनल्टी", + "frequency-penalty-hint": "टोकन की संभावना पर ऐसी पेनल्टी लागू करता है जो टेक्स्ट में उसकी आवृत्ति के आधार पर बढ़ती है।", + "max-output-tokens": "अधिकतम आउटपुट टोकन्स", + "max-output-tokens-hint": "एक ही रिस्पॉन्स में मॉडल कितने अधिकतम टोकन्स जेनरेट कर सकता है,\nइसे सेट करता है।", + "context-length": "कॉन्टेक्स्ट लंबाई", + "context-length-hint": "कॉन्टेक्स्ट विंडो का आकार टोकन्स में परिभाषित करता है। यह मान मॉडल के लिए कुल मेमोरी लिमिट सेट करता है, जिसमें उपयोगकर्ता का इनपुट और जेनरेट किया गया रिस्पॉन्स दोनों शामिल होते हैं।", + "endpoint": "एंडपॉइंट", + "endpoint-required": "एंडपॉइंट आवश्यक है।", + "baseurl": "Base URL", + "baseurl-required": "Base URL आवश्यक है।", + "service-version": "सर्विस संस्करण", + "check-connectivity": "कनेक्टिविटी जाँचें", + "check-connectivity-success": "टेस्ट अनुरोध सफल रहा", + "check-connectivity-failed": "टेस्ट अनुरोध असफल रहा", + "no-model-matching": "'{{entity}}' से मेल खाते कोई मॉडल नहीं मिले।", + "model-required": "मॉडल आवश्यक है।", + "no-model-text": "कोई मॉडल नहीं मिला।", + "authentication": "प्रमाणीकरण", + "authentication-basic-hint": "मानक HTTP बेसिक प्रमाणीकरण का उपयोग करता है। उपयोगकर्ता नाम और पासवर्ड को जोड़ा जाएगा, Base64-एन्कोड किया जाएगा और प्रत्येक अनुरोध के साथ Ollama सर्वर को भेजी जाने वाली \"प्राधिकरण\" हेडर में शामिल किया जाएगा।", + "authentication-token-hint": "Bearer टोकन प्रमाणीकरण का उपयोग करता है। दिया गया टोकन सीधे \"प्राधिकरण\" हेडर में Ollama सर्वर को भेजे जाने वाले प्रत्येक अनुरोध के साथ भेजा जाएगा।", + "authentication-type": { + "none": "कोई नहीं", + "basic": "बेसिक", + "token": "टोकन" + }, + "username": "उपयोगकर्ता नाम", + "username-required": "उपयोगकर्ता नाम आवश्यक है।", + "password": "पासवर्ड", + "password-required": "पासवर्ड आवश्यक है।", + "token": "टोकन", + "token-required": "टोकन आवश्यक है।" + }, + "confirm-on-exit": { + "message": "आपके पास बिना सहेजे गए परिवर्तन हैं। क्या आप वाकई इस पेज से बाहर जाना चाहते हैं?", + "html-message": "आपके पास बिना सहेजे गए परिवर्तन हैं.
    क्या आप वाकई इस पेज से बाहर जाना चाहते हैं?", + "title": "बिना सहेजे गए परिवर्तन" + }, + "contact": { + "country": "देश", + "country-required": "देश आवश्यक है।", + "country-object-required": "कृपया सूची से एक मान्य देश चुनें।", + "city": "शहर", + "state": "राज्य / प्रांत", + "postal-code": "ज़िप / डाक कोड", + "postal-code-invalid": "अमान्य ज़िप / डाक कोड फ़ॉर्मेट।", + "address": "पता", + "address2": "पता 2", + "phone": "फ़ोन", + "email": "ईमेल", + "no-address": "कोई पता नहीं", + "no-country-found": "कोई देश नहीं मिला।", + "no-country-matching": "'{{country}}' से मेल खाता कोई देश नहीं मिला।", + "state-max-length": "राज्य की लंबाई 256 से कम होनी चाहिए", + "phone-max-length": "फ़ोन नंबर 256 अक्षरों से कम होना चाहिए", + "city-max-length": "निर्दिष्ट शहर 256 अक्षरों से कम होना चाहिए" + }, + "common": { + "name": "नाम", + "type": "प्रकार", + "general": "सामान्य", + "username": "उपयोगकर्ता नाम", + "password": "पासवर्ड", + "data": "डेटा", + "timestamp": "टाइमस्टैम्प", + "enter-username": "उपयोगकर्ता नाम दर्ज करें", + "enter-password": "पासवर्ड दर्ज करें", + "enter-search": "खोज लिखें", + "created-time": "बनाए जाने का समय", + "disabled": "निष्क्रिय", + "loading": "लोड हो रहा है...", + "proceed": "आगे बढ़ें", + "open-details-page": "विवरण पृष्ठ खोलें", + "not-found": "नहीं मिला", + "value": "मान", + "documentation": "प्रलेखन", + "time-left": "{{time}} बाकी", + "output": "आउटपुट", + "sort-asc": "आरोही", + "sort-desc": "अवरोही", + "suffix": { + "s": "s", + "ms": "ms" + }, + "hint": { + "name-required": "नाम आवश्यक है।", + "name-pattern": "नाम अमान्य है।", + "name-max-length": "नाम 256 अक्षरों से कम होना चाहिए।", + "title-required": "शीर्षक आवश्यक है।", + "title-pattern": "शीर्षक अमान्य है।", + "title-max-length": "शीर्षक 256 अक्षरों से कम होना चाहिए।", + "key-required": "कुंजी आवश्यक है।", + "key-pattern": "कुंजी अमान्य है।", + "key-max-length": "कुंजी 256 अक्षरों से कम होनी चाहिए।" + }, + "required-fields": "आवश्यक फ़ील्ड्स भरी नहीं गई हैं" + }, + "content-type": { + "json": "JSON", + "text": "टेक्स्ट", + "binary": "बाइनरी (Base64)" + }, + "color": { + "color": "रंग" + }, + "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-customer-edges": "कस्टमर Edge प्रबंधित करें", + "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": "शीर्षक आवश्यक है।", + "title-max-length": "शीर्षक 256 अक्षरों से कम होना चाहिए", + "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": "{ count, plural, =1 {1 कस्टमर} other {# कस्टमर} } चयनित", + "edges": "कस्टमर Edge इंस्टेंस", + "manage-edges": "Edge इंस्टेंस प्रबंधित करें" + }, + "css-size": { + "size-value-required": "आकार मान आवश्यक है", + "invalid-size-value": "आकार मान अमान्य है" + }, + "date": { + "last-update-n-ago": "अंतिम अपडेट N पहले", + "last-update-n-ago-text": "अंतिम अपडेट {{ agoText }}", + "custom-date": "कस्टम तिथि", + "format": "प्रारूप", + "preview": "पूर्वावलोकन", + "auto": "ऑटो", + "time-granularity-formats": "समय ग्रैन्युलैरिटी प्रारूप", + "unit-year": "वर्ष", + "unit-month": "महीने", + "unit-day": "दिन", + "unit-hour": "घंटे", + "unit-minute": "मिनट", + "unit-second": "सेकंड", + "unit-millisecond": "मिलीसेकंड" + }, + "datetime": { + "date-from": "तिथि से", + "time-from": "समय से", + "date-to": "तिथि तक", + "time-to": "समय तक", + "from": "से", + "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": "नया विजेट जोड़ें", + "add-widget-button-text": "विजेट जोड़ें", + "title": "शीर्षक", + "image": "डैशबोर्ड छवि", + "mobile-app-settings": "मोबाइल एप्लिकेशन सेटिंग्स", + "mobile-order": "मोबाइल एप्लिकेशन में डैशबोर्ड क्रम", + "mobile-hide": "मोबाइल एप्लिकेशन में डैशबोर्ड छिपाएँ", + "update-image": "डैशबोर्ड छवि अपडेट करें", + "update-new-version": "नया संस्करण अपलोड करें", + "upload-file-to-update": "अपडेट करने के लिए फ़ाइल अपलोड करें", + "take-screenshot": "स्क्रीनशॉट लें", + "select-widget-title": "विजेट चुनें", + "select-widget-value": "{{title}}: विजेट चुनें", + "select-widget-subtitle": "उपलब्ध विजेट प्रकारों की सूची", + "delete": "डैशबोर्ड हटाएँ", + "title-required": "शीर्षक आवश्यक है।", + "title-max-length": "शीर्षक 256 अक्षरों से कम होना चाहिए", + "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": "कोई छवि चयनित नहीं है", + "empty-image": "कोई छवि नहीं", + "drop-image": "कोई छवि यहाँ छोड़ें या अपलोड करने के लिए फ़ाइल चुनने हेतु क्लिक करें।", + "maximum-upload-file-size": "अधिकतम अपलोड फ़ाइल आकार: {{ size }}", + "cannot-upload-file": "फ़ाइल अपलोड नहीं कर सकते", + "settings": "सेटिंग्स", + "move-all-widgets": "सभी विजेट्स को स्थानांतरित करें", + "move-by": "इतना स्थानांतरित करें", + "cols": "कॉलम", + "rows": "पंक्तियाँ", + "layout": "लेआउट", + "layout-type-default": "डिफ़ॉल्ट", + "layout-type-scada": "SCADA", + "layout-type-divider": "डिवाइडर", + "layout-settings-type": "लेआउट सेटिंग्स: {{ type }} ब्रेकपॉइंट", + "columns-count": "कॉलम की संख्या", + "columns-count-required": "कॉलम की संख्या आवश्यक है।", + "min-columns-count-message": "न्यूनतम कॉलम संख्या केवल 10 हो सकती है।", + "max-columns-count-message": "अधिकतम कॉलम संख्या केवल 1000 हो सकती है।", + "min-layout-width": "न्यूनतम लेआउट चौड़ाई", + "columns-suffix": "कॉलम", + "widgets-margins": "विजेट्स के बीच की मार्जिन", + "margin-required": "मार्जिन मान आवश्यक है।", + "min-margin-message": "न्यूनतम मार्जिन मान केवल 0 हो सकता है।", + "max-margin-message": "अधिकतम मार्जिन मान केवल 50 हो सकता है।", + "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 हो सकता है।", + "apply-outer-margin": "लेआउट के किनारों पर मार्जिन लागू करें", + "autofill-height": "लेआउट की ऊँचाई स्वतः भरें", + "mobile-layout": "मोबाइल लेआउट सेटिंग्स", + "mobile-row-height": "मोबाइल रो ऊँचाई", + "mobile-row-height-required": "मोबाइल रो ऊँचाई का मान आवश्यक है।", + "min-mobile-row-height-message": "न्यूनतम मोबाइल रो ऊँचाई मान केवल 5 पिक्सेल हो सकता है।", + "max-mobile-row-height-message": "अधिकतम मोबाइल रो ऊँचाई मान केवल 200 पिक्सेल हो सकता है।", + "row-height": "रो ऊँचाई", + "row-height-required": "रो ऊँचाई का मान आवश्यक है।", + "min-row-height-message": "न्यूनतम रो ऊँचाई मान केवल 5 पिक्सेल हो सकता है।", + "max-row-height-message": "अधिकतम रो ऊँचाई मान केवल 200 पिक्सेल हो सकता है।", + "display-first-in-mobile-view": "मोबाइल व्यू में सबसे पहले दिखाएँ", + "title-settings": "शीर्षक सेटिंग्स", + "display-title": "डैशबोर्ड शीर्षक दिखाएँ", + "title-color": "शीर्षक रंग", + "toolbar-settings": "टूलबार सेटिंग्स", + "hide-toolbar": "टूलबार छिपाएँ", + "toolbar-always-open": "टूलबार को हमेशा खुला रखें", + "display-dashboards-selection": "डैशबोर्ड चयन दिखाएँ", + "display-entities-selection": "एंटिटी चयन दिखाएँ", + "display-filters": "फ़िल्टर दिखाएँ", + "display-dashboard-timewindow": "समय विंडो दिखाएँ", + "display-dashboard-export": "एक्सपोर्ट विकल्प दिखाएँ", + "display-update-dashboard-image": "डैशबोर्ड इमेज अपडेट विकल्प दिखाएँ", + "dashboard-logo-settings": "डैशबोर्ड लोगो सेटिंग्स", + "display-dashboard-logo": "डैशबोर्ड फुलस्क्रीन मोड में लोगो दिखाएँ", + "dashboard-logo-image": "डैशबोर्ड लोगो इमेज", + "advanced-settings": "एडवांस्ड सेटिंग्स", + "dashboard-css": "डैशबोर्ड CSS", + "import": "डैशबोर्ड इम्पोर्ट करें", + "export": "डैशबोर्ड एक्सपोर्ट करें", + "export-failed-error": "डैशबोर्ड एक्सपोर्ट नहीं किया जा सका: {{error}}", + "export-prompt": "डैशबोर्ड इमेज और संसाधन एम्बेड करें", + "create-new-dashboard": "नया डैशबोर्ड बनाएँ", + "dashboard-file": "डैशबोर्ड फ़ाइल", + "invalid-dashboard-file-error": "डैशबोर्ड इम्पोर्ट नहीं किया जा सका: डैशबोर्ड डेटा संरचना अमान्य है।", + "dashboard-import-missing-aliases-title": "इम्पोर्ट किए गए डैशबोर्ड द्वारा उपयोग किए गए उपनाम (aliases) कॉन्फ़िगर करें", + "create-new-widget": "नया विजेट बनाएँ", + "import-widget": "विजेट इम्पोर्ट करें", + "widget-file": "विजेट फ़ाइल", + "invalid-widget-file-error": "विजेट इम्पोर्ट नहीं किया जा सका: विजेट डेटा संरचना अमान्य है।", + "widget-import-missing-aliases-title": "इम्पोर्ट किए गए विजेट द्वारा उपयोग किए गए उपनाम (aliases) कॉन्फ़िगर करें", + "open-toolbar": "डैशबोर्ड टूलबार खोलें", + "close-toolbar": "टूलबार बंद करें", + "configuration-error": "कॉन्फ़िगरेशन त्रुटि", + "alias-resolution-error-title": "डैशबोर्ड उपनाम कॉन्फ़िगरेशन त्रुटि", + "invalid-aliases-config": "कुछ उपनाम फ़िल्टर के लिए मेल खाते डिवाइस नहीं मिल सके।
    कृपया इस समस्या के समाधान के लिए अपने व्यवस्थापक से संपर्क करें।", + "select-devices": "डिवाइस चुनें", + "assignedToCustomer": "कस्टमर को असाइन किया गया", + "assignedToCustomers": "कस्टमर्स को असाइन किया गया", + "public": "सार्वजनिक", + "copyId": "डैशबोर्ड Id कॉपी करें", + "idCopiedMessage": "डैशबोर्ड Id क्लिपबोर्ड में कॉपी कर दी गई है", + "public-link": "सार्वजनिक लिंक", + "copy-public-link": "सार्वजनिक लिंक कॉपी करें", + "public-link-copied-message": "डैशबोर्ड का सार्वजनिक लिंक क्लिपबोर्ड में कॉपी कर दिया गया है", + "manage-states": "डैशबोर्ड स्टेट्स प्रबंधित करें", + "states": "डैशबोर्ड स्टेट्स", + "states-short": "स्टेट्स", + "search-states": "डैशबोर्ड स्टेट्स खोजें", + "selected-states": "{ count, plural, =1 {1 डैशबोर्ड स्टेट} other {# डैशबोर्ड स्टेट्स} } चयनित", + "edit-state": "डैशबोर्ड स्टेट संपादित करें", + "delete-state": "डैशबोर्ड स्टेट हटाएँ", + "add-state": "डैशबोर्ड स्टेट जोड़ें", + "no-states-text": "कोई स्टेट नहीं मिला", + "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": "स्टेट कंट्रोलर", + "state-controller-default": "स्टैटिक (अप्रचलित)", + "search": "डैशबोर्ड्स खोजें", + "selected-dashboards": "{ count, plural, =1 {1 डैशबोर्ड} other {# डैशबोर्ड्स} } चयनित", + "home-dashboard": "होम डैशबोर्ड", + "home-dashboard-hide-toolbar": "होम डैशबोर्ड टूलबार छिपाएँ", + "unassign-dashboard-from-edge-text": "पुष्टि के बाद डैशबोर्ड अनअसाइन कर दिया जाएगा और Edge द्वारा एक्सेस नहीं किया जा सकेगा।", + "unassign-dashboards-from-edge-title": "क्या आप वाकई { count, plural, =1 {1 डैशबोर्ड} other {# डैशबोर्ड्स} } को अनअसाइन करना चाहते हैं?", + "unassign-dashboards-from-edge-text": "पुष्टि के बाद सभी चयनित डैशबोर्ड्स अनअसाइन कर दिए जाएँगे और Edge द्वारा एक्सेस नहीं किए जा सकेंगे।", + "assign-dashboard-to-edge": "डैशबोर्ड(स) को Edge पर असाइन करें", + "assign-dashboard-to-edge-text": "कृपया वे डैशबोर्ड चुनें जिन्हें Edge को असाइन करना है", + "non-existent-dashboard-state-error": "Id \"{{ stateId }}\" वाला डैशबोर्ड स्टेट नहीं मिला", + "edit-mode": "एडिट मोड", + "duplicate-state-action": "स्टेट डुप्लिकेट करें", + "breakpoint-value": "ब्रेकपॉइंट ({{ value }})", + "breakpoints-id": { + "default": "डिफ़ॉल्ट", + "xs": "मोबाइल (xs)", + "sm": "टैबलेट (sm)", + "md": "लैपटॉप (md)", + "lg": "डेस्कटॉप (lg)", + "xl": "डेस्कटॉप (xl)" + }, + "view-format-type-grid": "ग्रिड", + "view-format-type-list": "सूची", + "view-format": "व्यू फ़ॉर्मेट" + }, + "datakey": { + "settings": "सेटिंग्स", + "general": "सामान्य", + "advanced": "एडवांस्ड", + "key": "कुंजी", + "keys": "कुंजियाँ", + "label": "लेबल", + "color": "रंग", + "units": "मान के बगल में दिखाने के लिए विशेष चिह्न", + "decimals": "दशमलव के बाद अंकों की संख्या", + "data-generation-func": "डेटा जनरेशन फ़ंक्शन", + "use-data-post-processing-func": "डेटा पोस्ट-प्रोसेसिंग फ़ंक्शन का उपयोग करें", + "configuration": "डेटा कुंजी कॉन्फ़िगरेशन", + "timeseries": "टाइम सीरीज़", + "attributes": "विशेषताएँ", + "entity-field": "एंटिटी फ़ील्ड", + "alarm": "अलार्म फ़ील्ड्स", + "timeseries-required": "एंटिटी टाइम सीरीज़ आवश्यक हैं।", + "timeseries-or-attributes-required": "एंटिटी टाइम सीरीज़/विशेषताएँ आवश्यक हैं।", + "alarm-fields-timeseries-or-attributes-required": "अलार्म फ़ील्ड्स या एंटिटी टाइम सीरीज़/विशेषताएँ आवश्यक हैं।", + "maximum-timeseries-or-attributes": "अधिकतम { count, plural, =1 {1 टाइम सीरीज़/विशेषता की अनुमति है।} other {# टाइम सीरीज़/विशेषताओं की अनुमति है} }", + "alarm-fields-required": "अलार्म फ़ील्ड्स आवश्यक हैं।", + "function-types": "फ़ंक्शन प्रकार", + "function-type": "फ़ंक्शन प्रकार", + "function-types-required": "फ़ंक्शन प्रकार आवश्यक हैं।", + "data-keys": "डेटा कुंजियाँ", + "data-key": "डेटा कुंजी", + "data-keys-required": "डेटा कुंजियाँ आवश्यक हैं।", + "data-key-required": "डेटा कुंजी आवश्यक है।", + "alarm-keys": "अलार्म डेटा कुंजियाँ", + "alarm-key": "अलार्म डेटा कुंजी", + "alarm-key-functions": "अलार्म कुंजी फ़ंक्शंस", + "alarm-key-function": "अलार्म कुंजी फ़ंक्शन", + "latest-keys": "नवीनतम डेटा कुंजियाँ", + "latest-key": "नवीनतम डेटा कुंजी", + "latest-key-functions": "नवीनतम कुंजी फ़ंक्शंस", + "latest-key-function": "नवीनतम कुंजी फ़ंक्शन", + "timeseries-keys": "टाइम सीरीज़ डेटा कुंजियाँ", + "timeseries-key": "टाइम सीरीज़ डेटा कुंजी", + "timeseries-key-functions": "टाइम सीरीज़ कुंजी फ़ंक्शंस", + "timeseries-key-function": "टाइम सीरीज़ कुंजी फ़ंक्शन", + "maximum-function-types": "अधिकतम { count, plural, =1 {1 फ़ंक्शन प्रकार की अनुमति है।} other {# फ़ंक्शन प्रकारों की अनुमति है} }", + "time-description": "वर्तमान मान का टाइमस्टैम्प;", + "value-description": "वर्तमान मान;", + "prev-value-description": "पिछले फ़ंक्शन कॉल का परिणाम;", + "time-prev-description": "पिछले मान का टाइमस्टैम्प;", + "prev-orig-value-description": "मूल पिछला मान;", + "aggregation": "एग्रीगेशन", + "aggregation-type-hint-common": "प्रदर्शन कारणों से, एग्रीगेटेड मानों की गणना केवल निश्चित समय अंतरालों (जैसे \"वर्तमान दिन\", \"वर्तमान माह\" आदि) के लिए उपलब्ध है और 'पिछले 30 मिनट' या 'पिछले 24 घंटे' जैसे स्लाइडिंग विंडो अंतरालों के लिए उपलब्ध नहीं है।", + "aggregation-type-none-hint": "नवीनतम मान लें।", + "aggregation-type-min-hint": "चयनित समय विंडो के भीतर डेटा पॉइंट्स में न्यूनतम मान खोजें।", + "aggregation-type-max-hint": "चयनित समय विंडो के भीतर डेटा पॉइंट्स में अधिकतम मान खोजें।", + "aggregation-type-avg-hint": "चयनित समय विंडो के भीतर डेटा पॉइंट्स का औसत मान निकालें।", + "aggregation-type-sum-hint": "चयनित समय विंडो के भीतर सभी डेटा पॉइंट्स के मानों का योग करें।", + "aggregation-type-count-hint": "चयनित समय विंडो के भीतर डेटा पॉइंट्स की कुल संख्या।", + "delta-calculation": "डेल्टा कैलक्युलेशन", + "enable-delta-calculation": "डेल्टा कैलक्युलेशन सक्षम करें", + "enable-delta-calculation-hint": "सक्षम होने पर, चयनित समय विंडो और निर्दिष्ट तुलना अवधि के लिए एग्रीगेटेड मानों के आधार पर डेटा कुंजी का मान गणना किया जाता है। प्रदर्शन कारणों से, डेल्टा कैलक्युलेशन केवल इतिहास समय विंडो के लिए उपलब्ध है, रियल-टाइम मानों के लिए नहीं। उदाहरण के लिए, आप कल की ऊर्जा खपत और परसों की ऊर्जा खपत के बीच डेल्टा की गणना कर सकते हैं।", + "delta-calculation-result": "डेल्टा कैलक्युलेशन परिणाम", + "delta-calculation-result-previous-value": "पिछला मान", + "delta-calculation-result-delta-absolute": "डेल्टा (पूर्ण)", + "delta-calculation-result-delta-percent": "डेल्टा (प्रतिशत)", + "source": "स्रोत", + "latest": "नवीनतम", + "latest-value": "नवीनतम मान", + "delta": "डेल्टा", + "percent": "प्रतिशत", + "absolute": "पूर्ण" + }, + "datasource": { + "type": "डेटा स्रोत प्रकार", + "name": "नाम", + "label": "लेबल", + "add-datasource-prompt": "कृपया डेटा स्रोत जोड़ें" + }, + "details": { + "details": "विवरण", + "edit-mode": "एडिट मोड", + "edit-json": "JSON संपादित करें", + "toggle-edit-mode": "एडिट मोड बदलें" + }, + "device": { + "device": "डिवाइस", + "device-required": "डिवाइस आवश्यक है।", + "devices": "डिवाइस", + "management": "डिवाइस प्रबंधन", + "view-devices": "डिवाइस देखें", + "device-alias": "डिवाइस उपनाम", + "device-type-max-length": "डिवाइस प्रकार 256 अक्षरों से कम होना चाहिए", + "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": "डिवाइस नाम अभिव्यक्ति", + "help-text": "आवश्यकतानुसार '%' का उपयोग करें: '%device_name_contains%', '%device_name_ends', 'device_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 {# डिवाइस} } अनअसाइन करें", + "unassign-device-from-edge-title": "क्या आप वाकई डिवाइस '{{deviceName}}' को Edge से अनअसाइन करना चाहते हैं?", + "unassign-device-from-edge-text": "पुष्टि के बाद डिवाइस अनअसाइन कर दिया जाएगा और Edge द्वारा एक्सेस नहीं किया जा सकेगा।", + "unassign-devices-from-edge": "Edge से डिवाइस अनअसाइन करें", + "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": "डिवाइस क्रेडेंशियल्स", + "loading-device-credentials": "डिवाइस क्रेडेंशियल्स लोड किए जा रहे हैं...", + "credentials-type": "क्रेडेंशियल्स प्रकार", + "access-token": "ऐक्सेस टोकन", + "access-token-required": "ऐक्सेस टोकन आवश्यक है।", + "access-token-invalid": "ऐक्सेस टोकन की लंबाई 1 से 32 अक्षरों के बीच होनी चाहिए।", + "certificate-pem-format": "PEM प्रारूप में प्रमाणपत्र", + "certificate-pem-format-required": "प्रमाणपत्र आवश्यक है।", + "copy-access-token": "ऐक्सेस टोकन कॉपी करें", + "copy-certificate": "प्रमाणपत्र कॉपी करें", + "copy-client-id": "क्लाइंट ID कॉपी करें", + "copy-user-name": "यूज़र नाम कॉपी करें", + "copy-password": "पासवर्ड कॉपी करें", + "generate-client-id": "क्लाइंट ID जेनरेट करें", + "generate-user-name": "यूज़र नाम जेनरेट करें", + "generate-password": "पासवर्ड जेनरेट करें", + "generate-access-token": "ऐक्सेस टोकन जेनरेट करें", + "lwm2m-security-config": { + "identity": "क्लाइंट पहचान", + "identity-required": "क्लाइंट पहचान आवश्यक है।", + "identity-tooltip": "PSK पहचानकर्ता (identifier) एक मनमाना PSK पहचानकर्ता होता है जिसकी लंबाई अधिकतम 128 बाइट तक हो सकती है, जैसा कि मानक [RFC7925] में वर्णित है।\nPSK पहचानकर्ता को पहले एक कैरेक्टर स्ट्रिंग में बदला जाना चाहिए और फिर UTF-8 का उपयोग करके ऑक्टेट्स में एनकोड किया जाना चाहिए।", + "client-key": "क्लाइंट कुंजी", + "client-key-required": "क्लाइंट कुंजी आवश्यक है।", + "client-key-tooltip-prk": "RPK सार्वजनिक कुंजी या आईडी मानक [RFC7250] के अनुरूप होनी चाहिए और Base64 फॉर्मेट में एनकोड की जानी चाहिए!", + "client-key-tooltip-psk": "PSK कुंजी मानक [RFC4279] के अनुरूप होनी चाहिए और HexDec फॉर्मेट में हो: 32, 64, 128 अक्षर!", + "endpoint": "एंडपॉइंट क्लाइंट नाम", + "endpoint-required": "एंडपॉइंट क्लाइंट नाम आवश्यक है।", + "client-public-key": "क्लाइंट सार्वजनिक कुंजी", + "client-public-key-hint": "यदि क्लाइंट सार्वजनिक कुंजी खाली है, तो विश्वसनीय प्रमाणपत्र का उपयोग किया जाएगा", + "client-public-key-tooltip": "X509 सार्वजनिक कुंजी DER-encoded X509v3 फॉर्मेट में होनी चाहिए, केवल EC algorithm को सपोर्ट करनी चाहिए और फिर Base64 फॉर्मेट में एनकोड की जानी चाहिए!", + "mode": "सुरक्षा कॉन्फ़िगरेशन मोड", + "client-tab": "क्लाइंट सुरक्षा कॉन्फ़िगरेशन", + "client-certificate": "क्लाइंट प्रमाणपत्र", + "bootstrap-tab": "Bootstrap क्लाइंट", + "bootstrap-server": "Bootstrap सर्वर", + "lwm2m-server": "LwM2M सर्वर", + "client-reboot": "रजिस्ट्रेशन अपडेट ट्रिगर", + "bootstrap-reboot": "बूटस्ट्रैप-रिक्वेस्ट ट्रिगर", + "client-publicKey-or-id": "क्लाइंट सार्वजनिक कुंजी या आईडी", + "client-publicKey-or-id-required": "क्लाइंट सार्वजनिक कुंजी या आईडी आवश्यक है।", + "client-publicKey-or-id-tooltip-psk": "PSK पहचानकर्ता (identifier) एक मनमाना PSK पहचानकर्ता होता है जिसकी लंबाई अधिकतम 128 बाइट तक हो सकती है, जैसा कि मानक [RFC7925] में वर्णित है।\nPSK पहचानकर्ता को पहले एक कैरेक्टर स्ट्रिंग में बदला जाना चाहिए और फिर UTF-8 का उपयोग करके ऑक्टेट्स में एनकोड किया जाना चाहिए।", + "client-publicKey-or-id-tooltip-rpk": "RPK सार्वजनिक कुंजी या आईडी मानक [RFC7250] के अनुरूप होनी चाहिए और Base64 फॉर्मेट में एनकोड की जानी चाहिए!", + "client-publicKey-or-id-tooltip-x509": "X509 सार्वजनिक कुंजी DER-encoded X509v3 फॉर्मेट में होनी चाहिए, केवल EC algorithm को सपोर्ट करनी चाहिए और फिर Base64 फॉर्मेट में एनकोड की जानी चाहिए।", + "client-secret-key": "क्लाइंट सीक्रेट कुंजी", + "client-secret-key-required": "क्लाइंट सीक्रेट कुंजी आवश्यक है।", + "client-secret-key-tooltip-psk": "PSK कुंजी मानक [RFC4279] के अनुरूप होनी चाहिए और HexDec फॉर्मेट में हो: 32, 64, 128 अक्षर!", + "client-secret-key-tooltip-prk": "RPK सीक्रेट कुंजी PKCS_8 फॉर्मेट (DER एनकोडिंग, मानक [RFC5958]) में होनी चाहिए और फिर Base64 फॉर्मेट में एनकोड की जानी चाहिए!", + "client-secret-key-tooltip-x509": "X509 सीक्रेट कुंजी PKCS_8 फॉर्मेट (DER एनकोडिंग, मानक [RFC5958]) में होनी चाहिए और फिर Base64 फॉर्मेट में एनकोड की जानी चाहिए!" + }, + "client-id": "क्लाइंट ID", + "client-id-pattern": "अमान्य वर्ण शामिल है।", + "user-name": "यूज़र नाम", + "user-name-required": "यूज़र नाम आवश्यक है।", + "client-id-or-user-name-necessary": "क्लाइंट ID और/या यूज़र नाम आवश्यक हैं", + "password": "पासवर्ड", + "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-profile-type-list-empty": "कम से कम एक डिवाइस प्रोफ़ाइल चयनित होनी चाहिए।", + "device-types": "डिवाइस प्रकार", + "name": "नाम", + "name-required": "नाम आवश्यक है।", + "name-max-length": "नाम 256 अक्षरों से कम होना चाहिए", + "label-max-length": "लेबल 256 अक्षरों से कम होना चाहिए", + "description": "विवरण", + "label": "लेबल", + "events": "इवेंट्स", + "details": "विवरण", + "copyId": "डिवाइस Id कॉपी करें", + "copyAccessToken": "ऐक्सेस टोकन कॉपी करें", + "copy-mqtt-authentication": "MQTT क्रेडेंशियल्स कॉपी करें", + "idCopiedMessage": "डिवाइस Id क्लिपबोर्ड में कॉपी कर दी गई है", + "accessTokenCopiedMessage": "डिवाइस ऐक्सेस टोकन क्लिपबोर्ड में कॉपी कर दिया गया है", + "mqtt-authentication-copied-message": "डिवाइस MQTT ऑथेंटिकेशन क्लिपबोर्ड में कॉपी कर दिया गया है", + "assignedToCustomer": "कस्टमर को असाइन किया गया", + "unable-delete-device-alias-title": "डिवाइस उपनाम हटाने में असमर्थ", + "unable-delete-device-alias-text": "डिवाइस उपनाम '{{deviceAlias}}' हटाया नहीं जा सकता क्योंकि इसका उपयोग इन विजेट(s) द्वारा किया जा रहा है:
    {{widgetsList}}", + "is-gateway": "Gateway है", + "overwrite-activity-time": "कनेक्टेड डिवाइस के गतिविधि समय को ओवरराइट करें", + "device-filter-title": "डिवाइस फ़िल्टर", + "filter-title": "फ़िल्टर", + "device-state": "डिवाइस स्थिति", + "state": "स्थिति", + "any": "कोई भी", + "active": "सक्रिय", + "inactive": "निष्क्रिय", + "public": "सार्वजनिक", + "device-public": "डिवाइस सार्वजनिक है", + "select-device": "डिवाइस चुनें", + "import": "डिवाइस इम्पोर्ट करें", + "device-file": "डिवाइस फ़ाइल", + "search": "डिवाइस खोजें", + "selected-devices": "{ count, plural, =1 {1 डिवाइस} other {# डिवाइस} } चयनित", + "device-configuration": "डिवाइस कॉन्फ़िगरेशन", + "transport-configuration": "ट्रांसपोर्ट कॉन्फ़िगरेशन", + "wizard": { + "device-details": "डिवाइस विवरण" + }, + "unassign-devices-from-edge-title": "क्या आप वाकई { count, plural, =1 {1 डिवाइस} other {# डिवाइस} } को अनअसाइन करना चाहते हैं?", + "unassign-devices-from-edge-text": "पुष्टि के बाद सभी चयनित डिवाइस अनअसाइन कर दिए जाएँगे और Edge द्वारा एक्सेस नहीं किए जा सकेंगे।", + "time": "समय", + "connectivity": { + "check-connectivity": "कनेक्टिविटी जाँचें", + "device-created-check-connectivity": "डिवाइस बना दिया गया है। चलिए कनेक्टिविटी जाँचते हैं!", + "loading-check-connectivity-command": "कनेक्टिविटी जाँचने वाले कमांड लोड किए जा रहे हैं...", + "use-following-instructions": "शेल का उपयोग करके डिवाइस की ओर से टेलीमेट्री भेजने के लिए निम्न निर्देशों का उपयोग करें", + "execute-following-command": "निम्न कमांड चलाएँ", + "install-curl-windows": "Windows 10 b17063 से cURL डिफ़ॉल्ट रूप से उपलब्ध है", + "install-curl-macos": "Mac OS X 10.2 6C115 (Jaguar) से cURL डिफ़ॉल्ट रूप से उपलब्ध है", + "install-mqtt-windows": "mosquitto_pub को डाउनलोड, इंस्टॉल, सेटअप और रन करने के लिए दिए गए निर्देशों का उपयोग करें", + "install-coap-client": "coap-client को डाउनलोड, इंस्टॉल, सेटअप और रन करने के लिए दिए गए निर्देशों का उपयोग करें", + "install-necessary-client-tools": "आवश्यक क्लाइंट टूल्स इंस्टॉल करें", + "mqtts-x509-command": "MQTT के माध्यम से X509 ऑथराइजेशन के साथ डिवाइस कनेक्ट करने के लिए निम्न डॉक्यूमेंटेशन का उपयोग करें", + "coaps-x509-command": "CoAP over DTLS के माध्यम से X509 ऑथराइजेशन के साथ डिवाइस कनेक्ट करने के लिए निम्न डॉक्यूमेंटेशन का उपयोग करें", + "snmp-command": "डिवाइस को SNMP के माध्यम से कनेक्ट करने के लिए निम्न डॉक्यूमेंटेशन का उपयोग करें।", + "sparkplug-command": "डिवाइस को MQTT Sparkplug के माध्यम से कनेक्ट करने के लिए निम्न डॉक्यूमेंटेशन का उपयोग करें।", + "lwm2m-command": "डिवाइस को LWM2M के माध्यम से कनेक्ट करने के लिए निम्न डॉक्यूमेंटेशन का उपयोग करें।" + } + }, + "dynamic-form": { + "property": { + "properties": "प्रॉपर्टीज़", + "property": "प्रॉपर्टी", + "id": "Id", + "name": "नाम", + "type": "प्रकार", + "type-text": "टेक्स्ट", + "type-password": "पासवर्ड", + "type-textarea": "टेक्स्ट एरिया", + "type-number": "नंबर", + "type-switch": "स्विच", + "type-select": "सेलेक्ट", + "type-radios": "रेडियो बटन", + "type-datetime": "तारीख/समय", + "type-image": "इमेज", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "Markdown", + "type-color": "रंग", + "type-color-settings": "रंग सेटिंग्स", + "type-font": "फ़ॉन्ट", + "type-units": "यूनिट्स", + "type-icon": "आइकन", + "type-fieldset": "फील्डसेट", + "type-array": "ऐरे", + "type-html-section": "HTML सेक्शन", + "group-title": "ग्रुप शीर्षक", + "no-properties": "कोई प्रॉपर्टीज़ कॉन्फ़िगर नहीं की गईं", + "add-property": "प्रॉपर्टी जोड़ें", + "property-settings": "प्रॉपर्टी सेटिंग्स", + "remove-property": "प्रॉपर्टी हटाएँ", + "default-value": "डिफ़ॉल्ट मान", + "value-required": "मान आवश्यक है।", + "number-settings": "नंबर सेटिंग्स", + "min": "न्यूनतम", + "max": "अधिकतम", + "step": "स्टेप", + "selected-options-limit": "चयनित विकल्पों की सीमा", + "advanced-ui-settings": "एडवांस्ड UI सेटिंग्स", + "disable-on-property": "प्रॉपर्टी के आधार पर डिसेबल करें", + "disable-on-property-none": "कोई नहीं (फ़ील्ड हमेशा सक्षम रहती है)", + "display-condition-function": "डिस्प्ले कंडीशन फ़ंक्शन", + "sub-label": "सब-लेबल", + "vertical-divider-after": "इसके बाद ऊर्ध्वाधर डिवाइडर", + "input-field-suffix": "इनपुट फ़ील्ड प्रत्यय", + "property-row-classes": "प्रॉपर्टी पंक्ति क्लासेज़", + "property-field-classes": "प्रॉपर्टी फ़ील्ड क्लासेज़", + "not-unique-property-ids-error": "प्रॉपर्टी Id यूनिक होने चाहिए!", + "enable-multiple-select": "मल्टीपल सेलेक्ट सक्षम करें", + "allow-empty-select-option": "खाली विकल्प की अनुमति दें", + "select-options": "सेलेक्ट विकल्प", + "not-unique-select-option-value-error": "सेलेक्ट विकल्प मान यूनिक होने चाहिए!", + "value": "मान", + "label": "लेबल", + "add-option": "विकल्प जोड़ें", + "no-options": "कोई विकल्प कॉन्फ़िगर नहीं किए गए हैं", + "remove-option": "विकल्प हटाएँ", + "textarea-rows": "टेक्स्टएरिया पंक्तियाँ", + "help-id": "हेल्प Id", + "buttons-direction": "बटन दिशा", + "direction-row": "पंक्ति", + "direction-column": "कॉलम", + "radio-button-options": "रेडियो बटन विकल्प", + "datetime-type": "तारीख/समय फ़ील्ड प्रकार", + "datetime-type-date": "तारीख", + "datetime-type-time": "समय", + "datetime-type-datetime": "तारीख/समय", + "enable-clear-button": "क्लियर बटन सक्षम करें", + "html-section-settings": "HTML सेक्शन सेटिंग्स", + "html-section-classes": "HTML सेक्शन क्लासेज़", + "html-section-content": "HTML सेक्शन सामग्री", + "array-item": "ऐरे आइटम", + "item-type": "आइटम प्रकार", + "item-name": "आइटम नाम", + "no-items": "कोई आइटम नहीं", + "support-unit-conversion": "यूनिट रूपांतरण का समर्थन करें" + }, + "clear-form": "फॉर्म साफ़ करें", + "clear-form-prompt": "क्या आप वाकई सभी फॉर्म प्रॉपर्टीज़ हटाना चाहते हैं?", + "import-form": "JSON से फॉर्म आयात करें", + "export-form": "फॉर्म को JSON में निर्यात करें", + "json-file": "JSON फ़ाइल", + "json-content": "JSON सामग्री", + "invalid-form-json-file-error": "JSON से फॉर्म आयात करने में असमर्थ: फॉर्म JSON डेटा संरचना अमान्य है।" + }, + "asset-profile": { + "asset-profile": "एसेट प्रोफ़ाइल", + "asset-profiles": "एसेट प्रोफ़ाइलें", + "all-asset-profiles": "सभी", + "add": "एसेट प्रोफ़ाइल जोड़ें", + "edit": "एसेट प्रोफ़ाइल संपादित करें", + "asset-profile-details": "एसेट प्रोफ़ाइल विवरण", + "no-asset-profiles-text": "कोई एसेट प्रोफ़ाइल नहीं मिली", + "search": "एसेट प्रोफ़ाइल खोजें", + "selected-asset-profiles": "{ count, plural, =1 {1 एसेट प्रोफ़ाइल} other {# एसेट प्रोफ़ाइलें} } चयनित", + "no-asset-profiles-matching": "कोई एसेट प्रोफ़ाइल '{{entity}}' से मेल नहीं खाती।", + "asset-profile-required": "एसेट प्रोफ़ाइल आवश्यक है", + "idCopiedMessage": "एसेट प्रोफ़ाइल Id क्लिपबोर्ड में कॉपी कर दी गई है", + "set-default": "एसेट प्रोफ़ाइल को डिफ़ॉल्ट बनाएं", + "delete": "एसेट प्रोफ़ाइल हटाएँ", + "copyId": "एसेट प्रोफ़ाइल Id कॉपी करें", + "name-max-length": "नाम 256 अक्षरों से कम होना चाहिए", + "new-device-profile-name": "एसेट प्रोफ़ाइल नाम", + "new-device-profile-name-required": "एसेट प्रोफ़ाइल नाम आवश्यक है।", + "name": "नाम", + "name-required": "नाम आवश्यक है।", + "image": "एसेट प्रोफ़ाइल इमेज", + "description": "विवरण", + "default": "डिफ़ॉल्ट", + "default-rule-chain": "डिफ़ॉल्ट नियम शृंखला", + "default-edge-rule-chain": "डिफ़ॉल्ट Edge नियम शृंखला", + "default-edge-rule-chain-hint": "Edge पर इस एसेट प्रोफ़ाइल के एसेट्स से आने वाले डेटा को प्रोसेस करने के लिए नियम शृंखला के रूप में उपयोग की जाती है", + "mobile-dashboard": "मोबाइल डैशबोर्ड", + "mobile-dashboard-hint": "मोबाइल एप्लिकेशन द्वारा एसेट विवरण डैशबोर्ड के रूप में उपयोग किया जाता है", + "select-queue-hint": "ड्रॉपडाउन सूची से चुनें।", + "delete-asset-profile-title": "क्या आप वाकई एसेट प्रोफ़ाइल '{{assetProfileName}}' को हटाना चाहते हैं?", + "delete-asset-profile-text": "सावधान रहें, पुष्टि के बाद एसेट प्रोफ़ाइल और इससे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "delete-asset-profiles-title": "क्या आप वाकई { count, plural, =1 {1 एसेट प्रोफ़ाइल} other {# एसेट प्रोफ़ाइलें} } हटाना चाहते हैं?", + "delete-asset-profiles-text": "सावधान रहें, पुष्टि के बाद सभी चयनित एसेट प्रोफ़ाइलें हटा दी जाएँगी और उनसे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "set-default-asset-profile-title": "क्या आप वाकई एसेट प्रोफ़ाइल '{{assetProfileName}}' को डिफ़ॉल्ट बनाना चाहते हैं?", + "set-default-asset-profile-text": "पुष्टि के बाद यह एसेट प्रोफ़ाइल डिफ़ॉल्ट के रूप में चिन्हित हो जाएगी और नई एसेट्स के लिए उपयोग होगी, जिनके लिए कोई प्रोफ़ाइल निर्दिष्ट नहीं है।", + "no-asset-profiles-found": "कोई एसेट प्रोफ़ाइल नहीं मिली।", + "create-new-asset-profile": "नया बनाएँ!", + "create-asset-profile": "नई एसेट प्रोफ़ाइल बनाएँ", + "import": "एसेट प्रोफ़ाइल इम्पोर्ट करें", + "export": "एसेट प्रोफ़ाइल एक्सपोर्ट करें", + "export-failed-error": "एसेट प्रोफ़ाइल एक्सपोर्ट करने में असमर्थ: {{error}}", + "asset-profile-file": "एसेट प्रोफ़ाइल फ़ाइल", + "invalid-asset-profile-file-error": "एसेट प्रोफ़ाइल इम्पोर्ट करने में असमर्थ: एसेट प्रोफ़ाइल डेटा संरचना अमान्य है।" + }, + "device-profile": { + "device-profile": "डिवाइस प्रोफ़ाइल", + "device-profiles": "डिवाइस प्रोफ़ाइलें", + "all-device-profiles": "सभी", + "add": "डिवाइस प्रोफ़ाइल जोड़ें", + "edit": "डिवाइस प्रोफ़ाइल संपादित करें", + "device-profile-details": "डिवाइस प्रोफ़ाइल विवरण", + "no-device-profiles-text": "कोई डिवाइस प्रोफ़ाइल नहीं मिली", + "search": "डिवाइस प्रोफ़ाइल खोजें", + "selected-device-profiles": "{ count, plural, =1 {1 डिवाइस प्रोफ़ाइल} other {# डिवाइस प्रोफ़ाइलें} } चयनित", + "no-device-profiles-matching": "कोई डिवाइस प्रोफ़ाइल '{{entity}}' से मेल नहीं खाती।", + "device-profile-required": "डिवाइस प्रोफ़ाइल आवश्यक है", + "idCopiedMessage": "डिवाइस प्रोफ़ाइल Id क्लिपबोर्ड में कॉपी कर दी गई है", + "set-default": "डिवाइस प्रोफ़ाइल को डिफ़ॉल्ट बनाएं", + "delete": "डिवाइस प्रोफ़ाइल हटाएँ", + "copyId": "डिवाइस प्रोफ़ाइल Id कॉपी करें", + "name-max-length": "नाम 256 अक्षरों से कम होना चाहिए", + "name": "नाम", + "name-required": "नाम आवश्यक है।", + "type": "प्रोफ़ाइल प्रकार", + "type-required": "प्रोफ़ाइल प्रकार आवश्यक है।", + "type-default": "डिफ़ॉल्ट", + "image": "डिवाइस प्रोफ़ाइल इमेज", + "transport-type": "ट्रांसपोर्ट प्रकार", + "transport-type-required": "ट्रांसपोर्ट प्रकार आवश्यक है।", + "transport-type-default": "डिफ़ॉल्ट", + "transport-type-default-hint": "बेसिक MQTT, HTTP और CoAP ट्रांसपोर्ट को सपोर्ट करता है", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "एडवांस्ड MQTT ट्रांसपोर्ट सेटिंग्स सक्षम करता है", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "एडवांस्ड CoAP ट्रांसपोर्ट सेटिंग्स सक्षम करता है", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "LWM2M ट्रांसपोर्ट प्रकार", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "SNMP ट्रांसपोर्ट कॉन्फ़िगरेशन निर्दिष्ट करें", + "transport-type-http": "HTTP", + "description": "विवरण", + "default": "डिफ़ॉल्ट", + "profile-configuration": "प्रोफ़ाइल कॉन्फ़िगरेशन", + "transport-configuration": "ट्रांसपोर्ट कॉन्फ़िगरेशन", + "default-rule-chain": "डिफ़ॉल्ट रूल चेन", + "default-edge-rule-chain": "डिफ़ॉल्ट Edge रूल चेन", + "default-edge-rule-chain-hint": "Edge पर इस डिवाइस प्रोफ़ाइल के डिवाइसों से आने वाले डेटा को प्रोसेस करने के लिए रूल चेन के रूप में उपयोग की जाती है", + "mobile-dashboard": "मोबाइल डैशबोर्ड", + "mobile-dashboard-hint": "मोबाइल एप्लिकेशन द्वारा डिवाइस विवरण डैशबोर्ड के रूप में उपयोग किया जाता है", + "select-queue-hint": "ड्रॉपडाउन सूची से चुनें।", + "delete-device-profile-title": "क्या आप वाकई डिवाइस प्रोफ़ाइल '{{deviceProfileName}}' को हटाना चाहते हैं?", + "delete-device-profile-text": "सावधान रहें, पुष्टि के बाद डिवाइस प्रोफ़ाइल और इससे संबंधित सभी डेटा, जिनमें संबंधित OTA अपडेट भी शामिल हैं, पुनर्प्राप्त नहीं किए जा सकेंगे।", + "delete-device-profiles-title": "क्या आप वाकई { count, plural, =1 {1 डिवाइस प्रोफ़ाइल} other {# डिवाइस प्रोफ़ाइलें} } हटाना चाहते हैं?", + "delete-device-profiles-text": "सावधान रहें, पुष्टि के बाद सभी चयनित डिवाइस प्रोफ़ाइलें और उनसे संबंधित सभी डेटा, जिनमें संबंधित OTA अपडेट भी शामिल हैं, पुनर्प्राप्त नहीं किए जा सकेंगे।", + "set-default-device-profile-title": "क्या आप वाकई डिवाइस प्रोफ़ाइल '{{deviceProfileName}}' को डिफ़ॉल्ट बनाना चाहते हैं?", + "set-default-device-profile-text": "पुष्टि के बाद यह डिवाइस प्रोफ़ाइल डिफ़ॉल्ट के रूप में चिन्हित हो जाएगी और उन नए डिवाइसों के लिए उपयोग होगी जिनके लिए कोई प्रोफ़ाइल निर्दिष्ट नहीं है।", + "no-device-profiles-found": "कोई डिवाइस प्रोफ़ाइल नहीं मिली।", + "create-new-device-profile": "नया बनाएँ!", + "mqtt-device-topic-filters": "MQTT डिवाइस टॉपिक फ़िल्टर", + "mqtt-device-topic-filters-unique": "MQTT डिवाइस टॉपिक फ़िल्टर यूनिक होने चाहिए।", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) नोड।", + "mqtt-device-topic-filters-spark-plug-hint": "Sparkplug B पेलोड और टॉपिक फ़ॉर्मेट के साथ EoN नोड्स से कनेक्शन की अनुमति दें।", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug मेट्रिक्स जिन्हें विशेषताओं के रूप में स्टोर करना है।", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "वे SparkPlug मेट्रिक नाम जो डिवाइस विशेषताओं के रूप में स्टोर किए जाएँगे। बाकी सभी मेट्रिक्स डिवाइस टेलीमेट्री के रूप में स्टोर होंगी।", + "mqtt-device-payload-type": "MQTT डिवाइस पेलोड", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "अन्य पेलोड फ़ॉर्मेट के साथ कम्पैटिबिलिटी सक्षम करें।", + "mqtt-enable-compatibility-with-json-payload-format-hint": "जब सक्षम होता है, प्लेटफ़ॉर्म डिफ़ॉल्ट रूप से Protobuf पेलोड फ़ॉर्मेट का उपयोग करेगा। यदि पार्सिंग विफल हो जाती है, तो प्लेटफ़ॉर्म JSON पेलोड फ़ॉर्मेट का उपयोग करने का प्रयास करेगा। यह फ़र्मवेयर अपडेट के दौरान बैकवर्ड कम्पैटिबिलिटी के लिए उपयोगी है। उदाहरण के लिए, फ़र्मवेयर का शुरुआती रिलीज Json का उपयोग करता है, जबकि नया रिलीज Protobuf का उपयोग करता है। डिवाइसों के पूरे बेड़े के लिए फ़र्मवेयर अपडेट की प्रक्रिया के दौरान Protobuf और JSON दोनों को एक साथ सपोर्ट करना आवश्यक होता है। कम्पैटिबिलिटी मोड थोड़ा प्रदर्शन कम कर देता है, इसलिए अनुशंसा की जाती है कि सभी डिवाइस अपडेट हो जाने के बाद इस मोड को निष्क्रिय कर दिया जाए।", + "mqtt-use-json-format-for-default-downlink-topics": "डिफ़ॉल्ट डाउनलिंक टॉपिक के लिए Json फ़ॉर्मेट उपयोग करें", + "mqtt-use-json-format-for-default-downlink-topics-hint": "जब सक्षम होता है, प्लेटफ़ॉर्म निम्न टॉपिक्स के माध्यम से विशेषताएँ और RPC पुश करने के लिए Json पेलोड फ़ॉर्मेट का उपयोग करेगा: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id। यह सेटिंग नई (v2) टॉपिक्स के माध्यम से भेजी गई विशेषता और RPC सदस्यताओं को प्रभावित नहीं करती: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id। जहाँ $request_id पूर्णांक रिक्वेस्ट पहचानकर्ता है।", + "mqtt-send-ack-on-validation-exception": "PUBLISH मैसेज वैलिडेशन विफल होने पर PUBACK भेजें", + "mqtt-send-ack-on-validation-exception-hint": "डिफ़ॉल्ट रूप से, मैसेज वैलिडेशन विफल होने पर प्लेटफ़ॉर्म MQTT सत्र को बंद कर देगा। जब सक्षम होता है, प्लेटफ़ॉर्म सत्र बंद करने के बजाय पब्लिश पुष्टिकरण भेजेगा।", + "mqtt-protocol-version": "प्रोटोकॉल संस्करण", + "snmp-add-mapping": "SNMP मैपिंग जोड़ें", + "snmp-mapping-not-configured": "OID से टाइम सीरीज़/टेलीमेट्री के लिए कोई मैपिंग कॉन्फ़िगर नहीं की गई", + "snmp-timseries-or-attribute-name": "मैपिंग के लिए टाइम सीरीज़/विशेषता नाम", + "snmp-timseries-or-attribute-type": "मैपिंग के लिए टाइम सीरीज़/विशेषता प्रकार", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "पेलोड प्रकार आवश्यक है।", + "coap-device-type": "CoAP डिवाइस प्रकार", + "coap-device-payload-type": "CoAP डिवाइस पेलोड", + "coap-device-type-required": "CoAP डिवाइस प्रकार आवश्यक है।", + "coap-device-type-default": "डिफ़ॉल्ट", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "सिंगल [+] और मल्टी-लेवल [#] वाइल्डकार्ड समर्थित हैं।", + "telemetry-topic-filter": "टेलीमेट्री टॉपिक फ़िल्टर", + "telemetry-topic-filter-required": "टेलीमेट्री टॉपिक फ़िल्टर आवश्यक है।", + "attributes-topic-filter": "विशेषताएँ पब्लिश टॉपिक फ़िल्टर", + "attributes-subscribe-topic-filter": "विशेषताएँ सब्सक्राइब टॉपिक फ़िल्टर", + "attributes-topic-filter-required": "विशेषताएँ पब्लिश टॉपिक फ़िल्टर आवश्यक है।", + "attributes-subscribe-topic-filter-required": "विशेषताएँ सब्सक्राइब टॉपिक फ़िल्टर आवश्यक है।", + "telemetry-proto-schema": "टेलीमेट्री प्रोटो स्कीमा", + "telemetry-proto-schema-required": "टेलीमेट्री प्रोटो स्कीमा आवश्यक है।", + "attributes-proto-schema": "विशेषताएँ प्रोटो स्कीमा", + "attributes-proto-schema-required": "विशेषताएँ प्रोटो स्कीमा आवश्यक है।", + "rpc-response-proto-schema": "RPC रिस्पॉन्स प्रोटो स्कीमा", + "rpc-response-proto-schema-required": "RPC रिस्पॉन्स प्रोटो स्कीमा आवश्यक है।", + "rpc-response-topic-filter": "RPC रिस्पॉन्स टॉपिक फ़िल्टर", + "rpc-response-topic-filter-required": "RPC रिस्पॉन्स टॉपिक फ़िल्टर आवश्यक है।", + "rpc-request-proto-schema": "RPC रिक्वेस्ट प्रोटो स्कीमा", + "rpc-request-proto-schema-required": "RPC रिक्वेस्ट प्रोटो स्कीमा आवश्यक है।", + "rpc-request-proto-schema-hint": "RPC रिक्वेस्ट मैसेज में हमेशा ये फ़ील्ड होने चाहिए: string method = 1; int32 requestId = 2; और params = 3 (किसी भी डेटा टाइप के)।", + "not-valid-pattern-topic-filter": "अमान्य पैटर्न टॉपिक फ़िल्टर", + "not-valid-single-character": "सिंगल-लेवल वाइल्डकार्ड कैरेक्टर का अमान्य उपयोग", + "not-valid-multi-character": "मल्टी-लेवल वाइल्डकार्ड कैरेक्टर का अमान्य उपयोग", + "single-level-wildcards-hint": "[+] किसी भी टॉपिक फ़िल्टर लेवल के लिए उपयुक्त है। उदाहरण: v1/devices/+/telemetry या +/devices/+/attributes।", + "multi-level-wildcards-hint": "[#] स्वयं टॉपिक फ़िल्टर को रिप्लेस कर सकता है और टॉपिक का अंतिम चिन्ह होना चाहिए। उदाहरण: # या v1/devices/me/#।", + "alarm-rules": "अलार्म नियम", + "alarm-rules-with-count": "अलार्म नियम ({{count}})", + "no-alarm-rules": "कोई अलार्म नियम कॉन्फ़िगर नहीं किए गए हैं", + "add-alarm-rule": "अलार्म नियम जोड़ें", + "edit-alarm-rule": "अलार्म नियम संपादित करें", + "alarm-type": "अलार्म प्रकार", + "alarm-type-required": "अलार्म प्रकार आवश्यक है।", + "alarm-type-unique": "अलार्म प्रकार डिवाइस प्रोफ़ाइल के अलार्म नियमों के भीतर यूनिक होना चाहिए।", + "alarm-type-max-length": "अलार्म प्रकार 256 अक्षरों से कम होना चाहिए।", + "create-alarm-pattern": "{{alarmType}} अलार्म बनाएँ", + "create-alarm-rules": "अलार्म नियम बनाएँ", + "no-create-alarm-rules": "कोई क्रिएशन कंडीशन कॉन्फ़िगर नहीं की गई है", + "add-create-alarm-rule-prompt": "कृपया क्रिएशन अलार्म नियम जोड़ें", + "clear-alarm-rule": "क्लियर अलार्म नियम", + "no-clear-alarm-rule": "कोई क्लियरिंग कंडीशन कॉन्फ़िगर नहीं की गई है", + "add-create-alarm-rule": "क्रिएशन कंडीशन जोड़ें", + "add-clear-alarm-rule": "क्लियरिंग कंडीशन जोड़ें", + "select-alarm-severity": "अलार्म गंभीरता चुनें", + "alarm-severity-required": "अलार्म गंभीरता आवश्यक है।", + "condition-duration": "कंडीशन अवधि", + "condition-duration-value": "अवधि मान", + "condition-duration-time-unit": "समय इकाई", + "condition-duration-value-range": "अवधि मान 1 से 2147483647 के बीच होना चाहिए।", + "condition-duration-value-pattern": "अवधि मान पूर्णांक होना चाहिए।", + "condition-duration-value-required": "अवधि मान आवश्यक है।", + "condition-duration-time-unit-required": "समय इकाई आवश्यक है।", + "advanced-settings": "उन्नत सेटिंग्स", + "alarm-rule-additional-info": "अतिरिक्त जानकारी", + "edit-alarm-rule-additional-info": "अतिरिक्त जानकारी संपादित करें", + "alarm-rule-additional-info-placeholder": "कृपया यहाँ अपनी टिप्पणियाँ और संशोधन लिखें, ताकि वे अलार्म विवरण में 'अतिरिक्त जानकारी' के अंतर्गत दिखाए जा सकें", + "alarm-rule-additional-info-hint": "संकेत: अलार्म नियम की कंडीशन में उपयोग की गई विशेषता या टेलीमेट्री कुंजियों के मानों को स्थानापन्न करने के लिए ${keyName} का उपयोग करें।", + "alarm-rule-mobile-dashboard": "मोबाइल डैशबोर्ड", + "alarm-rule-mobile-dashboard-hint": "मोबाइल एप्लिकेशन द्वारा अलार्म विवरण डैशबोर्ड के रूप में उपयोग किया जाता है", + "alarm-rule-no-mobile-dashboard": "कोई डैशबोर्ड चयनित नहीं है", + "propagate-alarm": "अलार्म को संबंधित एंटिटीज़ तक प्रोपेगेट करें", + "alarm-rule-relation-types-list": "रिलेशन प्रकार", + "alarm-rule-relation-types-list-hint": "संबंधित एंटिटीज़ को फ़िल्टर करने के लिए रिलेशन प्रकार को परिभाषित करता है। यदि सेट नहीं किया गया, तो अलार्म सभी संबंधित एंटिटीज़ तक प्रोपेगेट होगा।", + "propagate-alarm-to-owner": "अलार्म को एंटिटी ओनर (कस्टमर या टेनेंट) तक प्रोपेगेट करें", + "propagate-alarm-to-tenant": "अलार्म को टेनेंट तक प्रोपेगेट करें", + "alarm-rule-condition": "अलार्म नियम कंडीशन", + "enter-alarm-rule-condition-prompt": "कृपया अलार्म नियम कंडीशन जोड़ें", + "edit-alarm-rule-condition": "अलार्म नियम कंडीशन संपादित करें", + "device-provisioning": "डिवाइस प्रोविजनिंग", + "provision-strategy": "प्रोविजन रणनीति", + "provision-strategy-required": "प्रोविजन रणनीति आवश्यक है।", + "provision-strategy-disabled": "निष्क्रिय", + "provision-strategy-created-new": "नए डिवाइस बनाने की अनुमति दें", + "provision-strategy-check-pre-provisioned": "प्री-प्रोविज़न किए गए डिवाइसों की जाँच करें", + "provision-device-key": "डिवाइस प्रोविजन कुंजी", + "provision-device-key-required": "डिवाइस प्रोविजन कुंजी आवश्यक है।", + "copy-provision-key": "प्रोविजन कुंजी कॉपी करें", + "provision-key-copied-message": "प्रोविजन कुंजी क्लिपबोर्ड में कॉपी कर दी गई है", + "provision-device-secret": "डिवाइस प्रोविजन सीक्रेट", + "provision-device-secret-required": "डिवाइस प्रोविजन सीक्रेट आवश्यक है।", + "copy-provision-secret": "प्रोविजन सीक्रेट कॉपी करें", + "provision-secret-copied-message": "प्रोविजन सीक्रेट क्लिपबोर्ड में कॉपी कर दिया गया है", + "provision-strategy-x509": { + "certificate-chain": "X509 सर्टिफिकेट चेन", + "certificate-chain-hint": "X.509 सर्टिफिकेट स्ट्रेटेजी दो-तरफ़ा TLS कम्युनिकेशन में क्लाइंट सर्टिफिकेट्स द्वारा डिवाइसों को प्रोविज़न करने के लिए उपयोग की जाती है।", + "allow-create-new-devices": "नए डिवाइस बनाएँ", + "allow-create-new-devices-hint": "अगर चुना जाए तो नए डिवाइस बनाए जाएँगे और क्लाइंट सर्टिफिकेट को डिवाइस क्रेडेंशियल्स के रूप में उपयोग किया जाएगा।", + "certificate-value": "PEM फॉर्मेट में सर्टिफिकेट", + "certificate-value-required": "PEM फॉर्मेट में सर्टिफिकेट आवश्यक है।", + "cn-regex-variable": "CN रेगुलर एक्सप्रेशन वेरिएबल", + "cn-regex-variable-required": "CN रेगुलर एक्सप्रेशन वेरिएबल आवश्यक है।", + "cn-regex-variable-hint": "डिवाइस के X509 सर्टिफिकेट के कॉमन नेम से डिवाइस नाम निकालने के लिए आवश्यक।" + }, + "condition": "कंडीशन", + "condition-type": "कंडीशन प्रकार", + "condition-type-simple": "सिंपल", + "condition-type-duration": "अवधि", + "condition-during": "{{during}} के दौरान", + "condition-during-dynamic": "\"{{ attribute }}\" ({{during}}) के दौरान", + "condition-type-repeating": "दोहराव", + "condition-type-required": "कंडीशन प्रकार आवश्यक है।", + "condition-repeating-value": "इवेंट्स की संख्या", + "condition-repeating-value-range": "इवेंट्स की संख्या 1 से 2147483647 के बीच होनी चाहिए।", + "condition-repeating-value-pattern": "इवेंट्स की संख्या पूर्णांक होनी चाहिए।", + "condition-repeating-value-required": "इवेंट्स की संख्या आवश्यक है।", + "condition-repeat-times": "{ count, plural, =1 {1 बार} other {# बार} } दोहराता है", + "condition-repeat-times-dynamic": "\"{ attribute }\" ({ count, plural, =1 {1 बार} other {# बार} }) दोहराता है", + "schedule-type": "शेड्यूलर प्रकार", + "schedule-type-required": "शेड्यूलर प्रकार आवश्यक है।", + "schedule": "शेड्यूल", + "edit-schedule": "अलार्म शेड्यूल संपादित करें", + "schedule-any-time": "हर समय सक्रिय", + "schedule-specific-time": "किसी विशिष्ट समय पर सक्रिय", + "schedule-custom": "कस्टम", + "schedule-day": { + "monday": "सोमवार", + "tuesday": "मंगलवार", + "wednesday": "बुधवार", + "thursday": "गुरुवार", + "friday": "शुक्रवार", + "saturday": "शनिवार", + "sunday": "रविवार" + }, + "schedule-days": "दिन", + "schedule-time": "समय", + "schedule-time-from": "से", + "schedule-time-to": "तक", + "schedule-days-of-week-required": "सप्ताह के कम-से-कम एक दिन का चयन करना आवश्यक है।", + "create-device-profile": "नया डिवाइस प्रोफ़ाइल बनाएं", + "import": "डिवाइस प्रोफ़ाइल इम्पोर्ट करें", + "export": "डिवाइस प्रोफ़ाइल एक्सपोर्ट करें", + "export-failed-error": "डिवाइस प्रोफ़ाइल एक्सपोर्ट नहीं की जा सकी: {{error}}", + "device-profile-file": "डिवाइस प्रोफ़ाइल फ़ाइल", + "invalid-device-profile-file-error": "डिवाइस प्रोफ़ाइल इम्पोर्ट नहीं की जा सकी: डिवाइस प्रोफ़ाइल डेटा संरचना अमान्य है।", + "power-saving-mode": "पावर सेविंग मोड", + "power-saving-mode-type": { + "default": "डिवाइस प्रोफ़ाइल पावर सेविंग मोड का उपयोग करें", + "psm": "पावर सेविंग मोड (PSM)", + "drx": "डिसकंटीन्यूस रिसेप्शन (DRX)", + "edrx": "एक्सटेंडेड डिसकंटीन्यूस रिसेप्शन (eDRX)" + }, + "edrx-cycle": "eDRX साइकल", + "edrx-cycle-required": "eDRX साइकल आवश्यक है।", + "edrx-cycle-pattern": "eDRX साइकल एक धनात्मक पूर्णांक होना चाहिए।", + "edrx-cycle-min": "eDRX साइकल की न्यूनतम संख्या {{ min }} सेकंड है।", + "paging-transmission-window": "पेजिंग ट्रांसमिशन विंडो", + "paging-transmission-window-required": "पेजिंग ट्रांसमिशन विंडो आवश्यक है।", + "paging-transmission-window-pattern": "पेजिंग ट्रांसमिशन विंडो एक धनात्मक पूर्णांक होना चाहिए।", + "paging-transmission-window-min": "पेजिंग ट्रांसमिशन विंडो की न्यूनतम संख्या {{ min }} सेकंड है।", + "psm-activity-timer": "PSM एक्टिविटी टाइमर", + "psm-activity-timer-required": "PSM एक्टिविटी टाइमर आवश्यक है।", + "psm-activity-timer-pattern": "PSM एक्टिविटी टाइमर एक धनात्मक पूर्णांक होना चाहिए।", + "psm-activity-timer-min": "PSM एक्टिविटी टाइमर का न्यूनतम मान {{ min }} सेकंड है।", + "lwm2m": { + "object-list": "ऑब्जेक्ट सूची", + "object-list-empty": "कोई ऑब्जेक्ट चयनित नहीं है।", + "no-objects-found": "कोई ऑब्जेक्ट नहीं मिला।", + "no-objects-matching": "कोई ऑब्जेक्ट '{{object}}' से मेल नहीं खा रहा है।", + "model-tab": "LWM2M मॉडल", + "add-new-instances": "नए इंस्टेंस जोड़ें", + "instances-list": "इंस्टेंस सूची", + "instances-list-required": "इंस्टेंस सूची आवश्यक है।", + "instance-id-pattern": "इंस्टेंस ID एक धनात्मक पूर्णांक होना चाहिए।", + "instance-id-max": "इंस्टेंस ID का अधिकतम मान {{max}} है।", + "instance": "इंस्टेंस", + "resource-label": "#ID रिसोर्स नाम", + "observe-label": "निगरानी", + "attribute-label": "विशेषता", + "telemetry-label": "टेलीमेट्री", + "edit-observe-select": "Observe को संपादित करने के लिए टेलीमेट्री या विशेषता चुनें।", + "edit-attributes-select": "विशेषताएँ संपादित करने के लिए टेलीमेट्री या विशेषता चुनें।", + "no-attributes-set": "कोई विशेषताएँ सेट नहीं हैं।", + "key-name": "कुंजी नाम", + "key-name-required": "कुंजी नाम आवश्यक है।", + "attribute-name": "नाम विशेषता", + "attribute-name-required": "नाम विशेषता आवश्यक है।", + "attribute-value": "विशेषता मान", + "attribute-value-required": "विशेषता मान आवश्यक है।", + "attribute-value-pattern": "विशेषता मान एक धनात्मक पूर्णांक होना चाहिए।", + "edit-attributes": "विशेषताएँ संपादित करें: {{ name }}", + "view-attributes": "विशेषताएँ देखें: {{ name }}", + "add-attribute": "विशेषता जोड़ें", + "edit-attribute": "विशेषता संपादित करें", + "view-attribute": "विशेषता देखें", + "remove-attribute": "विशेषता हटाएँ", + "delete-server-text": "सावधान रहें, पुष्टि के बाद सर्वर कॉन्फ़िगरेशन को वापस नहीं लाया जा सकेगा।", + "delete-server-title": "क्या आप वाकई सर्वर को हटाना चाहते हैं?", + "mode": "सुरक्षा कॉन्फ़िग मोड", + "bootstrap-tab": "बूटस्ट्रैप", + "bootstrap-server-legend": "बूटस्ट्रैप सर्वर (ShortId...)", + "lwm2m-server-legend": "LwM2M सर्वर (ShortId...)", + "server": "सर्वर", + "short-id": "शॉर्ट सर्वर Id", + "short-id-tooltip": "सर्वर का शॉर्ट Id। सर्वर ऑब्जेक्ट इंस्टेंस को जोड़ने के लिए लिंक की तरह उपयोग किया जाता है।\nयह आइडेंटिफ़ायर LwM2M क्लाइंट के लिए कॉन्फ़िगर किए गए प्रत्येक LwM2M सर्वर को यूनिक रूप से पहचानता है।\nजब Bootstrap-Server रिसोर्स का मान 'false' हो, तब इस रिसोर्स को अवश्य सेट किया जाना चाहिए।\nLwM2M सर्वर की पहचान के लिए ID:0 और ID:65535 मानों का उपयोग नहीं किया जाना चाहिए।", + "short-id-tooltip-bootstrap": "सर्वर का शॉर्ट Id। सर्वर ऑब्जेक्ट इंस्टेंस को जोड़ने के लिए लिंक की तरह उपयोग किया जाता है।\nयह आइडेंटिफ़ायर LwM2M क्लाइंट के लिए कॉन्फ़िगर किए गए प्रत्येक LwM2M सर्वर को यूनिक रूप से पहचानता है।\nजब Bootstrap-Server रिसोर्स का मान 'false' हो, तब इस रिसोर्स को अवश्य सेट किया जाना चाहिए।", + "short-id-required": "शॉर्ट सर्वर Id आवश्यक है।", + "short-id-range": "शॉर्ट सर्वर Id {{ min }} से {{ max }} की रेंज में होना चाहिए।", + "short-id-pattern": "शॉर्ट सर्वर Id एक धनात्मक पूर्णांक होना चाहिए।", + "short-id-pattern-bs": "संक्षिप्त सर्वर ID केवल null होनी चाहिए", + "lifetime": "क्लाइंट रजिस्ट्रेशन लाइफ़टाइम", + "lifetime-required": "क्लाइंट रजिस्ट्रेशन लाइफ़टाइम आवश्यक है।", + "lifetime-pattern": "क्लाइंट रजिस्ट्रेशन लाइफ़टाइम एक धनात्मक पूर्णांक होना चाहिए।", + "default-min-period": "दो नोटिफ़िकेशन के बीच न्यूनतम अवधि (सेकंड)", + "default-min-period-tooltip": "जब ऑब्ज़र्वेशन में यह पैरामीटर शामिल नहीं होता, तो LwM2M क्लाइंट को ऑब्ज़र्वेशन की न्यूनतम अवधि के लिए जो डिफ़ॉल्ट मान उपयोग करना चाहिए।", + "default-min-period-required": "न्यूनतम अवधि आवश्यक है।", + "default-min-period-pattern": "न्यूनतम अवधि एक धनात्मक पूर्णांक होनी चाहिए।", + "notification-storing": "डिसेबल या ऑफ़लाइन होने पर नोटिफ़िकेशन स्टोर करना", + "binding": "बाइंडिंग", + "binding-type": { + "u": "U: क्लाइंट किसी भी समय UDP बाइंडिंग के माध्यम से पहुँचा जा सकता है।", + "m": "M: क्लाइंट किसी भी समय MQTT बाइंडिंग के माध्यम से पहुँचा जा सकता है।", + "h": "H: क्लाइंट किसी भी समय HTTP बाइंडिंग के माध्यम से पहुँचा जा सकता है।", + "t": "T: क्लाइंट किसी भी समय TCP बाइंडिंग के माध्यम से पहुँचा जा सकता है।", + "s": "S: क्लाइंट किसी भी समय SMS बाइंडिंग के माध्यम से पहुँचा जा सकता है।", + "n": "N: क्लाइंट को ऐसे अनुरोध के उत्तर Non-IP बाइंडिंग के माध्यम से भेजने होंगे (LWM2M 1.1 से समर्थित)।", + "uq": "UQ: UDP कनेक्शन क्यू मोड में (LWM2M 1.1 से समर्थित नहीं है)", + "uqs": "UQS: UDP और SMS दोनों कनेक्शन सक्रिय; UDP क्यू मोड में, SMS स्टैंडर्ड मोड में (LWM2M 1.1 से समर्थित नहीं है)", + "tq": "TQ: TCP कनेक्शन क्यू मोड में (LWM2M 1.1 से समर्थित नहीं है)", + "tqs": "TQS: TCP और SMS दोनों कनेक्शन सक्रिय; TCP क्यू मोड में, SMS स्टैंडर्ड मोड में (LWM2M 1.1 से समर्थित नहीं है)", + "sq": "SQ: SMS कनेक्शन क्यू मोड में (LWM2M 1.1 से समर्थित नहीं है)" + }, + "binding-tooltip": "यह LwM2M सर्वर ऑब्जेक्ट - /1/x/7 के \"बाइंडिंग\" रिसोर्स में मौजूद सूची है.\nयह LwM2M क्लाइंट में समर्थित बाइंडिंग मोड्स को दर्शाता है.\nयह मान डिवाइस ऑब्जेक्ट (/3/0/16) में “Supported Binding and Modes” रिसोर्स में दिए गए मान के समान होना चाहिए.\nहालाँकि कई ट्रांसपोर्ट समर्थित हैं, लेकिन पूरे ट्रांसपोर्ट सेशन के दौरान केवल एक ही ट्रांसपोर्ट बाइंडिंग का उपयोग किया जा सकता है.\nउदाहरण के लिए, जब UDP और SMS दोनों समर्थित हों, तो पूरे ट्रांसपोर्ट सेशन के दौरान LwM2M क्लाइंट और LwM2M सर्वर UDP या SMS में से किसी एक के माध्यम से संचार करने का चयन कर सकते हैं.", + "bootstrap-server": "बूटस्ट्रैप सर्वर", + "lwm2m-server": "LwM2M सर्वर", + "include-bootstrap-server": "बूटस्ट्रैप सर्वर अपडेट शामिल करें", + "bootstrap-update-title": "आपने पहले से ही बूटस्ट्रैप सर्वर कॉन्फ़िगर कर लिया है। क्या आप वाकई अपडेट को बाहर करना चाहते हैं?", + "bootstrap-update-text": "सावधान रहें, पुष्टि के बाद बूटस्ट्रैप सर्वर कॉन्फ़िगरेशन डेटा पुनर्प्राप्त नहीं किया जा सकेगा.", + "server-host": "होस्ट", + "server-host-required": "होस्ट आवश्यक है।", + "server-port": "पोर्ट", + "server-port-required": "पोर्ट आवश्यक है।", + "server-port-pattern": "पोर्ट एक धनात्मक पूर्णांक होना चाहिए।", + "server-port-range": "पोर्ट 1 से 65535 की सीमा में होना चाहिए।", + "server-public-key": "सर्वर सार्वजनिक कुंजी", + "server-public-key-required": "सर्वर सार्वजनिक कुंजी आवश्यक है।", + "client-hold-off-time": "होल्ड-ऑफ़ समय", + "client-hold-off-time-required": "होल्ड-ऑफ़ समय आवश्यक है।", + "client-hold-off-time-pattern": "होल्ड-ऑफ़ समय एक धनात्मक पूर्णांक होना चाहिए।", + "client-hold-off-time-tooltip": "केवल बूटस्ट्रैप सर्वर के साथ उपयोग के लिए क्लाइंट होल्ड-ऑफ़ समय।", + "account-after-timeout": "टाइमआउट के बाद खाता", + "account-after-timeout-required": "टाइमआउट के बाद खाता का मान आवश्यक है।", + "account-after-timeout-pattern": "टाइमआउट के बाद खाता का मान एक धनात्मक पूर्णांक होना चाहिए।", + "account-after-timeout-tooltip": "यह संसाधन बूटस्ट्रैप सर्वर के \"टाइमआउट के बाद खाता\" मान को निर्धारित करता है।", + "server-type": "सर्वर प्रकार", + "add-new-server-title": "नया सर्वर कॉन्फ़िग जोड़ें", + "add-server-config": "सर्वर कॉन्फ़िग जोड़ें", + "add-lwm2m-server-config": "LwM2M सर्वर जोड़ें", + "no-config-servers": "कोई सर्वर कॉन्फ़िगर नहीं है", + "others-tab": "अन्य सेटिंग्स", + "ota-update": "OTA अपडेट", + "use-object-19-for-ota-update": "OTA फ़ाइल मेटाडेटा (चेकसम, आकार, संस्करण, नाम) के लिए Object 19 का उपयोग करें", + "use-object-19-for-ota-update-hint": "OTA अपडेट के लिए Resource ObjectId = 19 का उपयोग करें: फ़र्मवेयर → InstanceId = 65534, सॉफ़्टवेयर → InstanceId = 65535। डेटा प्रारूप Base64 में लिपटा हुआ JSON है। यह JSON OTA फ़ाइल मेटाडेटा (फ़ाइल जानकारी) रखता है: \"Checksum\" (SHA256)। अतिरिक्त फ़ील्ड: \"Title\" (OTA नाम), \"Version\" (OTA संस्करण), \"File Name\" (क्लाइंट पर OTA संग्रहीत करने के लिए फ़ाइल नाम), \"File Size\" (बाइट्स में OTA आकार)।", + "client-strategy": "कनेक्शन के समय क्लाइंट की रणनीति", + "client-strategy-label": "रणनीति", + "client-strategy-only-observe": "प्रारंभिक कनेक्शन के बाद केवल Observe अनुरोध क्लाइंट को भेजें", + "client-strategy-read-all": "पंजीकरण के बाद सभी रिसोर्स पढ़ें और Observe अनुरोध क्लाइंट को भेजें", + "fw-update": "फ़र्मवेयर अपडेट", + "fw-update-strategy": "फ़र्मवेयर अपडेट रणनीति", + "fw-update-strategy-data": "Object 19 और Resource 0 (Data) का उपयोग करके फ़र्मवेयर अपडेट को बाइनरी फ़ाइल के रूप में पुश करें", + "fw-update-strategy-package": "Object 5 और Resource 0 (Package) का उपयोग करके फ़र्मवेयर अपडेट को बाइनरी फ़ाइल के रूप में पुश करें", + "fw-update-strategy-package-uri": "पैकेज डाउनलोड करने के लिए यूनिक CoAP URL स्वतः जेनरेट करें और फ़र्मवेयर अपडेट को Object 5 और Resource 1 (Package URI) के रूप में पुश करें", + "sw-update": "सॉफ़्टवेयर अपडेट", + "sw-update-strategy": "सॉफ़्टवेयर अपडेट रणनीति", + "sw-update-strategy-package": "Object 9 और Resource 2 (Package) का उपयोग करके बाइनरी फ़ाइल पुश करें", + "sw-update-strategy-package-uri": "पैकेज डाउनलोड करने के लिए यूनिक CoAP URL स्वतः जेनरेट करें और Object 9 और Resource 3 (Package URI) का उपयोग करके सॉफ़्टवेयर अपडेट पुश करें", + "fw-update-resource": "फ़र्मवेयर अपडेट CoAP रिसोर्स", + "fw-update-resource-required": "फ़र्मवेयर अपडेट CoAP रिसोर्स आवश्यक है।", + "sw-update-resource": "सॉफ़्टवेयर अपडेट CoAP रिसोर्स", + "sw-update-resource-required": "सॉफ़्टवेयर अपडेट CoAP रिसोर्स आवश्यक है।", + "config-json-tab": "डिवाइस प्रोफ़ाइल JSON कॉन्फ़िग", + "attributes-name": { + "min-period": "न्यूनतम अवधि", + "max-period": "अधिकतम अवधि", + "greater-than": "से बड़ा", + "less-than": "से छोटा", + "step": "स्टेप", + "min-evaluation-period": "न्यूनतम मूल्यांकन अवधि", + "max-evaluation-period": "अधिकतम मूल्यांकन अवधि" + }, + "default-object-id": "डिफ़ॉल्ट ऑब्जेक्ट वर्ज़न (विशेषता)", + "default-object-id-ver": { + "v1-0": "1.0", + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Observe रणनीति", + "single": "सिंगल", + "single-description": "प्रत्येक रिसोर्स के लिए एक Observe अनुरोध (ज़्यादा सटीकता, अधिक नेटवर्क ट्रैफिक)", + "composite-all": "कंपोज़िट (सभी)", + "composite-all-description": "सभी रिसोर्स एक ही कंपोज़िट Observe अनुरोध से ऑब्ज़र्व किए जाते हैं (ज़्यादा प्रभावी, कम लचीलापन)", + "composite-by-object": "ऑब्जेक्ट के अनुसार कंपोज़िट", + "composite-by-object-description": "रिसोर्स को ऑब्जेक्ट प्रकार के अनुसार समूहित करके अलग-अलग कंपोज़िट Observe अनुरोधों से ऑब्ज़र्व किया जाता है (संतुलित तरीका)" + }, + "init-attr-tel-as-obs-strategy": "गुण और टेलीमेट्री को Observe रणनीति का उपयोग करके प्रारंभ करें", + "init-attr-tel-as-obs-strategy-hint": "यदि गलत - गुण और टेलीमेट्री को उनके मानों को एक-एक करके पढ़कर प्रारंभ किया जाता है।\\nयदि सही - गुण और टेलीमेट्री को उनके मानों को Observe रणनीति का उपयोग करके सब्सक्राइब करके प्रारंभ किया जाता है।" + }, + "snmp": { + "add-communication-config": "कम्युनिकेशन कॉन्फ़िगरेशन जोड़ें", + "add-mapping": "मैपिंग जोड़ें", + "authentication-passphrase": "ऑथेंटिकेशन पासफ़्रेज़", + "authentication-passphrase-required": "ऑथेंटिकेशन पासफ़्रेज़ आवश्यक है।", + "authentication-protocol": "ऑथेंटिकेशन प्रोटोकॉल", + "authentication-protocol-required": "ऑथेंटिकेशन प्रोटोकॉल आवश्यक है।", + "communication-configs": "कम्युनिकेशन कॉन्फ़िगरेशन", + "community": "कम्युनिटी स्ट्रिंग", + "community-required": "कम्युनिटी स्ट्रिंग आवश्यक है।", + "context-name": "कॉन्टेक्स्ट नाम", + "data-key": "डेटा की", + "data-key-required": "डेटा की आवश्यक है।", + "data-type": "डेटा प्रकार", + "data-type-required": "डेटा प्रकार आवश्यक है।", + "engine-id": "इंजन ID", + "host": "होस्ट", + "host-required": "होस्ट आवश्यक है।", + "oid": "OID", + "oid-pattern": "अमान्य OID फ़ॉर्मेट।", + "oid-required": "OID आवश्यक है।", + "please-add-communication-config": "कृपया कम्युनिकेशन कॉन्फ़िगरेशन जोड़ें", + "please-add-mapping-config": "कृपया मैपिंग कॉन्फ़िगरेशन जोड़ें", + "port": "पोर्ट", + "port-format": "अमान्य पोर्ट फ़ॉर्मेट।", + "port-required": "पोर्ट आवश्यक है।", + "privacy-passphrase": "प्राइवेसी पासफ़्रेज़", + "privacy-passphrase-required": "प्राइवेसी पासफ़्रेज़ आवश्यक है।", + "privacy-protocol": "प्राइवेसी प्रोटोकॉल", + "privacy-protocol-required": "प्राइवेसी प्रोटोकॉल आवश्यक है।", + "protocol-version": "प्रोटोकॉल वर्ज़न", + "protocol-version-required": "प्रोटोकॉल वर्ज़न आवश्यक है।", + "querying-frequency": "क्वेरी फ़्रीक्वेंसी, ms", + "querying-frequency-invalid-format": "क्वेरी फ़्रीक्वेंसी एक धनात्मक पूर्णांक होना चाहिए।", + "querying-frequency-required": "क्वेरी फ़्रीक्वेंसी आवश्यक है।", + "retries": "रीट्राई", + "retries-invalid-format": "रीट्राई एक धनात्मक पूर्णांक होना चाहिए।", + "retries-required": "रीट्राई आवश्यक है।", + "scope": "स्कोप", + "scope-required": "स्कोप आवश्यक है।", + "security-name": "सिक्योरिटी नाम", + "security-name-required": "सिक्योरिटी नाम आवश्यक है।", + "timeout-ms": "टाइमआउट, ms", + "timeout-ms-invalid-format": "टाइमआउट एक धनात्मक पूर्णांक होना चाहिए।", + "timeout-ms-required": "टाइमआउट आवश्यक है।", + "user-name": "यूज़र नेम", + "user-name-required": "यूज़र नेम आवश्यक है।" + } + }, + "dialog": { + "close": "डायलॉग बंद करें", + "error-message-title": "त्रुटि संदेश:", + "error-details-title": "त्रुटि विवरण" + }, + "direction": { + "column": "स्तंभ", + "row": "पंक्ति" + }, + "edge": { + "edge": "Edge", + "edge-instances": "Edge इंस्टैंसेज़", + "instances": "इंस्टैंसेज़", + "edge-file": "Edge फ़ाइल", + "name-max-length": "नाम 256 से कम होना चाहिए", + "label-max-length": "लेबल 256 से कम होना चाहिए", + "type-max-length": "प्रकार 256 से कम होना चाहिए", + "management": "Edge प्रबंधन", + "no-edges-matching": "'{{entity}}' से मेल खाते कोई Edge नहीं मिले।", + "add": "Edge जोड़ें", + "no-edges-text": "कोई Edge नहीं मिले", + "edge-details": "Edge विवरण", + "add-edge-text": "नया Edge जोड़ें", + "delete": "Edge हटाएं", + "delete-edge-title": "क्या आप वाकई Edge '{{edgeName}}' हटाना चाहते हैं?", + "delete-edge-text": "सावधान, पुष्टि के बाद Edge और उससे संबंधित सभी डेटा पुनर्प्राप्त नहीं किए जा सकेंगे।", + "delete-edges-title": "क्या आप वाकई { count, plural, =1 {1 Edge} other {# Edge} } हटाना चाहते हैं?", + "delete-edges-text": "सावधान, पुष्टि के बाद सभी चयनित Edge हटा दिए जाएंगे और उनसे संबंधित सभी डेटा पुनर्प्राप्त नहीं किए जा सकेंगे।", + "name": "नाम", + "name-starts-with": "Edge नाम इससे शुरू होता है", + "name-required": "नाम आवश्यक है।", + "description": "विवरण", + "details": "विवरण", + "events": "इवेंट्स", + "copy-id": "Edge ID कॉपी करें", + "id-copied-message": "एज Id क्लिपबोर्ड में कॉपी कर दी गई है", + "sync": "Edge सिंक करें", + "edge-required": "Edge आवश्यक है", + "edge-type": "Edge प्रकार", + "edge-type-required": "Edge प्रकार आवश्यक है।", + "event-action": "इवेंट एक्शन", + "entity-id": "एंटिटी ID", + "select-edge-type": "Edge प्रकार चुनें", + "assign-to-customer": "कस्टमर को असाइन करें", + "assign-to-customer-text": "कृपया Edge असाइन करने के लिए कस्टमर चुनें", + "assign-edge-to-customer": "कस्टमर को Edge असाइन करें", + "assign-edge-to-customer-text": "कृपया कस्टमर को असाइन करने के लिए Edge चुनें", + "assignedToCustomer": "कस्टमर को असाइन किया गया", + "edge-public": "Edge सार्वजनिक है", + "assigned-to-customer": "असाइन किया गया: {{customerTitle}}", + "unassign-from-customer": "कस्टमर से अनअसाइन करें", + "unassign-edge-title": "क्या आप वाकई Edge '{{edgeName}}' को अनअसाइन करना चाहते हैं?", + "unassign-edge-text": "पुष्टि के बाद Edge अनअसाइन कर दिया जाएगा और कस्टमर के लिए उपलब्ध नहीं रहेगा।", + "unassign-edges-title": "क्या आप वाकई { count, plural, =1 {1 Edge} other {# Edge} } को अनअसाइन करना चाहते हैं?", + "unassign-edges-text": "पुष्टि के बाद सभी चयनित Edge अनअसाइन कर दिए जाएंगे और कस्टमर के लिए उपलब्ध नहीं रहेंगे।", + "make-public": "Edge को सार्वजनिक करें", + "make-public-edge-title": "क्या आप वाकई Edge '{{edgeName}}' को सार्वजनिक करना चाहते हैं?", + "make-public-edge-text": "पुष्टि के बाद Edge और उसका सारा डेटा सार्वजनिक कर दिया जाएगा और दूसरों के लिए उपलब्ध होगा।", + "make-private": "Edge को निजी करें", + "public": "सार्वजनिक", + "make-private-edge-title": "क्या आप वाकई Edge '{{edgeName}}' को निजी करना चाहते हैं?", + "make-private-edge-text": "पुष्टि के बाद Edge और उसका सारा डेटा निजी कर दिया जाएगा और दूसरों के लिए उपलब्ध नहीं होगा।", + "import": "Edge आयात करें", + "install-connect-instructions": "इंस्टॉल और कनेक्ट निर्देश", + "install-connect-instructions-edge-created": "Edge बनाया गया! इंस्टॉल और कनेक्ट निर्देश देखें", + "loading-edge-instructions": "Edge निर्देश लोड हो रहे हैं...", + "label": "लेबल", + "load-entity-error": "डेटा लोड नहीं हो सका। एंटिटी हटाई जा चुकी है।", + "assign-new-edge": "नया Edge असाइन करें", + "unassign-from-edge": "Edge से अनअसाइन करें", + "edge-key": "Edge कुंजी", + "copy-edge-key": "Edge कुंजी कॉपी करें", + "edge-key-copied-message": "Edge कुंजी क्लिपबोर्ड पर कॉपी कर दी गई है", + "edge-secret": "Edge सीक्रेट", + "copy-edge-secret": "Edge सीक्रेट कॉपी करें", + "edge-secret-copied-message": "Edge सीक्रेट क्लिपबोर्ड पर कॉपी कर दिया गया है", + "manage-assets": "एसेट प्रबंधित करें", + "manage-devices": "डिवाइस प्रबंधित करें", + "manage-entity-views": "एंटिटी व्यू प्रबंधित करें", + "manage-dashboards": "डैशबोर्ड प्रबंधित करें", + "manage-rulechains": "रूल चेन प्रबंधित करें", + "assets": "Edge एसेट", + "devices": "Edge डिवाइस", + "entity-views": "Edge एंटिटी व्यूज़", + "dashboard": "Edge डैशबोर्ड", + "dashboards": "Edge डैशबोर्ड्स", + "rulechain-templates": "रूल चेन टेम्पलेट्स", + "edge-rulechain-templates": "Edge रूल चेन टेम्पलेट्स", + "rulechains": "Edge रूल चेन्स", + "search": "Edge खोजें", + "selected-edges": "{ count, plural, =1 {1 edge} other {# edges} } चयनित", + "any-edge": "कोई भी Edge", + "no-edge-types-matching": "'{{entitySubtype}}' से मेल खाते कोई Edge प्रकार नहीं मिले।", + "edge-type-list-empty": "कोई Edge प्रकार चयनित नहीं है।", + "edge-types": "Edge प्रकार", + "enter-edge-type": "Edge प्रकार दर्ज करें", + "deployed": "डिप्लॉय्ड", + "pending": "लंबित", + "downlinks": "डाउनलिंक्स", + "no-downlinks-prompt": "कोई डाउनलिंक्स नहीं मिले", + "sync-process-started-successfully": "सिंक प्रक्रिया सफलतापूर्वक शुरू हो गई!", + "missing-related-rule-chains-title": "Edge में संबंधित रूल चेन(स) गायब हैं", + "missing-related-rule-chains-text": "Edge को असाइन की गई रूल चेन(स) ऐसे रूल नोड्स का उपयोग करती हैं जो संदेश(ओं) को उन रूल चेन(स) की ओर फ़ॉरवर्ड करती हैं जो इस Edge को असाइन नहीं हैं।

    गायब रूल चेन(स) की सूची:
    {{missingRuleChains}}", + "widget-datasource-error": "यह विजेट केवल EDGE एंटिटी डेटासोर्स को सपोर्ट करता है", + "upgrade-instructions": "अपग्रेड निर्देश", + "connected": "कनेक्टेड", + "disconnected": "डिस्कनेक्टेड" + }, + "edge-event": { + "type-dashboard": "डैशबोर्ड", + "type-asset": "एसेट", + "type-device": "डिवाइस", + "type-device-profile": "डिवाइस प्रोफ़ाइल", + "type-asset-profile": "एसेट प्रोफ़ाइल", + "type-entity-view": "एंटिटी व्यू", + "type-alarm": "अलार्म", + "type-rule-chain": "रूल चेन", + "type-rule-chain-metadata": "रूल चेन मेटाडेटा", + "type-edge": "Edge", + "type-user": "उपयोगकर्ता", + "type-tenant": "टेनेंट", + "type-tenant-profile": "टेनेंट प्रोफ़ाइल", + "type-customer": "कस्टमर", + "type-relation": "रिलेशन", + "type-widgets-bundle": "विजेट बंडल", + "type-widgets-type": "विजेट टाइप", + "type-admin-settings": "एडमिन सेटिंग्स", + "type-ota-package": "OTA पैकेज", + "type-queue": "क्यू", + "action-type-added": "जोड़ा गया", + "action-type-deleted": "हटाया गया", + "action-type-updated": "अपडेट किया गया", + "action-type-post-attributes": "विशेषताएँ पोस्ट की गईं", + "action-type-attributes-updated": "विशेषताएँ अपडेट की गईं", + "action-type-attributes-deleted": "विशेषताएँ हटाई गईं", + "action-type-timeseries-updated": "टाइम सीरीज़ अपडेट की गई", + "action-type-credentials-updated": "क्रेडेंशियल्स अपडेट किए गए", + "action-type-assigned-to-customer": "कस्टमर को असाइन किया गया", + "action-type-unassigned-from-customer": "कस्टमर से अनअसाइन किया गया", + "action-type-relation-add-or-update": "रिलेशन जोड़ा या अपडेट किया गया", + "action-type-relation-deleted": "रिलेशन हटाया गया", + "action-type-rpc-call": "RPC कॉल", + "action-type-alarm-ack": "अलार्म Ack", + "action-type-alarm-clear": "अलार्म क्लियर", + "action-type-alarm-assigned": "अलार्म असाइन किया गया", + "action-type-alarm-unassigned": "अलार्म अनअसाइन किया गया", + "action-type-assigned-to-edge": "Edge को असाइन किया गया", + "action-type-unassigned-from-edge": "Edge से अनअसाइन किया गया", + "action-type-credentials-request": "क्रेडेंशियल्स रिक्वेस्ट", + "action-type-entity-merge-request": "एंटिटी मर्ज रिक्वेस्ट" + }, + "error": { + "unable-to-connect": "सर्वर से कनेक्ट नहीं हो पा रहा है! कृपया अपना इंटरनेट कनेक्शन जाँचें.", + "unhandled-error-code": "अनहैंडल्ड एरर कोड: {{errorCode}}", + "unknown-error": "अज्ञात त्रुटि" + }, + "entity": { + "entity": "एंटिटी", + "entities": "एंटिटीज़", + "entities-count": "एंटिटीज़ की संख्या", + "alarms-count": "अलार्म की संख्या", + "aliases": "एंटिटी उपनाम", + "aliases-short": "उपनाम", + "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": "एंटिटी उपनाम जोड़ें", + "edit-alias": "एंटिटी उपनाम संपादित करें", + "entity-list": "एंटिटी सूची", + "entity-type": "एंटिटी प्रकार", + "entity-types": "एंटिटी प्रकार", + "entity-type-list": "एंटिटी प्रकार सूची", + "any-entity": "कोई भी एंटिटी", + "add-entity-type": "एंटिटी प्रकार जोड़ें", + "enter-entity-type": "एंटिटी प्रकार दर्ज करें", + "no-entities-matching": "'{{entity}}' से मेल खाने वाली कोई एंटिटी नहीं मिली.", + "no-entities-text": "कोई एंटिटी नहीं मिली.", + "no-entity-types-matching": "'{{entityType}}' से मेल खाने वाले कोई एंटिटी प्रकार नहीं मिले.", + "name-starts-with": "नाम अभिव्यक्ति", + "help-text": "आवश्यकतानुसार '%' का उपयोग करें: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with%'.", + "use-entity-name-filter": "फ़िल्टर उपयोग करें", + "entity-list-empty": "कोई एंटिटी चयनित नहीं है.", + "entity-type-list-required": "कम से कम एक एंटिटी प्रकार चुना जाना चाहिए.", + "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": "नया बनाएँ!", + "create-new": "नया बनाएँ", + "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-device-profile": "डिवाइस प्रोफ़ाइल", + "type-device-profiles": "डिवाइस प्रोफ़ाइल्स", + "clear-selected-profiles": "चयनित प्रोफ़ाइल्स साफ़ करें", + "list-of-device-profiles": "{ count, plural, =1 {एक डिवाइस प्रोफ़ाइल} other {# डिवाइस प्रोफ़ाइल की सूची} }", + "device-profile-name-starts-with": "वे डिवाइस प्रोफ़ाइल्स जिनके नाम '{{prefix}}' से शुरू होते हैं", + "type-asset-profile": "एसेट प्रोफ़ाइल", + "type-asset-profiles": "एसेट प्रोफ़ाइल्स", + "list-of-asset-profiles": "{ count, plural, =1 {एक एसेट प्रोफ़ाइल} other {# एसेट प्रोफ़ाइल की सूची} }", + "asset-profile-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-tenant-profile": "टेनेंट प्रोफ़ाइल", + "type-tenant-profiles": "टेनेंट प्रोफ़ाइल्स", + "list-of-tenant-profiles": "{ count, plural, =1 {एक टेनेंट प्रोफ़ाइल} other {# टेनेंट प्रोफ़ाइल की सूची} }", + "tenant-profile-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-api-key": "API कुंजी", + "type-api-keys": "API कुंजियाँ", + "type-current-customer": "वर्तमान कस्टमर", + "type-current-tenant": "वर्तमान टेनेंट", + "type-current-user": "वर्तमान उपयोगकर्ता", + "type-current-user-owner": "वर्तमान उपयोगकर्ता मालिक", + "type-calculated-field": "कैलक्युलेटेड फ़ील्ड", + "type-calculated-fields": "कैलक्युलेटेड फ़ील्ड्स", + "type-ai-model": "AI मॉडल", + "type-ai-models": "AI मॉडल्स", + "type-widgets-bundle": "विजेट बंडल", + "type-widgets-bundles": "विजेट बंडल्स", + "list-of-widgets-bundles": "{ count, plural, =1 {एक विजेट बंडल} other {# विजेट बंडल्स की सूची} }", + "type-widget": "विजेट", + "type-widgets": "विजेट", + "list-of-widgets": "{ count, plural, =1 {एक विजेट} other {# विजेट की सूची} }", + "search": "एंटिटीज़ खोजें", + "selected-entities": "{ count, plural, =1 {1 एंटिटी} other {# एंटिटीज़} } चयनित", + "entity-name": "एंटिटी नाम", + "entity-label": "एंटिटी लेबल", + "details": "एंटिटी विवरण", + "no-entities-prompt": "कोई एंटिटी नहीं मिली", + "no-data": "दिखाने के लिए कोई डेटा नहीं", + "show-all-columns": "सभी दिखाएं", + "columns-to-display": "दिखाने वाले कॉलम", + "type-api-usage-state": "API उपयोग स्थिति", + "type-edge": "Edge", + "type-edges": "Edge", + "list-of-edges": "{ count, plural, =1 {एक Edge} other {# Edge की सूची} }", + "edge-name-starts-with": "Edge जिनके नाम '{{prefix}}' से शुरू होते हैं", + "version-conflict": { + "message": "क्या आप मौजूदा संस्करण को ओवरराइट करना चाहते हैं या बदलावों को छोड़कर नवीनतम संस्करण लोड करना चाहते हैं?", + "link": "आप इसे उपयोग करके {{entityType}} का अपना संस्करण डाउनलोड कर सकते हैं", + "overwrite": "संस्करण ओवरराइट करें", + "discard": "बदलाव छोड़ें" + }, + "type-tb-resource": "रिसोर्स", + "type-tb-resources": "रिसोर्सेज़", + "list-of-tb-resources": "{ count, plural, =1 {एक रिसोर्स} other {# रिसोर्सेज़ की सूची} }", + "type-ota-package": "OTA पैकेज", + "type-ota-packages": "OTA पैकेजेज़", + "list-of-ota-packages": "{ count, plural, =1 {एक OTA पैकेज} other {# OTA पैकेजेज़ की सूची} }", + "type-rpc": "RPC", + "type-queue": "क्यू", + "type-queue-stats": "क्यू आँकड़े", + "type-queues-stats": "क्यूज़ आँकड़े", + "type-notification": "अधिसूचना", + "type-notification-rule": "अधिसूचना नियम", + "type-notification-rules": "अधिसूचना नियम", + "list-of-notification-rules": "{ count, plural, =1 {एक अधिसूचना नियम} other {# अधिसूचना नियमों की सूची} }", + "type-notification-target": "अधिसूचना प्राप्तकर्ता", + "type-notification-targets": "अधिसूचना प्राप्तकर्ता", + "list-of-notification-targets": "{ count, plural, =1 {एक अधिसूचना प्राप्तकर्ता} other {# अधिसूचना प्राप्तकर्ताओं की सूची} }", + "type-notification-request": "अधिसूचना अनुरोध", + "type-notification-template": "अधिसूचना टेम्पलेट", + "type-notification-templates": "अधिसूचना टेम्पलेट्स", + "list-of-notification-templates": "{ count, plural, =1 {एक अधिसूचना टेम्पलेट} other {# अधिसूचना टेम्पलेट्स की सूची} }", + "link": "लिंक", + "type-oauth2-client": "OAuth 2.0 क्लाइंट", + "type-oauth2-clients": "OAuth 2.0 क्लाइंट्स", + "list-of-oauth2-clients": "{ count, plural, =1 {एक OAuth 2.0 क्लाइंट} other {# OAuth 2.0 क्लाइंट्स की सूची} }", + "type-domain": "डोमेन", + "type-domains": "डोमेन्स", + "list-of-domains": "{ count, plural, =1 {एक डोमेन} other {# डोमेन्स की सूची} }", + "type-mobile-app": "मोबाइल एप्लिकेशन", + "type-mobile-apps": "मोबाइल एप्लिकेशन्स", + "list-of-mobile-apps": "{ count, plural, =1 {एक मोबाइल एप्लिकेशन} other {# मोबाइल एप्लिकेशन्स की सूची} }", + "type-mobile-app-bundle": "मोबाइल बंडल", + "type-mobile-app-bundles": "मोबाइल बंडल्स", + "list-of-mobile-app-bundles": "{ count, plural, =1 {एक मोबाइल बंडल} other {# मोबाइल बंडल्स की सूची} }", + "limit-reached": "सीमा तक पहुँच गई", + "limit-reached-text": "आपने {{ entities }} की सीमा तक पहुँच गया है। अधिक जोड़ने के लिए, कृपया अपने सिस्टम व्यवस्थापक से अपनी {{ entity }} सीमा बढ़ाने का अनुरोध करें।", + "request-limit-increase": "सीमा बढ़ाने का अनुरोध करें", + "request-sysadmin-text": "क्या आप सिस्टम व्यवस्थापक हैं?", + "login-here": "यहाँ लॉगिन करें", + "to-increase-limit": "सीमा बढ़ाने के लिए।", + "increase-limit-request-sent-title": "हमने आपकी सिस्टम व्यवस्थापक को सीमा बढ़ाने के लिए एक स्वचालित अनुरोध भेजा है", + "increase-limit-request-sent-text": "कृपया कुछ समय दें ताकि वे अनुरोध की समीक्षा कर सकें और सेटिंग्स अपडेट कर सकें। आपको परिवर्तन देखने के लिए इस पृष्ठ को रिफ्रेश करना पड़ सकता है।" + }, + "entity-field": { + "created-time": "बनाने का समय", + "name": "नाम", + "type": "प्रकार", + "first-name": "पहला नाम", + "last-name": "अंतिम नाम", + "email": "ईमेल", + "title": "शीर्षक", + "country": "देश", + "state": "राज्य", + "city": "शहर", + "address": "पता", + "address2": "पता 2", + "zip": "ज़िप", + "phone": "फ़ोन", + "label": "लेबल", + "queue-name": "क्यू नाम", + "service-id": "सर्विस Id", + "owner-name": "ओनर नाम", + "owner-type": "ओनर प्रकार" + }, + "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}}' से मेल खाने वाले कोई एंटिटी व्यूज़ नहीं मिले.", + "public": "पब्लिक", + "alias": "उपनाम", + "alias-required": "एंटिटी व्यू उपनाम आवश्यक है.", + "remove-alias": "एंटिटी व्यू उपनाम हटाएँ", + "add-alias": "एंटिटी व्यू उपनाम जोड़ें", + "name-starts-with": "एंटिटी व्यू नाम अभिव्यक्ति", + "help-text": "आवश्यकतानुसार '%' का उपयोग करें: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_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": "एंटिटी व्यू जोड़ें", + "entity-view-public": "एंटिटी व्यू पब्लिक है", + "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": "एंटिटी व्यूज़ हटाएँ", + "make-public": "एंटिटी व्यू को पब्लिक करें", + "make-private": "एंटिटी व्यू को प्राइवेट करें", + "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": "सावधान रहें, पुष्टि के बाद सभी चयनित एंटिटी व्यूज़ हटा दिए जाएँगे और उनसे संबंधित सभी डेटा हमेशा के लिए खो जाएगा.", + "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 एंटिटी व्यू} 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": "एंटिटी व्यू प्रकार", + "created-time": "बनाने का समय", + "name": "नाम", + "name-required": "नाम आवश्यक है.", + "name-max-length": "नाम 256 वर्णों से कम होना चाहिए", + "type-max-length": "एंटिटी व्यू प्रकार 256 वर्णों से कम होना चाहिए", + "description": "वर्णन", + "events": "इवेंट्स", + "details": "विवरण", + "copyId": "एंटिटी व्यू Id कॉपी करें", + "idCopiedMessage": "एंटिटी व्यू Id क्लिपबोर्ड पर कॉपी कर दी गई है", + "assignedToCustomer": "कस्टमर को असाइन किया गया", + "unable-entity-view-device-alias-title": "एंटिटी व्यू उपनाम हटाने में असमर्थ", + "unable-entity-view-device-alias-text": "डिवाइस उपनाम '{{entityViewAlias}}' को हटाया नहीं जा सकता क्योंकि यह निम्न विजेट(स) द्वारा उपयोग किया जा रहा है:
    {{widgetsList}}", + "select-entity-view": "एंटिटी व्यू चुनें", + "start-ts": "प्रारंभ समय", + "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": "टार्गेट एंटिटी के टाइम सीरीज़ डेटा की-ज़ कॉन्फ़िगर करें जो एंटिटी व्यू के लिए उपलब्ध होंगी। यह टाइम सीरीज़ डेटा केवल पढ़ने योग्य है.", + "search": "एंटिटी व्यूज़ खोजें", + "selected-entity-views": "{ count, plural, =1 {1 एंटिटी व्यू} other {# एंटिटी व्यूज़} } चयनित", + "assign-entity-view-to-edge": "एंटिटी व्यूज़ को Edge को असाइन करें", + "assign-entity-view-to-edge-text": "Edge को असाइन करने के लिए कृपया एंटिटी व्यूज़ चुनें", + "unassign-entity-view-from-edge-title": "क्या आप वाकई एंटिटी व्यू '{{entityViewName}}' को अनअसाइन करना चाहते हैं?", + "unassign-entity-view-from-edge-text": "पुष्टि के बाद एंटिटी व्यू अनअसाइन हो जाएगा और Edge के लिए सुलभ नहीं रहेगा.", + "unassign-entity-views-from-edge-action-title": "Edge से { count, plural, =1 {1 एंटिटी व्यू} other {# एंटिटी व्यूज़} } अनअसाइन करें", + "unassign-entity-view-from-edge": "एंटिटी व्यू अनअसाइन करें", + "unassign-entity-views-from-edge-title": "क्या आप वाकई { count, plural, =1 {1 एंटिटी व्यू} other {# एंटिटी व्यूज़} } को अनअसाइन करना चाहते हैं?", + "unassign-entity-views-from-edge-text": "पुष्टि के बाद सभी चयनित एंटिटी व्यूज़ अनअसाइन हो जाएँगे और Edge के लिए सुलभ नहीं रहेंगे." + }, + "event": { + "event-type": "इवेंट प्रकार", + "events-filter": "इवेंट्स फ़िल्टर", + "clean-events": "इवेंट्स साफ़ करें", + "type-error": "त्रुटि", + "type-lc-event": "लाइफसाइकल इवेंट", + "type-stats": "आँकड़े", + "type-debug-rule-node": "डिबग", + "type-debug-rule-chain": "डिबग", + "type-debug-calculated-field": "डिबग", + "arguments": "आर्गुमेंट्स", + "result": "परिणाम", + "no-events-prompt": "कोई इवेंट नहीं मिला", + "error": "त्रुटि", + "alarm": "अलार्म", + "event-time": "इवेंट समय", + "server": "सर्वर", + "body": "बॉडी", + "method": "मेथड", + "type": "प्रकार", + "metadata": "मेटाडेटा", + "message": "संदेश", + "message-id": "संदेश Id", + "copy-message-id": "संदेश Id कॉपी करें", + "message-type": "संदेश प्रकार", + "data-type": "डेटा प्रकार", + "relation-type": "रिलेशन प्रकार", + "data": "डेटा", + "event": "इवेंट", + "status": "स्थिति", + "success": "सफल", + "failed": "विफल", + "messages-processed": "प्रोसेस किए गए संदेश", + "max-messages-processed": "अधिकतम प्रोसेस किए गए संदेश", + "min-messages-processed": "न्यूनतम प्रोसेस किए गए संदेश", + "errors-occurred": "त्रुटियाँ हुईं", + "max-errors-occurred": "अधिकतम त्रुटियाँ हुईं", + "min-errors-occurred": "न्यूनतम त्रुटियाँ हुईं", + "min-value": "न्यूनतम मान 0 है.", + "all-events": "सभी", + "has-error": "त्रुटि है", + "entity-id": "एंटिटी Id", + "copy-entity-id": "एंटिटी Id कॉपी करें", + "entity-type": "एंटिटी प्रकार", + "clear-filter": "फ़िल्टर साफ़ करें", + "clear-request-title": "सभी इवेंट्स साफ़ करें", + "clear-request-text": "क्या आप वाकई सभी इवेंट्स साफ़ करना चाहते हैं?", + "started": "शुरू हुआ", + "updated": "अपडेट हुआ", + "stopped": "रुका हुआ" + }, + "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-tcp-reconnect": "स्वतः पुनः कनेक्ट करें", + "modbus-rtu-over-tcp": "RTU over TCP", + "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": "पोल अवधि (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": "अमान्य एक्सटेंशन फ़ाइल" + }, + "feature": { + "advanced-features": "एडवांस्ड फीचर्स" + }, + "filter": { + "add": "फ़िल्टर जोड़ें", + "edit": "फ़िल्टर संपादित करें", + "name": "फ़िल्टर नाम", + "name-required": "फ़िल्टर नाम आवश्यक है.", + "duplicate-filter": "इसी नाम वाला फ़िल्टर पहले से मौजूद है.", + "filters": "फ़िल्टर्स", + "unable-delete-filter-title": "फ़िल्टर हटाने में असमर्थ", + "unable-delete-filter-text": "फ़िल्टर '{{filter}}' को हटाया नहीं जा सकता क्योंकि यह निम्न विजेट(स) द्वारा उपयोग किया जा रहा है:
    {{widgetsList}}", + "duplicate-filter-error": "डुप्लिकेट फ़िल्टर '{{filter}}' मिला।
    डैशबोर्ड के भीतर फ़िल्टर्स अद्वितीय होने चाहिए.", + "missing-key-filters-error": "फ़िल्टर '{{filter}}' के लिए की फ़िल्टर्स नहीं दिए गए हैं.", + "filter": "फ़िल्टर", + "editable": "संपादन योग्य", + "editable-hint": "डैशबोर्ड्स में उपयोगकर्ता को फ़िल्टर मान बदलने की अनुमति दें.", + "no-filters-found": "कोई फ़िल्टर नहीं मिला.", + "no-filter-text": "कोई फ़िल्टर निर्दिष्ट नहीं है", + "add-filter-prompt": "कृपया फ़िल्टर जोड़ें", + "no-filter-matching": "'{{filter}}' नहीं मिला.", + "create-new-filter": "नया बनाएँ!", + "create-new": "नया बनाएँ", + "filter-required": "फ़िल्टर आवश्यक है.", + "operation": { + "operation": "ऑपरेशन", + "equal": "बराबर", + "not-equal": "बराबर नहीं", + "starts-with": "से शुरू होता है", + "ends-with": "पर समाप्त होता है", + "contains": "शामिल है", + "not-contains": "शामिल नहीं है", + "greater": "से बड़ा", + "less": "से छोटा", + "greater-or-equal": "बड़ा या बराबर", + "less-or-equal": "छोटा या बराबर", + "and": "और", + "or": "या", + "in": "में", + "not-in": "में नहीं" + }, + "ignore-case": "केस अनदेखा करें", + "value": "मान", + "remove-filter": "फ़िल्टर हटाएँ", + "duplicate-filter-action": "फ़िल्टर डुप्लिकेट करें", + "preview": "फ़िल्टर प्रीव्यू", + "no-filters": "कोई फ़िल्टर कॉन्फ़िगर नहीं है", + "add-filter": "फ़िल्टर जोड़ें", + "add-complex-filter": "कॉम्प्लेक्स फ़िल्टर जोड़ें", + "add-complex": "कॉम्प्लेक्स जोड़ें", + "complex-filter": "कॉम्प्लेक्स फ़िल्टर", + "edit-complex-filter": "कॉम्प्लेक्स फ़िल्टर संपादित करें", + "edit-filter-user-params": "फ़िल्टर प्रेडिकेट यूज़र पैरामीटर्स संपादित करें", + "filter-user-params": "फ़िल्टर प्रेडिकेट यूज़र पैरामीटर्स", + "user-parameters": "यूज़र पैरामीटर्स", + "display-label": "दिखाने के लिए लेबल", + "custom-label": "कस्टम लेबल", + "custom-label-hint": "फ़िल्टर के लिए अपना लेबल सेट करने हेतु सक्षम करें। अक्षम होने पर लेबल स्वतः जेनरेट हो जाएगा.", + "order-priority": "डिस्प्ले क्रम", + "key-filter": "की फ़िल्टर", + "key-filters": "की फ़िल्टर्स", + "key-name": "की नाम", + "key-name-required": "की नाम आवश्यक है.", + "key-type": { + "key-type": "की प्रकार", + "attribute": "विशेषता", + "timeseries": "टाइम सीरीज़", + "entity-field": "एंटिटी फ़ील्ड", + "constant": "कॉनस्टेंट", + "client-attribute": "क्लाइंट विशेषता", + "server-attribute": "सर्वर विशेषता", + "shared-attribute": "साझा विशेषता" + }, + "value-type": { + "value-type": "मान प्रकार", + "string": "स्ट्रिंग", + "numeric": "न्यूमेरिक", + "boolean": "बूलियन", + "date-time": "दिनांक-समय" + }, + "value-type-required": "की मान प्रकार आवश्यक है.", + "key-value-type-change-title": "क्या आप वाकई की मान प्रकार बदलना चाहते हैं?", + "key-value-type-change-message": "यदि आप नए मान प्रकार की पुष्टि करते हैं, तो दर्ज किए गए सभी की फ़िल्टर्स हटाए जाएँगे.", + "no-key-filters": "कोई की फ़िल्टर्स कॉन्फ़िगर नहीं हैं", + "add-key-filter": "की फ़िल्टर जोड़ें", + "remove-key-filter": "की फ़िल्टर हटाएँ", + "edit-key-filter": "की फ़िल्टर संपादित करें", + "date": "तारीख", + "time": "समय", + "current-tenant": "वर्तमान टेनेंट", + "current-customer": "वर्तमान कस्टमर", + "current-user": "वर्तमान उपयोगकर्ता", + "current-device": "वर्तमान डिवाइस", + "default-value": "डिफ़ॉल्ट मान", + "default-comma-separated-values": "डिफ़ॉल्ट कॉमा द्वारा अलग किए गए मान", + "dynamic-source-type": "डायनेमिक स्रोत प्रकार", + "dynamic-value": "डायनेमिक मान", + "no-dynamic-value": "कोई डायनेमिक मान नहीं", + "source-attribute": "स्रोत विशेषता", + "switch-to-dynamic-value": "डायनेमिक मान पर स्विच करें", + "switch-to-default-value": "डिफ़ॉल्ट मान पर स्विच करें", + "inherit-owner": "ओनर से विरासत में लें", + "source-attribute-not-set": "यदि स्रोत विशेषता सेट नहीं है", + "unit": "इकाई" + }, + "fullscreen": { + "expand": "फुलस्क्रीन पर फैलाएँ", + "exit": "फुलस्क्रीन से बाहर निकलें", + "toggle": "फुलस्क्रीन मोड टॉगल करें", + "fullscreen": "फुलस्क्रीन" + }, + "function": { + "function": "फ़ंक्शन" + }, + "gateway": { + "gateway-name": "Gateway नाम", + "gateway-name-required": "Gateway नाम आवश्यक है.", + "gateways": "Gateways", + "create-new-gateway": "नया Gateway बनाएँ", + "create-new-gateway-text": "क्या आप वाकई '{{gatewayName}}' नाम का नया Gateway बनाना चाहते हैं?", + "launch-command": "लॉन्च कमांड", + "no-gateway-found": "कोई Gateway नहीं मिला.", + "no-gateway-matching": " '{{item}}' नहीं मिला." + }, + "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": "हेल्प पेज पर जाएँ", + "show-help": "हेल्प दिखाएँ" + }, + "home": { + "home": "होम", + "profile": "प्रोफ़ाइल", + "logout": "लॉगआउट", + "menu": "मेन्यू", + "avatar": "अवतार", + "open-user-menu": "उपयोगकर्ता मेन्यू खोलें" + }, + "file-input": { + "browse-file": "फ़ाइल ब्राउज़ करें", + "browse-files": "फ़ाइलें ब्राउज़ करें" + }, + "image": { + "gallery": "इमेज गैलरी", + "search": "इमेज खोजें", + "selected-images": "{ count, plural, =1 {1 इमेज} other {# इमेज} } चयनित", + "created-time": "बनाने का समय", + "name": "नाम", + "name-required": "नाम आवश्यक है.", + "resolution": "रेज़ोल्यूशन", + "size": "आकार", + "system": "सिस्टम", + "download-image": "इमेज डाउनलोड करें", + "export-image": "इमेज को JSON में एक्सपोर्ट करें", + "import-image": "JSON से इमेज इम्पोर्ट करें", + "upload-image": "इमेज अपलोड करें", + "edit-image": "इमेज संपादित करें", + "image-details": "इमेज विवरण", + "no-images": "कोई इमेज नहीं मिली", + "delete-image": "इमेज हटाएँ", + "delete-image-title": "क्या आप वाकई इमेज '{{imageTitle}}' को हटाना चाहते हैं?", + "delete-image-text": "सावधान रहें, पुष्टि के बाद इमेज हमेशा के लिए खो जाएगी.", + "delete-images-title": "क्या आप वाकई { count, plural, =1 {1 इमेज} other {# इमेज} } को हटाना चाहते हैं?", + "delete-images-text": "सावधान रहें, पुष्टि के बाद सभी चयनित इमेज हटा दी जाएँगी और उनसे संबंधित सभी डेटा हमेशा के लिए खो जाएगा.", + "list-mode": "सूची दृश्य", + "grid-mode": "ग्रिड दृश्य", + "image-preview": "इमेज प्रीव्यू", + "update-image": "इमेज अपडेट करें", + "export-failed-error": "इमेज एक्सपोर्ट नहीं की जा सकी: {{error}}", + "image-json-file": "इमेज JSON फ़ाइल", + "invalid-image-json-file-error": "JSON से इमेज इम्पोर्ट नहीं कर सके: इमेज JSON डेटा संरचना अमान्य है.", + "image-is-in-use": "इमेज अन्य एंटिटीज़ द्वारा उपयोग की जा रही है", + "images-are-in-use": "इमेजें अन्य एंटिटीज़ द्वारा उपयोग की जा रही हैं", + "image-is-in-use-text": "इमेज '{{title}}' को नहीं हटाया गया क्योंकि इसे निम्न एंटिटीज़ द्वारा उपयोग किया जा रहा है:", + "images-are-in-use-text": "सभी इमेजें नहीं हटाई गईं क्योंकि वे अन्य एंटिटीज़ द्वारा उपयोग की जा रही हैं.
    आप संबंधित एंटिटीज़ को संबंधित इमेज की पंक्ति में संदर्भ बटन पर क्लिक करके देख सकते हैं.
    यदि आप फिर भी इन इमेजों को हटाना चाहते हैं, तो उन्हें नीचे दी गई तालिका में चुनें और चयनित हटाएँ बटन पर क्लिक करें.", + "delete-image-in-use-text": "यदि आप फिर भी इमेज हटाना चाहते हैं, तो फिर भी हटाएँ बटन पर क्लिक करें.", + "system-entities": "सिस्टम एंटिटीज़:", + "entities": "एंटिटीज़:", + "references": "संदर्भ", + "include-system-images": "सिस्टम इमेजें शामिल करें", + "clear-image": "इमेज साफ़ करें", + "no-image": "कोई इमेज नहीं", + "no-image-selected": "कोई इमेज चयनित नहीं है", + "browse-from-gallery": "गैलरी से ब्राउज़ करें", + "set-link": "लिंक सेट करें", + "image-link": "इमेज लिंक", + "link": "लिंक", + "copy-image-link": "इमेज लिंक कॉपी करें", + "embed-image": "इमेज एम्बेड करें", + "embed-to-html": "HTML में एम्बेड करें", + "embed-to-html-hint": "यह फीचर लिंक को किसी भी अनधिकृत उपयोगकर्ता के लिए उपलब्ध कर देगा.", + "embed-to-html-text": "निम्न कोड स्निपेट का उपयोग करके आप केवल HTML पर आधारित कॉम्पोनेंट्स में इमेज एम्बेड कर सकते हैं.
    ऐसे कॉम्पोनेंट्स में HTML कार्ड विजेट्स, सेल कंटेंट फ़ंक्शंस आदि शामिल हैं.", + "embed-to-angular-template": "Angular HTML टेम्पलेट में एम्बेड करें", + "embed-to-angular-template-text": "निम्न कोड स्निपेट का उपयोग करके आप इमेज को Angular HTML टेम्पलेट में एम्बेड कर सकते हैं जो कॉम्पोनेंट्स के लिए उपयोग किया जाएगा.
    ऐसे कॉम्पोनेंट्स में Markdown विजेट, विजेट एडिटर में HTML सेक्शन, कस्टम एक्शन्स आदि शामिल हैं." + }, + "image-input": { + "drop-images-or": "इमेजेस को ड्रैग-एंड-ड्रॉप करें या", + "drag-and-drop": "ड्रैग और ड्रॉप", + "or": "या", + "browse": "ब्राउज़ करें", + "no-images": "कोई इमेज चयनित नहीं है", + "images": "इमेजें" + }, + "import": { + "no-file": "कोई फ़ाइल चयनित नहीं है", + "drop-file": "JSON फ़ाइल छोड़ें या अपलोड करने के लिए फ़ाइल चुनने हेतु क्लिक करें.", + "drop-json-file-or": "JSON फ़ाइल को ड्रैग-एंड-ड्रॉप करें या", + "drop-file-csv": "CSV फ़ाइल छोड़ें या अपलोड करने के लिए फ़ाइल चुनने हेतु क्लिक करें.", + "drop-file-csv-or": "CSV फ़ाइल को ड्रैग-एंड-ड्रॉप करें या", + "column-value": "मान", + "column-title": "शीर्षक", + "column-example": "उदाहरण मान डेटा", + "column-key": "विशेषता/टेलीमेट्री कुंजी", + "credentials": "क्रेडेंशियल्स", + "csv-delimiter": "CSV डिलिमिटर", + "csv-first-line-header": "पहली पंक्ति में कॉलम नाम होते हैं", + "csv-update-data": "विशेषताएँ/टेलीमेट्री अपडेट करें", + "details": "विवरण", + "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": "एंटिटी फ़ील्ड", + "access-token": "ऐक्सेस टोकन", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT क्लाइंट ID", + "user-name": "MQTT यूज़र नेम", + "password": "MQTT पासवर्ड" + }, + "lwm2m": { + "client-endpoint": "LwM2M एंडपॉइंट क्लाइंट नाम", + "security-config-mode": "LwM2M सिक्योरिटी कॉन्फ़िग मोड", + "client-identity": "LwM2M क्लाइंट आइडेंटिटी", + "client-key": "LwM2M क्लाइंट कुंजी", + "client-cert": "LwM2M क्लाइंट पब्लिक की", + "bootstrap-server-security-mode": "LwM2M बूटस्ट्रैप सर्वर सिक्योरिटी मोड", + "bootstrap-server-secret-key": "LwM2M बूटस्ट्रैप सर्वर सीक्रेट की", + "bootstrap-server-public-key-id": "LwM2M बूटस्ट्रैप सर्वर पब्लिक की या Id", + "lwm2m-server-security-mode": "LwM2M सर्वर सिक्योरिटी मोड", + "lwm2m-server-secret-key": "LwM2M सर्वर सीक्रेट की", + "lwm2m-server-public-key-id": "LwM2M सर्वर पब्लिक की या Id" + }, + "snmp": { + "host": "SNMP होस्ट", + "port": "SNMP पोर्ट", + "version": "SNMP वर्ज़न (v1, v2c या v3)", + "community-string": "SNMP कम्युनिटी स्ट्रिंग" + }, + "isgateway": "Gateway है", + "activity-time-from-gateway-device": "Gateway डिवाइस से एक्टिविटी समय", + "description": "वर्णन", + "routing-key": "Edge की", + "secret": "Edge सीक्रेट" + }, + "stepper-text": { + "select-file": "फ़ाइल चुनें", + "configuration": "इम्पोर्ट कॉन्फ़िगरेशन", + "column-type": "कॉलम प्रकार चुनें", + "creat-entities": "नई एंटिटीज़ बनाई जा रही हैं" + }, + "message": { + "create-entities": "{{count}} नई एंटिटीज़ सफलतापूर्वक बनाई गईं.", + "update-entities": "{{count}} एंटिटीज़ सफलतापूर्वक अपडेट की गईं.", + "error-entities": "{{count}} एंटिटीज़ बनाते समय त्रुटि हुई." + } + }, + "scada": { + "symbols": "SCADA सिंबल्स", + "search": "सिंबल खोजें", + "selected-symbols": "{ count, plural, =1 {1 सिंबल} other {# सिंबल} } चयनित", + "download-symbol": "SCADA सिंबल डाउनलोड करें", + "export-symbol": "SCADA सिंबल को JSON में एक्सपोर्ट करें", + "import-symbol": "JSON से SCADA सिंबल इम्पोर्ट करें", + "upload-symbol": "SCADA सिंबल अपलोड करें", + "update-symbol": "SCADA सिंबल अपडेट करें", + "edit-symbol": "SCADA सिंबल संपादित करें", + "symbol-details": "SCADA सिंबल विवरण", + "mode-svg": "SVG", + "mode-xml": "XML", + "no-symbols": "कोई सिंबल नहीं मिला", + "show-hidden-elements": "छिपे हुए एलिमेंट्स दिखाएँ", + "hide-hidden-elements": "छिपे हुए एलिमेंट्स छिपाएँ", + "delete-symbol": "SCADA सिंबल हटाएँ", + "delete-symbol-title": "क्या आप वाकई SCADA सिंबल '{{imageTitle}}' को हटाना चाहते हैं?", + "delete-symbol-text": "सावधान रहें, पुष्टि के बाद SCADA सिंबल हमेशा के लिए खो जाएगा.", + "delete-symbols-title": "क्या आप वाकई { count, plural, =1 {1 SCADA सिंबल} other {# SCADA सिंबल्स} } को हटाना चाहते हैं?", + "delete-symbols-text": "सावधान रहें, पुष्टि के बाद सभी चयनित SCADA सिंबल्स हटा दिए जाएँगे और उनसे संबंधित सभी डेटा हमेशा के लिए खो जाएगा.", + "include-system-symbols": "सिस्टम सिंबल्स शामिल करें", + "symbol-preview": "सिंबल प्रीव्यू", + "general": "सामान्य", + "tags": "टैग्स", + "properties": "प्रॉपर्टीज़", + "title": "शीर्षक", + "description": "वर्णन", + "search-tags": "टैग्स खोजें", + "widget-size": "विजेट आकार", + "cols": "कॉलम", + "rows": "पंक्तियाँ", + "state-render-function": "स्टेट रेंडर फ़ंक्शन", + "preview": "प्रीव्यू", + "preview-widget-action-text": "विजेट क्रिया '{{type}}' सफलतापूर्वक कॉल की गई!", + "no-symbol": "कोई SCADA सिंबल नहीं", + "no-symbol-selected": "कोई SCADA सिंबल चयनित नहीं है", + "clear-symbol": "SCADA सिंबल साफ़ करें", + "browse-symbol-from-gallery": "गैलरी से SCADA सिंबल ब्राउज़ करें", + "zoom-in": "ज़ूम इन", + "zoom-out": "ज़ूम आउट", + "create-widget": "विजेट बनाएँ", + "create-widget-from-symbol": "SCADA सिंबल से विजेट बनाएँ", + "hidden": "छिपा हुआ", + "tag": { + "tag": "टैग", + "on-click-action": "क्लिक पर क्रिया", + "no-tags": "कोई टैग कॉन्फ़िगर नहीं है", + "delete-tag-text": "क्या आप वाकई {{elementType}} एलिमेंट से टैग
    {{tag}} हटाना चाहते हैं?", + "update-tag": "टैग अपडेट करें", + "enter-tag": "टैग दर्ज करें", + "tag-settings": "टैग सेटिंग्स", + "remove-tag": "टैग हटाएँ", + "add-tag": "टैग जोड़ें" + }, + "behavior": { + "behavior": "व्यवहार", + "id": "Id", + "name": "नाम", + "type": "प्रकार", + "no-behaviors": "कोई व्यवहार कॉन्फ़िगर नहीं है", + "add-behavior": "व्यवहार जोड़ें", + "type-action": "क्रिया", + "type-value": "मान", + "type-widget-action": "विजेट क्रिया", + "behavior-settings": "व्यवहार सेटिंग्स", + "remove-behavior": "व्यवहार हटाएँ", + "hint": "संकेत", + "group-title": "समूह शीर्षक", + "value-type": "मान प्रकार", + "default-value": "डिफ़ॉल्ट मान", + "true-label": "सत्य लेबल", + "false-label": "असत्य लेबल", + "state-label": "स्थिति लेबल", + "default-payload": "डिफ़ॉल्ट पेलोड", + "not-unique-behavior-ids-error": "व्यवहार Ids अद्वितीय होने चाहिए!", + "default-settings": "डिफ़ॉल्ट सेटिंग्स" + }, + "symbol": { + "symbol": "SCADA सिंबल", + "fluid-presence": "द्रव की उपस्थिति", + "fluid-presence-hint": "संकेत करता है कि पाइप में द्रव मौजूद है या नहीं.", + "fluid-present": "द्रव मौजूद", + "present": "मौजूद", + "absent": "अनुपस्थित", + "flow-presence": "प्रवाह की उपस्थिति", + "flow-presence-hint": "संकेत करता है कि पाइप में द्रव प्रवाहित हो रहा है या नहीं.", + "flow-present": "प्रवाह मौजूद", + "flow-direction": "प्रवाह की दिशा", + "flow-direction-hint": "द्रव के प्रवाह की दिशा दर्शाता है.", + "forward": "आगे", + "reverse": "उल्टा", + "flow-animation-speed": "प्रवाह एनीमेशन गति", + "flow-animation-speed-hint": "डबल मान जो प्रवाह की एनीमेशन गति दर्शाता है. 1 - सामान्य गति, 0 - कोई एनीमेशन नहीं, < 1 - धीमी एनीमेशन, > 1 - तेज़ एनीमेशन.", + "leak": "रिसाव", + "leak-hint": "संकेत करता है कि पाइप में रिसाव मौजूद है या नहीं.", + "leak-present": "रिसाव मौजूद", + "fluid-color": "द्रव का रंग", + "pipe-color": "पाइप का रंग", + "horizontal-pipe": "क्षैतिज पाइप", + "vertical-pipe": "ऊर्ध्वाधर पाइप", + "horizontal-fluid-color": "क्षैतिज द्रव का रंग", + "vertical-fluid-color": "ऊर्ध्वाधर द्रव का रंग", + "left-pipe": "बायाँ पाइप", + "right-pipe": "दायाँ पाइप", + "top-pipe": "ऊपरी पाइप", + "bottom-pipe": "निचला पाइप", + "left-fluid-color": "बाएँ द्रव का रंग", + "right-fluid-color": "दाएँ द्रव का रंग", + "top-fluid-color": "ऊपरी द्रव का रंग", + "bottom-fluid-color": "निचले द्रव का रंग", + "display": "डिस्प्ले", + "display-format": "डिस्प्ले फ़ॉर्मेट", + "value": "मान", + "decimals": "दशमलव", + "units": "इकाइयाँ", + "flow-meter-value-hint": "फ़्लो मीटर डिस्प्ले पर दिखाया जाने वाला डबल मान", + "value-hint": "वर्तमान मान दर्शाने वाला डबल मान", + "running": "रनिंग", + "running-hint": "दर्शाता है कि कंपोनेंट रनिंग स्थिति में है या नहीं.", + "warning-state": "चेतावनी स्थिति", + "warning": "चेतावनी", + "warning-click": "चेतावनी क्लिक", + "warning-state-hint": "दर्शाता है कि कंपोनेंट चेतावनी स्थिति में है या नहीं.", + "critical-state": "क्रिटिकल स्थिति", + "critical": "क्रिटिकल", + "critical-click": "क्रिटिकल क्लिक", + "critical-state-hint": "दर्शाता है कि कंपोनेंट क्रिटिकल स्थिति में है या नहीं.", + "critical-state-animation": "क्रिटिकल स्थिति एनीमेशन", + "critical-state-animation-hint": "जब कंपोनेंट क्रिटिकल स्थिति में हो, तो क्या ब्लिंकिंग एनीमेशन सक्षम हो.", + "warning-critical-state-animation": "चेतावनी/क्रिटिकल स्थिति एनीमेशन", + "warning-critical-state-animation-hint": "जब कंपोनेंट चेतावनी या क्रिटिकल स्थिति में हो, तो क्या ब्लिंकिंग एनीमेशन सक्षम हो.", + "animation": "एनीमेशन", + "broken": "टूटा हुआ", + "broken-hint": "दर्शाता है कि कंपोनेंट टूटा हुआ है या नहीं.", + "on-display-click": "डिस्प्ले पर क्लिक पर", + "on-display-click-hint": "जब उपयोगकर्ता डिस्प्ले पर क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "pipe": "पाइप", + "default-border-color": "डिफ़ॉल्ट बॉर्डर रंग", + "active-border-color": "एक्टिव बॉर्डर रंग", + "warning-border-color": "चेतावनी बॉर्डर रंग", + "critical-border-color": "क्रिटिकल बॉर्डर रंग", + "background-color": "पृष्ठभूमि रंग", + "rotation-animation-speed": "रोटेशन एनीमेशन गति", + "rotation-animation-speed-hint": "डबल मान जो रोटेशन एनीमेशन की गति दर्शाता है. 1 - सामान्य गति, 0 - कोई एनीमेशन नहीं, < 1 - धीमी एनीमेशन, > 1 - तेज़ एनीमेशन.", + "on-click": "क्लिक पर", + "on-click-hint": "जब उपयोगकर्ता कंपोनेंट पर क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "connectors-positions": "कनेक्टर्स की पोज़िशन", + "right-connector": "दायाँ कनेक्टर", + "right-top-connector": "दायाँ ऊपर कनेक्टर", + "right-bottom-connector": "दायाँ नीचे कनेक्टर", + "left-connector": "बायाँ कनेक्टर", + "left-top-connector": "बायाँ ऊपर कनेक्टर", + "left-bottom-connector": "बायाँ नीचे कनेक्टर", + "top-left-connector": "ऊपरी बायाँ कनेक्टर", + "top-right-connector": "ऊपरी दायाँ कनेक्टर", + "top-connector": "ऊपरी कनेक्टर", + "bottom-connector": "निचला कनेक्टर", + "running-color": "रनिंग रंग", + "stopped-color": "रुका हुआ रंग", + "stopped": "रुका हुआ", + "warning-color": "चेतावनी रंग", + "critical-color": "क्रिटिकल रंग", + "opened": "खुला हुआ", + "opened-hint": "दर्शाता है कि कंपोनेंट ओपन स्थिति में है या नहीं.", + "open": "ओपन करें", + "open-hint": "जब उपयोगकर्ता कंपोनेंट को ओपन करने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "close": "क्लोज़", + "close-hint": "जब उपयोगकर्ता कंपोनेंट को क्लोज़ करने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "close-state-animation": "क्लोज़ स्थिति एनीमेशन", + "close-state-animation-hint": "जब कंपोनेंट क्लोज़ स्थिति में हो, तो क्या ब्लिंकिंग एनीमेशन सक्षम हो.", + "opened-color": "ओपन स्थिति रंग", + "closed-color": "क्लोज़ स्थिति रंग", + "opened-rotation-angle": "ओपन स्थिति रोटेशन एंगल", + "closed-rotation-angle": "क्लोज़ स्थिति रोटेशन एंगल", + "tank-capacity": "टैंक क्षमता", + "tank-capacity-hint": "टैंक की कुल क्षमता दर्शाने वाला डबल मान.", + "current-volume": "वर्तमान वॉल्यूम", + "current-volume-hint": "वर्तमान भरे हुए वॉल्यूम को दर्शाने वाला डबल मान.", + "tank-color": "टैंक रंग", + "value-box": "वैल्यू बॉक्स", + "value-text": "वैल्यू टेक्स्ट", + "scale": "स्केल", + "transparent-mode": "ट्रांसपेरेंट मोड", + "major-ticks": "मेजर टिक्स", + "intervals": "इंटरवल्स", + "major-ticks-color": "मेजर टिक्स रंग", + "normal": "नॉर्मल", + "minor-ticks": "माइनर टिक्स", + "minor-ticks-color": "माइनर टिक्स रंग", + "temperature": "तापमान", + "temperature-hint": "वर्तमान तापमान दर्शाने वाला डबल मान.", + "update-temperature": "तापमान अपडेट करें", + "update-temperature-hint": "जब उपयोगकर्ता वर्तमान तापमान बदलने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "run": "रन", + "run-hint": "जब उपयोगकर्ता कंपोनेंट को रन करने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "stop": "रोकें", + "stop-hint": "जब उपयोगकर्ता कंपोनेंट को रोकने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "temperature-step": "तापमान स्टेप इंक्रीमेंट", + "heat-pump-color": "हीट पंप रंग", + "power-button-background": "पावर बटन बैकग्राउंड", + "value-box-background": "वैल्यू बॉक्स बैकग्राउंड", + "value-units": "वैल्यू यूनिट्स", + "enable-units-scale": "स्केल पर यूनिट्स सक्षम करें", + "filtration-mode": "फ़िल्ट्रेशन मोड", + "filtration-mode-hint": "वर्तमान फ़िल्ट्रेशन मोड दर्शाने वाला इंटीजर मान.", + "filtration-mode-update": "फ़िल्ट्रेशन मोड अपडेट स्थिति", + "filtration-mode-update-hint": "जब उपयोगकर्ता वर्तमान फ़िल्ट्रेशन मोड बदलने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "filter-mode": "फ़िल्टर", + "waste-mode": "वेस्ट", + "backwash-mode": "बैकवॉश", + "recirculate-mode": "रीसर्क्युलेट", + "rinse-mode": "रिन्स", + "closed-mode": "क्लोज़्ड", + "sand-filter-color": "सैंड फ़िल्टर रंग", + "mode-box-background": "मोड बॉक्स बैकग्राउंड", + "border-color": "बॉर्डर रंग", + "label-color": "लेबल रंग", + "water-leak-hint": "संकेत करता है कि रिसाव है या नहीं.", + "default-color": "डिफ़ॉल्ट रंग", + "leak-color": "रिसाव रंग", + "full-value": "फुल वैल्यू", + "full-value-hint": "फुल वैल्यू दर्शाने वाला डबल मान.", + "label": "लेबल", + "icon": "आइकन", + "button-color": "बटन रंग", + "on-label": "'On' लेबल टेक्स्ट", + "off-label": "'Off' लेबल टेक्स्ट", + "arrow-presence": "ऐरो की उपस्थिति", + "arrow-presence-hint": "संकेत करता है कि कनेक्टर में ऐरो मौजूद है या नहीं.", + "arrow-present": "ऐरो मौजूद", + "arrow-direction": "प्रवाह की दिशा", + "arrow-direction-hint": "प्रवाह की दिशा दर्शाता है.", + "flow-animation": "प्रवाह की उपस्थिति", + "flow-animation-hint": "संकेत करता है कि कनेक्टर में द्रव प्रवाहित हो रहा है या नहीं.", + "flow": "प्रवाह", + "flow-line": "लाइन", + "flow-line-style": "लाइन शैली", + "flow-style-hint": "डैश और गैप मान इस तरह सेट करें कि उनका योग 100 से पूरी तरह विभाज्य हो ताकि एनीमेशन सिंक्रोनाइज़ेशन परफेक्ट हो.", + "flow-dash-cap": "डैश कैप", + "dash-cap-butt": "सीधा", + "dash-cap-round": "गोल", + "dash-cap-square": "चौकोर", + "dash": "डैश", + "gap": "गैप", + "main-line": "मुख्य लाइन", + "line": "लाइन", + "line-color": "लाइन रंग", + "arrow-color": "ऐरो रंग", + "target-value": "टार्गेट वैल्यू", + "target-value-hint": "स्केल पर टार्गेट बिंदु दर्शाता है.", + "min-max-value": "न्यूनतम और अधिकतम मान", + "min-value": "न्यूनतम", + "max-value": "अधिकतम", + "progress-bar": "प्रोग्रेस बार", + "progress-arrow": "प्रोग्रेस ऐरो", + "warning-scale-color": "चेतावनी स्केल रंग", + "critical-scale-color": "क्रिटिकल स्केल रंग", + "scale-color": "स्केल रंग", + "target": "टार्गेट", + "high-warning-state": "हाई चेतावनी स्थिति", + "show-high-warning-scale": "हाई चेतावनी स्केल दिखाएँ", + "high-warning-scale": "हाई चेतावनी स्केल", + "high-warning-state-hint": "डबल मान जो अधिकतम क्रिटिकल या अधिकतम मान तक हाई चेतावनी रेंज को दर्शाता है.", + "low-warning-state": "लो चेतावनी स्थिति", + "show-low-warning-scale": "लो चेतावनी स्केल दिखाएँ", + "low-warning-scale": "लो चेतावनी स्केल", + "low-warning-state-hint": "डबल मान जो न्यूनतम क्रिटिकल या न्यूनतम मान तक लो चेतावनी रेंज को दर्शाता है.", + "high-critical-state": "हाई क्रिटिकल स्थिति", + "show-high-critical-scale": "हाई क्रिटिकल स्केल दिखाएँ", + "high-critical-scale": "हाई क्रिटिकल स्केल", + "high-critical-state-hint": "डबल मान जो अधिकतम मान स्केल तक हाई क्रिटिकल रेंज को दर्शाता है.", + "low-critical-state": "लो क्रिटिकल स्थिति", + "show-low-critical-scale": "लो क्रिटिकल स्थिति दिखाएँ", + "low-critical-scale": "लो क्रिटिकल स्थिति", + "low-critical-state-hint": "डबल मान जो न्यूनतम मान स्केल तक लो क्रिटिकल रेंज को दर्शाता है.", + "filter-color": "फ़िल्टर रंग", + "colors": "रंग", + "indicator-colors": "इंडिकेटर रंग", + "enabled": "सक्षम", + "disabled": "अक्षम", + "on": "चालू", + "off": "बंद", + "on-off-state": "चालू/बंद स्थिति", + "on-off-state-hint": "दर्शाता है कि कंपोनेंट चालू या बंद स्थिति में है.", + "on-update-state": "चालू स्थिति अपडेट", + "on-update-state-hint": "जब उपयोगकर्ता स्थिति को चालू करने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "off-update-state": "बंद स्थिति अपडेट", + "off-update-state-hint": "जब उपयोगकर्ता स्थिति को बंद करने के लिए क्लिक करता है तब कॉल की जाने वाली क्रिया.", + "voltage": "वोल्टेज", + "input-voltage": "इनपुट वोल्टेज", + "input-voltage-hint": "डबल मान जो इनपुट वोल्टेज मान दर्शाता है.", + "output-voltage": "आउटपुट वोल्टेज", + "output-voltage-hint": "डबल मान जो आउटपुट वोल्टेज मान दर्शाता है.", + "first-phase-voltage": "पहले फेज का वोल्टेज", + "second-phase-voltage": "दूसरे फेज का वोल्टेज", + "third-phase-voltage": "तीसरे फेज का वोल्टेज", + "phase-voltage-hint": "डबल मान जो वर्तमान फेज के वोल्टेज मान को दर्शाता है.", + "voltage-hint": "डबल मान जो वर्तमान वोल्टेज दर्शाता है.", + "current-voltage-color": "वर्तमान वोल्टेज रंग", + "phase-indicator-color": "फेज इंडिकेटर रंग", + "measured": "मापा गया", + "measured-hint": "डबल मान किलोवाट-घंटों में ऊर्जा खपत दर्शाता है.", + "day-rate": "दैनिक दर", + "night-rate": "रात्रि दर", + "off-peak-rate": "ऑफ़-पीक दर", + "peak-rate": "पीक दर", + "export-rate": "निर्यात दर", + "operating-mode": "ऑपरेटिंग मोड", + "bypass-mode": "बायपास", + "operating-mode-hint": "इंटीजर मान वर्तमान ऑपरेटिंग मोड दर्शाता है (0 - बंद, 1 - चालू, 2 - बायपास).", + "connected": "कनेक्टेड", + "connected-hint": "संकेत करता है कि कंपोनेंट कनेक्टेड स्थिति में है या नहीं.", + "disconnected": "डिसकनेक्टेड", + "indicator": "इंडिकेटर", + "operation-mode": "ऑपरेशन मोड", + "operation-mode-hint": "संकेत करता है कि इन्वर्टर मेन्स या इन्वर्टर मोड में है.", + "operation-mode-indicators-color": "ऑपरेशन मोड इंडिकेटर्स रंग", + "mains-on-mode": "मेन्स चालू", + "inverter-on-mode": "इन्वर्टर चालू", + "charging-mode": "चार्जिंग मोड", + "charging-mode-hint": "इंटीजर मान वर्तमान चार्जिंग मोड दर्शाता है (1 - बल्क, 2 - ऐब्सॉर्प्शन, 3 - फ़्लोट).", + "charging-mode-indicators-color": "चार्जिंग मोड इंडिकेटर्स रंग", + "inverter-faults": "फॉल्ट्स", + "inverter-fault-indicators-color": "फॉल्ट इंडिकेटर्स रंग", + "overload-fault": "ओवरलोड", + "overload-fault-hint": "संकेत करता है कि इन्वर्टर ओवरलोड स्थिति में है या नहीं.", + "low-battery-fault": "लो बैटरी", + "low-battery-fault-hint": "संकेत करता है कि बैटरी अत्यधिक डिस्चार्ज हो चुकी है या नहीं.", + "temperature-fault": "तापमान", + "temperature-fault-hint": "संकेत करता है कि इन्वर्टर के भीतर उच्च तापमान है या नहीं.", + "triangle": "त्रिभुज", + "socket": "सॉकेट", + "left-button": "बायाँ बटन", + "right-button": "दायाँ बटन", + "alarm-colors": "अलार्म रंग", + "hook-color": "हुक रंग" + } + }, + "item": { + "selected": "चयनित" + }, + "js-func": { + "no-return-error": "फ़ंक्शन को मान लौटाना चाहिए!", + "return-type-mismatch": "फ़ंक्शन को '{{type}}' प्रकार का मान लौटाना चाहिए!", + "tidy": "फॉर्मैट करें", + "mini": "मिनी", + "modules": "मॉड्यूल्स", + "remove-module": "मॉड्यूल हटाएँ", + "no-modules": "कोई मॉड्यूल कॉन्फ़िगर नहीं है", + "add-module": "मॉड्यूल जोड़ें", + "module-alias": "उपनाम", + "invalid-module-alias-name": "अमान्य उपनाम नाम", + "module-resource": "JS मॉड्यूल संसाधन", + "not-unique-module-aliases-error": "मॉड्यूल उपनाम अद्वितीय होने चाहिए!", + "show-module-info": "मॉड्यूल जानकारी दिखाएँ", + "show-module-source-code": "मॉड्यूल सोर्स कोड दिखाएँ", + "module-members": "मॉड्यूल सदस्य", + "module-no-members": "मॉड्यूल के कोई निर्यातित सदस्य नहीं हैं", + "module-load-error": "मॉड्यूल लोड त्रुटि", + "source-code": "सोर्स कोड", + "source-code-load-error": "सोर्स कोड लोड त्रुटि", + "no-js-module-text": "कोई JS मॉड्यूल नहीं मिला", + "no-js-module-matching": "'{{module}}' से मेल खाते कोई JS मॉड्यूल नहीं मिले." + }, + "key-val": { + "key": "कुंजी", + "value": "मान", + "remove-entry": "प्रविष्टि हटाएँ", + "add-entry": "प्रविष्टि जोड़ें", + "no-data": "कोई प्रविष्टि नहीं" + }, + "layout": { + "layout": "लेआउट", + "layouts": "लेआउट्स", + "manage": "लेआउट्स प्रबंधित करें", + "settings": "लेआउट सेटिंग्स", + "color": "रंग", + "main": "मुख्य", + "right": "दायाँ", + "left": "बायाँ", + "select": "लक्ष्य लेआउट चुनें", + "percentage-width": "प्रतिशत चौड़ाई (%)", + "fixed-width": "निश्चित चौड़ाई (px)", + "left-width": "बाएँ कॉलम (%)", + "right-width": "दाएँ कॉलम (%)", + "pick-fixed-side": "निश्चित साइड चुनें: ", + "layout-fixed-width": "निश्चित चौड़ाई (px)", + "value-min-error": "मान {{min}}{{unit}} से अधिक होना चाहिए", + "value-max-error": "मान {{max}}{{unit}} से कम होना चाहिए", + "layout-fixed-width-required": "निश्चित चौड़ाई आवश्यक है", + "right-width-percentage-required": "दाएँ प्रतिशत आवश्यक है", + "left-width-percentage-required": "बाएँ प्रतिशत आवश्यक है", + "divider": "डिवाइडर", + "right-side": "दाएँ साइड लेआउट", + "left-side": "बाएँ साइड लेआउट", + "add-new-breakpoint": "नया ब्रेकपॉइंट जोड़ें", + "breakpoint": "ब्रेकपॉइंट", + "breakpoints": "ब्रेकपॉइंट्स", + "copy-from": "से कॉपी करें", + "size": "आकार", + "delete-breakpoint-title": "क्या आप वाकई ब्रेकपॉइंट '{{name}}' को हटाना चाहते हैं?", + "delete-breakpoint-text": "कृपया ध्यान दें, पुष्टि के बाद यह ब्रेकपॉइंट हमेशा के लिए खो जाएगा और सेटिंग्स डिफ़ॉल्ट ब्रेकपॉइंट पर वापस चली जाएँगी." + }, + "legend": { + "direction": "दिशा", + "position": "स्थिति", + "show-values": "मान दिखाएँ", + "min-option": "न्यूनतम", + "max-option": "अधिकतम", + "average-option": "औसत", + "total-option": "कुल", + "latest-option": "नवीनतम", + "sort-legend": "लीजेंड में डेटा कीज़ को सॉर्ट करें", + "show-max": "अधिकतम मान दिखाएँ", + "show-min": "न्यूनतम मान दिखाएँ", + "show-avg": "औसत मान दिखाएँ", + "show-total": "कुल मान दिखाएँ", + "show-latest": "नवीनतम मान दिखाएँ", + "settings": "लीजेंड सेटिंग्स", + "min": "न्यूनतम", + "max": "अधिकतम", + "avg": "औसत", + "total": "कुल", + "latest": "नवीनतम", + "Min": "न्यूनतम", + "Max": "अधिकतम", + "Avg": "औसत", + "Total": "कुल", + "Latest": "नवीनतम", + "comparison-time-ago": { + "previousInterval": "(पिछला अंतराल)", + "customInterval": "(कस्टम अंतराल)", + "days": "(दिन पहले)", + "weeks": "(सप्ताह पहले)", + "months": "(महीना पहले)", + "years": "(साल पहले)" + }, + "column-title": "कॉलम शीर्षक", + "label": "लेबल", + "value": "मान" + }, + "login": { + "login": "लॉगिन", + "request-password-reset": "पासवर्ड रीसेट का अनुरोध करें", + "reset-password": "पासवर्ड रीसेट करें", + "create-password": "पासवर्ड बनाएँ", + "two-factor-authentication": "दो-कारक प्रमाणीकरण", + "passwords-mismatch-error": "दर्ज किए गए पासवर्ड समान होने चाहिए!", + "password-again": "पासवर्ड दोबारा", + "sign-in": "कृपया साइन इन करें", + "username": "यूज़रनेम (ईमेल)", + "remember-me": "मुझे याद रखें", + "forgot-password": "पासवर्ड भूल गए?", + "password-reset": "पासवर्ड रीसेट", + "expired-password-reset-message": "आपके क्रेडेंशियल्स की अवधि समाप्त हो गई है! \nकृपया नया पासवर्ड बनाएँ.", + "new-password": "नया पासवर्ड", + "new-password-again": "नया पासवर्ड पुष्टि करें", + "password-link-sent-message": "रीसेट लिंक भेज दिया गया है", + "email": "ईमेल", + "invalid-email-format": "अमान्य ईमेल फ़ॉर्मेट.", + "sign-in-with": "{{name}} के साथ साइन इन करें", + "sign-in-to-your-account": "अपने खाते में साइन इन करें", + "or": "या", + "error": "लॉगिन त्रुटि", + "verify-your-identity": "अपनी पहचान सत्यापित करें", + "select-way-to-verify": "सत्यापित करने का तरीका चुनें", + "resend-code": "कोड फिर से भेजें", + "resend-code-wait": "{ time, plural, =1 {1 सेकंड} other {# सेकंड} } में कोड फिर से भेजें", + "try-another-way": "कोई अन्य तरीका आज़माएँ", + "totp-auth-description": "कृपया अपने ऑथेंटिकेटर ऐप से सिक्योरिटी कोड दर्ज करें.", + "totp-auth-placeholder": "कोड", + "sms-auth-description": "आपके फ़ोन {{contact}} पर सिक्योरिटी कोड भेजा गया है.", + "sms-auth-placeholder": "SMS कोड", + "email-auth-description": "आपके ईमेल पते {{contact}} पर सिक्योरिटी कोड भेजा गया है.", + "email-auth-placeholder": "ईमेल कोड", + "backup-code-auth-description": "कृपया अपने बैकअप कोड्स में से एक दर्ज करें.", + "backup-code-auth-placeholder": "बैकअप कोड", + "activation-link-expired": "सक्रियन लिंक की अवधि समाप्त हो गई है", + "activation-link-expired-message": "आपके प्रोफ़ाइल को सक्रिय करने वाला लिंक समाप्त हो गया है। आप नया ईमेल प्राप्त करने के लिए लॉगिन पेज पर वापस जा सकते हैं.", + "reset-password-link-expired": "पासवर्ड रीसेट लिंक की अवधि समाप्त हो गई है", + "reset-password-link-expired-message": "आपके पासवर्ड को रीसेट करने वाला लिंक समाप्त हो गया है। आप नया ईमेल प्राप्त करने के लिए लॉगिन पेज पर वापस जा सकते हैं.", + "two-fa": "दो-कारक प्रमाणीकरण", + "two-fa-required": "दो-कारक प्रमाणीकरण आवश्यक है", + "set-up-verification-method": "जारी रखने के लिए एक सत्यापन विधि सेट करें", + "set-up-verification-method-login": "वेरिफिकेशन विधि सेट करें या लॉगिन करें", + "enable-authenticator-app": "ऑथेंटिकेटर ऐप सक्षम करें", + "enable-authenticator-app-description": "कृपया अपने ऑथेंटिकेटर ऐप से सुरक्षा कोड दर्ज करें", + "enable-authenticator-sms": "SMS ऑथेंटिकेटर सक्षम करें", + "enable-authenticator-sms-description": "वह 6-अंकों का कोड दर्ज करें जो हमने अभी भेजा है", + "enable-authenticator-email": "ईमेल ऑथेंटिकेटर सक्षम करें", + "enable-authenticator-email-description": "एक सुरक्षा कोड आपके ईमेल पते पर भेजा गया है", + "enter-key-manually": "या इस 32-अंकों की कुंजी को मैन्युअली दर्ज करें:", + "continue": "जारी रखें", + "confirm": "पुष्टि करें", + "authenticator-app-success": "ऑथेंटिकेटर ऐप सफलतापूर्वक सक्षम किया गया", + "authenticator-app-success-description": "अगली बार जब आप साइन इन करेंगे, तो आपको दो-कारक प्रमाणीकरण कोड प्रदान करना होगा", + "authenticator-sms-success": "SMS ऑथेंटिकेटर सफलतापूर्वक सक्षम किया गया", + "authenticator-sms-success-description": "अगली बार जब आप साइन इन करेंगे, तो आपको सुरक्षा कोड दर्ज करने के लिए प्रेरित किया जाएगा जो फोन नंबर पर भेजा जाएगा", + "authenticator-email-success": "ईमेल ऑथेंटिकेटर सफलतापूर्वक सक्षम किया गया", + "authenticator-email-success-description": "अगली बार जब आप साइन इन करेंगे, तो आपको सुरक्षा कोड दर्ज करने के लिए प्रेरित किया जाएगा जो आपके ईमेल पते पर भेजा जाएगा", + "authenticator-backup-code-success": "बैकअप कोड सफलतापूर्वक सक्षम किया गया", + "authenticator-backup-code-success-description": "अगली बार जब आप साइन इन करेंगे, तो आपको सुरक्षा कोड दर्ज करने के लिए या बैकअप कोड में से एक का उपयोग करने के लिए प्रेरित किया जाएगा", + "add-verification-method": "वेरिफिकेशन विधि जोड़ें", + "get-backup-code": "बैकअप कोड प्राप्त करें", + "copy-key": "कुंजी कॉपी करें", + "send-code": "कोड भेजें", + "email-label": "ईमेल", + "email-description": "एक ईमेल दर्ज करें जिसे आप अपना ऑथेंटिकेटर के रूप में उपयोग करेंगे।", + "sms-description": "एक फोन नंबर दर्ज करें जिसे आप अपना ऑथेंटिकेटर के रूप में उपयोग करेंगे।", + "backup-code-description": "कोड को प्रिंट करें ताकि जब आपको इन्हें अपने खाते में लॉगिन करने के लिए उपयोग करने की आवश्यकता हो, तो आपके पास हों। आप प्रत्येक बैकअप कोड का एक बार उपयोग कर सकते हैं।", + "backup-code-warn": "एक बार जब आप इस पृष्ठ को छोड़ देंगे, तो ये कोड फिर से नहीं दिखाए जाएंगे। इन्हें नीचे दिए गए विकल्पों का उपयोग करके सुरक्षित रूप से स्टोर करें।", + "download-txt": "डाउनलोड (txt)", + "print": "प्रिंट करें", + "verification-code": "6-अंकों का कोड", + "verification-code-invalid": "अमान्य सत्यापन कोड प्रारूप", + "verification-code-incorrect": "सत्यापन कोड गलत है", + "verification-code-many-request": "सत्यापन कोड जांचने के लिए बहुत अधिक अनुरोध", + "scan-qr-code": "अपने सत्यापन ऐप से इस QR कोड को स्कैन करें", + "phone-input": { + "phone-input-label": "फोन नंबर", + "phone-input-required": "फोन नंबर आवश्यक है", + "phone-input-validation": "फोन नंबर अमान्य है या संभव नहीं है", + "phone-input-pattern": "अमान्य फोन नंबर। E.164 प्रारूप में होना चाहिए, जैसे {{phoneNumber}}", + "phone-input-hint": "फोन नंबर E.164 प्रारूप में, जैसे {{phoneNumber}}" + } + }, + "mobile": { + "add-application": "एप्लिकेशन जोड़ें", + "app-id": "ऐप साइट एसोसिएशन ID", + "app-id-required": "ऐप साइट एसोसिएशन ID आवश्यक है", + "app-id-pattern": "अमान्य App Site Association ID फ़ॉर्मेट", + "app-store-link": "App Store लिंक", + "app-store-link-required": "App Store लिंक आवश्यक है", + "application-details": "एप्लिकेशन विवरण", + "application-package": "एप्लिकेशन पैकेज", + "application-secret": "एप्लिकेशन सीक्रेट", + "application-secret-required": "एप्लिकेशन सीक्रेट आवश्यक है", + "application": "एप्लिकेशन", + "applications": "एप्लिकेशन्स", + "copy-app-id": "App ID कॉपी करें", + "copy-app-store-link": "App Store लिंक कॉपी करें", + "copy-application-package": "एप्लिकेशन पैकेज कॉपी करें", + "copy-application-secret": "एप्लिकेशन सीक्रेट कॉपी करें", + "copy-google-play-link": "Google Play लिंक कॉपी करें", + "copy-sha256-certificate-fingerprints": "SHA256 सर्टिफिकेट फिंगरप्रिंट्स कॉपी करें", + "delete-application": "एप्लिकेशन हटाएँ", + "delete-application-button-text": "मैं परिणामों को समझता हूँ, एप्लिकेशन हटाएँ", + "delete-application-text": "यह क्रिया वापस नहीं ली जा सकती. यह आपकी एप्लिकेशन को स्थायी रूप से हटा देगी.
    यदि आप इसे स्थायी रूप से हटाना नहीं चाहते हैं तो आप एप्लिकेशन को अस्थायी रूप से निलंबित कर सकते हैं.
    एप्लिकेशन हटाने के लिए कृपया पुष्टि करने हेतु \"{{phrase}}\" टाइप करें.", + "delete-application-title-short": "क्या आप वाकई एप्लिकेशन '{{name}}' को हटाना चाहते हैं?", + "delete-application-text-short": "सावधान रहें, पुष्टि के बाद एप्लिकेशन्स और उनसे संबंधित सभी डेटा हमेशा के लिए खो जाएँगे.", + "delete-application-phrase": "delete application", + "delete-applications-bundle-text": "सावधान रहें, पुष्टि के बाद मोबाइल बंडल और उससे संबंधित सभी डेटा हमेशा के लिए खो जाएगा.", + "delete-applications-bundle-title": "क्या आप वाकई मोबाइल बंडल '{{bundleName}}' को हटाना चाहते हैं?", + "generate-application-secret": "एप्लिकेशन सीक्रेट जनरेट करें", + "google-play-link": "Google Play लिंक", + "google-play-link-required": "Google Play लिंक आवश्यक है", + "latest-version": "नवीनतम संस्करण", + "min-version": "न्यूनतम संस्करण", + "invalid-version-pattern": "अमान्य संस्करण फ़ॉर्मेट. कृपया यह फ़ॉर्मेट उपयोग करें: major.minor.patch (उदा., 1.0.0).", + "mobile-center": "मोबाइल केंद्र", + "mobile-package": "एप्लिकेशन पैकेज", + "mobile-package-max-length": "एप्लिकेशन पैकेज 256 वर्णों से कम होना चाहिए", + "mobile-package-required": "एप्लिकेशन पैकेज आवश्यक है.", + "mobile-package-pattern": "एप्लिकेशन पैकेज का फ़ॉर्मेट अमान्य है", + "mobile-package-title": "एप्लिकेशन शीर्षक", + "mobile-package-title-max-length": "एप्लिकेशन शीर्षक 256 वर्णों से कम होना चाहिए", + "no-application": "कोई एप्लिकेशन नहीं मिला", + "no-bundles": "कोई बंडल नहीं मिला", + "platform-type": "प्लेटफ़ॉर्म प्रकार", + "search-application": "एप्लिकेशन्स खोजें", + "search-bundles": "बंडल खोजें", + "set": "सेट करें", + "sha256-certificate-fingerprints": "SHA256 सर्टिफिकेट फिंगरप्रिंट्स", + "sha256-certificate-fingerprints-required": "SHA256 सर्टिफिकेट फิงरप्रिंट्स आवश्यक हैं", + "sha256-certificate-fingerprints-pattern": "अमान्य SHA256 सर्टिफिकेट फिंगरप्रिंट फ़ॉर्मेट", + "show-hidden-pages": "छिपे हुए पेज दिखाएँ", + "status": "स्थिति", + "status-type": { + "deprecated": "अप्रचलित", + "draft": "ड्राफ्ट", + "published": "प्रकाशित", + "suspended": "निलंबित" + }, + "store-information": "स्टोर जानकारी", + "version-information": "संस्करण जानकारी", + "min-version-release-notes": "न्यूनतम संस्करण रिलीज़ नोट्स", + "latest-version-release-notes": "नवीनतम संस्करण रिलीज़ नोट्स", + "bundle": "बंडल", + "bundles": "बंडल्स", + "add-bundle": "बंडल जोड़ें", + "title": "शीर्षक", + "title-required": "शीर्षक आवश्यक है", + "title-cannot-contain-only-spaces": "शीर्षक में केवल स्पेस नहीं हो सकते", + "title-max-length": "शीर्षक 256 वर्णों से कम होना चाहिए", + "oauth-clients": "OAuth 2.0 क्लाइंट्स", + "android-app": "Android ऐप", + "android-application": "Android एप्लिकेशन", + "ios-app": "iOS ऐप", + "ios-application": "iOS एप्लिकेशन", + "invalid-store-link": "अमान्य स्टोर लिंक", + "enable-oauth": "OAuth 2.0 सक्षम करें", + "enable-self-registration": "स्व-पंजीकरण सक्षम करें", + "edit-bundle": "बंडल संपादित करें", + "description": "वर्णन", + "basic-settings": "मूल सेटिंग्स", + "no-application-matching": "'{{entity}}' से मेल खाता कोई एप्लिकेशन नहीं मिला.", + "no-bundle-matching": "'{{entity}}' से मेल खाता कोई बंडल नहीं मिला.", + "application-required": "एप्लिकेशन आवश्यक है.", + "bundle-required": "बंडल आवश्यक है.", + "no-application-text": "कोई एप्लिकेशन नहीं मिला", + "no-bundle-text": "कोई बंडल नहीं मिला", + "layout": "लेआउट", + "pages": "पेज", + "hide-all-pages": "सभी पेज छिपाएँ", + "reset-to-default-pages": "पेज को डिफ़ॉल्ट पर रीसेट करें", + "add-specific-page": "विशिष्ट पेज जोड़ें", + "visible": "दृश्यमान", + "hidden": "छिपा हुआ", + "reset-to-page-default": "पेज को वापस डिफ़ॉल्ट पर रीसेट करें", + "mobile-599": "मोबाइल (अधिकतम 599px)", + "tablet-959": "टैबलेट (अधिकतम 959px)", + "max-element-number": "एलिमेंट्स की अधिकतम संख्या", + "page-name": "पेज नाम", + "page-name-required": "पेज नाम आवश्यक है.", + "page-name-cannot-contain-only-spaces": "पेज नाम में केवल स्पेस नहीं हो सकते.", + "page-name-max-length": "पेज नाम 256 वर्णों से कम होना चाहिए", + "page-type": "पेज प्रकार", + "pages-types": { + "dashboard": "डैशबोर्ड", + "web-view": "वेब व्यू", + "custom": "कस्टम" + }, + "url": "URL", + "invalid-url-format": "अमान्य URL फ़ॉर्मेट", + "path": "पाथ", + "invalid-path-format": "अमान्य पाथ फ़ॉर्मेट", + "custom-page": "कस्टम पेज", + "edit-page": "पेज संपादित करें", + "edit-custom-page": "कस्टम पेज संपादित करें", + "delete-page": "पेज हटाएँ", + "qr-code-widget": "QR कोड विजेट", + "type-here": "यहाँ लिखें", + "configuration-dialog": "कॉन्फ़िगरेशन डायलॉग", + "configuration-app": "कॉन्फ़िगरेशन ऐप", + "configuration-step": { + "prepare-environment-title": "डेवलपमेंट एनवायरनमेंट तैयार करें", + "prepare-environment-text": "Flutter ThingsBoard Mobile Application के लिए Flutter SDK आवश्यक है. Flutter SDK सेटअप करने के लिए निर्देशों का पालन करें.", + "get-source-code-title": "ऐप सोर्स कोड प्राप्त करें", + "get-source-code-text": "आप GitHub रिपॉज़िटरी से क्लोन करके Flutter ThingsBoard Mobile Application का सोर्स कोड प्राप्त कर सकते हैं:", + "configure-app-settings-title": "ऐप सेटिंग्स कॉन्फ़िगर करें", + "configure-app-settings-text": "कॉन्फ़िगरेशन फ़ाइल डाउनलोड करें और उसे उस प्रोजेक्ट की रूट डायरेक्टरी में रखें जिसे आपने पिछले चरण में क्लोन किया था.", + "download-file": "फ़ाइल डाउनलोड करें", + "run-app-title": "ऐप चलाएँ", + "run-app-text": "अपने IDE में दिए गए निर्देशों के अनुसार ऐप चलाएँ.\nयदि आप टर्मिनल का उपयोग कर रहे हैं, तो ऐप को निम्न कमांड से चलाएँ:", + "more-information": "विस्तृत जानकारी हमारे Getting Started डॉक्यूमेंटेशन में मिल सकती है.", + "getting-started": "शुरुआत की गाइड" + } + }, + "notification": { + "action-button": "एक्शन बटन", + "action-type": "क्रिया प्रकार", + "active": "सक्रिय", + "add-notification-recipients-group": "नोटिफिकेशन प्राप्तकर्ता समूह जोड़ें", + "add-notification-template": "नोटिफिकेशन टेम्पलेट जोड़ें", + "add-recipient": "प्राप्तकर्ता जोड़ें", + "add-recipients": "प्राप्तकर्ताओं को जोड़ें", + "add-rule": "नियम जोड़ें", + "add-stage": "चरण जोड़ें", + "add-template": "टेम्पलेट जोड़ें", + "after": "के बाद", + "alarm-assignment-trigger-settings": "अलार्म असाइनमेंट ट्रिगर सेटिंग्स", + "alarm-comment-trigger-settings": "अलार्म टिप्पणी ट्रिगर सेटिंग्स", + "alarm-trigger-settings": "अलार्म ट्रिगर सेटिंग्स", + "all": "सभी", + "api-feature-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी API फीचर्स पर लागू होगा", + "api-usage-trigger-settings": "API उपयोग ट्रिगर सेटिंग्स", + "new-platform-version-trigger-settings": "नई प्लेटफ़ॉर्म वर्ज़न ट्रिगर सेटिंग्स", + "rate-limits-trigger-settings": "रेट लिमिट पार होने पर ट्रिगर सेटिंग्स", + "task-processing-failure-trigger-settings": "टास्क प्रोसेसिंग असफलता ट्रिगर सेटिंग्स", + "resources-shortage-trigger-settings": "रिसोर्स कमी ट्रिगर सेटिंग्स", + "at-least-one-should-be-selected": "कम से कम एक को चुना जाना चाहिए", + "basic-settings": "मूल सेटिंग्स", + "button-text": "बटन टेक्स्ट", + "button-text-required": "बटन टेक्स्ट आवश्यक है", + "button-text-max-length": "बटन टेक्स्ट {{ length }} वर्णों से कम या बराबर होना चाहिए", + "compose": "संदेश तैयार करें", + "conversation": "वार्तालाप", + "conversation-required": "वार्तालाप आवश्यक है", + "copy-notification-template": "नोटिफिकेशन टेम्पलेट कॉपी करें", + "copy-rule": "नियम कॉपी करें", + "copy-template": "टेम्पलेट कॉपी करें", + "create-new": "नया बनाएँ", + "created": "बनाया गया", + "customize-messages": "संदेशों को अनुकूलित करें", + "cpu-threshold": "CPU थ्रेशहोल्ड", + "delete-notification-text": "सावधान रहें, पुष्टि के बाद नोटिफिकेशन हमेशा के लिए खो जाएगा.", + "delete-notification-title": "क्या आप वाकई नोटिफिकेशन हटाना चाहते हैं?", + "delete-notifications-text": "सावधान रहें, पुष्टि के बाद नोटिफिकेशन्स हमेशा के लिए खो जाएँगे.", + "delete-notifications-title": "क्या आप वाकई { count, plural, =1 {1 नोटिफिकेशन} other {# नोटिफिकेशन्स} } को हटाना चाहते हैं?", + "delete-recipient-text": "सावधान रहें, पुष्टि के बाद प्राप्तकर्ता हमेशा के लिए खो जाएगा.", + "delete-recipient-title": "क्या आप वाकई प्राप्तकर्ता '{{recipientName}}' को हटाना चाहते हैं?", + "delete-recipients-text": "सावधान रहें, पुष्टि के बाद प्राप्तकर्ता हमेशा के लिए खो जाएँगे.", + "delete-recipients-title": "क्या आप वाकई { count, plural, =1 {1 प्राप्तकर्ता} other {# प्राप्तकर्ता} } को हटाना चाहते हैं?", + "delete-request-text": "सावधान रहें, पुष्टि के बाद रिक्वेस्ट हमेशा के लिए खो जाएगी.", + "delete-request-title": "क्या आप वाकई रिक्वेस्ट हटाना चाहते हैं?", + "delete-requests-text": "सावधान रहें, पुष्टि के बाद रिक्वेस्ट्स हमेशा के लिए खो जाएँगी.", + "delete-requests-title": "क्या आप वाकई { count, plural, =1 {1 रिक्वेस्ट} other {# रिक्वेस्ट्स} } को हटाना चाहते हैं?", + "delete-rule-text": "सावधान रहें, पुष्टि के बाद नियम हमेशा के लिए खो जाएगा.", + "delete-rule-title": "क्या आप वाकई नियम '{{ruleName}}' को हटाना चाहते हैं?", + "delete-rules-text": "सावधान रहें, पुष्टि के बाद नियम हमेशा के लिए खो जाएँगे.", + "delete-rules-title": "क्या आप वाकई { count, plural, =1 {1 नियम} other {# नियम} } को हटाना चाहते हैं?", + "delete-template-text": "सावधान रहें, पुष्टि के बाद टेम्पलेट हमेशा के लिए खो जाएगा.", + "delete-template-title": "क्या आप वाकई टेम्पलेट '{{templateName}}' को हटाना चाहते हैं?", + "delete-templates-text": "सावधान रहें, पुष्टि के बाद टेम्पलेट्स हमेशा के लिए खो जाएँगे.", + "delete-templates-title": "क्या आप वाकई { count, plural, =1 {1 टेम्पलेट} other {# टेम्पलेट्स} } को हटाना चाहते हैं?", + "deleted": "हटाया गया", + "delivery-method": { + "delivery-method": "डिलीवरी मेथड", + "email": "ईमेल", + "email-preview": "ईमेल नोटिफिकेशन प्रीव्यू", + "slack": "स्लैक", + "slack-preview": "स्लैक नोटिफिकेशन प्रीव्यू", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams नोटिफिकेशन प्रीव्यू", + "sms": "SMS", + "sms-preview": "SMS नोटिफिकेशन प्रीव्यू", + "web": "वेब", + "web-preview": "वेब नोटिफिकेशन प्रीव्यू", + "mobile-app": "मोबाइल ऐप", + "mobile-app-preview": "मोबाइल ऐप नोटिफिकेशन प्रीव्यू" + }, + "delivery-method-not-configure-click": "डिलीवरी मेथड कॉन्फ़िगर नहीं है. सेटअप करने के लिए क्लिक करें.", + "delivery-method-not-configure-contact": "डिलीवरी मेथड कॉन्फ़िगर नहीं है. कृपया अपने सिस्टम एडमिनिस्ट्रेटर से संपर्क करें.", + "delivery-methods": "डिलीवरी मेथड्स", + "description": "वर्णन", + "device-activity-trigger-settings": "डिवाइस एक्टिव ट्रिगर सेटिंग्स", + "device-list-rule-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी डिवाइस पर लागू होगा", + "device-profiles-list-rule-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी डिवाइस प्रोफ़ाइल्स पर लागू होगा", + "disabled": "अक्षम", + "edge-trigger-settings": "Edge ट्रिगर सेटिंग्स", + "edge-list-rule-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी Edge इंस्टेंसेज़ पर लागू होगा", + "edit-notification-recipients-group": "नोटिफिकेशन प्राप्तकर्ता समूह संपादित करें", + "edit-notification-template": "नोटिफिकेशन टेम्पलेट संपादित करें", + "edit-rule": "नियम संपादित करें", + "edit-template": "टेम्पलेट संपादित करें", + "enabled": "सक्रिय", + "entities-limit-trigger-settings": "एंटिटीज़ लिमिट ट्रिगर सेटिंग्स", + "entity-action-trigger-settings": "एंटिटी एक्शन ट्रिगर सेटिंग्स", + "entity-type": "एंटिटी प्रकार", + "escalation-chain": "एस्केलेशन चेन", + "failed-send": "भेजने में असफलताएँ", + "fails": "{ count, plural, =1 {1 असफलता} other {# असफलताएँ} }", + "filter": "फ़िल्टर", + "first-recipient": "पहला प्राप्तकर्ता", + "inactive": "निष्क्रिय", + "inbox": "इनबॉक्स", + "notification-inbox": "नोटिफिकेशन्स / इनबॉक्स", + "input-field-support-templatization": "इनपुट फ़ील्ड टेम्पलेटाइज़ेशन सपोर्ट करता है.", + "input-fields-support-templatization": "इनपुट फ़ील्ड्स टेम्पलेटाइज़ेशन सपोर्ट करते हैं.", + "link": "लिंक", + "link-required": "लिंक आवश्यक है", + "link-max-length": "लिंक {{ length }} वर्णों से कम या बराबर होना चाहिए", + "link-type": { + "dashboard": "डैशबोर्ड खोलें", + "link": "URL लिंक खोलें" + }, + "loading-notifications": "नोटिफिकेशन्स लोड हो रहे हैं...", + "management": "नोटिफिकेशन प्रबंधन", + "mark-all-as-read": "सभी को पढ़ा हुआ चिह्नित करें", + "mark-as-read": "पढ़ा हुआ चिह्नित करें", + "message": "संदेश", + "message-required": "संदेश आवश्यक है", + "message-max-length": "संदेश {{ length }} वर्णों से कम या बराबर होना चाहिए", + "name": "नाम", + "name-required": "नाम आवश्यक है", + "new-notification": "नया नोटिफिकेशन", + "no-inbox-notification": "कोई नोटिफिकेशन नहीं मिला", + "no-notification-request": "कोई नोटिफिकेशन रिक्वेस्ट नहीं", + "no-notification-templates": "कोई नोटिफिकेशन टेम्पलेट नहीं मिला", + "no-notifications-yet": "अभी तक कोई नोटिफिकेशन नहीं", + "no-recipients-notification": "कोई प्राप्तकर्ता नोटिफिकेशन नहीं", + "no-recipients-matching": "'{{entity}}' से मेल खाता कोई प्राप्तकर्ता नहीं मिला.", + "no-recipients-text": "कोई प्राप्तकर्ता नहीं मिला", + "no-rule": "कोई नियम कॉन्फ़िगर नहीं है", + "no-rules-notification": "कोई नियम नोटिफिकेशन नहीं", + "no-severity-found": "कोई severity नहीं मिली", + "no-severity-matching": "'{{severity}}' नहीं मिला.", + "no-template-matching": "'{{template}}' से मेल खाता कोई संसाधन नहीं मिला.", + "create-new-template": "एक नया बनाएँ!", + "not-found-slack-recipient": "Slack प्राप्तकर्ता नहीं मिला", + "notification": "नोटिफिकेशन", + "notification-center": "अधिसूचना केंद्र", + "notification-tap-action": "नोटिफिकेशन टैप कार्रवाई", + "notification-tap-action-hint": "यदि सक्षम नहीं किया गया, तो डिफ़ॉल्ट अलार्म डैशबोर्ड का उपयोग किया जाएगा", + "notify": "सूचित करें", + "notify-again": "फिर से सूचित करें", + "notify-alarm-action": { + "acknowledged": "अलार्म स्वीकार किया गया", + "assigned": "अलार्म असाइन किया गया", + "cleared": "अलार्म क्लियर किया गया", + "created": "अलार्म बनाया गया", + "severity-changed": "अलार्म severity बदली गई", + "unassigned": "अलार्म अनअसाइन किया गया" + }, + "notify-on": "पर सूचित करें", + "notify-on-comment-update": "कमेंट अपडेट पर सूचित करें", + "notify-on-required": "'Notify on' आवश्यक है", + "notify-on-unassign": "अनअसाइन पर सूचित करें", + "notify-only-user-comments": "केवल उपयोगकर्ता कमेंट्स पर सूचित करें", + "only-rule-chain-lifecycle-failures": "केवल रूल चेन लाइफ़साइकल विफलताएँ", + "only-rule-node-lifecycle-failures": "केवल रूल नोड लाइफ़साइकल विफलताएँ", + "platform-users": "प्लेटफ़ॉर्म उपयोगकर्ता", + "ram-threshold": "RAM थ्रेशहोल्ड", + "rate-limits": "रेट लिमिट्स", + "rate-limits-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी रेट लिमिट्स पर लागू होगा", + "recipient": "प्राप्तकर्ता", + "recipient-group": "प्राप्तकर्ता समूह", + "recipient-type": { + "affected-tenant-administrators": "प्रभावित Tenant administrators", + "affected-user": "प्रभावित उपयोगकर्ता", + "all-users": "सभी उपयोगकर्ता", + "customer-users": "कस्टमर उपयोगकर्ता", + "system-administrators": "System administrators", + "tenant-administrators": "Tenant administrators", + "user-filters": "यूज़र फ़िल्टर", + "user-list": "यूज़र सूची", + "users-entity-owner": "एंटिटी मालिक के उपयोगकर्ता" + }, + "recipients": "प्राप्तकर्ता", + "notification-recipient": "नोटिफिकेशन प्राप्तकर्ता", + "notification-recipient-required": "नोटिफिकेशन प्राप्तकर्ता आवश्यक है.", + "notification-recipients": "नोटिफिकेशन्स / प्राप्तकर्ता", + "recipients-count": "{ count, plural, =1 {1 प्राप्तकर्ता} other {# प्राप्तकर्ता} }", + "recipients-required": "प्राप्तकर्ता आवश्यक हैं", + "refresh-allow-delivery-method": "अनुमत डिलीवरी मेथड रीफ़्रेश करें", + "request-search": "रिक्वेस्ट खोजें", + "request-status": { + "processing": "प्रोसेसिंग", + "scheduled": "शेड्यूल किया गया", + "sent": "भेजा गया" + }, + "review": "रिव्यू", + "rule": "नियम", + "rule-chain-list-rule-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी रूल चेन्स पर लागू होगा", + "rule-engine-events-trigger-settings": "रूल इंजन इवेंट्स ट्रिगर सेटिंग्स", + "rule-engine-filter": "रूल इंजन फ़िल्टर", + "rule-name": "नियम का नाम", + "rule-name-required": "नाम आवश्यक है", + "rule-disable": "नोटिफिकेशन नियम को अक्षम करें", + "rule-enable": "नोटिफिकेशन नियम सक्षम करें", + "rule-node-filter": "रूल नोड फ़िल्टर", + "rules": "नियम", + "notification-rules": "नोटिफिकेशन्स / नियम", + "scheduler-later": "बाद के लिए शेड्यूल करें", + "search-notification": "नोटिफिकेशन्स खोजें", + "search-recipients": "प्राप्तकर्ता खोजें", + "search-rules": "नियम खोजें", + "search-templates": "टेम्पलेट्स खोजें", + "see-documentation": "डॉक्यूमेंटेशन देखें", + "selected-notifications": "{ count, plural, =1 {1 नोटिफिकेशन} other {# नोटिफिकेशन्स} } चयनित", + "selected-recipients": "{ count, plural, =1 {1 प्राप्तकर्ता} other {# प्राप्तकर्ता} } चयनित", + "selected-requests": "{ count, plural, =1 {1 रिक्वेस्ट} other {# रिक्वेस्ट्स} } चयनित", + "selected-rules": "{ count, plural, =1 {1 नियम} other {# नियम} } चयनित", + "selected-template": "{ count, plural, =1 {1 टेम्पलेट} other {# टेम्पलेट्स} } चयनित", + "send-notification": "नोटिफिकेशन भेजें", + "sent": "भेजा गया", + "setup": "सेटअप करें", + "notification-sent": "नोटिफिकेशन्स / भेजे गए", + "set-entity-from-notification": "डैशबोर्ड स्टेट के लिए एंटिटी को नोटिफिकेशन से सेट करें", + "slack-chanel-type": "Slack चैनल प्रकार", + "slack-chanel-types": { + "direct": "डायरेक्ट संदेश", + "private-channel": "प्राइवेट चैनल", + "public-channel": "पब्लिक चैनल" + }, + "start-from-scratch": "शुरू से शुरू करें", + "status": "स्थिति", + "stop-escalation-alarm-status-become": "एस्केलेशन रोकें जब अलार्म स्टेटस यह हो जाए:", + "storage-threshold": "स्टोरेज थ्रेशहोल्ड", + "subject": "विषय", + "subject-required": "विषय आवश्यक है", + "subject-max-length": "विषय {{ length }} वर्णों से कम या बराबर होना चाहिए", + "template": "टेम्पलेट", + "template-name": "टेम्पलेट नाम", + "template-required": "टेम्पलेट आवश्यक है", + "template-type": { + "alarm": "अलार्म", + "alarm-assignment": "अलार्म असाइनमेंट", + "alarm-comment": "अलार्म टिप्पणी", + "api-usage-limit": "API उपयोग सीमा", + "device-activity": "डिवाइस एक्टिविटी", + "entities-limit": "एंटिटीज़ सीमा", + "entities-limit-increase-request": "एंटिटीज़ सीमा बढ़ाने का अनुरोध", + "entity-action": "एंटिटी एक्शन", + "general": "जनरल", + "rule-engine-lifecycle-event": "रूल इंजन लाइफ़साइकल इवेंट", + "rule-node": "रूल नोड", + "new-platform-version": "नया प्लेटफ़ॉर्म वर्ज़न", + "rate-limits": "रेट लिमिट पार", + "edge-communication-failure": "Edge कम्युनिकेशन फेल्यर", + "edge-connection": "Edge कनेक्शन", + "task-processing-failure": "टास्क प्रोसेसिंग फेल्यर", + "resources-shortage": "रिसोर्सेज़ की कमी" + }, + "templates": "टेम्पलेट्स", + "notification-templates": "नोटिफिकेशन्स / टेम्पलेट्स", + "tenant-profiles-list-rule-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी टेनेंट प्रोफ़ाइल्स पर लागू होगा", + "tenants-list-rule-hint": "यदि फ़ील्ड खाली है, तो ट्रिगर सभी टेनेंट्स पर लागू होगा", + "threshold": "थ्रेशहोल्ड", + "theme-color": "थीम रंग", + "time": "समय", + "track-rule-node-events": "रूल नोड इवेंट्स को ट्रैक करें", + "trigger": { + "alarm": "अलार्म", + "alarm-assignment": "अलार्म असाइनमेंट", + "alarm-comment": "अलार्म टिप्पणी", + "api-usage-limit": "API उपयोग सीमा", + "device-activity": "डिवाइस एक्टिविटी", + "entities-limit": "एंटिटीज़ सीमा", + "entity-action": "एंटिटी एक्शन", + "rule-engine-lifecycle-event": "रूल इंजन लाइफ़साइकल इवेंट", + "new-platform-version": "नया प्लेटफ़ॉर्म वर्ज़न", + "rate-limits": "रेट लिमिट पार", + "edge-connection": "Edge कनेक्शन", + "edge-communication-failure": "Edge कम्युनिकेशन फेल्यर", + "task-processing-failure": "टास्क प्रोसेसिंग फेल्यर", + "resources-shortage": "रिसोर्सेज़ की कमी", + "trigger": "ट्रिगर", + "trigger-required": "ट्रिगर आवश्यक है" + }, + "type": "प्रकार", + "unread": "अपठित", + "updated": "अपडेट किया गया", + "use-deprecated-webhook-connectors": "अप्रचलित Webhook कनेक्टर्स का उपयोग करें", + "use-old-api": "पुराने API का उपयोग करें", + "use-template": "टेम्पलेट का उपयोग करें", + "view-all": "सभी देखें", + "warning": "चेतावनी", + "webhook-url": "Webhook URL", + "webhook-url-required": "Webhook URL आवश्यक है", + "workflow-url": "वर्कफ़्लो URL", + "workflow-url-required": "वर्कफ़्लो URL आवश्यक है", + "channel-name": "चैनल नाम", + "channel-name-required": "चैनल नाम आवश्यक है", + "settings": { + "notification-settings": "नोटिफिकेशन सेटिंग्स", + "reset-all": "सभी सेटिंग्स रीसेट करें", + "reset-all-title": "क्या आप वाकई फ़ॉर्म रीसेट करना चाहते हैं?", + "reset-all-text": "पुष्टि के बाद, सेटिंग्स फ़ॉर्म डिफ़ॉल्ट मानों पर रीसेट हो जाएगा और सेव हो जाएगा.", + "type": "प्रकार", + "enable-all": "सभी सक्षम करें", + "disable-all": "सभी अक्षम करें", + "delivery-not-configured": "डिलीवरी मेथड कॉन्फ़िगर नहीं है" + } + }, + "ota-update": { + "add": "पैकेज जोड़ें", + "assign-firmware": "असाइन किया गया फ़र्मवेयर", + "assign-firmware-required": "असाइन किया गया फ़र्मवेयर आवश्यक है", + "assign-software": "असाइन किया गया सॉफ़्टवेयर", + "assign-software-required": "असाइन किया गया सॉफ़्टवेयर आवश्यक है", + "auto-generate-checksum": "चेकसम स्वतः जनरेट करें", + "checksum": "चेकसम", + "checksum-hint": "यदि चेकसम खाली है, तो इसे स्वतः जनरेट किया जाएगा", + "checksum-algorithm": "चेकसम एल्गोरिदम", + "checksum-copied-message": "पैकेज चेकसम क्लिपबोर्ड पर कॉपी कर दिया गया है", + "change-firmware": "फ़र्मवेयर में बदलाव से { count, plural, =1 {1 डिवाइस} other {# डिवाइस} } अपडेट हो सकता है.", + "change-software": "सॉफ़्टवेयर में बदलाव से { count, plural, =1 {1 डिवाइस} other {# डिवाइस} } अपडेट हो सकता है.", + "change-ota-setting-title": "क्या आप वाकई OTA सेटिंग्स बदलना चाहते हैं?", + "chose-compatible-device-profile": "अपलोड किया गया पैकेज केवल चुने हुए डिवाइस प्रोफ़ाइल वाले डिवाइसेज़ के लिए उपलब्ध होगा.", + "chose-firmware-distributed-device": "वह फ़र्मवेयर चुनें जो डिवाइस पर वितरित किया जाएगा", + "chose-software-distributed-device": "वह सॉफ़्टवेयर चुनें जो डिवाइस पर वितरित किया जाएगा", + "content-type": "कंटेंट प्रकार", + "copy-checksum": "चेकसम कॉपी करें", + "copy-direct-url": "डायरेक्ट URL कॉपी करें", + "copyId": "पैकेज Id कॉपी करें", + "copied": "कॉपी हो गया!", + "delete": "पैकेज हटाएँ", + "delete-ota-update-text": "सावधान रहें, पुष्टि के बाद OTA अपडेट हमेशा के लिए खो जाएगा.", + "delete-ota-update-title": "क्या आप वाकई OTA अपडेट '{{title}}' को हटाना चाहते हैं?", + "delete-ota-updates-text": "सावधान रहें, पुष्टि के बाद सभी चयनित OTA अपडेट्स हटा दिए जाएँगे.", + "delete-ota-updates-title": "क्या आप वाकई { count, plural, =1 {1 OTA अपडेट} other {# OTA अपडेट्स} } को हटाना चाहते हैं?", + "description": "वर्णन", + "direct-url": "डायरेक्ट URL", + "direct-url-copied-message": "पैकेज डायरेक्ट URL क्लिपबोर्ड पर कॉपी कर दिया गया है", + "direct-url-required": "डायरेक्ट URL आवश्यक है", + "download": "पैकेज डाउनलोड करें", + "drop-file": "पैकेज फ़ाइल ड्रॉप करें या अपलोड करने के लिए क्लिक करके फ़ाइल चुनें.", + "drop-package-file-or": "पैकेज फ़ाइल को ड्रैग और ड्रॉप करें या", + "file-name": "फ़ाइल नाम", + "file-size": "फ़ाइल आकार", + "file-size-bytes": "फ़ाइल आकार (बाइट्स में)", + "idCopiedMessage": "पैकेज Id क्लिपबोर्ड पर कॉपी कर दिया गया है", + "no-firmware-matching": "कोई संगत Firmware OTA अपडेट पैकेज '{{entity}}' से मेल खाते नहीं मिले.", + "no-firmware-text": "कोई संगत Firmware OTA अपडेट पैकेज प्रोविज़न नहीं किए गए.", + "no-packages-text": "कोई पैकेज नहीं मिला", + "no-software-matching": "कोई संगत Software OTA अपडेट पैकेज '{{entity}}' से मेल खाते नहीं मिले.", + "no-software-text": "कोई संगत Software OTA अपडेट पैकेज प्रोविज़न नहीं किए गए.", + "ota-update": "OTA अपडेट", + "ota-update-details": "OTA अपडेट विवरण", + "ota-updates": "OTA अपडेट्स", + "package-file": "पैकेज फ़ाइल", + "package-type": "पैकेज प्रकार", + "packages-repository": "पैकेज रिपॉज़िटरी", + "search": "पैकेज खोजें", + "selected-package": "{ count, plural, =1 {1 पैकेज} other {# पैकेज} } चयनित", + "title": "शीर्षक", + "title-required": "शीर्षक आवश्यक है.", + "title-max-length": "शीर्षक 256 वर्णों से कम होना चाहिए", + "types": { + "firmware": "फ़र्मवेयर", + "software": "सॉफ़्टवेयर" + }, + "upload-binary-file": "बाइनरी फ़ाइल अपलोड करें", + "use-external-url": "एक्सटर्नल URL का उपयोग करें", + "version": "वर्ज़न", + "version-required": "वर्ज़न आवश्यक है.", + "version-tag": "वर्ज़न टैग", + "version-tag-hint": "कस्टम टैग आपके डिवाइस द्वारा रिपोर्ट किए गए पैकेज वर्ज़न से मेल खाना चाहिए.", + "version-max-length": "वर्ज़न 256 वर्णों से कम होना चाहिए", + "warning-after-save-no-edit": "एक बार पैकेज अपलोड हो जाने के बाद, आप शीर्षक, वर्ज़न, डिवाइस प्रोफ़ाइल और पैकेज प्रकार को संशोधित नहीं कर पाएँगे." + }, + "position": { + "top": "ऊपर", + "bottom": "नीचे", + "left": "बायाँ", + "right": "दायाँ" + }, + "profile": { + "profile": "प्रोफ़ाइल", + "last-login-time": "अंतिम लॉगिन", + "change-password": "पासवर्ड बदलें", + "current-password": "वर्तमान पासवर्ड", + "copy-jwt-token": "JWT टोकन कॉपी करें", + "jwt-token": "JWT टोकन", + "token-valid-till": "टोकन इस समय तक वैध है", + "tokenCopiedSuccessMessage": "JWT टोकन क्लिपबोर्ड पर कॉपी कर दिया गया है", + "tokenCopiedWarnMessage": "JWT टोकन की अवधि समाप्त हो गई है! कृपया पेज रीफ़्रेश करें." + }, + "profiles": { + "profiles": "प्रोफ़ाइल्स" + }, + "security": { + "security": "सुरक्षा", + "general-settings": "सामान्य सुरक्षा सेटिंग्स", + "access-token": "एक्सेस टोकन", + "access-token-required": "एक्सेस टोकन आवश्यक है", + "clientId": "Client ID", + "clientId-required": "Client ID आवश्यक है", + "username": "यूज़रनेम", + "username-required": "यूज़रनेम आवश्यक है", + "ca-cert": "CA प्रमाणपत्र", + "2fa": { + "2fa": "दो-कारक प्रमाणीकरण", + "2fa-description": "दो-कारक प्रमाणीकरण आपके अकाउंट को अनधिकृत पहुँच से बचाता है. आपको बस लॉगिन करते समय एक सिक्योरिटी कोड दर्ज करना होता है.", + "authenticate-with": "आप इनसे ऑथेंटिकेट कर सकते हैं:", + "disable-2fa-provider-text": "{{name}} को निष्क्रिय करने से आपका अकाउंट कम सुरक्षित हो जाएगा", + "disable-2fa-provider-title": "क्या आप वाकई {{name}} को निष्क्रिय करना चाहते हैं?", + "get-new-code": "नया कोड प्राप्त करें", + "main-2fa-method": "मुख्य दो-कारक प्रमाणीकरण विधि के रूप में उपयोग करें", + "dialog": { + "activation-step-description-email": "अगली बार जब आप लॉगिन करेंगे, तो आपसे वह सुरक्षा कोड दर्ज करने के लिए कहा जाएगा जो आपके ईमेल पते पर भेजा जाएगा.", + "activation-step-description-sms": "अगली बार जब आप लॉगिन करेंगे, तो आपसे वह सुरक्षा कोड दर्ज करने के लिए कहा जाएगा जो आपके फ़ोन नंबर पर भेजा जाएगा.", + "activation-step-description-totp": "अगली बार जब आप लॉगिन करेंगे, तो आपको दो-कारक प्रमाणीकरण कोड प्रदान करना होगा.", + "activation-step-label": "सक्रियकरण", + "backup-code-description": "इन कोड्स को प्रिंट कर लें ताकि जब आपको अपने अकाउंट में लॉगिन करने के लिए उनकी ज़रूरत पड़े, तो वे आपके पास हों. आप प्रत्येक बैकअप कोड का केवल एक बार उपयोग कर सकते हैं.", + "backup-code-warn": "एक बार जब आप इस पेज से बाहर निकल जाते हैं, तो ये कोड दोबारा नहीं दिखाए जा सकते. नीचे दिए गए विकल्पों का उपयोग करके इन्हें सुरक्षित रूप से स्टोर करें.", + "download-txt": "डाउनलोड (txt)", + "email-step-description": "अपने ऑथेंटिकेटर के रूप में उपयोग करने के लिए एक ईमेल दर्ज करें.", + "email-step-label": "ईमेल", + "enable-email-title": "ईमेल ऑथेंटिकेटर सक्षम करें", + "enable-sms-title": "SMS ऑथेंटिकेटर सक्षम करें", + "enable-totp-title": "ऑथेंटिकेटर ऐप सक्षम करें", + "enter-verification-code": "यहाँ 6-अंकों का कोड दर्ज करें", + "get-backup-code-title": "बैकअप कोड प्राप्त करें", + "next": "आगे", + "scan-qr-code": "अपने वेरिफिकेशन ऐप से इस QR कोड को स्कैन करें", + "send-code": "कोड भेजें", + "sms-step-description": "अपने ऑथेंटिकेटर के रूप में उपयोग करने के लिए एक फ़ोन नंबर दर्ज करें.", + "sms-step-label": "फ़ोन नंबर", + "success": "सफलता!", + "totp-step-description-install": "आप Google Authenticator, Authy या Duo जैसी ऐप्स इंस्टॉल कर सकते हैं.", + "totp-step-description-open": "अपने मोबाइल फ़ोन पर ऑथेंटिकेटर ऐप खोलें.", + "totp-step-label": "ऐप प्राप्त करें", + "verification-code": "6-अंकों का कोड", + "verification-code-invalid": "अमान्य वेरिफिकेशन कोड फ़ॉर्मेट", + "verification-code-incorrect": "वेरिफिकेशन कोड गलत है", + "verification-code-many-request": "बहुत अधिक अनुरोध, कृपया वेरिफिकेशन कोड की जाँच करें", + "verification-step-description": "वह 6-अंकों का कोड दर्ज करें जो हमने अभी {{address}} पर भेजा है", + "verification-step-label": "वेरिफिकेशन" + }, + "provider": { + "email": "ईमेल", + "email-description": "ऑथेंटिकेट करने के लिए अपने ईमेल पते पर भेजे गए सिक्योरिटी कोड का उपयोग करें.", + "email-hint": "ऑथेंटिकेशन कोड्स ईमेल के माध्यम से {{ info }} पर भेजे जाते हैं", + "sms": "SMS", + "sms-description": "ऑथेंटिकेट करने के लिए अपने फ़ोन का उपयोग करें. जब आप लॉगिन करेंगे तो हम आपको SMS संदेश के माध्यम से सिक्योरिटी कोड भेजेंगे.", + "sms-hint": "ऑथेंटिकेशन कोड्स टेक्स्ट संदेश द्वारा {{ info }} पर भेजे जाते हैं", + "totp": "ऑथेंटिकेटर ऐप", + "totp-description": "अपने फ़ोन पर Google Authenticator, Authy या Duo जैसी ऐप्स का उपयोग करके ऑथेंटिकेट करें. यह लॉगिन के लिए सिक्योरिटी कोड जनरेट करेगी.", + "totp-hint": "आपके अकाउंट के लिए ऑथेंटिकेटर ऐप सेटअप किया गया है", + "backup_code": "बैकअप कोड", + "backup-code-description": "ये प्रिंट करने योग्य एक-बार उपयोग होने वाले पासकोड्स आपको तब साइन-इन करने देते हैं जब आप अपने फ़ोन से दूर हों, जैसे यात्रा करते समय.", + "backup-code-hint": "{{ info }} सिंगल-यूज़ कोड इस समय सक्रिय हैं" + } + }, + "password-requirement": { + "at-least": "कम से कम:", + "character": "{ count, plural, =1 {1 अक्षर} other {# अक्षर} }", + "digit": "{ count, plural, =1 {1 अंक} other {# अंक} }", + "password-tooltip-min-length": "कम से कम {{minimumLength}} अक्षर लंबा", + "password-tooltip-max-length": "अधिकतम {{maximumLength}} अक्षर लंबा", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} बड़े अक्षर", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} छोटे अक्षर", + "password-tooltip-digit": "{{minimumDigits}} संख्या", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} विशेष वर्ण", + "incorrect-password-try-again": "गलत पासवर्ड. दोबारा कोशिश करें", + "lowercase-letter": "{ count, plural, =1 {1 छोटे अक्षर} other {# छोटे अक्षर} }", + "new-passwords-not-match": "नए पासवर्ड आपस में मेल नहीं खाते", + "password-should-not-contain-spaces": "आपके पासवर्ड में स्पेस नहीं होना चाहिए", + "password-not-meet-requirements": "पासवर्ड आवश्यकताओं को पूरा नहीं करता", + "password-requirements": "पासवर्ड आवश्यकताएँ", + "password-should-difference": "नया पासवर्ड वर्तमान पासवर्ड से अलग होना चाहिए", + "special-character": "{ count, plural, =1 {1 विशेष वर्ण} other {# विशेष वर्ण} }", + "uppercase-letter": "{ count, plural, =1 {1 बड़े अक्षर} other {# बड़े अक्षर} }", + "at-most": "अधिकतम:" + } + }, + "relation": { + "relations": "रिलेशन", + "direction": "दिशा", + "clear-relation-type": "रिलेशन प्रकार साफ़ करें", + "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": "रिलेशन प्रकार आवश्यक है.", + "relation-type-max-length": "रिलेशन प्रकार 256 वर्णों से कम होना चाहिए", + "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": "रिलेशन फ़िल्टर हटाएँ", + "remove-filter": "फ़िल्टर हटाएँ", + "add-relation-filter": "रिलेशन फ़िल्टर जोड़ें", + "any-relation": "कोई भी रिलेशन", + "relation-filters": "रिलेशन फ़िल्टर्स", + "relation-filter": "रिलेशन फ़िल्टर", + "additional-info": "अतिरिक्त जानकारी (JSON)", + "invalid-additional-info": "अतिरिक्त जानकारी JSON को पार्स करने में असमर्थ.", + "no-relations-text": "कोई रिलेशन नहीं मिला", + "not": "नहीं", + "copy-type": "कॉपी प्रकार" + }, + "resource": { + "add": "संसाधन जोड़ें", + "all-types": "सभी", + "copyId": "संसाधन Id कॉपी करें", + "delete": "संसाधन हटाएँ", + "delete-resource-text": "सावधान रहें, पुष्टि के बाद संसाधन हमेशा के लिए खो जाएगा.", + "delete-resource-title": "क्या आप वाकई संसाधन '{{resourceTitle}}' को हटाना चाहते हैं?", + "delete-resources-action-title": "हटाएँ { count, plural, =1 {1 संसाधन} other {# संसाधन} }", + "delete-resources-text": "कृपया ध्यान दें कि चयनित संसाधन, भले ही उनका उपयोग डिवाइस प्रोफ़ाइल्स में हो, हटा दिए जाएँगे.", + "delete-resources-title": "क्या आप वाकई { count, plural, =1 {1 संसाधन} other {# संसाधन} } को हटाना चाहते हैं?", + "download": "संसाधन डाउनलोड करें", + "drop-file": "संसाधन फ़ाइल ड्रॉप करें या अपलोड करने के लिए क्लिक करके फ़ाइल चुनें.", + "drop-resource-file-or": "संसाधन फ़ाइल को ड्रैग और ड्रॉप करें या", + "empty": "संसाधन खाली है", + "file-name": "फ़ाइल नाम", + "idCopiedMessage": "संसाधन Id क्लिपबोर्ड पर कॉपी कर दिया गया है", + "no-resource-matching": "'{{widgetsBundle}}' से मेल खाता कोई संसाधन नहीं मिला.", + "no-resource-text": "कोई संसाधन नहीं मिला", + "open-widgets-bundle": "विजेट बंडल खोलें", + "resource": "संसाधन", + "resource-file": "संसाधन फ़ाइल", + "resource-files": "संसाधन फ़ाइलें", + "resource-library-details": "संसाधन विवरण", + "resource-type": "संसाधन प्रकार", + "resources-library": "संसाधन लाइब्रेरी", + "search": "संसाधन खोजें", + "selected-resources": "{ count, plural, =1 {1 संसाधन} other {# संसाधन} } चयनित", + "system": "सिस्टम", + "title": "शीर्षक", + "title-required": "शीर्षक आवश्यक है.", + "title-max-length": "शीर्षक 256 वर्णों से कम होना चाहिए", + "type": { + "jks": "JKS", + "js-module": "JS मॉड्यूल", + "lwm2m-model": "LWM2M मॉडल", + "pkcs-12": "PKCS #12", + "general": "सामान्य" + }, + "resource-sub-type": "सब-टाइप", + "sub-type": { + "image": "इमेज", + "scada-symbol": "SCADA सिंबल", + "extension": "एक्सटेंशन", + "module": "मॉड्यूल" + }, + "resource-is-in-use": "संसाधन अन्य एंटिटीज़ द्वारा उपयोग में है", + "resources-are-in-use": "संसाधन अन्य एंटिटीज़ द्वारा उपयोग में हैं", + "resource-is-in-use-text": "संसाधन '{{title}}' को नहीं हटाया गया क्योंकि इसका उपयोग निम्नलिखित एंटिटीज़ द्वारा किया जा रहा है:", + "resources-are-in-use-text": "सभी संसाधनों को नहीं हटाया गया क्योंकि उनका उपयोग अन्य एंटिटीज़ द्वारा किया जा रहा है.
    आप संदर्भित एंटिटीज़ को संबंधित संसाधन पंक्ति में References बटन पर क्लिक करके देख सकते हैं.
    यदि आप फिर भी इन संसाधनों को हटाना चाहते हैं, तो उन्हें नीचे दी गई तालिका में चुनें और चयनित हटाएँ बटन पर क्लिक करें.", + "delete-resource-in-use-text": "यदि आप फिर भी संसाधन को हटाना चाहते हैं, तो फिर भी हटाएँ बटन पर क्लिक करें." + }, + "javascript": { + "add": "JavaScript संसाधन जोड़ें", + "delete": "JavaScript संसाधन हटाएँ", + "delete-javascript-resource-text": "सावधान रहें, पुष्टि के बाद JavaScript संसाधन हमेशा के लिए खो जाएगा.", + "delete-javascript-resource-title": "क्या आप वाकई JavaScript संसाधन '{{resourceTitle}}' को हटाना चाहते हैं?", + "delete-javascript-resources-action-title": "JavaScript { count, plural, =1 {1 संसाधन} other {# संसाधन} } हटाएँ", + "delete-javascript-resources-text": "कृपया ध्यान दें कि चयनित JavaScript संसाधन, भले ही उनका उपयोग JavaScript फ़ंक्शन्स में हो, हटा दिए जाएँगे.", + "delete-javascript-resources-title": "क्या आप वाकई JavaScript { count, plural, =1 {1 संसाधन} other {# संसाधन} } हटाना चाहते हैं?", + "delete-javascript-resource-in-use-text": "यदि आप फिर भी JavaScript संसाधन को हटाना चाहते हैं, तो फिर भी हटाएँ बटन पर क्लिक करें.", + "download": "JavaScript संसाधन डाउनलोड करें", + "upload-from-file": "फ़ाइल से JavaScript अपलोड करें", + "resource-file": "JavaScript संसाधन फ़ाइल", + "drop-file": "JavaScript फ़ाइल ड्रॉप करें या अपलोड करने के लिए क्लिक करके फ़ाइल चुनें.", + "drop-resource-file-or": "JavaScript फ़ाइल को ड्रैग और ड्रॉप करें या", + "javascript-library": "JavaScript लाइब्रेरी", + "javascript-type": "JavaScript प्रकार", + "javascript-resource-details": "JavaScript संसाधन विवरण", + "javascript-resource-is-in-use": "JavaScript संसाधन अन्य एंटिटीज़ द्वारा उपयोग में है", + "javascript-resources-are-in-use": "JavaScript संसाधन अन्य एंटिटीज़ द्वारा उपयोग में हैं", + "javascript-resource-is-in-use-text": "JavaScript संसाधन '{{title}}' को नहीं हटाया गया क्योंकि इसका उपयोग निम्नलिखित एंटिटीज़ द्वारा किया जा रहा है:", + "javascript-resources-are-in-use-text": "सभी JavaScript संसाधनों को नहीं हटाया गया क्योंकि उनका उपयोग अन्य एंटिटीज़ द्वारा किया जा रहा है.
    आप संदर्भित एंटिटीज़ को संबंधित संसाधन पंक्ति में References बटन पर क्लिक करके देख सकते हैं.
    यदि आप फिर भी इन JavaScript संसाधनों को हटाना चाहते हैं, तो उन्हें नीचे दी गई तालिका में चुनें और चयनित हटाएँ बटन पर क्लिक करें.", + "search": "JavaScript संसाधन खोजें", + "selected-javascript-resources": "{ count, plural, =1 {1 JavaScript संसाधन} other {# JavaScript संसाधन} } चयनित", + "no-javascript-resource-text": "कोई JavaScript संसाधन नहीं मिला", + "all-types": "सभी", + "module-script": "मॉड्यूल स्क्रिप्ट" + }, + "rpc": { + "error": { + "target-device-is-not-set": "टार्गेट डिवाइस सेट नहीं है!", + "invalid-target-entity": "RPC कमांड्स {{entityType}} एंटिटी द्वारा समर्थित नहीं हैं.", + "failed-to-resolve-target-device": "टार्गेट डिवाइस रिसॉल्व करने में विफल!", + "request-timeout": "रिक्वेस्ट टाइमआउट", + "rpc-http-error": "त्रुटि: {{status}} - {{statusText}}" + } + }, + "rulechain": { + "rulechain": "रूल चेन", + "rulechain-events": "रूल चेन इवेंट्स", + "rulechains": "रूल चेन्स", + "root": "रूट", + "delete": "रूल चेन हटाएँ", + "name": "नाम", + "name-required": "नाम आवश्यक है.", + "name-max-length": "नाम 256 वर्णों से कम होना चाहिए", + "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": "डिबग मोड", + "search": "रूल चेन्स खोजें", + "selected-rulechains": "{ count, plural, =1 {1 रूल चेन} other {# रूल चेन्स} } चयनित", + "open-rulechain": "रूल चेन खोलें", + "edge-template-root": "टेम्पलेट रूट", + "assign-to-edge": "Edge को असाइन करें", + "edge-rulechain": "Edge रूल चेन", + "unassign-rulechain-from-edge-text": "पुष्टि के बाद रूल चेन अनअसाइन हो जाएगी और Edge द्वारा एक्सेस नहीं की जा सकेगी।", + "unassign-rulechains-from-edge-title": "क्या आप वाकई { count, plural, =1 {1 रूल चेन} other {# रूल चेन्स} } को अनअसाइन करना चाहते हैं?", + "unassign-rulechains-from-edge-text": "पुष्टि के बाद सभी चयनित रूल चेन्स अनअसाइन हो जाएँगी और Edge द्वारा एक्सेस नहीं की जा सकेंगी।", + "assign-rulechain-to-edge-title": "Edge को रूल चेन(्स) असाइन करें", + "assign-rulechain-to-edge-text": "कृपया Edge को असाइन करने के लिए रूल चेन्स चुनें", + "set-edge-template-root-rulechain": "रूल चेन को Edge टेम्पलेट रूट बनाएं", + "set-edge-template-root-rulechain-title": "क्या आप वाकई रूल चेन '{{ruleChainName}}' को Edge टेम्पलेट रूट बनाना चाहते हैं?", + "set-edge-template-root-rulechain-text": "पुष्टि के बाद रूल चेन Edge टेम्पलेट रूट बन जाएगी और नए बनाए गए Edge के लिए रूट रूल चेन होगी।", + "invalid-rulechain-type-error": "रूल चेन इम्पोर्ट नहीं की जा सकी: अमान्य रूल चेन प्रकार। अपेक्षित प्रकार {{expectedRuleChainType}} है।", + "set-auto-assign-to-edge": "बनाते समय रूल चेन को Edge(s) पर असाइन करें", + "set-auto-assign-to-edge-title": "क्या आप वाकई बनाते समय Edge रूल चेन '{{ruleChainName}}' को Edge(s) पर असाइन करना चाहते हैं?", + "set-auto-assign-to-edge-text": "पुष्टि के बाद Edge रूल चेन बनाते समय Edge(s) पर अपने-आप असाइन हो जाएगी।", + "unset-auto-assign-to-edge": "बनाते समय रूल चेन को Edge(s) पर असाइन न करें", + "unset-auto-assign-to-edge-title": "क्या आप वाकई बनाते समय Edge रूल चेन '{{ruleChainName}}' को Edge(s) पर असाइन नहीं करना चाहते हैं?", + "unset-auto-assign-to-edge-text": "पुष्टि के बाद Edge रूल चेन बनाते समय Edge(s) पर अपने-आप असाइन नहीं होगी।", + "unassign-rulechain-title": "क्या आप वाकई रूल चेन '{{ruleChainName}}' को अनअसाइन करना चाहते हैं?", + "unassign-rulechains": "रूल चेन्स अनअसाइन करें" + }, + "rulenode": { + "rule-node-events": "रूल नोड इवेंट्स", + "details": "विवरण", + "events": "इवेंट्स", + "search": "नोड्स खोजें", + "open-node-library": "नोड लाइब्रेरी खोलें", + "close-node-library": "नोड लाइब्रेरी बंद करें", + "add": "रूल नोड जोड़ें", + "name": "नाम", + "name-required": "नाम आवश्यक है.", + "name-max-length": "नाम 256 वर्णों से कम होना चाहिए", + "type": "प्रकार", + "rule-node-description": "रूल नोड विवरण", + "delete": "रूल नोड हटाएँ", + "select-all-objects": "सभी नोड्स और कनेक्शन्स चुनें", + "deselect-all-objects": "सभी नोड्स और कनेक्शन्स को अनचेक करें", + "delete-selected-objects": "चयनित नोड्स और कनेक्शन्स हटाएँ", + "delete-selected": "चयनित हटाएँ", + "create-nested-rulechain": "नेस्टेड रूल चेन बनाएँ", + "select-all": "सभी चुनें", + "copy-selected": "चयनित कॉपी करें", + "deselect-all": "सभी को अनचेक करें", + "rulenode-details": "रूल नोड विवरण", + "debug-mode": "डिबग मोड", + "singleton": "सिंगलटन", + "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-flow": "फ़्लो", + "type-flow-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": "स्क्रिप्ट फ़ंक्शन टेस्ट करें", + "script-lang-java-script": "JavaScript", + "script-lang-tbel": "TBEL", + "message": "मैसेज", + "message-type": "मैसेज प्रकार", + "select-message-type": "मैसेज प्रकार चुनें", + "message-type-required": "मैसेज प्रकार आवश्यक है", + "metadata": "मेटाडेटा", + "metadata-required": "मेटाडेटा एंट्रीज़ खाली नहीं हो सकतीं.", + "output": "आउटपुट", + "test": "टेस्ट", + "help": "सहायता", + "reset-debug-settings": "सभी नोड्स में डिबग सेटिंग्स रीसेट करें", + "test-with-this-message": "{{test}} इस मैसेज के साथ", + "queue-hint": "मैसेज को दूसरी क्व्यू पर फॉरवर्ड करने के लिए क्व्यू चुनें. 'Main' क्व्यू डिफ़ॉल्ट रूप से उपयोग की जाती है.", + "queue-singleton-hint": "मल्टी-इंस्टेंस एनवायरनमेंट्स में मैसेज फॉरवर्डिंग के लिए क्व्यू चुनें. 'Main' क्व्यू डिफ़ॉल्ट रूप से उपयोग की जाती है." + }, + "rule-node-config": { + "id": "Id", + "additional-info": "अतिरिक्त जानकारी", + "advanced-settings": "एडवांस्ड सेटिंग्स", + "create-entity-if-not-exists": "यदि एंटिटी मौजूद नहीं है तो नई एंटिटी बनाएँ", + "create-entity-if-not-exists-hint": "यदि सक्षम है, तो दिए गए पैरामीटर्स के साथ नई एंटिटी बनाई जाएगी, यदि वह पहले से मौजूद नहीं है। मौजूद एंटिटीज़ को रिलेशन के लिए जैसे हैं वैसा ही उपयोग किया जाएगा.", + "select-device-connectivity-event": "डिवाइस कनेक्टिविटी इवेंट चुनें", + "entity-name-pattern": "नाम पैटर्न", + "device-name-pattern": "डिवाइस नाम", + "asset-name-pattern": "एसेट नाम", + "entity-view-name-pattern": "एंटिटी व्यू नाम", + "customer-title-pattern": "कस्टमर शीर्षक", + "dashboard-name-pattern": "डैशबोर्ड शीर्षक", + "user-name-pattern": "उपयोगकर्ता ईमेल", + "edge-name-pattern": "Edge नाम", + "entity-name-pattern-required": "नाम पैटर्न आवश्यक है", + "entity-name-pattern-hint": "नाम पैटर्न फ़ील्ड टेम्पलेटाइजेशन सपोर्ट करता है। वैल्यू को मैसेज से निकालने के लिए $[messageKey] और मेटाडेटा से निकालने के लिए ${metadataKey} का उपयोग करें.", + "copy-message-type": "मैसेज प्रकार कॉपी करें", + "entity-type-pattern": "टाइप पैटर्न", + "entity-type-pattern-required": "टाइप पैटर्न आवश्यक है", + "message-type-value": "मैसेज टाइप वैल्यू", + "message-type-value-required": "मैसेज टाइप वैल्यू आवश्यक है", + "message-type-value-max-length": "मैसेज टाइप वैल्यू 256 वर्णों से कम होनी चाहिए", + "output-message-type": "आउटपुट मैसेज प्रकार", + "entity-cache-expiration": "एंटिटीज़ कैश एक्सपिरेशन समय (sec)", + "entity-cache-expiration-hint": "पाई गई एंटिटी रिकॉर्ड्स को स्टोर करने के लिए अधिकतम समय अंतराल निर्दिष्ट करता है। 0 का मतलब है कि रिकॉर्ड्स कभी एक्सपायर नहीं होंगे.", + "entity-cache-expiration-required": "एंटिटीज़ कैश एक्सपिरेशन समय आवश्यक है.", + "entity-cache-expiration-range": "एंटिटीज़ कैश एक्सपिरेशन समय 0 से अधिक या बराबर होना चाहिए.", + "customer-name-pattern": "कस्टमर शीर्षक", + "customer-name-pattern-required": "कस्टमर शीर्षक आवश्यक है", + "customer-name-pattern-hint": "मैसेज से वैल्यू निकालने के लिए $[messageKey] और मेटाडेटा से वैल्यू निकालने के लिए ${metadataKey} का उपयोग करें.", + "create-customer-if-not-exists": "यदि कस्टमर मौजूद नहीं है तो नया कस्टमर बनाएँ", + "unassign-from-customer": "यदि ओरिजिनेटर डैशबोर्ड है तो विशेष कस्टमर से अनअसाइन करें", + "unassign-from-customer-tooltip": "एक समय में कई कस्टमर्स को असाइन किए जाने वाले केवल डैशबोर्ड्स ही होते हैं। \nयदि मैसेज ओरिजिनेटर डैशबोर्ड है, तो आपको जिस कस्टमर से अनअसाइन करना है, उसका शीर्षक स्पष्ट रूप से बताना होगा.", + "customer-cache-expiration": "कस्टमर्स कैश एक्सपिरेशन समय (sec)", + "customer-cache-expiration-hint": "पाए गए कस्टमर रिकॉर्ड्स को स्टोर करने के लिए अधिकतम समय अंतराल निर्दिष्ट करता है। 0 का मतलब है कि रिकॉर्ड्स कभी एक्सपायर नहीं होंगे.", + "customer-cache-expiration-required": "कस्टमर्स कैश एक्सपिरेशन समय आवश्यक है.", + "customer-cache-expiration-range": "कस्टमर्स कैश एक्सपिरेशन समय 0 से अधिक या बराबर होना चाहिए.", + "interval-start": "इंटरवल शुरू", + "interval-end": "इंटरवल समाप्त", + "time-unit": "समय इकाई", + "fetch-mode": "फेच मोड", + "order-by-timestamp": "टाइमस्टैम्प के अनुसार क्रमबद्ध करें", + "limit": "सीमा", + "limit-hint": "न्यूनतम सीमा 2 और अधिकतम 1000 है। यदि आप केवल एक एंट्री फेच करना चाहते हैं, तो फेच मोड में 'First' या 'Last' चुनें.", + "limit-required": "सीमा आवश्यक है.", + "limit-range": "सीमा 2 से 1000 के बीच होनी चाहिए.", + "time-unit-milliseconds": "मिलीसेकंड", + "time-unit-seconds": "सेकंड", + "time-unit-minutes": "मिनट", + "time-unit-hours": "घंटे", + "time-unit-days": "दिन", + "time-value-range": "अनुमत सीमा 1 से 2147483647 तक है.", + "start-interval-value-required": "इंटरवल शुरू आवश्यक है.", + "end-interval-value-required": "इंटरवल समाप्त आवश्यक है.", + "filter": "फ़िल्टर", + "switch": "स्विच", + "math-templatization-tooltip": "यह फ़ील्ड टेम्पलेटाइजेशन सपोर्ट करता है. मैसेज से वैल्यू निकालने के लिए $[messageKey] और मेटाडेटा से वैल्यू निकालने के लिए ${metadataKey} का उपयोग करें.", + "add-message-type": "मैसेज प्रकार जोड़ें", + "select-message-types-required": "कम से कम एक मैसेज प्रकार चयनित होना चाहिए.", + "select-message-types": "मैसेज प्रकार चुनें", + "no-message-types-found": "कोई मैसेज प्रकार नहीं मिला", + "no-message-type-matching": "'{{messageType}}' नहीं मिला.", + "create-new-message-type": "नया बनाएँ.", + "message-types-required": "मैसेज प्रकार आवश्यक हैं.", + "client-attributes": "क्लाइंट विशेषताएँ", + "shared-attributes": "शेयर्ड विशेषताएँ", + "server-attributes": "सर्वर विशेषताएँ", + "attributes-keys": "विशेषताओं की कुंजियाँ", + "attributes-keys-required": "विशेषताओं की कुंजियाँ आवश्यक हैं", + "attributes-scope": "विशेषताओं का स्कोप", + "attributes-scope-value": "विशेषताओं के स्कोप की वैल्यू", + "attributes-scope-value-copy": "विशेषताओं के स्कोप की वैल्यू कॉपी करें", + "attributes-scope-hint": "प्रति मैसेज विशेषता स्कोप को डायनामिक रूप से सेट करने के लिए 'scope' मेटाडेटा कुंजी का उपयोग करें. यदि दिया गया है, तो यह कॉन्फ़िगरेशन में सेट स्कोप को ओवरराइड करेगा.", + "notify-device": "डिवाइस को ज़बरदस्ती नोटिफिकेशन भेजें", + "send-attributes-updated-notification": "अपडेटेड विशेषताओं की अधिसूचना भेजें", + "send-attributes-updated-notification-hint": "अपडेटेड विशेषताओं के बारे में नोटिफिकेशन को अलग मैसेज के रूप में रूल इंजन क्व्यू में भेजें.", + "send-attributes-deleted-notification": "डिलीट की गई विशेषताओं की अधिसूचना भेजें", + "send-attributes-deleted-notification-hint": "डिलीट की गई विशेषताओं के बारे में नोटिफिकेशन को अलग मैसेज के रूप में रूल इंजन क्व्यू में भेजें.", + "update-attributes-only-on-value-change": "केवल तब विशेषताएँ सेव करें जब वैल्यू बदलती है", + "update-attributes-only-on-value-change-hint": "हर इनकमिंग मैसेज पर विशेषताओं को अपडेट करता है, चाहे उनकी वैल्यू बदली हो या नहीं. इससे API उपयोग बढ़ता है और प्रदर्शन कम होता है.", + "update-attributes-only-on-value-change-hint-enabled": "केवल तब विशेषताओं को अपडेट करता है जब उनकी वैल्यू बदलती है. यदि वैल्यू नहीं बदली है, तो न तो विशेषता टाइमस्टैम्प अपडेट होगा और न ही विशेषता परिवर्तन नोटिफिकेशन भेजा जाएगा.", + "fetch-credentials-to-metadata": "क्रेडेंशियल्स को मेटाडेटा में फेच करें", + "notify-device-on-update-hint": "यदि सक्षम है, तो शेयर्ड विशेषताओं के अपडेट के बारे में डिवाइस को ज़बरदस्ती नोटिफिकेशन भेजें. यदि अक्षम है, तो नोटिफिकेशन व्यवहार इनकमिंग मैसेज मेटाडेटा के 'notifyDevice' पैरामीटर से नियंत्रित होगा. नोटिफिकेशन बंद करने के लिए मैसेज मेटाडेटा में 'notifyDevice' पैरामीटर 'false' पर सेट होना चाहिए. किसी भी अन्य स्थिति में डिवाइस को नोटिफिकेशन भेजा जाएगा.", + "notify-device-on-delete-hint": "यदि सक्षम है, तो शेयर्ड विशेषताओं के हटाए जाने के बारे में डिवाइस को ज़बरदस्ती नोटिफिकेशन भेजें. यदि अक्षम है, तो नोटिफिकेशन व्यवहार इनकमिंग मैसेज मेटाडेटा के 'notifyDevice' पैरामीटर से नियंत्रित होगा. नोटिफिकेशन चालू करने के लिए मैसेज मेटाडेटा में 'notifyDevice' पैरामीटर 'true' पर सेट होना चाहिए. किसी भी अन्य स्थिति में डिवाइस को नोटिफिकेशन नहीं भेजा जाएगा.", + "latest-timeseries": "नवीनतम टाइम सीरीज़ डेटा कीज़", + "timeseries-keys": "टाइम सीरीज़ कीज़", + "timeseries-keys-required": "कम से कम एक टाइम सीरीज़ की चयनित होनी चाहिए.", + "add-timeseries-key": "टाइम सीरीज़ की जोड़ें", + "add-message-field": "मैसेज फ़ील्ड जोड़ें", + "relation-search-parameters": "रिलेशन सर्च पैरामीटर्स", + "relation-parameters": "रिलेशन पैरामीटर्स", + "add-metadata-field": "मेटाडेटा फ़ील्ड जोड़ें", + "data-keys": "मैसेज फ़ील्ड नाम", + "copy-from": "से कॉपी करें", + "data-to-metadata": "डेटा से मेटाडेटा", + "metadata-to-data": "मेटाडेटा से डेटा", + "use-regular-expression-hint": "कीज़ को पैटर्न के अनुसार कॉपी करने के लिए रेगुलर एक्सप्रेशन का उपयोग करें.\n\nटिप्स और ट्रिक्स:\nफ़ील्ड नाम इनपुट पूरा करने के लिए 'Enter' दबाएँ.\nफ़ील्ड नाम हटाने के लिए 'Backspace' दबाएँ. कई फ़ील्ड नाम सपोर्टेड हैं.", + "interval": "इंटरवल", + "interval-required": "इंटरवल आवश्यक है", + "interval-hint": "डीडुप्लिकेशन इंटरवल (सेकंड में).", + "interval-min-error": "न्यूनतम अनुमत वैल्यू 1 है", + "max-pending-msgs": "अधिकतम पेंडिंग मैसेजेस", + "max-pending-msgs-hint": "प्रत्येक यूनिक डीडुप्लिकेशन Id के लिए मेमोरी में स्टोर किए जाने वाले मैसेजेस की अधिकतम संख्या.", + "max-pending-msgs-required": "अधिकतम पेंडिंग मैसेजेस आवश्यक हैं", + "max-pending-msgs-max-error": "अधिकतम अनुमत वैल्यू 1000 है", + "max-pending-msgs-min-error": "न्यूनतम अनुमत वैल्यू 1 है", + "max-retries": "अधिकतम रीट्राइज़", + "max-retries-required": "अधिकतम रीट्राइज़ आवश्यक हैं", + "max-retries-hint": "डीडुप्लिकेट किए गए मैसेजेस को क्व्यू में पुश करने के लिए अधिकतम रीट्राइज़ की संख्या. रीट्राइज़ के बीच 10 सेकंड की देरी उपयोग की जाती है", + "max-retries-max-error": "अधिकतम अनुमत वैल्यू 100 है", + "max-retries-min-error": "न्यूनतम अनुमत वैल्यू 0 है", + "strategy": "रणनीति", + "strategy-required": "रणनीति आवश्यक है", + "strategy-all-hint": "डीडुप्लिकेशन अवधि के दौरान आए सभी मैसेजेस को एक सिंगल JSON ऐरे मैसेज के रूप में रिटर्न करें. जहाँ प्रत्येक एलिमेंट msg और metadata इनर प्रॉपर्टीज़ वाला ऑब्जेक्ट होता है.", + "strategy-first-hint": "डीडुप्लिकेशन अवधि के दौरान आया पहला मैसेज रिटर्न करें.", + "strategy-last-hint": "डीडुप्लिकेशन अवधि के दौरान आया आखिरी मैसेज रिटर्न करें.", + "first": "पहला", + "last": "आखिरी", + "all": "सभी", + "output-msg-type-hint": "डीडुप्लिकेशन परिणाम का मैसेज प्रकार.", + "queue-name-hint": "वह क्व्यू नाम जहाँ डीडुप्लिकेशन परिणाम पब्लिश किया जाएगा.", + "keys": "कुंजियाँ", + "keys-required": "कुंजियाँ आवश्यक हैं", + "rename-keys-in": "इन में कुंजियों का नाम बदलें", + "data": "डेटा", + "message": "मैसेज", + "metadata": "मेटाडेटा", + "current-key-name": "मौजूदा कुंजी नाम", + "key-name-required": "कुंजी नाम आवश्यक है", + "new-key-name": "नई कुंजी का नाम", + "new-key-name-required": "नई कुंजी का नाम आवश्यक है", + "metadata-keys": "मेटाडेटा फ़ील्ड नाम", + "json-path-expression": "JSON पाथ एक्सप्रेशन", + "json-path-expression-required": "JSON पाथ एक्सप्रेशन आवश्यक है", + "json-path-expression-hint": "JSONPath, JSON स्ट्रक्चर में किसी एलिमेंट या एलिमेंट्स के सेट तक पाथ को स्पेसिफाई करता है. '$' रूट ऑब्जेक्ट या ऐरे को दर्शाता है.", + "relations-query": "रिलेशन्स क्वेरी", + "device-relations-query": "डिवाइस रिलेशन्स क्वेरी", + "max-relation-level": "अधिकतम रिलेशन लेवल", + "max-relation-level-error": "वैल्यू 0 से अधिक या अनिर्दिष्ट होनी चाहिए.", + "max-relation-level-invalid": "वैल्यू पूर्णांक होनी चाहिए.", + "relation-type": "रिलेशन टाइप", + "relation-type-pattern": "रिलेशन टाइप पैटर्न", + "relation-type-pattern-required": "रिलेशन टाइप पैटर्न आवश्यक है", + "relation-types-list": "प्रोपेगेट करने के लिए रिलेशन टाइप्स", + "relation-types-list-hint": "यदि 'रिलेशन टाइप्स प्रोपेगेट करें' चयनित नहीं हैं, तो अलार्म्स को रिलेशन टाइप द्वारा फ़िल्टर किए बिना प्रोपेगेट किया जाएगा.", + "unlimited-level": "अनलिमिटेड लेवल", + "latest-telemetry": "नवीनतम टेलीमेट्री", + "add-telemetry-key": "टेलीमेट्री की जोड़ें", + "delete-from": "से डिलीट करें", + "use-regular-expression-delete-hint": "कीज़ को पैटर्न के अनुसार डिलीट करने के लिए रेगुलर एक्सप्रेशन का उपयोग करें.\n\nटिप्स और ट्रिक्स:\nफ़ील्ड नाम इनपुट पूरा करने के लिए 'Enter' दबाएँ.\nफ़ील्ड नाम डिलीट करने के लिए 'Backspace' दबाएँ.\nएक से अधिक फ़ील्ड नाम सपोर्टेड हैं.", + "fetch-into": "में फेच करें", + "attr-mapping": "विशेषताओं की मैपिंग:", + "source-attribute": "सोर्स एट्रिब्यूट की", + "source-attribute-required": "सोर्स एट्रिब्यूट की आवश्यक है.", + "source-telemetry": "सोर्स टेलीमेट्री की", + "source-telemetry-required": "सोर्स टेलीमेट्री की आवश्यक है.", + "target-key": "टारगेट की", + "target-key-required": "टारगेट की आवश्यक है.", + "attr-mapping-required": "कम से कम एक मैपिंग एंट्री निर्धारित होनी चाहिए.", + "fields-mapping": "फ़ील्ड्स मैपिंग", + "fields-mapping-hint": "यदि मैसेज फ़ील्ड को $entityId पर सेट किया जाता है, तो मैसेज ओरिजिनेटर की Id निर्दिष्ट टेबल कॉलम में सेव की जाएगी.", + "relations-query-config-direction-suffix": "ओरिजिनेटर", + "profile-name": "प्रोफ़ाइल नाम", + "fetch-circle-parameter-info-from-metadata-hint": "मेटाडेटा फ़ील्ड '{{perimeterKeyName}}' को निम्न फ़ॉर्मेट में परिभाषित किया जाना चाहिए: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}", + "fetch-poligon-parameter-info-from-metadata-hint": "मेटाडेटा फ़ील्ड '{{perimeterKeyName}}' को निम्न फ़ॉर्मेट में परिभाषित किया जाना चाहिए: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]", + "short-templatization-tooltip": "मैसेज से वैल्यू निकालने के लिए $[messageKey] और मेटाडेटा से वैल्यू निकालने के लिए ${metadataKey} का उपयोग करें.", + "fields-mapping-required": "कम से कम एक फ़ील्ड मैपिंग निर्धारित होनी चाहिए.", + "at-least-one-field-required": "कम से कम एक इनपुट फ़ील्ड के लिए वैल्यू प्रदान की जानी चाहिए.", + "originator-fields-sv-map-hint": "टारगेट की फ़ील्ड्स टेम्पलेटाइजेशन सपोर्ट करती हैं. मैसेज से वैल्यू निकालने के लिए $[messageKey] और मेटाडेटा से वैल्यू निकालने के लिए ${metadataKey} का उपयोग करें.", + "sv-map-hint": "केवल टारगेट की फ़ील्ड्स टेम्पलेटाइजेशन सपोर्ट करती हैं. मैसेज से वैल्यू निकालने के लिए $[messageKey] और मेटाडेटा से वैल्यू निकालने के लिए ${metadataKey} का उपयोग करें.", + "source-field": "सोर्स फ़ील्ड", + "source-field-required": "सोर्स फ़ील्ड आवश्यक है.", + "originator-source": "ओरिजिनेटर सोर्स", + "new-originator": "नया ओरिजिनेटर", + "originator-customer": "कस्टमर", + "originator-tenant": "टेनेंट", + "originator-related": "संबंधित एंटिटी", + "originator-alarm-originator": "अलार्म ओरिजिनेटर", + "originator-entity": "नाम पैटर्न द्वारा एंटिटी", + "clone-message": "मैसेज क्लोन करें", + "transform": "ट्रांसफ़ॉर्म", + "default-ttl": "डिफ़ॉल्ट TTL", + "default-ttl-required": "डिफ़ॉल्ट TTL आवश्यक है.", + "default-ttl-hint": "रूल नोड टाइम-टू-लाइव (TTL) वैल्यू मैसेज मेटाडेटा से फ़ेच करेगा. यदि कोई वैल्यू मौजूद नहीं है, तो यह कॉन्फ़िगरेशन में निर्दिष्ट TTL को उपयोग करेगा. यदि वैल्यू 0 सेट है, तो टेनेंट प्रोफ़ाइल कॉन्फ़िगरेशन से TTL लागू होगी.", + "default-ttl-zero-hint": "यदि वैल्यू 0 सेट है तो TTL लागू नहीं होगी.", + "min-default-ttl-message": "केवल 0 न्यूनतम TTL की अनुमति है.", + "generation-parameters": "जेनेरेशन पैरामीटर्स", + "message-count": "जनरेटेड मैसेजेस लिमिट (0 - अनलिमिटेड)", + "message-count-required": "जनरेटेड मैसेजेस लिमिट आवश्यक है.", + "min-message-count-message": "केवल 0 न्यूनतम मैसेज काउंट की अनुमति है.", + "period-seconds": "पीरियड (सेकंड में)", + "period-seconds-required": "पीरियड आवश्यक है.", + "generation-frequency-seconds": "जेनेरेशन फ़्रीक्वेंसी (सेकंड में)", + "generation-frequency-required": "जेनेरेशन फ़्रीक्वेंसी आवश्यक है.", + "min-generation-frequency-message": "कम-से-कम 60 सेकंड का न्यूनतम मान अनुमत है.", + "script-lang-tbel": "TBEL", + "script-lang-js": "JS", + "use-metadata-period-in-seconds-patterns": "सेकंड में अवधि के पैटर्न का उपयोग करें", + "use-metadata-period-in-seconds-patterns-hint": "यदि चुना गया, तो रूल नोड मैसेज मेटाडेटा या डेटा से सेकंड में इंटरवल पैटर्न का उपयोग करेगा, यह मानते हुए कि इंटरवल सेकंड में हैं.", + "period-in-seconds-pattern": "सेकंड में अवधि का पैटर्न", + "period-in-seconds-pattern-required": "सेकंड में अवधि का पैटर्न आवश्यक है", + "min-period-seconds-message": "केवल 60 सेकंड की न्यूनतम अवधि की अनुमति है.", + "originator": "ओरिजिनेटर", + "message-body": "मैसेज बॉडी", + "message-metadata": "मैसेज मेटाडेटा", + "generate": "जनरेट करें", + "current-rule-node": "वर्तमान रूल नोड", + "current-tenant": "वर्तमान टेनेंट", + "generator-function": "जेनेरेटर फ़ंक्शन", + "test-generator-function": "जेनेरेटर फ़ंक्शन टेस्ट करें", + "generator": "जेनेरेटर", + "test-filter-function": "फ़िल्टर फ़ंक्शन टेस्ट करें", + "test-switch-function": "स्विच फ़ंक्शन टेस्ट करें", + "test-transformer-function": "ट्रांसफ़ॉर्मर फ़ंक्शन टेस्ट करें", + "transformer": "ट्रांसफ़ॉर्मर", + "alarm-create-condition": "अलार्म क्रिएट कंडीशन", + "test-condition-function": "कंडीशन फ़ंक्शन टेस्ट करें", + "alarm-clear-condition": "अलार्म क्लियर कंडीशन", + "alarm-details-builder": "अलार्म डीटेल्स बिल्डर", + "test-details-function": "डीटेल्स फ़ंक्शन टेस्ट करें", + "alarm-type": "अलार्म टाइप", + "select-entity-types": "एंटिटी टाइप्स चुनें", + "alarm-type-required": "अलार्म टाइप आवश्यक है.", + "alarm-severity": "अलार्म की गंभीरता", + "alarm-severity-required": "अलार्म की गंभीरता आवश्यक है", + "alarm-severity-pattern": "अलार्म गंभीरता पैटर्न", + "alarm-status-filter": "अलार्म स्टेटस फ़िल्टर", + "alarm-status-list-empty": "अलार्म स्टेटस सूची खाली है", + "no-alarm-status-matching": "कोई उपयुक्त अलार्म स्टेटस नहीं मिला.", + "propagate": "अलार्म को संबंधित एंटिटीज़ तक प्रोपेगेट करें", + "propagate-to-owner": "अलार्म को एंटिटी ओनर (कस्टमर या टेनेंट) तक प्रोपेगेट करें", + "propagate-to-tenant": "अलार्म को टेनेंट तक प्रोपेगेट करें", + "condition": "शर्त", + "details": "विवरण", + "to-string": "स्ट्रिंग में बदलें", + "test-to-string-function": "To string फ़ंक्शन टेस्ट करें", + "from-template": "प्रेषक", + "from-template-required": "प्रेषक आवश्यक है", + "message-to-metadata": "मैसेज से मेटाडेटा", + "metadata-to-message": "मेटाडेटा से मैसेज", + "from-message": "मैसेज से", + "from-metadata": "मेटाडेटा से", + "to-template": "प्राप्तकर्ता", + "to-template-required": "To टेम्पलेट आवश्यक है", + "mail-address-list-template-hint": "कॉमा सेपरेटेड एड्रेस सूची, मेटाडेटा से वैल्यू के लिए ${metadataKey} और मैसेज बॉडी से वैल्यू के लिए $[messageKey] का उपयोग करें", + "cc-template": "Cc", + "bcc-template": "Bcc", + "subject-template": "सब्जेक्ट", + "subject-template-required": "सब्जेक्ट टेम्पलेट आवश्यक है", + "body-template": "बॉडी", + "body-template-required": "बॉडी टेम्पलेट आवश्यक है", + "dynamic-mail-body-type": "डायनामिक मेल बॉडी टाइप", + "mail-body-type": "मेल बॉडी टाइप", + "body-type-template": "बॉडी टाइप टेम्पलेट", + "reply-routing-configuration": "रिप्लाई रूटिंग कॉन्फ़िगरेशन", + "rpc-reply-routing-configuration-hint": "ये कॉन्फ़िगरेशन पैरामीटर्स सर्विस, सेशन और रिक्वेस्ट की पहचान करने के लिए मेटाडेटा की के नाम निर्दिष्ट करते हैं, ताकि रिप्लाई वापस भेजी जा सके.", + "reply-routing-configuration-hint": "ये कॉन्फ़िगरेशन पैरामीटर्स सर्विस और रिक्वेस्ट की पहचान करने के लिए मेटाडेटा की के नाम निर्दिष्ट करते हैं, ताकि रिप्लाई वापस भेजी जा सके.", + "request-id-metadata-attribute": "रिक्वेस्ट Id", + "service-id-metadata-attribute": "सर्विस Id", + "session-id-metadata-attribute": "सेशन Id", + "timeout-sec": "टाइमआउट (सेकंड में)", + "timeout-required": "टाइमआउट आवश्यक है", + "min-timeout-message": "केवल 0 न्यूनतम टाइमआउट मान अनुमत है.", + "endpoint-url-pattern": "एंडपॉइंट URL पैटर्न", + "endpoint-url-pattern-required": "एंडपॉइंट URL पैटर्न आवश्यक है", + "request-method": "रिक्वेस्ट मेथड", + "use-simple-client-http-factory": "सिंपल क्लाइंट HTTP फ़ैक्टरी का उपयोग करें", + "ignore-request-body": "रिक्वेस्ट बॉडी के बिना", + "parse-to-plain-text": "प्लेन टेक्स्ट में पार्स करें", + "parse-to-plain-text-hint": "यदि चुना गया, तो रिक्वेस्ट बॉडी मैसेज पेलोड JSON स्ट्रिंग से प्लेन टेक्स्ट में बदल दिया जाएगा, उदाहरण: msg = \"नमस्ते,\\t\"दुनिया\"\" को पार्स करके नमस्ते, \"दुनिया\" बनाया जाएगा", + "read-timeout": "रीड टाइमआउट (मिलीसेकंड में)", + "read-timeout-hint": "मान 0 का अर्थ है अनलिमिटेड टाइमआउट", + "max-parallel-requests-count": "पैरलल रिक्वेस्ट्स की अधिकतम संख्या", + "max-parallel-requests-count-hint": "मान 0 का अर्थ है पैरलल प्रोसेसिंग पर कोई सीमा नहीं", + "max-response-size": "मैक्स रिस्पॉन्स साइज (KB में)", + "max-response-size-hint": "HTTP मैसेजेस (जैसे JSON या XML पेलोड्स) को डिकोड या एनकोड करते समय डेटा बफ़र करने के लिए आवंटित मेमोरी की अधिकतम मात्रा", + "headers": "हेडर्स", + "headers-hint": "मेटाडेटा से वैल्यू के लिए ${metadataKey} और मैसेज बॉडी से वैल्यू के लिए $[messageKey] का उपयोग हेडर/वैल्यू फ़ील्ड्स में करें", + "header": "हेडर", + "header-required": "हेडर आवश्यक है", + "value": "वैल्यू", + "value-required": "वैल्यू आवश्यक है", + "topic-pattern": "टॉपिक पैटर्न", + "key-pattern": "की पैटर्न", + "key-pattern-hint": "वैकल्पिक. यदि एक वैध पार्टिशन नंबर दिया गया है, तो रिकॉर्ड भेजते समय वही उपयोग होगा. यदि कोई पार्टिशन निर्दिष्ट नहीं है, तो की का उपयोग किया जाएगा. यदि दोनों में से कोई भी निर्दिष्ट नहीं है, तो पार्टिशन राउंड-रॉबिन तरीके से असाइन किया जाएगा.", + "topic-pattern-required": "टॉपिक पैटर्न आवश्यक है", + "topic": "टॉपिक", + "topic-required": "टॉपिक आवश्यक है", + "bootstrap-servers": "बूटस्ट्रैप सर्वर्स", + "bootstrap-servers-required": "बूटस्ट्रैप सर्वर्स का मान आवश्यक है", + "other-properties": "अन्य प्रॉपर्टीज़", + "key": "की", + "key-required": "की आवश्यक है", + "retries": "फेल होने पर ऑटोमैटिक रीट्राई की संख्या", + "min-retries-message": "केवल 0 न्यूनतम रीट्राई की अनुमति है.", + "batch-size-bytes": "प्रोड्यूस बैच साइज (बाइट्स में)", + "min-batch-size-bytes-message": "केवल 0 न्यूनतम बैच साइज की अनुमति है.", + "linger-ms": "लोकल बफ़र करने का समय (ms)", + "min-linger-ms-message": "केवल 0 ms न्यूनतम मान की अनुमति है.", + "buffer-memory-bytes": "क्लाइंट बफ़र का अधिकतम आकार (बाइट्स में)", + "min-buffer-memory-message": "केवल 0 न्यूनतम बफ़र आकार की अनुमति है.", + "memory-buffer-size-range": "मेमोरी बफ़र आकार 0 और {{max}} KB के बीच होना चाहिए", + "acks": "पुष्टिकरणों की संख्या", + "topic-arn-pattern": "टॉपिक ARN पैटर्न", + "topic-arn-pattern-required": "टॉपिक ARN पैटर्न आवश्यक है", + "aws-access-key-id": "AWS Access Key ID", + "aws-access-key-id-required": "AWS Access Key ID आवश्यक है", + "aws-secret-access-key": "AWS Secret Access Key", + "aws-secret-access-key-required": "AWS Secret Access Key आवश्यक है", + "aws-region": "AWS Region", + "aws-region-required": "AWS Region आवश्यक है", + "exchange-name-pattern": "एक्सचेंज नाम पैटर्न", + "routing-key-pattern": "रूटिंग की पैटर्न", + "message-properties": "संदेश प्रॉपर्टीज़", + "host": "होस्ट", + "host-required": "होस्ट आवश्यक है", + "port": "पोर्ट", + "port-required": "पोर्ट आवश्यक है", + "port-range": "पोर्ट 1 से 65535 की रेंज में होना चाहिए.", + "virtual-host": "वर्चुअल होस्ट", + "username": "यूज़रनेम", + "password": "पासवर्ड", + "automatic-recovery": "ऑटोमैटिक रिकवरी", + "connection-timeout-ms": "कनेक्शन टाइमआउट (ms)", + "min-connection-timeout-ms-message": "केवल 0 ms न्यूनतम मान की अनुमति है.", + "handshake-timeout-ms": "हैंडशेक टाइमआउट (ms)", + "min-handshake-timeout-ms-message": "केवल 0 ms न्यूनतम मान की अनुमति है.", + "client-properties": "क्लाइंट प्रॉपर्टीज़", + "queue-url-pattern": "क्यू URL पैटर्न", + "queue-url-pattern-required": "क्यू URL पैटर्न आवश्यक है", + "delay-seconds": "डिले (सेकंड)", + "min-delay-seconds-message": "केवल 0 सेकंड न्यूनतम मान की अनुमति है.", + "max-delay-seconds-message": "केवल 900 सेकंड अधिकतम मान की अनुमति है.", + "name": "नाम", + "name-required": "नाम आवश्यक है", + "queue-type": "क्यू टाइप", + "sqs-queue-standard": "स्टैंडर्ड", + "sqs-queue-fifo": "FIFO", + "gcp-project-id": "GCP प्रोजेक्ट ID", + "gcp-project-id-required": "GCP प्रोजेक्ट ID आवश्यक है", + "gcp-service-account-key": "GCP सर्विस अकाउंट की फ़ाइल", + "gcp-service-account-key-required": "GCP सर्विस अकाउंट की फ़ाइल आवश्यक है", + "pubsub-topic-name": "टॉपिक नाम", + "pubsub-topic-name-required": "टॉपिक नाम आवश्यक है", + "message-attributes": "संदेश विशेषताएँ", + "message-attributes-hint": "मेटाडेटा से मान के लिए ${metadataKey} का उपयोग करें, name/value फ़ील्ड्स में मैसेज बॉडी से मान के लिए $[messageKey] का उपयोग करें", + "connect-timeout": "कनेक्शन टाइमआउट (सेकंड)", + "connect-timeout-required": "कनेक्शन टाइमआउट आवश्यक है.", + "connect-timeout-range": "कनेक्शन टाइमआउट 1 से 200 की रेंज में होना चाहिए.", + "client-id": "क्लाइंट ID", + "client-id-hint": "वैकल्पिक. ऑटो-जनरेटेड क्लाइंट ID के लिए इसे खाली छोड़ें. क्लाइंट ID निर्दिष्ट करते समय सावधान रहें. अधिकांश MQTT ब्रोकर्स समान क्लाइंट ID के साथ कई कनेक्शनों की अनुमति नहीं देंगे. ऐसे ब्रोकर्स से कनेक्ट होने के लिए आपका MQTT क्लाइंट ID यूनिक होना चाहिए. जब प्लेटफ़ॉर्म माइक्रो-सर्विस मोड में चल रहा होता है, तो प्रत्येक माइक्रो-सर्विस में रूल नोड की कॉपी लॉन्च होती है. यह अपने-आप ही समान ID वाले कई MQTT क्लाइंट्स की ओर ले जाता है और रूल नोड की विफलता का कारण बन सकता है. ऐसी विफलताओं से बचने के लिए नीचे दिए गए \"क्लाइंट ID के साथ सर्विस ID को उपसर्ग के रूप में जोड़ें\" विकल्प को सक्षम करें.", + "append-client-id-suffix": "क्लाइंट ID के साथ सर्विस ID को उपसर्ग के रूप में जोड़ें", + "client-id-suffix-hint": "वैकल्पिक. तब लागू होता है जब \"Client ID\" स्पष्ट रूप से निर्दिष्ट हो. अगर चुना गया, तो Service ID को क्लाइंट ID के साथ उपसर्ग के रूप में जोड़ा जाएगा. यह तब विफलताओं से बचने में मदद करता है जब प्लेटफ़ॉर्म माइक्रो-सर्विस मोड में चल रहा हो.", + "device-id": "डिवाइस ID", + "device-id-required": "डिवाइस ID आवश्यक है.", + "clean-session": "क्लीन सेशन", + "enable-ssl": "SSL सक्षम करें", + "credentials": "क्रेडेंशियल्स", + "credentials-type": "क्रेडेंशियल्स प्रकार", + "credentials-type-required": "क्रेडेंशियल्स प्रकार आवश्यक है.", + "credentials-anonymous": "Anonymous", + "credentials-basic": "Basic", + "credentials-pem": "PEM", + "credentials-pem-hint": "कम-से-कम सर्वर CA सर्टिफ़िकेट फ़ाइल या क्लाइंट सर्टिफ़िकेट और क्लाइंट प्राइवेट की फ़ाइलों की जोड़ी आवश्यक है", + "credentials-sas": "Shared Access Signature", + "sas-key": "SAS Key", + "sas-key-required": "SAS Key आवश्यक है.", + "hostname": "होस्टनेम", + "hostname-required": "होस्टनेम आवश्यक है.", + "azure-ca-cert": "CA सर्टिफ़िकेट फ़ाइल", + "username-required": "यूज़रनेम आवश्यक है.", + "password-required": "पासवर्ड आवश्यक है.", + "ca-cert": "सर्वर CA सर्टिफ़िकेट फ़ाइल", + "private-key": "क्लाइंट प्राइवेट की फ़ाइल", + "cert": "क्लाइंट सर्टिफ़िकेट फ़ाइल", + "no-file": "कोई फ़ाइल चयनित नहीं.", + "drop-file": "फ़ाइल को ड्रॉप करें या अपलोड करने के लिए फ़ाइल चुनने हेतु क्लिक करें.", + "private-key-password": "प्राइवेट की पासवर्ड", + "use-system-smtp-settings": "सिस्टम SMTP सेटिंग्स का उपयोग करें", + "use-metadata-dynamic-interval": "डायनेमिक इंटरवल का उपयोग करें", + "metadata-dynamic-interval-hint": "इंटरवल प्रारंभ और समाप्ति इनपुट फ़ील्ड्स टेम्पलेटाइज़ेशन को सपोर्ट करते हैं. ध्यान दें कि सब्स्टिट्यूट किया गया टेम्पलेट मान मिलीसेकंड्स में सेट होना चाहिए. मैसेज से मान निकालने के लिए $[messageKey] और मेटाडेटा से मान निकालने के लिए ${metadataKey} का उपयोग करें.", + "use-metadata-interval-patterns-hint": "अगर चुना गया, तो रूल नोड मैसेज मेटाडेटा या डेटा से प्रारंभ और समाप्ति इंटरवल पैटर्न का उपयोग करेगा, यह मानते हुए कि इंटरवल्स मिलीसेकंड्स में हैं.", + "use-message-alarm-data": "मैसेज अलार्म डेटा का उपयोग करें", + "overwrite-alarm-details": "अलार्म विवरण को ओवरराइट करें", + "use-alarm-severity-pattern": "अलार्म सीवियरिटी /पैटर्न का उपयोग करें", + "check-all-keys": "जाँचें कि सभी निर्दिष्ट फ़ील्ड मौजूद हैं", + "check-all-keys-hint": "यदि चुना गया, तो यह जाँचता है कि संदेश डेटा और मेटाडेटा में सभी निर्दिष्ट कुंजियाँ मौजूद हैं।", + "check-relation-to-specific-entity": "विशिष्ट एंटिटी के साथ संबंध की जाँच करें", + "check-relation-to-specific-entity-tooltip": "यदि सक्षम किया गया, तो यह किसी विशिष्ट एंटिटी के साथ संबंध की उपस्थिति की जाँच करता है, अन्यथा किसी भी एंटिटी के साथ संबंध की उपस्थिति की जाँच करता है। दोनों ही मामलों में, संबंध खोज कॉन्फ़िगर की गई दिशा और प्रकार पर आधारित होती है।", + "check-relation-hint": "दिशा और संबंध प्रकार के आधार पर, किसी विशिष्ट एंटिटी या किसी भी एंटिटी के साथ संबंध के अस्तित्व की जाँच करता है।", + "delete-relation-with-specific-entity": "विशिष्ट एंटिटी के साथ संबंध हटाएँ", + "delete-relation-with-specific-entity-hint": "यदि सक्षम किया गया, तो केवल एक विशिष्ट एंटिटी के साथ संबंध हटाया जाएगा। अन्यथा, संबंध सभी मिलती-जुलती एंटिटीज़ के साथ हटाया जाएगा।", + "delete-relation-hint": "निर्धारित दिशा और प्रकार के आधार पर, आने वाले संदेश के ओरिजिनेटर से निर्दिष्ट एंटिटी या एंटिटीज़ की सूची तक संबंध हटाता है।", + "remove-current-relations": "वर्तमान संबंध हटाएँ", + "remove-current-relations-hint": "दिशा और प्रकार के आधार पर, आने वाले संदेश के ओरिजिनेटर से वर्तमान संबंध हटाता है।", + "change-originator-to-related-entity": "ओरिजिनेटर को संबंधित एंटिटी में बदलें", + "change-originator-to-related-entity-hint": "सबमिट किए गए संदेश को किसी दूसरी एंटिटी से आए संदेश के रूप में प्रोसेस करने के लिए उपयोग किया जाता है।", + "start-interval": "इंटरवल प्रारंभ", + "end-interval": "इंटरवल समाप्ति", + "start-interval-required": "इंटरवल प्रारंभ आवश्यक है।", + "end-interval-required": "इंटरवल समाप्ति आवश्यक है।", + "smtp-protocol": "प्रोटोकॉल", + "smtp-host": "SMTP होस्ट", + "smtp-host-required": "SMTP होस्ट आवश्यक है।", + "smtp-port": "SMTP पोर्ट", + "smtp-port-required": "आपको SMTP पोर्ट प्रदान करना होगा।", + "smtp-port-range": "SMTP पोर्ट 1 से 65535 की रेंज में होना चाहिए।", + "timeout-msec": "टाइमआउट (ms)", + "min-timeout-msec-message": "केवल 0 ms न्यूनतम मान की अनुमति है।", + "enter-username": "यूज़रनेम दर्ज करें", + "enter-password": "पासवर्ड दर्ज करें", + "enable-tls": "TLS सक्षम करें", + "tls-version": "TLS संस्करण", + "enable-proxy": "प्रॉक्सी सक्षम करें", + "use-system-proxy-properties": "सिस्टम प्रॉक्सी गुणों का उपयोग करें", + "proxy-host": "प्रॉक्सी होस्ट", + "proxy-host-required": "प्रॉक्सी होस्ट आवश्यक है।", + "proxy-port": "प्रॉक्सी पोर्ट", + "proxy-port-required": "प्रॉक्सी पोर्ट आवश्यक है।", + "proxy-port-range": "प्रॉक्सी पोर्ट 1 से 65535 की रेंज में होना चाहिए।", + "proxy-user": "प्रॉक्सी उपयोगकर्ता", + "proxy-password": "प्रॉक्सी पासवर्ड", + "proxy-scheme": "प्रॉक्सी स्कीम", + "numbers-to-template": "फ़ोन नंबर 'To' टेम्पलेट", + "numbers-to-template-required": "फ़ोन नंबर 'To' टेम्पलेट आवश्यक है।", + "numbers-to-template-hint": "कॉमा से अलग किए गए फ़ोन नंबर, मेटाडेटा से मान लेने के लिए ${metadataKey} का उपयोग करें, और मैसेज बॉडी से मान लेने के लिए $[messageKey] का उपयोग करें", + "sms-message-template": "SMS मैसेज टेम्पलेट", + "sms-message-template-required": "SMS मैसेज टेम्पलेट आवश्यक है।", + "use-system-sms-settings": "सिस्टम SMS प्रोवाइडर सेटिंग्स का उपयोग करें", + "min-period-0-seconds-message": "केवल 0 सेकंड का न्यूनतम पीरियड अनुमत है।", + "max-pending-messages": "अधिकतम पेंडिंग मैसेज", + "max-pending-messages-required": "अधिकतम पेंडिंग मैसेज आवश्यक है।", + "max-pending-messages-range": "अधिकतम पेंडिंग मैसेज 1 से 100000 के बीच होना चाहिए।", + "originator-types-filter": "ओरिजिनेटर टाइप फ़िल्टर", + "interval-seconds": "इंटरवल (सेकंड में)", + "interval-seconds-required": "इंटरवल आवश्यक है।", + "int-range": "मान अधिकतम पूर्णांक सीमा (2147483648) से अधिक नहीं होना चाहिए।", + "min-interval-seconds-message": "केवल 1 सेकंड का न्यूनतम इंटरवल अनुमत है।", + "output-timeseries-key-prefix": "आउटपुट टाइम सीरीज़ की प्रीफ़िक्स", + "output-timeseries-key-prefix-required": "आउटपुट टाइम सीरीज़ की प्रीफ़िक्स आवश्यक है।", + "separator-hint": "फ़ील्ड इनपुट पूरा करने के लिए आपको \"एंटर\" दबाना होगा।", + "select-details": "विवरण चुनें", + "entity-details-id": "आईडी", + "entity-details-title": "टाइटल", + "entity-details-country": "देश", + "entity-details-state": "राज्य", + "entity-details-city": "शहर", + "entity-details-zip": "ज़िप", + "entity-details-address": "पता", + "entity-details-address2": "पता 2", + "entity-details-additional_info": "अतिरिक्त जानकारी", + "entity-details-phone": "फ़ोन", + "entity-details-email": "ईमेल", + "email-sender": "ईमेल प्रेषक", + "fields-to-check": "जाँचने वाले फ़ील्ड्स", + "add-detail": "डिटेल जोड़ें", + "check-all-keys-tooltip": "यदि सक्षम है, तो इनकमिंग संदेश और उसकी मेटाडाटा में दिए गए संदेश और मेटाडाटा फ़ील्ड-नामों की सूची में शामिल सभी फ़ील्ड्स की मौजूदगी की जाँच करता है।", + "fields-to-check-hint": "फ़ील्ड नाम इनपुट पूरा करने के लिए \"एंटर\" दबाएँ। एक से अधिक फ़ील्ड नाम समर्थित हैं।", + "entity-details-list-empty": "कम से कम एक डिटेल चुनी जानी चाहिए।", + "alarm-status": "अलार्म स्टेटस", + "alarm-required": "कम से कम एक अलार्म स्टेटस चुना जाना चाहिए।", + "no-entity-details-matching": "कोई मिलते-जुलते एंटिटी विवरण नहीं मिले।", + "custom-table-name": "कस्टम टेबल नाम", + "custom-table-name-required": "टेबल नाम आवश्यक है।", + "custom-table-hint": "टेबल आपके Cassandra क्लस्टर में बनाई जानी चाहिए और उसका नाम 'cs_tb_' प्रीफ़िक्स से शुरू होना चाहिए ताकि डेटा कॉमन TB टेबल्स में न जाए। यहाँ टेबल नाम 'cs_tb_' प्रीफ़िक्स के बिना दर्ज करें।", + "message-field": "मैसेज फ़ील्ड", + "message-field-required": "मैसेज फ़ील्ड आवश्यक है।", + "table-col": "टेबल कॉलम", + "table-col-required": "टेबल कॉलम आवश्यक है।", + "latitude-field-name": "लैटिट्यूड फ़ील्ड नाम", + "longitude-field-name": "लॉन्गिट्यूड फ़ील्ड नाम", + "latitude-field-name-required": "लैटिट्यूड फ़ील्ड नाम आवश्यक है।", + "longitude-field-name-required": "लॉन्गिट्यूड फ़ील्ड नाम आवश्यक है।", + "fetch-perimeter-info-from-metadata": "मेटाडेटा से परिमीटर की जानकारी प्राप्त करें", + "fetch-perimeter-info-from-metadata-tooltip": "यदि परिमीटर प्रकार 'पॉलीगॉन' पर सेट है, तो मेटाडेटा फ़ील्ड '{{perimeterKeyName}}' का मान बिना अतिरिक्त पार्सिंग के परिमीटर डेफ़िनिशन के रूप में उपयोग किया जाएगा। अन्यथा, यदि परिमीटर प्रकार 'सर्कल' पर सेट है, तो मेटाडेटा फ़ील्ड '{{perimeterKeyName}}' के मान को पार्स करके 'latitude', 'longitude', 'radius', 'radiusUnit' फ़ील्ड निकाले जाएँगे, जो सर्कल परिमीटर डेफ़िनिशन के लिए उपयोग होते हैं।", + "perimeter-key-name": "परिमीटर की कुंजी का नाम", + "perimeter-key-name-hint": "मेटाडेटा फ़ील्ड नाम जिसमें परिमीटर की जानकारी होती है।", + "perimeter-key-name-required": "परिमीटर की कुंजी का नाम आवश्यक है।", + "perimeter-circle": "सर्कल", + "perimeter-polygon": "पॉलीगॉन", + "perimeter-type": "परिमीटर प्रकार", + "circle-center-latitude": "केंद्र का लैटिट्यूड", + "circle-center-latitude-required": "केंद्र का लैटिट्यूड आवश्यक है।", + "circle-center-longitude": "केंद्र का लॉन्गिट्यूड", + "circle-center-longitude-required": "केंद्र का लॉन्गिट्यूड आवश्यक है।", + "range-unit-meter": "मीटर", + "range-unit-kilometer": "किलोमीटर", + "range-unit-foot": "फ़ुट", + "range-unit-mile": "माइल", + "range-unit-nautical-mile": "समुद्री माइल", + "range-units": "रेंज इकाइयाँ", + "range-units-required": "रेंज इकाइयाँ आवश्यक हैं।", + "range": "रेंज", + "range-required": "रेंज आवश्यक है।", + "polygon-definition": "पॉलीगॉन डेफ़िनिशन", + "polygon-definition-required": "पॉलीगॉन डेफ़िनिशन आवश्यक है।", + "polygon-definition-hint": "पॉलीगॉन को मैन्युअली परिभाषित करने के लिए निम्न फ़ॉर्मेट का उपयोग करें: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].", + "min-inside-duration": "न्यूनतम भीतर रहने की अवधि", + "min-inside-duration-value-required": "न्यूनतम भीतर रहने की अवधि आवश्यक है।", + "min-inside-duration-time-unit": "न्यूनतम भीतर रहने की अवधि की समय इकाई", + "min-outside-duration": "न्यूनतम बाहर रहने की अवधि", + "min-outside-duration-value-required": "न्यूनतम बाहर रहने की अवधि आवश्यक है।", + "min-outside-duration-time-unit": "न्यूनतम बाहर रहने की अवधि की समय इकाई", + "tell-failure-if-absent": "असफलता बताएँ", + "tell-failure-if-absent-hint": "यदि चुनी गई कुंजियों में से कम से कम एक भी मौजूद नहीं है, तो आउटबाउंड संदेश \"असफलता\" रिपोर्ट करेगा।", + "get-latest-value-with-ts": "लेटेस्ट टेलीमेट्री वैल्यूज़ के लिए टाइमस्टैम्प प्राप्त करें", + "get-latest-value-with-ts-hint": "यदि चयनित है, तो लेटेस्ट टेलीमेट्री वैल्यूज़ में टाइमस्टैम्प भी शामिल होगा, उदाहरण के लिए: \"temp\": \"{\"ts\":1574329385897, \"value\":42}\"", + "ignore-null-strings": "नल (null) स्ट्रिंग्स को नज़रअंदाज़ करें", + "ignore-null-strings-hint": "यदि चयनित है, तो रूल नोड खाली मान वाले एंटिटी फ़ील्ड्स को नज़रअंदाज़ करेगा।", + "add-metadata-key-values-as-kafka-headers": "मैसेज मेटाडेटा की की-वैल्यू पेयर्स को Kafka रिकॉर्ड हेडर्स में जोड़ें", + "add-metadata-key-values-as-kafka-headers-hint": "यदि चयनित है, तो मैसेज मेटाडेटा से की-वैल्यू पेयर्स को आउटगोइंग रिकॉर्ड्स के हेडर्स में प्री-डिफ़ाइंड कैरेक्टर सेट एनकोडिंग के साथ बाइट ऐरेज़ के रूप में जोड़ा जाएगा।", + "charset-encoding": "कैरेक्टर सेट एनकोडिंग", + "charset-encoding-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": "क्यू का नाम ड्रॉप-डाउन सूची से चुना जा सकता है या आप कस्टम नाम जोड़ सकते हैं।", + "device-profile-node-hint": "यदि आपके पास अवधि या दोहराई जाने वाली कंडीशन हैं, तो अलार्म स्टेट के मूल्यांकन की निरंतरता सुनिश्चित करने के लिए यह उपयोगी है।", + "persist-alarm-rules": "अलार्म रूल्स की स्थिति को सुरक्षित रखें", + "persist-alarm-rules-hint": "यदि सक्षम है, तो रूल नोड प्रोसेसिंग की स्थिति को डेटाबेस में सहेजेगा।", + "fetch-alarm-rules": "अलार्म रूल्स की स्थिति प्राप्त करें", + "fetch-alarm-rules-hint": "यदि सक्षम है, तो रूल नोड इनिशियलाइज़ेशन पर प्रोसेसिंग की स्थिति को पुनर्स्थापित करेगा और यह सुनिश्चित करेगा कि सर्वर रीस्टार्ट के बाद भी अलार्म उठें। अन्यथा, स्थिति पहली बार डिवाइस से मैसेज आने पर पुनर्स्थापित होगी।", + "input-value-key": "इनपुट वैल्यू की", + "input-value-key-required": "इनपुट वैल्यू की आवश्यक है।", + "output-value-key": "आउटपुट वैल्यू की", + "output-value-key-required": "आउटपुट वैल्यू की आवश्यक है।", + "number-of-digits-after-floating-point": "दशमलव बिंदु के बाद अंकों की संख्या", + "number-of-digits-after-floating-point-range": "दशमलव बिंदु के बाद अंकों की संख्या 0 से 15 के बीच होनी चाहिए।", + "failure-if-delta-negative": "यदि डेल्टा निगेटिव हो तो असफलता बताएँ", + "failure-if-delta-negative-tooltip": "यदि डेल्टा मान निगेटिव है, तो रूल नोड मैसेज प्रोसेसिंग को असफल कर देगा।", + "use-caching": "कैशिंग का उपयोग करें", + "use-caching-tooltip": "परफॉर्मेंस सुधारने के लिए रूल नोड इनकमिंग मैसेज से आने वाली \"{{inputValueKey}}\" वैल्यू को कैश करेगा। ध्यान दें, यदि आप कहीं और \"{{inputValueKey}}\" वैल्यू बदलते हैं तो कैश अपडेट नहीं होगा।", + "add-time-difference-between-readings": "\"{{inputValueKey}}\" रीडिंग्स के बीच समयांतर जोड़ें", + "add-time-difference-between-readings-tooltip": "यदि सक्षम है, तो रूल नोड आउटबाउंड मैसेज में \"{{periodValueKey}}\" जोड़ देगा।", + "period-value-key": "पीरियड वैल्यू की", + "period-value-key-required": "पीरियड वैल्यू की आवश्यक है।", + "general-pattern-hint": "मेटाडेटा से मान के लिए ${metadataKey} और मैसेज बॉडी से मान के लिए $[messageKey] का उपयोग करें।", + "alarm-severity-pattern-hint": "मेटाडेटा से मान लेने के लिए ${metadataKey} और मैसेज बॉडी से मान लेने के लिए $[messageKey] का उपयोग करें। अलार्म की गंभीरता सिस्टम वैल्यू होनी चाहिए (CRITICAL, MAJOR आदि)।", + "output-node-name-hint": "रूल नोड नाम आउटपुट मैसेज के रिलेशन प्रकार के अनुरूप होता है और कॉलर रूल चेन में मैसेज को अन्य रूल नोड्स तक फॉरवर्ड करने के लिए उपयोग किया जाता है।", + "use-server-ts": "सर्वर टाइमस्टैम्प का उपयोग करें", + "use-server-ts-hint": "ऐसे टाइम-सीरीज़ डेटा के लिए जो स्पष्ट टाइमस्टैम्प नहीं भेजते, सर्वर का वर्तमान टाइमस्टैम्प उपयोग करें। यह कई स्रोतों से आने वाले या आउट-ऑफ-ऑर्डर मैसेजेस प्रोसेस करते समय सही क्रम बनाए रखने में मदद करता है।", + "kv-map-pattern-hint": "सभी इनपुट फ़ील्ड्स टेम्पलेटाइज़ेशन सपोर्ट करते हैं। मैसेज से मान निकालने के लिए $[messageKey] और मेटाडेटा से मान निकालने के लिए ${metadataKey} का उपयोग करें।", + "kv-map-single-pattern-hint": "इनपुट फ़ील्ड टेम्पलेटाइज़ेशन सपोर्ट करता है। मैसेज से मान निकालने के लिए $[messageKey] और मेटाडेटा से मान निकालने के लिए ${metadataKey} का उपयोग करें।", + "shared-scope": "शेयर्ड स्कोप", + "server-scope": "सर्वर स्कोप", + "client-scope": "क्लाइंट स्कोप", + "attribute-type": "विशेषता", + "attribute-type-description": "डेटाबेस से विशेषता मान प्राप्त करें", + "attribute-type-result-description": "परिणाम को डेटाबेस में एंटिटी विशेषता के रूप में सहेजें", + "constant-type": "कॉनस्टैंट", + "constant-type-description": "कॉनस्टैंट मान परिभाषित करें", + "time-series-type": "टाइम सीरीज़", + "time-series-type-description": "डाटाबेस से नवीनतम टाइम सीरीज़ मान प्राप्त करें", + "time-series-type-result-description": "परिणाम को डाटाबेस में एंटिटी टाइम सीरीज़ के रूप में सहेजें", + "message-body-type": "मैसेज", + "message-body-type-description": "इनकमिंग मैसेज से आर्ग्युमेंट मान प्राप्त करें", + "message-body-type-result-description": "परिणाम को आउटगोइंग मैसेज में जोड़ें", + "message-metadata-type": "मेटाडेटा", + "message-metadata-type-description": "इनकमिंग मैसेज मेटाडेटा से आर्ग्युमेंट मान प्राप्त करें", + "message-metadata-result-description": "परिणाम को आउटगोइंग मैसेज मेटाडेटा में जोड़ें", + "argument-tile": "आर्ग्युमेंट्स", + "no-arguments-prompt": "कोई आर्ग्युमेंट कॉन्फ़िगर नहीं किया गया है", + "result-title": "परिणाम", + "functions-field-input": "फ़ंक्शन्स", + "no-option-found": "कोई विकल्प नहीं मिला", + "argument-source-field-input": "स्रोत", + "argument-source-field-input-required": "आर्ग्युमेंट का स्रोत आवश्यक है.", + "argument-key-field-input": "की", + "argument-key-field-input-required": "आर्ग्युमेंट की आवश्यक है.", + "constant-value-field-input": "कॉनस्टैंट मान", + "constant-value-field-input-required": "कॉनस्टैंट मान आवश्यक है.", + "attribute-scope-field-input": "विशेषता स्कोप", + "attribute-scope-field-input-required": "विशेषता स्कोप आवश्यक है.", + "default-value-field-input": "डिफ़ॉल्ट मान", + "type-field-input": "टाइप", + "type-field-input-required": "टाइप आवश्यक है.", + "key-field-input": "की", + "add-entity-type": "एंटिटी टाइप जोड़ें", + "add-device-profile": "डिवाइस प्रोफ़ाइल जोड़ें", + "key-field-input-required": "की आवश्यक है.", + "number-floating-point-field-input": "दशमलव के बाद अंकों की संख्या", + "number-floating-point-field-input-hint": "परिणाम को पूर्णांक में बदलने के लिए 0 का उपयोग करें", + "add-to-message-field-input": "मैसेज में जोड़ें", + "add-to-metadata-field-input": "मेटाडेटा में जोड़ें", + "custom-expression-field-input": "गणितीय अभिव्यक्ति", + "custom-expression-field-input-required": "गणितीय अभिव्यक्ति आवश्यक है", + "custom-expression-field-input-hint": "जिसका मूल्यांकन करना है, वह गणितीय अभिव्यक्ति निर्दिष्ट करें। डिफ़ॉल्ट अभिव्यक्ति दिखाती है कि फ़ारेनहाइट को सेल्सियस में कैसे बदला जाए।", + "retained-message": "रिटेन्ड", + "attributes-mapping": "विशेषताओं की मैपिंग", + "latest-telemetry-mapping": "नवीनतम टेलीमेट्री मैपिंग", + "add-mapped-attribute-to": "मैप की गई विशेषताओं को इसमें जोड़ें", + "add-mapped-latest-telemetry-to": "मैप की गई नवीनतम टेलीमेट्री को इसमें जोड़ें", + "add-mapped-fields-to": "मैप किए गए फ़ील्ड्स को इसमें जोड़ें", + "add-selected-details-to": "चुने गए विवरण इसमें जोड़ें", + "clear-selected-types": "चुने गए टाइप्स साफ़ करें", + "clear-selected-details": "चुने गए विवरण साफ़ करें", + "clear-selected-fields": "चुने गए फ़ील्ड्स साफ़ करें", + "clear-selected-keys": "चुनी गई कीज़ साफ़ करें", + "geofence-configuration": "जियोफेंस कॉन्फ़िगरेशन", + "coordinate-field-names": "कोऑर्डिनेट फ़ील्ड नाम", + "coordinate-field-hint": "रूल नोड मैसेज से दिए गए फ़ील्ड प्राप्त करने की कोशिश करेगा। यदि वे वहाँ मौजूद नहीं हैं, तो उन्हें मेटाडेटा में खोजेगा।", + "presence-monitoring-strategy": "उपस्थिति मॉनिटरिंग स्ट्रैटेजी", + "presence-monitoring-strategy-on-first-message": "पहले मैसेज पर", + "presence-monitoring-strategy-on-each-message": "हर मैसेज पर", + "presence-monitoring-strategy-on-first-message-hint": "कॉन्फ़िगर की गई न्यूनतम अवधि, पिछली उपस्थिति स्थिति 'प्रवेश किया' या 'बाहर निकला' के अपडेट के बाद बीत जाने पर, पहले मैसेज पर उपस्थिति स्टेटस 'अंदर' या 'बाहर' की रिपोर्ट करता है।", + "presence-monitoring-strategy-on-each-message-hint": "उपस्थिति स्टेटस 'प्रवेश किया' या 'बाहर निकला' के अपडेट के बाद, हर मैसेज पर उपस्थिति स्टेटस 'अंदर' या 'बाहर' की रिपोर्ट करता है।", + "fetch-credentials-to": "क्रेडेंशियल्स यहाँ फ़ेच करें", + "add-originator-attributes-to": "ओरिजिनेटर विशेषताएँ इसमें जोड़ें", + "originator-attributes": "ओरिजिनेटर विशेषताएँ", + "fetch-latest-telemetry-with-timestamp": "नवीनतम टेलीमेट्री टाइमस्टैम्प सहित फ़ेच करें", + "fetch-latest-telemetry-with-timestamp-tooltip": "यदि चुना गया, तो नवीनतम टेलीमेट्री मान टाइमस्टैम्प सहित आउटबाउंड मेटाडेटा में जोड़ दिए जाएँगे, जैसे: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", + "tell-failure": "यदि कोई भी विशेषता गायब हो तो विफलता बताएँ", + "tell-failure-tooltip": "यदि चुनी हुई कीज़ में से कम से कम एक भी मौजूद नहीं है, तो आउटबाउंड मैसेज \"विफलता\" रिपोर्ट करेगा।", + "created-time": "बनाए जाने का समय", + "chip-help": "फील्ड {{inputName}} का इनपुट पूरा करने के लिए 'Enter' दबाएँ।\n{{inputName}} को हटाने के लिए 'Backspace' दबाएँ।\nएक से अधिक मान समर्थित हैं।", + "detail": "विवरण", + "field-name": "फ़ील्ड नाम", + "device-profile": "डिवाइस प्रोफ़ाइल", + "entity-type": "एंटिटी टाइप", + "message-type": "मैसेज टाइप", + "timeseries-key": "टाइम सीरीज़ की", + "type": "टाइप", + "first-name": "पहला नाम", + "last-name": "अंतिम नाम", + "label": "लेबल", + "originator-fields-mapping": "ओरिजिनेटर फ़ील्ड मैपिंग", + "add-mapped-originator-fields-to": "मैप किए गए ओरिजिनेटर फ़ील्ड्स इसमें जोड़ें", + "fields": "फ़ील्ड्स", + "skip-empty-fields": "खाली फ़ील्ड्स को छोड़ें", + "skip-empty-fields-tooltip": "जिन फ़ील्ड्स के मान खाली होंगे, उन्हें आउटपुट मैसेज/आउटपुट मेटाडेटा में नहीं जोड़ा जाएगा।", + "fetch-interval": "फ़ेच इंटरवल", + "fetch-strategy": "फ़ेच स्ट्रैटेजी", + "fetch-timeseries-from-to": "टाइम सीरीज़ {{startInterval}} {{startIntervalTimeUnit}} पहले से {{endInterval}} {{endIntervalTimeUnit}} पहले तक फ़ेच करें।", + "fetch-timeseries-from-to-invalid": "टाइम सीरीज़ फ़ेच अमान्य है (\"इंटरवल स्टार्ट\" \"इंटरवल एंड\" से कम होना चाहिए)।", + "use-metadata-dynamic-interval-tooltip": "यदि चुना गया, तो रूल नोड मैसेज और मेटाडेटा पैटर्न के आधार पर डायनेमिक इंटरवल स्टार्ट और एंड का उपयोग करेगा।", + "all-mode-hint": "यदि फ़ेच मोड \"सभी\" चुना गया है, तो रूल नोड कॉन्फ़िगर किए गए क्वेरी पैरामीटर्स के साथ फ़ेच इंटरवल से टेलीमेट्री प्राप्त करेगा।", + "first-mode-hint": "यदि फ़ेच मोड \"पहला\" चुना गया है, तो रूल नोड फ़ेच इंटरवल की शुरुआत के सबसे नज़दीक की टेलीमेट्री प्राप्त करेगा।", + "last-mode-hint": "यदि फ़ेच मोड \"आख़िरी\" चुना गया है, तो रूल नोड फ़ेच इंटरवल के अंत के सबसे नज़दीक की टेलीमेट्री प्राप्त करेगा।", + "ascending": "आरोही", + "descending": "अवरोही", + "min": "न्यूनतम", + "max": "अधिकतम", + "average": "औसत", + "sum": "योग", + "count": "गिनती", + "none": "कोई नहीं", + "last-level-relation-tooltip": "यदि चुना गया, तो रूल नोड केवल अधिकतम रिलेशन लेवल में सेट किए गए लेवल पर ही संबंधित एंटिटीज़ को खोजेगा।", + "last-level-device-relation-tooltip": "यदि चुना गया, तो रूल नोड केवल अधिकतम रिलेशन लेवल में सेट किए गए लेवल पर ही संबंधित डिवाइस को खोजेगा।", + "data-to-fetch": "फ़ेच करने वाला डेटा", + "mapping-of-customers": "कस्टमर की मैपिंग", + "map-fields-required": "सभी मैपिंग फ़ील्ड ज़रूरी हैं।", + "attributes": "विशेषताएँ", + "related-device-attributes": "संबंधित डिवाइस की विशेषताएँ", + "add-selected-attributes-to": "चुनी हुई विशेषताएँ इसमें जोड़ें", + "device-profiles": "डिवाइस प्रोफ़ाइल्स", + "mapping-of-tenant": "टेनेंट की मैपिंग", + "add-attribute-key": "एट्रिब्यूट की जोड़ें", + "message-template": "संदेश टेम्पलेट", + "message-template-required": "संदेश टेम्पलेट ज़रूरी है", + "use-system-slack-settings": "सिस्टम Slack प्रदाता सेटिंग्स का उपयोग करें", + "slack-api-token": "Slack API टोकन", + "slack-api-token-required": "Slack API टोकन ज़रूरी है", + "keys-mapping": "कुंजियों की मैपिंग", + "add-key": "कुंजी जोड़ें", + "recipients": "प्राप्तकर्ता", + "message-subject-and-content": "संदेश का विषय और सामग्री", + "template-rules-hint": "दोनों इनपुट फ़ील्ड्स टेम्पलेटाइज़ेशन को सपोर्ट करते हैं। मैसेज से मान निकालने के लिए $[messageKey] और मैसेज मेटाडेटा से मान निकालने के लिए ${metadataKey} का उपयोग करें।", + "originator-customer-desc": "इनकमिंग मैसेज के ओरिजिनेटर के कस्टमर को नए ओरिजिनेटर के रूप में उपयोग करें।", + "originator-tenant-desc": "वर्तमान टेनेंट को नए ओरिजिनेटर के रूप में उपयोग करें।", + "originator-related-entity-desc": "संबंधित एंटिटी को नए ओरिजिनेटर के रूप में उपयोग करें। लुकअप कॉन्फ़िगर किए गए रिलेशन टाइप और डायरेक्शन के आधार पर होगा।", + "originator-alarm-originator-desc": "अलार्म ओरिजिनेटर को नए ओरिजिनेटर के रूप में उपयोग करें। केवल तब जब इनकमिंग मैसेज ओरिजिनेटर अलार्म एंटिटी हो।", + "originator-entity-by-name-pattern-desc": "डेटाबेस से प्राप्त एंटिटी को नए ओरिजिनेटर के रूप में उपयोग करें। लुकअप एंटिटी टाइप और दिए गए नाम पैटर्न के आधार पर होगा।", + "email-from-template-hint": "मैसेज से मान निकालने के लिए $[messageKey] और मेटाडेटा से मान निकालने के लिए ${metadataKey} का उपयोग करें।", + "recipients-block-main-hint": "कॉमा से अलग की गई एड्रेस सूची। सभी इनपुट फ़ील्ड्स टेम्पलेटाइज़ेशन को सपोर्ट करते हैं। मैसेज से मान निकालने के लिए $[messageKey] और मेटाडेटा से मान निकालने के लिए ${metadataKey} का उपयोग करें।", + "forward-msg-default-rule-chain": "मैसेज को ओरिजिनेटर की डिफ़ॉल्ट रूल चेन पर फ़ॉरवर्ड करें", + "forward-msg-default-rule-chain-tooltip": "यदि सक्षम किया गया है, तो मैसेज को ओरिजिनेटर की डिफ़ॉल्ट रूल चेन पर फ़ॉरवर्ड किया जाएगा, या कॉन्फ़िगरेशन में दी गई रूल चेन पर, यदि एंटिटी प्रोफ़ाइल में ओरिजिनेटर के लिए कोई डिफ़ॉल्ट रूल चेन परिभाषित नहीं है।", + "exclude-zero-deltas": "आउटबाउंड मैसेज से शून्य डेल्टा को निकालें", + "exclude-zero-deltas-hint": "यदि सक्षम किया गया है, तो आउटबाउंड मैसेज में \"{{outputValueKey}}\" आउटपुट कुंजी केवल तभी जोड़ी जाएगी जब इसका मान शून्य न हो।", + "exclude-zero-deltas-time-difference-hint": "यदि सक्षम किया गया है, तो आउटबाउंड मैसेज में \"{{outputValueKey}}\" और \"{{periodValueKey}}\" आउटपुट कुंजियाँ केवल तभी जोड़ी जाएँगी जब \"{{outputValueKey}}\" का मान शून्य न हो।", + "search-direction-from": "ओरिजिनेटर से टारगेट एंटिटी तक", + "search-direction-to": "टारगेट एंटिटी से ओरिजिनेटर तक", + "del-relation-direction-from": "ओरिजिनेटर से", + "del-relation-direction-to": "ओरिजिनेटर तक", + "target-entity": "टारगेट एंटिटी", + "function-configuration": "फ़ंक्शन कॉन्फ़िगरेशन", + "function-name": "फ़ंक्शन नाम", + "function-name-required": "फ़ंक्शन नाम आवश्यक है।", + "qualifier": "क्वालिफ़ायर", + "qualifier-hint": "अगर क्वालिफ़ायर निर्दिष्ट नहीं है, तो डिफ़ॉल्ट क्वालिफ़ायर \"$LATEST\" उपयोग किया जाएगा।", + "aws-credentials": "AWS क्रेडेंशियल्स", + "connection-timeout": "कनेक्शन टाइमआउट", + "connection-timeout-required": "कनेक्शन टाइमआउट आवश्यक है।", + "connection-timeout-min": "न्यूनतम कनेक्शन टाइमआउट 0 है।", + "connection-timeout-hint": "कनेक्शन स्थापित करते समय छोड़ने और टाइमआउट देने से पहले प्रतीक्षा करने का समय (सेकंड में)। 0 मान का अर्थ अनंत है, और अनुशंसित नहीं है।", + "request-timeout": "रिक्वेस्ट टाइमआउट", + "request-timeout-required": "रिक्वेस्ट टाइमआउट आवश्यक है।", + "request-timeout-min": "न्यूनतम रिक्वेस्ट टाइमआउट 0 है।", + "request-timeout-hint": "रिक्वेस्ट पूरा होने के लिए छोड़ने और टाइमआउट देने से पहले प्रतीक्षा करने का समय (सेकंड में)। 0 मान का अर्थ अनंत है, और अनुशंसित नहीं है।", + "units": "इकाइयाँ", + "tell-failure-aws-lambda": "यदि AWS Lambda फ़ंक्शन के निष्पादन में कोई अपवाद उत्पन्न हो तो Failure बताएं", + "tell-failure-aws-lambda-hint": "यदि AWS Lambda फ़ंक्शन के निष्पादन के दौरान कोई अपवाद (exception) उत्पन्न होता है, तो रूल नोड मैसेज प्रोसेसिंग की Failure को मजबूर करेगा।", + "basic-mode": "बेसिक", + "advanced-mode": "एडवांस्ड", + "save-time-series": { + "processing-settings": "प्रोसेसिंग सेटिंग्स", + "processing-settings-hint": "इनकमिंग मैसेज कैसे प्रोसेस किए जाएँगे, यह परिभाषित करें। बेसिक प्रोसेसिंग सेटिंग्स आपको पहले से कॉन्फ़िगर की गई स्ट्रैटेजीज़ चुनने देती हैं, जबकि एडवांस्ड सेटिंग्स में आप हर एक एक्शन के लिए अलग-अलग प्रोसेसिंग स्ट्रैटेजी चुन सकते हैं।", + "advanced-settings-hint": "प्रोसेसिंग स्ट्रैटेजीज़ कॉन्फ़िगर करते समय सावधान रहें। कुछ संयोजन अप्रत्याशित व्यवहार का कारण बन सकते हैं।", + "strategy": "स्ट्रैटेजी", + "deduplication-interval": "डेडुप्लीकेशन इंटरवल", + "deduplication-interval-required": "डेडुप्लीकेशन इंटरवल आवश्यक है।", + "deduplication-interval-min-max-range": "डेडुप्लीकेशन इंटरवल कम से कम 1 सेकंड और अधिकतम 1 दिन होना चाहिए।", + "strategy-type": { + "every-message": "हर मैसेज पर", + "skip": "स्किप करें", + "deduplicate": "डेडुप्लीकेट", + "web-sockets-only": "केवल WebSockets" + }, + "time-series": "टाइम सीरीज़", + "latest": "नवीनतम मान", + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "कैल्क्युलेटेड फ़ील्ड्स और अलार्म नियम" + }, + "save-attribute": { + "processing-settings": "प्रोसेसिंग सेटिंग्स", + "processing-settings-hint": "इनकमिंग मैसेज कैसे प्रोसेस किए जाएँगे, यह परिभाषित करें। बेसिक प्रोसेसिंग सेटिंग्स आपको पहले से कॉन्फ़िगर की गई स्ट्रैटेजीज़ चुनने देती हैं, जबकि एडवांस्ड सेटिंग्स में आप हर एक एक्शन के लिए अलग-अलग प्रोसेसिंग स्ट्रैटेजी चुन सकते हैं।", + "advanced-settings-hint": "प्रोसेसिंग स्ट्रैटेजीज़ कॉन्फ़िगर करते समय सावधान रहें। कुछ संयोजन अप्रत्याशित व्यवहार का कारण बन सकते हैं।", + "strategy": "स्ट्रैटेजी", + "deduplication-interval": "डेडुप्लीकेशन इंटरवल", + "deduplication-interval-required": "डेडुप्लीकेशन इंटरवल आवश्यक है।", + "deduplication-interval-min-max-range": "डेडुप्लीकेशन इंटरवल कम से कम 1 सेकंड और अधिकतम 1 दिन होना चाहिए।", + "scope": "स्कोप", + "strategy-type": { + "every-message": "हर मैसेज पर", + "skip": "स्किप करें", + "deduplicate": "डेडुप्लीकेट", + "web-sockets-only": "केवल WebSockets" + }, + "attributes": "विशेषताएँ" + }, + "key-val": { + "key": "कुंजी", + "value": "मान", + "see-examples": "उदाहरण देखें।", + "remove-entry": "एंट्री हटाएँ", + "remove-mapping-entry": "मैपिंग हटाएँ", + "add-mapping-entry": "मैपिंग जोड़ें", + "add-entry": "एंट्री जोड़ें", + "copy-key-values-from": "की-वैल्यूज़ यहाँ से कॉपी करें", + "delete-key-values": "की-वैल्यूज़ हटाएँ", + "delete-key-values-from": "यहाँ से की-वैल्यूज़ हटाएँ", + "at-least-one-key-error": "कम से कम एक कुंजी चुनी जानी चाहिए।", + "unique-key-value-pair-error": "'{{keyText}}' को '{{valText}}' से अलग होना चाहिए!" + }, + "mail-body-types": { + "plain-text": "सादा टेक्स्ट", + "html": "HTML", + "dynamic": "डायनेमिक", + "use-body-type-template": "बॉडी टाइप टेम्पलेट का उपयोग करें", + "plain-text-description": "सरल, बिना फ़ॉर्मैटिंग वाला टेक्स्ट, जिसमें कोई विशेष स्टाइलिंग या फ़ॉर्मैटिंग नहीं होती।", + "html-text-description": "आपको अपने मेल बॉडी में फ़ॉर्मैटिंग, लिंक और इमेज के लिए HTML टैग उपयोग करने की अनुमति देता है।", + "dynamic-text-description": "टेम्पलेटाइज़ेशन फ़ीचर के आधार पर Plain Text या HTML बॉडी टाइप को डायनेमिक रूप से उपयोग करने की अनुमति देता है।", + "after-template-evaluation-hint": "टेम्पलेट के मूल्यांकन के बाद मान HTML के लिए true और Plain text के लिए false होना चाहिए।" + }, + "ai": { + "ai-model": "एआई मॉडल", + "model": "मॉडल", + "ai-model-hint": "इस रूल नोड द्वारा भेजे गए रिक्वेस्ट प्रोसेस करने के लिए प्री-कॉन्फ़िगर एआई मॉडल चुनें, या नया मॉडल कॉन्फ़िगर करने के लिए \"Create new\" का उपयोग करें।", + "prompt-settings": "प्रॉम्प्ट सेटिंग्स", + "prompt-settings-hint": "वैकल्पिक सिस्टम प्रॉम्प्ट एआई की सामान्य भूमिका और सीमाएँ सेट करता है, जबकि यूज़र प्रॉम्प्ट करने वाला विशिष्ट कार्य परिभाषित करता है। दोनों फ़ील्ड टेम्पलेटाइज़ेशन को सपोर्ट करते हैं।", + "system-prompt": "सिस्टम प्रॉम्प्ट", + "system-prompt-max-length": "सिस्टम प्रॉम्प्ट 500000 अक्षरों से अधिक नहीं होना चाहिए।", + "system-prompt-blank": "सिस्टम प्रॉम्प्ट खाली नहीं होना चाहिए।", + "user-prompt": "यूज़र प्रॉम्प्ट", + "user-prompt-required": "यूज़र प्रॉम्प्ट आवश्यक है।", + "user-prompt-max-length": "यूज़र प्रॉम्प्ट 500000 अक्षरों से अधिक नहीं होना चाहिए।", + "user-prompt-blank": "यूज़र प्रॉम्प्ट खाली नहीं होना चाहिए।", + "response-format": "उत्तर फ़ॉर्मैट", + "response-text": "टेक्स्ट", + "response-json": "JSON", + "response-json-schema": "JSON Schema", + "response-format-hint-TEXT": "मॉडल को मनचाहा टेक्स्ट जनरेट करने की अनुमति देता है, जो वैध JSON ऑब्जेक्ट हो भी सकता है और नहीं भी। अगर आउटपुट वैध JSON ऑब्जेक्ट नहीं है, तो इसे अपने-आप \"response\" कुंजी के अंदर एक JSON ऑब्जेक्ट में लपेट दिया जाएगा।", + "response-format-hint-JSON": "मॉडल से आवश्यक है कि वह वैध JSON के रूप में उत्तर जनरेट करे। अगर आउटपुट वैध JSON ऑब्जेक्ट नहीं है, तो इसे अपने-आप \"response\" कुंजी के अंदर एक JSON ऑब्जेक्ट में लपेट दिया जाएगा।", + "response-format-hint-JSON_SCHEMA": "मॉडल से आवश्यक है कि वह दिए गए स्कीमा में परिभाषित स्ट्रक्चर और डेटा टाइप से मेल खाता JSON जनरेट करे। अगर आउटपुट वैध JSON ऑब्जेक्ट नहीं है, तो इसे अपने-आप \"response\" कुंजी के अंदर एक JSON ऑब्जेक्ट में लपेट दिया जाएगा।", + "response-json-schema-hint": "आप कोई भी वैध JSON Schema दर्ज कर सकते हैं, लेकिन यह रूल नोड उसके केवल सीमित फ़ीचर्स को सपोर्ट करता है। विवरण के लिए नोड डॉक्यूमेंटेशन देखें।", + "response-json-schema-required": "JSON Schema आवश्यक है।", + "advanced-settings": "एडवांस्ड सेटिंग्स", + "timeout": "टाइमआउट", + "timeout-hint": "रिक्वेस्ट समाप्त होने से पहले एआई मॉडल से उत्तर की प्रतीक्षा करने का अधिकतम समय।", + "timeout-required": "टाइमआउट आवश्यक है।", + "timeout-validation": "1 सेकंड से 10 मिनट के बीच होना चाहिए।", + "force-acknowledgement": "फ़ोर्स ऐक्नॉलेजमेंट", + "force-acknowledgement-hint": "अगर सक्षम है, तो इनकमिंग मैसेज को तुरंत ऐक्नॉलेज कर दिया जाएगा। इसके बाद मॉडल का उत्तर एक अलग, नए मैसेज के रूप में क्यू में डाला जाएगा।", + "ai-resources": "एआई रिसोर्सेज़" + } + }, + "timezone": { + "timezone": "समय ज़ोन", + "select-timezone": "समय ज़ोन चुनें", + "no-timezones-matching": "'{{timezone}}' से मेल खाते कोई समय ज़ोन नहीं मिले।", + "timezone-required": "समय ज़ोन आवश्यक है।", + "browser-time": "ब्राउज़र समय" + }, + "queue": { + "queue-name": "क्यू", + "no-queues-found": "कोई क्यू नहीं मिली।", + "no-queues-matching": "'{{queue}}' से मेल खाती कोई क्यू नहीं मिली।", + "select-name": "क्यू नाम चुनें", + "name": "नाम", + "name-required": "क्यू नाम आवश्यक है!", + "name-unique": "क्यू नाम अद्वितीय नहीं है!", + "name-pattern": "क्यू नाम में ASCII अल्फ़ान्यूमेरिक, '.', '_' और '-' के अलावा अन्य कैरेक्टर नहीं होना चाहिए!", + "queue-required": "क्यू आवश्यक है!", + "topic-required": "क्यू टॉपिक आवश्यक है!", + "poll-interval-required": "पोल इंटरवल आवश्यक है!", + "poll-interval-min-value": "पोल इंटरवल का मान 1 से कम नहीं हो सकता।", + "partitions-required": "पार्टिशन आवश्यक हैं!", + "partitions-min-value": "पार्टिशन का मान 1 से कम नहीं हो सकता।", + "pack-processing-timeout-required": "प्रोसेसिंग टाइमआउट आवश्यक है।", + "pack-processing-timeout-min-value": "प्रोसेसिंग टाइमआउट का मान 1 से कम नहीं हो सकता।", + "batch-size-required": "बैच साइज आवश्यक है!", + "batch-size-min-value": "बैच साइज का मान 1 से कम नहीं हो सकता।", + "retries-required": "रीट्राइज़ आवश्यक हैं!", + "retries-min-value": "रीट्राइज़ का मान ऋणात्मक नहीं हो सकता।", + "failure-percentage-required": "फेल्योर प्रतिशत आवश्यक है!", + "failure-percentage-min-value": "फेल्योर प्रतिशत का मान 0 से कम नहीं हो सकता।", + "failure-percentage-max-value": "फेल्योर प्रतिशत का मान 100 से ज़्यादा नहीं हो सकता।", + "pause-between-retries-required": "रीट्राइज़ के बीच का पॉज़ आवश्यक है!", + "pause-between-retries-min-value": "रीट्राइज़ के बीच पॉज़ का मान 1 से कम नहीं हो सकता।", + "max-pause-between-retries-required": "रीट्राइज़ के बीच अधिकतम पॉज़ आवश्यक है!", + "max-pause-between-retries-min-value": "रीट्राइज़ के बीच अधिकतम पॉज़ का मान 1 से कम नहीं हो सकता।", + "submit-strategy-type-required": "सबमिट स्ट्रैटजी टाइप आवश्यक है!", + "processing-strategy-type-required": "प्रोसेसिंग स्ट्रैटजी टाइप आवश्यक है!", + "queues": "क्यूज़", + "selected-queues": "{ count, plural, =1 {1 क्यू} other {# क्यूज़} } चयनित", + "delete-queue-title": "क्या आप वाकई क्यू '{{queueName}}' को हटाना चाहते हैं?", + "delete-queues-title": "क्या आप वाकई { count, plural, =1 {1 क्यू} other {# क्यूज़} } हटाना चाहते हैं?", + "delete-queue-text": "सावधान रहें, पुष्टि के बाद यह क्यू और इससे जुड़ा सारा डेटा हमेशा के लिए हट जाएगा।", + "delete-queues-text": "पुष्टि के बाद चुनी गई सभी क्यूज़ हटा दी जाएँगी और उपलब्ध नहीं रहेंगी।", + "search": "क्यू खोजें", + "add": "क्यू जोड़ें", + "details": "क्यू विवरण", + "topic": "टॉपिक", + "submit-settings": "सबमिट सेटिंग्स", + "submit-strategy": "स्ट्रैटजी टाइप *", + "grouping-parameter": "ग्रूपिंग पैरामीटर", + "processing-settings": "रीट्राइज़ प्रोसेसिंग सेटिंग्स", + "processing-strategy": "प्रोसेसिंग टाइप *", + "retries-settings": "रीट्राइज़ सेटिंग्स", + "polling-settings": "पोलिंग सेटिंग्स", + "batch-processing": "बैच प्रोसेसिंग", + "poll-interval": "पोल इंटरवल", + "partitions": "पार्टिशन", + "immediate-processing": "तुरंत प्रोसेसिंग", + "consumer-per-partition": "प्रत्येक कंज़्यूमर के लिए मैसेज पोल भेजें", + "consumer-per-partition-hint": "प्रत्येक पार्टिशन के लिए अलग-अलग कंज़्यूमर सक्षम करें", + "duplicate-msg-to-all-partitions": "संदेश को सभी पार्टिशन में डुप्लिकेट करें", + "processing-timeout": "प्रोसेसिंग समय, ms", + "batch-size": "बैच साइज", + "retries": "रीट्राइज़ की संख्या (0 – अनलिमिटेड)", + "failure-percentage": "रीट्राइज़ छोड़ने के लिए फेल्योर संदेश, %", + "pause-between-retries": "रीट्राइज़ अंतराल, सेकंड", + "max-pause-between-retries": "अतिरिक्त रीट्राइज़ अंतराल, सेकंड", + "delete": "क्यू हटाएँ", + "copyId": "क्यू Id कॉपी करें", + "idCopiedMessage": "क्यू Id क्लिपबोर्ड पर कॉपी कर दी गई है", + "description": "विवरण", + "description-hint": "यह टेक्स्ट चुनी गई स्ट्रैटजी की जगह क्यू विवरण में दिखाया जाएगा", + "alt-description": "सबमिट स्ट्रैटजी: {{submitStrategy}}, प्रोसेसिंग स्ट्रैटजी: {{processingStrategy}}", + "custom-properties": "कस्टम प्रॉपर्टीज़", + "custom-properties-hint": "कस्टम क्यू (टॉपिक) क्रिएशन प्रॉपर्टीज़, उदाहरण: 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "ओरिजिनेटर के अनुसार सीक्वेंशियल", + "sequential-by-originator-hint": "उदाहरण के लिए डिवाइस A के लिए नया संदेश तब तक सबमिट नहीं होगा जब तक डिवाइस A के लिए पिछला संदेश अकनॉलेज न हो जाए", + "sequential-by-tenant-label": "टेनेंट के अनुसार सीक्वेंशियल", + "sequential-by-tenant-hint": "उदाहरण के लिए टेनेंट A के लिए नया संदेश तब तक सबमिट नहीं होगा जब तक टेनेंट A के लिए पिछला संदेश अकनॉलेज न हो जाए", + "sequential-label": "सीक्वेंशियल", + "sequential-hint": "नया संदेश तब तक सबमिट नहीं होगा जब तक पिछला संदेश अकनॉलेज न हो जाए", + "burst-label": "बर्स्ट", + "burst-hint": "सभी संदेश उनके आने के क्रम में रूल चेन्स को सबमिट किए जाते हैं", + "batch-label": "बैच", + "batch-hint": "नया बैच तब तक सबमिट नहीं होगा जब तक पिछला बैच अकनॉलेज न हो जाए", + "skip-all-failures-label": "सभी फेल्योर को स्किप करें", + "skip-all-failures-hint": "सभी फेल्योर को इग्नोर करें", + "skip-all-failures-and-timeouts-label": "सभी फेल्योर और टाइमआउट्स को स्किप करें", + "skip-all-failures-and-timeouts-hint": "सभी फेल्योर और टाइमआउट्स को इग्नोर करें", + "retry-all-label": "सब पर रीट्राइज़ करें", + "retry-all-hint": "प्रोसेसिंग पैक के सभी संदेशों पर रीट्राइज़ करें", + "retry-failed-label": "फेल्ड पर रीट्राइज़ करें", + "retry-failed-hint": "प्रोसेसिंग पैक के सभी फेल्ड संदेशों पर रीट्राइज़ करें", + "retry-timeout-label": "टाइमआउट पर रीट्राइज़ करें", + "retry-timeout-hint": "प्रोसेसिंग पैक के सभी टाइमआउट हुए संदेशों पर रीट्राइज़ करें", + "retry-failed-and-timeout-label": "फेल्ड और टाइमआउट पर रीट्राइज़ करें", + "retry-failed-and-timeout-hint": "प्रोसेसिंग पैक के सभी फेल्ड और टाइमआउट हुए संदेशों पर रीट्राइज़ करें" + } + }, + "queue-statistics": { + "queue-statistics": "क्यू सांख्यिकी", + "no-queue-statistics-matching": "कोई क्यू सांख्यिकी '{{entity}}' से मिलती नहीं मिली।", + "queue-statistics-required": "क्यू सांख्यिकी आवश्यक है।", + "list-of-queue-statistics": "{ count, plural, =1 {एक क्यू सांख्यिकी} other {# क्यू सांख्यिकी की सूची} }", + "selected-queue-statistics": "{ count, plural, =1 {1 क्यू सांख्यिकी} other {# क्यू सांख्यिकी} } चयनित", + "no-queue-statistics-text": "कोई क्यू सांख्यिकी नहीं मिली", + "queue-statistics-starts-with": "वे क्यू सांख्यिकी जिनके नाम '{{prefix}}' से शुरू होते हैं" + }, + "server-error": { + "general": "सामान्य सर्वर त्रुटि", + "authentication": "प्रमाणीकरण त्रुटि", + "jwt-token-expired": "JWT टोकन की समय-सीमा समाप्त हो गई", + "tenant-trial-expired": "टेनेंट ट्रायल की समय-सीमा समाप्त हो गई", + "credentials-expired": "क्रेडेंशियल्स की समय-सीमा समाप्त हो गई", + "permission-denied": "अनुमति अस्वीकृत", + "invalid-arguments": "अमान्य आर्ग्यूमेंट्स", + "bad-request-params": "खराब रिक्वेस्ट पैरामीटर्स", + "item-not-found": "आइटम नहीं मिला", + "too-many-requests": "बहुत अधिक रिक्वेस्ट", + "too-many-updates": "बहुत अधिक अपडेट्स", + "entities-limit-exceeded": "एंटिटीज़ की सीमा पार हो गई है" + }, + "tenant": { + "tenant": "टेनेंट", + "tenants": "टेनेंट्स", + "management": "टेनेंट प्रबंधन", + "add": "टेनेंट जोड़ें", + "admins": "प्रশासक", + "manage-tenant-admins": "टेनेंट प्रशासकों को प्रबंधित करें", + "delete": "टेनेंट हटाएँ", + "add-tenant-text": "नया टेनेंट जोड़ें", + "no-tenants-text": "कोई टेनेंट नहीं मिला", + "tenant-details": "टेनेंट विवरण", + "title-max-length": "शीर्षक 256 अक्षरों से कम होना चाहिए", + "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 {# टेनेंट्स} } चयनित", + "isolated-tb-rule-engine": "आइसोलेटेड थिंग्सबोर्ड रूल इंजन क्यूज़ का उपयोग करें", + "isolated-tb-rule-engine-details": "हर टेनेंट के पास समर्पित रूल इंजन क्यूज़ होंगी।" + }, + "tenant-profile": { + "tenant-profile": "टेनेंट प्रोफ़ाइल", + "tenant-profiles": "टेनेंट प्रोफ़ाइल्स", + "add": "टेनेंट प्रोफ़ाइल जोड़ें", + "add-profile": "प्रोफ़ाइल जोड़ें", + "debug": "डीबग", + "edit": "टेनेंट प्रोफ़ाइल संपादित करें", + "tenant-profile-details": "टेनेंट प्रोफ़ाइल विवरण", + "no-tenant-profiles-text": "कोई टेनेंट प्रोफ़ाइल नहीं मिली", + "name-max-length": "नाम 256 अक्षरों से कम होना चाहिए", + "search": "टेनेंट प्रोफ़ाइल खोजें", + "selected-tenant-profiles": "{ count, plural, =1 {1 टेनेंट प्रोफ़ाइल} other {# टेनेंट प्रोफ़ाइल्स} } चयनित", + "no-tenant-profiles-matching": "कोई टेनेंट प्रोफ़ाइल '{{entity}}' से मेल नहीं खाती मिली।", + "tenant-profile-required": "टेनेंट प्रोफ़ाइल आवश्यक है", + "idCopiedMessage": "टेनेंट प्रोफ़ाइल Id क्लिपबोर्ड पर कॉपी कर दी गई है", + "set-default": "टेनेंट प्रोफ़ाइल को डिफ़ॉल्ट बनाएं", + "delete": "टेनेंट प्रोफ़ाइल हटाएँ", + "copyId": "टेनेंट प्रोफ़ाइल Id कॉपी करें", + "name": "नाम", + "name-required": "नाम आवश्यक है।", + "data": "प्रोफ़ाइल डेटा", + "profile-configuration": "प्रोफ़ाइल कॉन्फ़िगरेशन", + "description": "विवरण", + "default": "डिफ़ॉल्ट", + "delete-tenant-profile-title": "क्या आप वाकई टेनेंट प्रोफ़ाइल '{{tenantProfileName}}' को हटाना चाहते हैं?", + "delete-tenant-profile-text": "सावधान रहें, पुष्टि के बाद टेनेंट प्रोफ़ाइल और उससे जुड़ा सारा डेटा स्थायी रूप से मिट जाएगा।", + "delete-tenant-profiles-title": "क्या आप वाकई { count, plural, =1 {1 टेनेंट प्रोफ़ाइल} other {# टेनेंट प्रोफ़ाइल्स} } को हटाना चाहते हैं?", + "delete-tenant-profiles-text": "सावधान रहें, पुष्टि के बाद सभी चयनित टेनेंट प्रोफ़ाइल्स हटा दी जाएँगी और उनसे जुड़ा सारा डेटा स्थायी रूप से मिट जाएगा।", + "set-default-tenant-profile-title": "क्या आप वाकई टेनेंट प्रोफ़ाइल '{{tenantProfileName}}' को डिफ़ॉल्ट बनाना चाहते हैं?", + "set-default-tenant-profile-text": "पुष्टि के बाद यह टेनेंट प्रोफ़ाइल डिफ़ॉल्ट के रूप में चिह्नित हो जाएगी और ऐसे नए टेनेंट्स के लिए उपयोग होगी जिनके लिए कोई प्रोफ़ाइल निर्दिष्ट नहीं है।", + "no-tenant-profiles-found": "कोई टेनेंट प्रोफ़ाइल नहीं मिली।", + "create-new-tenant-profile": "नई बनाएँ!", + "create-tenant-profile": "नई टेनेंट प्रोफ़ाइल बनाएँ", + "import": "टेनेंट प्रोफ़ाइल आयात करें", + "export": "टेनेंट प्रोफ़ाइल निर्यात करें", + "export-failed-error": "टेनेंट प्रोफ़ाइल निर्यात करने में असमर्थ: {{error}}", + "tenant-profile-file": "टेनेंट प्रोफ़ाइल फ़ाइल", + "invalid-tenant-profile-file-error": "टेनेंट प्रोफ़ाइल आयात करने में असमर्थ: अमान्य टेनेंट प्रोफ़ाइल डेटा संरचना।", + "advanced-settings": "उन्नत सेटिंग्स", + "entities": "एंटिटीज़", + "rule-engine": "रूल इंजन", + "time-to-live": "टाइम-टू-लाइव", + "calculated-fields": "कैल्क्युलेटेड फ़ील्ड्स", + "alarms-and-notifications": "अलार्म और अधिसूचनाएँ", + "ota-files-in-bytes": "फ़ाइलें", + "ws-title": "WS", + "unlimited": "(0 - अनलिमिटेड)", + "maximum-devices": "डिवाइस की अधिकतम संख्या", + "maximum-devices-required": "डिवाइस की अधिकतम संख्या आवश्यक है।", + "maximum-devices-range": "डिवाइस की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "maximum-assets": "एसेट की अधिकतम संख्या", + "maximum-assets-required": "एसेट की अधिकतम संख्या आवश्यक है।", + "maximum-assets-range": "एसेट की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "maximum-customers": "कस्टमर की अधिकतम संख्या", + "maximum-customers-required": "कस्टमर की अधिकतम संख्या आवश्यक है।", + "maximum-customers-range": "कस्टमर की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "maximum-users": "उपयोगकर्ताओं की अधिकतम संख्या", + "maximum-users-required": "उपयोगकर्ताओं की अधिकतम संख्या आवश्यक है।", + "maximum-users-range": "उपयोगकर्ताओं की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "maximum-dashboards": "डैशबोर्ड्स की अधिकतम संख्या", + "maximum-dashboards-required": "डैशबोर्ड्स की अधिकतम संख्या आवश्यक है।", + "maximum-dashboards-range": "डैशबोर्ड्स की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "maximum-edges": "एज की अधिकतम संख्या", + "maximum-edges-required": "एज की अधिकतम संख्या आवश्यक है।", + "maximum-edges-range": "एज की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "maximum-rule-chains": "रूल चेन्स की अधिकतम संख्या", + "maximum-rule-chains-required": "रूल चेन्स की अधिकतम संख्या आवश्यक है।", + "maximum-rule-chains-range": "रूल चेन्स की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "maximum-resources-sum-data-size": "रिसोर्स फ़ाइलों के कुल अधिकतम आकार (बाइट्स)", + "maximum-resources-sum-data-size-required": "रिसोर्स फ़ाइलों के कुल अधिकतम आकार की मान आवश्यक है।", + "maximum-resources-sum-data-size-range": "रिसोर्स फ़ाइलों के कुल अधिकतम आकार का मान नकारात्मक नहीं हो सकता", + "maximum-resource-size": "अधिकतम रिसोर्स फ़ाइल आकार (बाइट्स)", + "maximum-resource-size-required": "अधिकतम रिसोर्स फ़ाइल आकार आवश्यक है", + "maximum-resource-size-range": "अधिकतम रिसोर्स फ़ाइल आकार नकारात्मक नहीं हो सकता", + "maximum-ota-packages-sum-data-size": "OTA पैकेज फ़ाइलों के कुल अधिकतम आकार (बाइट्स)", + "maximum-ota-package-sum-data-size-required": "OTA पैकेज फ़ाइलों के कुल अधिकतम आकार की मान आवश्यक है।", + "maximum-ota-package-sum-data-size-range": "OTA पैकेज फ़ाइलों के कुल अधिकतम आकार का मान नकारात्मक नहीं हो सकता", + "maximum-debug-duration-min": "अधिकतम डीबग अवधि (मिनट)", + "maximum-debug-duration-min-range": "अधिकतम डीबग अवधि नकारात्मक नहीं हो सकती", + "rest-requests-for-tenant": "टेनेंट के लिए REST रिक्वेस्ट्स", + "transport-tenant-telemetry-msg-rate-limit": "ट्रांसपोर्ट टेनेंट टेलीमेट्री मैसेजेस", + "transport-tenant-telemetry-data-points-rate-limit": "ट्रांसपोर्ट टेनेंट टेलीमेट्री डेटा पॉइंट्स", + "transport-device-msg-rate-limit": "ट्रांसपोर्ट डिवाइस मैसेजेस", + "transport-device-telemetry-msg-rate-limit": "ट्रांसपोर्ट डिवाइस टेलीमेट्री मैसेजेस", + "transport-device-telemetry-data-points-rate-limit": "ट्रांसपोर्ट डिवाइस टेलीमेट्री डेटा पॉइंट्स", + "transport-gateway-msg-rate-limit": "ट्रांसपोर्ट Gateway संदेश", + "transport-gateway-telemetry-msg-rate-limit": "ट्रांसपोर्ट Gateway टेलीमेट्री संदेश", + "transport-gateway-telemetry-data-points-rate-limit": "ट्रांसपोर्ट Gateway टेलीमेट्री डेटा पॉइंट्स", + "transport-gateway-device-msg-rate-limit": "ट्रांसपोर्ट Gateway डिवाइस संदेश", + "transport-gateway-device-telemetry-msg-rate-limit": "ट्रांसपोर्ट Gateway डिवाइस टेलीमेट्री संदेश", + "transport-gateway-device-telemetry-data-points-rate-limit": "ट्रांसपोर्ट Gateway डिवाइस टेलीमेट्री डेटा पॉइंट्स", + "tenant-entity-export-rate-limit": "एंटिटी वर्ज़न क्रिएशन", + "tenant-entity-import-rate-limit": "एंटिटी वर्ज़न लोड", + "tenant-notification-request-rate-limit": "अधिसूचना रिक्वेस्ट्स", + "tenant-notification-requests-per-rule-rate-limit": "प्रति नोटिफिकेशन रूल अधिसूचना रिक्वेस्ट्स", + "max-calculated-fields": "प्रति एंटिटी कैलक्युलेटेड फ़ील्ड्स की अधिकतम संख्या", + "max-calculated-fields-range": "प्रति एंटिटी कैलक्युलेटेड फ़ील्ड्स की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-calculated-fields-required": "प्रति एंटिटी कैलक्युलेटेड फ़ील्ड्स की अधिकतम संख्या आवश्यक है", + "max-data-points-per-rolling-arg": "रोलिंग आर्ग्युमेंट्स में डेटा पॉइंट्स की अधिकतम संख्या", + "max-data-points-per-rolling-arg-range": "रोलिंग आर्ग्युमेंट्स में डेटा पॉइंट्स की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-data-points-per-rolling-arg-required": "रोलिंग आर्ग्युमेंट्स में डेटा पॉइंट्स की अधिकतम संख्या आवश्यक है", + "max-arguments-per-cf": "प्रति कैलक्युलेटेड फ़ील्ड आर्ग्युमेंट्स की अधिकतम संख्या", + "max-arguments-per-cf-range": "प्रति कैलक्युलेटेड फ़ील्ड आर्ग्युमेंट्स की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-arguments-per-cf-required": "प्रति कैलक्युलेटेड फ़ील्ड आर्ग्युमेंट्स की अधिकतम संख्या आवश्यक है", + "max-related-level-per-argument": "'संबंधित एंटिटीज़' तर्क के प्रति अधिकतम रिलेशन स्तर", + "max-related-level-per-argument-range": "'संबंधित एंटिटीज़' तर्क के प्रति रिलेशन स्तर की अधिकतम संख्या '1' से कम नहीं हो सकती", + "max-related-level-per-argument-required": "'संबंधित एंटिटीज़' तर्क के प्रति रिलेशन स्तर की अधिकतम संख्या आवश्यक है", + "min-allowed-scheduled-update-interval": "'संबंधित एंटिटीज़' तर्कों के लिए न्यूनतम अनुमत अपडेट अंतराल (सेकंड)", + "min-allowed-scheduled-update-interval-range": "न्यूनतम अनुमत अपडेट अंतराल का मान नकारात्मक नहीं हो सकता", + "min-allowed-deduplication-interval": "न्यूनतम अनुमत डेडुप्लिकेशन अंतराल (सेकंड)", + "min-allowed-deduplication-interval-range": "न्यूनतम अनुमत डेडुप्लिकेशन अंतराल का मान नकारात्मक नहीं हो सकता", + "min-allowed-deduplication-interval-required": "न्यूनतम अनुमत डेडुप्लिकेशन अंतराल आवश्यक है", + "intermediate-aggregation-interval": "मध्यवर्ती संकलन अंतराल (सेकंड)", + "intermediate-aggregation-interval-range": "मध्यवर्ती संकलन अंतराल का मान '1' से कम नहीं हो सकता", + "intermediate-aggregation-interval-required": "मध्यवर्ती संकलन अंतराल आवश्यक है", + "reevaluation-check-interval": "पुनः मूल्यांकन चेक अंतराल (सेकंड)", + "reevaluation-check-interval-range": "पुनः मूल्यांकन चेक अंतराल का मान '1' से कम नहीं हो सकता", + "reevaluation-check-interval-required": "पुनः मूल्यांकन चेक अंतराल आवश्यक है", + "alarms-reevaluation-interval": "अलार्म पुनः मूल्यांकन अंतराल (सेकंड)", + "alarms-reevaluation-interval-range": "अलार्म पुनः मूल्यांकन अंतराल का मान '1' से कम नहीं हो सकता", + "alarms-reevaluation-interval-required": "अलार्म पुनः मूल्यांकन अंतराल आवश्यक है", + "min-allowed-aggregation-interval": "न्यूनतम अनुमत संकलन अंतराल (सेकंड)", + "min-allowed-aggregation-interval-range": "न्यूनतम अनुमत संकलन अंतराल का मान नकारात्मक नहीं हो सकता", + "min-allowed-aggregation-interval-required": "न्यूनतम अनुमत संकलन अंतराल आवश्यक है", + "min-allowed-scheduled-update-interval-required": "न्यूनतम अनुमत अपडेट अंतराल का न्यूनतम मान आवश्यक है", + "max-state-size": "स्टेट का अधिकतम आकार (KB में)", + "max-state-size-range": "स्टेट का अधिकतम आकार (KB में) नकारात्मक नहीं हो सकता", + "max-state-size-required": "स्टेट का अधिकतम आकार (KB में) आवश्यक है", + "max-value-argument-size": "सिंगल वैल्यू आर्ग्युमेंट का अधिकतम आकार (KB में)", + "max-value-argument-size-range": "सिंगल वैल्यू आर्ग्युमेंट का अधिकतम आकार (KB में) नकारात्मक नहीं हो सकता", + "max-value-argument-size-required": "सिंगल वैल्यू आर्ग्युमेंट का अधिकतम आकार (KB में) आवश्यक है", + "max-transport-messages": "ट्रांसपोर्ट मैसेजेस की अधिकतम संख्या", + "max-transport-messages-required": "ट्रांसपोर्ट मैसेजेस की अधिकतम संख्या आवश्यक है।", + "max-transport-messages-range": "ट्रांसपोर्ट मैसेजेस की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-transport-data-points": "ट्रांसपोर्ट डेटा पॉइंट्स की अधिकतम संख्या", + "max-transport-data-points-required": "ट्रांसपोर्ट डेटा पॉइंट्स की अधिकतम संख्या आवश्यक है।", + "max-transport-data-points-range": "ट्रांसपोर्ट डेटा पॉइंट्स की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-r-e-executions": "रूल इंजन निष्पादनों की अधिकतम संख्या", + "max-r-e-executions-required": "रूल इंजन निष्पादनों की अधिकतम संख्या आवश्यक है.", + "max-r-e-executions-range": "रूल इंजन निष्पादनों की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-j-s-executions": "जावास्क्रिप्ट निष्पादनों की अधिकतम संख्या", + "max-j-s-executions-required": "जावास्क्रिप्ट निष्पादनों की अधिकतम संख्या आवश्यक है.", + "max-j-s-executions-range": "जावास्क्रिप्ट निष्पादनों की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-tbel-executions": "TBEL निष्पादनों की अधिकतम संख्या", + "max-tbel-executions-required": "TBEL निष्पादनों की अधिकतम संख्या आवश्यक है.", + "max-tbel-executions-range": "TBEL निष्पादनों की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-d-p-storage-days": "डेटा पॉइंट्स स्टोरेज दिनों की अधिकतम संख्या", + "max-d-p-storage-days-required": "डेटा पॉइंट्स स्टोरेज दिनों की अधिकतम संख्या आवश्यक है.", + "max-d-p-storage-days-range": "डेटा पॉइंट्स स्टोरेज दिनों की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "default-storage-ttl-days": "डिफॉल्ट स्टोरेज TTL (दिनों में)", + "default-storage-ttl-days-required": "डिफॉल्ट स्टोरेज TTL (दिनों में) आवश्यक है.", + "default-storage-ttl-days-range": "डिफॉल्ट स्टोरेज TTL (दिनों में) नकारात्मक नहीं हो सकता", + "alarms-ttl-days": "अलार्म्स TTL (दिनों में)", + "alarms-ttl-days-required": "अलार्म्स TTL (दिनों में) आवश्यक है", + "alarms-ttl-days-days-range": "अलार्म्स TTL (दिनों में) नकारात्मक नहीं हो सकता", + "rpc-ttl-days": "RPC TTL (दिनों में)", + "rpc-ttl-days-required": "RPC TTL (दिनों में) आवश्यक है", + "rpc-ttl-days-days-range": "RPC TTL (दिनों में) नकारात्मक नहीं हो सकता", + "queue-stats-ttl-days": "क्यू स्टैट्स TTL (दिनों में)", + "queue-stats-ttl-days-required": "क्यू स्टैट्स TTL (दिनों में) आवश्यक है", + "queue-stats-ttl-days-range": "क्यू स्टैट्स TTL (दिनों में) नकारात्मक नहीं हो सकता", + "rule-engine-exceptions-ttl-days": "रूल इंजन एक्सेप्शन्स TTL (दिनों में)", + "rule-engine-exceptions-ttl-days-required": "रूल इंजन एक्सेप्शन्स TTL (दिनों में) आवश्यक है", + "rule-engine-exceptions-ttl-days-range": "रूल इंजन एक्सेप्शन्स TTL (दिनों में) नकारात्मक नहीं हो सकता", + "max-rule-node-executions-per-message": "प्रति संदेश रूल नोड निष्पादनों की अधिकतम संख्या", + "max-rule-node-executions-per-message-required": "प्रति संदेश रूल नोड निष्पादनों की अधिकतम संख्या आवश्यक है.", + "max-rule-node-executions-per-message-range": "प्रति संदेश रूल नोड निष्पादनों की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-emails": "भेजे गए ईमेल की अधिकतम संख्या", + "max-emails-required": "भेजे गए ईमेल की अधिकतम संख्या आवश्यक है.", + "max-emails-range": "भेजे गए ईमेल की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "sms-enabled": "एसएमएस सक्षम", + "max-sms": "भेजे गए SMS की अधिकतम संख्या", + "max-sms-required": "भेजे गए SMS की अधिकतम संख्या आवश्यक है.", + "max-sms-range": "भेजे गए SMS की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "max-created-alarms": "बनाए गए अलार्म्स की अधिकतम संख्या", + "max-created-alarms-required": "बनाए गए अलार्म्स की अधिकतम संख्या आवश्यक है.", + "max-created-alarms-range": "बनाए गए अलार्म्स की अधिकतम संख्या नकारात्मक नहीं हो सकती", + "no-queue": "कोई कतार कॉन्फ़िगर नहीं है", + "add-queue": "कतार जोड़ें", + "queues-with-count": "कतारें ({{count}})", + "tenant-rest-limits": "टेनेंट के लिए REST अनुरोध", + "customer-rest-limits": "कस्टमर के लिए REST अनुरोध", + "incorrect-pattern-for-rate-limits": "फ़ॉर्मेट क्षमता और अवधि (सेकंड में) की कॉमा से अलग की गई जोड़ियों का होता है, जिनके बीच कॉलन होता है, जैसे 100:1,2000:60", + "too-small-value-zero": "मान 0 से बड़ा होना चाहिए", + "too-small-value-one": "मान 1 से बड़ा होना चाहिए", + "queue-size-is-limited-by-system-configuration": "कतार का आकार सिस्टम कॉन्फ़िगरेशन द्वारा भी सीमित होता है.", + "cassandra-write-tenant-core-limits-configuration": "REST API Cassandra लेखन क्वेरीज़", + "cassandra-read-tenant-core-limits-configuration": "REST API और WS टेलीमेट्री Cassandra पठन क्वेरीज़", + "cassandra-write-tenant-rule-engine-limits-configuration": "रूल इंजन टेलीमेट्री Cassandra लेखन क्वेरीज़", + "cassandra-read-tenant-rule-engine-limits-configuration": "रूल इंजन टेलीमेट्री Cassandra पठन क्वेरीज़", + "ws-limit-max-sessions-per-tenant": "प्रति टेनेंट सत्रों की अधिकतम संख्या", + "ws-limit-max-sessions-per-customer": "प्रति कस्टमर सत्रों की अधिकतम संख्या", + "ws-limit-max-sessions-per-regular-user": "प्रति सामान्य उपयोगकर्ता सत्रों की अधिकतम संख्या", + "ws-limit-max-sessions-per-public-user": "प्रति सार्वजनिक उपयोगकर्ता सत्रों की अधिकतम संख्या", + "ws-limit-queue-per-session": "प्रति सत्र संदेश कतार का अधिकतम आकार", + "ws-limit-max-subscriptions-per-tenant": "प्रति टेनेंट सब्स्क्रिप्शन्स की अधिकतम संख्या", + "ws-limit-max-subscriptions-per-customer": "प्रति कस्टमर सब्स्क्रिप्शन्स की अधिकतम संख्या", + "ws-limit-max-subscriptions-per-regular-user": "प्रति सामान्य उपयोगकर्ता सब्स्क्रिप्शन्स की अधिकतम संख्या", + "ws-limit-max-subscriptions-per-public-user": "प्रति सार्वजनिक उपयोगकर्ता सब्स्क्रिप्शन्स की अधिकतम संख्या", + "ws-limit-updates-per-session": "प्रति सत्र WS अपडेट्स", + "relation-search-entity-limit": "रिलेशन खोज एंटिटी सीमा", + "relation-search-entity-limit-hint": "रिलेशन पथ के अंतिम स्तर पर हल की गई एंटिटीज़ की संख्या को सीमित करता है। यह 'संबंधित एंटिटीज़' तर्कों और प्रसार फ़ील्ड्स पर लागू होता है।", + "relation-search-entity-limit-required": "रिलेशन खोज एंटिटी सीमा आवश्यक है", + "relation-search-entity-limit-range": "रिलेशन खोज एंटिटी सीमा '1' से कम नहीं हो सकती", + "rate-limits": { + "add-limit": "लिमिट जोड़ें", + "and-also-less-than": "और साथ ही इससे कम", + "advanced-settings": "एडवांस्ड सेटिंग्स", + "edit-limit": "लिमिट एडिट करें", + "calculated-field-debug-event-rate-limit": "कैलक्युलेटेड फ़ील्ड डिबग इवेंट्स", + "edit-calculated-field-debug-event-rate-limit": "कैलक्युलेटेड फ़ील्ड डिबग इवेंट्स के रेट लिमिट्स एडिट करें", + "edit-transport-tenant-msg-title": "ट्रांसपोर्ट टेनेंट संदेशों के रेट लिमिट्स एडिट करें", + "edit-transport-tenant-telemetry-msg-title": "ट्रांसपोर्ट टेनेंट टेलीमेट्री संदेशों के रेट लिमिट्स एडिट करें", + "edit-transport-tenant-telemetry-data-points-title": "ट्रांसपोर्ट टेनेंट टेलीमेट्री डेटा पॉइंट्स के रेट लिमिट्स एडिट करें", + "edit-transport-device-msg-title": "ट्रांसपोर्ट डिवाइस संदेशों के रेट लिमिट्स एडिट करें", + "edit-transport-device-telemetry-msg-title": "ट्रांसपोर्ट डिवाइस टेलीमेट्री संदेशों के रेट लिमिट्स एडिट करें", + "edit-transport-device-telemetry-data-points-title": "ट्रांसपोर्ट डिवाइस टेलीमेट्री डेटा पॉइंट्स के रेट लिमिट्स एडिट करें", + "edit-transport-gateway-msg-title": "ट्रांसपोर्ट Gateway संदेशों की रेट लिमिट्स संपादित करें", + "edit-transport-gateway-telemetry-msg-title": "ट्रांसपोर्ट Gateway टेलीमेट्री संदेशों की रेट लिमिट्स संपादित करें", + "edit-transport-gateway-telemetry-data-points-title": "ट्रांसपोर्ट Gateway टेलीमेट्री डेटा पॉइंट्स की रेट लिमिट्स संपादित करें", + "edit-transport-gateway-device-msg-title": "ट्रांसपोर्ट Gateway डिवाइस संदेशों की रेट लिमिट्स संपादित करें", + "edit-transport-gateway-device-telemetry-msg-title": "ट्रांसपोर्ट Gateway डिवाइस टेलीमेट्री संदेशों की रेट लिमिट्स संपादित करें", + "edit-transport-gateway-device-telemetry-data-points-title": "ट्रांसपोर्ट Gateway डिवाइस टेलीमेट्री डेटा पॉइंट्स की रेट लिमिट्स संपादित करें", + "edit-tenant-rest-limits-title": "टेनेंट के REST अनुरोधों के रेट लिमिट्स एडिट करें", + "edit-customer-rest-limits-title": "कस्टमर के REST अनुरोधों के रेट लिमिट्स एडिट करें", + "edit-ws-limit-updates-per-session-title": "प्रति सत्र WS अपडेट्स के रेट लिमिट्स एडिट करें", + "edit-cassandra-write-tenant-core-limits-configuration": "REST API Cassandra लेखन क्वेरीज़ के रेट लिमिट्स एडिट करें", + "edit-cassandra-read-tenant-core-limits-configuration": "REST API और WS टेलीमेट्री Cassandra पठन क्वेरीज़ के रेट लिमिट्स एडिट करें", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "रूल इंजन टेलीमेट्री Cassandra लेखन क्वेरीज़ के रेट लिमिट्स एडिट करें", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "रूल इंजन टेलीमेट्री Cassandra पठन क्वेरीज़ के रेट लिमिट्स एडिट करें", + "edit-tenant-entity-export-rate-limit-title": "एंटिटी वर्ज़न क्रिएशन के रेट लिमिट्स एडिट करें", + "edit-tenant-entity-import-rate-limit-title": "एंटिटी वर्ज़न लोड के रेट लिमिट्स एडिट करें", + "edit-tenant-notification-request-rate-limit-title": "अधिसूचना रिक्वेस्ट्स के रेट लिमिट्स एडिट करें", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "प्रति नोटिफिकेशन रूल अधिसूचना रिक्वेस्ट्स के रेट लिमिट्स एडिट करें", + "edit-edge-events-rate-limit": "एज इवेंट्स के रेट लिमिट्स एडिट करें", + "edit-edge-events-per-edge-rate-limit": "प्रति एज एज इवेंट्स के रेट लिमिट्स एडिट करें", + "edge-events-rate-limit": "एज इवेंट्स", + "edge-events-per-edge-rate-limit": "प्रति एज एज इवेंट्स", + "edit-edge-uplink-messages-rate-limit": "एज अपलिंक संदेशों के रेट लिमिट्स एडिट करें", + "edit-edge-uplink-messages-per-edge-rate-limit": "प्रति एज एज अपलिंक संदेशों के रेट लिमिट्स एडिट करें", + "edge-uplink-messages-rate-limit": "एज अपलिंक संदेश", + "edge-uplink-messages-per-edge-rate-limit": "प्रति एज एज अपलिंक संदेश", + "messages-per": "संदेश प्रति", + "not-set": "सेट नहीं है", + "number-of-messages": "संदेशों की संख्या", + "number-of-messages-required": "संदेशों की संख्या आवश्यक है.", + "number-of-messages-min": "न्यूनतम मान 1 है.", + "preview": "प्रीव्यू", + "per-seconds": "प्रति सेकंड", + "per-seconds-required": "टाइम रेट आवश्यक है।", + "per-seconds-min": "न्यूनतम मान 1 है।", + "per-seconds-duplicate": "डुप्लिकेट टाइम रेट। हर टाइम इंटरवल यूनिक होना चाहिए।", + "rate-limits": "रेट लिमिट्स", + "remove-limit": "लिमिट हटाएँ", + "transport-tenant-msg": "ट्रांसपोर्ट टेनेंट संदेश", + "transport-tenant-telemetry-msg": "ट्रांसपोर्ट टेनेंट टेलीमेट्री संदेश", + "transport-tenant-telemetry-data-points": "ट्रांसपोर्ट टेनेंट टेलीमेट्री डेटा पॉइंट्स", + "transport-device-msg": "ट्रांसपोर्ट डिवाइस संदेश", + "transport-device-telemetry-msg": "ट्रांसपोर्ट डिवाइस टेलीमेट्री संदेश", + "transport-device-telemetry-data-points": "ट्रांसपोर्ट डिवाइस टेलीमेट्री डेटा पॉइंट्स", + "transport-gateway-msg": "ट्रांसपोर्ट Gateway संदेश", + "transport-gateway-telemetry-msg": "ट्रांसपोर्ट Gateway टेलीमेट्री संदेश", + "transport-gateway-telemetry-data-points": "ट्रांसपोर्ट Gateway टेलीमेट्री डेटा पॉइंट्स", + "transport-gateway-device-msg": "ट्रांसपोर्ट Gateway डिवाइस संदेश", + "transport-gateway-device-telemetry-msg": "ट्रांसपोर्ट Gateway डिवाइस टेलीमेट्री संदेश", + "transport-gateway-device-telemetry-data-points": "ट्रांसपोर्ट Gateway डिवाइस टेलीमेट्री डेटा पॉइंट्स", + "sec": "सेकंड" + } + }, + "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": "एडवांस्ड", + "custom": "कस्टम", + "predefined": { + "yesterday": "कल", + "day-before-yesterday": "परसों", + "this-day-last-week": "पिछले सप्ताह का यही दिन", + "previous-week": "पिछला सप्ताह (रवि - शनि)", + "previous-week-iso": "पिछला सप्ताह (सोम - रवि)", + "previous-month": "पिछला महीना", + "previous-quarter": "पिछली तिमाही", + "previous-half-year": "पिछला आधा वर्ष", + "previous-year": "पिछला वर्ष", + "current-hour": "वर्तमान घंटा", + "current-day": "आज", + "current-day-so-far": "आज तक", + "current-week": "वर्तमान सप्ताह (रवि - शनि)", + "current-week-iso": "वर्तमान सप्ताह (सोम - रवि)", + "current-week-so-far": "वर्तमान सप्ताह अब तक (रवि - शनि)", + "current-week-iso-so-far": "वर्तमान सप्ताह अब तक (सोम - रवि)", + "current-month": "वर्तमान महीना", + "current-month-so-far": "वर्तमान महीना अब तक", + "current-quarter": "वर्तमान तिमाही", + "current-quarter-so-far": "वर्तमान तिमाही अब तक", + "current-half-year": "वर्तमान आधा वर्ष", + "current-half-year-so-far": "वर्तमान आधा वर्ष अब तक", + "current-year": "वर्तमान वर्ष", + "current-year-so-far": "वर्तमान वर्ष अब तक" + }, + "type": { + "week": "सप्ताह (रवि - शनि)", + "week-iso": "सप्ताह (सोम - रवि)", + "month": "महीना", + "quarter": "तिमाही" + } + }, + "timeunit": { + "milliseconds": "मिलीसेकंड", + "seconds": "सेकंड", + "minutes": "मिनट", + "hours": "घंटे", + "days": "दिन" + }, + "timewindow": { + "timewindow": "टाइम विंडो", + "timewindow-settings": "टाइम विंडो सेटिंग्स", + "years": "{ years, plural, =1 {1 वर्ष } other {# वर्ष } }", + "years-short": "{{ years }}y", + "months": "{ months, plural, =1 {1 महीना } other {# महीने } }", + "months-short": "{{ months }}M", + "weeks": "{ weeks, plural, =1 {1 सप्ताह } other {# सप्ताह } }", + "weeks-short": "{{ weeks }}w", + "days": "{ days, plural, =1 {1 दिन } other {# दिन } }", + "days-short": "{{ days }}d", + "hours": "{ hours, plural, =0 { घंटा } =1 {1 घंटा } other {# घंटे } }", + "hr": "{{ hr }} घंटा", + "hr-short": "{{ hr }}h", + "minutes": "{ minutes, plural, =0 { मिनट } =1 {1 मिनट } other {# मिनट } }", + "min": "{{ min }} मिनट", + "min-short": "{{ min }}m", + "seconds": "{ seconds, plural, =0 { सेकंड } =1 {1 सेकंड } other {# सेकंड } }", + "sec": "{{ sec }} सेकंड", + "sec-short": "{{ sec }}s", + "short": { + "years": "{ years, plural, =1 {1 वर्ष } other {# वर्ष } }", + "days": "{ days, plural, =1 {1 दिन } other {# दिन } }", + "hours": "{ hours, plural, =1 {1 घंटा } other {# घंटे } }", + "minutes": "{{minutes}} मिनट ", + "seconds": "{{seconds}} सेकंड " + }, + "realtime": "रीयलटाइम", + "history": "हिस्ट्री", + "last-prefix": "पिछले", + "period": "{{ startTime }} से {{ endTime }} तक", + "edit": "टाइम विंडो संपादित करें", + "date-range": "तिथि सीमा", + "for-all-time": "सभी समय के लिए", + "last": "पिछले", + "time-period": "समय अवधि", + "hide": "छिपाएँ", + "interval": "अंतराल", + "just-now": "अभी-अभी", + "just-now-lower": "अभी-अभी", + "ago": "पहले", + "style": "टाइम विंडो शैली", + "icon": "आइकन", + "icon-position": "आइकन स्थिति", + "icon-position-left": "बाएँ", + "icon-position-right": "दाएँ", + "font": "फ़ॉन्ट", + "color": "रंग", + "displayTypePrefix": "रीयलटाइम/हिस्ट्री प्रीफ़िक्स दिखाएँ", + "preview": "पूर्वावलोकन", + "relative": "सापेक्ष", + "range": "रेंज", + "hide-timewindow-section": "अंतिम उपयोगकर्ताओं से टाइम विंडो सेक्शन छिपाएँ", + "hide-last-interval": "अंतिम उपयोगकर्ताओं से \"पिछले\" अंतराल छिपाएँ", + "hide-relative-interval": "अंतिम उपयोगकर्ताओं से सापेक्ष अंतराल छिपाएँ", + "hide-fixed-interval": "अंतिम उपयोगकर्ताओं से स्थिर अंतराल छिपाएँ", + "hide-aggregation": "अंतिम उपयोगकर्ताओं से एग्रीगेशन छिपाएँ", + "hide-group-interval": "अंतिम उपयोगकर्ताओं से ग्रुपिंग अंतराल छिपाएँ", + "hide-max-values": "अंतिम उपयोगकर्ताओं से अधिकतम मान छिपाएँ", + "hide-timezone": "अंतिम उपयोगकर्ताओं से टाइम ज़ोन छिपाएँ", + "disable-custom-interval": "कस्टम अंतराल चयन निष्क्रिय करें", + "edit-aggregation-functions-list": "एग्रीगेशन फ़ंक्शन्स की सूची संपादित करें", + "edit-aggregation-functions-list-hint": "उपलब्ध विकल्पों की सूची निर्दिष्ट की जा सकती है।", + "allowed-aggregation-functions": "अनुमत एग्रीगेशन फ़ंक्शन्स", + "edit-intervals-list": "अंतराल सूची संपादित करें", + "allowed-agg-intervals": "ग्रुपिंग अंतराल", + "default-agg-interval": "डिफ़ॉल्ट ग्रुपिंग अंतराल", + "edit-intervals-list-hint": "उपलब्ध अंतराल विकल्पों की सूची निर्दिष्ट की जा सकती है।", + "edit-grouping-intervals-list-hint": "ग्रुपिंग अंतरालों की सूची और डिफ़ॉल्ट ग्रुपिंग अंतराल को कॉन्फ़िगर करना संभव है।", + "all": "सभी", + "save-current-settings-as-default": "वर्तमान सेटिंग्स को डिफ़ॉल्ट समय विंडो के रूप में सहेजें", + "hide-option-from-end-users": "एंड-यूज़र्स से विकल्प छुपाएं" + }, + "tooltip": { + "trigger": "ट्रिगर", + "trigger-point": "बिंदु", + "trigger-axis": "अक्ष", + "label": "लेबल", + "value": "मान", + "date": "तिथि", + "show-date-time-interval": "तिथि-समय अंतराल दिखाएँ", + "show-date-time-interval-hint": "डेटा एग्रीगेशन के अनुसार तिथि-समय अंतराल दिखाएँ।", + "hide-zero-tooltip-values": "शून्य मान छिपाएँ", + "show-stack-total": "स्टैक मोड में कुल मान दिखाएँ", + "background-color": "पृष्ठभूमि रंग", + "background-blur": "पृष्ठभूमि ब्लर" + }, + "unit": { + "set-unit-conversion": "इकाई रूपांतरण सेट करें", + "unit-settings": { + "unit-settings": "इकाई सेटिंग्स", + "source-unit": "स्रोत इकाई", + "source-unit-hint": "यह संग्रहित मान की इकाई है, यानी वह इकाई जिससे आप रूपांतरण कर रहे हैं। वह प्रतीक दर्ज करें जो आपका स्रोत डेटा उपयोग करता है (जैसे m, km, ft, in)।", + "target-metric-unit": "टारगेट मेट्रिक इकाई", + "target-metric-unit-hint": "वह मेट्रिक (SI) इकाई चुनें जिसमें आप अपने स्रोत मान को बदलना चाहते हैं (जैसे cm, mm, km)।", + "target-imperial-unit": "टारगेट इम्पीरियल इकाई", + "target-imperial-unit-hint": "वह इम्पीरियल इकाई चुनें जिसमें आप अपने स्रोत मान को बदलना चाहते हैं (जैसे in, ft, yd)।", + "target-hybrid-unit": "टारगेट हाइब्रिड इकाई", + "target-hybrid-unit-hint": "वह हाइब्रिड इकाई चुनें जिसमें आप अपने स्रोत मान को बदलना चाहते हैं (जैसे cm, in, km)। हाइब्रिड इकाइयाँ मेट्रिक या इम्पीरियल इकाइयों को मिलाती हैं।", + "enable-unit-conversion": "इकाई रूपांतरण सक्षम करें", + "enable-unit-conversion-hint": "रूपांतरण सक्रिय करने के लिए ऑन करें। ऑफ होने पर आपका स्रोत मान बिना बदले चला जाएगा। यदि संबंधित माप समूह (जैसे Luminous flux, AQI) में केवल एक ही इकाई हो, तो यह निष्क्रिय रहता है।" + }, + "unit-system": "इकाई सिस्टम", + "unit-system-type": { + "AUTO": "ऑटो", + "METRIC": "मेट्रिक", + "IMPERIAL": "इम्पीरियल", + "HYBRID": "हाइब्रिड" + }, + "measures": { + "absorbed-dose-rate": "अवशोषित डोज दर", + "acceleration": "त्वरण", + "acidity": "अम्लता", + "air-quality-index": "वायु गुणवत्ता सूचकांक", + "amount-of-substance": "पदार्थ की मात्रा", + "angle": "कोण", + "angular-acceleration": "कोणीय त्वरण", + "area": "क्षेत्रफल", + "area-density": "क्षेत्रीय घनत्व", + "capacitance": "धारिता", + "catalytic-activity": "उत्प्रेरक सक्रियता", + "catalytic-concentration": "उत्प्रेरक सान्द्रता", + "charge": "आवेश", + "current-density": "धारा घनत्व", + "data-transfer-rate": "डेटा ट्रांसफ़र दर", + "density": "घनत्व", + "digital": "डिजिटल", + "dimension-ratio": "आयाम अनुपात", + "dynamic-viscosity": "गतिशील श्यानता", + "earthquake-magnitude": "भूकंप परिमाण", + "electric-charge-density": "विद्युत आवेश घनत्व", + "electric-current": "विद्युत धारा", + "electric-dipole-moment": "विद्युत द्विध्रुव आघूर्ण", + "electric-field-strength": "विद्युत क्षेत्र की तीव्रता", + "electric-flux": "विद्युत फ्लक्स", + "electric-permittivity": "विद्युत पारगम्यता", + "electric-polarizability": "विद्युत ध्रुवणक्षमता", + "electrical-conductance": "विद्युत चालकता", + "electrical-conductivity": "विद्युत चालकता गुणांक", + "energy": "ऊर्जा", + "energy-density": "ऊर्जा घनत्व", + "force": "बल", + "frequency": "आवृत्ति", + "fuel-efficiency": "ईंधन दक्षता", + "heat-capacity": "ऊष्मा धारिता", + "illuminance": "प्रकाशितता", + "inductance": "प्रेरकत्व", + "kinematic-viscosity": "गतिज श्यानता", + "length": "लंबाई", + "light-exposure": "प्रकाश एक्सपोज़र", + "linear-charge-density": "रेखीय आवेश घनत्व", + "logarithmic-ratio": "लघुगणकीय अनुपात", + "luminous-efficacy": "आलोक दक्षता", + "luminous-flux": "आलोक फ्लक्स", + "luminous-intensity": "आलोक तीव्रता", + "magnetic-field-gradient": "चुंबकीय क्षेत्र ग्रेडिएंट", + "magnetic-flux": "चुंबकीय फ्लक्स", + "magnetic-flux-density": "चुंबकीय फ्लक्स घनत्व", + "magnetic-moment": "चुंबकीय आघूर्ण", + "magnetic-permeability": "चुंबकीय पारगम्यता", + "mass": "द्रव्यमान", + "mass-fraction": "द्रव्यमान अंश", + "molar-concentration": "मोलर सांद्रता", + "molar-energy": "मोलर ऊर्जा", + "molar-heat-capacity": "मोलर ऊष्मा धारिता", + "molar-mass": "मोलर द्रव्यमान", + "number-concentration": "संख्या सांद्रता", + "parts-per-million": "पार्ट्स पर मिलियन", + "power": "शक्ति", + "power-density": "शक्ति घनत्व", + "pressure": "दाब", + "radiance": "दीप्ति", + "radiant-intensity": "दीप्त तीव्रता", + "radiation-dose": "विकिरण मात्रा", + "radioactive-decay": "रेडियोधर्मी अपक्षय", + "radioactivity": "रेडियोधर्मिता", + "radioactivity-concentration": "रेडियोधर्मिता सांद्रता", + "reciprocal-length": "प्रतिलोम लंबाई", + "resistance": "प्रतिरोध", + "reynolds-number": "रेनॉल्ड्स संख्या", + "signal-level": "सिग्नल स्तर", + "solid-angle": "घन कोण", + "specific-energy": "विशिष्ट ऊर्जा", + "specific-heat-capacity": "विशिष्ट ऊष्मा धारिता", + "specific-humidity": "विशिष्ट आर्द्रता", + "specific-volume": "विशिष्ट आयतन", + "speed": "वेग", + "surface-charge-density": "सतही आवेश घनत्व", + "surface-tension": "पृष्ठ तनाव", + "temperature": "तापमान", + "thermal-conductivity": "ऊष्मीय चालकता", + "time": "समय", + "torque": "टॉर्क", + "turbidity": "मलिनता", + "voltage": "वोल्टेज", + "volume": "आयतन", + "volume-flow": "आयतनिक प्रवाह" + }, + "millimeter": "मिलीमीटर", + "centimeter": "सेंटीमीटर", + "decimeter": "डेसीमीटर", + "angstrom": "आंग्स्ट्रॉम", + "nanometer": "नैनोमीटर", + "micrometer": "माइक्रोमीटर", + "meter": "मीटर", + "kilometer": "किलोमीटर", + "inch": "इंच", + "foot": "फुट", + "foot-us": "फुट (US सर्वे)", + "yard": "यार्ड", + "mile": "मील", + "nautical-mile": "समुद्री मील", + "astronomical-unit": "खगोलीय इकाई", + "reciprocal-metre": "प्रति मीटर", + "meter-per-meter": "मीटर प्रति मीटर", + "steradian": "स्टेरैडियन", + "thou": "थाउ", + "barleycorn": "बार्लीकॉर्न", + "hand": "हैंड", + "chain": "चेन", + "furlong": "फर्लांग", + "league": "लीग", + "fathom": "फैथम", + "cable": "केबल", + "link": "लिंक", + "rod": "रॉড", + "nanogram": "नैनोग्राम", + "microgram": "माइक्रोग्राम", + "milligram": "मिलिग्राम", + "gram": "ग्राम", + "kilogram": "किलोग्राम", + "tonne": "टन", + "ounce": "औंस", + "pound": "पाउंड", + "stone": "स्टोन", + "hundredweight-count": "हंड्रेडवेट काउंट", + "short-tons": "शॉर्ट टन", + "dalton": "डाल्टन", + "grain": "ग्रेन", + "drachm": "ड्राम", + "quarter": "क्वार्टर", + "slug": "स्लग", + "carat": "कैरेट", + "cubic-millimeter": "घन मिलीमीटर", + "cubic-centimeter": "घन सेंटीमीटर", + "cubic-meter": "घन मीटर", + "cubic-kilometer": "घन किलोमीटर", + "microliter": "माइक्रोलीटर", + "milliliter": "मिलीलीटर", + "liter": "लीटर", + "hectoliter": "हेक्टोलीटर", + "cubic-inch": "घन इंच", + "cubic-foot": "घन फुट", + "cubic-yard": "घन यार्ड", + "fluid-ounce": "फ्लूइड औंस", + "fluid-ounce-per-second": "फ्लूइड औंस प्रति सेकंड", + "pint": "पिंट", + "quart": "क्वार्ट", + "gallon": "गैलन", + "oil-barrels": "ऑयल बैरल", + "cubic-meter-per-kilogram": "किलोग्राम प्रति घन मीटर", + "gill": "गिल", + "hogshead": "हॉग्सहेड", + "teaspoon": "टीस्पून", + "tablespoon": "टेबलस्पून", + "cup": "कप", + "celsius": "सेल्सियस", + "kelvin": "केल्विन", + "rankine": "रैंकिन", + "fahrenheit": "फारेनहाइट", + "percent": "प्रतिशत", + "meter-per-second": "मीटर प्रति सेकंड", + "kilometer-per-hour": "किलोमीटर प्रति घंटा", + "foot-per-second": "फुट प्रति सेकंड", + "foot-per-minute": "फुट प्रति मिनट", + "mile-per-hour": "मील प्रति घंटा", + "knot": "नॉट", + "inch-per-second": "इंच प्रति सेकंड", + "inch-per-hour": "इंच प्रति घंटा", + "millimeters-per-minute": "मिलीमीटर प्रति मिनट", + "meter-per-minute": "मीटर प्रति मिनट", + "kilometer-per-hour-squared": "किलोमीटर प्रति घंटा वर्ग", + "foot-per-second-squared": "फुट प्रति सेकंड वर्ग", + "pascal": "पास्कल", + "kilopascal": "किलोपास्कल", + "megapascal": "मेगापास्कल", + "gigapascal": "गिगापास्कल", + "millibar": "मिलीबार", + "bar": "बार", + "kilobar": "किलोबार", + "newton": "न्यूटन", + "newton-meter": "न्यूटन मीटर", + "foot-pounds": "फुट-पाउंड", + "inch-pounds": "इंच-पाउंड", + "newton-per-meter": "न्यूटन प्रति मीटर", + "atmospheres": "एटमॉस्फियर", + "pounds-per-square-inch": "पाउंड प्रति वर्ग इंच", + "kilopound-per-square-inch": "किलोपाउंड प्रति वर्ग इंच", + "torr": "टॉर", + "inches-of-mercury": "इंच ऑफ मर्करी", + "pascal-per-square-meter": "पास्कल प्रति वर्ग मीटर", + "pound-per-square-inch": "पाउंड प्रति वर्ग इंच", + "newton-per-square-meter": "न्यूटन प्रति वर्ग मीटर", + "kilogram-force-per-square-meter": "किलोग्राम-फोर्स प्रति वर्ग मीटर", + "pascal-per-square-centimeter": "पास्कल प्रति वर्ग सेंटीमीटर", + "ton-force-per-square-inch": "टन-फोर्स प्रति वर्ग इंच", + "kilonewton-per-square-meter": "किलोन्यूटन प्रति वर्ग मीटर", + "newton-per-square-millimeter": "न्यूटन प्रति वर्ग मिलीमीटर", + "microjoule": "माइक्रोजूल", + "millijoule": "मिलीजूल", + "joule": "जूल", + "kilojoule": "किलोजूल", + "megajoule": "मेगाजूल", + "gigajoule": "गिगाजूल", + "watt-hour": "वॉट-घंटा", + "watt-minute": "वॉट-मिनट", + "kilowatt-hour": "किलोवॉट-घंटा", + "milliwatt-hour": "मिलीवॉट-घंटा", + "megawatt-hour": "मेगावॉट-घंटा", + "gigawatt-hour": "गिगावॉट-घंटा", + "electron-volts": "इलेक्ट्रॉन वोल्ट", + "joules-per-coulomb": "जूल प्रति कूलॉम", + "british-thermal-unit": "ब्रिटिश थर्मल यूनिट", + "thousand-british-thermal-unit": "हज़ार ब्रिटिश थर्मल यूनिट", + "million-british-thermal-unit": "मिलियन ब्रिटिश थर्मल यूनिट", + "foot-pound": "फुट-पाउंड", + "calorie": "कैलोरी", + "small-calorie": "छोटी कैलोरी", + "kilocalorie": "किलोकैलोरी", + "joule-per-kelvin": "जूल प्रति केल्विन", + "joule-per-kilogram-kelvin": "जूल प्रति किलोग्राम-केल्विन", + "joule-per-kilogram": "जूल प्रति किलोग्राम", + "watt-per-meter-kelvin": "वॉट प्रति मीटर-केल्विन", + "joule-per-cubic-meter": "जूल प्रति घन मीटर", + "therm": "थर्म", + "electric-dipole-moment": "विद्युत द्विध्रुव आघूर्ण", + "magnetic-dipole-moment": "चुंबकीय द्विध्रुव आघूर्ण", + "debye": "डेबाई", + "coulomb-per-square-meter-per-volt": "कूलॉम प्रति वर्ग मीटर प्रति वोल्ट", + "milliwatt": "मिलिवॉट", + "microwatt": "माइक्रोवॉट", + "watt": "वॉट", + "kilowatt": "किलोवॉट", + "megawatt": "मेगावॉट", + "gigawatt": "गिगावॉट", + "metric-horsepower": "मेट्रिक हॉर्सपावर", + "milliwatt-per-square-centimeter": "मिलिवॉट प्रति वर्ग सेंटीमीटर", + "watt-per-square-centimeter": "वॉट प्रति वर्ग सेंटीमीटर", + "kilowatt-per-square-centimeter": "किलोवॉट प्रति वर्ग सेंटीमीटर", + "milliwatt-per-square-meter": "मिलिवॉट प्रति वर्ग मीटर", + "watt-per-square-meter": "वॉट प्रति वर्ग मीटर", + "kilowatt-per-square-meter": "किलोवॉट प्रति वर्ग मीटर", + "watt-per-square-inch": "वॉट प्रति वर्ग इंच", + "kilowatt-per-square-inch": "किलोवॉट प्रति वर्ग इंच", + "horsepower": "हॉर्सपावर", + "btu-per-hour": "ब्रिटिश थर्मल यूनिट प्रति घंटा", + "btu-per-second": "ब्रिटिश थर्मल यूनिट प्रति सेकंड", + "btu-per-day": "ब्रिटिश थर्मल यूनिट प्रति दिन", + "mbtu-per-hour": "हज़ार ब्रिटिश थर्मल यूनिट प्रति घंटा", + "mbtu-per-second": "हज़ार ब्रिटिश थर्मल यूनिट प्रति सेकंड", + "mbtu-per-day": "हज़ार ब्रिटिश थर्मल यूनिट प्रति दिन", + "mmbtu-per-hour": "मिलियन ब्रिटिश थर्मल यूनिट प्रति घंटा", + "mmbtu-per-second": "मिलियन ब्रिटिश थर्मल यूनिट प्रति सेकंड", + "mmbtu-per-day": "मिलियन ब्रिटिश थर्मल यूनिट प्रति दिन", + "foot-pound-per-second": "फुट-पाउंड प्रति सेकंड", + "coulomb": "कूलॉम", + "millicoulomb": "मिलीकूलॉम", + "microcoulomb": "माइक्रोकूलॉम", + "nanocoulomb": "नैनोकूलॉम", + "picocoulomb": "पिको कूलॉम", + "coulomb-per-meter": "कूलॉम प्रति मीटर", + "coulomb-per-cubic-meter": "कूलॉम प्रति घन मीटर", + "coulomb-per-square-meter": "कूलॉम प्रति वर्ग मीटर", + "square-millimeter": "वर्ग मिलीमीटर", + "square-centimeter": "वर्ग सेंटीमीटर", + "square-meter": "वर्ग मीटर", + "hectare": "हेक्टेयर", + "square-kilometer": "वर्ग किलोमीटर", + "square-inch": "वर्ग इंच", + "square-foot": "वर्ग फुट", + "square-yard": "वर्ग गज", + "acre": "एकड़", + "square-mile": "वर्ग मील", + "are": "आर", + "barn": "बार्न", + "circular-inch": "परिपत्र इंच", + "milliampere-hour": "मिलिअंपियर-घंटा", + "ampere-hours": "अंपियर-घंटे", + "kiloampere-hours": "किलोअंपियर-घंटे", + "nanoampere": "नैनोअंपियर", + "picoampere": "पिकोअंपियर", + "microampere": "माइक्रोअंपियर", + "milliampere": "मिलिअंपियर", + "ampere": "अंपियर", + "kiloampere": "किलोअंपियर", + "megaampere": "मेगा-अंपियर", + "gigaampere": "गिगा-अंपियर", + "microampere-per-square-centimeter": "माइक्रोअंपियर प्रति वर्ग सेंटीमीटर", + "ampere-per-square-meter": "अंपियर प्रति वर्ग मीटर", + "ampere-per-meter": "अंपियर प्रति मीटर", + "oersted": "ओएरस्टेड", + "bohr-magneton": "बोर मैग्नेटॉन", + "ampere-meter-squared": "अंपियर-मीटर स्क्वेर्ड", + "nanovolt": "नैनोवोल्ट", + "picovolt": "पिकोवोल्ट", + "millivolt": "मिलिवोल्ट", + "microvolt": "माइक्रोवोल्ट", + "volt": "वोल्ट", + "kilovolt": "किलोवोल्ट", + "megavolt": "मेगावोल्ट", + "dbmV": "डेसीबेल वोल्ट", + "dbm": "डेसीबेल-मिलिवॉट्स", + "volt-meter": "वोल्ट-मीटर", + "kilovolt-meter": "किलोवोल्ट-मीटर", + "megavolt-meter": "मेगावोल्ट-मीटर", + "microvolt-meter": "माइक्रोवोल्ट-मीटर", + "millivolt-meter": "मिलिवोल्ट-मीटर", + "nanovolt-meter": "नैनोवोल्ट-मीटर", + "ohm": "ओम", + "microohm": "माइक्रोओम", + "milliohm": "मिलिओम", + "kilohm": "किलोओम", + "megohm": "मेगाओम", + "gigohm": "गिगाओम", + "millihertz": "मिलिहर्ट्ज़", + "hertz": "हर्ट्ज़", + "kilohertz": "किलोहर्ट्ज़", + "megahertz": "मेगाहर्ट्ज़", + "gigahertz": "गिगाहर्ट्ज़", + "terahertz": "टेराहर्ट्ज़", + "rpm": "प्रति मिनट घूर्णन", + "candela-per-square-meter": "कैंडेला प्रति वर्ग मीटर", + "candela": "कैंडेला", + "lumen": "लुमेन", + "lux": "लक्स", + "foot-candle": "फ़ुट-कैंडल", + "lumen-per-square-meter": "लुमेन प्रति वर्ग मीटर", + "lux-second": "लक्स सेकंड", + "lumen-second": "लुमेन सेकंड", + "lumens-per-watt": "लुमेन प्रति वॉट", + "mole": "मोल", + "nanomole": "नैनोमोल", + "micromole": "माइक्रोमोल", + "millimole": "मिलिमोल", + "kilomole": "किलोमोल", + "mole-per-cubic-meter": "मोल प्रति घन मीटर", + "rssi": "प्राप्त सिग्नल शक्ति सूचक", + "ppm": "पार्ट्स पर मिलियन", + "ppb": "पार्ट्स पर बिलियन", + "micrograms-per-cubic-meter": "माइक्रोग्राम प्रति घन मीटर", + "aqi": "AQI", + "gram-per-cubic-meter": "ग्राम प्रति घन मीटर", + "gram-per-kilogram": "विशिष्ट आर्द्रता", + "millimeters-per-second": "मिलीमीटर प्रति सेकंड", + "neper": "नेपर", + "bel": "बेल", + "decibel": "डेसीबेल", + "meters-per-second-squared": "मीटर प्रति सेकंड वर्ग", + "becquerel": "बेकरेल", + "curie": "क्यूरी", + "gray": "ग्रे", + "sievert": "सीवर्ट", + "roentgen": "रोएंटजन", + "cps": "प्रति सेकंड काउंट", + "rad": "रैड", + "rem": "रेम", + "dps": "प्रति सेकंड विघटन", + "rutherford": "रदरफोर्ड", + "coulombs-per-kilogram": "कूलॉम्ब प्रति किलोग्राम", + "becquerels-per-cubic-meter": "बेकरेल प्रति घन मीटर", + "curies-per-liter": "क्यूरी प्रति लीटर", + "becquerels-per-second": "बेकरेल प्रति सेकंड", + "curies-per-second": "क्यूरी प्रति सेकंड", + "gy-per-second": "ग्रे प्रति सेकंड", + "watt-per-steradian": "वॉट प्रति स्टीरेडियन", + "watt-per-square-metre-steradian": "वॉट प्रति वर्ग मीटर-स्टीरेडियन", + "ph-level": "pH स्तर", + "turbidity": "टर्बिडिटी", + "mg-per-liter": "मिलिग्राम प्रति लीटर", + "microsiemens-per-centimeter": "माइक्रोसीमेन्स प्रति सेंटीमीटर", + "millisiemens-per-meter": "मिलिसीमेन्स प्रति मीटर", + "siemens-per-meter": "सीमेन्स प्रति मीटर", + "kilogram-per-cubic-meter": "किलोग्राम प्रति घन मीटर", + "gram-per-cubic-centimeter": "ग्राम प्रति घन सेंटीमीटर", + "kilogram-per-square-meter": "किलोग्राम प्रति वर्ग मीटर", + "milligram-per-milliliter": "मिलिग्राम प्रति मिलीलीटर", + "milligram-per-cubic-meter": "मिलिग्राम प्रति घन मीटर", + "pound-per-cubic-foot": "पाउंड प्रति घन फ़ुट", + "ounces-per-cubic-inch": "औंस प्रति घन इंच", + "tons-per-cubic-yard": "टन प्रति घन यार्ड", + "particle-density": "कण घनत्व", + "kilometers-per-liter": "किलोमीटर प्रति लीटर", + "miles-per-gallon": "मील प्रति गैलन", + "liters-per-100-km": "100 किलोमीटर पर लीटर", + "gallons-per-mile": "गैलन प्रति मील", + "liters-per-hour": "लीटर प्रति घंटा", + "gallons-per-hour": "गैलन प्रति घंटा", + "beats-per-minute": "बीट्स प्रति मिनट", + "millimeters-of-mercury": "पारे के मिलीमीटर", + "milligrams-per-deciliter": "मिलिग्राम प्रति डेसीलीटर", + "g-force": "जी-फ़ोर्स", + "kilonewton": "किलो न्यूटन", + "kilogram-force": "किलोग्राम-फ़ोर्स", + "pound-force": "पाउंड-फ़ोर्स", + "kilopound-force": "किलोपाउंड-फ़ोर्स", + "dyne": "डाइन", + "poundal": "पाउंडल", + "kip": "किप", + "gal": "गैल", + "gravity": "गुरुत्वाकर्षण", + "hectopascal": "हेक्टोपास्कल", + "atmosphere": "एटमॉस्फ़ियर", + "millibars": "मिलिबार", + "inch-of-mercury": "पारे का इंच", + "richter-scale": "रिक्टर स्केल", + "nanosecond": "नैनोसेकंड", + "microsecond": "माइक्रोसेकंड", + "millisecond": "मिलीसेकंड", + "second": "सेकंड", + "minute": "मिनट", + "hour": "घंटा", + "day": "दिन", + "week": "सप्ताह", + "month": "महीना", + "year": "साल", + "cubic-foot-per-minute": "घन फ़ुट प्रति मिनट", + "cubic-meters-per-hour": "घन मीटर प्रति घंटा", + "cubic-meters-per-second": "घन मीटर प्रति सेकंड", + "liter-per-second": "लीटर प्रति सेकंड", + "liter-per-minute": "लीटर प्रति मिनट", + "gallons-per-minute": "गैलन प्रति मिनट", + "cubic-foot-per-second": "घन फ़ुट प्रति सेकंड", + "milliliters-per-minute": "मिलीलीटर प्रति मिनट", + "cubic-decimeter-per-second": "घन डेसीमीटर प्रति सेकंड", + "bit": "बिट", + "byte": "बाइट", + "kilobyte": "किलोबाइट", + "megabyte": "मेगाबाइट", + "gigabyte": "गीगाबाइट", + "terabyte": "टेराबाइट", + "petabyte": "पेटाबाइट", + "exabyte": "एग्ज़ाबाइट", + "zettabyte": "ज़ेटाबाइट", + "yottabyte": "योटाबाइट", + "bit-per-second": "बिट प्रति सेकंड", + "kilobit-per-second": "किलोबिट प्रति सेकंड", + "megabit-per-second": "मेगाबिट प्रति सेकंड", + "gigabit-per-second": "गीगाबिट प्रति सेकंड", + "terabit-per-second": "टेराबिट प्रति सेकंड", + "byte-per-second": "बाइट प्रति सेकंड", + "kilobyte-per-second": "किलोबाइट प्रति सेकंड", + "megabyte-per-second": "मेगाबाइट प्रति सेकंड", + "gigabyte-per-second": "गीगाबाइट प्रति सेकंड", + "degree": "डिग्री", + "radian": "रेडियन", + "gradian": "ग्रेडियन", + "arcminute": "आर्कमिनट", + "arcsecond": "आर्कसेकंड", + "milliradian": "मिलीरैडियन", + "revolution": "रेवोल्यूशन", + "siemens": "सीमेन्स", + "millisiemens": "मिलीसीमेन्स", + "microsiemens": "माइक्रोसीमेन्स", + "kilosiemens": "किलोसीमेन्स", + "megasiemens": "मेगासीमेन्स", + "gigasiemens": "गीगासीमेन्स", + "farad": "फैराड", + "millifarad": "मिलिफैराड", + "microfarad": "माइक्रोफैराड", + "nanofarad": "नैनोफैराड", + "picofarad": "पिकोफैराड", + "kilofarad": "किलोफैराड", + "megafarad": "मेगाफैराड", + "gigafarad": "गीगाफैराड", + "terfarad": "टेराफैराड", + "farad-per-meter": "फैराड प्रति मीटर", + "tesla": "टेस्ला", + "gauss": "गॉस", + "kilogauss": "किलोगॉस", + "millitesla": "मिलिटेस्ला", + "microtesla": "माइक्रोटेस्ला", + "nanotesla": "नैनोटेस्ला", + "kilotesla": "किलोटेस्ला", + "megatesla": "मेगाटेस्ला", + "millitesla-square-meters": "मिलिटेस्ला वर्ग मीटर", + "gamma": "गामा", + "lambda": "लैम्ब्डा", + "square-meter-per-second": "वर्ग मीटर प्रति सेकंड", + "square-centimeter-per-second": "वर्ग सेंटीमीटर प्रति सेकंड", + "stoke": "स्टोक", + "centistokes": "सेंटीस्टोक्स", + "square-foot-per-second": "वर्ग फ़ुट प्रति सेकंड", + "square-inch-per-second": "वर्ग इंच प्रति सेकंड", + "pascal-second": "पास्कल-सेकंड", + "centipoise": "सेंटिपॉइज़", + "poise": "पॉइज़", + "reynolds": "रेनॉल्ड्स", + "pound-per-foot-hour": "पाउंड प्रति फ़ुट-घंटा", + "newton-second-per-square-meter": "न्यूटन सेकंड प्रति वर्ग मीटर", + "dyne-second-per-square-centimeter": "डाइन सेकंड प्रति वर्ग सेंटीमीटर", + "kilogram-per-meter-second": "किलोग्राम प्रति मीटर-सेकंड", + "tesla-square-meters": "टेस्ला वर्ग मीटर", + "maxwell": "मैक्सवेल", + "tesla-per-meter": "टेस्ला प्रति मीटर", + "gauss-per-centimeter": "गॉस प्रति सेंटीमीटर", + "weber": "वेबर", + "microweber": "माइक्रोवेबर", + "milliweber": "मिलिवेबर", + "gauss-square-centimeter": "गॉस वर्ग सेंटीमीटर", + "kilogauss-square-centimeter": "किलोगॉस वर्ग सेंटीमीटर", + "henry": "हेनरी", + "millihenry": "मिलिहेनरी", + "microhenry": "माइक्रोहेनरी", + "nanohenry": "नैनोहेनरी", + "henry-per-meter": "हेनरी प्रति मीटर", + "tesla-meter-per-ampere": "टेस्ला मीटर प्रति एंपियर", + "gauss-per-oersted": "गॉस प्रति ओएर्स्टेड", + "kilogram-per-mole": "किलोग्राम प्रति मोल", + "gram-per-mole": "ग्राम प्रति मोल", + "milligram-per-mole": "मिलिग्राम प्रति मोल", + "joule-per-mole": "जूल प्रति मोल", + "joule-per-mole-kelvin": "जूल प्रति मोल-केल्विन", + "millivolts-per-meter": "मिलिवोल्ट प्रति मीटर", + "volts-per-meter": "वोल्ट प्रति मीटर", + "kilovolts-per-meter": "किलोवोल्ट प्रति मीटर", + "radian-per-second": "रेडियन प्रति सेकंड", + "radian-per-second-squared": "रेडियन प्रति सेकंड स्क्वेयर", + "revolutions-per-minute-per-second": "कोणीय त्वरण", + "deg-per-second": "डिग्री प्रति सेकंड", + "rotation-per-minute": "रोटेशन प्रति मिनट", + "degrees-brix": "डिग्री ब्रिक्स", + "katal": "कैटल", + "katal-per-cubic-metre": "कैटल प्रति क्यूबिक मीटर", + "paris-inch": "पेरिस इंच" + }, + "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": "उपयोगकर्ता को सक्रिय करने के लिए निम्न सक्रियण लिंक का उपयोग करें ({{activationLinkTtl}} में समाप्त होगा) :", + "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": "उपयोगकर्ता खाता सफलतापूर्वक निष्क्रिय कर दिया गया!", + "copyId": "उपयोगकर्ता Id कॉपी करें", + "idCopiedMessage": "उपयोगकर्ता Id क्लिपबोर्ड पर कॉपी कर दी गई है", + "user-list": "उपयोगकर्ताओं की सूची", + "user-list-required": "उपयोगकर्ताओं की सूची आवश्यक है" + }, + "value": { + "type": "मान का प्रकार", + "string": "स्ट्रिंग", + "string-value": "स्ट्रिंग मान", + "string-value-required": "स्ट्रिंग मान आवश्यक है", + "integer": "इंटीजर", + "integer-value": "इंटीजर मान", + "integer-value-required": "इंटीजर मान आवश्यक है", + "invalid-integer-value": "अमान्य इंटीजर मान", + "double": "डबल", + "double-value": "डबल मान", + "double-value-required": "डबल मान आवश्यक है", + "boolean": "बूलियन", + "boolean-value": "बूलियन मान", + "false": "फॉल्स", + "true": "ट्रू", + "long": "लॉन्ग", + "json": "JSON", + "json-value": "JSON मान", + "json-value-invalid": "JSON मान का फ़ॉर्मेट अमान्य है", + "json-value-required": "JSON मान आवश्यक है।" + }, + "version-control": { + "version-control": "वर्ज़न कंट्रोल", + "management": "वर्ज़न कंट्रोल प्रबंधन", + "search": "वर्ज़न खोजें", + "branch": "ब्रांच", + "default": "डिफ़ॉल्ट", + "select-branch": "ब्रांच चुनें", + "branch-required": "ब्रांच आवश्यक है", + "create-entity-version": "एंटिटी वर्ज़न बनाएँ", + "version-name": "वर्ज़न नाम", + "version-name-required": "वर्ज़न नाम आवश्यक है", + "author": "लेखक", + "export-relations": "रिलेशन एक्सपोर्ट करें", + "export-attributes": "विशेषताएँ एक्सपोर्ट करें", + "export-credentials": "क्रेडेंशियल्स एक्सपोर्ट करें", + "export-calculated-fields": "कैलक्युलेटेड फ़ील्ड्स एक्सपोर्ट करें", + "export-alarm-rules": "अलार्म नियमों का निर्यात करें", + "entity-versions": "एंटिटी वर्ज़न", + "versions": "वर्ज़न", + "created-time": "बनाने का समय", + "version-id": "वर्ज़न ID", + "no-entity-versions-text": "कोई एंटिटी वर्ज़न नहीं मिला", + "no-versions-text": "कोई वर्ज़न नहीं मिला", + "copy-full-version-id": "पूरा वर्ज़न ID कॉपी करें", + "create-version": "वर्ज़न बनाएँ", + "creating-version": "वर्ज़न बनाया जा रहा है... कृपया प्रतीक्षा करें", + "nothing-to-commit": "कमीट करने के लिए कोई बदलाव नहीं", + "restore-version": "वर्ज़न पुनर्स्थापित करें", + "restore-entity-from-version": "वर्ज़न '{{versionName}}' से एंटिटी पुनर्स्थापित करें", + "restoring-entity-version": "एंटिटी वर्ज़न पुनर्स्थापित किया जा रहा है... कृपया प्रतीक्षा करें", + "load-relations": "रिलेशन लोड करें", + "load-attributes": "विशेषताएँ लोड करें", + "load-credentials": "क्रेडेंशियल्स लोड करें", + "load-calculated-fields": "कैलक्युलेटेड फ़ील्ड्स लोड करें", + "load-alarm-rules": "अलार्म नियम लोड करें", + "compare-with-current": "वर्तमान से तुलना करें", + "diff-entity-with-version": "एंटिटी वर्ज़न '{{versionName}}' से तुलना", + "previous-difference": "पिछला अंतर", + "next-difference": "अगला अंतर", + "current": "वर्तमान", + "differences": "{ count, plural, =1 {1 अंतर} other {# अंतर} }", + "create-entities-version": "एंटिटीज़ वर्ज़न बनाएँ", + "default-sync-strategy": "डिफ़ॉल्ट सिंक स्ट्रैटेजी", + "sync-strategy-merge": "मर्ज", + "sync-strategy-overwrite": "ओवरराइट", + "entities-to-export": "एक्सपोर्ट करने के लिए एंटिटीज़", + "entities-to-restore": "रिस्टोर करने के लिए एंटिटीज़", + "sync-strategy": "सिंक स्ट्रैटेजी", + "all-entities": "सभी एंटिटीज़", + "no-entities-to-export-prompt": "कृपया एक्सपोर्ट करने के लिए एंटिटीज़ निर्दिष्ट करें", + "no-entities-to-restore-prompt": "कृपया रिस्टोर करने के लिए एंटिटीज़ निर्दिष्ट करें", + "add-entity-type": "एंटिटी टाइप जोड़ें", + "remove-all": "सभी हटाएँ", + "version-create-result": "{ added, plural, =0 {कोई एंटिटी नहीं} =1 {1 एंटिटी} other {# एंटिटीज़} } जोड़ी गईं.
    { modified, plural, =0 {कोई एंटिटी नहीं} =1 {1 एंटिटी} other {# एंटिटीज़} } बदली गईं.
    { removed, plural, =0 {कोई एंटिटी नहीं} =1 {1 एंटिटी} other {# एंटिटीज़} } हटाई गईं.", + "remove-other-entities": "अन्य एंटिटीज़ हटाएँ", + "find-existing-entity-by-name": "नाम से मौजूदा एंटिटी खोजें", + "restore-entities-from-version": "वर्ज़न '{{versionName}}' से एंटिटीज़ रिस्टोर करें", + "restoring-entities-from-version": "एंटिटीज़ रिस्टोर की जा रही हैं... कृपया प्रतीक्षा करें", + "no-entities-restored": "कोई एंटिटी रिस्टोर नहीं की गई", + "created": "{{created}} बनाए गए", + "updated": "{{updated}} अपडेट किए गए", + "deleted": "{{deleted}} डिलीट किए गए", + "remove-other-entities-confirm-text": "सावधान रहें! इससे उन सभी वर्तमान एंटिटीज़ को स्थायी रूप से डिलीट कर दिया जाएगा
    जो उस वर्ज़न में मौजूद नहीं हैं जिसे आप रिस्टोर करना चाहते हैं।

    कन्फ़र्म करने के लिए कृपया \"अन्य एंटिटीज़ हटाएँ\" टाइप करें।", + "auto-commit-to-branch": "ऑटो-कमिट {{ branch }} ब्रांच पर", + "default-create-entity-version-name": "{{entityName}} अपडेट", + "sync-strategy-merge-hint": "रिपॉज़िटरी में चुनी गई एंटिटीज़ को बनाता या अपडेट करता है। अन्य सभी रिपॉज़िटरी एंटिटीज़ परिवर्तित नहीं की जातीं।", + "sync-strategy-overwrite-hint": "रिपॉज़िटरी में चुनी गई एंटिटीज़ को बनाता या अपडेट करता है। अन्य सभी रिपॉज़िटरी एंटिटीज़ डिलीट कर दी जाती हैं।", + "device-credentials-conflict": "एक्सटर्नल ID {{entityId}} वाले डिवाइस को लोड करने में विफल
    क्योंकि वही क्रेडेंशियल्स पहले से किसी दूसरे डिवाइस के लिए डेटाबेस में मौजूद हैं।
    कृपया रिस्टोर फॉर्म में लोड क्रेडेंशियल्स सेटिंग को डिसेबल करने पर विचार करें।", + "missing-referenced-entity": "एक्सटर्नल ID {{sourceEntityId}} वाली {{sourceEntityTypeName}} को लोड करने में विफल
    क्योंकि यह ID {{targetEntityId}} वाली गुम {{targetEntityTypeName}} को रेफ़र करती है।", + "runtime-failed": "विफल: {{message}}", + "auto-commit-settings-read-only-hint": "रिपॉज़िटरी सेटिंग्स में रीड-ओनली विकल्प सक्षम होने पर ऑटो-कमिट फ़ीचर काम नहीं करता।", + "rollback-on-error": "एरर होने पर रोलबैक", + "rollback-on-error-hint": "यदि आपके पास रिस्टोर करने के लिए एंटिटीज़ की संख्या बहुत अधिक है, तो प्रदर्शन बढ़ाने के लिए इस विकल्प को डिसेबल करने पर विचार करें।\n ध्यान दें, वर्ज़न लोड करते समय यदि कोई एरर हो जाता है, तो पहले से सेव की गई एंटिटीज़ (रिलेशन, विशेषताओं आदि के साथ) जैसी हैं वैसी ही बनी रहेंगी" + }, + "widget": { + "widget-library": "विजेट लाइब्रेरी", + "widget-bundle": "विजेट बंडल", + "all-bundles": "सभी बंडल्स", + "select-widgets-bundle": "विजेट बंडल चुनें", + "widgets": "विजेट", + "all-widgets": "सभी विजेट", + "widget": "विजेट", + "select-widget": "विजेट चुनें", + "no-widgets-matching": " '{{entity}}' से मेल खाते कोई विजेट नहीं मिले।", + "no-widgets": "अभी तक कोई विजेट नहीं", + "no-widgets-text": "कोई विजेट नहीं मिला", + "management": "विजेट प्रबंधन", + "editor": "विजेट एडिटर", + "confirm-to-exit-editor-html": "आपके पास असहेजे हुए विजेट सेटिंग्स हैं।
    क्या आप वाकई इस पेज से बाहर जाना चाहते हैं?", + "widget-type-not-found": "विजेट कॉन्फ़िगरेशन लोड करते समय समस्या हुई।
    संभवतः संबंधित\n विजेट प्रकार हटा दिया गया है।", + "widget-type-load-error": "निम्नलिखित त्रुटियों के कारण विजेट लोड नहीं किया जा सका:", + "remove": "विजेट हटाएँ", + "delete": "विजेट डिलीट करें", + "edit": "विजेट संपादित करें", + "remove-widget-title": "क्या आप वाकई विजेट '{{widgetTitle}}' हटाना चाहते हैं?", + "remove-widget-text": "पुष्टि के बाद यह विजेट और इससे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "replace-reference-with-widget-copy": "रेफ़्रेन्स को विजेट कॉपी से बदलें", + "timeseries": "टाइम सीरीज़", + "search-data": "डेटा खोजें", + "no-data-found": "कोई डेटा नहीं मिला", + "latest": "नवीनतम मान", + "rpc": "कंट्रोल विजेट", + "alarm": "अलार्म विजेट", + "static": "स्टैटिक विजेट", + "timeseries-short": "सीरीज़", + "latest-short": "नवीनतम", + "rpc-short": "कंट्रोल", + "alarm-short": "अलार्म", + "static-short": "स्टैटिक", + "select-widget-type": "विजेट प्रकार चुनें", + "missing-widget-title-error": "विजेट शीर्षक अवश्य निर्दिष्ट होना चाहिए!", + "widget-saved": "विजेट सहेजा गया", + "unable-to-save-widget-error": "विजेट को सहेजा नहीं जा सका! विजेट में त्रुटियाँ हैं!", + "save": "विजेट सहेजें", + "saveAs": "विजेट को इस रूप में सहेजें", + "move": "विजेट स्थानांतरित करें", + "save-widget-as": "विजेट को इस रूप में सहेजें", + "save-widget-as-text": "कृपया नया विजेट शीर्षक दर्ज करें", + "toggle-fullscreen": "फुलस्क्रीन टॉगल करें", + "run": "विजेट चलाएँ", + "widget-title": "विजेट शीर्षक", + "title": "शीर्षक", + "title-required": "विजेट शीर्षक आवश्यक है।", + "title-max-length": "शीर्षक 256 अक्षरों से कम होना चाहिए", + "system": "सिस्टम", + "type": "विजेट प्रकार", + "resources": "संसाधन", + "resource-url": "JavaScript/CSS URL", + "resource-is-extension": "एक्सटेंशन है", + "remove-resource": "संसाधन हटाएँ", + "add-resource": "संसाधन जोड़ें", + "html": "HTML", + "tidy": "साफ़ करें", + "css": "CSS", + "settings-form": "सेटिंग्स फ़ॉर्म", + "data-key-settings-form": "डेटा कुंजी सेटिंग्स फ़ॉर्म", + "latest-data-key-settings-form": "नवीनतम डेटा कुंजी सेटिंग्स फ़ॉर्म", + "widget-settings": "विजेट सेटिंग्स", + "description": "विवरण", + "tags": "टैग", + "image-preview": "छवि पूर्वावलोकन", + "settings-form-selector": "सेटिंग्स फ़ॉर्म चयनकर्ता", + "data-key-settings-form-selector": "डेटा कुंजी सेटिंग्स फ़ॉर्म चयनकर्ता", + "latest-data-key-settings-form-selector": "नवीनतम डेटा कुंजी सेटिंग्स फ़ॉर्म चयनकर्ता", + "all": "सभी", + "actual": "वास्तविक", + "scada": "SCADA प्रतीक", + "deprecated": "अप्रचलित", + "has-basic-mode": "बेसिक मोड उपलब्ध", + "basic-mode-form-selector": "बेसिक मोड फ़ॉर्म चयनकर्ता", + "basic-mode": "बेसिक", + "advanced-mode": "उन्नत", + "javascript": "JavaScript", + "js": "JS", + "delete-widget-title": "क्या आप वाकई विजेट '{{widgetName}}' हटाना चाहते हैं?", + "delete-widget-text": "पुष्टि के बाद यह विजेट और इससे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "delete-widgets-title": "क्या आप वाकई { count, plural, =1 {1 विजेट} other {# विजेट} } हटाना चाहते हैं?", + "delete-widgets-text": "सावधान रहें, पुष्टि के बाद सभी चयनित विजेट हटाए जाएँगे और उनसे संबंधित सभी डेटा पुनर्प्राप्त नहीं किया जा सकेगा।", + "delete-widget": "विजेट हटाएँ", + "widget-template-load-failed-error": "विजेट टेम्पलेट लोड करने में विफल!", + "details": "विवरण", + "widget-details": "विजेट विवरण", + "add": "विजेट जोड़ें", + "add-existing-widget": "मौजूदा विजेट जोड़ें", + "add-new-widget": "नया विजेट जोड़ें", + "search-widgets": "विजेट खोजें", + "selected-widgets": "{ count, plural, =1 {1 विजेट} other {# विजेट} } चयनित", + "undo": "विजेट परिवर्तनों को पूर्ववत करें", + "export": "विजेट निर्यात करें", + "export-prompt": "विजेट छवियों और संसाधनों को एम्बेड करें", + "export-widgets": "विजेट निर्यात करें", + "export-widgets-prompt": "विजेट की छवियाँ और संसाधन एम्बेड करें", + "import": "विजेट आयात करें", + "no-data": "विजेट पर दिखाने के लिए कोई डेटा नहीं", + "data-overflow": "विजेट {{total}} में से {{count}} एंटिटीज़ प्रदर्शित कर रहा है", + "alarm-data-overflow": "विजेट {{totalEntities}} एंटिटीज़ में से {{allowedEntities}} (अधिकतम अनुमत) एंटिटीज़ के लिए अलार्म प्रदर्शित कर रहा है", + "search": "विजेट खोजें", + "filter": "विजेट फ़िल्टर प्रकार", + "loading-widgets": "विजेट लोड किए जा रहे हैं...", + "widget-template-error": "अमान्य विजेट HTML टेम्पलेट।", + "reference": "संदर्भ" + }, + "widget-action": { + "header-button": "विजेट हेडर बटन", + "do-nothing": "कुछ न करें", + "open-dashboard-state": "नए डैशबोर्ड स्टेट पर जाएँ", + "update-dashboard-state": "वर्तमान डैशबोर्ड स्टेट अपडेट करें", + "open-dashboard": "अन्य डैशबोर्ड पर जाएँ", + "custom": "कस्टम एक्शन", + "custom-pretty": "कस्टम एक्शन (HTML टेम्पलेट सहित)", + "custom-pretty-error-title": "कस्टम डायलॉग त्रुटि", + "custom-pretty-template-error": "अमान्य कस्टम डायलॉग टेम्पलेट।", + "custom-pretty-controller-error": "कस्टम डायलॉग फ़ंक्शन का मूल्यांकन करते समय त्रुटि हुई।", + "mobile-action": "मोबाइल एक्शन", + "target-dashboard-state": "टार्गेट डैशबोर्ड स्टेट", + "target-dashboard-state-required": "टार्गेट डैशबोर्ड स्टेट आवश्यक है", + "set-entity-from-widget": "विजेट से एंटिटी सेट करें", + "target-dashboard": "टार्गेट डैशबोर्ड", + "select-target-dashboard": "टार्गेट डैशबोर्ड चुनें", + "target-dashboard-required": "टार्गेट डैशबोर्ड आवश्यक है।", + "open-right-layout": "दाएँ डैशबोर्ड लेआउट खोलें (मोबाइल व्यू)", + "state-display-type": "डैशबोर्ड स्टेट डिस्प्ले विकल्प", + "open-normal": "सामान्य", + "open-in-separate-dialog": "अलग डायलॉग में खोलें", + "open-in-popover": "पॉपओवर में खोलें", + "dialog-title": "डायलॉग शीर्षक", + "dialog-hide-dashboard-toolbar": "डायलॉग में डैशबोर्ड टूलबार छुपाएँ", + "dialog-width": "डायलॉग की चौड़ाई (व्यू-पोर्ट चौड़ाई का प्रतिशत)", + "dialog-height": "डायलॉग की ऊँचाई (व्यू-पोर्ट ऊँचाई का प्रतिशत)", + "dialog-size-range-error": "डायलॉग आकार प्रतिशत मान 1 से 100 के बीच होना चाहिए।", + "popover-preferred-placement": "पसंदीदा पॉपओवर प्लेसमेंट", + "popover-placement-top": "ऊपर", + "popover-placement-topLeft": "ऊपर-बाएँ", + "popover-placement-topRight": "ऊपर-दाएँ", + "popover-placement-right": "दाएँ", + "popover-placement-rightTop": "दाएँ-ऊपर", + "popover-placement-rightBottom": "दाएँ-नीचे", + "popover-placement-bottom": "नीचे", + "popover-placement-bottomLeft": "नीचे-बाएँ", + "popover-placement-bottomRight": "नीचे-दाएँ", + "popover-placement-left": "बाएँ", + "popover-placement-leftTop": "बाएँ-ऊपर", + "popover-placement-leftBottom": "बाएँ-नीचे", + "popover-hide-on-click-outside": "बाहर क्लिक करने पर पॉपओवर छुपाएँ", + "popover-hide-dashboard-toolbar": "पॉपओवर में डैशबोर्ड टूलबार छुपाएँ", + "popover-width": "पॉपओवर चौड़ाई", + "popover-height": "पॉपओवर ऊँचाई", + "popover-style": "पॉपओवर शैली", + "open-new-browser-tab": "नए ब्राउज़र टैब में खोलें", + "open-URL": "URL खोलें", + "URL": "URL", + "url-required": "URL आवश्यक है।", + "mobile": { + "device-provision": "डिवाइस प्रोविज़न", + "action-type": "मोबाइल एक्शन प्रकार", + "select-action-type": "मोबाइल एक्शन प्रकार चुनें", + "action-type-required": "मोबाइल एक्शन प्रकार आवश्यक है", + "take-picture-from-gallery": "गैलरी से चित्र लें", + "take-photo": "फ़ोटो लें", + "map-direction": "मैप दिशानिर्देश खोलें", + "map-location": "मैप स्थान खोलें", + "scan-qr-code": "QR कोड स्कैन करें", + "make-phone-call": "फ़ोन कॉल करें", + "get-location": "फ़ोन लोकेशन प्राप्त करें", + "take-screenshot": "स्क्रीनशॉट लें", + "handle-provision-success-function": "प्रोविजन सफलता फ़ंक्शन को हैंडल करें", + "get-location-function": "स्थान प्राप्त करें फ़ंक्शन", + "process-launch-result-function": "लॉन्च परिणाम फ़ंक्शन को प्रोसेस करें", + "get-phone-number-function": "फोन नंबर प्राप्त करें फ़ंक्शन", + "process-image-function": "छवि प्रोसेस फ़ंक्शन", + "process-qr-code-function": "QR कोड प्रोसेस फ़ंक्शन", + "process-location-function": "स्थान प्रोसेस फ़ंक्शन", + "handle-empty-result-function": "खाली परिणाम फ़ंक्शन को हैंडल करें", + "handle-error-function": "त्रुटि फ़ंक्शन को हैंडल करें", + "handle-non-mobile-fallback-function": "नॉन-मोबाइल फ़ॉलबैक फ़ंक्शन को हैंडल करें", + "save-to-gallery": "'इमेज गैलरी' में सहेजें", + "provision-type": "प्रोविजन प्रकार", + "auto": "स्वचालित", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "सॉफ्ट AP" + }, + "custom-action-function": "कस्टम एक्शन फ़ंक्शन", + "custom-pretty-function": "कस्टम एक्शन (HTML टेम्पलेट सहित) फ़ंक्शन", + "map-item-type": "मैप आइटम प्रकार", + "map-item": { + "marker": "मार्कर", + "polygon": "बहुभुज", + "rectangle": "आयत", + "circle": "वृत्त", + "polyline": "पॉलीलाइन" + }, + "place-map-item": "मैप आइटम रखें", + "map-item-tooltip": { + "customize-map-item-tooltips": "मैप आइटम टूलटिप्स अनुकूलित करें", + "place-marker": "मार्कर रखें", + "start-draw-rectangle": "आयत बनाना शुरू करें", + "finish-draw-rectangle": "आयत बनाना समाप्त करें", + "start-draw-polygon": "बहुभुज बनाना शुरू करें", + "continue-draw-polygon": "बहुभुज बनाना जारी रखें", + "finish-draw-polygon": "बहुभुज बनाना समाप्त करें", + "start-draw-circle": "वृत्त बनाना शुरू करें", + "finish-draw-circle": "वृत्त बनाना समाप्त करें", + "start-draw-polyline": "पॉलीलाइन ड्रॉ करना शुरू करें", + "finish-draw-polyline": "पॉलीलाइन ड्रॉ करना समाप्त करें" + } + }, + "widgets-bundle": { + "current": "वर्तमान बंडल", + "widgets-bundles": "विजेट बंडल्स", + "widgets-bundle-widgets": "विजेट बंडल के विजेट", + "add": "विजेट बंडल जोड़ें", + "delete": "विजेट बंडल हटाएँ", + "title": "शीर्षक", + "title-required": "शीर्षक आवश्यक है।", + "title-max-length": "शीर्षक 256 अक्षरों से कम होना चाहिए", + "description": "विवरण", + "image-preview": "छवि पूर्वावलोकन", + "scada": "SCADA विजेट बंडल", + "order": "क्रम", + "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-widgets-bundle-widgets-prompt": "निर्यात किए गए डेटा में बंडल के विजेट शामिल करें (अन्यथा केवल संदर्भित विजेट FQNs निर्यात किए जाएँगे)", + "export-failed-error": "विजेट बंडल निर्यात नहीं किया जा सका: {{error}}", + "create-new-widgets-bundle": "नया विजेट बंडल बनाएँ", + "widgets-bundle-file": "विजेट बंडल फ़ाइल", + "invalid-widgets-bundle-file-error": "विजेट बंडल आयात नहीं किया जा सका: अमान्य विजेट बंडल डेटा संरचना।", + "search": "विजेट बंडल खोजें", + "selected-widgets-bundles": "{ count, plural, =1 {1 विजेट बंडल} other {# विजेट बंडल्स} } चयनित", + "open-widgets-bundle": "विजेट बंडल खोलें", + "loading-widgets-bundles": "विजेट बंडल्स लोड किए जा रहे हैं...", + "create-new": "नया विजेट बंडल बनाएँ" + }, + "widget-config": { + "data": "डेटा", + "settings": "सेटिंग्स", + "advanced": "उन्नत", + "appearance": "दिखावट", + "widget-card": "विजेट कार्ड", + "mobile": "मोबाइल", + "title": "शीर्षक", + "title-tooltip": "शीर्षक टूलटिप", + "general-settings": "सामान्य सेटिंग्स", + "display-title": "विजेट शीर्षक प्रदर्शित करें", + "card-title": "कार्ड शीर्षक", + "drop-shadow": "ड्रॉप शैडो", + "enable-fullscreen": "फुलस्क्रीन सक्षम करें", + "background-color": "पृष्ठभूमि रंग", + "text-color": "पाठ रंग", + "border-radius": "बॉर्डर रेडियस", + "padding": "पैडिंग", + "margin": "मार्जिन", + "widget-style": "विजेट स्टाइल", + "widget-css": "विजेट CSS", + "title-style": "शीर्षक स्टाइल", + "mobile-mode-settings": "मोबाइल मोड", + "order": "क्रम", + "height": "ऊँचाई", + "mobile-hide": "मोबाइल मोड में विजेट छुपाएँ", + "desktop-hide": "डेस्कटॉप मोड में विजेट छुपाएँ", + "units": "मान के पास दिखाने हेतु विशेष प्रतीक", + "units-by-default": "डिफ़ॉल्ट इकाइयाँ", + "decimals": "दशमलव के बाद अंकों की संख्या", + "decimals-by-default": "डिफ़ॉल्ट दशमलव", + "default-data-key-parameter-hint": "यह पैरामीटर सभी विजेट मानों पर लागू होता है, जब तक कि डेटा कुंजी कॉन्फ़िगरेशन द्वारा अधिलेखित न किया जाए", + "units-short": "इकाइयाँ", + "decimals-short": "दशमलव", + "decimals-suffix": "दशमलव", + "digits-suffix": "अंक", + "timewindow": "टाइम विंडो", + "use-dashboard-timewindow": "डैशबोर्ड टाइम विंडो का उपयोग करें", + "use-widget-timewindow": "विजेट टाइम विंडो का उपयोग करें", + "display-timewindow": "टाइम विंडो प्रदर्शित करें", + "legend": "लीजेंड", + "display-legend": "लीजेंड प्रदर्शित करें", + "datasources": "डेटासोर्स", + "datasource": "डेटासोर्स", + "maximum-datasources": "अधिकतम { count, plural, =1 {1 डेटासोर्स की अनुमति है।} other {# डेटासोर्स की अनुमति है} }", + "timeseries-key-error": "कम से कम एक टाइम सीरीज़ डेटा कुंजी निर्दिष्ट होनी चाहिए", + "datasource-type": "प्रकार", + "datasource-parameters": "पैरामीटर्स", + "remove-datasource": "डेटासोर्स हटाएँ", + "add-datasource": "डेटासोर्स जोड़ें", + "target-device": "लक्षित डिवाइस", + "alarm-source": "अलार्म स्रोत", + "actions": "एक्शन्स", + "action": "एक्शन", + "add-action": "एक्शन जोड़ें", + "search-actions": "एक्शन खोजें", + "no-actions-text": "कोई एक्शन नहीं मिला", + "action-source": "एक्शन स्रोत", + "select-action-source": "एक्शन स्रोत चुनें", + "action-source-required": "एक्शन स्रोत आवश्यक है।", + "column-index": "कॉलम इंडेक्स", + "select-column-index": "कॉलम इंडेक्स चुनें", + "column-index-required": "कॉलम इंडेक्स आवश्यक है।", + "not-set": "सेट नहीं", + "action-name": "नाम", + "action-name-required": "एक्शन नाम आवश्यक है।", + "action-name-not-unique": "इस नाम वाला अन्य एक्शन पहले से मौजूद है।\nएक्शन नाम समान एक्शन स्रोत के भीतर अद्वितीय होना चाहिए।", + "action-icon": "आइकन", + "header-button": { + "button-settings": "बटन सेटिंग्स", + "button-type": "बटन प्रकार", + "button-type-basic": "बेसिक", + "button-type-raised": "रेज़्ड", + "button-type-stroked": "स्ट्रोक्ड", + "button-type-flat": "फ्लैट", + "button-type-icon": "आइकन", + "button-type-mini-fab": "FAB", + "colors": "रंग", + "color": "रंग", + "background": "पृष्ठभूमि", + "border": "बॉर्डर", + "advanced-button-style": "उन्नत बटन स्टाइल", + "button-style": "बटन स्टाइल" + }, + "show-hide-action-using-function": "फ़ंक्शन के माध्यम से एक्शन दिखाएँ/छुपाएँ", + "show-action-function": "एक्शन दिखाने का फ़ंक्शन", + "action-type": "प्रकार", + "action-type-required": "एक्शन प्रकार आवश्यक है।", + "edit-action": "एक्शन संपादित करें", + "delete-action": "एक्शन हटाएँ", + "delete-action-title": "विजेट एक्शन हटाएँ", + "delete-action-text": "क्या आप वाकई '{{actionName}}' नाम वाले विजेट एक्शन को हटाना चाहते हैं?", + "title-icon": "शीर्षक आइकन", + "display-icon": "शीर्षक आइकन प्रदर्शित करें", + "card-icon": "कार्ड आइकन", + "icon": "आइकन", + "icon-color": "आइकन रंग", + "icon-size": "आइकन आकार", + "advanced-settings": "उन्नत सेटिंग्स", + "data-settings": "डेटा सेटिंग्स", + "limits": "सीमाएँ", + "no-data-display-message": "\"दिखाने के लिए कोई डेटा नहीं\" का वैकल्पिक संदेश", + "data-page-size": "प्रति डेटासोर्स अधिकतम एंटिटीज़", + "settings-component-not-found": "सेलेक्टर '{{selector}}' के लिए सेटिंग्स फ़ॉर्म कंपोनेंट नहीं मिला", + "preview": "पूर्वावलोकन", + "set": "सेट करें", + "set-message": "संदेश सेट करें", + "advanced-title-style": "उन्नत शीर्षक शैली", + "card-style": "कार्ड शैली", + "text": "पाठ", + "background": "पृष्ठभूमि", + "advanced-widget-style": "उन्नत विजेट शैली", + "card-buttons": "कार्ड बटन", + "show-card-buttons": "कार्ड बटन दिखाएँ", + "card-border-radius": "कार्ड बॉर्डर रेडियस", + "card-padding": "कार्ड पैडिंग", + "card-appearance": "कार्ड दिखावट", + "color": "रंग", + "tooltip": "टूलटिप", + "units-required": "इकाई आवश्यक है।", + "list-layout": "सूची लेआउट", + "layout": "लेआउट", + "resize-options": "रीसाइज़ विकल्प", + "resizable": "रीसाइज़ योग्य", + "preserve-aspect-ratio": "आस्पेक्ट अनुपात बनाए रखें" + }, + "widget-type": { + "import": "विजेट प्रकार आयात करें", + "export": "विजेट प्रकार निर्यात करें", + "export-failed-error": "विजेट निर्यात नहीं किया जा सका: {{error}}", + "widget-file": "विजेट फ़ाइल", + "invalid-widget-file-error": "विजेट आयात नहीं किया जा सका: अमान्य विजेट डेटा संरचना।" + }, + "markdown": { + "edit": "संपादित करें", + "preview": "पूर्वावलोकन", + "copy-code": "कॉपी करने के लिए क्लिक करें", + "copied": "कॉपी किया गया!" + }, + "widgets": { + "mobile-app-qr-code": { + "configuration-hint": "कॉन्फ़िगरेशन प्लेटफ़ॉर्म की मुख्य सेटिंग्स में मोबाइल ऐप QR कोड विजेट पर निर्भर करता है", + "get-it-on-google-play": "Google Play पर प्राप्त करें", + "download-on-the-app-store": "App Store से डाउनलोड करें" + }, + "action-button": { + "behavior": "व्यवहार", + "on-click": "क्लिक पर", + "on-click-hint": "बटन क्लिक होने पर ट्रिगर होने वाली कार्रवाई", + "first-button-click": "पहले बटन का क्लिक", + "first-button-click-hint": "पहले बटन को दबाने पर कार्रवाई।", + "second-button-click": "दूसरे बटन का क्लिक", + "second-button-click-hint": "दूसरे बटन को दबाने पर कार्रवाई।", + "button-click-hint": "विजेट को दबाने पर कार्रवाई।" + }, + "command-button": { + "behavior": "व्यवहार", + "on-click": "क्लिक पर", + "on-click-hint": "बटन क्लिक होने पर की जाने वाली कार्रवाई।" + }, + "power-button": { + "behavior": "व्यवहार", + "power-on": "Power 'On'", + "power-on-hint": "कंपोनेंट को ON करने के लिए की जाने वाली कार्रवाई।", + "power-off": "Power 'Off'", + "power-off-hint": "कंपोनेंट को OFF करने के लिए की जाने वाली कार्रवाई।", + "on-label": "On", + "off-label": "Off", + "layout": "लेआउट", + "layout-default": "डिफ़ॉल्ट", + "layout-simplified": "सरलीकृत", + "layout-outlined": "आउटलाइन", + "layout-default-volume": "Default.Volume", + "layout-simplified-volume": "Simplified.Volume", + "layout-outlined-volume": "Outlined.Volume", + "layout-default-icon": "Default.Icon", + "layout-simplified-icon": "Simplified.Icon", + "layout-outlined-icon": "Outlined.Icon", + "main": "मुख्य", + "background": "पृष्ठभूमि", + "button-icon-on": "बटन आइकन 'On'", + "button-icon-off": "बटन आइकन 'Off'", + "power-on-colors": "Power 'On' रंग", + "power-off-colors": "Power 'Off' रंग", + "disabled-colors": "अक्षम रंग", + "button": "बटन" + }, + "toggle-button": { + "behavior": "व्यवहार", + "checked": "चेक किया हुआ", + "unchecked": "अनचेक", + "check": "चेक", + "check-hint": "कंपोनेंट को चेक करने के लिए की जाने वाली कार्रवाई।", + "uncheck": "अनचेक", + "uncheck-hint": "कंपोनेंट को अनचेक करने के लिए की जाने वाली कार्रवाई।", + "auto-scale": "ऑटो स्केल", + "horizontal-fill": "क्षैतिज भराव", + "vertical-fill": "ऊर्ध्वाधर भराव", + "button-appearance": "बटन दिखावट" + }, + "segmented-button": { + "layout": "लेआउट", + "layout-squared": "स्क्वायर्ड", + "layout-rounded": "राउंडेड", + "card-border": "कार्ड बॉर्डर", + "button-appearance": "बटन दिखावट", + "first": "पहला", + "second": "दूसरा", + "color-styles": "रंग शैलियाँ", + "selected": "चयनित", + "unselected": "अचयनित" + }, + "button": { + "layout": "लेआउट", + "outlined": "आउटलाइन", + "filled": "भरा हुआ", + "underlined": "अंडरलाइन", + "basic": "बेसिक", + "auto-scale": "ऑटो स्केल", + "label": "लेबल", + "icon": "आइकन", + "border-radius": "बॉर्डर रेडियस", + "color-palette": "रंग पैलेट", + "main": "मुख्य", + "background": "पृष्ठभूमि", + "border": "बॉर्डर", + "custom-styles": "कस्टम शैलियाँ", + "clear-style": "स्टाइल साफ़ करें", + "shadow": "छाया", + "enabled": "सक्रिय", + "disabled": "निष्क्रिय", + "preview": "पूर्वावलोकन", + "copy-style-from": "यहाँ से स्टाइल कॉपी करें" + }, + "value-stepper": { + "behavior": "व्यवहार", + "simplified": "सरलीकृत", + "filled": "भरा हुआ", + "outlined": "आउटलाइन", + "volume": "वॉल्यूम", + "initial-state": "प्रारंभिक स्थिति", + "initial-state-hint": "प्रारंभिक मान प्राप्त करने हेतु कार्रवाई।", + "disabled-state": "अक्षम स्थिति", + "disabled-state-hint": "वह स्थिति कॉन्फ़िगर करें जिसमें कंपोनेंट अक्षम हो जाए।", + "right-button-click": "दाएँ बटन का क्लिक", + "right-button-click-hint": "दाएँ बटन दबाने पर कार्रवाई।", + "left-button-click": "बाएँ बटन का क्लिक", + "left-button-click-hint": "बाएँ बटन दबाने पर कार्रवाई।", + "auto-scale": "ऑटो स्केल", + "value-range": "सीमा", + "min-range": "न्यूनतम", + "max-range": "अधिकतम", + "value-increment-decrement-step": "मान बढ़ाने/घटाने का चरण", + "value": "मान", + "value-box-background": "मान बॉक्स पृष्ठभूमि", + "border": "बॉर्डर", + "button-appearance": "बटन दिखावट", + "left": "बाएँ", + "right": "दाएँ", + "left-button": "बाएँ बटन", + "right-button": "दाएँ बटन", + "icon": "आइकन", + "color-palette": "रंग पैलेट", + "main": "मुख्य", + "background": "पृष्ठभूमि", + "button-icon-on": "बटन आइकन 'On'", + "button-on-colors": "Power 'On' रंग", + "disabled-colors": "अक्षम रंग" + }, + "button-state": { + "activated-state": "सक्रिय स्थिति", + "activated-state-hint": "वह स्थिति कॉन्फ़िगर करें जिसमें बटन सक्रिय हो।", + "disabled-state": "अक्षम स्थिति", + "disabled-state-hint": "वह स्थिति कॉन्फ़िगर करें जिसमें बटन अक्षम हो।", + "selected-state": "बटन चयन", + "selected-state-hint": "वह स्थिति कॉन्फ़िगर करें जिसमें बटन चयनित हो।", + "enabled": "सक्रिय", + "hovered": "होवर किया हुआ", + "pressed": "दबाया हुआ", + "activated": "सक्रिय", + "disabled": "निष्क्रिय", + "initial": "पहला बटन", + "first": "पहला", + "second": "दूसरा" + }, + "background": { + "background": "पृष्ठभूमि", + "background-settings": "पृष्ठभूमि सेटिंग्स", + "background-type-image": "छवि", + "background-type-color": "रंग", + "image-url": "छवि URL", + "overlay": "ओवरले", + "enable-overlay": "ओवरले सक्षम करें", + "blur": "ब्लर", + "preview": "पूर्वावलोकन" + }, + "bar-chart": { + "bar-appearance": "बार दिखावट", + "label-on-bar": "बार पर लेबल", + "value-on-bar": "बार पर मान", + "bar-chart-style": "बार चार्ट शैली", + "bar-axis": "बार अक्ष" + }, + "polar-area-chart": { + "polar-axis": "पोलर अक्ष", + "start-angle": "प्रारंभ कोण", + "polar-area-chart-style": "पोलर एरिया चार्ट शैली" + }, + "battery-level": { + "layout": "लेआउट", + "layout-vertical-solid": "वर्टिकल. ठोस", + "layout-horizontal-solid": "हॉरिज़ॉन्टल. ठोस", + "layout-vertical-divided": "वर्टिकल. विभाजित", + "layout-horizontal-divided": "हॉरिज़ॉन्टल. विभाजित", + "icon": "आइकन", + "value": "मान", + "auto-scale": "ऑटो स्केल", + "battery-level-color": "बैटरी स्तर रंग", + "battery-shape-color": "बैटरी आकृति रंग", + "battery-level-card-style": "बैटरी स्तर कार्ड शैली", + "sections-count": "सेक्शनों की संख्या" + }, + "signal-strength": { + "value": "मान", + "last-update": "अंतिम अपडेट", + "no-signal": "कोई सिग्नल नहीं", + "layout": "लेआउट", + "layout-wifi": "वाई-फाई", + "layout-cellular-bar": "सेलुलर बार", + "icon": "आइकन", + "date": "तारीख", + "active-bars-color": "सक्रिय सिग्नल बार्स का रंग", + "inactive-bars-color": "निष्क्रिय सिग्नल बार्स का रंग", + "signal-strength-card-style": "सिग्नल शक्ति कार्ड शैली", + "no-signal-rssi-value": "\"कोई सिग्नल नहीं\" rssi मान" + }, + "status-widget": { + "behavior": "व्यवहार", + "layout": "लेआउट", + "layout-default": "डिफ़ॉल्ट", + "layout-center": "केंद्र", + "layout-icon": "आइकन", + "on": "On", + "off": "Off", + "label": "लेबल", + "status": "स्थिति", + "icon": "आइकन", + "color-palette": "रंग पैलेट", + "disabled-color-palette": "अक्षम रंग पैलेट", + "primary": "प्राथमिक", + "primary-color-hint": "आइकन और लेबल का रंग", + "secondary": "द्वितीयक", + "secondary-color-hint": "स्थिति का रंग", + "background": "पृष्ठभूमि" + }, + "chart": { + "common-settings": "सामान्य सेटिंग्स", + "enable-stacking-mode": "स्टैकिंग मोड सक्षम करें", + "selection": "समय सीमा चयन", + "enable-selection-mode": "चयन मोड सक्षम करें", + "line-shadow-size": "लाइन छाया आकार", + "display-smooth-lines": "स्मूथ (घुमावदार) लाइनों को प्रदर्शित करें", + "default-bar-width": "गैर-संकलित डेटा के लिए डिफ़ॉल्ट बार चौड़ाई (मिलीसेकंड)", + "bar-alignment": "बार संरेखण", + "bar-alignment-left": "बाएँ", + "bar-alignment-right": "दाएँ", + "bar-alignment-center": "केंद्र", + "default-font": "डिफ़ॉल्ट फ़ॉन्ट", + "default-font-size": "डिफ़ॉल्ट फ़ॉन्ट आकार", + "default-font-color": "डिफ़ॉल्ट फ़ॉन्ट रंग", + "thresholds-line-width": "सभी थ्रेशहोल्ड्स के लिए डिफ़ॉल्ट लाइन चौड़ाई", + "tooltip-settings": "टूलटिप सेटिंग्स", + "tooltip": "टूलटिप", + "show-tooltip": "टूलटिप दिखाएँ", + "hover-individual-points": "व्यक्तिगत बिंदुओं पर होवर करें", + "show-cumulative-values": "स्टैकिंग मोड में संचयी मान दिखाएँ", + "hide-zero-false-values": "टूलटिप से शून्य/गलत मान छुपाएँ", + "tooltip-value-format-function": "टूलटिप मान फ़ॉर्मेट फ़ंक्शन", + "grid-settings": "ग्रिड सेटिंग्स", + "show-vertical-lines": "वर्टिकल लाइनें दिखाएँ", + "show-horizontal-lines": "हॉरिज़ॉन्टल लाइनें दिखाएँ", + "grid-outline-border-width": "ग्रिड आउटलाइन/बॉर्डर चौड़ाई (px)", + "primary-color": "प्राथमिक रंग", + "background-color": "पृष्ठभूमि रंग", + "ticks-color": "टिक्स रंग", + "xaxis-settings": "X अक्ष सेटिंग्स", + "axis-title": "अक्ष शीर्षक", + "xaxis-tick-labels-settings": "X अक्ष टिक लेबल सेटिंग्स", + "show-tick-labels": "अक्ष टिक लेबल दिखाएँ", + "yaxis-settings": "Y अक्ष सेटिंग्स", + "min-scale-value": "स्केल पर न्यूनतम मान", + "max-scale-value": "स्केल पर अधिकतम मान", + "yaxis-tick-labels-settings": "Y अक्ष टिक लेबल सेटिंग्स", + "tick-step-size": "टिक्स के बीच चरण आकार", + "number-of-decimals": "दिखाने के लिए दशमलवों की संख्या", + "ticks-formatter-function": "टिक्स फ़ॉर्मेटर फ़ंक्शन", + "comparison-settings": "तुलना सेटिंग्स", + "enable-comparison": "तुलना सक्षम करें", + "time-for-comparison": "तुलना अवधि", + "time-for-comparison-previous-interval": "पिछला अंतराल (डिफ़ॉल्ट)", + "time-for-comparison-days": "एक दिन पहले", + "time-for-comparison-weeks": "एक सप्ताह पहले", + "time-for-comparison-months": "एक महीने पहले", + "time-for-comparison-years": "एक वर्ष पहले", + "time-for-comparison-custom-interval": "कस्टम अंतराल", + "custom-interval-value": "कस्टम अंतराल मान (ms)", + "comparison-x-axis-settings": "तुलना X अक्ष सेटिंग्स", + "axis-position": "अक्ष स्थिति", + "axis-position-top": "ऊपर (डिफ़ॉल्ट)", + "axis-position-bottom": "नीचे", + "custom-legend-settings": "कस्टम लेजेंड सेटिंग्स", + "enable-custom-legend": "कस्टम लेजेंड सक्षम करें (यह आपको की लेबल्स में attribute/टाइम सीरीज़ मानों का उपयोग करने की अनुमति देता है)", + "key-name": "कुंजी नाम", + "key-name-required": "कुंजी नाम आवश्यक है", + "key-type": "कुंजी प्रकार", + "key-type-attribute": "विशेषता", + "key-type-timeseries": "टाइम सीरीज़", + "label-keys-list": "लेबल्स में उपयोग की जाने वाली कुंजियों की सूची", + "no-label-keys": "कोई कुंजियाँ कॉन्फ़िगर नहीं की गईं", + "add-label-key": "नई कुंजी जोड़ें", + "line-width": "लाइन चौड़ाई", + "color": "रंग", + "data-is-hidden-by-default": "डेटा डिफ़ॉल्ट रूप से छिपा हुआ है", + "disable-data-hiding": "डेटा छिपाना अक्षम करें", + "remove-from-legend": "लेजेंड से डेटा कुंजी हटाएँ", + "exclude-from-stacking": "स्टैकिंग से बाहर रखें (\"स्टैकिंग मोड\" मोड में उपलब्ध)", + "line-settings": "लाइन सेटिंग्स", + "show-line": "लाइन दिखाएँ", + "fill-line": "लाइन भरें", + "fill-line-opacity": "भराव की अपारदर्शिता", + "points-settings": "बिंदु सेटिंग्स", + "show-points": "बिंदु दिखाएँ", + "points-line-width": "बिंदुओं की लाइन चौड़ाई", + "points-radius": "बिंदुओं का त्रिज्या", + "point-shape": "बिंदु आकृति", + "point-shape-circle": "वृत्त", + "point-shape-cross": "क्रॉस", + "point-shape-diamond": "डायमंड", + "point-shape-square": "वर्ग", + "point-shape-triangle": "त्रिभुज", + "point-shape-custom": "कस्टम फ़ंक्शन", + "point-shape-draw-function": "बिंदु आकृति ड्रा फ़ंक्शन", + "show-separate-axis": "अलग अक्ष दिखाएँ", + "axis-position-left": "बाएँ", + "axis-position-right": "दाएँ", + "thresholds": "थ्रेशहोल्ड्स", + "no-thresholds": "कोई थ्रेशहोल्ड कॉन्फ़िगर नहीं किया गया", + "add-threshold": "थ्रेशहोल्ड जोड़ें", + "show-values-for-comparison": "तुलना के लिए ऐतिहासिक मान दिखाएँ", + "comparison-values-label": "ऐतिहासिक मान लेबल", + "comparison-line-color": "तुलना लाइन रंग", + "threshold-settings": "थ्रेशहोल्ड सेटिंग्स", + "use-as-threshold": "कुंजी मान को थ्रेशहोल्ड के रूप में उपयोग करें", + "threshold-line-width": "थ्रेशहोल्ड लाइन चौड़ाई", + "threshold-color": "थ्रेशहोल्ड रंग", + "common-pie-settings": "सामान्य पाई सेटिंग्स", + "radius": "त्रिज्या", + "inner-radius": "आंतरिक त्रिज्या", + "tilt": "झुकाव", + "common-pie-settings-range-error": "मान 0 से 1 की सीमा में होना चाहिए", + "stroke-settings": "स्ट्रोक सेटिंग्स", + "width-pixels": "चौड़ाई (पिक्सेल)", + "show-labels": "लेबल दिखाएँ", + "animation-settings": "एनिमेशन सेटिंग्स", + "animated-pie": "पाई एनिमेशन सक्षम करें (प्रायोगिक)", + "border-settings": "बॉर्डर सेटिंग्स", + "border-width": "बॉर्डर चौड़ाई", + "border-color": "बॉर्डर रंग", + "legend-settings": "लेजेंड सेटिंग्स", + "display-legend": "लेजेंड प्रदर्शित करें", + "labels-font-color": "लेबल फ़ॉन्ट रंग", + "series": "सीरीज़", + "add-series": "सीरीज़ जोड़ें", + "series-settings": "सीरीज़ सेटिंग्स", + "remove-series": "सीरीज़ हटाएँ", + "no-series": "कोई सीरीज़ कॉन्फ़िगर नहीं की गई", + "no-series-error": "कम से कम एक सीरीज़ निर्दिष्ट होनी चाहिए", + "chart-appearance": "चार्ट दिखावट", + "vertical-grid-lines": "वर्टिकल ग्रिड लाइनें", + "horizontal-grid-lines": "हॉरिज़ॉन्टल ग्रिड लाइनें", + "chart-background": "चार्ट पृष्ठभूमि", + "grid-lines-color": "ग्रिड लाइनों का रंग", + "border": "बॉर्डर", + "axis": "अक्ष", + "vertical-axis": "वर्टिकल अक्ष", + "ticks": "टिक्स", + "horizontal-axis": "हॉरिज़ॉन्टल अक्ष", + "shape-empty-circle": "खाली वृत्त", + "shape-circle": "वृत्त", + "shape-rect": "आयत", + "shape-round-rect": "गोल किनारों वाला आयत", + "shape-triangle": "त्रिभुज", + "shape-diamond": "डायमंड", + "shape-pin": "पिन", + "shape-arrow": "तीर", + "shape-none": "कोई नहीं", + "line-type-solid": "ठोस", + "line-type-dashed": "डैश्ड", + "line-type-dotted": "डॉटेड", + "label-position-top": "ऊपर", + "label-position-bottom": "नीचे", + "label-position-outside": "बाहर", + "label-position-inside": "अंदर", + "fill": "भराव", + "fill-type-none": "कोई नहीं", + "fill-type-solid": "ठोस", + "fill-type-opacity": "अपारदर्शिता", + "fill-type-gradient": "ग्रेडिएंट", + "background": "पृष्ठभूमि", + "opacity": "अपारदर्शिता", + "gradient-stops": "ग्रेडिएंट स्टॉप्स", + "gradient-start": "प्रारंभ", + "gradient-end": "अंत", + "animation": { + "animation": "एनीमेशन", + "animation-threshold": "एनीमेशन थ्रेशहोल्ड", + "animation-duration": "एनीमेशन अवधि", + "animation-easing": "एनीमेशन ईज़िंग", + "animation-delay": "एनीमेशन विलंब", + "update-animation-duration": "अपडेट एनीमेशन अवधि", + "update-animation-easing": "अपडेट एनीमेशन ईज़िंग", + "update-animation-delay": "अपडेट एनीमेशन विलंब" + }, + "chart-axis": { + "limit": "सीमा", + "source": "स्रोत", + "key-value": "कुंजी / मान", + "value-required": "मान आवश्यक है।", + "entity-key-required": "एंटिटी कुंजी आवश्यक है।", + "key-required": "कुंजी आवश्यक है।", + "scale-limits": "स्केल सीमाएँ", + "scale-appearance": "स्केल का रूप", + "scale": "स्केल", + "scale-min": "न्यूनतम", + "scale-max": "अधिकतम", + "scale-auto": "स्वचालित" + }, + "bar": { + "show-border": "बॉर्डर दिखाएँ", + "border-width": "बॉर्डर चौड़ाई", + "border-radius": "बॉर्डर रेडियस", + "bar-width": "बार चौड़ाई", + "label": "लेबल", + "label-hint": "बार के ऊपर लेबल प्रदर्शित करें।", + "series-label-hint": "बार के ऊपर मान सहित लेबल प्रदर्शित करें।", + "label-background": "लेबल पृष्ठभूमि" + } + }, + "color": { + "color-settings": "रंग सेटिंग्स", + "color-type-constant": "स्थायी", + "color-type-gradient": "ग्रेडिएंट", + "color-type-range": "सीमा", + "color-type-function": "फ़ंक्शन", + "color": "रंग", + "value-range": "मान सीमा", + "from": "से", + "to": "तक", + "color-function": "रंग फ़ंक्शन", + "copy-color-settings-from": "यहाँ से रंग सेटिंग्स कॉपी करें", + "copy-from": "यहाँ से कॉपी करें", + "settings-type": "सेटिंग्स प्रकार", + "basic-mode": "बेसिक", + "advanced-mode": "उन्नत", + "entity-alias": "एंटिटी उर्फ़", + "entity-attribute": "एंटिटी विशेषता", + "gradient-color": "ग्रेडिएंट रंग", + "gradient-color-min": "रंग", + "gradient-start": "ग्रेडिएंट प्रारंभ रंग", + "gradient-start-min": "प्रारंभ", + "gradient-end": "ग्रेडिएंट अंतिम रंग", + "gradient-end-min": "अंत", + "start-value": "प्रारंभ मान", + "end-value": "अंत मान", + "gradient-type": "ग्रेडिएंट प्रकार" + }, + "dashboard-state": { + "dashboard-state-settings": "डैशबोर्ड स्टेट सेटिंग्स", + "dashboard-state": "डैशबोर्ड स्टेट ID", + "autofill-state-layout": "डिफ़ॉल्ट रूप से स्टेट लेआउट ऊँचाई स्वतः भरें", + "default-margin": "डिफ़ॉल्ट विजेट मार्जिन", + "default-background-color": "डिफ़ॉल्ट पृष्ठभूमि रंग", + "sync-parent-state-params": "पैरेंट डैशबोर्ड के साथ स्टेट पैरामीटर्स सिंक करें" + }, + "date-range-navigator": { + "date-range-picker-settings": "डेट रेंज पिकर सेटिंग्स", + "hide-date-range-picker": "डेट रेंज पिकर छुपाएँ", + "picker-one-panel": "डेट रेंज पिकर एक पैनल", + "picker-auto-confirm": "डेट रेंज पिकर ऑटो पुष्टि", + "picker-show-template": "डेट रेंज पिकर टेम्पलेट दिखाएँ", + "first-day-of-week": "सप्ताह का पहला दिन", + "interval-settings": "अवधि सेटिंग्स", + "hide-interval": "अवधि छुपाएँ", + "initial-interval": "प्रारंभिक अवधि", + "interval-hour": "घंटा", + "interval-day": "दिन", + "interval-week": "सप्ताह", + "interval-two-weeks": "2 सप्ताह", + "interval-month": "महीना", + "interval-three-months": "3 महीने", + "interval-six-months": "6 महीने", + "step-settings": "चरण सेटिंग्स", + "hide-step-size": "चरण आकार छुपाएँ", + "initial-step-size": "प्रारंभिक चरण आकार", + "hide-labels": "लेबल छुपाएँ", + "use-session-storage": "सेशन स्टोरेज उपयोग करें", + "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": "ओके" + } + }, + "doughnut": { + "doughnut-appearance": "डोनट दिखावट", + "layout": "लेआउट", + "layout-default": "डिफ़ॉल्ट", + "layout-with-total": "कुल सहित", + "central-total-value": "केंद्रीय कुल मान", + "doughnut-card-style": "डोनट कार्ड शैली" + }, + "entities-hierarchy": { + "hierarchy-data-settings": "हाइरार्की डेटा सेटिंग्स", + "relations-query-function": "नोड संबंध क्वेरी फ़ंक्शन", + "has-children-function": "नोड में बच्चे हैं या नहीं फ़ंक्शन", + "node-state-settings": "नोड स्थिति सेटिंग्स", + "node-opened-function": "डिफ़ॉल्ट नोड खुला है फ़ंक्शन", + "node-disabled-function": "नोड अक्षम फ़ंक्शन", + "display-settings": "प्रदर्शन सेटिंग्स", + "node-icon-function": "नोड आइकन फ़ंक्शन", + "node-text-function": "नोड टेक्स्ट फ़ंक्शन", + "sort-settings": "सॉर्ट सेटिंग्स", + "nodes-sort-function": "नोड्स सॉर्ट फ़ंक्शन" + }, + "edge": { + "display-default-title": "डिफ़ॉल्ट शीर्षक प्रदर्शित करें" + }, + "gateway": { + "general-settings": "सामान्य सेटिंग्स", + "widget-title": "विजेट शीर्षक", + "default-archive-file-name": "डिफ़ॉल्ट आर्काइव फ़ाइल नाम", + "device-type-for-new-gateway": "नए Gateway के लिए डिवाइस प्रकार", + "messages-settings": "संदेश सेटिंग्स", + "save-config-success-message": "Gateway कॉन्फ़िगरेशन सफलतापूर्वक सहेजे जाने के बारे में टेक्स्ट संदेश", + "device-name-exists-message": "जब दर्ज किए गए नाम वाला डिवाइस पहले से मौजूद हो, तब का टेक्स्ट संदेश", + "gateway-title": "Gateway फ़ॉर्म", + "read-only": "केवल पढ़ने के लिए", + "events-title": "Gateway इवेंट्स फ़ॉर्म शीर्षक", + "events-filter": "इवेंट्स फ़िल्टर", + "event-key-contains": "इवेंट कुंजी में शामिल है...", + "show-connector": "कनेक्टर के लिए दिखाएँ", + "connector-state-param-key": "कनेक्टर स्टेट पैरामीटर कुंजी", + "message": "संदेश", + "level": "स्तर", + "created-time": "निर्मित समय" + }, + "gauge": { + "default-color": "डिफ़ॉल्ट रंग", + "radial-gauge-settings": "रेडियल गेज सेटिंग्स", + "ticks-settings": "टिक्स सेटिंग्स", + "min-value": "न्यूनतम मान", + "max-value": "अधिकतम मान", + "min-value-short": "न्यूनतम", + "max-value-short": "अधिकतम", + "start-ticks-angle": "टिक्स प्रारंभ कोण", + "ticks-angle": "टिक्स कोण", + "major-ticks": "मुख्य टिक्स", + "major-ticks-count": "मुख्य टिक्स की संख्या", + "major-ticks-color": "मुख्य टिक्स का रंग", + "minor-ticks": "मामूली टिक्स", + "minor-ticks-count": "मामूली टिक्स की संख्या", + "minor-ticks-color": "मामूली टिक्स का रंग", + "tick-numbers-font": "टिक नंबर फ़ॉन्ट", + "unit-title-settings": "यूनिट शीर्षक सेटिंग्स", + "show-unit-title": "यूनिट शीर्षक दिखाएँ", + "unit-title": "यूनिट शीर्षक", + "title-font": "शीर्षक टेक्स्ट फ़ॉन्ट", + "units-settings": "यूनिट सेटिंग्स", + "units-font": "यूनिट टेक्स्ट फ़ॉन्ट", + "value-box-settings": "मान बॉक्स सेटिंग्स", + "show-value-box": "मान बॉक्स दिखाएँ", + "value-box": "मान बॉक्स", + "value-int": "मान के पूर्णांक भाग के अंकों की संख्या", + "value-text": "मान टेक्स्ट", + "value-text-shadow": "मान टेक्स्ट छाया", + "value-font": "मान टेक्स्ट फ़ॉन्ट", + "rect-stroke-color-start": "आयत स्ट्रोक रंग - प्रारंभ ग्रेडिएंट", + "rect-stroke-color-end": "आयत स्ट्रोक रंग - अंत ग्रेडिएंट", + "background-color": "पृष्ठभूमि रंग", + "shadow-color": "छाया रंग", + "value-box-rect-stroke-color": "मान बॉक्स आयत स्ट्रोक रंग", + "value-box-rect-stroke-color-end": "मान बॉक्स आयत स्ट्रोक रंग - अंत ग्रेडिएंट", + "value-box-background-color": "मान बॉक्स पृष्ठभूमि रंग", + "value-box-shadow-color": "मान बॉक्स छाया रंग", + "plate-settings": "प्लेट सेटिंग्स", + "show-plate-border": "प्लेट बॉर्डर", + "plate-color": "प्लेट रंग", + "needle-settings": "सुई सेटिंग्स", + "needle-circle-size": "सुई सर्कल आकार", + "needle-color": "सुई रंग", + "needle-color-start": "सुई रंग - प्रारंभ ग्रेडिएंट", + "needle-color-end": "सुई रंग - अंत ग्रेडिएंट", + "needle-color-shadow-up": "सुई की ऊपरी आधी छाया का रंग", + "needle-color-shadow-down": "ड्रॉप शैडो", + "highlights-settings": "हाइलाइट सेटिंग्स", + "highlights-width": "हाइलाइट चौड़ाई", + "highlights": "हाइलाइट्स", + "highlight-from": "से", + "highlight-to": "तक", + "highlight-color": "रंग", + "no-highlights": "कोई हाइलाइट कॉन्फ़िगर नहीं की गई", + "add-highlight": "हाइलाइट जोड़ें", + "animation-settings": "एनिमेशन सेटिंग्स", + "enable-animation": "एनिमेशन", + "animation-duration-rule": "एनिमेशन अवधि और नियम", + "animation-duration": "एनिमेशन अवधि", + "animation-rule": "एनिमेशन नियम", + "animation-linear": "लीनियर", + "animation-quad": "क्वाड", + "animation-quint": "क्विंट", + "animation-cycle": "सायकल", + "animation-bounce": "बाउंस", + "animation-elastic": "इलास्टिक", + "animation-dequad": "डीक्वाड", + "animation-dequint": "डीक्विंट", + "animation-decycle": "डीसायकल", + "animation-debounce": "डीबाउंस", + "animation-delastic": "डीइलास्टिक", + "linear-gauge-settings": "लीनियर गेज सेटिंग्स", + "bar-stroke": "बार स्ट्रोक", + "bar-stroke-width": "बार स्ट्रोक चौड़ाई", + "bar-stroke-color": "बार स्ट्रोक रंग", + "bar-background-color": "बार पृष्ठभूमि रंग - प्रारंभ ग्रेडिएंट", + "bar-background-color-end": "बार पृष्ठभूमि रंग - अंत ग्रेडिएंट", + "progress-bar-color": "प्रोग्रेस बार रंग", + "progress-bar": "प्रोग्रेस बार", + "progress-bar-color-start": "प्रोग्रेस बार रंग - प्रारंभ ग्रेडिएंट", + "progress-bar-color-end": "प्रोग्रेस बार रंग - अंत ग्रेडिएंट", + "major-ticks-names": "मुख्य टिक्स नाम", + "show-stroke-ticks": "टिक्स स्ट्रोक दिखाएँ", + "major-ticks-font": "मुख्य टिक्स फ़ॉन्ट", + "border-color": "बॉर्डर रंग", + "border-width": "बॉर्डर चौड़ाई", + "needle-circle": "सुई सर्कल", + "needle-circle-color": "सुई सर्कल रंग", + "animation-target": "एनिमेशन लक्ष्य", + "animation-target-needle": "सुई", + "animation-target-plate": "प्लेट", + "common-settings": "सामान्य गेज सेटिंग्स", + "gauge-type": "गेज प्रकार", + "gauge-type-arc": "आर्क", + "gauge-type-donut": "डोनट", + "gauge-type-horizontal-bar": "क्षैतिज बार", + "gauge-type-vertical-bar": "ऊर्ध्वाधर बार", + "donut-start-angle": "आरंभ करने का कोण (डिग्री में)", + "bar-settings": "गेज बार सेटिंग्स", + "relative-bar-width": "सापेक्ष बार चौड़ाई", + "neon-glow-brightness": "नियॉन ग्लो प्रभाव की चमक (0-100)", + "neon-glow-brightness-hint": "0 - प्रभाव अक्षम", + "stripes-thickness": "पट्टियों की मोटाई", + "stripes-thickness-hint": "0 - कोई पट्टियाँ नहीं", + "rounded-line-cap": "गोल लाइन कैप", + "bar-color-settings": "बार रंग सेटिंग्स", + "use-precise-level-color-values": "सटीक रंग स्तरों का उपयोग करें", + "bar-colors": "बार रंग (निम्न से उच्च क्रम में)", + "color": "रंग", + "no-bar-colors": "कोई बार रंग कॉन्फ़िगर नहीं किया गया", + "add-bar-color": "बार रंग जोड़ें", + "from": "से", + "to": "तक", + "fixed-level-colors": "सीमाई मानों का उपयोग करके बार रंग", + "gauge-title-settings": "गेज शीर्षक सेटिंग्स", + "show-gauge-title": "गेज शीर्षक दिखाएँ", + "gauge-title": "गेज शीर्षक", + "gauge-title-font": "गेज शीर्षक फ़ॉन्ट", + "unit-title-and-timestamp-settings": "यूनिट शीर्षक और टाइमस्टैंप सेटिंग्स", + "show-timestamp": "टाइमस्टैंप दिखाएँ", + "timestamp-format": "टाइमस्टैंप फ़ॉर्मेट", + "label-font": "मान के नीचे दिखने वाले लेबल का फ़ॉन्ट", + "value-settings": "मान सेटिंग्स", + "show-value": "मान टेक्स्ट दिखाएँ", + "min-max-settings": "न्यूनतम/अधिकतम लेबल सेटिंग्स", + "show-min-max": "न्यूनतम और अधिकतम मान दिखाएँ", + "min-max-font": "न्यूनतम और अधिकतम लेबल का फ़ॉन्ट", + "show-ticks": "टिक्स दिखाएँ", + "tick-width": "टिक चौड़ाई", + "tick-color": "टिक रंग", + "tick-values": "टिक मान", + "no-tick-values": "कोई टिक मान कॉन्फ़िगर नहीं किया गया", + "add-tick-value": "टिक मान जोड़ें", + "gauge-appearance": "गेज दिखावट", + "units-title": "यूनिट शीर्षक", + "value": "मान", + "ticks": "टिक्स", + "arrow-and-scale-color": "तीर और स्केल का डिफ़ॉल्ट रंग", + "scale-settings": "स्केल सेटिंग्स", + "scale": "स्केल", + "scale-color": "स्केल रंग", + "compass-appearance": "कम्पास दिखावट", + "label": "लेबल", + "labels": "लेबल्स", + "label-style": "लेबल शैली", + "simple-gauge-type": "प्रकार", + "gauge-bar-background": "गेज बार पृष्ठभूमि", + "bar-color": "बार रंग", + "min-and-max-value": "न्यूनतम और अधिकतम मान", + "min-and-max-label": "न्यूनतम और अधिकतम लेबल", + "font": "फ़ॉन्ट", + "tick-width-and-color": "टिक चौड़ाई और रंग", + "min-max-validation-text": "अधिकतम मान न्यूनतम मान से बड़ा होना चाहिए" + }, + "gpio": { + "pin": "पिन", + "label": "लेबल", + "row": "पंक्ति", + "column": "स्तंभ", + "color": "रंग", + "panel-settings": "पैनल सेटिंग्स", + "background-color": "पृष्ठभूमि रंग", + "gpio-switches": "GPIO स्विचेज़", + "no-gpio-switches": "कोई GPIO स्विच कॉन्फ़िगर नहीं किया गया", + "add-gpio-switch": "GPIO स्विच जोड़ें", + "gpio-status-request": "GPIO स्थिति अनुरोध", + "method-name": "मेथड नाम", + "method-body": "मेथड बॉडी", + "gpio-status-change-request": "GPIO स्थिति परिवर्तन अनुरोध", + "parse-gpio-status-function": "GPIO स्थिति पार्स करने का फ़ंक्शन", + "gpio-leds": "GPIO LEDs", + "no-gpio-leds": "कोई GPIO LED कॉन्फ़िगर नहीं की गई", + "add-gpio-led": "GPIO LED जोड़ें" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, + "input-widgets": { + "attribute-not-allowed": "यह विजेट Attribute पैरामीटर का उपयोग नहीं कर सकता", + "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": "वर्तमान स्थान प्राप्त करें", + "invalid-date": "अमान्य तारीख", + "latitude": "अक्षांश", + "longitude": "देशांतर", + "min-value-error": "न्यूनतम मान {{value}} है", + "max-value-error": "अधिकतम मान {{value}} है", + "not-allowed-entity": "चयनित एंटिटी में शेयर की गई विशेषताएँ नहीं हो सकतीं", + "no-attribute-selected": "कोई विशेषता चयनित नहीं है", + "no-datakey-selected": "कोई डेटा-कुंजी चयनित नहीं है", + "no-coordinate-specified": "अक्षांश/देशांतर के लिए डेटाकुंजी निर्दिष्ट नहीं है", + "no-entity-selected": "कोई एंटिटी चयनित नहीं है", + "no-image": "कोई छवि नहीं", + "no-support-geolocation": "आपका ब्राउज़र जियोलोकेशन का समर्थन नहीं करता", + "no-support-web-camera": "आपका ब्राउज़र कैमरा का समर्थन नहीं करता", + "enable-https-use-widget": "कृपया इस विजेट का उपयोग करने के लिए HTTPS सक्षम करें", + "no-found-your-camera": "आपका कैमरा नहीं मिल सका", + "no-permission-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": "मान", + "general-settings": "सामान्य सेटिंग्स", + "widget-title": "विजेट शीर्षक", + "claim-button-label": "क्लेमिंग बटन लेबल", + "show-secret-key-field": "'सीक्रेट कुंजी' इनपुट फ़ील्ड दिखाएँ", + "labels-settings": "लेबल सेटिंग्स", + "show-labels": "लेबल दिखाएँ", + "device-name-label": "डिवाइस नाम इनपुट फ़ील्ड के लिए लेबल", + "secret-key-label": "सीक्रेट कुंजी इनपुट फ़ील्ड के लिए लेबल", + "messages-settings": "संदेश सेटिंग्स", + "claim-device-success-message": "डिवाइस सफलतापूर्वक क्लेम होने का संदेश", + "claim-device-not-found-message": "डिवाइस न मिलने पर संदेश", + "claim-device-failed-message": "डिवाइस क्लेम विफल होने का संदेश", + "claim-device-name-required-message": "'डिवाइस नाम आवश्यक है' त्रुटि संदेश", + "claim-device-secret-key-required-message": "'सीक्रेट कुंजी आवश्यक है' त्रुटि संदेश", + "show-label": "लेबल दिखाएँ", + "label": "लेबल", + "required": "आवश्यक", + "required-error-message": "'आवश्यक' त्रुटि संदेश", + "show-result-message": "परिणाम संदेश दिखाएँ", + "integer-field-settings": "इंटीजर फ़ील्ड सेटिंग्स", + "min-value": "न्यूनतम मान", + "max-value": "अधिकतम मान", + "double-field-settings": "डबल फ़ील्ड सेटिंग्स", + "text-field-settings": "टेक्स्ट फ़ील्ड सेटिंग्स", + "min-length": "न्यूनतम लंबाई", + "max-length": "अधिकतम लंबाई", + "checkbox-settings": "चेकबॉक्स सेटिंग्स", + "true-label": "चेक्ड लेबल", + "false-label": "अनचेक्ड लेबल", + "image-input-settings": "छवि इनपुट सेटिंग्स", + "display-preview": "पूर्वावलोकन दिखाएँ", + "display-clear-button": "क्लियर बटन दिखाएँ", + "display-apply-button": "अप्लाई बटन दिखाएँ", + "display-discard-button": "डिस्कार्ड बटन दिखाएँ", + "datetime-field-settings": "तारीख/समय फ़ील्ड सेटिंग्स", + "display-time-input": "समय इनपुट दिखाएँ", + "latitude-key-name": "अक्षांश कुंजी नाम", + "longitude-key-name": "देशांतर कुंजी नाम", + "show-get-location-button": "'वर्तमान स्थान प्राप्त करें' बटन दिखाएँ", + "use-high-accuracy": "उच्च सटीकता उपयोग करें", + "location-fields-settings": "स्थान फ़ील्ड सेटिंग्स", + "latitude-label": "अक्षांश के लिए लेबल", + "longitude-label": "देशांतर के लिए लेबल", + "input-fields-alignment": "इनपुट फ़ील्ड्स संरेखण", + "input-fields-alignment-column": "स्तंभ (डिफ़ॉल्ट)", + "input-fields-alignment-row": "पंक्ति", + "layout": "लेआउट", + "row-gap": "पंक्तियों के बीच का अंतर (पिक्सेल में)", + "column-gap": "स्तंभों के बीच का अंतर (पिक्सेल में)", + "latitude-field-required": "अक्षांश फ़ील्ड आवश्यक है", + "longitude-field-required": "देशांतर फ़ील्ड आवश्यक है", + "attribute-settings": "विशेषता सेटिंग्स", + "widget-mode": "विजेट मोड", + "widget-mode-update-attribute": "विशेषता अपडेट करें", + "widget-mode-update-timeseries": "टाइम सीरीज़ अपडेट करें", + "attribute-scope": "विशेषता स्कोप", + "attribute-scope-server": "सर्वर विशेषता", + "attribute-scope-shared": "शेयर की गई विशेषता", + "value-required": "मान आवश्यक है", + "image-settings": "छवि सेटिंग्स", + "image-format": "छवि फ़ॉर्मेट", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "छवि गुणवत्ता (हानिपूर्ण संपीड़न वाले फ़ॉर्मेट जैसे JPEG और WEBP के लिए)", + "max-image-width": "अधिकतम छवि चौड़ाई", + "max-image-height": "अधिकतम छवि ऊँचाई", + "action-buttons": "एक्शन बटन", + "show-action-buttons": "एक्शन बटन दिखाएँ", + "update-all-values": "सिर्फ़ बदलाव नहीं, सभी मान अपडेट करें", + "save-button-label": "'SAVE' बटन लेबल", + "reset-button-label": "'UNDO' बटन लेबल", + "group-settings": "समूह सेटिंग्स", + "show-group-title": "विभिन्न एंटिटीज़ से संबंधित फ़ील्ड्स के समूह का शीर्षक दिखाएँ", + "group-title": "समूह शीर्षक", + "fields-alignment": "फ़ील्ड्स संरेखण", + "fields-alignment-row": "पंक्ति (डिफ़ॉल्ट)", + "fields-alignment-column": "स्तंभ", + "fields-in-row": "पंक्ति में फ़ील्ड्स की संख्या", + "option-value": "मान (खाली विकल्प बनाने के लिए 'null' लिखें)", + "option-label": "लेबल", + "hide-input-field": "इनपुट फ़ील्ड छुपाएँ", + "datakey-type": "डेटाकुंजी प्रकार", + "datakey-type-server": "सर्वर विशेषता (डिफ़ॉल्ट)", + "datakey-type-shared": "शेयर की गई विशेषता", + "datakey-type-timeseries": "टाइम सीरीज़", + "datakey-value-type": "डेटाकुंजी मान प्रकार", + "datakey-value-type-string": "स्ट्रिंग", + "datakey-value-type-double": "डबल", + "datakey-value-type-integer": "इंटीजर", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "बूलियन (चेकबॉक्स)", + "datakey-value-type-boolean-switch": "बूलियन (स्विच)", + "datakey-value-type-date-time": "तारीख और समय", + "datakey-value-type-date": "तारीख", + "datakey-value-type-time": "समय", + "datakey-value-type-select": "सेलेक्ट", + "datakey-value-type-radio": "रेडियो", + "datakey-value-type-color": "रंग", + "value-is-required": "मान आवश्यक है", + "ability-to-edit-attribute": "विशेषता संपादित करने की अनुमति", + "ability-to-edit-attribute-editable": "संपादन योग्य (डिफ़ॉल्ट)", + "ability-to-edit-attribute-disabled": "अक्षम", + "ability-to-edit-attribute-readonly": "केवल पढ़ने योग्य", + "disable-on-datakey-name": "अन्य डेटाकुंजी के false मान पर अक्षम करें (डेटाकुंजी नाम निर्दिष्ट करें)", + "field-appearance": "फ़ील्ड दिखावट", + "appearance-fill": "भराव", + "appearance-outline": "आउटलाइन", + "subscript-sizing": "सबस्क्रिप्ट आकार निर्धारण", + "subscript-sizing-fixed": "स्थिर", + "subscript-sizing-dynamic": "डायनैमिक", + "slide-toggle-settings": "स्लाइड टॉगल सेटिंग्स", + "slide-toggle-label-position": "स्लाइड टॉगल लेबल स्थिति", + "slide-toggle-label-position-after": "बाद में", + "slide-toggle-label-position-before": "पहले", + "select-options": "सेलेक्ट विकल्प", + "no-select-options": "कोई सेलेक्ट विकल्प कॉन्फ़िगर नहीं किया गया", + "add-select-option": "सेलेक्ट विकल्प जोड़ें", + "numeric-field-settings": "न्यूमेरिक फ़ील्ड सेटिंग्स", + "step-interval": "मानों के बीच चरण अंतराल", + "error-messages": "त्रुटि संदेश", + "min-value-error-message": "'न्यूनतम मान' त्रुटि संदेश", + "max-value-error-message": "'अधिकतम मान' त्रुटि संदेश", + "invalid-date-error-message": "'अमान्य तारीख' त्रुटि संदेश", + "invalid-JSON-error-message": "'अमान्य JSON' त्रुटि संदेश", + "icon-settings": "आइकन सेटिंग्स", + "dialog-editor-settings": "डायलॉग संपादक सेटिंग्स", + "use-custom-icon": "कस्टम आइकन उपयोग करें", + "input-cell-icon": "इनपुट सेल से पहले दिखाने वाला आइकन", + "value-conversion-settings": "मान परिवर्तन सेटिंग्स", + "get-value-settings": "मान प्राप्त करने की सेटिंग्स", + "use-get-value-function": "getValue फ़ंक्शन उपयोग करें", + "get-value-function": "getValue फ़ंक्शन", + "set-value-settings": "मान सेट करने की सेटिंग्स", + "use-set-value-function": "setValue फ़ंक्शन उपयोग करें", + "set-value-function": "setValue फ़ंक्शन", + "json-invalid": "JSON मान का फ़ॉर्मेट अमान्य है", + "title": "शीर्षक", + "cancel-button-label": "'Cancel' बटन लेबल", + "radio-button-settings": "रेडियो बटन सेटिंग्स", + "color": "रंग", + "columns": "स्तंभ", + "radio-options": "रेडियो विकल्प", + "no-radio-options": "कोई रेडियो विकल्प कॉन्फ़िगर नहीं किए गए", + "add-radio-option": "रेडियो विकल्प जोड़ें", + "radio-label-position": "लेबल स्थिति", + "radio-label-position-before": "पहले", + "radio-label-position-after": "बाद में", + "save-image": "छवि सहेजें", + "save-to-gallery": "स्वचालित रूप से कैप्चर की गई छवियाँ इमेज गैलरी में स्टोर करें", + "public-image": "छवि को किसी भी अनधिकृत उपयोगकर्ता के लिए उपलब्ध बनाता है" + }, + "invalid-qr-code-text": "QR कोड के लिए इनपुट टेक्स्ट अमान्य है। इनपुट का प्रकार स्ट्रिंग होना चाहिए", + "qr-code": { + "use-qr-code-text-function": "QR कोड टेक्स्ट फ़ंक्शन उपयोग करें", + "qr-code-text-pattern": "QR कोड टेक्स्ट पैटर्न (उदाहरण: '${entityName} | ${keyName} - कुछ टेक्स्ट.')", + "qr-code-text-pattern-hint": "QR कोड टेक्स्ट पैटर्न एंटिटी उपनाम में एंटिटीज़ के भीतर पहली मिली कुंजी के मान का उपयोग करता है।", + "qr-code-text-pattern-required": "QR कोड टेक्स्ट पैटर्न आवश्यक है।", + "qr-code-text-function": "QR कोड टेक्स्ट फ़ंक्शन" + }, + "label-widget": { + "label-pattern": "पैटर्न", + "label-pattern-hint": "संकेत: उदाहरण: 'टेक्स्ट ${keyName} यूनिट्स.' या ${#<key index>} units'", + "label-pattern-required": "पैटर्न आवश्यक है", + "label-position": "स्थिति (पृष्ठभूमि के सापेक्ष प्रतिशत)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "पृष्ठभूमि रंग", + "font-settings": "फ़ॉन्ट सेटिंग्स", + "background-image": "पृष्ठभूमि छवि", + "labels": "लेबल्स", + "no-labels": "कोई लेबल कॉन्फ़िगर नहीं किए गए", + "add-label": "लेबल जोड़ें" + }, + "navigation": { + "title": "शीर्षक", + "navigation-path": "नेविगेशन पथ", + "filter-type": "फ़िल्टर प्रकार", + "filter-type-all": "सभी आइटम", + "filter-type-include": "आइटम शामिल करें", + "filter-type-exclude": "आइटम बाहर रखें", + "items": "आइटम", + "enter-urls-to-filter": "फ़िल्टर करने के लिए URLs दर्ज करें..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "संदेश प्रकार", + "method": "मेथड", + "params": "पैरामीटर्स", + "created-time": "निर्माण समय", + "expiration-time": "समाप्ति समय", + "retries": "पुनः प्रयास", + "status": "स्थिति", + "filter": "फ़िल्टर", + "refresh": "रिफ्रेश", + "add": "Persistent RPC अनुरोध जोड़ें", + "details": "विवरण", + "delete": "हटाएँ", + "delete-request-title": "Persistent RPC अनुरोध हटाएँ", + "delete-request-text": "क्या आप वाकई इस अनुरोध को हटाना चाहते हैं?", + "details-title": "विवरण RPC ID: ", + "additional-info": "अतिरिक्त जानकारी", + "response": "प्रतिक्रिया", + "any-status": "कोई भी स्थिति", + "rpc-status-list": "RPC स्थिति सूची", + "no-request-prompt": "प्रदर्शित करने के लिए कोई अनुरोध नहीं", + "send-request": "अनुरोध भेजें", + "add-title": "Persistent RPC अनुरोध बनाएँ", + "method-error": "मेथड आवश्यक है।", + "timeout-error": "न्यूनतम टाइमआउट मान 5000 (5 सेकंड) है।", + "white-space-error": "Whitespace की अनुमति नहीं है.", + "rpc-status": { + "QUEUED": "कतारबद्ध", + "SENT": "भेजा गया", + "DELIVERED": "डिलीवर किया गया", + "SUCCESSFUL": "सफल", + "TIMEOUT": "टाइमआउट", + "EXPIRED": "समाप्त", + "FAILED": "विफल" + }, + "rpc-search-status-all": "सभी", + "message-types": { + "false": "टू-वे", + "true": "वन-वे" + }, + "general-settings": "सामान्य सेटिंग्स", + "enable-filter": "फ़िल्टर सक्षम करें", + "enable-sticky-header": "स्क्रॉल करते समय हेडर दिखाएँ", + "enable-sticky-action": "स्क्रॉल करते समय एक्शन कॉलम दिखाएँ", + "display-request-details": "अनुरोध विवरण दिखाएँ", + "allow-send-request": "RPC अनुरोध भेजने की अनुमति दें", + "allow-delete-request": "अनुरोध हटाने की अनुमति दें", + "columns-settings": "कॉलम सेटिंग्स", + "display-columns": "दिखाने के लिए कॉलम", + "column": "कॉलम", + "no-columns-found": "कोई कॉलम नहीं मिला", + "no-columns-matching": "'{{column}}' नहीं मिला." + }, + "range-chart": { + "chart": "चार्ट", + "data-zoom": "डेटा ज़ूम", + "range-chart-appearance": "रेंज चार्ट दिखावट", + "range-colors": "रेंज रंग", + "out-of-range-color": "रेंज से बाहर का रंग", + "show-range-thresholds": "रेंज थ्रेशहोल्ड्स दिखाएँ", + "range-thresholds-settings": "रेंज थ्रेशहोल्ड्स सेटिंग्स", + "fill-area": "क्षेत्र भरें", + "fill-area-opacity": "क्षेत्र की अपारदर्शिता", + "range-chart-style": "रेंज चार्ट शैली" + }, + "knob": { + "behavior": "व्यवहार", + "initial-value": "प्रारंभिक मान", + "initial-value-hint": "नॉब के प्रारंभिक मान को प्राप्त करने के लिए क्रिया।", + "on-value-change": "मान बदलने पर", + "on-value-change-hint": "जब नॉब का मान बदलता है तब क्रिया ट्रिगर होती है।", + "range": "सीमा", + "min": "न्यूनतम", + "max": "अधिकतम", + "value": "मान", + "fallback-initial-value": "फॉलबैक प्रारंभिक मान" + }, + "rpc": { + "value-settings": "मान सेटिंग्स", + "initial-value": "प्रारंभिक मान", + "retrieve-value-settings": "On/Off मान प्राप्त करने की सेटिंग्स", + "retrieve-value-method": "मान प्राप्त करने की विधि", + "retrieve-value-method-none": "प्राप्त न करें", + "retrieve-value-method-rpc": "RPC get value मेथड कॉल करें", + "retrieve-value-method-attribute": "एट्रिब्यूट को सब्सक्राइब करें", + "retrieve-value-method-timeseries": "टाइम सीरीज़ को सब्सक्राइब करें", + "attribute-value-key": "एट्रिब्यूट कुंजी", + "timeseries-value-key": "टाइम सीरीज़ कुंजी", + "get-value-method": "RPC get value मेथड", + "parse-value-function": "मान पार्स करने का फ़ंक्शन", + "update-value-settings": "मान अपडेट करने की सेटिंग्स", + "set-value-method": "RPC set value मेथड", + "convert-value-function": "मान रूपांतरण फ़ंक्शन", + "rpc-settings": "RPC सेटिंग्स", + "request-timeout": "RPC अनुरोध टाइमआउट (ms)", + "persistent-rpc-settings": "Persistent RPC सेटिंग्स", + "request-persistent": "Persistent RPC अनुरोध", + "persistent-polling-interval": "Persistent RPC प्रतिक्रिया प्राप्त करने के लिए पोलिंग अंतराल (ms)", + "common-settings": "सामान्य सेटिंग्स", + "switch-title": "स्विच शीर्षक", + "show-on-off-labels": "On/Off लेबल दिखाएँ", + "slide-toggle-label": "स्लाइड टॉगल लेबल", + "label-position": "लेबल स्थिति", + "label-position-before": "पहले", + "label-position-after": "बाद में", + "slider-color": "स्लाइडर रंग", + "slider-color-primary": "प्राथमिक", + "slider-color-accent": "एक्सेंट", + "slider-color-warn": "चेतावनी", + "button-style": "बटन शैली", + "button-raised": "रेज़्ड बटन", + "button-primary": "प्राथमिक रंग", + "button-background-color": "बटन पृष्ठभूमि रंग", + "button-text-color": "बटन टेक्स्ट रंग", + "widget-title": "विजेट शीर्षक", + "button-label": "बटन लेबल", + "device-attribute-scope": "डिवाइस एट्रिब्यूट स्कोप", + "server-attribute": "सर्वर एट्रिब्यूट", + "shared-attribute": "शेयर की गई एट्रिब्यूट", + "device-attribute-parameters": "डिवाइस एट्रिब्यूट पैरामीटर्स", + "is-one-way-command": "वन-वे कमांड है", + "rpc-method": "RPC मेथड", + "rpc-method-params": "RPC मेथड पैरामीटर्स", + "show-rpc-error": "RPC कमांड निष्पादन त्रुटि दिखाएँ", + "led-title": "LED शीर्षक", + "led-color": "LED रंग", + "check-status-settings": "स्टेटस जाँच सेटिंग्स", + "perform-rpc-status-check": "RPC डिवाइस स्टेटस जाँच करें", + "retrieve-led-status-value-method": "LED स्टेटस मान को प्राप्त करने की विधि", + "led-status-value-attribute": "डिवाइस एट्रिब्यूट जिसमें LED स्टेटस मान है", + "led-status-value-timeseries": "डिवाइस टाइम सीरीज़ जिसमें LED स्टेटस मान है", + "check-status-method": "RPC डिवाइस स्टेटस जाँच मेथड", + "parse-led-status-value-function": "LED स्टेटस मान पार्स करने का फ़ंक्शन", + "knob-title": "नॉब शीर्षक" + }, + "maps": { + "map-type": { + "type": "मैप प्रकार", + "map": "मैप", + "image": "छवि" + }, + "image": { + "image-source": "छवि स्रोत", + "image-source-image": "छवि", + "image-source-entity-key": "एंटिटी कुंजी", + "source-entity-alias": "सोर्स एंटिटी उपनाम", + "image-url-key": "छवि URL कुंजी", + "image-url-key-required": "छवि URL कुंजी आवश्यक है" + }, + "control": { + "map-controls": "मैप नियंत्रण", + "position": "स्थिति", + "position-topleft": "ऊपर-बाएँ", + "position-topright": "ऊपर-दाएँ", + "position-bottomleft": "नीचे-बाएँ", + "position-bottomright": "नीचे-दाएँ", + "zoom-actions": "ज़ूम क्रियाएँ", + "zoom-scroll": "स्क्रॉल", + "zoom-double-click": "डबल-क्लिक", + "zoom-control-buttons": "कंट्रोल बटन", + "scale": "स्केल", + "scale-metric": "मेट्रिक", + "scale-imperial": "इम्पीरियल", + "switch-to-drag-mode-using-button": "बटन का उपयोग करके ड्रैग मोड पर स्विच करें" + }, + "timeline": { + "control-panel": "टाइमलाइन नियंत्रण पैनल", + "time-step": "समय चरण", + "speed-options": "गति विकल्प", + "timestamp": "टाइमस्टैम्प", + "snap-to-real-location": "वास्तविक स्थान पर स्नैप करें", + "location-snap-filter-function": "लोकेशन स्नैप फ़िल्टर फ़ंक्शन", + "no-trips-data-available": "कोई ट्रिप डेटा उपलब्ध नहीं" + }, + "map-action": { + "map-action-buttons": "मैप एक्शन बटन", + "label": "लेबल", + "icon": "आइकन", + "color": "रंग", + "action": "एक्शन", + "add-button": "बटन जोड़ें", + "no-action-buttons-configured": "कोई एक्शन बटन कॉन्फ़िगर नहीं किया गया", + "remove-action-button": "एक्शन बटन हटाएँ", + "map-action-button": "मैप एक्शन बटन", + "button-requires": "बटन के लिए लेबल या आइकन आवश्यक है" + }, + "common": { + "common-map-settings": "सामान्य मैप सेटिंग्स", + "fit-map-bounds": "सभी मार्कर्स को कवर करने के लिए मैप सीमा फिट करें", + "default-map-center-position": "डिफ़ॉल्ट मैप केंद्र स्थिति", + "default-map-zoom-level": "डिफ़ॉल्ट मैप ज़ूम स्तर", + "entities-limit": "लोड करने के लिए एंटिटीज़ की सीमा" + }, + "layer": { + "label": "लेबल", + "layer": "लेयर", + "layers": "लेयर्स", + "map-layers": "मैप लेयर्स", + "add-layer": "लेयर जोड़ें", + "layer-settings": "लेयर सेटिंग्स", + "remove-layer": "लेयर हटाएँ", + "no-layers": "कोई लेयर्स कॉन्फ़िगर नहीं की गईं", + "roadmap": "रोडमैप", + "satellite": "सैटेलाइट", + "hybrid": "हाइब्रिड", + "reference": { + "reference-layer": "रेफरेंस लेयर", + "no-layer": "कोई लेयर नहीं", + "openstreetmap-hybrid": "OpenStreetMap हाइब्रिड", + "world-edition-hybrid": "वर्ल्ड एडिशन हाइब्रिड", + "enhanced-contrast-hybrid": "एन्हांस्ड कॉन्ट्रास्ट हाइब्रिड" + }, + "provider": { + "provider": "प्रोवाइडर", + "openstreet": { + "title": "OpenStreet", + "mapnik": "Mapnik", + "hot": "HOT", + "esri-street": "WorldStreetMap", + "esri-topo": "WorldTopoMap", + "esri-imagery": "WorldImagery", + "cartodb-positron": "Positron", + "cartodb-dark-matter": "DarkMatter" + }, + "google": { + "title": "Google", + "roadmap": "Roadmap", + "satellite": "Satellite", + "hybrid": "Hybrid", + "terrain": "Terrain" + }, + "here": { + "title": "HERE", + "normal-day": "Normal day", + "normal-night": "Normal night", + "hybrid-day": "Hybrid day", + "terrain-day": "Terrain day" + }, + "tencent": { + "title": "Tencent", + "normal": "Normal", + "satellite": "Satellite", + "terrain": "Terrain" + }, + "custom": { + "title": "Custom", + "tile-url": "Tile URL" + } + }, + "credentials": { + "credentials": "Credentials", + "api-key": "API Key" + } + }, + "overlays": { + "overlays": "ओवरलेज़", + "overlays-hint": "मैप एंटिटीज़ के लिए डेटा स्रोत, दिखावट, व्यवहार, संपादन विकल्प और ग्रुपिंग कॉन्फ़िगर करें", + "trips": "ट्रिप्स", + "markers": "मार्कर्स", + "polygons": "पॉलीगॉन्स", + "circles": "सर्कल्स", + "polylines": "पॉलीलाइन्स" + }, + "data-layer": { + "source": "सोर्स", + "filter": "फ़िल्टर", + "additional-data-keys": "अतिरिक्त डेटा कीज़", + "additional-datasources": "अतिरिक्त डेटा सोर्सेज़", + "additional-datasources-hint": "उन एंटिटीज़ के एट्रिब्यूट्स या टेलीमेट्री तक पहुँचने के लिए डेटा सोर्स जिन्हें मैप पर प्रदर्शित नहीं किया जाता; इन्हें मैप ओवरले फ़ंक्शंस में उपयोग किया जा सकता है।", + "more-datasources": "अधिक डेटा सोर्सेज़", + "data-keys": "डेटा कीज़", + "add-datasource": "डेटा सोर्स जोड़ें", + "no-datasources": "कोई डेटा सोर्स कॉन्फ़िगर नहीं किया गया", + "remove-datasource": "डेटा सोर्स हटाएँ", + "behavior": "व्यवहार", + "on-click": "क्लिक पर", + "on-click-hint": "जब उपयोगकर्ता मैप आइटम पर क्लिक करता है तब क्रिया प्रारंभ होती है।", + "groups": "ग्रुप्स", + "groups-hint": "ओवरले को सौंपे गए ग्रुप नामों की सूची, जिनसे मैप पर उसकी दृश्यता को टॉगल किया जाता है।", + "color": "रंग", + "color-settings": "रंग सेटिंग्स", + "color-type-constant": "स्थिर", + "color-type-range": "रेंज", + "color-type-function": "फ़ंक्शन", + "color-range-source-key": "रंग रेंज सोर्स की", + "color-range-source-key-required": "रंग रेंज सोर्स की आवश्यक है", + "color-range": "रंग रेंज", + "color-function": "रंग फ़ंक्शन", + "label": "लेबल", + "tooltip": "टूलटिप", + "pattern-type-pattern": "पैटर्न", + "pattern-type-function": "फ़ंक्शन", + "label-pattern": "लेबल (पैटर्न उदाहरण: '${entityName}', '${entityName}: (टेक्स्ट ${keyName} यूनिट्स.)')", + "label-function": "लेबल फ़ंक्शन", + "tooltip-pattern": "टूलटिप (उदाहरण: 'टेक्स्ट ${keyName} यूनिट्स.' या लिंक टेक्स्ट)", + "tooltip-function": "टूलटिप फ़ंक्शन", + "tooltip-trigger": "टूलटिप ट्रिगर", + "tooltip-trigger-click": "क्लिक पर टूलटिप दिखाएँ", + "tooltip-trigger-hover": "होवर पर टूलटिप दिखाएँ", + "auto-close-tooltips": "टूलटिप्स स्वतः बंद करें", + "tooltip-offset": "टूलटिप ऑफ़सेट", + "tooltip-offset-horizontal": "क्षैतिज", + "tooltip-offset-vertical": "ऊर्ध्वाधर", + "tooltip-tag-actions": "टैग क्रियाएँ", + "add-tooltip-tag-action": "टैग क्रिया जोड़ें", + "edit-tooltip-tag-action": "टैग क्रिया संपादित करें", + "remove-tooltip-tag-action": "टैग क्रिया हटाएँ", + "action-add": "जोड़ें", + "action-edit": "संपादित करें", + "action-move": "स्थानांतरित करें", + "action-remove": "हटाएँ", + "edit-instruments": "उपकरण", + "persist-location-attribute-scope": "लोकेशन को संरक्षित करने के लिए एट्रिब्यूट का स्कोप", + "enable-snapping": "सटीक ड्रॉइंग के लिए अन्य वर्टिसेस के साथ स्नैपिंग सक्षम करें", + "enable-snapping-hint": "नई पॉइंट्स को मौजूदा आकृतियों के साथ स्वचालित रूप से संरेखित करता है जिससे ड्रॉइंग अधिक आसान और सटीक हो जाती है।", + "drag-drop-mode": "ड्रैग-ड्रॉप मोड", + "trip": { + "no-trips": "कोई ट्रिप कॉन्फ़िगर नहीं की गई", + "add-trip": "ट्रिप जोड़ें", + "trip-configuration": "ट्रिप कॉन्फ़िगरेशन", + "remove-trip": "ट्रिप हटाएँ" + }, + "marker": { + "marker": "मार्कर", + "latitude-key": "Latitude कुंजी", + "longitude-key": "Longitude कुंजी", + "x-pos-key": "X स्थिति कुंजी", + "y-pos-key": "Y स्थिति कुंजी", + "latitude-key-required": "Latitude कुंजी आवश्यक है", + "longitude-key-required": "Longitude कुंजी आवश्यक है", + "x-pos-key-required": "X स्थिति कुंजी आवश्यक है", + "y-pos-key-required": "Y स्थिति कुंजी आवश्यक है", + "no-markers": "कोई मार्कर्स कॉन्फ़िगर नहीं किए गए", + "add-marker": "मार्कर जोड़ें", + "marker-configuration": "मार्कर कॉन्फ़िगरेशन", + "remove-marker": "मार्कर हटाएँ", + "marker-type": "मार्कर प्रकार", + "marker-type-shape": "आकृति", + "marker-type-icon": "आइकन", + "marker-type-image": "छवि", + "shape": "आकृति", + "icon": "आइकन", + "image": "छवि", + "marker-shapes": "मार्कर आकृतियाँ", + "marker-icon": "मार्कर आइकन", + "marker-appearance": "मार्कर दिखावट", + "marker-image": "मार्कर छवि", + "marker-image-type-image": "छवि", + "marker-image-type-function": "फ़ंक्शन", + "custom-marker-image-size": "कस्टम मार्कर इमेज आकार", + "marker-image-function": "मार्कर इमेज फ़ंक्शन", + "marker-images": "मार्कर छवियाँ", + "marker-offset": "मार्कर ऑफ़सेट", + "offset-horizontal": "क्षैतिज", + "offset-vertical": "ऊर्ध्वाधर", + "rotate-marker": "मार्कर घुमाएँ", + "offset-angle": "ऑफ़सेट कोण", + "position-conversion": "स्थिति रूपांतरण", + "position-conversion-function": "स्थिति रूपांतरण फ़ंक्शन, जो 0 से 1 तक प्रत्येक के लिए double के रूप में x,y निर्देशांक वापस करता है", + "clustering": { + "use-map-markers-clustering": "मैप मार्कर्स क्लस्टरिंग का उपयोग करें", + "zoom-on-cluster-click": "क्लस्टर पर क्लिक करने पर ज़ूम करें", + "max-zoom": "अधिकतम ज़ूम स्तर जिस पर एक मार्कर क्लस्टर का हिस्सा हो सकता है (0 - 18)", + "max-radius": "अधिकतम त्रिज्या जो एक क्लस्टर कवर करेगा", + "zoom-animation": "ज़ूम करते समय मार्कर्स पर एनीमेशन", + "bounds-on-cluster-mouse-over": "क्लस्टर पर माउस ओवर होने पर मार्कर्स की सीमाएँ", + "spiderfy-max-zoom-level": "मैक्स ज़ूम स्तर पर स्पाइडरफाई (सभी क्लस्टर मार्कर्स देखने के लिए)", + "load-optimization": "लोड ऑप्टिमाइज़ेशन", + "chunked-load": "मार्कर्स जोड़ने के लिए चंक्स का उपयोग करें ताकि पेज फ्रीज़ न हो", + "lazy-load": "मार्कर्स जोड़ने के लिए लेज़ी लोड का उपयोग करें", + "use-cluster-marker-color-function": "क्लस्टर मार्कर्स का रंग फ़ंक्शन उपयोग करें", + "marker-color-function": "मार्कर रंग फ़ंक्शन" + }, + "edit": "मार्कर संपादित करें", + "remove-marker-for": "'{{entityName}}' के लिए मार्कर हटाएँ", + "place-marker": "मार्कर रखें", + "place-marker-hint": "मार्कर रखने के लिए क्लिक करें", + "place-marker-hint-with-entity": "'{{entityName}}' एंटिटी को रखने के लिए क्लिक करें" + }, + "path": { + "path": "पाथ", + "path-decorator": "पाथ डेकोरेटर", + "decorator-symbol": "डेकोरेटर प्रतीक", + "decorator-symbol-arrow-head": "तीर", + "decorator-symbol-dash": "डैश", + "decorator-arrangement": "डेकोरेटर व्यवस्था", + "decorator-offset": "आरंभ", + "decorator-end-offset": "समाप्ति", + "decorator-repeat": "दोहराएँ" + }, + "points": { + "points": "प्वाइंट्स", + "point-tooltip": "प्वाइंट टूलटिप" + }, + "shape": { + "fill": "भराव", + "fill-type-color": "रंग", + "fill-type-stripe": "स्ट्राइप", + "fill-type-image": "छवि", + "color": "रंग", + "stripe": "स्ट्राइप", + "image": "छवि", + "stroke": "स्ट्रोक", + "fill-image": "भराव छवि", + "fill-image-type-image": "छवि", + "fill-image-type-function": "फ़ंक्शन", + "preserve-aspect-ratio": "आस्पेक्ट अनुपात बनाए रखें", + "opacity": "अपारदर्शिता", + "angle": "रोटेशन कोण", + "scale": "स्केल", + "fill-image-function": "आकार भरने की छवि फ़ंक्शन", + "fill-images": "आकार भरने की छवियाँ", + "stripe-pattern": "स्ट्राइप पैटर्न", + "first-stripe": "पहला स्ट्राइप", + "second-stripe": "दूसरा स्ट्राइप" + }, + "polygon": { + "polygon-key": "पॉलीगॉन कुंजी", + "polygon-key-required": "पॉलीगॉन कुंजी आवश्यक है", + "no-polygons": "कोई पॉलीगॉन्स कॉन्फ़िगर नहीं किए गए", + "add-polygon": "पॉलीगॉन जोड़ें", + "polygon-configuration": "पॉलीगॉन कॉन्फ़िगरेशन", + "remove-polygon": "पॉलीगॉन हटाएँ", + "edit": "पॉलीगॉन संपादित करें", + "remove-polygon-for": "'{{entityName}}' के लिए पॉलीगॉन हटाएँ", + "cut": "पॉलीगॉन क्षेत्र काटें", + "rotate": "पॉलीगॉन घुमाएँ", + "draw-rectangle": "रेक्टैंगल बनाएं", + "draw-polygon": "पॉलीगॉन बनाएं", + "polygon-place-first-point-cut-hint": "पहला बिंदु रखने के लिए क्लिक करें", + "continue-polygon-cut-hint": "ड्रॉइंग जारी रखने के लिए क्लिक करें", + "finish-polygon-cut-hint": "समाप्त करने और सहेजने के लिए पहले मार्कर पर क्लिक करें", + "polygon-place-first-point-hint": "पॉलीगॉन: पहला बिंदु रखने के लिए क्लिक करें", + "polygon-place-first-point-hint-with-entity": "'{{entityName}}' के लिए पॉलीगॉन: पहला बिंदु रखने के लिए क्लिक करें", + "continue-polygon-hint": "पॉलीगॉन: ड्रॉइंग जारी रखने के लिए क्लिक करें", + "continue-polygon-hint-with-entity": "'{{entityName}}' के लिए पॉलीगॉन: ड्रॉइंग जारी रखने के लिए क्लिक करें", + "finish-polygon-hint": "पॉलीगॉन: ड्रॉइंग समाप्त करने के लिए पहले मार्कर पर क्लिक करें", + "finish-polygon-hint-with-entity": "'{{entityName}}' के लिए पॉलीगॉन: समाप्त करने और सहेजने के लिए पहले मार्कर पर क्लिक करें", + "rectangle-place-first-point-hint": "रेक्टैंगल: पहला बिंदु रखने के लिए क्लिक करें", + "rectangle-place-first-point-hint-with-entity": "'{{entityName}}' के लिए रेक्टैंगल: पहला बिंदु रखने के लिए क्लिक करें", + "finish-rectangle-hint": "रेक्टैंगल: ड्रॉइंग समाप्त करने के लिए क्लिक करें", + "finish-rectangle-hint-with-entity": "'{{entityName}}' के लिए रेक्टैंगल: समाप्त करने और सहेजने के लिए क्लिक करें" + }, + "circle": { + "circle-key": "सर्कल कुंजी", + "circle-key-required": "सर्कल कुंजी आवश्यक है", + "no-circles": "कोई सर्कल कॉन्फ़िगर नहीं किए गए", + "add-circle": "सर्कल जोड़ें", + "circle-configuration": "सर्कल कॉन्फ़िगरेशन", + "remove-circle": "सर्कल हटाएँ", + "edit": "सर्कल संपादित करें", + "remove-circle-for": "'{{entityName}}' के लिए सर्कल हटाएँ", + "draw-circle": "सर्कल बनाएं", + "place-circle-center-hint-with-entity": "'{{entityName}}' के लिए सर्कल: केंद्र रखने के लिए क्लिक करें", + "place-circle-center-hint": "सर्कल: केंद्र रखने के लिए क्लिक करें", + "finish-circle-hint-with-entity": "'{{entityName}}' के लिए सर्कल: समाप्त करने और सहेजने के लिए क्लिक करें", + "finish-circle-hint": "सर्कल: ड्रॉइंग समाप्त करने के लिए क्लिक करें" + }, + "polyline": { + "polyline-key": "पॉलीलाइन कुंजी", + "polyline-key-required": "पॉलीलाइन कुंजी आवश्यक है", + "no-polylines": "कोई पॉलीलाइन्स कॉन्फ़िगर नहीं की गई हैं", + "add-polylines": "पॉलीलाइन जोड़ें", + "polyline-configuration": "पॉलीलाइन कॉन्फ़िगरेशन", + "remove-polyline": "पॉलीलाइन हटाएं", + "edit": "पॉलीलाइन संपादित करें", + "cut": "पॉलीलाइन क्षेत्र काटें", + "rotate": "पॉलीलाइन घुमाएं", + "remove-polyline-for": "'{{entityName}}' के लिए पॉलीलाइन हटाएं", + "draw-polyline": "पॉलीलाइन ड्रॉ करें", + "polyline-place-first-point-hint-with-entity": "'{{entityName}}' के लिए पॉलीलाइन: पहले बिंदु को रखने के लिए क्लिक करें", + "polyline-place-first-point-hint": "पॉलीलाइन: पहले बिंदु को रखने के लिए क्लिक करें", + "finish-polyline-hint-with-entity": "'{{entityName}}' के लिए पॉलीलाइन: ड्रॉइंग समाप्त करने के लिए क्लिक करें", + "finish-polyline-hint": "पॉलीलाइन: ड्रॉइंग समाप्त करने के लिए क्लिक करें", + "polyline-place-first-point-cut-hint": "पहला बिंदु रखने के लिए क्लिक करें", + "finish-polyline-cut-hint": "पहले मार्कर को क्लिक करें ताकि समाप्त किया जा सके और सहेजा जा सके" + }, + "select-entity": "एंटिटी चुनें", + "select-entity-hint": "संकेत: चयन के बाद स्थिति सेट करने के लिए मानचित्र पर क्लिक करें" + }, + "select-entity": "एंटिटी चुनें", + "select-entity-hint": "संकेत: चयन के बाद स्थिति सेट करने के लिए मानचित्र पर क्लिक करें", + "tooltips": { + "placeMarker": "'{{entityName}}' एंटिटी को रखने के लिए क्लिक करें", + "firstVertex": "'{{entityName}}' के लिए पॉलीगॉन: पहला बिंदु रखने के लिए क्लिक करें", + "firstVertex-cut": "पहला बिंदु रखने के लिए क्लिक करें", + "continueLine": "'{{entityName}}' के लिए पॉलीगॉन: ड्रॉइंग जारी रखने के लिए क्लिक करें", + "continueLine-cut": "ड्रॉइंग जारी रखने के लिए क्लिक करें", + "finishLine": "समाप्त करने के लिए किसी भी मौजूदा मार्कर पर क्लिक करें", + "finishPoly": "'{{entityName}}' के लिए पॉलीगॉन: समाप्त करने और सहेजने के लिए पहले मार्कर पर क्लिक करें", + "finishPoly-cut": "समाप्त करने और सहेजने के लिए पहले मार्कर पर क्लिक करें", + "finishRect": "'{{entityName}}' के लिए पॉलीगॉन: समाप्त करने और सहेजने के लिए क्लिक करें", + "startCircle": "'{{entityName}}' के लिए सर्कल: सर्कल का केंद्र रखने के लिए क्लिक करें", + "finishCircle": "'{{entityName}}' के लिए सर्कल: सर्कल पूरा करने के लिए क्लिक करें", + "placeCircleMarker": "सर्कल मार्कर रखने के लिए क्लिक करें" + }, + "actions": { + "finish": "समाप्त करें", + "cancel": "रद्द करें", + "removeLastVertex": "अंतिम बिंदु हटाएँ" + }, + "buttonTitles": { + "drawMarkerButton": "एंटिटी रखें", + "drawPolyButton": "पॉलीगॉन बनाएँ", + "drawLineButton": "पॉलीलाइन बनाएँ", + "drawCircleButton": "सर्कल बनाएँ", + "drawRectButton": "रेक्टैंगल बनाएँ", + "editButton": "संपादन मोड", + "dragButton": "ड्रैग-ड्रॉप मोड", + "cutButton": "पॉलीगॉन क्षेत्र काटें", + "deleteButton": "हटाएँ", + "drawCircleMarkerButton": "सर्कल मार्कर बनाएँ", + "rotateButton": "पॉलीगॉन घुमाएँ" + }, + "map-provider-settings": "मैप प्रोवाइडर सेटिंग्स", + "map-provider": "मैप प्रोवाइडर", + "map-provider-google": "Google maps", + "map-provider-openstreet": "OpenStreet maps", + "map-provider-here": "HERE maps", + "map-provider-image": "Image map", + "map-provider-tencent": "Tencent maps", + "openstreet-provider": "OpenStreet map provider", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Default)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "कस्टम प्रोवाइडर उपयोग करें", + "custom-provider-tile-url": "कस्टम प्रोवाइडर टाइल URL", + "google-maps-api-key": "Google Maps API Key", + "default-map-type": "डिफ़ॉल्ट मैप प्रकार", + "google-map-type-roadmap": "Roadmap", + "google-map-type-satelite": "Satellite", + "google-map-type-hybrid": "Hybrid", + "google-map-type-terrain": "Terrain", + "map-layer": "मैप लेयर", + "here-map-normal-day": "HERE.normalDay (Default)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "क्रेडेंशियल्स", + "here-app-id": "HERE app id", + "here-app-code": "HERE app code", + "here-api-key": "HERE API key", + "here-use-new-version-api-3": "API version 3 उपयोग करें", + "tencent-maps-api-key": "Tencent Maps API Key", + "tencent-map-type-roadmap": "रोडमैप", + "tencent-map-type-satelite": "सैटेलाइट", + "tencent-map-type-hybrid": "हाइब्रिड", + "image-map-background": "इमेज मैप पृष्ठभूमि", + "image-map-background-from-entity-attribute": "एंटिटी एट्रिब्यूट से इमेज मैप पृष्ठभूमि प्राप्त करें", + "image-url-source-entity-alias": "इमेज URL सोर्स एंटिटी उपनाम", + "image-url-source-entity-attribute": "इमेज URL सोर्स एंटिटी एट्रिब्यूट", + "common-map-settings": "सामान्य मैप सेटिंग्स", + "x-pos-key-name": "X पोज़िशन कुंजी नाम", + "y-pos-key-name": "Y पोज़िशन कुंजी नाम", + "latitude-key-name": "Latitude कुंजी नाम", + "longitude-key-name": "Longitude कुंजी नाम", + "default-map-zoom-level": "डिफ़ॉल्ट मैप ज़ूम लेवल (0 - 20)", + "default-map-center-position": "डिफ़ॉल्ट मैप केंद्र स्थिति (0,0)", + "disable-scroll-zooming": "स्क्रॉल ज़ूमिंग अक्षम करें", + "disable-double-click-zooming": "डबल क्लिक ज़ूमिंग अक्षम करें", + "disable-zoom-control-buttons": "ज़ूम कंट्रोल बटन अक्षम करें", + "fit-map-bounds": "सभी मार्कर्स को कवर करने के लिए मैप सीमा फिट करें", + "use-default-map-center-position": "डिफ़ॉल्ट मैप केंद्र स्थिति का उपयोग करें", + "entities-limit": "लोड करने के लिए एंटिटीज़ की सीमा", + "markers-settings": "मार्कर सेटिंग्स", + "marker-offset-x": "मार्कर X ऑफ़सेट (स्थिति × मार्कर चौड़ाई)", + "marker-offset-y": "मार्कर Y ऑफ़सेट (स्थिति × मार्कर ऊँचाई)", + "position-function": "स्थिति रूपांतरण फ़ंक्शन, जो x,y को 0 से 1 की सीमा में double के रूप में लौटाए", + "draggable-marker": "ड्रैग करने योग्य मार्कर", + "label": "लेबल", + "show-label": "लेबल दिखाएँ", + "use-label-function": "लेबल फ़ंक्शन उपयोग करें", + "label-pattern": "लेबल (पैटर्न उदाहरण: '${entityName}', '${entityName}: (टेक्स्ट ${keyName} यूनिट्स.)' )", + "label-function": "लेबल फ़ंक्शन", + "tooltip": "टूलटिप", + "show-tooltip": "टूलटिप दिखाएँ", + "show-tooltip-action": "टूलटिप प्रदर्शित करने की कार्रवाई", + "show-tooltip-action-click": "क्लिक पर टूलटिप दिखाएँ (डिफ़ॉल्ट)", + "show-tooltip-action-hover": "होवर पर टूलटिप दिखाएँ", + "auto-close-tooltips": "टूलटिप्स को स्वतः बंद करें", + "use-tooltip-function": "टूलटिप फ़ंक्शन उपयोग करें", + "tooltip-pattern": "टूलटिप (उदाहरण: 'टेक्स्ट ${keyName} यूनिट्स.' या लिंक टेक्स्ट)", + "tooltip-function": "टूलटिप फ़ंक्शन", + "tooltip-offset-x": "टूलटिप X ऑफ़सेट (एंकर × मार्कर चौड़ाई)", + "tooltip-offset-y": "टूलटिप Y ऑफ़सेट (एंकर × मार्कर ऊँचाई)", + "color": "रंग", + "use-color-function": "रंग फ़ंक्शन उपयोग करें", + "color-function": "रंग फ़ंक्शन", + "marker-image": "मार्कर इमेज", + "use-marker-image-function": "मार्कर इमेज फ़ंक्शन उपयोग करें", + "custom-marker-image": "कस्टम मार्कर इमेज", + "custom-marker-image-size": "कस्टम मार्कर इमेज आकार (px)", + "marker-image-function": "मार्कर इमेज फ़ंक्शन", + "marker-images": "मार्कर इमेजेज़", + "polygon-settings": "पॉलीगॉन सेटिंग्स", + "show-polygon": "पॉलीगॉन दिखाएँ", + "polygon-key-name": "पॉलीगॉन की key", + "enable-polygon-edit": "पॉलीगॉन एडिट सक्षम करें", + "polygon-label": "पॉलीगॉन लेबल", + "show-polygon-label": "पॉलीगॉन लेबल दिखाएँ", + "use-polygon-label-function": "पॉलीगॉन लेबल फ़ंक्शन उपयोग करें", + "polygon-label-pattern": "पॉलीगॉन लेबल (पैटर्न उदाहरण: '${entityName}', '${entityName}: (टेक्स्ट ${keyName} यूनिट्स.)' )", + "polygon-label-function": "पॉलीगॉन लेबल फ़ंक्शन", + "polygon-tooltip": "पॉलीगॉन टूलटिप", + "show-polygon-tooltip": "पॉलीगॉन टूलटिप दिखाएँ", + "auto-close-polygon-tooltips": "पॉलीगॉन टूलटिप्स स्वतः बंद करें", + "use-polygon-tooltip-function": "पॉलीगॉन टूलटिप फ़ंक्शन उपयोग करें", + "polygon-tooltip-pattern": "टूलटिप (उदाहरण: 'टेक्स्ट ${keyName} यूनिट्स.' या लिंक टेक्स्ट)", + "polygon-tooltip-function": "पॉलीगॉन टूलटिप फ़ंक्शन", + "polygon-color": "पॉलीगॉन रंग", + "polygon-opacity": "पॉलीगॉन अपारदर्शिता", + "use-polygon-color-function": "पॉलीगॉन रंग फ़ंक्शन उपयोग करें", + "polygon-color-function": "पॉलीगॉन रंग फ़ंक्शन", + "polygon-stroke": "पॉलीगॉन स्ट्रोक", + "stroke-color": "स्ट्रोक रंग", + "stroke-opacity": "स्ट्रोक अपारदर्शिता", + "stroke-weight": "स्ट्रोक मोटाई", + "use-polygon-stroke-color-function": "पॉलीगॉन स्ट्रोक रंग फ़ंक्शन उपयोग करें", + "polygon-stroke-color-function": "पॉलीगॉन स्ट्रोक रंग फ़ंक्शन", + "circle-settings": "सर्कल सेटिंग्स", + "show-circle": "सर्कल दिखाएँ", + "circle-key-name": "सर्कल key नाम", + "enable-circle-edit": "सर्कल एडिट सक्षम करें", + "circle-label": "सर्कल लेबल", + "show-circle-label": "सर्कल लेबल दिखाएँ", + "use-circle-label-function": "सर्कल लेबल फ़ंक्शन उपयोग करें", + "circle-label-pattern": "सर्कल लेबल (पैटर्न उदाहरण: '${entityName}', '${entityName}: (टेक्स्ट ${keyName} यूनिट्स.)' )", + "circle-label-function": "सर्कल लेबल फ़ंक्शन", + "circle-tooltip": "सर्कल टूलटिप", + "show-circle-tooltip": "सर्कल टूलटिप दिखाएँ", + "auto-close-circle-tooltips": "सर्कल टूलटिप्स स्वतः बंद करें", + "use-circle-tooltip-function": "सर्कल टूलटिप फ़ंक्शन उपयोग करें", + "circle-tooltip-pattern": "टूलटिप (उदाहरण: 'टेक्स्ट ${keyName} यूनिट्स.' या लिंक टेक्स्ट)", + "circle-tooltip-function": "सर्कल टूलटिप फ़ंक्शन", + "circle-fill-color": "सर्कल फ़िल रंग", + "circle-fill-color-opacity": "सर्कल फ़िल रंग अपारदर्शिता", + "use-circle-fill-color-function": "सर्कल फ़िल रंग फ़ंक्शन उपयोग करें", + "circle-fill-color-function": "सर्कल फ़िल रंग फ़ंक्शन", + "circle-stroke": "सर्कल स्ट्रोक", + "use-circle-stroke-color-function": "सर्कल स्ट्रोक रंग फ़ंक्शन उपयोग करें", + "circle-stroke-color-function": "सर्कल स्ट्रोक रंग फ़ंक्शन", + "markers-clustering-settings": "मार्कर्स क्लस्टरिंग सेटिंग्स", + "use-map-markers-clustering": "मैप मार्कर्स क्लस्टरिंग उपयोग करें", + "zoom-on-cluster-click": "क्लस्टर पर क्लिक करने पर ज़ूम करें", + "max-cluster-zoom": "अधिकतम ज़ूम लेवल, जिस पर मार्कर क्लस्टर का हिस्सा हो सकता है (0 - 18)", + "max-cluster-radius-pixels": "क्लस्टर द्वारा कवर किए जाने वाला अधिकतम रेडियस (पिक्सल में)", + "cluster-zoom-animation": "ज़ूम करते समय मार्कर्स पर ऐनिमेशन दिखाएँ", + "show-markers-bounds-on-cluster-mouse-over": "क्लस्टर पर माउस ले जाने पर मार्कर्स की सीमा दिखाएँ", + "spiderfy-max-zoom-level": "अधिकतम ज़ूम लेवल पर स्पाइडरफ़ाई करें (सभी क्लस्टर मार्कर्स देखने हेतु)", + "load-optimization": "लोड ऑप्टिमाइज़ेशन", + "cluster-chunked-loading": "मार्कर्स जोड़ने के लिए चंक्स का उपयोग करें ताकि पेज फ्रीज़ न हो", + "cluster-markers-lazy-load": "मार्कर्स जोड़ने के लिए लेज़ी लोड का उपयोग करें", + "editor-settings": "एडिटर सेटिंग्स", + "enable-snapping": "सटीक ड्राइंग के लिए दूसरी वर्टिसेस पर स्नैपिंग सक्षम करें", + "init-draggable-mode": "मैप को ड्रैगेबल मोड में प्रारंभ करें", + "hide-all-edit-buttons": "सभी एडिट कंट्रोल बटन छिपाएँ", + "hide-draw-buttons": "ड्रॉ बटन छिपाएँ", + "hide-edit-buttons": "एडिट बटन छिपाएँ", + "hide-remove-button": "रिमूव बटन छिपाएँ", + "route-map-settings": "रूट मैप सेटिंग्स", + "trip-animation-settings": "ट्रिप ऐनिमेशन सेटिंग्स", + "normalization-step": "नॉर्मलाइज़ेशन डेटा स्टेप (ms)", + "tooltip-background-color": "टूलटिप बैकग्राउंड रंग", + "tooltip-font-color": "टूलटिप फ़ॉन्ट रंग", + "tooltip-opacity": "टूलटिप अपारदर्शिता (0-1)", + "auto-close-tooltip": "टूलटिप स्वतः बंद करें", + "rotation-angle": "मार्कर के लिए अतिरिक्त रोटेशन ऐंगल सेट करें (deg)", + "path-settings": "पाथ सेटिंग्स", + "path-color": "पाथ रंग", + "use-path-color-function": "पाथ रंग फ़ंक्शन उपयोग करें", + "path-color-function": "पाथ रंग फ़ंक्शन", + "path-decorator": "पाथ डेकोरेटर", + "use-path-decorator": "पाथ डेकोरेटर उपयोग करें", + "decorator-symbol": "डेकोरेटर प्रतीक", + "decorator-symbol-arrow-head": "तीर", + "decorator-symbol-dash": "डैश", + "decorator-symbol-size": "डेकोरेटर प्रतीक आकार (px)", + "use-path-decorator-custom-color": "पाथ डेकोरेटर कस्टम रंग उपयोग करें", + "decorator-custom-color": "डेकोरेटर कस्टम रंग", + "decorator-offset": "डेकोरेटर ऑफ़सेट", + "end-decorator-offset": "अंतिम डेकोरेटर ऑफ़सेट", + "decorator-repeat": "डेकोरेटर दोहराव", + "points-settings": "पॉइंट सेटिंग्स", + "show-points": "पॉइंट्स दिखाएँ", + "point-color": "पॉइंट रंग", + "point-size": "पॉइंट आकार (px)", + "use-point-color-function": "पॉइंट रंग फ़ंक्शन उपयोग करें", + "point-color-function": "पॉइंट रंग फ़ंक्शन", + "use-point-as-anchor": "पॉइंट को एंकर की तरह उपयोग करें", + "point-as-anchor-function": "पॉइंट एंकर फ़ंक्शन", + "independent-point-tooltip": "स्वतंत्र पॉइंट टूलटिप", + "clustering-markers": "क्लस्टरिंग मार्कर्स", + "use-icon-create-function": "मार्कर्स रंग फ़ंक्शन उपयोग करें", + "marker-color-function": "मार्कर रंग फ़ंक्शन" + }, + "markdown": { + "use-markdown-text-function": "मार्कडाउन/HTML वैल्यू फ़ंक्शन उपयोग करें", + "markdown-text-function": "मार्कडाउन/HTML वैल्यू फ़ंक्शन", + "markdown-text-pattern": "मार्कडाउन/HTML पैटर्न (मार्कडाउन या HTML वेरिएबल्स के साथ, उदाहरण: '${entityName} या ${keyName} - कुछ टेक्स्ट.')", + "apply-default-markdown-style": "डिफ़ॉल्ट मार्कडाउन स्टाइल लागू करें", + "markdown-css": "मार्कडाउन/HTML CSS" + }, + "simple-card": { + "label": "लेबल", + "label-position": "लेबल स्थिति", + "label-position-left": "बाएँ", + "label-position-top": "ऊपर" + }, + "single-switch": { + "behavior": "व्यवहार", + "layout": "लेआउट", + "layout-right": "दाएँ", + "layout-left": "बाएँ", + "layout-centered": "सेंटर", + "auto-scale": "ऑटो स्केल", + "label": "लेबल", + "icon": "आइकन", + "switch-color": "स्विच रंग", + "on": "ऑन", + "off": "ऑफ़", + "disabled": "अक्षम", + "tumbler-color": "टंबलर रंग", + "on-label": "ऑन लेबल", + "off-label": "ऑफ़ लेबल", + "switch": "स्विच" + }, + "slider": { + "behavior": "व्यवहार", + "initial-value": "प्रारंभिक मान", + "initial-value-hint": "स्लाइडर का प्रारंभिक मान प्राप्त करने की कार्रवाई।", + "on-value-change": "मान बदलने पर", + "on-value-change-hint": "जब स्लाइडर का मान बदला जाता है तो ट्रिगर होने वाली कार्रवाई।", + "layout": "लेआउट", + "layout-default": "डिफ़ॉल्ट", + "layout-extended": "विस्तारित", + "layout-simplified": "सरलीकृत", + "auto-scale": "ऑटो स्केल", + "icon": "आइकन", + "value": "मान", + "range": "सीमा", + "min": "न्यूनतम", + "max": "अधिकतम", + "range-ticks": "सीमा टिक", + "tick-marks": "टिक मार्क्स", + "colors": "रंग", + "main": "मुख्य", + "background": "पृष्ठभूमि", + "left-icon": "बाएँ आइकन", + "right-icon": "दाएँ आइकन", + "slider": "स्लाइडर" + }, + "value-card": { + "layout": "लेआउट", + "layout-square": "वर्गाकार", + "layout-vertical": "वर्टिकल", + "layout-centered": "केंद्रित", + "layout-simplified": "सरलीकृत", + "layout-horizontal": "हॉरिज़ॉन्टल", + "layout-horizontal-reversed": "हॉरिज़ॉन्टल (उल्टा)", + "label": "लेबल", + "icon": "आइकन", + "value": "मान", + "date": "तारीख़", + "value-card-style": "वैल्यू कार्ड स्टाइल", + "auto-scale": "ऑटो स्केल" + }, + "label-card": { + "auto-scale": "ऑटो स्केल", + "label": "लेबल", + "icon": "आइकन", + "label-card-style": "लेबल कार्ड स्टाइल" + }, + "label-value-card": { + "value": "मान", + "label-value-card-style": "लेबल और मान कार्ड शैली" + }, + "liquid-level-card": { + "layout-simple": "सरल", + "layout-percentage": "प्रतिशत", + "layout-absolute": "निरपेक्ष", + "layout": "लेआउट", + "background-overlay": "मान बैकग्राउंड ओवरले", + "total-volume": "कुल मात्रा", + "total-volume-units": "कुल मात्रा की इकाइयाँ", + "tank": "टैंक", + "shape": "आकार", + "datasource-units": "स्रोत इकाइयाँ", + "widget-units": "विजेट इकाइयाँ", + "decimals": "दशमलव", + "liquid": "तरल", + "liquid-color": "तरल का रंग", + "value": "मान", + "value-font": "मान का फ़ॉन्ट", + "level": "स्तर", + "last-update": "अंतिम अपडेट", + "shape-by-attribute": "एट्रिब्यूट नाम द्वारा टैंक का आकार सेट करें", + "tooltip-background": "बैकग्राउंड रंग", + "background-blur": "बैकग्राउंड ब्लर", + "tank-color": "टैंक का रंग", + "static": "स्थिर", + "see-examples": "उदाहरण देखें", + "attribute": "विशेषता", + "shape-type": "प्रकार", + "v-oval": "वर्टिकल ओवल", + "v-cylinder": "वर्टिकल सिलेंडर", + "v-capsule": "वर्टिकल कैप्सूल", + "rectangle": "रेक्टैंगल", + "h-oval": "हॉरिज़ॉन्टल ओवल", + "h-ellipse": "हॉरिज़ॉन्टल एलिप्स", + "h-dish-ends": "हॉरिज़ॉन्टल डिश एंड्स", + "h-cylinder": "हॉरिज़ॉन्टल सिलेंडर", + "h-capsule": "हॉरिज़ॉन्टल कैप्सूल", + "h-elliptical_2_1": "हॉरिज़ॉन्टल 2:1 एलिप्टिकल", + "icon": "कार्ड आइकन", + "title": "कार्ड शीर्षक", + "units": "इकाइयाँ", + "color-and-font": "रंग और फ़ॉन्ट", + "shape-attribute-name": "एट्रिब्यूट नाम", + "total-volume-required": "कुल मात्रा आवश्यक है।", + "attribute-name-required": "एट्रिब्यूट नाम आवश्यक है।", + "attribute-key-not-set": "एट्रिब्यूट '{{attributeName}}' की कुंजी सेट नहीं है", + "attribute-key-invalid": "एट्रिब्यूट '{{attributeName}}' की कुंजी अमान्य है" + }, + "aggregated-value-card": { + "subtitle": "उपशीर्षक", + "chart": "चार्ट", + "values": "मान", + "value-appearance": "मान की उपस्थिति", + "position": "स्थान", + "position-center": "केंद्र", + "position-right-top": "दाईं ओर ऊपर", + "position-right-bottom": "दाईं ओर नीचे", + "position-left-top": "बाईं ओर ऊपर", + "position-left-bottom": "बाईं ओर नीचे", + "font": "फ़ॉन्ट", + "color": "रंग", + "arrow": "तीर", + "display-up-down-arrow": "ऊपर/नीचे तीर दिखाएँ", + "add-value": "मान जोड़ें", + "remove-value": "मान हटाएँ", + "no-values": "कोई मान कॉन्फ़िगर नहीं किया गया", + "aggregation": "एग्रीगेशन", + "aggregated-value-card-style": "एग्रीगेटेड वैल्यू कार्ड शैली", + "auto-scale": "ऑटो स्केल" + }, + "value-chart-card": { + "layout": "लेआउट", + "layout-left": "बाएँ", + "layout-right": "दाएँ", + "auto-scale": "ऑटो स्केल", + "icon": "आइकन", + "value": "मान", + "chart": "चार्ट", + "value-chart-card-style": "वैल्यू चार्ट कार्ड शैली" + }, + "progress-bar": { + "layout": "लेआउट", + "layout-default": "डिफ़ॉल्ट", + "layout-simplified": "सरलीकृत", + "auto-scale": "ऑटो स्केल", + "icon": "आइकन", + "value": "मान", + "range": "सीमा", + "min": "न्यूनतम", + "max": "अधिकतम", + "range-ticks": "सीमा टिक", + "bar": "बार", + "bar-color": "बार रंग", + "bar-background": "बार बैकग्राउंड", + "progress-bar-card-style": "प्रोग्रेस बार कार्ड शैली" + }, + "notification": { + "max-notification-display": "दिखाने के लिए अधिकतम नोटिफ़िकेशन", + "counter": "काउंटर", + "counter-hint": "यदि \"विजेट शीर्षक\" सक्षम है तो काउंटर प्रदर्शित होगा", + "icon": "आइकन", + "counter-value": "मान", + "counter-color": "रंग", + "notification-button": "नोटिफ़िकेशन बटन", + "button-view-all": "सभी देखें", + "button-filter": "फ़िल्टर", + "type-filter": "प्रकार फ़िल्टर", + "button-mark-read": "सभी को पढ़ा हुआ चिह्नित करें", + "notification-types": "नोटिफ़िकेशन प्रकार", + "notification-type": "नोटिफ़िकेशन प्रकार", + "search-type": "खोज प्रकार", + "any-type": "कोई भी प्रकार" + }, + "alarm-count": { + "alarm-count-card-style": "अलार्म काउंट कार्ड शैली" + }, + "entity-count": { + "entity-count-card-style": "एंटिटी काउंट कार्ड शैली" + }, + "count": { + "layout": "लेआउट", + "layout-column": "कॉलम", + "layout-row": "पंक्ति", + "label": "लेबल", + "icon": "आइकन", + "icon-background": "आइकन पृष्ठभूमि", + "value": "मान", + "chevron": "चेवरॉन", + "auto-scale": "ऑटो स्केल" + }, + "table": { + "common-table-settings": "सामान्य टेबल सेटिंग्स", + "enable-search": "खोज सक्षम करें", + "enable-sticky-header": "हेडर हमेशा दिखाएँ", + "enable-sticky-action": "क्रियाओं वाला कॉलम हमेशा दिखाएँ", + "hidden-cell-button-display-mode": "छिपे हुए सेल बटन क्रियाओं का प्रदर्शन मोड", + "show-empty-space-hidden-action": "छिपे हुए सेल बटन क्रिया की जगह खाली स्थान दिखाएँ", + "dont-reserve-space-hidden-action": "छिपे हुए एक्शन बटनों के लिए स्थान आरक्षित न करें", + "display-timestamp": "टाइमस्टैम्प", + "timestamp-column-name": "टाइमस्टैम्प", + "display-pagination": "पेजिनेशन दिखाएँ", + "default-page-size": "डिफ़ॉल्ट पृष्ठ आकार", + "page-step-settings": "पृष्ठ स्टेप सेटिंग्स", + "page-step-count": "स्टेप की संख्या", + "page-step-increment": "स्टेप वृद्धि", + "page-step-count-format-message": "मान 1 से 100 की सीमा में पूर्णांक होना चाहिए।", + "page-step-increment-format-message": "1 या उससे अधिक का पूर्णांक मान होना चाहिए।", + "use-entity-label-tab-name": "टैब नाम में एंटिटी लेबल का उपयोग करें", + "hide-empty-lines": "खाली पंक्तियाँ छिपाएँ", + "row-style": "पंक्ति शैली", + "use-row-style-function": "पंक्ति शैली फ़ंक्शन का उपयोग करें", + "row-style-function": "पंक्ति शैली फ़ंक्शन", + "cell-style": "सेल शैली", + "use-cell-style-function": "सेल शैली फ़ंक्शन का उपयोग करें", + "cell-style-function": "सेल शैली फ़ंक्शन", + "cell-content": "सेल सामग्री", + "use-cell-content-function": "सेल सामग्री फ़ंक्शन का उपयोग करें", + "cell-content-function": "सेल सामग्री फ़ंक्शन", + "show-latest-data-column": "नवीनतम डेटा कॉलम दिखाएँ", + "latest-data-column-order": "नवीनतम डेटा कॉलम क्रम", + "entities-table-title": "एंटिटी टेबल शीर्षक", + "enable-select-column-display": "दिखाने के लिए कॉलम चुनना सक्षम करें", + "display-entity-name": "एंटिटी नाम कॉलम दिखाएँ", + "entity-name-column-title": "एंटिटी नाम कॉलम शीर्षक", + "display-entity-label": "एंटिटी लेबल कॉलम दिखाएँ", + "entity-label-column-title": "एंटिटी लेबल कॉलम शीर्षक", + "display-entity-type": "एंटिटी प्रकार कॉलम दिखाएँ", + "default-sort-order": "डिफ़ॉल्ट क्रमबद्धता क्रम", + "custom-title": "कस्टम हेडर शीर्षक", + "column-width": "कॉलम चौड़ाई (px या %)", + "default-column-visibility": "डिफ़ॉल्ट कॉलम दृश्यता", + "column-visibility-visible": "दृश्यमान", + "column-visibility-hidden": "छिपा हुआ", + "column-visibility-hidden-mobile": "मोबाइल मोड में छिपा हुआ", + "column-selection-to-display": "'प्रदर्शित करने के लिए कॉलम' में कॉलम चयन", + "column-selection-to-display-enabled": "सक्षम", + "column-selection-to-display-disabled": "अक्षम", + "alarms-table-title": "अलार्म टेबल शीर्षक", + "enable-alarms-selection": "अलार्म चयन सक्षम करें", + "enable-alarms-search": "अलार्म खोज सक्षम करें", + "enable-alarm-filter": "अलार्म फ़िल्टर सक्षम करें", + "display-alarm-details": "अलार्म विवरण दिखाएँ", + "allow-alarms-ack": "अलार्म की पुष्टि की अनुमति दें", + "allow-alarms-clear": "अलार्म साफ़ करने की अनुमति दें", + "display-alarm-activity": "अलार्म गतिविधि दिखाएँ", + "allow-alarms-assign": "अलार्म असाइन करने की अनुमति दें", + "columns": "कॉलम", + "column-settings": "कॉलम सेटिंग्स", + "remove-column": "कॉलम हटाएँ", + "add-column": "कॉलम जोड़ें", + "no-columns": "कोई कॉलम कॉन्फ़िगर नहीं", + "columns-to-display": "दिखाने के लिए कॉलम", + "table-header": "टेबल हेडर", + "header-buttons": "हेडर बटन", + "table-buttons": "टेबल बटन", + "pagination": "पृष्ठांकन", + "rows": "पंक्तियाँ", + "timeseries-column-error": "कम से कम एक टाइम सीरीज़ कॉलम निर्दिष्ट होना चाहिए", + "alarm-column-error": "कम से कम एक अलार्म कॉलम निर्दिष्ट होना चाहिए", + "table-tabs": "टेबल टैब्स", + "show-cell-actions-menu-mobile": "मोबाइल मोड में सेल एक्शन ड्रॉपडाउन मेनू दिखाएँ", + "disable-sorting": "सॉर्टिंग अक्षम करें", + "sort-by": "टैब्स को इस आधार पर क्रमबद्ध करें", + "sort-timestamp-option": "बनाने का समय" + }, + "latest-chart": { + "total": "कुल", + "auto-scale": "ऑटो स्केल", + "clockwise-layout": "क्लॉकवाइज़ लेआउट", + "sort-series": "लेबल के अनुसार सीरीज़ को सॉर्ट करें", + "tooltip-value-type-absolute": "एब्सोल्यूट", + "tooltip-value-type-percentage": "प्रतिशत" + }, + "pie-chart": { + "pie-chart-appearance": "पाई चार्ट उपस्थिति", + "label": "लेबल", + "border": "बॉर्डर", + "radius": "रेडियस", + "pie-chart-card-style": "पाई चार्ट कार्ड स्टाइल" + }, + "radar-chart": { + "radar-appearance": "रडार उपस्थिति", + "shape": "आकार", + "shape-polygon": "बहुभुज", + "shape-circle": "वृत्त", + "color": "रंग", + "line": "लाइन", + "points": "पॉइंट्स", + "points-label": "पॉइंट्स लेबल", + "radar-axis": "रडार अक्ष", + "axis-label": "अक्ष लेबल", + "ticks-label": "टिक लेबल", + "radar-chart-style": "रडार चार्ट स्टाइल", + "max-axes-scaling": "अधिकतम अक्ष स्केलिंग", + "max-axes-scaling-hint": "चुनें कि प्रत्येक रडार अक्ष का अपना अधिकतम मान हो (Separate) या विजेट डेटासेट के आधार पर सभी अक्ष सबसे बड़े मान को साझा करें (Common)।", + "separate": "अलग-अलग", + "common": "सामान्य" + }, + "time-series-chart": { + "chart": "चार्ट", + "chart-style": "चार्ट स्टाइल", + "data-zoom": "डेटा ज़ूम", + "stack-mode": "स्टैक मोड", + "stack-mode-hint": "चार्ट पर सीरीज़ को स्टैक करता है। समान यूनिट वाली सीरीज़ एक-दूसरे के ऊपर रखी जाएँगी।", + "axes": "अक्ष", + "y-axes": "Y अक्ष", + "line-type": "लाइन प्रकार", + "line-width": "लाइन चौड़ाई", + "type-line": "लाइन", + "type-bar": "बार", + "type-point": "प्वाइंट", + "no-aggregation-bar-width-strategy": "ग़ैर-एग्रीगेटेड डेटा के लिए बार चौड़ाई रणनीति", + "no-aggregation-bar-width-strategy-group": "ग्रुप", + "no-aggregation-bar-width-strategy-separate": "अलग-अलग", + "bar-group-width": "बार ग्रुप चौड़ाई", + "bar-width": "बार चौड़ाई", + "bar-width-relative": "टाइम विंडो का प्रतिशत", + "bar-width-absolute": "एब्सोल्यूट (ms)", + "comparison": { + "comparison": "तुलना", + "comparison-hint": "तुलना केवल ऐतिहासिक डेटा के साथ काम करती है!", + "show": "दिखाएँ", + "settings": "तुलना सेटिंग्स", + "show-values-for-comparison": "तुलना के लिए ऐतिहासिक डेटा दिखाएँ", + "comparison-values-label": "तुलना की कुंजी लेबल", + "comparison-values-label-auto": "ऑटो", + "comparison-data-color": "तुलना डेटा रंग" + }, + "threshold": { + "thresholds": "थ्रेशहोल्ड्स", + "source": "स्रोत", + "key-value": "कुंजी / मान", + "no-thresholds": "कोई थ्रेशहोल्ड कॉन्फ़िगर नहीं है", + "add-threshold": "थ्रेशहोल्ड जोड़ें", + "type-constant": "कॉनस्टेंट", + "type-latest-key": "कुंजी", + "type-entity": "एंटिटी", + "threshold-settings": "थ्रेशहोल्ड सेटिंग्स", + "remove-threshold": "थ्रेशहोल्ड हटाएँ", + "threshold-value-required": "थ्रेशहोल्ड मान आवश्यक है।", + "key-required": "कुंजी आवश्यक है।", + "entity-key-required": "एंटिटी कुंजी आवश्यक है।", + "line-appearance": "लाइन स्वरूप", + "line-color": "लाइन रंग", + "start-symbol": "स्टार्ट प्रतीक", + "end-symbol": "एंड प्रतीक", + "symbol-size": "आकार", + "label": "लेबल", + "label-position-start": "आरंभ", + "label-position-middle": "मध्य", + "label-position-end": "अंत", + "label-position-inside-start": "अंदर आरंभ", + "label-position-inside-start-top": "अंदर आरंभ शीर्ष", + "label-position-inside-start-bottom": "अंदर आरंभ नीचे", + "label-position-inside-middle": "अंदर मध्य", + "label-position-inside-middle-top": "अंदर मध्य शीर्ष", + "label-position-inside-middle-bottom": "अंदर मध्य नीचे", + "label-position-inside-end": "अंदर अंत", + "label-position-inside-end-top": "अंदर अंत शीर्ष", + "label-position-inside-end-bottom": "अंदर अंत नीचे", + "label-background": "लेबल बैकग्राउंड" + }, + "state": { + "states": "स्टेट्स", + "label": "लेबल", + "ticks-value": "टिक्स मान", + "source": "स्रोत", + "value-range": "मान / रेंज", + "no-states": "कोई स्टेट कॉन्फ़िगर नहीं है", + "add-state": "स्टेट जोड़ें", + "type-constant": "कॉनस्टेंट", + "type-range": "रेंज", + "from": "से", + "to": "तक", + "remove-state": "स्टेट हटाएँ" + }, + "grid": { + "grid": "ग्रिड", + "background-color": "बैकग्राउंड रंग", + "border": "बॉर्डर" + }, + "axis": { + "axes": "धुरियाँ", + "x-axis": "X धुरी", + "y-axis": "Y धुरी", + "y-axis-settings": "Y धुरी सेटिंग्स", + "comparison-x-axis-settings": "तुलना X धुरी सेटिंग्स", + "remove-y-axis": "Y धुरी हटाएँ", + "id": "आईडी", + "label": "लेबल", + "position": "स्थिति", + "position-left": "बाएँ", + "position-right": "दाएँ", + "position-top": "ऊपर", + "position-bottom": "नीचे", + "tick-labels": "टिक लेबल", + "ticks-formatter-function": "टिक्स फ़ॉर्मेटर फ़ंक्शन", + "ticks-generator-function": "टिक्स जनरेटर फ़ंक्शन", + "show-ticks": "टिक्स दिखाएँ", + "show-line": "लाइन दिखाएँ", + "show-split-lines": "स्प्लिट लाइन्स दिखाएँ", + "show-split-lines-x-axis-hint": "यदि सक्षम किया गया, तो चार्ट पर वर्टिकल लाइन्स दिखेंगी।", + "show-split-lines-y-axis-hint": "यदि सक्षम किया गया, तो चार्ट पर हॉरिज़ॉन्टल लाइन्स दिखेंगी।", + "ticks-interval": "टिक्स अंतराल", + "ticks-interval-hint": "धुरी के लिए विभाजन अंतराल को बाध्यतापूर्वक सेट करें।", + "split-number": "विभाजन संख्या", + "split-number-hint": "धुरी को जितने खंडों में विभाजित किया जाएगा उनकी संख्या।", + "min": "न्यूनतम", + "max": "अधिकतम", + "show": "दिखाएँ", + "add-y-axis": "Y धुरी जोड़ें" + }, + "series": { + "legend-settings": "लीजेंड सेटिंग्स", + "show-in-legend": "लीजेंड में दिखाएँ", + "show-in-legend-hint": "लीजेंड में सीरीज़ नाम और डेटा दिखाएँ।", + "hidden-by-default": "डिफ़ॉल्ट रूप से छिपा हुआ", + "hidden-by-default-hint": "सीरीज़ को डिफ़ॉल्ट रूप से लीजेंड में छिपाएँ।", + "series-type": "सीरीज़ प्रकार", + "type": "प्रकार", + "type-line": "लाइन", + "type-bar": "बार", + "line": { + "line": "लाइन", + "show-line": "लाइन दिखाएँ", + "step-line": "स्टेप लाइन", + "step-type-start": "शुरुआत", + "step-type-middle": "मध्य", + "step-type-end": "अंत", + "smooth-line": "स्मूथ लाइन" + }, + "point": { + "points": "पॉइंट्स", + "show-points": "पॉइंट्स दिखाएँ", + "point-label": "पॉइंट लेबल", + "point-label-hint": "सीरीज़ पॉइंट के ऊपर मान वाला लेबल दिखाएँ।", + "point-label-background": "पॉइंट लेबल बैकग्राउंड", + "point-shape": "पॉइंट आकार", + "point-size": "पॉइंट आकार" + } + } + }, + "wind-speed-direction": { + "layout": "लेआउट", + "layout-default": "डिफ़ॉल्ट", + "layout-advanced": "एडवांस्ड", + "layout-simplified": "सरल", + "values": "मान", + "wind-direction": "वायु दिशा", + "center-value": "केंद्रीय मान", + "icon": "आइकन", + "arrow": "तीर", + "ticks": "टिक", + "labels-type": "लेबल प्रकार", + "directional-names": "दिशात्मक नाम", + "degrees": "डिग्री", + "major-ticks": "मुख्य टिक", + "minor-ticks": "माइनर टिक", + "wind-speed-direction-card-style": "वायु गति और दिशा कार्ड शैली", + "ticks-color": "टिक का रंग", + "ticks-labels-type": "टिक लेबल प्रकार", + "arrow-color": "तीर का रंग" + }, + "value-source": { + "value-source": "मान स्रोत", + "predefined-value": "स्थिर", + "entity-attribute": "एंटिटी विशेषता", + "value": "मान", + "value-required": "मान आवश्यक है।", + "key-required": "की आवश्यक है।", + "entity-key-required": "एंटिटी की आवश्यक है।", + "source-entity-alias": "स्रोत एंटिटी उपनाम", + "source-entity-attribute": "स्रोत एंटिटी विशेषता", + "type-constant": "स्थिर", + "type-latest-key": "की", + "type-entity": "एंटिटी" + }, + "rpc-state": { + "initial-state": "प्रारंभिक स्थिति", + "initial-state-hint": "कम्पोनेंट की प्रारंभिक स्थिति (On/Off) प्राप्त करने की क्रिया।", + "disabled-state": "अक्षम स्थिति", + "disabled-state-hint": "वह शर्त कॉन्फ़िगर करें जिसके तहत कम्पोनेंट अक्षम होगा।", + "turn-on": "'On' करें", + "turn-on-hint": "जब स्लाइडर 'On' पर स्विच किया जाता है तब ट्रिगर होने वाली क्रिया।", + "turn-off": "'Off' करें", + "turn-off-hint": "जब स्लाइडर 'Off' पर स्विच किया जाता है तब ट्रिगर होने वाली क्रिया।", + "on": "On", + "off": "Off", + "disabled": "Disabled" + }, + "value-action": { + "do-nothing": "कुछ न करें", + "execute-rpc": "RPC निष्पादित करें", + "get-attribute": "विशेषता प्राप्त करें", + "set-attribute": "विशेषता सेट करें", + "get-time-series": "टाइम सीरीज़ प्राप्त करें", + "get-alarm-status": "अलार्म स्थिति प्राप्त करें", + "get-dashboard-state": "डैशबोर्ड स्टेट ID प्राप्त करें", + "get-dashboard-state-object": "डैशबोर्ड स्टेट ऑब्जेक्ट प्राप्त करें", + "add-time-series": "टाइम सीरीज़ जोड़ें", + "execute-rpc-text": "RPC मेथड '{{methodName}}' निष्पादित करें", + "get-time-series-text": "टाइम सीरीज़ '{{key}}' का उपयोग करें", + "get-attribute-text": "विशेषता '{{key}}' का उपयोग करें", + "get-alarm-status-text": "अलार्म स्थिति का उपयोग करें", + "get-dashboard-state-text": "डैशबोर्ड स्टेट का उपयोग करें", + "get-dashboard-state-object-text": "डैशबोर्ड स्टेट ऑब्जेक्ट का उपयोग करें", + "when-dashboard-state-is-text": "जब डैशबोर्ड स्टेट ID '{{state}}' हो", + "when-dashboard-state-function-is-text": "जब f(डैशबोर्ड स्टेट ID) '{{state}}' हो", + "when-dashboard-state-object-function-is-text": "जब f(डैशबोर्ड स्टेट ऑब्जेक्ट) '{{state}}' हो", + "set-attribute-to-value-text": "'{{key}}' विशेषता को सेट करें: {{value}}", + "add-time-series-value-text": "'{{key}}' टाइम सीरीज़ मान जोड़ें: {{value}}", + "set-attribute-text": "'{{key}}' विशेषता सेट करें", + "add-time-series-text": "'{{key}}' टाइम सीरीज़ जोड़ें", + "action": "क्रिया", + "value": "मान", + "init-value-hint": "वह मान जो तब तक सेट रहेगा जब तक डिवाइस डेटा नहीं भेजता।", + "method": "मेथड", + "method-name-required": "मेथड नाम आवश्यक है।", + "request-timeout-ms": "RPC अनुरोध टाइमआउट (ms)", + "request-timeout-required": "अनुरोध टाइमआउट आवश्यक है।", + "min-request-timeout-error": "अनुरोध टाइमआउट मान 5000 ms (5 सेकंड) या उससे अधिक होना चाहिए।", + "request-persistent": "स्थायी RPC अनुरोध", + "persistent-polling-interval": "स्थायी पोलिंग अंतराल (ms)", + "persistent-polling-interval-hint": "स्थायी RPC प्रतिक्रिया प्राप्त करने के लिए पोलिंग अंतराल (ms)", + "persistent-polling-interval-required": "स्थायी पोलिंग अंतराल आवश्यक है।", + "min-persistent-polling-interval-error": "स्थायी पोलिंग अंतराल मान 1000 ms (1 सेकंड) या उससे अधिक होना चाहिए।", + "attribute-scope": "विशेषता स्कोप", + "attribute-key": "विशेषता की", + "attribute-key-required": "विशेषता की आवश्यक है।", + "time-series-key": "टाइम सीरीज़ की", + "time-series-key-required": "टाइम सीरीज़ की आवश्यक है।", + "action-result-converter": "क्रिया परिणाम कन्वर्टर", + "converter-none": "कोई नहीं", + "converter-function": "फ़ंक्शन", + "converter-constant": "स्थिर", + "converter-value": "मान", + "parse-value-function": "मान पार्स करने का फ़ंक्शन", + "state-when-result-is": "'{{state}}' जब परिणाम हो", + "parameters": "पैरामीटर्स", + "convert-value-function": "मान कन्वर्ट करने का फ़ंक्शन", + "error": { + "target-entity-is-not-set": "लक्ष्य एंटिटी सेट नहीं है!", + "failed-to-perform-action": "{{ actionLabel }} क्रिया निष्पादित करने में विफल।", + "invalid-attribute-scope": "{{scope}} विशेषता स्कोप {{entityType}} एंटिटी द्वारा समर्थित नहीं है।" + } + }, + "widget-font": { + "font-settings": "फ़ॉन्ट सेटिंग्स", + "font-family": "फ़ॉन्ट परिवार", + "size": "आकार", + "relative-font-size": "सापेक्ष फ़ॉन्ट आकार (प्रतिशत)", + "font-style": "शैली", + "font-style-normal": "सामान्य", + "font-style-italic": "इटैलिक", + "font-style-oblique": "ओब्लिक", + "font-weight": "वज़न", + "font-weight-normal": "सामान्य", + "font-weight-bold": "बोल्ड", + "font-weight-bolder": "अधिक बोल्ड", + "font-weight-lighter": "हल्का", + "color": "रंग", + "shadow-color": "छाया रंग", + "preview": "पूर्वावलोकन", + "line-height": "लाइन ऊँचाई", + "auto": "ऑटो" + }, + "home": { + "no-data-available": "कोई डेटा उपलब्ध नहीं है" + }, + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "डिस्क", + "cpu-warning-text": "CPU उपयोग बहुत अधिक है। सिस्टम विफलता से बचने के लिए सिस्टम प्रदर्शन को ऑप्टिमाइज़ करें।", + "cpu-critical-text": "CPU उपयोग अत्यधिक उच्च है। सिस्टम विफलता से बचने के लिए सिस्टम प्रदर्शन को ऑप्टिमाइज़ करें।", + "ram-warning-text": "RAM का रिज़र्व कम हो रहा है। सिस्टम विफलता से बचने के लिए प्रदर्शन को ऑप्टिमाइज़ करें या RAM बढ़ाएँ।", + "ram-critical-text": "RAM का रिज़र्व बेहद कम है। सिस्टम विफलता से बचने के लिए प्रदर्शन को ऑप्टिमाइज़ करें या RAM बढ़ाएँ।", + "disk-warning-text": "डिस्क स्पेस कम हो रहा है। डेटा हानि से बचने के लिए डिस्क स्पेस खाली करें या बढ़ाएँ।", + "disk-critical-text": "डिस्क स्पेस अत्यधिक कम है। डेटा हानि से बचने के लिए डिस्क स्पेस खाली करें या बढ़ाएँ।" + }, + "cluster-info": { + "service-id": "सर्विस ID", + "service-type": "सर्विस प्रकार", + "no-data": "कोई डेटा नहीं" + }, + "transport-messages": { + "title": "ट्रांसपोर्ट संदेश", + "info": "सभी संदेश जो डिवाइस से आए हैं" + }, + "activity": { + "title": "गतिविधि" + }, + "documentation": { + "title": "प्रलेखन", + "add-link": "लिंक जोड़ें", + "add-link-title": "प्रलेखन लिंक जोड़ें", + "name": "नाम", + "name-required": "नाम आवश्यक है।", + "link": "लिंक", + "link-required": "लिंक आवश्यक है।", + "columns": "कॉलम" + }, + "quick-links": { + "title": "त्वरित लिंक", + "add-link": "लिंक जोड़ें", + "add-link-title": "त्वरित लिंक जोड़ें", + "quick-link": "त्वरित लिंक", + "quick-link-required": "त्वरित लिंक आवश्यक है।", + "no-links-matching": "'{{name}}' से मिलते लिंक नहीं मिले।", + "columns": "कॉलम" + }, + "recent-dashboards": { + "title": "डैशबोर्ड्स", + "last": "अंतिम बार देखा गया", + "starred": "स्टार्ड", + "name": "नाम", + "last-viewed": "अंतिम बार देखा गया", + "no-last-viewed-dashboards": "अभी तक कोई डैशबोर्ड अंतिम बार देखा नहीं गया" + }, + "configured-features": { + "title": "कन्फ़िगर की गई फ़ीचर्स", + "info": "ऐसी फ़ीचर्स की स्थिति जिन्हें कॉन्फ़िगरेशन की आवश्यकता है", + "email-feature": "ईमेल", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2FA", + "feature-configured": "फ़ीचर कन्फ़िगर है।\nसेटअप करने के लिए क्लिक करें", + "feature-not-configured": "फ़ीचर कन्फ़िगर नहीं है।\nसेटअप करने के लिए क्लिक करें" + }, + "version-info": { + "title": "संस्करण", + "contact-us": "हमसे संपर्क करें", + "current-version": "वर्तमान संस्करण", + "current": "वर्तमान", + "available-version": "उपलब्ध संस्करण", + "available": "उपलब्ध", + "upgrade": "अपग्रेड", + "version-is-up-to-date": "संस्करण नवीनतम है" + }, + "usage-info": { + "title": "उपयोग", + "entities": "एंटिटीज़", + "api-calls": "API कॉल्स" + }, + "functions": { + "title": "फ़ंक्शंस", + "pe-feature-tooltip": "केवल ThingsBoard\nProfessional Edition में", + "switch-to-pe": "PE पर स्विच करें", + "alarms": "अलार्म", + "dashboards": "डैशबोर्ड्स", + "entities-and-relations": "एंटिटीज़ और रिलेशन्स", + "profiles": "प्रोफ़ाइल्स", + "advanced-features": "एडवांस्ड फ़ीचर्स", + "notification-center": "अधिसूचना केंद्र", + "api-usage": "API उपयोग", + "customers": "कस्टमर", + "customers-hierarchy": "कस्टमर पदानुक्रम", + "roles-and-permissions": "रोल्स और परमिशन्स", + "groups": "ग्रुप्स", + "integrations": "इंटीग्रेशन्स", + "solution-templates": "सॉल्यूशन टेम्पलेट्स", + "scheduler": "शेड्यूलर", + "white-labeling": "व्हाइट लेबलिंग" + }, + "devices": { + "view-docs": "डॉक्स देखें", + "inactive": "निष्क्रिय", + "active": "सक्रिय", + "total": "कुल" + }, + "alarms": { + "critical": "क्रिटिकल", + "assigned-to-me": "मुझे असाइन किए गए", + "total": "कुल" + }, + "getting-started": { + "get-started": "शुरू करें", + "finish": "समाप्त", + "done-welcome-title": "आपका स्वागत है", + "done-welcome-text": "आपने यह बहुत अच्छी तरह किया!", + "sys-admin": { + "step1": { + "title": "टेनेंट और टेनेंट एडमिनिस्ट्रेटर बनाएँ", + "content": "

    टेनेंट वह व्यक्ति या संगठन होता है जो डिवाइस और एसेट का मालिक होता है या उनका उत्पादन करता है। टेनेंट के पास कई टेनेंट एडमिनिस्ट्रेटर उपयोगकर्ता, कस्टमर, डिवाइस और एसेट हो सकते हैं।

    टेनेंट एडमिनिस्ट्रेटर टेनेंट अकाउंट के भीतर डिवाइस, एसेट, कस्टमर और डैशबोर्ड बना और प्रबंधित कर सकता है।

    इसे कैसे करना है, इसकी डॉक्यूमेंटेशन देखें:

    ", + "how-to-create-tenant": "टेनेंट और टेनेंट एडमिनिस्ट्रेटर कैसे बनाएँ" + }, + "step2": { + "title": "फ़ीचर कॉन्फ़िगर करें: मेल सर्वर", + "content": "

    उपयोगकर्ता सक्रियण, पासवर्ड रिकवरी और अलार्म नोटिफिकेशन डिलीवरी के लिए मेल सर्वर कॉन्फ़िगरेशन आवश्यक है।

    इसे कैसे करना है, इसकी डॉक्यूमेंटेशन देखें:

    ", + "how-to-configure-mail-server": "मेल सर्वर कैसे कॉन्फ़िगर करें" + }, + "step3": { + "title": "फ़ीचर कॉन्फ़िगर करें: SMS प्रदाता", + "content": "

    SMS प्रदाताओं को कॉन्फ़िगर करें ताकि कस्टमर को अलार्म SMS द्वारा सूचित किया जा सके।

    इसे कैसे करना है, इसकी डॉक्यूमेंटेशन देखें:

    ", + "how-to-configure-sms-provider": "SMS प्रदाता कैसे कॉन्फ़िगर करें" + }, + "step4": { + "title": "फ़ीचर कॉन्फ़िगर करें: व्हाइट लेबलिंग", + "content": "

    बिना कोडिंग और बिना सेवा को रीस्टार्ट किए, अपनी कंपनी या प्रोडक्ट का लोगो और रंग योजना आसानी से अनुकूलित करें।

    इसे कैसे करना है, इसकी डॉक्यूमेंटेशन देखें:

    " + }, + "step5": { + "title": "फ़ीचर कॉन्फ़िगर करें: 2FA", + "content": "

    दो-स्तरीय प्रमाणीकरण का उपयोग करके प्लेटफ़ॉर्म अकाउंट्स की सुरक्षा बढ़ाएँ।

    इसे कैसे करना है, इसकी डॉक्यूमेंटेशन देखें:

    " + }, + "step6": { + "title": "फ़ीचर कॉन्फ़िगर करें: OAuth 2", + "content": "

    OAuth 2.0 के माध्यम से सिंगल साइन-ऑन फ़ंक्शन का उपयोग करके टेनेंट और कस्टमर उपयोगकर्ताओं के लिए लॉगिन प्रक्रिया सरल करें।

    इसे कैसे करना है, इसकी डॉक्यूमेंटेशन देखें:

    " + } + }, + "tenant-admin": { + "step1": { + "title": "डिवाइस बनाएँ", + "content": "

    चलिए UI के माध्यम से आपका पहला डिवाइस प्लेटफ़ॉर्म पर प्रोविजन करते हैं। इसे कैसे करना है, इसके लिए डॉक्यूमेंटेशन देखें:

    ", + "how-to-create-device": "डिवाइस कैसे बनाएँ" + }, + "step2": { + "title": "डिवाइस कनेक्ट करें", + "content-before": "

    डिवाइस को कनेक्ट करने के लिए आपको डिवाइस के क्रेडेंशियल्स प्राप्त करने होंगे। इस गाइड के लिए हम डिफ़ॉल्ट ऑटो-जनरेटेड क्रेडेंशियल्स (एक्सेस टोकन) का उपयोग करने की सलाह देते हैं।

    • डिवाइस टेबल पर जाएँ
    • डिवाइस की पंक्ति पर क्लिक करें ताकि डिवाइस विवरण खुल जाए
    • \"एक्सेस टोकन कॉपी करें\" बटन दबाएँ

    HTTP के माध्यम से डेटा प्रकाशित करने के लिए इन सरल commands का उपयोग करें। ध्यान रखें कि $ACCESS_TOKEN को अपने डिवाइस के एक्सेस टोकन से बदलें:

    ", + "ubuntu": { + "install-curl": "Ubuntu के लिए cURL इंस्टॉल करें:" + }, + "macos": { + "install-curl": "MacOS के लिए cURL इंस्टॉल करें:" + }, + "windows": { + "install-curl": "Windows 10 b17063 से शुरू होकर, cURL डिफ़ॉल्ट रूप से उपलब्ध है।" + }, + "replace-access-token": "$ACCESS_TOKEN को अपने डिवाइस के टोकन से बदलें:", + "content-after": "

    आप MQTT, CoAP आदि जैसे अन्य प्रोटोकॉल भी उपयोग कर सकते हैं।

    इसे कैसे करना है, इसके लिए डॉक्यूमेंटेशन देखें:

    ", + "how-to-connect-device": "डिवाइस कैसे कनेक्ट करें" + }, + "step3": { + "title": "डैशबोर्ड बनाएँ", + "content": "

    एंटिटीज़ जैसे एसेट, डिवाइस आदि से आने वाले डेटा को विज़ुअलाइज़ करने के लिए एक डैशबोर्ड बनाएँ।

    इसे कैसे करना है, इसके लिए डॉक्यूमेंटेशन देखें:

    ", + "how-to-create-dashboard": "डैशबोर्ड कैसे बनाएँ" + }, + "step4": { + "title": "अलार्म नियम कॉन्फ़िगर करें", + "alarm-rules": "अलार्म नियम", + "content": "

    चलिए तब एक अलार्म ट्रिगर करते हैं जब तापमान 25°C तक पहुँच जाए। इसे कैसे करना है, इसके लिए डॉक्यूमेंटेशन देखें:

    ", + "how-to-configure-alarm-rules": "अलार्म नियम कैसे कॉन्फ़िगर करें" + }, + "step5": { + "title": "अलार्म बनाएँ", + "content-before": "

    अलार्म ट्रिगर करने के लिए 26°C या इससे अधिक का नया टेलीमेट्री मान भेजें।

    ", + "replace-access-token": "$ACCESS_TOKEN को अपने डिवाइस के टोकन से बदलें:", + "content-after": "

    इसे कैसे करना है, इसके लिए डॉक्यूमेंटेशन देखें:

    ", + "how-to-create-alarm": "अलार्म कैसे बनाएँ" + }, + "step6": { + "title": "कस्टमर बनाएँ और डैशबोर्ड साझा करें", + "content": "

    एंड-यूज़र डैशबोर्ड बनाए जाने पर, कस्टमर उपयोगकर्ता केवल अपने डिवाइस देख सकेगा, और अन्य कस्टमर का डेटा छुपा रहेगा।

    इसे कैसे करना है, इसके लिए डॉक्यूमेंटेशन देखें:

    " + } + } + }, + "api-usage": { + "api-usage": "API उपयोग", + "label": "लेबल", + "state-name": "राज्य नाम", + "status": "स्थिति", + "status-required": "स्थिति आवश्यक है।", + "limit": "अधिकतम सीमा", + "limit-required": "अधिकतम सीमा आवश्यक है।", + "current-number": "वर्तमान संख्या", + "current-number-required": "वर्तमान संख्या आवश्यक है।", + "add-key": "कुंजी जोड़ें", + "no-key": "कोई कुंजी नहीं", + "delete-key": "कुंजी हटाएं", + "target-dashboard-state": "लक्ष्य डैशबोर्ड स्थिति", + "go-to-main-state": "डिफ़ॉल्ट दृश्य पर जाएं" + } + }, + "icon": { + "icon": "आइकन", + "icons": "आइकन्स", + "custom": "कस्टम", + "select-icon": "आइकन चुनें", + "material-icons": "मटेरियल आइकन्स", + "show-all": "सभी आइकन्स दिखाएँ", + "search-icon": "आइकन खोजें", + "no-icons-found": "'{{iconSearch}}' के लिए कोई आइकन नहीं मिला" + }, + "phone-input": { + "phone-input-label": "फ़ोन नंबर", + "phone-input-required": "फ़ोन नंबर आवश्यक है", + "phone-input-validation": "फ़ोन नंबर अमान्य है या संभव नहीं है", + "phone-input-pattern": "अमान्य फ़ोन नंबर। यह E.164 फ़ॉर्मेट में होना चाहिए, उदाहरण: {{phoneNumber}}", + "phone-input-hint": "E.164 फ़ॉर्मेट में फ़ोन नंबर, उदाहरण: {{phoneNumber}}" + }, + "custom": { + "widget-action": { + "action-cell-button": "एक्शन सेल बटन", + "row-click": "पंक्ति पर क्लिक", + "cell-click": "सेल पर क्लिक", + "polygon-click": "पॉलीगॉन पर क्लिक", + "marker-click": "मार्कर पर क्लिक", + "circle-click": "सर्कल पर क्लिक", + "tooltip-tag-action": "टूलटिप टैग एक्शन", + "node-selected": "नोड चयनित होने पर", + "element-click": "HTML एलिमेंट पर क्लिक", + "pie-slice-click": "स्लाइस पर क्लिक", + "row-double-click": "पंक्ति पर डबल क्लिक", + "cell-double-click": "सेल पर डबल क्लिक", + "card-click": "कार्ड पर क्लिक", + "click": "क्लिक पर" + } + }, + "paginator": { + "items-per-page": "प्रति पृष्ठ आइटम:", + "first-page-label": "पहला पृष्ठ", + "last-page-label": "अंतिम पृष्ठ", + "next-page-label": "अगला पृष्ठ", + "previous-page-label": "पिछला पृष्ठ", + "items-per-page-separator": "में से" + }, + "language": { + "auto": "Auto", + "language": "भाषा" + } +} \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-it_IT.json b/ui-ngx/src/assets/locale/locale.constant-it_IT.json index ac47330f30..00de105518 100644 --- a/ui-ngx/src/assets/locale/locale.constant-it_IT.json +++ b/ui-ngx/src/assets/locale/locale.constant-it_IT.json @@ -78,6 +78,7 @@ "show-more": "Mostra di più", "dont-show-again": "Non mostrare più", "see-documentation": "Vedi documentazione", + "see-debug-events": "Vedi eventi di debug", "clear": "Pulisci", "upload": "Carica", "delete-anyway": "Elimina comunque", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Autenticazione a due fattori", "available-providers": "Provider disponibili", + "available-providers-required": "Deve essere configurato almeno un provider 2FA.", "issuer-name": "Nome dell'emittente", "issuer-name-required": "Il nome dell'emittente è obbligatorio.", "max-verification-failures-before-user-lockout": "Numero massimo di verifiche fallite prima del blocco dell'utente", @@ -513,7 +515,9 @@ "verification-message-template-required": "Il template del messaggio di verifica è obbligatorio.", "within-time": "Entro il tempo (sec)", "within-time-pattern": "Il tempo deve essere un numero intero positivo.", - "within-time-required": "Il tempo è obbligatorio." + "within-time-required": "Il tempo è obbligatorio.", + "force-2fa": "Forza l'autenticazione a due fattori", + "enforce-for": "Forza per" }, "jwt": { "security-settings": "Impostazioni di sicurezza JWT", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Allarme", + "alarm-list": "Elenco allarmi", "alarms": "Allarmi", "all-alarms": "Tutti gli allarmi", "select-alarm": "Seleziona allarme", @@ -649,7 +654,16 @@ "alarm-type": "Tipo di allarme", "enter-alarm-type": "Inserisci tipo di allarme", "no-alarm-types-matching": "Nessun tipo di allarme corrispondente a '{{entitySubtype}}' trovato.", - "alarm-type-list-empty": "Nessun tipo di allarme selezionato." + "alarm-type-list-empty": "Nessun tipo di allarme selezionato.", + "system-comments": { + "acked-by-user": "L'allarme è stato confermato dall'utente {{userName}}", + "cleared-by-user": "L'allarme è stato ripristinato dall'utente {{userName}}", + "assigned-to-user": "L'allarme è stato assegnato dall'utente {{userName}} all'utente {{assigneeName}}", + "unassigned-to-user": "L'allarme è stato rimosso dall'utente {{userName}}", + "unassigned-from-deleted-user": "L'allarme è stato rimosso perché l'utente {{userName}} è stato eliminato", + "comment-deleted": "L'utente {{userName}} ha eliminato il suo commento", + "severity-changed": "La gravità dell'allarme è stata aggiornata da {{oldSeverity}} a {{newSeverity}}" + } }, "alarm-activity": { "add": "Aggiungi un commento...", @@ -754,6 +768,7 @@ "name-max-length": "Il nome deve essere inferiore a 256 caratteri", "label-max-length": "L'etichetta deve essere inferiore a 256 caratteri", "description": "Descrizione", + "description-required": "La descrizione è obbligatoria.", "type": "Tipo", "type-required": "Il tipo è obbligatorio.", "details": "Dettagli", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Attività mensile di allarmi creati", "data-points": "Punti dati", "data-points-storage-days": "Giorni di conservazione dei punti dati", + "data-points-storage-days-hourly-activity": "Attività oraria dei giorni di conservazione dei punti dati", + "data-points-storage-days-daily-activity": "Attività giornaliera dei giorni di conservazione dei punti dati", + "data-points-storage-days-monthly-activity": "Attività mensile dei giorni di conservazione dei punti dati", "device-api": "API dispositivo", "email": "Email", "email-messages": "Messaggi email", @@ -900,6 +918,7 @@ "rule-node": "Nodo regola", "sms": "SMS", "sms-messages": "Messaggi SMS", + "sms-messages-hourly-activity": "Attività oraria dei messaggi SMS", "sms-messages-daily-activity": "Attività giornaliera messaggi SMS", "sms-messages-monthly-activity": "Attività mensile messaggi SMS", "successful": "${entityName} riusciti", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Attività oraria persistenza telemetria", "telemetry-persistence-monthly-activity": "Attività mensile persistenza telemetria", "transport": "Trasporto", + "transport-msg-hourly-activity": "Transportberichten: uurlijkse activiteit", + "transport-msg-daily-activity": "Transportberichten: dagelijkse activiteit", + "transport-msg-monthly-activity": "Transportberichten: maandelijkse activiteit", "transport-daily-activity": "Attività giornaliera del trasporto", "transport-data-points": "Punti dati del trasporto", + "transport-data-points-hourly-activity": "Attività oraria dei punti dati di trasporto", + "transport-data-points-daily-activity": "Attività giornaliera dei punti dati di trasporto", + "transport-data-points-monthly-activity": "Attività mensile dei punti dati di trasporto", "view-details": "Visualizza dettagli", "view-statistics": "Visualizza statistiche", - "transport-messages": "Messaggi di trasporto" + "transport-messages": "Messaggi di trasporto", + "transport-messages-hourly-activity": "Attività oraria dei messaggi di trasporto", + "transport-data-point-hourly-activity": "Attività oraria del punto dati di trasporto", + "javascript-function-executions": "Esecuzioni delle funzioni JavaScript", + "javascript-function-executions-hourly-activity": "Attività oraria delle esecuzioni delle funzioni JavaScript", + "javascript-function-executions-daily-activity": "Attività giornaliera delle esecuzioni delle funzioni JavaScript", + "javascript-function-executions-monthly-activity": "Attività mensile delle esecuzioni delle funzioni JavaScript", + "tbel-function-executions": "Esecuzioni delle funzioni TBEL", + "tbel-function-executions-hourly-activity": "Attività oraria delle esecuzioni delle funzioni TBEL", + "tbel-function-executions-daily-activity": "Attività giornaliera delle esecuzioni delle funzioni TBEL", + "tbel-function-executions-monthly-activity": "Attività mensile delle esecuzioni delle funzioni TBEL", + "created-reports": "Report creati", + "created-reports-hourly-activity": "Attività oraria dei report creati", + "created-reports-daily-activity": "Attività giornaliera dei report creati", + "created-reports-monthly-activity": "Attività mensile dei report creati", + "emails": "Email", + "emails-hourly-activity": "Attività oraria delle email", + "emails-daily-activity": "Attività giornaliera delle email", + "emails-monthly-activity": "Attività mensile delle email", + "status": { + "enabled": "Abilitato", + "disabled": "Disabilitato", + "warning": "Avviso" + } }, "api-limit": { "cassandra-write-queries-core": "Query di scrittura Cassandra tramite Rest API", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Messaggi uplink Edge", "edge-uplink-messages-per-edge": "Messaggi uplink Edge per nodo Edge" }, + "api-key": { + "api-key": "Chiave API", + "api-keys": "Chiavi API", + "delete-api-key-title": "Sei sicuro di voler eliminare la chiave API '{{name}}'?", + "delete-api-key-text": "Attenzione, dopo la conferma la chiave sarà irrecuperabile.", + "delete-api-keys-title": "Sei sicuro di voler eliminare { count, plural, =1 {1 chiave API} other {# chiavi API} }?", + "delete-api-keys-text": "Attenzione, dopo la conferma tutte le chiavi selezionate saranno irrecuperabili.", + "expiration-date": "Data di scadenza", + "date": "Data", + "description": "Descrizione", + "disable": "Disabilita", + "edit-description": "Modifica descrizione", + "enable": "Abilita chiave API", + "expiration-time": "Tempo di scadenza", + "expiration-time-never": "Mai", + "expiration-time-custom": "Personalizzato", + "generate": "Genera", + "generate-title": "Genera chiave API", + "generate-text": "Nota: La chiave API eredita i permessi dell'utente per cui è stata creata.", + "generated-api-key-title": "Chiave API generata. Controlliamo la connettività!", + "generated-api-key-copy": "Assicurati di copiare e salvare la tua chiave API ora, poiché non potrai vederla di nuovo.", + "generated-api-key-command": "Usa le seguenti istruzioni per verificare la connettività. Come risultato, dovresti ricevere le informazioni dell'utente corrente:", + "generated-api-key-insecure-url": "Eseguire comandi tramite una connessione HTTP non sicura invierà la tua chiave API non criptata, rendendola vulnerabile all'intercettazione.", + "list": "{ count, plural, =1 {Una chiave API} other {Elenco di # chiavi API} }", + "manage": "Gestisci", + "manage-api-keys": "Gestisci chiavi API", + "no-found": "Nessuna chiave API trovata", + "selected-api-keys": "{ count, plural, =1 {1 chiave API} other {# chiavi API} } selezionata/e", + "search": "Cerca chiavi API", + "status": "Stato", + "status-active": "Attivo", + "status-inactive": "Inattivo", + "status-expired": "Scaduto" + }, "audit-log": { "audit": "Audit", "audit-logs": "Log di audit", @@ -991,7 +1073,11 @@ "type-provision-failure": "Provisioning dispositivo fallito", "type-timeseries-updated": "Telemetria aggiornata", "type-timeseries-deleted": "Telemetria eliminata", - "type-sms-sent": "SMS inviato" + "type-sms-sent": "SMS inviato", + "any-type": "Qualsiasi tipo", + "audit-log-filter-title": "Filtro log di audit", + "filter-title": "Filtro", + "filter-types": "Tipi di log di audit" }, "debug-settings": { "label": "Configurazione debug", @@ -1009,15 +1095,29 @@ "expression": "Espressione", "no-found": "Nessun campo calcolato trovato", "list": "{ count, plural, =1 {Un campo calcolato} other {Elenco di # campi calcolati} }", + "add": "Aggiungi campo calcolato", "selected-fields": "{ count, plural, =1 {1 campo calcolato} other {# campi calcolati} } selezionato/i", "type": { "simple": "Semplice", - "script": "Script" + "simple-hint": "Calcolo aritmetico semplice basato sugli argomenti di input.", + "script": "Script", + "script-hint": "Calcolo sugli argomenti definiti utilizzando uno script TBEL.", + "geofencing": "Geofencing", + "geofencing-hint": "Valutazione della posizione GPS dell'entità e delle transizioni contro i gruppi di zone di geofencing configurati.", + "propagation": "Propagazione", + "propagation-hint": "Propagazione dei dati a entità superiori o inferiori in base alla direzione e al tipo di relazione.", + "related-entities-aggregation": "Aggregazione entità correlate", + "related-entities-aggregation-hint": "Aggregazione dei dati più recenti dalle entità correlate.", + "time-series-data-aggregation": "Aggregazione dei dati delle serie temporali", + "time-series-data-aggregation-hint": "Aggregazione dei dati storici da un'entità corrente." }, + "preview": "Anteprima", "arguments": "Argomenti", "decimals-by-default": "Decimali di default", "debugging": "Debug del campo calcolato", + "calculated-field-details": "Dettagli campo calcolato", "argument-name": "Nome argomento", + "name": "Nome", "datasource": "Fonte dati", "add-argument": "Aggiungi argomento", "test-script-function": "Testa funzione script", @@ -1029,6 +1129,8 @@ "argument-asset": "Asset", "argument-customer": "Cliente", "argument-tenant": "Tenant corrente", + "argument-owner": "Proprietario corrente", + "argument-relation-query": "Entità correlate", "argument-type": "Tipo di argomento", "attribute": "Attributo", "copy-argument-name": "Copia nome argomento", @@ -1042,12 +1144,14 @@ "shared-attributes": "Attributi condivisi", "attribute-key": "Chiave attributo", "default-value": "Valore di default", + "default-value-required": "Il valore predefinito è obbligatorio.", "limit": "Valori massimi", "time-window": "Finestra temporale", "customer-name": "Nome cliente", "asset-name": "Nome asset", "timeseries": "Serie temporale", "output": "Output", + "output-hint": "Definisce come viene elaborato l'output.", "create": "Crea nuovo campo calcolato", "file": "File campo calcolato", "invalid-file-error": "Formato file non valido. Assicurati che il file sia un file JSON valido.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Attenzione, dopo la conferma tutti i campi calcolati selezionati saranno eliminati e tutti i dati correlati saranno irrimediabilmente persi.", "test-with-this-message": "Testa con questo messaggio", "use-latest-timestamp": "Usa il timestamp più recente", + "entity-coordinates": "Coordinate entità", + "latitude-time-series-key": "Chiave serie temporale latitudine", + "latitude-time-series-key-required": "La chiave serie temporale latitudine è obbligatoria.", + "longitude-time-series-key": "Chiave serie temporale longitudine", + "longitude-time-series-key-required": "La chiave serie temporale longitudine è obbligatoria.", + "geofencing-zone-groups": "Gruppi di zone geofencing", + "geofencing-zone-groups-settings": "Impostazioni gruppi di zone geofencing", + "target-zone": "Zona target", + "perimeter-key": "Chiave perimetro", + "report-strategy": "Strategia di report", + "no-zone-configured": "È richiesta almeno una zona.", + "no-zone-configured-required": "Deve essere configurato almeno un gruppo di zone.", + "add-zone-group": "Aggiungi gruppo di zone", + "report-transition-event-only": "Solo eventi di transizione", + "report-presence-status-only": "Solo stato di presenza", + "report-transition-event-and-presence": "Stato di presenza e eventi di transizione", + "perimeter-attribute-key": "Chiave attributo perimetro", + "perimeter-attribute-key-required": "La chiave attributo perimetro è obbligatoria.", + "perimeter-attribute-key-pattern": "La chiave attributo perimetro non è valida.", + "entity-zone-relationship": "Percorso dall'entità alle zone", + "direction": "Direzione relazione", + "direction-from": "Dall'entità alla zona", + "direction-to": "Dalla zona all'entità", + "relation-type": "Tipo di relazione", + "create-relation-with-matched-zones": "Crea relazioni per l'entità sorgente con le zone corrispondenti", + "relation-level": "Livello di relazione", + "fetch-last-available-level": "Recupera solo l'ultimo livello disponibile", + "zone-group-refresh-interval": "Intervallo di aggiornamento gruppi di zone", + "copy-zone-group-name": "Copia nome gruppo di zone", + "open-details-page": "Apri la pagina dei dettagli dell'entità", + "level": "Livello", + "direction-level": "Direzione", + "direction-up": "Su", + "direction-up-parent": "Su verso il genitore", + "direction-down": "Giù", + "direction-down-child": "Giù verso il figlio", + "add-level": "Aggiungi livello", + "delete-level": "Elimina livello", + "no-level": "Nessun livello configurato", + "levels-required": "Deve essere configurato almeno un livello.", + "max-allowed-levels-error": "Il livello di relazione supera il massimo consentito.", + "propagation-path-related-entities": "Percorso di propagazione verso le entità correlate", + "propagate-type": { + "arguments-only": "Solo argomenti", + "expression-result": "Risultato del calcolo" + }, + "script": "Script", + "data-propagate": "Dati da propagare", + "output-key": "Chiave di output", + "copy-output-key": "Copia chiave di output", + "aggregation-path-related-entities": "Percorso di aggregazione verso le entità correlate", + "deduplication-interval": "Intervallo di deduplicazione", + "deduplication-interval-min": "L'intervallo di deduplicazione deve essere almeno di {{ sec }} secondi.", + "deduplication-interval-hint": "Tempo minimo tra le aggregazioni di telemetria.", + "deduplication-interval-required": "L'intervallo di deduplicazione è obbligatorio.", + "calculated-field-filter-title": "Filtro campo calcolato", + "filter-title": "Filtro", + "calculated-field-types": "Tipi di campo calcolato", + "events": "Eventi", + "any-type": "Qualsiasi tipo", + "metrics": { + "metrics": "Metriche", + "metrics-empty": "È richiesta almeno una metrica.", + "metric-name": "Nome metrica", + "metric-name-required": "Il nome della metrica è obbligatorio.", + "metric-name-pattern": "Il nome della metrica non è valido.", + "metric-name-duplicate": "Una metrica con questo nome esiste già.", + "metric-name-max-length": "Il nome della metrica deve essere inferiore a 256 caratteri.", + "metric-name-forbidden": "Il nome della metrica è riservato e non può essere utilizzato.", + "copy-metric-name": "Copia nome metrica", + "argument-name": "Nome argomento", + "aggregation": "Aggregazione", + "aggregation-type": { + "avg": "Media", + "min": "Minimo", + "max": "Massimo", + "sum": "Somma", + "count": "Conteggio", + "count-unique": "Conteggio univoco" + }, + "filtered": "Filtrato", + "value-source": "Fonte del valore", + "value-source-hint": "Definisce come viene ottenuto il valore per l'aggregazione.", + "value-source-type": { + "key": "Chiave", + "function": "Funzione" + }, + "no-metrics-configured": "È richiesta almeno una metrica.", + "add-metric": "Aggiungi metrica", + "max-metrics": "Numero massimo di metriche raggiunto.", + "metric-settings": "Impostazioni metrica", + "filter": "Filtro", + "filter-hint": "Abilita il filtro delle entità durante l'aggregazione. La funzione del filtro deve restituire un valore booleano e può utilizzare tutti gli argomenti configurati." + }, + "output-strategy": { + "strategy": "Strategia", + "process-right-away": "Elabora immediatamente", + "process-rule-chains": "Elabora tramite Rule Chains", + "save-time-series": "Salva nella serie temporale", + "save-database": "Salva nel database", + "save-latest-values": "Salva nei valori più recenti", + "send-web-sockets": "Invia a WebSockets", + "save-calculated-fields": "Invia a Campi calcolati", + "update-attribute-only-on-value-change": "Aggiorna l'attributo solo se il valore cambia", + "send-attributes-updated-notification": "Invia notifica di aggiornamento degli attributi", + "ttl": "TTL personalizzato", + "ttl-required": "TTL è obbligatorio", + "ttl-min": "È consentito solo un TTL minimo di 0", + "processing-parameters": "Parametri di elaborazione", + "hint": { + "strategy": "Controlla se il risultato viene elaborato immediatamente o inviato a una catena di regole per ulteriori elaborazioni.", + "processing-options": "Opzioni di elaborazione", + "update-attribute-only-on-value-change": "Aggiorna l'attributo per ogni messaggio in ingresso, indipendentemente dal fatto che il valore sia cambiato. Ciò aumenta l'uso delle API e riduce le prestazioni.", + "update-attribute-only-on-value-change-enabled": "Aggiorna l'attributo solo quando il valore cambia. Se il valore non cambia, i timestamp non vengono aggiornati e non vengono inviate notifiche.", + "send-attributes-updated-notification": "Invia un evento di aggiornamento attributi alla catena di regole predefinita.", + "save-time-series": "Salva i dati della serie temporale nella tabella ts_kv del database.", + "save-database": "Salva i dati degli attributi nel database.", + "save-latest-values": "Aggiorna i dati della serie temporale nella tabella ts_kv_latest del database se il nuovo valore è più recente.", + "send-web-sockets-attribute": "Notifica le sottoscrizioni WebSocket sugli aggiornamenti dei dati dell'attributo.", + "send-web-sockets-time-series": "Notifica le sottoscrizioni WebSocket sugli aggiornamenti dei dati della serie temporale.", + "save-calculated-fields-attribute": "Notifica i campi calcolati sugli aggiornamenti dei dati dell'attributo.", + "save-calculated-fields-time-series": "Notifica i campi calcolati sugli aggiornamenti dei dati della serie temporale.", + "ttl": "Definisce il periodo di conservazione dei dati della serie temporale. Se disabilitato, viene utilizzato il TTL del Profilo tenant." + } + }, + "aggregate-interval-type": "Tipo intervallo di aggregazione", + "aggregate-interval-value": "Valore intervallo di aggregazione", + "aggregate-interval-value-required": "Il valore dell'intervallo di aggregazione è obbligatorio.", + "aggregate-interval-value-min": "Il valore dell'intervallo di aggregazione deve essere almeno { sec, plural, =0 {0 secondo} =1 {1 secondo} other {# secondi} }.", + "aggregate-interval-value-step-multiple-of": "Il valore dell'intervallo di aggregazione deve essere un divisore o un multiplo di 1 giorno.", + "aggregate-period": { + "hour": "Ora", + "day": "Giorno", + "week": "Settimana (Lun - Dom)", + "week-sun-sat": "Settimana (Dom - Sab)", + "month": "Mese", + "quarter": "Trimestre", + "year": "Anno", + "custom": "Personalizzato" + }, + "aggregate-period-hint-offset": "Il tuo intervallo di aggregazione sarà: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Il tuo intervallo di aggregazione sarà: {{ interval }} e così via.", + "entity-aggregation": { + "argument-hint": "I dati saranno recuperati dall'entità corrente.", + "argument-title-hint": "Definisce gli argomenti di input utilizzati per l'aggregazione.", + "argument-setting-hint": "L'ultima telemetria è l'unico tipo di argomento disponibile per questo campo calcolato.", + "aggregation-interval": "Intervallo di aggregazione", + "aggregation-interval-hint": "Definisce la frequenza con cui viene eseguita l'aggregazione. Esempio: ogni 1 ora, aggrega i dati alle 00:00, 01:00, 02:00, ecc. I risultati dell'aggregazione sono memorizzati con il timestamp corrispondente all'inizio dell'intervallo di aggregazione.", + "apply-offset": "Applica offset all'intervallo di aggregazione", + "apply-offset-hint": "Definisce di quanto spostare l'inizio di ogni periodo di aggregazione (ad esempio, +10 minuti - 00:10, 01:10).", + "offset-value": "Valore offset", + "offset-value-required": "Il valore offset è obbligatorio.", + "offset-value-min": "Il valore offset deve essere un numero intero positivo.", + "offset-value-max": "Il valore offset deve essere inferiore al valore dell'intervallo di aggregazione.", + "wait-delay": "Applica timeout di attesa per telemetria ritardata", + "wait-delay-hint": "Definisce quanto tempo attendere per la telemetria ritardata dopo che l'intervallo è terminato. Se tale telemetria arriva, il risultato per quell'intervallo verrà ricalcolato.", + "duration": "Durata", + "duration-required": "La durata è obbligatoria.", + "duration-min": "La durata deve essere almeno di 1 minuto.", + "duration-hint": "Quanto tempo attendere per i dati ritardati dopo che l'intervallo è terminato.", + "produce-intermediate-result": "Produci risultato intermedio", + "produce-intermediate-result-hint": "Calcola le metriche durante l'intervallo corrente per produrre un risultato intermedio. Gli aggiornamenti avvengono non più frequentemente di una volta ogni {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Un campo calcolato di tipo semplice non dovrebbe contenere chiavi con tipo di serie temporale rolling.", + "arguments-propagate-arguments-with-rolling": "Il tipo 'Serie temporale rolling' è incompatibile con la propagazione 'Solo argomenti'.", + "arguments-propagate-argument-entity-type": "Il tipo entità è incompatibile con la propagazione 'Solo argomenti'.", + "arguments-propagate-argument-must-current-entity": "Almeno un argomento deve essere configurato con il tipo entità sorgente 'Entità corrente'.", "arguments-empty": "Almeno un argomento deve essere specificato.", "expression-required": "L'espressione è obbligatoria.", "expression-invalid": "L'espressione non è valida.", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Esiste già un argomento con questo nome.", "argument-name-max-length": "Il nome dell'argomento deve essere inferiore a 256 caratteri.", "argument-name-forbidden": "Il nome dell'argomento è riservato e non può essere utilizzato.", + "output-key-required": "La chiave di output è obbligatoria.", + "output-key-pattern": "La chiave di output non è valida.", + "output-key-duplicate": "Esiste già una chiave con questo nome.", + "output-key-max-length": "La chiave di output deve essere inferiore a 256 caratteri.", + "output-key-forbidden": "La chiave di output è riservata e non può essere utilizzata.", + "entity-type-required": "Il tipo entità è obbligatorio.", + "name-required": "Il nome è obbligatorio.", + "name-pattern": "Il nome non è valido.", + "name-duplicate": "Esiste già un nome con questo nome.", + "name-max-length": "Il nome deve essere inferiore a 256 caratteri.", + "name-forbidden": "Il nome è riservato e non può essere utilizzato.", "argument-type-required": "Il tipo di argomento è obbligatorio.", "max-args": "Numero massimo di argomenti raggiunto.", "decimals-range": "I decimali di default devono essere un numero compreso tra 0 e 15.", "expression": "L'espressione predefinita dimostra come trasformare una temperatura da Fahrenheit a Celsius.", "arguments-entity-not-found": "Entità di destinazione dell'argomento non trovata.", - "use-latest-timestamp": "Se abilitato, il valore calcolato verrà memorizzato utilizzando il timestamp più recente dalla telemetria degli argomenti, invece del tempo del server." + "use-latest-timestamp": "Se abilitato, il valore calcolato verrà memorizzato utilizzando il timestamp più recente dalla telemetria degli argomenti, invece del tempo del server.", + "entity-coordinates": "Specifica le chiavi della serie temporale che forniscono le coordinate GPS dell'entità (latitudine e longitudine).", + "geofencing-zone-groups": "Definisci uno o più gruppi di zone di geofencing da verificare (ad esempio, 'allowedZones', 'restrictedZones'). Ogni gruppo deve avere un nome univoco, che viene utilizzato come prefisso per le chiavi di telemetria di output del campo calcolato.", + "perimeter-attribute-key": "Imposta la chiave attributo che contiene la definizione del perimetro della zona di geofencing. Il perimetro viene sempre preso dagli attributi lato server dell'entità zona.", + "report-strategy": "Lo stato di presenza segnala se l'entità è attualmente DENTRO o FUORI dal gruppo di zone. Gli eventi di transizione segnalano quando l'entità È ENTRATA o È USCITA dal gruppo di zone.", + "create-relation-with-matched-zones": "Crea automaticamente e mantieni le relazioni tra l'entità e le zone in cui si trova attualmente. Le relazioni vengono rimosse quando l'entità lascia una zona e create quando entra in una nuova.", + "relation-type-required": "Il tipo di relazione è obbligatorio.", + "relation-level-required": "Il livello della relazione è obbligatorio.", + "relation-level-min": "Il valore minimo del livello di relazione è 1.", + "relation-level-max": "Il valore massimo del livello di relazione è {{max}}.", + "geofencing-empty": "È richiesto configurare almeno un gruppo di zone.", + "geofencing-entity-not-found": "Entità target di geofencing non trovata.", + "max-geofencing-zone": "Numero massimo di zone di geofencing raggiunto.", + "zone-group-refresh-interval": "Definisce con quale frequenza vengono aggiornati i gruppi di zone configurati tramite entità correlate.", + "zone-group-refresh-interval-required": "L'intervallo di aggiornamento dei gruppi di zone è obbligatorio.", + "zone-group-refresh-interval-min": "L'intervallo di aggiornamento del gruppo di zone deve essere almeno di {{ min }} secondi.", + "propagation-path-related-entities": "Definisce un percorso diretto a un'entità correlata basato sulla direzione e sul tipo di relazione selezionati. Sono supportate solo le relazioni tra entità dispositivo, asset, cliente e tenant. Il numero massimo di entità risolte tramite il percorso di relazione è {{ max }}.", + "data-propagate": "Definisce i dati da propagare dagli argomenti configurati di seguito. 'Solo argomenti' utilizza direttamente i dati recuperati, mentre 'Risultato espressione' calcola un nuovo valore da quei dati.", + "aggregation-path-related-entities": "Definisce un percorso di aggregazione a livello singolo tramite relazioni dirette con entità genitore o figlio, basato sulla direzione e sul tipo di relazione. Sono supportate solo le relazioni tra entità dispositivo, asset, cliente e tenant. Il numero massimo di entità risolte tramite il percorso di relazione è {{ max }}.", + "arguments-aggregation": "Definisce gli argomenti di input utilizzati per il filtraggio e l'aggregazione.", + "setting-arguments-aggregation": "I dati saranno recuperati dalle entità correlate configurate nel percorso di aggregazione.", + "metrics": "Definisce le metriche aggregate in base agli argomenti configurati.", + "entity-aggregation-metrics": "Definisce le metriche aggregate in base agli argomenti configurati sugli intervalli di tempo specificati.", + "import-invalid-calculated-field-type": "Impossibile importare il campo calcolato: struttura del campo calcolato non valida.", + "simple-expression-title": "Espressione aritmetica che definisce come viene calcolato il valore.", + "script-title": "Script TBEL che definisce la logica di calcolo e i valori di output.", + "simple-arguments": "Espressione aritmetica che definisce come viene calcolato il valore.", + "script-arguments": "Definisce gli argomenti di input disponibili per lo script." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Regole allarme", + "alarm-rule": "Regola allarme", + "alarm-rules": "Regole allarme", + "alarm-rules-old": "Vecchie", + "alarm-rules-actual": "Attuali", + "severities": "Gravità", + "cleared": "Condizione di cancellazione", + "delete-title": "Sei sicuro di voler eliminare la regola allarme '{{title}}'?", + "delete-text": "Attenzione, dopo la conferma la regola allarme e tutti i dati correlati diventeranno irrecuperabili.", + "delete-multiple-title": "Sei sicuro di voler eliminare { count, plural, =1 {1 regola allarme} other {# regole allarme} }?", + "delete-multiple-text": "Attenzione, dopo la conferma tutte le regole allarme selezionate verranno rimosse e tutti i dati correlati diventeranno irrecuperabili.", + "create": "Crea nuova regola allarme", + "add": "Aggiungi regola allarme", + "copy": "Copia configurazione regola allarme", + "details": "Dettagli regola allarme", + "no-found": "Nessuna regola allarme trovata", + "list": "{ count, plural, =1 {Una regola allarme} other {Elenco di # regole allarme} }", + "selected-fields": "{ count, plural, =1 {1 regola allarme} other {# regole allarme} } selezionata/e", + "import": "Importa regola allarme", + "file": "File regola allarme", + "export": "Esporta regola allarme", + "export-failed-error": "Impossibile esportare la regola allarme: {{error}}", + "entity-type": "Tipo entità", + "entity-type-required": "Il tipo entità è obbligatorio.", + "alarm-type": "Tipo di allarme", + "alarm-type-hint": "Identificatore unico (ad esempio, HighTempAlarm) nell'ambito dell'origine dell'allarme (Dispositivo, Asset, ecc.) per evitare conflitti.", + "alarm-type-required": "Il tipo di allarme è obbligatorio.", + "alarm-type-pattern": "Il tipo di allarme non è valido.", + "alarm-type-max-length": "Il tipo di allarme deve essere inferiore a 256 caratteri.", + "clear-alarm": "Cancella allarme", + "value-argument": "Argomento", + "value-argument-required": "L'argomento è obbligatorio.", + "static-settings": "Impostazioni statiche", + "configuration": "Configurazione", + "static-schedule": "Statico", + "dynamic-schedule": "Dinamico", + "operation-and": "E", + "operation-or": "O", + "condition-during": "Durante {{during}}", + "condition-during-dynamic": "Durante \"{{ attribute }}\"", + "condition-repeat-times": "Si ripete { count, plural, =1 {1 volta} other {# volte} }", + "condition-repeat-times-dynamic": "Si ripete \"{{ attribute }}\" volte", + "filter-preview": "Anteprima filtro", + "condition-settings": "Impostazioni condizione", + "static": "Statico", + "dynamic": "Dinamico", + "argument-filters": "Filtri argomento", + "argument-name": "Nome argomento", + "value-type": "Tipo di valore", + "general": "Generale", + "filters": "Filtri", + "date-time-hint": "L'argomento deve essere in millisecondi epoch. Esempio: 1698839340000 corrisponde a 2023-11-01 12:49:00 UTC.", + "operation": "Operazione", + "value-source": "Fonte del valore", + "value": "Valore", + "ignore-case": "Ignora maiuscole/minuscole", + "condition": "Condizione", + "script": "Script", + "add-filter": "Aggiungi filtro argomento", + "edit-filter": "Filtro argomento", + "remove-filter": "Rimuovi filtro argomento", + "no-filter": "È richiesto almeno un filtro.", + "conditions": { + "simple": "Semplice", + "duration": "Durata", + "repeating": "Ripetizione" + }, + "schedule-title": "Pianificazione", + "edit-schedule": "Modifica pianificazione allarme", + "schedule-type": "Tipo di pianificatore", + "schedule-type-required": "Il tipo di pianificatore è obbligatorio.", + "schedule": { + "any-time": "Attivo tutto il tempo", + "specific-time": "Attivo a un orario specifico", + "custom": "Personalizzato" + }, + "schedule-day": { + "monday": "Lunedì", + "tuesday": "Martedì", + "wednesday": "Mercoledì", + "thursday": "Giovedì", + "friday": "Venerdì", + "saturday": "Sabato", + "sunday": "Domenica" + }, + "schedule-days": "Giorni", + "schedule-time": "Orario", + "schedule-time-from": "Da", + "schedule-time-to": "A", + "schedule-days-of-week-required": "È necessario selezionare almeno un giorno della settimana.", + "tbel": "TBEL", + "expression-type": { + "simple": "Semplice", + "script": "Script" + }, + "operation-type": { + "and": "E", + "or": "O" + }, + "filter-predicate-type": { + "string": "Stringa", + "numeric": "Numerico", + "boolean": "Booleano", + "complex": "Complesso" + }, + "alarm-rule-additional-info": "Informazioni aggiuntive", + "edit-alarm-rule-additional-info": "Modifica informazioni aggiuntive", + "alarm-rule-additional-info-placeholder": "Fornisci i tuoi commenti e aggiustamenti qui per mostrarli nei dettagli dell'allarme sotto Informazioni aggiuntive", + "alarm-rule-additional-info-hint": "Suggerimento: usa ${Nome argomento} per sostituire i valori degli argomenti usati nella condizione della regola allarme.", + "alarm-rule-additional-info-icon-hint": "Usa Nome argomento per sostituire i valori degli argomenti usati nella condizione della regola allarme.", + "alarm-rule-mobile-dashboard": "Dashboard mobile", + "alarm-rule-mobile-dashboard-hint": "Utilizzato dall'app mobile come dashboard dei dettagli dell'allarme.", + "alarm-rule-no-mobile-dashboard": "Nessuna dashboard selezionata", + "alarm-rule-condition": "Condizione della regola allarme", + "enter-alarm-rule-condition-prompt": "Aggiungi condizione", + "enter-alarm-rule-clear-condition-prompt": "Aggiungi condizione di cancellazione", + "edit-alarm-rule-condition": "Condizione allarme", + "condition-type": "Tipo di condizione", + "condition-type-hint": "Le opzioni \"Durata\" e \"Ripetizione\" non sono disponibili quando l'operazione \"Missing for\" è utilizzata nel filtro.", + "select-alarm-severity": "Seleziona gravità allarme", + "add-create-alarm-rule-prompt": "È richiesta almeno una condizione di attivazione.", + "add-create-alarm-rule": "Aggiungi condizione di attivazione", + "add-clear-alarm-rule": "Aggiungi condizione di cancellazione", + "condition-duration": "Durata condizione", + "condition-duration-value": "Valore durata", + "condition-duration-time-unit": "Unità di tempo", + "condition-duration-value-range": "Il valore della durata deve essere compreso tra 1 e 2147483647.", + "condition-duration-value-pattern": "Il valore della durata deve essere un numero intero.", + "condition-duration-value-required": "Il valore della durata è obbligatorio.", + "condition-duration-time-unit-required": "L'unità di tempo è obbligatoria.", + "condition-repeating-value": "Conteggio eventi", + "condition-repeating-value-hint": "L'aggiornamento di qualsiasi argomento della regola allarme sarà conteggiato come evento", + "condition-repeating-value-range": "Il conteggio degli eventi deve essere compreso tra 1 e 2147483647.", + "condition-repeating-value-pattern": "Il conteggio degli eventi deve essere un numero intero.", + "condition-repeating-value-required": "Il conteggio degli eventi è obbligatorio.", + "create-conditions": "Condizioni di attivazione", + "clear-condition": "Condizione di cancellazione", + "no-clear-alarm-rule": "Nessuna condizione di cancellazione configurata.", + "advanced-settings": "Impostazioni avanzate", + "propagate-alarm": "Propaga allarme alle entità correlate", + "alarm-rule-relation-types-list": "Tipi di relazione", + "alarm-rule-relation-types-list-hint": "Definisce i tipi di relazione per filtrare le entità correlate. Se non impostato, l'allarme verrà propagato a tutte le entità correlate.", + "propagate-alarm-to-owner": "Propaga allarme al proprietario dell'entità (Cliente o Tenant)", + "propagate-alarm-to-tenant": "Propaga allarme al Tenant", + "alarm-rule-filter-title": "Filtro regola allarme", + "filter-title": "Filtro", + "debugging": "Debugging regola allarme", + "any-type": "Qualsiasi tipo", + "enter-alarm-rule-type": "Inserisci tipo di allarme", + "no-alarm-rule-types-matching": "Nessun tipo di allarme corrispondente a '{{entitySubtype}}' trovato.", + "alarm-rule-type-list-empty": "Nessun tipo di allarme selezionato.", + "alarm-rule-type-list": "Elenco tipi di allarme", + "alarm-rule-entity-list": "Elenco entità", + "missing-for": "mancante per", + "time-unit": "Unità di tempo", + "mode": "Modalità", + "type": "Tipo", + "value-required": "Il valore è obbligatorio.", + "min-value": "Il valore deve essere 1 o maggiore.", + "argument-in-use": "L'argomento è utilizzato come argomento generale.", + "import-invalid-alarm-rule-type": "Impossibile importare la regola allarme: struttura della regola allarme non valida.", + "no-filter-preview": "Nessun filtro specificato", + "filter-operation": { + "and": "E", + "or": "O" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Documentazione", "time-left": "{{time}} rimanenti", "output": "Output", + "sort-asc": "Ascendente", + "sort-desc": "Discendente", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Ordine della dashboard nell'app mobile", "mobile-hide": "Nascondi dashboard nell'app mobile", "update-image": "Aggiorna immagine dashboard", + "update-new-version": "Carica nuova versione", + "upload-file-to-update": "Carica file per aggiornamento", "take-screenshot": "Cattura screenshot", "select-widget-title": "Seleziona widget", "select-widget-value": "{{title}}: seleziona widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Client bootstrap", "bootstrap-server": "Server Bootstrap", "lwm2m-server": "Server LwM2M", + "client-reboot": "Attivazione aggiornamento registrazione", + "bootstrap-reboot": "Attivazione richiesta bootstrap", "client-publicKey-or-id": "Chiave pubblica o ID del client", "client-publicKey-or-id-required": "Chiave pubblica o ID del client è obbligatoria.", "client-publicKey-or-id-tooltip-psk": "L'identificatore PSK è un identificatore arbitrario fino a 128 byte, come descritto nello standard [RFC7925].\nL'identificatore PSK DEVE essere prima convertito in una stringa e poi codificato in UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "ID server breve obbligatorio.", "short-id-range": "L'ID server breve deve essere compreso tra {{ min }} e {{ max }}.", "short-id-pattern": "L'ID server breve deve essere un numero intero positivo.", + "short-id-pattern-bs": "Il breve ID del server deve essere solo nullo", "lifetime": "Durata della registrazione del client", "lifetime-required": "Durata registrazione client obbligatoria.", "lifetime-pattern": "La durata della registrazione deve essere un numero intero positivo.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Tutte le risorse sono osservate con un’unica richiesta Composite Observe (più efficiente, meno flessibile)", "composite-by-object": "Composita per oggetti", "composite-by-object-description": "Le risorse sono raggruppate per tipo di oggetto e osservate tramite richieste Composite Observe separate (approccio bilanciato)" - } + }, + "init-attr-tel-as-obs-strategy": "Inizializza attributi e telemetria utilizzando la strategia Observe", + "init-attr-tel-as-obs-strategy-hint": "Se falso - gli attributi e la telemetria vengono inizializzati leggendo i loro valori uno per uno.\\nSe vero - gli attributi e la telemetria vengono inizializzati sottoscrivendo i loro valori utilizzando la strategia Observe." }, "snmp": { "add-communication-config": "Aggiungi configurazione di comunicazione", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Nodi rule", "list-of-rulenodes": "{ count, plural, =1 {Un nodo rule} other {Elenco di # nodi rule} }", "rulenode-name-starts-with": "Nodi rule i cui nomi iniziano con '{{prefix}}'", + "type-api-key": "Chiave API", + "type-api-keys": "Chiavi API", "type-current-customer": "Cliente corrente", "type-current-tenant": "Tenant corrente", "type-current-user": "Utente corrente", @@ -2656,6 +3143,7 @@ "details": "Dettagli entità", "no-entities-prompt": "Nessuna entità trovata", "no-data": "Nessun dato da visualizzare", + "show-all-columns": "Mostra tutto", "columns-to-display": "Colonne da visualizzare", "type-api-usage-state": "Stato utilizzo API", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Un'applicazione mobile} other {Elenco di # applicazioni mobili} }", "type-mobile-app-bundle": "Pacchetto mobile", "type-mobile-app-bundles": "Pacchetti mobili", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Un pacchetto mobile} other {Elenco di # pacchetti mobili} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Un pacchetto mobile} other {Elenco di # pacchetti mobili} }", + "limit-reached": "Limite raggiunto", + "limit-reached-text": "Hai raggiunto il limite di {{ entities }}. Per aggiungere altri, chiedi al tuo Amministratore di Sistema di aumentare il limite di {{ entity }}.", + "request-limit-increase": "Richiedi aumento del limite", + "request-sysadmin-text": "Sei l'Amministratore di Sistema?", + "login-here": "Accedi qui", + "to-increase-limit": "per aumentare il limite.", + "increase-limit-request-sent-title": "Abbiamo inviato una richiesta automatica al tuo Amministratore di Sistema per aumentare il limite", + "increase-limit-request-sent-text": "Consenti del tempo per la revisione della richiesta e l'aggiornamento delle impostazioni. Potrebbe essere necessario aggiornare questa pagina per vedere le modifiche." }, "entity-field": { "created-time": "Data di creazione", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Link di reimpostazione inviato", "email": "Email", "invalid-email-format": "Formato email non valido.", + "sign-in-with": "Accedi con {{name}}", + "sign-in-to-your-account": "Accedi al tuo account", "or": "oppure", "error": "Errore di accesso", "verify-your-identity": "Verifica la tua identità", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Il link di attivazione è scaduto", "activation-link-expired-message": "Il link per attivare il tuo profilo è scaduto. Puoi tornare alla pagina di accesso per ricevere una nuova email.", "reset-password-link-expired": "Il link per reimpostare la password è scaduto", - "reset-password-link-expired-message": "Il link per reimpostare la password è scaduto. Puoi tornare alla pagina di accesso per ricevere una nuova email." + "reset-password-link-expired-message": "Il link per reimpostare la password è scaduto. Puoi tornare alla pagina di accesso per ricevere una nuova email.", + "two-fa": "Autenticazione a due fattori", + "two-fa-required": "L'autenticazione a due fattori è obbligatoria", + "set-up-verification-method": "Configura un metodo di verifica per continuare", + "set-up-verification-method-login": "Configura un metodo di verifica o accedi", + "enable-authenticator-app": "Abilita l'app di autenticazione", + "enable-authenticator-app-description": "Inserisci il codice di sicurezza dalla tua app di autenticazione", + "enable-authenticator-sms": "Abilita l'autenticatore SMS", + "enable-authenticator-sms-description": "Inserisci un codice a 6 cifre che ti abbiamo appena inviato a ", + "enable-authenticator-email": "Abilita l'autenticatore email", + "enable-authenticator-email-description": "Un codice di sicurezza è stato inviato al tuo indirizzo email a ", + "enter-key-manually": "oppure inserisci manualmente questa chiave a 32 cifre:", + "continue": "Continua", + "confirm": "Conferma", + "authenticator-app-success": "App di autenticazione abilitata con successo", + "authenticator-app-success-description": "La prossima volta che effettuerai l'accesso, dovrai fornire un codice di autenticazione a due fattori", + "authenticator-sms-success": "Autenticatore SMS abilitato con successo", + "authenticator-sms-success-description": "La prossima volta che effettuerai l'accesso, ti verrà richiesto di inserire il codice di sicurezza che verrà inviato al numero di telefono", + "authenticator-email-success": "Autenticatore email abilitato con successo", + "authenticator-email-success-description": "La prossima volta che effettuerai l'accesso, ti verrà richiesto di inserire il codice di sicurezza che verrà inviato al tuo indirizzo email", + "authenticator-backup-code-success": "Codice di backup abilitato con successo", + "authenticator-backup-code-success-description": "La prossima volta che effettuerai l'accesso, ti verrà richiesto di inserire il codice di sicurezza o utilizzare uno dei codici di backup.", + "add-verification-method": "Aggiungi metodo di verifica", + "get-backup-code": "Ottieni codice di backup", + "copy-key": "Copia chiave", + "send-code": "Invia codice", + "email-label": "Email", + "email-description": "Inserisci un'email da utilizzare come tuo autenticatore.", + "sms-description": "Inserisci un numero di telefono da utilizzare come tuo autenticatore.", + "backup-code-description": "Stampa i codici in modo da averli a portata di mano quando ne avrai bisogno per accedere al tuo account. Puoi utilizzare ogni codice di backup una sola volta.", + "backup-code-warn": "Una volta che lasci questa pagina, questi codici non potranno essere mostrati di nuovo. Conservali in modo sicuro utilizzando le opzioni sottostanti.", + "download-txt": "Scarica (txt)", + "print": "Stampa", + "verification-code": "Codice a 6 cifre", + "verification-code-invalid": "Formato del codice di verifica non valido", + "verification-code-incorrect": "Codice di verifica errato", + "verification-code-many-request": "Troppe richieste per verificare il codice", + "scan-qr-code": "Scansiona questo codice QR con la tua app di verifica", + "phone-input": { + "phone-input-label": "Numero di telefono", + "phone-input-required": "Il numero di telefono è obbligatorio", + "phone-input-validation": "Il numero di telefono non è valido o non possibile", + "phone-input-pattern": "Numero di telefono non valido. Deve essere nel formato E.164, es. {{phoneNumber}}", + "phone-input-hint": "Numero di telefono nel formato E.164, es. {{phoneNumber}}" + } }, "mobile": { "add-application": "Aggiungi applicazione", @@ -4175,6 +4717,7 @@ "api-usage-limit": "Limite utilizzo API", "device-activity": "Attività dispositivo", "entities-limit": "Limite entità", + "entities-limit-increase-request": "Richiesta aumento limite entità", "entity-action": "Azione entità", "general": "Generale", "rule-engine-lifecycle-event": "Evento ciclo di vita Rule Engine", @@ -4392,6 +4935,12 @@ "at-least": "Almeno:", "character": "{ count, plural, =1 {1 carattere} other {# caratteri} }", "digit": "{ count, plural, =1 {1 cifra} other {# cifre} }", + "password-tooltip-min-length": "Almeno {{minimumLength}} caratteri", + "password-tooltip-max-length": "Al massimo {{maximumLength}} caratteri", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} carattere maiuscolo", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} carattere minuscolo", + "password-tooltip-digit": "{{minimumDigits}} numero", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} carattere speciale", "incorrect-password-try-again": "Password errata. Riprova", "lowercase-letter": "{ count, plural, =1 {1 lettera minuscola} other {# lettere minuscole} }", "new-passwords-not-match": "La nuova password non corrisponde", @@ -4450,7 +4999,8 @@ "additional-info": "Informazioni aggiuntive (JSON)", "invalid-additional-info": "Impossibile analizzare il JSON delle informazioni aggiuntive.", "no-relations-text": "Nessuna relazione trovata", - "not": "Non" + "not": "Non", + "copy-type": "Tipo di copia" }, "resource": { "add": "Aggiungi risorsa", @@ -5400,7 +5950,8 @@ }, "time-series": "Serie temporale", "latest": "Ultimi valori", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Campi calcolati e regole allarme" }, "save-attribute": { "processing-settings": "Impostazioni di elaborazione", @@ -5595,7 +6146,8 @@ "bad-request-params": "Parametri di richiesta errati", "item-not-found": "Elemento non trovato", "too-many-requests": "Troppe richieste", - "too-many-updates": "Troppe modifiche" + "too-many-updates": "Troppe modifiche", + "entities-limit-exceeded": "Limite entità superato" }, "tenant": { "tenant": "Tenant", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Numero massimo di argomenti per campo calcolato", "max-arguments-per-cf-range": "Il numero massimo di argomenti per campo calcolato non può essere negativo", "max-arguments-per-cf-required": "Il numero massimo di argomenti per campo calcolato è richiesto", + "max-related-level-per-argument": "Numero massimo di livello di relazione per l'argomento 'Entità correlate'", + "max-related-level-per-argument-range": "Il numero massimo di livello di relazione per l'argomento 'Entità correlate' non può essere inferiore a '1'", + "max-related-level-per-argument-required": "Il numero massimo di livello di relazione per l'argomento 'Entità correlate' è obbligatorio", + "min-allowed-scheduled-update-interval": "Intervallo minimo consentito per l'aggiornamento dell'argomento 'Entità correlate' (secondi)", + "min-allowed-scheduled-update-interval-range": "Il numero minimo dell'intervallo di aggiornamento consentito non può essere negativo", + "min-allowed-deduplication-interval": "Intervallo minimo consentito di deduplicazione (secondi)", + "min-allowed-deduplication-interval-range": "Il valore dell'intervallo minimo consentito di deduplicazione non può essere negativo", + "min-allowed-deduplication-interval-required": "L'intervallo minimo consentito di deduplicazione è obbligatorio", + "intermediate-aggregation-interval": "Intervallo di aggregazione intermedio (secondi)", + "intermediate-aggregation-interval-range": "Il valore dell'intervallo di aggregazione intermedio non può essere inferiore a '1'", + "intermediate-aggregation-interval-required": "L'intervallo di aggregazione intermedio è obbligatorio", + "reevaluation-check-interval": "Intervallo di controllo della rivalutazione (secondi)", + "reevaluation-check-interval-range": "Il valore dell'intervallo di controllo della rivalutazione non può essere inferiore a '1'", + "reevaluation-check-interval-required": "L'intervallo di controllo della rivalutazione è obbligatorio", + "alarms-reevaluation-interval": "Intervallo di rivalutazione degli allarmi (secondi)", + "alarms-reevaluation-interval-range": "Il valore dell'intervallo di rivalutazione degli allarmi non può essere inferiore a '1'", + "alarms-reevaluation-interval-required": "L'intervallo di rivalutazione degli allarmi è obbligatorio", + "min-allowed-aggregation-interval": "Intervallo minimo consentito di aggregazione (secondi)", + "min-allowed-aggregation-interval-range": "Il valore dell'intervallo minimo consentito di aggregazione non può essere negativo", + "min-allowed-aggregation-interval-required": "L'intervallo minimo consentito di aggregazione è obbligatorio", + "min-allowed-scheduled-update-interval-required": "Il numero minimo dell'intervallo di aggiornamento consentito è obbligatorio", "max-state-size": "Dimensione massima dello stato in KB", "max-state-size-range": "La dimensione massima dello stato in KB non può essere negativa", "max-state-size-required": "La dimensione massima dello stato in KB è richiesta", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Numero massimo di sottoscrizioni per utente regolare", "ws-limit-max-subscriptions-per-public-user": "Numero massimo di sottoscrizioni per utente pubblico", "ws-limit-updates-per-session": "Aggiornamenti WS per sessione", + "relation-search-entity-limit": "Limite entità ricerca relazione", + "relation-search-entity-limit-hint": "Limita il numero di entità risolte all'ultimo livello del percorso di relazione. Si applica agli argomenti 'Entità correlate' e ai campi di propagazione.", + "relation-search-entity-limit-required": "Il limite delle entità nella ricerca di relazione è obbligatorio", + "relation-search-entity-limit-range": "Il limite delle entità nella ricerca di relazione non può essere inferiore a '1'", "rate-limits": { "add-limit": "Aggiungi limite", "and-also-less-than": "e anche inferiore a", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Intervallo di raggruppamento predefinito", "edit-intervals-list-hint": "È possibile specificare un elenco di opzioni di intervallo disponibili.", "edit-grouping-intervals-list-hint": "È possibile configurare l'elenco degli intervalli di raggruppamento e l'intervallo di raggruppamento predefinito.", - "all": "Tutti" + "all": "Tutti", + "save-current-settings-as-default": "Salva le impostazioni correnti come finestra temporale predefinita", + "hide-option-from-end-users": "Nascondi l'opzione dagli utenti finali" }, "tooltip": { "trigger": "Trigger", @@ -6648,6 +7227,7 @@ "export-attributes": "Esporta attributi", "export-credentials": "Esporta credenziali", "export-calculated-fields": "Esporta campi calcolati \ne regole allarme", + "export-alarm-rules": "Esporta regole allarme", "entity-versions": "Versioni entità", "versions": "Versioni", "created-time": "Ora di creazione", @@ -6665,6 +7245,7 @@ "load-attributes": "Carica attributi", "load-credentials": "Carica credenziali", "load-calculated-fields": "Carica campi calcolati", + "load-alarm-rules": "Carica regole allarme", "compare-with-current": "Confronta con l'attuale", "diff-entity-with-version": "Differenze con la versione '{{versionName}}'", "previous-difference": "Differenza precedente", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Scansiona codice QR", "make-phone-call": "Effettua una chiamata", "get-location": "Ottieni la posizione del telefono", - "take-screenshot": "Cattura schermata" + "take-screenshot": "Cattura schermata", + "handle-provision-success-function": "Gestisci la funzione di successo della provision", + "get-location-function": "Funzione per ottenere la posizione", + "process-launch-result-function": "Funzione per elaborare il risultato del lancio", + "get-phone-number-function": "Funzione per ottenere il numero di telefono", + "process-image-function": "Funzione per elaborare l'immagine", + "process-qr-code-function": "Funzione per elaborare il codice QR", + "process-location-function": "Funzione per elaborare la posizione", + "handle-empty-result-function": "Funzione per gestire il risultato vuoto", + "handle-error-function": "Funzione per gestire l'errore", + "handle-non-mobile-fallback-function": "Funzione per gestire il fallback Non-Mobile", + "save-to-gallery": "Salva nella 'Galleria Immagini'", + "provision-type": "Tipo di provision", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Funzione azione personalizzata", "custom-pretty-function": "Funzione azione personalizzata (con template HTML)", @@ -6883,7 +7480,8 @@ "marker": "Indicatore", "polygon": "Poligono", "rectangle": "Rettangolo", - "circle": "Cerchio" + "circle": "Cerchio", + "polyline": "Polilinea" }, "place-map-item": "Posiziona elemento sulla mappa", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Continua disegno poligono", "finish-draw-polygon": "Termina disegno poligono", "start-draw-circle": "Inizia a disegnare cerchio", - "finish-draw-circle": "Termina disegno cerchio" + "finish-draw-circle": "Termina disegno cerchio", + "start-draw-polyline": "Inizia a disegnare la polilinea", + "finish-draw-polyline": "Termina il disegno della polilinea" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Ritardo aggiornamento animazione" }, "chart-axis": { + "limit": "Limite", + "source": "Fonte", + "key-value": "Chiave / Valore", + "value-required": "Il valore è obbligatorio.", + "entity-key-required": "La chiave dell'entità è obbligatoria.", + "key-required": "La chiave è obbligatoria.", + "scale-limits": "Limiti scala", + "scale-appearance": "Aspetto scala", "scale": "Scala", "scale-min": "min", "scale-max": "max", @@ -8005,7 +8613,10 @@ "add-radio-option": "Aggiungi opzione radio", "radio-label-position": "Posizione dell'etichetta", "radio-label-position-before": "Prima", - "radio-label-position-after": "Dopo" + "radio-label-position-after": "Dopo", + "save-image": "Salva immagine", + "save-to-gallery": "Archivia automaticamente le immagini catturate nella Galleria Immagini", + "public-image": "Rende l'immagine disponibile per qualsiasi utente non autorizzato" }, "invalid-qr-code-text": "Testo non valido per codice QR. L'input deve essere di tipo stringa", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Percorsi", "markers": "Marker", "polygons": "Poligoni", - "circles": "Cerchi" + "circles": "Cerchi", + "polylines": "Polilinee" }, "data-layer": { "source": "Sorgente", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Cerchio per '{{entityName}}': clicca per terminare e salvare", "finish-circle-hint": "Cerchio: clicca per terminare il disegno" }, + "polyline": { + "polyline-key": "Chiave polilinea", + "polyline-key-required": "Chiave polilinea obbligatoria", + "no-polylines": "Nessuna polilinea configurata", + "add-polylines": "Aggiungi polilinea", + "polyline-configuration": "Configurazione polilinea", + "remove-polyline": "Rimuovi polilinea", + "edit": "Modifica polilinea", + "cut": "Taglia area polilinea", + "rotate": "Ruota polilinea", + "remove-polyline-for": "Rimuovi polilinea per '{{entityName}}'", + "draw-polyline": "Disegna polilinea", + "polyline-place-first-point-hint-with-entity": "Polilinea per '{{entityName}}': clicca per posizionare il primo punto", + "polyline-place-first-point-hint": "Polilinea: clicca per posizionare il primo punto", + "finish-polyline-hint-with-entity": "Polilinea per '{{entityName}}': clicca per terminare il disegno", + "finish-polyline-hint": "Polilinea: clicca per terminare il disegno", + "polyline-place-first-point-cut-hint": "Clicca per posizionare il primo punto", + "finish-polyline-cut-hint": "Clicca sul primo marcatore per terminare e salvare" + }, "select-entity": "Seleziona entità", "select-entity-hint": "Suggerimento: dopo la selezione clicca sulla mappa per impostare la posizione" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Mostra spazio vuoto al posto del pulsante di azione nascosto", "dont-reserve-space-hidden-action": "Non riservare spazio per i pulsanti di azione nascosti", "display-timestamp": "Timestamp", + "timestamp-column-name": "Timestamp", "display-pagination": "Mostra paginazione", "default-page-size": "Dimensione pagina predefinita", "page-step-settings": "Impostazioni passo di pagina", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Deve essere specificata almeno una colonna allarmi", "table-tabs": "Schede della tabella", "show-cell-actions-menu-mobile": "Mostra menu azioni cella in modalità mobile", - "disable-sorting": "Disabilita ordinamento" + "disable-sorting": "Disabilita ordinamento", + "sort-by": "Ordina schede per", + "sort-timestamp-option": "Orario di creazione" }, "latest-chart": { "total": "Totale", @@ -9491,11 +10125,28 @@ "content": "

    Creando dashboard per gli utenti finali, ogni cliente può vedere solo i propri dispositivi; i dati degli altri clienti saranno nascosti.

    Segui la documentazione per sapere come fare:

    " } } + }, + "api-usage": { + "api-usage": "Uso API", + "label": "Etichetta", + "state-name": "Nome stato", + "status": "Stato", + "status-required": "Lo stato è obbligatorio.", + "limit": "Limite massimo", + "limit-required": "Il limite massimo è obbligatorio.", + "current-number": "Numero attuale", + "current-number-required": "Il numero attuale è obbligatorio.", + "add-key": "Aggiungi chiave", + "no-key": "Nessuna chiave", + "delete-key": "Elimina chiave", + "target-dashboard-state": "Stato della dashboard di destinazione", + "go-to-main-state": "Vai alla vista predefinita" } }, "icon": { "icon": "Icona", "icons": "Icone", + "custom": "Personalizzato", "select-icon": "Seleziona icona", "material-icons": "Icone Material", "show-all": "Mostra tutte le icone", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "di" }, "language": { + "auto": "Auto", "language": "Lingua" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json index b7b87fd579..84345c6216 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json +++ b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json @@ -78,6 +78,7 @@ "show-more": "もっと表示", "dont-show-again": "今後表示しない", "see-documentation": "ドキュメントを見る", + "see-debug-events": "デバッグイベントを表示", "clear": "クリア", "upload": "アップロード", "delete-anyway": "それでも削除", @@ -485,6 +486,7 @@ "2fa": { "2fa": "二要素認証", "available-providers": "利用可能なプロバイダー", + "available-providers-required": "少なくとも 1 つの 2FA プロバイダーを設定してください。", "issuer-name": "発行者名", "issuer-name-required": "発行者名は必須です。", "max-verification-failures-before-user-lockout": "ユーザーがロックアウトされるまでの検証失敗回数の上限", @@ -513,7 +515,9 @@ "verification-message-template-required": "検証メッセージテンプレートは必須です。", "within-time": "時間内(秒)", "within-time-pattern": "時間は正の整数である必要があります。", - "within-time-required": "時間は必須です。" + "within-time-required": "時間は必須です。", + "force-2fa": "二要素認証を強制", + "enforce-for": "適用対象" }, "jwt": { "security-settings": "JWTセキュリティ設定", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "アラーム", + "alarm-list": "アラーム一覧", "alarms": "アラーム", "all-alarms": "すべてのアラーム", "select-alarm": "アラームを選択", @@ -649,7 +654,16 @@ "alarm-type": "アラームタイプ", "enter-alarm-type": "アラームタイプを入力", "no-alarm-types-matching": "'{{entitySubtype}}'に一致するアラームタイプが見つかりません。", - "alarm-type-list-empty": "選択されたアラームタイプがありません。" + "alarm-type-list-empty": "選択されたアラームタイプがありません。", + "system-comments": { + "acked-by-user": "アラームはユーザー {{userName}} により確認されました", + "cleared-by-user": "アラームはユーザー {{userName}} によりクリアされました", + "assigned-to-user": "アラームはユーザー {{userName}} によりユーザー {{assigneeName}} に割り当てられました", + "unassigned-to-user": "アラームの割り当てはユーザー {{userName}} により解除されました", + "unassigned-from-deleted-user": "ユーザー {{userName}} - が削除されたため、アラームの割り当てが解除されました", + "comment-deleted": "ユーザー {{userName}} が自分のコメントを削除しました", + "severity-changed": "アラームの重大度が {{oldSeverity}} から {{newSeverity}} に更新されました" + } }, "alarm-activity": { "add": "コメントを追加...", @@ -754,6 +768,7 @@ "name-max-length": "名前は256未満である必要があります", "label-max-length": "ラベルは256未満である必要があります", "description": "説明", + "description-required": "説明は必須です。", "type": "タイプ", "type-required": "タイプは必須です。", "details": "詳細", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "作成されたアラームの月別アクティビティ", "data-points": "データポイント", "data-points-storage-days": "データポイント保存日数", + "data-points-storage-days-hourly-activity": "データポイント保存日数の時間別アクティビティ", + "data-points-storage-days-daily-activity": "データポイント保存日数の日別アクティビティ", + "data-points-storage-days-monthly-activity": "データポイント保存日数の月別アクティビティ", "device-api": "デバイスAPI", "email": "Email", "email-messages": "Emailメッセージ", @@ -900,6 +918,7 @@ "rule-node": "ルールノード", "sms": "SMS", "sms-messages": "SMSメッセージ", + "sms-messages-hourly-activity": "SMS メッセージの時間別アクティビティ", "sms-messages-daily-activity": "SMSメッセージの日別アクティビティ", "sms-messages-monthly-activity": "SMSメッセージの月別アクティビティ", "successful": "${entityName} 成功", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "テレメトリ永続化の時間別アクティビティ", "telemetry-persistence-monthly-activity": "テレメトリ永続化の月別アクティビティ", "transport": "トランスポート", + "transport-msg-hourly-activity": "トランスポートメッセージの時間別アクティビティ", + "transport-msg-daily-activity": "トランスポートメッセージの日別アクティビティ", + "transport-msg-monthly-activity": "トランスポートメッセージの月別アクティビティ", "transport-daily-activity": "トランスポートの日別アクティビティ", "transport-data-points": "トランスポートデータポイント", + "transport-data-points-hourly-activity": "トランスポートデータポイントの時間別アクティビティ", + "transport-data-points-daily-activity": "トランスポートデータポイントの日別アクティビティ", + "transport-data-points-monthly-activity": "トランスポートデータポイントの月別アクティビティ", "view-details": "詳細を表示", "view-statistics": "統計を表示", - "transport-messages": "トランスポートメッセージ" + "transport-messages": "トランスポートメッセージ", + "transport-messages-hourly-activity": "トランスポートメッセージの時間別アクティビティ", + "transport-data-point-hourly-activity": "トランスポートデータポイントの時間別アクティビティ", + "javascript-function-executions": "JavaScript 関数の実行", + "javascript-function-executions-hourly-activity": "JavaScript 関数実行の時間別アクティビティ", + "javascript-function-executions-daily-activity": "JavaScript 関数実行の日別アクティビティ", + "javascript-function-executions-monthly-activity": "JavaScript 関数実行の月別アクティビティ", + "tbel-function-executions": "TBEL 関数の実行", + "tbel-function-executions-hourly-activity": "TBEL 関数実行の時間別アクティビティ", + "tbel-function-executions-daily-activity": "TBEL 関数実行の日別アクティビティ", + "tbel-function-executions-monthly-activity": "TBEL 関数実行の月別アクティビティ", + "created-reports": "作成済みレポート", + "created-reports-hourly-activity": "作成済みレポートの時間別アクティビティ", + "created-reports-daily-activity": "作成済みレポートの日別アクティビティ", + "created-reports-monthly-activity": "作成済みレポートの月別アクティビティ", + "emails": "Emails", + "emails-hourly-activity": "Emails の時間別アクティビティ", + "emails-daily-activity": "Emails の日別アクティビティ", + "emails-monthly-activity": "Emails の月別アクティビティ", + "status": { + "enabled": "有効", + "disabled": "無効", + "warning": "警告" + } }, "api-limit": { "cassandra-write-queries-core": "Rest API Cassandra 書き込みクエリ", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Edgeアップリンクメッセージ", "edge-uplink-messages-per-edge": "EdgeあたりのEdgeアップリンクメッセージ" }, + "api-key": { + "api-key": "API キー", + "api-keys": "API キー", + "delete-api-key-title": "API キー '{{name}}' を削除してもよろしいですか?", + "delete-api-key-text": "注意: 確認後、このキーは復元できなくなります。", + "delete-api-keys-title": "{ count, plural, =1 {1 API キー} other {# API キー} } を削除してもよろしいですか?", + "delete-api-keys-text": "注意: 確認後、選択したすべてのキーは復元できなくなります。", + "expiration-date": "有効期限", + "date": "日付", + "description": "説明", + "disable": "無効化", + "edit-description": "説明を編集", + "enable": "API キーを有効化", + "expiration-time": "有効期限", + "expiration-time-never": "なし", + "expiration-time-custom": "カスタム", + "generate": "生成", + "generate-title": "API キーを生成", + "generate-text": "注: API キーは、作成対象のユーザーの権限を継承します。", + "generated-api-key-title": "API キーが生成されました。接続を確認しましょう!", + "generated-api-key-copy": "この API キーは再表示できません。今すぐコピーして安全に保存してください。", + "generated-api-key-command": "接続を確認するには次の手順を使用してください。結果として、現在のユーザー情報を受け取るはずです:", + "generated-api-key-insecure-url": "安全でない HTTP 接続でコマンドを実行すると、API キーが暗号化されずに送信され、盗聴される恐れがあります。", + "list": "{ count, plural, =1 {API キー} other {API キー一覧 (# 件)} }", + "manage": "管理", + "manage-api-keys": "API キーを管理", + "no-found": "API キーが見つかりません", + "selected-api-keys": "{ count, plural, =1 {API キー} other {API キー (# 件)} } を選択", + "search": "API キーを検索", + "status": "ステータス", + "status-active": "アクティブ", + "status-inactive": "非アクティブ", + "status-expired": "期限切れ" + }, "audit-log": { "audit": "監査", "audit-logs": "監査ログ", @@ -991,7 +1073,11 @@ "type-provision-failure": "デバイスのプロビジョニングに失敗しました", "type-timeseries-updated": "テレメトリを更新", "type-timeseries-deleted": "テレメトリを削除", - "type-sms-sent": "SMSを送信" + "type-sms-sent": "SMSを送信", + "any-type": "すべてのタイプ", + "audit-log-filter-title": "監査ログフィルター", + "filter-title": "フィルター", + "filter-types": "監査ログタイプ" }, "debug-settings": { "label": "デバッグ設定", @@ -1009,15 +1095,29 @@ "expression": "式", "no-found": "計算フィールドが見つかりません", "list": "{ count, plural, =1 {計算フィールド} other {計算フィールド一覧(# 件)} }", + "add": "計算フィールドを追加", "selected-fields": "{ count, plural, =1 {1 件の計算フィールド} other {# 件の計算フィールド} } を選択", "type": { "simple": "シンプル", - "script": "スクリプト" + "simple-hint": "入力引数に基づくシンプルな算術計算。", + "script": "スクリプト", + "script-hint": "TBEL スクリプトを使用して、定義された引数に対して計算します。", + "geofencing": "ジオフェンシング", + "geofencing-hint": "設定されたジオフェンシングゾーングループに対して、エンティティの GPS 位置と遷移を評価します。", + "propagation": "伝播", + "propagation-hint": "リレーションの方向とタイプに基づき、親または子エンティティへデータを伝播します。", + "related-entities-aggregation": "関連エンティティ集計", + "related-entities-aggregation-hint": "関連エンティティの最新データを集計します。", + "time-series-data-aggregation": "時系列データ集計", + "time-series-data-aggregation-hint": "現在のエンティティの過去データを集計します。" }, + "preview": "プレビュー", "arguments": "引数", "decimals-by-default": "既定で小数を使用", "debugging": "計算フィールドのデバッグ", + "calculated-field-details": "計算フィールドの詳細", "argument-name": "引数名", + "name": "名前", "datasource": "データソース", "add-argument": "引数を追加", "test-script-function": "スクリプト関数をテスト", @@ -1029,6 +1129,8 @@ "argument-asset": "アセット", "argument-customer": "顧客", "argument-tenant": "現在のテナント", + "argument-owner": "現在の所有者", + "argument-relation-query": "関連エンティティ", "argument-type": "引数タイプ", "attribute": "属性", "copy-argument-name": "引数名をコピー", @@ -1042,12 +1144,14 @@ "shared-attributes": "共有属性", "attribute-key": "属性キー", "default-value": "既定値", + "default-value-required": "既定値は必須です。", "limit": "最大値数", "time-window": "時間ウィンドウ", "customer-name": "顧客名", "asset-name": "アセット名", "timeseries": "時系列", "output": "出力", + "output-hint": "出力の処理方法を定義します。", "create": "計算フィールドを新規作成", "file": "計算フィールドファイル", "invalid-file-error": "無効なファイル形式です。有効な JSON ファイルであることを確認してください。", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "注意: 確認後、選択した計算フィールドはすべて削除され、関連するすべてのデータは復元できなくなります。", "test-with-this-message": "このメッセージでテスト", "use-latest-timestamp": "最新タイムスタンプを使用", + "entity-coordinates": "エンティティ座標", + "latitude-time-series-key": "緯度の時系列キー", + "latitude-time-series-key-required": "緯度の時系列キーは必須です。", + "longitude-time-series-key": "経度の時系列キー", + "longitude-time-series-key-required": "経度の時系列キーは必須です。", + "geofencing-zone-groups": "ジオフェンシングゾーングループ", + "geofencing-zone-groups-settings": "ジオフェンシングゾーングループ設定", + "target-zone": "対象ゾーン", + "perimeter-key": "境界キー", + "report-strategy": "レポート戦略", + "no-zone-configured": "少なくとも 1 つのゾーンが必要です。", + "no-zone-configured-required": "少なくとも 1 つのゾーングループを設定する必要があります。", + "add-zone-group": "ゾーングループを追加", + "report-transition-event-only": "遷移イベントのみ", + "report-presence-status-only": "在席ステータスのみ", + "report-transition-event-and-presence": "在席ステータスと遷移イベント", + "perimeter-attribute-key": "境界属性キー", + "perimeter-attribute-key-required": "境界属性キーは必須です。", + "perimeter-attribute-key-pattern": "境界属性キーが無効です。", + "entity-zone-relationship": "エンティティからゾーンへのパス", + "direction": "リレーション方向", + "direction-from": "エンティティ → ゾーン", + "direction-to": "ゾーン → エンティティ", + "relation-type": "リレーションタイプ", + "create-relation-with-matched-zones": "一致したゾーンに対して、ソースエンティティのリレーションを作成", + "relation-level": "リレーションレベル", + "fetch-last-available-level": "利用可能な最終レベルのみ取得", + "zone-group-refresh-interval": "ゾーングループ更新間隔", + "copy-zone-group-name": "ゾーングループ名をコピー", + "open-details-page": "エンティティ詳細ページを開く", + "level": "レベル", + "direction-level": "方向", + "direction-up": "上", + "direction-up-parent": "親へ上方向", + "direction-down": "下", + "direction-down-child": "子へ下方向", + "add-level": "レベルを追加", + "delete-level": "レベルを削除", + "no-level": "レベルが設定されていません", + "levels-required": "少なくとも 1 つのレベルを設定する必要があります。", + "max-allowed-levels-error": "リレーションレベルが許可されている最大値を超えています。", + "propagation-path-related-entities": "関連エンティティへの伝播パス", + "propagate-type": { + "arguments-only": "引数のみ", + "expression-result": "計算結果" + }, + "script": "スクリプト", + "data-propagate": "伝播するデータ", + "output-key": "出力キー", + "copy-output-key": "出力キーをコピー", + "aggregation-path-related-entities": "関連エンティティへの集計パス", + "deduplication-interval": "重複排除間隔", + "deduplication-interval-min": "重複排除間隔は少なくとも {{ sec }} 秒にしてください。", + "deduplication-interval-hint": "テレメトリ集計間の最小時間です。", + "deduplication-interval-required": "重複排除間隔は必須です。", + "calculated-field-filter-title": "計算フィールドフィルター", + "filter-title": "フィルター", + "calculated-field-types": "計算フィールドタイプ", + "events": "イベント", + "any-type": "すべてのタイプ", + "metrics": { + "metrics": "メトリクス", + "metrics-empty": "少なくとも 1 つのメトリクスを設定する必要があります。", + "metric-name": "メトリクス名", + "metric-name-required": "メトリクス名は必須です。", + "metric-name-pattern": "メトリクス名が無効です。", + "metric-name-duplicate": "同じ名前のメトリクスがすでに存在します。", + "metric-name-max-length": "メトリクス名は 256 文字未満にしてください。", + "metric-name-forbidden": "メトリクス名は予約されているため使用できません。", + "copy-metric-name": "メトリクス名をコピー", + "argument-name": "引数名", + "aggregation": "集計", + "aggregation-type": { + "avg": "平均", + "min": "最小", + "max": "最大", + "sum": "合計", + "count": "カウント", + "count-unique": "ユニーク数" + }, + "filtered": "フィルター済み", + "value-source": "値ソース", + "value-source-hint": "集計する値の取得方法を定義します。", + "value-source-type": { + "key": "キー", + "function": "関数" + }, + "no-metrics-configured": "少なくとも 1 つのメトリクスが必要です。", + "add-metric": "メトリクスを追加", + "max-metrics": "メトリクス数が上限に達しました。", + "metric-settings": "メトリクス設定", + "filter": "フィルター", + "filter-hint": "集計中のエンティティをフィルタリングできます。フィルター関数は boolean 値を返す必要があり、設定済みのすべての引数を使用できます。" + }, + "output-strategy": { + "strategy": "戦略", + "process-right-away": "直ちに処理", + "process-rule-chains": "ルールチェーン経由で処理", + "save-time-series": "時系列に保存", + "save-database": "データベースに保存", + "save-latest-values": "最新値に保存", + "send-web-sockets": "WebSockets に送信", + "save-calculated-fields": "計算フィールドに送信", + "update-attribute-only-on-value-change": "値が変化した場合のみ属性を更新", + "send-attributes-updated-notification": "属性更新通知を送信", + "ttl": "カスタム TTL", + "ttl-required": "TTL は必須です", + "ttl-min": "最小 TTL は 0 のみ許可されています", + "processing-parameters": "処理パラメーター", + "hint": { + "strategy": "結果を即時に処理するか、追加処理のためにルールチェーンへ送信するかを制御します。", + "processing-options": "処理オプション", + "update-attribute-only-on-value-change": "値が変化しているかどうかに関係なく、受信したすべてのメッセージで属性を更新します。API 使用量が増え、パフォーマンスが低下します。", + "update-attribute-only-on-value-change-enabled": "値が変化した場合のみ属性を更新します。値が変化しない場合、タイムスタンプは更新されず、通知も送信されません。", + "send-attributes-updated-notification": "デフォルトのルールチェーンに「属性更新」イベントを送信します。", + "save-time-series": "データベースの ts_kv テーブルに時系列データを保存します。", + "save-database": "属性データをデータベースに保存します。", + "save-latest-values": "新しい値がより新しい場合、データベースの ts_kv_latest テーブルの時系列データを更新します。", + "send-web-sockets-attribute": "属性データの更新について WebSocket サブスクリプションに通知します。", + "send-web-sockets-time-series": "時系列データの更新について WebSocket サブスクリプションに通知します。", + "save-calculated-fields-attribute": "属性データの更新について計算フィールドに通知します。", + "save-calculated-fields-time-series": "時系列データの更新について計算フィールドに通知します。", + "ttl": "時系列データの保持期間を定義します。無効の場合は、テナントプロファイルの TTL が使用されます。" + } + }, + "aggregate-interval-type": "集計間隔タイプ", + "aggregate-interval-value": "集計間隔値", + "aggregate-interval-value-required": "集計間隔値は必須です。", + "aggregate-interval-value-min": "集計間隔値は少なくとも { sec, plural, =0 {0 秒} =1 {1 秒} other {# 秒} } にしてください。", + "aggregate-interval-value-step-multiple-of": "集計間隔値は 1 日の約数または倍数である必要があります。", + "aggregate-period": { + "hour": "時間", + "day": "日", + "week": "週 (月 - 日)", + "week-sun-sat": "週 (日 - 土)", + "month": "月", + "quarter": "四半期", + "year": "年", + "custom": "カスタム" + }, + "aggregate-period-hint-offset": "集計間隔は次のとおりです: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "集計間隔は次のとおりです: {{ interval }} など。", + "entity-aggregation": { + "argument-hint": "データは現在のエンティティから取得されます。", + "argument-title-hint": "集計に使用する入力引数を定義します。", + "argument-setting-hint": "この計算フィールドで利用できる引数タイプは最新テレメトリのみです。", + "aggregation-interval": "集計間隔", + "aggregation-interval-hint": "集計を実行する頻度を定義します。例: 1 時間ごとに 00:00、01:00、02:00... のデータを集計します。集計結果は、集計間隔の開始時刻に対応するタイムスタンプで保存されます。", + "apply-offset": "集計間隔にオフセットを適用", + "apply-offset-hint": "各集計期間の開始時刻をどれだけずらすかを定義します(例: +10 分 - 00:10、01:10)。", + "offset-value": "オフセット値", + "offset-value-required": "オフセット値は必須です。", + "offset-value-min": "オフセット値は正の整数である必要があります。", + "offset-value-max": "オフセット値は集計間隔値より小さくする必要があります。", + "wait-delay": "遅延テレメトリの待機タイムアウトを適用", + "wait-delay-hint": "間隔終了後に遅延テレメトリを待機する時間を定義します。そのようなテレメトリが到着した場合、その間隔の結果は再計算されます。", + "duration": "待機時間", + "duration-required": "待機時間は必須です。", + "duration-min": "待機時間は少なくとも 1 分にしてください。", + "duration-hint": "間隔終了後に遅延データを待機する時間。", + "produce-intermediate-result": "中間結果を生成", + "produce-intermediate-result-hint": "現在の間隔中にメトリクスを計算して中間結果を生成します。更新は {{ time }} に 1 回を超えない頻度で行われます。" + }, "hint": { "arguments-simple-with-rolling": "シンプルタイプの計算フィールドには、時系列ローリングタイプのキーを含めることはできません。", + "arguments-propagate-arguments-with-rolling": "「時系列ローリング」タイプは「引数のみ」伝播と互換性がありません。", + "arguments-propagate-argument-entity-type": "エンティティタイプは「引数のみ」伝播と互換性がありません。", + "arguments-propagate-argument-must-current-entity": "少なくとも 1 つの引数は、ソースエンティティタイプとして「現在のエンティティ」を設定する必要があります。", "arguments-empty": "引数を空にすることはできません。", "expression-required": "式は必須です。", "expression-invalid": "式が無効です", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "この名前の引数はすでに存在します。", "argument-name-max-length": "引数名は256文字未満である必要があります。", "argument-name-forbidden": "引数名は予約語のため使用できません。", + "output-key-required": "出力キーは必須です。", + "output-key-pattern": "出力キーが無効です。", + "output-key-duplicate": "同じ名前のキーがすでに存在します。", + "output-key-max-length": "出力キーは 256 文字未満にしてください。", + "output-key-forbidden": "出力キーは予約されているため使用できません。", + "entity-type-required": "エンティティタイプは必須です", + "name-required": "名前は必須です。", + "name-pattern": "名前が無効です。", + "name-duplicate": "同じ名前がすでに存在します。", + "name-max-length": "名前は 256 文字未満にしてください。", + "name-forbidden": "名前は予約されているため使用できません。", "argument-type-required": "引数タイプは必須です。", "max-args": "引数の最大数に達しました。", "decimals-range": "デフォルトの小数桁は0〜15の数値である必要があります。", "expression": "デフォルトの式は、温度を華氏から摂氏に変換する方法を示します。", "arguments-entity-not-found": "引数の対象エンティティが見つかりません。", - "use-latest-timestamp": "有効にすると、計算値はサーバー時刻ではなく、引数のテレメトリの最新タイムスタンプを使用して永続化されます。" + "use-latest-timestamp": "有効にすると、計算値はサーバー時刻ではなく、引数のテレメトリの最新タイムスタンプを使用して永続化されます。", + "entity-coordinates": "エンティティの GPS 座標(緯度・経度)を提供する時系列キーを指定します。", + "geofencing-zone-groups": "チェックするジオフェンシングゾーングループを 1 つ以上定義します(例: 'allowedZones'、'restrictedZones')。各グループには一意の名前が必要で、その名前は計算フィールドの出力テレメトリキーのプレフィックスとして使用されます。", + "perimeter-attribute-key": "ジオフェンシングゾーンの境界定義を含む属性キーを設定します。境界は常にゾーンエンティティのサーバー側属性から取得されます。", + "report-strategy": "在席ステータスは、エンティティが現在ゾーングループの内側(INSIDE)か外側(OUTSIDE)かを報告します。遷移イベントは、エンティティがゾーングループに入った(ENTERED)または出た(LEFT)タイミングを報告します。", + "create-relation-with-matched-zones": "エンティティと、現在その内側にあるゾーンとのリレーションを自動的に作成・維持します。エンティティがゾーンから出るとリレーションは削除され、新しいゾーンに入ると作成されます。", + "relation-type-required": "リレーションタイプは必須です。", + "relation-level-required": "リレーションレベルは必須です。", + "relation-level-min": "リレーションレベルの最小値は 1 です。", + "relation-level-max": "リレーションレベルの最大値は {{max}} です。", + "geofencing-empty": "少なくとも 1 つのゾーングループを設定する必要があります。", + "geofencing-entity-not-found": "ジオフェンシング対象エンティティが見つかりません。", + "max-geofencing-zone": "ジオフェンシングゾーン数が上限に達しました。", + "zone-group-refresh-interval": "関連エンティティ経由で設定されたゾーングループを更新する頻度を定義します。", + "zone-group-refresh-interval-required": "ゾーングループ更新間隔は必須です。", + "zone-group-refresh-interval-min": "ゾーングループ更新間隔は少なくとも {{ min }} 秒にしてください。", + "propagation-path-related-entities": "選択した方向とリレーションタイプに基づき、関連エンティティへの直接の単一レベルのパスを定義します。", + "data-propagate": "下で設定された引数から伝播するデータを定義します。「引数のみ」は取得したデータをそのまま使用し、「計算結果」はそのデータから新しい値を計算します。", + "aggregation-path-related-entities": "方向とリレーションタイプに基づき、親または子エンティティとの直接リレーションを介した単一レベルの集計パスを定義します。デバイス、アセット、顧客、テナントの各エンティティ間のリレーションのみがサポートされます。", + "arguments-aggregation": "フィルタリングと集計に使用する入力引数を定義します。", + "setting-arguments-aggregation": "データは、集計パスで設定された関連エンティティから取得されます。", + "metrics": "設定された引数に基づいて集計されるメトリクスを定義します。", + "entity-aggregation-metrics": "指定した時間間隔にわたり、設定された引数に基づいて集計されるメトリクスを定義します。", + "import-invalid-calculated-field-type": "計算フィールドをインポートできません: 計算フィールド構造が無効です。", + "simple-expression-title": "計算値の算出方法を定義する算術式。", + "script-title": "計算ロジックと出力値を定義する TBEL スクリプト。", + "simple-arguments": "計算値の算出方法を定義する算術式。", + "script-arguments": "スクリプトで利用できる入力引数を定義します。" + } + }, + "alarm-rule": { + "alarm-rules-tab": "アラームルール", + "alarm-rule": "アラームルール", + "alarm-rules": "アラームルール", + "alarm-rules-old": "旧", + "alarm-rules-actual": "現在", + "severities": "重大度", + "cleared": "クリア条件", + "delete-title": "アラームルール '{{title}}' を削除してもよろしいですか?", + "delete-text": "注意: 確認後、アラームルールと関連するすべてのデータは復元できなくなります。", + "delete-multiple-title": "{ count, plural, =1 {アラームルール 1 件} other {アラームルール # 件} } を削除してもよろしいですか?", + "delete-multiple-text": "注意: 確認後、選択したアラームルールはすべて削除され、関連するすべてのデータは復元できなくなります。", + "create": "アラームルールを新規作成", + "add": "アラームルールを追加", + "copy": "アラームルール設定をコピー", + "details": "アラームルールの詳細", + "no-found": "アラームルールが見つかりません", + "list": "{ count, plural, =1 {アラームルール} other {アラームルール一覧 (# 件)} }", + "selected-fields": "{ count, plural, =1 {アラームルール} other {アラームルール (# 件)} } を選択", + "import": "アラームルールをインポート", + "file": "アラームルールファイル", + "export": "アラームルールをエクスポート", + "export-failed-error": "アラームルールをエクスポートできません: {{error}}", + "entity-type": "エンティティタイプ", + "entity-type-required": "エンティティタイプは必須です。", + "alarm-type": "アラームタイプ", + "alarm-type-hint": "競合を防ぐため、アラーム発生元(デバイス、アセットなど)のスコープ内で一意となる識別子(例: HighTempAlarm)。", + "alarm-type-required": "アラームタイプは必須です。", + "alarm-type-pattern": "アラームタイプが無効です。", + "alarm-type-max-length": "アラームタイプは 256 文字未満にしてください。", + "clear-alarm": "アラームをクリア", + "value-argument": "引数", + "value-argument-required": "引数は必須です。", + "static-settings": "固定設定", + "configuration": "設定", + "static-schedule": "固定", + "dynamic-schedule": "動的", + "operation-and": "AND", + "operation-or": "OR", + "condition-during": "{{during}} の間", + "condition-during-dynamic": "\"{{ attribute }}\" の間", + "condition-repeat-times": "{ count, plural, =1 {1 回} other {# 回} }繰り返す", + "condition-repeat-times-dynamic": "\"{{ attribute }}\" 回繰り返す", + "filter-preview": "フィルタープレビュー", + "condition-settings": "条件設定", + "static": "固定", + "dynamic": "動的", + "argument-filters": "引数フィルター", + "argument-name": "引数名", + "value-type": "値タイプ", + "general": "一般", + "filters": "フィルター", + "date-time-hint": "引数はエポックミリ秒である必要があります。例: 1698839340000 は 2023-11-01 12:49:00 UTC に相当します。", + "operation": "演算", + "value-source": "値ソース", + "value": "値", + "ignore-case": "大文字/小文字を区別しない", + "condition": "条件", + "script": "スクリプト", + "add-filter": "引数フィルターを追加", + "edit-filter": "引数フィルター", + "remove-filter": "引数フィルターを削除", + "no-filter": "少なくとも 1 つのフィルターが必要です。", + "conditions": { + "simple": "シンプル", + "duration": "継続時間", + "repeating": "繰り返し" + }, + "schedule-title": "スケジュール", + "edit-schedule": "アラームスケジュールを編集", + "schedule-type": "スケジューラータイプ", + "schedule-type-required": "スケジューラータイプは必須です。", + "schedule": { + "any-time": "常にアクティブ", + "specific-time": "特定の時間にアクティブ", + "custom": "カスタム" + }, + "schedule-day": { + "monday": "月曜日", + "tuesday": "火曜日", + "wednesday": "水曜日", + "thursday": "木曜日", + "friday": "金曜日", + "saturday": "土曜日", + "sunday": "日曜日" + }, + "schedule-days": "日", + "schedule-time": "時間", + "schedule-time-from": "開始", + "schedule-time-to": "終了", + "schedule-days-of-week-required": "少なくとも 1 つの曜日を選択してください。", + "tbel": "TBEL", + "expression-type": { + "simple": "シンプル", + "script": "スクリプト" + }, + "operation-type": { + "and": "AND", + "or": "OR" + }, + "filter-predicate-type": { + "string": "文字列", + "numeric": "数値", + "boolean": "真偽値", + "complex": "複合" + }, + "alarm-rule-additional-info": "追加情報", + "edit-alarm-rule-additional-info": "追加情報を編集", + "alarm-rule-additional-info-placeholder": "ここにコメントや調整内容を入力すると、アラーム詳細の「追加情報」に表示されます", + "alarm-rule-additional-info-hint": "ヒント: ${Argument name} を使用して、アラームルール条件で使用される引数の値を置換できます。", + "alarm-rule-additional-info-icon-hint": "引数名を使用して、アラームルール条件で使用される引数の値を置換します。", + "alarm-rule-mobile-dashboard": "モバイルダッシュボード", + "alarm-rule-mobile-dashboard-hint": "モバイルアプリケーションでアラーム詳細ダッシュボードとして使用されます。", + "alarm-rule-no-mobile-dashboard": "ダッシュボードが選択されていません", + "alarm-rule-condition": "アラームルール条件", + "enter-alarm-rule-condition-prompt": "条件を追加", + "enter-alarm-rule-clear-condition-prompt": "クリア条件を追加", + "edit-alarm-rule-condition": "アラーム条件", + "condition-type": "条件タイプ", + "condition-type-hint": "\"継続時間\" と \"繰り返し\" オプションは、フィルターで \"欠落期間\" の演算を使用している場合は利用できません。", + "select-alarm-severity": "アラーム重大度を選択", + "add-create-alarm-rule-prompt": "少なくとも 1 つのトリガー条件が必要です。", + "add-create-alarm-rule": "トリガー条件を追加", + "add-clear-alarm-rule": "クリア条件を追加", + "condition-duration": "条件の継続時間", + "condition-duration-value": "継続時間の値", + "condition-duration-time-unit": "時間単位", + "condition-duration-value-range": "継続時間の値は 1 ~ 2147483647 の範囲にしてください。", + "condition-duration-value-pattern": "継続時間の値は整数にしてください。", + "condition-duration-value-required": "継続時間の値は必須です。", + "condition-duration-time-unit-required": "時間単位は必須です。", + "condition-repeating-value": "イベント数", + "condition-repeating-value-hint": "アラームルール引数の更新はすべてイベントとしてカウントされます", + "condition-repeating-value-range": "イベント数は 1 ~ 2147483647 の範囲にしてください。", + "condition-repeating-value-pattern": "イベント数は整数にしてください。", + "condition-repeating-value-required": "イベント数は必須です。", + "create-conditions": "トリガー条件", + "clear-condition": "クリア条件", + "no-clear-alarm-rule": "クリア条件が設定されていません。", + "advanced-settings": "詳細設定", + "propagate-alarm": "関連エンティティへアラームを伝播", + "alarm-rule-relation-types-list": "リレーションタイプ", + "alarm-rule-relation-types-list-hint": "関連エンティティをフィルタリングするためのリレーションタイプを定義します。未設定の場合、アラームはすべての関連エンティティに伝播されます。", + "propagate-alarm-to-owner": "エンティティ所有者(顧客またはテナント)へアラームを伝播", + "propagate-alarm-to-tenant": "テナントへアラームを伝播", + "alarm-rule-filter-title": "アラームルールフィルター", + "filter-title": "フィルター", + "debugging": "アラームルールのデバッグ", + "any-type": "すべてのタイプ", + "enter-alarm-rule-type": "アラームタイプを入力", + "no-alarm-rule-types-matching": "'{{entitySubtype}}' に一致するアラームタイプが見つかりません。", + "alarm-rule-type-list-empty": "アラームタイプが選択されていません。", + "alarm-rule-type-list": "アラームタイプ一覧", + "alarm-rule-entity-list": "エンティティ一覧", + "missing-for": "欠落期間", + "time-unit": "単位", + "mode": "モード", + "type": "タイプ", + "value-required": "値は必須です。", + "min-value": "値は 1 以上である必要があります。", + "argument-in-use": "引数は一般引数として使用されています。", + "import-invalid-alarm-rule-type": "アラームルールをインポートできません: アラームルール構造が無効です。", + "no-filter-preview": "フィルターが指定されていません", + "filter-operation": { + "and": "AND", + "or": "OR" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "ドキュメント", "time-left": "残り{{time}}", "output": "出力", + "sort-asc": "昇順", + "sort-desc": "降順", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "モバイルアプリケーションでのダッシュボード順序", "mobile-hide": "モバイルアプリケーションでダッシュボードを非表示", "update-image": "ダッシュボード画像を更新", + "update-new-version": "新しいバージョンをアップロード", + "upload-file-to-update": "更新するファイルをアップロード", "take-screenshot": "スクリーンショットを撮る", "select-widget-title": "ウィジェットを選択", "select-widget-value": "{{title}}: ウィジェットを選択", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "ブートストラップクライアント", "bootstrap-server": "ブートストラップサーバー", "lwm2m-server": "LwM2Mサーバー", + "client-reboot": "登録更新トリガー", + "bootstrap-reboot": "ブートストラップ要求トリガー", "client-publicKey-or-id": "クライアント公開鍵またはID", "client-publicKey-or-id-required": "クライアント公開鍵またはIDは必須です。", "client-publicKey-or-id-tooltip-psk": "PSK識別子は、標準[RFC7925]で説明されているとおり、最大128バイトの任意のPSK識別子です。\nPSK識別子はまず文字列に変換し、その後UTF-8を使用してオクテットにエンコードしなければなりません。", @@ -2225,6 +2707,7 @@ "short-id-required": "短縮サーバーIDは必須です。", "short-id-range": "短縮サーバーIDは {{ min }} から {{ max }} の範囲である必要があります。", "short-id-pattern": "短縮サーバーIDは正の整数である必要があります。", + "short-id-pattern-bs": "ショートサーバー ID は null のみ指定できます", "lifetime": "クライアント登録の有効期間", "lifetime-required": "クライアント登録の有効期間は必須です。", "lifetime-pattern": "クライアント登録の有効期間は正の整数である必要があります。", @@ -2319,7 +2802,9 @@ "composite-all-description": "すべてのリソースを1回の複合Observeリクエストで観察(効率的だが柔軟性に欠ける)", "composite-by-object": "オブジェクトごとに複合的に観察", "composite-by-object-description": "リソースをオブジェクトタイプごとにグループ化し、複数のComposite Observeリクエストで観察(バランスの取れたアプローチ)" - } + }, + "init-attr-tel-as-obs-strategy": "Observe 戦略を使用して属性とテレメトリを初期化", + "init-attr-tel-as-obs-strategy-hint": "false の場合 - 属性とテレメトリは値を 1 つずつ読み取って初期化されます。\\ntrue の場合 - Observe 戦略を使用して値をサブスクライブすることで、属性とテレメトリが初期化されます。" }, "snmp": { "add-communication-config": "通信設定を追加", @@ -2635,6 +3120,8 @@ "type-rulenodes": "ルールノード", "list-of-rulenodes": "{ count, plural, =1 {1 つのルールノード} other {# 個のルールノード} }", "rulenode-name-starts-with": "名前が '{{prefix}}' で始まるルールノード", + "type-api-key": "API キー", + "type-api-keys": "API キー", "type-current-customer": "現在の顧客", "type-current-tenant": "現在のテナント", "type-current-user": "現在のユーザー", @@ -2656,6 +3143,7 @@ "details": "エンティティ詳細", "no-entities-prompt": "エンティティが見つかりません", "no-data": "表示するデータがありません", + "show-all-columns": "すべて表示", "columns-to-display": "表示する列", "type-api-usage-state": "API使用状況", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {モバイルアプリケーション 1 件} other {モバイルアプリケーション一覧 # 件} }", "type-mobile-app-bundle": "モバイルバンドル", "type-mobile-app-bundles": "モバイルバンドル", - "list-of-mobile-app-bundles": "{ count, plural, =1 {モバイルバンドル 1 件} other {モバイルバンドル一覧 # 件} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {モバイルバンドル 1 件} other {モバイルバンドル一覧 # 件} }", + "limit-reached": "上限に達しました", + "limit-reached-text": "{{ entities }} の上限に達しました。さらに追加するには、システム管理者に {{ entity }} の上限引き上げを依頼してください。", + "request-limit-increase": "上限引き上げを依頼", + "request-sysadmin-text": "システム管理者ですか?", + "login-here": "ここでログイン", + "to-increase-limit": "上限を引き上げるには。", + "increase-limit-request-sent-title": "上限を引き上げるための自動リクエストをシステム管理者に送信しました", + "increase-limit-request-sent-text": "リクエストの確認と設定更新まで少し時間がかかる場合があります。変更を確認するには、このページを更新する必要がある場合があります。" }, "entity-field": { "created-time": "作成日時", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "リセットリンクが送信されました", "email": "メールアドレス", "invalid-email-format": "無効なメールアドレス形式です。", + "sign-in-with": "{{name}} でサインイン", + "sign-in-to-your-account": "アカウントにサインイン", "or": "または", "error": "ログインエラー", "verify-your-identity": "本人確認を行ってください", @@ -3807,7 +4305,51 @@ "activation-link-expired": "アクティベーションリンクの有効期限が切れました", "activation-link-expired-message": "プロファイルをアクティブにするリンクの有効期限が切れました。新しいメールを受け取るためにログインページに戻ってください。", "reset-password-link-expired": "パスワードリセットリンクの有効期限が切れました", - "reset-password-link-expired-message": "パスワードをリセットするリンクの有効期限が切れました。新しいメールを受け取るためにログインページに戻ってください。" + "reset-password-link-expired-message": "パスワードをリセットするリンクの有効期限が切れました。新しいメールを受け取るためにログインページに戻ってください。", + "two-fa": "二要素認証", + "two-fa-required": "二要素認証が必要です", + "set-up-verification-method": "続行するには認証方法を設定してください", + "set-up-verification-method-login": "認証方法を設定するか、ログインしてください", + "enable-authenticator-app": "認証アプリを有効化", + "enable-authenticator-app-description": "認証アプリのセキュリティコードを入力してください", + "enable-authenticator-sms": "SMS 認証を有効化", + "enable-authenticator-sms-description": "次に送信した 6 桁のコードを入力してください: ", + "enable-authenticator-email": "email 認証を有効化", + "enable-authenticator-email-description": "セキュリティコードを次のメールアドレスに送信しました: ", + "enter-key-manually": "または、この 32 桁のキーを手動で入力してください:", + "continue": "続行", + "confirm": "確認", + "authenticator-app-success": "認証アプリを有効化しました", + "authenticator-app-success-description": "次回ログイン時は、二要素認証コードの入力が必要です", + "authenticator-sms-success": "SMS 認証を有効化しました", + "authenticator-sms-success-description": "次回ログイン時に、電話番号に送信されるセキュリティコードの入力を求められます", + "authenticator-email-success": "Email 認証を有効化しました", + "authenticator-email-success-description": "次回ログイン時に、メールアドレスに送信されるセキュリティコードの入力を求められます", + "authenticator-backup-code-success": "バックアップコードを有効化しました", + "authenticator-backup-code-success-description": "次回ログイン時に、セキュリティコードの入力、またはバックアップコードのいずれか 1 つの使用を求められます。", + "add-verification-method": "認証方法を追加", + "get-backup-code": "バックアップコードを取得", + "copy-key": "キーをコピー", + "send-code": "コードを送信", + "email-label": "Email", + "email-description": "認証に使用するメールアドレスを入力してください。", + "sms-description": "認証に使用する電話番号を入力してください。", + "backup-code-description": "アカウントにログインする際に使用できるよう、コードを印刷して手元に保管してください。各バックアップコードは 1 回のみ使用できます。", + "backup-code-warn": "このページを離れると、これらのコードは再表示できません。以下のオプションを使用して安全に保管してください。", + "download-txt": "ダウンロード(txt)", + "print": "印刷", + "verification-code": "6 桁のコード", + "verification-code-invalid": "認証コード形式が無効です", + "verification-code-incorrect": "認証コードが正しくありません", + "verification-code-many-request": "認証コード確認のリクエストが多すぎます", + "scan-qr-code": "認証アプリでこの QR コードをスキャンしてください", + "phone-input": { + "phone-input-label": "電話番号", + "phone-input-required": "電話番号は必須です", + "phone-input-validation": "電話番号が無効、または使用できません", + "phone-input-pattern": "無効な電話番号です。E.164 形式である必要があります(例: {{phoneNumber}})", + "phone-input-hint": "E.164 形式の電話番号(例: {{phoneNumber}})" + } }, "mobile": { "add-application": "アプリケーションを追加", @@ -4175,6 +4717,7 @@ "api-usage-limit": "API 使用量上限", "device-activity": "デバイスアクティビティ", "entities-limit": "エンティティ上限", + "entities-limit-increase-request": "エンティティ上限引き上げリクエスト", "entity-action": "エンティティアクション", "general": "一般", "rule-engine-lifecycle-event": "ルールエンジンライフサイクルイベント", @@ -4392,6 +4935,12 @@ "at-least": "少なくとも:", "character": "{ count, plural, =1 {1 文字} other {# 文字} }", "digit": "{ count, plural, =1 {1 桁} other {# 桁} }", + "password-tooltip-min-length": "長さは少なくとも {{minimumLength}} 文字", + "password-tooltip-max-length": "長さは最大 {{maximumLength}} 文字", + "password-tooltip-uppercase": "大文字 {{minimumUppercaseLetters}} 文字", + "password-tooltip-lowercase": "小文字 {{minimumLowercaseLetters}} 文字", + "password-tooltip-digit": "数字 {{minimumDigits}} 個", + "password-tooltip-special-characters": "特殊文字 {{minimumSpecialCharacters}} 文字", "incorrect-password-try-again": "パスワードが正しくありません。もう一度お試しください", "lowercase-letter": "{ count, plural, =1 {小文字 1 文字} other {小文字 # 文字} }", "new-passwords-not-match": "新しいパスワードが一致しません", @@ -4450,7 +4999,8 @@ "additional-info": "追加情報(JSON)", "invalid-additional-info": "追加情報のJSONの解析に失敗しました。", "no-relations-text": "リレーションが見つかりません", - "not": "ない" + "not": "ない", + "copy-type": "タイプをコピー" }, "resource": { "add": "リソースを追加", @@ -5400,7 +5950,8 @@ }, "time-series": "時系列", "latest": "最新値", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "計算フィールドとアラームルール" }, "save-attribute": { "processing-settings": "処理設定", @@ -5595,7 +6146,8 @@ "bad-request-params": "リクエストパラメータが不正です", "item-not-found": "項目が見つかりません", "too-many-requests": "リクエストが多すぎます", - "too-many-updates": "更新が多すぎます" + "too-many-updates": "更新が多すぎます", + "entities-limit-exceeded": "エンティティ上限を超えました" }, "tenant": { "tenant": "テナント", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "計算フィールドあたりの引数最大数", "max-arguments-per-cf-range": "計算フィールドあたりの引数最大数は負にできません", "max-arguments-per-cf-required": "計算フィールドあたりの引数最大数は必須です", + "max-related-level-per-argument": "「関連エンティティ」引数あたりの最大リレーションレベル", + "max-related-level-per-argument-range": "「関連エンティティ」引数あたりのリレーションレベルの最大値は '1' 未満にできません", + "max-related-level-per-argument-required": "「関連エンティティ」引数あたりのリレーションレベルの最大値は必須です", + "min-allowed-scheduled-update-interval": "「関連エンティティ」引数の許可される最小更新間隔(秒)", + "min-allowed-scheduled-update-interval-range": "許可される最小更新間隔の最小値は負の値にできません", + "min-allowed-deduplication-interval": "許可される最小重複排除間隔(秒)", + "min-allowed-deduplication-interval-range": "許可される最小重複排除間隔の値は負の値にできません", + "min-allowed-deduplication-interval-required": "許可される最小重複排除間隔は必須です", + "intermediate-aggregation-interval": "中間集計間隔(秒)", + "intermediate-aggregation-interval-range": "中間集計間隔の値は '1' 未満にできません", + "intermediate-aggregation-interval-required": "中間集計間隔は必須です", + "reevaluation-check-interval": "再評価チェック間隔(秒)", + "reevaluation-check-interval-range": "再評価チェック間隔の値は '1' 未満にできません", + "reevaluation-check-interval-required": "再評価チェック間隔は必須です", + "alarms-reevaluation-interval": "アラーム再評価間隔(秒)", + "alarms-reevaluation-interval-range": "アラーム再評価間隔の値は '1' 未満にできません", + "alarms-reevaluation-interval-required": "アラーム再評価間隔は必須です", + "min-allowed-aggregation-interval": "許可される最小集計間隔(秒)", + "min-allowed-aggregation-interval-range": "許可される最小集計間隔の値は負の値にできません", + "min-allowed-aggregation-interval-required": "許可される最小集計間隔は必須です", + "min-allowed-scheduled-update-interval-required": "許可される最小更新間隔の最小値は必須です", "max-state-size": "状態の最大サイズ(KB)", "max-state-size-range": "状態の最大サイズ(KB)は負にできません", "max-state-size-required": "状態の最大サイズ(KB)は必須です", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "通常ユーザーあたりのサブスクリプション最大数", "ws-limit-max-subscriptions-per-public-user": "パブリックユーザーあたりのサブスクリプション最大数", "ws-limit-updates-per-session": "セッションあたりの WS 更新", + "relation-search-entity-limit": "リレーション検索エンティティ上限", + "relation-search-entity-limit-hint": "リレーションパスの最終レベルで解決されるエンティティ数を制限します。「関連エンティティ」引数および伝播フィールドに適用されます。", + "relation-search-entity-limit-required": "リレーション検索エンティティ上限は必須です", + "relation-search-entity-limit-range": "リレーション検索エンティティ上限は '1' 未満にできません", "rate-limits": { "add-limit": "制限を追加", "and-also-less-than": "かつ次より小さい", @@ -5993,7 +6570,9 @@ "default-agg-interval": "デフォルトのグルーピング間隔", "edit-intervals-list-hint": "利用可能な間隔オプションのリストを指定できます。", "edit-grouping-intervals-list-hint": "グルーピング間隔リストとデフォルトのグルーピング間隔を設定できます。", - "all": "すべて" + "all": "すべて", + "save-current-settings-as-default": "現在の設定を既定の時間ウィンドウとして保存", + "hide-option-from-end-users": "エンドユーザーにオプションを表示しない" }, "tooltip": { "trigger": "トリガー", @@ -6648,6 +7227,7 @@ "export-attributes": "属性をエクスポート", "export-credentials": "認証情報をエクスポート", "export-calculated-fields": "計算フィールドをエクスポート", + "export-alarm-rules": "アラームルールをエクスポート", "entity-versions": "エンティティバージョン", "versions": "バージョン", "created-time": "作成時間", @@ -6665,6 +7245,7 @@ "load-attributes": "属性を読み込む", "load-credentials": "認証情報を読み込む", "load-calculated-fields": "計算フィールドを読み込む", + "load-alarm-rules": "アラームルールを読み込み", "compare-with-current": "現在のバージョンと比較", "diff-entity-with-version": "エンティティバージョン '{{versionName}}' と差分を表示", "previous-difference": "前の差分", @@ -6874,7 +7455,23 @@ "scan-qr-code": "QRコードをスキャン", "make-phone-call": "電話をかける", "get-location": "電話の位置を取得", - "take-screenshot": "スクリーンショットを撮る" + "take-screenshot": "スクリーンショットを撮る", + "handle-provision-success-function": "プロビジョニング成功処理関数", + "get-location-function": "位置情報取得関数", + "process-launch-result-function": "起動結果処理関数", + "get-phone-number-function": "電話番号取得関数", + "process-image-function": "画像処理関数", + "process-qr-code-function": "QR コード処理関数", + "process-location-function": "位置情報処理関数", + "handle-empty-result-function": "空結果処理関数", + "handle-error-function": "エラー処理関数", + "handle-non-mobile-fallback-function": "非モバイルフォールバック処理関数", + "save-to-gallery": "'画像ギャラリー'に保存", + "provision-type": "プロビジョニングタイプ", + "auto": "自動", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "カスタムアクション機能", "custom-pretty-function": "カスタムアクション(HTMLテンプレート付き)機能", @@ -6883,7 +7480,8 @@ "marker": "マーカー", "polygon": "ポリゴン", "rectangle": "長方形", - "circle": "円" + "circle": "円", + "polyline": "ポリライン" }, "place-map-item": "マップアイテムを配置", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "ポリゴンの描画を続ける", "finish-draw-polygon": "ポリゴンを描き終わる", "start-draw-circle": "円を描き始める", - "finish-draw-circle": "円を描き終わる" + "finish-draw-circle": "円を描き終わる", + "start-draw-polyline": "ポリラインの描画を開始", + "finish-draw-polyline": "ポリラインの描画を完了" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "アニメーションの遅延を更新" }, "chart-axis": { + "limit": "上限", + "source": "ソース", + "key-value": "キー / 値", + "value-required": "値は必須です。", + "entity-key-required": "エンティティキーは必須です。", + "key-required": "キーは必須です。", + "scale-limits": "スケール上限/下限", + "scale-appearance": "スケール表示", "scale": "スケール", "scale-min": "最小", "scale-max": "最大", @@ -8005,7 +8613,10 @@ "add-radio-option": "ラジオオプションを追加", "radio-label-position": "ラベル位置", "radio-label-position-before": "前", - "radio-label-position-after": "後" + "radio-label-position-after": "後", + "save-image": "画像を保存", + "save-to-gallery": "撮影した画像を画像ギャラリーに自動保存", + "public-image": "画像を未認証のユーザーにも公開します" }, "invalid-qr-code-text": "QRコードの入力テキストが無効です。入力は文字列タイプである必要があります", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "旅行", "markers": "マーカー", "polygons": "ポリゴン", - "circles": "円" + "circles": "円", + "polylines": "ポリライン" }, "data-layer": { "source": "ソース", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "'{{entityName}}'の円: 描画を終了して保存するにはクリック", "finish-circle-hint": "円: 描画を終了するにはクリック" }, + "polyline": { + "polyline-key": "ポリラインキー", + "polyline-key-required": "ポリラインキーは必須です", + "no-polylines": "ポリラインが設定されていません", + "add-polylines": "ポリラインを追加", + "polyline-configuration": "ポリライン設定", + "remove-polyline": "ポリラインを削除", + "edit": "ポリラインを編集", + "cut": "ポリライン領域を切り取り", + "rotate": "ポリラインを回転", + "remove-polyline-for": "'{{entityName}}' のポリラインを削除", + "draw-polyline": "ポリラインを描画", + "polyline-place-first-point-hint-with-entity": "'{{entityName}}' のポリライン: クリックして最初のポイントを配置", + "polyline-place-first-point-hint": "ポリライン: クリックして最初のポイントを配置", + "finish-polyline-hint-with-entity": "'{{entityName}}' のポリライン: クリックして描画を完了", + "finish-polyline-hint": "ポリライン: クリックして描画を完了", + "polyline-place-first-point-cut-hint": "クリックして最初のポイントを配置", + "finish-polyline-cut-hint": "最初のマーカーをクリックして完了し保存" + }, "select-entity": "エンティティを選択", "select-entity-hint": "ヒント: 選択後、マップをクリックして位置を設定します" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "隠れたセルボタンアクションの代わりに空白を表示", "dont-reserve-space-hidden-action": "隠れたアクションボタンのためにスペースを予約しない", "display-timestamp": "タイムスタンプ", + "timestamp-column-name": "タイムスタンプ", "display-pagination": "ページネーションを表示", "default-page-size": "デフォルトのページサイズ", "page-step-settings": "ページステップ設定", @@ -8999,7 +9631,9 @@ "alarm-column-error": "少なくとも1つのアラーム列を指定する必要があります", "table-tabs": "テーブルタブ", "show-cell-actions-menu-mobile": "モバイルモードでセルアクションのドロップダウンメニューを表示", - "disable-sorting": "ソートを無効にする" + "disable-sorting": "ソートを無効にする", + "sort-by": "タブの並べ替え基準", + "sort-timestamp-option": "作成日時" }, "latest-chart": { "total": "合計", @@ -9491,11 +10125,28 @@ "content": "

    エンドユーザー用のダッシュボードを作成することにより、顧客ユーザーは自分のデバイスのみを表示でき、他の顧客のデータは非表示にされます。

    作成方法については、以下のドキュメントを参照してください:

    " } } + }, + "api-usage": { + "api-usage": "API 使用状況", + "label": "ラベル", + "state-name": "状態名", + "status": "ステータス", + "status-required": "ステータスは必須です。", + "limit": "最大上限", + "limit-required": "最大上限は必須です。", + "current-number": "現在の数", + "current-number-required": "現在の数は必須です。", + "add-key": "キーを追加", + "no-key": "キーがありません", + "delete-key": "キーを削除", + "target-dashboard-state": "対象ダッシュボード状態", + "go-to-main-state": "既定ビューに移動" } }, "icon": { "icon": "アイコン", "icons": "アイコン", + "custom": "カスタム", "select-icon": "アイコンを選択", "material-icons": "マテリアルアイコン", "show-all": "すべてのアイコンを表示", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "の" }, "language": { + "auto": "自動", "language": "言語" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json index ae8ce2b943..996e5b86c5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json @@ -1250,7 +1250,6 @@ "create-password": "პაროლის შექმნა", "passwords-mismatch-error": "პაროლები არ ემთხვევა", "password-again": "შეიყვანეთ პაროლი თავიდა", - "sign-in": "სისტემაში შესვლა", "username": "მომხმარებლის სახელი", "remember-me": "დამახსოვრება", "forgot-password": " დაგავიწყდა პაროლი?", diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index a122a04ee7..0351963ed9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -10,11 +10,16 @@ "permission-denied": "권한이 없습니다", "permission-denied-text": "이 작업을 수행할 권한이 없습니다!" }, + "account": { + "account": "계정", + "notification-settings": "알림 설정" + }, "action": { - "activate": "활설화", + "activate": "활성화", "suspend": "비활성화", "save": "저장", "saveAs": "다른 이름으로 저장", + "move": "이동", "cancel": "취소", "ok": "확인", "delete": "삭제", @@ -35,6 +40,7 @@ "edit-mode": "수정 모드", "enter-edit-mode": "수정 모드 진입", "decline-changes": "변경사항 포기", + "decline": "취소", "close": "닫기", "back": "뒤로", "run": "실행", @@ -55,10 +61,29 @@ "continue": "계속", "discard-changes": "변경 취소", "download": "다운로드", + "next": "다음", "next-with-label": "다음: {{label}}", "read-more": "더보기", "hide": "숨기기", - "done": "완료" + "test": "테스트", + "done": "완료", + "print": "인쇄", + "restore": "복원", + "confirm": "확인", + "more": "더보기", + "less": "줄이기", + "skip": "건너뛰기", + "send": "보내기", + "reset": "초기화", + "show-more": "더보기", + "dont-show-again": "다시 표시하지 않음", + "see-documentation": "문서 보기", + "see-debug-events": "디버그 이벤트 보기", + "clear": "지우기", + "upload": "업로드", + "delete-anyway": "무시하고 삭제", + "delete-selected": "선택 삭제", + "set": "설정" }, "aggregation": { "aggregation": "집합", @@ -73,8 +98,11 @@ "none": "없음" }, "admin": { + "settings": "설정", "general": "일반", "general-settings": "일반 설정", + "home-settings": "홈 설정", + "home": "홈", "outgoing-mail": "메일 전송", "outgoing-mail-settings": "메일 전송 설정", "system-settings": "시스템 설정", @@ -83,6 +111,23 @@ "base-url-required": "기본 URL을 입력해야 합니다.", "prohibit-different-url": "클라이언트 요청 헤더로부터 호스트 이름 사용을 제한", "prohibit-different-url-hint": "이 설정은 프로덕션 환경에서 활성화되어야 합니다. 비활성화되면 보안 문제가 발생할 수 있습니다.", + "device-connectivity": { + "device-connectivity": "장치 연결", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPS", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "호스트 또는 포트 필드가 비어 있으면 기본 프로토콜 값이 사용됩니다.", + "host": "호스트", + "port": "포트", + "port-pattern": "포트는 양의 정수여야 합니다.", + "port-range": "포트를 1~65535 범위 내에서 입력하세요." + }, "mail-from": "보내는 사람", "mail-from-required": "보내는 사람을 입력해야 합니다.", "smtp-protocol": "SMTP 프로토콜", @@ -95,7 +140,7 @@ "timeout-required": "제한시간이 입력되지 않았습니다.", "timeout-invalid": "제한시간이 올바르게 입력되지 않았습니다.", "enable-tls": "TLS 사용", - "tls-version" : "TLS 버전", + "tls-version": "TLS 버전", "enable-proxy": "프록시 사용", "proxy-host": "프록시 호스트", "proxy-host-required": "프록시 호스트를 입력하세요.", @@ -104,6 +149,7 @@ "proxy-port-range": "프록시 포트를 1~65535 범위 내에서 입력하세요.", "proxy-user": "프록시 사용자", "proxy-password": "프록시 비밀번호", + "change-password": "비밀번호 변경", "send-test-mail": "테스트 메일 보내기", "sms-provider": "SMS 제공자", "sms-provider-settings": "SMS 제공자 설정", @@ -111,18 +157,21 @@ "sms-provider-type-required": "SMS 제공자 유형을 입력하세요.", "sms-provider-type-aws-sns": "Amazon SNS", "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", "aws-access-key-id": "AWS 액세스 키 ID", "aws-access-key-id-required": "AWS 액세스 ID를 입력하세요.", "aws-secret-access-key": "AWS 보안 액세스 키", "aws-secret-access-key-required": "AWS 보안 액세스 키를 입력하세요", "aws-region": "AWS 지역", "aws-region-required": "AWS 지역을 입력하세요", - "number-from": "Phone Number From", - "number-from-required": "Phone Number From을(를) 입력하세요.", - "number-to": "Phone Number To", - "number-to-required": "Phone Number To을(를) 입력하세요.", - "phone-number-hint": "Phone Number in E.164 format, ex. +19995550123", - "phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.", + "number-from": "발신 전화번호", + "number-from-required": "발신 전화번호를 입력하세요.", + "number-to": "수신 전화번호", + "number-to-required": "수신 전화번호를 입력하세요.", + "phone-number-hint": "E.164 형식의 전화번호 (예: +821012345678)", + "phone-number-hint-twilio": "전화번호는 E.164 형식/전화번호 SID/메시징 서비스 SID이어야 합니다. 예: +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "잘못된 전화번호 형식입니다. E.164 형식을 사용하세요 (예: +821012345678).", + "phone-number-pattern-twilio": "잘못된 전화번호입니다. E.164 형식/전화번호 SID/메시징 서비스 SID이어야 합니다. 예: +19995550123/PNXXX/MGXXX", "sms-message": "SMS 메시지", "sms-message-required": "SMS 메시지를 입력하세요.", "sms-message-max-length": "SMS 메시지는 1600자를 넘을 수 없습니다.", @@ -137,6 +186,9 @@ "minimum-password-length": "최소 비밀번호 길이", "minimum-password-length-required": "최소 비밀번호 길이를 입력하세요", "minimum-password-length-range": "최소 비밀번호 길이를 5~50 범위 내에서 입력하세요", + "maximum-password-length": "최대 비밀번호 길이", + "maximum-password-length-min": "최대 비밀번호 길이는 최소 6자 이상이어야 합니다.", + "maximum-password-length-less-min": "최대 비밀번호 길이는 최소 길이보다 커야 합니다.", "minimum-uppercase-letters": "최소 대문자 수", "minimum-uppercase-letters-range": "최소 대문자 수는 음수가 될 수 없습니다", "minimum-lowercase-letters": "최소 소문자 수", @@ -149,83 +201,372 @@ "password-expiration-period-days-range": "비밀번호 유효 주기는 음수가 될 수 없습니다", "password-reuse-frequency-days": "비밀번호 재사용 주기(일)", "password-reuse-frequency-days-range": "비밀번호 재사용 주기는 음수가 될 수 없습니다", + "allow-whitespace": "공백 문자 허용", + "force-reset-password-if-no-valid": "유효하지 않은 경우 비밀번호 재설정 강제", + "force-reset-password-if-no-valid-hint": "이 기능을 활성화할 때 주의하십시오. 유효하지 않은 비밀번호를 사용하는 사용자는 이메일을 통해 비밀번호를 재설정해야 합니다.", "general-policy": "일반 정책", "max-failed-login-attempts": "계정이 잠기기 전까지 허용되는 최대 로그인 실패 시도 수", "minimum-max-failed-login-attempts-range": "최대 로그인 실패 시도 수는 음수가 될 수 없습니다", "user-lockout-notification-email": "계정이 잠길 경우 이메일로 안내 발송", + "user-activation-token-ttl": "사용자 활성화 링크 TTL (시간)", + "user-activation-token-ttl-range": "사용자 활성화 링크 TTL은 1~24시간 범위여야 합니다.", + "password-reset-token-ttl": "비밀번호 재설정 링크 TTL (시간)", + "password-reset-token-ttl-range": "비밀번호 재설정 링크 TTL은 1~24시간 범위여야 합니다.", + "mobile-secret-key-length": "모바일 비밀 키 길이", + "mobile-secret-key-length-range": "모바일 비밀 키 길이는 양수여야 합니다.", "domain-name": "도메인 이름", "domain-name-unique": "도메인 이름과 프로토콜은 중복되지 않아야 합니다.", + "domain-name-max-length": "도메인 이름은 256자 미만이어야 합니다.", "error-verification-url": "도메인 이름은 '/'이나 ':'와 같은 문자를 포함할 수 없습니다. 예: thingsboard.io", + "connection-settings": "연결 설정", "oauth2": { - "access-token-uri": "액세스 토큰 URI", - "access-token-uri-required": "Access token URI를 입력하세요.", - "activate-user": "사용자 활성화", - "add-domain": "도메인 추가", - "delete-domain": "도메인 삭제", - "add-provider": "제공자 추가", - "delete-provider": "제공자 삭제", - "allow-user-creation": "사용자 생성 허용", - "always-fullscreen": "항상 최대화면으로", - "authorization-uri": "Authorization URI", - "authorization-uri-required": "Authorization URI를 입력하세요.", - "client-authentication-method": "클라이언트 authentication 방법", - "client-id": "Client ID", - "client-id-required": "Client ID를 입력하세요.", - "client-secret": "Client secret", - "client-secret-required": "Client secret을(를) 입력하세요.", - "custom-setting": "사용자 정의 설정", - "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 속성 키", - "email-attribute-key-required": "Email attribute key을(를) 입력하세요.", - "first-name-attribute-key": "이름 속성 키", - "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을(를) 입력하세요.", - "login-provider": "로그인 제공자", - "mapper": "Mapper", - "new-domain": "새로운 도메인", - "oauth2": "OAuth2", - "redirect-uri-template": "Redirect URI 탬플릿", - "copy-redirect-uri": "리디렉션 URI 복사", - "registration-id": "Registration ID", - "registration-id-required": "Registration ID을(를) 입력하세요.", - "registration-id-unique": "Registration ID는 시스템 상에서 중복될 수 없습니다.", - "scope": "Scope", - "scope-required": "Scope을(를) 입력하세요.", - "tenant-name-pattern": "테넌트 이름 규칙", - "tenant-name-pattern-required": "테넌트 이름 규칙을 입력하세요.", - "tenant-name-strategy": "Tenant name strategy", - "type": "Mapper type", - "uri-pattern-error": "유효하지 않은 URI 형식입니다.", - "url": "URL", - "url-pattern": "유효하지 않은 URL 형식입니다.", - "url-required": "URL을(를) 입력하세요.", - "user-info-uri": "User info URI", - "user-info-uri-required": "사용자 정보 URI을(를) 입력하세요.", - "user-name-attribute-name": "사용자 이름 속성 키", - "user-name-attribute-name-required": "사용자 이름 속성 키를 입력하세요", - "protocol": "Protocol", - "domain-schema-http": "HTTP", - "domain-schema-https": "HTTPS", - "domain-schema-mixed": "HTTP+HTTPS", - "enable": "OAuth2 설정 사용" - } + "access-token-uri": "액세스 토큰 URI", + "access-token-uri-required": "액세스 토큰 URI를 입력하세요.", + "activate-user": "사용자 활성화", + "add-domain": "도메인 추가", + "delete-domain": "도메인 삭제", + "add-provider": "제공자 추가", + "delete-provider": "제공자 삭제", + "allow-user-creation": "사용자 생성 허용", + "always-fullscreen": "항상 최대화면으로", + "authorization-uri": "인증 URI", + "authorization-uri-required": "인증 URI를 입력하세요.", + "add-client": "OAuth 2.0 클라이언트 추가", + "client-details": "OAuth 2.0 클라이언트 세부 정보", + "client": "OAuth 2.0 클라이언트", + "clients": "OAuth 2.0 클라이언트", + "no-oauth2-clients": "OAuth 2.0 클라이언트가 없습니다", + "search-oauth2-clients": "OAuth 2.0 클라이언트 검색", + "delete-client-title": "OAuth 2.0 클라이언트 '{{clientName}}'을(를) 삭제하시겠습니까?", + "delete-client-text": "주의: 확인 후 클라이언트와 모든 관련 데이터를 사용할 수 없게 됩니다.", + "delete-mobile-app-title": "모바일 애플리케이션 '{{applicationName}}'을(를) 삭제하시겠습니까?", + "delete-mobile-app-text": "주의: 확인 후 모바일 애플리케이션과 모든 관련 데이터를 사용할 수 없게 됩니다.", + "title": "제목", + "client-title-required": "제목은 필수 항목입니다", + "client-title-max-length": "제목은 100자 미만이어야 합니다.", + "advanced-settings": "고급 설정", + "domain-details": "도메인 세부 정보", + "no-domains": "도메인이 없습니다", + "search-domains": "도메인 검색", + "mobile-app-details": "모바일 애플리케이션 세부 정보", + "add-mobile-app": "모바일 애플리케이션 추가", + "no-mobile-apps": "모바일 애플리케이션이 없습니다", + "search-mobile-apps": "모바일 애플리케이션 검색", + "send-token": "토큰 전송", + "create-new": "새로 만들기", + "client-authentication-method": "클라이언트 인증 방법", + "client-id": "클라이언트 ID", + "client-id-required": "클라이언트 ID를 입력하세요.", + "client-id-max-length": "클라이언트 ID는 256자 미만이어야 합니다.", + "client-secret": "클라이언트 비밀키", + "client-secret-required": "클라이언트 비밀키를 입력하세요.", + "client-secret-max-length": "클라이언트 비밀키는 2049자 미만이어야 합니다.", + "custom-setting": "사용자 정의 설정", + "customer-name-pattern": "고객 이름 패턴", + "customer-name-pattern-max-length": "고객 이름 패턴은 256자 미만이어야 합니다.", + "default-dashboard-name": "기본 대시보드 이름", + "default-dashboard-name-max-length": "기본 대시보드 이름은 256자 미만이어야 합니다.", + "delete-domain-text": "주의: 확인 후 도메인과 모든 공급자 데이터를 사용할 수 없게 됩니다.", + "delete-domain-title": "'{{domainName}}' 도메인을 삭제하시겠습니까?", + "delete-registration-text": "주의: 확인 후 공급자 데이터를 사용할 수 없게 됩니다.", + "delete-registration-title": "'{{name}}' 제공자를 삭제하시겠습니까?", + "email-attribute-key": "이메일 속성 키", + "email-attribute-key-required": "이메일 속성 키를 입력하세요.", + "email-attribute-key-max-length": "이메일 속성 키는 32자 미만이어야 합니다.", + "first-name-attribute-key": "이름 속성 키", + "first-name-attribute-key-max-length": "이름 속성 키는 32자 미만이어야 합니다.", + "general": "일반", + "jwk-set-uri": "JSON 웹 키 URI", + "last-name-attribute-key": "성 속성 키", + "last-name-attribute-key-max-length": "성 속성 키는 32자 미만이어야 합니다.", + "login-button-icon": "로그인 버튼 아이콘", + "login-button-label": "제공자 레이블", + "login-button-label-placeholder": "$(제공자 레이블)로 로그인", + "login-button-label-required": "제공자 레이블을 입력하세요.", + "login-provider": "로그인 제공자", + "mapper": "매퍼", + "new-domain": "새로운 도메인", + "oauth2": "OAuth2", + "password-max-length": "비밀번호는 256자 미만이어야 합니다.", + "redirect-uri-template": "리디렉션 URI 탬플릿", + "copy-redirect-uri": "리디렉션 URI 복사", + "registration-id": "등록 ID", + "registration-id-required": "등록 ID를 입력하세요.", + "registration-id-unique": "등록 ID는 시스템 상에서 중복될 수 없습니다.", + "scope": "범위", + "scope-required": "범위를 입력하세요.", + "tenant-name-pattern": "테넌트 이름 규칙", + "tenant-name-pattern-required": "테넌트 이름 규칙을 입력하세요.", + "tenant-name-pattern-max-length": "테넌트 이름 패턴은 256자 미만이어야 합니다.", + "tenant-name-strategy": "테넌트 이름 전략", + "type": "매퍼 유형", + "uri-pattern-error": "유효하지 않은 URI 형식입니다.", + "url": "URL", + "url-pattern": "유효하지 않은 URL 형식입니다.", + "url-required": "URL을(를) 입력하세요.", + "url-max-length": "URL은 256자 미만이어야 합니다.", + "user-info-uri": "사용자 정보 URI", + "user-info-uri-required": "사용자 정보 URI을(를) 입력하세요.", + "username-max-length": "사용자 이름은 256자 미만이어야 합니다.", + "user-name-attribute-name": "사용자 이름 속성 키", + "user-name-attribute-name-required": "사용자 이름 속성 키를 입력하세요", + "protocol": "프로토콜", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "OAuth2 설정 사용", + "disable": "OAuth 2.0 설정 비활성화", + "edge": "엣지에 전파", + "edge-enable": "엣지 전파 활성화", + "edge-disable": "엣지 전파 비활성화", + "domains": "도메인", + "mobile-apps": "모바일 애플리케이션", + "mobile-package": "애플리케이션 패키지", + "mobile-package-placeholder": "예: my.example.app", + "mobile-package-hint": "Android: 고유한 애플리케이션 ID. iOS: 제품 번들 식별자.", + "mobile-package-unique": "애플리케이션 패키지는 고유해야 합니다.", + "mobile-package-required": "애플리케이션 패키지가 필요합니다.", + "mobile-package-max-length": "애플리케이션 패키지는 256자 미만이어야 합니다", + "mobile-package-spaces": "애플리케이션 패키지에는 공백이 포함되지 않아야 합니다.", + "mobile-app-secret": "애플리케이션 비밀키", + "mobile-app-secret-hint": "최소 512비트 데이터를 나타내는 Base64 인코딩 문자열.", + "mobile-app-secret-required": "애플리케이션 비밀키는 필수 항목입니다.", + "mobile-app-secret-min-length": "애플리케이션 비밀키는 최소 512비트여야 합니다.", + "mobile-app-secret-base64": "애플리케이션 비밀키는 Base64 형식이어야 합니다.", + "invalid-mobile-app-secret": "애플리케이션 비밀키는 영숫자만 포함해야 하며", + "copy-mobile-app-secret": "애플리케이션 비밀키 복사", + "delete-mobile-app": "애플리케이션 정보 삭제", + "providers": "제공자", + "platform-web": "웹", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "모든 플랫폼", + "smtp-provider": "SMTP 제공자", + "allowed-platforms": "허용된 플랫폼", + "authentication": "인증", + "basic": "기본", + "provider": "제공자", + "redirect-url": "리디렉션 URI", + "domain-name": "도메인 이름", + "domain-name-required": "도메인 이름은 필수 항목입니다.", + "redirect-url-template": "리디렉션 URI 탬플릿", + "microsoft-tenant-id": "디렉터리 (테넌트) ID", + "microsoft-tenant-id-required": "디렉터리 (테넌트) ID는 필수 항목입니다.", + "token-uri": "토큰 URI", + "token-uri-required": "토큰 URI는 필수 항목입니다.", + "redirect-uri": "리디렉션 URI", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "사용자 지정", + "generate-access-token": "액세스 토큰 생성", + "update-access-token": "액세스 토큰 업데이트", + "access-token-status": "액세스 토큰 상태:", + "token-status-generated": "생성됨", + "token-status-not-generated": "생성되지 않음" + }, + "smpp-provider": { + "smpp-version": "SMPP 버전", + "smpp-host": "SMPP 호스트", + "smpp-host-required": "SMPP 호스트는 필수 항목입니다.", + "smpp-port": "SMPP 포트", + "smpp-port-required": "SMPP 포트는 필수 항목입니다.", + "system-id": "시스템 ID", + "system-id-required": "시스템 ID는 필수 항목입니다.", + "password": "비밀번호", + "password-required": "비밀번호는 필수 항목입니다.", + "type-settings": "유형 설정", + "source-settings": "소스 설정", + "destination-settings": "대상 설정", + "additional-settings": "추가 설정", + "system-type": "시스템 유형", + "bind-type": "바인드 유형", + "service-type": "서비스 유형", + "source-address": "소스 주소", + "source-ton": "소스 TON", + "source-npi": "소스 NPI", + "destination-ton": "대상 TON (번호 유형)", + "destination-npi": "대상 NPI (번호 계획 식별)", + "address-range": "주소 범위", + "coding-scheme": "코딩 체계", + "bind-type-tx": "송신기", + "bind-type-rx": "수신기", + "bind-type-trx": "송수신기", + "ton-unknown": "알 수 없음", + "ton-international": "국제", + "ton-national": "국내", + "ton-network-specific": "네트워크 특정", + "ton-subscriber-number": "가입자 번호", + "ton-alphanumeric": "영숫자", + "ton-abbreviated": "약어", + "npi-unknown": "0 - 알 수 없음", + "npi-isdn": "1 - ISDN/전화 번호 계획 (E163/E164)", + "npi-data-numbering-plan": "3 - 데이터 번호 계획 (X.121)", + "npi-telex-numbering-plan": "4 - 텔렉스 번호 계획 (F.69)", + "npi-land-mobile": "6 - 육상 이동통신 (E.212)", + "npi-national-numbering-plan": "8 - 국가 번호 계획", + "npi-private-numbering-plan": "9 - 사설 번호 계획", + "npi-ermes-numbering-plan": "10 - ERMES 번호 계획 (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - 인터넷 (IP)", + "npi-wap-client-id": "18 - WAP 클라이언트 ID (WAP 포럼에서 정의 예정)", + "scheme-smsc": "0 - SMSC 기본 알파벳 (단문 및 장문 코드용 ASCII, 무료용 GSM)", + "scheme-ia5": "1 - IA5 (단문 및 장문 코드용 ASCII, 무료용 Latin 9 (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - 옥텟 미지정 (8비트 바이너리)", + "scheme-latin-1": "3 - 라틴 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - 옥텟 미지정 (8비트 바이너리)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - 키릴 문자 (ISO-8859-5)", + "scheme-latin-hebrew": "7 - 라틴/히브리어 (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - 픽토그램 인코딩", + "scheme-music-codes": "10 - 음악 코드 (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - 확장 칸지 JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - 한국어 그래픽 문자 세트 (KS C 5601/KS X 1001)" + }, + "queue-select-name": "큐 이름 선택", + "queue-name": "이름", + "queue-name-required": "큐 이름은 필수 항목입니다!", + "queues": "큐", + "queue-partitions": "파티션", + "queue-submit-strategy": "제출 전략", + "queue-processing-strategy": "처리 전략", + "queue-configuration": "큐 구성", + "repository-settings": "저장소 설정", + "repository": "저장소", + "repository-url": "저장소 URL", + "repository-url-required": "저장소 URL은 필수 항목입니다.", + "default-branch": "기본 브랜치 이름", + "repository-read-only": "읽기 전용", + "show-merge-commits": "병합 커밋 표시", + "authentication-settings": "인증 설정", + "auth-method": "인증 방법", + "auth-method-username-password": "비밀번호 / 액세스 토큰", + "auth-method-username-password-hint": "GitHub 사용자는 액세스 토큰을 사용해야 합니다", + "auth-method-private-key": "개인 키", + "password-access-token": "비밀번호 / 액세스 토큰", + "change-password-access-token": "비밀번호 / 액세스 토큰 변경", + "private-key": "개인 키", + "drop-private-key-file-or": "개인 키 파일을 끌어다 놓거나", + "passphrase": "암호 문구", + "enter-passphrase": "암호 문구 입력", + "change-passphrase": "암호 문구 변경", + "check-access": "액세스 확인", + "check-repository-access-success": "저장소 액세스가 성공적으로 확인되었습니다!", + "delete-repository-settings-title": "저장소 설정을 삭제하시겠습니까?", + "delete-repository-settings-text": "주의: 확인 후 저장소 설정이 제거되고", + "auto-commit-settings": "자동 커밋 설정", + "auto-commit": "자동 커밋", + "auto-commit-entities": "자동 커밋 엔티티", + "no-auto-commit-entities-prompt": "자동 커밋에 구성된 엔티티가 없습니다", + "delete-auto-commit-settings-title": "자동 커밋 설정을 삭제하시겠습니까?", + "delete-auto-commit-settings-text": "주의: 확인 후에는 자동 커밋 설정이 삭제되며, 모든 엔티티의 자동 커밋 기능이 비활성화됩니다.", + "mobile-app": { + "mobile-app": "모바일 앱", + "mobile-app-qr-code-widget-settings": "모바일 앱 QR 코드 위젯 설정", + "applications": "애플리케이션", + "default": "기본값", + "custom": "사용자 지정", + "android": "Android", + "ios": "iOS", + "appearance": "모양", + "appearance-on-home-page": "홈 페이지 모양", + "enabled": "사용", + "disabled": "비활성화됨", + "badges": "배지", + "label": "라벨", + "label-required": "레이블은 필수 항목입니다", + "label-max-length": "레이블은 50자 이하여야 합니다.", + "right": "오른쪽", + "left": "왼쪽", + "set": "설정", + "preview": "미리보기", + "connect-mobile-app": "모바일 앱 연결", + "use-system-settings": "시스템 설정 사용" + }, + "2fa": { + "2fa": "2단계 인증", + "available-providers": "사용 가능한 제공자", + "available-providers-required": "최소 하나의 2FA 제공자를 구성해야 합니다.", + "issuer-name": "발급자 이름", + "issuer-name-required": "발급자 이름은 필수 항목입니다.", + "max-verification-failures-before-user-lockout": "사용자 잠금 전 최대 인증 실패 횟수", + "max-verification-failures-before-user-lockout-pattern": "최대 인증 실패 횟수는 양의 정수여야 합니다.", + "number-of-checking-attempts": "확인 시도 횟수", + "number-of-checking-attempts-pattern": "확인 시도 횟수는 양의 정수여야 합니다.", + "number-of-checking-attempts-required": "확인 시도 횟수는 필수 항목입니다.", + "number-of-codes": "코드 수", + "number-of-codes-pattern": "코드 수는 양의 정수여야 합니다.", + "number-of-codes-required": "코드 수는 필수 항목입니다.", + "provider": "제공자", + "retry-verification-code-period": "인증 코드 재시도 기간", + "retry-verification-code-period-pattern": "최소 기간은 5초입니다.", + "retry-verification-code-period-required": "인증 코드 재시도 기간은 필수 항목입니다.", + "total-allowed-time-for-verification": "인증 총 허용 시간", + "total-allowed-time-for-verification-pattern": "최소 총 허용 시간은 60초입니다.", + "total-allowed-time-for-verification-required": "총 허용 시간은 필수 항목입니다.", + "use-system-two-factor-auth-settings": "시스템 2단계 인증 설정 사용", + "verification-code-check-rate-limit": "인증 코드 확인 속도 제한", + "verification-code-lifetime": "인증 코드 수명", + "verification-code-lifetime-pattern": "인증 코드 수명은 양의 정수여야 합니다.", + "verification-code-lifetime-required": "인증 코드 수명은 필수 항목입니다.", + "verification-message-template": "인증 메시지 템플릿", + "verification-limitations": "인증 제한", + "verification-message-template-pattern": "인증 메시지에는 ${code} 패턴이 포함되어야 합니다.", + "verification-message-template-required": "인증 메시지 템플릿은 필수 항목입니다.", + "within-time": "시간 내", + "within-time-pattern": "시간은 양의 정수여야 합니다.", + "within-time-required": "시간은 필수 항목입니다.", + "force-2fa": "2단계 인증 강제", + "enforce-for": "강제 적용 대상" + }, + "jwt": { + "security-settings": "JWT 보안 설정", + "issuer-name": "발급자 이름", + "issuer-name-required": "발급자 이름은 필수 항목입니다.", + "signings-key": "서명 키", + "signings-key-hint": "최소 512비트 데이터를 나타내는 Base64 인코딩 문자열.", + "signings-key-required": "서명 키는 필수 항목입니다.", + "signings-key-min-length": "서명 키는 최소 512비트여야 합니다.", + "signings-key-base64": "서명 키는 Base64 형식이어야 합니다.", + "expiration-time": "토큰 만료 시간 (초)", + "expiration-time-required": "토큰 만료 시간은 필수 항목입니다.", + "expiration-time-max": "최대 허용 시간은 2147483647초 (68년)입니다.", + "expiration-time-min": "최소 시간은 60초 (1분)입니다.", + "refresh-expiration-time": "새로고침 토큰 만료 시간 (초)", + "refresh-expiration-time-required": "새로고침 토큰 만료 시간은 필수 항목입니다.", + "refresh-expiration-time-max": "최대 허용 시간은 2147483647초 (68년)입니다.", + "refresh-expiration-time-min": "최소 시간은 900초 (15분)입니다.", + "refresh-expiration-time-less-token": "새로고침 토큰 시간은 토큰 시간보다 커야 합니다.", + "generate-key": "키 생성", + "info-header": "모든 사용자가 다시 로그인해야 합니다", + "info-message": "JWT 서명 키 변경 시 발급된 모든 토큰이 무효화됩니다. 모든 사용자가" + }, + "resources": "리소스", + "notifications": "알림", + "notifications-settings": "알림 설정", + "slack-api-token": "Slack API 토큰", + "slack": "Slack", + "slack-settings": "Slack 설정", + "mobile-settings": "모바일 설정", + "firebase-service-account-file": "Firebase 서비스 계정 자격 증명 JSON 파일", + "select-firebase-service-account-file": "Firebase 서비스 계정 자격 증명 파일을 끌어다 놓거나", + "trendz": "Trendz", + "trendz-settings": "Trendz 설정", + "trendz-url": "Trendz URL", + "trendz-url-required": "Trendz URL은 필수 항목입니다", + "trendz-api-key": "Trendz API 키", + "trendz-enable": "Trendz 사용" }, "alarm": { "alarm": "알람", + "alarm-list": "알람 목록", "alarms": "알람", + "all-alarms": "모든 알람", "select-alarm": "알람 선택", "no-alarms-matching": "'{{entity}}'에 대한 알람이 존재하지 않습니다.", "alarm-required": "알람이 필요합니다", + "alarm-filter": "알람 필터", + "filter": "필터", "alarm-status": "알람 상태", "alarm-status-list": "알람 상태 목록", "any-status": "모든 상태", @@ -249,12 +590,26 @@ "originator": "만든 사람", "originator-type": "만든 사람 유형", "details": "상세 내역", + "originator-label": "발생자 라벨", + "assign": "할당", + "assignments": "할당", + "assignee": "담당자", + "assignee-id": "담당자 ID", + "assignee-first-name": "담당자 이름", + "assignee-last-name": "담당자 성", + "assignee-email": "담당자 이메일", + "unassigned": "할당 해제됨", + "user-deleted": "사용자 삭제됨", + "assignee-not-set": "모두", "status": "상태", "alarm-details": "알람 상세", "start-time": "시작 시각", + "assign-time": "할당 시간", "end-time": "마지막 시각", "ack-time": "확인 시간", "clear-time": "해제 시간", + "duration": "기간", + "alarm-severity": "알람 심각도", "alarm-severity-list": "알람 심각도 목록", "any-severity": "모든 심각도", "severity-critical": "심각한", @@ -264,6 +619,7 @@ "severity-indeterminate": "중간", "acknowledge": "확인", "clear": "지우기", + "delete": "삭제", "search": "알람 검색", "selected-alarms": "{ count, plural, =1 {1 개 알람} other {# 개 알람} }이 선택됨", "no-data": "표시할 데이터가 없습니다", @@ -274,67 +630,116 @@ "aknowledge-alarms-text": "{ count, plural, =1 {1 개 알람} other {# 개 알람} }의 알람을 수용하시겠습니까?", "aknowledge-alarm-title": "알람 확인", "aknowledge-alarm-text": "알람을 확인하시겠습니까??", + "selected-alarms-are-acknowledged": "선택된 알람은 이미 확인되었습니다", "clear-alarms-title": "{ count, plural, =1 {1 개 알람} other {# 개 알람} } 해제", "clear-alarms-text": "{ count, plural, =1 {1 개 알람} other {# 개 알람} }을 해제하시겠습니까?", "clear-alarm-title": "알람 해제", "clear-alarm-text": "알람을 해제하시겠습니까?", + "delete-alarms-title": "{ count, plural, =1 {1개 알람} other {#개 알람} } 삭제", + "delete-alarms-text": "{ count, plural, =1 {1개 알람} other {#개 알람} }을(를) 삭제하시겠습니까?", + "selected-alarms-are-cleared": "선택된 알람은 이미 해제되었습니다", "alarm-status-filter": "알람 상태 필터", - "alarm-filter": "알람 필터", + "alarm-filter-title": "알람 필터", + "assigned": "할당됨", + "filter-title": "필터", "max-count-load": "불러올 최대 알람 수를 0~무제한 범위 내에서 입력하세요", "max-count-load-required": "불러올 최대 알람 수를 입력하세요.", "max-count-load-error-min": "최소 값은 0입니다.", - "fetch-size": "Fetch size", - "fetch-size-required": "Fetch size를 입력하세요.", + "fetch-size": "페치 크기", + "fetch-size-required": "페치 크기를 입력하세요.", "fetch-size-error-min": "최소 값은 10입니다.", + "alarm-types": "알람 유형", "alarm-type-list": "알람 유형 목록", "any-type": "모든 유형", - "search-propagated-alarms": "전파된 알람 검색" + "assigned-to-current-user": "현재 사용자에게 할당됨", + "assigned-to-me": "나에게 할당됨", + "search-propagated-alarms": "전파된 알람 검색", + "comments": "알람 댓글", + "show-more": "더보기", + "additional-info": "추가 정보", + "alarm-type": "알람 유형", + "enter-alarm-type": "알람 유형 입력", + "no-alarm-types-matching": "'{{entitySubtype}}'과 일치하는 알람 유형이 없습니다.", + "alarm-type-list-empty": "선택된 알람 유형이 없습니다.", + "system-comments": { + "acked-by-user": "사용자 {{userName}}이(가) 알람을 확인했습니다", + "cleared-by-user": "사용자 {{userName}}이(가) 알람을 해제했습니다", + "assigned-to-user": "사용자 {{userName}}이(가) 알람을 사용자 {{assigneeName}}에게 할당했습니다", + "unassigned-to-user": "사용자 {{userName}}이(가) 알람 할당을 해제했습니다", + "unassigned-from-deleted-user": "사용자 {{userName}}이(가) 삭제되어 알람 할당이 해제되었습니다", + "comment-deleted": "사용자 {{userName}}이(가) 자신의 댓글을 삭제했습니다", + "severity-changed": "알람 심각도가 {{oldSeverity}}에서 {{newSeverity}}(으)로 업데이트되었습니다" + } + }, + "alarm-activity": { + "add": "댓글 추가...", + "alarm-comment": "알람 댓글", + "comments": "댓글", + "delete-alarm-comment": "이 댓글을 삭제하시겠습니까?", + "refresh": "새로고침", + "oldest-first": "오래된 순", + "newest-first": "최신 순", + "activity": "활동", + "export": "CSV로 내보내기", + "author": "작성자", + "created-date": "생성 날짜", + "edited-date": "수정 날짜", + "text": "텍스트", + "system": "시스템" }, "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": "개체 taken from dashboard state parameters", + "add": "별명 추가", + "edit": "별명 편집", + "name": "별명", + "name-required": "별명을 입력하세요.", + "duplicate-alias": "동일한 별명이 이미 존재합니다.", + "filter-type-single-entity": "단일 엔티티", + "filter-type-entity-list": "엔티티 목록", + "filter-type-entity-name": "엔티티 이름", + "filter-type-entity-type": "엔티티 유형", + "filter-type-state-entity": "대시보드 상태로 부터의 엔티티", + "filter-type-state-entity-description": "대시보드 상태 파라미터에서 가져온 엔티티", "filter-type-asset-type": "자산 유형", "filter-type-asset-type-description": "'{{assetTypes}}' 유형의 자산", "filter-type-asset-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{assetTypes}}' 유형의 자산", "filter-type-device-type": "장치 유형", "filter-type-device-type-description": "'{{deviceTypes}}' 유형의 장치", "filter-type-device-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{deviceTypes}}' 유형의 장치", - "filter-type-entity-view-type": "Entity View type", - "filter-type-entity-view-type-description": "Entity Views of type '{{entityViewTypes}}'", - "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewTypes}}' and with name starting with '{{prefix}}'", + "filter-type-entity-view-type": "엔티티 뷰 유형", + "filter-type-entity-view-type-description": "유형 '{{entityViewTypes}}'의 엔티티 뷰", + "filter-type-entity-view-type-and-name-description": "유형이 '{{entityViewTypes}}'이고 이름이 '{{prefix}}'로 시작하는 엔티티 뷰", + "filter-type-edge-type": "엣지 유형", + "filter-type-edge-type-description": "유형 '{{edgeTypes}}'의 엣지", + "filter-type-edge-type-and-name-description": "유형 '{{edgeTypes}}'이고 이름이 '{{prefix}}'로 시작하는 엣지", "filter-type-relations-query": "관계 질의", - "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-relations-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 {{entities}}", "filter-type-asset-search-query": "자산 검색 질의", - "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-asset-search-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 유형 {{assetTypes}}의 자산", "filter-type-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": "개체 보기 검색 질의", - "filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-device-search-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 유형 {{deviceTypes}}의 장치", + "filter-type-entity-view-search-query": "엔티티 뷰 검색 질의", + "filter-type-entity-view-search-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 유형 {{entityViewTypes}}의 엔티티 뷰", "filter-type-apiUsageState": "Api 사용량 상태", - "entity-filter": "개체 필터", - "resolve-multiple": "Resolve as multiple entities", + "filter-type-edge-search-query": "엣지 검색 쿼리", + "filter-type-edge-search-query-description": "유형 {{edgeTypes}}이(가) {{relationType}} 관계 {{direction}}을(를) 가진 엣지", + "entity-filter": "엔티티 필터", + "resolve-multiple": "여러 엔티티로 확인", + "resolve-multiple-hint": "필터링된 모든 엔티티의 데이터를 동시에 렌더링하도록 활성화합니다.\n비활성화할 경우, 위젯에는 선택된 엔티티의 데이터만 표시됩니다.", "filter-type": "필터 유형", "filter-type-required": "필터 유형을 입력하세요.", - "entity-filter-no-entity-matched": "지정된 필터에 해당되는 개체가 없습니다.", - "no-entity-filter-specified": "개체 필터가 지정되지 않음", - "root-state-entity": "루트로 대시보드 상태 개체를 사용", - "last-level-relation": "Fetch last level relation only", - "root-entity": "루트 개체", - "state-entity-parameter-name": "상태 개체 파라미터 이름", - "default-state-entity": "기본 상태 개체", + "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": "기본값", + "query-options": "쿼리 옵션", "max-relation-level": "최대 관계 수준", "unlimited-level": "제한 수준", - "state-entity": "대시보드 상태 개체", - "all-entities": "모든 개체", + "state-entity": "대시보드 상태 엔티티", + "all-entities": "모든 엔티티", "any-relation": "전체" }, "asset": { @@ -343,16 +748,17 @@ "management": "자산 관리", "view-assets": "자산 보기", "add": "자산 추가", - "assign-to-customer": "커스터머에게 자산 할당", - "assign-asset-to-customer": "자산을 커스터머에게 할당", - "assign-asset-to-customer-text": "커스터머에게 할당할 자산을 선택하세요", + "asset-type-max-length": "자산 유형은 256자 미만이어야 합니다.", + "assign-to-customer": "고객에게 자산 할당", + "assign-asset-to-customer": "자산을 고객에게 할당", + "assign-asset-to-customer-text": "고객에게 할당할 자산을 선택하세요", "no-assets-text": "아무 자산도 없습니다", - "assign-to-customer-text": "자산을 할당할 커스터머를 선택하세요", + "assign-to-customer-text": "자산을 할당할 고객를 선택하세요", "public": "공개", - "assignedToCustomer": "할당된 커스터머", + "assignedToCustomer": "할당된 고객", "make-public": "자산을 공개로 설정", "make-private": "자산을 비공개로 설정", - "unassign-from-customer": "커스터머 할당 해제", + "unassign-from-customer": "고객 할당 해제", "delete": "자산 삭제", "asset-public": "공개된 자산", "asset-type": "자산 유형", @@ -365,7 +771,10 @@ "asset-types": "자산 유형", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "label-max-length": "레이블은 256자 미만이어야 합니다.", "description": "설명", + "description-required": "설명은 필수 항목입니다.", "type": "유형", "type-required": "유형일 입력하세요.", "details": "상세 내역", @@ -373,10 +782,12 @@ "add-asset-text": "새로운 자산 추가", "asset-details": "자산 상세 내역", "assign-assets": "자산 할당", - "assign-assets-text": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 커스터머에게 할당", + "assign-assets-text": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 고객에게 할당", + "assign-asset-to-edge-title": "엣지에 자산 할당", + "assign-asset-to-edge-text": "엣지에 할당할 자산을 선택하세요", "delete-assets": "자산 삭제", "unassign-assets": "자산 할당 해제", - "unassign-assets-action-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 커스터머로부터 할당 해제", + "unassign-assets-action-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 고객로부터 할당 해제", "assign-new-asset": "새로운 자산 할당", "delete-asset-title": "자산 '{{assetName}}'을(를) 삭제하시겠습니까?", "delete-asset-text": "자산 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", @@ -388,37 +799,54 @@ "make-private-asset-title": "자산 '{{assetName}}'을(를) 비공개로 변경하시겠습니까?", "make-private-asset-text": "자산 및 관련 데이터가 비공개로 전환되면 다른 이들이 접근할 수 없게 됩니다.", "unassign-asset-title": "자산 '{{assetName}}'을 할당 해제 하시겠습니까?", - "unassign-asset-text": "자산이 할당 해제되면 커스터머가 접근할 수 없게 됩니다.", + "unassign-asset-text": "자산이 할당 해제되면 고객가 접근할 수 없게 됩니다.", "unassign-asset": "자산 할당 해제", "unassign-assets-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 할당 해제 하시겠습니까?", - "unassign-assets-text": "자산이 할당 해제되면 커스터머가 접근할 수 없게 됩니다.", + "unassign-assets-text": "자산이 할당 해제되면 고객가 접근할 수 없게 됩니다.", + "unassign-assets-from-edge": "엣지에서 자산 할당 해제", "copyId": "자산 ID 복사", "idCopiedMessage": "자산 ID가 클립보드로 복사되었습니다", "select-asset": "자산 선택", "no-assets-matching": "'{{entity}}'와 일치하는 자산이 없습니다.", "asset-required": "자산을 입력하세요", "name-starts-with": "다음으로 시작되는 자산: ", + "help-text": "필요에 따라 '%'를 사용하세요: '%asset_name_contains%', '%asset_name_ends', 'asset_s", "import": "자산 불러오기", "asset-file": "자산 파일", + "label": "라벨", "search": "자산 검색", - "selected-assets": "{ count, plural, =1 {1 개 자산} other {# 개 자산} } 선택됨", - "label": "라벨" + "assign-asset-to-edge": "엣지에 자산 할당", + "unassign-asset-from-edge": "자산 할당 해제", + "unassign-asset-from-edge-title": "자산 '{{assetName}}'을 할당 해제 하시겠습니까?", + "unassign-asset-from-edge-text": "확인 후 자산 할당이 해제되고", + "unassign-assets-from-edge-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 할당 해제 하시겠습니까?", + "unassign-assets-from-edge-text": "확인 후 선택한 모든 자산의 할당이 해제되고", + "selected-assets": "{ count, plural, =1 {1 개 자산} other {# 개 자산} } 선택됨" }, "attribute": { "attributes": "속성", "latest-telemetry": "최근 데이터", + "no-latest-telemetry": "최신 텔레메트리 없음", "attributes-scope": "장치 속성 범위", - "scope-telemetry": "원격 측정", + "scope-telemetry": "텔레메트리", "scope-latest-telemetry": "최근 데이터", "scope-client": "클라이언트 속성", "scope-server": "서버 속성", "scope-shared": "공유 속성", + "scope-client-short": "클라이언트", + "scope-server-short": "서버", + "scope-shared-short": "공유", + "scope-latest-short": "최신", + "scope-any": "전체", "add": "속성 추가", "key": "키", + "key-max-length": "키는 256자 미만이어야 합니다.", "last-update-time": "마지막 수정된 시간", "key-required": "속성 키를 입력하세요.", - "value": "Value", + "value": "값", "value-required": "속성 값을 입력하세요.", + "telemetry-key-required": "텔레메트리 키는 필수 항목입니다", + "telemetry-value-required": "텔레메트리 값은 필수 항목입니다", "delete-attributes-title": "{ count, plural, =1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", "delete-attributes": "속성 삭제", @@ -432,63 +860,179 @@ "selected-attributes": "{ count, plural, =1 {1 개 속성} other {# 개 속성} }이 선택됨", "selected-telemetry": "{ count, plural, =1 {1 개의 텔레메트리 단위} other {# 개의 텔레메트리 단위} }가 선택됨", "no-attributes-text": "아무 속성도 찾을 수 없습니다", - "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다." + "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다.", + "copy-key": "키 복사", + "add-telemetry": "텔레메트리 추가", + "copy-value": "값 복사", + "delete-timeseries": { + "start-time": "시작 시각", + "ends-on": "종료일", + "strategy": "전략", + "delete-strategy": "삭제 전략", + "all-data": "모든 데이터 삭제", + "all-data-except-latest-value": "최신 값을 제외한 모든 데이터 삭제", + "latest-value": "최신 값 삭제", + "all-data-for-time-period": "기간 내 모든 데이터 삭제", + "rewrite-latest-value": "최신 값 다시 쓰기" + } }, "api-usage": { + "api-features": "API 기능", "api-usage": "Api 사용량", + "alarm": "알람", + "alarms-created": "생성된 알람", + "queue-stats": "큐 통계", + "processing-failures-and-timeouts": "처리 실패 및 시간 초과", + "exceptions": "예외", + "alarms-created-daily-activity": "일일 알람 생성 활동", + "alarms-created-hourly-activity": "시간별 알람 생성 활동", + "alarms-created-monthly-activity": "월별 알람 생성 활동", "data-points": "데이터 포인트", "data-points-storage-days": "데이터 포인트 저장 일수", - "email": "Email", + "data-points-storage-days-hourly-activity": "데이터 포인트 저장 일수 시간별 활동", + "data-points-storage-days-daily-activity": "데이터 포인트 저장 일수 일별 활동", + "data-points-storage-days-monthly-activity": "데이터 포인트 저장 일수 월별 활동", + "device-api": "장치 API", + "email": "이메일", "email-messages": "Email 메시지", "email-messages-daily-activity": "일별 Email 메세지 활동", - "email-messages-hourly-activity": "시간별 Email 메시지 활동", "email-messages-monthly-activity": "월별 Email 메시지 활동", - "exceptions": "예외", - "executions": "Executions", + "executions": "실행", + "scripts": "스크립트", + "scripts-hourly-activity": "시간별 스크립트 활동", + "scripts-daily-activity": "일일 스크립트 활동", + "scripts-monthly-activity": "월별 스크립트 활동", "javascript": "JavaScript", - "javascript-executions": "JavaScript executions", + "javascript-executions": "JavaScript 실행", + "tbel": "TBEL", + "tbel-executions": "TBEL 실행", "latest-error": "최근 오류", "messages": "메시지", - "permanent-failures": "${entityName} Permanent Failures", - "permanent-timeouts": "${entityName} Permanent Timeouts", - "processing-failures": "${entityName} Processing Failures", - "processing-failures-and-timeouts": "Processing Failures and Timeouts", - "processing-timeouts": "${entityName} Processing Timeouts", - "queue-stats": "Queue Stats", + "notifications": "알림", + "notifications-email-sms": "알림 (이메일/SMS)", + "notifications-hourly-activity": "알림 시간별 활동", + "permanent-failures": "${entityName} 영구 실패", + "permanent-timeouts": "${entityName} 영구 시간 초과", + "processing-failures": "${entityName} 처리 실패", + "processing-timeouts": "${entityName} 처리 시간 초과", "rule-chain": "규칙 사슬", "rule-engine": "규칙 엔진", - "rule-engine-daily-activity": "Rule Engine daily activity", - "rule-engine-executions": "Rule Engine executions", - "rule-engine-hourly-activity": "Rule Engine hourly activity", - "rule-engine-monthly-activity": "Rule Engine monthly activity", - "rule-engine-statistics": "Rule Engine Statistics", + "rule-engine-executions": "규칙 엔진 실행", + "rule-engine-hourly-activity": "규칙 엔진 시간별 활동", + "rule-engine-daily-activity": "규칙 엔진 일별 활동", + "rule-engine-monthly-activity": "규칙 엔진 월별 활동", + "rule-engine-statistics": "규칙 엔진 통계", "rule-node": "규칙 노드", "sms": "SMS", "sms-messages": "SMS 메시지", - "sms-messages-daily-activity": "일별 SMS 메시지 활동", "sms-messages-hourly-activity": "시간별 SMS 메시지 활동", + "sms-messages-daily-activity": "일별 SMS 메시지 활동", "sms-messages-monthly-activity": "월별 SMS 메시지 활동", - "successful": "${entityName} Successful", + "successful": "${entityName} 성공", "telemetry": "텔레메트리", - "telemetry-persistence": "Telemetry persistence", - "telemetry-persistence-daily-activity": "Telemetry persistence daily activity", - "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", - "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", + "telemetry-persistence": "텔레메트리 저장", + "telemetry-persistence-daily-activity": "텔레메트리 저장 일별 활동", + "telemetry-persistence-hourly-activity": "텔레메트리 저장 시간별 활동", + "telemetry-persistence-monthly-activity": "텔레메트리 저장 월별 활동", "transport": "전송", - "transport-daily-activity": "Transport daily activity", - "transport-data-points": "Transport data points", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "Transport messages", - "transport-monthly-activity": "Transport monthly activity", + "transport-msg-hourly-activity": "전송 메시지 시간별 활동", + "transport-msg-daily-activity": "전송 메시지 일별 활동", + "transport-msg-monthly-activity": "전송 메시지 월별 활동", + "transport-daily-activity": "전송 일별 활동", + "transport-data-points": "전송 데이터 포인트", + "transport-data-points-hourly-activity": "전송 데이터 포인트 시간별 활동", + "transport-data-points-daily-activity": "전송 데이터 포인트 일별 활동", + "transport-data-points-monthly-activity": "전송 데이터 포인트 월별 활동", "view-details": "상세 내역", - "view-statistics": "통계 보기" + "view-statistics": "통계 보기", + "transport-messages": "전송 메시지", + "transport-messages-hourly-activity": "전송 메시지 시간별 활동", + "transport-data-point-hourly-activity": "전송 데이터 포인트 시간별 활동", + "javascript-function-executions": "JavaScript 함수 실행", + "javascript-function-executions-hourly-activity": "JavaScript 함수 실행 시간별 활동", + "javascript-function-executions-daily-activity": "JavaScript 함수 실행 일별 활동", + "javascript-function-executions-monthly-activity": "JavaScript 함수 실행 월별 활동", + "tbel-function-executions": "TBEL 함수 실행", + "tbel-function-executions-hourly-activity": "TBEL 함수 실행 시간별 활동", + "tbel-function-executions-daily-activity": "TBEL 함수 실행 일별 활동", + "tbel-function-executions-monthly-activity": "TBEL 함수 실행 월별 활동", + "created-reports": "생성된 보고서", + "created-reports-hourly-activity": "생성된 보고서 시간별 활동", + "created-reports-daily-activity": "생성된 보고서 일별 활동", + "created-reports-monthly-activity": "생성된 보고서 월별 활동", + "emails": "이메일", + "emails-hourly-activity": "이메일 시간별 활동", + "emails-daily-activity": "이메일 일별 활동", + "emails-monthly-activity": "이메일 월별 활동", + "status": { + "enabled": "사용", + "disabled": "비활성화됨", + "warning": "경고" + } + }, + "api-limit": { + "cassandra-write-queries-core": "REST API Cassandra 쓰기 쿼리", + "cassandra-read-queries-core": "REST API 및 WS 텔레메트리 Cassandra 읽기 쿼리", + "cassandra-write-queries-rule-engine": "규칙 엔진 텔레메트리 Cassandra 쓰기 쿼리", + "cassandra-read-queries-rule-engine": "규칙 엔진 텔레메트리 Cassandra 읽기 쿼리", + "cassandra-write-queries-monolith": "Monolith 텔레메트리 Cassandra 쓰기 쿼리", + "cassandra-read-queries-monolith": "Monolith 텔레메트리 Cassandra 읽기 쿼리", + "entity-version-creation": "엔티티 버전 생성", + "entity-version-load": "엔티티 버전 불러오기", + "notification-requests": "알림 요청", + "notification-requests-per-rule": "규칙당 알림 요청", + "rest-api-requests": "REST API 요청", + "rest-api-requests-per-customer": "고객당 REST API 요청", + "transport-messages": "전송 메시지", + "transport-messages-per-device": "장치당 전송 메시지", + "transport-messages-per-gateway": "게이트웨이당 전송 메시지", + "transport-messages-per-gateway-device": "게이트웨이 장치당 전송 메시지", + "ws-updates-per-session": "세션당 WS 업데이트", + "edge-events": "엣지 이벤트", + "edge-events-per-edge": "엣지당 엣지 이벤트", + "edge-uplink-messages": "엣지 업링크 메시지", + "edge-uplink-messages-per-edge": "엣지당 업링크 메시지" + }, + "api-key": { + "api-key": "API 키", + "api-keys": "API 키", + "delete-api-key-title": "API 키 '{{name}}'을(를) 삭제하시겠습니까?", + "delete-api-key-text": "주의: 확인 후 키를 복구할 수 없습니다.", + "delete-api-keys-title": "{ count, plural, =1 {1개 API 키} other {#개 API 키} }를 삭제하시겠습니까?", + "delete-api-keys-text": "주의: 확인 후 선택한 모든 키를 복구할 수 없게 됩니다.", + "expiration-date": "만료 날짜", + "date": "날짜", + "description": "설명", + "disable": "비활성화", + "edit-description": "설명 수정", + "enable": "API 키 사용", + "expiration-time": "만료 시간", + "expiration-time-never": "없음", + "expiration-time-custom": "사용자 지정", + "generate": "생성", + "generate-title": "API 키 생성", + "generate-text": "참고: API 키는 생성된 사용자의 권한을 상속받습니다.", + "generated-api-key-title": "API 키가 생성되었습니다. 연결을 확인하세요!", + "generated-api-key-copy": "지금 API 키를 복사하여 저장하세요. 다시 볼 수 없습니다.", + "generated-api-key-command": "다음 지침에 따라 연결을 확인하세요. 결과적으로", + "generated-api-key-insecure-url": "보안되지 않은 HTTP 연결을 통해 명령을 실행하면 API 키가 암호화되지 않고 전송됩니다.", + "list": "{ count, plural, =1 {1개 API 키} other {#개 API 키 목록} }", + "manage": "관리", + "manage-api-keys": "API 키 관리", + "no-found": "API 키가 없습니다", + "selected-api-keys": "{ count, plural, =1 {1개 API 키} other {#개 API 키} } 선택됨", + "search": "API 키 검색", + "status": "상태", + "status-active": "활성", + "status-inactive": "비활성", + "status-expired": "만료됨" }, "audit-log": { "audit": "감사", "audit-logs": "감사 로그", "timestamp": "타임스탬프", - "entity-type": "개체 유형", - "entity-name": "개체 이름", + "entity-type": "엔티티 유형", + "entity-name": "엔티티 이름", "user": "사용자", "type": "유형", "status": "상태", @@ -498,10 +1042,12 @@ "type-updated": "수정됨", "type-attributes-updated": "속성이 수정되었습니다", "type-attributes-deleted": "속성이 삭제되었습니다", - "type-rpc-call": "RPC call", + "type-rpc-call": "RPC 호출", "type-credentials-updated": "자격 증명이 갱신되었습니다", - "type-assigned-to-customer": "커스터머에게 할당", - "type-unassigned-from-customer": "지정된 커스터머 해제", + "type-assigned-to-customer": "고객에게 할당", + "type-unassigned-from-customer": "지정된 고객 해제", + "type-assigned-to-edge": "엣지에 할당됨", + "type-unassigned-from-edge": "엣지에서 할당 해제됨", "type-activated": "활성", "type-suspended": "일시 중지", "type-credentials-read": "자격 증명 읽기", @@ -511,6 +1057,12 @@ "type-relations-delete": "모든 관계가 삭제되었습니다", "type-alarm-ack": "확인", "type-alarm-clear": "해제", + "type-alarm-delete": "알람 삭제됨", + "type-alarm-assign": "알람 할당됨", + "type-alarm-unassign": "알람 할당 해제", + "type-added-comment": "댓글 추가됨", + "type-updated-comment": "댓글 수정됨", + "type-deleted-comment": "댓글 삭제됨", "type-login": "로그인", "type-logout": "로그아웃", "type-lockout": "잠금", @@ -525,7 +1077,587 @@ "type-assigned-from-tenant": "테넌트로부터 지정", "type-assigned-to-tenant": "테넌트로 지정", "type-provision-success": "장치가 프로비전됨", - "type-provision-failure": "장치 프로비저닝이 실패했습니다" + "type-provision-failure": "장치 프로비저닝이 실패했습니다", + "type-timeseries-updated": "텔레메트리 수정됨", + "type-timeseries-deleted": "텔레메트리 삭제됨", + "type-sms-sent": "SMS 전송됨", + "any-type": "모든 유형", + "audit-log-filter-title": "감사 로그 필터", + "filter-title": "필터", + "filter-types": "감사 로그 유형" + }, + "debug-settings": { + "label": "디버그 설정", + "on-failure": "실패만 (24/7)", + "all-messages": "모든 메시지 ({{time}})", + "failures": "실패", + "entity": "엔티티", + "hint": { + "main-limited": "{{time}}당 최대 {{msg}}개의 {{entity}} 디버그 메시지가 기록됩니다.", + "on-failure": "오류 메시지만 기록합니다.", + "all-messages": "모든 디버그 메시지를 기록합니다." + } + }, + "calculated-fields": { + "expression": "표현식", + "no-found": "계산 필드가 없습니다", + "add": "계산 필드 추가", + "list": "{ count, plural, =1 {1개 계산 필드} other {#개 계산 필드 목록} }", + "selected-fields": "{ count, plural, =1 {1개 계산 필드} other {#개 계산 필드} } 선택됨", + "type": { + "simple": "간단", + "simple-hint": "입력 인수를 기반으로 한 단순 산술 계산.", + "script": "스크립트", + "script-hint": "TBEL 스크립트를 사용하여 정의된 인수에 대한 계산.", + "geofencing": "지오펜싱", + "geofencing-hint": "구성된 지오펜싱 영역 그룹에 대한 엔티티 GPS 위치 및 전환 평가.", + "propagation": "전파", + "propagation-hint": "관계 방향 및 유형에 따라 부모 또는 자식 엔티티로 데이터 전파.", + "related-entities-aggregation": "관련 엔티티 집계", + "related-entities-aggregation-hint": "관련 엔티티의 최신 데이터 집계.", + "time-series-data-aggregation": "시계열 데이터 집계", + "time-series-data-aggregation-hint": "현재 엔티티의 과거 데이터 집계." + }, + "preview": "미리보기", + "arguments": "인수", + "decimals-by-default": "기본 소수 자릿수", + "debugging": "계산 필드 디버깅", + "calculated-field-details": "계산 필드 상세", + "argument-name": "인수 이름", + "name": "이름", + "datasource": "데이터소스", + "add-argument": "인수 추가", + "test-script-function": "스크립트 함수 테스트", + "no-arguments": "최소 하나의 인수가 필요합니다.", + "argument-settings": "인수 설정", + "argument-current": "현재 엔티티", + "argument-current-tenant": "현재 테넌트", + "argument-device": "장치", + "argument-asset": "자산", + "argument-customer": "고객", + "argument-tenant": "현재 테넌트", + "argument-owner": "현재 소유자", + "argument-relation-query": "관련 엔티티", + "argument-type": "인수 유형", + "attribute": "속성", + "copy-argument-name": "인수 이름 복사", + "timeseries-key": "시계열 키", + "device-name": "장치 이름", + "latest-telemetry": "최근 데이터", + "rolling": "시계열 롤링", + "attribute-scope": "속성 범위", + "server-attributes": "서버 속성", + "client-attributes": "클라이언트 속성", + "shared-attributes": "공유 속성", + "attribute-key": "속성 키", + "default-value": "기본값", + "default-value-required": "기본값이 필요합니다.", + "limit": "최대 값", + "time-window": "타임윈도우", + "customer-name": "고객 이름", + "asset-name": "자산 이름", + "timeseries": "시계열", + "output": "출력", + "output-hint": "출력 처리 방법을 정의합니다.", + "create": "새 계산 필드 만들기", + "file": "계산 필드 파일", + "invalid-file-error": "잘못된 파일 형식입니다. 파일이 올바른 JSON 파일인지 확인하세요.", + "import": "계산 필드 가져오기", + "export": "계산 필드 내보내기", + "export-failed-error": "계산 필드를 내보낼 수 없습니다: {{error}}", + "output-type": "출력 유형", + "delete-title": "계산 필드 '{{title}}'을(를) 삭제하시겠습니까?", + "delete-text": "주의: 확인 후 계산 필드와 모든 관련 데이터를", + "delete-multiple-title": "{ count, plural, =1 {1개 계산 필드} other {#개 계산 필드} }를 삭제하시겠습니까?", + "delete-multiple-text": "주의: 확인 후 선택한 모든 계산 필드가 제거되고", + "test-with-this-message": "이 메시지로 테스트", + "use-latest-timestamp": "최신 타임스탬프 사용", + "entity-coordinates": "엔티티 좌표", + "latitude-time-series-key": "위도 시계열 키", + "latitude-time-series-key-required": "위도 시계열 키가 필요합니다.", + "longitude-time-series-key": "경도 시계열 키", + "longitude-time-series-key-required": "경도 시계열 키가 필요합니다.", + "geofencing-zone-groups": "지오펜싱 영역 그룹", + "geofencing-zone-groups-settings": "지오펜싱 영역 그룹 설정", + "target-zone": "대상 영역", + "perimeter-key": "경계 키", + "report-strategy": "보고 전략", + "no-zone-configured": "최소 하나의 영역이 필요합니다.", + "no-zone-configured-required": "최소 하나의 영역 그룹을 구성해야 합니다.", + "add-zone-group": "영역 그룹 추가", + "report-transition-event-only": "전환 이벤트만", + "report-presence-status-only": "존재 상태만", + "report-transition-event-and-presence": "존재 상태 및 전환 이벤트", + "perimeter-attribute-key": "경계 속성 키", + "perimeter-attribute-key-required": "경계 속성 키가 필요합니다.", + "perimeter-attribute-key-pattern": "경계 속성 키가 올바르지 않습니다.", + "entity-zone-relationship": "엔티티에서 영역으로의 경로", + "direction": "관계 방향", + "direction-from": "엔티티에서 영역으로", + "direction-to": "영역에서 엔티티로", + "relation-type": "관계 유형", + "create-relation-with-matched-zones": "일치하는 영역과 소스 엔티티의 관계 생성", + "relation-level": "관계 수준", + "fetch-last-available-level": "마지막 사용 가능한 수준만 가져오기", + "zone-group-refresh-interval": "영역 그룹 새로고침 간격", + "copy-zone-group-name": "영역 그룹 이름 복사", + "open-details-page": "엔티티 상세 페이지 열기", + "level": "수준", + "direction-level": "범례 방향", + "direction-up": "위로", + "direction-up-parent": "부모로 위로", + "direction-down": "아래로", + "direction-down-child": "자식으로 아래로", + "add-level": "수준 추가", + "delete-level": "수준 삭제", + "no-level": "구성된 수준 없음", + "levels-required": "최소 하나의 수준을 구성해야 합니다.", + "max-allowed-levels-error": "관계 수준이 최대 허용값을 초과합니다.", + "propagation-path-related-entities": "관련 엔티티로의 전파 경로", + "propagate-type": { + "arguments-only": "인수만", + "expression-result": "계산 결과" + }, + "script": "스크립트", + "data-propagate": "전파할 데이터", + "output-key": "출력 키", + "copy-output-key": "출력 키 복사", + "aggregation-path-related-entities": "관련 엔티티로의 집계 경로", + "deduplication-interval": "중복 제거 간격", + "deduplication-interval-min": "중복 제거 간격은 최소 {{ sec }}초여야 합니다.", + "deduplication-interval-hint": "텔레메트리 집계 사이의 최소 시간.", + "deduplication-interval-required": "중복 제거 간격이 필요합니다.", + "calculated-field-filter-title": "계산 필드 필터", + "filter-title": "필터", + "calculated-field-types": "계산 필드 유형", + "events": "이벤트", + "any-type": "모든 유형", + "metrics": { + "metrics": "메트릭", + "metrics-empty": "최소 하나의 메트릭을 구성해야 합니다.", + "metric-name": "메트릭 이름", + "metric-name-required": "메트릭 이름이 필요합니다.", + "metric-name-pattern": "메트릭 이름이 올바르지 않습니다.", + "metric-name-duplicate": "같은 이름의 메트릭이 이미 존재합니다.", + "metric-name-max-length": "메트릭 이름은 256자 미만이어야 합니다.", + "metric-name-forbidden": "메트릭 이름은 예약어이므로 사용할 수 없습니다.", + "copy-metric-name": "메트릭 이름 복사", + "argument-name": "인수 이름", + "aggregation": "집합", + "aggregation-type": { + "avg": "평균", + "min": "최소", + "max": "최대", + "sum": "합계", + "count": "숫자", + "count-unique": "고유 개수" + }, + "filtered": "필터됨", + "value-source": "값 소스", + "value-source-hint": "집계에 사용할 값을 얻는 방법을 정의합니다.", + "value-source-type": { + "key": "키", + "function": "함수" + }, + "no-metrics-configured": "최소 하나의 메트릭이 필요합니다.", + "add-metric": "메트릭 추가", + "max-metrics": "최대 메트릭 수에 도달했습니다.", + "metric-settings": "메트릭 설정", + "filter": "필터", + "filter-hint": "집계 중 엔티티 필터링을 활성화합니다. 필터 함수는 부울 값을 반환해야 하며 구성된 모든 인수를 사용할 수 있습니다." + }, + "output-strategy": { + "strategy": "전략", + "process-right-away": "즉시 처리", + "process-rule-chains": "규칙 사슬을 통해 처리", + "save-time-series": "시계열에 저장", + "save-database": "데이터베이스에 저장", + "save-latest-values": "최신 값에 저장", + "send-web-sockets": "WebSocket으로 전송", + "save-calculated-fields": "계산 필드로 전송", + "update-attribute-only-on-value-change": "값 변경 시에만 속성 업데이트", + "send-attributes-updated-notification": "속성 업데이트 알림 전송", + "ttl": "사용자 지정 TTL", + "ttl-required": "TTL이 필요합니다", + "ttl-min": "최소 TTL은 0만 허용됩니다", + "processing-parameters": "처리 매개변수", + "hint": { + "strategy": "결과를 즉시 처리할지 추가 처리를 위해 규칙 사슬로 전송할지 제어합니다.", + "processing-options": "처리 옵션", + "update-attribute-only-on-value-change": "값이 변경되었는지 여부에 관계없이 들어오는 모든 메시지에서 속성을 업데이트합니다. 이는 API 사용량을 증가시키고 성능을 저하시킵니다.", + "update-attribute-only-on-value-change-enabled": "값이 변경될 때만 속성을 업데이트합니다. 값이 변경되지 않으면 타임스탬프가 업데이트되지 않고 알림이 전송되지 않습니다.", + "send-attributes-updated-notification": "기본 규칙 사슬로 속성 업데이트 이벤트를 전송합니다.", + "save-time-series": "시계열 데이터를 데이터베이스의 ts_kv 테이블에 저장합니다.", + "save-database": "속성 데이터를 데이터베이스에 저장합니다.", + "save-latest-values": "새 값이 더 최근인 경우 데이터베이스의 ts_kv_latest 테이블에서 시계열 데이터를 업데이트합니다.", + "send-web-sockets-attribute": "속성 데이터 업데이트에 대해 WebSocket 구독에 알립니다.", + "send-web-sockets-time-series": "시계열 데이터 업데이트에 대해 WebSocket 구독에 알립니다.", + "save-calculated-fields-attribute": "속성 데이터 업데이트에 대해 계산 필드에 알립니다.", + "save-calculated-fields-time-series": "시계열 데이터 업데이트에 대해 계산 필드에 알립니다.", + "ttl": "시계열 데이터의 보존 기간을 정의합니다. 비활성화하면 테넌트 프로필 TTL이 사용됩니다." + } + }, + "aggregate-interval-type": "집계 간격 유형", + "aggregate-interval-value": "집계 간격 값", + "aggregate-interval-value-required": "집계 간격 값이 필요합니다.", + "aggregate-interval-value-min": "집계 간격 값은 최소 { sec, plural, =0 {0초} =1 {1초} other {#초} }여야 합니다.", + "aggregate-interval-value-step-multiple-of": "집계 간격 값은 1일의 약수 또는 배수여야 합니다.", + "aggregate-period": { + "hour": "시간", + "day": "일", + "week": "주 (월 - 일)", + "week-sun-sat": "주 (일 - 토)", + "month": "달", + "quarter": "분기", + "year": "년", + "custom": "사용자 지정" + }, + "aggregate-period-hint-offset": "집계 간격은 {{ interval }}입니다.", + "aggregate-period-hint-offset-and-so-on": "집계 간격은 {{ interval }} 등입니다.", + "entity-aggregation": { + "argument-hint": "현재 엔티티에서 데이터를 가져옵니다.", + "argument-title-hint": "집계에 사용할 입력 인수를 정의합니다.", + "argument-setting-hint": "최신 텔레메트리는 이 계산 필드에서 사용 가능한 유일한 인수 유형입니다.", + "aggregation-interval": "집계 간격", + "aggregation-interval-hint": "집계 수행 빈도를 정의합니다. 예: 1시간마다 00:00, 01:00, 02:00 등에 데이터를 집계합니다. 집계 결과는 집계 간격의 시작에 해당하는 타임스탬프와 함께 저장됩니다.", + "apply-offset": "집계 간격에 오프셋 적용", + "apply-offset-hint": "각 집계 기간의 시작을 얼마나 이동할지 정의합니다 (예: +10분 - 00:10, 01:10).", + "offset-value": "오프셋 값", + "offset-value-required": "오프셋 값이 필요합니다.", + "offset-value-min": "오프셋 값은 양의 정수여야 합니다.", + "offset-value-max": "오프셋 값은 집계 간격 값보다 작아야 합니다.", + "wait-delay": "지연된 텔레메트리에 대한 대기 시간 초과 적용", + "wait-delay-hint": "간격이 끝난 후 지연된 텔레메트리를 얼마나 기다릴지 정의합니다. 이러한 텔레메트리가 도착하면 해당 간격의 결과가 다시 계산됩니다.", + "duration": "기간", + "duration-required": "지속 시간이 필요합니다.", + "duration-min": "지속 시간은 최소 1분이어야 합니다.", + "duration-hint": "간격이 끝난 후 지연된 데이터를 얼마나 기다릴지.", + "produce-intermediate-result": "중간 결과 생성", + "produce-intermediate-result-hint": "현재 간격 동안 지표를 계산하여 중간 결과를 생성합니다." + }, + "hint": { + "arguments-simple-with-rolling": "단순 유형 계산 필드에는 시계열 롤링 유형의 키가 포함되지 않아야 합니다.", + "arguments-propagate-arguments-with-rolling": "'시계열 롤링' 유형은 '인수만' 전파와 호환되지 않습니다.", + "arguments-propagate-argument-entity-type": "엔티티 유형은 '인수만' 전파와 호환되지 않습니다.", + "arguments-propagate-argument-must-current-entity": "최소 하나의 인수가 '현재 엔티티' 소스 엔티티로 구성되어야 합니다.", + "arguments-empty": "최소 하나의 인수를 지정해야 합니다.", + "expression-required": "표현식이 필요합니다.", + "expression-invalid": "표현식이 올바르지 않습니다", + "expression-max-length": "표현식 길이는 255자 미만이어야 합니다.", + "argument-name-required": "인수 이름이 필요합니다.", + "argument-name-pattern": "인수 이름이 올바르지 않습니다.", + "argument-name-duplicate": "같은 이름의 인수가 이미 존재합니다.", + "argument-name-max-length": "인수 이름은 256자 미만이어야 합니다.", + "argument-name-forbidden": "인수 이름은 예약어이므로 사용할 수 없습니다.", + "output-key-required": "출력 키가 필요합니다.", + "output-key-pattern": "출력 키가 올바르지 않습니다.", + "output-key-duplicate": "같은 이름의 키가 이미 존재합니다.", + "output-key-max-length": "출력 키는 256자 미만이어야 합니다.", + "output-key-forbidden": "출력 키는 예약어이므로 사용할 수 없습니다.", + "entity-type-required": "엔티티 유형이 필요합니다", + "name-required": "이름을 입력하세요.", + "name-pattern": "이름이 올바르지 않습니다.", + "name-duplicate": "같은 이름의 이름이 이미 존재합니다.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "name-forbidden": "이름은 예약어이므로 사용할 수 없습니다.", + "argument-type-required": "인수 유형이 필요합니다.", + "max-args": "최대 인수 수에 도달했습니다.", + "decimals-range": "기본 소수 자릿수는 0~15 사이의 숫자여야 합니다.", + "expression": "기본 식은 온도를 화씨에서 섭씨로 변환하는 방법을 보여줍니다.", + "arguments-entity-not-found": "인수 대상 엔티티를 찾을 수 없습니다.", + "use-latest-timestamp": "사용하면 계산된 값이 가장 최신 타임스탬프를 사용하여 저장됩니다.", + "entity-coordinates": "엔티티 GPS 좌표(위도 및 경도)를 제공하는 시계열 키를 지정하세요.", + "geofencing-zone-groups": "확인할 하나 이상의 지오펜싱 영역 그룹을 정의하세요 (예: 'allowedZones', 'restr", + "perimeter-attribute-key": "지오펜싱 영역 경계 정의가 포함된 속성 키를 설정하세요.", + "report-strategy": "현재 상태 보고서는 엔티티가 영역 내부 또는 외부에 있는지 보고합니다.", + "create-relation-with-matched-zones": "엔티티와 해당 엔티티가 포함된 영역 간의 관계를 자동으로 생성하고 유지 관리합니다.", + "relation-type-required": "관계 유형을 입력하세요.", + "relation-level-required": "관계 수준이 필요합니다.", + "relation-level-min": "최소 관계 수준 값은 1입니다.", + "relation-level-max": "최대 관계 수준 값은 {{max}}입니다.", + "geofencing-empty": "최소 하나의 영역 그룹을 구성해야 합니다.", + "geofencing-entity-not-found": "지오펜싱 대상 엔티티를 찾을 수 없습니다.", + "max-geofencing-zone": "최대 지오펜싱 영역 수에 도달했습니다.", + "zone-group-refresh-interval": "관련 엔티티를 통해 구성된 영역 그룹이 새로고침되는 빈도를 정의합니다.", + "zone-group-refresh-interval-required": "영역 그룹 새로고침 간격이 필요합니다.", + "zone-group-refresh-interval-min": "영역 그룹 새로고침 간격은 최소 {{ min }}초여야 합니다.", + "propagation-path-related-entities": "선택한 방향을 기반으로 관련 엔티티에 대한 직접적인 단일 수준 경로를 정의합니다.", + "data-propagate": "아래 구성된 인수에서 전파할 데이터를 정의합니다. '인수만'", + "aggregation-path-related-entities": "부모 또는 자식과의 직접 관계를 통한 단일 수준 집계 경로를 정의합니다.", + "arguments-aggregation": "필터링 및 집계에 사용되는 입력 인수를 정의합니다.", + "setting-arguments-aggregation": "집계 경로에 구성된 관련 엔티티에서 데이터를 가져옵니다.", + "metrics": "구성된 인수를 기반으로 집계된 지표를 정의합니다.", + "entity-aggregation-metrics": "지정된 시간 동안 구성된 인수를 기반으로 집계된 지표를 정의합니다.", + "import-invalid-calculated-field-type": "계산 필드를 가져올 수 없습니다: 잘못된 계산 필드 구조.", + "simple-expression-title": "계산된 값을 계산하는 방법을 정의하는 산술 식입니다.", + "script-title": "계산 로직과 출력 값을 정의하는 TBEL 스크립트입니다.", + "simple-arguments": "계산된 값을 계산하는 방법을 정의하는 산술 식입니다.", + "script-arguments": "스크립트에서 사용할 수 있는 입력 인수를 정의합니다." + } + }, + "alarm-rule": { + "alarm-rules-tab": "알람 규칙", + "alarm-rule": "알람 규칙", + "alarm-rules": "알람 규칙", + "alarm-rules-old": "이전", + "alarm-rules-actual": "실제", + "severities": "심각도", + "cleared": "해제 조건", + "delete-title": "알람 규칙 '{{title}}'을(를) 삭제하시겠습니까?", + "delete-text": "주의: 확인 후 알람 규칙과 모든 관련 데이터를", + "delete-multiple-title": "{ count, plural, =1 {1개 알람 규칙} other {#개 알람 규칙} }을(를) 삭제하시겠습니까?", + "delete-multiple-text": "주의: 확인 후 선택한 모든 알람 규칙이 제거되고", + "create": "새 알람 규칙 만들기", + "add": "알람 규칙 추가", + "copy": "알람 규칙 구성 복사", + "details": "알람 규칙 상세", + "no-found": "알람 규칙이 없습니다", + "list": "{ count, plural, =1 {1개 알람 규칙} other {#개 알람 규칙 목록} }", + "selected-fields": "{ count, plural, =1 {1개 알람 규칙} other {#개 알람 규칙} } 선택됨", + "import": "알람 규칙 가져오기", + "file": "알람 규칙 파일", + "export": "알람 규칙 내보내기", + "export-failed-error": "알람 규칙을 내보낼 수 없습니다: {{error}}", + "entity-type": "엔티티 유형", + "entity-type-required": "엔티티의 유형을 입력하세요.", + "alarm-type": "알람 유형", + "alarm-type-hint": "충돌을 방지하기 위해 알람 발생자(장치, 자산 등) 범위 내의 고유 식별자(예: HighTempAlarm).", + "alarm-type-required": "알람 유형은 필수 항목입니다.", + "alarm-type-pattern": "알람 유형이 올바르지 않습니다.", + "alarm-type-max-length": "알람 유형은 256자 미만이어야 합니다.", + "clear-alarm": "알람 해제", + "value-argument": "인수", + "value-argument-required": "인수가 필요합니다.", + "static-settings": "정적 설정", + "configuration": "설정", + "static-schedule": "정적", + "dynamic-schedule": "동적", + "operation-and": "그리고", + "operation-or": "또는", + "condition-during": "{{during}} 동안", + "condition-during-dynamic": "\"{{ attribute }}\" 동안", + "condition-repeat-times": "{ count, plural, =1 {1회} other {#회} } 반복", + "condition-repeat-times-dynamic": "\"{{ attribute }}\" 번 반복", + "filter-preview": "필터 미리보기", + "condition-settings": "조건 설정", + "static": "정적", + "dynamic": "동적", + "argument-filters": "인수 필터", + "argument-name": "인수 이름", + "value-type": "값 유형", + "general": "일반", + "filters": "필터", + "date-time-hint": "인수는 에포크 밀리초여야 합니다. 예: 1698839340000은 2023-11-01입니다.", + "operation": "연산", + "value-source": "값 소스", + "value": "값", + "ignore-case": "대소문자 무시", + "condition": "조건", + "script": "스크립트", + "add-filter": "인수 필터 추가", + "edit-filter": "인수 필터", + "remove-filter": "인수 필터 제거", + "no-filter": "최소 하나의 필터가 필요합니다.", + "conditions": { + "simple": "간단", + "duration": "기간", + "repeating": "반복" + }, + "schedule-title": "일정", + "edit-schedule": "알람 일정 편집", + "schedule-type": "스케줄러 유형", + "schedule-type-required": "스케줄러 유형은 필수 항목입니다.", + "schedule": { + "any-time": "항상 활성", + "specific-time": "특정 시간에 활성", + "custom": "사용자 지정" + }, + "schedule-day": { + "monday": "월요일", + "tuesday": "화요일", + "wednesday": "수요일", + "thursday": "목요일", + "friday": "금요일", + "saturday": "토요일", + "sunday": "일요일" + }, + "schedule-days": "요일", + "schedule-time": "시간", + "schedule-time-from": "부터", + "schedule-time-to": "까지", + "schedule-days-of-week-required": "최소 하나의 요일을 선택해야 합니다.", + "tbel": "TBEL", + "expression-type": { + "simple": "간단", + "script": "스크립트" + }, + "operation-type": { + "and": "그리고", + "or": "또는" + }, + "filter-predicate-type": { + "string": "문자열", + "numeric": "숫자", + "boolean": "불리언", + "complex": "복합" + }, + "alarm-rule-additional-info": "추가 정보", + "edit-alarm-rule-additional-info": "추가 정보 수정", + "alarm-rule-additional-info-placeholder": "알람 세부 정보 내에 표시할 설명과 조정 사항을 여기에 입력하세요.", + "alarm-rule-additional-info-hint": "힌트: ${인수 이름{{dashboardTitle}}는 이제 공개되어서 다음 공개 링크를 통해 접근될 수 있습니다:", "public-dashboard-notice": "참고: 관련된 장치에 접근하기 위해서는 장치들 또한 공개되어야 합니다.", "make-private-dashboard-title": "대시보드 '{{dashboardTitle}}'를 비공개로 전환하시겠습니까?", "make-private-dashboard-text": "대시보드는 비공개로 전환되어 다른 이들이 접근할 수 없게 됩니다.", "make-private-dashboard": "대시보드 비공개 전환", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-text": "'{{dashboardTitle}}' - ThingsBoard 제공", + "socialshare-title": "'{{dashboardTitle}}' - ThingsBoard 제공", "select-dashboard": "대시보드 선택", "no-dashboards-matching": "'{{entity}}'와 일치하는 대시보드가 없습니다.", "dashboard-required": "대시보드를 입력하세요.", @@ -682,12 +1892,26 @@ "background-image": "배경 이미지", "background-size-mode": "배경 사이즈 모드", "no-image": "이미지 없음", + "empty-image": "이미지 없음", "drop-image": "이곳에 이미지를 끌어다놓거나 이곳을 클릭하여 파일을 선택하고 업로드하세요.", + "maximum-upload-file-size": "최대 업로드 파일 크기: {{ size }}", + "cannot-upload-file": "파일을 업로드할 수 없습니다", "settings": "설정", + "move-all-widgets": "모든 위젯 이동", + "move-by": "이동 거리", + "cols": "열", + "rows": "행", + "layout": "레이아웃", + "layout-type-default": "기본값", + "layout-type-scada": "SCADA", + "layout-type-divider": "구분선", + "layout-settings-type": "레이아웃 설정: {{ type }} 브레이크포인트", "columns-count": "열 개수", "columns-count-required": "열 개수를 입력하세요.", "min-columns-count-message": "열 개수를 최소 10 이상으로 입력하세요.", "max-columns-count-message": "열 개수를 최대 100 이하로 입력하세요.", + "min-layout-width": "최소 레이아웃 너비", + "columns-suffix": "열", "widgets-margins": "위젯 사이 여백 크기", "margin-required": "여백 값을 입력하세요.", "min-margin-message": "최소 여백 값은 0보다 커야 합니다.", @@ -700,23 +1924,39 @@ "vertical-margin-required": "가로 여백 값을 입력하세요.", "min-vertical-margin-message": "최소 가로 여백 값은 0보다 커야 합니다.", "max-vertical-margin-message": "최대 가로 여백 값은 50보다 작아야 합니다.", + "apply-outer-margin": "레이아웃 측면에 여백 적용", "autofill-height": "레이아웃 세로 길이 자동 채우기", "mobile-layout": "모바일 레이아웃 설정", "mobile-row-height": "모바일 행 높이, px", "mobile-row-height-required": "모바일 행 높이를 입력하세요.", "min-mobile-row-height-message": "최소 모바일 행 높이 값은 5 픽셀보다 커야 합니다.", "max-mobile-row-height-message": "최대 모바일 행 높이 값은 200 픽셀보다 작아야 합니다.", + "row-height": "행 높이", + "row-height-required": "행 높이 값은 필수 항목입니다.", + "min-row-height-message": "최소 행 높이 값은 5픽셀만 허용됩니다.", + "max-row-height-message": "최대 행 높이 값은 200픽셀만 허용됩니다.", + "display-first-in-mobile-view": "모바일 보기에서 먼저 표시", + "title-settings": "제목 설정", "display-title": "대시보드 제목 표시", - "toolbar-always-open": "도구 상자를 열어두기", "title-color": "제목 색상", + "toolbar-settings": "도구 모음 설정", + "hide-toolbar": "도구 모음 숨기기", + "toolbar-always-open": "도구 상자를 열어두기", "display-dashboards-selection": "대시보드 선택 표시", - "display-entities-selection": "개체 선택 표시", + "display-entities-selection": "엔티티 선택 표시", "display-filters": "필터 표시", "display-dashboard-timewindow": "타임윈도우 표시", "display-dashboard-export": "디스플레이 내보내기", + "display-update-dashboard-image": "대시보드 이미지 업데이트 표시", + "dashboard-logo-settings": "대시보드 로고 설정", + "display-dashboard-logo": "대시보드 전체 화면 모드에서 로고 표시", + "dashboard-logo-image": "대시보드 로고 이미지", + "advanced-settings": "고급 설정", + "dashboard-css": "대시보드 CSS", "import": "대시보드 가져오기", "export": "대시보드 내보내기", "export-failed-error": "대시보드 내보내기를 할 수 없습니다.: {error}", + "export-prompt": "대시보드 이미지 및 리소스 포함", "create-new-dashboard": "대시보드 생성", "dashboard-file": "대시보드 파일", "invalid-dashboard-file-error": "대시보드 가져오기를 할 수 없습니다.: 대시보드 데이터 구조가 잘못되었습니다.", @@ -732,14 +1972,17 @@ "alias-resolution-error-title": "대시보드 별명 구성 오류", "invalid-aliases-config": "일부 별명 필터와 일치하는 장치를 찾을 수 없습니다.
    이 문제를 해결하려면 관리자에게 문의하십시오.", "select-devices": "장치 선택", - "assignedToCustomer": "할당된 커스터머", - "assignedToCustomers": "할당된 커스터머", + "assignedToCustomer": "할당된 고객", + "assignedToCustomers": "할당된 고객", "public": "공개", + "copyId": "대시보드 ID 복사", + "idCopiedMessage": "대시보드 ID가 클립보드에 복사되었습니다", "public-link": "공개 링크", "copy-public-link": "공개 링크 복사", "public-link-copied-message": "대시보드 공개 링크가 클립보드로 복사되었습니다", "manage-states": "대시보드 상태 관리", "states": "대시보드 상태", + "states-short": "상태", "search-states": "대시보드 상태 검색", "selected-states": "{ count, plural, =1 {1 개 대시보드 상태} other {# 개 대시보드 상태} } 선택됨", "edit-state": "대시보드 상태 편집", @@ -759,12 +2002,38 @@ "hide-details": "상세 내역 숨기기", "select-state": "대상 상태 선택", "state-controller": "상태 컨트롤러", + "state-controller-default": "정적 (사용 중단)", "search": "대시보드 검색", - "selected-dashboards": "{ count, plural, =1 {1 개 대시보드} other {# 개 대시보드} } 선택됨" + "selected-dashboards": "{ count, plural, =1 {1 개 대시보드} other {# 개 대시보드} } 선택됨", + "home-dashboard": "홈 대시보드", + "home-dashboard-hide-toolbar": "홈 대시보드 도구 모음 숨기기", + "unassign-dashboard-from-edge-text": "확인 후 대시보드 할당이 해제되고", + "unassign-dashboards-from-edge-title": "{ count, plural, =1 {1 개 대시보드} other {# 개 대시보드} }의 할당을 취소하시겠습니까?", + "unassign-dashboards-from-edge-text": "확인 후 선택한 모든 대시보드 할당이 해제되고", + "assign-dashboard-to-edge": "엣지에 대시보드 할당", + "assign-dashboard-to-edge-text": "엣지에 할당할 대시보드를 선택하세요", + "non-existent-dashboard-state-error": "ID가 \"{{ stateId }}\"인 대시보드 상태를 찾을 수 없습니다", + "edit-mode": "수정 모드", + "duplicate-state-action": "상태 복제", + "breakpoint-value": "브레이크포인트 ({{ value }})", + "breakpoints-id": { + "default": "기본값", + "xs": "모바일 (xs)", + "sm": "태블릿 (sm)", + "md": "노트북 (md)", + "lg": "데스크톱 (lg)", + "xl": "데스크톱 (xl)" + }, + "view-format-type-grid": "그리드", + "view-format-type-list": "목록", + "view-format": "보기 형식" }, "datakey": { "settings": "설정", + "general": "일반", "advanced": "고급", + "key": "키", + "keys": "키", "label": "라벨", "color": "색상", "units": "값 뒤에 표기할 특수 기호", @@ -774,25 +2043,64 @@ "configuration": "데이터 키 구성", "timeseries": "시계열", "attributes": "속성", - "entity-field": "개체 필드", + "entity-field": "엔티티 필드", "alarm": "알람 필드", "timeseries-required": "장치 시계열 를 입력하세요.", "timeseries-or-attributes-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-timeseries-or-attributes-required": "알람 필드 또는 엔티티 시계열/속성이 필요합니다.", + "maximum-timeseries-or-attributes": "최대 { count, plural, =1 {1개의 시계열/속성이 허용됩니다.} other {#개의 시계열/속성이 허용됩니다.} }", "alarm-fields-required": "알람 필드를 입력하세요.", "function-types": "함수 유형", + "function-type": "함수 유형", "function-types-required": "함수 유형을 입력하세요.", + "data-keys": "데이터 키", + "data-key": "데이터 키", + "data-keys-required": "데이터 키는 필수 항목입니다.", + "data-key-required": "데이터 키는 필수 항목입니다.", + "alarm-keys": "알람 데이터 키", + "alarm-key": "알람 데이터 키", + "alarm-key-functions": "알람 키 함수", + "alarm-key-function": "알람 키 함수", + "latest-keys": "최신 데이터 키", + "latest-key": "최신 데이터 키", + "latest-key-functions": "최신 키 함수", + "latest-key-function": "최신 키 함수", + "timeseries-keys": "시계열 데이터 키", + "timeseries-key": "시계열 데이터 키", + "timeseries-key-functions": "시계열 키 함수", + "timeseries-key-function": "시계열 키 함수", "maximum-function-types": "최대 { count, plural, =1 {1 개 함수} other {# 개 함수} } 종류만 허용됩니다.", "time-description": "현재 값의 타임스탬프;", "value-description": "현재 값;", "prev-value-description": "이전 함수 호출의 결과;", "time-prev-description": "이전 값의 타임스탬프;", - "prev-orig-value-description": "원래 이전 값;" + "prev-orig-value-description": "원래 이전 값;", + "aggregation": "집합", + "aggregation-type-hint-common": "성능상의 이유로 집계 값 계산은 다음에만 사용할 수 있습니다", + "aggregation-type-none-hint": "최신 값을 가져옵니다.", + "aggregation-type-min-hint": "선택한 시간 창 내의 데이터 포인트 중 최소값을 찾습니다.", + "aggregation-type-max-hint": "선택한 시간 창 내의 데이터 포인트 중 최대값을 찾습니다.", + "aggregation-type-avg-hint": "선택한 시간 창 내의 데이터 포인트 평균을 계산합니다.", + "aggregation-type-sum-hint": "선택한 시간 창 내의 데이터 포인트의 모든 값을 합산합니다.", + "aggregation-type-count-hint": "선택한 시간 창 내의 데이터 포인트 총 수입니다.", + "delta-calculation": "델타 계산", + "enable-delta-calculation": "델타 계산 사용", + "enable-delta-calculation-hint": "사용하면 데이터 키 값이 집계된 값을 기반으로 계산됩니다.", + "delta-calculation-result": "델타 계산 결과", + "delta-calculation-result-previous-value": "이전 값", + "delta-calculation-result-delta-absolute": "델타 (절대값)", + "delta-calculation-result-delta-percent": "델타 (퍼센트)", + "source": "소스", + "latest": "최신", + "latest-value": "최신 값", + "delta": "델타", + "percent": "퍼센트", + "absolute": "절대값" }, "datasource": { "type": "데이터소스 유형", "name": "이름", + "label": "라벨", "add-datasource-prompt": "데이터소스를 추가하세요." }, "details": { @@ -808,6 +2116,7 @@ "management": "장치 관리", "view-devices": "장치 보기", "device-alias": "장치 별명", + "device-type-max-length": "장치 유형은 256자 미만이어야 합니다.", "aliases": "장치 별명", "no-alias-matching": "'{{alias}}' 를 찾을 수 없습니다.", "no-aliases-found": "별명이 없습니다.", @@ -823,51 +2132,101 @@ "remove-alias": "장치 별명 삭제", "add-alias": "장치 별명 추가", "name-starts-with": "시작되는 이름", + "help-text": "필요에 따라 '%'를 사용하세요: '%device_name_contains%', '%device_name_ends', 'devic", "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": "커스터머에게 할당할 장치를 선택하십시오.", + "assign-to-customer": "고객에게 할당", + "assign-device-to-customer": "장치를 고객에게 할당", + "assign-device-to-customer-text": "고객에게 할당할 장치를 선택하십시오.", + "assign-device-to-edge-title": "엣지에 장치 할당", + "assign-device-to-edge-text": "엣지에 할당할 장치를 선택하세요", "make-public": "장치를 공개로 전환", "make-private": "장치를 비공개로 전환", "no-devices-text": "장치 없음", - "assign-to-customer-text": "장치에 할당할 커스터머를 선택하세요.", + "assign-to-customer-text": "장치에 할당할 고객을 선택하세요.", "device-details": "장치 상세 정보", "add-device-text": "장치 추가", "credentials": "자격 증명", "manage-credentials": "자격 증명 관리", "delete": "장치 삭제", "assign-devices": "장치 할당", - "assign-devices-text": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 커스터머에 할당", + "assign-devices-text": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 고객에 할당", "delete-devices": "장치 삭제", - "unassign-from-customer": "커스터머 할당 해제", + "unassign-from-customer": "고객 할당 해제", "unassign-devices": "장치 할당 취소", - "unassign-devices-action-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 커스터머에게서 할당 해제", + "unassign-devices-action-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 고객에게서 할당 해제", + "unassign-device-from-edge-title": "'{{deviceName}}' 장치 할당을 해제하시겠습니까?", + "unassign-device-from-edge-text": "확인 후 장치 할당이 해제되고", + "unassign-devices-from-edge": "엣지에서 장치 할당 해제", "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": "크리덴셜 보기", + "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-text": "장치가 할당 해제되고 고객은 액세스 할 수 없게됩니다.", "unassign-device": "장치 할당 취소", "unassign-devices-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }의 할당을 해제하시겠습니까??", - "unassign-devices-text": "선택된 장치가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", - "device-credentials": "장치 크리덴셜", - "credentials-type": "크리덴셜 타입", - "access-token": "억세스 토큰", + "unassign-devices-text": "선택된 장치가 할당 해제되고 고객은 액세스 할 수 없게됩니다.", + "device-credentials": "장치 자격 증명", + "loading-device-credentials": "장치 자격 증명 로딩 중...", + "credentials-type": "자격 증명 타입", + "access-token": "액세스 토큰", "access-token-required": "액세스 토큰을 입력하세요.", "access-token-invalid": "액세스 토큰 길이는 1 - 32 자 여야합니다.", + "certificate-pem-format": "PEM 형식의 인증서", + "certificate-pem-format-required": "인증서는 필수 항목입니다.", + "copy-access-token": "액세스 토큰 복사", + "copy-certificate": "인증서 복사", + "copy-client-id": "클라이언트 ID 복사", + "copy-user-name": "사용자 이름 복사", + "copy-password": "비밀번호 복사", + "generate-client-id": "클라이언트 ID 생성", + "generate-user-name": "사용자 이름 생성", + "generate-password": "비밀번호 생성", + "generate-access-token": "액세스 토큰 생성", + "lwm2m-security-config": { + "identity": "클라이언트 ID", + "identity-required": "클라이언트 ID는 필수 항목입니다.", + "identity-tooltip": "PSK 식별자는 [RFC4279]에 설명된 대로 최대 128바이트의 임의 PSK 식별자입니다.", + "client-key": "클라이언트 키", + "client-key-required": "클라이언트 키는 필수 항목입니다.", + "client-key-tooltip-prk": "RPK 공개 키 또는 ID는 표준 [RFC7250]에 있어야 하며 Base64로 인코딩되어야 합니다.", + "client-key-tooltip-psk": "PSK 키는 표준 [RFC4279]에 있어야 하며 HexDec 형식이어야 합니다: 32, 64, 128자", + "endpoint": "엔드포인트 클라이언트 이름", + "endpoint-required": "엔드포인트 클라이언트 이름은 필수 항목입니다.", + "client-public-key": "클라이언트 공개 키", + "client-public-key-hint": "클라이언트 공개 키가 비어 있으면 신뢰할 수 있는 인증서가 사용됩니다.", + "client-public-key-tooltip": "X509 공개 키는 DER 인코딩된 X509v3 형식이어야 하며 EC만 지원합니다.", + "mode": "보안 구성 모드", + "client-tab": "클라이언트 보안 구성", + "client-certificate": "클라이언트 인증서", + "bootstrap-tab": "부트스트랩 클라이언트", + "bootstrap-server": "부트스트랩 서버", + "lwm2m-server": "LwM2M 서버", + "client-reboot": "등록 업데이트 트리거", + "bootstrap-reboot": "부트스트랩 요청 트리거", + "client-publicKey-or-id": "클라이언트 공개 키 또는 ID", + "client-publicKey-or-id-required": "클라이언트 공개 키 또는 ID는 필수 항목입니다.", + "client-publicKey-or-id-tooltip-psk": "PSK 식별자는 [RFC4279]에 설명된 대로 최대 128바이트의 임의 PSK 식별자입니다.", + "client-publicKey-or-id-tooltip-rpk": "RPK 공개 키 또는 ID는 표준 [RFC7250]에 있어야 하며 Base64로 인코딩되어야 합니다.", + "client-publicKey-or-id-tooltip-x509": "X509 공개 키는 DER 인코딩된 X509v3 형식이어야 하며 EC만 지원합니다.", + "client-secret-key": "클라이언트 비밀 키", + "client-secret-key-required": "클라이언트 비밀 키는 필수 항목입니다.", + "client-secret-key-tooltip-psk": "PSK 키는 표준 [RFC4279]에 있어야 하며 HexDec 형식이어야 합니다: 32, 64, 128자", + "client-secret-key-tooltip-prk": "RPK 비밀 키는 PKCS_8 형식(DER 인코딩, 표준 [RFC5958])이어야 합니다.", + "client-secret-key-tooltip-x509": "X509 비밀 키는 PKCS_8 형식(DER 인코딩, 표준 [RFC5958])이어야 합니다." + }, "client-id": "클라이언트 ID", "client-id-pattern": "잘못된 문자가 포함되어 있습니다.", "user-name": "사용자 이름", @@ -883,23 +2242,34 @@ "any-device": "모든 장치", "no-device-types-matching": "'{{entitySubtype}}'에 해당되는 장치 유형을 찾을 수 없습니다.", "device-type-list-empty": "아무 장치 유형도 선택되지 않았습니다.", + "device-profile-type-list-empty": "최소 하나의 장치 프로필을 선택해야 합니다.", "device-types": "장치 유형", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "label-max-length": "레이블은 256자 미만이어야 합니다.", "description": "설명", "label": "라벨", "events": "이벤트", "details": "상세", "copyId": "장치 아이디 복사", "copyAccessToken": "액세스 토큰 복사", - "copy-mqtt-authentication": "MQTT 크리덴셜 복사", + "copy-mqtt-authentication": "MQTT 자격 증명 복사", "idCopiedMessage": "장치 아이디가 클립보드에 복사되었습니다.", - "accessTokenCopiedMessage": "장치 억세스 토큰이 클립보드에 복사되었습니다.", - "mqtt-authentication-copied-message": "장치 MQTT 크리덴셜이 클립보드에 복사되었습니다.", - "assignedToCustomer": "커스터머에 할당됨", + "accessTokenCopiedMessage": "장치 액세스 토큰이 클립보드에 복사되었습니다.", + "mqtt-authentication-copied-message": "장치 MQTT 자격 증명이 클립보드에 복사되었습니다.", + "assignedToCustomer": "고객에 할당됨", "unable-delete-device-alias-title": "장치 별명을 삭제할 수 없습니다.", "unable-delete-device-alias-text": "'{{deviceAlias}}' 장치 별명을 삭제할 수 없습니다. 다음 위젯에서 사용하고 있습니다.
    {{widgetsList}}", "is-gateway": "게이트웨이 여부", + "overwrite-activity-time": "연결된 장치의 활동 시간 덮어쓰기", + "device-filter-title": "장치 필터", + "filter-title": "필터", + "device-state": "장치 상태", + "state": "상태", + "any": "전체", + "active": "활성", + "inactive": "비활성", "public": "공개", "device-public": "장치가 공개됨", "select-device": "장치 선택", @@ -911,161 +2281,739 @@ "transport-configuration": "전송 설정", "wizard": { "device-details": "장치 상세 정보" + }, + "unassign-devices-from-edge-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }의 할당을 해제하시겠습니까??", + "unassign-devices-from-edge-text": "확인 후 선택한 모든 장치의 할당이 해제되고", + "time": "시간", + "connectivity": { + "check-connectivity": "연결 확인", + "device-created-check-connectivity": "장치가 생성되었습니다. 연결을 확인하세요!", + "loading-check-connectivity-command": "연결 확인 명령 로딩 중...", + "use-following-instructions": "다음 지침을 사용하여 장치 대신 텔레메트리를 전송하세요.", + "execute-following-command": "다음 명령을 실행하세요", + "install-curl-windows": "Windows 10 b17063부터 cURL이 기본적으로 사용할 수 있습니다.", + "install-curl-macos": "Mac OS X 10.2 6C115 (Jaguar)부터 cURL이 기본적으로 사용할 수 있습니다.", + "install-mqtt-windows": "mosquitto_pub을 다운로드, 설치, 설정 및 실행하는 지침을 사용하세요.", + "install-coap-client": "coap-client를 다운로드, 설치, 설정 및 실행하는 지침을 사용하세요.", + "install-necessary-client-tools": "필요한 클라이언트 도구 설치", + "mqtts-x509-command": "다음 문서를 사용하여 인증서로 MQTT를 통해 장치를 연결하세요.", + "coaps-x509-command": "다음 문서를 사용하여 인증서로 CoAP over DTLS를 통해 장치를 연결하세요.", + "snmp-command": "다음 문서를 사용하여 SNMP를 통해 장치를 연결하세요.", + "sparkplug-command": "다음 문서를 사용하여 MQTT Sparkplug를 통해 장치를 연결하세요.", + "lwm2m-command": "다음 문서를 사용하여 LWM2M을 통해 장치를 연결하세요." } }, + "dynamic-form": { + "property": { + "properties": "속성", + "property": "속성", + "id": "ID", + "name": "이름", + "type": "유형", + "type-text": "텍스트", + "type-password": "비밀번호", + "type-textarea": "텍스트 영역", + "type-number": "숫자", + "type-switch": "스위치", + "type-select": "선택", + "type-radios": "라디오 버튼", + "type-datetime": "날짜/시간", + "type-image": "이미지", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "마크다운", + "type-color": "색상", + "type-color-settings": "색상 설정", + "type-font": "글꼴", + "type-units": "단위", + "type-icon": "아이콘", + "type-fieldset": "필드셋", + "type-array": "배열", + "type-html-section": "HTML 섹션", + "group-title": "그룹 제목", + "no-properties": "구성된 속성 없음", + "add-property": "속성 추가", + "property-settings": "속성 설정", + "remove-property": "속성 제거", + "default-value": "기본값", + "value-required": "값 필요", + "number-settings": "숫자 설정", + "min": "최소", + "max": "최대", + "step": "단계", + "selected-options-limit": "선택 옵션 제한", + "advanced-ui-settings": "고급 UI 설정", + "disable-on-property": "속성 비활성화", + "disable-on-property-none": "없음 (필드 항상 활성화)", + "display-condition-function": "표시 조건 함수", + "sub-label": "보조 라벨", + "vertical-divider-after": "다음에 수직 구분선", + "input-field-suffix": "입력 필드 접미사", + "property-row-classes": "속성 행 클래스", + "property-field-classes": "속성 필드 클래스", + "not-unique-property-ids-error": "속성 ID는 고유해야 합니다!", + "enable-multiple-select": "다중 선택 활성화", + "allow-empty-select-option": "빈 옵션 허용", + "select-options": "선택 옵션", + "not-unique-select-option-value-error": "선택 옵션 값은 고유해야 합니다!", + "value": "값", + "label": "라벨", + "add-option": "옵션 추가", + "no-options": "구성된 옵션 없음", + "remove-option": "옵션 제거", + "textarea-rows": "텍스트 영역 행 수", + "help-id": "도움말 ID", + "buttons-direction": "버튼 방향", + "direction-row": "행", + "direction-column": "열", + "radio-button-options": "라디오 버튼 옵션", + "datetime-type": "날짜/시간 필드 유형", + "datetime-type-date": "날짜", + "datetime-type-time": "시간", + "datetime-type-datetime": "날짜/시간", + "enable-clear-button": "지우기 버튼 활성화", + "html-section-settings": "HTML 섹션 설정", + "html-section-classes": "HTML 섹션 클래스", + "html-section-content": "HTML 섹션 내용", + "array-item": "배열 항목", + "item-type": "항목 유형", + "item-name": "항목 이름", + "no-items": "항목 없음", + "support-unit-conversion": "단위 변환 지원" + }, + "clear-form": "양식 지우기", + "clear-form-prompt": "모든 양식 속성을 제거하시겠습니까?", + "import-form": "JSON에서 양식 가져오기", + "export-form": "양식을 JSON으로 내보내기", + "json-file": "JSON 파일", + "json-content": "JSON 내용", + "invalid-form-json-file-error": "JSON에서 양식을 가져올 수 없습니다: 잘못된 양식 JSON 데이터 구조입니다." + }, + "asset-profile": { + "asset-profile": "자산 프로필", + "asset-profiles": "자산 프로필", + "all-asset-profiles": "모두", + "add": "자산 프로필 추가", + "edit": "자산 프로필 수정", + "asset-profile-details": "자산 프로필 상세", + "no-asset-profiles-text": "자산 프로필이 없습니다", + "search": "자산 프로필 검색", + "selected-asset-profiles": "{ count, plural, =1 {1개 자산 프로필} other {#개 자산 프로필} } 선택됨", + "no-asset-profiles-matching": "'{{entity}}'와(과) 일치하는 자산 프로필이 없습니다.", + "asset-profile-required": "자산 프로필이 필요합니다", + "idCopiedMessage": "자산 프로필 ID가 클립보드에 복사되었습니다", + "set-default": "자산 프로필 기본값으로 설정", + "delete": "자산 프로필 삭제", + "copyId": "자산 프로필 ID 복사", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "new-device-profile-name": "자산 프로필 이름", + "new-device-profile-name-required": "자산 프로필 이름이 필요합니다.", + "name": "이름", + "name-required": "이름을 입력하세요.", + "image": "자산 프로필 이미지", + "description": "설명", + "default": "기본값", + "default-rule-chain": "기본 규칙 사슬", + "default-edge-rule-chain": "기본 엣지 규칙 사슬", + "default-edge-rule-chain-hint": "이 자산 프로필의 자산에 대한 들어오는 데이터를 처리하는 규칙 사슬로 엣지에서 사용됩니다", + "mobile-dashboard": "모바일 대시보드", + "mobile-dashboard-hint": "모바일 애플리케이션에서 자산 상세 대시보드로 사용됩니다", + "select-queue-hint": "드롭다운 목록에서 선택하세요.", + "delete-asset-profile-title": "자산 프로필 '{{assetProfileName}}'을(를) 삭제하시겠습니까?", + "delete-asset-profile-text": "주의: 확인 후 자산 프로필과 모든 관련 데이터가", + "delete-asset-profiles-title": "{ count, plural, =1 {1개 자산 프로필} other {#개 자산 프로필} }을(를) 삭제하시겠습니까?", + "delete-asset-profiles-text": "주의: 확인 후 선택한 모든 자산 프로필이 제거되고", + "set-default-asset-profile-title": "자산 프로필 '{{assetProfileName}}'을(를) 기본값으로 설정하시겠습니까?", + "set-default-asset-profile-text": "확인 후 자산 프로필이 기본값으로 표시되고", + "no-asset-profiles-found": "자산 프로필이 없습니다.", + "create-new-asset-profile": "새로 만들기!", + "create-asset-profile": "새 자산 프로필 만들기", + "import": "자산 프로필 가져오기", + "export": "자산 프로필 내보내기", + "export-failed-error": "자산 프로필을 내보낼 수 없습니다: {{error}}", + "asset-profile-file": "자산 프로필 파일", + "invalid-asset-profile-file-error": "자산 프로필을 가져올 수 없습니다: 잘못된 자산 프로필 데이터 구조입니다." + }, "device-profile": { - "device-profile": "장치 프로파일", - "device-profiles": "장치 프로파일", + "device-profile": "장치 프로필", + "device-profiles": "장치 프로필", "all-device-profiles": "모두", - "add": "장치 프로파일 추가", - "edit": "장치 프로파일 편집", - "device-profile-details": "장치 프로파일 상세 정보", - "no-device-profiles-text": "아무 장치 프로파일도 찾을 수 없습니다", - "search": "장치 프로파일 검색", - "selected-device-profiles": "{ count, plural, =1 {1 개 장치 프로파일} other {# 개 장치 프로파일} } 선택됨", - "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", - "device-profile-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", + "add": "장치 프로필 추가", + "edit": "장치 프로필 편집", + "device-profile-details": "장치 프로필 상세 정보", + "no-device-profiles-text": "아무 장치 프로필도 찾을 수 없습니다", + "search": "장치 프로필 검색", + "selected-device-profiles": "{ count, plural, =1 {1 개 장치 프로필} other {# 개 장치 프로필} } 선택됨", + "no-device-profiles-matching": "'{{entity}}'와(과) 일치하는 장치 프로필을 찾을 수 없습니다.", + "device-profile-required": "장치 프로필을 입력하세요", + "idCopiedMessage": "장치 프로필 ID가 클립보드에 복사되었습니다.", + "set-default": "장치 프로필 기본값으로 설정", + "delete": "장치 프로필 삭제", + "copyId": "장치 프로필 ID 복사", + "name-max-length": "이름은 256자 미만이어야 합니다.", "name": "이름", "name-required": "이름을 입력하세요.", - "type": "프로파일 유형", - "type-required": "프로파일 유형을 입력하세요.", + "type": "프로필 유형", + "type-required": "프로필 유형을 입력하세요.", "type-default": "기본값", + "image": "장치 프로필 이미지", "transport-type": "전송 유형", "transport-type-required": "전송 유형을 입력하세요.", "transport-type-default": "기본값", "transport-type-default-hint": "기본 MQTT, HTTP, CoAP 전송을 지원", "transport-type-mqtt": "MQTT", "transport-type-mqtt-hint": "고급 MQTT 전송 설정을 활성화", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "고급 CoAP 전송 설정을 활성화합니다.", "transport-type-lwm2m": "LWM2M", "transport-type-lwm2m-hint": "LWM2M 전송 유형", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "SNMP 전송 구성을 지정하세요.", + "transport-type-http": "HTTP", "description": "설명", "default": "기본값", - "profile-configuration": "프로파일 설정", + "profile-configuration": "프로필 설정", "transport-configuration": "전송 설정", "default-rule-chain": "기본 규칙 사슬", - "select-queue-hint": "Select from a drop-down list.", - "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 개 장치 프로파일} other {# 개 장치 프로파일} }?", - "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-topic-filters-unique": "MQTT device topic filters need to be unique.", - "mqtt-device-payload-type": "MQTT device payload", + "default-edge-rule-chain": "기본 엣지 규칙 사슬", + "default-edge-rule-chain-hint": "엣지에서 이 장치 프로필의 장치에 대한 들어오는 데이터를 처리하는 규칙 체인으로 사용됩니다.", + "mobile-dashboard": "모바일 대시보드", + "mobile-dashboard-hint": "모바일 애플리케이션에서 장치 세부 정보 대시보드로 사용됩니다.", + "select-queue-hint": "드롭다운 목록에서 선택하세요.", + "delete-device-profile-title": "장치 프로필 '{{deviceProfileName}}'을(를) 삭제하시겠습니까?", + "delete-device-profile-text": "주의: 확인 후 장치 프로필과 관련된 모든 데이터(OTA 업데이트 포함)는 복구할 수 없게 됩니다.", + "delete-device-profiles-title": "정말 { count, plural, =1 {1개의 장치 프로필} other {#개의 장치 프로필} }을 삭제하시겠습니까?", + "delete-device-profiles-text": "주의: 확인 후 선택한 모든 장치 프로필이 제거되고 관련된 모든 데이터(OTA 업데이트 포함)는 복구할 수 없게 됩니다.", + "set-default-device-profile-title": "장치 프로필 '{{deviceProfileName}}'을(를) 기본값으로 설정하시겠습니까?", + "set-default-device-profile-text": "확인 후 장치 프로필이 기본값으로 표시되며 프로필이 지정되지 않은 새 장치에 사용됩니다.", + "no-device-profiles-found": "장치 프로필을 찾을 수 없습니다.", + "create-new-device-profile": "새로 만들기!", + "mqtt-device-topic-filters": "MQTT 장치 토픽 필터", + "mqtt-device-topic-filters-unique": "MQTT 장치 토픽 필터는 고유해야 합니다.", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) 노드.", + "mqtt-device-topic-filters-spark-plug-hint": "Sparkplug B 페이로드 및 토픽 형식의 EoN 노드에서 연결을 허용합니다.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "속성으로 저장할 SparkPlug 지표.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "장치 속성으로 저장될 SparkPlug 지표의 이름입니다. 다른 모든 지표는", + "mqtt-device-payload-type": "MQTT 장치 페이로드", "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 publish topic filter", - "attributes-subscribe-topic-filter": "Attributes subscribe topic filter", - "attributes-topic-filter-required": "Attributes publish topic filter is required.", - "attributes-subscribe-topic-filter-required": "Attributes subscribe topic is required", - "telemetry-proto-schema": "Telemetry proto schema", - "telemetry-proto-schema-required": "Telemetry proto schema is required.", - "attributes-proto-schema": "Attributes proto schema", - "attributes-proto-schema-required": "Attributes proto schema 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/#.", + "mqtt-enable-compatibility-with-json-payload-format": "다른 페이로드 형식과의 호환성 사용.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "사용하면 플랫폼은 기본적으로 Protobuf 페이로드 형식을 사용합니다. 파싱이", + "mqtt-use-json-format-for-default-downlink-topics": "기본 다운링크 토픽에 JSON 형식 사용", + "mqtt-use-json-format-for-default-downlink-topics-hint": "사용하면 플랫폼은 속성 및 RPC를 푸시하는 데 JSON 페이로드 형식을 사용합니다.", + "mqtt-send-ack-on-validation-exception": "PUBLISH 메시지 유효성 검사 실패 시 PUBACK 전송", + "mqtt-send-ack-on-validation-exception-hint": "기본적으로 플랫폼은 메시지 유효성 검사 실패 시 MQTT 세션을 닫습니다.", + "mqtt-protocol-version": "프로토콜 버전", + "snmp-add-mapping": "SNMP 매핑 추가", + "snmp-mapping-not-configured": "OID를 시계열/텔레메트리에 매핑하도록 구성되지 않음", + "snmp-timseries-or-attribute-name": "매핑을 위한 시계열/속성 이름", + "snmp-timseries-or-attribute-type": "매핑을 위한 시계열/속성 유형", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "페이로드 유형은 필수 항목입니다.", + "coap-device-type": "CoAP 장치 유형", + "coap-device-payload-type": "CoAP 장치 페이로드", + "coap-device-type-required": "CoAP 장치 유형은 필수 항목입니다.", + "coap-device-type-default": "기본값", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "단일 레벨 [+] 및 다중 레벨 [#] 와일드카드가 지원됩니다.", + "telemetry-topic-filter": "텔레메트리 토픽 필터", + "telemetry-topic-filter-required": "텔레메트리 토픽 필터는 필수 항목입니다.", + "attributes-topic-filter": "속성 게시 토픽 필터", + "attributes-subscribe-topic-filter": "속성 구독 토픽 필터", + "attributes-topic-filter-required": "속성 게시 토픽 필터는 필수 항목입니다.", + "attributes-subscribe-topic-filter-required": "속성 구독 토픽은 필수 항목입니다.", + "telemetry-proto-schema": "텔레메트리 프로토 스키마", + "telemetry-proto-schema-required": "텔레메트리 프로토 스키마는 필수 항목입니다.", + "attributes-proto-schema": "속성 프로토 스키마", + "attributes-proto-schema-required": "속성 프로토 스키마는 필수 항목입니다.", + "rpc-response-proto-schema": "RPC 응답 프로토 스키마", + "rpc-response-proto-schema-required": "RPC 응답 프로토 스키마는 필수 항목입니다.", + "rpc-response-topic-filter": "RPC 응답 토픽 필터", + "rpc-response-topic-filter-required": "RPC 응답 토픽 필터는 필수 항목입니다.", + "rpc-request-proto-schema": "RPC 요청 프로토 스키마", + "rpc-request-proto-schema-required": "RPC 요청 프로토 스키마는 필수 항목입니다.", + "rpc-request-proto-schema-hint": "RPC 요청 메시지에는 항상 다음 필드가 있어야 합니다: string method = 1; int32 requestId", + "not-valid-pattern-topic-filter": "유효하지 않은 패턴 토픽 필터", + "not-valid-single-character": "단일 수준 와일드카드 문자의 잘못된 사용", + "not-valid-multi-character": "다중 수준 와일드카드 문자의 잘못된 사용", + "single-level-wildcards-hint": "[+]는 모든 토픽 필터 레벨에 적합합니다. 예: v1/devices/+/telemetry 또는 +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#]는 토픽 필터 자체를 대체할 수 있으며 토픽의 마지막 기호여야 합니다. 예: # 또는 v1/devices/me/#.", "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-unique": "Alarm type must be unique within the device profile alarm rules.", - "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", - "propagate-alarm": "Propagate alarm", - "alarm-rule-relation-types-list": "Relation types", - "alarm-rule-relation-types-list-hint": "Defines relation types to filter the related entities. If not set, the alarm will be propagated to all related entities.", - "alarm-rule-condition": "Alarm rule condition", - "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", - "edit-alarm-rule-condition": "Edit alarm rule condition", + "alarm-rules-with-count": "알람 규칙 ({{count}})", + "no-alarm-rules": "구성된 알람 규칙 없음", + "add-alarm-rule": "알람 규칙 추가", + "edit-alarm-rule": "알람 규칙 편집", + "alarm-type": "알람 유형", + "alarm-type-required": "알람 유형은 필수 항목입니다.", + "alarm-type-unique": "알람 유형은 장치 프로필 알람 규칙 내에서 고유해야 합니다.", + "alarm-type-max-length": "알람 유형은 256자 미만이어야 합니다.", + "create-alarm-pattern": "{{alarmType}} 알람 생성", + "create-alarm-rules": "알람 규칙 생성", + "no-create-alarm-rules": "구성된 생성 조건 없음", + "add-create-alarm-rule-prompt": "알람 생성 규칙을 추가하세요.", + "clear-alarm-rule": "알람 규칙 지우기", + "no-clear-alarm-rule": "지움 조건이 구성되지 않음", + "add-create-alarm-rule": "생성 조건 추가", + "add-clear-alarm-rule": "지움 조건 추가", + "select-alarm-severity": "알람 심각도 선택", + "alarm-severity-required": "알람 심각도는 필수 항목입니다.", + "condition-duration": "조건 기간", + "condition-duration-value": "기간 값", + "condition-duration-time-unit": "시간 단위", + "condition-duration-value-range": "기간 값은 1에서 2147483647 범위여야 합니다.", + "condition-duration-value-pattern": "기간 값은 정수여야 합니다.", + "condition-duration-value-required": "기간 값은 필수 항목입니다.", + "condition-duration-time-unit-required": "시간 단위는 필수 항목입니다.", + "advanced-settings": "고급 설정", + "alarm-rule-additional-info": "추가 정보", + "edit-alarm-rule-additional-info": "추가 정보 수정", + "alarm-rule-additional-info-placeholder": "알람 세부 정보 내에 표시할 설명과 조정 사항을 여기에 입력하세요.", + "alarm-rule-additional-info-hint": "힌트: ${keyName
    누락된 규칙 체인 목록:
    {{missingRuleChains}}", + "upgrade-instructions": "업그레이드 지침", + "widget-datasource-error": "이 위젯은 엣지 엔티티 데이터소스만 지원합니다", + "connected": "연결됨", + "disconnected": "연결 해제됨" + }, + "edge-event": { + "type-dashboard": "대시보드", + "type-asset": "자산", + "type-device": "장치", + "type-device-profile": "장치 프로필", + "type-asset-profile": "자산 프로필", + "type-entity-view": "엔티티 뷰", + "type-alarm": "알람", + "type-rule-chain": "규칙 사슬", + "type-rule-chain-metadata": "규칙 사슬 메타데이터", + "type-edge": "엣지", + "type-user": "사용자", + "type-tenant": "테넌트", + "type-tenant-profile": "테넌트 프로필", + "type-customer": "고객", + "type-relation": "관계", + "type-widgets-bundle": "위젯 번들", + "type-widgets-type": "위젯 유형", + "type-admin-settings": "관리자 설정", + "type-ota-package": "OTA 패키지", + "type-queue": "큐", + "action-type-added": "추가됨", + "action-type-deleted": "삭제됨", + "action-type-updated": "수정됨", + "action-type-post-attributes": "속성 게시", + "action-type-attributes-updated": "속성 업데이트됨", + "action-type-attributes-deleted": "속성 삭제됨", + "action-type-timeseries-updated": "시계열 업데이트됨", + "action-type-credentials-updated": "자격 증명 업데이트됨", + "action-type-assigned-to-customer": "고객에게 할당", + "action-type-unassigned-from-customer": "지정된 고객 해제", + "action-type-relation-add-or-update": "관계 추가 또는 업데이트", + "action-type-relation-deleted": "관계 삭제됨", + "action-type-rpc-call": "RPC 호출", + "action-type-alarm-ack": "알람 확인", + "action-type-alarm-clear": "알람 해제", + "action-type-alarm-assigned": "알람 할당됨", + "action-type-alarm-unassigned": "알람 할당 해제", + "action-type-assigned-to-edge": "엣지에 할당됨", + "action-type-unassigned-from-edge": "엣지에서 할당 해제됨", + "action-type-credentials-request": "자격 증명 요청", + "action-type-entity-merge-request": "엔티티 병합 요청" }, "error": { "unable-to-connect": "서버에 연결할 수 없습니다! 인터넷 연결을 확인하십시오.", @@ -1073,58 +3021,72 @@ "unknown-error": "알 수 없는 오류" }, "entity": { - "entity": "개체", - "entities": "개체", - "aliases": "개체 별명", - "entity-alias": "개체 별명", - "unable-delete-entity-alias-title": "개체의 별명을 삭제할 수 없습니다.", - "unable-delete-entity-alias-text": "개체 별명 '{{entityAlias}}'은 다음 위젯에서 사용되고 있어 삭제할 수 없습니다:
    {{widgetsList}}", - "duplicate-alias-error": "별명 '{{alias}}'은 이미 사용중 입니다.
    대시보드 내에서 개체의 별명은 중복될 수 없습니다.", + "entity": "엔티티", + "entities": "엔티티", + "entities-count": "엔티티 수", + "alarms-count": "알람 수", + "aliases": "엔티티 별명", + "aliases-short": "별명", + "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}}'에 해당되는 개체 유형을 찾을 수 없습니다.", + "alias-required": "엔티티 별명을 입력하세요.", + "remove-alias": "엔티티 별명 삭제", + "add-alias": "엔티티 별명 추가", + "edit-alias": "엔티티 별명 편집", + "entity-list": "엔티티 목록", + "entity-type": "엔티티 유형", + "entity-types": "엔티티 유형", + "entity-type-list": "엔티티 유형 목록", + "any-entity": "모든 엔티티", + "add-entity-type": "엔티티 유형 추가", + "enter-entity-type": "엔티티 유형 입력", + "no-entities-matching": "'{{entity}}'에 해당되는 엔티티를 찾을 수 없습니다.", + "no-entities-text": "아무 엔티티도 없습니다", + "no-entity-types-matching": "'{{entityType}}'에 해당되는 엔티티 유형을 찾을 수 없습니다.", "name-starts-with": "다음으로 시작하는 이름", + "help-text": "필요에 따라 '%'를 사용하세요: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with%'.", "use-entity-name-filter": "필터 사용", - "entity-list-empty": "아무 개체도 선택되지 않았습니다.", - "entity-name-filter-required": "개체 이름 필터를 입력하세요.", - "entity-name-filter-no-entity-matched": "'{{entity}}'로 시작되는 개체를 찾을 수 없습니다.", + "entity-list-empty": "아무 엔티티도 선택되지 않았습니다.", + "entity-type-list-required": "최소 하나의 엔티티 유형을 선택해야 합니다.", + "entity-name-filter-required": "엔티티 이름 필터를 입력하세요.", + "entity-name-filter-no-entity-matched": "'{{entity}}'로 시작되는 엔티티를 찾을 수 없습니다.", "all-subtypes": "모두", - "select-entities": "선택된 개체", - "no-aliases-found": "아무 개체도 없습니다.", - "no-alias-matching": "개체 '{{alias}}'을(를) 찾을 수 없습니다.", + "select-entities": "선택된 엔티티", + "no-aliases-found": "아무 엔티티도 없습니다.", + "no-alias-matching": "엔티티 '{{alias}}'을(를) 찾을 수 없습니다.", "create-new-alias": "새로 별명 만들기", + "create-new": "새로 만들기", "key": "키", "key-name": "키 이름", "no-keys-found": "아무 키도 찾을 수 없습니다.", "no-key-matching": "'{{key}}'를 찾을 수 없습니다.", "create-new-key": "새로 키 만들기", "type": "유형", - "type-required": "개체의 유형을 입력하세요.", + "type-required": "엔티티의 유형을 입력하세요.", "type-device": "장치", "type-devices": "장치", "list-of-devices": "{ count, plural, =1 {1 개 장치} other {# 개 장치} }", "device-name-starts-with": "이름이 '{{prefix}}'로 시작되는 장치", - "type-device-profile": "Device profile", - "type-device-profiles": "Device profiles", - "list-of-device-profiles": "{ count, plural, =1 {1 개 장치 프로파일} other {# 개 장치 프로파일 목록} }", - "device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'", + "type-device-profile": "장치 프로필", + "type-device-profiles": "장치 프로필", + "clear-selected-profiles": "선택한 프로필 지우기", + "list-of-device-profiles": "{ count, plural, =1 {1 개 장치 프로필} other {# 개 장치 프로필 목록} }", + "device-profile-name-starts-with": "이름이 '{{prefix}}'로 시작하는 장치 프로필", + "type-asset-profile": "자산 프로필", + "type-asset-profiles": "자산 프로필", + "list-of-asset-profiles": "{ count, plural, =1 {하나의 자산 프로필} other {#개 자산 프로필 목록} }", + "asset-profile-name-starts-with": "이름이 '{{prefix}}'로 시작하는 자산 프로필", "type-asset": "자산", "type-assets": "자산", "list-of-assets": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }", "asset-name-starts-with": "이름이 '{{prefix}}'로 시작되는 자산", - "type-entity-view": "개체 보기", - "type-entity-views": "개체 보기", + "type-entity-view": "엔티티 뷰", + "type-entity-views": "엔티티 뷰", "list-of-entity-views": "{ count, plural, =1 {1 개 자산 보기} other {# 개 자산 보기} }", "entity-view-name-starts-with": "이름이 '{{prefix}}'로 시작되는 자산 보기", "type-rule": "규칙", @@ -1139,14 +3101,14 @@ "type-tenants": "테넌트", "list-of-tenants": "{ count, plural, =1 {1 개 테넌트} other {# 개 테넌트} }", "tenant-name-starts-with": "이름이 '{{prefix}}'로 시작되는 테넌트", - "type-tenant-profile": "테넌트 프로파일", - "type-tenant-profiles": "테넌트 프로파일", - "list-of-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로파일} other {# 개 테넌트 프로파일 목록} }", - "tenant-profile-name-starts-with": "'이름이 {{prefix}}'로 시작하는 테넌트 프로파일", - "type-customer": "커스터머", - "type-customers": "커스터머", - "list-of-customers": "{ count, plural, =1 {1 개 커스터머} other {# 개 커스터머} }", - "customer-name-starts-with": "이름이 '{{prefix}}'로 시작되는 커스터머", + "type-tenant-profile": "테넌트 프로필", + "type-tenant-profiles": "테넌트 프로필", + "list-of-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로필} other {# 개 테넌트 프로필 목록} }", + "tenant-profile-name-starts-with": "'이름이 {{prefix}}'로 시작하는 테넌트 프로필", + "type-customer": "고객", + "type-customers": "고객", + "list-of-customers": "{ count, plural, =1 {1 개 고객} other {# 개 고객} }", + "customer-name-starts-with": "이름이 '{{prefix}}'로 시작되는 고객", "type-user": "사용자", "type-users": "사용자", "list-of-users": "{ count, plural, =1 {1 개 사용자} other {# 개 사용자} }", @@ -1167,19 +3129,84 @@ "type-rulenodes": "규칙 노드", "list-of-rulenodes": "{ count, plural, =1 {1 개 규칙 노드} other {# 개 규칙 노드의 목록} }", "rulenode-name-starts-with": "이름이 '{{prefix}}'로 시작되는 규칙 노트", - "type-current-customer": "현재 커스터머", + "type-api-key": "API 키", + "type-api-keys": "API 키", + "type-current-customer": "현재 고객", "type-current-tenant": "현재 테넌트", "type-current-user": "현재 사용자", "type-current-user-owner": "현재 소유자", - "search": "개체 검색", - "selected-entities": "{ count, plural, =1 {1 개체} other {# 개체} } 선택됨", - "entity-name": "개체 이름", - "entity-label": "개체 라벨", - "details": "개체 상세", - "no-entities-prompt": "아무 개체도 없습니다", + "type-calculated-field": "계산 필드", + "type-calculated-fields": "계산 필드", + "type-ai-model": "AI 모델", + "type-ai-models": "AI 모델", + "type-widgets-bundle": "위젯 번들", + "type-widgets-bundles": "위젯 번들", + "list-of-widgets-bundles": "{ count, plural, =1 {하나의 위젯 번들} other {#개 위젯 번들 목록} }", + "type-widget": "위젯", + "type-widgets": "위젯", + "list-of-widgets": "{ count, plural, =1 {1개 위젯} other {#개 위젯 목록} }", + "search": "엔티티 검색", + "selected-entities": "{ count, plural, =1 {1 엔티티} other {# 엔티티} } 선택됨", + "entity-name": "엔티티 이름", + "entity-label": "엔티티 라벨", + "details": "엔티티 상세", + "no-entities-prompt": "아무 엔티티도 없습니다", "no-data": "표시할 데이터가 없습니다", + "show-all-columns": "모두 표시", "columns-to-display": "표시할 컬럼", - "type-api-usage-state": "Api 사용량 상태" + "type-api-usage-state": "Api 사용량 상태", + "type-edge": "엣지", + "type-edges": "엣지", + "list-of-edges": "{ count, plural, =1 {1개 Edge} other {#개 Edge 목록} }", + "edge-name-starts-with": "이름이 '{{prefix}}'로 시작하는 Edge", + "version-conflict": { + "message": "기존 버전을 덮어쓰거나 변경 사항을 삭제하고 최신 버전을 로드하시겠습니까?", + "link": "이것을 사용하여 {{entityType}}의 버전을 다운로드할 수 있습니다.", + "overwrite": "버전 덮어쓰기", + "discard": "변경 취소" + }, + "type-tb-resource": "리소스", + "type-tb-resources": "리소스", + "list-of-tb-resources": "{ count, plural, =1 {1개 리소스} other {#개 리소스 목록} }", + "type-ota-package": "OTA 패키지", + "type-ota-packages": "OTA 패키지", + "list-of-ota-packages": "{ count, plural, =1 {1개 OTA 패키지} other {#개 OTA 패키지 목록} }", + "type-rpc": "RPC", + "type-queue": "큐", + "type-queue-stats": "큐 통계", + "type-queues-stats": "큐 통계", + "type-notification": "알림", + "type-notification-rule": "알림 규칙", + "type-notification-rules": "알림 규칙", + "list-of-notification-rules": "{ count, plural, =1 {하나의 알림 규칙} other {#개 알림 규칙 목록} }", + "type-notification-target": "알림 수신자", + "type-notification-targets": "알림 수신자", + "list-of-notification-targets": "{ count, plural, =1 {하나의 알림 수신자} other {#개 알림 수신자 목록} }", + "type-notification-request": "알림 요청", + "type-notification-template": "알림 템플릿", + "type-notification-templates": "알림 템플릿", + "list-of-notification-templates": "{ count, plural, =1 {하나의 알림 템플릿} other {#개 알림 템플릿 목록} }", + "link": "링크", + "type-oauth2-client": "OAuth 2.0 클라이언트", + "type-oauth2-clients": "OAuth 2.0 클라이언트", + "list-of-oauth2-clients": "{ count, plural, =1 {하나의 OAuth 2.0 클라이언트} other {#개 OAuth 2.0 클라이언트 목록} }", + "type-domain": "도메인", + "type-domains": "도메인", + "list-of-domains": "{ count, plural, =1 {1개 도메인} other {#개 도메인 목록} }", + "type-mobile-app": "모바일 애플리케이션", + "type-mobile-apps": "모바일 애플리케이션", + "list-of-mobile-apps": "{ count, plural, =1 {하나의 모바일 애플리케이션} other {#개 모바일 애플리케이션 목록} }", + "type-mobile-app-bundle": "모바일 번들", + "type-mobile-app-bundles": "모바일 번들", + "list-of-mobile-app-bundles": "{ count, plural, =1 {하나의 모바일 번들} other {#개 모바일 번들 목록} }", + "limit-reached": "한도 도달", + "limit-reached-text": "{{ entities }}의 한도에 도달했습니다. 더 추가하려면 시스템 관리자에게 {{ entity }} 한도 증가를 요청하세요.", + "request-limit-increase": "한도 증가 요청", + "request-sysadmin-text": "시스템 관리자입니까?", + "login-here": "여기에 로그인", + "to-increase-limit": "하여 한도를 늘리세요.", + "increase-limit-request-sent-title": "시스템 관리자에게 한도 증가를 요청하는 자동 요청을 보냈습니다", + "increase-limit-request-sent-text": "요청을 검토하고 설정을 업데이트할 시간을 기다려 주세요. 변경 사항을 보려면 이 페이지를 새로고침해야 할 수 있습니다." }, "entity-field": { "created-time": "생성 일시", @@ -1187,7 +3214,7 @@ "type": "유형", "first-name": "이름", "last-name": "성", - "email": "Email", + "email": "이메일", "title": "제목", "country": "국가", "state": "주", @@ -1196,88 +3223,94 @@ "address2": "주소 2", "zip": "우편번호", "phone": "전화", - "label": "라벨" + "label": "라벨", + "queue-name": "큐 이름", + "service-id": "서비스 ID", + "owner-name": "소유자 이름", + "owner-type": "소유자 유형" }, "entity-view": { - "entity-view": "개체 보기", - "entity-view-required": "개체 보기를 입력하세요.", - "entity-views": "개체 보기", - "management": "개체 보기 관리", - "view-entity-views": "개체 보기 보기", - "entity-view-alias": "개체 보기 별명", - "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": "개체 보기 상세 정보", - "add-entity-view-text": "새로운 개체 보기 추가", - "delete": "Delete entity view", - "assign-entity-views": "개체 보기 할당", - "assign-entity-views-text": "{ count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }를 커스터머에 할당", - "delete-entity-views": "Delete entity views", - "unassign-from-customer": "Unassign from customer", - "unassign-entity-views": "Unassign entity views", - "unassign-entity-views-action-title": "{ count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }를 커스터머로 부터 할당 해제", - "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 개 개체 보기} other {# 개 개체 보기} }?", - "delete-entity-views-action-title": "Delete { count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }", - "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 개 개체 보기} other {# 개 개체 보기} }?", - "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-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", + "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": "Key가 없습니다.", + "create-new-alias": "새로 만들기!", + "create-new-key": "새로 만들기!", + "duplicate-alias-error": "중복 별명 '{{alias}}'이(가) 발견되었습니다.
    엔티티 뷰 별명은 대시보드 내에서 고유해야 합니다.", + "configure-alias": "'{{alias}}' 별명 구성", + "no-entity-views-matching": "'{{entity}}'와(과) 일치하는 엔티티 뷰를 찾을 수 없습니다.", + "public": "공개", + "alias": "별명", + "alias-required": "엔티티 뷰 별명은 필수 항목입니다.", + "remove-alias": "엔티티 뷰 별명 제거", + "add-alias": "엔티티 뷰 별명 추가", + "name-starts-with": "엔티티 뷰 이름 표현식", + "help-text": "필요에 따라 '%'를 사용하세요: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_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": "엔티티 뷰 추가", + "entity-view-public": "엔티티 뷰가 공개됨", + "assign-to-customer": "고객에게 자산 할당", + "assign-entity-view-to-customer": "고객에 엔티티 뷰 할당", + "assign-entity-view-to-customer-text": "고객에 할당할 엔티티 뷰를 선택하세요", + "assign-entity-view-to-edge-title": "엣지에 엔티티 뷰 할당", + "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": "Delete { 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": "엔티티 뷰 유형", + "created-time": "생성 일시", + "name": "이름", + "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "type-max-length": "엔티티 뷰 유형은 256자 미만이어야 합니다.", + "description": "설명", "events": "이벤트", "details": "상세 정보", - "copyId": "개체 보기 ID 복사", - "idCopiedMessage": "개체 보기 ID가 클립보드에 복사되었습니다", - "assignedToCustomer": "할당된 커스터머", - "unable-entity-view-device-alias-title": "개체 보기 별명을 삭제할 수 없습니다", - "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": "개체 보기 선택", - "make-public": "개체 보기를 공개로 전환", - "make-private": "개체 보기를 비공개로 전환", - "start-date": "시작 날짜", + "copyId": "엔티티 뷰 ID 복사", + "idCopiedMessage": "엔티티 뷰 ID가 클립보드에 복사되었습니다", + "assignedToCustomer": "할당된 고객", + "unable-entity-view-device-alias-title": "엔티티 뷰 별명을 삭제할 수 없습니다", + "unable-entity-view-device-alias-text": "장치 별명 '{{entityViewAlias}}'은(는) 다음 위젯에서 사용 중이므로 삭제할 수 없습니다:
    {{widgetsList}}", + "select-entity-view": "엔티티 뷰 선택", + "make-public": "엔티티 뷰를 공개로 전환", + "make-private": "엔티티 뷰를 비공개로 전환", "start-ts": "시작 시간", - "end-date": "종료 날짜", "end-ts": "종료 시간", "date-limits": "날짜 제한", "client-attributes": "클라이언트 속성", @@ -1288,34 +3321,49 @@ "shared-attributes-placeholder": "공유 속성", "server-attributes-placeholder": "서버 속성", "timeseries-placeholder": "시계열", - "target-entity": "대상 개체", + "target-entity": "대상 엔티티", "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.", + "attributes-propagation-hint": "엔티티 뷰는 이 엔티티 뷰를 저장하거나 업데이트할 때마다 대상 엔티티에서 지정된 속성을 자동으로 복사합니다. 성능상의 이유로 대상 엔티티 속성은 각 속성 변경 시 엔티티 뷰에 전파되지 않습니다. 규칙 체인에서 \"뷰로 복사\" 규칙 노드를 구성하고 \"속성 게시\" 및 \"속성 업데이트됨\" 메시지를 새 규칙 노드에 연결하여 자동 전파를 활성화할 수 있습니다.", "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 개 개체 보기} other {# 개 개체 보기} } 선택됨" + "timeseries-data-hint": "엔티티 뷰에서 액세스할 수 있는 대상 엔티티의 시계열 데이터 키를 구성합니다. 이 시계열 데이터는 읽기 전용입니다.", + "search": "엔티티 뷰 검색", + "selected-entity-views": "{ count, plural, =1 {1 개 엔티티 뷰} other {# 개 엔티티 뷰} } 선택됨", + "make-public-entity-view-title": "엔티티 뷰 '{{entityViewName}}'을(를) 공개하시겠습니까?", + "make-public-entity-view-text": "확인 후 엔티티 뷰와 모든 데이터가 공개되어 다른 사용자가 액세스할 수 있습니다.", + "make-private-entity-view-title": "엔티티 뷰 '{{entityViewName}}'을(를) 비공개로 설정하시겠습니까?", + "make-private-entity-view-text": "확인 후 엔티티 뷰와 모든 데이터가 비공개되어 다른 사용자가 액세스할 수 없습니다.", + "assign-entity-view-to-edge": "엣지에 엔티티 뷰 할당", + "assign-entity-view-to-edge-text": "엣지에 할당할 엔티티 뷰를 선택하세요", + "unassign-entity-view-from-edge-title": "엔티티 뷰 '{{entityViewName}}'을(를) 엣지에서 할당 해제하시겠습니까?", + "unassign-entity-view-from-edge-text": "확인 후 엔티티 뷰가 할당 해제되어 엣지에서 액세스할 수 없습니다.", + "unassign-entity-views-from-edge-action-title": "엣지에서 { count, plural, =1 {1개 엔티티 뷰} other {#개 엔티티 뷰} } 할당 해제", + "unassign-entity-view-from-edge": "엔티티 뷰 할당 해제", + "unassign-entity-views-from-edge-title": "정말 { count, plural, =1 {1개의 엔티티 뷰} other {#개의 엔티티 뷰} }를 할당 해제하시겠습니까?", + "unassign-entity-views-from-edge-text": "확인 후 선택한 모든 엔티티 뷰가 할당 해제되어 엣지에서 액세스할 수 없습니다." }, "event": { "event-type": "이벤트 타입", + "events-filter": "이벤트 필터", + "clean-events": "이벤트 지우기", "type-error": "에러", "type-lc-event": "주기적 이벤트", "type-stats": "통계", "type-debug-rule-node": "디버그", "type-debug-rule-chain": "디버그", + "type-debug-calculated-field": "디버그", + "arguments": "인수", + "result": "결과", "no-events-prompt": "이벤트 없음", "error": "에러", "alarm": "알람", "event-time": "이벤트 발생 시간", "server": "서버", - "body": "Body", + "body": "본문", "method": "방법", "type": "유형", + "message": "메시지", "message-id": "메시지 ID", + "copy-message-id": "메시지 ID 복사", "message-type": "메시지 유형", "data-type": "데이터 유형", "relation-type": "관계 유형", @@ -1326,9 +3374,23 @@ "success": "성공", "failed": "실패", "messages-processed": "처리된 메시지", + "max-messages-processed": "처리된 최대 메시지 수", + "min-messages-processed": "처리된 최소 메시지 수", "errors-occurred": "오류가 발생했습니다", + "max-errors-occurred": "발생한 최대 오류 수", + "min-errors-occurred": "발생한 최소 오류 수", + "min-value": "최소 값은 0입니다.", "all-events": "모두", - "entity-type": "개체 유형" + "has-error": "오류 있음", + "entity-id": "엔티티 ID", + "copy-entity-id": "엔티티 ID 복사", + "entity-type": "엔티티 유형", + "clear-filter": "필터 지우기", + "clear-request-title": "모든 이벤트 지우기", + "clear-request-text": "모든 이벤트를 지우시겠습니까?", + "started": "시작됨", + "updated": "수정됨", + "stopped": "정지됨" }, "extension": { "extensions": "확장", @@ -1358,13 +3420,13 @@ "device-name-expression": "장치 이름 표현", "device-type-expression": "장치 유형 표현", "custom": "사용자 정의", - "to-double": "To Double", - "transformer": "Transformer", + "to-double": "실수로 변환", + "transformer": "변환기", "json-required": "Transformer json을(를) 입력하세요.", - "json-parse": "Unable to parse transformer json.", + "json-parse": "변환기 JSON을 파싱할 수 없습니다.", "attributes": "속성", "add-attribute": "속성 추가", - "add-map": "Add mapping element", + "add-map": "매핑 요소 추가", "timeseries": "시계열", "add-timeseries": "시계열 추가", "field-required": "필드를 입력하세요.", @@ -1373,7 +3435,7 @@ "host": "호스트", "port": "포트", "port-range": "포트를 1~65535 범위 내에서 입력하세요.", - "ssl": "Ssl", + "ssl": "SSL", "credentials": "인증", "username": "사용자 이름", "password": "비밀 번호", @@ -1386,23 +3448,23 @@ "cert": "인증 파일 *", "no-file": "아무 파일도 선택되지 않았습니다.", "drop-file": "여기로 파일을 드래그 하거나, 여기를 클릭해서 파일을 선택하세요.", - "mapping": "Mapping", - "topic-filter": "주제 필터", + "mapping": "매핑", + "topic-filter": "토픽 필터", "converter-type": "변환기 유형", - "converter-json": "Json", + "converter-json": "JSON", "json-name-expression": "장치 이름 JSON 표현", - "topic-name-expression": "장치 이름 주제 표현", + "topic-name-expression": "장치 이름 토픽 표현", "json-type-expression": "장치 유형 JSON 표현", - "topic-type-expression": "장치 유형 주제 표현", + "topic-type-expression": "장치 유형 토픽 표현", "attribute-key-expression": "속성 키 표현", "attr-json-key-expression": "속성 키 JSON 표현", - "attr-topic-key-expression": "속성 키 주제 표현", + "attr-topic-key-expression": "속성 키 토픽 표현", "request-id-expression": "요청 ID 표현", "request-id-json-expression": "요청 ID JSON 표현", - "request-id-topic-expression": "요청 ID 주제 표현", - "response-topic-expression": "응답 주제 표현", + "request-id-topic-expression": "요청 ID 토픽 표현", + "response-topic-expression": "응답 토픽 표현", "value-expression": "값 표현", - "topic": "주제", + "topic": "토픽", "timeout": "시간 제한(ms)", "converter-json-required": "JSON 변환기가 필요합니다.", "converter-json-parse": "JSON 변환기를 파싱할 수 없습니다.", @@ -1415,15 +3477,15 @@ "add-attribute-request": "속성 요청 추가", "attribute-updates": "속성 업데이트", "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", + "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": "서버 추가", @@ -1432,7 +3494,7 @@ "opc-application-uri": "애플리케이션 URI", "opc-scan-period-in-seconds": "스캔 주기(초)", "opc-security": "보안", - "opc-identity": "Identity", + "opc-identity": "ID", "opc-keystore": "키스톤", "opc-type": "유형", "opc-keystore-type": "유형", @@ -1447,31 +3509,31 @@ "modbus-add-server-prompt": "서버/보조를 추가해주세요", "modbus-transport": "전송", "modbus-tcp-reconnect": "자동으로 재접속", - "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-rtu-over-tcp": "TCP over RTU", "modbus-port-name": "시리얼 포트 이름", "modbus-encoding": "인코딩", - "modbus-parity": "Parity", + "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-unit-id": "유닛 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": "Function", + "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": "Byte order", + "modbus-byte-order": "바이트 순서", "sync": { "status": "상태", "sync": "동기화", @@ -1487,91 +3549,110 @@ "file": "확장 파일", "invalid-file-error": "올바르지 않은 확장 파일" }, + "feature": { + "advanced-features": "고급 기능" + }, "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": "필터", - "editable": "편집 가능", - "no-filters-found": "아무 필터도 찾을 수 없습니다.", - "no-filter-text": "필터가 특정되지 않았습니다", - "add-filter-prompt": "필터를 추가해주세요", - "no-filter-matching": "필터 '{{filter}}'를 찾을 수 없습니다.", - "create-new-filter": "새로 필터를 만드세요!", - "filter-required": "필터를 입력하세요.", - "operation": { - "operation": "Operation", - "equal": "같음", - "not-equal": "같이 않음", - "starts-with": "다음으로 시작함", - "ends-with": "다음으로 끝남", - "contains": "포함함", - "not-contains": "포함하지 않음", - "greater": "큼", - "less": "작음", - "greater-or-equal": "크거나 같음", - "less-or-equal": "작거나 같음", - "and": "그리고", - "or": "또는" - }, - "ignore-case": "ignore case", - "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", - "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": "현재 장치", - "default-value": "기본값", - "dynamic-source-type": "Dynamic source type", - "no-dynamic-value": "No dynamic value", - "source-attribute": "소스 속성", - "switch-to-dynamic-value": "Switch to dynamic value", - "switch-to-default-value": "Switch to default value" + "add": "필터 추가", + "edit": "필터 편집", + "name": "필터 이름", + "name-required": "필터 이름은 필수 항목입니다.", + "duplicate-filter": "같은 이름의 필터가 이미 존재합니다.", + "filters": "필터", + "unable-delete-filter-title": "필터를 삭제할 수 없습니다", + "unable-delete-filter-text": "필터 '{{filter}}'은(는) 다음 위젯에서 사용 중이므로 삭제할 수 없습니다:
    {{widgetsList}}", + "duplicate-filter-error": "중복 필터 '{{filter}}'이(가) 발견되었습니다.
    필터는 대시보드 내에서 고유해야 합니다.", + "missing-key-filters-error": "'{{filter}}' 필터에 키 필터가 없습니다.", + "filter": "필터", + "editable": "편집 가능", + "editable-hint": "사용자가 대시보드에서 필터 값을 변경할 수 있도록 허용.", + "no-filters-found": "아무 필터도 찾을 수 없습니다.", + "no-filter-text": "필터가 특정되지 않았습니다", + "add-filter-prompt": "필터를 추가해주세요", + "no-filter-matching": "필터 '{{filter}}'를 찾을 수 없습니다.", + "create-new-filter": "새로 필터를 만드세요!", + "create-new": "새로 만들기", + "filter-required": "필터를 입력하세요.", + "operation": { + "operation": "연산", + "equal": "같음", + "not-equal": "같이 않음", + "starts-with": "다음으로 시작함", + "ends-with": "다음으로 끝남", + "contains": "포함함", + "not-contains": "포함하지 않음", + "greater": "큼", + "less": "작음", + "greater-or-equal": "크거나 같음", + "less-or-equal": "작거나 같음", + "and": "그리고", + "or": "또는", + "in": "포함", + "not-in": "미포함" + }, + "ignore-case": "대소문자 무시", + "value": "값", + "remove-filter": "필터 제거", + "duplicate-filter-action": "필터 복제", + "preview": "필터 미리보기", + "no-filters": "구성된 필터 없음", + "add-filter": "필터 추가", + "add-complex-filter": "복합 필터 추가", + "add-complex": "복합 추가", + "complex-filter": "복합 필터", + "edit-complex-filter": "복합 필터 편집", + "edit-filter-user-params": "필터 술어 사용자 매개변수 편집", + "filter-user-params": "필터 술어 사용자 매개변수", + "user-parameters": "사용자 매개변수", + "display-label": "표시할 레이블", + "custom-label": "사용자 지정 레이블", + "custom-label-hint": "필터에 사용자 지정 레이블을 설정하려면 활성화합니다. 비활성화하면 레이블이 자동으로 생성됩니다.", + "order-priority": "표시 순서", + "key-filter": "키 필터", + "key-filters": "키 필터", + "key-name": "키 이름", + "key-name-required": "키 이름은 필수 항목입니다.", + "key-type": { + "key-type": "키 유형", + "attribute": "속성", + "timeseries": "시계열", + "entity-field": "엔티티 필드", + "constant": "상수", + "client-attribute": "클라이언트 속성", + "server-attribute": "서버 속성", + "shared-attribute": "공유된 속성" + }, + "value-type": { + "value-type": "값 유형", + "string": "문자열", + "numeric": "숫자", + "boolean": "불리언", + "date-time": "날짜/시간" + }, + "value-type-required": "키 값 유형은 필수 항목입니다.", + "key-value-type-change-title": "키 값 유형을 변경하시겠습니까?", + "key-value-type-change-message": "확인하면 입력한 모든 키 필터가 제거됩니다.", + "no-key-filters": "구성된 키 필터 없음", + "add-key-filter": "키 필터 추가", + "remove-key-filter": "키 필터 제거", + "edit-key-filter": "키 필터 편집", + "date": "날짜", + "time": "시간", + "current-tenant": "현재 테넌트", + "current-customer": "현재 고객", + "current-user": "현재 사용자", + "current-device": "현재 장치", + "default-value": "기본값", + "default-comma-separated-values": "기본 쉼표로 구분된 값", + "dynamic-source-type": "동적 소스 유형", + "dynamic-value": "동적 값", + "no-dynamic-value": "동적 값 없음", + "source-attribute": "소스 속성", + "switch-to-dynamic-value": "동적 값으로 전환", + "switch-to-default-value": "기본값으로 전환", + "inherit-owner": "소유자로부터 상속", + "source-attribute-not-set": "소스 속성이 설정되지 않은 경우", + "unit": "단위" }, "fullscreen": { "expand": "전체화면으로 확장", @@ -1583,15 +3664,14 @@ "function": "기능" }, "gateway": { - "create-new-gateway": "Create a new gateway", - "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", - "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.", - "no-gateway-found": "No gateway found.", - "no-gateway-matching": " '{{item}}' not found." + "gateway-name": "게이트웨이 이름", + "gateway-name-required": "게이트웨이 이름은 필수 항목입니다.", + "gateways": "게이트웨이", + "create-new-gateway": "새 게이트웨이 생성", + "create-new-gateway-text": "이름이 '{{gatewayName}}'인 새 게이트웨이를 생성하시겠습니까?", + "launch-command": "실행 명령", + "no-gateway-found": "게이트웨이를 찾을 수 없습니다.", + "no-gateway-matching": " '{{item}}'을(를) 찾을 수 없습니다." }, "grid": { "delete-item-title": "이 항목을 삭제 하시겠습니까?", @@ -1607,28 +3687,98 @@ "scroll-to-top": "스크롤 맨 위로" }, "help": { - "goto-help-page": "도움" + "goto-help-page": "도움", + "show-help": "도움말 표시" }, "home": { "home": "홈", - "profile": "프로파일", + "profile": "프로필", "logout": "로그아웃", "menu": "메뉴", - "avatar": "Avatar", + "avatar": "아바타", "open-user-menu": "사용자 메뉴 열기" }, + "file-input": { + "browse-file": "파일 찾아보기", + "browse-files": "파일 찾아보기" + }, + "image": { + "gallery": "이미지 갤러리", + "search": "이미지 검색", + "selected-images": "{ count, plural, =1 {1개 이미지} other {#개 이미지} } 선택됨", + "created-time": "생성 일시", + "name": "이름", + "name-required": "이름을 입력하세요.", + "resolution": "해상도", + "size": "크기", + "system": "시스템", + "download-image": "이미지 다운로드", + "export-image": "이미지를 JSON으로 내보내기", + "import-image": "JSON에서 이미지 가져오기", + "upload-image": "이미지 업로드", + "edit-image": "이미지 수정", + "image-details": "이미지 상세", + "no-images": "이미지가 없습니다", + "delete-image": "이미지 삭제", + "delete-image-title": "이미지 '{{imageTitle}}'을(를) 삭제하시겠습니까?", + "delete-image-text": "주의: 확인 후 이미지를 복구할 수 없게 됩니다.", + "delete-images-title": "{ count, plural, =1 {1개 이미지} other {#개 이미지} }를 삭제하시겠습니까?", + "delete-images-text": "주의: 확인 후 선택한 모든 이미지가 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "list-mode": "목록 보기", + "grid-mode": "그리드 보기", + "image-preview": "이미지 미리보기", + "update-image": "이미지 업데이트", + "export-failed-error": "이미지를 내보낼 수 없습니다: {{error}}", + "image-json-file": "이미지 JSON 파일", + "invalid-image-json-file-error": "JSON에서 이미지를 가져올 수 없습니다: 잘못된 이미지 JSON 데이터 구조입니다.", + "image-is-in-use": "이미지가 다른 엔티티에서 사용 중입니다", + "images-are-in-use": "이미지가 다른 엔티티에서 사용 중입니다", + "image-is-in-use-text": "'{{title}}' 이미지가 다음 엔티티에서 사용 중이므로 삭제되지 않았습니다:", + "images-are-in-use-text": "모든 이미지가 삭제되지 않았습니다. 다른 엔티티에서 사용 중이기 때문입니다.
    해당 이미지 행의 참조 버튼을 클릭하여 참조된 엔티티를 볼 수 있습니다.
    이 이미지를 계속 삭제하려면 아래 표에서 선택하고 선택 삭제 버튼을 클릭하세요.", + "delete-image-in-use-text": "이미지를 계속 삭제하려면 무조건 삭제 버튼을 클릭하세요.", + "system-entities": "시스템 엔티티:", + "entities": "엔티티:", + "references": "참조", + "include-system-images": "시스템 이미지 포함", + "clear-image": "이미지 지우기", + "no-image": "이미지 없음", + "no-image-selected": "이미지 없음", + "browse-from-gallery": "갤러리에서 찾아보기", + "set-link": "링크 설정", + "image-link": "이미지 링크", + "link": "링크", + "copy-image-link": "이미지 링크 복사", + "embed-image": "이미지 포함", + "embed-to-html": "HTML에 포함", + "embed-to-html-hint": "이 기능은 승인되지 않은 사용자도 링크에 액세스할 수 있게 합니다.", + "embed-to-html-text": "다음 코드 스니펫을 사용하여 일반 HTML 기반 컴포넌트에 이미지를 포함할 수 있습니다.
    이러한 컴포넌트에는 HTML 카드 위젯, 셀 콘텐츠 함수 등이 포함됩니다.", + "embed-to-angular-template": "Angular HTML 템플릿에 포함", + "embed-to-angular-template-text": "다음 코드 스니펫을 사용하여 컴포넌트에 사용될 Angular HTML 템플릿에 이미지를 포함할 수 있습니다.
    이러한 컴포넌트에는 마크다운 위젯, 위젯 편집기의 HTML 섹션, 사용자 지정 작업 등이 포함됩니다." + }, + "image-input": { + "drop-images-or": "이미지를 끌어다 놓거나", + "drag-and-drop": "끌어다 놓기", + "or": "또는", + "browse": "찾아보기", + "no-images": "선택된 이미지 없음", + "images": "이미지" + }, "import": { "no-file": "선택된 파일이 없습니다.", "drop-file": "업로드 할 JSON 파일을 끌어다 놓거나 여기 클릭하여 파일을 선택하십시오.", + "drop-json-file-or": "JSON 파일을 끌어다 놓거나", "drop-file-csv": "업로드 할 CSV 파일을 끌어다 놓거나 여기 클릭하여 파일을 선택하십시오.", + "drop-file-csv-or": "CSV 파일을 끌어다 놓거나", "column-value": "값", "column-title": "제목", "column-example": "값 데이터 예시", "column-key": "속성/텔레메트리 키", - "csv-delimiter": "CSV delimiter", + "credentials": "자격 증명", + "csv-delimiter": "CSV 구분자", "csv-first-line-header": "첫번째 줄에 컬럼 이름 포함", "csv-update-data": "속성/텔레메트리 업데이트", - "import-csv-number-columns-error": "A file should contain at least two columns", + "details": "상세 내역", + "import-csv-number-columns-error": "파일에는 최소 두 개의 열이 포함되어야 합니다.", "import-csv-invalid-format-error": "유효하지 않은 파일 포맷. Line: '{{line}}'", "column-type": { "name": "이름", @@ -1639,21 +3789,388 @@ "shared-attribute": "공유된 속성", "server-attribute": "서버 속성", "timeseries": "시계열", - "entity-field": "개체 필드", + "entity-field": "엔티티 필드", "access-token": "액세스 토큰", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT 클라이언트 ID", + "user-name": "MQTT 사용자 이름", + "password": "MQTT 비밀번호" + }, + "lwm2m": { + "client-endpoint": "LwM2M 엔드포인트 클라이언트 이름", + "security-config-mode": "LwM2M 보안 구성 모드", + "client-identity": "LwM2M 클라이언트 ID", + "client-key": "LwM2M 클라이언트 키", + "client-cert": "LwM2M 클라이언트 공개 키", + "bootstrap-server-security-mode": "LwM2M 부트스트랩 서버 보안 모드", + "bootstrap-server-secret-key": "LwM2M 부트스트랩 서버 비밀 키", + "bootstrap-server-public-key-id": "LwM2M 부트스트랩 서버 공개 키 또는 ID", + "lwm2m-server-security-mode": "LwM2M 서버 보안 모드", + "lwm2m-server-secret-key": "LwM2M 서버 비밀 키", + "lwm2m-server-public-key-id": "LwM2M 서버 공개 키 또는 ID" + }, + "snmp": { + "host": "SNMP 호스트", + "port": "SNMP 포트", + "version": "SNMP 버전 (v1, v2c 또는 v3)", + "community-string": "SNMP 커뮤니티 문자열" + }, "isgateway": "게이트웨이 여부", - "description": "설정" + "activity-time-from-gateway-device": "게이트웨이 장치의 활동 시간", + "description": "설정", + "routing-key": "엣지 키", + "secret": "엣지 시크릿" }, - "stepper-text":{ + "stepper-text": { "select-file": "파일 선택", "configuration": "설정 불러오기", "column-type": "컬럼 유형 선택", - "creat-entities": "새로운 개체 생성" + "creat-entities": "새로운 엔티티 생성" }, "message": { - "create-entities": "{{count}}개의 새로운 개체사 성공적으로 생성되었습니다.", - "update-entities": "{{count}}개의 개체가 성공적으로 업데이트 되었습니다.", - "error-entities": "{{count}}개의 개체를 생성하는데 오류가 발생했습니다." + "create-entities": "{{count}}개의 새로운 엔티티사 성공적으로 생성되었습니다.", + "update-entities": "{{count}}개의 엔티티가 성공적으로 업데이트 되었습니다.", + "error-entities": "{{count}}개의 엔티티를 생성하는데 오류가 발생했습니다." + } + }, + "scada": { + "symbols": "SCADA 심볼", + "search": "심볼 검색", + "selected-symbols": "{ count, plural, =1 {1개 심볼} other {#개 심볼} } 선택됨", + "download-symbol": "SCADA 심볼 다운로드", + "export-symbol": "SCADA 심볼을 JSON으로 내보내기", + "import-symbol": "JSON에서 SCADA 심볼 가져오기", + "upload-symbol": "SCADA 심볼 업로드", + "update-symbol": "SCADA 심볼 업데이트", + "edit-symbol": "SCADA 심볼 수정", + "symbol-details": "SCADA 심볼 상세", + "mode-svg": "SVG", + "mode-xml": "XML", + "no-symbols": "심볼이 없습니다", + "show-hidden-elements": "숨겨진 요소 표시", + "hide-hidden-elements": "숨겨진 요소 숨기기", + "delete-symbol": "SCADA 심볼 삭제", + "delete-symbol-title": "SCADA 심볼 '{{imageTitle}}'을(를) 삭제하시겠습니까?", + "delete-symbol-text": "주의: 확인 후 SCADA 심볼을 복구할 수 없게 됩니다.", + "delete-symbols-title": "{ count, plural, =1 {1개 SCADA 심볼} other {#개 SCADA 심볼} }을(를) 삭제하시겠습니까?", + "delete-symbols-text": "주의: 확인 후 선택한 모든 SCADA 심볼이 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "include-system-symbols": "시스템 심볼 포함", + "symbol-preview": "심볼 미리보기", + "general": "일반", + "tags": "태그", + "properties": "속성", + "title": "제목", + "description": "설명", + "search-tags": "태그 검색", + "widget-size": "위젯 크기", + "cols": "열", + "rows": "행", + "state-render-function": "상태 렌더링 함수", + "preview": "미리보기", + "preview-widget-action-text": "위젯 액션 '{{type}}'이(가) 성공적으로 호출되었습니다!", + "no-symbol": "SCADA 심볼 없음", + "no-symbol-selected": "선택된 SCADA 심볼 없음", + "clear-symbol": "SCADA 심볼 지우기", + "browse-symbol-from-gallery": "갤러리에서 SCADA 심볼 찾아보기", + "zoom-in": "확대", + "zoom-out": "축소", + "create-widget": "위젯 만들기", + "create-widget-from-symbol": "SCADA 심볼로 위젯 만들기", + "hidden": "숨겨짐", + "tag": { + "tag": "태그", + "on-click-action": "클릭 시 액션", + "no-tags": "구성된 태그 없음", + "delete-tag-text": "{{elementType}} 요소에서 태그
    {{tag}}을(를) 삭제하시겠습니까?", + "update-tag": "태그 업데이트", + "enter-tag": "태그 입력", + "tag-settings": "태그 설정", + "remove-tag": "태그 제거", + "add-tag": "태그 추가" + }, + "behavior": { + "behavior": "동작", + "id": "ID", + "name": "이름", + "type": "유형", + "no-behaviors": "구성된 동작 없음", + "add-behavior": "동작 추가", + "type-action": "동작", + "type-value": "값", + "type-widget-action": "위젯 액션", + "behavior-settings": "동작 설정", + "remove-behavior": "동작 제거", + "hint": "힌트", + "group-title": "그룹 제목", + "value-type": "값 유형", + "default-value": "기본값", + "true-label": "참 라벨", + "false-label": "거짓 라벨", + "state-label": "상태 라벨", + "default-payload": "기본 페이로드", + "not-unique-behavior-ids-error": "동작 ID는 고유해야 합니다!", + "default-settings": "기본 설정" + }, + "symbol": { + "symbol": "SCADA 심볼", + "fluid-presence": "유체 존재", + "fluid-presence-hint": "파이프에 유체가 있는지 나타냅니다.", + "fluid-present": "유체 있음", + "present": "있음", + "absent": "없음", + "flow-presence": "흐름 존재", + "flow-presence-hint": "파이프에서 유체가 흐르고 있는지 나타냅니다.", + "flow-present": "흐름 있음", + "flow-direction": "흐름 방향", + "flow-direction-hint": "유체 흐름 방향을 나타냅니다.", + "forward": "전방", + "reverse": "역방향", + "flow-animation-speed": "흐름 애니메이션 속도", + "flow-animation-speed-hint": "흐름의 애니메이션 속도를 나타내는 double 값. 1 - 정상 속도, 0 - 애니메이션 없음, < 1 - 느린 애니메이션, > 1 - 빠른 애니메이션.", + "leak": "누출", + "leak-hint": "파이프에 누출이 있는지 나타냅니다.", + "leak-present": "누출 있음", + "fluid-color": "유체 색상", + "pipe-color": "파이프 색상", + "horizontal-pipe": "수평 파이프", + "vertical-pipe": "수직 파이프", + "horizontal-fluid-color": "수평 유체 색상", + "vertical-fluid-color": "수직 유체 색상", + "left-pipe": "왼쪽 파이프", + "right-pipe": "오른쪽 파이프", + "top-pipe": "상단 파이프", + "bottom-pipe": "하단 파이프", + "left-fluid-color": "왼쪽 유체 색상", + "right-fluid-color": "오른쪽 유체 색상", + "top-fluid-color": "상단 유체 색상", + "bottom-fluid-color": "하단 유체 색상", + "display": "디스플레이", + "display-format": "디스플레이 형식", + "value": "값", + "decimals": "소수 자릿수", + "units": "단위", + "flow-meter-value-hint": "유량계 디스플레이에 표시되는 double 값", + "value-hint": "현재 값을 나타내는 double 값", + "running": "실행 중", + "running-hint": "구성 요소가 실행 상태인지 나타냅니다.", + "warning-state": "경고 상태", + "warning": "경고", + "warning-click": "경고 클릭", + "warning-state-hint": "구성 요소가 경고 상태인지 나타냅니다.", + "critical-state": "심각 상태", + "critical": "심각한", + "critical-click": "중요 클릭", + "critical-state-hint": "구성 요소가 심각 상태인지 나타냅니다.", + "critical-state-animation": "심각 상태 애니메이션", + "critical-state-animation-hint": "구성 요소가 심각 상태일 때 깜박이는 애니메이션을 활성화할지 여부.", + "warning-critical-state-animation": "경고/심각 상태 애니메이션", + "warning-critical-state-animation-hint": "구성 요소가 경고 또는 심각 상태일 때 깜박이는 애니메이션을 활성화할지 여부.", + "animation": "애니메이션", + "broken": "손상됨", + "broken-hint": "구성 요소가 손상되었는지 나타냅니다.", + "on-display-click": "디스플레이 클릭 시", + "on-display-click-hint": "사용자가 디스플레이를 클릭할 때 호출되는 액션.", + "pipe": "파이프", + "default-border-color": "기본 테두리 색상", + "active-border-color": "활성 테두리 색상", + "warning-border-color": "경고 테두리 색상", + "critical-border-color": "심각 테두리 색상", + "background-color": "배경색", + "rotation-animation-speed": "회전 애니메이션 속도", + "rotation-animation-speed-hint": "회전 애니메이션 속도를 나타내는 double 값. 1 - 정상 속도, 0 - 애니메이션 없음, < 1 - 느린 애니메이션, > 1 - 빠른 애니메이션.", + "on-click": "클릭 시", + "on-click-hint": "사용자가 구성 요소를 클릭할 때 호출되는 액션.", + "connectors-positions": "커넥터 위치", + "right-connector": "오른쪽 커넥터", + "right-top-connector": "오른쪽 상단 커넥터", + "right-bottom-connector": "오른쪽 하단 커넥터", + "left-connector": "왼쪽 커넥터", + "left-top-connector": "왼쪽 상단 커넥터", + "left-bottom-connector": "왼쪽 하단 커넥터", + "top-left-connector": "상단 왼쪽 커넥터", + "top-right-connector": "상단 오른쪽 커넥터", + "top-connector": "상단 커넥터", + "bottom-connector": "하단 커넥터", + "running-color": "실행 색상", + "stopped-color": "정지 색상", + "stopped": "정지됨", + "warning-color": "경고 색상", + "critical-color": "심각 색상", + "opened": "열림", + "opened-hint": "구성 요소가 열린 상태인지 나타냅니다.", + "open": "열기", + "open-hint": "사용자가 구성 요소를 열기 위해 클릭할 때 호출되는 액션.", + "close": "닫기", + "close-hint": "사용자가 구성 요소를 닫기 위해 클릭할 때 호출되는 액션.", + "close-state-animation": "닫힘 상태 애니메이션", + "close-state-animation-hint": "구성 요소가 닫힌 상태일 때 깜박이는 애니메이션을 활성화할지 여부.", + "opened-color": "열림 색상", + "closed-color": "닫힘 색상", + "opened-rotation-angle": "열림 회전 각도", + "closed-rotation-angle": "닫힘 회전 각도", + "tank-capacity": "탱크 용량", + "tank-capacity-hint": "총 탱크 용량을 나타내는 double 값.", + "current-volume": "현재 볼륨", + "current-volume-hint": "현재 사용 중인 볼륨을 나타내는 double 값.", + "tank-color": "탱크 색상", + "value-box": "값 상자", + "value-text": "값 텍스트", + "scale": "눈금", + "transparent-mode": "투명 모드", + "major-ticks": "주 눈금", + "intervals": "간격", + "major-ticks-color": "주 눈금 색상", + "normal": "정상", + "minor-ticks": "보조 눈금", + "minor-ticks-color": "보조 눈금 색상", + "temperature": "온도", + "temperature-hint": "현재 온도를 나타내는 double 값.", + "update-temperature": "온도 업데이트", + "update-temperature-hint": "사용자가 현재 온도를 변경하기 위해 클릭할 때 호출되는 액션.", + "run": "실행", + "run-hint": "사용자가 구성 요소를 실행하기 위해 클릭할 때 호출되는 액션.", + "stop": "정지", + "stop-hint": "사용자가 구성 요소를 정지하기 위해 클릭할 때 호출되는 액션.", + "temperature-step": "온도 단계 증가", + "heat-pump-color": "히트 펌프 색상", + "power-button-background": "전원 버튼 배경", + "value-box-background": "값 상자 배경", + "value-units": "값 단위", + "enable-units-scale": "눈금에 단위 표시", + "filtration-mode": "필터 모드", + "filtration-mode-hint": "현재 필터 모드를 나타내는 정수 값.", + "filtration-mode-update": "필터 모드 업데이트 상태", + "filtration-mode-update-hint": "사용자가 현재 필터 모드를 변경하기 위해 클릭할 때 호출되는 동작.", + "filter-mode": "필터", + "waste-mode": "폐기", + "backwash-mode": "역세척", + "recirculate-mode": "순환", + "rinse-mode": "헹굼", + "closed-mode": "닫힘", + "sand-filter-color": "모래 필터 색상", + "mode-box-background": "모드 상자 배경", + "border-color": "테두리 색상", + "label-color": "레이블 색상", + "water-leak-hint": "누출 여부를 나타냅니다.", + "default-color": "기본 색상", + "leak-color": "누출 색상", + "full-value": "전체 값", + "full-value-hint": "전체 값을 나타내는 실수 값.", + "label": "라벨", + "icon": "아이콘", + "button-color": "버튼 색상", + "on-label": "'켜짐' 레이블 텍스트", + "off-label": "'꺼짐' 레이블 텍스트", + "arrow-presence": "화살표 표시", + "arrow-presence-hint": "연결기에 화살표가 있는지 나타냅니다.", + "arrow-present": "화살표 있음", + "arrow-direction": "흐름 방향", + "arrow-direction-hint": "흐름 방향을 나타냅니다.", + "flow-animation": "흐름 존재", + "flow-animation-hint": "연결기에 유체가 흐르고 있는지 나타냅니다.", + "flow": "흐름", + "flow-line": "선", + "flow-line-style": "선 스타일", + "flow-style-hint": "완벽한 애니메이션 동기화를 위해 Dash와 Gap 값의 합이 100으로 나누어 떨어지도록 설정하세요.", + "flow-dash-cap": "대시 끝", + "dash-cap-butt": "평면", + "dash-cap-round": "둥글게", + "dash-cap-square": "사각형", + "dash": "대시", + "gap": "간격", + "main-line": "메인 선", + "line": "선", + "line-color": "선 색상", + "arrow-color": "화살표 색상", + "target-value": "목표 값", + "target-value-hint": "눈금의 목표 지점을 나타냅니다.", + "min-max-value": "최소 및 최대 값", + "min-value": "최소", + "max-value": "최대", + "progress-bar": "진행 막대", + "progress-arrow": "진행 화살표", + "warning-scale-color": "경고 눈금 색상", + "critical-scale-color": "중요 눈금 색상", + "scale-color": "스케일 색상", + "target": "대상", + "high-warning-state": "높은 경고 상태", + "show-high-warning-scale": "높은 경고 스케일 표시", + "high-warning-scale": "높은 경고 스케일", + "high-warning-state-hint": "double 값은 높은 경고 범위가 높은 위험 또는 최대값까지임을 나타냅니다.", + "low-warning-state": "낮은 경고 상태", + "show-low-warning-scale": "낮은 경고 스케일 표시", + "low-warning-scale": "낮은 경고 스케일", + "low-warning-state-hint": "double 값은 낮은 경고 범위가 낮은 위험 또는 최소값까지임을 나타냅니다.", + "high-critical-state": "높은 위험 상태", + "show-high-critical-scale": "높은 위험 스케일 표시", + "high-critical-scale": "높은 위험 스케일", + "high-critical-state-hint": "double 값은 높은 위험 범위가 최대값 스케일까지임을 나타냅니다.", + "low-critical-state": "낮은 위험 상태", + "show-low-critical-scale": "낮은 위험 상태 표시", + "low-critical-scale": "낮은 위험 상태", + "low-critical-state-hint": "double 값은 낮은 위험 범위가 최소값 스케일까지임을 나타냅니다.", + "filter-color": "필터 색상", + "colors": "색상", + "indicator-colors": "표시기 색상", + "enabled": "사용", + "disabled": "비활성화됨", + "on": "켜짐", + "off": "꺼짐", + "on-off-state": "켜짐/꺼짐 상태", + "on-off-state-hint": "컴포넌트가 켜짐 또는 꺼짐 상태인지 나타냅니다.", + "on-update-state": "켜짐 업데이트 상태", + "on-update-state-hint": "사용자가 상태를 켜짐으로 업데이트하기 위해 클릭할 때 호출되는 작업입니다.", + "off-update-state": "꺼짐 업데이트 상태", + "off-update-state-hint": "사용자가 상태를 꺼짐으로 업데이트하기 위해 클릭할 때 호출되는 작업입니다.", + "voltage": "전압", + "input-voltage": "입력 전압", + "input-voltage-hint": "double 값은 입력 전압 값을 나타냅니다.", + "output-voltage": "출력 전압", + "output-voltage-hint": "출력 전압 값을 나타내는 실수 값.", + "first-phase-voltage": "1상 전압", + "second-phase-voltage": "2상 전압", + "third-phase-voltage": "3상 전압", + "phase-voltage-hint": "현재 위상의 전압 값을 나타내는 실수 값", + "voltage-hint": "현재 전압을 나타내는 실수 값", + "current-voltage-color": "현재 전압 색상", + "phase-indicator-color": "위상 표시기 색상", + "measured": "측정됨", + "measured-hint": "킬로와트시 단위의 에너지 사용량을 나타내는 실수 값", + "day-rate": "주간 요금", + "night-rate": "야간 요금", + "off-peak-rate": "비성수기 요금", + "peak-rate": "성수기 요금", + "export-rate": "수출 요금", + "operating-mode": "운영 모드", + "bypass-mode": "바이패스", + "operating-mode-hint": "현재 작동 모드를 나타내는 정수 값 (0 - 꺼짐, 1 - 켜짐, 2 - 바이패스)", + "connected": "연결됨", + "connected-hint": "구성 요소가 연결 상태인지 나타냅니다.", + "disconnected": "연결 해제됨", + "indicator": "표시기", + "operation-mode": "운영 모드", + "operation-mode-hint": "인버터가 메인 또는 인버터 모드인지 나타냅니다.", + "operation-mode-indicators-color": "운영 모드 표시기 색상", + "mains-on-mode": "메인 켜짐", + "inverter-on-mode": "인버터 켜짐", + "charging-mode": "충전 모드", + "charging-mode-hint": "현재 충전 모드를 나타내는 정수 값 (1 - 벌크, 2 - 흡수, 3 - 플로트)", + "charging-mode-indicators-color": "충전 모드 표시기 색상", + "inverter-faults": "고장", + "inverter-fault-indicators-color": "고장 표시기 색상", + "overload-fault": "과부하", + "overload-fault-hint": "인버터가 과부하 상태인지 나타냅니다.", + "low-battery-fault": "배터리 부족", + "low-battery-fault-hint": "배터리가 과도하게 방전되었는지 나타냅니다.", + "temperature-fault": "온도", + "temperature-fault-hint": "인버터에 고온이 있는지 나타냅니다.", + "triangle": "삼각형", + "socket": "소켓", + "left-button": "왼쪽 버튼", + "right-button": "오른쪽 버튼", + "alarm-colors": "알람 색상", + "hook-color": "후크 색상" } }, "item": { @@ -1662,8 +4179,25 @@ "js-func": { "no-return-error": "함수는 값을 반환해야 합니다!", "return-type-mismatch": "함수는 '{{type}}' 유형의 값을 반환해야 합니다!", - "tidy": "Tidy", - "mini": "Mini" + "tidy": "깔끔한", + "mini": "미니", + "modules": "모듈", + "remove-module": "모듈 제거", + "no-modules": "구성된 모듈 없음", + "add-module": "모듈 추가", + "module-alias": "별명", + "invalid-module-alias-name": "잘못된 별명 이름", + "module-resource": "JS 모듈 리소스", + "not-unique-module-aliases-error": "모듈 별명은 고유해야 합니다!", + "show-module-info": "모듈 정보 표시", + "show-module-source-code": "모듈 소스 코드 표시", + "module-members": "모듈 멤버", + "module-no-members": "모듈에 내보낸 멤버가 없습니다", + "module-load-error": "모듈 로드 오류", + "source-code": "소스 코드", + "source-code-load-error": "소스 코드 로드 오류", + "no-js-module-text": "JS 모듈을 찾을 수 없음", + "no-js-module-matching": "'{{module}}'와(과) 일치하는 JS 모듈을 찾을 수 없습니다." }, "key-val": { "key": "키", @@ -1674,38 +4208,80 @@ }, "layout": { "layout": "레이아웃", + "layouts": "레이아웃", "manage": "레이아웃 관리", "settings": "레이아웃 설정", "color": "색", - "main": "Main", + "main": "메인", "right": "오른쪽", - "select": "대상 레이아웃 선택" + "left": "왼쪽", + "select": "대상 레이아웃 선택", + "percentage-width": "백분율 너비 (%)", + "fixed-width": "고정 너비 (px)", + "left-width": "왼쪽 열 (%)", + "right-width": "오른쪽 열 (%)", + "pick-fixed-side": "고정 측면: ", + "layout-fixed-width": "고정 너비 (px)", + "value-min-error": "값은 {{min}}{{unit}}보다 커야 합니다.", + "value-max-error": "값은 {{max}}{{unit}}보다 작아야 합니다.", + "layout-fixed-width-required": "고정 너비는 필수 항목입니다.", + "right-width-percentage-required": "오른쪽 백분율은 필수 항목입니다.", + "left-width-percentage-required": "왼쪽 백분율은 필수 항목입니다.", + "divider": "구분선", + "right-side": "오른쪽 레이아웃", + "left-side": "왼쪽 레이아웃", + "add-new-breakpoint": "새 브레이크포인트 추가", + "breakpoint": "브레이크포인트", + "breakpoints": "브레이크포인트", + "copy-from": "복사 원본", + "size": "크기", + "delete-breakpoint-title": "브레이크포인트 '{{name}}'을(를) 삭제하시겠습니까?", + "delete-breakpoint-text": "참고: 확인 후 중단점을 복구할 수 없게 되며 설정이 기본 중단점으로 되돌아갑니다." }, "legend": { "direction": "범례 방향", "position": "범례 위치", - "sort-legend": "범례에 데이터키 정렬", + "show-values": "값 표시", + "min-option": "최소", + "max-option": "최대", + "average-option": "평균", + "total-option": "합계", + "latest-option": "최신", + "sort-legend": "범례에 데이터키 정렬", "show-max": "최대값 표시", "show-min": "최소값 표시", "show-avg": "평균값 표시", "show-total": "총합 표시", + "show-latest": "최신 값 표시", "settings": "범례 설정", "min": "최소", "max": "최대", "avg": "평균", "total": "합계", + "latest": "최신", + "Min": "최소", + "Max": "최대", + "Avg": "평균", + "Total": "합계", + "Latest": "최신", "comparison-time-ago": { + "previousInterval": "(이전 간격)", + "customInterval": "(사용자 지정 간격)", "days": "(일 전)", "weeks": "(주 전)", "months": "(달 전)", "years": "(년 전)" - } + }, + "column-title": "열 제목", + "label": "라벨", + "value": "값" }, "login": { "login": "로그인", "request-password-reset": "비밀번호 재설정", "reset-password": "비밀번호 재설정", "create-password": "비밀번호 생성", + "two-factor-authentication": "2단계 인증", "passwords-mismatch-error": "입력된 비밀번호는 같아야 합니다!", "password-again": "비밀번호 확인", "sign-in": "로그인", @@ -1713,14 +4289,579 @@ "remember-me": "아이디 저장", "forgot-password": "비밀번호찾기", "password-reset": "비밀번호 재설정", - "expired-password-reset-message": "당신의 크리덴셜이 만료되었습니다! 새로운 비밀번호를 생성하세요.", + "expired-password-reset-message": "당신의 자격 증명이 만료되었습니다! 새로운 비밀번호를 생성하세요.", "new-password": "새 비밀번호", "new-password-again": "새 비밀번호 확인", "password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!", "email": "이메일", - "login-with": "{{name}}으로 로그인", + "invalid-email-format": "잘못된 Email 형식입니다.", + "sign-in-with": "{{name}}(으)로 로그인", + "sign-in-to-your-account": "계정에 로그인하세요", "or": "또는", - "error": "로그인 오류" + "error": "로그인 오류", + "verify-your-identity": "신원 확인", + "select-way-to-verify": "확인 방법 선택", + "resend-code": "코드 재전송", + "resend-code-wait": "{ time, plural, =1 {1초} other {#초} } 후에 코드 재전송", + "try-another-way": "다른 방법 시도", + "totp-auth-description": "인증기 앱에서 보안 코드를 입력하세요.", + "totp-auth-placeholder": "코드", + "sms-auth-description": "보안 코드가 {{contact}}의 전화번호로 전송되었습니다.", + "sms-auth-placeholder": "SMS 코드", + "email-auth-description": "보안 코드가 {{contact}}의 이메일 주소로 전송되었습니다.", + "email-auth-placeholder": "이메일 코드", + "backup-code-auth-description": "백업 코드 중 하나를 입력하세요.", + "backup-code-auth-placeholder": "백업 코드", + "activation-link-expired": "활성화 링크가 만료되었습니다", + "activation-link-expired-message": "프로필 활성화 링크가 만료되었습니다. 로그인 페이지로 돌아가서 새 이메일을 받을 수 있습니다.", + "reset-password-link-expired": "비밀번호 재설정 링크가 만료되었습니다", + "reset-password-link-expired-message": "비밀번호 재설정 링크가 만료되었습니다. 로그인 페이지로 돌아가서 새 이메일을 받을 수 있습니다.", + "two-fa": "2단계 인증", + "two-fa-required": "2단계 인증이 필요합니다", + "set-up-verification-method": "계속하려면 확인 방법을 설정하세요", + "set-up-verification-method-login": "확인 방법을 설정하거나 로그인하세요", + "enable-authenticator-app": "인증기 앱 활성화", + "enable-authenticator-app-description": "인증기 앱에서 보안 코드를 입력하세요", + "enable-authenticator-sms": "SMS 인증기 활성화", + "enable-authenticator-sms-description": "방금 전송된 6자리 코드를 입력하세요: ", + "enable-authenticator-email": "이메일 인증기 활성화", + "enable-authenticator-email-description": "보안 코드가 이메일 주소로 전송되었습니다: ", + "enter-key-manually": "또는 이 32자리 키를 직접 입력하세요:", + "continue": "계속", + "confirm": "확인", + "authenticator-app-success": "인증기 앱이 활성화되었습니다", + "authenticator-app-success-description": "다음에 로그인할 때 2단계 인증 코드를 제공해야 합니다", + "authenticator-sms-success": "SMS 인증기가 활성화되었습니다", + "authenticator-sms-success-description": "다음에 로그인할 때 전화번호로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다", + "authenticator-email-success": "이메일 인증기가 활성화되었습니다", + "authenticator-email-success-description": "다음에 로그인할 때 이메일 주소로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다", + "authenticator-backup-code-success": "백업 코드가 활성화되었습니다", + "authenticator-backup-code-success-description": "다음에 로그인할 때 보안 코드를 입력하거나 백업 코드 중 하나를 사용하라는 메시지가 표시됩니다.", + "add-verification-method": "확인 방법 추가", + "get-backup-code": "백업 코드 받기", + "copy-key": "키 복사", + "send-code": "코드 전송", + "email-label": "이메일", + "email-description": "인증기로 사용할 이메일을 입력하세요.", + "sms-description": "인증기로 사용할 전화번호를 입력하세요.", + "backup-code-description": "코드를 인쇄하여 계정에 로그인할 때 필요할 때 사용할 수 있도록 하세요. 각 백업 코드는 한 번만 사용할 수 있습니다.", + "backup-code-warn": "이 페이지를 떠나면 이 코드를 다시 볼 수 없습니다. 아래 옵션을 사용하여 안전하게 보관하세요.", + "download-txt": "다운로드 (txt)", + "print": "인쇄", + "verification-code": "6자리 코드", + "verification-code-invalid": "잘못된 확인 코드 형식입니다.", + "verification-code-incorrect": "확인 코드가 올바르지 않습니다.", + "verification-code-many-request": "확인 코드 확인 요청이 너무 많습니다.", + "scan-qr-code": "인증 앱으로 이 QR 코드를 스캔하세요", + "phone-input": { + "phone-input-label": "전화번호", + "phone-input-required": "전화번호는 필수 항목입니다.", + "phone-input-validation": "전화번호가 유효하지 않거나 불가능합니다.", + "phone-input-pattern": "잘못된 전화번호입니다. E.164 형식을 사용하세요 (예: {{phoneNumber}})", + "phone-input-hint": "E.164 형식의 전화번호 (예: {{phoneNumber}})" + } + }, + "markdown": { + "edit": "수정", + "preview": "미리보기", + "copy-code": "클릭하여 복사", + "copied": "복사됨!" + }, + "mobile": { + "add-application": "애플리케이션 추가", + "app-id": "앱 사이트 연결 ID", + "app-id-required": "앱 사이트 연결 ID가 필요합니다", + "app-id-pattern": "잘못된 앱 사이트 연결 ID 형식입니다.", + "app-store-link": "App Store 링크", + "app-store-link-required": "App Store 링크가 필요합니다", + "application-details": "애플리케이션 상세", + "application-package": "애플리케이션 패키지", + "application-secret": "애플리케이션 시크릿", + "application-secret-required": "애플리케이션 시크릿이 필요합니다", + "application": "애플리케이션", + "applications": "애플리케이션", + "copy-app-id": "앱 ID 복사", + "copy-app-store-link": "App Store 링크 복사", + "copy-application-package": "애플리케이션 패키지 복사", + "copy-application-secret": "애플리케이션 비밀키 복사", + "copy-google-play-link": "Google Play 링크 복사", + "copy-sha256-certificate-fingerprints": "SHA256 인증서 지문 복사", + "delete-application": "애플리케이션 삭제", + "delete-application-button-text": "결과를 이해하며 애플리케이션을 삭제합니다", + "delete-application-text": "이 작업은 취소할 수 없습니다. 애플리케이션이 영구적으로 삭제됩니다.
    영구 삭제하지 않으려면 애플리케이션을 임시로 중단할 수 있습니다.
    그래도 애플리케이션을 삭제하려면 \"{{phrase}}\"를 입력하여 확인하세요.", + "delete-application-title-short": "애플리케이션 '{{name}}'을(를) 삭제하시겠습니까?", + "delete-application-text-short": "주의: 확인 후 애플리케이션과 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-application-phrase": "애플리케이션 삭제", + "delete-applications-bundle-text": "주의: 확인 후 모바일 번들과 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-applications-bundle-title": "모바일 번들 '{{bundleName}}'을(를) 삭제하시겠습니까?", + "generate-application-secret": "애플리케이션 비밀키 생성", + "google-play-link": "Google Play 링크", + "google-play-link-required": "Google Play 링크는 필수 항목입니다.", + "latest-version": "최신 버전", + "min-version": "최소 버전", + "invalid-version-pattern": "잘못된 버전 형식입니다. major.minor.patch 형식을 사용하세요 (예: 1.0.0).", + "mobile-center": "모바일 센터", + "mobile-package": "애플리케이션 패키지", + "mobile-package-max-length": "애플리케이션 패키지는 256자 미만이어야 합니다", + "mobile-package-required": "애플리케이션 패키지가 필요합니다.", + "mobile-package-pattern": "애플리케이션 패키지 형식이 올바르지 않습니다", + "mobile-package-title": "애플리케이션 제목", + "mobile-package-title-max-length": "애플리케이션 제목은 256자 미만이어야 합니다", + "no-application": "애플리케이션을 찾을 수 없습니다", + "no-bundles": "번들을 찾을 수 없습니다", + "platform-type": "플랫폼 유형", + "search-application": "애플리케이션 검색", + "search-bundles": "번들 검색", + "set": "설정", + "sha256-certificate-fingerprints": "SHA256 인증서 지문", + "sha256-certificate-fingerprints-required": "SHA256 인증서 지문은 필수 항목입니다.", + "sha256-certificate-fingerprints-pattern": "잘못된 SHA256 인증서 지문 형식입니다.", + "show-hidden-pages": "숨겨진 페이지 표시", + "status": "상태", + "status-type": { + "deprecated": "사용 중단됨", + "draft": "초안", + "published": "게시됨", + "suspended": "일시 중지" + }, + "store-information": "스토어 정보", + "version-information": "버전 정보", + "min-version-release-notes": "최소 버전 릴리스 정보", + "latest-version-release-notes": "최신 버전 릴리스 정보", + "bundle": "번들", + "bundles": "번들", + "add-bundle": "번들 추가", + "title": "제목", + "title-required": "제목은 필수 항목입니다", + "title-cannot-contain-only-spaces": "제목은 공백만 포함할 수 없습니다.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "oauth-clients": "OAuth 2.0 클라이언트", + "android-app": "Android 앱", + "android-application": "Android 애플리케이션", + "ios-app": "iOS 앱", + "ios-application": "iOS 애플리케이션", + "invalid-store-link": "잘못된 스토어 링크입니다.", + "enable-oauth": "OAuth 2.0 활성화", + "enable-self-registration": "셀프 등록 활성화", + "edit-bundle": "번들 편집", + "description": "설명", + "basic-settings": "기본 설정", + "no-application-matching": "'{{entity}}'와(과) 일치하는 애플리케이션을 찾을 수 없습니다.", + "no-bundle-matching": "'{{entity}}'와(과) 일치하는 번들을 찾을 수 없습니다.", + "application-required": "애플리케이션이 필요합니다.", + "bundle-required": "번들이 필요합니다.", + "no-application-text": "애플리케이션을 찾을 수 없습니다", + "no-bundle-text": "번들을 찾을 수 없습니다", + "layout": "레이아웃", + "pages": "페이지", + "hide-all-pages": "모든 페이지 숨기기", + "reset-to-default-pages": "기본 페이지로 재설정", + "add-specific-page": "특정 페이지 추가", + "visible": "표시", + "hidden": "숨김", + "reset-to-page-default": "페이지를 기본값으로 재설정", + "mobile-599": "모바일 (최대 599px)", + "tablet-959": "태블릿 (최대 959px)", + "max-element-number": "최대 요소 수", + "page-name": "페이지 이름", + "page-name-required": "페이지 이름은 필수 항목입니다.", + "page-name-cannot-contain-only-spaces": "페이지 이름은 공백만 포함할 수 없습니다.", + "page-name-max-length": "페이지 이름은 256자 미만이어야 합니다.", + "page-type": "페이지 유형", + "pages-types": { + "dashboard": "대시보드", + "web-view": "웹 보기", + "custom": "사용자 지정" + }, + "url": "URL", + "invalid-url-format": "잘못된 URL 형식입니다.", + "path": "경로", + "invalid-path-format": "잘못된 경로 형식입니다.", + "custom-page": "사용자 지정 페이지", + "edit-page": "페이지 편집", + "edit-custom-page": "사용자 지정 페이지 편집", + "delete-page": "페이지 삭제", + "qr-code-widget": "QR 코드 위젯", + "type-here": "유형 히어로", + "configuration-dialog": "구성 대화상자", + "configuration-app": "구성 앱", + "configuration-step": { + "prepare-environment-title": "개발 환경 준비", + "prepare-environment-text": "Flutter ThingsBoard 모바일 애플리케이션에는 Flutter SDK가 필요합니다. 지침에 따라 Flutter SDK를 설정하세요.", + "get-source-code-title": "앱 소스 코드 얻기", + "get-source-code-text": "GitHub 저장소에서 복제하여 Flutter ThingsBoard 모바일 애플리케이션 소스 코드를 얻을 수 있습니다:", + "configure-app-settings-title": "앱 설정 구성", + "configure-app-settings-text": "구성 파일을 다운로드하여 이전 단계에서 복제한 프로젝트의 루트 디렉토리에 배치하세요.", + "download-file": "파일 다운로드", + "run-app-title": "앱 실행", + "run-app-text": "IDE에 설명된 대로 앱을 실행하세요.\n터미널을 사용하는 경우 다음 명령으로 앱을 실행하세요:", + "more-information": "자세한 정보는 시작하기 문서에서 찾을 수 있습니다.", + "getting-started": "시작하기" + } + }, + "notification": { + "action-button": "액션 버튼", + "action-type": "액션 유형", + "active": "활성", + "add-notification-recipients-group": "알림 수신자 그룹 추가", + "add-notification-template": "알림 템플릿 추가", + "add-recipient": "수신자 추가", + "add-recipients": "수신자 추가", + "add-rule": "규칙 추가", + "add-stage": "단계 추가", + "add-template": "템플릿 추가", + "after": "이후", + "alarm-assignment-trigger-settings": "알람 할당 트리거 설정", + "alarm-comment-trigger-settings": "알람 댓글 트리거 설정", + "alarm-trigger-settings": "알람 트리거 설정", + "all": "모두", + "api-feature-hint": "필드가 비어 있으면 트리거가 모든 API 기능에 적용됩니다.", + "api-usage-trigger-settings": "API 사용 트리거 설정", + "new-platform-version-trigger-settings": "새 플랫폼 버전 트리거 설정", + "rate-limits-trigger-settings": "속도 제한 초과 트리거 설정", + "task-processing-failure-trigger-settings": "작업 처리 실패 트리거 설정", + "resources-shortage-trigger-settings": "리소스 부족 트리거 설정", + "at-least-one-should-be-selected": "하나 이상 선택해야 합니다", + "basic-settings": "기본 설정", + "button-text": "버튼 텍스트", + "button-text-required": "버튼 텍스트가 필요합니다", + "button-text-max-length": "버튼 텍스트는 {{ length }}자 이하여야 합니다", + "compose": "작성", + "conversation": "대화", + "conversation-required": "대화는 필수 항목입니다.", + "copy-notification-template": "알림 템플릿 복사", + "copy-rule": "규칙 복사", + "copy-template": "템플릿 복사", + "create-new": "새로 만들기", + "created": "생성됨", + "customize-messages": "메시지 사용자 지정", + "cpu-threshold": "CPU 임계값", + "delete-notification-text": "주의: 확인 후 알림을 복구할 수 없게 됩니다.", + "delete-notification-title": "알림을 삭제하시겠습니까?", + "delete-notifications-text": "주의: 확인 후 알림을 복구할 수 없게 됩니다.", + "delete-notifications-title": "{ count, plural, =1 {1개 알림} other {#개 알림} }을(를) 삭제하시겠습니까?", + "delete-recipient-text": "주의: 확인 후 수신자를 복구할 수 없게 됩니다.", + "delete-recipient-title": "수신자 '{{recipientName}}'을(를) 삭제하시겠습니까?", + "delete-recipients-text": "주의: 확인 후 수신자를 복구할 수 없게 됩니다.", + "delete-recipients-title": "{ count, plural, =1 {1개 수신자} other {#개 수신자} }를 삭제하시겠습니까?", + "delete-request-text": "주의: 확인 후 요청을 복구할 수 없게 됩니다.", + "delete-request-title": "요청을 삭제하시겠습니까?", + "delete-requests-text": "주의: 확인 후 요청을 복구할 수 없게 됩니다.", + "delete-requests-title": "{ count, plural, =1 {1개 요청} other {#개 요청} }을(를) 삭제하시겠습니까?", + "delete-rule-text": "주의: 확인 후 규칙을 복구할 수 없게 됩니다.", + "delete-rule-title": "규칙 '{{ruleName}}'을(를) 삭제하시겠습니까?", + "delete-rules-text": "주의: 확인 후 규칙을 복구할 수 없게 됩니다.", + "delete-rules-title": "{ count, plural, =1 {1개 규칙} other {#개 규칙} }을(를) 삭제하시겠습니까?", + "delete-template-text": "주의: 확인 후 템플릿을 복구할 수 없게 됩니다.", + "delete-template-title": "템플릿 '{{templateName}}'을(를) 삭제하시겠습니까?", + "delete-templates-text": "주의: 확인 후 템플릿을 복구할 수 없게 됩니다.", + "delete-templates-title": "{ count, plural, =1 {1개 템플릿} other {#개 템플릿} }을(를) 삭제하시겠습니까?", + "deleted": "삭제됨", + "delivery-method": { + "delivery-method": "전달 방법", + "email": "이메일", + "email-preview": "이메일 알림 미리보기", + "slack": "Slack", + "slack-preview": "Slack 알림 미리보기", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams 알림 미리보기", + "sms": "SMS", + "sms-preview": "SMS 알림 미리보기", + "web": "웹", + "web-preview": "웹 알림 미리보기", + "mobile-app": "모바일 앱", + "mobile-app-preview": "모바일 앱 알림 미리보기" + }, + "delivery-method-not-configure-click": "전달 방법이 구성되지 않았습니다. 클릭하여 설정하세요.", + "delivery-method-not-configure-contact": "전달 방법이 구성되지 않았습니다. 시스템 관리자에게 문의하세요.", + "delivery-methods": "전달 방법", + "description": "설명", + "device-activity-trigger-settings": "장치 활성 트리거 설정", + "device-list-rule-hint": "필드가 비어 있으면 트리거가 모든 장치에 적용됩니다.", + "device-profiles-list-rule-hint": "필드가 비어 있으면 트리거가 모든 장치 프로필에 적용됩니다", + "disabled": "비활성화됨", + "edge-trigger-settings": "Edge 트리거 설정", + "edge-list-rule-hint": "필드가 비어 있으면 트리거가 모든 엣지 인스턴스에 적용됩니다", + "edit-notification-recipients-group": "알림 수신자 그룹 편집", + "edit-notification-template": "알림 템플릿 편집", + "edit-rule": "규칙 편집", + "edit-template": "템플릿 편집", + "enabled": "사용", + "entities-limit-trigger-settings": "엔티티 제한 트리거 설정", + "entity-action-trigger-settings": "엔티티 동작 트리거 설정", + "entity-type": "엔티티 유형", + "escalation-chain": "에스컬레이션 체인", + "failed-send": "전송 실패", + "fails": "{ count, plural, =1 {1회 실패} other {#회 실패} }", + "filter": "필터", + "first-recipient": "첫 번째 수신자", + "inactive": "비활성", + "inbox": "받은편지함", + "notification-inbox": "알림 / 받은편지함", + "input-field-support-templatization": "입력 필드는 템플릿화를 지원합니다.", + "input-fields-support-templatization": "입력 필드는 템플릿화를 지원합니다.", + "link": "링크", + "link-required": "링크는 필수 항목입니다.", + "link-max-length": "링크는 {{ length }}자 이하여야 합니다.", + "link-type": { + "dashboard": "대시보드 열기", + "link": "URL 링크 열기" + }, + "loading-notifications": "알림 로딩 중...", + "management": "알림 관리", + "mark-all-as-read": "모두 읽음으로 표시", + "mark-as-read": "읽음으로 표시", + "message": "메시지", + "message-required": "메시지는 필수 항목입니다.", + "message-max-length": "메시지는 {{ length }}자 이하여야 합니다.", + "name": "이름", + "name-required": "이름은 필수 항목입니다.", + "new-notification": "새 알림", + "no-inbox-notification": "알림을 찾을 수 없음", + "no-notification-request": "알림 요청 없음", + "no-notification-templates": "알림 템플릿을 찾을 수 없음", + "no-notifications-yet": "아직 알림 없음", + "no-recipients-notification": "수신자 알림 없음", + "no-recipients-matching": "'{{entity}}'와(과) 일치하는 수신자를 찾을 수 없습니다.", + "no-recipients-text": "수신자를 찾을 수 없음", + "no-rule": "구성된 규칙 없음", + "no-rules-notification": "규칙 알림 없음", + "no-severity-found": "심각도를 찾을 수 없음", + "no-severity-matching": "'{{severity}}'을(를) 찾을 수 없습니다.", + "no-template-matching": "'{{template}}'와(과) 일치하는 리소스를 찾을 수 없습니다.", + "create-new-template": "새로 만들기!", + "not-found-slack-recipient": "Slack 수신자를 찾을 수 없음", + "notification": "알림", + "notification-center": "알림 센터", + "notification-tap-action": "알림 탭 동작", + "notification-tap-action-hint": "비활성화하면 기본 알람 대시보드가 사용됩니다.", + "notify": "알림", + "notify-again": "다시 알림", + "notify-alarm-action": { + "acknowledged": "확인", + "assigned": "알람 할당됨", + "cleared": "해제", + "created": "알람 생성됨", + "severity-changed": "알람 심각도 변경됨", + "unassigned": "알람 할당 해제" + }, + "notify-on": "알림 설정", + "notify-on-comment-update": "댓글 업데이트 시 알림", + "notify-on-required": "알림 설정은 필수 항목입니다.", + "notify-on-unassign": "할당 해제 시 알림", + "notify-only-user-comments": "사용자 댓글에만 알림", + "only-rule-chain-lifecycle-failures": "규칙 체인 수명 주기 실패만", + "only-rule-node-lifecycle-failures": "규칙 노드 수명 주기 실패만", + "platform-users": "플랫폼 사용자", + "ram-threshold": "RAM 임계값", + "rate-limits": "속도 제한", + "rate-limits-hint": "필드가 비어 있으면 트리거가 모든 속도 제한에 적용됩니다.", + "recipient": "수신자", + "recipient-group": "수신자 그룹", + "recipient-type": { + "affected-tenant-administrators": "영향받는 테넌트 관리자", + "affected-user": "영향받는 사용자", + "all-users": "모든 사용자", + "customer-users": "고객 사용자", + "system-administrators": "시스템 관리자", + "tenant-administrators": "테넌트 관리자", + "user-filters": "사용자 필터", + "user-list": "사용자 목록", + "users-entity-owner": "엔티티 소유자의 사용자" + }, + "recipients": "수신자", + "notification-recipient": "알림 수신자", + "notification-recipient-required": "알림 수신자는 필수 항목입니다.", + "notification-recipients": "알림 / 수신자", + "recipients-count": "{ count, plural, =1 {1명 수신자} other {#명 수신자} }", + "recipients-required": "수신자는 필수 항목입니다.", + "refresh-allow-delivery-method": "전달 방법 새로고침 허용", + "request-search": "요청 검색", + "request-status": { + "processing": "처리 중", + "scheduled": "예약됨", + "sent": "전송됨" + }, + "review": "검토", + "rule": "규칙", + "rule-chain-list-rule-hint": "필드가 비어 있으면 트리거가 모든 규칙 체인에 적용됩니다.", + "rule-engine-events-trigger-settings": "규칙 엔진 이벤트 트리거 설정", + "rule-engine-filter": "규칙 엔진 필터", + "rule-name": "규칙 이름", + "rule-name-required": "이름은 필수 항목입니다.", + "rule-disable": "알림 규칙 비활성화", + "rule-enable": "알림 규칙 활성화", + "rule-node-filter": "규칙 노드 필터", + "rules": "규칙", + "notification-rules": "알림 / 규칙", + "scheduler-later": "나중에 예약", + "search-notification": "알림 검색", + "search-recipients": "수신자 검색", + "search-rules": "규칙 검색", + "search-templates": "템플릿 검색", + "see-documentation": "문서 보기", + "selected-notifications": "{ count, plural, =1 {1개 알림} other {#개 알림} } 선택됨", + "selected-recipients": "{ count, plural, =1 {1명 수신자} other {#명 수신자} } 선택됨", + "selected-requests": "{ count, plural, =1 {1개 요청} other {#개 요청} } 선택됨", + "selected-rules": "{ count, plural, =1 {1개 규칙} other {#개 규칙} } 선택됨", + "selected-template": "{ count, plural, =1 {1개 템플릿} other {#개 템플릿} } 선택됨", + "send-notification": "알림 전송", + "sent": "전송됨", + "setup": "설정", + "notification-sent": "알림 / 전송됨", + "set-entity-from-notification": "알림에서 대시보드 상태로 엔티티 설정", + "slack-chanel-type": "Slack 채널 유형", + "slack-chanel-types": { + "direct": "다이렉트 메시지", + "private-channel": "비공개 채널", + "public-channel": "공개 채널" + }, + "start-from-scratch": "처음부터 시작", + "status": "상태", + "stop-escalation-alarm-status-become": "알람 상태가 다음이 되면 에스컬레이션 중지:", + "storage-threshold": "저장소 임계값", + "subject": "제목", + "subject-required": "제목이 필요합니다", + "subject-max-length": "제목은 {{ length }}자 이하여야 합니다", + "template": "템플릿", + "template-name": "템플릿 이름", + "template-required": "템플릿이 필요합니다", + "template-type": { + "alarm": "알람", + "alarm-assignment": "알람 할당", + "alarm-comment": "알람 댓글", + "api-usage-limit": "API 사용 한도", + "device-activity": "장치 활동", + "entities-limit": "엔티티 한도", + "entities-limit-increase-request": "엔티티 한도 증가 요청", + "entity-action": "엔티티 작업", + "general": "일반", + "rule-engine-lifecycle-event": "규칙 엔진 수명 주기 이벤트", + "rule-node": "규칙 노드", + "new-platform-version": "새 플랫폼 버전", + "rate-limits": "속도 제한 초과", + "edge-communication-failure": "엣지 통신 실패", + "edge-connection": "엣지 연결", + "task-processing-failure": "작업 처리 실패", + "resources-shortage": "리소스 부족" + }, + "templates": "템플릿", + "notification-templates": "알림 / 템플릿", + "tenant-profiles-list-rule-hint": "필드가 비어 있으면 트리거가 모든 테넌트 프로필에 적용됩니다", + "tenants-list-rule-hint": "필드가 비어 있으면 트리거가 모든 테넌트에 적용됩니다", + "threshold": "임계값", + "theme-color": "테마 색상", + "time": "시간", + "track-rule-node-events": "규칙 노드 이벤트 추적", + "trigger": { + "alarm": "알람", + "alarm-assignment": "알람 할당", + "alarm-comment": "알람 댓글", + "api-usage-limit": "API 사용 한도", + "device-activity": "장치 활동", + "entities-limit": "엔티티 한도", + "entity-action": "엔티티 작업", + "rule-engine-lifecycle-event": "규칙 엔진 수명 주기 이벤트", + "new-platform-version": "새 플랫폼 버전", + "rate-limits": "속도 제한 초과", + "edge-connection": "엣지 연결", + "edge-communication-failure": "엣지 통신 실패", + "task-processing-failure": "작업 처리 실패", + "resources-shortage": "리소스 부족", + "trigger": "트리거", + "trigger-required": "트리거가 필요합니다" + }, + "type": "유형", + "unread": "읽지 않음", + "updated": "수정됨", + "use-deprecated-webhook-connectors": "더 이상 사용되지 않는 웹훅 커넥터 사용", + "use-old-api": "이전 API 사용", + "use-template": "템플릿 사용", + "view-all": "모두 보기", + "warning": "경고", + "webhook-url": "웹훅 URL", + "webhook-url-required": "웹훅 URL이 필요합니다", + "workflow-url": "워크플로 URL", + "workflow-url-required": "워크플로 URL이 필요합니다", + "channel-name": "채널 이름", + "channel-name-required": "채널 이름이 필요합니다", + "settings": { + "notification-settings": "알림 설정", + "reset-all": "모든 설정 재설정", + "reset-all-title": "폼을 재설정하시겠습니까?", + "reset-all-text": "확인 후 설정 폼이 기본값으로 재설정되고 저장됩니다.", + "type": "유형", + "enable-all": "모두 활성화", + "disable-all": "모두 비활성화", + "delivery-not-configured": "배달 방법이 구성되지 않음" + } + }, + "ota-update": { + "add": "패키지 추가", + "assign-firmware": "할당된 펌웨어", + "assign-firmware-required": "할당된 펌웨어가 필요합니다", + "assign-software": "할당된 소프트웨어", + "assign-software-required": "할당된 소프트웨어가 필요합니다", + "auto-generate-checksum": "체크섬 자동 생성", + "checksum": "체크섬", + "checksum-hint": "체크섬이 비어 있으면 자동으로 생성됩니다.", + "checksum-algorithm": "체크섬 알고리즘", + "checksum-copied-message": "패키지 체크섬이 클립보드에 복사되었습니다.", + "change-firmware": "펌웨어 변경으로 { count, plural, =1 {1개 장치} other {#개 장치} }가 업데이트될 수 있습니다.", + "change-software": "소프트웨어 변경으로 { count, plural, =1 {1개 장치} other {#개 장치} }가 업데이트될 수 있습니다.", + "change-ota-setting-title": "OTA 설정을 변경하시겠습니까?", + "chose-compatible-device-profile": "업로드된 패키지는 선택한 프로필이 있는 장치에서만 사용할 수 있습니다.", + "chose-firmware-distributed-device": "장치에 배포할 펌웨어를 선택하세요.", + "chose-software-distributed-device": "장치에 배포할 소프트웨어를 선택하세요.", + "content-type": "콘텐츠 유형", + "copy-checksum": "체크섬 복사", + "copy-direct-url": "직접 URL 복사", + "copyId": "패키지 ID 복사", + "copied": "복사됨!", + "delete": "패키지 삭제", + "delete-ota-update-text": "주의: 확인 후 OTA 업데이트를 복구할 수 없게 됩니다.", + "delete-ota-update-title": "OTA 업데이트 '{{title}}'을(를) 삭제하시겠습니까?", + "delete-ota-updates-text": "주의: 확인 후 선택한 모든 OTA 업데이트가 제거됩니다.", + "delete-ota-updates-title": "{ count, plural, =1 {1개 OTA 업데이트} other {#개 OTA 업데이트} }을(를) 삭제하시겠습니까?", + "description": "설명", + "direct-url": "직접 URL", + "direct-url-copied-message": "패키지 직접 URL이 클립보드에 복사되었습니다.", + "direct-url-required": "직접 URL은 필수 항목입니다.", + "download": "패키지 다운로드", + "drop-file": "패키지 파일을 끌어다 놓거나 클릭하여 업로드할 파일을 선택하세요.", + "drop-package-file-or": "패키지 파일을 끌어다 놓거나", + "file-name": "파일 이름", + "file-size": "파일 크기", + "file-size-bytes": "파일 크기 (바이트)", + "idCopiedMessage": "패키지 ID가 클립보드에 복사되었습니다.", + "no-firmware-matching": "'{{entity}}'과(와) 일치하는 호환되는 펌웨어 OTA 업데이트 패키지가 없습니다.", + "no-firmware-text": "프로비저닝된 호환되는 펌웨어 OTA 업데이트 패키지가 없습니다.", + "no-packages-text": "패키지를 찾을 수 없습니다", + "no-software-matching": "'{{entity}}'과(와) 일치하는 호환되는 소프트웨어 OTA 업데이트 패키지가 없습니다.", + "no-software-text": "프로비저닝된 호환되는 소프트웨어 OTA 업데이트 패키지가 없습니다.", + "ota-update": "OTA 업데이트", + "ota-update-details": "OTA 업데이트 세부 정보", + "ota-updates": "OTA 업데이트", + "package-file": "패키지 파일", + "package-type": "패키지 유형", + "packages-repository": "패키지 저장소", + "search": "패키지 검색", + "selected-package": "{ count, plural, =1 {1개 패키지} other {#개 패키지} } 선택됨", + "title": "제목", + "title-required": "제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "types": { + "firmware": "펌웨어", + "software": "소프트웨어" + }, + "upload-binary-file": "바이너리 파일 업로드", + "use-external-url": "외부 URL 사용", + "version": "버전", + "version-required": "버전은 필수 항목입니다.", + "version-tag": "버전 태그", + "version-tag-hint": "사용자 지정 태그는 장치에서 보고한 패키지 버전과 일치해야 합니다.", + "version-max-length": "버전은 256자 미만이어야 합니다.", + "warning-after-save-no-edit": "패키지가 업로드되면 제목, 버전, 장치 프로필 및 패키지 유형을 수정할 수 없습니다." }, "position": { "top": "상단", @@ -1729,14 +4870,109 @@ "right": "오른쪽" }, "profile": { - "profile": "프로파일", + "profile": "프로필", "last-login-time": "마지막 로그인", "change-password": "비밀번호 변경", - "current-password": "현재 비밀번호" + "current-password": "현재 비밀번호", + "copy-jwt-token": "JWT 토큰 복사", + "jwt-token": "JWT 토큰", + "token-valid-till": "토큰 유효 기간", + "tokenCopiedSuccessMessage": "JWT 토큰이 클립보드에 복사되었습니다.", + "tokenCopiedWarnMessage": "JWT 토큰이 만료되었습니다! 페이지를 새로고침하세요." + }, + "profiles": { + "profiles": "프로필" + }, + "security": { + "security": "보안", + "general-settings": "일반 보안 설정", + "access-token": "액세스 토큰", + "access-token-required": "액세스 토큰이 필요합니다", + "clientId": "클라이언트 ID", + "clientId-required": "클라이언트 ID가 필요합니다", + "username": "사용자명", + "username-required": "사용자 이름은 필수 항목입니다.", + "ca-cert": "CA 인증서", + "2fa": { + "2fa": "2단계 인증", + "2fa-description": "2단계 인증은 무단 액세스로부터 계정을 보호합니다. 로그인할 때 보안 코드를 입력하기만 하면 됩니다.", + "authenticate-with": "다음으로 인증할 수 있습니다:", + "disable-2fa-provider-text": "{{name}}을(를) 비활성화하면 계정 보안이 약화됩니다.", + "disable-2fa-provider-title": "{{name}}을(를) 비활성화하시겠습니까?", + "get-new-code": "새 코드 받기", + "main-2fa-method": "기본 2단계 인증 방법으로 사용", + "dialog": { + "activation-step-description-email": "다음에 로그인할 때 이메일 주소로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다.", + "activation-step-description-sms": "다음에 로그인할 때 전화번호로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다.", + "activation-step-description-totp": "다음에 로그인할 때 2단계 인증 코드를 제공해야 합니다.", + "activation-step-label": "활성화", + "backup-code-description": "코드를 인쇄하여 계정에 로그인할 때 필요할 때 사용할 수 있도록 하세요. 각 백업 코드는 한 번만 사용할 수 있습니다.", + "backup-code-warn": "이 페이지를 떠나면 이 코드를 다시 볼 수 없습니다. 아래 옵션을 사용하여 안전하게 보관하세요.", + "download-txt": "다운로드 (txt)", + "email-step-description": "인증기로 사용할 이메일을 입력하세요.", + "email-step-label": "이메일", + "enable-email-title": "이메일 인증기 활성화", + "enable-sms-title": "SMS 인증기 활성화", + "enable-totp-title": "인증기 앱 활성화", + "enter-verification-code": "여기에 6자리 코드를 입력하세요", + "get-backup-code-title": "백업 코드 받기", + "next": "다음", + "scan-qr-code": "인증 앱으로 이 QR 코드를 스캔하세요", + "send-code": "코드 전송", + "sms-step-description": "인증기로 사용할 전화번호를 입력하세요.", + "sms-step-label": "전화번호", + "success": "성공!", + "totp-step-description-install": "Google Authenticator, Authy 또는 Duo와 같은 앱을 설치할 수 있습니다.", + "totp-step-description-open": "휴대전화에서 인증기 앱을 엽니다.", + "totp-step-label": "앱 받기", + "verification-code": "6자리 코드", + "verification-code-invalid": "잘못된 확인 코드 형식입니다.", + "verification-code-incorrect": "확인 코드가 올바르지 않습니다.", + "verification-code-many-request": "확인 코드 확인 요청이 너무 많습니다.", + "verification-step-description": "{{address}}(으)로 방금 전송된 6자리 코드를 입력하세요", + "verification-step-label": "확인" + }, + "provider": { + "email": "이메일", + "email-description": "이메일 주소로 전송된 보안 코드를 사용하여 인증하세요.", + "email-hint": "인증 코드가 {{ info }}(으)로 이메일로 전송됩니다", + "sms": "SMS", + "sms-description": "휴대전화를 사용하여 인증하세요. 로그인할 때 SMS 메시지로 보안 코드를 보내드립니다.", + "sms-hint": "인증 코드가 {{ info }}(으)로 문자 메시지로 전송됩니다", + "totp": "인증기 앱", + "totp-description": "휴대전화에서 Google Authenticator, Authy 또는 Duo와 같은 앱을 사용하여 인증하세요. 로그인할 보안 코드를 생성합니다.", + "totp-hint": "계정에 인증기 앱이 설정되었습니다", + "backup_code": "백업 코드", + "backup-code-description": "이 인쇄 가능한 일회용 암호는 여행 중처럼 휴대전화를 사용할 수 없을 때 로그인할 수 있도록 해줍니다.", + "backup-code-hint": "현재 {{ info }}개의 일회용 코드가 활성화되어 있습니다." + } + }, + "password-requirement": { + "at-least": "최소:", + "character": "{ count, plural, =1 {1자} other {#자} }", + "digit": "{ count, plural, =1 {1자리 숫자} other {#자리 숫자} }", + "password-tooltip-min-length": "최소 {{minimumLength}}자 이상", + "password-tooltip-max-length": "최대 {{maximumLength}}자 이하", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}}개 대문자", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}}개 소문자", + "password-tooltip-digit": "{{minimumDigits}}개 숫자", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}}개 특수 문자", + "incorrect-password-try-again": "잘못된 비밀번호입니다. 다시 시도하세요.", + "lowercase-letter": "{ count, plural, =1 {1개 소문자} other {#개 소문자} }", + "new-passwords-not-match": "새 비밀번호가 일치하지 않습니다.", + "password-should-not-contain-spaces": "비밀번호에 공백이 포함되지 않아야 합니다.", + "password-not-meet-requirements": "비밀번호가 요구 사항을 충족하지 않습니다.", + "password-requirements": "비밀번호 요구 사항", + "password-should-difference": "새 비밀번호는 현재 비밀번호와 달라야 합니다.", + "special-character": "{ count, plural, =1 {1개 특수 문자} other {#개 특수 문자} }", + "uppercase-letter": "{ count, plural, =1 {1개 대문자} other {#개 대문자} }", + "at-most": "최대:" + } }, "relation": { "relations": "관계", "direction": "방향", + "clear-relation-type": "관계 유형 지우기", "search-direction": { "FROM": "보내는 사람", "TO": "받는 사람" @@ -1749,41 +4985,135 @@ "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": "보내는 개체", + "to-entity-type": "받는 엔티티 유형", + "to-entity-name": "받는 엔티티 이름", + "from-entity-type": "보내는 엔티티 유형", + "from-entity-name": "받는 엔티티 이름", + "to-entity": "받는 엔티티", + "from-entity": "보내는 엔티티", "delete": "관계 삭제", "relation-type": "관계 유형", "relation-type-required": "관계 유형을 입력하세요.", + "relation-type-max-length": "관계 유형은 256자 미만이어야 합니다.", "any-relation-type": "전체 유형", "add": "관계 추가", "edit": "관계 편집", - "delete-to-relation-title": "개체 '{{entityName}}'을(를) 삭제하시겠습니까?", - "delete-to-relation-text": "주의: 개체 '{{entityName}}'는 현재 개체와 관계가 해제됩니다.", + "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-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": "주의: 모든 선택된 관계는 제거되며, 현제 개체는 해당되는 대체와 관계가 해제됩니다.", + "delete-from-relations-text": "주의: 모든 선택된 관계는 제거되며, 현제 엔티티는 해당되는 대체와 관계가 해제됩니다.", "remove-relation-filter": "관계 필터 삭제", + "remove-filter": "필터 제거", "add-relation-filter": "관계 필터 추가", "any-relation": "전체 관계", "relation-filters": "관계 필터", + "relation-filter": "관계 필터", "additional-info": "추가 정보 (JSON)", "invalid-additional-info": "추가적인 정보 JSON을 파싱할 수 없습니다.", - "no-relations-text": "아무 관계도 찾을 수 없습니다" + "no-relations-text": "아무 관계도 찾을 수 없습니다", + "not": "제외", + "copy-type": "유형 복사" + }, + "resource": { + "add": "리소스 추가", + "all-types": "모두", + "copyId": "리소스 ID 복사", + "delete": "리소스 삭제", + "delete-resource-text": "주의: 확인 후 리소스를 복구할 수 없게 됩니다.", + "delete-resource-title": "리소스 '{{resourceTitle}}'을(를) 삭제하시겠습니까?", + "delete-resources-action-title": "{ count, plural, =1 {1개 리소스} other {#개 리소스} } 삭제", + "delete-resources-text": "선택한 리소스는 장치 프로필에서 사용 중이더라도 삭제됩니다.", + "delete-resources-title": "{ count, plural, =1 {1개 리소스} other {#개 리소스} }를 삭제하시겠습니까?", + "download": "리소스 다운로드", + "drop-file": "리소스 파일을 끌어다 놓거나 클릭하여 업로드할 파일을 선택하세요.", + "drop-resource-file-or": "리소스 파일을 끌어다 놓거나", + "empty": "리소스가 비어 있습니다", + "file-name": "파일 이름", + "idCopiedMessage": "리소스 ID가 클립보드에 복사되었습니다.", + "no-resource-matching": "'{{widgetsBundle}}'와(과) 일치하는 리소스를 찾을 수 없습니다.", + "no-resource-text": "리소스를 찾을 수 없습니다", + "open-widgets-bundle": "위젯 번들 열기", + "resource": "리소스", + "resource-file": "리소스 파일", + "resource-files": "리소스 파일", + "resource-library-details": "리소스 세부 정보", + "resource-type": "리소스 유형", + "resources-library": "리소스 라이브러리", + "search": "리소스 검색", + "selected-resources": "{ count, plural, =1 {1개 리소스} other {#개 리소스} } 선택됨", + "system": "시스템", + "title": "제목", + "title-required": "제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "type": { + "jks": "JKS", + "js-module": "JS 모듈", + "lwm2m-model": "LWM2M 모델", + "pkcs-12": "PKCS #12", + "general": "일반" + }, + "resource-sub-type": "하위 유형", + "sub-type": { + "image": "이미지", + "scada-symbol": "SCADA 심볼", + "extension": "확장", + "module": "모듈" + }, + "resource-is-in-use": "리소스가 다른 엔티티에서 사용 중입니다", + "resources-are-in-use": "리소스가 다른 엔티티에서 사용 중입니다", + "resource-is-in-use-text": "'{{title}}' 리소스가 다음 엔티티에서 사용 중이므로 삭제되지 않았습니다:", + "resources-are-in-use-text": "모든 리소스가 삭제되지 않았습니다. 다른 엔티티에서 사용 중이기 때문입니다.
    해당 리소스 행의 참조 버튼을 클릭하여 참조된 엔티티를 볼 수 있습니다.
    이 리소스를 계속 삭제하려면 아래 표에서 선택하고 선택 삭제 버튼을 클릭하세요.", + "delete-resource-in-use-text": "리소스를 계속 삭제하려면 무조건 삭제 버튼을 클릭하세요." + }, + "javascript": { + "add": "JavaScript 리소스 추가", + "delete": "JavaScript 리소스 삭제", + "delete-javascript-resource-text": "주의: 확인 후 JavaScript 리소스를 복구할 수 없게 됩니다.", + "delete-javascript-resource-title": "JavaScript 리소스 '{{resourceTitle}}'을(를) 삭제하시겠습니까?", + "delete-javascript-resources-action-title": "JavaScript { count, plural, =1 {1개 리소스} other {#개 리소스} } 삭제", + "delete-javascript-resources-text": "선택한 JavaScript 리소스는 JavaScript 함수에서 사용 중이더라도 삭제됩니다.", + "delete-javascript-resources-title": "JavaScript { count, plural, =1 {1개 리소스} other {#개 리소스} }를 삭제하시겠습니까?", + "delete-javascript-resource-in-use-text": "JavaScript 리소스를 계속 삭제하려면 무조건 삭제 버튼을 클릭하세요.", + "download": "JavaScript 리소스 다운로드", + "upload-from-file": "파일에서 JavaScript 업로드", + "resource-file": "JavaScript 리소스 파일", + "drop-file": "JavaScript 파일을 끌어다 놓거나 클릭하여 업로드할 파일을 선택하세요.", + "drop-resource-file-or": "JavaScript 파일을 끌어다 놓거나", + "javascript-library": "JavaScript 라이브러리", + "javascript-type": "JavaScript 유형", + "javascript-resource-details": "JavaScript 리소스 세부 정보", + "javascript-resource-is-in-use": "JavaScript 리소스가 다른 엔티티에서 사용 중입니다", + "javascript-resources-are-in-use": "JavaScript 리소스가 다른 엔티티에서 사용 중입니다", + "javascript-resource-is-in-use-text": "'{{title}}' JavaScript 리소스가 다음 엔티티에서 사용 중이므로 삭제되지 않았습니다:", + "javascript-resources-are-in-use-text": "모든 JavaScript 리소스가 삭제되지 않았습니다. 다른 엔티티에서 사용 중이기 때문입니다.
    해당 리소스 행의 참조 버튼을 클릭하여 참조된 엔티티를 볼 수 있습니다.
    이 JavaScript 리소스를 계속 삭제하려면 아래 표에서 선택하고 선택 삭제 버튼을 클릭하세요.", + "search": "JavaScript 리소스 검색", + "selected-javascript-resources": "{ count, plural, =1 {1개 JavaScript 리소스} other {#개 JavaScript 리소스} } 선택됨", + "no-javascript-resource-text": "JavaScript 리소스를 찾을 수 없습니다", + "all-types": "모두", + "module-script": "모듈 스크립트" + }, + "rpc": { + "error": { + "target-device-is-not-set": "대상 장치가 설정되지 않았습니다!", + "invalid-target-entity": "{{entityType}} 엔티티는 RPC 명령을 지원하지 않습니다.", + "failed-to-resolve-target-device": "대상 장치를 확인하지 못했습니다!", + "request-timeout": "요청 시간 초과", + "rpc-http-error": "오류: {{status}} - {{statusText}}" + } }, "rulechain": { "rulechain": "규칙 사슬", + "rulechain-events": "규칙 체인 이벤트", "rulechains": "규칙 사슬", "root": "루트", "delete": "규칙 사슬 삭제", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", "description": "설명", "add": "규칙 사슬 추가", "set-root": "규칙 사슬 루트로 전환", @@ -1815,27 +5145,53 @@ "debug-mode": "디버그 모드", "search": "규칙 사슬 검색", "selected-rulechains": "{ count, plural, =1 {1 개 규칙 사슬} other {# 개 규칙 사슬} } 선택됨", - "open-rulechain": "규칙 사슬 열기" + "open-rulechain": "규칙 사슬 열기", + "edge-template-root": "템플릿 루트", + "assign-to-edge": "엣지에 할당", + "edge-rulechain": "엣지 규칙 체인", + "unassign-rulechain-from-edge-text": "확인 후 규칙 체인이 할당 해제되어 엣지에서 액세스할 수 없습니다.", + "unassign-rulechains-from-edge-title": "{ count, plural, =1 {1개 규칙 체인} other {#개 규칙 체인} }을(를) 할당 해제하시겠습니까?", + "unassign-rulechains-from-edge-text": "확인 후 선택한 모든 규칙 체인이 할당 해제되어 엣지에서 액세스할 수 없습니다.", + "assign-rulechain-to-edge-title": "Edge에 규칙 체인 할당", + "assign-rulechain-to-edge-text": "Edge에 할당할 규칙 체인을 선택하세요", + "set-edge-template-root-rulechain": "규칙 체인을 Edge 템플릿 루트로 만들기", + "set-edge-template-root-rulechain-title": "규칙 체인 '{{ruleChainName}}'을(를) 엣지 템플릿 루트로 설정하시겠습니까?", + "set-edge-template-root-rulechain-text": "확인 후 규칙 체인이 엣지 템플릿 루트가 되며 새로 생성된 엣지의 루트 규칙 체인이 됩니다.", + "invalid-rulechain-type-error": "규칙 체인을 가져올 수 없습니다: 잘못된 규칙 체인 유형입니다. 예상 유형은 {{expectedRuleChainType}}입니다.", + "set-auto-assign-to-edge": "생성 시 Edge에 규칙 체인 할당", + "set-auto-assign-to-edge-title": "엣지 규칙 체인 '{{ruleChainName}}'을(를) 생성 시 엣지에 할당하시겠습니까?", + "set-auto-assign-to-edge-text": "확인 후 엣지 규칙 체인이 생성 시 엣지에 자동으로 할당됩니다.", + "unset-auto-assign-to-edge": "생성 시 Edge에 규칙 체인 할당 안 함", + "unset-auto-assign-to-edge-title": "엣지 규칙 체인 '{{ruleChainName}}'을(를) 생성 시 엣지에 할당하지 않으시겠습니까?", + "unset-auto-assign-to-edge-text": "확인 후 엣지 규칙 체인이 생성 시 엣지에 더 이상 자동으로 할당되지 않습니다.", + "unassign-rulechain-title": "규칙 체인 '{{ruleChainName}}'의 할당을 해제하시겠습니까?", + "unassign-rulechains": "규칙 체인 할당 해제" }, "rulenode": { + "rule-node-events": "규칙 노드 이벤트", "details": "상세 내역", "events": "이벤트", "search": "노드 검색", "open-node-library": "노드 라이브러리 열기", + "close-node-library": "노드 라이브러리 닫기", "add": "규칙 노드 추가", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", "type": "유형", + "rule-node-description": "규칙 노드 설명", "delete": "규칙 노드 삭제", "select-all-objects": "모든 노드와 연결을 선택", "deselect-all-objects": "모든 노드와 연결을 선택 해제", "delete-selected-objects": "선택된 노드와 연결을 삭제", "delete-selected": "선택 삭제", + "create-nested-rulechain": "중첩 규칙 체인 생성", "select-all": "모두 선택", "copy-selected": "선택 복사", "deselect-all": "선택 해제", "rulenode-details": "규칙 노드 상세 정보", "debug-mode": "디버그 모드", + "singleton": "싱글톤", "configuration": "설정", "link": "링크", "link-details": "규칙 노드 링크 상세 정보", @@ -1850,45 +5206,964 @@ "no-link-label-matching": "'{{label}}' 을 찾을 수 없습니다.", "create-new-link-label": "새로 링크 라벨 만들기", "type-filter": "필터", - "type-filter-details": "Filter incoming messages with configured conditions", - "type-enrichment": "Enrichment", + "type-filter-details": "구성된 조건으로 들어오는 메시지 필터링", + "type-enrichment": "보강", "type-enrichment-details": "메시지 메타데이터에 추가적인 정보를 추가", - "type-transformation": "Transformation", - "type-transformation-details": "Change Message payload and Metadata", - "type-action": "", + "type-transformation": "변환", + "type-transformation-details": "메시지 페이로드 및 메타데이터 변경", + "type-action": "동작", "type-action-details": "특별한 조치를 수행", "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-external-details": "외부 시스템과 상호 작용", + "type-rule-chain": "규칙 사슬", + "type-rule-chain-details": "수신 메시지를 지정된 규칙 체인으로 전달", + "type-flow": "흐름", + "type-flow-details": "메시지 흐름 구성", "type-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", + "type-input-details": "규칙 체인의 논리적 입력, 수신 메시지를 다음 관련 규칙 노드로 전달", + "type-unknown": "알 수 없음", + "type-unknown-details": "확인되지 않은 규칙 노드", + "directive-is-not-loaded": "정의된 구성 지시문 '{{directiveName}}'을(를) 사용할 수 없습니다.", + "ui-resources-load-error": "구성 UI 리소스를 로드하지 못했습니다.", + "invalid-target-rulechain": "대상 규칙 체인을 확인할 수 없습니다!", + "test-script-function": "스크립트 함수 테스트", + "script-lang-java-script": "JavaScript", + "script-lang-tbel": "TBEL", "message": "메시지", "message-type": "메시지 유형", - "select-message-type": "메시지 유형 선택", + "select-message-type": "메시지 유형 선택", "message-type-required": "메시지 유형을 입력하세요.", "metadata": "메타데이터", "metadata-required": "메타데이터 엔트리를 입력하세요.", "output": "출력", "test": "테스트", - "help": "도움말" + "help": "도움말", + "reset-debug-settings": "모든 노드에서 디버그 설정 재설정", + "test-with-this-message": "이 메시지로 {{test}}", + "queue-hint": "메시지를 다른 큐로 전달할 큐를 선택하세요. 기본적으로 '기본' 큐가 사용됩니다.", + "queue-singleton-hint": "다중 인스턴스 환경에서 메시지 전달을 위한 큐를 선택하세요. 기본적으로 '기본' 큐가 사용됩니다." + }, + "rule-node-config": { + "id": "ID", + "additional-info": "추가 정보", + "advanced-settings": "고급 설정", + "create-entity-if-not-exists": "존재하지 않는 경우 새 엔티티 생성", + "create-entity-if-not-exists-hint": "활성화하면 지정된 매개변수가 있는 새 엔티티가 이미 존재하지 않는 한 생성됩니다. 기존 엔티티는 관계에 그대로 사용됩니다.", + "select-device-connectivity-event": "장치 연결 이벤트 선택", + "entity-name-pattern": "이름 패턴", + "device-name-pattern": "장치 이름", + "asset-name-pattern": "자산 이름", + "entity-view-name-pattern": "엔티티 뷰 이름", + "customer-title-pattern": "고객 제목", + "dashboard-name-pattern": "대시보드 제목", + "user-name-pattern": "사용자 이메일", + "edge-name-pattern": "엣지 이름", + "entity-name-pattern-required": "이름 패턴이 필요합니다", + "entity-name-pattern-hint": "이름 패턴 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "copy-message-type": "메시지 유형 복사", + "entity-type-pattern": "유형 패턴", + "entity-type-pattern-required": "유형 패턴이 필요합니다", + "message-type-value": "메시지 유형 값", + "message-type-value-required": "메시지 유형 값이 필요합니다", + "message-type-value-max-length": "메시지 유형 값은 256자 미만이어야 합니다", + "output-message-type": "출력 메시지 유형", + "entity-cache-expiration": "엔티티 캐시 만료 시간 (초)", + "entity-cache-expiration-hint": "찾은 엔티티 레코드를 저장할 수 있는 최대 시간 간격을 지정합니다. 0 값은 레코드가 만료되지 않음을 의미합니다.", + "entity-cache-expiration-required": "엔티티 캐시 만료 시간이 필요합니다.", + "entity-cache-expiration-range": "엔티티 캐시 만료 시간은 0 이상이어야 합니다.", + "customer-name-pattern": "고객 제목", + "customer-name-pattern-required": "고객 제목이 필요합니다", + "customer-name-pattern-hint": "$[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "create-customer-if-not-exists": "존재하지 않는 경우 새 고객 생성", + "unassign-from-customer": "발신자가 대시보드인 경우 특정 고객에서 할당 해제", + "unassign-from-customer-tooltip": "대시보드만 한 번에 여러 고객에게 할당할 수 있습니다.\n메시지 발신자가 대시보드인 경우 할당을 해제할 고객의 제목을 명시적으로 지정해야 합니다.", + "customer-cache-expiration": "고객 캐시 만료 시간 (초)", + "customer-cache-expiration-hint": "찾은 고객 레코드를 저장할 수 있는 최대 시간 간격을 지정합니다. 0 값은 레코드가 만료되지 않음을 의미합니다.", + "customer-cache-expiration-required": "고객 캐시 만료 시간이 필요합니다.", + "customer-cache-expiration-range": "고객 캐시 만료 시간은 0 이상이어야 합니다.", + "interval-start": "간격 시작", + "interval-end": "간격 종료", + "time-unit": "시간 단위", + "fetch-mode": "가져오기 모드", + "order-by-timestamp": "타임스탬프로 정렬", + "limit": "제한", + "limit-hint": "최소 제한 값은 2, 최대는 1000입니다. 단일 항목을 가져오려면 가져오기 모드 '첫 번째' 또는 '마지막'을 선택하세요.", + "limit-required": "제한이 필요합니다.", + "limit-range": "제한은 2에서 1000 사이여야 합니다.", + "time-unit-milliseconds": "밀리초", + "time-unit-seconds": "초", + "time-unit-minutes": "분", + "time-unit-hours": "시간", + "time-unit-days": "요일", + "time-value-range": "1~2147483647 범위를 허용합니다.", + "start-interval-value-required": "간격 시작은 필수 항목입니다.", + "end-interval-value-required": "간격 종료는 필수 항목입니다.", + "filter": "필터", + "switch": "스위치", + "math-templatization-tooltip": "이 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "add-message-type": "메시지 유형 추가", + "select-message-types-required": "최소 하나의 메시지 유형을 선택해야 합니다.", + "select-message-types": "메시지 유형 선택", + "no-message-types-found": "메시지 유형을 찾을 수 없음", + "no-message-type-matching": "'{{messageType}}'을(를) 찾을 수 없습니다.", + "create-new-message-type": "새로 만들기", + "message-types-required": "메시지 유형이 필요합니다.", + "client-attributes": "클라이언트 속성", + "shared-attributes": "공유 속성", + "server-attributes": "서버 속성", + "attributes-keys": "속성 키", + "attributes-keys-required": "속성 키가 필요합니다", + "attributes-scope": "속성 범위", + "attributes-scope-value": "속성 범위 값", + "attributes-scope-value-copy": "속성 범위 값 복사", + "attributes-scope-hint": "'scope' 메타데이터 키를 사용하여 메시지별로 속성 범위를 동적으로 설정하세요. 제공된 경우 구성에 설정된 범위를 재정의합니다.", + "notify-device": "장치에 알림 강제 전송", + "send-attributes-updated-notification": "속성 업데이트 알림 전송", + "send-attributes-updated-notification-hint": "업데이트된 속성에 대한 알림을 별도의 메시지로 규칙 엔진 큐에 보냅니다.", + "send-attributes-deleted-notification": "속성 삭제 알림 보내기", + "send-attributes-deleted-notification-hint": "삭제된 속성에 대한 알림을 별도의 메시지로 규칙 엔진 큐에 보냅니다.", + "update-attributes-only-on-value-change": "값이 변경된 경우에만 속성 저장", + "update-attributes-only-on-value-change-hint": "값이 변경되었는지 여부에 관계없이 모든 수신 메시지에서 속성을 업데이트합니다. API 사용량이 증가하고 성능이 저하됩니다.", + "update-attributes-only-on-value-change-hint-enabled": "값이 변경된 경우에만 속성을 업데이트합니다. 값이 변경되지 않으면 속성 타임스탬프 업데이트나 속성 변경 알림이 전송되지 않습니다.", + "fetch-credentials-to-metadata": "메타데이터에 자격 증명 가져오기", + "notify-device-on-update-hint": "활성화하면 공유 속성 업데이트에 대한 알림을 장치에 강제로 보냅니다. 비활성화하면 알림 동작이 수신 메시지 메타데이터의 'notifyDevice' 매개변수에 의해 제어됩니다. 알림을 끄려면 메시지 메타데이터에 'notifyDevice' 매개변수가 'false'로 설정되어 있어야 합니다. 다른 모든 경우에는 장치에 알림이 전송됩니다.", + "notify-device-on-delete-hint": "활성화하면 공유 속성 제거에 대한 알림을 장치에 강제로 보냅니다. 비활성화하면 알림 동작이 수신 메시지 메타데이터의 'notifyDevice' 매개변수에 의해 제어됩니다. 알림을 켜려면 메시지 메타데이터에 'notifyDevice' 매개변수가 'true'로 설정되어 있어야 합니다. 다른 모든 경우에는 장치에 알림이 전송되지 않습니다.", + "latest-timeseries": "최신 시계열 데이터 키", + "timeseries-keys": "시계열 키", + "timeseries-keys-required": "최소 하나의 시계열 키를 선택해야 합니다.", + "add-timeseries-key": "시계열 키 추가", + "add-message-field": "메시지 필드 추가", + "relation-search-parameters": "관계 검색 매개변수", + "relation-parameters": "관계 매개변수", + "add-metadata-field": "메타데이터 필드 추가", + "data-keys": "메시지 필드 이름", + "copy-from": "복사 원본", + "data-to-metadata": "데이터를 메타데이터로", + "metadata-to-data": "메타데이터를 데이터로", + "use-regular-expression-hint": "정규 표현식을 사용하여 패턴별로 키를 복사하세요.\n\n팁 & 요령:\n'Enter'를 눌러 필드 이름 입력을 완료하세요.\n'Backspace'를 눌러 필드 이름을 삭제하세요. 여러 필드 이름이 지원됩니다.", + "interval": "간격", + "interval-required": "간격이 필요합니다", + "interval-hint": "중복 제거 간격 (초).", + "interval-min-error": "최소 허용 값은 1입니다", + "max-pending-msgs": "보류 중인 최대 메시지 수", + "max-pending-msgs-hint": "각 고유 중복 제거 ID에 대해 메모리에 저장되는 최대 메시지 수입니다.", + "max-pending-msgs-required": "보류 중인 최대 메시지 수가 필요합니다", + "max-pending-msgs-max-error": "최대 허용 값은 1000입니다", + "max-pending-msgs-min-error": "최소 허용 값은 1입니다", + "max-retries": "최대 재시도 횟수", + "max-retries-required": "최대 재시도 횟수가 필요합니다", + "max-retries-hint": "중복 제거된 메시지를 큐에 푸시하기 위한 최대 재시도 횟수입니다. 재시도 사이에 10초 지연이 사용됩니다", + "max-retries-max-error": "최대 허용 값은 100입니다", + "max-retries-min-error": "최소 허용 값은 0입니다", + "strategy": "전략", + "strategy-required": "전략이 필요합니다", + "strategy-all-hint": "중복 제거 기간 동안 도착한 모든 메시지를 단일 JSON 배열 메시지로 반환합니다. 각 요소는 msg 및 metadata 내부 속성이 있는 엔티티를 나타냅니다.", + "strategy-first-hint": "중복 제거 기간 동안 도착한 첫 번째 메시지를 반환합니다.", + "strategy-last-hint": "중복 제거 기간 동안 도착한 마지막 메시지를 반환합니다.", + "first": "첫 번째", + "last": "과거", + "all": "모두", + "output-msg-type-hint": "중복 제거 결과의 메시지 유형입니다.", + "queue-name-hint": "중복 제거 결과가 게시될 큐 이름입니다.", + "keys": "키", + "keys-required": "키가 필요합니다", + "rename-keys-in": "키 이름 변경 위치", + "data": "데이터", + "message": "메시지", + "metadata": "메타데이터", + "current-key-name": "현재 키 이름", + "key-name-required": "키 이름은 필수 항목입니다.", + "new-key-name": "새 키 이름", + "new-key-name-required": "새 키 이름이 필요합니다", + "metadata-keys": "메타데이터 필드 이름", + "json-path-expression": "JSON 경로 표현식", + "json-path-expression-required": "JSON 경로 표현식이 필요합니다", + "json-path-expression-hint": "JSONPath는 JSON 구조에서 요소 또는 요소 집합의 경로를 지정합니다. '$'는 루트 엔티티 또는 배열을 나타냅니다.", + "relations-query": "관계 질의", + "device-relations-query": "장치 관계 쿼리", + "max-relation-level": "최대 관계 수준", + "max-relation-level-error": "값은 0보다 크거나 지정되지 않아야 합니다.", + "max-relation-level-invalid": "값은 정수여야 합니다.", + "relation-type": "관계 유형", + "relation-type-pattern": "관계 유형 패턴", + "relation-type-pattern-required": "관계 유형 패턴이 필요합니다", + "relation-types-list": "전파할 관계 유형", + "relation-types-list-hint": "전파 관계 유형이 선택되지 않으면 관계 유형별 필터링 없이 알람이 전파됩니다.", + "unlimited-level": "제한 수준", + "latest-telemetry": "최근 데이터", + "add-telemetry-key": "텔레메트리 키 추가", + "delete-from": "삭제 위치", + "use-regular-expression-delete-hint": "정규 표현식을 사용하여 패턴별로 키를 삭제하세요.\n\n팁 & 요령:\n'Enter'를 눌러 필드 이름 입력을 완료하세요.\n'Backspace'를 눌러 필드 이름을 삭제하세요.\n여러 필드 이름이 지원됩니다.", + "fetch-into": "가져오기 위치", + "attr-mapping": "속성 매핑:", + "source-attribute": "소스 속성 키", + "source-attribute-required": "소스 속성 키가 필요합니다.", + "source-telemetry": "소스 텔레메트리 키", + "source-telemetry-required": "소스 텔레메트리 키가 필요합니다.", + "target-key": "대상 키", + "target-key-required": "대상 키가 필요합니다.", + "attr-mapping-required": "최소 하나의 매핑 항목을 지정해야 합니다.", + "fields-mapping": "필드 매핑", + "fields-mapping-hint": "메시지 필드가 $entityId로 설정된 경우 메시지 발신자의 ID가 지정된 테이블 열에 저장됩니다.", + "relations-query-config-direction-suffix": "발신자", + "profile-name": "프로필 이름", + "fetch-circle-parameter-info-from-metadata-hint": "메타데이터 필드 '{{perimeterKeyName}}'은(는) 다음 형식으로 정의되어야 합니다: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}", + "fetch-poligon-parameter-info-from-metadata-hint": "메타데이터 필드 '{{perimeterKeyName}}'은(는) 다음 형식으로 정의되어야 합니다: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]", + "short-templatization-tooltip": "$[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "fields-mapping-required": "최소 하나의 필드 매핑을 지정해야 합니다.", + "at-least-one-field-required": "최소 하나의 입력 필드에 값이 제공되어야 합니다.", + "originator-fields-sv-map-hint": "대상 키 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "sv-map-hint": "대상 키 필드만 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "source-field": "소스 필드", + "source-field-required": "소스 필드가 필요합니다.", + "originator-source": "발신자 소스", + "new-originator": "새 발신자", + "originator-customer": "고객", + "originator-tenant": "테넌트", + "originator-related": "관련 엔티티", + "originator-alarm-originator": "알람 발생자", + "originator-entity": "이름 패턴으로 엔티티 찾기", + "clone-message": "메시지 복제", + "transform": "변환", + "default-ttl": "기본 TTL", + "default-ttl-required": "기본 TTL은 필수 항목입니다.", + "default-ttl-hint": "규칙 노드는 메시지 메타데이터에서 TTL(Time-to-Live) 값을 가져옵니다. 값이 없으면 구성에 지정된 TTL이 기본값이 됩니다. 값이 0으로 설정되면 테넌트 프로필 구성의 TTL이 적용됩니다.", + "default-ttl-zero-hint": "값이 0이면 TTL이 적용되지 않습니다.", + "min-default-ttl-message": "최소 TTL은 0만 허용됩니다.", + "generation-parameters": "생성 매개변수", + "message-count": "생성 메시지 제한 (0 - 무제한)", + "message-count-required": "생성 메시지 제한은 필수 항목입니다.", + "min-message-count-message": "최소 메시지 수는 0만 허용됩니다.", + "period-seconds": "기간 (초)", + "period-seconds-required": "기간은 필수 항목입니다.", + "generation-frequency-seconds": "생성 빈도 (초)", + "generation-frequency-required": "생성 빈도는 필수 항목입니다.", + "min-generation-frequency-message": "최소 1초만 허용됩니다.", + "script-lang-tbel": "TBEL", + "script-lang-js": "JS", + "use-metadata-period-in-seconds-patterns": "초 단위 기간 패턴 사용", + "use-metadata-period-in-seconds-patterns-hint": "선택하면 규칙 노드는 간격이 초 단위라고 가정하여 메시지 메타데이터 또는 데이터의 초 간격 패턴을 사용합니다.", + "period-in-seconds-pattern": "초 단위 기간 패턴", + "period-in-seconds-pattern-required": "초 단위 기간 패턴은 필수 항목입니다.", + "min-period-seconds-message": "최소 기간은 1초만 허용됩니다.", + "originator": "만든 사람", + "message-body": "메시지 본문", + "message-metadata": "메시지 메타데이터", + "generate": "생성", + "current-rule-node": "현재 규칙 노드", + "current-tenant": "현재 테넌트", + "generator-function": "생성기 함수", + "test-generator-function": "생성기 함수 테스트", + "generator": "생성기", + "test-filter-function": "필터 함수 테스트", + "test-switch-function": "스위치 함수 테스트", + "test-transformer-function": "변환기 함수 테스트", + "transformer": "변환기", + "alarm-create-condition": "알람 생성 조건", + "test-condition-function": "조건 함수 테스트", + "alarm-clear-condition": "알람 해제 조건", + "alarm-details-builder": "알람 상세 빌더", + "test-details-function": "세부 정보 함수 테스트", + "alarm-type": "알람 유형", + "select-entity-types": "엔티티 유형 선택", + "alarm-type-required": "알람 유형은 필수 항목입니다.", + "alarm-severity": "알람 심각도", + "alarm-severity-required": "알람 심각도가 필요합니다", + "alarm-severity-pattern": "알람 심각도 패턴", + "alarm-status-filter": "알람 상태 필터", + "alarm-status-list-empty": "알람 상태 목록이 비어 있습니다", + "no-alarm-status-matching": "일치하는 알람 상태를 찾을 수 없습니다.", + "propagate": "관련 엔티티에 알람 전파", + "propagate-to-owner": "엔티티 소유자(고객 또는 테넌트)에게 알람 전파", + "propagate-to-tenant": "테넌트에게 알람 전파", + "condition": "조건", + "details": "상세 내역", + "to-string": "문자열로 변환", + "test-to-string-function": "문자열 변환 함수 테스트", + "from-template": "부터", + "from-template-required": "보낸 사람은 필수 항목입니다.", + "message-to-metadata": "메시지에서 메타데이터로", + "metadata-to-message": "메타데이터에서 메시지로", + "from-message": "메시지에서", + "from-metadata": "메타데이터에서", + "to-template": "받는 사람", + "to-template-required": "받는 사람 템플릿은 필수 항목입니다.", + "mail-address-list-template-hint": "쉼표로 구분된 주소 목록, 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "cc-template": "참조", + "bcc-template": "숨은 참조", + "subject-template": "제목", + "subject-template-required": "제목 템플릿은 필수 항목입니다.", + "body-template": "본문", + "body-template-required": "본문 템플릿은 필수 항목입니다.", + "dynamic-mail-body-type": "동적 메일 본문 유형", + "mail-body-type": "메일 본문 유형", + "body-type-template": "본문 유형 템플릿", + "reply-routing-configuration": "응답 라우팅 구성", + "rpc-reply-routing-configuration-hint": "이 구성 매개변수는 응답을 보내기 위해 서비스, 세션 및 요청을 식별하는 데 사용되는 메타데이터 키 이름을 지정합니다.", + "reply-routing-configuration-hint": "이 구성 매개변수는 응답을 보내기 위해 서비스와 요청을 식별하는 데 사용되는 메타데이터 키 이름을 지정합니다.", + "request-id-metadata-attribute": "요청 ID", + "service-id-metadata-attribute": "서비스 ID", + "session-id-metadata-attribute": "세션 ID", + "timeout-sec": "시간 초과 (초)", + "timeout-required": "시간 초과는 필수 항목입니다.", + "min-timeout-message": "최소 시간 초과 값은 0만 허용됩니다.", + "endpoint-url-pattern": "엔드포인트 URL 패턴", + "endpoint-url-pattern-required": "엔드포인트 URL 패턴은 필수 항목입니다.", + "request-method": "요청 방법", + "use-simple-client-http-factory": "간단한 클라이언트 HTTP 팩토리 사용", + "ignore-request-body": "요청 본문 없음", + "parse-to-plain-text": "일반 텍스트로 파싱", + "parse-to-plain-text-hint": "선택하면 요청 본문 메시지 페이로드가 JSON 문자열에서 일반 텍스트로 변환됩니다. 예: msg = \"Hello,\\t\"world\"\"는 Hello, \"world\"로 구문 분석됩니다.", + "read-timeout": "읽기 시간 초과 (밀리초)", + "read-timeout-hint": "0 값은 무한 시간 초과를 의미합니다.", + "max-parallel-requests-count": "최대 병렬 요청 수", + "max-parallel-requests-count-hint": "0 값은 병렬 처리에 제한이 없음을 지정합니다.", + "max-response-size": "최대 응답 크기 (KB)", + "max-response-size-hint": "JSON 또는 XML 페이로드와 같은 HTTP 메시지를 디코딩하거나 인코딩할 때 데이터 버퍼링에 할당된 최대 메모리 양", + "headers": "헤더", + "headers-hint": "헤더/값 필드에서 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "header": "헤더", + "header-required": "헤더는 필수 항목입니다.", + "value": "값", + "value-required": "값은 필수 항목입니다.", + "topic-pattern": "토픽 패턴", + "key-pattern": "키 패턴", + "key-pattern-hint": "선택 사항. 유효한 파티션 번호가 지정되면 레코드를 보낼 때 사용됩니다. 파티션이 지정되지 않으면 키가 대신 사용됩니다. 둘 다 지정되지 않으면 라운드 로빈 방식으로 파티션이 할당됩니다.", + "topic-pattern-required": "토픽 패턴은 필수 항목입니다.", + "topic": "토픽", + "topic-required": "토픽은 필수 항목입니다.", + "bootstrap-servers": "부트스트랩 서버", + "bootstrap-servers-required": "부트스트랩 서버 값이 필요합니다", + "other-properties": "기타 속성", + "key": "키", + "key-required": "키는 필수 항목입니다.", + "retries": "실패 시 자동 재시도 횟수", + "min-retries-message": "최소 재시도는 0만 허용됩니다.", + "batch-size-bytes": "생성된 배치 크기 (바이트)", + "min-batch-size-bytes-message": "최소 배치 크기는 0만 허용됩니다.", + "linger-ms": "로컬 버퍼링 시간 (ms)", + "min-linger-ms-message": "최소 0ms만 허용됩니다.", + "buffer-memory-bytes": "클라이언트 버퍼 최대 크기 (바이트)", + "min-buffer-memory-message": "최소 버퍼 크기는 0만 허용됩니다.", + "memory-buffer-size-range": "메모리 버퍼 크기는 0에서 {{max}} KB 사이여야 합니다.", + "acks": "확인 수", + "topic-arn-pattern": "토픽 ARN 패턴", + "topic-arn-pattern-required": "토픽 ARN 패턴은 필수 항목입니다.", + "aws-access-key-id": "AWS 액세스 키 ID", + "aws-access-key-id-required": "AWS 액세스 ID를 입력하세요.", + "aws-secret-access-key": "AWS 보안 액세스 키", + "aws-secret-access-key-required": "AWS 보안 액세스 키를 입력하세요", + "aws-region": "AWS 지역", + "aws-region-required": "AWS 지역을 입력하세요", + "exchange-name-pattern": "교환 이름 패턴", + "routing-key-pattern": "라우팅 키 패턴", + "message-properties": "메시지 속성", + "host": "호스트", + "host-required": "호스트는 필수 항목입니다.", + "port": "포트", + "port-required": "포트는 필수 항목입니다.", + "port-range": "포트를 1~65535 범위 내에서 입력하세요.", + "virtual-host": "가상 호스트", + "username": "사용자명", + "password": "비밀번호", + "automatic-recovery": "자동 복구", + "connection-timeout-ms": "연결 시간 초과 (ms)", + "min-connection-timeout-ms-message": "최소 0ms만 허용됩니다.", + "handshake-timeout-ms": "핸드셰이크 시간 초과 (ms)", + "min-handshake-timeout-ms-message": "최소 0ms만 허용됩니다.", + "client-properties": "클라이언트 속성", + "queue-url-pattern": "큐 URL 패턴", + "queue-url-pattern-required": "큐 URL 패턴은 필수 항목입니다.", + "delay-seconds": "지연 (초)", + "min-delay-seconds-message": "최소 0초만 허용됩니다.", + "max-delay-seconds-message": "최대 900초만 허용됩니다.", + "name": "이름", + "name-required": "이름은 필수 항목입니다.", + "queue-type": "큐 유형", + "sqs-queue-standard": "표준", + "sqs-queue-fifo": "FIFO", + "gcp-project-id": "GCP 프로젝트 ID", + "gcp-project-id-required": "GCP 프로젝트 ID는 필수 항목입니다.", + "gcp-service-account-key": "GCP 서비스 계정 키 파일", + "gcp-service-account-key-required": "GCP 서비스 계정 키 파일은 필수 항목입니다.", + "pubsub-topic-name": "토픽 이름", + "pubsub-topic-name-required": "토픽 이름은 필수 항목입니다.", + "message-attributes": "메시지 속성", + "message-attributes-hint": "이름/값 필드에서 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "connect-timeout": "연결 시간 초과 (초)", + "connect-timeout-required": "연결 시간 초과는 필수 항목입니다.", + "connect-timeout-range": "연결 시간 초과는 1에서 200 범위여야 합니다.", + "client-id": "클라이언트 ID", + "client-id-hint": "선택 사항. 자동 생성된 클라이언트 ID를 사용하려면 비워 두세요. 클라이언트 ID를 지정할 때 주의하세요. 대부분의 MQTT 브로커는 동일한 클라이언트 ID로 여러 연결을 허용하지 않습니다. 이러한 브로커에 연결하려면 MQTT 클라이언트 ID가 고유해야 합니다. 플랫폼이 마이크로 서비스 모드로 실행 중인 경우 각 마이크로 서비스에서 규칙 노드의 복사본이 시작됩니다. 이로 인해 동일한 ID를 가진 여러 MQTT 클라이언트가 자동으로 생성되어 규칙 노드에 오류가 발생할 수 있습니다. 이러한 오류를 방지하려면 아래의 \"서비스 ID를 클라이언트 ID에 접미사로 추가\" 옵션을 활성화하세요.", + "append-client-id-suffix": "클라이언트 ID에 서비스 ID를 접미사로 추가", + "client-id-suffix-hint": "선택 사항. \"클라이언트 ID\"가 명시적으로 지정된 경우에 적용됩니다. 선택하면 서비스 ID가 클라이언트 ID에 접미사로 추가됩니다. 플랫폼이 마이크로 서비스 모드로 실행 중일 때 오류를 방지하는 데 도움이 됩니다.", + "device-id": "장치 ID", + "device-id-required": "장치 ID는 필수 항목입니다.", + "clean-session": "클린 세션", + "enable-ssl": "SSL 활성화", + "credentials": "자격 증명", + "credentials-type": "자격 증명 타입", + "credentials-type-required": "자격 증명 유형은 필수 항목입니다.", + "credentials-anonymous": "익명", + "credentials-basic": "기본", + "credentials-pem": "PEM", + "credentials-pem-hint": "최소 서버 CA 인증서 파일 또는 클라이언트 인증서와 클라이언트 개인 키 파일 쌍이 필요합니다", + "credentials-sas": "공유 액세스 서명", + "sas-key": "SAS 키", + "sas-key-required": "SAS 키는 필수 항목입니다.", + "hostname": "호스트 이름", + "hostname-required": "호스트 이름은 필수 항목입니다.", + "azure-ca-cert": "CA 인증서 파일", + "username-required": "사용자 이름이 필요합니다.", + "password-required": "비밀번호가 필요합니다.", + "ca-cert": "서버 CA 인증서 파일", + "private-key": "클라이언트 개인 키 파일", + "cert": "클라이언트 인증서 파일", + "no-file": "아무 파일도 선택되지 않았습니다.", + "drop-file": "여기로 파일을 드래그 하거나, 여기를 클릭해서 파일을 선택하세요.", + "private-key-password": "개인 키 비밀번호", + "use-system-smtp-settings": "시스템 SMTP 설정 사용", + "use-metadata-dynamic-interval": "동적 간격 사용", + "metadata-dynamic-interval-hint": "간격 시작 및 종료 입력 필드는 템플릿을 지원합니다. 대체된 템플릿 값은 밀리초로 설정되어야 합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "use-metadata-interval-patterns-hint": "선택하면 규칙 노드는 간격이 밀리초라고 가정하여 메시지 메타데이터 또는 데이터의 시작 및 종료 간격 패턴을 사용합니다.", + "use-message-alarm-data": "메시지 알람 데이터 사용", + "overwrite-alarm-details": "알람 세부 정보 덮어쓰기", + "use-alarm-severity-pattern": "알람 심각도 패턴 사용", + "check-all-keys": "모든 지정된 필드가 있는지 확인", + "check-all-keys-hint": "선택하면 지정된 모든 키가 메시지 데이터와 메타데이터에 있는지 확인합니다.", + "check-relation-to-specific-entity": "특정 엔티티와의 관계 확인", + "check-relation-to-specific-entity-tooltip": "활성화하면 특정 엔티티와의 관계 존재 여부를 확인하고, 그렇지 않으면 모든 엔티티와의 관계 존재 여부를 확인합니다. 두 경우 모두 관계 조회는 구성된 방향과 유형을 기반으로 합니다.", + "check-relation-hint": "방향과 관계 유형을 기반으로 특정 엔티티 또는 모든 엔티티와의 관계 존재 여부를 확인합니다.", + "delete-relation-with-specific-entity": "특정 엔티티와의 관계 삭제", + "delete-relation-with-specific-entity-hint": "활성화하면 하나의 특정 엔티티와의 관계만 삭제됩니다. 그렇지 않으면 일치하는 모든 엔티티와의 관계가 제거됩니다.", + "delete-relation-hint": "수신 메시지의 발신자에서 지정된 엔티티 또는 엔티티 목록으로의 관계를 방향과 유형에 따라 삭제합니다.", + "remove-current-relations": "현재 관계 제거", + "remove-current-relations-hint": "수신 메시지의 발신자에서 방향과 유형에 따라 현재 관계를 제거합니다.", + "change-originator-to-related-entity": "발신자를 관련 엔티티로 변경", + "change-originator-to-related-entity-hint": "제출된 메시지를 다른 엔티티의 메시지로 처리하는 데 사용됩니다.", + "start-interval": "간격 시작", + "end-interval": "간격 종료", + "start-interval-required": "간격 시작은 필수 항목입니다.", + "end-interval-required": "간격 종료는 필수 항목입니다.", + "smtp-protocol": "프로토콜", + "smtp-host": "SMTP 호스트", + "smtp-host-required": "SMTP 호스트를 입력해야 합니다.", + "smtp-port": "SMTP 포트", + "smtp-port-required": "SMTP 포트를 입력해야 합니다.", + "smtp-port-range": "SMTP 포트는 1에서 65535 범위여야 합니다.", + "timeout-msec": "시간 초과 (ms)", + "min-timeout-msec-message": "최소 0ms만 허용됩니다.", + "enter-username": "사용자명을 입력하세요.", + "enter-password": "비밀번호를 입력하세요.", + "enable-tls": "TLS 사용", + "tls-version": "TLS 버전", + "enable-proxy": "프록시 사용", + "use-system-proxy-properties": "시스템 프록시 속성 사용", + "proxy-host": "프록시 호스트", + "proxy-host-required": "프록시 호스트를 입력하세요.", + "proxy-port": "프록시 포트", + "proxy-port-required": "프록시 포트를 입력하세요.", + "proxy-port-range": "프록시 포트를 1~65535 범위 내에서 입력하세요.", + "proxy-user": "프록시 사용자", + "proxy-password": "프록시 비밀번호", + "proxy-scheme": "프록시 스키마", + "numbers-to-template": "받는 사람 전화번호 템플릿", + "numbers-to-template-required": "받는 사람 전화번호 템플릿은 필수 항목입니다.", + "numbers-to-template-hint": "쉼표로 구분된 전화번호, 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "sms-message-template": "SMS 메시지 템플릿", + "sms-message-template-required": "SMS 메시지 템플릿은 필수 항목입니다.", + "use-system-sms-settings": "시스템 SMS 제공자 설정 사용", + "min-period-0-seconds-message": "최소 기간은 0초만 허용됩니다.", + "max-pending-messages": "최대 대기 메시지", + "max-pending-messages-required": "최대 대기 메시지는 필수 항목입니다.", + "max-pending-messages-range": "최대 대기 메시지는 1에서 100000 범위여야 합니다.", + "originator-types-filter": "발신자 유형 필터", + "interval-seconds": "간격 (초)", + "interval-seconds-required": "간격은 필수 항목입니다.", + "int-range": "값은 최대 정수 제한(2147483648)을 초과하지 않아야 합니다.", + "min-interval-seconds-message": "최소 간격은 1초만 허용됩니다.", + "output-timeseries-key-prefix": "출력 시계열 키 접두사", + "output-timeseries-key-prefix-required": "출력 시계열 키 접두사는 필수 항목입니다.", + "separator-hint": "\"Enter\"를 눌러 필드 입력을 완료하세요.", + "select-details": "세부 정보 선택", + "entity-details-id": "ID", + "entity-details-title": "제목", + "entity-details-country": "국가", + "entity-details-state": "주", + "entity-details-city": "시", + "entity-details-zip": "우편번호", + "entity-details-address": "주소", + "entity-details-address2": "상세 주소", + "entity-details-additional_info": "추가 정보", + "entity-details-phone": "전화번호", + "entity-details-email": "이메일", + "email-sender": "이메일 보낸 사람", + "fields-to-check": "확인할 필드", + "add-detail": "상세 추가", + "check-all-keys-tooltip": "활성화하면 수신 메시지와 해당 메타데이터 내의 메시지 및 메타데이터 필드 이름에 나열된 모든 필드의 존재 여부를 확인합니다.", + "fields-to-check-hint": "\"Enter\"를 눌러 필드 이름 입력을 완료하세요. 여러 필드 이름이 지원됩니다.", + "entity-details-list-empty": "최소 하나의 세부 정보를 선택해야 합니다.", + "alarm-status": "알람 상태", + "alarm-required": "최소 하나의 알람 상태를 선택해야 합니다.", + "no-entity-details-matching": "일치하는 엔티티 세부 정보를 찾을 수 없습니다.", + "custom-table-name": "사용자 지정 테이블 이름", + "custom-table-name-required": "테이블 이름이 필요합니다", + "custom-table-hint": "테이블은 Cassandra 클러스터에 생성되어야 하며 일반 TB 테이블에 데이터가 삽입되지 않도록 이름이 'cs_tb_' 접두사로 시작해야 합니다. 여기에 'cs_tb_' 접두사 없이 테이블 이름을 입력하세요.", + "message-field": "메시지 필드", + "message-field-required": "메시지 필드가 필요합니다.", + "table-col": "테이블 열", + "table-col-required": "테이블 열이 필요합니다.", + "latitude-field-name": "위도 필드 이름", + "longitude-field-name": "경도 필드 이름", + "latitude-field-name-required": "위도 필드 이름이 필요합니다.", + "longitude-field-name-required": "경도 필드 이름이 필요합니다.", + "fetch-perimeter-info-from-metadata": "메타데이터에서 경계 정보 가져오기", + "fetch-perimeter-info-from-metadata-tooltip": "경계 유형이 '다각형'으로 설정된 경우 메타데이터 필드 '{{perimeterKeyName}}'의 값이 추가 구문 분석 없이 경계 정의로 설정됩니다. 그렇지 않고 경계 유형이 '원'으로 설정된 경우 '{{perimeterKeyName}}' 메타데이터 필드의 값이 구문 분석되어 원 경계 정의에 사용되는 'latitude', 'longitude', 'radius', 'radiusUnit' 필드가 추출됩니다.", + "perimeter-key-name": "경계 키 이름", + "perimeter-key-name-hint": "경계 정보를 포함하는 메타데이터 필드 이름입니다.", + "perimeter-key-name-required": "경계 키 이름이 필요합니다.", + "perimeter-circle": "원", + "perimeter-polygon": "다각형", + "perimeter-type": "경계 유형", + "circle-center-latitude": "중심 위도", + "circle-center-latitude-required": "중심 위도가 필요합니다.", + "circle-center-longitude": "중심 경도", + "circle-center-longitude-required": "중심 경도가 필요합니다.", + "range-unit-meter": "미터", + "range-unit-kilometer": "킬로미터", + "range-unit-foot": "피트", + "range-unit-mile": "마일", + "range-unit-nautical-mile": "해리", + "range-units": "범위 단위", + "range-units-required": "범위 단위가 필요합니다.", + "range": "범위", + "range-required": "범위가 필요합니다.", + "polygon-definition": "다각형 정의", + "polygon-definition-required": "다각형 정의가 필요합니다.", + "polygon-definition-hint": "다각형을 수동으로 정의하려면 다음 형식을 사용하세요: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].", + "min-inside-duration": "최소 내부 지속 시간", + "min-inside-duration-value-required": "최소 내부 지속 시간이 필요합니다", + "min-inside-duration-time-unit": "최소 내부 지속 시간 단위", + "min-outside-duration": "최소 외부 지속 시간", + "min-outside-duration-value-required": "최소 외부 지속 시간이 필요합니다", + "min-outside-duration-time-unit": "최소 외부 지속 시간 단위", + "tell-failure-if-absent": "실패 알림", + "tell-failure-if-absent-hint": "선택한 키 중 하나라도 존재하지 않으면 아웃바운드 메시지가 \"실패\"를 보고합니다.", + "get-latest-value-with-ts": "최신 텔레메트리 값에 대한 타임스탬프 가져오기", + "get-latest-value-with-ts-hint": "선택하면 최신 텔레메트리 값에 타임스탬프도 포함됩니다. 예: \"temp\": \"{\"ts\":1574329385897, \"value\":42}\"", + "ignore-null-strings": "빈 문자열 무시", + "ignore-null-strings-hint": "선택하면 규칙 노드가 빈 값이 있는 엔티티 필드를 무시합니다.", + "add-metadata-key-values-as-kafka-headers": "Kafka 레코드 헤더에 메시지 메타데이터 키-값 쌍 추가", + "add-metadata-key-values-as-kafka-headers-hint": "선택하면 메시지 메타데이터의 키-값 쌍이 미리 정의된 문자셋 인코딩으로 바이트 배열로 나가는 레코드 헤더에 추가됩니다.", + "charset-encoding": "문자셋 인코딩", + "charset-encoding-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": "큐 이름은 드롭다운 목록에서 선택하거나 사용자 지정 이름을 추가할 수 있습니다.", + "device-profile-node-hint": "알람 상태 평가의 연속성을 보장하기 위해 지속 시간 또는 반복 조건이 있는 경우에 유용합니다.", + "persist-alarm-rules": "알람 규칙 상태 저장", + "persist-alarm-rules-hint": "활성화하면 규칙 노드가 처리 상태를 데이터베이스에 저장합니다.", + "fetch-alarm-rules": "알람 규칙 상태 가져오기", + "fetch-alarm-rules-hint": "활성화하면 규칙 노드가 초기화 시 처리 상태를 복원하고 서버가 다시 시작된 후에도 알람이 발생하도록 합니다. 그렇지 않으면 장치에서 첫 번째 메시지가 도착할 때 상태가 복원됩니다.", + "input-value-key": "입력 값 키", + "input-value-key-required": "입력 값 키가 필요합니다.", + "output-value-key": "출력 값 키", + "output-value-key-required": "출력 값 키가 필요합니다.", + "number-of-digits-after-floating-point": "소숫점 뒤 자릿수", + "number-of-digits-after-floating-point-range": "소수점 이하 자릿수는 0에서 15 사이여야 합니다.", + "failure-if-delta-negative": "델타가 음수인 경우 실패 알림", + "failure-if-delta-negative-tooltip": "규칙 노드는 델타 값이 음수인 경우 메시지 처리 실패를 강제합니다.", + "use-caching": "캐싱 사용", + "use-caching-tooltip": "규칙 노드는 성능을 향상시키기 위해 수신 메시지에서 도착하는 \"{{inputValueKey}}\"의 값을 캐시합니다. 다른 곳에서 \"{{inputValueKey}}\" 값을 수정하면 캐시가 업데이트되지 않습니다.", + "add-time-difference-between-readings": "\"{{inputValueKey}}\" 판독 간의 시간 차이 추가", + "add-time-difference-between-readings-tooltip": "활성화하면 규칙 노드가 \"{{periodValueKey}}\"를 아웃바운드 메시지에 추가합니다.", + "period-value-key": "기간 값 키", + "period-value-key-required": "기간 값 키가 필요합니다.", + "general-pattern-hint": "메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요.", + "alarm-severity-pattern-hint": "메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요. 알람 심각도는 시스템(CRITICAL, MAJOR 등)이어야 합니다.", + "output-node-name-hint": "규칙 노드 이름은 출력 메시지의 관계 유형에 해당하며, 호출자 규칙 체인의 다른 규칙 노드로 메시지를 전달하는 데 사용됩니다.", + "use-server-ts": "서버 타임스탬프 사용", + "use-server-ts-hint": "명시적 타임스탬프가 없는 시계열 데이터에 대해 서버의 현재 타임스탬프를 사용합니다. 이는 여러 소스에서 메시지를 처리하거나 메시지가 순서 없이 도착할 때 적절한 순서를 유지하는 데 도움이 됩니다.", + "kv-map-pattern-hint": "모든 입력 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "kv-map-single-pattern-hint": "입력 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "shared-scope": "공유 범위", + "server-scope": "서버 범위", + "client-scope": "클라이언트 범위", + "attribute-type": "속성", + "attribute-type-description": "데이터베이스에서 속성 값 가져오기", + "attribute-type-result-description": "결과를 데이터베이스의 엔티티 속성으로 저장", + "constant-type": "상수", + "constant-type-description": "상수 값 정의", + "time-series-type": "시계열", + "time-series-type-description": "데이터베이스에서 최신 시계열 값 가져오기", + "time-series-type-result-description": "결과를 데이터베이스의 엔티티 시계열로 저장", + "message-body-type": "메시지", + "message-body-type-description": "수신 메시지에서 인수 값 가져오기", + "message-body-type-result-description": "아웃바운드 메시지에 결과 추가", + "message-metadata-type": "메타데이터", + "message-metadata-type-description": "수신 메시지 메타데이터에서 인수 값 가져오기", + "message-metadata-result-description": "아웃바운드 메시지 메타데이터에 결과 추가", + "argument-tile": "인수", + "no-arguments-prompt": "구성된 인수 없음", + "result-title": "결과", + "functions-field-input": "함수", + "no-option-found": "옵션을 찾을 수 없음", + "argument-source-field-input": "소스", + "argument-source-field-input-required": "인수 소스가 필요합니다.", + "argument-key-field-input": "키", + "argument-key-field-input-required": "인수 키가 필요합니다.", + "constant-value-field-input": "상수 값", + "constant-value-field-input-required": "상수 값이 필요합니다.", + "attribute-scope-field-input": "속성 범위", + "attribute-scope-field-input-required": "속성 범위가 필요합니다.", + "default-value-field-input": "기본값", + "type-field-input": "유형", + "type-field-input-required": "유형일 입력하세요.", + "key-field-input": "키", + "add-entity-type": "엔티티 유형 추가", + "add-device-profile": "장치 프로필 추가", + "key-field-input-required": "키는 필수 항목입니다.", + "number-floating-point-field-input": "소숫점 뒤 자릿수", + "number-floating-point-field-input-hint": "결과를 정수로 변환하려면 0을 사용하세요", + "add-to-message-field-input": "메시지에 추가", + "add-to-metadata-field-input": "메타데이터에 추가", + "custom-expression-field-input": "수학 표현식", + "custom-expression-field-input-required": "수학 표현식이 필요합니다", + "custom-expression-field-input-hint": "평가할 수학 표현식을 지정하세요. 기본 표현식은 화씨를 섭씨로 변환하는 방법을 보여줍니다", + "retained-message": "보존됨", + "attributes-mapping": "속성 매핑", + "latest-telemetry-mapping": "최신 텔레메트리 매핑", + "add-mapped-attribute-to": "매핑된 속성 추가 위치", + "add-mapped-latest-telemetry-to": "매핑된 최신 텔레메트리 추가 위치", + "add-mapped-fields-to": "매핑된 필드 추가 위치", + "add-selected-details-to": "선택한 세부 정보 추가 위치", + "clear-selected-types": "선택된 유형 지우기", + "clear-selected-details": "선택된 상세 지우기", + "clear-selected-fields": "선택된 필드 지우기", + "clear-selected-keys": "선택된 키 지우기", + "geofence-configuration": "지오펜스 구성", + "coordinate-field-names": "좌표 필드 이름", + "coordinate-field-hint": "규칙 노드는 메시지에서 지정된 필드를 검색하려고 시도합니다. 필드가 없으면 메타데이터에서 조회합니다.", + "presence-monitoring-strategy": "존재 모니터링 전략", + "presence-monitoring-strategy-on-first-message": "첫 번째 메시지에서", + "presence-monitoring-strategy-on-each-message": "각 메시지에서", + "presence-monitoring-strategy-on-first-message-hint": "이전 존재 상태 '진입' 또는 '이탈' 업데이트 이후 구성된 최소 지속 시간이 지난 후 첫 번째 메시지에서 존재 상태 '내부' 또는 '외부'를 보고합니다.", + "presence-monitoring-strategy-on-each-message-hint": "존재 상태 '진입' 또는 '이탈' 업데이트 후 각 메시지에서 존재 상태 '내부' 또는 '외부'를 보고합니다.", + "fetch-credentials-to": "자격 증명 가져오기 위치", + "add-originator-attributes-to": "발신자 속성 추가 위치", + "originator-attributes": "발신자 속성", + "fetch-latest-telemetry-with-timestamp": "타임스탬프와 함께 최신 텔레메트리 가져오기", + "fetch-latest-telemetry-with-timestamp-tooltip": "선택하면 최신 텔레메트리 값이 타임스탬프와 함께 아웃바운드 메타데이터에 추가됩니다. 예: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", + "tell-failure": "속성이 누락된 경우 실패 알림", + "tell-failure-tooltip": "선택한 키 중 하나라도 존재하지 않으면 아웃바운드 메시지가 \"실패\"를 보고합니다.", + "created-time": "생성 일시", + "chip-help": "'Enter'를 눌러 {{inputName}} 입력을 완료하세요.\n'Backspace'를 눌러 {{inputName}}을(를) 삭제하세요.\n여러 값이 지원됩니다.", + "detail": "상세", + "field-name": "필드 이름", + "device-profile": "장치 프로필", + "entity-type": "엔티티 유형", + "message-type": "메시지 유형", + "timeseries-key": "시계열 키", + "type": "유형", + "first-name": "이름", + "last-name": "성", + "label": "라벨", + "originator-fields-mapping": "발신자 필드 매핑", + "add-mapped-originator-fields-to": "매핑된 발신자 필드 추가 위치", + "fields": "필드", + "skip-empty-fields": "빈 필드 건너뛰기", + "skip-empty-fields-tooltip": "빈 값이 있는 필드는 출력 메시지/출력 메타데이터에 추가되지 않습니다.", + "fetch-interval": "가져오기 간격", + "fetch-strategy": "가져오기 전략", + "fetch-timeseries-from-to": "{{startInterval}} {{startIntervalTimeUnit}} 전부터 {{endInterval}} {{endIntervalTimeUnit}} 전까지의 시계열을 가져옵니다.", + "fetch-timeseries-from-to-invalid": "시계열 가져오기가 잘못되었습니다 (\"간격 시작\"은 \"간격 종료\"보다 작아야 함).", + "use-metadata-dynamic-interval-tooltip": "선택하면 규칙 노드가 메시지와 메타데이터 패턴을 기반으로 동적 간격 시작 및 종료를 사용합니다.", + "all-mode-hint": "가져오기 모드 \"모두\"를 선택하면 규칙 노드가 구성 가능한 쿼리 매개변수로 가져오기 간격에서 텔레메트리를 검색합니다.", + "first-mode-hint": "가져오기 모드 \"첫 번째\"를 선택하면 규칙 노드가 가져오기 간격의 시작에 가장 가까운 텔레메트리를 검색합니다.", + "last-mode-hint": "가져오기 모드 \"마지막\"을 선택하면 규칙 노드가 가져오기 간격의 종료에 가장 가까운 텔레메트리를 검색합니다.", + "ascending": "오름차순", + "descending": "내림차순", + "min": "최소", + "max": "최대", + "average": "평균", + "sum": "합계", + "count": "숫자", + "none": "없음", + "last-level-relation-tooltip": "선택하면 규칙 노드가 최대 관계 레벨에 설정된 레벨에서만 관련 엔티티를 검색합니다.", + "last-level-device-relation-tooltip": "선택하면 규칙 노드가 최대 관계 레벨에 설정된 레벨에서만 관련 장치를 검색합니다.", + "data-to-fetch": "가져올 데이터", + "mapping-of-customers": "고객 매핑", + "map-fields-required": "모든 매핑 필드가 필요합니다.", + "attributes": "속성", + "related-device-attributes": "관련 장치 속성", + "add-selected-attributes-to": "선택한 속성 추가 위치", + "device-profiles": "장치 프로필", + "mapping-of-tenant": "테넌트 매핑", + "add-attribute-key": "속성 키 추가", + "message-template": "메시지 템플릿", + "message-template-required": "메시지 템플릿이 필요합니다", + "use-system-slack-settings": "시스템 Slack 설정 사용", + "slack-api-token": "Slack API 토큰", + "slack-api-token-required": "Slack API 토큰이 필요합니다", + "keys-mapping": "키 매핑", + "add-key": "키 추가", + "recipients": "수신자", + "message-subject-and-content": "메시지 제목 및 내용", + "template-rules-hint": "두 입력 필드 모두 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메시지 메타데이터에서 값을 추출하세요.", + "originator-customer-desc": "수신 메시지 발신자의 고객을 새 발신자로 사용합니다.", + "originator-tenant-desc": "현재 테넌트를 새 발신자로 사용합니다.", + "originator-related-entity-desc": "관련 엔티티를 새 발신자로 사용합니다. 구성된 관계 유형과 방향을 기반으로 조회합니다.", + "originator-alarm-originator-desc": "알람 발신자를 새 발신자로 사용합니다. 수신 메시지 발신자가 알람 엔티티인 경우에만.", + "originator-entity-by-name-pattern-desc": "DB에서 가져온 엔티티를 새 발신자로 사용합니다. 엔티티 유형과 지정된 이름 패턴을 기반으로 조회합니다.", + "email-from-template-hint": "$[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "recipients-block-main-hint": "쉼표로 구분된 주소 목록입니다. 모든 입력 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "forward-msg-default-rule-chain": "메시지를 발신자의 기본 규칙 체인으로 전달", + "forward-msg-default-rule-chain-tooltip": "활성화하면 메시지가 발신자의 기본 규칙 체인으로 전달되거나, 발신자가 엔티티 프로필에 기본 규칙 체인을 정의하지 않은 경우 구성의 규칙 체인으로 전달됩니다.", + "exclude-zero-deltas": "아웃바운드 메시지에서 0 델타 제외", + "exclude-zero-deltas-hint": "활성화하면 값이 0이 아닌 경우 \"{{outputValueKey}}\" 출력 키가 아웃바운드 메시지에 추가됩니다.", + "exclude-zero-deltas-time-difference-hint": "활성화하면 \"{{outputValueKey}}\" 값이 0이 아닌 경우에만 \"{{outputValueKey}}\" 및 \"{{periodValueKey}}\" 출력 키가 아웃바운드 메시지에 추가됩니다.", + "search-direction-from": "발신자에서 대상 엔티티로", + "search-direction-to": "대상 엔티티에서 발신자로", + "del-relation-direction-from": "발신자에서", + "del-relation-direction-to": "발신자로", + "target-entity": "대상 엔티티", + "function-configuration": "함수 구성", + "function-name": "함수 이름", + "function-name-required": "함수 이름이 필요합니다.", + "qualifier": "한정자", + "qualifier-hint": "한정자가 지정되지 않으면 기본 한정자 \"$LATEST\"가 사용됩니다.", + "aws-credentials": "AWS 자격 증명", + "connection-timeout": "연결 시간 제한", + "connection-timeout-required": "연결 시간 초과는 필수 항목입니다.", + "connection-timeout-min": "최소 연결 시간 제한은 0입니다.", + "connection-timeout-hint": "연결을 처음 설정할 때 포기하고 시간 초과하기 전까지 기다리는 시간(초)입니다. 값이 0이면 무한을 의미하며 권장되지 않습니다.", + "request-timeout": "요청 시간 초과", + "request-timeout-required": "요청 시간 제한이 필요합니다", + "request-timeout-min": "최소 요청 시간 초과는 0입니다.", + "request-timeout-hint": "요청이 완료될 때까지 포기하고 시간 초과하기 전까지 기다리는 시간(초)입니다. 값이 0이면 무한을 의미하며 권장되지 않습니다.", + "units": "단위", + "tell-failure-aws-lambda": "AWS Lambda 함수 실행에서 예외가 발생하면 실패 알림", + "tell-failure-aws-lambda-hint": "AWS Lambda 함수 실행이 예외를 발생시키면 규칙 노드가 메시지 처리 실패를 강제합니다.", + "basic-mode": "기본", + "advanced-mode": "고급", + "save-time-series": { + "processing-settings": "처리 설정", + "processing-settings-hint": "수신 메시지가 처리되는 방식을 정의합니다. 기본 처리 설정에서는 미리 구성된 전략을 선택할 수 있고, 고급 설정에서는 각 작업에 대한 개별 처리 전략을 선택할 수 있습니다.", + "advanced-settings-hint": "처리 전략을 구성할 때 주의하세요. 특정 조합은 예기치 않은 동작을 초래할 수 있습니다.", + "strategy": "전략", + "deduplication-interval": "중복 제거 간격", + "deduplication-interval-required": "중복 제거 간격은 필수 항목입니다.", + "deduplication-interval-min-max-range": "중복 제거 간격은 최소 1초에서 최대 1일이어야 합니다.", + "strategy-type": { + "every-message": "모든 메시지마다", + "skip": "건너뛰기", + "deduplicate": "중복 제거", + "web-sockets-only": "WebSocket만" + }, + "time-series": "시계열", + "latest": "최근 값", + "web-sockets": "웹소켓", + "calculated-fields-and-alarm-rules": "계산 필드 및 알람 규칙" + }, + "save-attribute": { + "processing-settings": "처리 설정", + "processing-settings-hint": "수신 메시지가 처리되는 방식을 정의합니다. 기본 처리 설정에서는 미리 구성된 전략을 선택할 수 있고, 고급 설정에서는 각 작업에 대한 개별 처리 전략을 선택할 수 있습니다.", + "advanced-settings-hint": "처리 전략을 구성할 때 주의하세요. 특정 조합은 예기치 않은 동작을 초래할 수 있습니다.", + "strategy": "전략", + "deduplication-interval": "중복 제거 간격", + "deduplication-interval-required": "중복 제거 간격은 필수 항목입니다.", + "deduplication-interval-min-max-range": "중복 제거 간격은 최소 1초에서 최대 1일이어야 합니다.", + "scope": "범위", + "strategy-type": { + "every-message": "모든 메시지마다", + "skip": "건너뛰기", + "deduplicate": "중복 제거", + "web-sockets-only": "WebSocket만" + }, + "attributes": "속성" + }, + "key-val": { + "key": "키", + "value": "값", + "see-examples": "예제를 참조하세요.", + "remove-entry": "앤트리 삭제", + "remove-mapping-entry": "매핑 항목 제거", + "add-mapping-entry": "매핑 추가", + "add-entry": "앤트리 추가", + "copy-key-values-from": "키-값 복사 원본", + "delete-key-values": "키-값 삭제", + "delete-key-values-from": "키-값 삭제 원본", + "at-least-one-key-error": "최소 하나의 키를 선택해야 합니다.", + "unique-key-value-pair-error": "'{{keyText}}'은(는) '{{valText}}'와(과) 달라야 합니다!" + }, + "mail-body-types": { + "plain-text": "일반 텍스트", + "html": "HTML", + "dynamic": "동적", + "use-body-type-template": "본문 유형 템플릿 사용", + "plain-text-description": "특별한 스타일이나 형식이 없는 간단한 서식 없는 텍스트입니다.", + "html-text-description": "메일 본문에 서식, 링크 및 이미지를 위한 HTML 태그를 사용할 수 있습니다.", + "dynamic-text-description": "템플릿 기능을 기반으로 일반 텍스트 또는 HTML 본문 유형을 동적으로 사용할 수 있습니다.", + "after-template-evaluation-hint": "템플릿 평가 후 값은 HTML의 경우 true, 일반 텍스트의 경우 false여야 합니다." + }, + "ai": { + "ai-model": "AI 모델", + "model": "모델", + "ai-model-hint": "이 규칙 노드에서 보낸 요청을 처리할 미리 구성된 AI 모델을 선택하거나 \"새로 만들기\"를 사용하여 새 모델을 구성하세요.", + "prompt-settings": "프롬프트 설정", + "prompt-settings-hint": "선택적 시스템 프롬프트는 AI의 일반적인 역할과 제약 조건을 설정하고, 사용자 프롬프트는 수행할 특정 작업을 정의합니다. 두 필드 모두 템플릿을 지원합니다.", + "system-prompt": "시스템 프롬프트", + "system-prompt-max-length": "시스템 프롬프트는 500000자 이하여야 합니다.", + "system-prompt-blank": "시스템 프롬프트는 비워둘 수 없습니다.", + "user-prompt": "사용자 프롬프트", + "user-prompt-required": "사용자 프롬프트는 필수 항목입니다.", + "user-prompt-max-length": "사용자 프롬프트는 500000자 이하여야 합니다.", + "user-prompt-blank": "사용자 프롬프트는 비워둘 수 없습니다.", + "response-format": "응답 형식", + "response-text": "텍스트", + "response-json": "JSON", + "response-json-schema": "JSON 스키마", + "response-format-hint-TEXT": "모델이 임의의 텍스트를 생성할 수 있습니다. 이는 유효한 JSON 엔티티이거나 아닐 수 있습니다. 출력이 유효한 JSON 엔티티가 아니면 \"response\" 키 아래의 JSON 엔티티 내에 자동으로 래핑됩니다.", + "response-format-hint-JSON": "모델은 유효한 JSON인 응답을 생성해야 합니다. 출력이 유효한 JSON 엔티티가 아니면 \"response\" 키 아래의 JSON 엔티티 내에 자동으로 래핑됩니다.", + "response-format-hint-JSON_SCHEMA": "모델은 제공된 스키마에 정의된 특정 구조와 데이터 유형과 일치하는 JSON을 생성해야 합니다. 출력이 유효한 JSON 엔티티가 아니면 \"response\" 키 아래의 JSON 엔티티 내에 자동으로 래핑됩니다.", + "response-json-schema-hint": "유효한 JSON 스키마를 입력할 수 있지만 이 규칙 노드는 기능의 제한된 하위 집합만 지원합니다. 자세한 내용은 노드 문서를 참조하세요.", + "response-json-schema-required": "JSON 스키마는 필수 항목입니다.", + "advanced-settings": "고급 설정", + "timeout": "시간 초과", + "timeout-hint": "요청이 종료되기 전에 AI 모델로부터 응답을 기다리는 최대 시간.", + "timeout-required": "시간 초과는 필수 항목입니다.", + "timeout-validation": "1초에서 10분 사이여야 합니다.", + "force-acknowledgement": "강제 확인", + "force-acknowledgement-hint": "활성화하면 수신 메시지가 즉시 확인됩니다. 그런 다음 모델의 응답이 별도의 새 메시지로 큐에 추가됩니다.", + "ai-resources": "AI 리소스" + } }, "timezone": { - "timezone": "Timezone", - "select-timezone": "Select timezone", - "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", - "timezone-required": "Timezone is required." + "timezone": "시간대", + "select-timezone": "시간대 선택", + "no-timezones-matching": "'{{timezone}}'와(과) 일치하는 시간대를 찾을 수 없습니다.", + "timezone-required": "시간대는 필수 항목입니다.", + "browser-time": "브라우저 시간" }, "queue": { - "select_name": "Select queue name", - "name": "Queue Name", - "name_required": "Queue name is required!" + "queue-name": "큐", + "no-queues-found": "큐를 찾을 수 없습니다.", + "no-queues-matching": "'{{queue}}'와(과) 일치하는 큐를 찾을 수 없습니다.", + "select-name": "큐 이름 선택", + "name": "이름", + "name-required": "큐 이름은 필수 항목입니다!", + "name-unique": "큐 이름이 고유하지 않습니다!", + "name-pattern": "큐 이름에 ASCII 영숫자, '.', '_', '-' 이외의 문자가 포함되어 있습니다!", + "queue-required": "큐는 필수 항목입니다!", + "topic-required": "큐 토픽은 필수 항목입니다!", + "poll-interval-required": "폴링 간격은 필수 항목입니다!", + "poll-interval-min-value": "폴링 간격 값은 1 미만일 수 없습니다.", + "partitions-required": "파티션은 필수 항목입니다!", + "partitions-min-value": "파티션 값은 1 미만일 수 없습니다.", + "pack-processing-timeout-required": "처리 시간 초과는 필수 항목입니다.", + "pack-processing-timeout-min-value": "처리 시간 초과 값은 1 미만일 수 없습니다.", + "batch-size-required": "배치 크기는 필수 항목입니다!", + "batch-size-min-value": "배치 크기 값은 1 미만일 수 없습니다.", + "retries-required": "재시도는 필수 항목입니다!", + "retries-min-value": "재시도 값은 음수일 수 없습니다.", + "failure-percentage-required": "실패 비율은 필수 항목입니다!", + "failure-percentage-min-value": "실패 비율 값은 0 미만일 수 없습니다.", + "failure-percentage-max-value": "실패 비율 값은 100을 초과할 수 없습니다.", + "pause-between-retries-required": "재시도 사이의 일시 중지는 필수 항목입니다!", + "pause-between-retries-min-value": "재시도 사이의 일시 중지 값은 1 미만일 수 없습니다.", + "max-pause-between-retries-required": "재시도 사이의 최대 일시 중지는 필수 항목입니다!", + "max-pause-between-retries-min-value": "재시도 사이의 최대 일시 중지 값은 1 미만일 수 없습니다.", + "submit-strategy-type-required": "제출 전략 유형은 필수 항목입니다!", + "processing-strategy-type-required": "처리 전략 유형은 필수 항목입니다!", + "queues": "큐", + "selected-queues": "{ count, plural, =1 {1개 큐} other {#개 큐} } 선택됨", + "delete-queue-title": "큐 '{{queueName}}'을(를) 삭제하시겠습니까?", + "delete-queues-title": "{ count, plural, =1 {1개 큐} other {#개 큐} }를 삭제하시겠습니까?", + "delete-queue-text": "주의: 확인 후 큐와 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-queues-text": "확인 후 선택한 모든 큐가 삭제되어 액세스할 수 없게 됩니다.", + "search": "큐 검색", + "add": "큐 추가", + "details": "큐 세부 정보", + "topic": "토픽", + "submit-settings": "제출 설정", + "submit-strategy": "전략 유형 *", + "grouping-parameter": "그룹화 매개변수", + "processing-settings": "재시도 처리 설정", + "processing-strategy": "처리 유형 *", + "retries-settings": "재시도 설정", + "polling-settings": "폴링 설정", + "batch-processing": "배치 처리", + "poll-interval": "폴링 간격", + "partitions": "파티션", + "immediate-processing": "즉시 처리", + "consumer-per-partition": "각 소비자에게 메시지 폴 전송", + "consumer-per-partition-hint": "각 파티션에 별도의 소비자 활성화", + "duplicate-msg-to-all-partitions": "모든 파티션에 메시지 복제", + "processing-timeout": "처리 시간 내 (ms)", + "batch-size": "배치 크기", + "retries": "재시도 횟수 (0 – 무제한)", + "failure-percentage": "재시도 건너뛰기 실패 메시지 (%)", + "pause-between-retries": "재시도 시간 내 (초)", + "max-pause-between-retries": "추가 재시도 시간 내 (초)", + "delete": "큐 삭제", + "copyId": "큐 ID 복사", + "idCopiedMessage": "큐 ID가 클립보드에 복사되었습니다.", + "description": "설명", + "description-hint": "이 텍스트는 선택한 전략 대신 큐 설명에 표시됩니다", + "alt-description": "제출 전략: {{submitStrategy}}, 처리 전략: {{processingStrategy}}", + "custom-properties": "사용자 지정 속성", + "custom-properties-hint": "사용자 지정 큐 (토픽) 생성 속성, 예: 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "발신자별 순차", + "sequential-by-originator-hint": "예: 장치 A의 이전 메시지가 확인될 때까지 장치 A의 새 메시지가 제출되지 않습니다", + "sequential-by-tenant-label": "테넌트별 순차", + "sequential-by-tenant-hint": "예: 테넌트 A의 이전 메시지가 확인될 때까지 테넌트 A의 새 메시지가 제출되지 않습니다", + "sequential-label": "순차", + "sequential-hint": "새 메시지는 이전 메시지가 확인될 때까지 제출되지 않습니다.", + "burst-label": "버스트", + "burst-hint": "모든 메시지가 도착한 순서대로 규칙 체인에 제출됩니다.", + "batch-label": "배치", + "batch-hint": "새 배치는 이전 배치가 확인될 때까지 제출되지 않습니다.", + "skip-all-failures-label": "모든 실패 건너뛰기", + "skip-all-failures-hint": "모든 실패 무시", + "skip-all-failures-and-timeouts-label": "모든 실패 및 시간 초과 건너뛰기", + "skip-all-failures-and-timeouts-hint": "모든 실패 및 시간 초과 무시", + "retry-all-label": "모두 재시도", + "retry-all-hint": "처리 팩의 모든 메시지 재시도", + "retry-failed-label": "실패 재시도", + "retry-failed-hint": "처리 팩의 모든 실패한 메시지 재시도", + "retry-timeout-label": "시간 초과 재시도", + "retry-timeout-hint": "처리 팩의 모든 시간 초과된 메시지 재시도", + "retry-failed-and-timeout-label": "실패 및 시간 초과 재시도", + "retry-failed-and-timeout-hint": "처리 팩의 모든 실패 및 시간 초과된 메시지 재시도" + } + }, + "queue-statistics": { + "queue-statistics": "큐 통계", + "no-queue-statistics-matching": "'{{entity}}'와(과) 일치하는 큐 통계를 찾을 수 없습니다.", + "queue-statistics-required": "큐 통계는 필수 항목입니다.", + "list-of-queue-statistics": "{ count, plural, =1 {하나의 큐 통계} other {#개 큐 통계 목록} }", + "selected-queue-statistics": "{ count, plural, =1 {1개 큐 통계} other {#개 큐 통계} } 선택됨", + "no-queue-statistics-text": "큐 통계를 찾을 수 없습니다", + "queue-statistics-starts-with": "이름이 '{{prefix}}'로 시작하는 큐 통계" + }, + "server-error": { + "general": "일반 서버 오류", + "authentication": "인증 오류", + "jwt-token-expired": "JWT 토큰 만료", + "tenant-trial-expired": "테넌트 평가판 만료", + "credentials-expired": "자격 증명 만료", + "permission-denied": "권한 거부", + "invalid-arguments": "잘못된 인수", + "bad-request-params": "잘못된 요청 매개변수", + "item-not-found": "항목을 찾을 수 없음", + "too-many-requests": "요청이 너무 많음", + "too-many-updates": "업데이트가 너무 많음", + "entities-limit-exceeded": "엔티티 제한 초과" }, "tenant": { "tenant": "테넌트", @@ -1901,6 +6176,7 @@ "add-tenant-text": "테넌트 추가", "no-tenants-text": "테넌트가 없습니다.", "tenant-details": "테넌트 상세 정보", + "title-max-length": "제목은 256자 미만이어야 합니다.", "delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?", "delete-tenant-text": "테넌트 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-tenants-title": "{ count, plural, =1 {테넌트 1 개} other {테넌트 # 개} }를 삭제하시겠습니까?", @@ -1918,91 +6194,277 @@ "tenant-required": "테넌트가 필요합니다.", "search": "테넌트 검색", "selected-tenants": "{ count, plural, =1 {1 개 테넌트} other {# 개 테넌트} } 선택됨", - "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" - }, + "isolated-tb-rule-engine": "격리된 ThingsBoard 규칙 엔진 큐 사용", + "isolated-tb-rule-engine-details": "각 테넌트는 전용 규칙 엔진 큐를 갖습니다" + }, "tenant-profile": { - "tenant-profile": "테넌트 프로파일", - "tenant-profiles": "테넌트 프로파일", - "add": "테넌트 프로파일 추가", - "edit": "테넌트 프로파일 편집", - "tenant-profile-details": "Tenant profile details", - "no-tenant-profiles-text": "아무 테넌트 프로파일도 찾지 못했습니다", - "search": "테넌트 프로파일 검색", - "selected-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로파일} other {# 개 테넌트 프로파일} } 선택됨", - "no-tenant-profiles-matching": "No tenant profile matching '{{entity}}' were found.", - "tenant-profile-required": "테넌트 프로파일을 입력하세요", - "idCopiedMessage": "테넌트 프로파일 ID가 클립보드에 저장되었습니다", - "set-default": "테넌트 프로파일 기본값으로 지정", - "delete": "테넌트 프로파일 삭제", - "copyId": "테넌트 프로파일 ID 복사", + "tenant-profile": "테넌트 프로필", + "tenant-profiles": "테넌트 프로필", + "add": "테넌트 프로필 추가", + "add-profile": "프로필 추가", + "debug": "디버그", + "edit": "테넌트 프로필 편집", + "tenant-profile-details": "테넌트 프로필 세부 정보", + "no-tenant-profiles-text": "아무 테넌트 프로필도 찾지 못했습니다", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "search": "테넌트 프로필 검색", + "selected-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로필} other {# 개 테넌트 프로필} } 선택됨", + "no-tenant-profiles-matching": "'{{entity}}'와(과) 일치하는 테넌트 프로필을 찾을 수 없습니다.", + "tenant-profile-required": "테넌트 프로필을 입력하세요", + "idCopiedMessage": "테넌트 프로필 ID가 클립보드에 저장되었습니다", + "set-default": "테넌트 프로필 기본값으로 지정", + "delete": "테넌트 프로필 삭제", + "copyId": "테넌트 프로필 ID 복사", "name": "이름", "name-required": "이름을 입력하세요.", - "data": "프로파일 데이터", - "profile-configuration": "프로파일 설정", + "data": "프로필 데이터", + "profile-configuration": "프로필 설정", "description": "설명", "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": "{ count, plural, =1 {1 개 테넌트 프로파일} other {# 개 테넌트 프로파일} }을 삭제하시겠습니까?", - "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": "아무 테넌트 프로파일도 찾지 못했습니다.", - "create-new-tenant-profile": "Create a new one!", + "delete-tenant-profile-title": "테넌트 프로필 '{{tenantProfileName}}'을(를) 삭제하시겠습니까?", + "delete-tenant-profile-text": "주의: 확인 후 테넌트 프로필과 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-tenant-profiles-title": "{ count, plural, =1 {1 개 테넌트 프로필} other {# 개 테넌트 프로필} }을 삭제하시겠습니까?", + "delete-tenant-profiles-text": "주의: 확인 후 선택한 모든 테넌트 프로필이 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "set-default-tenant-profile-title": "테넌트 프로필 '{{tenantProfileName}}'을(를) 기본값으로 설정하시겠습니까?", + "set-default-tenant-profile-text": "확인 후 테넌트 프로필이 기본값으로 표시되며 프로필이 지정되지 않은 새 테넌트에 사용됩니다.", + "no-tenant-profiles-found": "아무 테넌트 프로필도 찾지 못했습니다.", + "create-new-tenant-profile": "새로 만들기!", + "create-tenant-profile": "새 테넌트 프로필 생성", + "import": "테넌트 프로필 가져오기", + "export": "테넌트 프로필 내보내기", + "export-failed-error": "테넌트 프로필을 내보낼 수 없습니다: {{error}}", + "tenant-profile-file": "테넌트 프로필 파일", + "invalid-tenant-profile-file-error": "테넌트 프로필을 가져올 수 없습니다: 잘못된 테넌트 프로필 데이터 구조입니다.", + "advanced-settings": "고급 설정", + "entities": "엔티티", + "rule-engine": "규칙 엔진", + "time-to-live": "수명", + "calculated-fields": "계산 필드", + "alarms-and-notifications": "알람 및 알림", + "ota-files-in-bytes": "파일", + "ws-title": "WS", + "unlimited": "(0 - 무제한)", "maximum-devices": "최대 장치 수 (0 - 무제한)", - "maximum-devices-required": "Maximum number of devices is required.", - "maximum-devices-range": "Minimum number of devices can't be negative", + "maximum-devices-required": "장치 최대 수는 필수 항목입니다.", + "maximum-devices-range": "장치 최대 수는 음수일 수 없습니다.", "maximum-assets": "최대 자산 수 (0 - 무제한)", - "maximum-assets-required": "Maximum number of assets is required.", - "maximum-assets-range": "Maximum number of assets can't be negative", - "maximum-customers": "최대 커스터머 수 (0 - 무제한)", - "maximum-customers-required": "최대 커스터머 수를 입력하세요.", - "maximum-customers-range": "Maximum number of customers can't be negative", - "maximum-users": "Maximum number of users (0 - unlimited)", - "maximum-users-required": "Maximum number of users is required.", - "maximum-users-range": "Maximum number of users can't be negative", - "maximum-dashboards": "Maximum number of dashboards (0 - unlimited)", - "maximum-dashboards-required": "Maximum number of dashboards is required.", - "maximum-dashboards-range": "Maximum number of dashboards can't be negative", - "maximum-edges": "Edges maximum number (0 - unlimited)", - "maximum-edges-required": "Edges maximum number is required.", - "maximum-edges-range": "Edges maximum number can't be negative", - "maximum-rule-chains": "Maximum number of rule chains (0 - unlimited)", - "maximum-rule-chains-required": "Maximum number of rule chains is required.", - "maximum-rule-chains-range": "Maximum number of rule chains can't be negative", - "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": "Maximum 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": "Maximum 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": "Maximum 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": "Maximum number of data points storage days can't be negative", - "default-storage-ttl-days": "Default storage TTL days (0 - unlimited)", - "default-storage-ttl-days-required": "Default storage TTL days is required.", - "default-storage-ttl-days-range": "Default storage TTL 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": "Maximum number of rule node executions per message can't be negative", - "max-emails": "Maximum number of emails sent (0 - unlimited)", - "max-emails-required": "Maximum number of emails sent is required.", - "max-emails-range": "Maximum number of emails sent can't be negative", - "max-sms": "Maximum number of SMS sent (0 - unlimited)", - "max-sms-required": "Maximum number of SMS sent is required.", - "max-sms-range": "Maximum number of SMS sent can't be negative" + "maximum-assets-required": "자산 최대 수는 필수 항목입니다.", + "maximum-assets-range": "자산 최대 수는 음수일 수 없습니다.", + "maximum-customers": "최대 고객 수 (0 - 무제한)", + "maximum-customers-required": "최대 고객 수를 입력하세요.", + "maximum-customers-range": "고객 최대 수는 음수일 수 없습니다.", + "maximum-users": "사용자 최대 수", + "maximum-users-required": "사용자 최대 수는 필수 항목입니다.", + "maximum-users-range": "사용자 최대 수는 음수일 수 없습니다.", + "maximum-dashboards": "대시보드 최대 수", + "maximum-dashboards-required": "대시보드 최대 수는 필수 항목입니다.", + "maximum-dashboards-range": "대시보드 최대 수는 음수일 수 없습니다.", + "maximum-edges": "엣지 최대 수", + "maximum-edges-required": "Edge 최대 수는 필수 항목입니다.", + "maximum-edges-range": "Edge 최대 수는 음수일 수 없습니다.", + "maximum-rule-chains": "규칙 체인 최대 수", + "maximum-rule-chains-required": "규칙 체인 최대 수는 필수 항목입니다.", + "maximum-rule-chains-range": "규칙 체인 최대 수는 음수일 수 없습니다.", + "maximum-resources-sum-data-size": "리소스 파일의 최대 총 크기 (바이트)", + "maximum-resources-sum-data-size-required": "리소스 파일의 최대 총 크기는 필수 항목입니다.", + "maximum-resources-sum-data-size-range": "리소스 파일의 최대 총 크기는 음수일 수 없습니다.", + "maximum-resource-size": "최대 리소스 파일 크기 (바이트)", + "maximum-resource-size-required": "최대 리소스 파일 크기는 필수 항목입니다.", + "maximum-resource-size-range": "최대 리소스 파일 크기는 음수일 수 없습니다.", + "maximum-ota-packages-sum-data-size": "OTA 패키지 파일의 최대 총 크기 (바이트)", + "maximum-ota-package-sum-data-size-required": "OTA 패키지 파일의 최대 총 크기는 필수 항목입니다.", + "maximum-ota-package-sum-data-size-range": "OTA 패키지 파일의 최대 총 크기는 음수일 수 없습니다.", + "maximum-debug-duration-min": "최대 디버그 기간 (분)", + "maximum-debug-duration-min-range": "최대 디버그 기간은 음수일 수 없습니다.", + "rest-requests-for-tenant": "테넌트에 대한 REST 요청", + "transport-tenant-telemetry-msg-rate-limit": "테넌트 텔레메트리 메시지 전송", + "transport-tenant-telemetry-data-points-rate-limit": "테넌트 텔레메트리 데이터 포인트 전송", + "transport-device-msg-rate-limit": "장치 메시지 전송", + "transport-device-telemetry-msg-rate-limit": "장치 텔레메트리 메시지 전송", + "transport-device-telemetry-data-points-rate-limit": "장치 텔레메트리 데이터 포인트 전송", + "transport-gateway-msg-rate-limit": "전송 게이트웨이 메시지", + "transport-gateway-telemetry-msg-rate-limit": "전송 게이트웨이 텔레메트리 메시지", + "transport-gateway-telemetry-data-points-rate-limit": "전송 게이트웨이 텔레메트리 데이터 포인트", + "transport-gateway-device-msg-rate-limit": "전송 게이트웨이 장치 메시지", + "transport-gateway-device-telemetry-msg-rate-limit": "전송 게이트웨이 장치 텔레메트리 메시지", + "transport-gateway-device-telemetry-data-points-rate-limit": "전송 게이트웨이 장치 텔레메트리 데이터 포인트", + "tenant-entity-export-rate-limit": "엔티티 버전 생성", + "tenant-entity-import-rate-limit": "엔티티 버전 불러오기", + "tenant-notification-request-rate-limit": "알림 요청", + "tenant-notification-requests-per-rule-rate-limit": "알림 규칙당 알림 요청", + "max-calculated-fields": "엔티티당 계산 필드 최대 수", + "max-calculated-fields-range": "엔티티당 계산 필드 최대 수는 음수일 수 없습니다", + "max-calculated-fields-required": "엔티티당 계산 필드 최대 수가 필요합니다", + "max-data-points-per-rolling-arg": "롤링 인수의 최대 데이터 포인트 수", + "max-data-points-per-rolling-arg-range": "롤링 인수의 최대 데이터 포인트 수는 음수일 수 없습니다", + "max-data-points-per-rolling-arg-required": "롤링 인수의 최대 데이터 포인트 수가 필요합니다", + "max-arguments-per-cf": "계산 필드당 인수 최대 수", + "max-arguments-per-cf-range": "계산 필드당 인수 최대 수는 음수일 수 없습니다", + "max-arguments-per-cf-required": "계산 필드당 인수 최대 수가 필요합니다", + "max-related-level-per-argument": "'관련 엔티티' 인수당 최대 관계 레벨", + "max-related-level-per-argument-range": "'관련 엔티티' 인수당 관계 레벨 최대 수는 '1'보다 작을 수 없습니다", + "max-related-level-per-argument-required": "'관련 엔티티' 인수당 관계 레벨 최대 수가 필요합니다", + "min-allowed-scheduled-update-interval": "'관련 엔티티' 인수에 허용되는 최소 업데이트 간격 (초)", + "min-allowed-scheduled-update-interval-range": "허용되는 최소 업데이트 간격 최소 수는 음수일 수 없습니다", + "min-allowed-deduplication-interval": "허용되는 최소 중복 제거 간격 (초)", + "min-allowed-deduplication-interval-range": "허용되는 최소 중복 제거 간격 값은 음수일 수 없습니다", + "min-allowed-deduplication-interval-required": "허용되는 최소 중복 제거 간격이 필요합니다", + "intermediate-aggregation-interval": "중간 집계 간격 (초)", + "intermediate-aggregation-interval-range": "중간 집계 간격 값은 '1'보다 작을 수 없습니다", + "intermediate-aggregation-interval-required": "중간 집계 간격이 필요합니다", + "reevaluation-check-interval": "재평가 확인 간격 (초)", + "reevaluation-check-interval-range": "재평가 확인 간격 값은 '1'보다 작을 수 없습니다", + "reevaluation-check-interval-required": "재평가 확인 간격이 필요합니다", + "alarms-reevaluation-interval": "알람 재평가 간격 (초)", + "alarms-reevaluation-interval-range": "알람 재평가 간격 값은 '1'보다 작을 수 없습니다", + "alarms-reevaluation-interval-required": "알람 재평가 간격이 필요합니다", + "min-allowed-aggregation-interval": "허용되는 최소 집계 간격 (초)", + "min-allowed-aggregation-interval-range": "허용되는 최소 집계 간격 값은 음수일 수 없습니다", + "min-allowed-aggregation-interval-required": "허용되는 최소 집계 간격이 필요합니다", + "min-allowed-scheduled-update-interval-required": "허용되는 최소 업데이트 간격 최소 수가 필요합니다", + "max-state-size": "상태 최대 크기 (KB)", + "max-state-size-range": "상태 최대 크기 (KB)는 음수일 수 없습니다", + "max-state-size-required": "상태 최대 크기 (KB)가 필요합니다", + "max-value-argument-size": "단일 값 인수 최대 크기 (KB)", + "max-value-argument-size-range": "단일 값 인수 최대 크기 (KB)는 음수일 수 없습니다", + "max-value-argument-size-required": "단일 값 인수 최대 크기 (KB)가 필요합니다", + "max-transport-messages": "전송 메시지 최대 수", + "max-transport-messages-required": "전송 메시지 최대 수는 필수 항목입니다.", + "max-transport-messages-range": "전송 메시지 최대 수는 음수일 수 없습니다.", + "max-transport-data-points": "전송 데이터 포인트 최대 수", + "max-transport-data-points-required": "전송 데이터 포인트 최대 수는 필수 항목입니다.", + "max-transport-data-points-range": "전송 데이터 포인트 최대 수는 음수일 수 없습니다.", + "max-r-e-executions": "규칙 엔진 실행 최대 수", + "max-r-e-executions-required": "규칙 엔진 실행 최대 수는 필수 항목입니다.", + "max-r-e-executions-range": "규칙 엔진 실행 최대 수는 음수일 수 없습니다.", + "max-j-s-executions": "JavaScript 실행 최대 수", + "max-j-s-executions-required": "JavaScript 실행 최대 수는 필수 항목입니다.", + "max-j-s-executions-range": "JavaScript 실행 최대 수는 음수일 수 없습니다.", + "max-tbel-executions": "TBEL 실행 최대 수", + "max-tbel-executions-required": "TBEL 실행 최대 수가 필요합니다.", + "max-tbel-executions-range": "TBEL 실행 최대 수는 음수일 수 없습니다", + "max-d-p-storage-days": "데이터 포인트 저장 일수 최대 수", + "max-d-p-storage-days-required": "데이터 포인트 저장 일수 최대 수는 필수 항목입니다.", + "max-d-p-storage-days-range": "데이터 포인트 저장 일수 최대 수는 음수일 수 없습니다.", + "default-storage-ttl-days": "기본 저장소 TTL 일수", + "default-storage-ttl-days-required": "기본 저장소 TTL 일수는 필수 항목입니다.", + "default-storage-ttl-days-range": "기본 저장소 TTL 일수는 음수일 수 없습니다.", + "alarms-ttl-days": "알람 TTL 일수", + "alarms-ttl-days-required": "알람 TTL 일수가 필요합니다", + "alarms-ttl-days-days-range": "알람 TTL 일수는 음수일 수 없습니다", + "rpc-ttl-days": "RPC TTL 일수", + "rpc-ttl-days-required": "RPC TTL 일수가 필요합니다", + "rpc-ttl-days-days-range": "RPC TTL 일수는 음수일 수 없습니다", + "queue-stats-ttl-days": "큐 통계 TTL 일수", + "queue-stats-ttl-days-required": "큐 통계 TTL 일수가 필요합니다", + "queue-stats-ttl-days-range": "큐 통계 TTL 일수는 음수일 수 없습니다", + "rule-engine-exceptions-ttl-days": "규칙 엔진 예외 TTL 일수", + "rule-engine-exceptions-ttl-days-required": "규칙 엔진 예외 TTL 일수가 필요합니다", + "rule-engine-exceptions-ttl-days-range": "규칙 엔진 예외 TTL 일수는 음수일 수 없습니다", + "max-rule-node-executions-per-message": "메시지당 규칙 노드 실행 최대 수", + "max-rule-node-executions-per-message-required": "메시지당 규칙 노드 실행 최대 수는 필수 항목입니다.", + "max-rule-node-executions-per-message-range": "메시지당 규칙 노드 실행 최대 수는 음수일 수 없습니다.", + "max-emails": "발송 이메일 최대 수", + "max-emails-required": "발송 이메일 최대 수는 필수 항목입니다.", + "max-emails-range": "발송 이메일 최대 수는 음수일 수 없습니다.", + "sms-enabled": "SMS 활성화됨", + "max-sms": "발송 SMS 최대 수", + "max-sms-required": "발송 SMS 최대 수는 필수 항목입니다.", + "max-sms-range": "발송 SMS 최대 수는 음수일 수 없습니다.", + "max-created-alarms": "생성된 알람 최대 수", + "max-created-alarms-required": "생성된 알람 최대 수가 필요합니다.", + "max-created-alarms-range": "생성된 알람 최대 수는 음수일 수 있습니다", + "no-queue": "구성된 큐 없음", + "add-queue": "큐 추가", + "queues-with-count": "큐 ({{count}})", + "tenant-rest-limits": "테넌트에 대한 REST 요청", + "customer-rest-limits": "고객에 대한 REST 요청", + "incorrect-pattern-for-rate-limits": "형식은 용량과 기간(초)의 쉼표로 구분된 쌍이며 콜론으로 구분됩니다. 예: 100:1,2000:60", + "too-small-value-zero": "값은 0보다 커야 합니다", + "too-small-value-one": "값은 1보다 커야 합니다", + "queue-size-is-limited-by-system-configuration": "큐 크기는 시스템 구성에 의해서도 제한됩니다.", + "cassandra-write-tenant-core-limits-configuration": "REST API Cassandra 쓰기 쿼리", + "cassandra-read-tenant-core-limits-configuration": "REST API 및 WS 텔레메트리 Cassandra 읽기 쿼리", + "cassandra-write-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 쓰기 쿼리", + "cassandra-read-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 읽기 쿼리", + "ws-limit-max-sessions-per-tenant": "테넌트당 최대 세션 수", + "ws-limit-max-sessions-per-customer": "고객당 최대 세션 수", + "ws-limit-max-sessions-per-regular-user": "일반 사용자당 최대 세션 수", + "ws-limit-max-sessions-per-public-user": "공개 사용자당 최대 세션 수", + "ws-limit-queue-per-session": "세션당 최대 메시지 큐 크기", + "ws-limit-max-subscriptions-per-tenant": "테넌트당 최대 구독 수", + "ws-limit-max-subscriptions-per-customer": "고객당 최대 구독 수", + "ws-limit-max-subscriptions-per-regular-user": "일반 사용자당 최대 구독 수", + "ws-limit-max-subscriptions-per-public-user": "공개 사용자당 최대 구독 수", + "ws-limit-updates-per-session": "세션당 WS 업데이트", + "relation-search-entity-limit": "관계 검색 엔티티 제한", + "relation-search-entity-limit-hint": "관계 경로의 마지막 레벨에서 확인되는 엔티티 수를 제한합니다. '관련 엔티티' 인수 및 전파 필드에 적용됩니다.", + "relation-search-entity-limit-required": "관계 검색 엔티티 제한", + "relation-search-entity-limit-range": "관계 검색 엔티티 제한은 '1'보다 작을 수 없습니다", + "rate-limits": { + "add-limit": "제한 추가", + "and-also-less-than": "그리고 다음보다 작음", + "advanced-settings": "고급 설정", + "edit-limit": "제한 편집", + "calculated-field-debug-event-rate-limit": "계산 필드 디버그 이벤트", + "edit-calculated-field-debug-event-rate-limit": "계산 필드 디버그 이벤트 속도 제한 편집", + "edit-transport-tenant-msg-title": "전송 테넌트 메시지 속도 제한 편집", + "edit-transport-tenant-telemetry-msg-title": "전송 테넌트 텔레메트리 메시지 속도 제한 편집", + "edit-transport-tenant-telemetry-data-points-title": "전송 테넌트 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-transport-device-msg-title": "전송 장치 메시지 속도 제한 편집", + "edit-transport-device-telemetry-msg-title": "전송 장치 텔레메트리 메시지 속도 제한 편집", + "edit-transport-device-telemetry-data-points-title": "전송 장치 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-transport-gateway-msg-title": "전송 게이트웨이 메시지 속도 제한 편집", + "edit-transport-gateway-telemetry-msg-title": "전송 게이트웨이 텔레메트리 메시지 속도 제한 편집", + "edit-transport-gateway-telemetry-data-points-title": "전송 게이트웨이 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-transport-gateway-device-msg-title": "전송 게이트웨이 장치 메시지 속도 제한 편집", + "edit-transport-gateway-device-telemetry-msg-title": "전송 게이트웨이 장치 텔레메트리 메시지 속도 제한 편집", + "edit-transport-gateway-device-telemetry-data-points-title": "전송 게이트웨이 장치 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-tenant-rest-limits-title": "테넌트에 대한 REST 요청 속도 제한 편집", + "edit-customer-rest-limits-title": "고객에 대한 REST 요청 속도 제한 편집", + "edit-ws-limit-updates-per-session-title": "세션당 WS 업데이트 속도 제한 편집", + "edit-cassandra-write-tenant-core-limits-configuration": "Rest API Cassandra 쓰기 쿼리 편집", + "edit-cassandra-read-tenant-core-limits-configuration": "Rest API 및 WS 텔레메트리 Cassandra 읽기 쿼리 편집", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 쓰기 쿼리 편집", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 읽기 쿼리 편집", + "edit-tenant-entity-export-rate-limit-title": "엔티티 버전 생성 속도 제한 편집", + "edit-tenant-entity-import-rate-limit-title": "엔티티 버전 로드 속도 제한 편집", + "edit-tenant-notification-request-rate-limit-title": "알림 요청 속도 제한 편집", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "알림 규칙당 알림 요청 속도 제한 편집", + "edit-edge-events-rate-limit": "엣지 이벤트 속도 제한 편집", + "edit-edge-events-per-edge-rate-limit": "엣지당 엣지 이벤트 속도 제한 편집", + "edge-events-rate-limit": "엣지 이벤트", + "edge-events-per-edge-rate-limit": "엣지당 엣지 이벤트", + "edit-edge-uplink-messages-rate-limit": "엣지 업링크 메시지 속도 제한 편집", + "edit-edge-uplink-messages-per-edge-rate-limit": "엣지당 엣지 업링크 메시지 속도 제한 편집", + "edge-uplink-messages-rate-limit": "엣지 업링크 메시지", + "edge-uplink-messages-per-edge-rate-limit": "엣지당 업링크 메시지", + "messages-per": "메시지당", + "not-set": "설정되지 않음", + "number-of-messages": "메시지 수", + "number-of-messages-required": "메시지 수가 필요합니다.", + "number-of-messages-min": "최소 값은 1입니다.", + "preview": "미리보기", + "per-seconds": "초당", + "per-seconds-required": "시간 속도가 필요합니다.", + "per-seconds-min": "최소 값은 1입니다.", + "per-seconds-duplicate": "중복 시간 속도입니다. 각 시간 간격은 고유해야 합니다.", + "rate-limits": "속도 제한", + "remove-limit": "제한 제거", + "transport-tenant-msg": "전송 테넌트 메시지", + "transport-tenant-telemetry-msg": "테넌트 텔레메트리 메시지 전송", + "transport-tenant-telemetry-data-points": "테넌트 텔레메트리 데이터 포인트 전송", + "transport-device-msg": "장치 메시지 전송", + "transport-device-telemetry-msg": "장치 텔레메트리 메시지 전송", + "transport-device-telemetry-data-points": "장치 텔레메트리 데이터 포인트 전송", + "transport-gateway-msg": "전송 게이트웨이 메시지", + "transport-gateway-telemetry-msg": "전송 게이트웨이 텔레메트리 메시지", + "transport-gateway-telemetry-data-points": "전송 게이트웨이 텔레메트리 데이터 포인트", + "transport-gateway-device-msg": "전송 게이트웨이 장치 메시지", + "transport-gateway-device-telemetry-msg": "전송 게이트웨이 장치 텔레메트리 메시지", + "transport-gateway-device-telemetry-data-points": "전송 게이트웨이 장치 텔레메트리 데이터 포인트", + "sec": "초" + } }, "timeinterval": { "seconds-interval": "{ seconds, plural, =1 {1 초} other {# 초} }", @@ -2013,37 +6475,691 @@ "hours": "시간", "minutes": "분", "seconds": "초", - "advanced": "고급" + "advanced": "고급", + "custom": "사용자 지정", + "predefined": { + "yesterday": "어제", + "day-before-yesterday": "그저께", + "this-day-last-week": "지난주 오늘", + "previous-week": "지난주 (일 - 토)", + "previous-week-iso": "지난주 (월 - 일)", + "previous-month": "지난달", + "previous-quarter": "지난 분기", + "previous-half-year": "지난 반기", + "previous-year": "작년", + "current-hour": "현재 시간", + "current-day": "오늘", + "current-day-so-far": "오늘 현재", + "current-week": "이번주 (일 - 토)", + "current-week-iso": "이번주 (월 - 일)", + "current-week-so-far": "이번주 현재 (일 - 토)", + "current-week-iso-so-far": "이번주 현재 (월 - 일)", + "current-month": "이번달", + "current-month-so-far": "이번달 현재", + "current-quarter": "이번 분기", + "current-quarter-so-far": "이번 분기 현재", + "current-half-year": "이번 반기", + "current-half-year-so-far": "이번 반기 현재", + "current-year": "올해", + "current-year-so-far": "올해 현재" + }, + "type": { + "week": "주 (일 - 토)", + "week-iso": "주 (월 - 일)", + "month": "달", + "quarter": "분기" + } }, "timeunit": { + "milliseconds": "밀리초", "seconds": "초", "minutes": "분", "hours": "시간", "days": "일" }, "timewindow": { + "timewindow": "타임윈도우", + "timewindow-settings": "시간 창 설정", + "years": "{ years, plural, =1 {1년 } other {#년 } }", + "years-short": "{{ years }}년", + "months": "{ months, plural, =1 {1개월 } other {#개월 } }", + "months-short": "{{ months }}개월", + "weeks": "{ weeks, plural, =1 {1주 } other {#주 } }", + "weeks-short": "{{ weeks }}주", "days": "{ days, plural, =1 {1 일 } other {# 일 } }", + "days-short": "{{ days }}일", "hours": "{ hours, plural, =0 {0 시간 } =1 {1 시간 } other {# 시간 } }", + "hr": "{{ hr }} 시간", + "hr-short": "{{ hr }}시간", "minutes": "{ minutes, plural, =0 {0 분 } =1 {1 분 } other {# 분 } }", + "min": "{{ min }} 분", + "min-short": "{{ min }}분", "seconds": "{ seconds, plural, =0 {0 초 } =1 {1 초 } other {# 초 } }", + "sec": "{{ sec }} 초", + "sec-short": "{{ sec }}초", + "short": { + "years": "{ years, plural, =1 {1년 } other {#년 } }", + "days": "{ days, plural, =1 {1 일 } other {# 일 } }", + "hours": "{ hours, plural, =1 {1시간 } other {#시간 } }", + "minutes": "{{minutes}}분 ", + "seconds": "{{seconds}}초 " + }, "realtime": "실시간", "history": "기록", "last-prefix": "과거", "period": "{{ startTime }}부터 {{ endTime }}까지", "edit": "타임윈도우 편집", "date-range": "날짜 범위", + "for-all-time": "전체 기간", "last": "과거", "time-period": "기간", - "hide": "숨기기" + "hide": "숨기기", + "interval": "간격", + "just-now": "방금", + "just-now-lower": "방금", + "ago": "전", + "style": "시간 창 스타일", + "icon": "아이콘", + "icon-position": "아이콘 위치", + "icon-position-left": "왼쪽", + "icon-position-right": "오른쪽", + "font": "글꼴", + "color": "색상", + "displayTypePrefix": "실시간/기록 접두사 표시", + "preview": "미리보기", + "relative": "상대적", + "range": "범위", + "hide-timewindow-section": "최종 사용자에게 시간 창 섹션 숨기기", + "hide-last-interval": "최종 사용자에게 마지막 간격 숨기기", + "hide-relative-interval": "최종 사용자에게 상대적 간격 숨기기", + "hide-fixed-interval": "최종 사용자에게 고정 간격 숨기기", + "hide-aggregation": "최종 사용자에게 집계 숨기기", + "hide-group-interval": "최종 사용자에게 그룹화 간격 숨기기", + "hide-max-values": "최종 사용자에게 최대 값 숨기기", + "hide-timezone": "최종 사용자에게 시간대 숨기기", + "disable-custom-interval": "사용자 지정 간격 선택 비활성화", + "edit-aggregation-functions-list": "집계 함수 목록 편집", + "edit-aggregation-functions-list-hint": "사용 가능한 옵션 목록을 지정할 수 있습니다.", + "allowed-aggregation-functions": "허용된 집계 함수", + "edit-intervals-list": "간격 목록 편집", + "allowed-agg-intervals": "그룹화 간격", + "default-agg-interval": "기본 그룹화 간격", + "edit-intervals-list-hint": "사용 가능한 간격 옵션 목록을 지정할 수 있습니다.", + "edit-grouping-intervals-list-hint": "그룹화 간격 목록과 기본 그룹화 간격을 구성할 수 있습니다.", + "all": "모두", + "save-current-settings-as-default": "현재 설정을 기본 시간 창으로 저장", + "hide-option-from-end-users": "최종 사용자에게 옵션 숨기기" + }, + "tooltip": { + "trigger": "트리거", + "trigger-point": "지점", + "trigger-axis": "축", + "label": "라벨", + "value": "값", + "date": "날짜", + "show-date-time-interval": "날짜 시간 간격 표시", + "show-date-time-interval-hint": "데이터 집계에 따라 날짜 시간 간격을 표시합니다.", + "hide-zero-tooltip-values": "0 값 숨기기", + "show-stack-total": "스택 모드에서 총 값 표시", + "background-color": "배경색", + "background-blur": "배경 블러" + }, + "unit": { + "set-unit-conversion": "단위 변환 설정", + "unit-settings": { + "unit-settings": "단위 설정", + "source-unit": "소스 단위", + "source-unit-hint": "저장된 값의 단위입니다. 변환할 원본 단위입니다. 소스 데이터에서 사용하는 기호를 입력하세요 (예: m, km, ft, in).", + "target-metric-unit": "대상 미터법 단위", + "target-metric-unit-hint": "소스 값을 변환할 미터법 단위(SI)를 선택하세요 (예: cm, mm, km).", + "target-imperial-unit": "대상 야드파운드법 단위", + "target-imperial-unit-hint": "소스 값을 변환할 야드파운드법 단위를 선택하세요 (예: in, ft, yd).", + "target-hybrid-unit": "대상 하이브리드 단위", + "target-hybrid-unit-hint": "소스 값을 변환할 하이브리드 단위를 선택하세요 (예: cm, in, km). 하이브리드 단위는 미터법 또는 야드파운드법 단위를 결합합니다.", + "enable-unit-conversion": "단위 변환 활성화", + "enable-unit-conversion-hint": "켜기로 전환하여 변환을 활성화합니다. 꺼져 있으면 소스 값이 변경되지 않고 전달됩니다. 해당 측정 그룹에 단위가 하나만 있는 경우 비활성화됩니다 (예: 광속, AQI)." + }, + "unit-system": "단위 시스템", + "unit-system-type": { + "AUTO": "자동", + "METRIC": "미터법", + "IMPERIAL": "야드파운드법", + "HYBRID": "하이브리드" + }, + "measures": { + "absorbed-dose-rate": "흡수 선량률", + "acceleration": "가속도", + "acidity": "산도", + "air-quality-index": "대기질 지수", + "amount-of-substance": "물질량", + "angle": "각도", + "angular-acceleration": "각가속도", + "area": "영역", + "area-density": "면적 밀도", + "capacitance": "정전 용량", + "catalytic-activity": "촉매 활성", + "catalytic-concentration": "촉매 농도", + "charge": "전하", + "current-density": "전류 밀도", + "data-transfer-rate": "데이터 전송 속도", + "density": "밀도", + "digital": "디지털", + "dimension-ratio": "차원 비율", + "dynamic-viscosity": "동점도", + "earthquake-magnitude": "지진 규모", + "electric-charge-density": "전하 밀도", + "electric-current": "전류", + "electric-dipole-moment": "전기 쌍극자 모멘트", + "electric-field-strength": "전기장 세기", + "electric-flux": "전기 선속", + "electric-permittivity": "유전율", + "electric-polarizability": "전기 분극률", + "electrical-conductance": "전도도", + "electrical-conductivity": "전도율", + "energy": "에너지", + "energy-density": "에너지 밀도", + "force": "힘", + "frequency": "주파수", + "fuel-efficiency": "연료 효율", + "heat-capacity": "열용량", + "illuminance": "조도", + "inductance": "인덕턴스", + "kinematic-viscosity": "동점도", + "length": "길이", + "light-exposure": "노광량", + "linear-charge-density": "선형 전하 밀도", + "logarithmic-ratio": "로그 비율", + "luminous-efficacy": "발광 효율", + "luminous-flux": "광선속", + "luminous-intensity": "광도", + "magnetic-field-gradient": "자기장 기울기", + "magnetic-flux": "자기 선속", + "magnetic-flux-density": "자기 선속 밀도", + "magnetic-moment": "자기 모멘트", + "magnetic-permeability": "투자율", + "mass": "질량", + "mass-fraction": "질량 분율", + "molar-concentration": "몰 농도", + "molar-energy": "몰 에너지", + "molar-heat-capacity": "몰 열용량", + "molar-mass": "몰 질량", + "number-concentration": "수 농도", + "parts-per-million": "백만분율", + "power": "전력", + "power-density": "전력 밀도", + "pressure": "압력", + "radiance": "복사 휘도", + "radiant-intensity": "복사 강도", + "radiation-dose": "방사선량", + "radioactive-decay": "방사성 붕괴", + "radioactivity": "방사능", + "radioactivity-concentration": "방사능 농도", + "reciprocal-length": "역 길이", + "resistance": "저항", + "reynolds-number": "레이놀즈 수", + "signal-level": "신호 레벨", + "solid-angle": "입체각", + "specific-energy": "비에너지", + "specific-heat-capacity": "비열", + "specific-humidity": "비습도", + "specific-volume": "비체적", + "speed": "속도", + "surface-charge-density": "표면 전하 밀도", + "surface-tension": "표면 장력", + "temperature": "온도", + "thermal-conductivity": "열전도율", + "time": "시간", + "torque": "토크", + "turbidity": "탁도", + "voltage": "전압", + "volume": "부피", + "volume-flow": "체적 유량" + }, + "millimeter": "밀리미터", + "centimeter": "센티미터", + "decimeter": "데시미터", + "angstrom": "옹스트롬", + "nanometer": "나노미터", + "micrometer": "마이크로미터", + "meter": "미터", + "kilometer": "킬로미터", + "inch": "인치", + "foot": "피트", + "foot-us": "피트 (미국 측량)", + "yard": "야드", + "mile": "마일", + "nautical-mile": "해리", + "astronomical-unit": "천문 단위", + "reciprocal-metre": "역 미터", + "meter-per-meter": "미터당 미터", + "steradian": "스테라디안", + "thou": "파운드", + "barleycorn": "보리콘", + "hand": "핸드", + "chain": "체인", + "furlong": "퍼롱", + "league": "리그", + "fathom": "패덤", + "cable": "케이블", + "link": "링크", + "rod": "로드", + "nanogram": "나노그램", + "microgram": "마이크로그램", + "milligram": "밀리그램", + "gram": "그램", + "kilogram": "킬로그램", + "tonne": "톤", + "ounce": "온스", + "pound": "파운드", + "stone": "스톤", + "hundredweight-count": "헌드레드웨이트", + "short-tons": "단톤", + "dalton": "달튼", + "grain": "그레인", + "drachm": "드램", + "quarter": "분기", + "slug": "슬러그", + "carat": "캐럿", + "cubic-millimeter": "세제곱밀리미터", + "cubic-centimeter": "세제곱센티미터", + "cubic-meter": "세제곱미터", + "cubic-kilometer": "세제곱킬로미터", + "microliter": "마이크로리터", + "milliliter": "밀리리터", + "liter": "리터", + "hectoliter": "헥토리터", + "cubic-inch": "세제곱인치", + "cubic-foot": "세제곱피트", + "cubic-yard": "세제곱야드", + "fluid-ounce": "액량 온스", + "fluid-ounce-per-second": "초당 액량 온스", + "pint": "파인트", + "quart": "쿼트", + "gallon": "갤런", + "oil-barrels": "석유 배럴", + "cubic-meter-per-kilogram": "킬로그램당 세제곱미터", + "gill": "길", + "hogshead": "혹스헤드", + "teaspoon": "티스푼", + "tablespoon": "테이블스푼", + "cup": "컵", + "celsius": "섭씨", + "kelvin": "켈빈", + "rankine": "랭킨", + "fahrenheit": "화씨", + "percent": "퍼센트", + "meter-per-second": "미터 매 초", + "kilometer-per-hour": "킬로미터 매 시간", + "foot-per-second": "피트 매 초", + "foot-per-minute": "피트 매 분", + "mile-per-hour": "마일 매 시간", + "knot": "노트", + "inch-per-second": "인치 매 초", + "inch-per-hour": "인치 매 시간", + "millimeters-per-minute": "밀리미터 매 분", + "meter-per-minute": "미터 매 분", + "kilometer-per-hour-squared": "킬로미터 매 시간 제곱", + "foot-per-second-squared": "피트 매 초 제곱", + "pascal": "파스칼", + "kilopascal": "킬로파스칼", + "megapascal": "메가파스칼", + "gigapascal": "기가파스칼", + "millibar": "밀리바", + "bar": "막대", + "kilobar": "킬로바", + "newton": "뉴턴", + "newton-meter": "뉴턴 미터", + "foot-pounds": "피트파운드", + "inch-pounds": "인치파운드", + "newton-per-meter": "뉴턴 매 미터", + "atmospheres": "기압", + "pounds-per-square-inch": "파운드 매 제곱인치", + "kilopound-per-square-inch": "킬로파운드 매 제곱인치", + "torr": "토르", + "inches-of-mercury": "수은주 인치", + "pascal-per-square-meter": "파스칼 매 제곱미터", + "pound-per-square-inch": "파운드 매 제곱인치", + "newton-per-square-meter": "뉴턴 매 제곱미터", + "kilogram-force-per-square-meter": "킬로그램힘 매 제곱미터", + "pascal-per-square-centimeter": "파스칼 매 제곱센티미터", + "ton-force-per-square-inch": "톤힘 매 제곱인치", + "kilonewton-per-square-meter": "킬로뉴턴 매 제곱미터", + "newton-per-square-millimeter": "뉴턴 매 제곱밀리미터", + "microjoule": "마이크로줄", + "millijoule": "밀리줄", + "joule": "줄", + "kilojoule": "킬로줄", + "megajoule": "메가줄", + "gigajoule": "기가줄", + "watt-hour": "와트시", + "watt-minute": "와트분", + "kilowatt-hour": "킬로와트시", + "milliwatt-hour": "밀리와트시", + "megawatt-hour": "메가와트시", + "gigawatt-hour": "기가와트시", + "electron-volts": "전자볼트", + "joules-per-coulomb": "줄 매 쿨롱", + "british-thermal-unit": "영국 열량 단위", + "thousand-british-thermal-unit": "천 영국 열량 단위", + "million-british-thermal-unit": "백만 영국 열량 단위", + "foot-pound": "피트파운드", + "calorie": "칼로리", + "small-calorie": "소칼로리", + "kilocalorie": "킬로칼로리", + "joule-per-kelvin": "줄 매 켈빈", + "joule-per-kilogram-kelvin": "줄 매 킬로그램켈빈", + "joule-per-kilogram": "줄 매 킬로그램", + "watt-per-meter-kelvin": "와트 매 미터켈빈", + "joule-per-cubic-meter": "줄 매 세제곱미터", + "therm": "덤", + "electric-dipole-moment": "전기 쌍극자 모멘트", + "magnetic-dipole-moment": "자기 쌍극자 모멘트", + "debye": "데바이", + "coulomb-per-square-meter-per-volt": "쿨롱 매 제곱미터 매 볼트", + "milliwatt": "밀리와트", + "microwatt": "마이크로와트", + "watt": "와트", + "kilowatt": "킬로와트", + "megawatt": "메가와트", + "gigawatt": "기가와트", + "metric-horsepower": "미터법 마력", + "milliwatt-per-square-centimeter": "밀리와트 매 제곱센티미터", + "watt-per-square-centimeter": "와트 매 제곱센티미터", + "kilowatt-per-square-centimeter": "킬로와트 매 제곱센티미터", + "milliwatt-per-square-meter": "밀리와트 매 제곱미터", + "watt-per-square-meter": "와트 매 제곱미터", + "kilowatt-per-square-meter": "킬로와트 매 제곱미터", + "watt-per-square-inch": "와트 매 제곱인치", + "kilowatt-per-square-inch": "킬로와트 매 제곱인치", + "horsepower": "마력", + "btu-per-hour": "영국 열량 단위 매 시간", + "btu-per-second": "영국 열량 단위 매 초", + "btu-per-day": "영국 열량 단위 매 일", + "mbtu-per-hour": "천 영국 열량 단위 매 시간", + "mbtu-per-second": "천 영국 열량 단위 매 초", + "mbtu-per-day": "천 영국 열량 단위 매 일", + "mmbtu-per-hour": "백만 영국 열량 단위 매 시간", + "mmbtu-per-second": "백만 영국 열량 단위 매 초", + "mmbtu-per-day": "백만 영국 열량 단위 매 일", + "foot-pound-per-second": "피트파운드 매 초", + "coulomb": "쿨롱", + "millicoulomb": "밀리쿨롱", + "microcoulomb": "마이크로쿨롱", + "nanocoulomb": "나노쿨롱", + "picocoulomb": "피코쿨롱", + "coulomb-per-meter": "쿨롱 매 미터", + "coulomb-per-cubic-meter": "쿨롱 매 세제곱미터", + "coulomb-per-square-meter": "쿨롱 매 제곱미터", + "square-millimeter": "제곱밀리미터", + "square-centimeter": "제곱센티미터", + "square-meter": "제곱미터", + "hectare": "헥타르", + "square-kilometer": "제곱킬로미터", + "square-inch": "제곱인치", + "square-foot": "제곱피트", + "square-yard": "제곱야드", + "acre": "에이커", + "square-mile": "제곱마일", + "are": "아르", + "barn": "반", + "circular-inch": "원형 인치", + "milliampere-hour": "밀리암페어시", + "ampere-hours": "암페어시", + "kiloampere-hours": "킬로암페어시", + "nanoampere": "나노암페어", + "picoampere": "피코암페어", + "microampere": "마이크로암페어", + "milliampere": "밀리암페어", + "ampere": "암페어", + "kiloampere": "킬로암페어", + "megaampere": "메가암페어", + "gigaampere": "기가암페어", + "microampere-per-square-centimeter": "마이크로암페어 매 제곱센티미터", + "ampere-per-square-meter": "암페어 매 제곱미터", + "ampere-per-meter": "암페어 매 미터", + "oersted": "에르스테드", + "bohr-magneton": "보어 마그네톤", + "ampere-meter-squared": "암페어미터 제곱", + "nanovolt": "나노볼트", + "picovolt": "피코볼트", + "millivolt": "밀리볼트", + "microvolt": "마이크로볼트", + "volt": "볼트", + "kilovolt": "킬로볼트", + "megavolt": "메가볼트", + "dbmV": "데시벨 볼트", + "dbm": "데시벨 밀리와트", + "volt-meter": "볼트미터", + "kilovolt-meter": "킬로볼트미터", + "megavolt-meter": "메가볼트미터", + "microvolt-meter": "마이크로볼트미터", + "millivolt-meter": "밀리볼트미터", + "nanovolt-meter": "나노볼트미터", + "ohm": "옴", + "microohm": "마이크로옴", + "milliohm": "밀리옴", + "kilohm": "킬로옴", + "megohm": "메가옴", + "gigohm": "기가옴", + "millihertz": "밀리헤르츠", + "hertz": "헤르츠", + "kilohertz": "킬로헤르츠", + "megahertz": "메가헤르츠", + "gigahertz": "기가헤르츠", + "terahertz": "테라헤르츠", + "rpm": "분당 회전수", + "candela-per-square-meter": "칸델라 매 제곱미터", + "candela": "칸델라", + "lumen": "루멘", + "lux": "럭스", + "foot-candle": "피트캔들", + "lumen-per-square-meter": "루멘 매 제곱미터", + "lux-second": "럭스 초", + "lumen-second": "루멘 초", + "lumens-per-watt": "루멘 매 와트", + "mole": "몰", + "nanomole": "나노몰", + "micromole": "마이크로몰", + "millimole": "밀리몰", + "kilomole": "킬로몰", + "mole-per-cubic-meter": "몰 매 세제곱미터", + "rssi": "수신 신호 강도 표시기", + "ppm": "백만분율", + "ppb": "10억분의 1", + "micrograms-per-cubic-meter": "마이크로그램 매 세제곱미터", + "aqi": "AQI", + "gram-per-cubic-meter": "그램 매 세제곱미터", + "gram-per-kilogram": "비습도", + "millimeters-per-second": "밀리미터 매 초", + "neper": "네퍼", + "bel": "벨", + "decibel": "데시벨", + "meters-per-second-squared": "미터 매 초 제곱", + "becquerel": "베크렐", + "curie": "퀴리", + "gray": "그레이", + "sievert": "시버트", + "roentgen": "뢴트겐", + "cps": "초당 카운트", + "rad": "라드", + "rem": "렘", + "dps": "초당 붕괴", + "rutherford": "러더퍼드", + "coulombs-per-kilogram": "쿨롱 매 킬로그램", + "becquerels-per-cubic-meter": "베크렐 매 세제곱미터", + "curies-per-liter": "퀴리 매 리터", + "becquerels-per-second": "초당 베크렐", + "curies-per-second": "초당 퀴리", + "gy-per-second": "초당 그레이", + "watt-per-steradian": "스테라디안당 와트", + "watt-per-square-metre-steradian": "제곱미터-스테라디안당 와트", + "ph-level": "pH 레벨", + "turbidity": "탁도", + "mg-per-liter": "리터당 밀리그램", + "microsiemens-per-centimeter": "센티미터당 마이크로지멘스", + "millisiemens-per-meter": "미터당 밀리지멘스", + "siemens-per-meter": "미터당 지멘스", + "kilogram-per-cubic-meter": "세제곱미터당 킬로그램", + "gram-per-cubic-centimeter": "세제곱센티미터당 그램", + "kilogram-per-square-meter": "제곱미터당 킬로그램", + "milligram-per-milliliter": "밀리리터당 밀리그램", + "milligram-per-cubic-meter": "세제곱미터당 밀리그램", + "pound-per-cubic-foot": "세제곱피트당 파운드", + "ounces-per-cubic-inch": "세제곱인치당 온스", + "tons-per-cubic-yard": "세제곱야드당 톤", + "particle-density": "입자 밀도", + "kilometers-per-liter": "리터당 킬로미터", + "miles-per-gallon": "갤런당 마일", + "liters-per-100-km": "100km당 리터", + "gallons-per-mile": "마일당 갤런", + "liters-per-hour": "시간당 리터", + "gallons-per-hour": "시간당 갤런", + "beats-per-minute": "분당 박동수", + "millimeters-of-mercury": "수은주 밀리미터", + "milligrams-per-deciliter": "데시리터당 밀리그램", + "g-force": "중력 가속도", + "kilonewton": "킬로뉴턴", + "kilogram-force": "킬로그램힘", + "pound-force": "파운드힘", + "kilopound-force": "킬로파운드힘", + "dyne": "다인", + "poundal": "파운달", + "kip": "킵", + "gal": "갈", + "gravity": "중력", + "hectopascal": "헥토파스칼", + "atmosphere": "기압", + "millibars": "밀리바", + "inch-of-mercury": "수은주 인치", + "richter-scale": "리히터 규모", + "nanosecond": "나노초", + "microsecond": "마이크로초", + "millisecond": "밀리초", + "second": "초", + "minute": "분", + "hour": "시간", + "day": "일", + "week": "주", + "month": "달", + "year": "년", + "cubic-foot-per-minute": "분당 세제곱피트", + "cubic-meters-per-hour": "시간당 세제곱미터", + "cubic-meters-per-second": "초당 세제곱미터", + "liter-per-second": "초당 리터", + "liter-per-minute": "분당 리터", + "gallons-per-minute": "분당 갤런", + "cubic-foot-per-second": "초당 세제곱피트", + "milliliters-per-minute": "분당 밀리리터", + "cubic-decimeter-per-second": "초당 세제곱데시미터", + "bit": "비트", + "byte": "바이트", + "kilobyte": "킬로바이트", + "megabyte": "메가바이트", + "gigabyte": "기가바이트", + "terabyte": "테라바이트", + "petabyte": "페타바이트", + "exabyte": "엑사바이트", + "zettabyte": "제타바이트", + "yottabyte": "요타바이트", + "bit-per-second": "초당 비트", + "kilobit-per-second": "초당 킬로비트", + "megabit-per-second": "초당 메가비트", + "gigabit-per-second": "초당 기가비트", + "terabit-per-second": "초당 테라비트", + "byte-per-second": "초당 바이트", + "kilobyte-per-second": "초당 킬로바이트", + "megabyte-per-second": "초당 메가바이트", + "gigabyte-per-second": "초당 기가바이트", + "degree": "도", + "radian": "라디안", + "gradian": "그라디안", + "arcminute": "각분", + "arcsecond": "각초", + "milliradian": "밀리라디안", + "revolution": "회전", + "siemens": "지멘스", + "millisiemens": "밀리지멘스", + "microsiemens": "마이크로지멘스", + "kilosiemens": "킬로지멘스", + "megasiemens": "메가지멘스", + "gigasiemens": "기가지멘스", + "farad": "패럿", + "millifarad": "밀리패럿", + "microfarad": "마이크로패럿", + "nanofarad": "나노패럿", + "picofarad": "피코패럿", + "kilofarad": "킬로패럿", + "megafarad": "메가패럿", + "gigafarad": "기가패럿", + "terfarad": "테라패럿", + "farad-per-meter": "미터당 패럿", + "tesla": "테슬라", + "gauss": "가우스", + "kilogauss": "킬로가우스", + "millitesla": "밀리테슬라", + "microtesla": "마이크로테슬라", + "nanotesla": "나노테슬라", + "kilotesla": "킬로테슬라", + "megatesla": "메가테슬라", + "millitesla-square-meters": "밀리테슬라 제곱미터", + "gamma": "감마", + "lambda": "람다", + "square-meter-per-second": "초당 제곱미터", + "square-centimeter-per-second": "초당 제곱센티미터", + "stoke": "스토크", + "centistokes": "센티스토크", + "square-foot-per-second": "초당 제곱피트", + "square-inch-per-second": "초당 제곱인치", + "pascal-second": "파스칼-초", + "centipoise": "센티푸아즈", + "poise": "푸아즈", + "reynolds": "레이놀즈", + "pound-per-foot-hour": "피트-시간당 파운드", + "newton-second-per-square-meter": "제곱미터당 뉴턴 초", + "dyne-second-per-square-centimeter": "제곱센티미터당 다인 초", + "kilogram-per-meter-second": "미터-초당 킬로그램", + "tesla-square-meters": "테슬라 제곱미터", + "maxwell": "맥스웰", + "tesla-per-meter": "미터당 테슬라", + "gauss-per-centimeter": "센티미터당 가우스", + "weber": "웨버", + "microweber": "마이크로웨버", + "milliweber": "밀리웨버", + "gauss-square-centimeter": "가우스-제곱센티미터", + "kilogauss-square-centimeter": "킬로가우스-제곱센티미터", + "henry": "헨리", + "millihenry": "밀리헨리", + "microhenry": "마이크로헨리", + "nanohenry": "나노헨리", + "henry-per-meter": "미터당 헨리", + "tesla-meter-per-ampere": "암페어당 테슬라 미터", + "gauss-per-oersted": "에르스테드당 가우스", + "kilogram-per-mole": "몰당 킬로그램", + "gram-per-mole": "몰당 그램", + "milligram-per-mole": "몰당 밀리그램", + "joule-per-mole": "몰당 줄", + "joule-per-mole-kelvin": "몰-켈빈당 줄", + "millivolts-per-meter": "미터당 밀리볼트", + "volts-per-meter": "미터당 볼트", + "kilovolts-per-meter": "미터당 킬로볼트", + "radian-per-second": "초당 라디안", + "radian-per-second-squared": "초 제곱당 라디안", + "revolutions-per-minute-per-second": "각가속도", + "deg-per-second": "초당 도", + "rotation-per-minute": "분당 회전", + "degrees-brix": "브릭스 도", + "katal": "카탈", + "katal-per-cubic-metre": "세제곱미터당 카탈", + "paris-inch": "파리 인치" }, "user": { "user": "사용자", "users": "사용자", - "customer-users": "커스터머 사용자", + "customer-users": "고객 사용자", "tenant-admins": "테넌트 관리자", "sys-admin": "시스템 관리자", "tenant-admin": "테넌트 관리자", - "customer": "커스터머", + "customer": "고객", "anonymous": "익명", "add": "사용자 추가", "delete": "사용자 삭제", @@ -2057,9 +7173,9 @@ "delete-users-text": "사용자 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "activation-email-sent-message": "활성화 이메일을 보냈습니다!", "resend-activation": "활성화 재전송", - "email": "Email", + "email": "이메일", "email-required": "Email을 입력하세요.", - "invalid-email-format": "잘못된 Email 형식입니다.", + "invalid-email-format": "잘못된 Email 형식입니다.", "first-name": "이름", "last-name": "성", "description": "설명", @@ -2077,22 +7193,27 @@ "activation-link-copied-message": "사용자 활성화 링크가 클립보드로 복사되었습니다.", "details": "상세", "login-as-tenant-admin": "테넌트 관리자로 로그인", - "login-as-customer-user": "커스터머 사용자로 로그인", + "login-as-customer-user": "고객 사용자로 로그인", "search": "사용자 검색", "selected-users": "{ count, plural, =1 {1명 사용자} other {#명 사용자} } 선택됨", "disable-account": "사용자 계정 비활성화", "enable-account": "사용자 계정 활성", "enable-account-message": "사용자 계정이 성공적으로 활성화 되었습니다!", - "disable-account-message": "사용자 계정이 성공적으로 비활성화 되었습니다!" + "disable-account-message": "사용자 계정이 성공적으로 비활성화 되었습니다!", + "copyId": "사용자 ID 복사", + "idCopiedMessage": "사용자 ID가 클립보드에 복사되었습니다.", + "user-list": "사용자 목록", + "user-list-required": "사용자 목록은 필수 항목입니다.", + "printable-one-time-password-hint": "이 인쇄 가능한 일회용 암호는 여행 중처럼 휴대전화를 사용할 수 없을 때 로그인할 수 있도록 해줍니다." }, "value": { "type": "값 유형", "string": "문자열", "string-value": "문자열 값", - "string-value-required": "문자열 값을 입력하세요", + "string-value-required": "문자열 값을 입력하세요", "integer": "정수", "integer-value": "정수 값", - "integer-value-required": "정수 값을 입력하세요", + "integer-value-required": "정수 값을 입력하세요", "invalid-integer-value": "유효하지 않은 정수 값", "double": "실수", "double-value": "실수 값", @@ -2101,24 +7222,109 @@ "boolean-value": "불리언 값", "false": "거짓", "true": "참", - "long": "Long", + "long": "정수", "json": "JSON", "json-value": "JSON 값", "json-value-invalid": "JSON 값의 형식이 잘못되었습니다", "json-value-required": "JSON 값을 입력하세요." }, + "version-control": { + "version-control": "버전 관리", + "management": "버전 관리 관리", + "search": "버전 검색", + "branch": "브랜치", + "default": "기본값", + "select-branch": "브랜치 선택", + "branch-required": "브랜치가 필요합니다", + "create-entity-version": "엔티티 버전 생성", + "version-name": "버전 이름", + "version-name-required": "버전 이름은 필수 항목입니다.", + "author": "작성자", + "export-relations": "관계 내보내기", + "export-attributes": "속성 내보내기", + "export-credentials": "자격 증명 내보내기", + "export-calculated-fields": "계산 필드 및 알람 규칙 내보내기", + "export-alarm-rules": "알람 규칙 내보내기", + "entity-versions": "엔티티 버전", + "versions": "버전", + "created-time": "생성 일시", + "version-id": "버전 ID", + "no-entity-versions-text": "엔티티 버전을 찾을 수 없습니다.", + "no-versions-text": "버전을 찾을 수 없습니다.", + "copy-full-version-id": "전체 버전 ID 복사", + "create-version": "버전 생성", + "creating-version": "버전 생성 중... 잠시 기다려주세요", + "nothing-to-commit": "커밋할 변경 사항 없음", + "restore-version": "버전 복원", + "restore-entity-from-version": "버전 '{{versionName}}'에서 엔티티 복원", + "restoring-entity-version": "엔티티 버전 복원 중... 잠시 기다려주세요", + "load-relations": "관계 로드", + "load-attributes": "속성 로드", + "load-credentials": "자격 증명 로드", + "load-calculated-fields": "계산 필드 및 알람 규칙 로드", + "load-alarm-rules": "알람 규칙 로드", + "compare-with-current": "현재와 비교", + "diff-entity-with-version": "엔티티 버전 '{{versionName}}'과(와) 차이점", + "previous-difference": "이전 차이점", + "next-difference": "다음 차이점", + "current": "현재", + "differences": "{ count, plural, =1 {1개 차이점} other {#개 차이점} }", + "create-entities-version": "엔티티 버전 생성", + "default-sync-strategy": "기본 동기화 전략", + "sync-strategy-merge": "병합", + "sync-strategy-overwrite": "덮어쓰기", + "entities-to-export": "내보낼 엔티티", + "entities-to-restore": "복원할 엔티티", + "sync-strategy": "동기화 전략", + "all-entities": "모든 엔티티", + "no-entities-to-export-prompt": "내보낼 엔티티를 지정하세요", + "no-entities-to-restore-prompt": "복원할 엔티티를 지정하세요", + "add-entity-type": "엔티티 유형 추가", + "remove-all": "모두 제거", + "version-create-result": "{ added, plural, =0 {엔티티 없음} =1 {1개 엔티티} other {#개 엔티티} } 추가됨.
    { modified, plural, =0 {엔티티 없음} =1 {1개 엔티티} other {#개 엔티티} } 수정됨.
    { removed, plural, =0 {엔티티 없음} =1 {1개 엔티티} other {#개 엔티티} } 제거됨.", + "remove-other-entities": "다른 엔티티 제거", + "find-existing-entity-by-name": "이름으로 기존 엔티티 찾기", + "restore-entities-from-version": "버전 '{{versionName}}'에서 엔티티 복원", + "restoring-entities-from-version": "엔티티 복원 중... 잠시 기다려 주세요", + "no-entities-restored": "복원된 엔티티 없음", + "created": "{{created}}개 생성됨", + "updated": "{{updated}}개 업데이트됨", + "deleted": "{{deleted}}개 삭제됨", + "remove-other-entities-confirm-text": "주의! 이 작업은 복원하려는 버전에 없는 모든 현재 엔티티를 영구적으로 삭제합니다.

    확인하려면 \"다른 엔티티 제거\"를 입력하세요.", + "auto-commit-to-branch": "{{ branch }} 브랜치에 자동 커밋", + "default-create-entity-version-name": "{{entityName}} 업데이트", + "sync-strategy-merge-hint": "저장소에서 선택한 엔티티를 생성하거나 업데이트합니다. 다른 모든 저장소 엔티티는 수정되지 않습니다.", + "sync-strategy-overwrite-hint": "저장소에서 선택한 엔티티를 생성하거나 업데이트합니다. 다른 모든 저장소 엔티티는 삭제됩니다.", + "device-credentials-conflict": "외부 ID가 {{entityId}}인 장치를 로드하지 못했습니다.
    동일한 자격 증명이 이미 다른 장치에 대해 데이터베이스에 있기 때문입니다.
    복원 양식에서 자격 증명 로드 설정을 비활성화하는 것을 고려하세요.", + "missing-referenced-entity": "외부 ID가 {{sourceEntityId}}인 {{sourceEntityTypeName}}을(를) 로드하지 못했습니다.
    ID가 {{targetEntityId}}인 누락된 {{targetEntityTypeName}}을(를) 참조하기 때문입니다.", + "runtime-failed": "실패: {{message}}", + "auto-commit-settings-read-only-hint": "자동 커밋 기능은 저장소 설정에서 읽기 전용 옵션이 활성화된 경우 작동하지 않습니다.", + "rollback-on-error": "오류 발생 시 롤백", + "rollback-on-error-hint": "복원할 엔티티가 많은 경우 이 옵션을 비활성화하여 성능을 향상시키는 것을 고려하세요.\n 참고: 버전 로드 중에 오류가 발생하면 이미 저장된 엔티티(관계, 속성 등 포함)가 그대로 유지됩니다." + }, "widget": { "widget-library": "위젯 라이브러리", "widget-bundle": "위젯 번들", + "all-bundles": "모든 번들", "select-widgets-bundle": "위젯 번들 선택", + "widgets": "위젯", + "all-widgets": "모든 위젯", + "widget": "위젯", + "select-widget": "위젯 선택", + "no-widgets-matching": "'{{entity}}'과(와) 일치하는 위젯이 없습니다.", + "no-widgets": "아직 위젯이 없음", + "no-widgets-text": "위젯을 찾을 수 없음", "management": "위젯 관리", "editor": "위젯 편집기", + "confirm-to-exit-editor-html": "저장되지 않은 위젯 설정이 있습니다.
    이 페이지를 떠나시겠습니까?", "widget-type-not-found": "위젯 구성을 로드하는 중 문제가 발생했습니다.
    연결된 위젯 타입이 삭제되었습니다.", "widget-type-load-error": "다음과 같은 오류로 인해 위젯을 불러오지 못했습니다:", "remove": "위젯 삭제", + "delete": "위젯 삭제", "edit": "위젯 수정", "remove-widget-title": "'{{widgetTitle}}' 위젯을 삭제하시겠습니까?", "remove-widget-text": "위젯 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "replace-reference-with-widget-copy": "참조를 위젯 복사본으로 교체", "timeseries": "시계열", "search-data": "데이터 검색", "no-data-found": "아무 데이터도 없습니다", @@ -2126,59 +7332,202 @@ "rpc": "컨트롤 위젯", "alarm": "알람 위젯", "static": "상태 위젯", + "timeseries-short": "시리즈", + "latest-short": "최신", + "rpc-short": "제어", + "alarm-short": "알람", + "static-short": "정적", "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": "새로운 위젯 이름과 위젯 번들을 선택하세요.", + "move": "위젯 이동", + "save-widget-as": "다른 이름으로 위젯 저장", + "save-widget-as-text": "새 위젯 제목을 입력하세요", "toggle-fullscreen": "전체화면 전환", "run": "위젯 실행", + "widget-title": "위젯 제목", "title": "위젯 제목", "title-required": "위젯 제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "system": "시스템", "type": "위젯 타입", "resources": "리소스", - "resource-url": "JavaScript/CSS URI", + "resource-url": "JavaScript/CSS URL", + "resource-is-extension": "확장 프로그램임", "remove-resource": "리소스 삭제", "add-resource": "리소스 추가", "html": "HTML", - "tidy": "Tidy", + "tidy": "깔끔한", "css": "CSS", - "settings-schema": "스키마 설정", - "datakey-settings-schema": "데이터 키 설정 스키마", - "javascript": "Javascript", + "settings-form": "설정 폼", + "data-key-settings-form": "데이터 키 설정 폼", + "latest-data-key-settings-form": "최신 데이터 키 설정 폼", + "widget-settings": "위젯 설정", + "description": "설명", + "tags": "태그", + "image-preview": "이미지 미리보기", + "settings-form-selector": "설정 폼 선택기", + "data-key-settings-form-selector": "데이터 키 설정 폼 선택기", + "latest-data-key-settings-form-selector": "최신 데이터 키 설정 폼 선택기", + "all": "모두", + "actual": "실제", + "scada": "SCADA 심볼", + "deprecated": "사용 중단됨", + "has-basic-mode": "기본 모드 있음", + "basic-mode-form-selector": "기본 모드 폼 선택기", + "basic-mode": "기본", + "advanced-mode": "고급", + "javascript": "JavaScript", "js": "JS", - "add-widget-type": "새로운 위젯 타입 추가", + "delete-widget-title": "위젯 '{{widgetName}}'을(를) 삭제하시겠습니까?", + "delete-widget-text": "위젯 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "delete-widgets-title": "{ count, plural, =1 {1개 위젯} other {#개 위젯} }을(를) 삭제하시겠습니까?", + "delete-widgets-text": "주의: 확인 후 선택한 모든 위젯이 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-widget": "위젯 삭제", "widget-template-load-failed-error": "위젯 템플릿을 로드하지 못했습니다!", + "details": "상세 내역", + "widget-details": "위젯 세부 정보", "add": "위젯 추가", + "add-existing-widget": "기존 위젯 추가", + "add-new-widget": "위젯 추가", + "search-widgets": "위젯 검색", + "selected-widgets": "{ count, plural, =1 {1개 위젯} other {#개 위젯} } 선택됨", "undo": "위젯 변경사항 취소", "export": "위젯 내보내기", - "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" + "export-prompt": "위젯 이미지 및 리소스 포함", + "export-widgets": "위젯 내보내기", + "export-widgets-prompt": "위젯 이미지 및 리소스 포함", + "import": "위젯 가져오기", + "no-data": "위젯에 표시할 데이터 없음", + "data-overflow": "위젯이 {{total}}개 엔티티 중 {{count}}개를 표시합니다", + "alarm-data-overflow": "위젯이 {{totalEntities}}개 엔티티 중 {{allowedEntities}}개(최대 허용)에 대한 알람을 표시합니다", + "search": "위젯 검색", + "filter": "위젯 필터 유형", + "loading-widgets": "위젯 로드 중...", + "widget-template-error": "잘못된 위젯 HTML 템플릿입니다.", + "reference": "참조" }, "widget-action": { "header-button": "위젯 헤더 버튼", + "do-nothing": "아무것도 하지 않음", "open-dashboard-state": "새로운 대시보드 상태 탐색", "update-dashboard-state": "현재 대시보드 상태 업데이트", "open-dashboard": "다른 대시보드 탐색", - "custom": "Custom action", + "custom": "사용자 지정 작업", "custom-pretty": "Custom action (HTML 템플릿)", + "custom-pretty-error-title": "사용자 지정 대화 상자 오류", + "custom-pretty-template-error": "잘못된 사용자 지정 대화 상자 템플릿입니다.", + "custom-pretty-controller-error": "사용자 지정 대화 상자 함수를 평가하는 동안 오류가 발생했습니다.", + "mobile-action": "모바일 작업", "target-dashboard-state": "대상 대시보드 상태", "target-dashboard-state-required": "대상 대시보드 상태가 필요합니다.", "set-entity-from-widget": "위젯으로 부터 객체 설정", "target-dashboard": "대상 대시보드", - "open-right-layout": "Open right dashboard layout (모바일 보기)" + "select-target-dashboard": "대상 대시보드 선택", + "target-dashboard-required": "대상 대시보드가 필요합니다.", + "open-right-layout": "Open right dashboard layout (모바일 보기)", + "state-display-type": "대시보드 상태 표시 옵션", + "open-normal": "정상", + "open-in-separate-dialog": "별도의 대화 상자에서 열기", + "open-in-popover": "팝오버에서 열기", + "dialog-title": "대화 상자 제목", + "dialog-hide-dashboard-toolbar": "대화 상자에서 대시보드 도구 모음 숨기기", + "dialog-width": "뷰포트 너비에 대한 대화 상자 너비 비율", + "dialog-height": "뷰포트 높이에 대한 대화 상자 높이 비율", + "dialog-size-range-error": "대화 상자 크기 비율 값은 1에서 100 사이여야 합니다.", + "popover-preferred-placement": "선호하는 팝오버 배치", + "popover-placement-top": "상단", + "popover-placement-topLeft": "왼쪽 상단", + "popover-placement-topRight": "오른쪽 상단", + "popover-placement-right": "오른쪽", + "popover-placement-rightTop": "오른쪽 상단", + "popover-placement-rightBottom": "오른쪽 하단", + "popover-placement-bottom": "하단", + "popover-placement-bottomLeft": "왼쪽 하단", + "popover-placement-bottomRight": "오른쪽 하단", + "popover-placement-left": "왼쪽", + "popover-placement-leftTop": "왼쪽 상단", + "popover-placement-leftBottom": "왼쪽 하단", + "popover-hide-on-click-outside": "외부 클릭 시 팝오버 숨기기", + "popover-hide-dashboard-toolbar": "팝오버에서 대시보드 도구 모음 숨기기", + "popover-width": "팝오버 너비", + "popover-height": "팝오버 높이", + "popover-style": "팝오버 스타일", + "open-new-browser-tab": "새 브라우저 탭에서 열기", + "open-URL": "URL 열기", + "URL": "URL", + "url-required": "URL을(를) 입력하세요.", + "mobile": { + "device-provision": "장치 프로비저닝", + "action-type": "모바일 작업 유형", + "select-action-type": "모바일 작업 유형 선택", + "action-type-required": "모바일 작업 유형이 필요합니다", + "take-picture-from-gallery": "갤러리에서 사진 가져오기", + "take-photo": "사진 촬영", + "map-direction": "지도 경로 열기", + "map-location": "지도 위치 열기", + "scan-qr-code": "QR 코드 스캔", + "make-phone-call": "전화 걸기", + "get-location": "휴대전화 위치 가져오기", + "take-screenshot": "스크린샷 촬영", + "handle-provision-success-function": "프로비저닝 성공 처리 함수", + "get-location-function": "위치 가져오기 함수", + "process-launch-result-function": "실행 결과 처리 함수", + "get-phone-number-function": "전화번호 가져오기 함수", + "process-image-function": "이미지 처리 함수", + "process-qr-code-function": "QR 코드 처리 함수", + "process-location-function": "위치 처리 함수", + "handle-empty-result-function": "빈 결과 처리 함수", + "handle-error-function": "오류 처리 함수", + "handle-non-mobile-fallback-function": "비모바일 폴백 처리 함수", + "save-to-gallery": "갤러리에 저장", + "provision-type": "프로비저닝 유형", + "auto": "자동", + "wi-fi": "와이파이", + "ble": "BLE", + "soft-ap": "소프트 AP" + }, + "custom-action-function": "사용자 지정 작업 함수", + "custom-pretty-function": "사용자 지정 작업 (HTML 템플릿 포함) 함수", + "map-item-type": "지도 항목 유형", + "map-item": { + "marker": "마커", + "polygon": "다각형", + "rectangle": "사각형", + "circle": "원", + "polyline": "폴리라인" + }, + "place-map-item": "지도 항목 배치", + "map-item-tooltip": { + "customize-map-item-tooltips": "지도 항목 툴팁 사용자 지정", + "place-marker": "마커 배치", + "start-draw-rectangle": "사각형 그리기 시작", + "finish-draw-rectangle": "사각형 그리기 완료", + "start-draw-polygon": "다각형 그리기 시작", + "continue-draw-polygon": "다각형 그리기 계속", + "finish-draw-polygon": "다각형 그리기 완료", + "start-draw-circle": "원 그리기 시작", + "finish-draw-circle": "원 그리기 완료", + "start-draw-polyline": "폴리라인 그리기 시작", + "finish-draw-polyline": "폴리라인 그리기 완료" + } }, "widgets-bundle": { "current": "현재 번들", "widgets-bundles": "위젯 번들", + "widgets-bundle-widgets": "위젯 번들 위젯", "add": "위젯 번들 추가", "delete": "위젯 번들 삭제", "title": "제목", "title-required": "제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "description": "설명", + "image-preview": "이미지 미리보기", + "scada": "SCADA 위젯 번들", + "order": "순서", "add-widgets-bundle-text": "위젯 번들 추가", "no-widgets-bundles-text": "위젯 번들이 없습니다.", "empty": "위젯 번들이 비어있습니다.", @@ -2194,41 +7543,63 @@ "system": "시스템", "import": "위젯 번들 가져오기", "export": "위젯 번들 내보내기", + "export-widgets-bundle-widgets-prompt": "내보낸 데이터에 번들 위젯 포함 (그렇지 않으면 참조된 위젯 FQN만 내보내짐)", "export-failed-error": "위젯 번들을 내보내기 할 수 없습니다.: {{error}}", "create-new-widgets-bundle": "새로운 위젯 번들 생성", "widgets-bundle-file": "위젯 번들 파일", "invalid-widgets-bundle-file-error": "위젯 번들을 가져오기 할 수 없습니다.: 잘못된 위젯 번들 데이터 구조입니다.", "search": "위젯 번들 검색", "selected-widgets-bundles": "{ count, plural, =1 {1 개 위젯 번들} other {# 개 위젯 번들} } 선택됨", - "open-widgets-bundle": "위젯 번들 열기" + "open-widgets-bundle": "위젯 번들 열기", + "loading-widgets-bundles": "위젯 번들 로드 중...", + "create-new": "새 위젯 번들 만들기" }, "widget-config": { "data": "데이터", "settings": "설정", "advanced": "고급", + "appearance": "외관", + "widget-card": "위젯 카드", + "mobile": "모바일", "title": "제목", "title-tooltip": "제목 툴팁", "general-settings": "일반 설정", "display-title": "제목 표시", + "card-title": "카드 제목", "drop-shadow": "그림자", "enable-fullscreen": "전체화면 사용 ", "background-color": "배경 색", "text-color": "글자 색", + "border-radius": "테두리 반경", "padding": "패딩", "margin": "여백", "widget-style": "위젯 스타일", + "widget-css": "위젯 CSS", "title-style": "제목 스타일", "mobile-mode-settings": "모바일 모드 설정", "order": "순서", "height": "높이", + "mobile-hide": "모바일 모드에서 위젯 숨기기", + "desktop-hide": "데스크톱 모드에서 위젯 숨기기", "units": "값 옆에 표시할 특수 기호", + "units-by-default": "기본 단위", "decimals": "소수점 이하 자릿수", + "decimals-by-default": "기본 소수 자릿수", + "default-data-key-parameter-hint": "이 매개변수는 데이터 키 구성에 의해 재정의되지 않는 한 모든 위젯 값에 적용됩니다", + "units-short": "단위", + "decimals-short": "소수 자릿수", + "decimals-suffix": "소수 자릿수", + "digits-suffix": "자릿수", "timewindow": "타임윈도우", "use-dashboard-timewindow": "대시보드 타임윈도우", + "use-widget-timewindow": "위젯 시간 창 사용", "display-timewindow": "타임윈도우 표시", + "legend": "범례", "display-legend": "범례 표시", "datasources": "데이터 소스", - "maximum-datasources": "최대 { count, plural, =1 {1 개의 데이터 소스만 허용됩니다.} other {# 개의 데이터 소스만 허용됩니다.} }", + "datasource": "데이터소스", + "maximum-datasources": "최대 { count, plural, =1 {1 개의 데이터 소스만 허용됩니다.} other {# 개의 데이터 소스만 허용됩니다.} }", + "timeseries-key-error": "최소 하나의 시계열 데이터 키를 지정해야 합니다", "datasource-type": "유형", "datasource-parameters": "파라미터", "remove-datasource": "데이터소스 삭제", @@ -2241,31 +7612,548 @@ "search-actions": "액션 검색", "no-actions-text": "아무 액션도 없습니다", "action-source": "액션 소스", + "select-action-source": "작업 소스 선택", "action-source-required": "액션 소스를 입력하세요.", + "column-index": "열 인덱스", + "select-column-index": "열 인덱스 선택", + "column-index-required": "열 인덱스가 필요합니다.", + "not-set": "설정되지 않음", "action-name": "이름", "action-name-required": "액션 이름을 입력하세요.", "action-name-not-unique": "같은 이름의 액션이 이미 존재합니다.\n같은 액션 소스에서 액션 이름이 중복될 수 없습니다.", "action-icon": "아이콘", + "header-button": { + "button-settings": "버튼 설정", + "button-type": "버튼 유형", + "button-type-basic": "기본", + "button-type-raised": "양각", + "button-type-stroked": "윤곽선", + "button-type-flat": "평면", + "button-type-icon": "아이콘", + "button-type-mini-fab": "플로팅 버튼", + "colors": "색상", + "color": "색상", + "background": "배경", + "border": "테두리", + "advanced-button-style": "고급 버튼 스타일", + "button-style": "버튼 스타일" + }, + "show-hide-action-using-function": "함수를 사용하여 작업 표시/숨기기", + "show-action-function": "작업 표시 함수", "action-type": "유형", "action-type-required": "액션 유형을 입력하세요.", "edit-action": "액션 편집", "delete-action": "액션 삭제", "delete-action-title": "위젯 액션 삭제", "delete-action-text": "위젯 액션 '{{actionName}}'을(를) 삭제하시겠습니까?", + "title-icon": "제목 아이콘", "display-icon": "제목 아이콘 표시", + "card-icon": "카드 아이콘", + "icon": "아이콘", "icon-color": "아이콘 색상", - "icon-size": "아이콘 크기" + "icon-size": "아이콘 크기", + "advanced-settings": "고급 설정", + "data-settings": "데이터 설정", + "limits": "제한", + "no-data-display-message": "\"표시할 데이터 없음\" 대체 메시지", + "data-page-size": "데이터 소스당 최대 엔티티 수", + "settings-component-not-found": "선택기 '{{selector}}'에 대한 설정 폼 구성 요소를 찾을 수 없습니다", + "preview": "미리보기", + "set": "설정", + "set-message": "메시지 설정", + "advanced-title-style": "고급 제목 스타일", + "card-style": "카드 스타일", + "text": "텍스트", + "background": "배경", + "advanced-widget-style": "고급 위젯 스타일", + "card-buttons": "카드 버튼", + "show-card-buttons": "카드 버튼 표시", + "card-border-radius": "카드 테두리 반경", + "card-padding": "카드 패딩", + "card-appearance": "카드 외관", + "color": "색상", + "tooltip": "툴팁", + "units-required": "단위는 필수 항목입니다.", + "list-layout": "목록 레이아웃", + "layout": "레이아웃", + "resize-options": "크기 조정 옵션", + "resizable": "크기 조정 가능", + "preserve-aspect-ratio": "가로세로 비율 유지" }, "widget-type": { "import": "위젯 타입 가져오기", "export": "위젯 타입 내보내기", "export-failed-error": "위젯 타입을 내보내기 할 수 없습니다.: {{error}}", - "create-new-widget-type": "새로운 위젯 타입 생성", - "widget-type-file": "위젯 타입 파일", - "invalid-widget-type-file-error": "위젯 타입을 가져오기 할 수 없습니다.: 잘못된 위젯 타입 데이터 구조입니다." + "widget-file": "위젯 파일", + "invalid-widget-file-error": "위젯 가져오기를 할 수 없습니다: 위젯 데이터 구조가 잘못되었습니다." }, "widgets": { + "mobile-app-qr-code": { + "configuration-hint": "구성은 플랫폼 기본 설정의 모바일 앱 QR 코드 위젯에 따라 달라집니다", + "get-it-on-google-play": "Google Play에서 받기", + "download-on-the-app-store": "App Store에서 다운로드" + }, + "action-button": { + "behavior": "동작", + "on-click": "클릭 시", + "on-click-hint": "버튼을 클릭할 때 트리거되는 동작", + "first-button-click": "첫 번째 버튼 클릭", + "first-button-click-hint": "첫 번째 버튼을 누르는 동안 동작.", + "second-button-click": "두 번째 버튼 클릭", + "second-button-click-hint": "두 번째 버튼을 누르는 동안 동작.", + "button-click-hint": "위젯을 누르는 동안 동작." + }, + "command-button": { + "behavior": "동작", + "on-click": "클릭 시", + "on-click-hint": "버튼을 클릭할 때 수행되는 동작." + }, + "power-button": { + "behavior": "동작", + "power-on": "전원 '켜기'", + "power-on-hint": "구성 요소의 전원을 켜기 위해 수행되는 동작.", + "power-off": "전원 '끄기'", + "power-off-hint": "구성 요소의 전원을 끄기 위해 수행되는 동작.", + "on-label": "켜기", + "off-label": "끄기", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-simplified": "간소화", + "layout-outlined": "윤곽선", + "layout-default-volume": "기본.볼륨", + "layout-simplified-volume": "간소화.볼륨", + "layout-outlined-volume": "윤곽선.볼륨", + "layout-default-icon": "기본.아이콘", + "layout-simplified-icon": "간소화.아이콘", + "layout-outlined-icon": "윤곽선.아이콘", + "main": "메인", + "background": "배경", + "button-icon-on": "버튼 아이콘 '켜기'", + "button-icon-off": "버튼 아이콘 '끄기'", + "power-on-colors": "전원 '켜기' 색상", + "power-off-colors": "전원 '끄기' 색상", + "disabled-colors": "비활성화 색상", + "button": "버튼" + }, + "toggle-button": { + "behavior": "동작", + "checked": "선택됨", + "unchecked": "선택 해제됨", + "check": "선택", + "check-hint": "구성 요소를 선택하기 위해 수행되는 동작.", + "uncheck": "선택 해제", + "uncheck-hint": "구성 요소를 선택 해제하기 위해 수행되는 동작.", + "auto-scale": "자동 크기 조정", + "horizontal-fill": "가로 채우기", + "vertical-fill": "세로 채우기", + "button-appearance": "버튼 외관" + }, + "segmented-button": { + "layout": "레이아웃", + "layout-squared": "사각형", + "layout-rounded": "둥근", + "card-border": "카드 테두리", + "button-appearance": "버튼 외관", + "first": "첫 번째", + "second": "초", + "color-styles": "색상 스타일", + "selected": "선택됨", + "unselected": "선택되지 않음" + }, + "button": { + "layout": "레이아웃", + "outlined": "윤곽선", + "filled": "채워짐", + "underlined": "밑줄", + "basic": "기본", + "auto-scale": "자동 크기 조정", + "label": "라벨", + "icon": "아이콘", + "border-radius": "테두리 반경", + "color-palette": "색상 팔레트", + "main": "메인", + "background": "배경", + "border": "테두리", + "custom-styles": "사용자 지정 스타일", + "clear-style": "스타일 지우기", + "shadow": "그림자", + "enabled": "사용", + "disabled": "비활성화됨", + "preview": "미리보기", + "copy-style-from": "스타일 복사 원본" + }, + "value-stepper": { + "behavior": "동작", + "simplified": "간소화", + "filled": "채워짐", + "outlined": "윤곽선", + "volume": "부피", + "initial-state": "초기 상태", + "initial-state-hint": "초기 값을 가져오는 동작.", + "disabled-state": "비활성화 상태", + "disabled-state-hint": "구성 요소가 비활성화되는 조건을 구성합니다.", + "right-button-click": "오른쪽 버튼 클릭", + "right-button-click-hint": "오른쪽 버튼을 누르는 동안 동작.", + "left-button-click": "왼쪽 버튼 클릭", + "left-button-click-hint": "왼쪽 버튼을 누르는 동안 동작.", + "auto-scale": "자동 크기 조정", + "value-range": "범위", + "min-range": "최소", + "max-range": "최대", + "value-increment-decrement-step": "값 증가/감소 단계", + "value": "값", + "value-box-background": "값 상자 배경", + "border": "테두리", + "button-appearance": "버튼 외관", + "left": "왼쪽", + "right": "오른쪽", + "left-button": "왼쪽 버튼", + "right-button": "오른쪽 버튼", + "icon": "아이콘", + "color-palette": "색상 팔레트", + "main": "메인", + "background": "배경", + "button-icon-on": "버튼 아이콘 '켜기'", + "button-on-colors": "전원 '켜기' 색상", + "disabled-colors": "비활성화 색상" + }, + "button-state": { + "activated-state": "활성화 상태", + "activated-state-hint": "버튼이 활성화되는 조건을 구성합니다.", + "disabled-state": "비활성화 상태", + "disabled-state-hint": "버튼이 비활성화되는 조건을 구성합니다.", + "selected-state": "선택 버튼", + "selected-state-hint": "버튼이 선택되는 조건을 구성합니다.", + "enabled": "사용", + "hovered": "호버됨", + "pressed": "눌림", + "activated": "활성", + "disabled": "비활성화됨", + "initial": "첫 번째 버튼", + "first": "첫 번째", + "second": "초" + }, + "background": { + "background": "배경", + "background-settings": "배경 설정", + "background-type-image": "이미지", + "background-type-color": "색상", + "image-url": "이미지 URL", + "overlay": "오버레이", + "enable-overlay": "오버레이 활성화", + "blur": "흐림", + "preview": "미리보기" + }, + "bar-chart": { + "bar-appearance": "막대 외관", + "label-on-bar": "막대에 레이블", + "value-on-bar": "막대에 값", + "bar-chart-style": "막대 차트 스타일", + "bar-axis": "막대 축" + }, + "polar-area-chart": { + "polar-axis": "극축", + "start-angle": "시작 각도", + "polar-area-chart-style": "극 영역 차트 스타일" + }, + "battery-level": { + "layout": "레이아웃", + "layout-vertical-solid": "수직. 실선", + "layout-horizontal-solid": "수평. 실선", + "layout-vertical-divided": "수직. 분할", + "layout-horizontal-divided": "수평. 분할", + "icon": "아이콘", + "value": "값", + "auto-scale": "자동 크기 조정", + "battery-level-color": "배터리 레벨 색상", + "battery-shape-color": "배터리 모양 색상", + "battery-level-card-style": "배터리 레벨 카드 스타일", + "sections-count": "섹션 수" + }, + "signal-strength": { + "value": "값", + "last-update": "마지막 업데이트", + "no-signal": "신호 없음", + "layout": "레이아웃", + "layout-wifi": "와이파이", + "layout-cellular-bar": "셀룰러 막대", + "icon": "아이콘", + "date": "날짜", + "active-bars-color": "활성 신호 막대 색상", + "inactive-bars-color": "비활성 신호 막대 색상", + "signal-strength-card-style": "신호 강도 카드 스타일", + "no-signal-rssi-value": "\"신호 없음\" RSSI 값" + }, + "status-widget": { + "behavior": "동작", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-center": "중앙", + "layout-icon": "아이콘", + "on": "켜기", + "off": "끄기", + "label": "라벨", + "status": "상태", + "icon": "아이콘", + "color-palette": "색상 팔레트", + "disabled-color-palette": "비활성화 색상 팔레트", + "primary": "기본", + "primary-color-hint": "아이콘 및 레이블 색상", + "secondary": "보조", + "secondary-color-hint": "상태 색상", + "background": "배경" + }, + "chart": { + "common-settings": "일반 설정", + "enable-stacking-mode": "스택 모드 활성화", + "selection": "시간 범위 선택", + "enable-selection-mode": "선택 모드 활성화", + "line-shadow-size": "선 그림자 크기", + "display-smooth-lines": "부드러운 (곡선) 선 표시", + "default-bar-width": "비집계 데이터의 기본 막대 너비 (밀리초)", + "bar-alignment": "막대 정렬", + "bar-alignment-left": "왼쪽", + "bar-alignment-right": "오른쪽", + "bar-alignment-center": "중앙", + "default-font": "기본 글꼴", + "default-font-size": "기본 글꼴 크기", + "default-font-color": "기본 글꼴 색상", + "thresholds-line-width": "모든 임계값의 기본 선 너비", + "tooltip-settings": "툴팁 설정", + "tooltip": "툴팁", + "show-tooltip": "툴팁 표시", + "hover-individual-points": "개별 지점 호버", + "show-cumulative-values": "스택 모드에서 누적 값 표시", + "hide-zero-false-values": "툴팁에서 0/false 값 숨기기", + "tooltip-value-format-function": "툴팁 값 형식 함수", + "grid-settings": "그리드 설정", + "show-vertical-lines": "수직선 표시", + "show-horizontal-lines": "수평선 표시", + "grid-outline-border-width": "그리드 윤곽선/테두리 너비 (px)", + "primary-color": "기본 색상", + "background-color": "배경색", + "ticks-color": "눈금 색상", + "xaxis-settings": "X축 설정", + "axis-title": "축 제목", + "xaxis-tick-labels-settings": "X축 눈금 레이블 설정", + "show-tick-labels": "축 눈금 레이블 표시", + "yaxis-settings": "Y축 설정", + "min-scale-value": "눈금의 최소 값", + "max-scale-value": "눈금의 최대 값", + "yaxis-tick-labels-settings": "Y축 눈금 레이블 설정", + "tick-step-size": "눈금 사이의 단계 크기", + "number-of-decimals": "표시할 소수 자릿수", + "ticks-formatter-function": "눈금 형식 함수", + "comparison-settings": "비교 설정", + "enable-comparison": "비교 활성화", + "time-for-comparison": "비교 기간", + "time-for-comparison-previous-interval": "이전 간격 (기본값)", + "time-for-comparison-days": "1일 전", + "time-for-comparison-weeks": "1주 전", + "time-for-comparison-months": "1개월 전", + "time-for-comparison-years": "1년 전", + "time-for-comparison-custom-interval": "사용자 지정 간격", + "custom-interval-value": "사용자 지정 간격 값 (ms)", + "comparison-x-axis-settings": "비교 X축 설정", + "axis-position": "축 위치", + "axis-position-top": "위 (기본값)", + "axis-position-bottom": "하단", + "custom-legend-settings": "사용자 지정 범례 설정", + "enable-custom-legend": "사용자 지정 범례 활성화 (키 레이블에 속성/시계열 값을 사용할 수 있음)", + "key-name": "키 이름", + "key-name-required": "키 이름은 필수 항목입니다.", + "key-type": "키 유형", + "key-type-attribute": "속성", + "key-type-timeseries": "시계열", + "label-keys-list": "레이블에 사용할 키 목록", + "no-label-keys": "구성된 키 없음", + "add-label-key": "새 키 추가", + "line-width": "선 너비", + "color": "색상", + "data-is-hidden-by-default": "데이터가 기본적으로 숨겨짐", + "disable-data-hiding": "데이터 숨기기 비활성화", + "remove-from-legend": "범례에서 데이터 키 제거", + "exclude-from-stacking": "스택에서 제외 (\"스택\" 모드에서 사용 가능)", + "line-settings": "선 설정", + "show-line": "선 표시", + "fill-line": "선 채우기", + "fill-line-opacity": "채우기 불투명도", + "points-settings": "포인트 설정", + "show-points": "지점 표시", + "points-line-width": "포인트 선 너비", + "points-radius": "포인트 반지름", + "point-shape": "포인트 모양", + "point-shape-circle": "원", + "point-shape-cross": "십자가", + "point-shape-diamond": "다이아몬드", + "point-shape-square": "사각형", + "point-shape-triangle": "삼각형", + "point-shape-custom": "사용자 지정 함수", + "point-shape-draw-function": "포인트 모양 그리기 함수", + "show-separate-axis": "별도의 축 표시", + "axis-position-left": "왼쪽", + "axis-position-right": "오른쪽", + "thresholds": "임계값", + "no-thresholds": "구성된 임계값 없음", + "add-threshold": "임계값 추가", + "show-values-for-comparison": "비교를 위한 과거 값 표시", + "comparison-values-label": "과거 값 레이블", + "comparison-line-color": "비교 선 색상", + "threshold-settings": "임계값 설정", + "use-as-threshold": "키 값을 임계값으로 사용", + "threshold-line-width": "임계값 선 너비", + "threshold-color": "임계값 색상", + "common-pie-settings": "공통 원형 차트 설정", + "radius": "반경", + "inner-radius": "내부 반지름", + "tilt": "기울기", + "common-pie-settings-range-error": "값은 0에서 1 사이여야 합니다", + "stroke-settings": "획 설정", + "width-pixels": "너비 (픽셀)", + "show-labels": "레이블 표시", + "animation-settings": "애니메이션 설정", + "animated-pie": "원형 차트 애니메이션 활성화 (실험적)", + "border-settings": "테두리 설정", + "border-width": "테두리 너비", + "border-color": "테두리 색상", + "legend-settings": "범례 설정", + "display-legend": "범례 표시", + "labels-font-color": "레이블 글꼴 색상", + "series": "시리즈", + "add-series": "시리즈 추가", + "series-settings": "시리즈 설정", + "remove-series": "시리즈 제거", + "no-series": "구성된 시리즈 없음", + "no-series-error": "최소 하나의 시리즈를 지정해야 합니다", + "chart-appearance": "차트 모양", + "vertical-grid-lines": "수직 격자선", + "horizontal-grid-lines": "수평 격자선", + "chart-background": "차트 배경", + "grid-lines-color": "격자선 색상", + "border": "테두리", + "axis": "축", + "vertical-axis": "수직 축", + "ticks": "눈금", + "horizontal-axis": "수평 축", + "shape-empty-circle": "빈 원", + "shape-circle": "원", + "shape-rect": "사각형", + "shape-round-rect": "둥근 사각형", + "shape-triangle": "삼각형", + "shape-diamond": "다이아몬드", + "shape-pin": "핀", + "shape-arrow": "화살표", + "shape-none": "없음", + "line-type-solid": "실선", + "line-type-dashed": "파선", + "line-type-dotted": "점선", + "label-position-top": "상단", + "label-position-bottom": "하단", + "label-position-outside": "외부", + "label-position-inside": "내부", + "fill": "채우기", + "fill-type-none": "없음", + "fill-type-solid": "실선", + "fill-type-opacity": "불투명도", + "fill-type-gradient": "그라데이션", + "background": "배경", + "opacity": "불투명도", + "gradient-stops": "그라데이션 중지점", + "gradient-start": "시작", + "gradient-end": "종료", + "animation": { + "animation": "애니메이션", + "animation-threshold": "애니메이션 임계값", + "animation-duration": "애니메이션 지속 시간", + "animation-easing": "애니메이션 완화", + "animation-delay": "애니메이션 지연", + "update-animation-duration": "업데이트 애니메이션 지속 시간", + "update-animation-easing": "업데이트 애니메이션 완화", + "update-animation-delay": "업데이트 애니메이션 지연" + }, + "chart-axis": { + "limit": "제한", + "source": "소스", + "key-value": "키 / 값", + "value-required": "값이 필요합니다.", + "entity-key-required": "엔티티 키는 필수 항목입니다.", + "key-required": "키는 필수 항목입니다.", + "scale-limits": "스케일 제한", + "scale-appearance": "스케일 모양", + "scale": "눈금", + "scale-min": "최소", + "scale-max": "최대", + "scale-auto": "자동" + }, + "bar": { + "show-border": "테두리 표시", + "border-width": "테두리 너비", + "border-radius": "테두리 반경", + "bar-width": "막대 너비", + "label": "라벨", + "label-hint": "막대 위에 레이블 표시.", + "series-label-hint": "막대 위에 값이 있는 레이블 표시.", + "label-background": "레이블 배경" + } + }, + "color": { + "color-settings": "색상 설정", + "color-type-constant": "상수", + "color-type-gradient": "그라데이션", + "color-type-range": "범위", + "color-type-function": "함수", + "color": "색상", + "value-range": "값 범위", + "from": "부터", + "to": "까지", + "color-function": "색상 함수", + "copy-color-settings-from": "색상 설정 복사 위치", + "copy-from": "복사 원본", + "settings-type": "설정 유형", + "basic-mode": "기본", + "advanced-mode": "고급", + "entity-alias": "엔티티 별명", + "entity-attribute": "엔티티 속성", + "gradient-color": "그라데이션 색상", + "gradient-color-min": "색상", + "gradient-start": "그라데이션 시작 색상", + "gradient-start-min": "시작", + "gradient-end": "그라데이션 종료 색상", + "gradient-end-min": "끝", + "start-value": "시작 값", + "end-value": "종료 값", + "gradient-type": "그라데이션 유형" + }, + "dashboard-state": { + "dashboard-state-settings": "대시보드 상태 설정", + "dashboard-state": "대시보드 상태 ID", + "autofill-state-layout": "기본적으로 상태 레이아웃 높이 자동 채우기", + "default-margin": "기본 위젯 여백", + "default-background-color": "기본 배경색", + "sync-parent-state-params": "상태 매개변수를 부모 대시보드와 동기화" + }, "date-range-navigator": { + "date-range-picker-settings": "날짜 범위 선택기 설정", + "hide-date-range-picker": "날짜 범위 선택기 숨기기", + "picker-one-panel": "날짜 범위 선택기 단일 패널", + "picker-auto-confirm": "날짜 범위 선택기 자동 확인", + "picker-show-template": "날짜 범위 선택기 템플릿 표시", + "first-day-of-week": "주의 첫 번째 요일", + "interval-settings": "간격 설정", + "hide-interval": "간격 숨기기", + "initial-interval": "초기 간격", + "interval-hour": "시간", + "interval-day": "일", + "interval-week": "주", + "interval-two-weeks": "2주", + "interval-month": "달", + "interval-three-months": "3개월", + "interval-six-months": "6개월", + "step-settings": "단계 설정", + "hide-step-size": "단계 크기 숨기기", + "initial-step-size": "초기 단계 크기", + "hide-labels": "레이블 숨기기", + "use-session-storage": "세션 저장소 사용", "localizationMap": { "Sun": "일", "Mon": "월", @@ -2322,6 +8210,228 @@ "Ok": "확인" } }, + "doughnut": { + "doughnut-appearance": "도넛 모양", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-with-total": "합계 포함", + "central-total-value": "중앙 합계 값", + "doughnut-card-style": "도넛 카드 스타일" + }, + "entities-hierarchy": { + "hierarchy-data-settings": "계층 데이터 설정", + "relations-query-function": "노드 관계 쿼리 함수", + "has-children-function": "노드에 자식 있음 함수", + "node-state-settings": "노드 상태 설정", + "node-opened-function": "기본 노드 열림 함수", + "node-disabled-function": "노드 비활성화 함수", + "display-settings": "표시 설정", + "node-icon-function": "노드 아이콘 함수", + "node-text-function": "노드 텍스트 함수", + "sort-settings": "정렬 설정", + "nodes-sort-function": "노드 정렬 함수" + }, + "edge": { + "display-default-title": "기본 제목 표시" + }, + "gateway": { + "general-settings": "일반 설정", + "widget-title": "위젯 제목", + "default-archive-file-name": "기본 아카이브 파일 이름", + "device-type-for-new-gateway": "새 게이트웨이에 대한 장치 유형", + "messages-settings": "메시지 설정", + "save-config-success-message": "게이트웨이 구성이 성공적으로 저장되었다는 텍스트 메시지", + "device-name-exists-message": "입력한 이름의 장치가 이미 존재할 때 텍스트 메시지", + "gateway-title": "게이트웨이 양식", + "read-only": "읽기 전용", + "events-title": "게이트웨이 이벤트 양식 제목", + "events-filter": "이벤트 필터", + "event-key-contains": "이벤트 키 포함...", + "show-connector": "커넥터에 표시", + "connector-state-param-key": "커넥터 상태 매개변수 키", + "message": "메시지", + "level": "수준", + "created-time": "생성 일시" + }, + "gauge": { + "default-color": "기본 색상", + "radial-gauge-settings": "방사형 게이지 설정", + "ticks-settings": "눈금 설정", + "min-value": "최소값", + "max-value": "최대값", + "min-value-short": "최소", + "max-value-short": "최대", + "start-ticks-angle": "시작 눈금 각도", + "ticks-angle": "눈금 각도", + "major-ticks": "주 눈금", + "major-ticks-count": "주 눈금 수", + "major-ticks-color": "주 눈금 색상", + "minor-ticks": "보조 눈금", + "minor-ticks-count": "보조 눈금 수", + "minor-ticks-color": "보조 눈금 색상", + "tick-numbers-font": "눈금 숫자 글꼴", + "unit-title-settings": "단위 제목 설정", + "show-unit-title": "단위 제목", + "unit-title": "단위 제목", + "title-font": "제목 텍스트 글꼴", + "units-settings": "단위 설정", + "units-font": "단위 텍스트 글꼴", + "value-box-settings": "값 상자 설정", + "show-value-box": "값 상자 표시", + "value-box": "값 상자", + "value-int": "값의 정수 부분 자릿수", + "value-text": "값 텍스트", + "value-text-shadow": "값 텍스트 그림자", + "value-font": "값 텍스트 글꼴", + "rect-stroke-color-start": "사각형 획 색상 - 시작 그라데이션", + "rect-stroke-color-end": "사각형 획 색상 - 종료 그라데이션", + "background-color": "배경색", + "shadow-color": "그림자 색상", + "value-box-rect-stroke-color": "값 상자 사각형 획 색상", + "value-box-rect-stroke-color-end": "값 상자 사각형 획 색상 - 종료 그라데이션", + "value-box-background-color": "값 상자 배경색", + "value-box-shadow-color": "값 상자 그림자 색상", + "plate-settings": "플레이트 설정", + "show-plate-border": "플레이트 테두리", + "plate-color": "플레이트 색상", + "needle-settings": "바늘 설정", + "needle-circle-size": "바늘 원 크기", + "needle-color": "바늘 색상", + "needle-color-start": "바늘 색상 - 시작 그라데이션", + "needle-color-end": "바늘 색상 - 종료 그라데이션", + "needle-color-shadow-up": "바늘 상반부 그림자 색상", + "needle-color-shadow-down": "그림자", + "highlights-settings": "하이라이트 설정", + "highlights-width": "하이라이트 너비", + "highlights": "하이라이트", + "highlight-from": "부터", + "highlight-to": "까지", + "highlight-color": "색상", + "no-highlights": "구성된 하이라이트 없음", + "add-highlight": "하이라이트 추가", + "animation-settings": "애니메이션 설정", + "enable-animation": "애니메이션", + "animation-duration-rule": "애니메이션 지속 시간 및 규칙", + "animation-duration": "애니메이션 지속 시간", + "animation-rule": "애니메이션 규칙", + "animation-linear": "선형", + "animation-quad": "2차", + "animation-quint": "5차", + "animation-cycle": "사이클", + "animation-bounce": "바운스", + "animation-elastic": "탄성", + "animation-dequad": "역2차", + "animation-dequint": "역5차", + "animation-decycle": "역사이클", + "animation-debounce": "역바운스", + "animation-delastic": "역탄성", + "linear-gauge-settings": "선형 게이지 설정", + "bar-stroke": "막대 획", + "bar-stroke-width": "막대 획 너비", + "bar-stroke-color": "막대 획 색상", + "bar-background-color": "막대 배경색 - 시작 그라데이션", + "bar-background-color-end": "막대 배경색 - 끝 그라데이션", + "progress-bar-color": "진행 막대 색상", + "progress-bar": "진행 막대", + "progress-bar-color-start": "진행 막대 색상 - 시작 그라데이션", + "progress-bar-color-end": "진행 막대 색상 - 끝 그라데이션", + "major-ticks-names": "주요 눈금 이름", + "show-stroke-ticks": "눈금 획 표시", + "major-ticks-font": "주요 눈금 글꼴", + "border-color": "테두리 색상", + "border-width": "테두리 너비", + "needle-circle": "바늘 원", + "needle-circle-color": "바늘 원 색상", + "animation-target": "애니메이션 대상", + "animation-target-needle": "바늘", + "animation-target-plate": "플레이트", + "common-settings": "일반 게이지 설정", + "gauge-type": "게이지 유형", + "gauge-type-arc": "호", + "gauge-type-donut": "도넛", + "gauge-type-horizontal-bar": "수평 막대", + "gauge-type-vertical-bar": "수직 막대", + "donut-start-angle": "시작 각도 (도)", + "bar-settings": "게이지 막대 설정", + "relative-bar-width": "상대적 막대 너비", + "neon-glow-brightness": "네온 글로우 효과 밝기 (0-100)", + "neon-glow-brightness-hint": "0 - 효과 비활성화", + "stripes-thickness": "줄무늬 두께", + "stripes-thickness-hint": "0 - 줄무늬 없음", + "rounded-line-cap": "둥근 선 끝", + "bar-color-settings": "막대 색상 설정", + "use-precise-level-color-values": "정밀 색상 레벨 사용", + "bar-colors": "막대 색상, 아래에서 위로", + "color": "색상", + "no-bar-colors": "구성된 막대 색상 없음", + "add-bar-color": "막대 색상 추가", + "from": "부터", + "to": "까지", + "fixed-level-colors": "경계 값을 사용한 막대 색상", + "gauge-title-settings": "게이지 제목 설정", + "show-gauge-title": "게이지 제목 표시", + "gauge-title": "게이지 제목", + "gauge-title-font": "게이지 제목 글꼴", + "unit-title-and-timestamp-settings": "단위 제목 및 타임스탬프 설정", + "show-timestamp": "타임스탬프", + "timestamp-format": "타임스탬프 형식", + "label-font": "값 아래에 표시되는 레이블 글꼴", + "value-settings": "값 설정", + "show-value": "값 텍스트 표시", + "min-max-settings": "최소/최대 레이블 설정", + "show-min-max": "최소 및 최대 값 표시", + "min-max-font": "최소 및 최대 레이블 글꼴", + "show-ticks": "눈금 표시", + "tick-width": "눈금 너비", + "tick-color": "눈금 색상", + "tick-values": "눈금 값", + "no-tick-values": "구성된 눈금 값 없음", + "add-tick-value": "눈금 값 추가", + "gauge-appearance": "게이지 외관", + "units-title": "단위 제목", + "value": "값", + "ticks": "눈금", + "arrow-and-scale-color": "화살표 및 눈금 기본 색상", + "scale-settings": "눈금 설정", + "scale": "눈금", + "scale-color": "눈금 색상", + "compass-appearance": "나침반 외관", + "label": "라벨", + "labels": "레이블", + "label-style": "레이블 스타일", + "simple-gauge-type": "유형", + "gauge-bar-background": "게이지 막대 배경", + "bar-color": "막대 색상", + "min-and-max-value": "최소 및 최대 값", + "min-and-max-label": "최소 및 최대 레이블", + "font": "글꼴", + "tick-width-and-color": "눈금 너비 및 색상", + "min-max-validation-text": "최대 값은 최소 값보다 커야 합니다." + }, + "gpio": { + "pin": "핀", + "label": "라벨", + "row": "행", + "column": "열", + "color": "색상", + "panel-settings": "패널 설정", + "background-color": "배경색", + "gpio-switches": "GPIO 스위치", + "no-gpio-switches": "구성된 GPIO 스위치 없음", + "add-gpio-switch": "GPIO 스위치 추가", + "gpio-status-request": "GPIO 상태 요청", + "method-name": "메서드 이름", + "method-body": "메서드 본문", + "gpio-status-change-request": "GPIO 상태 변경 요청", + "parse-gpio-status-function": "GPIO 상태 파싱 함수", + "gpio-leds": "GPIO LED", + "no-gpio-leds": "구성된 GPIO LED 없음", + "add-gpio-led": "GPIO LED 추가" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, "input-widgets": { "attribute-not-allowed": "이 위젯에서 속성 파라미터를 사용할 수 없습니다", "blocked-location": "당신의 브라우저에서 위치 정보가 차단되었습니다", @@ -2333,20 +8443,20 @@ "device-name": "장치 이름", "device-name-required": "장치 이름을 입력하세요", "discard-changes": "변경 취소", - "entity-attribute-required": "개체 소성을 입력하세요", + "entity-attribute-required": "엔티티 소성을 입력하세요", "entity-coordinate-required": "위도와 경도를 입력하세요", - "entity-timeseries-required": "개체 시계열을 입력하세요", + "entity-timeseries-required": "엔티티 시계열을 입력하세요", "get-location": "현재 위치 얻기", "invalid-date": "잘못된 날짜", "latitude": "위도", "longitude": "경도", "min-value-error": "최솟값은 {{value}}입니다", "max-value-error": "최댓값은{{value}}입니다", - "not-allowed-entity": "선택된 개체는 속성을 공유할 수 없습니다", + "not-allowed-entity": "선택된 엔티티는 속성을 공유할 수 없습니다", "no-attribute-selected": "아무 속성도 선택되지 않았습니다", "no-datakey-selected": "아무 데이터 키도 선택되지 않았습니다", "no-coordinate-specified": "위도/경도를 위한 데이터가 특정되지 않았습니다", - "no-entity-selected": "아무 개체도 선택되지 않았습니다", + "no-entity-selected": "아무 엔티티도 선택되지 않았습니다", "no-image": "이미지 없음", "no-support-geolocation": "당신의 브라우저는 위치정보를 지원하지 않습니다", "no-support-web-camera": "당신의 브라우저는 카메라를 지원하지 않습니다", @@ -2356,9 +8466,9 @@ "no-timeseries-selected": "아무 시계열도 선택되지 않았습니다", "secret-key": "보안키", "secret-key-required": "보안키를 입력하세요", - "switch-attribute-value": "개체 속성 값으로 전환", + "switch-attribute-value": "엔티티 속성 값으로 전환", "switch-camera": "카메라 전환", - "switch-timeseries-value": "개체 시계열 값으로 전환", + "switch-timeseries-value": "엔티티 시계열 값으로 전환", "take-photo": "사진 촬영", "time": "시간", "timeseries-not-allowed": "이 위젯에서 시계열 파라미터를 사용할 수 없습니다", @@ -2366,29 +8476,1739 @@ "update-successful": "업데이트 성공", "update-attribute": "속성 업데이트", "update-timeseries": "시계열 업데이트", - "value": "값" + "value": "값", + "general-settings": "일반 설정", + "widget-title": "위젯 제목", + "claim-button-label": "등록 버튼 레이블", + "show-secret-key-field": "'비밀 키' 입력 필드 표시", + "labels-settings": "레이블 설정", + "show-labels": "레이블 표시", + "device-name-label": "장치 이름 입력 필드 레이블", + "secret-key-label": "비밀 키 입력 필드 레이블", + "messages-settings": "메시지 설정", + "claim-device-success-message": "장치 등록 성공 메시지", + "claim-device-not-found-message": "장치를 찾을 수 없을 때 메시지", + "claim-device-failed-message": "장치 등록 실패 메시지", + "claim-device-name-required-message": "'장치 이름 필요' 오류 메시지", + "claim-device-secret-key-required-message": "'비밀 키 필요' 오류 메시지", + "show-label": "레이블 표시", + "label": "라벨", + "required": "필수", + "required-error-message": "'필수' 오류 메시지", + "show-result-message": "결과 메시지 표시", + "integer-field-settings": "정수 필드 설정", + "min-value": "최소 값", + "max-value": "최대 값", + "double-field-settings": "실수 필드 설정", + "text-field-settings": "텍스트 필드 설정", + "min-length": "최소 길이", + "max-length": "최대 길이", + "checkbox-settings": "체크박스 설정", + "true-label": "선택됨 레이블", + "false-label": "선택 해제됨 레이블", + "image-input-settings": "이미지 입력 설정", + "display-preview": "미리보기 표시", + "display-clear-button": "지우기 버튼 표시", + "display-apply-button": "적용 버튼 표시", + "display-discard-button": "취소 버튼 표시", + "datetime-field-settings": "날짜/시간 필드 설정", + "display-time-input": "시간 입력 표시", + "latitude-key-name": "위도 키 이름", + "longitude-key-name": "경도 키 이름", + "show-get-location-button": "'현재 위치 가져오기' 버튼 표시", + "use-high-accuracy": "고정밀 사용", + "location-fields-settings": "위치 필드 설정", + "latitude-label": "위도 레이블", + "longitude-label": "경도 레이블", + "input-fields-alignment": "입력 필드 정렬", + "input-fields-alignment-column": "열 (기본값)", + "input-fields-alignment-row": "행", + "layout": "레이아웃", + "row-gap": "행 사이 간격 (픽셀)", + "column-gap": "열 사이 간격 (픽셀)", + "latitude-field-required": "위도 필드 필수", + "longitude-field-required": "경도 필드 필수", + "attribute-settings": "속성 설정", + "widget-mode": "위젯 모드", + "widget-mode-update-attribute": "속성 업데이트", + "widget-mode-update-timeseries": "시계열 업데이트", + "attribute-scope": "속성 범위", + "attribute-scope-server": "서버 속성", + "attribute-scope-shared": "공유된 속성", + "value-required": "값 필요", + "image-settings": "이미지 출력 설정", + "image-format": "이미지 형식", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "이미지 품질", + "max-image-width": "최대 너비", + "max-image-height": "최대 높이", + "action-buttons": "동작 버튼", + "show-action-buttons": "동작 버튼 표시", + "update-all-values": "수정된 것뿐만 아니라 모든 값 업데이트", + "save-button-label": "'저장' 버튼 레이블", + "reset-button-label": "'실행 취소' 버튼 레이블", + "group-settings": "그룹 설정", + "show-group-title": "다른 엔티티와 관련된 필드 그룹의 제목 표시", + "group-title": "그룹 제목", + "fields-alignment": "필드 정렬", + "fields-alignment-row": "행 (기본값)", + "fields-alignment-column": "열", + "fields-in-row": "행의 필드 수", + "option-value": "값 ('null'을 입력하여 빈 옵션 생성)", + "option-label": "라벨", + "hide-input-field": "입력 필드 숨기기", + "datakey-type": "데이터 키 유형", + "datakey-type-server": "서버 속성 (기본값)", + "datakey-type-shared": "공유된 속성", + "datakey-type-timeseries": "시계열", + "datakey-value-type": "데이터 키 값 유형", + "datakey-value-type-string": "문자열", + "datakey-value-type-double": "실수", + "datakey-value-type-integer": "정수", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "불리언 (체크박스)", + "datakey-value-type-boolean-switch": "불리언 (스위치)", + "datakey-value-type-date-time": "날짜 및 시간", + "datakey-value-type-date": "날짜", + "datakey-value-type-time": "시간", + "datakey-value-type-select": "선택", + "datakey-value-type-radio": "라디오", + "datakey-value-type-color": "색상", + "value-is-required": "값은 필수 항목입니다.", + "ability-to-edit-attribute": "속성 편집 가능 여부", + "ability-to-edit-attribute-editable": "편집 가능 (기본값)", + "ability-to-edit-attribute-disabled": "비활성화됨", + "ability-to-edit-attribute-readonly": "읽기 전용", + "disable-on-datakey-name": "다른 데이터 키의 false 값에서 비활성화 (데이터 키 이름 지정)", + "field-appearance": "필드 외관", + "appearance-fill": "채우기", + "appearance-outline": "윤곽선", + "subscript-sizing": "아래 첨자 크기", + "subscript-sizing-fixed": "고정", + "subscript-sizing-dynamic": "동적", + "slide-toggle-settings": "슬라이드 토글 설정", + "slide-toggle-label-position": "슬라이드 토글 레이블 위치", + "slide-toggle-label-position-after": "이후", + "slide-toggle-label-position-before": "앞", + "select-options": "선택 옵션", + "no-select-options": "구성된 선택 옵션 없음", + "add-select-option": "선택 옵션 추가", + "numeric-field-settings": "숫자 필드 설정", + "step-interval": "값 사이의 단계 간격", + "error-messages": "오류 메시지", + "min-value-error-message": "'최소 값' 오류 메시지", + "max-value-error-message": "'최대 값' 오류 메시지", + "invalid-date-error-message": "'잘못된 날짜' 오류 메시지", + "invalid-JSON-error-message": "'잘못된 JSON' 오류 메시지", + "icon-settings": "아이콘 설정", + "dialog-editor-settings": "대화 상자 편집기 설정", + "use-custom-icon": "사용자 지정 아이콘 사용", + "input-cell-icon": "입력 셀 앞에 표시할 아이콘", + "value-conversion-settings": "값 변환 설정", + "get-value-settings": "값 가져오기 설정", + "use-get-value-function": "getValue 함수 사용", + "get-value-function": "getValue 함수", + "set-value-settings": "값 설정 설정", + "use-set-value-function": "setValue 함수 사용", + "set-value-function": "setValue 함수", + "json-invalid": "JSON 값의 형식이 잘못되었습니다", + "title": "제목", + "cancel-button-label": "'취소' 버튼 레이블", + "radio-button-settings": "라디오 버튼 설정", + "color": "색상", + "columns": "열", + "radio-options": "라디오 옵션", + "no-radio-options": "구성된 라디오 옵션 없음", + "add-radio-option": "라디오 옵션 추가", + "radio-label-position": "레이블 위치", + "radio-label-position-before": "앞", + "radio-label-position-after": "이후", + "save-image": "이미지 저장", + "save-to-gallery": "캡처된 이미지를 이미지 갤러리에 자동 저장", + "public-image": "이미지를 승인되지 않은 모든 사용자가 사용할 수 있게 함" + }, + "invalid-qr-code-text": "QR 코드에 대한 잘못된 입력 텍스트입니다. 입력은 문자열 유형이어야 합니다", + "qr-code": { + "use-qr-code-text-function": "QR 코드 텍스트 함수 사용", + "qr-code-text-pattern": "QR 코드 텍스트 패턴 (예: '${entityName} | ${keyName} - 일부 텍스트.')", + "qr-code-text-pattern-hint": "QR 코드 텍스트 패턴은 엔티티 별명의 엔티티에서 처음 찾은 키의 값을 사용합니다.", + "qr-code-text-pattern-required": "QR 코드 텍스트 패턴이 필요합니다.", + "qr-code-text-function": "QR 코드 텍스트 함수" + }, + "label-widget": { + "label-pattern": "패턴", + "label-pattern-hint": "힌트: 예. '텍스트 ${keyName} 단위.' 또는 ${#<키 인덱스>} 단위'", + "label-pattern-required": "패턴이 필요합니다", + "label-position": "위치 (배경에 대한 비율)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "배경색", + "font-settings": "글꼴 설정", + "background-image": "배경 이미지", + "labels": "레이블", + "no-labels": "구성된 레이블 없음", + "add-label": "레이블 추가" + }, + "navigation": { + "title": "제목", + "navigation-path": "탐색 경로", + "filter-type": "필터 유형", + "filter-type-all": "모든 항목", + "filter-type-include": "항목 포함", + "filter-type-exclude": "항목 제외", + "items": "항목", + "enter-urls-to-filter": "필터링할 URL 입력..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "메시지 유형", + "method": "방법", + "params": "매개변수", + "created-time": "생성 일시", + "expiration-time": "만료 시간", + "retries": "재시도", + "status": "상태", + "filter": "필터", + "refresh": "새로고침", + "add": "RPC 요청 추가", + "details": "상세 내역", + "delete": "삭제", + "delete-request-title": "영구 RPC 요청 삭제", + "delete-request-text": "요청을 삭제하시겠습니까?", + "details-title": "세부 정보 RPC ID: ", + "additional-info": "추가 정보", + "response": "응답", + "any-status": "모든 상태", + "rpc-status-list": "RPC 상태 목록", + "no-request-prompt": "표시할 요청 없음", + "send-request": "요청 보내기", + "add-title": "영구 RPC 요청 생성", + "method-error": "메서드가 필요합니다.", + "timeout-error": "최소 시간 제한 값은 5000 (5초)입니다.", + "white-space-error": "공백은 허용되지 않습니다.", + "rpc-status": { + "QUEUED": "대기열에 있음", + "SENT": "전송됨", + "DELIVERED": "전달됨", + "SUCCESSFUL": "성공", + "TIMEOUT": "시간 초과", + "EXPIRED": "만료됨", + "FAILED": "실패" + }, + "rpc-search-status-all": "모두", + "message-types": { + "false": "양방향", + "true": "단방향" + }, + "general-settings": "일반 설정", + "enable-filter": "필터 활성화", + "enable-sticky-header": "스크롤하는 동안 헤더 표시", + "enable-sticky-action": "스크롤하는 동안 작업 열 표시", + "display-request-details": "요청 세부 정보 표시", + "allow-send-request": "RPC 요청 보내기 허용", + "allow-delete-request": "요청 삭제 허용", + "columns-settings": "열 설정", + "display-columns": "표시할 열", + "column": "열", + "no-columns-found": "열을 찾을 수 없음", + "no-columns-matching": "'{{column}}'을(를) 찾을 수 없습니다." + }, + "range-chart": { + "chart": "차트", + "data-zoom": "데이터 확대/축소", + "range-chart-appearance": "범위 차트 모양", + "range-colors": "범위 색상", + "out-of-range-color": "범위 외 색상", + "show-range-thresholds": "범위 임계값 표시", + "range-thresholds-settings": "범위 임계값 설정", + "fill-area": "영역 채우기", + "fill-area-opacity": "영역 채우기 불투명도", + "range-chart-style": "범위 차트 스타일" + }, + "knob": { + "behavior": "동작", + "initial-value": "초기 값", + "initial-value-hint": "노브의 초기 값을 가져오는 작업입니다.", + "on-value-change": "값 변경 시", + "on-value-change-hint": "노브 값이 변경될 때 트리거되는 작업입니다.", + "range": "범위", + "min": "최소", + "max": "최대", + "value": "값", + "fallback-initial-value": "대체 초기 값" + }, + "rpc": { + "value-settings": "값 설정", + "initial-value": "초기 값", + "retrieve-value-settings": "켜짐/꺼짐 값 가져오기 설정", + "retrieve-value-method": "메서드를 사용하여 값 가져오기", + "retrieve-value-method-none": "가져오지 않음", + "retrieve-value-method-rpc": "RPC 값 가져오기 메서드 호출", + "retrieve-value-method-attribute": "속성 구독", + "retrieve-value-method-timeseries": "시계열 구독", + "attribute-value-key": "속성 키", + "timeseries-value-key": "시계열 키", + "get-value-method": "RPC 값 가져오기 메서드", + "parse-value-function": "값 구문 분석 함수", + "update-value-settings": "값 업데이트 설정", + "set-value-method": "RPC 값 설정 메서드", + "convert-value-function": "값 변환 함수", + "rpc-settings": "RPC 설정", + "request-timeout": "RPC 요청 시간 제한 (ms)", + "persistent-rpc-settings": "영구 RPC 설정", + "request-persistent": "RPC 요청 영구", + "persistent-polling-interval": "영구 RPC 명령 응답을 가져오기 위한 폴링 간격 (ms)", + "common-settings": "일반 설정", + "switch-title": "스위치 제목", + "show-on-off-labels": "켜짐/꺼짐 레이블 표시", + "slide-toggle-label": "슬라이드 토글 레이블", + "label-position": "레이블 위치", + "label-position-before": "앞", + "label-position-after": "이후", + "slider-color": "슬라이더 색상", + "slider-color-primary": "기본", + "slider-color-accent": "강조", + "slider-color-warn": "경고", + "button-style": "버튼 스타일", + "button-raised": "양각 버튼", + "button-primary": "기본 색상", + "button-background-color": "버튼 배경색", + "button-text-color": "버튼 텍스트 색상", + "widget-title": "위젯 제목", + "button-label": "버튼 레이블", + "device-attribute-scope": "장치 속성 범위", + "server-attribute": "서버 속성", + "shared-attribute": "공유된 속성", + "device-attribute-parameters": "장치 속성 매개변수", + "is-one-way-command": "단방향 명령임", + "rpc-method": "RPC 메서드", + "rpc-method-params": "RPC 메서드 매개변수", + "show-rpc-error": "RPC 명령 실행 오류 표시", + "led-title": "LED 제목", + "led-color": "LED 색상", + "check-status-settings": "상태 확인 설정", + "perform-rpc-status-check": "RPC 장치 상태 확인 수행", + "retrieve-led-status-value-method": "메서드를 사용하여 LED 상태 값 가져오기", + "led-status-value-attribute": "LED 상태 값을 포함하는 장치 속성", + "led-status-value-timeseries": "LED 상태 값을 포함하는 장치 시계열", + "check-status-method": "RPC 장치 상태 확인 메서드", + "parse-led-status-value-function": "LED 상태 값 구문 분석 함수", + "knob-title": "노브 제목" + }, + "maps": { + "map-type": { + "type": "지도 유형", + "map": "지도", + "image": "이미지" + }, + "image": { + "image-source": "이미지 소스", + "image-source-image": "이미지", + "image-source-entity-key": "엔티티 키", + "source-entity-alias": "소스 엔티티 별명", + "image-url-key": "이미지 URL 키", + "image-url-key-required": "이미지 URL 키가 필요합니다" + }, + "control": { + "map-controls": "지도 컨트롤", + "position": "범례 위치", + "position-topleft": "왼쪽 상단", + "position-topright": "오른쪽 상단", + "position-bottomleft": "왼쪽 하단", + "position-bottomright": "오른쪽 하단", + "zoom-actions": "확대/축소 작업", + "zoom-scroll": "스크롤", + "zoom-double-click": "더블 클릭", + "zoom-control-buttons": "컨트롤 버튼", + "scale": "눈금", + "scale-metric": "미터법", + "scale-imperial": "야드파운드법", + "switch-to-drag-mode-using-button": "버튼을 사용하여 드래그 모드로 전환" + }, + "timeline": { + "control-panel": "타임라인 컨트롤 패널", + "time-step": "시간 단계", + "speed-options": "속도 옵션", + "timestamp": "타임스탬프", + "snap-to-real-location": "실제 위치에 맞추기", + "location-snap-filter-function": "위치 맞춤 필터 함수", + "no-trips-data-available": "사용 가능한 이동 데이터 없음" + }, + "map-action": { + "map-action-buttons": "지도 작업 버튼", + "label": "라벨", + "icon": "아이콘", + "color": "색상", + "action": "동작", + "add-button": "버튼 추가", + "no-action-buttons-configured": "구성된 작업 버튼 없음", + "remove-action-button": "작업 버튼 제거", + "map-action-button": "지도 작업 버튼", + "button-requires": "버튼에 레이블 또는 아이콘이 필요합니다" + }, + "common": { + "common-map-settings": "공통 지도 설정", + "fit-map-bounds": "모든 마커를 포함하도록 지도 범위 맞춤", + "default-map-center-position": "기본 지도 중심 위치", + "default-map-zoom-level": "기본 지도 확대/축소 레벨", + "entities-limit": "로드할 엔티티 제한" + }, + "layer": { + "label": "라벨", + "layer": "레이어", + "layers": "레이어", + "map-layers": "지도 레이어", + "add-layer": "레이어 추가", + "layer-settings": "레이어 설정", + "remove-layer": "레이어 제거", + "no-layers": "구성된 레이어 없음", + "roadmap": "도로 지도", + "satellite": "위성", + "hybrid": "하이브리드", + "reference": { + "reference-layer": "참조 레이어", + "no-layer": "레이어 없음", + "openstreetmap-hybrid": "OpenStreetMap 하이브리드", + "world-edition-hybrid": "월드 에디션 하이브리드", + "enhanced-contrast-hybrid": "향상된 대비 하이브리드" + }, + "provider": { + "provider": "제공자", + "openstreet": { + "title": "OpenStreet", + "mapnik": "Mapnik", + "hot": "HOT", + "esri-street": "세계 거리 지도", + "esri-topo": "세계 지형도", + "esri-imagery": "세계 이미지", + "cartodb-positron": "포지트론", + "cartodb-dark-matter": "다크 매터" + }, + "google": { + "title": "Google", + "roadmap": "도로 지도", + "satellite": "위성", + "hybrid": "하이브리드", + "terrain": "지형" + }, + "here": { + "title": "HERE", + "normal-day": "일반 주간", + "normal-night": "일간 야간", + "hybrid-day": "하이브리드 주간", + "terrain-day": "지형 주간" + }, + "tencent": { + "title": "Tencent", + "normal": "정상", + "satellite": "위성", + "terrain": "지형" + }, + "custom": { + "title": "사용자 지정", + "tile-url": "타일 URL" + } + }, + "credentials": { + "credentials": "자격 증명", + "api-key": "API 키" + } + }, + "overlays": { + "overlays": "오버레이", + "overlays-hint": "지도 엔티티에 대한 데이터 소스, 모양, 동작, 편집 옵션 및 그룹화 구성", + "trips": "여행", + "markers": "마커", + "polygons": "다각형", + "circles": "원", + "polylines": "폴리라인" + }, + "data-layer": { + "source": "소스", + "filter": "필터", + "additional-data-keys": "추가 데이터 키", + "additional-datasources": "추가 데이터 소스", + "additional-datasources-hint": "지도에 표시되지 않는 엔티티의 속성 또는 텔레메트리에 액세스하기 위한 데이터 소스, 지도 오버레이 함수에서 사용 가능.", + "more-datasources": "더 많은 데이터 소스", + "data-keys": "데이터 키", + "add-datasource": "데이터소스 추가", + "no-datasources": "구성된 데이터 소스 없음", + "remove-datasource": "데이터소스 삭제", + "behavior": "동작", + "on-click": "클릭 시", + "on-click-hint": "사용자가 지도 항목을 클릭할 때 호출되는 동작.", + "groups": "그룹", + "groups-hint": "오버레이에 할당된 그룹 이름 목록, 지도에서 가시성을 전환하는 데 사용됩니다.", + "color": "색상", + "color-settings": "색상 설정", + "color-type-constant": "상수", + "color-type-range": "범위", + "color-type-function": "함수", + "color-range-source-key": "색상 범위 소스 키", + "color-range-source-key-required": "색상 범위 소스 키는 필수 항목입니다.", + "color-range": "색상 범위", + "color-function": "색상 함수", + "label": "라벨", + "tooltip": "툴팁", + "pattern-type-pattern": "패턴", + "pattern-type-function": "함수", + "label-pattern": "레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "label-function": "레이블 함수", + "tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트)", + "tooltip-function": "툴팁 함수", + "tooltip-trigger": "툴팁 트리거", + "tooltip-trigger-click": "클릭 시 툴팁 표시", + "tooltip-trigger-hover": "호버 시 툴팁 표시", + "auto-close-tooltips": "툴팁 자동 닫기", + "tooltip-offset": "툴팁 오프셋", + "tooltip-offset-horizontal": "가로", + "tooltip-offset-vertical": "세로", + "tooltip-tag-actions": "태그 동작", + "add-tooltip-tag-action": "태그 동작 추가", + "edit-tooltip-tag-action": "태그 동작 편집", + "remove-tooltip-tag-action": "태그 동작 제거", + "action-add": "추가", + "action-edit": "수정", + "action-move": "이동", + "action-remove": "제거", + "edit-instruments": "계기", + "persist-location-attribute-scope": "위치를 저장할 속성의 범위", + "enable-snapping": "정밀 그리기를 위해 다른 정점에 스냅 활성화", + "enable-snapping-hint": "그리기를 더 쉽고 정확하게 만들기 위해 새 포인트를 기존 모양에 자동으로 정렬합니다.", + "drag-drop-mode": "드래그 앤 드롭 모드", + "trip": { + "no-trips": "구성된 여행 없음", + "add-trip": "여행 추가", + "trip-configuration": "여행 구성", + "remove-trip": "여행 제거" + }, + "marker": { + "marker": "마커", + "latitude-key": "위도 키", + "longitude-key": "경도 키", + "x-pos-key": "X 위치 키", + "y-pos-key": "Y 위치 키", + "latitude-key-required": "위도 키 필수", + "longitude-key-required": "경도 키 필수", + "x-pos-key-required": "X 위치 키 필수", + "y-pos-key-required": "Y 위치 키 필수", + "no-markers": "구성된 마커 없음", + "add-marker": "마커 추가", + "marker-configuration": "마커 구성", + "remove-marker": "마커 제거", + "marker-type": "마커 유형", + "marker-type-shape": "모양", + "marker-type-icon": "아이콘", + "marker-type-image": "이미지", + "shape": "모양", + "icon": "아이콘", + "image": "이미지", + "marker-shapes": "마커 모양", + "marker-icon": "마커 아이콘", + "marker-appearance": "마커 외관", + "marker-image": "마커 이미지", + "marker-image-type-image": "이미지", + "marker-image-type-function": "함수", + "custom-marker-image-size": "사용자 지정 마커 이미지 크기", + "marker-image-function": "마커 이미지 함수", + "marker-images": "마커 이미지", + "marker-offset": "마커 오프셋", + "offset-horizontal": "가로", + "offset-vertical": "세로", + "rotate-marker": "마커 회전", + "offset-angle": "오프셋 각도", + "position-conversion": "위치 변환", + "position-conversion-function": "위치 변환 함수, 각각 0에서 1 사이의 double로 x,y 좌표를 반환해야 합니다", + "clustering": { + "use-map-markers-clustering": "지도 마커 클러스터링 사용", + "zoom-on-cluster-click": "클러스터 클릭 시 확대", + "max-zoom": "마커가 클러스터의 일부가 될 수 있는 최대 확대 수준 (0 - 18)", + "max-radius": "클러스터가 커버할 최대 반경", + "zoom-animation": "확대/축소 시 마커 애니메이션", + "bounds-on-cluster-mouse-over": "클러스터 위에 마우스를 올릴 때 마커 경계", + "spiderfy-max-zoom-level": "최대 확대 수준에서 스파이더파이 (모든 클러스터 마커 보기)", + "load-optimization": "로드 최적화", + "chunked-load": "페이지가 멈추지 않도록 청크를 사용하여 마커 추가", + "lazy-load": "마커 추가에 지연 로드 사용", + "use-cluster-marker-color-function": "클러스터 마커 색상 함수 사용", + "marker-color-function": "마커 색상 함수" + }, + "edit": "마커 편집", + "remove-marker-for": "'{{entityName}}'의 마커 제거", + "place-marker": "마커 배치", + "place-marker-hint": "클릭하여 마커 배치", + "place-marker-hint-with-entity": "클릭하여 '{{entityName}}' 엔티티 배치" + }, + "path": { + "path": "경로", + "path-decorator": "경로 장식", + "decorator-symbol": "장식 기호", + "decorator-symbol-arrow-head": "화살표", + "decorator-symbol-dash": "대시", + "decorator-arrangement": "장식 배열", + "decorator-offset": "시작", + "decorator-end-offset": "끝", + "decorator-repeat": "반복" + }, + "points": { + "points": "지점", + "point-tooltip": "지점 툴팁" + }, + "shape": { + "fill": "채우기", + "fill-type-color": "색상", + "fill-type-stripe": "줄무늬", + "fill-type-image": "이미지", + "color": "색상", + "stripe": "줄무늬", + "image": "이미지", + "stroke": "획", + "fill-image": "채우기 이미지", + "fill-image-type-image": "이미지", + "fill-image-type-function": "함수", + "preserve-aspect-ratio": "가로세로 비율 유지", + "opacity": "불투명도", + "angle": "회전 각도", + "scale": "눈금", + "fill-image-function": "모양 채우기 이미지 함수", + "fill-images": "모양 채우기 이미지", + "stripe-pattern": "줄무늬 패턴", + "first-stripe": "첫 번째 줄무늬", + "second-stripe": "두 번째 줄무늬" + }, + "polygon": { + "polygon-key": "다각형 키", + "polygon-key-required": "다각형 키 필수", + "no-polygons": "구성된 다각형 없음", + "add-polygon": "다각형 추가", + "polygon-configuration": "다각형 구성", + "remove-polygon": "다각형 제거", + "edit": "다각형 편집", + "remove-polygon-for": "'{{entityName}}'의 다각형 제거", + "cut": "다각형 영역 자르기", + "rotate": "다각형 회전", + "draw-rectangle": "사각형 그리기", + "draw-polygon": "다각형 그리기", + "polygon-place-first-point-cut-hint": "클릭하여 첫 번째 지점 배치", + "continue-polygon-cut-hint": "클릭하여 계속 그리기", + "finish-polygon-cut-hint": "첫 번째 마커를 클릭하여 완료 및 저장", + "polygon-place-first-point-hint": "다각형: 클릭하여 첫 번째 지점 배치", + "polygon-place-first-point-hint-with-entity": "'{{entityName}}'의 다각형: 클릭하여 첫 번째 지점 배치", + "continue-polygon-hint": "다각형: 클릭하여 계속 그리기", + "continue-polygon-hint-with-entity": "'{{entityName}}'의 다각형: 클릭하여 계속 그리기", + "finish-polygon-hint": "다각형: 첫 번째 마커를 클릭하여 그리기 완료", + "finish-polygon-hint-with-entity": "'{{entityName}}'의 다각형: 첫 번째 마커를 클릭하여 완료 및 저장", + "rectangle-place-first-point-hint": "사각형: 클릭하여 첫 번째 지점 배치", + "rectangle-place-first-point-hint-with-entity": "'{{entityName}}'의 사각형: 클릭하여 첫 번째 지점 배치", + "finish-rectangle-hint": "사각형: 클릭하여 그리기 완료", + "finish-rectangle-hint-with-entity": "'{{entityName}}'의 사각형: 클릭하여 완료 및 저장" + }, + "circle": { + "circle-key": "원 키", + "circle-key-required": "원 키 필수", + "no-circles": "구성된 원 없음", + "add-circle": "원 추가", + "circle-configuration": "원 구성", + "remove-circle": "원 제거", + "edit": "원 편집", + "remove-circle-for": "'{{entityName}}'의 원 제거", + "draw-circle": "원 그리기", + "place-circle-center-hint-with-entity": "'{{entityName}}'의 원: 클릭하여 원 중심 배치", + "place-circle-center-hint": "원: 클릭하여 원 중심 배치", + "finish-circle-hint-with-entity": "'{{entityName}}'의 원: 클릭하여 원 완료 및 저장", + "finish-circle-hint": "원: 클릭하여 그리기 완료" + }, + "polyline": { + "polyline-key": "폴리라인 키", + "polyline-key-required": "폴리라인 키 필수", + "no-polylines": "구성된 폴리라인 없음", + "add-polylines": "폴리라인 추가", + "polyline-configuration": "폴리라인 구성", + "remove-polyline": "폴리라인 제거", + "edit": "폴리라인 편집", + "cut": "폴리라인 영역 자르기", + "rotate": "폴리라인 회전", + "remove-polyline-for": "'{{entityName}}'의 폴리라인 제거", + "draw-polyline": "폴리라인 그리기", + "polyline-place-first-point-hint-with-entity": "'{{entityName}}'의 폴리라인: 클릭하여 첫 번째 지점 배치", + "polyline-place-first-point-hint": "폴리라인: 클릭하여 첫 번째 지점 배치", + "finish-polyline-hint-with-entity": "'{{entityName}}'의 폴리라인: 클릭하여 그리기 완료", + "finish-polyline-hint": "폴리라인: 클릭하여 그리기 완료", + "polyline-place-first-point-cut-hint": "클릭하여 첫 번째 지점 배치", + "finish-polyline-cut-hint": "첫 번째 마커를 클릭하여 완료 및 저장" + }, + "select-entity": "엔티티 선택", + "select-entity-hint": "힌트: 선택 후 지도를 클릭하여 위치 설정" + }, + "select-entity": "엔티티 선택", + "select-entity-hint": "힌트: 선택 후 지도를 클릭하여 위치 설정", + "tooltips": { + "placeMarker": "클릭하여 '{{entityName}}' 엔티티 배치", + "firstVertex": "'{{entityName}}'의 다각형: 클릭하여 첫 번째 지점 배치", + "firstVertex-cut": "클릭하여 첫 번째 지점 배치", + "continueLine": "'{{entityName}}'의 다각형: 클릭하여 계속 그리기", + "continueLine-cut": "클릭하여 계속 그리기", + "finishLine": "완료하려면 기존 마커 클릭", + "finishPoly": "'{{entityName}}'의 다각형: 첫 번째 마커를 클릭하여 완료 및 저장", + "finishPoly-cut": "첫 번째 마커를 클릭하여 완료 및 저장", + "finishRect": "'{{entityName}}'의 다각형: 클릭하여 완료 및 저장", + "startCircle": "'{{entityName}}'의 원: 클릭하여 원 중심 배치", + "finishCircle": "'{{entityName}}'의 원: 클릭하여 원 완료", + "placeCircleMarker": "클릭하여 원 마커 배치" + }, + "actions": { + "finish": "완료", + "cancel": "취소", + "removeLastVertex": "마지막 지점 제거" + }, + "buttonTitles": { + "drawMarkerButton": "엔티티 배치", + "drawPolyButton": "다각형 생성", + "drawLineButton": "폴리라인 생성", + "drawCircleButton": "원 생성", + "drawRectButton": "사각형 생성", + "editButton": "수정 모드", + "dragButton": "드래그 앤 드롭 모드", + "cutButton": "다각형 영역 자르기", + "deleteButton": "제거", + "drawCircleMarkerButton": "원 마커 생성", + "rotateButton": "다각형 회전" + }, + "map-provider-settings": "지도 제공자 설정", + "map-provider": "지도 제공자", + "map-provider-google": "Google 지도", + "map-provider-openstreet": "OpenStreet 지도", + "map-provider-here": "HERE 지도", + "map-provider-image": "이미지 지도", + "map-provider-tencent": "Tencent 지도", + "openstreet-provider": "OpenStreet 지도 공급자", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (기본)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "사용자 지정 공급자 사용", + "custom-provider-tile-url": "사용자 지정 공급자 타일 URL", + "google-maps-api-key": "Google 지도 API 키", + "default-map-type": "기본 지도 유형", + "google-map-type-roadmap": "도로 지도", + "google-map-type-satelite": "위성", + "google-map-type-hybrid": "하이브리드", + "google-map-type-terrain": "지형", + "map-layer": "지도 레이어", + "here-map-normal-day": "HERE.normalDay (기본)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "자격 증명", + "here-app-id": "HERE 앱 ID", + "here-app-code": "HERE 앱 코드", + "here-api-key": "HERE API 키", + "here-use-new-version-api-3": "API 버전 3 사용", + "tencent-maps-api-key": "Tencent 지도 API 키", + "tencent-map-type-roadmap": "도로 지도", + "tencent-map-type-satelite": "위성", + "tencent-map-type-hybrid": "하이브리드", + "image-map-background": "이미지 지도 배경", + "image-map-background-from-entity-attribute": "엔티티 속성에서 이미지 지도 배경 가져오기", + "image-url-source-entity-alias": "이미지 URL 소스 엔티티 별명", + "image-url-source-entity-attribute": "이미지 URL 소스 엔티티 속성", + "common-map-settings": "공통 지도 설정", + "x-pos-key-name": "X 위치 키 이름", + "y-pos-key-name": "Y 위치 키 이름", + "latitude-key-name": "위도 키 이름", + "longitude-key-name": "경도 키 이름", + "default-map-zoom-level": "기본 지도 확대/축소 레벨 (0 - 20)", + "default-map-center-position": "기본 지도 중심 위치 (0,0)", + "disable-scroll-zooming": "스크롤 확대/축소 비활성화", + "disable-double-click-zooming": "더블 클릭 확대/축소 비활성화", + "disable-zoom-control-buttons": "확대/축소 컨트롤 버튼 비활성화", + "fit-map-bounds": "모든 마커를 포함하도록 지도 범위 맞춤", + "use-default-map-center-position": "기본 지도 중심 위치 사용", + "entities-limit": "로드할 엔티티 제한", + "markers-settings": "마커 설정", + "marker-offset-x": "마커 너비를 곱한 위치에 대한 마커 X 오프셋", + "marker-offset-y": "마커 높이를 곱한 위치에 대한 마커 Y 오프셋", + "position-function": "위치 변환 함수, 각각 0에서 1 사이의 double로 x,y 좌표를 반환해야 합니다", + "draggable-marker": "드래그 가능한 마커", + "label": "라벨", + "show-label": "레이블 표시", + "use-label-function": "레이블 함수 사용", + "label-pattern": "레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "label-function": "레이블 함수", + "tooltip": "툴팁", + "show-tooltip": "툴팁 표시", + "show-tooltip-action": "툴팁 표시 작업", + "show-tooltip-action-click": "클릭 시 툴팁 표시 (기본)", + "show-tooltip-action-hover": "호버 시 툴팁 표시", + "auto-close-tooltips": "툴팁 자동 닫기", + "use-tooltip-function": "툴팁 함수 사용", + "tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트')", + "tooltip-function": "툴팁 함수", + "tooltip-offset-x": "마커 너비를 곱한 마커 앵커에 대한 툴팁 X 오프셋", + "tooltip-offset-y": "마커 높이를 곱한 마커 앵커에 대한 툴팁 Y 오프셋", + "color": "색상", + "use-color-function": "색상 함수 사용", + "color-function": "색상 함수", + "marker-image": "마커 이미지", + "use-marker-image-function": "마커 이미지 함수 사용", + "custom-marker-image": "사용자 지정 마커 이미지", + "custom-marker-image-size": "사용자 지정 마커 이미지 크기 (px)", + "marker-image-function": "마커 이미지 함수", + "marker-images": "마커 이미지", + "polygon-settings": "다각형 설정", + "show-polygon": "다각형 표시", + "polygon-key-name": "다각형 키 이름", + "enable-polygon-edit": "다각형 편집 활성화", + "polygon-label": "다각형 레이블", + "show-polygon-label": "다각형 레이블 표시", + "use-polygon-label-function": "다각형 레이블 함수 사용", + "polygon-label-pattern": "다각형 레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "polygon-label-function": "다각형 레이블 함수", + "polygon-tooltip": "다각형 툴팁", + "show-polygon-tooltip": "다각형 툴팁 표시", + "auto-close-polygon-tooltips": "다각형 툴팁 자동 닫기", + "use-polygon-tooltip-function": "다각형 툴팁 함수 사용", + "polygon-tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트')", + "polygon-tooltip-function": "다각형 툴팁 함수", + "polygon-color": "다각형 색상", + "polygon-opacity": "다각형 불투명도", + "use-polygon-color-function": "다각형 색상 함수 사용", + "polygon-color-function": "다각형 색상 함수", + "polygon-stroke": "다각형 획", + "stroke-color": "획 색상", + "stroke-opacity": "획 불투명도", + "stroke-weight": "획 두께", + "use-polygon-stroke-color-function": "다각형 획 색상 함수 사용", + "polygon-stroke-color-function": "다각형 획 색상 함수", + "circle-settings": "원 설정", + "show-circle": "원 표시", + "circle-key-name": "원 키 이름", + "enable-circle-edit": "원 편집 활성화", + "circle-label": "원 레이블", + "show-circle-label": "원 레이블 표시", + "use-circle-label-function": "원 레이블 함수 사용", + "circle-label-pattern": "원 레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "circle-label-function": "원 레이블 함수", + "circle-tooltip": "원 툴팁", + "show-circle-tooltip": "원 툴팁 표시", + "auto-close-circle-tooltips": "원 툴팁 자동 닫기", + "use-circle-tooltip-function": "원 툴팁 함수 사용", + "circle-tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트')", + "circle-tooltip-function": "원 툴팁 함수", + "circle-fill-color": "원 채우기 색상", + "circle-fill-color-opacity": "원 채우기 색상 불투명도", + "use-circle-fill-color-function": "원 채우기 색상 함수 사용", + "circle-fill-color-function": "원 채우기 색상 함수", + "circle-stroke": "원 획", + "use-circle-stroke-color-function": "원 획 색상 함수 사용", + "circle-stroke-color-function": "원 획 색상 함수", + "markers-clustering-settings": "마커 클러스터링 설정", + "use-map-markers-clustering": "지도 마커 클러스터링 사용", + "zoom-on-cluster-click": "클러스터 클릭 시 확대", + "max-cluster-zoom": "마커가 클러스터의 일부가 될 수 있는 최대 확대 수준 (0 - 18)", + "max-cluster-radius-pixels": "클러스터가 덮을 최대 반지름 (픽셀)", + "cluster-zoom-animation": "확대/축소 시 마커에 애니메이션 표시", + "show-markers-bounds-on-cluster-mouse-over": "클러스터 위에 마우스를 올릴 때 마커 범위 표시", + "spiderfy-max-zoom-level": "최대 확대 수준에서 스파이더파이 (모든 클러스터 마커 보기)", + "load-optimization": "로드 최적화", + "cluster-chunked-loading": "페이지가 멈추지 않도록 청크를 사용하여 마커 추가", + "cluster-markers-lazy-load": "마커 추가에 지연 로드 사용", + "editor-settings": "편집기 설정", + "enable-snapping": "정밀 그리기를 위해 다른 정점에 스냅 활성화", + "init-draggable-mode": "드래그 모드로 지도 초기화", + "hide-all-edit-buttons": "모든 편집 컨트롤 버튼 숨기기", + "hide-draw-buttons": "그리기 버튼 숨기기", + "hide-edit-buttons": "편집 버튼 숨기기", + "hide-remove-button": "제거 버튼 숨기기", + "route-map-settings": "경로 지도 설정", + "trip-animation-settings": "이동 애니메이션 설정", + "normalization-step": "정규화 데이터 단계 (ms)", + "tooltip-background-color": "툴팁 배경색", + "tooltip-font-color": "툴팁 글꼴 색상", + "tooltip-opacity": "툴팁 불투명도 (0-1)", + "auto-close-tooltip": "툴팁 자동 닫기", + "rotation-angle": "마커에 대한 추가 회전 각도 설정 (도)", + "path-settings": "경로 설정", + "path-color": "경로 색상", + "use-path-color-function": "경로 색상 함수 사용", + "path-color-function": "경로 색상 함수", + "path-decorator": "경로 장식", + "use-path-decorator": "경로 데코레이터 사용", + "decorator-symbol": "장식 기호", + "decorator-symbol-arrow-head": "화살표", + "decorator-symbol-dash": "대시", + "decorator-symbol-size": "데코레이터 기호 크기 (px)", + "use-path-decorator-custom-color": "경로 데코레이터 사용자 지정 색상 사용", + "decorator-custom-color": "데코레이터 사용자 지정 색상", + "decorator-offset": "데코레이터 오프셋", + "end-decorator-offset": "종료 데코레이터 오프셋", + "decorator-repeat": "데코레이터 반복", + "points-settings": "포인트 설정", + "show-points": "지점 표시", + "point-color": "포인트 색상", + "point-size": "포인트 크기 (px)", + "use-point-color-function": "포인트 색상 함수 사용", + "point-color-function": "포인트 색상 함수", + "use-point-as-anchor": "포인트를 앵커로 사용", + "point-as-anchor-function": "포인트를 앵커로 사용 함수", + "independent-point-tooltip": "독립 포인트 툴팁", + "clustering-markers": "클러스터링 마커", + "use-icon-create-function": "마커 색상 함수 사용", + "marker-color-function": "마커 색상 함수" + }, + "markdown": { + "use-markdown-text-function": "마크다운/HTML 값 함수 사용", + "markdown-text-function": "마크다운/HTML 값 함수", + "markdown-text-pattern": "마크다운/HTML 패턴 (변수가 있는 마크다운 또는 HTML, 예: '${entityName} 또는 ${keyName} - 일부 텍스트.')", + "apply-default-markdown-style": "기본 마크다운 스타일 적용", + "markdown-css": "마크다운/HTML CSS" + }, + "simple-card": { + "label": "라벨", + "label-position": "레이블 위치", + "label-position-left": "왼쪽", + "label-position-top": "상단" + }, + "single-switch": { + "behavior": "동작", + "layout": "레이아웃", + "layout-right": "오른쪽", + "layout-left": "왼쪽", + "layout-centered": "중앙 정렬", + "auto-scale": "자동 크기 조정", + "label": "라벨", + "icon": "아이콘", + "switch-color": "스위치 색상", + "on": "켜기", + "off": "끄기", + "disabled": "비활성화됨", + "tumbler-color": "텀블러 색상", + "on-label": "켜짐 레이블", + "off-label": "꺼짐 레이블", + "switch": "스위치" + }, + "slider": { + "behavior": "동작", + "initial-value": "초기 값", + "initial-value-hint": "슬라이더의 초기 값을 가져오는 작업입니다.", + "on-value-change": "값 변경 시", + "on-value-change-hint": "슬라이더 값이 변경될 때 트리거되는 작업입니다.", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-extended": "확장", + "layout-simplified": "간소화", + "auto-scale": "자동 크기 조정", + "icon": "아이콘", + "value": "값", + "range": "범위", + "min": "최소", + "max": "최대", + "range-ticks": "범위 눈금", + "tick-marks": "눈금 표시", + "colors": "색상", + "main": "메인", + "background": "배경", + "left-icon": "왼쪽 아이콘", + "right-icon": "오른쪽 아이콘", + "slider": "슬라이더" + }, + "value-card": { + "layout": "레이아웃", + "layout-square": "사각형", + "layout-vertical": "세로", + "layout-centered": "중앙 정렬", + "layout-simplified": "간소화", + "layout-horizontal": "가로", + "layout-horizontal-reversed": "수평 반전", + "label": "라벨", + "icon": "아이콘", + "value": "값", + "date": "날짜", + "value-card-style": "값 카드 스타일", + "auto-scale": "자동 크기 조정" + }, + "label-card": { + "auto-scale": "자동 크기 조정", + "label": "라벨", + "icon": "아이콘", + "label-card-style": "레이블 카드 스타일" + }, + "label-value-card": { + "value": "값", + "label-value-card-style": "레이블 및 값 카드 스타일" + }, + "liquid-level-card": { + "layout-simple": "간단", + "layout-percentage": "백분율", + "layout-absolute": "절대값", + "layout": "레이아웃", + "background-overlay": "값 배경 오버레이", + "total-volume": "총 볼륨", + "total-volume-units": "총 볼륨 단위", + "tank": "탱크", + "shape": "모양", + "datasource-units": "소스 단위", + "widget-units": "위젯 단위", + "decimals": "소수 자릿수", + "liquid": "액체", + "liquid-color": "액체 색상", + "value": "값", + "value-font": "값 글꼴", + "level": "수준", + "last-update": "마지막 업데이트", + "shape-by-attribute": "속성 이름으로 탱크 모양 설정", + "tooltip-background": "배경색", + "background-blur": "배경 블러", + "tank-color": "탱크 색상", + "static": "정적", + "see-examples": "예시 보기", + "attribute": "속성", + "shape-type": "유형", + "v-oval": "수직 타원", + "v-cylinder": "수직 원통", + "v-capsule": "수직 캡슐", + "rectangle": "사각형", + "h-oval": "수평 타원", + "h-ellipse": "수평 타원", + "h-dish-ends": "수평 접시 끝", + "h-cylinder": "수평 원통", + "h-capsule": "수평 캡슐", + "h-elliptical_2_1": "수평 2:1 타원", + "icon": "카드 아이콘", + "title": "카드 제목", + "units": "단위", + "color-and-font": "색상 및 글꼴", + "shape-attribute-name": "속성 이름", + "total-volume-required": "총 볼륨이 필요합니다.", + "attribute-name-required": "속성 이름이 필요합니다.", + "attribute-key-not-set": "속성 '{{attributeName}}' 키가 설정되지 않음", + "attribute-key-invalid": "속성 '{{attributeName}}' 키가 유효하지 않음" + }, + "aggregated-value-card": { + "subtitle": "부제목", + "chart": "차트", + "values": "값", + "value-appearance": "값 외관", + "position": "범례 위치", + "position-center": "중앙", + "position-right-top": "오른쪽 상단", + "position-right-bottom": "오른쪽 하단", + "position-left-top": "왼쪽 상단", + "position-left-bottom": "왼쪽 하단", + "font": "글꼴", + "color": "색상", + "arrow": "화살표", + "display-up-down-arrow": "위/아래 화살표 표시", + "add-value": "값 추가", + "remove-value": "값 제거", + "no-values": "구성된 값 없음", + "aggregation": "집합", + "aggregated-value-card-style": "집계 값 카드 스타일", + "auto-scale": "자동 크기 조정" + }, + "value-chart-card": { + "layout": "레이아웃", + "layout-left": "왼쪽", + "layout-right": "오른쪽", + "auto-scale": "자동 크기 조정", + "icon": "아이콘", + "value": "값", + "chart": "차트", + "value-chart-card-style": "값 차트 카드 스타일" + }, + "progress-bar": { + "layout": "레이아웃", + "layout-default": "기본값", + "layout-simplified": "간소화", + "auto-scale": "자동 크기 조정", + "icon": "아이콘", + "value": "값", + "range": "범위", + "min": "최소", + "max": "최대", + "range-ticks": "범위 눈금", + "bar": "막대", + "bar-color": "막대 색상", + "bar-background": "막대 배경", + "progress-bar-card-style": "진행 막대 카드 스타일" + }, + "notification": { + "max-notification-display": "표시할 최대 알림 수", + "counter": "카운터", + "counter-hint": "\"위젯 제목\"이 활성화되면 카운터가 표시됩니다.", + "icon": "아이콘", + "counter-value": "값", + "counter-color": "색상", + "notification-button": "알림 버튼", + "button-view-all": "모두 보기", + "button-filter": "필터", + "type-filter": "유형 필터", + "button-mark-read": "모두 읽음으로 표시", + "notification-types": "알림 유형", + "notification-type": "알림 유형", + "search-type": "유형 검색", + "any-type": "모든 유형" + }, + "alarm-count": { + "alarm-count-card-style": "알람 수 카드 스타일" + }, + "entity-count": { + "entity-count-card-style": "엔티티 수 카드 스타일" + }, + "count": { + "layout": "레이아웃", + "layout-column": "열", + "layout-row": "행", + "label": "라벨", + "icon": "아이콘", + "icon-background": "아이콘 배경", + "value": "값", + "chevron": "갈매기", + "auto-scale": "자동 크기 조정" + }, + "table": { + "common-table-settings": "일반 테이블 설정", + "enable-search": "검색 활성화", + "enable-sticky-header": "항상 헤더 표시", + "enable-sticky-action": "항상 동작 열 표시", + "hidden-cell-button-display-mode": "숨겨진 셀 버튼 동작 표시 모드", + "show-empty-space-hidden-action": "숨겨진 셀 버튼 동작 대신 빈 공간 표시", + "dont-reserve-space-hidden-action": "숨겨진 동작 버튼에 공간 예약 안 함", + "display-timestamp": "타임스탬프", + "timestamp-column-name": "타임스탬프", + "display-pagination": "페이지 매김 표시", + "default-page-size": "기본 페이지 크기", + "page-step-settings": "페이지 단계 설정", + "page-step-count": "단계 수", + "page-step-increment": "단계 증가", + "page-step-count-format-message": "1에서 100 범위의 정수 값이어야 합니다.", + "page-step-increment-format-message": "1 이상의 정수 값이어야 합니다.", + "use-entity-label-tab-name": "탭 이름에 엔티티 레이블 사용", + "hide-empty-lines": "빈 줄 숨기기", + "row-style": "행 스타일", + "use-row-style-function": "행 스타일 함수 사용", + "row-style-function": "행 스타일 함수", + "cell-style": "셀 스타일", + "use-cell-style-function": "셀 스타일 함수 사용", + "cell-style-function": "셀 스타일 함수", + "cell-content": "셀 내용", + "use-cell-content-function": "셀 내용 함수 사용", + "cell-content-function": "셀 내용 함수", + "show-latest-data-column": "최신 데이터 열 표시", + "latest-data-column-order": "최신 데이터 열 순서", + "entities-table-title": "엔티티 테이블 제목", + "enable-select-column-display": "표시할 열 선택 활성화", + "display-entity-name": "엔티티 이름 열 표시", + "entity-name-column-title": "엔티티 이름 열 제목", + "display-entity-label": "엔티티 레이블 열 표시", + "entity-label-column-title": "엔티티 레이블 열 제목", + "display-entity-type": "엔티티 유형 열 표시", + "default-sort-order": "기본 정렬 순서", + "custom-title": "사용자 지정 헤더 제목", + "column-width": "열 너비 (px 또는 %)", + "default-column-visibility": "기본 열 가시성", + "column-visibility-visible": "표시", + "column-visibility-hidden": "숨김", + "column-visibility-hidden-mobile": "모바일 모드에서 숨김", + "column-selection-to-display": "'표시할 열'에서 열 선택", + "column-selection-to-display-enabled": "사용", + "column-selection-to-display-disabled": "비활성화됨", + "alarms-table-title": "알람 테이블 제목", + "enable-alarms-selection": "알람 선택 활성화", + "enable-alarms-search": "알람 검색 활성화", + "enable-alarm-filter": "알람 필터 활성화", + "display-alarm-details": "알람 세부 정보 표시", + "allow-alarms-ack": "알람 확인 허용", + "allow-alarms-clear": "알람 지우기 허용", + "display-alarm-activity": "알람 활동 표시", + "allow-alarms-assign": "알람 할당 허용", + "columns": "열", + "column-settings": "열 설정", + "remove-column": "열 제거", + "add-column": "열 추가", + "no-columns": "구성된 열 없음", + "columns-to-display": "표시할 열", + "table-header": "테이블 헤더", + "header-buttons": "헤더 버튼", + "table-buttons": "테이블 버튼", + "pagination": "페이지 매김", + "rows": "행", + "timeseries-column-error": "최소 하나의 시계열 열을 지정해야 합니다.", + "alarm-column-error": "최소 하나의 알람 열을 지정해야 합니다.", + "table-tabs": "테이블 탭", + "show-cell-actions-menu-mobile": "모바일 모드에서 셀 동작 드롭다운 메뉴 표시", + "disable-sorting": "정렬 비활성화", + "sort-by": "탭 정렬 기준", + "sort-timestamp-option": "생성 일시" + }, + "latest-chart": { + "total": "합계", + "auto-scale": "자동 크기 조정", + "clockwise-layout": "시계 방향 레이아웃", + "sort-series": "레이블로 시리즈 정렬", + "tooltip-value-type-absolute": "절대값", + "tooltip-value-type-percentage": "백분율" + }, + "pie-chart": { + "pie-chart-appearance": "원형 차트 외관", + "label": "라벨", + "border": "테두리", + "radius": "반경", + "pie-chart-card-style": "원형 차트 카드 스타일" + }, + "radar-chart": { + "radar-appearance": "레이더 외관", + "shape": "모양", + "shape-polygon": "다각형", + "shape-circle": "원", + "color": "색상", + "line": "선", + "points": "지점", + "points-label": "지점 레이블", + "radar-axis": "레이더 축", + "axis-label": "축 레이블", + "ticks-label": "눈금 레이블", + "radar-chart-style": "레이더 차트 스타일", + "max-axes-scaling": "최대 축 크기 조정", + "max-axes-scaling-hint": "각 레이더 축이 자체 최대값(별도)을 갖는지 또는 위젯 데이터 세트를 기반으로 모든 축에서 가장 높은 값(공통)을 공유하는지 선택합니다.", + "separate": "분리", + "common": "공통" + }, + "time-series-chart": { + "chart": "차트", + "chart-style": "차트 스타일", + "data-zoom": "데이터 확대/축소", + "stack-mode": "스택 모드", + "stack-mode-hint": "차트에 시리즈를 스택합니다. 동일한 단위의 시리즈는 서로 위에 배치됩니다.", + "axes": "축", + "y-axes": "Y축", + "line-type": "선 유형", + "line-width": "선 너비", + "type-line": "선", + "type-bar": "막대", + "type-point": "지점", + "no-aggregation-bar-width-strategy": "비집계 데이터의 막대 너비 전략", + "no-aggregation-bar-width-strategy-group": "그룹", + "no-aggregation-bar-width-strategy-separate": "분리", + "bar-group-width": "막대 그룹 너비", + "bar-width": "막대 너비", + "bar-width-relative": "시간 창의 백분율", + "bar-width-absolute": "절대값 (ms)", + "comparison": { + "comparison": "비교", + "comparison-hint": "비교는 기록 데이터에서만 작동합니다!", + "show": "표시", + "settings": "비교 설정", + "show-values-for-comparison": "비교를 위한 기록 데이터 표시", + "comparison-values-label": "비교 키 레이블", + "comparison-values-label-auto": "자동", + "comparison-data-color": "비교 데이터 색상" + }, + "threshold": { + "thresholds": "임계값", + "source": "소스", + "key-value": "키 / 값", + "no-thresholds": "구성된 임계값 없음", + "add-threshold": "임계값 추가", + "type-constant": "상수", + "type-latest-key": "키", + "type-entity": "엔티티", + "threshold-settings": "임계값 설정", + "remove-threshold": "임계값 제거", + "threshold-value-required": "임계값은 필수 항목입니다.", + "key-required": "키는 필수 항목입니다.", + "entity-key-required": "엔티티 키는 필수 항목입니다.", + "line-appearance": "선 외관", + "line-color": "선 색상", + "start-symbol": "시작 기호", + "end-symbol": "끝 기호", + "symbol-size": "크기", + "label": "라벨", + "label-position-start": "시작", + "label-position-middle": "중간", + "label-position-end": "끝", + "label-position-inside-start": "내부 시작", + "label-position-inside-start-top": "내부 시작 상단", + "label-position-inside-start-bottom": "내부 시작 하단", + "label-position-inside-middle": "내부 중간", + "label-position-inside-middle-top": "내부 중간 상단", + "label-position-inside-middle-bottom": "내부 중간 하단", + "label-position-inside-end": "내부 끝", + "label-position-inside-end-top": "내부 끝 상단", + "label-position-inside-end-bottom": "내부 끝 하단", + "label-background": "레이블 배경" + }, + "state": { + "states": "상태", + "label": "라벨", + "ticks-value": "눈금 값", + "source": "소스", + "value-range": "값 / 범위", + "no-states": "구성된 상태 없음", + "add-state": "상태 추가", + "type-constant": "상수", + "type-range": "범위", + "from": "부터", + "to": "까지", + "remove-state": "상태 제거" + }, + "grid": { + "grid": "그리드", + "background-color": "배경색", + "border": "테두리" + }, + "axis": { + "axes": "축", + "x-axis": "X축", + "y-axis": "Y축", + "y-axis-settings": "Y축 설정", + "comparison-x-axis-settings": "비교 X축 설정", + "remove-y-axis": "Y축 제거", + "id": "ID", + "label": "라벨", + "position": "범례 위치", + "position-left": "왼쪽", + "position-right": "오른쪽", + "position-top": "상단", + "position-bottom": "하단", + "tick-labels": "눈금 레이블", + "ticks-formatter-function": "눈금 형식 함수", + "ticks-generator-function": "눈금 생성기 함수", + "show-ticks": "눈금 표시", + "show-line": "선 표시", + "show-split-lines": "분할선 표시", + "show-split-lines-x-axis-hint": "활성화하면 차트에 수직선이 표시됩니다.", + "show-split-lines-y-axis-hint": "활성화하면 차트에 수평선이 표시됩니다.", + "ticks-interval": "눈금 간격", + "ticks-interval-hint": "축의 분할 간격을 강제로 설정합니다.", + "split-number": "분할 수", + "split-number-hint": "축이 분할되는 세그먼트 수입니다.", + "min": "최소", + "max": "최대", + "show": "표시", + "add-y-axis": "Y축 추가" + }, + "series": { + "legend-settings": "범례 설정", + "show-in-legend": "범례에 표시", + "show-in-legend-hint": "범례에 시리즈 이름과 데이터를 표시합니다.", + "hidden-by-default": "기본적으로 숨김", + "hidden-by-default-hint": "시리즈를 기본적으로 범례에서 숨깁니다.", + "series-type": "시리즈 유형", + "type": "유형", + "type-line": "선", + "type-bar": "막대", + "line": { + "line": "선", + "show-line": "선 표시", + "step-line": "계단선", + "step-type-start": "시작", + "step-type-middle": "중간", + "step-type-end": "끝", + "smooth-line": "부드러운 선" + }, + "point": { + "points": "지점", + "show-points": "지점 표시", + "point-label": "지점 레이블", + "point-label-hint": "시리즈 지점 위에 값이 있는 레이블을 표시합니다.", + "point-label-background": "지점 레이블 배경", + "point-shape": "포인트 모양", + "point-size": "포인트 크기" + } + } + }, + "wind-speed-direction": { + "layout": "레이아웃", + "layout-default": "기본값", + "layout-advanced": "고급", + "layout-simplified": "간소화", + "values": "값", + "wind-direction": "풍향", + "center-value": "중심 값", + "icon": "아이콘", + "arrow": "화살표", + "ticks": "눈금", + "labels-type": "레이블 유형", + "directional-names": "방향 이름", + "degrees": "도", + "major-ticks": "주 눈금", + "minor-ticks": "보조 눈금", + "wind-speed-direction-card-style": "풍속 및 풍향 카드 스타일", + "ticks-color": "눈금 색상", + "ticks-labels-type": "눈금 레이블 유형", + "arrow-color": "화살표 색상" + }, + "value-source": { + "value-source": "값 소스", + "predefined-value": "상수", + "entity-attribute": "엔티티 속성", + "value": "값", + "value-required": "값이 필요합니다.", + "key-required": "키는 필수 항목입니다.", + "entity-key-required": "엔티티 키는 필수 항목입니다.", + "source-entity-alias": "소스 엔티티 별명", + "source-entity-attribute": "소스 엔티티 속성", + "type-constant": "상수", + "type-latest-key": "키", + "type-entity": "엔티티" + }, + "rpc-state": { + "initial-state": "초기 상태", + "initial-state-hint": "컴포넌트의 초기 상태(켜짐/꺼짐)를 가져오는 작업입니다.", + "disabled-state": "비활성화 상태", + "disabled-state-hint": "구성 요소가 비활성화되는 조건을 구성합니다.", + "turn-on": "'켜짐'으로 전환", + "turn-on-hint": "슬라이더가 '켜짐'으로 전환될 때 트리거되는 작업입니다.", + "turn-off": "'꺼짐'으로 전환", + "turn-off-hint": "슬라이더가 '꺼짐'으로 전환될 때 트리거되는 작업입니다.", + "on": "켜기", + "off": "끄기", + "disabled": "비활성화됨" + }, + "value-action": { + "do-nothing": "아무것도 하지 않음", + "execute-rpc": "RPC 실행", + "get-attribute": "속성 가져오기", + "set-attribute": "속성 설정", + "get-time-series": "시계열 가져오기", + "get-alarm-status": "알람 상태 가져오기", + "get-dashboard-state": "대시보드 상태 ID 가져오기", + "get-dashboard-state-object": "대시보드 상태 엔티티 가져오기", + "add-time-series": "시계열 추가", + "execute-rpc-text": "RPC 메서드 '{{methodName}}' 실행", + "get-time-series-text": "시계열 '{{key}}' 사용", + "get-attribute-text": "속성 '{{key}}' 사용", + "get-alarm-status-text": "알람 상태 사용", + "get-dashboard-state-text": "대시보드 상태 사용", + "get-dashboard-state-object-text": "대시보드 상태 엔티티 사용", + "when-dashboard-state-is-text": "대시보드 상태 ID가 '{{state}}'인 경우", + "when-dashboard-state-function-is-text": "f(대시보드 상태 ID)가 '{{state}}'인 경우", + "when-dashboard-state-object-function-is-text": "f(대시보드 상태 엔티티)가 '{{state}}'인 경우", + "set-attribute-to-value-text": "'{{key}}' 속성을 다음으로 설정: {{value}}", + "add-time-series-value-text": "'{{key}}' 시계열 값 추가: {{value}}", + "set-attribute-text": "'{{key}}' 속성 설정", + "add-time-series-text": "'{{key}}' 시계열 추가", + "action": "동작", + "value": "값", + "init-value-hint": "장치가 데이터를 보낼 때까지 설정될 값입니다.", + "method": "방법", + "method-name-required": "메서드 이름이 필요합니다.", + "request-timeout-ms": "RPC 요청 시간 제한 (ms)", + "request-timeout-required": "요청 시간 제한이 필요합니다.", + "min-request-timeout-error": "요청 시간 제한 값은 5000ms (5초) 이상이어야 합니다.", + "request-persistent": "RPC 요청 영구", + "persistent-polling-interval": "영구 폴링 간격 (ms)", + "persistent-polling-interval-hint": "영구 RPC 명령 응답을 가져오기 위한 폴링 간격 (ms)", + "persistent-polling-interval-required": "영구 폴링 간격이 필요합니다.", + "min-persistent-polling-interval-error": "영구 폴링 간격 값은 1000ms (1초) 이상이어야 합니다.", + "attribute-scope": "속성 범위", + "attribute-key": "속성 키", + "attribute-key-required": "속성 키를 입력하세요.", + "time-series-key": "시계열 키", + "time-series-key-required": "시계열 키가 필요합니다.", + "action-result-converter": "작업 결과 변환기", + "converter-none": "없음", + "converter-function": "함수", + "converter-constant": "상수", + "converter-value": "값", + "parse-value-function": "값 구문 분석 함수", + "state-when-result-is": "결과가 다음인 경우 '{{state}}'", + "parameters": "파라미터", + "convert-value-function": "값 변환 함수", + "error": { + "target-entity-is-not-set": "대상 엔티티가 설정되지 않았습니다!", + "failed-to-perform-action": "{{ actionLabel }} 작업을 수행하지 못했습니다.", + "invalid-attribute-scope": "{{scope}} 속성 범위는 {{entityType}} 엔티티에서 지원되지 않습니다." + } + }, + "widget-font": { + "font-settings": "글꼴 설정", + "font-family": "글꼴 패밀리", + "size": "크기", + "relative-font-size": "상대 글꼴 크기 (백분율)", + "font-style": "스타일", + "font-style-normal": "정상", + "font-style-italic": "이탤릭", + "font-style-oblique": "기울임", + "font-weight": "두께", + "font-weight-normal": "정상", + "font-weight-bold": "굵게", + "font-weight-bolder": "더 굵게", + "font-weight-lighter": "더 얇게", + "color": "색상", + "shadow-color": "그림자 색상", + "preview": "미리보기", + "line-height": "줄 높이", + "auto": "자동" + }, + "home": { + "no-data-available": "사용 가능한 데이터 없음" + }, + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "디스크", + "cpu-warning-text": "CPU 사용량이 높습니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하세요.", + "cpu-critical-text": "CPU 사용량이 매우 높습니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하세요.", + "ram-warning-text": "RAM 여유 공간이 부족합니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하거나 RAM 크기를 늘리세요.", + "ram-critical-text": "RAM 여유 공간이 매우 부족합니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하거나 RAM 크기를 늘리세요.", + "disk-warning-text": "디스크 공간이 부족합니다. 데이터 손실을 방지하려면 디스크 공간을 확보하거나 늘리세요.", + "disk-critical-text": "디스크 공간이 매우 부족합니다. 데이터 손실을 방지하려면 디스크 공간을 확보하거나 늘리세요." + }, + "cluster-info": { + "service-id": "서비스 ID", + "service-type": "서비스 유형", + "no-data": "데이터 없음" + }, + "transport-messages": { + "title": "전송 메시지", + "info": "장치에서 온 모든 메시지" + }, + "activity": { + "title": "활동" + }, + "documentation": { + "title": "문서", + "add-link": "링크 추가", + "add-link-title": "문서 링크 추가", + "name": "이름", + "name-required": "이름을 입력하세요.", + "link": "링크", + "link-required": "링크가 필요합니다.", + "columns": "열" + }, + "quick-links": { + "title": "빠른 링크", + "add-link": "링크 추가", + "add-link-title": "빠른 링크 추가", + "quick-link": "빠른 링크", + "quick-link-required": "빠른 링크가 필요합니다.", + "no-links-matching": "'{{name}}'과(와) 일치하는 링크가 없습니다.", + "columns": "열" + }, + "recent-dashboards": { + "title": "대시보드", + "last": "최근 본 항목", + "starred": "즐겨찾기", + "name": "이름", + "last-viewed": "최근 본 항목", + "no-last-viewed-dashboards": "아직 최근 본 대시보드 없음" + }, + "configured-features": { + "title": "구성된 기능", + "info": "구성이 필요한 기능 상태", + "email-feature": "이메일", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2단계 인증", + "feature-configured": "기능이 구성되었습니다.\n클릭하여 설정", + "feature-not-configured": "기능이 구성되지 않았습니다.\n클릭하여 설정" + }, + "version-info": { + "title": "버전", + "contact-us": "문의하기", + "current-version": "현재 버전", + "current": "현재", + "available-version": "사용 가능한 버전", + "available": "사용 가능", + "upgrade": "업그레이드", + "version-is-up-to-date": "버전이 최신 상태입니다" + }, + "usage-info": { + "title": "사용량", + "entities": "엔티티", + "api-calls": "API 호출" + }, + "functions": { + "title": "함수", + "pe-feature-tooltip": "ThingsBoard\nProfessional Edition에서만", + "switch-to-pe": "PE로 전환", + "alarms": "알람", + "dashboards": "대시보드", + "entities-and-relations": "엔티티 및 관계", + "profiles": "프로필", + "advanced-features": "고급 기능", + "notification-center": "알림 센터", + "api-usage": "Api 사용량", + "customers": "고객", + "customers-hierarchy": "고객 계층 구조", + "roles-and-permissions": "역할 및 권한", + "groups": "그룹", + "integrations": "통합", + "solution-templates": "솔루션 템플릿", + "scheduler": "스케줄러", + "white-labeling": "화이트 라벨링" + }, + "devices": { + "view-docs": "문서 보기", + "inactive": "비활성", + "active": "활성", + "total": "합계" + }, + "alarms": { + "critical": "심각한", + "assigned-to-me": "나에게 할당됨", + "total": "합계" + }, + "getting-started": { + "get-started": "시작하기", + "finish": "완료", + "done-welcome-title": "환영합니다", + "done-welcome-text": "잘 해결했습니다!", + "sys-admin": { + "step1": { + "title": "테넌트 및 테넌트 관리자 생성", + "content": "

    테넌트는 장치와 자산을 소유하거나 생산하는 개인 또는 조직입니다. 테넌트는 여러 테넌트 관리자 사용자, 고객, 장치 및 자산을 가질 수 있습니다.

    테넌트 관리자는 테넌트 계정 내에서 장치, 자산, 고객 및 대시보드를 생성하고 관리할 수 있습니다.

    방법은 문서를 참조하세요:

    ", + "how-to-create-tenant": "테넌트 및 테넌트 관리자 생성 방법" + }, + "step2": { + "title": "기능 구성: 메일 서버", + "content": "

    메일 서버 구성은 사용자 활성화, 비밀번호 복구 및 알림 전달에 필수적입니다.

    방법은 문서를 참조하세요:

    ", + "how-to-configure-mail-server": "메일 서버 구성 방법" + }, + "step3": { + "title": "기능 구성: SMS 공급자", + "content": "

    SMS 공급자를 구성하여 고객에게 SMS로 알림을 보냅니다.

    방법은 문서를 참조하세요:

    ", + "how-to-configure-sms-provider": "SMS 공급자 구성 방법" + }, + "step4": { + "title": "기능 구성: 2FA", + "content": "

    2단계 인증으로 플랫폼 계정의 보안을 강화하세요.

    방법은 문서를 참조하세요:

    ", + "how-to-configure-2fa": "2FA 구성 방법" + }, + "step5": { + "title": "기능 구성: OAuth 2", + "content": "

    OAuth 2.0을 통한 Single Sign-On 기능으로 테넌트 및 고객 사용자의 로그인을 간소화하세요.

    방법은 문서를 참조하세요:

    ", + "how-to-configure-oauth2": "OAuth 2 구성 방법" + }, + "step6": { + "title": "기능 구성: Slack", + "content": "

    설정한 알림 규칙에 따라 Slack을 통해 테넌트 및 고객 사용자에게 알림을 배포합니다.

    방법은 문서를 참조하세요:

    ", + "how-to-configure-notifications": "Slack 구성 방법" + } + }, + "tenant-admin": { + "step1": { + "title": "장치 생성", + "content": "

    UI를 통해 플랫폼에 첫 번째 장치를 프로비저닝해 보겠습니다. 방법은 문서를 참조하세요:

    ", + "how-to-create-device": "장치 생성 방법" + }, + "step2": { + "title": "장치 연결", + "content-before": "

    장치를 연결하려면 장치 자격 증명을 가져와야 합니다. 이 가이드에서는 기본 자동 생성 자격 증명인 액세스 토큰을 사용하는 것을 권장합니다.

    • 장치 테이블로 이동
    • 장치 행을 클릭하여 장치 세부 정보 열기
    • \"액세스 토큰 복사\" 버튼 누르기

    간단한 명령을 사용하여 HTTP를 통해 데이터를 게시합니다. $ACCESS_TOKEN을 장치 액세스 토큰으로 교체하는 것을 잊지 마세요:

    ", + "ubuntu": { + "install-curl": "Ubuntu용 cURL 설치:" + }, + "macos": { + "install-curl": "MacOS용 cURL 설치:" + }, + "windows": { + "install-curl": "Windows 10 b17063부터 cURL이 기본적으로 사용 가능합니다." + }, + "replace-access-token": "$ACCESS_TOKEN을 장치의 토큰으로 교체:", + "content-after": "

    MQTT, CoAP 등 다른 프로토콜도 사용할 수 있습니다.

    방법은 문서를 참조하세요:

    ", + "how-to-connect-device": "장치 연결 방법" + }, + "step3": { + "title": "대시보드 생성", + "content": "

    자산, 장치 등의 엔티티에서 데이터를 시각화하는 대시보드를 만듭니다.

    방법은 문서를 참조하세요:

    ", + "how-to-create-dashboard": "대시보드 생성 방법" + }, + "step4": { + "title": "알람 규칙 구성", + "alarm-rules": "알람 규칙", + "content": "

    온도가 25°C에 도달하면 알림을 발생시켜 보겠습니다. 방법은 문서를 참조하세요:

    ", + "how-to-configure-alarm-rules": "알림 규칙 구성 방법" + }, + "step5": { + "title": "알람 생성", + "content-before": "

    알림을 트리거하려면 26°C 이상의 새 텔레메트리 값을 보냅니다.

    ", + "replace-access-token": "$ACCESS_TOKEN을 장치의 토큰으로 교체:", + "content-after": "

    방법은 문서를 참조하세요:

    ", + "how-to-create-alarm": "알림 생성 방법" + }, + "step6": { + "title": "고객 생성 및 대시보드 할당", + "content": "

    최종 사용자 대시보드를 생성하면 고객 사용자는 자신의 장치만 볼 수 있으며 다른 고객의 데이터는 숨겨집니다.

    방법은 문서를 참조하세요:

    ", + "how-to-create-customer-and-assign-dashboard": "고객 생성 및 대시보드 할당 방법" + } + } + }, + "api-usage": { + "api-usage": "Api 사용량", + "label": "라벨", + "state-name": "상태 이름", + "status": "상태", + "status-required": "상태는 필수 항목입니다.", + "limit": "최대 제한", + "limit-required": "최대 제한이 필요합니다.", + "current-number": "현재 수", + "current-number-required": "현재 수는 필수 항목입니다.", + "add-key": "키 추가", + "no-key": "키 없음", + "delete-key": "키 삭제", + "target-dashboard-state": "대상 대시보드 상태", + "go-to-main-state": "기본 보기로 이동" + }, + "unit-conversion": { + "hint": "저장된 값의 단위입니다. 변환할 원본 단위입니다. 소스 데이터에서 사용하는 기호를 입력하세요 (예: m, km, ft, in)." } }, + "color": { + "color": "색상" + }, "icon": { "icon": "아이콘", + "icons": "아이콘", + "custom": "사용자 지정", "select-icon": "선택된 아이콘", - "material-icons": "Material icons", - "show-all": "모든 아이콘 보기" + "material-icons": "머티리얼 아이콘", + "show-all": "모든 아이콘 보기", + "search-icon": "아이콘 검색", + "no-icons-found": "'{{iconSearch}}'에 대한 아이콘을 찾을 수 없음" + }, + "phone-input": { + "phone-input-label": "전화번호", + "phone-input-required": "전화번호는 필수 항목입니다.", + "phone-input-validation": "전화번호가 유효하지 않거나 불가능합니다.", + "phone-input-pattern": "잘못된 전화번호입니다. E.164 형식을 사용하세요 (예: {{phoneNumber}})", + "phone-input-hint": "E.164 형식의 전화번호 (예: {{phoneNumber}})" }, "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" + "action-cell-button": "동작 셀 버튼", + "row-click": "행 클릭 시", + "cell-click": "셀 클릭 시", + "polygon-click": "다각형 클릭 시", + "marker-click": "마커 클릭 시", + "circle-click": "원 클릭 시", + "tooltip-tag-action": "툴팁 태그 동작", + "node-selected": "노드 선택 시", + "element-click": "HTML 요소 클릭 시", + "pie-slice-click": "슬라이스 클릭 시", + "row-double-click": "행 더블 클릭 시", + "cell-double-click": "셀 더블 클릭 시", + "card-click": "카드 클릭 시", + "click": "클릭 시" } }, + "paginator": { + "items-per-page": "페이지당 항목:", + "first-page-label": "첫 페이지", + "last-page-label": "마지막 페이지", + "next-page-label": "다음 페이지", + "previous-page-label": "이전 페이지", + "items-per-page-separator": "/" + }, "language": { + "auto": "자동", "language": "언어(Language)" } } diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json index c3ddb5af74..893453031b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -3767,7 +3767,6 @@ "two-factor-authentication": "Dviejų veiksnių autentifikavimas", "passwords-mismatch-error": "Įvesti slaptažodžiai turi sutapti!", "password-again": "Pakartokite slaptažodį", - "sign-in": "Prisijungti", "username": "Vartotojo vardas (el. paštas)", "remember-me": "Prisiminti mane", "forgot-password": "Pamiršote slaptažodį?", diff --git a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json index bba71609ec..1eed936bf0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json +++ b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json @@ -1178,7 +1178,6 @@ "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?", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index 3c8bfbde60..965cc9a3aa 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -3847,7 +3847,6 @@ "two-factor-authentication": "Twee-factor-authenticatie", "passwords-mismatch-error": "Ingevoerde wachtwoorden moeten hetzelfde zijn.", "password-again": "Wachtwoord opnieuw ingeven", - "sign-in": "Gelieve in te loggen", "username": "Gebruikersnaam (e-mail)", "remember-me": "Onthoud mij", "forgot-password": "Wachtwoord vergeten?", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_NL.json b/ui-ngx/src/assets/locale/locale.constant-nl_NL.json index f9adb7f86c..7926c16961 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_NL.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_NL.json @@ -78,6 +78,7 @@ "show-more": "Meer weergeven", "dont-show-again": "Niet opnieuw weergeven", "see-documentation": "Bekijk documentatie", + "see-debug-events": "Debuggebeurtenissen bekijken", "clear": "Wissen", "upload": "Uploaden", "delete-anyway": "Toch verwijderen", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Twee-factor-authenticatie", "available-providers": "Beschikbare providers", + "available-providers-required": "Er moet ten minste één 2FA-provider zijn geconfigureerd.", "issuer-name": "Naam uitgever", "issuer-name-required": "Naam uitgever is verplicht.", "max-verification-failures-before-user-lockout": "Maximaal aantal verificatiefouten vóór accountvergrendeling", @@ -513,7 +515,9 @@ "verification-message-template-required": "Sjabloon verificatiebericht is verplicht.", "within-time": "Binnen tijd (sec)", "within-time-pattern": "Tijd moet een positief geheel getal zijn.", - "within-time-required": "Tijd is verplicht." + "within-time-required": "Tijd is verplicht.", + "force-2fa": "Twee-factor-authenticatie afdwingen", + "enforce-for": "Afdwingen voor" }, "jwt": { "security-settings": "JWT-beveiligingsinstellingen", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarm", + "alarm-list": "Alarmlijst", "alarms": "Alarmen", "all-alarms": "Alle alarmen", "select-alarm": "Selecteer alarm", @@ -649,7 +654,16 @@ "alarm-type": "Alarmtype", "enter-alarm-type": "Voer alarmtype in", "no-alarm-types-matching": "Geen alarmtypen gevonden die overeenkomen met '{{entitySubtype}}'.", - "alarm-type-list-empty": "Geen alarmtypen geselecteerd." + "alarm-type-list-empty": "Geen alarmtypen geselecteerd.", + "system-comments": { + "acked-by-user": "Alarm is bevestigd door gebruiker {{userName}}", + "cleared-by-user": "Alarm is beëindigd door gebruiker {{userName}}", + "assigned-to-user": "Alarm is toegewezen door gebruiker {{userName}} aan gebruiker {{assigneeName}}", + "unassigned-to-user": "Alarmtoewijzing is verwijderd door gebruiker {{userName}}", + "unassigned-from-deleted-user": "Alarmtoewijzing is verwijderd omdat gebruiker {{userName}} - is verwijderd", + "comment-deleted": "Gebruiker {{userName}} heeft de opmerking verwijderd", + "severity-changed": "Alarmernst is bijgewerkt van {{oldSeverity}} naar {{newSeverity}}" + } }, "alarm-activity": { "add": "Voeg een opmerking toe...", @@ -754,6 +768,7 @@ "name-max-length": "Naam mag maximaal 256 tekens bevatten", "label-max-length": "Label mag maximaal 256 tekens bevatten", "description": "Beschrijving", + "description-required": "Beschrijving is vereist.", "type": "Type", "type-required": "Type is verplicht.", "details": "Details", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Maandelijkse activiteit gegenereerde alarmen", "data-points": "Datapunten", "data-points-storage-days": "Opslagduur van datapunten (dagen)", + "data-points-storage-days-hourly-activity": "Datapuntenopslagdagen: uurlijkse activiteit", + "data-points-storage-days-daily-activity": "Datapuntenopslagdagen: dagelijkse activiteit", + "data-points-storage-days-monthly-activity": "Datapuntenopslagdagen: maandelijkse activiteit", "device-api": "Apparaat-API", "email": "E-mail", "email-messages": "E-mailberichten", @@ -900,6 +918,7 @@ "rule-node": "Rule node", "sms": "SMS", "sms-messages": "SMS-berichten", + "sms-messages-hourly-activity": "SMS-berichten: uurlijkse activiteit", "sms-messages-daily-activity": "Dagelijkse activiteit SMS-berichten", "sms-messages-monthly-activity": "Maandelijkse activiteit SMS-berichten", "successful": "${entityName} Succesvol", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Uurlijkse activiteit telemetrie-opslag", "telemetry-persistence-monthly-activity": "Maandelijkse activiteit telemetrie-opslag", "transport": "Transport", + "transport-msg-hourly-activity": "Transportberichten: uurlijkse activiteit", + "transport-msg-daily-activity": "Transportberichten: dagelijkse activiteit", + "transport-msg-monthly-activity": "Transportberichten: maandelijkse activiteit", "transport-daily-activity": "Dagelijkse activiteit transport", "transport-data-points": "Transport datapunten", + "transport-data-points-hourly-activity": "Transportdatapunten: uurlijkse activiteit", + "transport-data-points-daily-activity": "Transportdatapunten: dagelijkse activiteit", + "transport-data-points-monthly-activity": "Transportdatapunten: maandelijkse activiteit", "view-details": "Details bekijken", "view-statistics": "Statistieken bekijken", - "transport-messages": "Transportberichten" + "transport-messages": "Transportberichten", + "transport-messages-hourly-activity": "Transportberichten: uurlijkse activiteit", + "transport-data-point-hourly-activity": "Transportdatapunt: uurlijkse activiteit", + "javascript-function-executions": "JavaScript-functie-uitvoeringen", + "javascript-function-executions-hourly-activity": "JavaScript-functie-uitvoeringen: uurlijkse activiteit", + "javascript-function-executions-daily-activity": "JavaScript-functie-uitvoeringen: dagelijkse activiteit", + "javascript-function-executions-monthly-activity": "JavaScript-functie-uitvoeringen: maandelijkse activiteit", + "tbel-function-executions": "TBEL-functie-uitvoeringen", + "tbel-function-executions-hourly-activity": "TBEL-functie-uitvoeringen: uurlijkse activiteit", + "tbel-function-executions-daily-activity": "TBEL-functie-uitvoeringen: dagelijkse activiteit", + "tbel-function-executions-monthly-activity": "TBEL-functie-uitvoeringen: maandelijkse activiteit", + "created-reports": "Aangemaakte rapporten", + "created-reports-hourly-activity": "Aangemaakte rapporten: uurlijkse activiteit", + "created-reports-daily-activity": "Aangemaakte rapporten: dagelijkse activiteit", + "created-reports-monthly-activity": "Aangemaakte rapporten: maandelijkse activiteit", + "emails": "E-mails", + "emails-hourly-activity": "E-mails: uurlijkse activiteit", + "emails-daily-activity": "E-mails: dagelijkse activiteit", + "emails-monthly-activity": "E-mails: maandelijkse activiteit", + "status": { + "enabled": "Ingeschakeld", + "disabled": "Uitgeschakeld", + "warning": "Waarschuwing" + } }, "api-limit": { "cassandra-write-queries-core": "Rest API Cassandra schrijfbewerkingen", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Edge-uplinkberichten", "edge-uplink-messages-per-edge": "Edge-uplinkberichten per edge" }, + "api-key": { + "api-key": "API-sleutel", + "api-keys": "API-sleutels", + "delete-api-key-title": "Weet u zeker dat u de API-sleutel '{{name}}' wilt verwijderen?", + "delete-api-key-text": "Let op: na bevestiging kan de sleutel niet meer worden hersteld.", + "delete-api-keys-title": "Weet u zeker dat u { count, plural, =1 {1 API-sleutel} other {# API-sleutels} } wilt verwijderen?", + "delete-api-keys-text": "Let op: na bevestiging kunnen alle geselecteerde sleutels niet meer worden hersteld.", + "expiration-date": "Vervaldatum", + "date": "datum", + "description": "Beschrijving", + "disable": "Uitschakelen", + "edit-description": "Beschrijving bewerken", + "enable": "API-sleutel inschakelen ", + "expiration-time": "Vervaltijd", + "expiration-time-never": "Nooit", + "expiration-time-custom": "Aangepast", + "generate": "Genereren", + "generate-title": "API-sleutel genereren", + "generate-text": "Opmerking: de API-sleutel erft de machtigingen van de gebruiker waarvoor deze is aangemaakt.", + "generated-api-key-title": "API-sleutel gegenereerd. Laten we de connectiviteit controleren!", + "generated-api-key-copy": "Zorg ervoor dat u uw API-sleutel nu kopieert en opslaat, want u kunt deze niet opnieuw bekijken.", + "generated-api-key-command": "Gebruik de volgende instructies om de connectiviteit te controleren. Als resultaat zou u de huidige gebruikersinformatie moeten ontvangen:", + "generated-api-key-insecure-url": "Het uitvoeren van opdrachten via een onveilige HTTP-verbinding verzendt uw API-sleutel onversleuteld, waardoor deze kwetsbaar is voor onderschepping.", + "list": "{ count, plural, =1 {Eén API-sleutel} other {Lijst met # API-sleutels} }", + "manage": "Beheren", + "manage-api-keys": "API-sleutels beheren", + "no-found": "Geen API-sleutels gevonden", + "selected-api-keys": "{ count, plural, =1 {1 API-sleutel} other {# API-sleutels} } geselecteerd", + "search": "API-sleutels zoeken", + "status": "Status", + "status-active": "Actief", + "status-inactive": "Inactief", + "status-expired": "Verlopen" + }, "audit-log": { "audit": "Audit", "audit-logs": "Auditlogs", @@ -991,7 +1073,11 @@ "type-provision-failure": "Provisioning apparaat mislukt", "type-timeseries-updated": "Telemetrie bijgewerkt", "type-timeseries-deleted": "Telemetrie verwijderd", - "type-sms-sent": "SMS verzonden" + "type-sms-sent": "SMS verzonden", + "any-type": "Alle typen", + "audit-log-filter-title": "Auditlogfilter", + "filter-title": "Filter", + "filter-types": "Auditlogtypen" }, "debug-settings": { "label": "Debugconfiguratie", @@ -1009,15 +1095,29 @@ "expression": "Expressie", "no-found": "Geen berekende velden gevonden", "list": "{ count, plural, =1 {Eén berekend veld} other {Lijst van # berekende velden} }", + "add": "Berekend veld toevoegen", "selected-fields": "{ count, plural, =1 {1 berekend veld} other {# berekende velden} } geselecteerd", "type": { "simple": "Eenvoudig", - "script": "Script" + "simple-hint": "Eenvoudige rekenkundige berekening op basis van invoerargumenten.", + "script": "Script", + "script-hint": "Berekening over gedefinieerde argumenten met behulp van een TBEL-script.", + "geofencing": "Geofencing", + "geofencing-hint": "Evaluatie van de GPS-positie van de entiteit en overgangen ten opzichte van geconfigureerde geofencingzonegroepen.", + "propagation": "Propagatie", + "propagation-hint": "Propagatie van gegevens naar bovenliggende of onderliggende entiteiten op basis van relatierichting en relatietype.", + "related-entities-aggregation": "Aggregatie van gerelateerde entiteiten", + "related-entities-aggregation-hint": "Aggregatie van de nieuwste gegevens van gerelateerde entiteiten.", + "time-series-data-aggregation": "Aggregatie van tijdreeksgegevens", + "time-series-data-aggregation-hint": "Aggregatie van historische gegevens van een huidige entiteit." }, + "preview": "Voorbeeld", "arguments": "Argumenten", "decimals-by-default": "Standaard aantal decimalen", "debugging": "Debuggen berekend veld", + "calculated-field-details": "Details van berekend veld", "argument-name": "Argumentnaam", + "name": "Naam", "datasource": "Databron", "add-argument": "Argument toevoegen", "test-script-function": "Scriptfunctie testen", @@ -1029,6 +1129,8 @@ "argument-asset": "Asset", "argument-customer": "Klant", "argument-tenant": "Huidige tenant", + "argument-owner": "Huidige eigenaar", + "argument-relation-query": "Gerelateerde entiteiten", "argument-type": "Argumenttype", "attribute": "Attribuut", "copy-argument-name": "Argumentnaam kopiëren", @@ -1042,12 +1144,14 @@ "shared-attributes": "Gedeelde attributen", "attribute-key": "Attribuutsleutel", "default-value": "Standaardwaarde", + "default-value-required": "Standaardwaarde is vereist.", "limit": "Maximale waarden", "time-window": "Tijdsvenster", "customer-name": "Klantnaam", "asset-name": "Assetnaam", "timeseries": "Tijdreeks", "output": "Uitvoer", + "output-hint": "Bepaalt hoe de uitvoer wordt verwerkt.", "create": "Nieuw berekend veld aanmaken", "file": "Berekend veld-bestand", "invalid-file-error": "Ongeldig bestandsformaat. Zorg ervoor dat het een geldig JSON-bestand is.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Let op: na bevestiging worden alle geselecteerde berekende velden verwijderd en zijn de gegevens onherstelbaar.", "test-with-this-message": "Test met dit bericht", "use-latest-timestamp": "Gebruik laatste tijdstempel", + "entity-coordinates": "Entiteitscoördinaten", + "latitude-time-series-key": "Tijdreeksleutel voor breedtegraad", + "latitude-time-series-key-required": "Tijdreeksleutel voor breedtegraad is vereist.", + "longitude-time-series-key": "Tijdreeksleutel voor lengtegraad", + "longitude-time-series-key-required": "Tijdreeksleutel voor lengtegraad is vereist.", + "geofencing-zone-groups": "Geofencingzonegroepen", + "geofencing-zone-groups-settings": "Instellingen voor geofencingzonegroepen", + "target-zone": "Doelzone", + "perimeter-key": "Perimetersleutel", + "report-strategy": "Rapportagestrategie", + "no-zone-configured": "Er is ten minste één zone vereist.", + "no-zone-configured-required": "Er moet ten minste één zonegroep zijn geconfigureerd.", + "add-zone-group": "Zonegroep toevoegen", + "report-transition-event-only": "Alleen overgangsgebeurtenissen", + "report-presence-status-only": "Alleen aanwezigheidsstatus", + "report-transition-event-and-presence": "Aanwezigheidsstatus en overgangsgebeurtenissen", + "perimeter-attribute-key": "Perimeterattribuutsleutel", + "perimeter-attribute-key-required": "Perimeterattribuutsleutel is vereist.", + "perimeter-attribute-key-pattern": "Perimeterattribuutsleutel is ongeldig.", + "entity-zone-relationship": "Pad van entiteit naar zones", + "direction": "Relatierichting", + "direction-from": "Van entiteit naar zone", + "direction-to": "Van zone naar entiteit", + "relation-type": "Relatietype", + "create-relation-with-matched-zones": "Relaties aanmaken voor bronentiteit met overeenkomende zones", + "relation-level": "Relatieniveau", + "fetch-last-available-level": "Alleen laatst beschikbare niveau ophalen", + "zone-group-refresh-interval": "Vernieuwingsinterval voor zonegroepen", + "copy-zone-group-name": "Zonegroepnaam kopiëren", + "open-details-page": "Detailspagina van entiteit openen", + "level": "Niveau", + "direction-level": "Richting", + "direction-up": "Omhoog", + "direction-up-parent": "Omhoog naar bovenliggend", + "direction-down": "Omlaag", + "direction-down-child": "Omlaag naar onderliggend", + "add-level": "Niveau toevoegen", + "delete-level": "Niveau verwijderen", + "no-level": "Geen niveau geconfigureerd", + "levels-required": "Er moet ten minste één niveau zijn geconfigureerd.", + "max-allowed-levels-error": "Relatieniveau overschrijdt het maximaal toegestane.", + "propagation-path-related-entities": "Propagatiepad naar gerelateerde entiteiten", + "propagate-type": { + "arguments-only": "Alleen argumenten", + "expression-result": "Berekeningsresultaat" + }, + "script": "Script", + "data-propagate": "Gegevens om te propageren", + "output-key": "Uitvoersleutel", + "copy-output-key": "Uitvoersleutel kopiëren", + "aggregation-path-related-entities": "Aggregatiepad naar gerelateerde entiteiten", + "deduplication-interval": "Deduplicatie-interval", + "deduplication-interval-min": "Deduplicatie-interval moet ten minste {{ sec }} seconden zijn.", + "deduplication-interval-hint": "Minimale tijd tussen telemetrie-aggregaties.", + "deduplication-interval-required": "Deduplicatie-interval is vereist.", + "calculated-field-filter-title": "Filter voor berekend veld", + "filter-title": "Filter", + "calculated-field-types": "Typen berekend veld", + "events": "Gebeurtenissen", + "any-type": "Alle typen", + "metrics": { + "metrics": "Metrieken", + "metrics-empty": "Er moet ten minste één metriek zijn geconfigureerd.", + "metric-name": "Metrieknaam", + "metric-name-required": "Metrieknaam is vereist.", + "metric-name-pattern": "Metrieknaam is ongeldig.", + "metric-name-duplicate": "Er bestaat al een metriek met deze naam.", + "metric-name-max-length": "Metrieknaam moet korter zijn dan 256 tekens.", + "metric-name-forbidden": "Metrieknaam is gereserveerd en kan niet worden gebruikt.", + "copy-metric-name": "Metrieknaam kopiëren", + "argument-name": "Argumentnaam", + "aggregation": "Aggregatie", + "aggregation-type": { + "avg": "Gemiddelde", + "min": "Minimum", + "max": "Maximum", + "sum": "Som", + "count": "Aantal", + "count-unique": "Uniek aantal" + }, + "filtered": "Gefilterd", + "value-source": "Waardebron", + "value-source-hint": "Bepaalt hoe de waarde voor aggregatie wordt verkregen.", + "value-source-type": { + "key": "Sleutel", + "function": "Functie" + }, + "no-metrics-configured": "Er is ten minste één metriek vereist.", + "add-metric": "Metriek toevoegen", + "max-metrics": "Maximumaantal metrieken bereikt.", + "metric-settings": "Metriekinstellingen", + "filter": "Filter", + "filter-hint": "Schakelt het filteren van entiteiten tijdens aggregatie in. De filterfunctie moet een booleaanse waarde retourneren en kan alle geconfigureerde argumenten gebruiken." + }, + "output-strategy": { + "strategy": "Strategie", + "process-right-away": "Onmiddellijk verwerken", + "process-rule-chains": "Verwerken via Regelketens", + "save-time-series": "Opslaan in tijdreeksgegevens", + "save-database": "Opslaan in database", + "save-latest-values": "Opslaan in nieuwste waarden", + "send-web-sockets": "Verzenden naar WebSockets", + "save-calculated-fields": "Verzenden naar Berekende velden", + "update-attribute-only-on-value-change": "Attribuut alleen bij waardewijziging bijwerken", + "send-attributes-updated-notification": "'Attributes Updated'-notificatie verzenden", + "ttl": "Aangepaste TTL", + "ttl-required": "TTL is vereist", + "ttl-min": "Alleen een minimale TTL van 0 is toegestaan", + "processing-parameters": "Verwerkingsparameters", + "hint": { + "strategy": "Bepaalt of het resultaat onmiddellijk wordt verwerkt of naar een regelketen wordt verzonden voor aanvullende verwerking.", + "processing-options": "Verwerkingsopties", + "update-attribute-only-on-value-change": "Werkt het attribuut bij bij elk binnenkomend bericht, ongeacht of de waarde is gewijzigd. Dit verhoogt het API-gebruik en verlaagt de prestaties.", + "update-attribute-only-on-value-change-enabled": "Werkt het attribuut alleen bij wanneer de waarde wijzigt. Als de waarde ongewijzigd is, worden tijdstempels niet bijgewerkt en worden er geen notificaties verzonden.", + "send-attributes-updated-notification": "Verzendt een 'Attributes Updated'-gebeurtenis naar de standaardregelketen.", + "save-time-series": "Slaat tijdreeksgegevens op in de ts_kv-tabel in de database.", + "save-database": "Slaat attribuutgegevens op in de database.", + "save-latest-values": "Werkt tijdreeksgegevens bij in de ts_kv_latest-tabel in de database als de nieuwe waarde recenter is.", + "send-web-sockets-attribute": "Stelt WebSocket-abonnementen op de hoogte van updates van de attribuutgegevens.", + "send-web-sockets-time-series": "Stelt WebSocket-abonnementen op de hoogte van updates van de tijdreeksgegevens.", + "save-calculated-fields-attribute": "Stelt berekende velden op de hoogte van updates van de attribuutgegevens.", + "save-calculated-fields-time-series": "Stelt berekende velden op de hoogte van updates van de tijdreeksgegevens.", + "ttl": "Bepaalt de bewaarperiode voor tijdreeksgegevens. Als dit is uitgeschakeld, wordt de TTL van het Tenantprofiel gebruikt." + } + }, + "aggregate-interval-type": "Aggregatieintervaltype", + "aggregate-interval-value": "Aggregatieintervalwaarde", + "aggregate-interval-value-required": "Aggregatieintervalwaarde is vereist.", + "aggregate-interval-value-min": "Aggregatieintervalwaarde moet ten minste { sec, plural, =0 {0 seconde} =1 {1 seconde} other {# seconden} } zijn.", + "aggregate-interval-value-step-multiple-of": "Aggregatieintervalwaarde moet een deler of veelvoud van 1 dag zijn.", + "aggregate-period": { + "hour": "Uur", + "day": "Dag", + "week": "Week (ma - zo)", + "week-sun-sat": "Week (zo - za)", + "month": "Maand", + "quarter": "Kwartaal", + "year": "Jaar", + "custom": "Aangepast" + }, + "aggregate-period-hint-offset": "Uw aggregatieinterval is: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Uw aggregatieinterval is: {{ interval }} en zo verder.", + "entity-aggregation": { + "argument-hint": "Gegevens worden opgehaald van de huidige entiteit.", + "argument-title-hint": "Bepaalt de invoerargumenten die voor aggregatie worden gebruikt.", + "argument-setting-hint": "Nieuwste telemetrie is het enige beschikbare argumenttype voor dit berekende veld.", + "aggregation-interval": "Aggregatieinterval", + "aggregation-interval-hint": "Bepaalt hoe vaak aggregatie wordt uitgevoerd. Voorbeeld: elke 1 uur worden gegevens geaggregeerd om 00:00, 01:00, 02:00, enz. Aggregatieresultaten worden opgeslagen met de tijdstempel die overeenkomt met het begin van het aggregatieinterval.", + "apply-offset": "Offset toepassen op aggregatieinterval", + "apply-offset-hint": "Bepaalt hoeveel het begin van elke aggregatieperiode wordt verschoven (bijv. +10 minuten - 00:10, 01:10).", + "offset-value": "Offsetwaarde", + "offset-value-required": "Offsetwaarde is vereist.", + "offset-value-min": "Offsetwaarde moet een positief geheel getal zijn.", + "offset-value-max": "Offsetwaarde moet kleiner zijn dan de waarde van het aggregatieinterval.", + "wait-delay": "Wachttime-out toepassen voor vertraagde telemetrie", + "wait-delay-hint": "Bepaalt hoe lang er na het einde van het interval op vertraagde telemetrie wordt gewacht. Als dergelijke telemetrie binnenkomt, wordt het resultaat voor dat interval opnieuw berekend.", + "duration": "Duur", + "duration-required": "Duur is vereist.", + "duration-min": "Duur moet minimaal 1 minuut zijn.", + "duration-hint": "Hoelang er wordt gewacht op vertraagde gegevens nadat het interval is afgelopen.", + "produce-intermediate-result": "Tussenresultaat genereren", + "produce-intermediate-result-hint": "Berekent metrieken tijdens het huidige interval om een tussenresultaat te genereren. Updates vinden niet vaker plaats dan één keer per {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Een berekend veld van het type Eenvoudig mag geen sleutels bevatten met het type 'Time series rolling'.", + "arguments-propagate-arguments-with-rolling": "Het type 'Time series rolling' is niet compatibel met propagatie van 'Arguments only'.", + "arguments-propagate-argument-entity-type": "Entiteittype is niet compatibel met propagatie van 'Arguments only'.", + "arguments-propagate-argument-must-current-entity": "Ten minste één argument moet zijn geconfigureerd met het bronentiteittype 'Current entity'.", "arguments-empty": "Er moet ten minste één argument worden opgegeven.", "expression-required": "Expressie is vereist.", "expression-invalid": "Expressie is ongeldig", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Een argument met deze naam bestaat al.", "argument-name-max-length": "Argumentnaam mag niet langer zijn dan 256 tekens.", "argument-name-forbidden": "Deze argumentnaam is gereserveerd en mag niet worden gebruikt.", + "output-key-required": "Uitvoersleutel is vereist.", + "output-key-pattern": "Uitvoersleutel is ongeldig.", + "output-key-duplicate": "Er bestaat al een sleutel met deze naam.", + "output-key-max-length": "Uitvoersleutel moet korter zijn dan 256 tekens.", + "output-key-forbidden": "Uitvoersleutel is gereserveerd en kan niet worden gebruikt.", + "entity-type-required": "Entiteittype is vereist", + "name-required": "Naam is vereist.", + "name-pattern": "Naam is ongeldig.", + "name-duplicate": "Er bestaat al een item met deze naam.", + "name-max-length": "Naam moet korter zijn dan 256 tekens.", + "name-forbidden": "Naam is gereserveerd en kan niet worden gebruikt.", "argument-type-required": "Argumenttype is vereist.", "max-args": "Maximum aantal argumenten bereikt.", "decimals-range": "Het aantal decimalen moet een getal zijn tussen 0 en 15.", "expression": "Standaardexpressie toont hoe temperatuur van Fahrenheit naar Celsius wordt omgerekend.", "arguments-entity-not-found": "Doelentiteit van het argument niet gevonden.", - "use-latest-timestamp": "Indien ingeschakeld, wordt de berekende waarde opgeslagen met de meest recente tijdstempel van de telemetrie van de argumenten, in plaats van met de servertijd." + "use-latest-timestamp": "Indien ingeschakeld, wordt de berekende waarde opgeslagen met de meest recente tijdstempel van de telemetrie van de argumenten, in plaats van met de servertijd.", + "entity-coordinates": "Geef de tijdreeksleutels op die de GPS-coördinaten van de entiteit (breedtegraad en lengtegraad) leveren.", + "geofencing-zone-groups": "Definieer één of meer geofencingzonegroepen om te controleren (bijv. 'allowedZones', 'restrictedZones'). Elke groep moet een unieke naam hebben, die wordt gebruikt als voorvoegsel voor uitvoertelemetriesleutels van het berekende veld.", + "perimeter-attribute-key": "Stel de attribuutsleutel in die de perimeterdefinitie van de geofencingzone bevat. De perimeter wordt altijd genomen uit server-side-attributen van de zone-entiteit.", + "report-strategy": "Aanwezigheidsstatus rapporteert of de entiteit zich momenteel BINNEN of BUITEN de zonegroep bevindt. Overgangsgebeurtenissen rapporteren wanneer de entiteit de zonegroep BETRAD of VERLIET.", + "create-relation-with-matched-zones": "Maak automatisch relaties aan en onderhoud deze tussen de entiteit en de zones waarin deze zich momenteel bevindt. Relaties worden verwijderd wanneer de entiteit een zone verlaat en aangemaakt wanneer deze een nieuwe zone binnenkomt.", + "relation-type-required": "Relatietype is vereist.", + "relation-level-required": "Relatieniveau is vereist.", + "relation-level-min": "Minimale waarde voor relatieniveau is 1.", + "relation-level-max": "Maximale waarde voor relatieniveau is {{max}}.", + "geofencing-empty": "Er moet ten minste één zonegroep zijn geconfigureerd.", + "geofencing-entity-not-found": "Geofencingdoelentiteit niet gevonden.", + "max-geofencing-zone": "Maximumaantal geofencingzones bereikt.", + "zone-group-refresh-interval": "Bepaalt hoe vaak zonegroepen die via gerelateerde entiteiten zijn geconfigureerd, worden vernieuwd.", + "zone-group-refresh-interval-required": "Vernieuwingsinterval voor zonegroepen is vereist.", + "zone-group-refresh-interval-min": "Vernieuwingsinterval voor zonegroepen moet ten minste {{ min }} seconden zijn.", + "propagation-path-related-entities": "Definieert een direct pad op één niveau naar een gerelateerde entiteit op basis van de geselecteerde richting en het relatietype. Alleen relaties tussen apparaat-, asset-, klant- en tenantentiteiten worden ondersteund. Het maximumaantal entiteiten dat via het relatiepad wordt opgelost is {{ max }}.", + "data-propagate": "Bepaalt de gegevens die moeten worden gepropageerd vanuit de hieronder geconfigureerde argumenten. 'Alleen argumenten' gebruikt de opgehaalde gegevens rechtstreeks, terwijl 'Berekeningsresultaat' een nieuwe waarde berekent op basis van die gegevens.", + "aggregation-path-related-entities": "Definieert een aggregatiepad op één niveau via directe relaties met bovenliggende of onderliggende entiteiten, op basis van richting en relatietype. Alleen relaties tussen apparaat-, asset-, klant- en tenantentiteiten worden ondersteund. Het maximumaantal entiteiten dat via het relatiepad wordt opgelost is {{ max }}.", + "arguments-aggregation": "Bepaalt de invoerargumenten die worden gebruikt voor filteren en aggregatie.", + "setting-arguments-aggregation": "Gegevens worden opgehaald van gerelateerde entiteiten die zijn geconfigureerd in het aggregatiepad.", + "metrics": "Bepaalt metrieken die worden geaggregeerd op basis van geconfigureerde argumenten.", + "entity-aggregation-metrics": "Bepaalt metrieken die op basis van geconfigureerde argumenten over de opgegeven tijdintervallen worden geaggregeerd.", + "import-invalid-calculated-field-type": "Kan berekend veld niet importeren: Ongeldige structuur van berekend veld.", + "simple-expression-title": "Rekenkundige expressie die bepaalt hoe de berekende waarde wordt berekend.", + "script-title": "TBEL-script dat de berekeningslogica en uitvoerwaarden definieert.", + "simple-arguments": "Rekenkundige expressie die bepaalt hoe de berekende waarde wordt berekend.", + "script-arguments": "Bepaalt de invoerargumenten die beschikbaar zijn voor het script." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Alarmregels", + "alarm-rule": "Alarmregel", + "alarm-rules": "Alarmregels", + "alarm-rules-old": "Oud", + "alarm-rules-actual": "Actueel", + "severities": "Ernstniveaus", + "cleared": "Beëindigingsvoorwaarde", + "delete-title": "Weet u zeker dat u de alarmregel '{{title}}' wilt verwijderen?", + "delete-text": "Let op: na bevestiging kunnen de alarmregel en alle gerelateerde gegevens niet meer worden hersteld.", + "delete-multiple-title": "Weet u zeker dat u { count, plural, =1 {1 alarmregel} other {# alarmregels} } wilt verwijderen?", + "delete-multiple-text": "Let op: na bevestiging worden alle geselecteerde alarmregels verwijderd en kunnen alle gerelateerde gegevens niet meer worden hersteld.", + "create": "Nieuwe alarmregel maken", + "add": "Alarmregel toevoegen", + "copy": "Alarmregelconfiguratie kopiëren", + "details": "Alarmregeldetails", + "no-found": "Geen alarmregels gevonden", + "list": "{ count, plural, =1 {Eén alarmregel} other {Lijst met # alarmregels} }", + "selected-fields": "{ count, plural, =1 {1 alarmregel} other {# alarmregels} } geselecteerd", + "import": "Alarmregel importeren", + "file": "Alarmregelbestand", + "export": "Alarmregel exporteren", + "export-failed-error": "Kan alarmregel niet exporteren: {{error}}", + "entity-type": "Entiteittype", + "entity-type-required": "Entiteittype is vereist.", + "alarm-type": "Alarmtype", + "alarm-type-hint": "Unieke identificatie (bijv. HighTempAlarm) binnen het bereik van de alarminitiator (Apparaat, Asset, enz.) om conflicten te voorkomen.", + "alarm-type-required": "Alarmtype is vereist.", + "alarm-type-pattern": "Alarmtype is ongeldig.", + "alarm-type-max-length": "Alarmtype moet korter zijn dan 256 tekens.", + "clear-alarm": "Alarm beëindigen", + "value-argument": "Argument", + "value-argument-required": "Argument is vereist.", + "static-settings": "Statische instellingen", + "configuration": "Configuratie", + "static-schedule": "Statisch", + "dynamic-schedule": "Dynamisch", + "operation-and": "EN", + "operation-or": "OF", + "condition-during": "Gedurende {{during}}", + "condition-during-dynamic": "Gedurende \"{{ attribute }}\"", + "condition-repeat-times": "Herhaalt { count, plural, =1 {1 keer} other {# keer} }", + "condition-repeat-times-dynamic": "Herhaalt \"{{ attribute }}\" keer", + "filter-preview": "Filtervoorbeeld", + "condition-settings": "Voorwaarde-instellingen", + "static": "Statisch", + "dynamic": "Dynamisch", + "argument-filters": "Argumentfilters", + "argument-name": "Argumentnaam", + "value-type": "Waardetype", + "general": "Algemeen", + "filters": "Filters", + "date-time-hint": "Het argument moet in epoch-millisconden zijn. Voorbeeld: 1698839340000 is gelijk aan 2023-11-01 12:49:00 UTC.", + "operation": "Bewerking", + "value-source": "Waardebron", + "value": "Waarde", + "ignore-case": "Hoofdlettergebruik negeren", + "condition": "Voorwaarde", + "script": "Script", + "add-filter": "Argumentfilter toevoegen", + "edit-filter": "Argumentfilter", + "remove-filter": "Argumentfilter verwijderen", + "no-filter": "Er is ten minste één filter vereist.", + "conditions": { + "simple": "Eenvoudig", + "duration": "Duur", + "repeating": "Herhalend" + }, + "schedule-title": "Planning", + "edit-schedule": "Alarmplanning bewerken", + "schedule-type": "Plannertype", + "schedule-type-required": "Plannertype is vereist.", + "schedule": { + "any-time": "Altijd actief", + "specific-time": "Actief op een specifiek tijdstip", + "custom": "Aangepast" + }, + "schedule-day": { + "monday": "Maandag", + "tuesday": "Dinsdag", + "wednesday": "Woensdag", + "thursday": "Donderdag", + "friday": "Vrijdag", + "saturday": "Zaterdag", + "sunday": "Zondag" + }, + "schedule-days": "Dagen", + "schedule-time": "Tijd", + "schedule-time-from": "Van", + "schedule-time-to": "Tot", + "schedule-days-of-week-required": "Er moet ten minste één dag van de week worden geselecteerd.", + "tbel": "TBEL", + "expression-type": { + "simple": "Eenvoudig", + "script": "Script" + }, + "operation-type": { + "and": "En", + "or": "Of" + }, + "filter-predicate-type": { + "string": "String", + "numeric": "Numeriek", + "boolean": "Booleaans", + "complex": "Complex" + }, + "alarm-rule-additional-info": "Aanvullende informatie", + "edit-alarm-rule-additional-info": "Aanvullende informatie bewerken", + "alarm-rule-additional-info-placeholder": "Geef hier uw opmerkingen en aanpassingen op om ze te tonen in Alarmdetails onder Aanvullende informatie", + "alarm-rule-additional-info-hint": "Tip: gebruik ${Argumentnaam} om waarden te vervangen van de argumenten die in de alarmregelvoorwaarde worden gebruikt.", + "alarm-rule-additional-info-icon-hint": "Gebruik Argumentnaam om waarden te vervangen van de argumenten die in de alarmregelvoorwaarde worden gebruikt.", + "alarm-rule-mobile-dashboard": "Mobiel dashboard", + "alarm-rule-mobile-dashboard-hint": "Wordt door de mobiele applicatie gebruikt als dashboard voor alarmdetails.", + "alarm-rule-no-mobile-dashboard": "Geen dashboard geselecteerd", + "alarm-rule-condition": "Alarmregelvoorwaarde", + "enter-alarm-rule-condition-prompt": "Voorwaarde toevoegen", + "enter-alarm-rule-clear-condition-prompt": "Beëindigingsvoorwaarde toevoegen", + "edit-alarm-rule-condition": "Alarmvoorwaarde", + "condition-type": "Voorwaardetype", + "condition-type-hint": "De opties \"Duur\" en \"Herhalend\" zijn niet beschikbaar wanneer de bewerking \"Missing for\" in het filter wordt gebruikt.", + "select-alarm-severity": "Alarmernst selecteren", + "add-create-alarm-rule-prompt": "Er is ten minste één triggervoorwaarde vereist.", + "add-create-alarm-rule": "Triggervoorwaarde toevoegen", + "add-clear-alarm-rule": "Beëindigingsvoorwaarde toevoegen", + "condition-duration": "Voorwaardenduur", + "condition-duration-value": "Duurwaarde", + "condition-duration-time-unit": "Tijdseenheid", + "condition-duration-value-range": "Duurwaarde moet in een bereik van 1 tot 2147483647 liggen.", + "condition-duration-value-pattern": "Duurwaarde moet een geheel getal zijn.", + "condition-duration-value-required": "Duurwaarde is vereist.", + "condition-duration-time-unit-required": "Tijdseenheid is vereist.", + "condition-repeating-value": "Aantal gebeurtenissen", + "condition-repeating-value-hint": "Een update van elk alarmregelargument wordt als gebeurtenis geteld", + "condition-repeating-value-range": "Aantal gebeurtenissen moet in een bereik van 1 tot 2147483647 liggen.", + "condition-repeating-value-pattern": "Aantal gebeurtenissen moet een geheel getal zijn.", + "condition-repeating-value-required": "Aantal gebeurtenissen is vereist.", + "create-conditions": "Triggervoorwaarden", + "clear-condition": "Beëindigingsvoorwaarde", + "no-clear-alarm-rule": "Geen beëindigingsvoorwaarde geconfigureerd.", + "advanced-settings": "Geavanceerde instellingen", + "propagate-alarm": "Alarm naar gerelateerde entiteiten propageren", + "alarm-rule-relation-types-list": "Relatietypen", + "alarm-rule-relation-types-list-hint": "Definieert relatietypen om de gerelateerde entiteiten te filteren. Als dit niet is ingesteld, wordt het alarm naar alle gerelateerde entiteiten gepropageerd.", + "propagate-alarm-to-owner": "Alarm naar entiteiteigenaar propageren (Klant of Tenant)", + "propagate-alarm-to-tenant": "Alarm naar Tenant propageren", + "alarm-rule-filter-title": "Alarmregelfilter", + "filter-title": "Filter", + "debugging": "Alarmregeldebugging", + "any-type": "Alle typen", + "enter-alarm-rule-type": "Alarmtype invoeren", + "no-alarm-rule-types-matching": "Geen alarmtypen gevonden die overeenkomen met '{{entitySubtype}}'.", + "alarm-rule-type-list-empty": "Geen alarmtypen geselecteerd.", + "alarm-rule-type-list": "Alarmtypelijst", + "alarm-rule-entity-list": "Entiteitenlijst", + "missing-for": "ontbreekt gedurende", + "time-unit": "Eenheid", + "mode": "Modus", + "type": "Type", + "value-required": "Waarde is vereist.", + "min-value": "Waarde moet 1 of hoger zijn.", + "argument-in-use": "Argument wordt gebruikt als algemeen argument.", + "import-invalid-alarm-rule-type": "Kan alarmregel niet importeren: Ongeldige alarmregelstructuur.", + "no-filter-preview": "Geen filter opgegeven", + "filter-operation": { + "and": "En", + "or": "Of" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Documentatie", "time-left": "{{time}} resterend", "output": "Uitvoer", + "sort-asc": "Oplopend", + "sort-desc": "Aflopend", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Volgorde in mobiele applicatie", "mobile-hide": "Verberg dashboard in mobiele applicatie", "update-image": "Dashboardafbeelding bijwerken", + "update-new-version": "Nieuwe versie uploaden", + "upload-file-to-update": "Bestand uploaden om bij te werken", "take-screenshot": "Schermafbeelding maken", "select-widget-title": "Selecteer widget", "select-widget-value": "{{title}}: selecteer widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrapclient", "bootstrap-server": "Bootstrapserver", "lwm2m-server": "LwM2M-server", + "client-reboot": "Registratie-update-trigger", + "bootstrap-reboot": "Bootstrap-Request-trigger", "client-publicKey-or-id": "Client publieke sleutel of ID", "client-publicKey-or-id-required": "Client publieke sleutel of ID is verplicht.", "client-publicKey-or-id-tooltip-psk": "De PSK-identificatie is een willekeurige identificatie tot 128 bytes zoals beschreven in de standaard [RFC7925].\nDe PSK-identificatie MOET eerst worden omgezet naar een tekenreeks en vervolgens in UTF-8 worden gecodeerd.", @@ -2225,6 +2707,7 @@ "short-id-required": "Korte server-ID is verplicht.", "short-id-range": "Korte server-ID moet tussen {{ min }} en {{ max }} liggen.", "short-id-pattern": "Korte server-ID moet een positief geheel getal zijn.", + "short-id-pattern-bs": "Kort server-ID mag uitsluitend null zijn", "lifetime": "Clientregistratielevensduur", "lifetime-required": "Registratieduur is verplicht.", "lifetime-pattern": "Registratieduur moet een positief geheel getal zijn.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Alle resources worden geobserveerd met één samengesteld Observe-verzoek (efficiënter, minder flexibel)", "composite-by-object": "Samengesteld per object", "composite-by-object-description": "Resources worden gegroepeerd per objecttype en geobserveerd via afzonderlijke samengestelde Observe-verzoeken (gebalanceerde aanpak)" - } + }, + "init-attr-tel-as-obs-strategy": "Attributen en telemetrie initialiseren met Observe-strategie", + "init-attr-tel-as-obs-strategy-hint": "Als false - attributen en telemetrie worden geïnitialiseerd door hun waarden één voor één te lezen.\\nAls true - attributen en telemetrie worden geïnitialiseerd door op hun waarden te abonneren met behulp van de Observe-strategie." }, "snmp": { "add-communication-config": "Communicatieconfiguratie toevoegen", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Regelknopen", "list-of-rulenodes": "{ count, plural, =1 {Eén regelknoop} other {Lijst van # regelknopen} }", "rulenode-name-starts-with": "Regelknopen waarvan de namen beginnen met '{{prefix}}'", + "type-api-key": "API-sleutel", + "type-api-keys": "API-sleutels", "type-current-customer": "Huidige klant", "type-current-tenant": "Huidige tenant", "type-current-user": "Huidige gebruiker", @@ -2656,6 +3143,7 @@ "details": "Entiteitdetails", "no-entities-prompt": "Geen entiteiten gevonden", "no-data": "Geen gegevens om weer te geven", + "show-all-columns": "Alles weergeven", "columns-to-display": "Kolommen om weer te geven", "type-api-usage-state": "API-gebruikstoestand", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Eén mobiele applicatie} other {Lijst van # mobiele applicaties} }", "type-mobile-app-bundle": "Mobiele bundel", "type-mobile-app-bundles": "Mobiele bundels", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Eén mobiele bundel} other {Lijst van # mobiele bundels} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Eén mobiele bundel} other {Lijst van # mobiele bundels} }", + "limit-reached": "Limiet bereikt", + "limit-reached-text": "U heeft de limiet van {{ entities }} bereikt. Vraag uw systeembeheerder om uw limiet voor {{ entity }} te verhogen en meer toe te voegen.", + "request-limit-increase": "Limietverhoging aanvragen", + "request-sysadmin-text": "Bent u de systeembeheerder?", + "login-here": "Hier aanmelden", + "to-increase-limit": "om de limiet te verhogen.", + "increase-limit-request-sent-title": "We hebben een geautomatiseerd verzoek naar uw systeembeheerder gestuurd om de limiet te verhogen", + "increase-limit-request-sent-text": "Geef hen de tijd om het verzoek te beoordelen en de instellingen bij te werken. Mogelijk moet u deze pagina vernieuwen om de wijzigingen te zien." }, "entity-field": { "created-time": "Aangemaakt op", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Resetlink is verzonden", "email": "E-mail", "invalid-email-format": "Ongeldig e-mailformaat.", + "sign-in-with": "Aanmelden met {{name}}", + "sign-in-to-your-account": "Aanmelden bij uw account", "or": "of", "error": "Inlogfout", "verify-your-identity": "Verifieer uw identiteit", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Activatielink is verlopen", "activation-link-expired-message": "De link om uw profiel te activeren is verlopen. U kunt terugkeren naar de inlogpagina om een nieuwe e-mail te ontvangen.", "reset-password-link-expired": "Wachtwoord resetlink is verlopen", - "reset-password-link-expired-message": "De link om uw wachtwoord opnieuw in te stellen is verlopen. U kunt terugkeren naar de inlogpagina om een nieuwe e-mail te ontvangen." + "reset-password-link-expired-message": "De link om uw wachtwoord opnieuw in te stellen is verlopen. U kunt terugkeren naar de inlogpagina om een nieuwe e-mail te ontvangen.", + "two-fa": "Twee-factorauthenticatie", + "two-fa-required": "Twee-factorauthenticatie is vereist", + "set-up-verification-method": "Stel een verificatiemethode in om door te gaan", + "set-up-verification-method-login": "Stel een verificatiemethode in of meld u aan", + "enable-authenticator-app": "Authenticator-app inschakelen", + "enable-authenticator-app-description": "Voer de beveiligingscode in uit uw authenticator-app", + "enable-authenticator-sms": "SMS-authenticator inschakelen", + "enable-authenticator-sms-description": "Voer een 6-cijferige code in die we zojuist hebben verzonden naar ", + "enable-authenticator-email": "E-mailauthenticator inschakelen", + "enable-authenticator-email-description": "Er is een beveiligingscode verzonden naar uw e-mailadres op ", + "enter-key-manually": "of voer deze 32-cijferige sleutel handmatig in:", + "continue": "Doorgaan", + "confirm": "Bevestigen", + "authenticator-app-success": "Authenticator-app succesvol ingeschakeld", + "authenticator-app-success-description": "De volgende keer dat u zich aanmeldt, moet u een twee-factorauthenticatiecode invoeren", + "authenticator-sms-success": "SMS-authenticator succesvol ingeschakeld", + "authenticator-sms-success-description": "De volgende keer dat u zich aanmeldt, wordt u gevraagd de beveiligingscode in te voeren die naar het telefoonnummer wordt verzonden", + "authenticator-email-success": "E-mailauthenticator succesvol ingeschakeld", + "authenticator-email-success-description": "De volgende keer dat u zich aanmeldt, wordt u gevraagd de beveiligingscode in te voeren die naar uw e-mailadres wordt verzonden", + "authenticator-backup-code-success": "Back-upcode succesvol ingeschakeld", + "authenticator-backup-code-success-description": "De volgende keer dat u zich aanmeldt, wordt u gevraagd de beveiligingscode in te voeren of een van de back-upcodes te gebruiken.", + "add-verification-method": "Verificatiemethode toevoegen", + "get-backup-code": "Back-upcode ophalen", + "copy-key": "Sleutel kopiëren", + "send-code": "Code verzenden", + "email-label": "E-mail", + "email-description": "Voer een e-mail in om als authenticator te gebruiken.", + "sms-description": "Voer een telefoonnummer in om als authenticator te gebruiken.", + "backup-code-description": "Druk de codes af zodat u ze bij de hand heeft wanneer u ze nodig heeft om u aan te melden bij uw account. U kunt elke back-upcode één keer gebruiken.", + "backup-code-warn": "Zodra u deze pagina verlaat, kunnen deze codes niet opnieuw worden weergegeven. Bewaar ze veilig met de onderstaande opties.", + "download-txt": "Downloaden (txt)", + "print": "Afdrukken", + "verification-code": "6-cijferige code", + "verification-code-invalid": "Ongeldig verificatiecodeformaat", + "verification-code-incorrect": "Verificatiecode is onjuist", + "verification-code-many-request": "Te veel verzoeken om verificatiecode te controleren", + "scan-qr-code": "Scan deze QR-code met uw verificatie-app", + "phone-input": { + "phone-input-label": "Telefoonnummer", + "phone-input-required": "Telefoonnummer is vereist", + "phone-input-validation": "Telefoonnummer is ongeldig of niet mogelijk", + "phone-input-pattern": "Ongeldig telefoonnummer. Moet in E.164-formaat zijn, bijv. {{phoneNumber}}", + "phone-input-hint": "Telefoonnummer in E.164-formaat, bijv. {{phoneNumber}}" + } }, "mobile": { "add-application": "Applicatie toevoegen", @@ -4175,6 +4717,7 @@ "api-usage-limit": "API-gebruikslimiet", "device-activity": "Apparaatactiviteit", "entities-limit": "Entiteitenlimiet", + "entities-limit-increase-request": "Verzoek om verhoging van entiteitenlimiet", "entity-action": "Entiteithandeling", "general": "Algemeen", "rule-engine-lifecycle-event": "Levenscyclusgebeurtenis van regelengine", @@ -4392,6 +4935,12 @@ "at-least": "Ten minste:", "character": "{ count, plural, =1 {1 teken} other {# tekens} }", "digit": "{ count, plural, =1 {1 cijfer} other {# cijfers} }", + "password-tooltip-min-length": "Minimaal {{minimumLength}} tekens lang", + "password-tooltip-max-length": "Maximaal {{maximumLength}} tekens lang", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} hoofdletter", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} kleine letter", + "password-tooltip-digit": "{{minimumDigits}} cijfer", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} speciaal teken", "incorrect-password-try-again": "Onjuist wachtwoord. Probeer opnieuw", "lowercase-letter": "{ count, plural, =1 {1 kleine letter} other {# kleine letters} }", "new-passwords-not-match": "Nieuw wachtwoord komt niet overeen", @@ -4450,7 +4999,8 @@ "additional-info": "Aanvullende info (JSON)", "invalid-additional-info": "Kan aanvullende info JSON niet ontleden.", "no-relations-text": "Geen relaties gevonden", - "not": "Niet" + "not": "Niet", + "copy-type": "Type kopiëren" }, "resource": { "add": "Bron toevoegen", @@ -5400,7 +5950,8 @@ }, "time-series": "Tijdreeksen", "latest": "Laatste waarden", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Berekende velden en alarmregels" }, "save-attribute": { "processing-settings": "Verwerkingsinstellingen", @@ -5595,7 +6146,8 @@ "bad-request-params": "Ongeldige aanvraagparameters", "item-not-found": "Item niet gevonden", "too-many-requests": "Te veel aanvragen", - "too-many-updates": "Te veel updates" + "too-many-updates": "Te veel updates", + "entities-limit-exceeded": "Entiteitenlimiet overschreden" }, "tenant": { "tenant": "Tenant", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Maximaal aantal argumenten per berekend veld", "max-arguments-per-cf-range": "Maximaal aantal argumenten mag niet negatief zijn", "max-arguments-per-cf-required": "Maximaal aantal argumenten is verplicht", + "max-related-level-per-argument": "Maximaal relatieniveau per argument 'Gerelateerde entiteiten'", + "max-related-level-per-argument-range": "Het maximale relatieniveau per argument 'Gerelateerde entiteiten' mag niet lager zijn dan '1'", + "max-related-level-per-argument-required": "Het maximale relatieniveau per argument 'Gerelateerde entiteiten' is vereist", + "min-allowed-scheduled-update-interval": "Minimaal toegestaan update-interval voor argumenten 'Gerelateerde entiteiten' (seconden)", + "min-allowed-scheduled-update-interval-range": "De minimale waarde voor het minimaal toegestane update-interval mag niet negatief zijn", + "min-allowed-deduplication-interval": "Minimaal toegestaan deduplicatie-interval (seconden)", + "min-allowed-deduplication-interval-range": "De waarde van het minimaal toegestane deduplicatie-interval mag niet negatief zijn", + "min-allowed-deduplication-interval-required": "Minimaal toegestaan deduplicatie-interval is vereist", + "intermediate-aggregation-interval": "Tussentijds aggregatie-interval (seconden)", + "intermediate-aggregation-interval-range": "De waarde van het tussentijds aggregatie-interval mag niet lager zijn dan '1'", + "intermediate-aggregation-interval-required": "Tussentijds aggregatie-interval is vereist", + "reevaluation-check-interval": "Herbeoordelingscontrole-interval (seconden)", + "reevaluation-check-interval-range": "De waarde van het herbeoordelingscontrole-interval mag niet lager zijn dan '1'", + "reevaluation-check-interval-required": "Herbeoordelingscontrole-interval is vereist", + "alarms-reevaluation-interval": "Herbeoordelingsinterval voor alarmen (seconden)", + "alarms-reevaluation-interval-range": "De waarde van het herbeoordelingsinterval voor alarmen mag niet lager zijn dan '1'", + "alarms-reevaluation-interval-required": "Herbeoordelingsinterval voor alarmen is vereist", + "min-allowed-aggregation-interval": "Minimaal toegestaan aggregatie-interval (seconden)", + "min-allowed-aggregation-interval-range": "De waarde van het minimaal toegestane aggregatie-interval mag niet negatief zijn", + "min-allowed-aggregation-interval-required": "Minimaal toegestaan aggregatie-interval is vereist", + "min-allowed-scheduled-update-interval-required": "De minimale waarde voor het minimaal toegestane update-interval is vereist", "max-state-size": "Maximale staatsgrootte in KB", "max-state-size-range": "Staatsgrootte mag niet negatief zijn", "max-state-size-required": "Staatsgrootte is verplicht", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Maximaal aantal abonnementen per reguliere gebruiker", "ws-limit-max-subscriptions-per-public-user": "Maximaal aantal abonnementen per publieke gebruiker", "ws-limit-updates-per-session": "WS-updates per sessie", + "relation-search-entity-limit": "Relatiezoekentiteitenlimiet", + "relation-search-entity-limit-hint": "Beperkt het aantal entiteiten dat op het laatste niveau van het relatiepad wordt opgelost. Van toepassing op argumenten 'Gerelateerde entiteiten' en propagatievelden.", + "relation-search-entity-limit-required": "Relatiezoekentiteitenlimiet is vereist", + "relation-search-entity-limit-range": "Relatiezoekentiteitenlimiet kan niet lager zijn dan '1'", "rate-limits": { "add-limit": "Limiet toevoegen", "and-also-less-than": "en ook minder dan", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Standaard groeperingsinterval", "edit-intervals-list-hint": "Lijst met beschikbare intervalopties kan worden gespecificeerd.", "edit-grouping-intervals-list-hint": "Het is mogelijk om de lijst met groeperingsintervallen en het standaardinterval te configureren.", - "all": "Alles" + "all": "Alles", + "save-current-settings-as-default": "Huidige instellingen opslaan als standaardtijdvenster", + "hide-option-from-end-users": "Optie verbergen voor eindgebruikers" }, "tooltip": { "trigger": "Trigger", @@ -6648,6 +7227,7 @@ "export-attributes": "Exporteer attributen", "export-credentials": "Exporteer inloggegevens", "export-calculated-fields": "Berekende velden exporteren \nen alarmregels", + "export-alarm-rules": "Alarmregels exporteren", "entity-versions": "Entiteitversies", "versions": "Versies", "created-time": "Aangemaakt op", @@ -6665,6 +7245,7 @@ "load-attributes": "Attributen laden", "load-credentials": "Inloggegevens laden", "load-calculated-fields": "Berekende velden laden", + "load-alarm-rules": "Alarmregels laden", "compare-with-current": "Vergelijk met huidige", "diff-entity-with-version": "Verschil met entiteitversie '{{versionName}}'", "previous-difference": "Vorige verschil", @@ -6874,7 +7455,23 @@ "scan-qr-code": "QR-code scannen", "make-phone-call": "Telefoongesprek voeren", "get-location": "Telefoonlocatie ophalen", - "take-screenshot": "Schermafbeelding maken" + "take-screenshot": "Schermafbeelding maken", + "handle-provision-success-function": "Provisioning-succesfunctie afhandelen", + "get-location-function": "Locatiefunctie ophalen", + "process-launch-result-function": "Startresultaatfunctie verwerken", + "get-phone-number-function": "Telefoonnummerfunctie ophalen", + "process-image-function": "Afbeeldingsfunctie verwerken", + "process-qr-code-function": "QR-codefunctie verwerken", + "process-location-function": "Locatiefunctie verwerken", + "handle-empty-result-function": "Leegresultaatfunctie afhandelen", + "handle-error-function": "Foutfunctie afhandelen", + "handle-non-mobile-fallback-function": "Non-Mobile-terugvalfunctie afhandelen", + "save-to-gallery": "Opslaan in 'Afbeeldingsgalerij'", + "provision-type": "Provisioningtype", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Aangepaste actiefunctie", "custom-pretty-function": "Aangepaste actie (met HTML-sjabloon) functie", @@ -6883,7 +7480,8 @@ "marker": "Markering", "polygon": "Polygoon", "rectangle": "Rechthoek", - "circle": "Cirkel" + "circle": "Cirkel", + "polyline": "Polylijn" }, "place-map-item": "Kaartobject plaatsen", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Doorgaan met polygoon tekenen", "finish-draw-polygon": "Teken polygoon beëindigen", "start-draw-circle": "Teken cirkel starten", - "finish-draw-circle": "Teken cirkel beëindigen" + "finish-draw-circle": "Teken cirkel beëindigen", + "start-draw-polyline": "Polylijn tekenen starten", + "finish-draw-polyline": "Polylijn tekenen voltooien" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Vertraging update-animatie" }, "chart-axis": { + "limit": "Limiet", + "source": "Bron", + "key-value": "Sleutel / Waarde", + "value-required": "Waarde is vereist.", + "entity-key-required": "Entiteitsleutel is vereist.", + "key-required": "Sleutel is vereist.", + "scale-limits": "Schaallimieten", + "scale-appearance": "Schaalweergave", "scale": "Schaal", "scale-min": "min", "scale-max": "max", @@ -8005,7 +8613,10 @@ "add-radio-option": "Voeg radio-optie toe", "radio-label-position": "Labelpositie", "radio-label-position-before": "Voor", - "radio-label-position-after": "Na" + "radio-label-position-after": "Na", + "save-image": "Afbeelding opslaan", + "save-to-gallery": "Vastgelegde afbeeldingen automatisch opslaan in Afbeeldingsgalerij", + "public-image": "Maakt de afbeelding beschikbaar voor elke onbevoegde gebruiker" }, "invalid-qr-code-text": "Ongeldige invoertekst voor QR-code. Invoer moet van het type string zijn", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Reizen", "markers": "Markeringen", "polygons": "Polygonen", - "circles": "Cirkels" + "circles": "Cirkels", + "polylines": "Polylijnen" }, "data-layer": { "source": "Bron", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Cirkel voor '{{entityName}}': klik om te voltooien en op te slaan", "finish-circle-hint": "Cirkel: klik om tekenen te voltooien" }, + "polyline": { + "polyline-key": "Polylijnsleutel", + "polyline-key-required": "Polylijnsleutel is vereist", + "no-polylines": "Geen polylijnen geconfigureerd", + "add-polylines": "Polylijn toevoegen", + "polyline-configuration": "Polylijnconfiguratie", + "remove-polyline": "Polylijn verwijderen", + "edit": "Polylijn bewerken", + "cut": "Polylijngebied uitsnijden", + "rotate": "Polylijn roteren", + "remove-polyline-for": "Polylijn verwijderen voor '{{entityName}}'", + "draw-polyline": "Polylijn tekenen", + "polyline-place-first-point-hint-with-entity": "Polylijn voor '{{entityName}}': klik om het eerste punt te plaatsen", + "polyline-place-first-point-hint": "Polylijn: klik om het eerste punt te plaatsen", + "finish-polyline-hint-with-entity": "Polylijn voor '{{entityName}}': klik om het tekenen te voltooien", + "finish-polyline-hint": "Polylijn: klik om het tekenen te voltooien", + "polyline-place-first-point-cut-hint": "Klik om het eerste punt te plaatsen", + "finish-polyline-cut-hint": "Klik op de eerste markering om te voltooien en op te slaan" + }, "select-entity": "Entiteit selecteren", "select-entity-hint": "Tip: klik na selectie op de kaart om de positie in te stellen" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Lege ruimte tonen in plaats van verborgen knopactie", "dont-reserve-space-hidden-action": "Geen ruimte reserveren voor verborgen actieknoppen", "display-timestamp": "Tijdstempel", + "timestamp-column-name": "Tijdstempel", "display-pagination": "Paginering weergeven", "default-page-size": "Standaard paginagrootte", "page-step-settings": "Instellingen paginastappen", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Ten minste één alarmkolom moet worden opgegeven", "table-tabs": "Tabeltabs", "show-cell-actions-menu-mobile": "Toon keuzemenu celacties in mobiele modus", - "disable-sorting": "Sortering uitschakelen" + "disable-sorting": "Sortering uitschakelen", + "sort-by": "Tabbladen sorteren op", + "sort-timestamp-option": "Aanmaaktijd" }, "latest-chart": { "total": "Totaal", @@ -9491,11 +10125,28 @@ "content": "

    Door dashboards voor eindgebruikers aan te maken, kan een klantgebruiker alleen zijn eigen apparaten zien. Gegevens van andere klanten blijven verborgen.

    Volg de documentatie om dit te doen:

    " } } + }, + "api-usage": { + "api-usage": "API-gebruik", + "label": "Label", + "state-name": "Statusnaam", + "status": "Status", + "status-required": "Status is vereist.", + "limit": "Maximale limiet", + "limit-required": "Maximale limiet is vereist.", + "current-number": "Huidig aantal", + "current-number-required": "Huidig aantal is vereist.", + "add-key": "Sleutel toevoegen", + "no-key": "Geen sleutel", + "delete-key": "Sleutel verwijderen", + "target-dashboard-state": "Doeldashboardstatus", + "go-to-main-state": "Naar standaardweergave gaan" } }, "icon": { "icon": "Pictogram", "icons": "Pictogrammen", + "custom": "Aangepast", "select-icon": "Selecteer pictogram", "material-icons": "Material pictogrammen", "show-all": "Toon alle pictogrammen", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "van" }, "language": { + "auto": "Auto", "language": "Taal" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-no_NO.json b/ui-ngx/src/assets/locale/locale.constant-no_NO.json index c7d1a4d7f3..be53c9624d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-no_NO.json +++ b/ui-ngx/src/assets/locale/locale.constant-no_NO.json @@ -78,6 +78,7 @@ "show-more": "Vis mer", "dont-show-again": "Ikke vis igjen", "see-documentation": "Se dokumentasjon", + "see-debug-events": "Se feilsøkingshendelser", "clear": "Tøm", "upload": "Last opp", "delete-anyway": "Slett likevel", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Tofaktorautentisering", "available-providers": "Tilgjengelige leverandører", + "available-providers-required": "Minst én 2FA-leverandør må konfigureres.", "issuer-name": "Utstederens navn", "issuer-name-required": "Utstederens navn er påkrevd.", "max-verification-failures-before-user-lockout": "Maks antall mislykkede verifiseringer før brukerkonto låses", @@ -513,7 +515,9 @@ "verification-message-template-required": "Meldingsmal for verifisering er påkrevd.", "within-time": "Innen tid (sek)", "within-time-pattern": "Tiden må være et positivt heltall.", - "within-time-required": "Tid er påkrevd." + "within-time-required": "Tid er påkrevd.", + "force-2fa": "Håndhev tofaktorautentisering", + "enforce-for": "Håndhev for" }, "jwt": { "security-settings": "JWT sikkerhetsinnstillinger", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarm", + "alarm-list": "Alarmliste", "alarms": "Alarmer", "all-alarms": "Alle alarmer", "select-alarm": "Velg alarm", @@ -649,7 +654,16 @@ "alarm-type": "Alarmtype", "enter-alarm-type": "Angi alarmtype", "no-alarm-types-matching": "Ingen alarmtyper som samsvarer med '{{entitySubtype}}' ble funnet.", - "alarm-type-list-empty": "Ingen alarmtyper valgt." + "alarm-type-list-empty": "Ingen alarmtyper valgt.", + "system-comments": { + "acked-by-user": "Alarmen ble kvittert av bruker {{userName}}", + "cleared-by-user": "Alarmen ble nullstilt av bruker {{userName}}", + "assigned-to-user": "Alarmen ble tilordnet av bruker {{userName}} til bruker {{assigneeName}}", + "unassigned-to-user": "Tilordningen av alarmen ble fjernet av bruker {{userName}}", + "unassigned-from-deleted-user": "Tilordningen av alarmen ble fjernet fordi brukeren {{userName}} - ble slettet", + "comment-deleted": "Bruker {{userName}} slettet kommentaren sin", + "severity-changed": "Alarmens alvorlighetsgrad ble oppdatert fra {{oldSeverity}} til {{newSeverity}}" + } }, "alarm-activity": { "add": "Legg til en kommentar...", @@ -754,6 +768,7 @@ "name-max-length": "Navnet må være kortere enn 256 tegn", "label-max-length": "Etiketten må være kortere enn 256 tegn", "description": "Beskrivelse", + "description-required": "Beskrivelse er påkrevd.", "type": "Type", "type-required": "Type er påkrevd.", "details": "Detaljer", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Månedlig aktivitet for opprettede alarmer", "data-points": "Datapunkter", "data-points-storage-days": "Lagringsdager for datapunkter", + "data-points-storage-days-hourly-activity": "Lagringsdager for datapunkter – timeaktivitet", + "data-points-storage-days-daily-activity": "Lagringsdager for datapunkter – daglig aktivitet", + "data-points-storage-days-monthly-activity": "Lagringsdager for datapunkter – månedlig aktivitet", "device-api": "Enhets-API", "email": "E-post", "email-messages": "E-postmeldinger", @@ -900,6 +918,7 @@ "rule-node": "Regelnode", "sms": "SMS", "sms-messages": "SMS-meldinger", + "sms-messages-hourly-activity": "SMS-meldinger – timeaktivitet", "sms-messages-daily-activity": "Daglig aktivitet for SMS-meldinger", "sms-messages-monthly-activity": "Månedlig aktivitet for SMS-meldinger", "successful": "${entityName} Vellykket", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Timebasert aktivitet for telemetrilagring", "telemetry-persistence-monthly-activity": "Månedlig aktivitet for telemetrilagring", "transport": "Transport", + "transport-msg-hourly-activity": "Transportmeldinger – timeaktivitet", + "transport-msg-daily-activity": "Transportmeldinger – daglig aktivitet", + "transport-msg-monthly-activity": "Transportmeldinger – månedlig aktivitet", "transport-daily-activity": "Transport – daglig aktivitet", "transport-data-points": "Transportdatapunkter", + "transport-data-points-hourly-activity": "Transportdatapunkter – timeaktivitet", + "transport-data-points-daily-activity": "Transportdatapunkter – daglig aktivitet", + "transport-data-points-monthly-activity": "Transportdatapunkter – månedlig aktivitet", "view-details": "Vis detaljer", "view-statistics": "Vis statistikk", - "transport-messages": "Transportmeldinger" + "transport-messages": "Transportmeldinger", + "transport-messages-hourly-activity": "Transportmeldinger – timeaktivitet", + "transport-data-point-hourly-activity": "Transportdatapunkt – timeaktivitet", + "javascript-function-executions": "JavaScript-funksjonskjøringer", + "javascript-function-executions-hourly-activity": "JavaScript-funksjonskjøringer – timeaktivitet", + "javascript-function-executions-daily-activity": "JavaScript-funksjonskjøringer – daglig aktivitet", + "javascript-function-executions-monthly-activity": "JavaScript-funksjonskjøringer – månedlig aktivitet", + "tbel-function-executions": "TBEL-funksjonskjøringer", + "tbel-function-executions-hourly-activity": "TBEL-funksjonskjøringer – timeaktivitet", + "tbel-function-executions-daily-activity": "TBEL-funksjonskjøringer – daglig aktivitet", + "tbel-function-executions-monthly-activity": "TBEL-funksjonskjøringer – månedlig aktivitet", + "created-reports": "Opprettede rapporter", + "created-reports-hourly-activity": "Opprettede rapporter – timeaktivitet", + "created-reports-daily-activity": "Opprettede rapporter – daglig aktivitet", + "created-reports-monthly-activity": "Opprettede rapporter – månedlig aktivitet", + "emails": "E-poster", + "emails-hourly-activity": "E-poster – timeaktivitet", + "emails-daily-activity": "E-poster – daglig aktivitet", + "emails-monthly-activity": "E-poster – månedlig aktivitet", + "status": { + "enabled": "Aktivert", + "disabled": "Deaktivert", + "warning": "Advarsel" + } }, "api-limit": { "cassandra-write-queries-core": "REST API Cassandra-skrivespørringer", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Edge-opplinkmeldinger", "edge-uplink-messages-per-edge": "Edge-opplinkmeldinger per edge" }, + "api-key": { + "api-key": "API-nøkkel", + "api-keys": "API-nøkler", + "delete-api-key-title": "Er du sikker på at du vil slette API-nøkkelen '{{name}}'?", + "delete-api-key-text": "Vær forsiktig, etter bekreftelsen vil nøkkelen ikke kunne gjenopprettes.", + "delete-api-keys-title": "Er du sikker på at du vil slette { count, plural, =1 {1 API-nøkkel} other {# API-nøkler} }?", + "delete-api-keys-text": "Vær forsiktig, etter bekreftelsen vil alle valgte nøkler ikke kunne gjenopprettes.", + "expiration-date": "Utløpsdato", + "date": "dato", + "description": "Beskrivelse", + "disable": "Deaktiver", + "edit-description": "Rediger beskrivelse", + "enable": "Aktiver API-nøkkel ", + "expiration-time": "Utløpstid", + "expiration-time-never": "Aldri", + "expiration-time-custom": "Egendefinert", + "generate": "Generer", + "generate-title": "Generer API-nøkkel", + "generate-text": "Merk: API-nøkkelen arver tillatelsene til brukeren den opprettes for.", + "generated-api-key-title": "API-nøkkel generert. La oss sjekke tilkoblingen!", + "generated-api-key-copy": "Sørg for å kopiere og lagre API-nøkkelen din nå, siden du ikke vil kunne se den igjen.", + "generated-api-key-command": "Bruk følgende instruksjoner for å sjekke tilkoblingen. Som resultat bør du motta informasjon om gjeldende bruker:", + "generated-api-key-insecure-url": "Å kjøre kommandoer over en usikker HTTP-tilkobling vil sende API-nøkkelen din ukryptert, noe som gjør den sårbar for avlytting.", + "list": "{ count, plural, =1 {Én API-nøkkel} other {Liste over # API-nøkler} }", + "manage": "Administrer", + "manage-api-keys": "Administrer API-nøkler", + "no-found": "Fant ingen API-nøkler", + "selected-api-keys": "{ count, plural, =1 {1 API-nøkkel} other {# API-nøkler} } valgt", + "search": "Søk etter API-nøkler", + "status": "Status", + "status-active": "Aktiv", + "status-inactive": "Inaktiv", + "status-expired": "Utløpt" + }, "audit-log": { "audit": "Revisjon", "audit-logs": "Revisjonslogger", @@ -991,7 +1073,11 @@ "type-provision-failure": "Klargjøring av enhet mislyktes", "type-timeseries-updated": "Telemetri oppdatert", "type-timeseries-deleted": "Telemetri slettet", - "type-sms-sent": "SMS sendt" + "type-sms-sent": "SMS sendt", + "any-type": "Enhver type", + "audit-log-filter-title": "Revisjonsloggfilter", + "filter-title": "Filter", + "filter-types": "Revisjonsloggtyper" }, "debug-settings": { "label": "Feilsøkingskonfigurasjon", @@ -1009,15 +1095,29 @@ "expression": "Uttrykk", "no-found": "Ingen beregnede felt funnet", "list": "{ count, plural, =1 {Ett beregnet felt} other {Liste med # beregnede felt} }", + "add": "Legg til beregnet felt", "selected-fields": "{ count, plural, =1 {1 beregnet felt} other {# beregnede felt} } valgt", "type": { "simple": "Enkel", - "script": "Skript" + "simple-hint": "Enkel aritmetisk beregning basert på inndataargumenter.", + "script": "Skript", + "script-hint": "Beregning over definerte argumenter ved hjelp av et TBEL-skript.", + "geofencing": "Geofencing", + "geofencing-hint": "Evaluering av entitetens GPS-posisjon og overganger mot konfigurerte geofencing-sonegrupper.", + "propagation": "Propagering", + "propagation-hint": "Propagering av data til overordnede eller underordnede entiteter basert på relasjonsretning og -type.", + "related-entities-aggregation": "Aggregering av relaterte entiteter", + "related-entities-aggregation-hint": "Aggregering av siste data fra relaterte entiteter.", + "time-series-data-aggregation": "Aggregering av tidsseriedata", + "time-series-data-aggregation-hint": "Aggregering av historiske data fra en gjeldende entitet." }, + "preview": "Forhåndsvisning", "arguments": "Argumenter", "decimals-by-default": "Desimaler som standard", "debugging": "Feilsøking av beregnet felt", + "calculated-field-details": "Detaljer for beregnet felt", "argument-name": "Argumentnavn", + "name": "Navn", "datasource": "Datakilde", "add-argument": "Legg til argument", "test-script-function": "Test skriptfunksjon", @@ -1029,6 +1129,8 @@ "argument-asset": "Eiendel", "argument-customer": "Kunde", "argument-tenant": "Gjeldende leietaker", + "argument-owner": "Nåværende eier", + "argument-relation-query": "Relaterte entiteter", "argument-type": "Argumenttype", "attribute": "Attributt", "copy-argument-name": "Kopier argumentnavn", @@ -1042,12 +1144,14 @@ "shared-attributes": "Delte attributter", "attribute-key": "Attributtnøkkel", "default-value": "Standardverdi", + "default-value-required": "Standardverdi er påkrevd.", "limit": "Maksverdier", "time-window": "Tidsvindu", "customer-name": "Kundenavn", "asset-name": "Eiendelsnavn", "timeseries": "Tidsserie", "output": "Utdata", + "output-hint": "Definerer hvordan output behandles.", "create": "Opprett nytt beregnet felt", "file": "Fil for beregnet felt", "invalid-file-error": "Ugyldig filformat. Vennligst sørg for at filen er en gyldig JSON-fil.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Vær forsiktig, etter bekreftelse vil alle valgte beregnede felt bli fjernet og all relatert data vil ikke kunne gjenopprettes.", "test-with-this-message": "Test med denne meldingen", "use-latest-timestamp": "Bruk nyeste tidsstempel", + "entity-coordinates": "Entitetskoordinater", + "latitude-time-series-key": "Breddegrad tidsserienøkkel", + "latitude-time-series-key-required": "Breddegrad tidsserienøkkel er påkrevd.", + "longitude-time-series-key": "Lengdegrad tidsserienøkkel", + "longitude-time-series-key-required": "Lengdegrad tidsserienøkkel er påkrevd.", + "geofencing-zone-groups": "Geofencing sonegrupper", + "geofencing-zone-groups-settings": "Innstillinger for geofencing sonegruppe", + "target-zone": "Målsone", + "perimeter-key": "Perimeter-nøkkel", + "report-strategy": "Rapporteringsstrategi", + "no-zone-configured": "Minst én sone er påkrevd.", + "no-zone-configured-required": "Minst én sonegruppe må være konfigurert.", + "add-zone-group": "Legg til sonegruppe", + "report-transition-event-only": "Kun overgangshendelser", + "report-presence-status-only": "Kun tilstedeværelsesstatus", + "report-transition-event-and-presence": "Tilstedeværelsesstatus og overgangshendelser", + "perimeter-attribute-key": "Perimeter-attributtnøkkel", + "perimeter-attribute-key-required": "Perimeter-attributtnøkkel er påkrevd.", + "perimeter-attribute-key-pattern": "Perimeter-attributtnøkkel er ugyldig.", + "entity-zone-relationship": "Sti fra entitet til soner", + "direction": "Relasjonsretning", + "direction-from": "Fra entitet til sone", + "direction-to": "Fra sone til entitet", + "relation-type": "Relasjonstype", + "create-relation-with-matched-zones": "Opprett relasjoner for kildeentitet med samsvarende soner", + "relation-level": "Relasjonsnivå", + "fetch-last-available-level": "Hent kun siste tilgjengelige nivå", + "zone-group-refresh-interval": "Oppdateringsintervall for sonegrupper", + "copy-zone-group-name": "Kopier sonegruppens navn", + "open-details-page": "Åpne entitetsdetaljside", + "level": "Nivå", + "direction-level": "Retning", + "direction-up": "Opp", + "direction-up-parent": "Opp til overordnet", + "direction-down": "Ned", + "direction-down-child": "Ned til underordnet", + "add-level": "Legg til nivå", + "delete-level": "Slett nivå", + "no-level": "Ingen nivå konfigurert", + "levels-required": "Minst ett nivå må være konfigurert.", + "max-allowed-levels-error": "Relasjonsnivået overskrider maks tillatt nivå.", + "propagation-path-related-entities": "Propageringssti til relaterte entiteter", + "propagate-type": { + "arguments-only": "Kun argumenter", + "expression-result": "Beregningens resultat" + }, + "script": "Skript", + "data-propagate": "Data som skal propageres", + "output-key": "Output-nøkkel", + "copy-output-key": "Kopier output-nøkkel", + "aggregation-path-related-entities": "Aggregasjonssti til relaterte entiteter", + "deduplication-interval": "Dedupliseringsintervall", + "deduplication-interval-min": "Dedupliseringsintervallet bør være minst {{ sec }} sekunder.", + "deduplication-interval-hint": "Minimalt tidsrom mellom telemetriaggregasjoner.", + "deduplication-interval-required": "Dedupliseringsintervall er påkrevd.", + "calculated-field-filter-title": "Filter for beregnet felt", + "filter-title": "Filter", + "calculated-field-types": "Typer av beregnede felt", + "events": "Hendelser", + "any-type": "Enhver type", + "metrics": { + "metrics": "Metrikker", + "metrics-empty": "Minst én metrikk må være konfigurert.", + "metric-name": "Metrikknavn", + "metric-name-required": "Metrikknavn er påkrevd.", + "metric-name-pattern": "Metrikknavn er ugyldig.", + "metric-name-duplicate": "Metrikk med dette navnet finnes allerede.", + "metric-name-max-length": "Metrikknavnet bør være mindre enn 256 tegn.", + "metric-name-forbidden": "Metrikknavnet er reservert og kan ikke brukes.", + "copy-metric-name": "Kopier metrikknavn", + "argument-name": "Argumentnavn", + "aggregation": "Aggregering", + "aggregation-type": { + "avg": "Gjennomsnitt", + "min": "Minimum", + "max": "Maksimum", + "sum": "Sum", + "count": "Antall", + "count-unique": "Antall unike" + }, + "filtered": "Filtrert", + "value-source": "Verdi kilde", + "value-source-hint": "Definerer hvordan verdien for aggregering hentes.", + "value-source-type": { + "key": "Nøkkel", + "function": "Funksjon" + }, + "no-metrics-configured": "Minst én metrikk er påkrevd.", + "add-metric": "Legg til metrikk", + "max-metrics": "Maksimalt antall metrikker er nådd.", + "metric-settings": "Metrikkinnstillinger", + "filter": "Filter", + "filter-hint": "Aktiverer filtrering av entiteter under aggregering. Filterfunksjonen må returnere en boolsk verdi og kan bruke alle konfigurerte argumenter." + }, + "output-strategy": { + "strategy": "Strategi", + "process-right-away": "Behandle umiddelbart", + "process-rule-chains": "Behandle via Regelkjeder", + "save-time-series": "Lagre i tidsserie", + "save-database": "Lagre i database", + "save-latest-values": "Lagre i siste verdier", + "send-web-sockets": "Send til WebSockets", + "save-calculated-fields": "Send til beregnede felt", + "update-attribute-only-on-value-change": "Oppdater attributt kun ved verdiendring", + "send-attributes-updated-notification": "Send varsel om oppdaterte attributter", + "ttl": "Egendefinert TTL", + "ttl-required": "TTL er påkrevd", + "ttl-min": "Kun minimum TTL på 0 er tillatt", + "processing-parameters": "Behandlingsparametere", + "hint": { + "strategy": "Styrer om resultatet behandles umiddelbart eller sendes til en regelkjede for videre behandling.", + "processing-options": "Behandlingsalternativer", + "update-attribute-only-on-value-change": "Oppdaterer attributtet ved hver innkommende melding, uavhengig av om verdien har endret seg. Dette øker API-bruken og reduserer ytelsen.", + "update-attribute-only-on-value-change-enabled": "Oppdaterer attributtet kun når verdien endres. Hvis verdien er uendret, oppdateres ikke tidsstempler og varsler sendes ikke.", + "send-attributes-updated-notification": "Sender en «Attributter oppdatert»-hendelse til standard regelkjede.", + "save-time-series": "Lagrer tidsseriedata i ts_kv-tabellen i databasen.", + "save-database": "Lagrer attributtdata i databasen.", + "save-latest-values": "Oppdaterer tidsseriedata i ts_kv_latest-tabellen i databasen hvis den nye verdien er nyere.", + "send-web-sockets-attribute": "Varsler WebSocket-abonnementer om oppdateringer av attributtdata.", + "send-web-sockets-time-series": "Varsler WebSocket-abonnementer om oppdateringer av tidsseriedata.", + "save-calculated-fields-attribute": "Varsler beregnede felt om oppdateringer av attributtdata.", + "save-calculated-fields-time-series": "Varsler beregnede felt om oppdateringer av tidsseriedata.", + "ttl": "Definerer oppbevaringsperioden for tidsseriedata. Hvis deaktivert, brukes TTL fra Leietakerprofil." + } + }, + "aggregate-interval-type": "Aggregeringsintervalltype", + "aggregate-interval-value": "Aggregeringsintervallverdi", + "aggregate-interval-value-required": "Aggregeringsintervallverdi er påkrevd.", + "aggregate-interval-value-min": "Aggregeringsintervallverdi bør være minst { sec, plural, =0 {0 sekund} =1 {1 sekund} other {# sekunder} }.", + "aggregate-interval-value-step-multiple-of": "Aggregeringsintervallverdi må være en divisor eller et multiplum av 1 dag.", + "aggregate-period": { + "hour": "Time", + "day": "Dag", + "week": "Uke (man–søn)", + "week-sun-sat": "Uke (søn–lør)", + "month": "Måned", + "quarter": "Kvartal", + "year": "År", + "custom": "Egendefinert" + }, + "aggregate-period-hint-offset": "Aggregeringsintervallet ditt blir: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Aggregeringsintervallet ditt blir: {{ interval }} og så videre.", + "entity-aggregation": { + "argument-hint": "Data hentes fra gjeldende entitet.", + "argument-title-hint": "Definerer inndataargumentene som brukes for aggregering.", + "argument-setting-hint": "Siste telemetri er den eneste tilgjengelige argumenttypen for dette beregnede feltet.", + "aggregation-interval": "Aggregeringsintervall", + "aggregation-interval-hint": "Definerer hvor ofte aggregering utføres. Eksempel: hver 1 time aggregerer data kl. 00:00, 01:00, 02:00 osv. Aggregeringsresultater lagres med tidsstempelet som tilsvarer starten på aggregeringsintervallet.", + "apply-offset": "Bruk forskyvning på aggregeringsintervall", + "apply-offset-hint": "Definerer hvor mye starten på hver aggregeringsperiode skal forskyves (f.eks. +10 minutter – 00:10, 01:10).", + "offset-value": "Forskyvningsverdi", + "offset-value-required": "Forskyvningsverdi er påkrevd.", + "offset-value-min": "Forskyvningsverdi må være et positivt heltall.", + "offset-value-max": "Forskyvningsverdi bør være mindre enn aggregeringsintervallverdien.", + "wait-delay": "Bruk ventetidsavbrudd for forsinket telemetri", + "wait-delay-hint": "Definerer hvor lenge det skal ventes på forsinket telemetri etter at intervallet er avsluttet. Hvis slik telemetri kommer inn, beregnes resultatet for intervallet på nytt.", + "duration": "Varighet", + "duration-required": "Varighet er påkrevd.", + "duration-min": "Varighet bør være minst 1 minutt.", + "duration-hint": "Hvor lenge det skal ventes på forsinkede data etter at intervallet er avsluttet.", + "produce-intermediate-result": "Produser mellomresultat", + "produce-intermediate-result-hint": "Beregner metrikker i løpet av gjeldende intervall for å produsere et mellomresultat. Oppdateringer skjer ikke oftere enn én gang hver {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Beregnet felt av typen «Enkel» skal ikke inneholde nøkler med tidsserie-rulleringstype.", + "arguments-propagate-arguments-with-rolling": "Typen «Tidsserie-rullering» er ikke kompatibel med «Kun argumenter»-propagering.", + "arguments-propagate-argument-entity-type": "Entitetstype er ikke kompatibel med «Kun argumenter»-propagering.", + "arguments-propagate-argument-must-current-entity": "Minst ett argument må være konfigurert med kildentitetstypen «Gjeldende entitet».", "arguments-empty": "Argumenter skal ikke være tomme.", "expression-required": "Uttrykk er påkrevd.", "expression-invalid": "Uttrykk er ugyldig", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Et argument med dette navnet finnes allerede.", "argument-name-max-length": "Argumentnavn må være kortere enn 256 tegn.", "argument-name-forbidden": "Argumentnavnet er reservert og kan ikke brukes.", + "output-key-required": "Output-nøkkel er påkrevd.", + "output-key-pattern": "Output-nøkkel er ugyldig.", + "output-key-duplicate": "Nøkkel med dette navnet finnes allerede.", + "output-key-max-length": "Output-nøkkel bør være mindre enn 256 tegn.", + "output-key-forbidden": "Output-nøkkel er reservert og kan ikke brukes.", + "entity-type-required": "Entitetstype er påkrevd", + "name-required": "Navn er påkrevd.", + "name-pattern": "Navn er ugyldig.", + "name-duplicate": "Navn med dette navnet finnes allerede.", + "name-max-length": "Navn bør være mindre enn 256 tegn.", + "name-forbidden": "Navn er reservert og kan ikke brukes.", "argument-type-required": "Argumenttype er påkrevd.", "max-args": "Maksimalt antall argumenter er nådd.", "decimals-range": "Desimaler som standard må være et tall mellom 0 og 15.", "expression": "Standarduttrykket viser hvordan man konverterer temperatur fra Fahrenheit til Celsius.", "arguments-entity-not-found": "Målenheten for argument ikke funnet.", - "use-latest-timestamp": "Hvis aktivert, lagres den beregnede verdien ved å bruke det nyeste tidsstempelet fra argumentenes telemetri, i stedet for servertid." + "use-latest-timestamp": "Hvis aktivert, lagres den beregnede verdien ved å bruke det nyeste tidsstempelet fra argumentenes telemetri, i stedet for servertid.", + "entity-coordinates": "Angi tidsserienøklene som gir entitetens GPS-koordinater (breddegrad og lengdegrad).", + "geofencing-zone-groups": "Definer én eller flere geofencing-sonegrupper som skal sjekkes (f.eks. «allowedZones», «restrictedZones»). Hver gruppe må ha et unikt navn, som brukes som prefiks for telemetrinøkler i utdata fra beregnet felt.", + "perimeter-attribute-key": "Angi attributtnøkkelen som inneholder definisjonen av geofencing-sonens omkrets. Omkretsen hentes alltid fra attributter på serversiden for soneentiteten.", + "report-strategy": "Tilstedeværelsesstatus rapporterer om entiteten for øyeblikket er INNE i eller UTENFOR sonegruppen. Overgangshendelser rapporterer når entiteten GIKK INN I eller FORLOT sonegruppen.", + "create-relation-with-matched-zones": "Opprett og vedlikehold automatisk relasjoner mellom entiteten og sonene den for øyeblikket er inne i. Relasjoner fjernes når entiteten forlater en sone og opprettes når den går inn i en ny.", + "relation-type-required": "Relasjonstype er påkrevd.", + "relation-level-required": "Relasjonsnivå er påkrevd.", + "relation-level-min": "Minimumsverdi for relasjonsnivå er 1.", + "relation-level-max": "Maksimumsverdi for relasjonsnivå er {{max}}.", + "geofencing-empty": "Minst én sonegruppe må være konfigurert.", + "geofencing-entity-not-found": "Geofencing-målentitet ble ikke funnet.", + "max-geofencing-zone": "Maksimalt antall geofencing-soner er nådd.", + "zone-group-refresh-interval": "Definerer hvor ofte sonegrupper konfigurert via relaterte entiteter oppdateres.", + "zone-group-refresh-interval-required": "Oppdateringsintervall for sonegrupper er påkrevd.", + "zone-group-refresh-interval-min": "Oppdateringsintervallet for sonegrupper bør være minst {{ min }} sekunder.", + "propagation-path-related-entities": "Definerer en direkte sti på ett nivå til en relatert entitet basert på valgt retning og relasjonstype. Kun relasjoner mellom enhets-, eiendels-, kunde- og leietakerentiteter støttes. Maksimalt antall entiteter som løses via relasjonsstien er {{ max }}.", + "data-propagate": "Definerer dataene som skal propageres fra argumentene konfigurert nedenfor. «Kun argumenter» bruker de hentede dataene direkte, mens «Beregningens resultat» beregner en ny verdi fra disse dataene.", + "aggregation-path-related-entities": "Definerer en aggregeringssti på ett nivå via direkte relasjoner med overordnede eller underordnede entiteter, basert på retning og relasjonstype. Kun relasjoner mellom enhets-, eiendels-, kunde- og leietakerentiteter støttes. Maksimalt antall entiteter som løses via relasjonsstien er {{ max }}.", + "arguments-aggregation": "Definerer inndataargumentene som brukes for filtrering og aggregering.", + "setting-arguments-aggregation": "Data hentes fra relaterte entiteter konfigurert i aggregeringsstien.", + "metrics": "Definerer metrikker som aggregeres basert på konfigurerte argumenter.", + "entity-aggregation-metrics": "Definerer metrikker som aggregeres basert på konfigurerte argumenter over de angitte tidsintervallene.", + "import-invalid-calculated-field-type": "Kan ikke importere beregnet felt: Ugyldig struktur for beregnet felt.", + "simple-expression-title": "Aritmetisk uttrykk som definerer hvordan den beregnede verdien beregnes.", + "script-title": "TBEL-skript som definerer beregningslogikken og utdataverdiene.", + "simple-arguments": "Aritmetisk uttrykk som definerer hvordan den beregnede verdien beregnes.", + "script-arguments": "Definerer inndataargumentene som er tilgjengelige for skriptet." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Alarmregler", + "alarm-rule": "Alarmregel", + "alarm-rules": "Alarmregler", + "alarm-rules-old": "Gammel", + "alarm-rules-actual": "Aktuell", + "severities": "Alvorlighetsgrader", + "cleared": "Tømmekondisjon", + "delete-title": "Er du sikker på at du vil slette alarmregelen '{{title}}'?", + "delete-text": "Vær forsiktig, etter bekreftelsen vil alarmregelen og alle tilknyttede data ikke kunne gjenopprettes.", + "delete-multiple-title": "Er du sikker på at du vil slette { count, plural, =1 {1 alarmregel} other {# alarmregler} }?", + "delete-multiple-text": "Vær forsiktig, etter bekreftelsen vil alle valgte alarmregler bli fjernet og alle tilknyttede data vil ikke kunne gjenopprettes.", + "create": "Opprett ny alarmregel", + "add": "Legg til alarmregel", + "copy": "Kopier konfigurasjon for alarmregel", + "details": "Detaljer for alarmregel", + "no-found": "Fant ingen alarmregler", + "list": "{ count, plural, =1 {Én alarmregel} other {Liste over # alarmregler} }", + "selected-fields": "{ count, plural, =1 {1 alarmregel} other {# alarmregler} } valgt", + "import": "Importer alarmregel", + "file": "Alarmregelfil", + "export": "Eksporter alarmregel", + "export-failed-error": "Kan ikke eksportere alarmregel: {{error}}", + "entity-type": "Entitetstype", + "entity-type-required": "Entitetstype er påkrevd.", + "alarm-type": "Alarmtype", + "alarm-type-hint": "Unik identifikator (f.eks. HighTempAlarm) innenfor omfanget til alarmens opphav (Enhet, Eiendel osv.) for å unngå konflikter.", + "alarm-type-required": "Alarmtype er påkrevd.", + "alarm-type-pattern": "Alarmtype er ugyldig.", + "alarm-type-max-length": "Alarmtype bør være mindre enn 256 tegn.", + "clear-alarm": "Nullstill alarm", + "value-argument": "Argument", + "value-argument-required": "Argument er påkrevd.", + "static-settings": "Statiske innstillinger", + "configuration": "Konfigurasjon", + "static-schedule": "Statisk", + "dynamic-schedule": "Dynamisk", + "operation-and": "OG", + "operation-or": "ELLER", + "condition-during": "I løpet av {{during}}", + "condition-during-dynamic": "I løpet av \"{{ attribute }}\"", + "condition-repeat-times": "Gjentas { count, plural, =1 {1 gang} other {# ganger} }", + "condition-repeat-times-dynamic": "Gjentas \"{{ attribute }}\" ganger", + "filter-preview": "Forhåndsvisning av filter", + "condition-settings": "Betingelsesinnstillinger", + "static": "Statisk", + "dynamic": "Dynamisk", + "argument-filters": "Argumentfiltre", + "argument-name": "Argumentnavn", + "value-type": "Verditype", + "general": "Generelt", + "filters": "Filtre", + "date-time-hint": "Argumentet må være i epoch-millisekunder. Eksempel: 1698839340000 tilsvarer 2023-11-01 12:49:00 UTC.", + "operation": "Operasjon", + "value-source": "Verdikilde", + "value": "Verdi", + "ignore-case": "Ignorer store/små bokstaver", + "condition": "Betingelse", + "script": "Skript", + "add-filter": "Legg til argumentfilter", + "edit-filter": "Argumentfilter", + "remove-filter": "Fjern argumentfilter", + "no-filter": "Minst ett filter er påkrevd.", + "conditions": { + "simple": "Enkel", + "duration": "Varighet", + "repeating": "Gjentakende" + }, + "schedule-title": "Tidsplan", + "edit-schedule": "Rediger alarmtidsplan", + "schedule-type": "Tidsplanleggertype", + "schedule-type-required": "Tidsplanleggertype er påkrevd.", + "schedule": { + "any-time": "Aktiv hele tiden", + "specific-time": "Aktiv på et bestemt tidspunkt", + "custom": "Egendefinert" + }, + "schedule-day": { + "monday": "Mandag", + "tuesday": "Tirsdag", + "wednesday": "Onsdag", + "thursday": "Torsdag", + "friday": "Fredag", + "saturday": "Lørdag", + "sunday": "Søndag" + }, + "schedule-days": "Dager", + "schedule-time": "Tid", + "schedule-time-from": "Fra", + "schedule-time-to": "Til", + "schedule-days-of-week-required": "Minst én ukedag må velges.", + "tbel": "TBEL", + "expression-type": { + "simple": "Enkel", + "script": "Skript" + }, + "operation-type": { + "and": "Og", + "or": "Eller" + }, + "filter-predicate-type": { + "string": "Streng", + "numeric": "Numerisk", + "boolean": "Boolsk", + "complex": "Kompleks" + }, + "alarm-rule-additional-info": "Tilleggsinformasjon", + "edit-alarm-rule-additional-info": "Rediger tilleggsinformasjon", + "alarm-rule-additional-info-placeholder": "Legg inn kommentarer og justeringer her for å vise dem i Alarminformasjon under Tilleggsinformasjon", + "alarm-rule-additional-info-hint": "Tips: bruk ${Argumentnavn} for å erstatte verdier for argumentene som brukes i alarmregelbetingelsen.", + "alarm-rule-additional-info-icon-hint": "Bruk Argumentnavn for å erstatte verdier for argumentene som brukes i alarmregelbetingelsen.", + "alarm-rule-mobile-dashboard": "Mobilt dashbord", + "alarm-rule-mobile-dashboard-hint": "Brukes av mobilapplikasjonen som et dashbord for alarmdetaljer.", + "alarm-rule-no-mobile-dashboard": "Ingen dashbord valgt", + "alarm-rule-condition": "Alarmregelbetingelse", + "enter-alarm-rule-condition-prompt": "Legg til betingelse", + "enter-alarm-rule-clear-condition-prompt": "Legg til tømmebetingelse", + "edit-alarm-rule-condition": "Alarmbetingelse", + "condition-type": "Betingelsestype", + "condition-type-hint": "\"Varighet\"- og \"Gjentakende\"-alternativene er ikke tilgjengelige når operasjonen \"Mangler i\" brukes i filteret.", + "select-alarm-severity": "Velg alarmaalvorlighetsgrad", + "add-create-alarm-rule-prompt": "Minst én utløserbetingelse er påkrevd.", + "add-create-alarm-rule": "Legg til utløserbetingelse", + "add-clear-alarm-rule": "Legg til tømmebetingelse", + "condition-duration": "Betingelsesvarighet", + "condition-duration-value": "Varighetsverdi", + "condition-duration-time-unit": "Tidsenhet", + "condition-duration-value-range": "Varighetsverdi må være i området fra 1 til 2147483647.", + "condition-duration-value-pattern": "Varighetsverdi må være heltall.", + "condition-duration-value-required": "Varighetsverdi er påkrevd.", + "condition-duration-time-unit-required": "Tidsenhet er påkrevd.", + "condition-repeating-value": "Antall hendelser", + "condition-repeating-value-hint": "Oppdatering av ethvert argument i alarmregelen vil telles som en hendelse", + "condition-repeating-value-range": "Antall hendelser må være i området fra 1 til 2147483647.", + "condition-repeating-value-pattern": "Antall hendelser må være heltall.", + "condition-repeating-value-required": "Antall hendelser er påkrevd.", + "create-conditions": "Utløserbetingelser", + "clear-condition": "Tømmebetingelse", + "no-clear-alarm-rule": "Ingen tømmebetingelse er konfigurert.", + "advanced-settings": "Avanserte innstillinger", + "propagate-alarm": "Propager alarm til relaterte entiteter", + "alarm-rule-relation-types-list": "Relasjonstyper", + "alarm-rule-relation-types-list-hint": "Definerer relasjonstyper for å filtrere relaterte entiteter. Hvis ikke angitt, propageres alarmen til alle relaterte entiteter.", + "propagate-alarm-to-owner": "Propager alarm til entitetseier (Kunde eller Leietaker)", + "propagate-alarm-to-tenant": "Propager alarm til Leietaker", + "alarm-rule-filter-title": "Alarmregelfilter", + "filter-title": "Filter", + "debugging": "Feilsøking av alarmregel", + "any-type": "Enhver type", + "enter-alarm-rule-type": "Angi alarmtype", + "no-alarm-rule-types-matching": "Fant ingen alarmtyper som samsvarer med '{{entitySubtype}}'.", + "alarm-rule-type-list-empty": "Ingen alarmtyper valgt.", + "alarm-rule-type-list": "Alarmtypeliste", + "alarm-rule-entity-list": "Entitetsliste", + "missing-for": "mangler i", + "time-unit": "Enhet", + "mode": "Modus", + "type": "Type", + "value-required": "Verdi er påkrevd.", + "min-value": "Verdien må være 1 eller større.", + "argument-in-use": "Argumentet brukes som generelt argument.", + "import-invalid-alarm-rule-type": "Kan ikke importere alarmregel: Ugyldig struktur for alarmregel.", + "no-filter-preview": "Ingen filter angitt", + "filter-operation": { + "and": "Og", + "or": "Eller" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Dokumentasjon", "time-left": "{{time}} igjen", "output": "Utdata", + "sort-asc": "Stigende", + "sort-desc": "Synkende", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Rekkefølge i mobilapp", "mobile-hide": "Skjul dashbord i mobilapp", "update-image": "Oppdater dashbordbilde", + "update-new-version": "Last opp ny versjon", + "upload-file-to-update": "Last opp fil for å oppdatere", "take-screenshot": "Ta skjermbilde", "select-widget-title": "Velg widget", "select-widget-value": "{{title}}: velg widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrap-klient", "bootstrap-server": "Bootstrap-server", "lwm2m-server": "LwM2M-server", + "client-reboot": "Utløser for registreringsoppdatering", + "bootstrap-reboot": "Utløser for Bootstrap-forespørsel", "client-publicKey-or-id": "Klient offentlig nøkkel eller ID", "client-publicKey-or-id-required": "Klient offentlig nøkkel eller ID er påkrevd.", "client-publicKey-or-id-tooltip-psk": "PSK-identifikatoren er en vilkårlig PSK-ID opptil 128 byte, som beskrevet i standarden [RFC7925].\nMÅ konverteres til en tegnstreng og kodes i UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "Kort server-ID er påkrevd.", "short-id-range": "Kort server-ID må være mellom {{ min }} og {{ max }}.", "short-id-pattern": "Kort server-ID må være et positivt heltall.", + "short-id-pattern-bs": "Kort server-ID må kun være null", "lifetime": "Klientens registreringsvarighet", "lifetime-required": "Klientens registreringsvarighet er påkrevd.", "lifetime-pattern": "Registreringsvarighet må være et positivt heltall.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Alle ressurser observeres med én enkelt Composite Observe-forespørsel (mer effektivt, mindre fleksibelt)", "composite-by-object": "Kompositt etter objekter", "composite-by-object-description": "Ressurser grupperes etter objekttype og observeres ved hjelp av separate Composite Observe-forespørsler (balansert tilnærming)" - } + }, + "init-attr-tel-as-obs-strategy": "Initialiser attributter og telemetri ved hjelp av Observe-strategi", + "init-attr-tel-as-obs-strategy-hint": "Hvis false - attributter og telemetri initialiseres ved å lese verdiene én etter én.\\nHvis true - attributter og telemetri initialiseres ved å abonnere på verdiene ved hjelp av Observe-strategien." }, "snmp": { "add-communication-config": "Legg til kommunikasjonskonfigurasjon", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Regelnoder", "list-of-rulenodes": "{ count, plural, =1 {Én regelnode} other {Liste over # regelnoder} }", "rulenode-name-starts-with": "Regelnoder som starter med '{{prefix}}'", + "type-api-key": "API-nøkkel", + "type-api-keys": "API-nøkler", "type-current-customer": "Gjeldende kunde", "type-current-tenant": "Gjeldende leietaker", "type-current-user": "Gjeldende bruker", @@ -2656,6 +3143,7 @@ "details": "Enhetsdetaljer", "no-entities-prompt": "Ingen enheter funnet", "no-data": "Ingen data å vise", + "show-all-columns": "Vis alle", "columns-to-display": "Kolonner å vise", "type-api-usage-state": "API-bruksstatus", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Én mobilapplikasjon} other {Liste over # mobilapplikasjoner} }", "type-mobile-app-bundle": "Mobilpakke", "type-mobile-app-bundles": "Mobilpakker", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Én mobilpakke} other {Liste over # mobilpakker} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Én mobilpakke} other {Liste over # mobilpakker} }", + "limit-reached": "Grense nådd", + "limit-reached-text": "Du har nådd grensen på {{ entities }}. For å legge til flere, be systemadministratoren din om å øke grensen for {{ entity }}.", + "request-limit-increase": "Be om grenseøkning", + "request-sysadmin-text": "Er du systemadministratoren?", + "login-here": "Logg inn her", + "to-increase-limit": "for å øke grensen.", + "increase-limit-request-sent-title": "Vi har sendt en automatisk forespørsel til systemadministratoren din om å øke grensen", + "increase-limit-request-sent-text": "Gi dem litt tid til å vurdere forespørselen og oppdatere innstillingene. Du må kanskje oppdatere denne siden for å se endringene." }, "entity-field": { "created-time": "Opprettelsestid", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Tilbakestillingslenke er sendt", "email": "Epost", "invalid-email-format": "Ugyldig epostformat.", + "sign-in-with": "Logg inn med {{name}}", + "sign-in-to-your-account": "Logg inn på kontoen din", "or": "eller", "error": "Innloggingsfeil", "verify-your-identity": "Bekreft din identitet", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Aktiveringslenken er utløpt", "activation-link-expired-message": "Lenken for å aktivere profilen din er utløpt. Du kan gå tilbake til innloggingssiden for å motta en ny epost.", "reset-password-link-expired": "Lenken for tilbakestilling av passord er utløpt", - "reset-password-link-expired-message": "Lenken for å tilbakestille passordet ditt er utløpt. Du kan gå tilbake til innloggingssiden for å motta en ny epost." + "reset-password-link-expired-message": "Lenken for å tilbakestille passordet ditt er utløpt. Du kan gå tilbake til innloggingssiden for å motta en ny epost.", + "two-fa": "Tofaktorautentisering", + "two-fa-required": "Tofaktorautentisering er påkrevd", + "set-up-verification-method": "Konfigurer en verifiseringsmetode for å fortsette", + "set-up-verification-method-login": "Konfigurer en verifiseringsmetode eller logg inn", + "enable-authenticator-app": "Aktiver autentiseringsapp", + "enable-authenticator-app-description": "Angi sikkerhetskoden fra autentiseringsappen din", + "enable-authenticator-sms": "Aktiver SMS-autentisering", + "enable-authenticator-sms-description": "Angi en 6-sifret kode vi nettopp sendte til ", + "enable-authenticator-email": "Aktiver E-post-autentisering", + "enable-authenticator-email-description": "En sikkerhetskode er sendt til e-postadressen din på ", + "enter-key-manually": "eller angi denne 32-sifrede nøkkelen manuelt:", + "continue": "Fortsett", + "confirm": "Bekreft", + "authenticator-app-success": "Autentiseringsapp ble aktivert", + "authenticator-app-success-description": "Neste gang du logger inn, må du oppgi en tofaktorautentiseringskode", + "authenticator-sms-success": "SMS-autentisering ble aktivert", + "authenticator-sms-success-description": "Neste gang du logger inn, blir du bedt om å angi sikkerhetskoden som sendes til telefonnummeret", + "authenticator-email-success": "E-post-autentisering ble aktivert", + "authenticator-email-success-description": "Neste gang du logger inn, blir du bedt om å angi sikkerhetskoden som sendes til e-postadressen din", + "authenticator-backup-code-success": "Reservekode ble aktivert", + "authenticator-backup-code-success-description": "Neste gang du logger inn, blir du bedt om å angi sikkerhetskoden eller bruke én av reservekodene.", + "add-verification-method": "Legg til verifiseringsmetode", + "get-backup-code": "Hent reservekoder", + "copy-key": "Kopier nøkkel", + "send-code": "Send kode", + "email-label": "E-post", + "email-description": "Angi en e-postadresse som skal brukes som autentisering.", + "sms-description": "Angi et telefonnummer som skal brukes som autentisering.", + "backup-code-description": "Skriv ut kodene slik at du har dem tilgjengelig når du trenger dem for å logge inn på kontoen din. Du kan bruke hver reservekode én gang.", + "backup-code-warn": "Når du forlater denne siden, kan disse kodene ikke vises igjen. Oppbevar dem trygt ved å bruke alternativene nedenfor.", + "download-txt": "Last ned (txt)", + "print": "Skriv ut", + "verification-code": "6-sifret kode", + "verification-code-invalid": "Ugyldig format for verifiseringskode", + "verification-code-incorrect": "Verifiseringskoden er feil", + "verification-code-many-request": "For mange forespørsler om å sjekke verifiseringskode", + "scan-qr-code": "Skann denne QR-koden med verifiseringsappen din", + "phone-input": { + "phone-input-label": "Telefonnummer", + "phone-input-required": "Telefonnummer er påkrevd", + "phone-input-validation": "Telefonnummeret er ugyldig eller ikke mulig", + "phone-input-pattern": "Ugyldig telefonnummer. Skal være i E.164-format, f.eks. {{phoneNumber}}", + "phone-input-hint": "Telefonnummer i E.164-format, f.eks. {{phoneNumber}}" + } }, "mobile": { "add-application": "Legg til applikasjon", @@ -4175,6 +4717,7 @@ "api-usage-limit": "API-bruksgrense", "device-activity": "Enhetsaktivitet", "entities-limit": "Entitetsgrense", + "entities-limit-increase-request": "Forespørsel om økning av entitetsgrense", "entity-action": "Entitetshandling", "general": "Generelt", "rule-engine-lifecycle-event": "Regelmotor livssyklushendelse", @@ -4392,6 +4935,12 @@ "at-least": "Minst:", "character": "{ count, plural, =1 {1 tegn} other {# tegn} }", "digit": "{ count, plural, =1 {1 siffer} other {# sifre} }", + "password-tooltip-min-length": "Minst {{minimumLength}} tegn", + "password-tooltip-max-length": "Høyst {{maximumLength}} tegn", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} stor bokstav", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} liten bokstav", + "password-tooltip-digit": "{{minimumDigits}} tall", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} spesialtegn", "incorrect-password-try-again": "Feil passord. Prøv igjen", "lowercase-letter": "{ count, plural, =1 {1 liten bokstav} other {# små bokstaver} }", "new-passwords-not-match": "Nytt passord samsvarer ikke", @@ -4450,7 +4999,8 @@ "additional-info": "Tilleggsinfo (JSON)", "invalid-additional-info": "Kan ikke tolke tilleggsinfo som gyldig JSON.", "no-relations-text": "Ingen relasjoner funnet", - "not": "Ikke" + "not": "Ikke", + "copy-type": "Kopier type" }, "resource": { "add": "Legg til ressurs", @@ -5400,7 +5950,8 @@ }, "time-series": "Tidsserie", "latest": "Siste verdier", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Beregnede felt og alarmregler" }, "save-attribute": { "processing-settings": "Behandlingsinnstillinger", @@ -5595,7 +6146,8 @@ "bad-request-params": "Feil forespørselsparametere", "item-not-found": "Element ikke funnet", "too-many-requests": "For mange forespørsler", - "too-many-updates": "For mange oppdateringer" + "too-many-updates": "For mange oppdateringer", + "entities-limit-exceeded": "Entitetsgrense overskredet" }, "tenant": { "tenant": "Leietaker", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Maksimalt antall argumenter per beregnet felt", "max-arguments-per-cf-range": "Maksimalt antall argumenter per beregnet felt kan ikke være negativt", "max-arguments-per-cf-required": "Maksimalt antall argumenter per beregnet felt er påkrevd", + "max-related-level-per-argument": "Maksimalt relasjonsnivå per 'Relaterte entiteter'-argument", + "max-related-level-per-argument-range": "Maksimalt relasjonsnivå per 'Relaterte entiteter'-argument kan ikke være mindre enn '1'", + "max-related-level-per-argument-required": "Maksimalt relasjonsnivå per 'Relaterte entiteter'-argument er påkrevd", + "min-allowed-scheduled-update-interval": "Min tillatt oppdateringsintervall for 'Relaterte entiteter'-argumenter (sekunder)", + "min-allowed-scheduled-update-interval-range": "Min tillatt oppdateringsintervall kan ikke være negativt", + "min-allowed-deduplication-interval": "Min tillatt dedupliseringsintervall (sekunder)", + "min-allowed-deduplication-interval-range": "Min tillatt dedupliseringsintervallverdi kan ikke være negativ", + "min-allowed-deduplication-interval-required": "Min tillatt dedupliseringsintervall er påkrevd", + "intermediate-aggregation-interval": "Mellomliggende aggregeringsintervall (sekunder)", + "intermediate-aggregation-interval-range": "Mellomliggende aggregeringsintervallverdi kan ikke være mindre enn '1'", + "intermediate-aggregation-interval-required": "Mellomliggende aggregeringsintervall er påkrevd", + "reevaluation-check-interval": "Sjekkintervall for revurdering (sekunder)", + "reevaluation-check-interval-range": "Sjekkintervallverdi for revurdering kan ikke være mindre enn '1'", + "reevaluation-check-interval-required": "Sjekkintervall for revurdering er påkrevd", + "alarms-reevaluation-interval": "Revurderingsintervall for alarmer (sekunder)", + "alarms-reevaluation-interval-range": "Revurderingsintervallverdi for alarmer kan ikke være mindre enn '1'", + "alarms-reevaluation-interval-required": "Revurderingsintervall for alarmer er påkrevd", + "min-allowed-aggregation-interval": "Min tillatt aggregeringsintervall (sekunder)", + "min-allowed-aggregation-interval-range": "Min tillatt aggregeringsintervallverdi kan ikke være negativ", + "min-allowed-aggregation-interval-required": "Min tillatt aggregeringsintervall er påkrevd", + "min-allowed-scheduled-update-interval-required": "Min tillatt oppdateringsintervall er påkrevd", "max-state-size": "Maksimal tilstandsstørrelse i KB", "max-state-size-range": "Maksimal tilstandsstørrelse i KB kan ikke være negativ", "max-state-size-required": "Maksimal tilstandsstørrelse i KB er påkrevd", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Maksimalt antall abonnementer per vanlig bruker", "ws-limit-max-subscriptions-per-public-user": "Maksimalt antall abonnementer per offentlig bruker", "ws-limit-updates-per-session": "WS-oppdateringer per økt", + "relation-search-entity-limit": "Entitetsgrense for relasjonssøk", + "relation-search-entity-limit-hint": "Begrenser antall entiteter som løses på siste nivå i relasjonsstien. Gjelder for 'Relaterte entiteter'-argumenter og propageringsfelter.", + "relation-search-entity-limit-required": "Entitetsgrense for relasjonssøk", + "relation-search-entity-limit-range": "Entitetsgrense for relasjonssøk kan ikke være mindre enn '1'", "rate-limits": { "add-limit": "Legg til grense", "and-also-less-than": "og også mindre enn", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Standard grupperingsintervall", "edit-intervals-list-hint": "Liste over tilgjengelige intervallalternativer kan spesifiseres.", "edit-grouping-intervals-list-hint": "Det er mulig å konfigurere listen over grupperingsintervaller og standardintervall.", - "all": "Alle" + "all": "Alle", + "save-current-settings-as-default": "Lagre gjeldende innstillinger som standard tidsvindu", + "hide-option-from-end-users": "Skjul alternativet for sluttbrukere" }, "tooltip": { "trigger": "Utløser", @@ -6648,6 +7227,7 @@ "export-attributes": "Eksporter attributter", "export-credentials": "Eksporter legitimasjon", "export-calculated-fields": "Eksporter beregnede felt \nog alarmregler", + "export-alarm-rules": "Eksporter alarmregler", "entity-versions": "Entitetsversjoner", "versions": "Versjoner", "created-time": "Opprettet tidspunkt", @@ -6665,6 +7245,7 @@ "load-attributes": "Last inn attributter", "load-credentials": "Last inn legitimasjon", "load-calculated-fields": "Last inn beregnede felt", + "load-alarm-rules": "Last inn alarmregler", "compare-with-current": "Sammenlign med nåværende", "diff-entity-with-version": "Forskjell med entitetsversjon '{{versionName}}'", "previous-difference": "Forrige forskjell", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Skann QR-kode", "make-phone-call": "Ring telefonsamtale", "get-location": "Hent telefonens posisjon", - "take-screenshot": "Ta skjermbilde" + "take-screenshot": "Ta skjermbilde", + "handle-provision-success-function": "Håndter funksjon for vellykket klargjøring", + "get-location-function": "Hent posisjonsfunksjon", + "process-launch-result-function": "Behandle funksjon for startresultat", + "get-phone-number-function": "Hent telefonnummerfunksjon", + "process-image-function": "Behandle bildefunksjon", + "process-qr-code-function": "Behandle QR-kodefunksjon", + "process-location-function": "Behandle posisjonsfunksjon", + "handle-empty-result-function": "Håndter funksjon for tomt resultat", + "handle-error-function": "Håndter feilfunksjon", + "handle-non-mobile-fallback-function": "Håndter fallback-funksjon for ikke-mobil", + "save-to-gallery": "Lagre i 'Bildegalleriet'", + "provision-type": "Klargjøringstype", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Egendefinert handlingsfunksjon", "custom-pretty-function": "Egendefinert handling (med HTML-mal) funksjon", @@ -6883,7 +7480,8 @@ "marker": "Markør", "polygon": "Polygon", "rectangle": "Rektangel", - "circle": "Sirkel" + "circle": "Sirkel", + "polyline": "Polylinje" }, "place-map-item": "Plasser kartobjekt", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Fortsett å tegne polygon", "finish-draw-polygon": "Fullfør tegning av polygon", "start-draw-circle": "Start å tegne sirkel", - "finish-draw-circle": "Fullfør tegning av sirkel" + "finish-draw-circle": "Fullfør tegning av sirkel", + "start-draw-polyline": "Start tegning av polylinje", + "finish-draw-polyline": "Fullfør tegning av polylinje" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Oppdater animasjonsforsinkelse" }, "chart-axis": { + "limit": "Grense", + "source": "Kilde", + "key-value": "Nøkkel / verdi", + "value-required": "Verdi er påkrevd.", + "entity-key-required": "Entitetsnøkkel er påkrevd.", + "key-required": "Nøkkel er påkrevd.", + "scale-limits": "Skaleringsgrenser", + "scale-appearance": "Skaleringsutseende", "scale": "Skala", "scale-min": "min", "scale-max": "maks", @@ -8005,7 +8613,10 @@ "add-radio-option": "Legg til radioknappealternativ", "radio-label-position": "Etikettplassering", "radio-label-position-before": "Før", - "radio-label-position-after": "Etter" + "radio-label-position-after": "Etter", + "save-image": "Lagre bilde", + "save-to-gallery": "Lagre automatisk bilder som tas opp i Bildegalleriet", + "public-image": "Gjør bildet tilgjengelig for alle uautoriserte brukere" }, "invalid-qr-code-text": "Ugyldig inputtekst for QR-kode. Input må være av typen streng", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Turer", "markers": "Markører", "polygons": "Polygoner", - "circles": "Sirkler" + "circles": "Sirkler", + "polylines": "Polylinjer" }, "data-layer": { "source": "Kilde", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Sirkel for '{{entityName}}': klikk for å avslutte og lagre sirkel", "finish-circle-hint": "Sirkel: klikk for å avslutte tegning" }, + "polyline": { + "polyline-key": "Polylinjenøkkel", + "polyline-key-required": "Polylinjenøkkel er påkrevd", + "no-polylines": "Ingen polylinjer konfigurert", + "add-polylines": "Legg til polylinje", + "polyline-configuration": "Polylinjekonfigurasjon", + "remove-polyline": "Fjern polylinje", + "edit": "Rediger polylinje", + "cut": "Klipp polylinjeområde", + "rotate": "Roter polylinje", + "remove-polyline-for": "Fjern polylinje for '{{entityName}}'", + "draw-polyline": "Tegn polylinje", + "polyline-place-first-point-hint-with-entity": "Polylinje for '{{entityName}}': klikk for å plassere første punkt", + "polyline-place-first-point-hint": "Polylinje: klikk for å plassere første punkt", + "finish-polyline-hint-with-entity": "Polylinje for '{{entityName}}': klikk for å fullføre tegningen", + "finish-polyline-hint": "Polylinje: klikk for å fullføre tegningen", + "polyline-place-first-point-cut-hint": "Klikk for å plassere første punkt", + "finish-polyline-cut-hint": "Klikk på første markør for å fullføre og lagre" + }, "select-entity": "Velg entitet", "select-entity-hint": "Tips: etter valg, klikk på kartet for å angi posisjon" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Vis tomt område i stedet for skjult cellehandling", "dont-reserve-space-hidden-action": "Ikke reserver plass til skjulte handlingsknapper", "display-timestamp": "Tidsstempel", + "timestamp-column-name": "Tidsstempel", "display-pagination": "Vis paginering", "default-page-size": "Standard sidestørrelse", "page-step-settings": "Sideinnstillinger", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Minst én alarmkolonne må angis", "table-tabs": "Tabellfaner", "show-cell-actions-menu-mobile": "Vis rullegardinmeny for cellehandlinger i mobilmodus", - "disable-sorting": "Deaktiver sortering" + "disable-sorting": "Deaktiver sortering", + "sort-by": "Sorter faner etter", + "sort-timestamp-option": "Opprettet tid" }, "latest-chart": { "total": "Totalt", @@ -9491,11 +10125,28 @@ "content": "

    Ved å opprette dashbord for sluttbruker, kan kundebrukeren bare se sine egne enheter og data. Data fra andre kunder vil være skjult.

    Følg dokumentasjonen for hvordan du gjør det:

    " } } + }, + "api-usage": { + "api-usage": "API-bruk", + "label": "Etikett", + "state-name": "Tilstandsnavn", + "status": "Status", + "status-required": "Status er påkrevd.", + "limit": "Maksgrense", + "limit-required": "Maksgrense er påkrevd.", + "current-number": "Gjeldende antall", + "current-number-required": "Gjeldende antall er påkrevd.", + "add-key": "Legg til nøkkel", + "no-key": "Ingen nøkkel", + "delete-key": "Slett nøkkel", + "target-dashboard-state": "Måltilstand for dashbord", + "go-to-main-state": "Gå til standardvisning" } }, "icon": { "icon": "Ikon", "icons": "Ikoner", + "custom": "Egendefinert", "select-icon": "Velg ikon", "material-icons": "Materialikoner", "show-all": "Vis alle ikoner", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "av" }, "language": { + "auto": "Auto", "language": "Språk" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index 6dd6ca467e..7c69ed7751 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -3802,7 +3802,6 @@ "two-factor-authentication": "Uwierzytelnianie dwuskładnikowe", "passwords-mismatch-error": "Wpisane hasła muszą być takie same!", "password-again": "Hasło ponownie", - "sign-in": "proszę, zaloguj się", "username": "Nazwa użytkownika (e-mail)", "remember-me": "Zapamiętaj mnie", "forgot-password": "Zapomniałeś hasła?", diff --git a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json index f13ad03a3b..a602a04529 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json +++ b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json @@ -78,6 +78,7 @@ "show-more": "Mostrar mais", "dont-show-again": "Não mostrar novamente", "see-documentation": "Ver documentação", + "see-debug-events": "Ver eventos de depuração", "clear": "Limpar", "upload": "Carregar", "delete-anyway": "Excluir mesmo assim", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Autenticação de dois fatores", "available-providers": "Provedores disponíveis", + "available-providers-required": "Pelo menos um provedor 2FA deve ser configurado.", "issuer-name": "Nome do emissor", "issuer-name-required": "Nome do emissor é obrigatório.", "max-verification-failures-before-user-lockout": "Máximo de falhas de verificação antes do bloqueio do usuário", @@ -513,7 +515,9 @@ "verification-message-template-required": "Modelo de mensagem de verificação é obrigatório.", "within-time": "Dentro do tempo", "within-time-pattern": "O tempo deve ser um número inteiro positivo.", - "within-time-required": "Tempo é obrigatório." + "within-time-required": "Tempo é obrigatório.", + "force-2fa": "Impor autenticação de dois fatores", + "enforce-for": "Impor para" }, "jwt": { "security-settings": "Configurações de segurança JWT", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarme", + "alarm-list": "Lista de alarmes", "alarms": "Alarmes", "all-alarms": "Todos os alarmes", "select-alarm": "Selecionar alarme", @@ -649,7 +654,16 @@ "alarm-type": "Tipo de alarme", "enter-alarm-type": "Inserir tipo de alarme", "no-alarm-types-matching": "Nenhum tipo de alarme correspondente a '{{entitySubtype}}' foi encontrado.", - "alarm-type-list-empty": "Nenhum tipo de alarme selecionado." + "alarm-type-list-empty": "Nenhum tipo de alarme selecionado.", + "system-comments": { + "acked-by-user": "O alarme foi reconhecido pelo usuário {{userName}}", + "cleared-by-user": "O alarme foi limpo pelo usuário {{userName}}", + "assigned-to-user": "O alarme foi atribuído pelo usuário {{userName}} ao usuário {{assigneeName}}", + "unassigned-to-user": "O alarme foi desatribuído pelo usuário {{userName}}", + "unassigned-from-deleted-user": "O alarme foi desatribuído porque o usuário {{userName}} - foi excluído", + "comment-deleted": "O usuário {{userName}} excluiu seu comentário", + "severity-changed": "A severidade do alarme foi atualizada de {{oldSeverity}} para {{newSeverity}}" + } }, "alarm-activity": { "add": "Adicionar um comentário...", @@ -754,6 +768,7 @@ "name-max-length": "O nome deve ter menos de 256 caracteres", "label-max-length": "O rótulo deve ter menos de 256 caracteres", "description": "Descrição", + "description-required": "A descrição é obrigatória.", "type": "Tipo", "type-required": "O tipo é obrigatório.", "details": "Detalhes", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Atividade mensal de alarmes criados", "data-points": "Pontos de dados", "data-points-storage-days": "Dias de armazenamento de pontos de dados", + "data-points-storage-days-hourly-activity": "Atividade horária de dias de armazenamento de pontos de dados", + "data-points-storage-days-daily-activity": "Atividade diária de dias de armazenamento de pontos de dados", + "data-points-storage-days-monthly-activity": "Atividade mensal de dias de armazenamento de pontos de dados", "device-api": "API do dispositivo", "email": "Email", "email-messages": "Mensagens de email", @@ -900,6 +918,7 @@ "rule-node": "Nó de regra", "sms": "SMS", "sms-messages": "Mensagens SMS", + "sms-messages-hourly-activity": "Atividade horária de mensagens SMS", "sms-messages-daily-activity": "Atividade diária de mensagens SMS", "sms-messages-monthly-activity": "Atividade mensal de mensagens SMS", "successful": "${entityName} Bem-sucedido", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Atividade horária de persistência de telemetria", "telemetry-persistence-monthly-activity": "Atividade mensal de persistência de telemetria", "transport": "Transporte", + "transport-msg-hourly-activity": "Atividade horária de mensagens de transporte", + "transport-msg-daily-activity": "Atividade diária de mensagens de transporte", + "transport-msg-monthly-activity": "Atividade mensal de mensagens de transporte", "transport-daily-activity": "Atividade diária de transporte", "transport-data-points": "Pontos de dados de transporte", + "transport-data-points-hourly-activity": "Atividade horária de pontos de dados de transporte", + "transport-data-points-daily-activity": "Atividade diária de pontos de dados de transporte", + "transport-data-points-monthly-activity": "Atividade mensal de pontos de dados de transporte", "view-details": "Visualizar detalhes", "view-statistics": "Visualizar estatísticas", - "transport-messages": "Mensagens de transporte" + "transport-messages": "Mensagens de transporte", + "transport-messages-hourly-activity": "Atividade horária de mensagens de transporte", + "transport-data-point-hourly-activity": "Atividade horária de ponto de dados de transporte", + "javascript-function-executions": "Execuções de funções JavaScript", + "javascript-function-executions-hourly-activity": "Atividade horária de execuções de funções JavaScript", + "javascript-function-executions-daily-activity": "Atividade diária de execuções de funções JavaScript", + "javascript-function-executions-monthly-activity": "Atividade mensal de execuções de funções JavaScript", + "tbel-function-executions": "Execuções de funções TBEL", + "tbel-function-executions-hourly-activity": "Atividade horária de execuções de funções TBEL", + "tbel-function-executions-daily-activity": "Atividade diária de execuções de funções TBEL", + "tbel-function-executions-monthly-activity": "Atividade mensal de execuções de funções TBEL", + "created-reports": "Relatórios criados", + "created-reports-hourly-activity": "Atividade horária de relatórios criados", + "created-reports-daily-activity": "Atividade diária de relatórios criados", + "created-reports-monthly-activity": "Atividade mensal de relatórios criados", + "emails": "Emails", + "emails-hourly-activity": "Atividade horária de emails", + "emails-daily-activity": "Atividade diária de emails", + "emails-monthly-activity": "Atividade mensal de emails", + "status": { + "enabled": "Ativado", + "disabled": "Desativado", + "warning": "Aviso" + } }, "api-limit": { "cassandra-write-queries-core": "Consultas de escrita Cassandra da API Rest", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Mensagens uplink de edge", "edge-uplink-messages-per-edge": "Mensagens uplink de edge por edge" }, + "api-key": { + "api-key": "Chave de API", + "api-keys": "Chaves de API", + "delete-api-key-title": "Tem certeza de que deseja excluir a chave de API '{{name}}'?", + "delete-api-key-text": "Atenção: após a confirmação, a chave se tornará irrecuperável.", + "delete-api-keys-title": "Tem certeza de que deseja excluir { count, plural, =1 {1 chave de API} other {# chaves de API} }?", + "delete-api-keys-text": "Atenção: após a confirmação, todas as chaves selecionadas se tornarão irrecuperáveis.", + "expiration-date": "Data de expiração", + "date": "data", + "description": "Descrição", + "disable": "Desativar", + "edit-description": "Editar descrição", + "enable": "Ativar chave de API ", + "expiration-time": "Tempo de expiração", + "expiration-time-never": "Nunca", + "expiration-time-custom": "Personalizado", + "generate": "Gerar", + "generate-title": "Gerar chave de API", + "generate-text": "Nota: A chave de API herda as permissões do usuário para o qual foi criada.", + "generated-api-key-title": "Chave de API gerada. Vamos verificar a conectividade!", + "generated-api-key-copy": "Certifique-se de copiar e salvar sua chave de API agora, pois não será possível visualizá-la novamente.", + "generated-api-key-command": "Use as instruções a seguir para verificar a conectividade. Como resultado, você receberá as informações do usuário atual:", + "generated-api-key-insecure-url": "A execução de comandos por uma conexão HTTP não segura enviará sua chave de API sem criptografia, tornando-a vulnerável à interceptação.", + "list": "{ count, plural, =1 {Uma chave de API} other {Lista de # chaves de API} }", + "manage": "Gerenciar", + "manage-api-keys": "Gerenciar chaves de API", + "no-found": "Nenhuma chave de API encontrada", + "selected-api-keys": "{ count, plural, =1 {1 chave de API} other {# chaves de API} } selecionada(s)", + "search": "Pesquisar chaves de API", + "status": "Status", + "status-active": "Ativo", + "status-inactive": "Inativo", + "status-expired": "Expirado" + }, "audit-log": { "audit": "Auditoria", "audit-logs": "Logs de auditoria", @@ -991,7 +1073,11 @@ "type-provision-failure": "Falha no provisionamento do dispositivo", "type-timeseries-updated": "Telemetria atualizada", "type-timeseries-deleted": "Telemetria excluída", - "type-sms-sent": "SMS enviado" + "type-sms-sent": "SMS enviado", + "any-type": "Qualquer tipo", + "audit-log-filter-title": "Filtro de log de auditoria", + "filter-title": "Filtro", + "filter-types": "Tipos de log de auditoria" }, "debug-settings": { "label": "Configuração de depuração", @@ -1009,15 +1095,29 @@ "expression": "Expressão", "no-found": "Nenhum campo calculado encontrado", "list": "{ count, plural, =1 {Um campo calculado} other {Lista de # campos calculados} }", + "add": "Adicionar campo calculado", "selected-fields": "{ count, plural, =1 {1 campo calculado} other {# campos calculados} } selecionado(s)", "type": { "simple": "Simples", - "script": "Script" + "simple-hint": "Cálculo aritmético simples baseado em argumentos de entrada.", + "script": "Script", + "script-hint": "Cálculo sobre argumentos definidos usando um script TBEL.", + "geofencing": "Geofencing", + "geofencing-hint": "Avaliação da posição GPS da entidade e transições em relação aos grupos de zonas de geofencing configurados.", + "propagation": "Propagação", + "propagation-hint": "Propagação de dados para entidades pai ou filho com base na direção e tipo de relação.", + "related-entities-aggregation": "Agregação de entidades relacionadas", + "related-entities-aggregation-hint": "Agregação dos dados mais recentes de entidades relacionadas.", + "time-series-data-aggregation": "Agregação de dados de série temporal", + "time-series-data-aggregation-hint": "Agregação de dados históricos de uma entidade atual." }, + "preview": "Pré-visualização", "arguments": "Argumentos", "decimals-by-default": "Casas decimais padrão", "debugging": "Depuração de campo calculado", + "calculated-field-details": "Detalhes do campo calculado", "argument-name": "Nome do argumento", + "name": "Nome", "datasource": "Fonte de dados", "add-argument": "Adicionar argumento", "test-script-function": "Testar função do script", @@ -1029,6 +1129,8 @@ "argument-asset": "Ativo", "argument-customer": "Cliente", "argument-tenant": "Tenant atual", + "argument-owner": "Proprietário atual", + "argument-relation-query": "Entidades relacionadas", "argument-type": "Tipo de argumento", "attribute": "Atributo", "copy-argument-name": "Copiar nome do argumento", @@ -1042,12 +1144,14 @@ "shared-attributes": "Atributos compartilhados", "attribute-key": "Chave do atributo", "default-value": "Valor padrão", + "default-value-required": "O valor padrão é obrigatório.", "limit": "Máximo de valores", "time-window": "Janela de tempo", "customer-name": "Nome do cliente", "asset-name": "Nome do ativo", "timeseries": "Série temporal", "output": "Saída", + "output-hint": "Define como a saída é processada.", "create": "Criar novo campo calculado", "file": "Arquivo de campo calculado", "invalid-file-error": "Formato de arquivo inválido. Certifique-se de que o arquivo é um arquivo JSON válido.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Atenção: após a confirmação, todos os campos calculados selecionados serão removidos e todos os dados relacionados serão irrecuperáveis.", "test-with-this-message": "Testar com esta mensagem", "use-latest-timestamp": "Usar timestamp mais recente", + "entity-coordinates": "Coordenadas da entidade", + "latitude-time-series-key": "Chave de série temporal de latitude", + "latitude-time-series-key-required": "A chave de série temporal de latitude é obrigatória.", + "longitude-time-series-key": "Chave de série temporal de longitude", + "longitude-time-series-key-required": "A chave de série temporal de longitude é obrigatória.", + "geofencing-zone-groups": "Grupos de zonas de geofencing", + "geofencing-zone-groups-settings": "Configurações do grupo de zonas de geofencing", + "target-zone": "Zona alvo", + "perimeter-key": "Chave do perímetro", + "report-strategy": "Estratégia de relatório", + "no-zone-configured": "É necessária pelo menos uma zona.", + "no-zone-configured-required": "Pelo menos um grupo de zonas deve ser configurado.", + "add-zone-group": "Adicionar grupo de zonas", + "report-transition-event-only": "Somente eventos de transição", + "report-presence-status-only": "Somente status de presença", + "report-transition-event-and-presence": "Status de presença e eventos de transição", + "perimeter-attribute-key": "Chave do atributo de perímetro", + "perimeter-attribute-key-required": "A chave do atributo de perímetro é obrigatória.", + "perimeter-attribute-key-pattern": "A chave do atributo de perímetro é inválida.", + "entity-zone-relationship": "Caminho da entidade para as zonas", + "direction": "Direção da relação", + "direction-from": "Da entidade para a zona", + "direction-to": "Da zona para a entidade", + "relation-type": "Tipo de relação", + "create-relation-with-matched-zones": "Criar relações para a entidade de origem com as zonas correspondentes", + "relation-level": "Nível de relação", + "fetch-last-available-level": "Buscar apenas o último nível disponível", + "zone-group-refresh-interval": "Intervalo de atualização dos grupos de zonas", + "copy-zone-group-name": "Copiar nome do grupo de zonas", + "open-details-page": "Abrir página de detalhes da entidade", + "level": "Nível", + "direction-level": "Direção", + "direction-up": "Acima", + "direction-up-parent": "Acima para o pai", + "direction-down": "Abaixo", + "direction-down-child": "Abaixo para o filho", + "add-level": "Adicionar nível", + "delete-level": "Excluir nível", + "no-level": "Nenhum nível configurado", + "levels-required": "Pelo menos um nível deve ser configurado.", + "max-allowed-levels-error": "O nível de relação excede o máximo permitido.", + "propagation-path-related-entities": "Caminho de propagação para entidades relacionadas", + "propagate-type": { + "arguments-only": "Somente argumentos", + "expression-result": "Resultado do cálculo" + }, + "script": "Script", + "data-propagate": "Dados a propagar", + "output-key": "Chave de saída", + "copy-output-key": "Copiar chave de saída", + "aggregation-path-related-entities": "Caminho de agregação para entidades relacionadas", + "deduplication-interval": "Intervalo de deduplicação", + "deduplication-interval-min": "O intervalo de deduplicação deve ser de pelo menos {{ sec }} segundos.", + "deduplication-interval-hint": "Tempo mínimo entre agregações de telemetria.", + "deduplication-interval-required": "O intervalo de deduplicação é obrigatório.", + "calculated-field-filter-title": "Filtro de campo calculado", + "filter-title": "Filtro", + "calculated-field-types": "Tipos de campo calculado", + "events": "Eventos", + "any-type": "Qualquer tipo", + "metrics": { + "metrics": "Métricas", + "metrics-empty": "Pelo menos uma métrica deve ser configurada.", + "metric-name": "Nome da métrica", + "metric-name-required": "O nome da métrica é obrigatório.", + "metric-name-pattern": "O nome da métrica é inválido.", + "metric-name-duplicate": "Já existe uma métrica com esse nome.", + "metric-name-max-length": "O nome da métrica deve ter menos de 256 caracteres.", + "metric-name-forbidden": "O nome da métrica é reservado e não pode ser usado.", + "copy-metric-name": "Copiar nome da métrica", + "argument-name": "Nome do argumento", + "aggregation": "Agregação", + "aggregation-type": { + "avg": "Média", + "min": "Mínimo", + "max": "Máximo", + "sum": "Soma", + "count": "Contagem", + "count-unique": "Contagem única" + }, + "filtered": "Filtrado", + "value-source": "Fonte do valor", + "value-source-hint": "Define como o valor para agregação é obtido.", + "value-source-type": { + "key": "Chave", + "function": "Função" + }, + "no-metrics-configured": "É necessária pelo menos uma métrica.", + "add-metric": "Adicionar métrica", + "max-metrics": "Número máximo de métricas atingido.", + "metric-settings": "Configurações da métrica", + "filter": "Filtro", + "filter-hint": "Habilita a filtragem de entidades durante a agregação. A função de filtro deve retornar um valor booleano e pode usar todos os argumentos configurados." + }, + "output-strategy": { + "strategy": "Estratégia", + "process-right-away": "Processar imediatamente", + "process-rule-chains": "Processar via Cadeias de regras", + "save-time-series": "Salvar em série temporal", + "save-database": "Salvar no banco de dados", + "save-latest-values": "Salvar nos valores mais recentes", + "send-web-sockets": "Enviar para WebSockets", + "save-calculated-fields": "Enviar para Campos calculados", + "update-attribute-only-on-value-change": "Atualizar atributo somente na mudança de valor", + "send-attributes-updated-notification": "Enviar notificação de atributos atualizados", + "ttl": "TTL personalizado", + "ttl-required": "O TTL é obrigatório", + "ttl-min": "Somente TTL mínimo de 0 é permitido", + "processing-parameters": "Parâmetros de processamento", + "hint": { + "strategy": "Controla se o resultado é processado imediatamente ou enviado a uma cadeia de regras para processamento adicional.", + "processing-options": "Opções de processamento", + "update-attribute-only-on-value-change": "Atualiza o atributo em cada mensagem recebida, independentemente de o valor ter mudado. Isso aumenta o uso da API e reduz o desempenho.", + "update-attribute-only-on-value-change-enabled": "Atualiza o atributo somente quando o valor muda. Se o valor não mudar, os timestamps não são atualizados e as notificações não são enviadas.", + "send-attributes-updated-notification": "Envia um evento de Atributos Atualizados para a cadeia de regras padrão.", + "save-time-series": "Salva os dados de série temporal na tabela ts_kv do banco de dados.", + "save-database": "Salva os dados de atributos no banco de dados.", + "save-latest-values": "Atualiza os dados de série temporal na tabela ts_kv_latest do banco de dados se o novo valor for mais recente.", + "send-web-sockets-attribute": "Notifica as assinaturas WebSocket sobre atualizações nos dados de atributos.", + "send-web-sockets-time-series": "Notifica as assinaturas WebSocket sobre atualizações nos dados de série temporal.", + "save-calculated-fields-attribute": "Notifica os campos calculados sobre atualizações nos dados de atributos.", + "save-calculated-fields-time-series": "Notifica os campos calculados sobre atualizações nos dados de série temporal.", + "ttl": "Define o período de retenção dos dados de série temporal. Se desabilitado, o TTL do Perfil do tenant é usado." + } + }, + "aggregate-interval-type": "Tipo de intervalo de agregação", + "aggregate-interval-value": "Valor do intervalo de agregação", + "aggregate-interval-value-required": "O valor do intervalo de agregação é obrigatório.", + "aggregate-interval-value-min": "O valor do intervalo de agregação deve ser de pelo menos { sec, plural, =0 {0 segundo} =1 {1 segundo} other {# segundos} }.", + "aggregate-interval-value-step-multiple-of": "O valor do intervalo de agregação deve ser divisor ou múltiplo de 1 dia.", + "aggregate-period": { + "hour": "Hora", + "day": "Dia", + "week": "Semana (Seg - Dom)", + "week-sun-sat": "Semana (Dom - Sáb)", + "month": "Mês", + "quarter": "Trimestre", + "year": "Ano", + "custom": "Personalizado" + }, + "aggregate-period-hint-offset": "Seu intervalo de agregação será: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Seu intervalo de agregação será: {{ interval }} e assim por diante.", + "entity-aggregation": { + "argument-hint": "Os dados serão buscados da entidade atual.", + "argument-title-hint": "Define os argumentos de entrada usados para agregação.", + "argument-setting-hint": "A telemetria mais recente é o único tipo de argumento disponível para este campo calculado.", + "aggregation-interval": "Intervalo de agregação", + "aggregation-interval-hint": "Define com que frequência a agregação é realizada. Exemplo: a cada 1 hora agrega dados às 00:00, 01:00, 02:00, etc. Os resultados da agregação são armazenados com o timestamp correspondente ao início do intervalo de agregação.", + "apply-offset": "Aplicar deslocamento ao intervalo de agregação", + "apply-offset-hint": "Define quanto deslocar o início de cada período de agregação (ex.: +10 minutos - 00:10, 01:10).", + "offset-value": "Valor do deslocamento", + "offset-value-required": "O valor do deslocamento é obrigatório.", + "offset-value-min": "O valor do deslocamento deve ser um número inteiro positivo.", + "offset-value-max": "O valor do deslocamento deve ser menor que o valor do intervalo de agregação.", + "wait-delay": "Aplicar tempo de espera para telemetria atrasada", + "wait-delay-hint": "Define quanto tempo aguardar pela telemetria atrasada após o término do intervalo. Se tal telemetria chegar, o resultado para aquele intervalo será recalculado.", + "duration": "Duração", + "duration-required": "A duração é obrigatória.", + "duration-min": "A duração deve ser de pelo menos 1 minuto.", + "duration-hint": "Quanto tempo aguardar pelos dados atrasados após o término do intervalo.", + "produce-intermediate-result": "Produzir resultado intermediário", + "produce-intermediate-result-hint": "Calcula métricas durante o intervalo atual para produzir um resultado intermediário. As atualizações ocorrem no máximo uma vez a cada {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "O campo calculado do tipo simples não deve conter chaves com tipo de série temporal deslizante.", + "arguments-propagate-arguments-with-rolling": "O tipo 'Série temporal deslizante' é incompatível com a propagação 'Somente argumentos'.", + "arguments-propagate-argument-entity-type": "O tipo de entidade é incompatível com a propagação 'Somente argumentos'.", + "arguments-propagate-argument-must-current-entity": "Pelo menos um argumento deve ser configurado com o tipo de entidade de origem 'Entidade atual'.", "arguments-empty": "Pelo menos um argumento deve ser especificado.", "expression-required": "A expressão é obrigatória.", "expression-invalid": "A expressão é inválida", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Já existe um argumento com esse nome.", "argument-name-max-length": "O nome do argumento deve ter menos de 256 caracteres.", "argument-name-forbidden": "O nome do argumento é reservado e não pode ser usado.", + "output-key-required": "A chave de saída é obrigatória.", + "output-key-pattern": "A chave de saída é inválida.", + "output-key-duplicate": "Já existe uma chave com esse nome.", + "output-key-max-length": "A chave de saída deve ter menos de 256 caracteres.", + "output-key-forbidden": "A chave de saída é reservada e não pode ser usada.", + "entity-type-required": "O tipo de entidade é obrigatório", + "name-required": "O nome é obrigatório.", + "name-pattern": "O nome é inválido.", + "name-duplicate": "Já existe um nome com esse nome.", + "name-max-length": "O nome deve ter menos de 256 caracteres.", + "name-forbidden": "O nome é reservado e não pode ser usado.", "argument-type-required": "O tipo de argumento é obrigatório.", "max-args": "Número máximo de argumentos atingido.", "decimals-range": "As casas decimais padrão devem ser um número entre 0 e 15.", "expression": "A expressão padrão demonstra como transformar uma temperatura de Fahrenheit para Celsius.", "arguments-entity-not-found": "Entidade alvo do argumento não encontrada.", - "use-latest-timestamp": "Se habilitado, o valor calculado será persistido usando o timestamp mais recente da telemetria dos argumentos, em vez do horário do servidor." + "use-latest-timestamp": "Se habilitado, o valor calculado será persistido usando o timestamp mais recente da telemetria dos argumentos, em vez do horário do servidor.", + "entity-coordinates": "Especifique as chaves de série temporal que fornecem as coordenadas GPS da entidade (latitude e longitude).", + "geofencing-zone-groups": "Defina um ou mais grupos de zonas de geofencing para verificar (ex.: 'allowedZones', 'restrictedZones'). Cada grupo deve ter um nome único, que é usado como prefixo para as chaves de telemetria de saída do campo calculado.", + "perimeter-attribute-key": "Defina a chave do atributo que contém a definição do perímetro da zona de geofencing. O perímetro é sempre obtido dos atributos do lado do servidor da entidade de zona.", + "report-strategy": "O status de presença informa se a entidade está atualmente DENTRO ou FORA do grupo de zonas. Os eventos de transição informam quando a entidade ENTROU ou SAIU do grupo de zonas.", + "create-relation-with-matched-zones": "Criar e manter automaticamente relações entre a entidade e as zonas em que ela está atualmente. As relações são removidas quando a entidade sai de uma zona e criadas quando ela entra em uma nova.", + "relation-type-required": "O tipo de relação é obrigatório.", + "relation-level-required": "O nível de relação é obrigatório.", + "relation-level-min": "O valor mínimo do nível de relação é 1.", + "relation-level-max": "O valor máximo do nível de relação é {{max}}.", + "geofencing-empty": "Pelo menos um grupo de zonas deve ser configurado.", + "geofencing-entity-not-found": "Entidade alvo do geofencing não encontrada.", + "max-geofencing-zone": "Número máximo de zonas de geofencing atingido.", + "zone-group-refresh-interval": "Define com que frequência os grupos de zonas configurados via entidades relacionadas são atualizados.", + "zone-group-refresh-interval-required": "O intervalo de atualização dos grupos de zonas é obrigatório.", + "zone-group-refresh-interval-min": "O intervalo de atualização do grupo de zonas deve ser de pelo menos {{ min }} segundos.", + "propagation-path-related-entities": "Define um caminho direto de nível único para uma entidade relacionada com base na direção selecionada e no tipo de relação. Apenas relações entre entidades de dispositivo, ativo, cliente e tenant são suportadas. O máximo de entidades resolvidas pelo caminho de relação é {{ max }}.", + "data-propagate": "Define os dados a serem propagados a partir dos argumentos configurados abaixo. 'Somente argumentos' usa os dados recuperados diretamente, enquanto 'Resultado do cálculo' calcula um novo valor a partir desses dados.", + "aggregation-path-related-entities": "Define um caminho de agregação de nível único via relações diretas com entidades pai ou filho, com base na direção e no tipo de relação. Apenas relações entre entidades de dispositivo, ativo, cliente e tenant são suportadas. O máximo de entidades resolvidas pelo caminho de relação é {{ max }}.", + "arguments-aggregation": "Define os argumentos de entrada usados para filtragem e agregação.", + "setting-arguments-aggregation": "Os dados serão buscados das entidades relacionadas configuradas no caminho de agregação.", + "metrics": "Define as métricas agregadas com base nos argumentos configurados.", + "entity-aggregation-metrics": "Define as métricas agregadas com base nos argumentos configurados ao longo dos intervalos de tempo especificados.", + "import-invalid-calculated-field-type": "Não foi possível importar o campo calculado: Estrutura de campo calculado inválida.", + "simple-expression-title": "Expressão aritmética que define como o valor calculado é computado.", + "script-title": "Script TBEL que define a lógica de cálculo e os valores de saída.", + "simple-arguments": "Expressão aritmética que define como o valor calculado é computado.", + "script-arguments": "Define os argumentos de entrada disponíveis para o script." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Regras de alarme", + "alarm-rule": "Regra de alarme", + "alarm-rules": "Regras de alarme", + "alarm-rules-old": "Antigas", + "alarm-rules-actual": "Atuais", + "severities": "Severidades", + "cleared": "Condição de limpeza", + "delete-title": "Tem certeza de que deseja excluir a regra de alarme '{{title}}'?", + "delete-text": "Atenção: após a confirmação, a regra de alarme e todos os dados relacionados serão irrecuperáveis.", + "delete-multiple-title": "Tem certeza de que deseja excluir { count, plural, =1 {1 regra de alarme} other {# regras de alarme} }?", + "delete-multiple-text": "Atenção: após a confirmação, todas as regras de alarme selecionadas serão removidas e todos os dados relacionados serão irrecuperáveis.", + "create": "Criar nova regra de alarme", + "add": "Adicionar regra de alarme", + "copy": "Copiar configuração da regra de alarme", + "details": "Detalhes da regra de alarme", + "no-found": "Nenhuma regra de alarme encontrada", + "list": "{ count, plural, =1 {Uma regra de alarme} other {Lista de # regras de alarme} }", + "selected-fields": "{ count, plural, =1 {1 regra de alarme} other {# regras de alarme} } selecionada(s)", + "import": "Importar regra de alarme", + "file": "Arquivo de regra de alarme", + "export": "Exportar regra de alarme", + "export-failed-error": "Não foi possível exportar a regra de alarme: {{error}}", + "entity-type": "Tipo de entidade", + "entity-type-required": "O tipo de entidade é obrigatório.", + "alarm-type": "Tipo de alarme", + "alarm-type-hint": "Identificador único (ex.: HighTempAlarm) no escopo do originador do alarme (Dispositivo, Ativo, etc.) para evitar conflitos.", + "alarm-type-required": "O tipo de alarme é obrigatório.", + "alarm-type-pattern": "O tipo de alarme é inválido.", + "alarm-type-max-length": "O tipo de alarme deve ter menos de 256 caracteres.", + "clear-alarm": "Limpar alarme", + "value-argument": "Argumento", + "value-argument-required": "O argumento é obrigatório.", + "static-settings": "Configurações estáticas", + "configuration": "Configuração", + "static-schedule": "Estático", + "dynamic-schedule": "Dinâmico", + "operation-and": "E", + "operation-or": "OU", + "condition-during": "Durante {{during}}", + "condition-during-dynamic": "Durante \"{{ attribute }}\"", + "condition-repeat-times": "Repete { count, plural, =1 {1 vez} other {# vezes} }", + "condition-repeat-times-dynamic": "Repete \"{{ attribute }}\" vezes", + "filter-preview": "Pré-visualização do filtro", + "condition-settings": "Configurações da condição", + "static": "Estático", + "dynamic": "Dinâmico", + "argument-filters": "Filtros de argumento", + "argument-name": "Nome do argumento", + "value-type": "Tipo de valor", + "general": "Geral", + "filters": "Filtros", + "date-time-hint": "O argumento deve estar em milissegundos epoch. Exemplo: 1698839340000 equivale a 2023-11-01 12:49:00 UTC.", + "operation": "Operação", + "value-source": "Fonte do valor", + "value": "Valor", + "ignore-case": "Ignorar maiúsculas/minúsculas", + "condition": "Condição", + "script": "Script", + "add-filter": "Adicionar filtro de argumento", + "edit-filter": "Filtro de argumento", + "remove-filter": "Remover filtro de argumento", + "no-filter": "É necessário pelo menos um filtro.", + "conditions": { + "simple": "Simples", + "duration": "Duração", + "repeating": "Repetição" + }, + "schedule-title": "Agendamento", + "edit-schedule": "Editar agendamento do alarme", + "schedule-type": "Tipo de agendador", + "schedule-type-required": "O tipo de agendador é obrigatório.", + "schedule": { + "any-time": "Ativo o tempo todo", + "specific-time": "Ativo em um horário específico", + "custom": "Personalizado" + }, + "schedule-day": { + "monday": "Segunda-feira", + "tuesday": "Terça-feira", + "wednesday": "Quarta-feira", + "thursday": "Quinta-feira", + "friday": "Sexta-feira", + "saturday": "Sábado", + "sunday": "Domingo" + }, + "schedule-days": "Dias", + "schedule-time": "Horário", + "schedule-time-from": "De", + "schedule-time-to": "Até", + "schedule-days-of-week-required": "Pelo menos um dia da semana deve ser selecionado.", + "tbel": "TBEL", + "expression-type": { + "simple": "Simples", + "script": "Script" + }, + "operation-type": { + "and": "E", + "or": "Ou" + }, + "filter-predicate-type": { + "string": "String", + "numeric": "Numérico", + "boolean": "Booleano", + "complex": "Complexo" + }, + "alarm-rule-additional-info": "Informações adicionais", + "edit-alarm-rule-additional-info": "Editar informações adicionais", + "alarm-rule-additional-info-placeholder": "Forneça seus comentários e ajustes aqui para exibi-los nos detalhes do Alarme em Informações adicionais", + "alarm-rule-additional-info-hint": "Dica: use ${Argument name} para substituir os valores dos argumentos usados na condição da regra de alarme.", + "alarm-rule-additional-info-icon-hint": "Use o nome do argumento para substituir os valores dos argumentos usados na condição da regra de alarme.", + "alarm-rule-mobile-dashboard": "Dashboard móvel", + "alarm-rule-mobile-dashboard-hint": "Usado pelo aplicativo móvel como dashboard de detalhes do alarme.", + "alarm-rule-no-mobile-dashboard": "Nenhum dashboard selecionado", + "alarm-rule-condition": "Condição da regra de alarme", + "enter-alarm-rule-condition-prompt": "Adicionar condição", + "enter-alarm-rule-clear-condition-prompt": "Adicionar condição de limpeza", + "edit-alarm-rule-condition": "Condição do alarme", + "condition-type": "Tipo de condição", + "condition-type-hint": "As opções \"Duração\" e \"Repetição\" não estão disponíveis quando a operação \"Ausente por\" é usada no filtro.", + "select-alarm-severity": "Selecionar severidade do alarme", + "add-create-alarm-rule-prompt": "É necessária pelo menos uma condição de acionamento.", + "add-create-alarm-rule": "Adicionar condição de acionamento", + "add-clear-alarm-rule": "Adicionar condição de limpeza", + "condition-duration": "Duração da condição", + "condition-duration-value": "Valor da duração", + "condition-duration-time-unit": "Unidade de tempo", + "condition-duration-value-range": "O valor da duração deve estar no intervalo de 1 a 2147483647.", + "condition-duration-value-pattern": "O valor da duração deve ser um número inteiro.", + "condition-duration-value-required": "O valor da duração é obrigatório.", + "condition-duration-time-unit-required": "A unidade de tempo é obrigatória.", + "condition-repeating-value": "Contagem de eventos", + "condition-repeating-value-hint": "A atualização de qualquer argumento da regra de alarme será contada como evento", + "condition-repeating-value-range": "A contagem de eventos deve estar no intervalo de 1 a 2147483647.", + "condition-repeating-value-pattern": "A contagem de eventos deve ser um número inteiro.", + "condition-repeating-value-required": "A contagem de eventos é obrigatória.", + "create-conditions": "Condições de acionamento", + "clear-condition": "Condição de limpeza", + "no-clear-alarm-rule": "Nenhuma condição de limpeza configurada.", + "advanced-settings": "Configurações avançadas", + "propagate-alarm": "Propagar alarme para entidades relacionadas", + "alarm-rule-relation-types-list": "Tipos de relação", + "alarm-rule-relation-types-list-hint": "Define os tipos de relação para filtrar as entidades relacionadas. Se não definido, o alarme será propagado para todas as entidades relacionadas.", + "propagate-alarm-to-owner": "Propagar alarme para o proprietário da entidade (Cliente ou Tenant)", + "propagate-alarm-to-tenant": "Propagar alarme para o Tenant", + "alarm-rule-filter-title": "Filtro de regra de alarme", + "filter-title": "Filtro", + "debugging": "Depuração de regra de alarme", + "any-type": "Qualquer tipo", + "enter-alarm-rule-type": "Inserir tipo de alarme", + "no-alarm-rule-types-matching": "Nenhum tipo de alarme correspondente a '{{entitySubtype}}' foi encontrado.", + "alarm-rule-type-list-empty": "Nenhum tipo de alarme selecionado.", + "alarm-rule-type-list": "Lista de tipos de alarme", + "alarm-rule-entity-list": "Lista de entidades", + "missing-for": "ausente por", + "time-unit": "Unidade", + "mode": "Modo", + "type": "Tipo", + "value-required": "O valor é obrigatório.", + "min-value": "O valor deve ser 1 ou maior.", + "argument-in-use": "O argumento está sendo usado como argumento geral.", + "import-invalid-alarm-rule-type": "Não foi possível importar a regra de alarme: Estrutura de regra de alarme inválida.", + "no-filter-preview": "Nenhum filtro especificado", + "filter-operation": { + "and": "E", + "or": "Ou" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Documentação", "time-left": "{{time}} restante", "output": "Saída", + "sort-asc": "Crescente", + "sort-desc": "Decrescente", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Ordem do dashboard no aplicativo móvel", "mobile-hide": "Ocultar dashboard no aplicativo móvel", "update-image": "Atualizar imagem do dashboard", + "update-new-version": "Carregar nova versão", + "upload-file-to-update": "Carregar arquivo para atualizar", "take-screenshot": "Tirar captura de tela", "select-widget-title": "Selecionar widget", "select-widget-value": "{{title}}: selecionar widget", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrap do cliente", "bootstrap-server": "Servidor Bootstrap", "lwm2m-server": "Servidor LwM2M", + "client-reboot": "Gatilho de atualização de registro", + "bootstrap-reboot": "Gatilho de Bootstrap-Request", "client-publicKey-or-id": "Chave pública ou Id do cliente", "client-publicKey-or-id-required": "A chave pública ou Id do cliente é obrigatória.", "client-publicKey-or-id-tooltip-psk": "O identificador PSK é um identificador PSK arbitrário de até 128 bytes, conforme descrito no padrão [RFC7925].\nO identificador PSK DEVE ser primeiro convertido em uma string de caracteres e depois codificado em octetos usando UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "O ID curto do servidor é obrigatório.", "short-id-range": "O ID curto do servidor deve estar entre {{ min }} e {{ max }}.", "short-id-pattern": "O ID curto do servidor deve ser um número inteiro positivo.", + "short-id-pattern-bs": "O ID curto do servidor deve ser apenas nulo", "lifetime": "Tempo de vida do registro do cliente", "lifetime-required": "O tempo de vida do registro do cliente é obrigatório.", "lifetime-pattern": "O tempo de vida do registro do cliente deve ser um número inteiro positivo.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Todos os recursos são observados com uma única solicitação Composite Observe (mais eficiente, menos flexível)", "composite-by-object": "Composto por objetos", "composite-by-object-description": "Os recursos são agrupados por tipo de objeto e observados usando solicitações Composite Observe separadas (abordagem equilibrada)" - } + }, + "init-attr-tel-as-obs-strategy": "Inicializar atributos e telemetria usando a estratégia Observe", + "init-attr-tel-as-obs-strategy-hint": "Se falso - atributos e telemetria são inicializados lendo seus valores um por um.\\nSe verdadeiro - atributos e telemetria são inicializados assinando seus valores usando a estratégia Observe." }, "snmp": { "add-communication-config": "Adicionar configuração de comunicação", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Nós de regra", "list-of-rulenodes": "{ count, plural, =1 {Um nó de regra} other {Lista de # nós de regra} }", "rulenode-name-starts-with": "Nós de regra cujos nomes começam com '{{prefix}}'", + "type-api-key": "Chave de API", + "type-api-keys": "Chaves de API", "type-current-customer": "Cliente atual", "type-current-tenant": "Tenant atual", "type-current-user": "Usuário atual", @@ -2656,6 +3143,7 @@ "details": "Detalhes da entidade", "no-entities-prompt": "Nenhuma entidade encontrada", "no-data": "Nenhum dado para exibir", + "show-all-columns": "Mostrar todos", "columns-to-display": "Colunas a exibir", "type-api-usage-state": "Estado de uso da API", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Um aplicativo móvel} other {Lista de # aplicativos móveis} }", "type-mobile-app-bundle": "Pacote móvel", "type-mobile-app-bundles": "Pacotes móveis", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Um pacote móvel} other {Lista de # pacotes móveis} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Um pacote móvel} other {Lista de # pacotes móveis} }", + "limit-reached": "Limite atingido", + "limit-reached-text": "Você atingiu o limite de {{ entities }}. Para adicionar mais, solicite ao Administrador do sistema que aumente o limite de {{ entity }}.", + "request-limit-increase": "Solicitar aumento do limite", + "request-sysadmin-text": "Você é o Administrador do sistema?", + "login-here": "Faça login aqui", + "to-increase-limit": "para aumentar o limite.", + "increase-limit-request-sent-title": "Enviamos uma solicitação automática ao Administrador do sistema para aumentar o limite", + "increase-limit-request-sent-text": "Aguarde enquanto a solicitação é revisada e as configurações são atualizadas. Pode ser necessário atualizar esta página para ver as alterações." }, "entity-field": { "created-time": "Data de criação", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Link de redefinição foi enviado", "email": "Email", "invalid-email-format": "Formato de email inválido.", + "sign-in-with": "Entrar com {{name}}", + "sign-in-to-your-account": "Entrar na sua conta", "or": "ou", "error": "Erro de login", "verify-your-identity": "Verificar sua identidade", @@ -3807,7 +4305,51 @@ "activation-link-expired": "O link de ativação expirou", "activation-link-expired-message": "O link para ativar seu perfil expirou. Você pode retornar à página de login para receber um novo email.", "reset-password-link-expired": "O link de redefinição de senha expirou", - "reset-password-link-expired-message": "O link para redefinir sua senha expirou. Você pode retornar à página de login para receber um novo email." + "reset-password-link-expired-message": "O link para redefinir sua senha expirou. Você pode retornar à página de login para receber um novo email.", + "two-fa": "Autenticação de dois fatores", + "two-fa-required": "A autenticação de dois fatores é obrigatória", + "set-up-verification-method": "Configurar um método de verificação para continuar", + "set-up-verification-method-login": "Configurar um método de verificação ou fazer login", + "enable-authenticator-app": "Habilitar aplicativo autenticador", + "enable-authenticator-app-description": "Insira o código de segurança do seu aplicativo autenticador", + "enable-authenticator-sms": "Habilitar autenticador SMS", + "enable-authenticator-sms-description": "Insira o código de 6 dígitos que acabamos de enviar para ", + "enable-authenticator-email": "Habilitar autenticador de email", + "enable-authenticator-email-description": "Um código de segurança foi enviado para o seu endereço de email em ", + "enter-key-manually": "ou insira esta chave de 32 dígitos manualmente:", + "continue": "Continuar", + "confirm": "Confirmar", + "authenticator-app-success": "Aplicativo autenticador habilitado com sucesso", + "authenticator-app-success-description": "Na próxima vez que fizer login, será necessário fornecer um código de autenticação de dois fatores", + "authenticator-sms-success": "Autenticador SMS habilitado com sucesso", + "authenticator-sms-success-description": "Na próxima vez que fizer login, será solicitado que insira o código de segurança que será enviado para o número de telefone", + "authenticator-email-success": "Autenticador de email habilitado com sucesso", + "authenticator-email-success-description": "Na próxima vez que fizer login, será solicitado que insira o código de segurança que será enviado para o seu endereço de email", + "authenticator-backup-code-success": "Código de backup habilitado com sucesso", + "authenticator-backup-code-success-description": "Na próxima vez que fizer login, será solicitado que insira o código de segurança ou use um dos códigos de backup.", + "add-verification-method": "Adicionar método de verificação", + "get-backup-code": "Obter código de backup", + "copy-key": "Copiar chave", + "send-code": "Enviar código", + "email-label": "Email", + "email-description": "Insira um email para usar como autenticador.", + "sms-description": "Insira um número de telefone para usar como autenticador.", + "backup-code-description": "Imprima os códigos para tê-los à mão quando precisar usá-los para entrar na sua conta. Cada código de backup pode ser usado uma vez.", + "backup-code-warn": "Após sair desta página, esses códigos não poderão ser exibidos novamente. Armazene-os com segurança usando as opções abaixo.", + "download-txt": "Baixar (txt)", + "print": "Imprimir", + "verification-code": "Código de 6 dígitos", + "verification-code-invalid": "Formato de código de verificação inválido", + "verification-code-incorrect": "O código de verificação está incorreto", + "verification-code-many-request": "Muitas solicitações para verificar o código de verificação", + "scan-qr-code": "Escaneie este código QR com seu aplicativo de verificação", + "phone-input": { + "phone-input-label": "Número de telefone", + "phone-input-required": "O número de telefone é obrigatório", + "phone-input-validation": "O número de telefone é inválido ou não é possível", + "phone-input-pattern": "Número de telefone inválido. Deve estar no formato E.164, ex. {{phoneNumber}}", + "phone-input-hint": "Número de telefone no formato E.164, ex. {{phoneNumber}}" + } }, "mobile": { "add-application": "Adicionar aplicação", @@ -4175,6 +4717,7 @@ "api-usage-limit": "Limite de uso de API", "device-activity": "Atividade do dispositivo", "entities-limit": "Limite de entidades", + "entities-limit-increase-request": "Solicitação de aumento do limite de entidades", "entity-action": "Ação de entidade", "general": "Geral", "rule-engine-lifecycle-event": "Evento de ciclo de vida do motor de regras", @@ -4392,6 +4935,12 @@ "at-least": "Pelo menos:", "character": "{ count, plural, =1 {1 caractere} other {# caracteres} }", "digit": "{ count, plural, =1 {1 dígito} other {# dígitos} }", + "password-tooltip-min-length": "Pelo menos {{minimumLength}} caracteres", + "password-tooltip-max-length": "No máximo {{maximumLength}} caracteres", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} letra maiúscula", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} letra minúscula", + "password-tooltip-digit": "{{minimumDigits}} número", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} caractere especial", "incorrect-password-try-again": "Senha incorreta. Tente novamente", "lowercase-letter": "{ count, plural, =1 {1 letra minúscula} other {# letras minúsculas} }", "new-passwords-not-match": "A nova senha não coincide", @@ -4450,7 +4999,8 @@ "additional-info": "Informações adicionais (JSON)", "invalid-additional-info": "Não foi possível analisar o JSON de informações adicionais.", "no-relations-text": "Nenhuma relação encontrada", - "not": "Não" + "not": "Não", + "copy-type": "Copiar tipo" }, "resource": { "add": "Adicionar recurso", @@ -5400,7 +5950,8 @@ }, "time-series": "Série temporal", "latest": "Valores mais recentes", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Campos calculados e regras de alarme" }, "save-attribute": { "processing-settings": "Configurações de processamento", @@ -5595,7 +6146,8 @@ "bad-request-params": "Parâmetros de requisição inválidos", "item-not-found": "Item não encontrado", "too-many-requests": "Muitas requisições", - "too-many-updates": "Muitas atualizações" + "too-many-updates": "Muitas atualizações", + "entities-limit-exceeded": "Limite de entidades excedido" }, "tenant": { "tenant": "Tenant", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Número máximo de argumentos por campo calculado", "max-arguments-per-cf-range": "O número máximo de argumentos por campo calculado não pode ser negativo", "max-arguments-per-cf-required": "O número máximo de argumentos por campo calculado é obrigatório", + "max-related-level-per-argument": "Nível máximo de relação por argumento de 'Entidades relacionadas'", + "max-related-level-per-argument-range": "O número máximo de nível de relação por argumento de 'Entidades relacionadas' não pode ser menor que '1'", + "max-related-level-per-argument-required": "O número máximo de nível de relação por argumento de 'Entidades relacionadas' é obrigatório", + "min-allowed-scheduled-update-interval": "Intervalo mínimo permitido de atualização para argumentos de 'Entidades relacionadas' (segundos)", + "min-allowed-scheduled-update-interval-range": "O número mínimo do intervalo mínimo permitido de atualização não pode ser negativo", + "min-allowed-deduplication-interval": "Intervalo mínimo permitido de deduplicação (segundos)", + "min-allowed-deduplication-interval-range": "O valor do intervalo mínimo permitido de deduplicação não pode ser negativo", + "min-allowed-deduplication-interval-required": "O intervalo mínimo permitido de deduplicação é obrigatório", + "intermediate-aggregation-interval": "Intervalo de agregação intermediária (segundos)", + "intermediate-aggregation-interval-range": "O valor do intervalo de agregação intermediária não pode ser menor que '1'", + "intermediate-aggregation-interval-required": "O intervalo de agregação intermediária é obrigatório", + "reevaluation-check-interval": "Intervalo de verificação de reavaliação (segundos)", + "reevaluation-check-interval-range": "O valor do intervalo de verificação de reavaliação não pode ser menor que '1'", + "reevaluation-check-interval-required": "O intervalo de verificação de reavaliação é obrigatório", + "alarms-reevaluation-interval": "Intervalo de reavaliação de alarmes (segundos)", + "alarms-reevaluation-interval-range": "O valor do intervalo de reavaliação de alarmes não pode ser menor que '1'", + "alarms-reevaluation-interval-required": "O intervalo de reavaliação de alarmes é obrigatório", + "min-allowed-aggregation-interval": "Intervalo mínimo permitido de agregação (segundos)", + "min-allowed-aggregation-interval-range": "O valor do intervalo mínimo permitido de agregação não pode ser negativo", + "min-allowed-aggregation-interval-required": "O intervalo mínimo permitido de agregação é obrigatório", + "min-allowed-scheduled-update-interval-required": "O número mínimo do intervalo mínimo permitido de atualização é obrigatório", "max-state-size": "Tamanho máximo do estado em KB", "max-state-size-range": "O tamanho máximo do estado em KB não pode ser negativo", "max-state-size-required": "O tamanho máximo do estado em KB é obrigatório", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Número máximo de assinaturas por usuário regular", "ws-limit-max-subscriptions-per-public-user": "Número máximo de assinaturas por usuário público", "ws-limit-updates-per-session": "Atualizações WS por sessão", + "relation-search-entity-limit": "Limite de entidades na pesquisa de relações", + "relation-search-entity-limit-hint": "Limita o número de entidades resolvidas no último nível do caminho de relação. Aplica-se aos argumentos de 'Entidades relacionadas' e campos de Propagação.", + "relation-search-entity-limit-required": "Limite de entidades na pesquisa de relações", + "relation-search-entity-limit-range": "O limite de entidades na pesquisa de relações não pode ser menor que '1'", "rate-limits": { "add-limit": "Adicionar limite", "and-also-less-than": "e também menor que", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Intervalo de agrupamento padrão", "edit-intervals-list-hint": "É possível especificar a lista de opções de intervalo disponíveis.", "edit-grouping-intervals-list-hint": "É possível configurar a lista de intervalos de agrupamento e o intervalo de agrupamento padrão.", - "all": "Todos" + "all": "Todos", + "save-current-settings-as-default": "Salvar configurações atuais como janela de tempo padrão", + "hide-option-from-end-users": "Ocultar opção dos usuários finais" }, "tooltip": { "trigger": "Acionador", @@ -6648,6 +7227,7 @@ "export-attributes": "Exportar atributos", "export-credentials": "Exportar credenciais", "export-calculated-fields": "Exportar campos calculados \ne regras de alarme", + "export-alarm-rules": "Exportar regras de alarme", "entity-versions": "Versões da entidade", "versions": "Versões", "created-time": "Hora de criação", @@ -6665,6 +7245,7 @@ "load-attributes": "Carregar atributos", "load-credentials": "Carregar credenciais", "load-calculated-fields": "Carregar campos calculados e regras de alarme", + "load-alarm-rules": "Carregar regras de alarme", "compare-with-current": "Comparar com o atual", "diff-entity-with-version": "Diferença com a versão da entidade '{{versionName}}'", "previous-difference": "Diferença anterior", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Escanear QR Code", "make-phone-call": "Fazer ligação", "get-location": "Obter localização do telefone", - "take-screenshot": "Tirar screenshot" + "take-screenshot": "Tirar screenshot", + "handle-provision-success-function": "Função para tratar sucesso do provisionamento", + "get-location-function": "Função para obter localização", + "process-launch-result-function": "Função para processar resultado de inicialização", + "get-phone-number-function": "Função para obter número de telefone", + "process-image-function": "Função para processar imagem", + "process-qr-code-function": "Função para processar QR code", + "process-location-function": "Função para processar localização", + "handle-empty-result-function": "Função para tratar resultado vazio", + "handle-error-function": "Função para tratar erro", + "handle-non-mobile-fallback-function": "Função de fallback para ambiente não mobile", + "save-to-gallery": "Salvar na 'Galeria de Imagens'", + "provision-type": "Tipo de provisionamento", + "auto": "Automático", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Função de ação personalizada", "custom-pretty-function": "Função de ação personalizada (com template HTML)", @@ -6883,7 +7480,8 @@ "marker": "Marcador", "polygon": "Polígono", "rectangle": "Retângulo", - "circle": "Círculo" + "circle": "Círculo", + "polyline": "Polilinha" }, "place-map-item": "Posicionar item no mapa", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Continuar desenho de polígono", "finish-draw-polygon": "Finalizar desenho de polígono", "start-draw-circle": "Iniciar desenho de círculo", - "finish-draw-circle": "Finalizar desenho de círculo" + "finish-draw-circle": "Finalizar desenho de círculo", + "start-draw-polyline": "Iniciar desenho de polilinha", + "finish-draw-polyline": "Finalizar desenho de polilinha" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Atraso da animação de atualização" }, "chart-axis": { + "limit": "Limite", + "source": "Fonte", + "key-value": "Chave / Valor", + "value-required": "Valor é obrigatório.", + "entity-key-required": "Chave de entidade é obrigatória.", + "key-required": "Chave é obrigatória.", + "scale-limits": "Limites de escala", + "scale-appearance": "Aparência da escala", "scale": "Escala", "scale-min": "mín", "scale-max": "máx", @@ -8005,7 +8613,10 @@ "add-radio-option": "Adicionar opção de rádio", "radio-label-position": "Posição do rótulo", "radio-label-position-before": "Antes", - "radio-label-position-after": "Depois" + "radio-label-position-after": "Depois", + "save-image": "Salvar imagem", + "save-to-gallery": "Armazenar automaticamente imagens capturadas na Galeria de Imagens", + "public-image": "Torna a imagem disponível para qualquer usuário não autenticado" }, "invalid-qr-code-text": "Texto de entrada inválido para o código QR. A entrada deve ser do tipo string", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Viagens", "markers": "Marcadores", "polygons": "Polígonos", - "circles": "Círculos" + "circles": "Círculos", + "polylines": "Polilinhas" }, "data-layer": { "source": "Fonte", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Círculo para '{{entityName}}': clique para concluir e salvar o círculo", "finish-circle-hint": "Círculo: clique para concluir o desenho" }, + "polyline": { + "polyline-key": "Chave da polilinha", + "polyline-key-required": "Chave da polilinha obrigatória", + "no-polylines": "Nenhuma polilinha configurada", + "add-polylines": "Adicionar polilinha", + "polyline-configuration": "Configuração de polilinha", + "remove-polyline": "Remover polilinha", + "edit": "Editar polilinha", + "cut": "Recortar área da polilinha", + "rotate": "Girar polilinha", + "remove-polyline-for": "Remover polilinha para '{{entityName}}'", + "draw-polyline": "Desenhar polilinha", + "polyline-place-first-point-hint-with-entity": "Polilinha para '{{entityName}}': clique para posicionar o primeiro ponto", + "polyline-place-first-point-hint": "Polilinha: clique para posicionar o primeiro ponto", + "finish-polyline-hint-with-entity": "Polilinha para '{{entityName}}': clique para concluir o desenho", + "finish-polyline-hint": "Polilinha: clique para concluir o desenho", + "polyline-place-first-point-cut-hint": "Clique para posicionar o primeiro ponto", + "finish-polyline-cut-hint": "Clique no primeiro marcador para concluir e salvar" + }, "select-entity": "Selecionar entidade", "select-entity-hint": "Dica: após a seleção, clique no mapa para definir a posição" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Exibir espaço vazio em vez da ação de botão de célula oculta", "dont-reserve-space-hidden-action": "Não reservar espaço para botões de ação ocultos", "display-timestamp": "Timestamp", + "timestamp-column-name": "Timestamp", "display-pagination": "Exibir paginação", "default-page-size": "Tamanho padrão da página", "page-step-settings": "Configurações de passo de página", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Pelo menos uma coluna de alarme deve ser especificada", "table-tabs": "Abas da tabela", "show-cell-actions-menu-mobile": "Exibir menu suspenso de ações de célula no modo móvel", - "disable-sorting": "Desativar classificação" + "disable-sorting": "Desativar classificação", + "sort-by": "Classificar abas por", + "sort-timestamp-option": "Hora de criação" }, "latest-chart": { "total": "Total", @@ -9491,11 +10125,28 @@ "content": "

    Ao criar dashboards para usuários finais, um usuário cliente pode ver apenas seus próprios dispositivos, e os dados de outro cliente serão ocultados.

    Siga a documentação sobre como fazer isso:

    " } } + }, + "api-usage": { + "api-usage": "Uso de API", + "label": "Rótulo", + "state-name": "Nome do estado", + "status": "Status", + "status-required": "O status é obrigatório.", + "limit": "Limite máximo", + "limit-required": "O limite máximo é obrigatório.", + "current-number": "Número atual", + "current-number-required": "O número atual é obrigatório.", + "add-key": "Adicionar chave", + "no-key": "Sem chave", + "delete-key": "Excluir chave", + "target-dashboard-state": "Estado do Dashboard de destino", + "go-to-main-state": "Ir para a visualização padrão" } }, "icon": { "icon": "Ícone", "icons": "Ícones", + "custom": "Personalizado", "select-icon": "Selecionar ícone", "material-icons": "Material icons", "show-all": "Mostrar todos os ícones", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "de" }, "language": { + "auto": "Automático", "language": "Idioma" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json index 1054b812a1..f1b712e205 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json +++ b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json @@ -1236,7 +1236,6 @@ "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?", diff --git a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json index 41dea2e169..7b3b1fb197 100644 --- a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json +++ b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json @@ -1708,7 +1708,6 @@ "create-password": "Ustvari geslo", "passwords-mismatch-error": "Vnesena gesla morajo biti enaka!", "password-again": "Ponovi geslo", - "sign-in": "Prosimo, prijavite se", "username": "Uporabniško ime (e-pošta)", "remember-me": "Zapomni si me", "forgot-password": "Ste pozabili geslo?", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index 66bc099e84..0027c7d8ed 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -78,6 +78,7 @@ "show-more": "Daha fazla göster", "dont-show-again": "Bir daha gösterme", "see-documentation": "Dokümantasyonu görüntüle", + "see-debug-events": "Hata Ayıklama Olaylarını Görüntüle", "clear": "Temizle", "upload": "Yükle", "delete-anyway": "Yine de sil", @@ -485,6 +486,7 @@ "2fa": { "2fa": "İki faktörlü kimlik doğrulama", "available-providers": "Mevcut sağlayıcılar", + "available-providers-required": "En az bir 2FA sağlayıcısı yapılandırılmalıdır.", "issuer-name": "Yayımlayıcı adı", "issuer-name-required": "Yayımlayıcı adı gereklidir.", "max-verification-failures-before-user-lockout": "Kullanıcı kilitlenmeden önceki maksimum doğrulama başarısızlığı", @@ -513,7 +515,9 @@ "verification-message-template-required": "Doğrulama mesajı şablonu gereklidir.", "within-time": "Belirli süre içinde (sn)", "within-time-pattern": "Süre pozitif bir tamsayı olmalıdır.", - "within-time-required": "Süre gereklidir." + "within-time-required": "Süre gereklidir.", + "force-2fa": "İki Faktörlü Kimlik Doğrulamayı Zorunlu Kıl", + "enforce-for": "Şunun İçin Zorunlu Kıl" }, "jwt": { "security-settings": "JWT güvenlik ayarları", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Alarm", + "alarm-list": "Alarm Listesi", "alarms": "Alarmlar", "all-alarms": "Tüm alarmlar", "select-alarm": "Alarm seçin", @@ -649,7 +654,16 @@ "alarm-type": "Alarm türü", "enter-alarm-type": "Alarm türünü girin", "no-alarm-types-matching": "'{{entitySubtype}}' ile eşleşen alarm türü bulunamadı.", - "alarm-type-list-empty": "Seçilmiş alarm türü yok." + "alarm-type-list-empty": "Seçilmiş alarm türü yok.", + "system-comments": { + "acked-by-user": "Alarm, {{userName}} adlı kullanıcı tarafından onaylandı", + "cleared-by-user": "Alarm, {{userName}} adlı kullanıcı tarafından temizlendi", + "assigned-to-user": "Alarm, {{userName}} adlı kullanıcı tarafından {{assigneeName}} adlı kullanıcıya atandı", + "unassigned-to-user": "Alarm, {{userName}} adlı kullanıcı tarafından atamadan çıkarıldı", + "unassigned-from-deleted-user": "{{userName}} adlı kullanıcı silindiği için alarm atamadan çıkarıldı", + "comment-deleted": "{{userName}} adlı kullanıcı yorumunu sildi", + "severity-changed": "Alarm önceliği {{oldSeverity}} değerinden {{newSeverity}} değerine güncellendi" + } }, "alarm-activity": { "add": "Yorum ekle...", @@ -754,6 +768,7 @@ "name-max-length": "Ad 256 karakterden kısa olmalıdır", "label-max-length": "Etiket 256 karakterden kısa olmalıdır", "description": "Açıklama", + "description-required": "Açıklama gereklidir.", "type": "Tür", "type-required": "Tür gereklidir.", "details": "Detaylar", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Aylık oluşturulan alarmlar", "data-points": "Veri noktaları", "data-points-storage-days": "Veri noktası saklama süresi (gün)", + "data-points-storage-days-hourly-activity": "Veri noktaları depolama günleri saatlik etkinlik", + "data-points-storage-days-daily-activity": "Veri noktaları depolama günleri günlük etkinlik", + "data-points-storage-days-monthly-activity": "Veri noktaları depolama günleri aylık etkinlik", "device-api": "Cihaz API'si", "email": "E-posta", "email-messages": "E-posta mesajları", @@ -900,6 +918,7 @@ "rule-node": "Kural Düğümü", "sms": "SMS", "sms-messages": "SMS mesajları", + "sms-messages-hourly-activity": "SMS mesajları saatlik etkinlik", "sms-messages-daily-activity": "Günlük SMS mesajları", "sms-messages-monthly-activity": "Aylık SMS mesajları", "successful": "${entityName} Başarılı", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Telemetri kalıcılığı saatlik etkinliği", "telemetry-persistence-monthly-activity": "Telemetri kalıcılığı aylık etkinliği", "transport": "İletim", + "transport-msg-hourly-activity": "Taşıma mesajları saatlik etkinlik", + "transport-msg-daily-activity": "Taşıma mesajları günlük etkinlik", + "transport-msg-monthly-activity": "Taşıma mesajları aylık etkinlik", "transport-daily-activity": "İletim günlük etkinliği", "transport-data-points": "İletim veri noktaları", + "transport-data-points-hourly-activity": "Taşıma veri noktaları saatlik etkinlik", + "transport-data-points-daily-activity": "Taşıma veri noktaları günlük etkinlik", + "transport-data-points-monthly-activity": "Taşıma veri noktaları aylık etkinlik", "view-details": "Detayları görüntüle", "view-statistics": "İstatistikleri görüntüle", - "transport-messages": "Taşıma mesajları" + "transport-messages": "Taşıma mesajları", + "transport-messages-hourly-activity": "Taşıma mesajları saatlik etkinlik", + "transport-data-point-hourly-activity": "Taşıma veri noktası saatlik etkinlik", + "javascript-function-executions": "JavaScript işlev yürütmeleri", + "javascript-function-executions-hourly-activity": "JavaScript işlev yürütmeleri saatlik etkinlik", + "javascript-function-executions-daily-activity": "JavaScript işlev yürütmeleri günlük etkinlik", + "javascript-function-executions-monthly-activity": "JavaScript işlev yürütmeleri aylık etkinlik", + "tbel-function-executions": "TBEL işlev yürütmeleri", + "tbel-function-executions-hourly-activity": "TBEL işlev yürütmeleri saatlik etkinlik", + "tbel-function-executions-daily-activity": "TBEL işlev yürütmeleri günlük etkinlik", + "tbel-function-executions-monthly-activity": "TBEL işlev yürütmeleri aylık etkinlik", + "created-reports": "Oluşturulan raporlar", + "created-reports-hourly-activity": "Oluşturulan raporlar saatlik etkinlik", + "created-reports-daily-activity": "Oluşturulan raporlar günlük etkinlik", + "created-reports-monthly-activity": "Oluşturulan raporlar aylık etkinlik", + "emails": "Emails", + "emails-hourly-activity": "Emails saatlik etkinlik", + "emails-daily-activity": "Emails günlük etkinlik", + "emails-monthly-activity": "Emails aylık etkinlik", + "status": { + "enabled": "Etkin", + "disabled": "Devre Dışı", + "warning": "Uyarı" + } }, "api-limit": { "cassandra-write-queries-core": "Rest API Cassandra yazma sorguları", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Edge yukarı bağlantı mesajları", "edge-uplink-messages-per-edge": "Edge başına yukarı bağlantı mesajları" }, + "api-key": { + "api-key": "API anahtarı", + "api-keys": "API anahtarları", + "delete-api-key-title": "“{{name}}” API anahtarını silmek istediğinizden emin misiniz?", + "delete-api-key-text": "Dikkat: Onaydan sonra anahtar kurtarılamaz.", + "delete-api-keys-title": "{ count, plural, =1 {1 API anahtarı} other {# API anahtarı} } silmek istediğinizden emin misiniz?", + "delete-api-keys-text": "Dikkat: Onaydan sonra seçilen tüm anahtarlar kurtarılamaz.", + "expiration-date": "Son Kullanma Tarihi", + "date": "tarih", + "description": "Açıklama", + "disable": "Devre Dışı Bırak", + "edit-description": "Açıklamayı Düzenle", + "enable": "API Anahtarını Etkinleştir ", + "expiration-time": "Son Kullanma Saati", + "expiration-time-never": "Asla", + "expiration-time-custom": "Özel", + "generate": "Oluştur", + "generate-title": "API Anahtarı Oluştur", + "generate-text": "Not: API anahtarı, oluşturulduğu kullanıcının izinlerini devralır.", + "generated-api-key-title": "API anahtarı oluşturuldu. Bağlantıyı kontrol edelim!", + "generated-api-key-copy": "API anahtarınızı şimdi kopyalayıp kaydettiğinizden emin olun; çünkü bir daha göremeyeceksiniz.", + "generated-api-key-command": "Bağlantıyı kontrol etmek için aşağıdaki talimatları kullanın. Sonuç olarak, geçerli kullanıcı bilgilerini almanız gerekir:", + "generated-api-key-insecure-url": "Güvenli olmayan bir HTTP bağlantısı üzerinden komut çalıştırmak, API anahtarınızı şifrelenmemiş şekilde gönderir ve ele geçirilmeye karşı savunmasız hâle getirir.", + "list": "{ count, plural, =1 {Bir API anahtarı} other {# API anahtarı listesi} }", + "manage": "Yönet", + "manage-api-keys": "API Anahtarlarını Yönet", + "no-found": "API anahtarı bulunamadı", + "selected-api-keys": "{ count, plural, =1 {1 API anahtarı} other {# API anahtarı} } seçildi", + "search": "API anahtarlarını ara", + "status": "Durum", + "status-active": "Etkin", + "status-inactive": "Etkin Değil", + "status-expired": "Süresi Doldu" + }, "audit-log": { "audit": "Denetim", "audit-logs": "Denetim günlükleri", @@ -991,7 +1073,11 @@ "type-provision-failure": "Cihaz tedarik işlemi başarısız oldu", "type-timeseries-updated": "Telemetri güncellendi", "type-timeseries-deleted": "Telemetri silindi", - "type-sms-sent": "SMS gönderildi" + "type-sms-sent": "SMS gönderildi", + "any-type": "Herhangi Bir Tür", + "audit-log-filter-title": "Denetim Günlüğü Filtresi", + "filter-title": "Filtre", + "filter-types": "Denetim günlüğü türleri" }, "debug-settings": { "label": "Hata Ayıklama Yapılandırması", @@ -1009,15 +1095,29 @@ "expression": "İfade", "no-found": "Hesaplanmış alan bulunamadı", "list": "{ count, plural, =1 {Bir hesaplanmış alan} other {# hesaplanmış alan listesi} }", + "add": "Hesaplanmış alan ekle", "selected-fields": "{ count, plural, =1 {1 hesaplanmış alan} other {# hesaplanmış alan} } seçildi", "type": { "simple": "Basit", - "script": "Betik" + "simple-hint": "Girdi bağımsız değişkenlerine dayalı basit aritmetik hesaplama.", + "script": "Betik", + "script-hint": "TBEL betiği kullanılarak tanımlı bağımsız değişkenler üzerinde hesaplama.", + "geofencing": "Coğrafi Çitleme", + "geofencing-hint": "Varlık GPS konumu ve geçişlerinin, yapılandırılmış coğrafi çitleme bölge gruplarına göre değerlendirilmesi.", + "propagation": "Yayılım", + "propagation-hint": "İlişki yönü ve türüne bağlı olarak verilerin üst veya alt varlıklara yayılması.", + "related-entities-aggregation": "İlgili varlıkların toplulaştırılması", + "related-entities-aggregation-hint": "İlgili varlıklardan en son verilerin toplulaştırılması.", + "time-series-data-aggregation": "Zaman serisi verilerinin toplulaştırılması", + "time-series-data-aggregation-hint": "Geçerli bir varlıktan geçmiş verilerin toplulaştırılması." }, + "preview": "Önizleme", "arguments": "Argümanlar", "decimals-by-default": "Varsayılan ondalık", "debugging": "Hesaplanmış alan hata ayıklama", + "calculated-field-details": "Hesaplanmış alan ayrıntıları", "argument-name": "Argüman adı", + "name": "Ad", "datasource": "Veri kaynağı", "add-argument": "Argüman ekle", "test-script-function": "Komut dosyası işlevini test et", @@ -1029,6 +1129,8 @@ "argument-asset": "Varlık", "argument-customer": "Müşteri", "argument-tenant": "Geçerli kiracı", + "argument-owner": "Geçerli sahip", + "argument-relation-query": "İlgili varlıklar", "argument-type": "Argüman türü", "attribute": "Öznitelik", "copy-argument-name": "Argüman adını kopyala", @@ -1042,12 +1144,14 @@ "shared-attributes": "Paylaşılan öznitelikler", "attribute-key": "Öznitelik anahtarı", "default-value": "Varsayılan değer", + "default-value-required": "Varsayılan değer gereklidir.", "limit": "Maksimum değer", "time-window": "Zaman aralığı", "customer-name": "Müşteri adı", "asset-name": "Varlık adı", "timeseries": "Zaman serisi", "output": "Çıktı", + "output-hint": "Çıktının nasıl işlendiğini tanımlar.", "create": "Yeni hesaplanmış alan oluştur", "file": "Hesaplanmış alan dosyası", "invalid-file-error": "Geçersiz dosya biçimi. Lütfen dosyanın geçerli bir JSON dosyası olduğundan emin olun.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Dikkat, onaydan sonra seçilen tüm hesaplanmış alanlar ve ilgili tüm veriler geri alınamaz hale gelecektir.", "test-with-this-message": "Bu mesaj ile test et", "use-latest-timestamp": "En son zaman damgasını kullan", + "entity-coordinates": "Varlık koordinatları", + "latitude-time-series-key": "Enlem zaman serisi anahtarı", + "latitude-time-series-key-required": "Enlem zaman serisi anahtarı gereklidir.", + "longitude-time-series-key": "Boylam zaman serisi anahtarı", + "longitude-time-series-key-required": "Boylam zaman serisi anahtarı gereklidir.", + "geofencing-zone-groups": "Coğrafi çitleme bölge grupları", + "geofencing-zone-groups-settings": "Coğrafi çitleme bölge grubu ayarları", + "target-zone": "Hedef bölge", + "perimeter-key": "Çevre anahtarı", + "report-strategy": "Raporlama stratejisi", + "no-zone-configured": "En az bir bölge gereklidir.", + "no-zone-configured-required": "En az bir bölge grubu yapılandırılmalıdır.", + "add-zone-group": "Bölge Grubu Ekle", + "report-transition-event-only": "Yalnızca geçiş olayları", + "report-presence-status-only": "Yalnızca bulunma durumu", + "report-transition-event-and-presence": "Bulunma durumu ve geçiş olayları", + "perimeter-attribute-key": "Çevre öznitelik anahtarı", + "perimeter-attribute-key-required": "Çevre öznitelik anahtarı gereklidir.", + "perimeter-attribute-key-pattern": "Çevre öznitelik anahtarı geçersiz.", + "entity-zone-relationship": "Varlıktan Bölgelere Giden Yol", + "direction": "İlişki yönü", + "direction-from": "Varlıktan bölgeye", + "direction-to": "Bölgeden varlığa", + "relation-type": "İlişki türü", + "create-relation-with-matched-zones": "Kaynak varlık için eşleşen bölgelerle ilişkiler oluştur", + "relation-level": "İlişki düzeyi", + "fetch-last-available-level": "Yalnızca son kullanılabilir düzeyi getir", + "zone-group-refresh-interval": "Bölge grupları yenileme aralığı", + "copy-zone-group-name": "Bölge grubu adını kopyala", + "open-details-page": "Varlık ayrıntıları sayfasını aç", + "level": "Düzey", + "direction-level": "Yön", + "direction-up": "Yukarı", + "direction-up-parent": "Üste (üst öğeye)", + "direction-down": "Aşağı", + "direction-down-child": "Alta (alt öğeye)", + "add-level": "Düzey Ekle", + "delete-level": "Düzeyi Sil", + "no-level": "Yapılandırılmış düzey yok", + "levels-required": "En az bir düzey yapılandırılmalıdır.", + "max-allowed-levels-error": "İlişki düzeyi izin verilen azami değeri aşıyor.", + "propagation-path-related-entities": "İlgili varlıklara yayılım yolu", + "propagate-type": { + "arguments-only": "Yalnızca bağımsız değişkenler", + "expression-result": "Hesaplama sonucu" + }, + "script": "Betik", + "data-propagate": "Yayılacak veri", + "output-key": "Çıktı anahtarı", + "copy-output-key": "Çıktı anahtarını kopyala", + "aggregation-path-related-entities": "İlgili varlıklara toplulaştırma yolu", + "deduplication-interval": "Yinelenenleri ayıklama aralığı", + "deduplication-interval-min": "Yinelenenleri ayıklama aralığı en az {{ sec }} saniye olmalıdır.", + "deduplication-interval-hint": "Telemetri toplulaştırmaları arasındaki asgari süre.", + "deduplication-interval-required": "Yinelenenleri ayıklama aralığı gereklidir.", + "calculated-field-filter-title": "Hesaplanmış alan filtresi", + "filter-title": "Filtre", + "calculated-field-types": "Hesaplanmış alan türleri", + "events": "Olaylar", + "any-type": "Herhangi Bir Tür", + "metrics": { + "metrics": "Metrikler", + "metrics-empty": "En az bir metrik yapılandırılmalıdır.", + "metric-name": "Metrik adı", + "metric-name-required": "Metrik adı gereklidir.", + "metric-name-pattern": "Metrik adı geçersiz.", + "metric-name-duplicate": "Bu isimde bir metrik zaten mevcut.", + "metric-name-max-length": "Metrik adı 256 karakterden kısa olmalıdır.", + "metric-name-forbidden": "Metrik adı rezerve edilmiştir ve kullanılamaz.", + "copy-metric-name": "Metrik adını kopyala", + "argument-name": "Bağımsız değişken adı", + "aggregation": "Toplama", + "aggregation-type": { + "avg": "Ortalama", + "min": "Minimum", + "max": "Maksimum", + "sum": "Toplam", + "count": "Sayım", + "count-unique": "Benzersiz sayım" + }, + "filtered": "Filtrelenmiş", + "value-source": "Değer kaynağı", + "value-source-hint": "Toplama için değerin nasıl elde edileceğini tanımlar.", + "value-source-type": { + "key": "Anahtar", + "function": "Fonksiyon" + }, + "no-metrics-configured": "En az bir metrik gereklidir.", + "add-metric": "Metrik Ekle", + "max-metrics": "Maksimum metrik sayısına ulaşıldı.", + "metric-settings": "Metrik ayarları", + "filter": "Filtre", + "filter-hint": "Toplama sırasında varlıkların filtrelenmesini sağlar. Filtre fonksiyonu bir boolean değer döndürmeli ve tüm yapılandırılmış bağımsız değişkenleri kullanabilir." + }, + "output-strategy": { + "strategy": "Strateji", + "process-right-away": "Hemen işleme", + "process-rule-chains": "Kural Zincirleri ile işleme", + "save-time-series": "Zaman serilerine kaydet", + "save-database": "Veritabanına kaydet", + "save-latest-values": "Son değerlere kaydet", + "send-web-sockets": "Web Soketlerine gönder", + "save-calculated-fields": "Hesaplanmış alanlara gönder", + "update-attribute-only-on-value-change": "Yalnızca değer değiştiğinde özniteliği güncelle", + "send-attributes-updated-notification": "Öznitelik güncelleme bildirimini gönder", + "ttl": "Özel TTL", + "ttl-required": "TTL gereklidir", + "ttl-min": "Sadece 0 minimum TTL izni verilir", + "processing-parameters": "İşleme parametreleri", + "hint": { + "strategy": "Sonucun hemen işlenip işlenmeyeceğini veya ek işlem için kural zincirine gönderilip gönderilmeyeceğini kontrol eder.", + "processing-options": "İşleme seçenekleri", + "update-attribute-only-on-value-change": "Değer değişse de değişmese de her gelen mesajda öznitelik güncellenir. Bu, API kullanımını artırır ve performansı düşürür.", + "update-attribute-only-on-value-change-enabled": "Öznitelik yalnızca değer değiştiğinde güncellenir. Değer değişmediyse, zaman damgaları güncellenmez ve bildirim gönderilmez.", + "send-attributes-updated-notification": "Öznitelikler güncellendi olayı, varsayılan kural zincirine gönderilir.", + "save-time-series": "Zaman serisi verisi, veritabanındaki ts_kv tablosuna kaydedilir.", + "save-database": "Öznitelik verisi veritabanına kaydedilir.", + "save-latest-values": "Yeni değer daha güncelse, zaman serisi verisi veritabanındaki ts_kv_latest tablosuna güncellenir.", + "send-web-sockets-attribute": "WebSocket aboneliklerine, öznitelik verisi güncellemeleri hakkında bildirim gönderir.", + "send-web-sockets-time-series": "WebSocket aboneliklerine, zaman serisi verisi güncellemeleri hakkında bildirim gönderir.", + "save-calculated-fields-attribute": "Hesaplanmış alanlara, öznitelik verisi güncellemeleri hakkında bildirim gönderir.", + "save-calculated-fields-time-series": "Hesaplanmış alanlara, zaman serisi verisi güncellemeleri hakkında bildirim gönderir.", + "ttl": "Zaman serisi verisinin saklama süresini tanımlar. Devre dışı bırakıldığında, Kiracı Profili TTL'si kullanılır." + } + }, + "aggregate-interval-type": "Toplama aralığı türü", + "aggregate-interval-value": "Toplama aralığı değeri", + "aggregate-interval-value-required": "Toplama aralığı değeri gereklidir.", + "aggregate-interval-value-min": "Toplama aralığı değeri en az { sec, plural, =0 {0 saniye} =1 {1 saniye} other {# saniye} } olmalıdır.", + "aggregate-interval-value-step-multiple-of": "Toplama aralığı değeri, 1 günün katları veya böleni olmalıdır.", + "aggregate-period": { + "hour": "Saat", + "day": "Gün", + "week": "Hafta (Pzt - Pazar)", + "week-sun-sat": "Hafta (Pazar - Cumartesi)", + "month": "Ay", + "quarter": "Çeyrek", + "year": "Yıl", + "custom": "Özel" + }, + "aggregate-period-hint-offset": "Toplama aralığınız: {{ interval }} olacak.", + "aggregate-period-hint-offset-and-so-on": "Toplama aralığınız: {{ interval }} olacak ve devam edecek.", + "entity-aggregation": { + "argument-hint": "Veri mevcut varlıktan alınacaktır.", + "argument-title-hint": "Toplama için kullanılan giriş bağımsız değişkenlerini tanımlar.", + "argument-setting-hint": "Son telemetri, bu hesaplanmış alan için mevcut tek bağımsız değişken türüdür.", + "aggregation-interval": "Toplama aralığı", + "aggregation-interval-hint": "Toplamanın ne sıklıkla yapılacağını tanımlar. Örnek: her 1 saat, 00:00, 01:00, 02:00, vb. saatlerde veri toplar. Toplama sonuçları, toplama aralığının başlangıcına karşılık gelen zaman damgası ile saklanır.", + "apply-offset": "Toplama aralığına offset uygula", + "apply-offset-hint": "Her toplama döneminin başlangıcını ne kadar kaydıracağını tanımlar (örneğin, +10 dakika - 00:10, 01:10).", + "offset-value": "Offset değeri", + "offset-value-required": "Offset değeri gereklidir.", + "offset-value-min": "Offset değeri pozitif bir tam sayı olmalıdır.", + "offset-value-max": "Offset değeri, toplama aralığı değerinden küçük olmalıdır.", + "wait-delay": "Gecikmeli telemetri için bekleme süresi uygula", + "wait-delay-hint": "Aralık bittiğinde gecikmeli telemetriyi beklemek için ne kadar süre bekleyeceğinizi tanımlar. Eğer böyle bir telemetri gelirse, o aralık için sonuç yeniden hesaplanacaktır.", + "duration": "Süre", + "duration-required": "Süre gereklidir.", + "duration-min": "Süre en az 1 dakika olmalıdır.", + "duration-hint": "Aralık bittiğinde gecikmeli veriyi beklemek için ne kadar süre bekleyeceğinizi tanımlar.", + "produce-intermediate-result": "Ara sonuç üret", + "produce-intermediate-result-hint": "Mevcut aralıkta metrikler hesaplanarak ara sonuç üretilir. Güncellemeler, her {{ time }}'de bir kez yapılacaktır." + }, "hint": { "arguments-simple-with-rolling": "Basit türdeki hesaplanmış alan, zaman serisi yuvarlama türüyle anahtarlar içermemelidir.", + "arguments-propagate-arguments-with-rolling": "'Zaman serisi yuvarlama' türü, 'Yalnızca bağımsız değişkenler' yayılımıyla uyumsuzdur.", + "arguments-propagate-argument-entity-type": "Varlık türü, 'Yalnızca bağımsız değişkenler' yayılımıyla uyumsuzdur.", + "arguments-propagate-argument-must-current-entity": "En az bir bağımsız değişken, 'Mevcut varlık' kaynak varlık türü ile yapılandırılmalıdır.", "arguments-empty": "En az bir bağımsız değişken belirtilmelidir.", "expression-required": "İfade gereklidir.", "expression-invalid": "İfade geçersiz.", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Bu isimde bir bağımsız değişken zaten mevcut.", "argument-name-max-length": "Bağımsız değişken adı 256 karakterden kısa olmalıdır.", "argument-name-forbidden": "Bağımsız değişken adı rezerve edilmiştir ve kullanılamaz.", + "output-key-required": "Çıktı anahtarı gereklidir.", + "output-key-pattern": "Çıktı anahtarı geçersiz.", + "output-key-duplicate": "Bu isimde bir anahtar zaten mevcut.", + "output-key-max-length": "Çıktı anahtarı 256 karakterden kısa olmalıdır.", + "output-key-forbidden": "Çıktı anahtarı rezerve edilmiştir ve kullanılamaz.", + "entity-type-required": "Varlık türü gereklidir.", + "name-required": "Ad gereklidir.", + "name-pattern": "Ad geçersiz.", + "name-duplicate": "Bu isimde bir ad zaten mevcut.", + "name-max-length": "Ad 256 karakterden kısa olmalıdır.", + "name-forbidden": "Ad rezerve edilmiştir ve kullanılamaz.", "argument-type-required": "Bağımsız değişken türü gereklidir.", "max-args": "Maksimum bağımsız değişken sayısına ulaşıldı.", "decimals-range": "Ondalıklar varsayılan olarak 0 ile 15 arasında bir sayı olmalıdır.", "expression": "Varsayılan ifade, bir sıcaklık değerini Fahrenheit'ten Celsius'a nasıl dönüştüreceğini gösterir.", "arguments-entity-not-found": "Bağımsız değişken hedef varlığı bulunamadı.", - "use-latest-timestamp": "Etkinleştirilirse, hesaplanan değer, bağımsız değişkenlerin telemetrisindeki en son zaman damgası kullanılarak saklanacaktır, sunucu zamanı yerine." + "use-latest-timestamp": "Etkinleştirilirse, hesaplanan değer, bağımsız değişkenlerin telemetrisindeki en son zaman damgası kullanılarak saklanacaktır, sunucu zamanı yerine.", + "entity-coordinates": "Varlık GPS koordinatlarını (enlem ve boylam) sağlayan zaman serisi anahtarlarını belirtin.", + "geofencing-zone-groups": "Kontrol edilecek bir veya daha fazla coğrafi çitleme bölgesi grubu tanımlayın (örneğin 'allowedZones', 'restrictedZones'). Her grup, hesaplanmış alan çıktısı telemetri anahtarları için önek olarak kullanılacak benzersiz bir ada sahip olmalıdır.", + "perimeter-attribute-key": "Coğrafi çitleme bölgesi çevre tanımını içeren öznitelik anahtarını ayarlayın. Çevre her zaman bölge varlıklarının sunucu tarafı özniteliklerinden alınır.", + "report-strategy": "Bulunma durumu, varlığın şu anda bölge grubunun İÇİNDE mi yoksa DIŞINDA mı olduğunu rapor eder. Geçiş olayları, varlığın bölge grubuna GİRDİĞİ veya ÇIKTIĞI zamanı rapor eder.", + "create-relation-with-matched-zones": "Varlık ile şu anda içinde bulunduğu bölgeler arasında ilişkiler otomatik olarak oluşturulup korunur. Varlık bir bölgeden ayrıldığında ilişkiler kaldırılır ve yeni bir bölgeye girdiğinde oluşturulur.", + "relation-type-required": "İlişki türü gereklidir.", + "relation-level-required": "İlişki düzeyi gereklidir.", + "relation-level-min": "Minimum ilişki düzeyi değeri 1'dir.", + "relation-level-max": "Maksimum ilişki düzeyi değeri {{max}}'dir.", + "geofencing-empty": "En az bir bölge grubu yapılandırılmalıdır.", + "geofencing-entity-not-found": "Coğrafi çitleme hedef varlığı bulunamadı.", + "max-geofencing-zone": "Maksimum coğrafi çitleme bölgesi sayısına ulaşıldı.", + "zone-group-refresh-interval": "İlgili varlıklar aracılığıyla yapılandırılan bölge gruplarının ne sıklıkla yenileneceğini tanımlar.", + "zone-group-refresh-interval-required": "Bölge gruplarının yenileme aralığı gereklidir.", + "zone-group-refresh-interval-min": "Bölge grubu yenileme aralığı en az {{ min }} saniye olmalıdır.", + "propagation-path-related-entities": "Seçilen yön ve ilişki türüne göre, ilgili bir varlığa doğrudan, tek seviyeli bir yolu tanımlar. Yalnızca cihaz, varlık, müşteri ve kiracı varlıkları arasındaki ilişkiler desteklenir. İlişki yolu ile çözülmüş maksimum varlık sayısı {{ max }}'dir.", + "data-propagate": "Aşağıda yapılandırılan bağımsız değişkenlerden hangi verilerin yayılacağını tanımlar. 'Yalnızca bağımsız değişkenler', alınan verileri doğrudan kullanırken, 'İfade sonucu' bu verilerden yeni bir değer hesaplar.", + "aggregation-path-related-entities": "Yön ve ilişki türüne dayalı olarak, üst veya alt varlıklarla doğrudan ilişkiler aracılığıyla tek seviyeli bir toplama yolunu tanımlar. Yalnızca cihaz, varlık, müşteri ve kiracı varlıkları arasındaki ilişkiler desteklenir. İlişki yolu ile çözülmüş maksimum varlık sayısı {{ max }}'dir.", + "arguments-aggregation": "Filtreleme ve toplama için kullanılan giriş bağımsız değişkenlerini tanımlar.", + "setting-arguments-aggregation": "Veriler, toplama yolunda yapılandırılan ilgili varlıklardan alınacaktır.", + "metrics": "Yapılandırılmış bağımsız değişkenlere dayalı olarak toplanan metrikleri tanımlar.", + "entity-aggregation-metrics": "Belirtilen zaman aralıklarında yapılandırılmış bağımsız değişkenlere dayalı olarak toplanan metrikleri tanımlar.", + "import-invalid-calculated-field-type": "Hesaplanmış alan ithal edilemedi: Geçersiz hesaplanmış alan yapısı.", + "simple-expression-title": "Hesaplanan değerin nasıl hesaplanacağını tanımlayan aritmetik ifade.", + "script-title": "Hesaplama mantığını ve çıktı değerlerini tanımlayan TBEL betiği.", + "simple-arguments": "Hesaplanan değerin nasıl hesaplanacağını tanımlayan aritmetik ifade.", + "script-arguments": "Betiğe mevcut olan giriş bağımsız değişkenlerini tanımlar." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Alarm kuralları", + "alarm-rule": "Alarm kuralı", + "alarm-rules": "Alarm kuralları", + "alarm-rules-old": "Eski", + "alarm-rules-actual": "Geçerli", + "severities": "Öncelikler", + "cleared": "Koşul temizlendi", + "delete-title": "“{{title}}” alarm kuralını silmek istediğinizden emin misiniz?", + "delete-text": "Dikkat, onaydan sonra alarm kuralı ve tüm ilişkili veriler geri getirilemez olacaktır.", + "delete-multiple-title": "{ count, plural, =1 {1 alarm kuralı} other {# alarm kuralı} } silmek istediğinizden emin misiniz?", + "delete-multiple-text": "Dikkat, onaydan sonra tüm seçilen alarm kuralları silinecek ve tüm ilişkili veriler geri getirilemez olacaktır.", + "create": "Yeni alarm kuralı oluştur", + "add": "Alarm kuralı ekle", + "copy": "Alarm kuralı yapılandırmasını kopyala", + "details": "Alarm kuralı ayrıntıları", + "no-found": "Alarm kuralı bulunamadı", + "list": "{ count, plural, =1 {Bir alarm kuralı} other {# alarm kuralı listesi} }", + "selected-fields": "{ count, plural, =1 {1 alarm kuralı} other {# alarm kuralı} } seçildi", + "import": "Alarm kuralı içe aktar", + "file": "Alarm kuralı dosyası", + "export": "Alarm kuralı dışa aktar", + "export-failed-error": "Alarm kuralı dışa aktarılamadı: {{error}}", + "entity-type": "Varlık türü", + "entity-type-required": "Varlık türü gereklidir.", + "alarm-type": "Alarm türü", + "alarm-type-hint": "Alarm kaynağı (Cihaz, Varlık, vb.) kapsamında çakışmaları önlemek için benzersiz tanımlayıcı (örneğin, HighTempAlarm).", + "alarm-type-required": "Alarm türü gereklidir.", + "alarm-type-pattern": "Alarm türü geçersiz.", + "alarm-type-max-length": "Alarm türü 256 karakterden kısa olmalıdır.", + "clear-alarm": "Alarmı temizle", + "value-argument": "Bağımsız değişken", + "value-argument-required": "Bağımsız değişken gereklidir.", + "static-settings": "Statik ayarlar", + "configuration": "Yapılandırma", + "static-schedule": "Statik", + "dynamic-schedule": "Dinamik", + "operation-and": "VE", + "operation-or": "VEYA", + "condition-during": "{{during}} süresince", + "condition-during-dynamic": "\"{{ attribute }}\" süresince", + "condition-repeat-times": "{ count, plural, =1 {1 kez} other {# kez} } tekrarlar", + "condition-repeat-times-dynamic": "\"{{ attribute }}\" kadar tekrarlar", + "filter-preview": "Filtre önizlemesi", + "condition-settings": "Koşul ayarları", + "static": "Statik", + "dynamic": "Dinamik", + "argument-filters": "Bağımsız değişken filtreleri", + "argument-name": "Bağımsız değişken adı", + "value-type": "Değer türü", + "general": "Genel", + "filters": "Filtreler", + "date-time-hint": "Bağımsız değişken epoch milisaniye cinsinden olmalıdır. Örnek: 1698839340000, 2023-11-01 12:49:00 UTC'ye eşittir.", + "operation": "İşlem", + "value-source": "Değer kaynağı", + "value": "Değer", + "ignore-case": "Büyük/küçük harf duyarlılığını göz ardı et", + "condition": "Koşul", + "script": "Betik", + "add-filter": "Bağımsız değişken filtresi ekle", + "edit-filter": "Bağımsız değişken filtresi", + "remove-filter": "Bağımsız değişken filtresini kaldır", + "no-filter": "En az bir filtre gereklidir.", + "conditions": { + "simple": "Basit", + "duration": "Süre", + "repeating": "Tekrarlayan" + }, + "schedule-title": "Zamanlama", + "edit-schedule": "Alarm zamanlamasını düzenle", + "schedule-type": "Zamanlayıcı türü", + "schedule-type-required": "Zamanlayıcı türü gereklidir.", + "schedule": { + "any-time": "Her zaman etkin", + "specific-time": "Belirli bir zamanda etkin", + "custom": "Özel" + }, + "schedule-day": { + "monday": "Pazartesi", + "tuesday": "Salı", + "wednesday": "Çarşamba", + "thursday": "Perşembe", + "friday": "Cuma", + "saturday": "Cumartesi", + "sunday": "Pazar" + }, + "schedule-days": "Günler", + "schedule-time": "Zaman", + "schedule-time-from": "Başlangıç", + "schedule-time-to": "Bitiş", + "schedule-days-of-week-required": "En az bir hafta günü seçilmelidir.", + "tbel": "TBEL", + "expression-type": { + "simple": "Basit", + "script": "Betik" + }, + "operation-type": { + "and": "Ve", + "or": "Veya" + }, + "filter-predicate-type": { + "string": "Dize", + "numeric": "Sayısal", + "boolean": "Mantıksal", + "complex": "Karmaşık" + }, + "alarm-rule-additional-info": "Ekstra bilgi", + "edit-alarm-rule-additional-info": "Ekstra bilgiyi düzenle", + "alarm-rule-additional-info-placeholder": "Lütfen alarm ayrıntılarında Ekstra bilgi altında görüntülenmesi için yorumlarınızı ve düzenlemelerinizi buraya yazın", + "alarm-rule-additional-info-hint": "İpucu: alarm kuralı koşulunda kullanılan bağımsız değişkenlerin değerlerini yerine koymak için ${Bağımsız değişken adı} kullanın.", + "alarm-rule-additional-info-icon-hint": "Bağımsız değişkenlerin değerlerini yerine koymak için Bağımsız değişken adını kullanın.", + "alarm-rule-mobile-dashboard": "Mobil gösterge paneli", + "alarm-rule-mobile-dashboard-hint": "Mobil uygulama tarafından alarm ayrıntıları gösterge paneli olarak kullanılır.", + "alarm-rule-no-mobile-dashboard": "Hiçbir gösterge paneli seçilmedi", + "alarm-rule-condition": "Alarm kuralı koşulu", + "enter-alarm-rule-condition-prompt": "Koşul ekle", + "enter-alarm-rule-clear-condition-prompt": "Temizleme koşulu ekle", + "edit-alarm-rule-condition": "Alarm koşulu", + "condition-type": "Koşul türü", + "condition-type-hint": "\"Süre\" ve \"Tekrarlayan\" seçenekleri, filtrede \"Eksik\" işlemi kullanıldığında mevcut değildir.", + "select-alarm-severity": "Alarm önceliğini seçin", + "add-create-alarm-rule-prompt": "En az bir tetikleyici koşul gereklidir.", + "add-create-alarm-rule": "Tetikleyici koşulu ekle", + "add-clear-alarm-rule": "Temizleme koşulu ekle", + "condition-duration": "Koşul süresi", + "condition-duration-value": "Süre değeri", + "condition-duration-time-unit": "Zaman birimi", + "condition-duration-value-range": "Süre değeri 1 ile 2147483647 arasında olmalıdır.", + "condition-duration-value-pattern": "Süre değeri tamsayı olmalıdır.", + "condition-duration-value-required": "Süre değeri gereklidir.", + "condition-duration-time-unit-required": "Zaman birimi gereklidir.", + "condition-repeating-value": "Olay sayısı", + "condition-repeating-value-hint": "Herhangi bir alarm kuralı bağımsız değişkeninin güncellenmesi olay olarak sayılır.", + "condition-repeating-value-range": "Olay sayısı 1 ile 2147483647 arasında olmalıdır.", + "condition-repeating-value-pattern": "Olay sayısı tamsayı olmalıdır.", + "condition-repeating-value-required": "Olay sayısı gereklidir.", + "create-conditions": "Tetikleyici koşullar", + "clear-condition": "Temizleme koşulu", + "no-clear-alarm-rule": "Temizleme koşulu yapılandırılmadı.", + "advanced-settings": "Gelişmiş ayarlar", + "propagate-alarm": "Alarmı ilgili varlıklara yay", + "alarm-rule-relation-types-list": "İlişki türleri", + "alarm-rule-relation-types-list-hint": "İlgili varlıkları filtrelemek için ilişki türlerini tanımlar. Belirtilmezse, alarm tüm ilgili varlıklara yayılacaktır.", + "propagate-alarm-to-owner": "Alarmı varlık sahibine yay (Müşteri veya Kiracı)", + "propagate-alarm-to-tenant": "Alarmı Kiracıya yay", + "alarm-rule-filter-title": "Alarm kuralı filtresi", + "filter-title": "Filtre", + "debugging": "Alarm kuralı hata ayıklama", + "any-type": "Herhangi bir tür", + "enter-alarm-rule-type": "Alarm türünü girin", + "no-alarm-rule-types-matching": "‘{{entitySubtype}}’ için eşleşen alarm türleri bulunamadı.", + "alarm-rule-type-list-empty": "Hiç alarm türü seçilmedi.", + "alarm-rule-type-list": "Alarm türü listesi", + "alarm-rule-entity-list": "Varlık listesi", + "missing-for": "Eksik", + "time-unit": "Zaman birimi", + "mode": "Mod", + "type": "Tür", + "value-required": "Değer gereklidir.", + "min-value": "Değer 1 veya daha büyük olmalıdır.", + "argument-in-use": "Bağımsız değişken genel bağımsız değişken olarak kullanılıyor.", + "import-invalid-alarm-rule-type": "Alarm kuralı içe aktarılamadı: Geçersiz alarm kuralı yapısı.", + "no-filter-preview": "Filtre belirtilmedi", + "filter-operation": { + "and": "Ve", + "or": "Veya" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Dokümantasyon", "time-left": "{{time}} kaldı", "output": "Çıktı", + "sort-asc": "Artan", + "sort-desc": "Azalan", "suffix": { "s": "sn", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "Mobil uygulamadaki pano sırası", "mobile-hide": "Panoyu mobil uygulamada gizle", "update-image": "Pano görselini güncelle", + "update-new-version": "Yeni sürüm yükle", + "upload-file-to-update": "Yükseltmek için dosya yükle", "take-screenshot": "Ekran görüntüsü al", "select-widget-title": "Widget seç", "select-widget-value": "{{title}}: widget seç", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Başlatma İstemcisi", "bootstrap-server": "Başlatma Sunucusu", "lwm2m-server": "LwM2M Sunucusu", + "client-reboot": "Kayıt Güncelleme Tetikleyicisi", + "bootstrap-reboot": "Bootstrap-Request Tetikleyicisi", "client-publicKey-or-id": "İstemci Genel Anahtarı veya Kimliği", "client-publicKey-or-id-required": "İstemci Genel Anahtarı veya Kimliği gereklidir.", "client-publicKey-or-id-tooltip-psk": "[RFC7925] standardına göre PSK tanımlayıcısı en fazla 128 baytlık rastgele bir tanımlayıcıdır.\nTanımlayıcı önce karakter dizisine çevrilmeli ve ardından UTF-8 ile kodlanmalıdır.", @@ -2225,6 +2707,7 @@ "short-id-required": "Kısa sunucu kimliği gereklidir.", "short-id-range": "Kısa sunucu kimliği {{ min }} ile {{ max }} arasında olmalıdır.", "short-id-pattern": "Kısa sunucu kimliği pozitif bir tamsayı olmalıdır.", + "short-id-pattern-bs": "Kısa sunucu ID'si yalnızca null olmalıdır", "lifetime": "İstemci kayıt süresi", "lifetime-required": "İstemci kayıt süresi gereklidir.", "lifetime-pattern": "İstemci kayıt süresi pozitif bir tamsayı olmalıdır.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Tüm kaynaklar tek Composite Observe isteğiyle gözlemlenir (daha verimli, daha az esnek)", "composite-by-object": "Nesnelere göre birleştir", "composite-by-object-description": "Kaynaklar nesne türüne göre gruplanır ve ayrı Composite Observe istekleri ile gözlemlenir (dengeli yaklaşım)" - } + }, + "init-attr-tel-as-obs-strategy": "Öznitelikleri ve telemetrileri Observe stratejisi ile başlat", + "init-attr-tel-as-obs-strategy-hint": "Eğer yanlışsa - öznitelikler ve telemetri değerleri tek tek okunarak başlatılır.\\nEğer doğruysa - öznitelikler ve telemetri, Observe stratejisi kullanılarak değerlerine abone olunarak başlatılır." }, "snmp": { "add-communication-config": "İletişim yapılandırması ekle", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Kural düğümleri", "list-of-rulenodes": "{ count, plural, =1 {Bir kural düğümü} other {# kural düğümü listesi} }", "rulenode-name-starts-with": "'{{prefix}}' ile başlayan kural düğümleri", + "type-api-key": "API anahtarı", + "type-api-keys": "API anahtarları", "type-current-customer": "Mevcut Müşteri", "type-current-tenant": "Mevcut Kiracı", "type-current-user": "Mevcut Kullanıcı", @@ -2656,6 +3143,7 @@ "details": "Varlık ayrıntıları", "no-entities-prompt": "Varlık bulunamadı", "no-data": "Görüntülenecek veri yok", + "show-all-columns": "Tümünü Göster", "columns-to-display": "Görüntülenecek sütunlar", "type-api-usage-state": "API Kullanım Durumu", "type-edge": "Uç", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Bir mobil uygulama} other {# mobil uygulama listesi} }", "type-mobile-app-bundle": "Mobil paket", "type-mobile-app-bundles": "Mobil paketler", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Bir mobil paket} other {# mobil paket listesi} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Bir mobil paket} other {# mobil paket listesi} }", + "limit-reached": "Sınır aşıldı", + "limit-reached-text": "{{ entities }} sınırına ulaştınız. Daha fazla eklemek için lütfen Sistem Yöneticinizden {{ entity }} sınırınızı artırmasını isteyin.", + "request-limit-increase": "Sınır artışı talep et", + "request-sysadmin-text": "Sistem Yöneticisi misiniz?", + "login-here": "Buradan giriş yap", + "to-increase-limit": "sınırı artırmak için.", + "increase-limit-request-sent-title": "Sistem Yöneticinize sınır artışı için otomatik bir talep gönderdik", + "increase-limit-request-sent-text": "Lütfen talepleri inceleyip ayarları güncellemeleri için biraz zaman verin. Değişiklikleri görmek için bu sayfayı yenileyebilirsiniz." }, "entity-field": { "created-time": "Oluşturulma zamanı", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Sıfırlama bağlantısı gönderildi", "email": "E-posta", "invalid-email-format": "Geçersiz e-posta formatı.", + "sign-in-with": "{{name}} ile giriş yap", + "sign-in-to-your-account": "Hesabınıza giriş yapın", "or": "veya", "error": "Giriş hatası", "verify-your-identity": "Kimliğinizi doğrulayın", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Aktivasyon bağlantısının süresi doldu", "activation-link-expired-message": "Profilinizi aktifleştirmek için gönderilen bağlantının süresi doldu. Yeni bir e-posta almak için giriş sayfasına dönebilirsiniz.", "reset-password-link-expired": "Şifre sıfırlama bağlantısının süresi doldu", - "reset-password-link-expired-message": "Şifre sıfırlama bağlantısının süresi doldu. Yeni bir e-posta almak için giriş sayfasına dönebilirsiniz." + "reset-password-link-expired-message": "Şifre sıfırlama bağlantısının süresi doldu. Yeni bir e-posta almak için giriş sayfasına dönebilirsiniz.", + "two-fa": "İki faktörlü kimlik doğrulama", + "two-fa-required": "İki faktörlü kimlik doğrulama gereklidir", + "set-up-verification-method": "Devam etmek için bir doğrulama yöntemi ayarlayın", + "set-up-verification-method-login": "Bir doğrulama yöntemi ayarlayın veya giriş yapın", + "enable-authenticator-app": "Authenticator uygulamasını etkinleştir", + "enable-authenticator-app-description": "Lütfen doğrulama uygulamanızdan güvenlik kodunu girin", + "enable-authenticator-sms": "SMS doğrulayıcıyı etkinleştir", + "enable-authenticator-sms-description": "Az önce gönderdiğimiz 6 haneli kodu girin", + "enable-authenticator-email": "E-posta doğrulayıcıyı etkinleştir", + "enable-authenticator-email-description": "Bir güvenlik kodu, e-posta adresinize gönderildiği için lütfen kontrol edin", + "enter-key-manually": "veya bu 32 haneli anahtarı manuel olarak girin:", + "continue": "Devam et", + "confirm": "Onayla", + "authenticator-app-success": "Authenticator uygulaması başarıyla etkinleştirildi", + "authenticator-app-success-description": "Bir dahaki sefere giriş yaptığınızda, iki faktörlü kimlik doğrulama kodu girmeniz gerekecek", + "authenticator-sms-success": "SMS doğrulayıcı başarıyla etkinleştirildi", + "authenticator-sms-success-description": "Bir dahaki sefere giriş yaptığınızda, telefon numarasına gönderilecek güvenlik kodunu girmeniz istenecektir", + "authenticator-email-success": "E-posta doğrulayıcı başarıyla etkinleştirildi", + "authenticator-email-success-description": "Bir dahaki sefere giriş yaptığınızda, e-posta adresinize gönderilecek güvenlik kodunu girmeniz istenecektir", + "authenticator-backup-code-success": "Yedek kod başarıyla etkinleştirildi", + "authenticator-backup-code-success-description": "Bir dahaki sefere giriş yaptığınızda, güvenlik kodunu girmeniz veya yedek kodlardan birini kullanmanız istenecektir.", + "add-verification-method": "Doğrulama yöntemi ekle", + "get-backup-code": "Yedek kodu al", + "copy-key": "Anahtarı kopyala", + "send-code": "Kodu gönder", + "email-label": "E-posta", + "email-description": "Doğrulayıcı olarak kullanmak için bir e-posta adresi girin.", + "sms-description": "Doğrulayıcı olarak kullanmak için bir telefon numarası girin.", + "backup-code-description": "Kodları yazdırarak, hesabınıza giriş yapmak için gerektiğinde kullanabilirsiniz. Her yedek kodu bir kez kullanılabilir.", + "backup-code-warn": "Bu sayfayı terk ettiğinizde, bu kodlar bir daha gösterilemez. Aşağıdaki seçenekleri kullanarak güvenli bir şekilde saklayın.", + "download-txt": "İndir (txt)", + "print": "Yazdır", + "verification-code": "6 haneli kod", + "verification-code-invalid": "Geçersiz doğrulama kodu formatı", + "verification-code-incorrect": "Doğrulama kodu yanlış", + "verification-code-many-request": "Doğrulama kodunu kontrol etmek için çok fazla istek yapıldı", + "scan-qr-code": "Bu QR kodunu doğrulama uygulamanızla tarayın", + "phone-input": { + "phone-input-label": "Telefon numarası", + "phone-input-required": "Telefon numarası gereklidir", + "phone-input-validation": "Telefon numarası geçersiz veya mümkün değil", + "phone-input-pattern": "Geçersiz telefon numarası. E.164 formatında olmalıdır, örn. {{phoneNumber}}", + "phone-input-hint": "Telefon Numarası E.164 formatında olmalıdır, örn. {{phoneNumber}}" + } }, "mobile": { "add-application": "Uygulama ekle", @@ -4175,6 +4717,7 @@ "api-usage-limit": "API kullanım limiti", "device-activity": "Cihaz etkinliği", "entities-limit": "Varlık sınırı", + "entities-limit-increase-request": "Varlık sınırı artırma talebi", "entity-action": "Varlık işlemi", "general": "Genel", "rule-engine-lifecycle-event": "Kural motoru yaşam döngüsü olayı", @@ -4392,6 +4935,12 @@ "at-least": "En az:", "character": "{ count, plural, =1 {1 karakter} other {# karakter} }", "digit": "{ count, plural, =1 {1 rakam} other {# rakam} }", + "password-tooltip-min-length": "En az {{minimumLength}} karakter uzunluğunda olmalı", + "password-tooltip-max-length": "En fazla {{maximumLength}} karakter uzunluğunda olmalı", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} büyük harf karakter", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} küçük harf karakter", + "password-tooltip-digit": "{{minimumDigits}} rakam", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} özel karakter", "incorrect-password-try-again": "Hatalı şifre. Lütfen tekrar deneyin", "lowercase-letter": "{ count, plural, =1 {1 küçük harf} other {# küçük harf} }", "new-passwords-not-match": "Yeni şifreler eşleşmedi", @@ -4450,7 +4999,8 @@ "additional-info": "Ek bilgi (JSON)", "invalid-additional-info": "Ek bilgi json'u ayrıştırılamadı.", "no-relations-text": "İlişki bulunamadı", - "not": "Değil" + "not": "Değil", + "copy-type": "Kopyalama türü" }, "resource": { "add": "Kaynak ekle", @@ -5400,7 +5950,8 @@ }, "time-series": "Zaman serisi", "latest": "Son değerler", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Hesaplanmış alanlar ve alarm kuralları" }, "save-attribute": { "processing-settings": "İşleme ayarları", @@ -5595,7 +6146,8 @@ "bad-request-params": "Hatalı istek parametreleri", "item-not-found": "Öğe bulunamadı", "too-many-requests": "Çok fazla istek yapıldı", - "too-many-updates": "Çok fazla güncelleme yapıldı" + "too-many-updates": "Çok fazla güncelleme yapıldı", + "entities-limit-exceeded": "Varlık sınırı aşıldı" }, "tenant": { "tenant": "Kiracı", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Hesaplanmış alan başına maksimum argüman sayısı", "max-arguments-per-cf-range": "Hesaplanmış alan başına maksimum argüman sayısı negatif olamaz", "max-arguments-per-cf-required": "Hesaplanmış alan başına maksimum argüman sayısı gereklidir", + "max-related-level-per-argument": "'İlgili varlıklar' bağımsız değişkeni için maksimum ilişki düzeyi", + "max-related-level-per-argument-range": "'İlgili varlıklar' bağımsız değişkeni için ilişki düzeyi maksimum sayısı '1' den küçük olamaz", + "max-related-level-per-argument-required": "'İlgili varlıklar' bağımsız değişkeni için ilişki düzeyi maksimum sayısı gereklidir", + "min-allowed-scheduled-update-interval": "'İlgili varlıklar' bağımsız değişkenleri için minimum izin verilen güncelleme aralığı (saniye)", + "min-allowed-scheduled-update-interval-range": "Minimum izin verilen güncelleme aralığı negatif olamaz", + "min-allowed-deduplication-interval": "Minimum izin verilen yinelenen ayıklama aralığı (saniye)", + "min-allowed-deduplication-interval-range": "Minimum izin verilen yinelenen ayıklama aralığı değeri negatif olamaz", + "min-allowed-deduplication-interval-required": "Minimum izin verilen yinelenen ayıklama aralığı gereklidir", + "intermediate-aggregation-interval": "Ara toplama aralığı (saniye)", + "intermediate-aggregation-interval-range": "Ara toplama aralığı değeri '1' den küçük olamaz", + "intermediate-aggregation-interval-required": "Ara toplama aralığı gereklidir", + "reevaluation-check-interval": "Yeniden değerlendirme kontrol aralığı (saniye)", + "reevaluation-check-interval-range": "Yeniden değerlendirme kontrol aralığı değeri '1' den küçük olamaz", + "reevaluation-check-interval-required": "Yeniden değerlendirme kontrol aralığı gereklidir", + "alarms-reevaluation-interval": "Alarm yeniden değerlendirme aralığı (saniye)", + "alarms-reevaluation-interval-range": "Alarm yeniden değerlendirme aralığı değeri '1' den küçük olamaz", + "alarms-reevaluation-interval-required": "Alarm yeniden değerlendirme aralığı gereklidir", + "min-allowed-aggregation-interval": "Minimum izin verilen toplama aralığı (saniye)", + "min-allowed-aggregation-interval-range": "Minimum izin verilen toplama aralığı değeri negatif olamaz", + "min-allowed-aggregation-interval-required": "Minimum izin verilen toplama aralığı gereklidir", + "min-allowed-scheduled-update-interval-required": "Minimum izin verilen güncelleme aralığı sayısı gereklidir", "max-state-size": "Durumun maksimum boyutu (KB)", "max-state-size-range": "Durumun maksimum boyutu (KB) negatif olamaz", "max-state-size-required": "Durumun maksimum boyutu (KB) gereklidir", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Normal kullanıcı başına abonelik maksimum sayısı", "ws-limit-max-subscriptions-per-public-user": "Genel kullanıcı başına abonelik maksimum sayısı", "ws-limit-updates-per-session": "Oturum başına WS güncellemeleri", + "relation-search-entity-limit": "İlişki arama varlık sınırı", + "relation-search-entity-limit-hint": "İlişki yolunun son seviyesinde çözümlenen varlık sayısını sınırlar. 'İlgili varlıklar' bağımsız değişkenlerine ve Yayılma alanlarına uygulanır.", + "relation-search-entity-limit-required": "İlişki arama varlık sınırı gereklidir", + "relation-search-entity-limit-range": "İlişki arama varlık sınırı '1' den küçük olamaz", "rate-limits": { "add-limit": "Sınır ekle", "and-also-less-than": "ve ayrıca daha az", @@ -5993,7 +6570,9 @@ "default-agg-interval": "Varsayılan gruplama aralığı", "edit-intervals-list-hint": "Kullanılabilir aralık seçeneklerinin listesi belirtilebilir.", "edit-grouping-intervals-list-hint": "Gruplama aralıkları listesi ve varsayılan gruplama aralığı yapılandırılabilir.", - "all": "Tümü" + "all": "Tümü", + "save-current-settings-as-default": "Geçerli ayarları varsayılan zaman penceresi olarak kaydet", + "hide-option-from-end-users": "Son kullanıcılardan seçeneği gizle" }, "tooltip": { "trigger": "Tetikleyici", @@ -6648,6 +7227,7 @@ "export-attributes": "Öznitelikleri dışa aktar", "export-credentials": "Kimlik bilgilerini dışa aktar", "export-calculated-fields": "Hesaplanmış alanları ve alarm kurallarını dışa aktar", + "export-alarm-rules": "Alarm kurallarını dışa aktar", "entity-versions": "Varlık sürümleri", "versions": "Sürümler", "created-time": "Oluşturulma zamanı", @@ -6665,6 +7245,7 @@ "load-attributes": "Öznitelikleri yükle", "load-credentials": "Kimlik bilgilerini yükle", "load-calculated-fields": "Hesaplanmış alanları yükle", + "load-alarm-rules": "Alarm kurallarını yükle", "compare-with-current": "Mevcut ile karşılaştır", "diff-entity-with-version": "'{{versionName}}' sürümü ile farkları karşılaştır", "previous-difference": "Önceki fark", @@ -6874,7 +7455,23 @@ "scan-qr-code": "QR Kodu tara", "make-phone-call": "Telefon araması yap", "get-location": "Telefon konumunu al", - "take-screenshot": "Ekran görüntüsü al" + "take-screenshot": "Ekran görüntüsü al", + "handle-provision-success-function": "Sağlama başarı fonksiyonunu işle", + "get-location-function": "Konum alma fonksiyonu", + "process-launch-result-function": "Başlatma sonucu fonksiyonunu işle", + "get-phone-number-function": "Telefon numarası alma fonksiyonu", + "process-image-function": "Resim işleme fonksiyonu", + "process-qr-code-function": "QR kodunu işleme fonksiyonu", + "process-location-function": "Konum işleme fonksiyonu", + "handle-empty-result-function": "Boş sonuç fonksiyonunu işle", + "handle-error-function": "Hata fonksiyonunu işle", + "handle-non-mobile-fallback-function": "Mobil olmayan geri dönüş fonksiyonunu işle", + "save-to-gallery": "'Resim Galerisi'ne kaydet", + "provision-type": "Sağlama türü", + "auto": "Otomatik", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Özel eylem işlevi", "custom-pretty-function": "Özel eylem (HTML şablonlu) işlevi", @@ -6883,7 +7480,8 @@ "marker": "İşaretçi", "polygon": "Poligon", "rectangle": "Dikdörtgen", - "circle": "Daire" + "circle": "Daire", + "polyline": "Çokgen" }, "place-map-item": "Harita öğesi yerleştir", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Poligon çizimine devam et", "finish-draw-polygon": "Poligon çizimini bitir", "start-draw-circle": "Daire çizmeye başla", - "finish-draw-circle": "Daire çizimini bitir" + "finish-draw-circle": "Daire çizimini bitir", + "start-draw-polyline": "Çokgen çizmeye başla", + "finish-draw-polyline": "Çokgen çizmeyi bitir" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Güncelleme animasyon gecikmesi" }, "chart-axis": { + "limit": "Sınır", + "source": "Kaynak", + "key-value": "Anahtar / Değer", + "value-required": "Değer gereklidir.", + "entity-key-required": "Varlık anahtarı gereklidir.", + "key-required": "Anahtar gereklidir.", + "scale-limits": "Ölçek sınırları", + "scale-appearance": "Ölçek görünümü", "scale": "Ölçek", "scale-min": "min", "scale-max": "max", @@ -8005,7 +8613,10 @@ "add-radio-option": "Radyo seçeneği ekle", "radio-label-position": "Etiket konumu", "radio-label-position-before": "Önce", - "radio-label-position-after": "Sonra" + "radio-label-position-after": "Sonra", + "save-image": "Resmi kaydet", + "save-to-gallery": "Yakalanan resimleri otomatik olarak Resim Galerisi'ne kaydet", + "public-image": "Resmi, yetkisiz kullanıcılar için erişilebilir hale getir" }, "invalid-qr-code-text": "QR kod için geçersiz giriş metni. Giriş metni dize türünde olmalıdır", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Rotalar", "markers": "İşaretçiler", "polygons": "Poligonlar", - "circles": "Daireler" + "circles": "Daireler", + "polylines": "Çokgenler" }, "data-layer": { "source": "Kaynak", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "'{{entityName}}' için daire: tamamlamak ve kaydetmek için tıklayın", "finish-circle-hint": "Daire: çizimi tamamlamak için tıklayın" }, + "polyline": { + "polyline-key": "Çokgen anahtarı", + "polyline-key-required": "Çokgen anahtarı gereklidir", + "no-polylines": "Yapılandırılmış çokgen yok", + "add-polylines": "Çokgen ekle", + "polyline-configuration": "Çokgen yapılandırması", + "remove-polyline": "Çokgeni kaldır", + "edit": "Çokgeni düzenle", + "cut": "Çokgen alanını kes", + "rotate": "Çokgeni döndür", + "remove-polyline-for": "'{{entityName}}' için çokgeni kaldır", + "draw-polyline": "Çokgen çiz", + "polyline-place-first-point-hint-with-entity": "'{{entityName}}' için çokgen: İlk noktayı yerleştirmek için tıklayın", + "polyline-place-first-point-hint": "Çokgen: İlk noktayı yerleştirmek için tıklayın", + "finish-polyline-hint-with-entity": "'{{entityName}}' için çokgen: Çizmeyi bitirmek için tıklayın", + "finish-polyline-hint": "Çokgen: Çizmeyi bitirmek için tıklayın", + "polyline-place-first-point-cut-hint": "İlk noktayı yerleştirmek için tıklayın", + "finish-polyline-cut-hint": "İlk işarete tıklayarak bitir ve kaydet" + }, "select-entity": "Varlık seç", "select-entity-hint": "İpucu: Seçim yaptıktan sonra, konum belirlemek için haritaya tıklayın" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Gizli hücre buton eylemi yerine boş alan göster", "dont-reserve-space-hidden-action": "Gizli eylem düğmeleri için alan ayırma", "display-timestamp": "Zaman damgası", + "timestamp-column-name": "Zaman damgası", "display-pagination": "Sayfalama göster", "default-page-size": "Varsayılan sayfa boyutu", "page-step-settings": "Sayfa adımı ayarları", @@ -8999,7 +9631,9 @@ "alarm-column-error": "En az bir alarm sütunu belirtilmelidir", "table-tabs": "Tablo sekmeleri", "show-cell-actions-menu-mobile": "Mobil modda hücre eylem açılır menüsünü göster", - "disable-sorting": "Sıralamayı devre dışı bırak" + "disable-sorting": "Sıralamayı devre dışı bırak", + "sort-by": "Sekmeleri şuna göre sırala", + "sort-timestamp-option": "Oluşma zamanı" }, "latest-chart": { "total": "Toplam", @@ -9491,11 +10125,28 @@ "content": "

    Son kullanıcı panoları oluşturarak, müşteri kullanıcısı yalnızca kendi cihazlarını görebilir ve diğer müşterilerin verileri gizli olur.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    " } } + }, + "api-usage": { + "api-usage": "API kullanımı", + "label": "Etiket", + "state-name": "Durum adı", + "status": "Durum", + "status-required": "Durum gereklidir.", + "limit": "Maksimum sınır", + "limit-required": "Maksimum sınır gereklidir.", + "current-number": "Mevcut sayı", + "current-number-required": "Mevcut sayı gereklidir.", + "add-key": "Anahtar ekle", + "no-key": "Anahtar yok", + "delete-key": "Anahtarı sil", + "target-dashboard-state": "Hedef pano durumu", + "go-to-main-state": "Varsayılan görünüme git" } }, "icon": { "icon": "Simge", "icons": "Simgeler", + "custom": "Özel", "select-icon": "Simge seç", "material-icons": "Material simgeleri", "show-all": "Tüm simgeleri göster", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "toplam" }, "language": { + "auto": "Auto", "language": "Dil" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 654f305ae0..5e46c556ad 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -78,6 +78,7 @@ "show-more": "Показати більше", "dont-show-again": "Більше не показувати", "see-documentation": "Переглянути документацію", + "see-debug-events": "Переглянути події налагодження", "clear": "Очистити", "upload": "Завантажити", "delete-anyway": "Видалити все одно", @@ -485,6 +486,7 @@ "2fa": { "2fa": "Двофакторна автентифікація", "available-providers": "Доступні постачальники", + "available-providers-required": "Потрібно налаштувати щонайменше одного постачальника 2FA.", "issuer-name": "Назва емітента", "issuer-name-required": "Потрібно вказати назву емітента.", "max-verification-failures-before-user-lockout": "Максимальна кількість невдалих перевірок перед блокуванням користувача", @@ -513,7 +515,9 @@ "verification-message-template-required": "Потрібно вказати шаблон повідомлення підтвердження.", "within-time": "Протягом часу", "within-time-pattern": "Час має бути додатним цілим числом.", - "within-time-required": "Потрібно вказати час." + "within-time-required": "Потрібно вказати час.", + "force-2fa": "Примусово вимагати двофакторну автентифікацію", + "enforce-for": "Вимагати для" }, "jwt": { "security-settings": "Налаштування безпеки JWT", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "Тривога", + "alarm-list": "Список тривог", "alarms": "Тривоги", "all-alarms": "Усі тривоги", "select-alarm": "Вибрати тривогу", @@ -649,7 +654,16 @@ "alarm-type": "Тип тривоги", "enter-alarm-type": "Ввести тип тривоги", "no-alarm-types-matching": "Не знайдено типів тривог, що відповідають '{{entitySubtype}}'.", - "alarm-type-list-empty": "Типи тривог не вибрано." + "alarm-type-list-empty": "Типи тривог не вибрано.", + "system-comments": { + "acked-by-user": "Тривогу підтверджено користувачем {{userName}}", + "cleared-by-user": "Тривогу очищено користувачем {{userName}}", + "assigned-to-user": "Тривогу призначено користувачем {{userName}} користувачу {{assigneeName}}", + "unassigned-to-user": "Призначення тривоги скасовано користувачем {{userName}}", + "unassigned-from-deleted-user": "Призначення тривоги скасовано, оскільки користувача {{userName}} було видалено", + "comment-deleted": "Користувач {{userName}} видалив свій коментар", + "severity-changed": "Серйозність тривоги оновлено з {{oldSeverity}} на {{newSeverity}}" + } }, "alarm-activity": { "add": "Додати коментар...", @@ -754,6 +768,7 @@ "name-max-length": "Назва має бути меншою за 256", "label-max-length": "Маркування має бути меншим за 256", "description": "Опис", + "description-required": "Потрібно вказати опис.", "type": "Тип", "type-required": "Потрібно вказати тип.", "details": "Деталі", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "Щомісячна активність створення тривог", "data-points": "Точки даних", "data-points-storage-days": "Дні зберігання точок даних", + "data-points-storage-days-hourly-activity": "Погодинна активність днів зберігання точок даних", + "data-points-storage-days-daily-activity": "Щоденна активність днів зберігання точок даних", + "data-points-storage-days-monthly-activity": "Щомісячна активність днів зберігання точок даних", "device-api": "API пристроїв", "email": "Email", "email-messages": "Повідомлення Email", @@ -900,6 +918,7 @@ "rule-node": "Вузол правила", "sms": "SMS", "sms-messages": "SMS-повідомлення", + "sms-messages-hourly-activity": "Погодинна активність SMS-повідомлень", "sms-messages-daily-activity": "Щоденна активність SMS-повідомлень", "sms-messages-monthly-activity": "Щомісячна активність SMS-повідомлень", "successful": "${entityName} Успішно", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "Погодинна активність збереження телеметрії", "telemetry-persistence-monthly-activity": "Щомісячна активність збереження телеметрії", "transport": "Транспорт", + "transport-msg-hourly-activity": "Погодинна активність транспортних повідомлень", + "transport-msg-daily-activity": "Щоденна активність транспортних повідомлень", + "transport-msg-monthly-activity": "Щомісячна активність транспортних повідомлень", "transport-daily-activity": "Щоденна активність транспорту", "transport-data-points": "Транспортні точки даних", + "transport-data-points-hourly-activity": "Погодинна активність транспортних точок даних", + "transport-data-points-daily-activity": "Щоденна активність транспортних точок даних", + "transport-data-points-monthly-activity": "Щомісячна активність транспортних точок даних", "view-details": "Переглянути деталі", "view-statistics": "Переглянути статистику", - "transport-messages": "Транспортні повідомлення" + "transport-messages": "Транспортні повідомлення", + "transport-messages-hourly-activity": "Погодинна активність транспортних повідомлень", + "transport-data-point-hourly-activity": "Погодинна активність транспортних точок даних", + "javascript-function-executions": "Виконання функцій JavaScript", + "javascript-function-executions-hourly-activity": "Погодинна активність виконання функцій JavaScript", + "javascript-function-executions-daily-activity": "Щоденна активність виконання функцій JavaScript", + "javascript-function-executions-monthly-activity": "Щомісячна активність виконання функцій JavaScript", + "tbel-function-executions": "Виконання функцій TBEL", + "tbel-function-executions-hourly-activity": "Погодинна активність виконання функцій TBEL", + "tbel-function-executions-daily-activity": "Щоденна активність виконання функцій TBEL", + "tbel-function-executions-monthly-activity": "Щомісячна активність виконання функцій TBEL", + "created-reports": "Створені звіти", + "created-reports-hourly-activity": "Погодинна активність створення звітів", + "created-reports-daily-activity": "Щоденна активність створення звітів", + "created-reports-monthly-activity": "Щомісячна активність створення звітів", + "emails": "Emails", + "emails-hourly-activity": "Emails погодинна активність", + "emails-daily-activity": "Emails щоденна активність", + "emails-monthly-activity": "Emails щомісячна активність", + "status": { + "enabled": "Увімкнено", + "disabled": "Вимкнено", + "warning": "Попередження" + } }, "api-limit": { "cassandra-write-queries-core": "Запити запису Cassandra для Rest API", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Повідомлення uplink Edge", "edge-uplink-messages-per-edge": "Повідомлення uplink Edge на Edge" }, + "api-key": { + "api-key": "API-ключ", + "api-keys": "API-ключі", + "delete-api-key-title": "Ви впевнені, що хочете видалити API-ключ '{{name}}'?", + "delete-api-key-text": "Будьте обережні: після підтвердження ключ буде неможливо відновити.", + "delete-api-keys-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 API-ключ} other {# API-ключів} }?", + "delete-api-keys-text": "Будьте обережні: після підтвердження всі вибрані ключі буде неможливо відновити.", + "expiration-date": "Дата закінчення дії", + "date": "дата", + "description": "Опис", + "disable": "Вимкнути", + "edit-description": "Редагувати опис", + "enable": "Увімкнути API-ключ ", + "expiration-time": "Час закінчення дії", + "expiration-time-never": "Ніколи", + "expiration-time-custom": "Користувацький", + "generate": "Згенерувати", + "generate-title": "Згенерувати API-ключ", + "generate-text": "Примітка: API-ключ успадковує дозволи користувача, для якого його створено.", + "generated-api-key-title": "API-ключ згенеровано. Перевірмо підключення!", + "generated-api-key-copy": "Обов'язково скопіюйте та збережіть Ваш API-ключ зараз, оскільки Ви більше не зможете його побачити.", + "generated-api-key-command": "Скористайтеся наведеними нижче інструкціями, щоб перевірити підключення. У результаті Ви маєте отримати інформацію про поточного користувача:", + "generated-api-key-insecure-url": "Виконання команд через незахищене HTTP-з'єднання надішле Ваш API-ключ без шифрування, що робить його вразливим до перехоплення.", + "list": "{ count, plural, =1 {Один API-ключ} other {Список із # API-ключів} }", + "manage": "Керувати", + "manage-api-keys": "Керувати API-ключами", + "no-found": "API-ключів не знайдено", + "selected-api-keys": "Вибрано { count, plural, =1 {1 API-ключ} other {# API-ключів} }", + "search": "Пошук API-ключів", + "status": "Статус", + "status-active": "Активний", + "status-inactive": "Неактивний", + "status-expired": "Термін дії минув" + }, "audit-log": { "audit": "Аудит", "audit-logs": "Журнали аудиту", @@ -991,7 +1073,11 @@ "type-provision-failure": "Не вдалося підготувати пристрій", "type-timeseries-updated": "Телеметрію оновлено", "type-timeseries-deleted": "Телеметрію видалено", - "type-sms-sent": "SMS надіслано" + "type-sms-sent": "SMS надіслано", + "any-type": "Будь-який тип", + "audit-log-filter-title": "Фільтр журналу аудиту", + "filter-title": "Фільтр", + "filter-types": "Типи журналу аудиту" }, "debug-settings": { "label": "Конфігурація налагодження", @@ -1009,15 +1095,29 @@ "expression": "Вираз", "no-found": "Розрахункових полів не знайдено", "list": "{ count, plural, =1 {Одне розрахункове поле} other {Список із # розрахункових полів} }", + "add": "Додати розрахункове поле", "selected-fields": "Вибрано { count, plural, =1 {1 розрахункове поле} other {# розрахункових полів} }", "type": { "simple": "Просте", - "script": "Скрипт" + "simple-hint": "Простий арифметичний розрахунок на основі вхідних аргументів.", + "script": "Скрипт", + "script-hint": "Розрахунок для визначених аргументів із використанням скрипту TBEL.", + "geofencing": "Геозони", + "geofencing-hint": "Оцінювання GPS-позиції сутності та переходів відносно налаштованих груп геозон.", + "propagation": "Поширення", + "propagation-hint": "Поширення даних на батьківські або дочірні сутності на основі напряму та типу зв'язку.", + "related-entities-aggregation": "Агрегація пов'язаних сутностей", + "related-entities-aggregation-hint": "Агрегація останніх даних із пов'язаних сутностей.", + "time-series-data-aggregation": "Агрегація даних часових рядів", + "time-series-data-aggregation-hint": "Агрегація історичних даних поточної сутності." }, + "preview": "Попередній перегляд", "arguments": "Аргументи", "decimals-by-default": "Десяткові знаки за замовчуванням", "debugging": "Налагодження розрахункових полів", + "calculated-field-details": "Деталі розрахункового поля", "argument-name": "Назва аргументу", + "name": "Назва", "datasource": "Джерело даних", "add-argument": "Додати аргумент", "test-script-function": "Тестувати функцію скрипту", @@ -1029,6 +1129,8 @@ "argument-asset": "Актив", "argument-customer": "Клієнт", "argument-tenant": "Поточний тенант", + "argument-owner": "Поточний власник", + "argument-relation-query": "Пов'язані сутності", "argument-type": "Тип аргументу", "attribute": "Атрибут", "copy-argument-name": "Копіювати назву аргументу", @@ -1042,12 +1144,14 @@ "shared-attributes": "Спільні атрибути", "attribute-key": "Ключ атрибуту", "default-value": "Значення за замовчуванням", + "default-value-required": "Потрібно вказати значення за замовчуванням.", "limit": "Максимум значень", "time-window": "Часове вікно", "customer-name": "Назва клієнта", "asset-name": "Назва активу", "timeseries": "Часовий ряд", "output": "Вихідні дані", + "output-hint": "Визначає, як обробляються вихідні дані.", "create": "Створити нове розрахункове поле", "file": "Файл розрахункового поля", "invalid-file-error": "Некоректний формат файлу. Будь ласка, переконайтеся, що файл є валідним JSON-файлом.", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "Будьте обережні: після підтвердження всі вибрані розрахункові поля буде видалено, а всі пов'язані дані буде неможливо відновити.", "test-with-this-message": "Тестувати з цим повідомленням", "use-latest-timestamp": "Використовувати останню мітку часу", + "entity-coordinates": "Координати сутності", + "latitude-time-series-key": "Ключ часового ряду широти", + "latitude-time-series-key-required": "Потрібно вказати ключ часового ряду широти.", + "longitude-time-series-key": "Ключ часового ряду довготи", + "longitude-time-series-key-required": "Потрібно вказати ключ часового ряду довготи.", + "geofencing-zone-groups": "Групи геозон", + "geofencing-zone-groups-settings": "Налаштування груп геозон", + "target-zone": "Цільова зона", + "perimeter-key": "Ключ периметра", + "report-strategy": "Стратегія звітування", + "no-zone-configured": "Потрібна щонайменше одна зона.", + "no-zone-configured-required": "Потрібно налаштувати щонайменше одну групу зон.", + "add-zone-group": "Додати групу зон", + "report-transition-event-only": "Лише події переходу", + "report-presence-status-only": "Лише статус присутності", + "report-transition-event-and-presence": "Статус присутності та події переходу", + "perimeter-attribute-key": "Ключ атрибуту периметра", + "perimeter-attribute-key-required": "Потрібно вказати ключ атрибуту периметра.", + "perimeter-attribute-key-pattern": "Некоректний ключ атрибуту периметра.", + "entity-zone-relationship": "Шлях від сутності до зон", + "direction": "Напрям зв'язку", + "direction-from": "Від сутності до зони", + "direction-to": "Від зони до сутності", + "relation-type": "Тип зв'язку", + "create-relation-with-matched-zones": "Створювати зв'язки для вихідної сутності зі знайденими зонами", + "relation-level": "Рівень зв'язку", + "fetch-last-available-level": "Отримувати лише останній доступний рівень", + "zone-group-refresh-interval": "Інтервал оновлення груп зон", + "copy-zone-group-name": "Копіювати назву групи зон", + "open-details-page": "Відкрити сторінку деталей сутності", + "level": "Рівень", + "direction-level": "Напрям", + "direction-up": "Вгору", + "direction-up-parent": "Вгору до батьківської", + "direction-down": "Вниз", + "direction-down-child": "Вниз до дочірньої", + "add-level": "Додати рівень", + "delete-level": "Видалити рівень", + "no-level": "Рівні не налаштовано", + "levels-required": "Потрібно налаштувати щонайменше один рівень.", + "max-allowed-levels-error": "Рівень зв'язку перевищує максимально дозволений.", + "propagation-path-related-entities": "Шлях поширення до пов'язаних сутностей", + "propagate-type": { + "arguments-only": "Лише аргументи", + "expression-result": "Результат розрахунку" + }, + "script": "Скрипт", + "data-propagate": "Дані для поширення", + "output-key": "Вихідний ключ", + "copy-output-key": "Копіювати вихідний ключ", + "aggregation-path-related-entities": "Шлях агрегації до пов'язаних сутностей", + "deduplication-interval": "Інтервал дедуплікації", + "deduplication-interval-min": "Інтервал дедуплікації має бути щонайменше {{ sec }} секунд.", + "deduplication-interval-hint": "Мінімальний час між агрегаціями телеметрії.", + "deduplication-interval-required": "Потрібно вказати інтервал дедуплікації.", + "calculated-field-filter-title": "Фільтр розрахункових полів", + "filter-title": "Фільтр", + "calculated-field-types": "Типи розрахункових полів", + "events": "Події", + "any-type": "Будь-який тип", + "metrics": { + "metrics": "Метрики", + "metrics-empty": "Потрібно налаштувати щонайменше одну метрику.", + "metric-name": "Назва метрики", + "metric-name-required": "Потрібно вказати назву метрики.", + "metric-name-pattern": "Некоректна назва метрики.", + "metric-name-duplicate": "Метрика з такою назвою вже існує.", + "metric-name-max-length": "Назва метрики має бути меншою за 256 символів.", + "metric-name-forbidden": "Назва метрики зарезервована і не може використовуватися.", + "copy-metric-name": "Копіювати назву метрики", + "argument-name": "Назва аргументу", + "aggregation": "Агрегація", + "aggregation-type": { + "avg": "Середнє", + "min": "Мінімум", + "max": "Максимум", + "sum": "Сума", + "count": "Кількість", + "count-unique": "Кількість унікальних" + }, + "filtered": "Відфільтровано", + "value-source": "Джерело значення", + "value-source-hint": "Визначає, як отримується значення для агрегації.", + "value-source-type": { + "key": "Ключ", + "function": "Функція" + }, + "no-metrics-configured": "Потрібна щонайменше одна метрика.", + "add-metric": "Додати метрику", + "max-metrics": "Досягнуто максимальної кількості метрик.", + "metric-settings": "Налаштування метрики", + "filter": "Фільтр", + "filter-hint": "Увімкніть, щоб фільтрувати сутності під час агрегації. Функція фільтра має повертати булеве значення та може використовувати всі налаштовані аргументи." + }, + "output-strategy": { + "strategy": "Стратегія", + "process-right-away": "Обробляти одразу", + "process-rule-chains": "Обробляти через ланцюги правил", + "save-time-series": "Зберігати у часові ряди", + "save-database": "Зберігати в базу даних", + "save-latest-values": "Зберігати останні значення", + "send-web-sockets": "Надсилати до WebSockets", + "save-calculated-fields": "Надсилати до розрахункових полів", + "update-attribute-only-on-value-change": "Оновлювати атрибут лише при зміні значення", + "send-attributes-updated-notification": "Надсилати сповіщення про оновлення атрибутів", + "ttl": "Користувацький TTL", + "ttl-required": "Потрібно вказати TTL", + "ttl-min": "Дозволено лише мінімальний TTL 0", + "processing-parameters": "Параметри обробки", + "hint": { + "strategy": "Керує тим, чи обробляється результат одразу, чи надсилається до ланцюга правил для додаткової обробки.", + "processing-options": "Параметри обробки", + "update-attribute-only-on-value-change": "Оновлює атрибут для кожного вхідного повідомлення незалежно від того, чи змінилося значення. Це збільшує використання API та знижує продуктивність.", + "update-attribute-only-on-value-change-enabled": "Оновлює атрибут лише коли значення змінюється. Якщо значення не змінилося, мітки часу не оновлюються і сповіщення не надсилаються.", + "send-attributes-updated-notification": "Надсилає подію Attributes Updated до ланцюга правил за замовчуванням.", + "save-time-series": "Зберігає дані часових рядів у таблиці ts_kv в базі даних.", + "save-database": "Зберігає дані атрибутів у базі даних.", + "save-latest-values": "Оновлює дані часових рядів у таблиці ts_kv_latest в базі даних, якщо нове значення є більш актуальним.", + "send-web-sockets-attribute": "Сповіщає підписки WebSocket про оновлення даних атрибутів.", + "send-web-sockets-time-series": "Сповіщає підписки WebSocket про оновлення даних часових рядів.", + "save-calculated-fields-attribute": "Сповіщає розрахункові поля про оновлення даних атрибутів.", + "save-calculated-fields-time-series": "Сповіщає розрахункові поля про оновлення даних часових рядів.", + "ttl": "Визначає період зберігання даних часових рядів. Якщо вимкнено, використовується TTL профілю тенанта." + } + }, + "aggregate-interval-type": "Тип інтервалу агрегації", + "aggregate-interval-value": "Значення інтервалу агрегації", + "aggregate-interval-value-required": "Потрібно вказати значення інтервалу агрегації.", + "aggregate-interval-value-min": "Значення інтервалу агрегації має бути щонайменше { sec, plural, =0 {0 с} =1 {1 с} other {# с} }.", + "aggregate-interval-value-step-multiple-of": "Значення інтервалу агрегації має бути дільником або кратним 1 добі.", + "aggregate-period": { + "hour": "Година", + "day": "День", + "week": "Тиждень (Пн - Нд)", + "week-sun-sat": "Тиждень (Нд - Сб)", + "month": "Місяць", + "quarter": "Квартал", + "year": "Рік", + "custom": "Користувацький" + }, + "aggregate-period-hint-offset": "Ваш інтервал агрегації буде: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Ваш інтервал агрегації буде: {{ interval }} тощо.", + "entity-aggregation": { + "argument-hint": "Дані буде отримано з поточної сутності.", + "argument-title-hint": "Визначає вхідні аргументи, що використовуються для агрегації.", + "argument-setting-hint": "Остання телеметрія — єдиний доступний тип аргументу для цього розрахункового поля.", + "aggregation-interval": "Інтервал агрегації", + "aggregation-interval-hint": "Визначає, як часто виконується агрегація. Приклад: кожна 1 година агрегує дані о 00:00, 01:00, 02:00 тощо. Результати агрегації зберігаються з міткою часу, що відповідає початку інтервалу агрегації.", + "apply-offset": "Застосувати зсув до інтервалу агрегації", + "apply-offset-hint": "Визначає, на скільки зсунути початок кожного періоду агрегації (наприклад, +10 хвилин — 00:10, 01:10).", + "offset-value": "Значення зсуву", + "offset-value-required": "Потрібно вказати значення зсуву.", + "offset-value-min": "Значення зсуву має бути додатним цілим числом.", + "offset-value-max": "Значення зсуву має бути меншим за значення інтервалу агрегації.", + "wait-delay": "Застосувати тайм-аут очікування для затриманої телеметрії", + "wait-delay-hint": "Визначає, як довго чекати на затриману телеметрію після завершення інтервалу. Якщо така телеметрія надійде, результат для цього інтервалу буде перераховано.", + "duration": "Тривалість", + "duration-required": "Потрібно вказати тривалість.", + "duration-min": "Тривалість має бути щонайменше 1 хвилина.", + "duration-hint": "Як довго чекати на затримані дані після завершення інтервалу.", + "produce-intermediate-result": "Формувати проміжний результат", + "produce-intermediate-result-hint": "Обчислює метрики протягом поточного інтервалу, щоб сформувати проміжний результат. Оновлення відбуваються не частіше ніж один раз на {{ time }}." + }, "hint": { "arguments-simple-with-rolling": "Розрахункове поле типу 'Просте' не повинно містити ключів типу 'Ковзне вікно часового ряду'.", + "arguments-propagate-arguments-with-rolling": "Тип 'Ковзне вікно часового ряду' несумісний із поширенням 'Лише аргументи'.", + "arguments-propagate-argument-entity-type": "Тип сутності несумісний із поширенням 'Лише аргументи'.", + "arguments-propagate-argument-must-current-entity": "Потрібно налаштувати щонайменше один аргумент із типом вихідної сутності 'Поточна сутність'.", "arguments-empty": "Потрібно вказати щонайменше один аргумент.", "expression-required": "Потрібно вказати вираз.", "expression-invalid": "Некоректний вираз", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "Аргумент з такою назвою вже існує.", "argument-name-max-length": "Назва аргументу має бути меншою за 256 символів.", "argument-name-forbidden": "Назва аргументу зарезервована і не може використовуватися.", + "output-key-required": "Потрібно вказати вихідний ключ.", + "output-key-pattern": "Некоректний вихідний ключ.", + "output-key-duplicate": "Ключ з такою назвою вже існує.", + "output-key-max-length": "Вихідний ключ має бути меншим за 256 символів.", + "output-key-forbidden": "Вихідний ключ зарезервований і не може використовуватися.", + "entity-type-required": "Потрібно вказати тип сутності", + "name-required": "Потрібно вказати назву.", + "name-pattern": "Некоректна назва.", + "name-duplicate": "Назва з таким значенням уже існує.", + "name-max-length": "Назва має бути меншою за 256 символів.", + "name-forbidden": "Назва зарезервована і не може використовуватися.", "argument-type-required": "Потрібно вказати тип аргументу.", "max-args": "Досягнуто максимальної кількості аргументів.", "decimals-range": "Десяткові знаки за замовчуванням мають бути числом від 0 до 15.", "expression": "Вираз за замовчуванням демонструє, як перетворити температуру з Фаренгейта на Цельсій.", "arguments-entity-not-found": "Цільову сутність аргументу не знайдено.", - "use-latest-timestamp": "Якщо увімкнено, розраховане значення буде збережено з використанням найновішої мітки часу з телеметрії аргументів замість часу сервера." + "use-latest-timestamp": "Якщо увімкнено, розраховане значення буде збережено з використанням найновішої мітки часу з телеметрії аргументів замість часу сервера.", + "entity-coordinates": "Вкажіть ключі часових рядів, які надають GPS-координати сутності (широта та довгота).", + "geofencing-zone-groups": "Визначте одну або кілька груп геозон для перевірки (наприклад, 'allowedZones', 'restrictedZones'). Кожна група має мати унікальну назву, яка використовується як префікс для вихідних ключів телеметрії розрахункового поля.", + "perimeter-attribute-key": "Встановіть ключ атрибуту, який містить визначення периметра геозони. Периметр завжди береться з серверних атрибутів сутності зони.", + "report-strategy": "Статус присутності показує, чи перебуває сутність наразі INSIDE або OUTSIDE групи зон. Події переходу повідомляють, коли сутність ENTERED або LEFT групу зон.", + "create-relation-with-matched-zones": "Автоматично створювати та підтримувати зв'язки між сутністю та зонами, у яких вона наразі знаходиться. Зв'язки видаляються, коли сутність залишає зону, і створюються, коли вона входить у нову.", + "relation-type-required": "Потрібно вказати тип зв'язку.", + "relation-level-required": "Потрібно вказати рівень зв'язку.", + "relation-level-min": "Мінімальне значення рівня зв'язку — 1.", + "relation-level-max": "Максимальне значення рівня зв'язку — {{max}}.", + "geofencing-empty": "Потрібно налаштувати щонайменше одну групу геозон.", + "geofencing-entity-not-found": "Цільову сутність геозонування не знайдено.", + "max-geofencing-zone": "Досягнуто максимальної кількості геозон.", + "zone-group-refresh-interval": "Визначає, як часто оновлюються групи геозон, налаштовані через пов'язані сутності.", + "zone-group-refresh-interval-required": "Потрібно вказати інтервал оновлення груп геозон.", + "zone-group-refresh-interval-min": "Інтервал оновлення груп геозон має бути щонайменше {{ min }} секунд.", + "propagation-path-related-entities": "Визначає прямий одно-рівневий шлях до пов'язаної сутності на основі вибраного напряму та типу зв'язку. Підтримуються лише зв'язки між сутностями пристрою, активу, клієнта та тенанта. Максимальна кількість сутностей, що визначаються за шляхом зв'язку, — {{ max }}.", + "data-propagate": "Визначає дані, які потрібно поширити з аргументів, налаштованих нижче. 'Лише аргументи' використовує отримані дані безпосередньо, а 'Результат розрахунку' обчислює нове значення на основі цих даних.", + "aggregation-path-related-entities": "Визначає одно-рівневий шлях агрегації через прямі зв'язки з батьківськими або дочірніми сутностями на основі напряму та типу зв'язку. Підтримуються лише зв'язки між сутностями пристрою, активу, клієнта та тенанта. Максимальна кількість сутностей, що визначаються за шляхом зв'язку, — {{ max }}.", + "arguments-aggregation": "Визначає вхідні аргументи, що використовуються для фільтрації та агрегації.", + "setting-arguments-aggregation": "Дані буде отримано з пов'язаних сутностей, налаштованих у шляху агрегації.", + "metrics": "Визначає метрики, агреговані на основі налаштованих аргументів.", + "entity-aggregation-metrics": "Визначає метрики, агреговані на основі налаштованих аргументів за вказаними часовими інтервалами.", + "import-invalid-calculated-field-type": "Не вдалося імпортувати розрахункове поле: некоректна структура розрахункового поля.", + "simple-expression-title": "Арифметичний вираз, що визначає, як обчислюється розраховане значення.", + "script-title": "Скрипт TBEL, що визначає логіку розрахунку та вихідні значення.", + "simple-arguments": "Арифметичний вираз, що визначає, як обчислюється розраховане значення.", + "script-arguments": "Визначає вхідні аргументи, доступні для скрипту." + } + }, + "alarm-rule": { + "alarm-rules-tab": "Правила тривог", + "alarm-rule": "Правило тривоги", + "alarm-rules": "Правила тривог", + "alarm-rules-old": "Старі", + "alarm-rules-actual": "Актуальні", + "severities": "Рівні серйозності", + "cleared": "Умова очищення", + "delete-title": "Ви впевнені, що хочете видалити правило тривоги '{{title}}'?", + "delete-text": "Будьте обережні: після підтвердження правило тривоги та всі пов'язані дані буде неможливо відновити.", + "delete-multiple-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 правило тривоги} other {# правил тривог} }?", + "delete-multiple-text": "Будьте обережні: після підтвердження всі вибрані правила тривог буде видалено, а всі пов'язані дані буде неможливо відновити.", + "create": "Створити нове правило тривоги", + "add": "Додати правило тривоги", + "copy": "Клонувати конфігурацію правила тривоги", + "details": "Деталі правила тривоги", + "no-found": "Правил тривог не знайдено", + "list": "{ count, plural, =1 {Одне правило тривоги} other {Список із # правил тривог} }", + "selected-fields": "Вибрано { count, plural, =1 {1 правило тривоги} other {# правил тривог} }", + "import": "Імпортувати правило тривоги", + "file": "Файл правила тривоги", + "export": "Експортувати правило тривоги", + "export-failed-error": "Не вдалося експортувати правило тривоги: {{error}}", + "entity-type": "Тип сутності", + "entity-type-required": "Потрібно вказати тип сутності.", + "alarm-type": "Тип тривоги", + "alarm-type-hint": "Унікальний ідентифікатор (наприклад, HighTempAlarm) у межах ініціатора тривоги (Пристрій, Актив тощо) для запобігання конфліктам.", + "alarm-type-required": "Потрібно вказати тип тривоги.", + "alarm-type-pattern": "Некоректний тип тривоги.", + "alarm-type-max-length": "Тип тривоги має бути меншим за 256 символів.", + "clear-alarm": "Очистити тривогу", + "value-argument": "Аргумент", + "value-argument-required": "Потрібно вказати аргумент.", + "static-settings": "Статичні налаштування", + "configuration": "Конфігурація", + "static-schedule": "Статичний", + "dynamic-schedule": "Динамічний", + "operation-and": "ТА", + "operation-or": "АБО", + "condition-during": "Протягом {{during}}", + "condition-during-dynamic": "Протягом \"{{ attribute }}\"", + "condition-repeat-times": "Повторюється { count, plural, =1 {1 раз} other {# разів} }", + "condition-repeat-times-dynamic": "Повторюється \"{{ attribute }}\" разів", + "filter-preview": "Попередній перегляд фільтра", + "condition-settings": "Налаштування умов", + "static": "Статичний", + "dynamic": "Динамічний", + "argument-filters": "Фільтри аргументів", + "argument-name": "Назва аргументу", + "value-type": "Тип значення", + "general": "Загальні", + "filters": "Фільтри", + "date-time-hint": "Аргумент має бути в мілісекундах епохи. Приклад: 1698839340000 дорівнює 2023-11-01 12:49:00 UTC.", + "operation": "Операція", + "value-source": "Джерело значення", + "value": "Значення", + "ignore-case": "Ігнорувати регістр", + "condition": "Умова", + "script": "Скрипт", + "add-filter": "Додати фільтр аргументу", + "edit-filter": "Фільтр аргументу", + "remove-filter": "Вилучити фільтр аргументу", + "no-filter": "Потрібен щонайменше один фільтр.", + "conditions": { + "simple": "Проста", + "duration": "Тривалість", + "repeating": "Повторювана" + }, + "schedule-title": "Розклад", + "edit-schedule": "Редагувати розклад тривоги", + "schedule-type": "Тип планувальника", + "schedule-type-required": "Потрібно вказати тип планувальника.", + "schedule": { + "any-time": "Активна весь час", + "specific-time": "Активна у визначений час", + "custom": "Користувацький" + }, + "schedule-day": { + "monday": "Понеділок", + "tuesday": "Вівторок", + "wednesday": "Середа", + "thursday": "Четвер", + "friday": "П'ятниця", + "saturday": "Субота", + "sunday": "Неділя" + }, + "schedule-days": "Дні", + "schedule-time": "Час", + "schedule-time-from": "Від", + "schedule-time-to": "До", + "schedule-days-of-week-required": "Потрібно вибрати щонайменше один день тижня.", + "tbel": "TBEL", + "expression-type": { + "simple": "Простий", + "script": "Скрипт" + }, + "operation-type": { + "and": "ТА", + "or": "Або" + }, + "filter-predicate-type": { + "string": "Рядок", + "numeric": "Числовий", + "boolean": "Логічний", + "complex": "Комплексний" + }, + "alarm-rule-additional-info": "Додаткова інформація", + "edit-alarm-rule-additional-info": "Редагувати додаткову інформацію", + "alarm-rule-additional-info-placeholder": "Будь ласка, надайте тут Ваші коментарі та уточнення, щоб відображати їх у деталях тривоги в розділі \"Додаткова інформація\"", + "alarm-rule-additional-info-hint": "Підказка: використовуйте ${Назва аргументу}, щоб підставляти значення аргументів, які використовуються в умові правила тривоги.", + "alarm-rule-additional-info-icon-hint": "Використовуйте назву аргументу, щоб підставляти значення аргументів, які використовуються в умові правила тривоги.", + "alarm-rule-mobile-dashboard": "Мобільний дашборд", + "alarm-rule-mobile-dashboard-hint": "Використовується мобільним застосунком як дашборд деталей тривоги.", + "alarm-rule-no-mobile-dashboard": "Дашборд не вибрано", + "alarm-rule-condition": "Умова правила тривоги", + "enter-alarm-rule-condition-prompt": "Додати умову", + "enter-alarm-rule-clear-condition-prompt": "Додати умову очищення", + "edit-alarm-rule-condition": "Умова тривоги", + "condition-type": "Тип умови", + "condition-type-hint": "Параметри \"Тривалість\" і \"Повторювана\" недоступні, коли у фільтрі використовується операція \"відсутній протягом\".", + "select-alarm-severity": "Вибрати серйозність тривоги", + "add-create-alarm-rule-prompt": "Потрібна щонайменше одна умова спрацювання.", + "add-create-alarm-rule": "Додати умову спрацювання", + "add-clear-alarm-rule": "Додати умову очищення", + "condition-duration": "Тривалість умови", + "condition-duration-value": "Значення тривалості", + "condition-duration-time-unit": "Одиниця часу", + "condition-duration-value-range": "Значення тривалості має бути в діапазоні від 1 до 2147483647.", + "condition-duration-value-pattern": "Значення тривалості має бути цілим числом.", + "condition-duration-value-required": "Потрібно вказати значення тривалості.", + "condition-duration-time-unit-required": "Потрібно вказати одиницю часу.", + "condition-repeating-value": "Кількість подій", + "condition-repeating-value-hint": "Оновлення будь-якого аргументу правила тривоги буде зараховано як подію", + "condition-repeating-value-range": "Кількість подій має бути в діапазоні від 1 до 2147483647.", + "condition-repeating-value-pattern": "Кількість подій має бути цілим числом.", + "condition-repeating-value-required": "Потрібно вказати кількість подій.", + "create-conditions": "Умови спрацювання", + "clear-condition": "Умова очищення", + "no-clear-alarm-rule": "Умову очищення не налаштовано.", + "advanced-settings": "Розширені налаштування", + "propagate-alarm": "Поширювати тривогу на пов'язані сутності", + "alarm-rule-relation-types-list": "Типи зв'язків", + "alarm-rule-relation-types-list-hint": "Визначає типи зв'язків для фільтрації пов'язаних сутностей. Якщо не задано, тривога буде поширена на всі пов'язані сутності.", + "propagate-alarm-to-owner": "Поширювати тривогу на власника сутності (Клієнт або Тенант)", + "propagate-alarm-to-tenant": "Поширювати тривогу на тенанта", + "alarm-rule-filter-title": "Фільтр правил тривог", + "filter-title": "Фільтр", + "debugging": "Налагодження правила тривоги", + "any-type": "Будь-який тип", + "enter-alarm-rule-type": "Ввести тип тривоги", + "no-alarm-rule-types-matching": "Не знайдено типів тривог, що відповідають '{{entitySubtype}}'.", + "alarm-rule-type-list-empty": "Типи тривог не вибрано.", + "alarm-rule-type-list": "Список типів тривог", + "alarm-rule-entity-list": "Список сутностей", + "missing-for": "відсутній протягом", + "time-unit": "Одиниця", + "mode": "Режим", + "type": "Тип", + "value-required": "Потрібно вказати значення.", + "min-value": "Значення має бути 1 або більше.", + "argument-in-use": "Аргумент використовується як загальний аргумент.", + "import-invalid-alarm-rule-type": "Не вдалося імпортувати правило тривоги: некоректна структура правила тривоги.", + "no-filter-preview": "Фільтр не задано", + "filter-operation": { + "and": "ТА", + "or": "Або" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "Документація", "time-left": "Залишилося {{time}}", "output": "Вихідні дані", + "sort-asc": "За зростанням", + "sort-desc": "За спаданням", "suffix": { "s": "с", "ms": "мс" @@ -1357,6 +1835,8 @@ "mobile-order": "Порядок дашбордів у мобільному додатку", "mobile-hide": "Сховати дашборд у мобільному додатку", "update-image": "Оновити зображення дашборду", + "update-new-version": "Завантажити нову версію", + "upload-file-to-update": "Завантажити файл для оновлення", "take-screenshot": "Зробити скріншот", "select-widget-title": "Вибрати віджет", "select-widget-value": "{{title}}: вибрати віджет", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrap клієнт", "bootstrap-server": "Bootstrap сервер", "lwm2m-server": "LwM2M сервер", + "client-reboot": "Тригер оновлення реєстрації", + "bootstrap-reboot": "Тригер запиту Bootstrap", "client-publicKey-or-id": "Публічний ключ клієнта або ID", "client-publicKey-or-id-required": "Публічний ключ клієнта або ID є обов'язковим.", "client-publicKey-or-id-tooltip-psk": "Ідентифікатор PSK є довільним ідентифікатором PSK до 128 байтів, як описано в стандарті [RFC7925].\nІдентифікатор PSK ПОВИНЕН бути спочатку перетворений у рядок символів і потім закодований в октети з використанням UTF-8.", @@ -2225,6 +2707,7 @@ "short-id-required": "Короткий ID сервера є обов'язковим.", "short-id-range": "Короткий ID сервера повинен бути в діапазоні від {{ min }} до {{ max }}.", "short-id-pattern": "Короткий ID сервера повинен бути додатним цілим числом.", + "short-id-pattern-bs": "Короткий ID сервера повинен бути тільки null", "lifetime": "Тривалість реєстрації клієнта", "lifetime-required": "Тривалість реєстрації клієнта є обов'язковою.", "lifetime-pattern": "Тривалість реєстрації клієнта повинна бути додатним цілим числом.", @@ -2319,7 +2802,9 @@ "composite-all-description": "Усі ресурси спостерігаються за допомогою одного комбінованого запиту спостереження (більш ефективно, менше гнучкості)", "composite-by-object": "Комбіноване за об'єктами", "composite-by-object-description": "Ресурси групуються за типом об'єкта і спостерігаються за допомогою окремих комбінованих запитів спостереження (збалансований підхід)" - } + }, + "init-attr-tel-as-obs-strategy": "Ініціалізувати атрибути та телеметрію за допомогою стратегії спостереження", + "init-attr-tel-as-obs-strategy-hint": "Якщо false - атрибути та телеметрія ініціалізуються шляхом послідовного зчитування їх значень.\\nЯкщо true - атрибути та телеметрія ініціалізуються шляхом підписки на їх значення за допомогою стратегії спостереження." }, "snmp": { "add-communication-config": "Додати конфігурацію зв'язку", @@ -2635,6 +3120,8 @@ "type-rulenodes": "Вузли правил", "list-of-rulenodes": "{ count, plural, =1 {Один вузол правила} other {Список # вузлів правил} }", "rulenode-name-starts-with": "Вузли правил, назви яких починаються з '{{prefix}}'", + "type-api-key": "Ключ API", + "type-api-keys": "Ключі API", "type-current-customer": "Поточний клієнт", "type-current-tenant": "Поточний тенант", "type-current-user": "Поточний користувач", @@ -2656,6 +3143,7 @@ "details": "Деталі сутності", "no-entities-prompt": "Сутності не знайдено", "no-data": "Немає даних для відображення", + "show-all-columns": "Показати все", "columns-to-display": "Стовпчики для відображення", "type-api-usage-state": "Стан використання API", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {Один мобільний застосунок} other {Список # мобільних застосунків} }", "type-mobile-app-bundle": "Мобільний набір", "type-mobile-app-bundles": "Мобільні набори", - "list-of-mobile-app-bundles": "{ count, plural, =1 {Один мобільний набір} other {Список # мобільних наборів} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {Один мобільний набір} other {Список # мобільних наборів} }", + "limit-reached": "Досягнуто ліміту", + "limit-reached-text": "Ви досягли ліміту {{ entities }}. Щоб додати більше, будь ласка, попросіть Вашого Системного адміністратора збільшити Ваш ліміт для {{ entity }}.", + "request-limit-increase": "Запросити збільшення ліміту", + "request-sysadmin-text": "Ви є Системним адміністратором?", + "login-here": "Увійти тут", + "to-increase-limit": "щоб збільшити ліміт.", + "increase-limit-request-sent-title": "Ми надіслали автоматичний запит Вашому Системному адміністратору на збільшення ліміту", + "increase-limit-request-sent-text": "Будь ласка, дайте їм трохи часу, щоб переглянути запит і оновити налаштування. Можливо, Вам потрібно буде оновити цю сторінку, щоб побачити зміни." }, "entity-field": { "created-time": "Час створення", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "Посилання для скидання надіслано", "email": "Email", "invalid-email-format": "Некоректний формат email.", + "sign-in-with": "Увійти за допомогою {{name}}", + "sign-in-to-your-account": "Увійти до Вашого облікового запису", "or": "або", "error": "Помилка входу", "verify-your-identity": "Підтвердити Вашу особу", @@ -3807,7 +4305,51 @@ "activation-link-expired": "Посилання активації втратило чинність", "activation-link-expired-message": "Посилання для активації Вашого профілю втратило чинність. Ви можете повернутися на сторінку входу, щоб отримати новий email.", "reset-password-link-expired": "Посилання для скидання пароля втратило чинність", - "reset-password-link-expired-message": "Посилання для скидання Вашого пароля втратило чинність. Ви можете повернутися на сторінку входу, щоб отримати новий email." + "reset-password-link-expired-message": "Посилання для скидання Вашого пароля втратило чинність. Ви можете повернутися на сторінку входу, щоб отримати новий email.", + "two-fa": "Двофакторна автентифікація", + "two-fa-required": "Двофакторна автентифікація є обов'язковою", + "set-up-verification-method": "Налаштуйте метод підтвердження, щоб продовжити", + "set-up-verification-method-login": "Налаштуйте метод підтвердження або увійдіть", + "enable-authenticator-app": "Увімкнути застосунок автентифікації", + "enable-authenticator-app-description": "Будь ласка, введіть код безпеки із Вашого застосунку автентифікації", + "enable-authenticator-sms": "Увімкнути SMS-автентифікатор", + "enable-authenticator-sms-description": "Введіть 6-цифровий код, який ми щойно надіслали на ", + "enable-authenticator-email": "Увімкнути email-автентифікатор", + "enable-authenticator-email-description": "Код безпеки було надіслано на Вашу email адресу ", + "enter-key-manually": "або введіть цей 32-цифровий ключ вручну:", + "continue": "Продовжити", + "confirm": "Підтвердити", + "authenticator-app-success": "Застосунок автентифікації успішно увімкнено", + "authenticator-app-success-description": "Наступного разу під час входу Вам потрібно буде надати код двофакторної автентифікації", + "authenticator-sms-success": "SMS-автентифікатор успішно увімкнено", + "authenticator-sms-success-description": "Наступного разу під час входу Вам буде запропоновано ввести код безпеки, який буде надіслано на номер телефону", + "authenticator-email-success": "Email автентифікатор успішно увімкнено", + "authenticator-email-success-description": "Наступного разу під час входу Вам буде запропоновано ввести код безпеки, який буде надіслано на Вашу email адресу", + "authenticator-backup-code-success": "Резервний код успішно увімкнено", + "authenticator-backup-code-success-description": "Наступного разу під час входу Вам буде запропоновано ввести код безпеки або використати один із резервних кодів.", + "add-verification-method": "Додати метод підтвердження", + "get-backup-code": "Отримати резервний код", + "copy-key": "Копіювати ключ", + "send-code": "Надіслати код", + "email-label": "Email", + "email-description": "Введіть email, щоб використовувати його як Ваш автентифікатор.", + "sms-description": "Введіть номер телефону, щоб використовувати його як Ваш автентифікатор.", + "backup-code-description": "Роздрукуйте коди, щоб вони були під рукою, коли Вам потрібно буде використати їх для входу у Ваш обліковий запис. Ви можете використати кожен резервний код один раз.", + "backup-code-warn": "Щойно Ви покинете цю сторінку, ці коди не можна буде показати знову. Зберігайте їх безпечно, використовуючи варіанти нижче.", + "download-txt": "Завантажити (txt)", + "print": "Друкувати", + "verification-code": "6-цифровий код", + "verification-code-invalid": "Некоректний формат коду підтвердження", + "verification-code-incorrect": "Код підтвердження неправильний", + "verification-code-many-request": "Занадто багато запитів на перевірку коду підтвердження", + "scan-qr-code": "Скануйте цей QR код за допомогою Вашого застосунку підтвердження", + "phone-input": { + "phone-input-label": "Номер телефону", + "phone-input-required": "Номер телефону є обов'язковим", + "phone-input-validation": "Номер телефону некоректний або неможливий", + "phone-input-pattern": "Некоректний номер телефону. Має бути у форматі E.164, наприклад {{phoneNumber}}", + "phone-input-hint": "Номер телефону у форматі E.164, наприклад {{phoneNumber}}" + } }, "mobile": { "add-application": "Додати застосунок", @@ -4175,6 +4717,7 @@ "api-usage-limit": "Ліміт використання API", "device-activity": "Активність пристрою", "entities-limit": "Ліміт сутностей", + "entities-limit-increase-request": "Запит на збільшення ліміту сутностей", "entity-action": "Дія сутності", "general": "Загальні", "rule-engine-lifecycle-event": "Подія життєвого циклу механізму правил", @@ -4392,6 +4935,12 @@ "at-least": "Щонайменше:", "character": "{ count, plural, =1 {1 символ} other {# символи} }", "digit": "{ count, plural, =1 {1 цифра} other {# цифр} }", + "password-tooltip-min-length": "Щонайменше {{minimumLength}} символів", + "password-tooltip-max-length": "Не більше {{maximumLength}} символів", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} великих літер", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} малих літер", + "password-tooltip-digit": "{{minimumDigits}} числа", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} спеціальний символ", "incorrect-password-try-again": "Невірний пароль. Спробуйте ще раз", "lowercase-letter": "{ count, plural, =1 {1 мала літера} other {# малих літер} }", "new-passwords-not-match": "Новий пароль не співпадає", @@ -4450,7 +4999,8 @@ "additional-info": "Додаткова інформація (JSON)", "invalid-additional-info": "Не вдалося розібрати додаткову інформацію JSON.", "no-relations-text": "Відносин не знайдено", - "not": "Не" + "not": "Не", + "copy-type": "Копіювати тип" }, "resource": { "add": "Додати ресурс", @@ -5400,7 +5950,8 @@ }, "time-series": "Часові ряди", "latest": "Останні значення", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "Розрахункові поля та правила тривог" }, "save-attribute": { "processing-settings": "Налаштування обробки", @@ -5595,7 +6146,8 @@ "bad-request-params": "Невірні параметри запиту", "item-not-found": "Елемент не знайдений", "too-many-requests": "Занадто багато запитів", - "too-many-updates": "Занадто багато оновлень" + "too-many-updates": "Занадто багато оновлень", + "entities-limit-exceeded": "Перевищено ліміт сутностей" }, "tenant": { "tenant": "Тенант", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "Максимальна кількість аргументів для розрахункового поля", "max-arguments-per-cf-range": "Максимальна кількість аргументів для розрахункового поля не може бути від'ємною", "max-arguments-per-cf-required": "Максимальна кількість аргументів для розрахункового поля обов'язкова", + "max-related-level-per-argument": "Максимальний рівень зв'язку для аргументу 'Пов'язані сутності'", + "max-related-level-per-argument-range": "Максимальна кількість рівнів зв'язку для аргументу 'Пов'язані сутності' не може бути меншою за '1'", + "max-related-level-per-argument-required": "Максимальна кількість рівнів зв'язку для аргументу 'Пов'язані сутності' обов'язкова", + "min-allowed-scheduled-update-interval": "Мінімальний дозволений інтервал оновлення для аргументів 'Пов'язані сутності' (секунди)", + "min-allowed-scheduled-update-interval-range": "Мінімальний дозволений інтервал оновлення не може бути від'ємним", + "min-allowed-deduplication-interval": "Мінімальний дозволений інтервал дедуплікації (секунди)", + "min-allowed-deduplication-interval-range": "Мінімальний дозволений інтервал дедуплікації не може бути від'ємним", + "min-allowed-deduplication-interval-required": "Мінімальний дозволений інтервал дедуплікації обов'язковий", + "intermediate-aggregation-interval": "Інтервал проміжної агрегації (секунди)", + "intermediate-aggregation-interval-range": "Значення інтервалу проміжної агрегації не може бути меншим за '1'", + "intermediate-aggregation-interval-required": "Інтервал проміжної агрегації обов'язковий", + "reevaluation-check-interval": "Інтервал перевірки повторної оцінки (секунди)", + "reevaluation-check-interval-range": "Значення інтервалу перевірки повторної оцінки не може бути меншим за '1'", + "reevaluation-check-interval-required": "Інтервал перевірки повторної оцінки обов'язковий", + "alarms-reevaluation-interval": "Інтервал повторної оцінки тривог (секунди)", + "alarms-reevaluation-interval-range": "Значення інтервалу повторної оцінки тривог не може бути меншим за '1'", + "alarms-reevaluation-interval-required": "Інтервал повторної оцінки тривог обов'язковий", + "min-allowed-aggregation-interval": "Мінімальний дозволений інтервал агрегації (секунди)", + "min-allowed-aggregation-interval-range": "Мінімальний дозволений інтервал агрегації не може бути від'ємним", + "min-allowed-aggregation-interval-required": "Мінімальний дозволений інтервал агрегації обов'язковий", + "min-allowed-scheduled-update-interval-required": "Мінімальний дозволений інтервал оновлення обов'язковий", "max-state-size": "Максимальний розмір стану в КБ", "max-state-size-range": "Максимальний розмір стану в КБ не може бути від'ємним", "max-state-size-required": "Максимальний розмір стану в КБ обов'язковий", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Максимальна кількість підписок на звичайного користувача", "ws-limit-max-subscriptions-per-public-user": "Максимальна кількість підписок на публічного користувача", "ws-limit-updates-per-session": "Кількість оновлень WS на сесію", + "relation-search-entity-limit": "Обмеження на пошук сутностей у зв'язках", + "relation-search-entity-limit-hint": "Обмежує кількість сутностей, що вирішуються на останньому рівні шляху зв'язку. Застосовується до аргументів 'Пов'язані сутності' та полів пропаганди.", + "relation-search-entity-limit-required": "Обмеження на пошук сутностей у зв'язках обов'язкове", + "relation-search-entity-limit-range": "Обмеження на пошук сутностей у зв'язках не може бути меншим за '1'", "rate-limits": { "add-limit": "Додати ліміт", "and-also-less-than": "а також менше ніж", @@ -5993,7 +6570,9 @@ "default-agg-interval": "За замовчуванням інтервал групування", "edit-intervals-list-hint": "Можна вказати список доступних опцій інтервалів.", "edit-grouping-intervals-list-hint": "Можна налаштувати список інтервалів групування та інтервал групування за замовчуванням.", - "all": "Усі" + "all": "Усі", + "save-current-settings-as-default": "Зберегти поточні налаштування як стандартний часовий інтервал", + "hide-option-from-end-users": "Приховати опцію від кінцевих користувачів" }, "tooltip": { "trigger": "Тригер", @@ -6648,6 +7227,7 @@ "export-attributes": "Експортувати атрибути", "export-credentials": "Експортувати облікові дані", "export-calculated-fields": "Експортувати розрахункові поля \nта правила тривог", + "export-alarm-rules": "Експортувати правила тривог", "entity-versions": "Версії сутностей", "versions": "Версії", "created-time": "Час створення", @@ -6665,6 +7245,7 @@ "load-attributes": "Завантажити атрибути", "load-credentials": "Завантажити облікові дані", "load-calculated-fields": "Завантажити розрахункові поля та правила тривог", + "load-alarm-rules": "Завантажити правила тривог", "compare-with-current": "Порівняти з поточною", "diff-entity-with-version": "Порівняти з версією сутності '{{versionName}}'", "previous-difference": "Попередня різниця", @@ -6874,7 +7455,23 @@ "scan-qr-code": "Сканувати QR код", "make-phone-call": "Зробити телефонний дзвінок", "get-location": "Отримати місцезнаходження телефону", - "take-screenshot": "Зробити скріншот" + "take-screenshot": "Зробити скріншот", + "handle-provision-success-function": "Обробка функції успішного налаштування", + "get-location-function": "Функція отримання місцезнаходження", + "process-launch-result-function": "Функція обробки результату запуску", + "get-phone-number-function": "Функція отримання номера телефону", + "process-image-function": "Функція обробки зображення", + "process-qr-code-function": "Функція обробки QR коду", + "process-location-function": "Функція обробки місцезнаходження", + "handle-empty-result-function": "Функція обробки порожнього результату", + "handle-error-function": "Функція обробки помилки", + "handle-non-mobile-fallback-function": "Функція обробки резервної ситуації для не мобільних пристроїв", + "save-to-gallery": "Зберегти в 'Галерею зображень'", + "provision-type": "Тип налаштування", + "auto": "Авто", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Функція користувацької дії", "custom-pretty-function": "Користувацька дія (з HTML шаблоном) функція", @@ -6883,7 +7480,8 @@ "marker": "Маркер", "polygon": "Полігон", "rectangle": "Прямокутник", - "circle": "Коло" + "circle": "Коло", + "polyline": "Лінія" }, "place-map-item": "Розмістити елемент карти", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "Продовжити малювати полігон", "finish-draw-polygon": "Завершити малювання полігону", "start-draw-circle": "Почати малювати коло", - "finish-draw-circle": "Завершити малювання кола" + "finish-draw-circle": "Завершити малювання кола", + "start-draw-polyline": "Почати малювати лінію", + "finish-draw-polyline": "Завершити малювання лінії" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "Оновити затримку анімації" }, "chart-axis": { + "limit": "Ліміт", + "source": "Джерело", + "key-value": "Ключ / Значення", + "value-required": "Значення обов'язкове.", + "entity-key-required": "Ключ сутності обов'язковий.", + "key-required": "Ключ обов'язковий.", + "scale-limits": "Ліміти шкали", + "scale-appearance": "Зовнішній вигляд шкали", "scale": "Шкала", "scale-min": "мін", "scale-max": "макс", @@ -8005,7 +8613,10 @@ "add-radio-option": "Додати опцію радіокнопки", "radio-label-position": "Позиція мітки", "radio-label-position-before": "До", - "radio-label-position-after": "Після" + "radio-label-position-after": "Після", + "save-image": "Зберегти зображення", + "save-to-gallery": "Автоматично зберігати захоплені зображення в Галереї зображень", + "public-image": "Зробити зображення доступним для будь-якого неавторизованого користувача" }, "invalid-qr-code-text": "Невірний текст для QR-коду. Вхідні дані повинні мати тип рядка", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "Поїздки", "markers": "Маркери", "polygons": "Многоугольники", - "circles": "Коло" + "circles": "Коло", + "polylines": "Полілінії" }, "data-layer": { "source": "Джерело", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "Коло для '{{entityName}}': натисніть, щоб завершити та зберегти коло", "finish-circle-hint": "Коло: натисніть, щоб завершити малювання" }, + "polyline": { + "polyline-key": "Ключ полілінії", + "polyline-key-required": "Ключ полілінії необхідний", + "no-polylines": "Полілінії не налаштовані", + "add-polylines": "Додати полілінію", + "polyline-configuration": "Налаштування полілінії", + "remove-polyline": "Видалити полілінію", + "edit": "Редагувати полілінію", + "cut": "Обрізати область полілінії", + "rotate": "Поворот полілінії", + "remove-polyline-for": "Видалити полілінію для '{{entityName}}'", + "draw-polyline": "Малювати полілінію", + "polyline-place-first-point-hint-with-entity": "Полілінія для '{{entityName}}': натисніть, щоб розмістити першу точку", + "polyline-place-first-point-hint": "Полілінія: натисніть, щоб розмістити першу точку", + "finish-polyline-hint-with-entity": "Полілінія для '{{entityName}}': натисніть, щоб завершити малювання", + "finish-polyline-hint": "Полілінія: натисніть, щоб завершити малювання", + "polyline-place-first-point-cut-hint": "Натисніть, щоб розмістити першу точку", + "finish-polyline-cut-hint": "Натисніть на перший маркер, щоб завершити та зберегти" + }, "select-entity": "Вибрати сутність", "select-entity-hint": "Підказка: після вибору натисніть на карті, щоб встановити позицію" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "Показувати порожнє місце замість прихованої дії кнопки комірки", "dont-reserve-space-hidden-action": "Не резервувати місце для прихованих кнопок дій", "display-timestamp": "Мітка часу", + "timestamp-column-name": "Назва стовпця мітки часу", "display-pagination": "Показувати пагінацію", "default-page-size": "Розмір сторінки за замовчуванням", "page-step-settings": "Налаштування кроку пагінації", @@ -8999,7 +9631,9 @@ "alarm-column-error": "Потрібно вказати хоча б один стовпець тривоги", "table-tabs": "Вкладки таблиці", "show-cell-actions-menu-mobile": "Показувати випадаюче меню дій комірки в мобільному режимі", - "disable-sorting": "Вимкнути сортування" + "disable-sorting": "Вимкнути сортування", + "sort-by": "Сортувати вкладки за", + "sort-timestamp-option": "Час створення" }, "latest-chart": { "total": "Загалом", @@ -9491,11 +10125,28 @@ "content": "

    Після створення дашбордів для кінцевих користувачів користувач клієнта може бачити лише власні пристрої, а дані іншого клієнта буде приховано.

    Перегляньте документацію, як це зробити:

    " } } + }, + "api-usage": { + "api-usage": "Використання API", + "label": "Маркування", + "state-name": "Назва стану", + "status": "Статус", + "status-required": "Потрібен статус.", + "limit": "Максимальний ліміт", + "limit-required": "Потрібен максимальний ліміт.", + "current-number": "Поточне значення", + "current-number-required": "Потрібне поточне значення.", + "add-key": "Додати ключ", + "no-key": "Немає ключа", + "delete-key": "Видалити ключ", + "target-dashboard-state": "Цільовий стан дашборда", + "go-to-main-state": "Перейти до перегляду за замовчуванням" } }, "icon": { "icon": "Піктограма", "icons": "Піктограми", + "custom": "Користувацька", "select-icon": "Вибрати піктограму", "material-icons": "Material icons", "show-all": "Показати всі піктограми", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "з" }, "language": { + "auto": "Auto", "language": "Мова" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index fb1c849057..8ed93ba0a4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -78,6 +78,7 @@ "show-more": "显示更多", "dont-show-again": "不再显示", "see-documentation": "查看文档", + "see-debug-events": "查看调试事件", "clear": "清除", "upload": "上传", "delete-anyway": "仍然删除", @@ -485,6 +486,7 @@ "2fa": { "2fa": "双因素认证", "available-providers": "可用提供商", + "available-providers-required": "至少需要配置一个双因素认证提供商。", "issuer-name": "签发者名称", "issuer-name-required": "签发者名称为必填项。", "max-verification-failures-before-user-lockout": "用户锁定前最大验证失败次数", @@ -513,7 +515,9 @@ "verification-message-template-required": "验证消息模板为必填项。", "within-time": "时间范围", "within-time-pattern": "时间必须为正整数。", - "within-time-required": "时间为必填项。" + "within-time-required": "时间为必填项。", + "force-2fa": "强制双因素认证", + "enforce-for": "强制对象" }, "jwt": { "security-settings": "JWT 安全设置", @@ -549,6 +553,7 @@ }, "alarm": { "alarm": "告警", + "alarm-list": "告警列表", "alarms": "告警", "all-alarms": "所有告警", "select-alarm": "选择告警", @@ -649,7 +654,16 @@ "alarm-type": "告警类型", "enter-alarm-type": "输入告警类型", "no-alarm-types-matching": "未找到匹配“{{entitySubtype}}”的告警类型。", - "alarm-type-list-empty": "未选择告警类型。" + "alarm-type-list-empty": "未选择告警类型。", + "system-comments": { + "acked-by-user": "告警已由用户 {{userName}} 确认", + "cleared-by-user": "告警已由用户 {{userName}} 清除", + "assigned-to-user": "告警已由用户 {{userName}} 分配给用户 {{assigneeName}}", + "unassigned-to-user": "告警已由用户 {{userName}} 取消分配", + "unassigned-from-deleted-user": "告警已取消分配,因为用户 {{userName}} 已被删除", + "comment-deleted": "用户 {{userName}} 删除了评论", + "severity-changed": "告警严重程度已从 {{oldSeverity}} 更新为 {{newSeverity}}" + } }, "alarm-activity": { "add": "添加评论...", @@ -754,6 +768,7 @@ "name-max-length": "名称应少于 256 个字符", "label-max-length": "标签应少于 256 个字符", "description": "描述", + "description-required": "描述为必填项。", "type": "类型", "type-required": "类型为必填项。", "details": "详情", @@ -867,6 +882,9 @@ "alarms-created-monthly-activity": "已创建告警每月活动", "data-points": "数据点", "data-points-storage-days": "数据点存储天数", + "data-points-storage-days-hourly-activity": "数据点存储天数每小时活动", + "data-points-storage-days-daily-activity": "数据点存储天数每日活动", + "data-points-storage-days-monthly-activity": "数据点存储天数每月活动", "device-api": "设备 API", "email": "Email", "email-messages": "Email 消息", @@ -900,6 +918,7 @@ "rule-node": "规则节点", "sms": "SMS", "sms-messages": "SMS 消息", + "sms-messages-hourly-activity": "SMS 消息每小时活动", "sms-messages-daily-activity": "SMS 消息每日活动", "sms-messages-monthly-activity": "SMS 消息每月活动", "successful": "${entityName} 成功", @@ -909,11 +928,40 @@ "telemetry-persistence-hourly-activity": "遥测持久化每小时活动", "telemetry-persistence-monthly-activity": "遥测持久化每月活动", "transport": "传输", + "transport-msg-hourly-activity": "传输消息每小时活动", + "transport-msg-daily-activity": "传输消息每日活动", + "transport-msg-monthly-activity": "传输消息每月活动", "transport-daily-activity": "传输每日活动", "transport-data-points": "传输数据点", + "transport-data-points-hourly-activity": "传输数据点每小时活动", + "transport-data-points-daily-activity": "传输数据点每日活动", + "transport-data-points-monthly-activity": "传输数据点每月活动", "view-details": "查看详情", "view-statistics": "查看统计", - "transport-messages": "传输消息" + "transport-messages": "传输消息", + "transport-messages-hourly-activity": "传输消息每小时活动", + "transport-data-point-hourly-activity": "传输数据点每小时活动", + "javascript-function-executions": "JavaScript 函数执行次数", + "javascript-function-executions-hourly-activity": "JavaScript 函数执行每小时活动", + "javascript-function-executions-daily-activity": "JavaScript 函数执行每日活动", + "javascript-function-executions-monthly-activity": "JavaScript 函数执行每月活动", + "tbel-function-executions": "TBEL 函数执行次数", + "tbel-function-executions-hourly-activity": "TBEL 函数执行每小时活动", + "tbel-function-executions-daily-activity": "TBEL 函数执行每日活动", + "tbel-function-executions-monthly-activity": "TBEL 函数执行每月活动", + "created-reports": "已创建的报表", + "created-reports-hourly-activity": "已创建报表每小时活动", + "created-reports-daily-activity": "已创建报表每日活动", + "created-reports-monthly-activity": "已创建报表每月活动", + "emails": "Emails", + "emails-hourly-activity": "Emails 每小时活动", + "emails-daily-activity": "Emails 每日活动", + "emails-monthly-activity": "Emails 每月活动", + "status": { + "enabled": "已启用", + "disabled": "已禁用", + "warning": "警告" + } }, "api-limit": { "cassandra-write-queries-core": "Rest API Cassandra 写入查询", @@ -938,6 +986,40 @@ "edge-uplink-messages": "Edge 上行消息", "edge-uplink-messages-per-edge": "每 Edge 上行消息" }, + "api-key": { + "api-key": "API 密钥", + "api-keys": "API 密钥", + "delete-api-key-title": "确定要删除 API 密钥“{{name}}”吗?", + "delete-api-key-text": "请注意,确认后密钥将无法恢复。", + "delete-api-keys-title": "确定要删除 { count, plural, =1 {1 个 API 密钥} other {# 个 API 密钥} }吗?", + "delete-api-keys-text": "请注意,确认后所有选中的密钥将无法恢复。", + "expiration-date": "过期日期", + "date": "日期", + "description": "描述", + "disable": "禁用", + "edit-description": "编辑描述", + "enable": "启用 API 密钥 ", + "expiration-time": "过期时间", + "expiration-time-never": "永不", + "expiration-time-custom": "自定义", + "generate": "生成", + "generate-title": "生成 API 密钥", + "generate-text": "注意:API 密钥继承创建它的用户的权限。", + "generated-api-key-title": "API 密钥已生成。请检查连接!", + "generated-api-key-copy": "请确保立即复制并保存您的 API 密钥,因为之后将无法再次查看。", + "generated-api-key-command": "使用以下说明检查连接。结果应返回当前用户信息:", + "generated-api-key-insecure-url": "通过不安全的 HTTP 连接执行命令将以未加密的方式发送您的 API 密钥,使其容易被拦截。", + "list": "{ count, plural, =1 {一个 API 密钥} other {# 个 API 密钥列表} }", + "manage": "管理", + "manage-api-keys": "管理 API 密钥", + "no-found": "未找到 API 密钥", + "selected-api-keys": "{ count, plural, =1 {1 个 API 密钥} other {# 个 API 密钥} }已选择", + "search": "搜索 API 密钥", + "status": "状态", + "status-active": "活动", + "status-inactive": "未激活", + "status-expired": "已过期" + }, "audit-log": { "audit": "审计", "audit-logs": "审计日志", @@ -991,7 +1073,11 @@ "type-provision-failure": "设备配置失败", "type-timeseries-updated": "遥测已更新", "type-timeseries-deleted": "遥测已删除", - "type-sms-sent": "SMS 已发送" + "type-sms-sent": "SMS 已发送", + "any-type": "任意类型", + "audit-log-filter-title": "审计日志过滤器", + "filter-title": "过滤器", + "filter-types": "审计日志类型" }, "debug-settings": { "label": "调试配置", @@ -1009,15 +1095,29 @@ "expression": "表达式", "no-found": "未找到计算字段", "list": "{ count, plural, =1 {一个计算字段} other {# 个计算字段列表} }", + "add": "添加计算字段", "selected-fields": "{ count, plural, =1 {1 个计算字段} other {# 个计算字段} }已选择", "type": { "simple": "简单", - "script": "脚本" + "simple-hint": "基于输入参数的简单算术计算。", + "script": "脚本", + "script-hint": "使用 TBEL 脚本对定义的参数进行计算。", + "geofencing": "地理围栏", + "geofencing-hint": "评估实体 GPS 位置以及与配置的地理围栏区域组的转换关系。", + "propagation": "传播", + "propagation-hint": "根据关联方向和类型将数据传播到父级或子级实体。", + "related-entities-aggregation": "关联实体聚合", + "related-entities-aggregation-hint": "聚合来自关联实体的最新数据。", + "time-series-data-aggregation": "时间序列数据聚合", + "time-series-data-aggregation-hint": "聚合来自当前实体的历史数据。" }, + "preview": "预览", "arguments": "参数", "decimals-by-default": "默认小数位数", "debugging": "计算字段调试", + "calculated-field-details": "计算字段详情", "argument-name": "参数名称", + "name": "名称", "datasource": "数据源", "add-argument": "添加参数", "test-script-function": "测试脚本函数", @@ -1029,6 +1129,8 @@ "argument-asset": "资产", "argument-customer": "客户", "argument-tenant": "当前租户", + "argument-owner": "当前所有者", + "argument-relation-query": "关联实体", "argument-type": "参数类型", "attribute": "属性", "copy-argument-name": "复制参数名称", @@ -1042,12 +1144,14 @@ "shared-attributes": "共享属性", "attribute-key": "属性键", "default-value": "默认值", + "default-value-required": "默认值为必填项。", "limit": "最大值数量", "time-window": "时间窗口", "customer-name": "客户名称", "asset-name": "资产名称", "timeseries": "时间序列", "output": "输出", + "output-hint": "定义输出的处理方式。", "create": "创建新的计算字段", "file": "计算字段文件", "invalid-file-error": "文件格式无效。请确保文件为有效的 JSON 文件。", @@ -1061,8 +1165,174 @@ "delete-multiple-text": "请注意,确认后所有选中的计算字段将被移除,所有相关数据将无法恢复。", "test-with-this-message": "使用此消息测试", "use-latest-timestamp": "使用最新时间戳", + "entity-coordinates": "实体坐标", + "latitude-time-series-key": "纬度时间序列键", + "latitude-time-series-key-required": "纬度时间序列键为必填项。", + "longitude-time-series-key": "经度时间序列键", + "longitude-time-series-key-required": "经度时间序列键为必填项。", + "geofencing-zone-groups": "地理围栏区域组", + "geofencing-zone-groups-settings": "地理围栏区域组设置", + "target-zone": "目标区域", + "perimeter-key": "周界键", + "report-strategy": "报告策略", + "no-zone-configured": "至少需要一个区域。", + "no-zone-configured-required": "至少需要配置一个区域组。", + "add-zone-group": "添加区域组", + "report-transition-event-only": "仅转换事件", + "report-presence-status-only": "仅存在状态", + "report-transition-event-and-presence": "存在状态和转换事件", + "perimeter-attribute-key": "周界属性键", + "perimeter-attribute-key-required": "周界属性键为必填项。", + "perimeter-attribute-key-pattern": "周界属性键无效。", + "entity-zone-relationship": "从实体到区域的路径", + "direction": "关联方向", + "direction-from": "从实体到区域", + "direction-to": "从区域到实体", + "relation-type": "关联类型", + "create-relation-with-matched-zones": "为源实体与匹配的区域创建关联", + "relation-level": "关联层级", + "fetch-last-available-level": "仅获取最后可用层级", + "zone-group-refresh-interval": "区域组刷新间隔", + "copy-zone-group-name": "复制区域组名称", + "open-details-page": "打开实体详情页", + "level": "层级", + "direction-level": "方向", + "direction-up": "向上", + "direction-up-parent": "向上到父级", + "direction-down": "向下", + "direction-down-child": "向下到子级", + "add-level": "添加层级", + "delete-level": "删除层级", + "no-level": "未配置层级", + "levels-required": "至少需要配置一个层级。", + "max-allowed-levels-error": "关联层级超过允许的最大值。", + "propagation-path-related-entities": "到关联实体的传播路径", + "propagate-type": { + "arguments-only": "仅参数", + "expression-result": "计算结果" + }, + "script": "脚本", + "data-propagate": "要传播的数据", + "output-key": "输出键", + "copy-output-key": "复制输出键", + "aggregation-path-related-entities": "到关联实体的聚合路径", + "deduplication-interval": "去重间隔", + "deduplication-interval-min": "去重间隔至少应为 {{ sec }} 秒。", + "deduplication-interval-hint": "遥测聚合之间的最小时间。", + "deduplication-interval-required": "去重间隔为必填项。", + "calculated-field-filter-title": "计算字段过滤器", + "filter-title": "过滤器", + "calculated-field-types": "计算字段类型", + "events": "事件", + "any-type": "任意类型", + "metrics": { + "metrics": "指标", + "metrics-empty": "至少需要配置一个指标。", + "metric-name": "指标名称", + "metric-name-required": "指标名称为必填项。", + "metric-name-pattern": "指标名称无效。", + "metric-name-duplicate": "同名指标已存在。", + "metric-name-max-length": "指标名称应少于 256 个字符。", + "metric-name-forbidden": "指标名称为保留名称,无法使用。", + "copy-metric-name": "复制指标名称", + "argument-name": "参数名称", + "aggregation": "聚合", + "aggregation-type": { + "avg": "平均值", + "min": "最小值", + "max": "最大值", + "sum": "总和", + "count": "计数", + "count-unique": "唯一计数" + }, + "filtered": "已过滤", + "value-source": "值来源", + "value-source-hint": "定义聚合值的获取方式。", + "value-source-type": { + "key": "键", + "function": "函数" + }, + "no-metrics-configured": "至少需要一个指标。", + "add-metric": "添加指标", + "max-metrics": "已达到最大指标数量。", + "metric-settings": "指标设置", + "filter": "过滤器", + "filter-hint": "启用聚合期间的实体过滤。过滤函数必须返回布尔值,并且可以使用所有已配置的参数。" + }, + "output-strategy": { + "strategy": "策略", + "process-right-away": "立即处理", + "process-rule-chains": "通过规则链处理", + "save-time-series": "保存到时间序列", + "save-database": "保存到数据库", + "save-latest-values": "保存到最新值", + "send-web-sockets": "发送到 WebSockets", + "save-calculated-fields": "发送到计算字段", + "update-attribute-only-on-value-change": "仅在值变更时更新属性", + "send-attributes-updated-notification": "发送属性更新通知", + "ttl": "自定义 TTL", + "ttl-required": "TTL 为必填项", + "ttl-min": "最小 TTL 仅允许为 0", + "processing-parameters": "处理参数", + "hint": { + "strategy": "控制结果是立即处理还是发送到规则链进行额外处理。", + "processing-options": "处理选项", + "update-attribute-only-on-value-change": "在每条传入消息上更新属性,无论值是否已更改。这会增加 API 使用量并降低性能。", + "update-attribute-only-on-value-change-enabled": "仅在值更改时更新属性。如果值未更改,则不更新时间戳且不发送通知。", + "send-attributes-updated-notification": "向默认规则链发送属性更新事件。", + "save-time-series": "将时间序列数据保存到数据库的 ts_kv 表中。", + "save-database": "将属性数据保存到数据库。", + "save-latest-values": "如果新值更新,则更新数据库 ts_kv_latest 表中的时间序列数据。", + "send-web-sockets-attribute": "通知 WebSocket 订阅有关属性数据的更新。", + "send-web-sockets-time-series": "通知 WebSocket 订阅有关时间序列数据的更新。", + "save-calculated-fields-attribute": "通知计算字段有关属性数据的更新。", + "save-calculated-fields-time-series": "通知计算字段有关时间序列数据的更新。", + "ttl": "定义时间序列数据的保留期限。如果禁用,则使用租户配置的 TTL。" + } + }, + "aggregate-interval-type": "聚合间隔类型", + "aggregate-interval-value": "聚合间隔值", + "aggregate-interval-value-required": "聚合间隔值为必填项。", + "aggregate-interval-value-min": "聚合间隔值至少应为 { sec, plural, =0 {0 秒} =1 {1 秒} other {# 秒} }。", + "aggregate-interval-value-step-multiple-of": "聚合间隔值必须是 1 天的因子或倍数。", + "aggregate-period": { + "hour": "小时", + "day": "天", + "week": "周(周一至周日)", + "week-sun-sat": "周(周日至周六)", + "month": "月", + "quarter": "季度", + "year": "年", + "custom": "自定义" + }, + "aggregate-period-hint-offset": "您的聚合间隔将为:{{ interval }}", + "aggregate-period-hint-offset-and-so-on": "您的聚合间隔将为:{{ interval }} 以此类推。", + "entity-aggregation": { + "argument-hint": "数据将从当前实体获取。", + "argument-title-hint": "定义用于聚合的输入参数。", + "argument-setting-hint": "此计算字段仅支持最新遥测作为参数类型。", + "aggregation-interval": "聚合间隔", + "aggregation-interval-hint": "定义执行聚合的频率。例如:每 1 小时在 00:00、01:00、02:00 等时间点聚合数据。聚合结果使用聚合间隔开始时间的时间戳存储。", + "apply-offset": "应用偏移到聚合间隔", + "apply-offset-hint": "定义每个聚合周期开始的偏移量(例如,+10 分钟 - 00:10、01:10)。", + "offset-value": "偏移值", + "offset-value-required": "偏移值为必填项。", + "offset-value-min": "偏移值必须为正整数。", + "offset-value-max": "偏移值应小于聚合间隔值。", + "wait-delay": "对延迟遥测应用等待超时", + "wait-delay-hint": "定义间隔结束后等待延迟遥测的时间。如果此类遥测到达,将重新计算该间隔的结果。", + "duration": "持续时间", + "duration-required": "持续时间为必填项。", + "duration-min": "持续时间至少为 1 分钟。", + "duration-hint": "间隔结束后等待延迟数据的时间。", + "produce-intermediate-result": "生成中间结果", + "produce-intermediate-result-hint": "在当前间隔内计算指标以生成中间结果。更新频率不超过每 {{ time }} 一次。" + }, "hint": { "arguments-simple-with-rolling": "简单类型的计算字段不应包含时间序列滚动类型的键。", + "arguments-propagate-arguments-with-rolling": "“时间序列滚动”类型与“仅参数”传播不兼容。", + "arguments-propagate-argument-entity-type": "实体类型与“仅参数”传播不兼容。", + "arguments-propagate-argument-must-current-entity": "至少需要配置一个源实体类型为“当前实体”的参数。", "arguments-empty": "至少需要指定一个参数。", "expression-required": "表达式为必填项。", "expression-invalid": "表达式无效", @@ -1072,12 +1342,218 @@ "argument-name-duplicate": "同名参数已存在。", "argument-name-max-length": "参数名称应少于 256 个字符。", "argument-name-forbidden": "参数名称为保留名称,无法使用。", + "output-key-required": "输出键为必填项。", + "output-key-pattern": "输出键无效。", + "output-key-duplicate": "同名键已存在。", + "output-key-max-length": "输出键应少于 256 个字符。", + "output-key-forbidden": "输出键为保留名称,无法使用。", + "entity-type-required": "实体类型为必填项", + "name-required": "名称为必填项。", + "name-pattern": "名称无效。", + "name-duplicate": "同名名称已存在。", + "name-max-length": "名称应少于 256 个字符。", + "name-forbidden": "名称为保留名称,无法使用。", "argument-type-required": "参数类型为必填项。", "max-args": "已达到最大参数数量。", "decimals-range": "默认小数位数应为 0 到 15 之间的数字。", "expression": "默认表达式演示如何将温度从华氏度转换为摄氏度。", "arguments-entity-not-found": "未找到参数目标实体。", - "use-latest-timestamp": "启用后,计算值将使用参数遥测中的最新时间戳进行持久化,而非服务器时间。" + "use-latest-timestamp": "启用后,计算值将使用参数遥测中的最新时间戳进行持久化,而非服务器时间。", + "entity-coordinates": "指定提供实体 GPS 坐标(纬度和经度)的时间序列键。", + "geofencing-zone-groups": "定义一个或多个要检查的地理围栏区域组(例如“allowedZones”、“restrictedZones”)。每个组必须有唯一的名称,该名称用作计算字段输出遥测键的前缀。", + "perimeter-attribute-key": "设置包含地理围栏区域周界定义的属性键。周界始终取自区域实体的服务端属性。", + "report-strategy": "存在状态报告实体当前是否在区域组内部或外部。转换事件报告实体何时进入或离开区域组。", + "create-relation-with-matched-zones": "自动创建并维护实体与其当前所在区域之间的关联。当实体离开区域时移除关联,进入新区域时创建关联。", + "relation-type-required": "关联类型为必填项。", + "relation-level-required": "关联层级为必填项。", + "relation-level-min": "最小关联层级值为 1。", + "relation-level-max": "最大关联层级值为 {{max}}。", + "geofencing-empty": "至少需要配置一个区域组。", + "geofencing-entity-not-found": "未找到地理围栏目标实体。", + "max-geofencing-zone": "已达到最大地理围栏区域数量。", + "zone-group-refresh-interval": "定义通过关联实体配置的区域组的刷新频率。", + "zone-group-refresh-interval-required": "区域组刷新间隔为必填项。", + "zone-group-refresh-interval-min": "区域组刷新间隔至少应为 {{ min }} 秒。", + "propagation-path-related-entities": "定义基于所选方向和关联类型到关联实体的直接单级路径。仅支持设备、资产、客户和租户实体之间的关联。关联路径解析的最大实体数为 {{ max }}。", + "data-propagate": "定义要从下方配置的参数中传播的数据。“仅参数”直接使用检索到的数据,而“计算结果”从该数据计算新值。", + "aggregation-path-related-entities": "定义基于方向和关联类型通过与父级或子级实体的直接关联进行的单级聚合路径。仅支持设备、资产、客户和租户实体之间的关联。关联路径解析的最大实体数为 {{ max }}。", + "arguments-aggregation": "定义用于过滤和聚合的输入参数。", + "setting-arguments-aggregation": "数据将从聚合路径中配置的关联实体获取。", + "metrics": "定义基于已配置参数进行聚合的指标。", + "entity-aggregation-metrics": "定义基于已配置参数在指定时间间隔内进行聚合的指标。", + "import-invalid-calculated-field-type": "无法导入计算字段:计算字段结构无效。", + "simple-expression-title": "定义计算值计算方式的算术表达式。", + "script-title": "定义计算逻辑和输出值的 TBEL 脚本。", + "simple-arguments": "定义计算值计算方式的算术表达式。", + "script-arguments": "定义脚本可用的输入参数。" + } + }, + "alarm-rule": { + "alarm-rules-tab": "告警规则", + "alarm-rule": "告警规则", + "alarm-rules": "告警规则", + "alarm-rules-old": "旧版", + "alarm-rules-actual": "当前", + "severities": "严重程度", + "cleared": "清除条件", + "delete-title": "确定要删除告警规则“{{title}}”吗?", + "delete-text": "请注意,确认后告警规则及所有相关数据将无法恢复。", + "delete-multiple-title": "确定要删除{ count, plural, =1 {1 条告警规则} other {# 条告警规则} }吗?", + "delete-multiple-text": "请注意,确认后所有选中的告警规则将被移除,所有相关数据将无法恢复。", + "create": "创建新告警规则", + "add": "添加告警规则", + "copy": "复制告警规则配置", + "details": "告警规则详情", + "no-found": "未找到告警规则", + "list": "{ count, plural, =1 {1 条告警规则} other {# 条告警规则列表} }", + "selected-fields": "已选择{ count, plural, =1 {1 条告警规则} other {# 条告警规则} }", + "import": "导入告警规则", + "file": "告警规则文件", + "export": "导出告警规则", + "export-failed-error": "无法导出告警规则:{{error}}", + "entity-type": "实体类型", + "entity-type-required": "实体类型为必填项。", + "alarm-type": "告警类型", + "alarm-type-hint": "在告警发起者(设备、资产等)范围内的唯一标识符(例如 HighTempAlarm),以防止冲突。", + "alarm-type-required": "告警类型为必填项。", + "alarm-type-pattern": "告警类型无效。", + "alarm-type-max-length": "告警类型应少于 256 个字符。", + "clear-alarm": "清除告警", + "value-argument": "参数", + "value-argument-required": "参数为必填项。", + "static-settings": "静态设置", + "configuration": "配置", + "static-schedule": "静态", + "dynamic-schedule": "动态", + "operation-and": "AND", + "operation-or": "OR", + "condition-during": "持续 {{during}}", + "condition-during-dynamic": "持续\"{{ attribute }}\"", + "condition-repeat-times": "重复{ count, plural, =1 {1 次} other {# 次} }", + "condition-repeat-times-dynamic": "重复\"{{ attribute }}\"次", + "filter-preview": "过滤器预览", + "condition-settings": "条件设置", + "static": "静态", + "dynamic": "动态", + "argument-filters": "参数过滤器", + "argument-name": "参数名称", + "value-type": "值类型", + "general": "通用", + "filters": "过滤器", + "date-time-hint": "参数必须为纪元毫秒。示例:1698839340000 等于 2023-11-01 12:49:00 UTC。", + "operation": "操作", + "value-source": "值来源", + "value": "值", + "ignore-case": "忽略大小写", + "condition": "条件", + "script": "脚本", + "add-filter": "添加参数过滤器", + "edit-filter": "参数过滤器", + "remove-filter": "移除参数过滤器", + "no-filter": "至少需要一个过滤器。", + "conditions": { + "simple": "简单", + "duration": "持续时间", + "repeating": "重复" + }, + "schedule-title": "调度", + "edit-schedule": "编辑告警调度", + "schedule-type": "调度器类型", + "schedule-type-required": "调度器类型为必填项。", + "schedule": { + "any-time": "始终激活", + "specific-time": "在特定时间激活", + "custom": "自定义" + }, + "schedule-day": { + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday": "星期六", + "sunday": "星期日" + }, + "schedule-days": "天数", + "schedule-time": "时间", + "schedule-time-from": "从", + "schedule-time-to": "到", + "schedule-days-of-week-required": "至少应选择一个星期中的某天。", + "tbel": "TBEL", + "expression-type": { + "simple": "简单", + "script": "脚本" + }, + "operation-type": { + "and": "与", + "or": "或" + }, + "filter-predicate-type": { + "string": "字符串", + "numeric": "数值", + "boolean": "布尔", + "complex": "复合" + }, + "alarm-rule-additional-info": "附加信息", + "edit-alarm-rule-additional-info": "编辑附加信息", + "alarm-rule-additional-info-placeholder": "请在此处提供您的备注和调整内容,以便在告警详情的附加信息中显示", + "alarm-rule-additional-info-hint": "提示:使用 ${参数名称} 替换告警规则条件中使用的参数值。", + "alarm-rule-additional-info-icon-hint": "使用参数名称替换告警规则条件中使用的参数值。", + "alarm-rule-mobile-dashboard": "移动端仪表板", + "alarm-rule-mobile-dashboard-hint": "移动应用程序用作告警详情仪表板。", + "alarm-rule-no-mobile-dashboard": "未选择仪表板", + "alarm-rule-condition": "告警规则条件", + "enter-alarm-rule-condition-prompt": "添加条件", + "enter-alarm-rule-clear-condition-prompt": "添加清除条件", + "edit-alarm-rule-condition": "告警条件", + "condition-type": "条件类型", + "condition-type-hint": "当过滤器中使用了“缺失超过”操作时,“持续时间”和“重复”选项不可用。", + "select-alarm-severity": "选择告警严重程度", + "add-create-alarm-rule-prompt": "至少需要一个触发条件。", + "add-create-alarm-rule": "添加触发条件", + "add-clear-alarm-rule": "添加清除条件", + "condition-duration": "条件持续时间", + "condition-duration-value": "持续时间值", + "condition-duration-time-unit": "时间单位", + "condition-duration-value-range": "持续时间值应在 1 到 2147483647 的范围内。", + "condition-duration-value-pattern": "持续时间值应为整数。", + "condition-duration-value-required": "持续时间值为必填项。", + "condition-duration-time-unit-required": "时间单位为必填项。", + "condition-repeating-value": "事件次数", + "condition-repeating-value-hint": "告警规则参数的任何更新都将被计为一次事件", + "condition-repeating-value-range": "事件次数应在 1 到 2147483647 的范围内。", + "condition-repeating-value-pattern": "事件次数应为整数。", + "condition-repeating-value-required": "事件次数为必填项。", + "create-conditions": "触发条件", + "clear-condition": "清除条件", + "no-clear-alarm-rule": "未配置清除条件。", + "advanced-settings": "高级设置", + "propagate-alarm": "将告警传播到关联实体", + "alarm-rule-relation-types-list": "关联类型", + "alarm-rule-relation-types-list-hint": "定义关联类型以过滤关联实体。如果未设置,告警将传播到所有关联实体。", + "propagate-alarm-to-owner": "将告警传播到实体所有者(客户或租户)", + "propagate-alarm-to-tenant": "将告警传播到租户", + "alarm-rule-filter-title": "告警规则过滤器", + "filter-title": "过滤器", + "debugging": "告警规则调试", + "any-type": "任意类型", + "enter-alarm-rule-type": "输入告警类型", + "no-alarm-rule-types-matching": "未找到匹配“{{entitySubtype}}”的告警类型。", + "alarm-rule-type-list-empty": "未选择告警类型。", + "alarm-rule-type-list": "告警类型列表", + "alarm-rule-entity-list": "实体列表", + "missing-for": "缺失超过", + "time-unit": "单位", + "mode": "模式", + "type": "类型", + "value-required": "值为必填项。", + "min-value": "值必须大于等于 1。", + "argument-in-use": "参数已被用作通用参数。", + "import-invalid-alarm-rule-type": "无法导入告警规则:告警规则结构无效。", + "no-filter-preview": "未指定过滤器", + "filter-operation": { + "and": "与", + "or": "或" } }, "ai-models": { @@ -1221,6 +1697,8 @@ "documentation": "文档", "time-left": "剩余 {{time}}", "output": "输出", + "sort-asc": "升序", + "sort-desc": "降序", "suffix": { "s": "s", "ms": "ms" @@ -1357,6 +1835,8 @@ "mobile-order": "仪表板在移动应用中的排序", "mobile-hide": "在移动应用中隐藏仪表板", "update-image": "更新仪表板图片", + "update-new-version": "上传新版本", + "upload-file-to-update": "上传文件以更新", "take-screenshot": "截图", "select-widget-title": "选择部件", "select-widget-value": "{{title}}:选择部件", @@ -1725,6 +2205,8 @@ "bootstrap-tab": "Bootstrap 客户端", "bootstrap-server": "Bootstrap 服务器", "lwm2m-server": "LwM2M 服务器", + "client-reboot": "注册更新触发器", + "bootstrap-reboot": "Bootstrap 请求触发器", "client-publicKey-or-id": "客户端公钥或 Id", "client-publicKey-or-id-required": "客户端公钥或 Id 为必填项。", "client-publicKey-or-id-tooltip-psk": "PSK 标识符是最多 128 字节的任意 PSK 标识符,如标准 [RFC7925] 所述。\nPSK 标识符必须先转换为字符串,然后使用 UTF-8 编码为八位字节。", @@ -2225,6 +2707,7 @@ "short-id-required": "服务器短 ID 为必填项。", "short-id-range": "服务器短 ID 应在 {{ min }} 到 {{ max }} 的范围内。", "short-id-pattern": "服务器短 ID 必须为正整数。", + "short-id-pattern-bs": "服务器短 ID 必须仅为 null", "lifetime": "客户端注册生命周期", "lifetime-required": "客户端注册生命周期为必填项。", "lifetime-pattern": "客户端注册生命周期必须为正整数。", @@ -2319,7 +2802,9 @@ "composite-all-description": "所有资源使用单个 Composite Observe 请求进行观察(更高效,灵活性较低)", "composite-by-object": "按对象组合", "composite-by-object-description": "资源按对象类型分组,使用单独的 Composite Observe 请求进行观察(平衡方案)" - } + }, + "init-attr-tel-as-obs-strategy": "使用观察策略初始化属性和遥测", + "init-attr-tel-as-obs-strategy-hint": "如果为 false,属性和遥测通过逐一读取其值来初始化。\\n如果为 true,属性和遥测通过使用观察策略订阅其值来初始化。" }, "snmp": { "add-communication-config": "添加通信配置", @@ -2635,6 +3120,8 @@ "type-rulenodes": "规则节点", "list-of-rulenodes": "{ count, plural, =1 {1 个规则节点} other {# 个规则节点列表} }", "rulenode-name-starts-with": "名称以 '{{prefix}}' 开头的规则节点", + "type-api-key": "API 密钥", + "type-api-keys": "API 密钥", "type-current-customer": "当前客户", "type-current-tenant": "当前租户", "type-current-user": "当前用户", @@ -2656,6 +3143,7 @@ "details": "实体详情", "no-entities-prompt": "未找到实体", "no-data": "无数据可显示", + "show-all-columns": "显示全部", "columns-to-display": "显示列", "type-api-usage-state": "API 使用状态", "type-edge": "Edge", @@ -2701,7 +3189,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {1 个移动应用} other {# 个移动应用列表} }", "type-mobile-app-bundle": "移动包", "type-mobile-app-bundles": "移动包", - "list-of-mobile-app-bundles": "{ count, plural, =1 {1 个移动包} other {# 个移动包列表} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {1 个移动包} other {# 个移动包列表} }", + "limit-reached": "已达上限", + "limit-reached-text": "您已达到 {{ entities }} 的上限。要添加更多,请联系系统管理员增加您的 {{ entity }} 上限。", + "request-limit-increase": "请求增加上限", + "request-sysadmin-text": "您是系统管理员吗?", + "login-here": "在此登录", + "to-increase-limit": "以增加上限。", + "increase-limit-request-sent-title": "我们已向您的系统管理员发送了自动请求以增加上限", + "increase-limit-request-sent-text": "请等待他们审核请求并更新设置。您可能需要刷新此页面以查看更改。" }, "entity-field": { "created-time": "创建时间", @@ -3789,6 +4285,8 @@ "password-link-sent-message": "重置链接已发送", "email": "Email", "invalid-email-format": "无效的 Email 格式。", + "sign-in-with": "使用 {{name}} 登录", + "sign-in-to-your-account": "登录您的账户", "or": "或", "error": "登录错误", "verify-your-identity": "验证您的身份", @@ -3807,7 +4305,51 @@ "activation-link-expired": "激活链接已过期", "activation-link-expired-message": "激活您个人资料的链接已过期。您可以返回登录页面以接收新的 Email。", "reset-password-link-expired": "密码重置链接已过期", - "reset-password-link-expired-message": "重置密码的链接已过期。您可以返回登录页面以接收新的 Email。" + "reset-password-link-expired-message": "重置密码的链接已过期。您可以返回登录页面以接收新的 Email。", + "two-fa": "双因素认证", + "two-fa-required": "需要双因素认证", + "set-up-verification-method": "设置验证方式以继续", + "set-up-verification-method-login": "设置验证方式或登录", + "enable-authenticator-app": "启用身份验证器应用", + "enable-authenticator-app-description": "请输入您的身份验证器应用中的安全码", + "enable-authenticator-sms": "启用短信验证", + "enable-authenticator-sms-description": "请输入我们刚发送到以下号码的 6 位验证码:", + "enable-authenticator-email": "启用 Email 验证", + "enable-authenticator-email-description": "安全码已发送到您的 Email 地址:", + "enter-key-manually": "或手动输入此 32 位密钥:", + "continue": "继续", + "confirm": "确认", + "authenticator-app-success": "身份验证器应用已成功启用", + "authenticator-app-success-description": "下次登录时,您需要提供双因素认证验证码", + "authenticator-sms-success": "短信验证已成功启用", + "authenticator-sms-success-description": "下次登录时,系统将提示您输入发送到该手机号码的安全码", + "authenticator-email-success": "Email 验证已成功启用", + "authenticator-email-success-description": "下次登录时,系统将提示您输入发送到您 Email 地址的安全码", + "authenticator-backup-code-success": "备用验证码已成功启用", + "authenticator-backup-code-success-description": "下次登录时,系统将提示您输入安全码或使用备用验证码之一。", + "add-verification-method": "添加验证方式", + "get-backup-code": "获取备用验证码", + "copy-key": "复制密钥", + "send-code": "发送验证码", + "email-label": "Email", + "email-description": "输入用于身份验证的 Email。", + "sms-description": "输入用于身份验证的手机号码。", + "backup-code-description": "打印这些验证码,以便在需要登录账户时随时使用。每个备用验证码只能使用一次。", + "backup-code-warn": "离开此页面后,这些验证码将无法再次显示。请使用以下选项安全保存。", + "download-txt": "下载(txt)", + "print": "打印", + "verification-code": "6 位验证码", + "verification-code-invalid": "无效的验证码格式", + "verification-code-incorrect": "验证码不正确", + "verification-code-many-request": "验证码校验请求过多", + "scan-qr-code": "使用您的验证应用扫描此二维码", + "phone-input": { + "phone-input-label": "手机号码", + "phone-input-required": "手机号码为必填项", + "phone-input-validation": "手机号码无效或不可用", + "phone-input-pattern": "无效的手机号码。应为 E.164 格式,例如 {{phoneNumber}}", + "phone-input-hint": "E.164 格式的手机号码,例如 {{phoneNumber}}" + } }, "mobile": { "add-application": "添加应用程序", @@ -4175,6 +4717,7 @@ "api-usage-limit": "API 使用限制", "device-activity": "设备活跃", "entities-limit": "实体限制", + "entities-limit-increase-request": "实体限制提升请求", "entity-action": "实体操作", "general": "通用", "rule-engine-lifecycle-event": "规则引擎生命周期事件", @@ -4392,6 +4935,12 @@ "at-least": "至少:", "character": "{ count, plural, =1 {1 个字符} other {# 个字符} }", "digit": "{ count, plural, =1 {1 个数字} other {# 个数字} }", + "password-tooltip-min-length": "至少 {{minimumLength}} 个字符", + "password-tooltip-max-length": "最多 {{maximumLength}} 个字符", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}} 个大写字符", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}} 个小写字符", + "password-tooltip-digit": "{{minimumDigits}} 个数字", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}} 个特殊字符", "incorrect-password-try-again": "密码不正确,请重试", "lowercase-letter": "{ count, plural, =1 {1 个小写字母} other {# 个小写字母} }", "new-passwords-not-match": "新密码不匹配", @@ -4450,7 +4999,8 @@ "additional-info": "附加信息(JSON)", "invalid-additional-info": "无法解析附加信息 JSON。", "no-relations-text": "未找到关联", - "not": "非" + "not": "非", + "copy-type": "复制类型" }, "resource": { "add": "添加资源", @@ -5400,7 +5950,8 @@ }, "time-series": "时间序列", "latest": "最新值", - "web-sockets": "WebSockets" + "web-sockets": "WebSockets", + "calculated-fields-and-alarm-rules": "计算字段和告警规则" }, "save-attribute": { "processing-settings": "处理设置", @@ -5595,7 +6146,8 @@ "bad-request-params": "请求参数错误", "item-not-found": "未找到该项", "too-many-requests": "请求过多", - "too-many-updates": "更新过多" + "too-many-updates": "更新过多", + "entities-limit-exceeded": "实体数量超出限制" }, "tenant": { "tenant": "租户", @@ -5733,6 +6285,27 @@ "max-arguments-per-cf": "每个计算字段的最大参数数量", "max-arguments-per-cf-range": "每个计算字段的最大参数数量不能为负数", "max-arguments-per-cf-required": "每个计算字段的最大参数数量为必填项", + "max-related-level-per-argument": "“关联实体”参数的最大关联层级", + "max-related-level-per-argument-range": "“关联实体”参数的最大关联层级不能小于“1”", + "max-related-level-per-argument-required": "“关联实体”参数的最大关联层级为必填项", + "min-allowed-scheduled-update-interval": "“关联实体”参数的最小允许更新间隔(秒)", + "min-allowed-scheduled-update-interval-range": "最小允许更新间隔不能为负数", + "min-allowed-deduplication-interval": "最小允许去重间隔(秒)", + "min-allowed-deduplication-interval-range": "最小允许去重间隔不能为负数", + "min-allowed-deduplication-interval-required": "最小允许去重间隔为必填项", + "intermediate-aggregation-interval": "中间聚合间隔(秒)", + "intermediate-aggregation-interval-range": "中间聚合间隔不能小于“1”", + "intermediate-aggregation-interval-required": "中间聚合间隔为必填项", + "reevaluation-check-interval": "重新评估检查间隔(秒)", + "reevaluation-check-interval-range": "重新评估检查间隔不能小于“1”", + "reevaluation-check-interval-required": "重新评估检查间隔为必填项", + "alarms-reevaluation-interval": "告警重新评估间隔(秒)", + "alarms-reevaluation-interval-range": "告警重新评估间隔不能小于“1”", + "alarms-reevaluation-interval-required": "告警重新评估间隔为必填项", + "min-allowed-aggregation-interval": "最小允许聚合间隔(秒)", + "min-allowed-aggregation-interval-range": "最小允许聚合间隔不能为负数", + "min-allowed-aggregation-interval-required": "最小允许聚合间隔为必填项", + "min-allowed-scheduled-update-interval-required": "最小允许更新间隔为必填项", "max-state-size": "状态最大大小(KB)", "max-state-size-range": "状态最大大小(KB)不能为负数", "max-state-size-required": "状态最大大小(KB)为必填项", @@ -5808,6 +6381,10 @@ "ws-limit-max-subscriptions-per-regular-user": "每个普通用户的最大订阅数", "ws-limit-max-subscriptions-per-public-user": "每个公共用户的最大订阅数", "ws-limit-updates-per-session": "每个会话的 WS 更新", + "relation-search-entity-limit": "关联搜索实体限制", + "relation-search-entity-limit-hint": "限制关联路径最后一级解析的实体数量。适用于“关联实体”参数和传播字段。", + "relation-search-entity-limit-required": "关联搜索实体限制", + "relation-search-entity-limit-range": "关联搜索实体限制不能小于“1”", "rate-limits": { "add-limit": "添加限制", "and-also-less-than": "且小于", @@ -5993,7 +6570,9 @@ "default-agg-interval": "默认分组间隔", "edit-intervals-list-hint": "可以指定可用间隔选项列表。", "edit-grouping-intervals-list-hint": "可以配置分组间隔列表和默认分组间隔。", - "all": "全部" + "all": "全部", + "save-current-settings-as-default": "将当前设置保存为默认时间窗口", + "hide-option-from-end-users": "对终端用户隐藏选项" }, "tooltip": { "trigger": "触发器", @@ -6648,6 +7227,7 @@ "export-attributes": "导出属性", "export-credentials": "导出凭证", "export-calculated-fields": "导出计算字段\n及告警规则", + "export-alarm-rules": "导出告警规则", "entity-versions": "实体版本", "versions": "版本", "created-time": "创建时间", @@ -6665,6 +7245,7 @@ "load-attributes": "加载属性", "load-credentials": "加载凭证", "load-calculated-fields": "加载计算字段和告警规则", + "load-alarm-rules": "加载告警规则", "compare-with-current": "与当前版本比较", "diff-entity-with-version": "与实体版本“{{versionName}}”比较差异", "previous-difference": "上一个差异", @@ -6874,7 +7455,23 @@ "scan-qr-code": "扫描二维码", "make-phone-call": "拨打电话", "get-location": "获取手机位置", - "take-screenshot": "截屏" + "take-screenshot": "截屏", + "handle-provision-success-function": "处理配网成功函数", + "get-location-function": "获取位置函数", + "process-launch-result-function": "处理启动结果函数", + "get-phone-number-function": "获取电话号码函数", + "process-image-function": "处理图片函数", + "process-qr-code-function": "处理二维码函数", + "process-location-function": "处理位置函数", + "handle-empty-result-function": "处理空结果函数", + "handle-error-function": "处理错误函数", + "handle-non-mobile-fallback-function": "处理非移动端回退函数", + "save-to-gallery": "保存到'图片库'", + "provision-type": "配网类型", + "auto": "自动", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "自定义操作函数", "custom-pretty-function": "自定义操作(带 HTML 模板)函数", @@ -6883,7 +7480,8 @@ "marker": "标记", "polygon": "多边形", "rectangle": "矩形", - "circle": "圆形" + "circle": "圆形", + "polyline": "折线" }, "place-map-item": "放置地图项目", "map-item-tooltip": { @@ -6895,7 +7493,9 @@ "continue-draw-polygon": "继续绘制多边形", "finish-draw-polygon": "完成绘制多边形", "start-draw-circle": "开始绘制圆形", - "finish-draw-circle": "完成绘制圆形" + "finish-draw-circle": "完成绘制圆形", + "start-draw-polyline": "开始绘制折线", + "finish-draw-polyline": "完成绘制折线" } }, "widgets-bundle": { @@ -7461,6 +8061,14 @@ "update-animation-delay": "更新动画延迟" }, "chart-axis": { + "limit": "限制", + "source": "来源", + "key-value": "键 / 值", + "value-required": "值为必填项。", + "entity-key-required": "实体键为必填项。", + "key-required": "键为必填项。", + "scale-limits": "刻度限制", + "scale-appearance": "刻度外观", "scale": "刻度", "scale-min": "最小", "scale-max": "最大", @@ -8005,7 +8613,10 @@ "add-radio-option": "添加单选选项", "radio-label-position": "标签位置", "radio-label-position-before": "之前", - "radio-label-position-after": "之后" + "radio-label-position-after": "之后", + "save-image": "保存图片", + "save-to-gallery": "自动将拍摄的图片存储到图片库", + "public-image": "使图片对所有未授权用户可用" }, "invalid-qr-code-text": "二维码的输入文本无效。输入应为字符串类型", "qr-code": { @@ -8300,7 +8911,8 @@ "trips": "行程", "markers": "标记", "polygons": "多边形", - "circles": "圆形" + "circles": "圆形", + "polylines": "折线" }, "data-layer": { "source": "来源", @@ -8497,6 +9109,25 @@ "finish-circle-hint-with-entity": "“{{entityName}}”的圆形:点击以完成并保存", "finish-circle-hint": "圆形:点击以完成绘制" }, + "polyline": { + "polyline-key": "折线键", + "polyline-key-required": "折线键为必填项", + "no-polylines": "未配置折线", + "add-polylines": "添加折线", + "polyline-configuration": "折线配置", + "remove-polyline": "移除折线", + "edit": "编辑折线", + "cut": "裁剪折线区域", + "rotate": "旋转折线", + "remove-polyline-for": "移除“{{entityName}}”的折线", + "draw-polyline": "绘制折线", + "polyline-place-first-point-hint-with-entity": "“{{entityName}}”的折线:点击以放置第一个点", + "polyline-place-first-point-hint": "折线:点击以放置第一个点", + "finish-polyline-hint-with-entity": "“{{entityName}}”的折线:点击以完成绘制", + "finish-polyline-hint": "折线:点击以完成绘制", + "polyline-place-first-point-cut-hint": "点击以放置第一个点", + "finish-polyline-cut-hint": "点击第一个标记以完成并保存" + }, "select-entity": "选择实体", "select-entity-hint": "提示:选择后点击地图以设置位置" }, @@ -8938,6 +9569,7 @@ "show-empty-space-hidden-action": "显示空白代替隐藏的单元格按钮操作", "dont-reserve-space-hidden-action": "不为隐藏的操作按钮预留空间", "display-timestamp": "时间戳", + "timestamp-column-name": "时间戳", "display-pagination": "显示分页", "default-page-size": "默认页面大小", "page-step-settings": "分页步长设置", @@ -8999,7 +9631,9 @@ "alarm-column-error": "至少应指定一个告警列", "table-tabs": "表格标签页", "show-cell-actions-menu-mobile": "在移动模式下显示单元格操作下拉菜单", - "disable-sorting": "禁用排序" + "disable-sorting": "禁用排序", + "sort-by": "标签页排序方式", + "sort-timestamp-option": "创建时间" }, "latest-chart": { "total": "总计", @@ -9491,11 +10125,28 @@ "content": "

    通过创建终端用户仪表板,客户用户只能查看自己的设备,其他客户的数据将被隐藏。

    请参考以下文档了解操作方法:

    " } } + }, + "api-usage": { + "api-usage": "API 使用情况", + "label": "标签", + "state-name": "状态名称", + "status": "状态", + "status-required": "状态为必填项。", + "limit": "最大限制", + "limit-required": "最大限制为必填项。", + "current-number": "当前数量", + "current-number-required": "当前数量为必填项。", + "add-key": "添加键", + "no-key": "无键", + "delete-key": "删除键", + "target-dashboard-state": "目标仪表板状态", + "go-to-main-state": "前往默认视图" } }, "icon": { "icon": "图标", "icons": "图标", + "custom": "自定义", "select-icon": "选择图标", "material-icons": "Material 图标", "show-all": "显示全部图标", @@ -9536,6 +10187,7 @@ "items-per-page-separator": "/" }, "language": { + "auto": "自动", "language": "语言" } } \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index 7b0575f739..08fc40e4b1 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -2404,7 +2404,6 @@ "two-factor-authentication": "雙因素身份驗證", "passwords-mismatch-error": "輸入的密碼必須相同!", "password-again": "再次輸入密碼", - "sign-in": "登入 ", "username": "用戶名(電子郵件)", "remember-me": "記住我", "forgot-password": "忘記密碼?", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index a694e04a65..bb5a76b6e9 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -107,6 +107,9 @@ } } } + &.disabled { + color: var(--mat-form-field-outlined-disabled-input-text-color); + } .mat-expansion-panel { &.tb-settings { box-shadow: none; @@ -160,6 +163,13 @@ } } + .tb-form-panel-disabled { + color: var(--mat-form-field-outlined-disabled-input-text-color); + .tb-form-panel { + color: var(--mat-form-field-outlined-disabled-input-text-color); + } + } + .tb-form-panel-title { font-weight: 500; font-size: 16px; @@ -168,10 +178,11 @@ } &.tb-required::after { - font-size: 13px; - color: rgba(0, 0, 0, .54); + font-size: inherit; + color: inherit; vertical-align: top; - content: " *"; + content: "*"; + margin-left: 1px; } } .tb-form-panel-hint { @@ -257,6 +268,12 @@ flex: 1; } } + &.flex-lt-lg { + @media #{$mat-lt-lg} { + width: auto; + flex: 1; + } + } } .fixed-title-width { min-width: 200px; @@ -282,10 +299,11 @@ margin: 8px 0; } .tb-required::after { - font-size: 13px; - color: rgba(0, 0, 0, .54); + font-size: inherit; + color: inherit; vertical-align: top; - content: " *"; + content: "*"; + margin-left: 1px; } } @@ -422,10 +440,13 @@ } } .mat-mdc-form-field-infix { + --mat-form-field-container-vertical-padding: 8px; padding-top: 8px; padding-bottom: 8px; min-height: 40px; width: auto; + font-weight: 400; + font-size: 14px; .mdc-text-field__input, .mat-mdc-select { font-weight: 400; font-size: 14px; diff --git a/ui-ngx/src/scss/constants.scss b/ui-ngx/src/scss/constants.scss index aca02be591..922e7b3290 100644 --- a/ui-ngx/src/scss/constants.scss +++ b/ui-ngx/src/scss/constants.scss @@ -36,4 +36,4 @@ $tb-secondary-color: #527dad; $tb-hue3-color: #a7c1de; $tb-dark-primary-color: #9fa8da; - +$tb-primary-color-light: #7986cb; diff --git a/ui-ngx/src/scss/mixins.scss b/ui-ngx/src/scss/mixins.scss index 054c63ecf7..95a28784e9 100644 --- a/ui-ngx/src/scss/mixins.scss +++ b/ui-ngx/src/scss/mixins.scss @@ -26,6 +26,10 @@ width: #{$size}px; height: #{$size}px; } + img { + width: #{$size}px; + height: #{$size}px; + } } @mixin tb-mat-icon-button-size($size) { diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 9058ce4960..93422abfde 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -636,6 +636,43 @@ pre.tb-highlight { } } + .tb-autofilled:has(input:-webkit-autofill){ + --mat-form-field-outlined-label-text-color: var(--mat-form-field-outlined-input-text-color); + --mat-form-field-outlined-hover-label-text-color: var(--mat-form-field-outlined-input-text-color); + .mat-mdc-form-field-flex { + &:before { + display: block; + height: auto; + content: ""; + position: absolute; + inset: 0; + background: #7986cb; + opacity: 0.5; + pointer-events: none; + border-radius: 4px; + } + } + + .mdc-floating-label { + --mat-mdc-form-field-label-transform: translateY(calc(calc(6.7px + var(--mat-form-field-container-height) / 2) * -1)) scale(var(--mat-mdc-form-field-floating-label-scale, 0.75)); + } + .mat-mdc-form-field-infix { + input { + &:-webkit-autofill, + &:-webkit-autofill:hover, + &:-webkit-autofill:focus, + &:-webkit-autofill:active { + -webkit-background-clip: text !important; + -webkit-text-fill-color: var(--mat-form-field-outlined-input-text-color); + } + &::spelling-error { + color: var(--mat-form-field-outlined-input-text-color); + text-decoration: none; + } + } + } + } + // Material table .mat-toolbar.mat-primary { @@ -692,7 +729,7 @@ pre.tb-highlight { mat-toolbar.mat-mdc-table-toolbar:not(.mat-primary), .mat-mdc-cell, .mat-expansion-panel-header, mat-card-header.mat-mdc-card-header { button.mat-mdc-icon-button { .mat-icon { - color: rgba(0, 0, 0, .54); + color:var(--mat-icon-color, rgba(0, 0, 0, .54)); } &[disabled][disabled] { .mat-icon { @@ -760,11 +797,11 @@ pre.tb-highlight { transition: background-color .2s; &:hover:not(.tb-current-entity) { - background-color: #f4f4f4; + background-color: var(--tb-hover-color, #f4f4f4); } &.tb-current-entity { - background-color: #e9e9e9; + background-color: var(--tb-current-entity-color, #e9e9e9); } &.tb-pointer { @@ -816,7 +853,7 @@ pre.tb-highlight { vertical-align: middle; border-width: 0; border-bottom-width: 1px; - border-bottom-color: rgba(0, 0, 0, 0.12); + border-bottom-color: var(--mat-table-row-item-outline-color,rgba(0, 0, 0, 0.12)); border-style: solid; text-overflow: ellipsis; touch-action: auto !important; @@ -896,7 +933,7 @@ pre.tb-highlight { } .mat-icon { - svg { + svg, img { vertical-align: inherit; } &.tb-mat-12 { diff --git a/ui-ngx/src/theme.scss b/ui-ngx/src/theme.scss index 653368ebbc..cb6e47a637 100644 --- a/ui-ngx/src/theme.scss +++ b/ui-ngx/src/theme.scss @@ -27,7 +27,7 @@ $tb-mat-indigo: ( 50: #e8eaf6, 100: #c5cae9, 200: #9fa8da, - 300: #7986cb, + 300: $tb-primary-color-light, 400: #5c6bc0, 500: $tb-primary-color, 600: $tb-secondary-color, diff --git a/ui-ngx/structure.md b/ui-ngx/structure.md new file mode 100644 index 0000000000..23d85b5f56 --- /dev/null +++ b/ui-ngx/structure.md @@ -0,0 +1,746 @@ +# ThingsBoard CE Frontend — Navigation & Testing Guide + +## 1. Quick Reference + +| Key | Value | +|-----|-------| +| Angular | 20 | +| Angular Material | 20 | +| Component prefix | `tb` | +| TypeScript | 5.9 | +| RxJS | 7.8 | + +**Path Aliases** (from `tsconfig.json`): + +| Alias | Maps to | +|-------|---------| +| `@app/*` | `src/app/*` | +| `@env/*` | `src/environments/*` | +| `@core/*` | `src/app/core/*` | +| `@modules/*` | `src/app/modules/*` | +| `@home/*` | `src/app/modules/home/*` | +| `@shared/*` | `src/app/shared/*` | + +**Key Directories:** + +``` +src/app/ +├── core/ +│ ├── http/ # HTTP services (one per entity type) +│ └── services/ # Menu, auth, utils +├── modules/ +│ ├── home/ +│ │ ├── components/ # Reusable home components (entity table, details panel, alarm, dashboard) +│ │ │ └── entity/ # EntitiesTableComponent, EntityDetailsPageComponent, EntityComponent base +│ │ └── pages/ # Page modules (one directory per page/feature) +│ └── login/ # Login, reset password, 2FA +└── shared/ + ├── components/ # Shared components (autocompletes, dialogs, time, image) + └── models/ # TypeScript interfaces/enums +``` + +--- + +## 2. Route-to-Source Mapping + +All page source paths are relative to `src/app/modules/home/pages/`. +Page types: `entity-table` = EntitiesTableComponent + resolver, `entity-details` = EntityDetailsPageComponent, `settings-form` = standalone form with ConfirmOnExitGuard, `dashboard-view` = DashboardViewComponent with resolver, `custom` = unique component. + +### Login (unauthenticated) + +Source paths below are relative to `src/app/modules/` (not `home/pages/`). + +| URL | Source | Type | +|-----|--------|------| +| `/login` | `login/pages/login/` | custom (LoginComponent) | +| `/login/resetPasswordRequest` | `login/pages/login/` | custom (ResetPasswordRequestComponent) | +| `/login/resetPassword` | `login/pages/login/` | custom (ResetPasswordComponent) | +| `/login/createPassword` | `login/pages/login/` | custom (CreatePasswordComponent) | +| `/login/mfa` | `login/pages/login/` | custom (TwoFactorAuthLoginComponent) | + +### Home + +| URL | Source | Type | Auth | +|-----|--------|------|------| +| `/home` | `home-links/` | custom (HomeLinksComponent + dashboard resolver) | ALL | + +### Tenants (SYS_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/tenants` | `tenant/` | entity-table | `TenantsTableConfigResolver` | +| `/tenants/:entityId` | `tenant/` | entity-details | `TenantsTableConfigResolver` | +| `/tenants/:tenantId/users` | `tenant/` + `user/` | entity-table | `UsersTableConfigResolver` | +| `/tenantProfiles` | `tenant-profile/` | entity-table | `TenantProfilesTableConfigResolver` | +| `/tenantProfiles/:entityId` | `tenant-profile/` | entity-details | `TenantProfilesTableConfigResolver` | + +### Alarms Center (TENANT_ADMIN, CUSTOMER_USER) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/alarms` | `alarm/` | custom (RouterTabsComponent) | — | +| `/alarms/alarms` | `alarm/` | custom (AlarmTableComponent) | — | +| `/alarms/alarm-rules` | `alarm/` | custom (AlarmRulesTableComponent) | — | +| `/alarms/alarm-rules/:entityId` | `alarm/` | entity-details | `AlarmRulesTableConfigResolver` | + +### Dashboards (TENANT_ADMIN, CUSTOMER_USER) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/dashboards` | `dashboard/` | entity-table | `DashboardsTableConfigResolver` | +| `/dashboards/:dashboardId` | `dashboard/` | custom (DashboardPageComponent) | `DashboardResolver` | + +### Entities (TENANT_ADMIN, CUSTOMER_USER) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/entities/devices` | `device/` | entity-table | `DevicesTableConfigResolver` | +| `/entities/devices/:entityId` | `device/` | entity-details | `DevicesTableConfigResolver` | +| `/entities/assets` | `asset/` | entity-table | `AssetsTableConfigResolver` | +| `/entities/assets/:entityId` | `asset/` | entity-details | `AssetsTableConfigResolver` | +| `/entities/entityViews` | `entity-view/` | entity-table | `EntityViewsTableConfigResolver` | +| `/entities/entityViews/:entityId` | `entity-view/` | entity-details | `EntityViewsTableConfigResolver` | +| `/entities/gateways` | `gateways/` | dashboard-view | `gatewaysDashboardResolver` | + +### Profiles (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/profiles/deviceProfiles` | `device-profile/` | entity-table | `DeviceProfilesTableConfigResolver` | +| `/profiles/deviceProfiles/:entityId` | `device-profile/` | entity-details | `DeviceProfilesTableConfigResolver` | +| `/profiles/assetProfiles` | `asset-profile/` | entity-table | `AssetProfilesTableConfigResolver` | +| `/profiles/assetProfiles/:entityId` | `asset-profile/` | entity-details | `AssetProfilesTableConfigResolver` | + +### Customers (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/customers` | `customer/` | entity-table | `CustomersTableConfigResolver` | +| `/customers/:entityId` | `customer/` | entity-details | `CustomersTableConfigResolver` | +| `/customers/:customerId/users` | `customer/` + `user/` | entity-table | `UsersTableConfigResolver` | +| `/customers/:customerId/devices` | `customer/` + `device/` | entity-table | `DevicesTableConfigResolver` | +| `/customers/:customerId/assets` | `customer/` + `asset/` | entity-table | `AssetsTableConfigResolver` | +| `/customers/:customerId/dashboards` | `customer/` + `dashboard/` | entity-table | `DashboardsTableConfigResolver` | +| `/customers/:customerId/edgeInstances` | `customer/` + `edge/` | entity-table | `EdgesTableConfigResolver` | + +### Calculated Fields (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/calculatedFields` | `calculated-fields/` | custom (CalculatedFieldsTableComponent) | — | +| `/calculatedFields/:entityId` | `calculated-fields/` | entity-details | `CalculatedFieldsTableConfigResolver` | + +### Rule Chains (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/ruleChains` | `rulechain/` | entity-table | `RuleChainsTableConfigResolver` | +| `/ruleChains/:ruleChainId` | `rulechain/` | custom (RuleChainPageComponent, lazy) | `RuleChainResolver` + others | + +### Edge Management (TENANT_ADMIN, conditional on edgesSupportEnabled) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/edgeManagement/instances` | `edge/` | entity-table | `EdgesTableConfigResolver` | +| `/edgeManagement/instances/:entityId` | `edge/` | entity-details | `EdgesTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/assets` | `edge/` + `asset/` | entity-table | `AssetsTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/devices` | `edge/` + `device/` | entity-table | `DevicesTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/entityViews` | `edge/` + `entity-view/` | entity-table | `EntityViewsTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/dashboards` | `edge/` + `dashboard/` | entity-table | `DashboardsTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/ruleChains` | `edge/` + `rulechain/` | entity-table | `RuleChainsTableConfigResolver` | +| `/edgeManagement/ruleChains` | `edge/` + `rulechain/` | entity-table | `RuleChainsTableConfigResolver` | + +### Advanced Features (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/features/otaUpdates` | `ota-update/` | entity-table | `OtaUpdateTableConfigResolve` | +| `/features/otaUpdates/:entityId` | `ota-update/` | entity-details | `OtaUpdateTableConfigResolve` | +| `/features/vc` | `vc/` | custom (VersionControlComponent) | — | + +### Resources (SYS_ADMIN, TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/resources/widgets-library/widget-types` | `widget/` | entity-table | `WidgetTypesTableConfigResolver` | +| `/resources/widgets-library/widgets-bundles` | `widget/` | entity-table | `WidgetsBundlesTableConfigResolver` | +| `/resources/widgets-library/widgets-bundles/:widgetsBundleId` | `widget/` | custom (WidgetsBundleWidgetsComponent) | — | +| `/resources/widgets-library/widget-types/:widgetTypeId` | `widget/` | custom (WidgetEditorComponent) | — | +| `/resources/images` | `admin/` | custom (ImageGalleryComponent) | — | +| `/resources/scada-symbols` | `admin/` | custom (ImageGalleryComponent) | — | +| `/resources/scada-symbols/:type/:key` | `scada-symbol/` | custom (ScadaSymbolComponent) | — | +| `/resources/resources-library` | `admin/` | entity-table | `ResourcesLibraryTableConfigResolver` | +| `/resources/resources-library/:entityId` | `admin/` | entity-details | `ResourcesLibraryTableConfigResolver` | +| `/resources/javascript-library` | `admin/` | entity-table | `JsLibraryTableConfigResolver` | +| `/resources/javascript-library/:entityId` | `admin/` | entity-details | `JsLibraryTableConfigResolver` | + +### Notifications (ALL authorities) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/notification/inbox` | `notification/inbox/` | entity-table | `InboxTableConfigResolver` | +| `/notification/sent` | `notification/sent/` | entity-table | `SentTableConfigResolver` | +| `/notification/recipients` | `notification/recipient/` | entity-table | `RecipientTableConfigResolver` | +| `/notification/templates` | `notification/template/` | entity-table | `TemplateTableConfigResolver` | +| `/notification/rules` | `notification/rule/` | entity-table | `RuleTableConfigResolver` | + +### Mobile Center (SYS_ADMIN, TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/mobile-center/bundles` | `mobile/bundes/` | entity-table | `MobileBundleTableConfigResolver` | +| `/mobile-center/applications` | `mobile/applications/` | entity-table | `MobileAppTableConfigResolver` | +| `/mobile-center/applications/:entityId` | `mobile/applications/` | entity-details | `MobileAppTableConfigResolver` | +| `/mobile-center/qr-code-widget` | `mobile/qr-code-widget/` | settings-form | — | + +### Settings — SYS_ADMIN + +| URL | Source | Type | +|-----|--------|------| +| `/settings/general` | `admin/` | settings-form (GeneralSettingsComponent) | +| `/settings/outgoing-mail` | `admin/` | settings-form (MailServerComponent) | +| `/settings/notifications` | `admin/` | settings-form (SmsProviderComponent) | +| `/settings/queues` | `admin/` | entity-table (`QueuesTableConfigResolver`) | + +### Settings — TENANT_ADMIN + +| URL | Source | Type | +|-----|--------|------| +| `/settings/home` | `admin/` | settings-form (HomeSettingsComponent) | +| `/settings/notifications` | `admin/` | settings-form (SmsProviderComponent) | +| `/settings/repository` | `admin/` | settings-form (RepositoryAdminSettingsComponent) | +| `/settings/auto-commit` | `admin/` | settings-form (AutoCommitAdminSettingsComponent) | +| `/settings/trendz` | `admin/` | settings-form (TrendzSettingsComponent) | +| `/settings/ai-models` | `ai-model/` | entity-table (`AiModelsTableConfigResolver`) | + +### Security Settings (SYS_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/security-settings/general` | `admin/` | settings-form (SecuritySettingsComponent) | — | +| `/security-settings/2fa` | `admin/` | settings-form (TwoFactorAuthSettingsComponent) | — | +| `/security-settings/oauth2/domains` | `admin/` | entity-table | `DomainTableConfigResolver` | +| `/security-settings/oauth2/clients` | `admin/` | entity-table | `ClientsTableConfigResolver` | +| `/security-settings/auditLogs` | `admin/` | custom (audit log table) | — | + +### Account (ALL authorities) + +| URL | Source | Type | +|-----|--------|------| +| `/account/profile` | `profile/` | settings-form (ProfileComponent) | +| `/account/security` | `security/` | settings-form (SecurityComponent) | + +### API Usage (TENANT_ADMIN) + +| URL | Source | Type | +|-----|--------|------| +| `/usage` | `api-usage/` | dashboard-view (loads `api_usage.json`) | + +--- + +## 3. Entity Table Pattern + +Most pages follow a 3-file pattern per entity type inside `pages//`: + +``` +-table-config.resolver.ts — Column definitions, actions, component wiring +.component.ts + .html — Entity detail form (extends EntityComponent) +-tabs.component.ts + .html — Additional detail tabs (relations, attributes, events, etc.) +``` + +**Framework files** (in `home/components/entity/`): +- `entities-table.component.ts` — The generic table component. Renders columns, actions, pagination from config. +- `entity-details-page.component.ts` — Full-page entity details with tabs. +- `entity-details-panel.component.ts` — Slide-out drawer panel for entity details. +- `entity.component.ts` — Abstract base directive all entity forms extend. +- `entities-table-config.models.ts` — TypeScript interfaces for table configuration. + +**Resolver structure** — each resolver returns an `EntityTableConfig` with: +- `entityType` — enum value (e.g., `EntityType.DEVICE`) +- `columns` — array of column definitions (header key, cell property/template, width) +- `cellActionDescriptors` — per-row action buttons +- `groupActionDescriptors` — bulk-selection action buttons +- `headerActionDescriptors` — toolbar action buttons (add, import) +- `entityComponent` — reference to the detail form component class +- `entityTabsComponent` — reference to the tabs component class +- `addDialogComponent` — optional dialog for add flow (e.g., `DeviceWizardDialogComponent`) + +### All Table Config Resolvers + +| Resolver | Entity Type | Source Dir | Entity Component | Tabs Component | +|----------|-------------|-----------|------------------|----------------| +| `TenantsTableConfigResolver` | TENANT | `tenant/` | `TenantComponent` | `TenantTabsComponent` | +| `TenantProfilesTableConfigResolver` | TENANT_PROFILE | `tenant-profile/` | `TenantProfileComponent` | `TenantProfileTabsComponent` | +| `DevicesTableConfigResolver` | DEVICE | `device/` | `DeviceComponent` | `DeviceTabsComponent` | +| `DeviceProfilesTableConfigResolver` | DEVICE_PROFILE | `device-profile/` | `DeviceProfileComponent` | `DeviceProfileTabsComponent` | +| `AssetsTableConfigResolver` | ASSET | `asset/` | `AssetComponent` | `AssetTabsComponent` | +| `AssetProfilesTableConfigResolver` | ASSET_PROFILE | `asset-profile/` | `AssetProfileComponent` | `AssetProfileTabsComponent` | +| `EntityViewsTableConfigResolver` | ENTITY_VIEW | `entity-view/` | `EntityViewComponent` | `EntityViewTabsComponent` | +| `CustomersTableConfigResolver` | CUSTOMER | `customer/` | `CustomerComponent` | `CustomerTabsComponent` | +| `UsersTableConfigResolver` | USER | `user/` | `UserComponent` | `UserTabsComponent` | +| `DashboardsTableConfigResolver` | DASHBOARD | `dashboard/` | `DashboardFormComponent` | `DashboardTabsComponent` | +| `EdgesTableConfigResolver` | EDGE | `edge/` | `EdgeComponent` | `EdgeTabsComponent` | +| `RuleChainsTableConfigResolver` | RULE_CHAIN | `rulechain/` | `RuleChainComponent` | `RuleChainTabsComponent` | +| `OtaUpdateTableConfigResolve` | OTA_PACKAGE | `ota-update/` | `OtaUpdateComponent` | `OtaUpdateTabsComponent` | +| `CalculatedFieldsTableConfigResolver` | CALCULATED_FIELD | `calculated-fields/` (config in `home/components/calculated-fields/`) | `CalculatedFieldComponent` | `CalculatedFieldsTabsComponent` | +| `WidgetTypesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetTypeComponent` | `WidgetTypeTabsComponent` | +| `WidgetsBundlesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetsBundleComponent` | `WidgetsBundleTabsComponent` | +| `ResourcesLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | `ResourcesLibraryComponent` | `ResourceLibraryTabsComponent` | +| `JsLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | `JsResourceComponent` | `ResourceTabsComponent` | +| `QueuesTableConfigResolver` | QUEUE | `admin/queue/` | `QueueComponent` | — | +| `AiModelsTableConfigResolver` | AI_MODEL | `ai-model/` | — | — | +| `ClientsTableConfigResolver` | OAUTH2_CLIENT | `admin/oauth2/clients/` | `ClientComponent` | — | +| `InboxTableConfigResolver` | NOTIFICATION | `notification/inbox/` | — (dialog) | — | +| `SentTableConfigResolver` | NOTIFICATION_REQUEST | `notification/sent/` | — (dialog) | — | +| `RecipientTableConfigResolver` | NOTIFICATION_TARGET | `notification/recipient/` | — (dialog) | — | +| `TemplateTableConfigResolver` | NOTIFICATION_TEMPLATE | `notification/template/` | — (dialog) | — | +| `RuleTableConfigResolver` | NOTIFICATION_RULE | `notification/rule/` | — (dialog) | — | +| `MobileAppTableConfigResolver` | MOBILE_APP | `mobile/applications/` | `MobileAppComponent` | — | +| `MobileBundleTableConfigResolver` | MOBILE_BUNDLE | `mobile/bundes/` | — | — | +| `AlarmRulesTableConfigResolver` | CALCULATED_FIELD | `alarm/` (config in `home/components/alarm-rules/`) | — | — | +| `DomainTableConfigResolver` | DOMAIN | `admin/oauth2/domains/` | `DomainComponent` | — | + +--- + +## 4. Shared Component Catalog + +### `@shared/components/entity/` + +| Selector | Purpose | +|----------|---------| +| `tb-entity-autocomplete` | Autocomplete input for selecting a single entity | +| `tb-entity-select` | Dropdown select for entity | +| `tb-entity-list` | Chip list for multiple entities | +| `tb-entity-list-select` | Dropdown with entity list selection | +| `tb-entity-key-autocomplete` | Autocomplete for entity attribute/telemetry keys | +| `tb-entity-keys-list` | Chip list for multiple entity keys | +| `tb-entity-type-select` | Dropdown for entity type enum | +| `tb-entity-type-list` | Chip list for multiple entity types | +| `tb-entity-subtype-select` | Dropdown for entity subtype (e.g., device type) | +| `tb-entity-subtype-list` | Chip list for multiple subtypes | +| `tb-entity-subtype-autocomplete` | Autocomplete for entity subtype | +| `tb-entity-gateway-select` | Dropdown for selecting a gateway device | + +### `@shared/components/time/` + +| Selector | Purpose | +|----------|---------| +| `tb-timewindow` | Timewindow selector (realtime/history) | +| `tb-timewindow-panel` | Expanded panel for timewindow config | +| `tb-timewindow-config-dialog` | Dialog for advanced timewindow settings | +| `tb-datetime` | Date-time picker input | +| `tb-datetime-period` | Date range picker | +| `tb-timezone` | Timezone display | +| `tb-timezone-select` | Dropdown for timezone selection | +| `tb-quick-time-interval` | Quick interval preset selector | +| `tb-timeinterval` | Duration input (number + unit) | +| `tb-datapoints-limit` | Numeric input for data point limits | +| `tb-aggregation-type-select` | Dropdown for aggregation type (AVG, SUM, etc.) | +| `tb-history-selector` | History navigation selector | + +### `@shared/components/image/` + +| Selector | Purpose | +|----------|---------| +| `tb-image-gallery` | Grid gallery of uploaded images | +| `tb-image-gallery-dialog` | Dialog wrapper for image gallery | +| `tb-image-input` | Single image upload input | +| `tb-gallery-image-input` | Image input that opens gallery for selection | +| `tb-multiple-gallery-image-input` | Multi-image input with gallery | +| `tb-upload-image-dialog` | Upload dialog | +| `tb-embed-image-dialog` | Embed external image dialog | +| `tb-scada-symbol-input` | SCADA symbol selector input | + +### `@shared/components/dialog/` + +| Selector | Purpose | +|----------|---------| +| `tb-confirm-dialog` | Yes/No confirmation dialog | +| `tb-alert-dialog` | Information alert dialog | +| `tb-error-alert-dialog` | Error message dialog | +| `tb-color-picker-dialog` | Color picker in a dialog | +| `tb-material-icons-dialog` | Material icon browser dialog | +| `tb-node-script-test-dialog` | Script testing dialog for rule nodes | +| `tb-object-edit-dialog` | JSON object editor dialog | +| `tb-todo-dialog` | Todo/checklist dialog | + +### `@shared/components/` (other) + +| Selector | Purpose | +|----------|---------| +| `tb-json-content` | JSON syntax-highlighted viewer | +| `tb-json-object-edit` | Editable JSON object component | +| `tb-json-object-view` | Read-only JSON object display | +| `tb-markdown` | Markdown editor/viewer | +| `tb-breadcrumb` | Breadcrumb navigation bar | +| `tb-icon` | Custom icon component | +| `tb-logo` | ThingsBoard logo | +| `tb-copy-button` | Copy-to-clipboard button | +| `tb-toggle-password` | Password visibility toggle | +| `tb-file-input` | File upload input | +| `tb-phone-input` | International phone input | +| `tb-unit-input` | Measurement unit input | +| `tb-value-input` | Generic value input with type selection | +| `tb-color-input` | Color picker input | +| `tb-color-picker` | Inline color picker | +| `tb-key-val-map` | Key-value pair map editor | +| `tb-nav-tree` | Tree navigation component | +| `tb-user-menu` | User profile dropdown menu | +| `tb-notification-bell` | Notification bell icon with badge | +| `tb-footer` | Page footer | +| `tb-scroll-grid` | Virtual-scroll grid | +| `tb-social-share-panel` | Social sharing buttons | + +### Autocomplete / Select Components + +| Selector | Purpose | +|----------|---------| +| `tb-dashboard-autocomplete` | Autocomplete for dashboard selection | +| `tb-dashboard-select` | Dropdown for dashboard | +| `tb-dashboard-state-autocomplete` | Autocomplete for dashboard states | +| `tb-country-autocomplete` | Country autocomplete | +| `tb-string-autocomplete` | Generic string autocomplete | +| `tb-message-type-autocomplete` | Rule engine message type autocomplete | +| `tb-queue-autocomplete` | Queue name autocomplete | +| `tb-relation-type-autocomplete` | Relation type autocomplete | +| `tb-resource-autocomplete` | Resource autocomplete | +| `tb-ota-package-autocomplete` | OTA package autocomplete | +| `tb-branch-autocomplete` | Git branch autocomplete | +| `tb-rule-chain-select` | Rule chain select dropdown | +| `tb-widgets-bundle-select` | Widgets bundle select dropdown | +| `tb-template-autocomplete` | Notification template autocomplete | +| `tb-material-icon-select` | Material icon picker | +| `tb-slack-conversation-autocomplete` | Slack conversation autocomplete | + +### `@home/components/` (key components) + +| Selector | Purpose | +|----------|---------| +| `tb-entities-table` | Main entity table (columns, sort, pagination, actions) | +| `tb-entity-details-panel` | Slide-out panel for entity details | +| `tb-entity-details-page` | Full-page entity details view | +| `tb-add-entity-dialog` | Generic add-entity dialog | +| `tb-entity-filter` | Entity data filter component | +| `tb-entity-chips` | Chip display for entity references | +| `tb-details-panel` | Base details panel (toolbar + edit/apply FABs) | +| `tb-alarm-table` | Alarm list table | +| `tb-alarm-details-dialog` | Alarm detail dialog with ack/clear | +| `tb-alarm-assignee` | Alarm assignee selector | +| `tb-alarm-comment` | Alarm comment thread | +| `tb-alarm-filter-config` | Alarm filter configuration | +| `tb-audit-log-table` | Audit log table | +| `tb-attribute-table` | Entity attribute key-value table | +| `tb-relation-table` | Entity relation table | +| `tb-event-table` | Entity event table | +| `tb-dashboard` | Dashboard renderer | +| `tb-dashboard-page` | Dashboard page (toolbar + dashboard) | +| `tb-dashboard-toolbar` | Dashboard edit/view toolbar | +| `tb-edit-widget` | Widget editor | +| `tb-router-tabs` | Tab navigation via router (used for section tabs) | +| `tb-device-credentials` | Device credentials form | + +--- + +## 5. Angular Material DOM Patterns + +**No `data-testid` attributes** — selectors must use Angular Material structure, `formControlName`, `matColumnDef`, labels, roles, and classes. + +### Login Form + +```html + + + + + + + + + + + + + + + +``` + +Key selectors: `#username-input`, `#password-input`, `button[type="submit"]`. + +### Main Layout + +```html + + + + + + + + + + + + + + +``` + +Key selectors: `mat-sidenav#sidenav`, `button#main`, `tb-breadcrumb`, `tb-user-menu`. + +### Entity Table + +```html + + + + + +
    + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + +
    +
    + +
    +
    +
    +``` + +Key selectors: `table[mat-table]`, `mat-row`, `mat-cell`, `mat-paginator`, `[matColumnDef="select"]`, `[matColumnDef="actions"]`. Note: the search input uses Angular template ref `#searchInput` (not an HTML `id`); target it via `.tb-entity-table mat-toolbar input[matInput]` in the search toolbar context. + +### Entity Details Page + +```html + + + +
    +
    + + +
    +
    + + + + + + +
    +``` + +Key selectors: `mat-toolbar.details-toolbar`, `button[mat-fab][color="accent"]`, `mat-tab-group`, `mat-tab`. + +### Details Panel (Slide-Out Drawer) + +```html +
    + + + +
    + + +
    +
    +
    +
    + +
    +``` + +### Entity Detail Form (inside a tab) + +```html +
    +
    +
    + + + + + + + + + + + + + + +
    +
    +
    +``` + +Key selectors: `input[formControlName="name"]`, `mat-slide-toggle[formControlName="..."]`, `textarea[formControlName="description"]`. + +### Dialog + +```html +
    + +

    + + +
    + +
    + +
    +
    + + +
    + +``` + +Key selectors: `[mat-dialog-content]`, `[mat-dialog-actions]`, `button[type="submit"]`, `mat-toolbar[color="primary"] h2`. + +### Common Material Selector Patterns + +``` +# Find a form field by its control name +input[formControlName="name"] +mat-select[formControlName="type"] +mat-slide-toggle[formControlName="gateway"] +textarea[formControlName="description"] + +# Find table column content +[matColumnDef="name"] mat-cell + +# Find buttons by tooltip +button[matTooltip="Search"] + +# Find a specific tab +mat-tab-group mat-tab (by index or label text) + +# Find menu items (after opening menu) +button[mat-menu-item] + +# Find dialog actions +[mat-dialog-actions] button[mat-raised-button] +``` + +--- + +## 6. Form Structure Guide + +All entity detail forms extend the abstract `EntityComponent` directive (`@home/components/entity/entity.component.ts`). + +**Key properties:** +- `entityForm: UntypedFormGroup` — the reactive form instance +- `entity: T` — current entity data (input) +- `isEdit: boolean` — whether form is in edit mode (input) + +**Lifecycle:** + +1. **`buildForm(entity: T): UntypedFormGroup`** — Called in constructor. Creates the FormGroup with controls, validators, and initial values. Subscribe to valueChanges here for cascading updates. + +2. **`updateForm(entity: T): void`** — Called when entity input changes. Uses `entityForm.patchValue({...}, {emitEvent: false})` to populate form without triggering change events. + +3. **`updateFormState(): void`** — Called when `isEdit` changes. Calls `entityForm.enable()` or `entityForm.disable()` with `{emitEvent: false}`. + +4. **`entityFormValue(): any`** — Returns `entityForm.getRawValue()` (reads disabled fields too), passed through `prepareFormValue()` (default: `deepTrim`). + +**Read-only pattern:** Templates use `
    ` to visually disable all inputs. The form group is also programmatically disabled via `entityForm.disable()`. + +**Saving:** Merges `{...this.entity, ...this.entityFormValue()}` and calls the HTTP service. + +**Common validators:** `Validators.required`, `Validators.maxLength(255)`, custom `validateEmail`. + +**Nested FormGroups** — `additionalInfo` is almost always a nested group: +```typescript +additionalInfo: this.fb.group({ + description: [''], + gateway: [false], + // ... +}) +``` + +--- + +## 7. HTTP Services Reference + +All services are in `src/app/core/http/`. + +| Entity / Area | Service File | Class | +|---------------|-------------|-------| +| Device | `device.service.ts` | `DeviceService` | +| Asset | `asset.service.ts` | `AssetService` | +| Device Profile | `device-profile.service.ts` | `DeviceProfileService` | +| Asset Profile | `asset-profile.service.ts` | `AssetProfileService` | +| Entity View | `entity-view.service.ts` | `EntityViewService` | +| Customer | `customer.service.ts` | `CustomerService` | +| User | `user.service.ts` | `UserService` | +| Tenant | `tenant.service.ts` | `TenantService` | +| Tenant Profile | `tenant-profile.service.ts` | `TenantProfileService` | +| Dashboard | `dashboard.service.ts` | `DashboardService` | +| Rule Chain | `rule-chain.service.ts` | `RuleChainService` | +| Edge | `edge.service.ts` | `EdgeService` | +| Alarm | `alarm.service.ts` | `AlarmService` | +| Alarm Comment | `alarm-comment.service.ts` | `AlarmCommentService` | +| OTA Package | `ota-package.service.ts` | `OtaPackageService` | +| Calculated Fields | `calculated-fields.service.ts` | `CalculatedFieldsService` | +| Widget | `widget.service.ts` | `WidgetService` | +| Resource | `resource.service.ts` | `ResourceService` | +| Queue | `queue.service.ts` | `QueueService` | +| AI Model | `ai-model.service.ts` | `AiModelService` | +| Notification | `notification.service.ts` | `NotificationService` | +| OAuth2 | `oauth2.service.ts` | `OAuth2Service` | +| Domain | `domain.service.ts` | `DomainService` | +| Mobile App | `mobile-app.service.ts` | `MobileAppService` | +| Attribute | `attribute.service.ts` | `AttributeService` | +| Entity Relation | `entity-relation.service.ts` | `EntityRelationService` | +| Entity (generic) | `entity.service.ts` | `EntityService` | +| Event | `event.service.ts` | `EventService` | +| Audit Log | `audit-log.service.ts` | `AuditLogService` | +| Admin Settings | `admin.service.ts` | `AdminService` | +| Version Control | `entities-version-control.service.ts` | `EntitiesVersionControlService` | +| 2FA | `two-factor-authentication.service.ts` | `TwoFactorAuthenticationService` | +| User Settings | `user-settings.service.ts` | `UserSettingsService` | +| API Key | `api-key.service.ts` | `ApiKeyService` | +| Image | `image.service.ts` | `ImageService` | +| Trendz Settings | `trendz-settings.service.ts` | `TrendzSettingsService` | +| Usage Info | `usage-info.service.ts` | `UsageInfoService` | +| Component Descriptor | `component-descriptor.service.ts` | `ComponentDescriptorService` | +| GitHub | `git-hub.service.ts` | `GitHubService` | +| Mobile Application | `mobile-application.service.ts` | `MobileApplicationService` | +| UI Settings | `ui-settings.service.ts` | `UiSettingsService` | diff --git a/ui-ngx/tailwind.config.js b/ui-ngx/tailwind.config.js index e90e873df3..a10fac840c 100644 --- a/ui-ngx/tailwind.config.js +++ b/ui-ngx/tailwind.config.js @@ -84,7 +84,8 @@ module.exports = { '25': '6.25rem', '37.5': '9.375rem', '62.5': '15.625rem', - '72.5': '18.125rem' + '72.5': '18.125rem', + '40%': '40%' }, flex: { full: '1 1 100%' diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 7a77f42959..e99905fb9e 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -7976,9 +7976,9 @@ ngx-drag-drop@^20.0.1: dependencies: tslib "^2.3.0" -"ngx-flowchart@https://github.com/thingsboard/ngx-flowchart.git#release/4.0.0": - version "4.0.0" - resolved "https://github.com/thingsboard/ngx-flowchart.git#735bc818ef218a169ac50e87edc6a093b3e97715" +"ngx-flowchart@https://github.com/thingsboard/ngx-flowchart.git#release/4.1.0": + version "4.1.0" + resolved "https://github.com/thingsboard/ngx-flowchart.git#a3dee1761ecaa96c7d0ca22b1c6538b9e27103af" dependencies: tslib "^2.3.0"